Add text formatting commands to the LaTeX grammar (#19607)

GitOrigin-RevId: f69cd323992c80de3f0a458a637fa8f160017076
This commit is contained in:
Alf Eaton
2024-08-02 08:06:11 +00:00
committed by Copybot
parent 37e897260c
commit 603ff28df0
14 changed files with 389 additions and 294 deletions
@@ -7,11 +7,13 @@ import {
syntaxTree,
} from '@codemirror/language'
import { SyntaxNode } from '@lezer/common'
import {
ancestorOfNodeWithType,
isUnknownCommandWithName,
} from '../utils/tree-query'
import { ancestorOfNodeWithType } from '../utils/tree-query'
import { lastAncestorAtEndPosition } from '../utils/tree-operations/ancestors'
import {
formattingCommandMap,
type FormattingCommand,
type FormattingNodeType,
} from '@/features/source-editor/utils/tree-operations/formatting'
export const wrapRanges =
(
@@ -208,13 +210,20 @@ export const duplicateSelection = (view: EditorView) => {
return true
}
const skipParentNodeTypes = [
'LongArg',
'TextArgument',
'OpenBrace',
'CloseBrace',
]
function getParentNode(
position: number | SyntaxNode,
state: EditorState,
assoc: 0 | 1 | -1 = 1
): SyntaxNode | undefined {
const tree = ensureSyntaxTree(state, 1000)
let node: SyntaxNode | undefined | null = null
let node: SyntaxNode | undefined | null
if (typeof position === 'number') {
node = tree?.resolveInner(position, assoc)?.parent
@@ -226,13 +235,10 @@ function getParentNode(
node = position?.parent
}
while (
['LongArg', 'TextArgument', 'OpenBrace', 'CloseBrace'].includes(
node?.type.name || ''
)
) {
node = node!.parent
while (node && skipParentNodeTypes.includes(node.type.name)) {
node = node.parent
}
return node || undefined
}
@@ -274,18 +280,17 @@ function validateReplacement(expected: string, actual: string) {
function getWrappingAncestor(
node: SyntaxNode,
command: string,
state: EditorState
nodeType: FormattingNodeType
): SyntaxNode | null {
for (
let ancestor: SyntaxNode | null = node;
ancestor;
ancestor = ancestor.parent
) {
if (isUnknownCommandWithName(ancestor, command, state)) {
if (ancestor.type.is(nodeType)) {
return ancestor
}
if (ancestor.type.is('UnknownCommand')) {
if (formattingCommandMap[ancestor.type.name as FormattingCommand]) {
// We could multiple levels deep in bold/non-bold. So bail out in this case
return null
}
@@ -294,7 +299,7 @@ function getWrappingAncestor(
}
function adjustRangeIfNeeded(
command: string,
nodeType: FormattingNodeType,
range: SelectionRange,
state: EditorState
) {
@@ -311,50 +316,51 @@ function adjustRangeIfNeeded(
const nodeLeft = tree.resolveInner(range.from, 1)
const nodeRight = tree.resolveInner(range.to, -1)
const parentLeft = getWrappingAncestor(nodeLeft, command, state)
const parentRight = getWrappingAncestor(nodeRight, command, state)
const parentLeft = getWrappingAncestor(nodeLeft, nodeType)
const parentRight = getWrappingAncestor(nodeRight, nodeType)
const parent = getParentNode(nodeLeft, state)
if (parent?.type.is('UnknownCommand') && spansWholeArgument(parent, range)) {
if (
parent?.type.is('$ToggleTextFormattingCommand') &&
spansWholeArgument(parent, range)
) {
return bubbleUpRange(
command,
ancestorOfNodeWithType(nodeLeft, 'UnknownCommand'),
range,
state
nodeType,
ancestorOfNodeWithType(nodeLeft, '$ToggleTextFormattingCommand'),
range
)
}
if (!parentLeft) {
// We're not trying to unbold, so don't bother adjusting range
return bubbleUpRange(
command,
ancestorOfNodeWithType(nodeLeft, 'UnknownCommand'),
range,
state
nodeType,
ancestorOfNodeWithType(nodeLeft, '$ToggleTextFormattingCommand'),
range
)
}
if (nodeLeft.type.is('CtrlSeq') && range.from === range.to) {
const command = nodeLeft.parent?.parent
if (!command) {
if (nodeLeft.type.is('$CtrlSeq') && range.from === range.to) {
const commandNode = nodeLeft.parent?.parent?.parent
if (!commandNode) {
return range
}
return EditorSelection.cursor(command.from)
return EditorSelection.cursor(commandNode.from)
}
let { from, to } = range
if (nodeLeft.type.is('CtrlSeq')) {
if (nodeLeft.type.is('$CtrlSeq')) {
from = nodeLeft.to + 1
}
if (nodeLeft.type.is('OpenBrace')) {
from = nodeLeft.to
}
// We know that parentLeft is the UnknownCommand, so now we check if we're
// We know that parentLeft is the $ToggleTextFormattingCommand, so now we check if we're
// to the right of the closing brace. (parent is TextArgument, grandparent is
// UnknownCommand)
// $ToggleTextFormattingCommand)
if (parentLeft === parentRight && nodeRight.type.is('CloseBrace')) {
to = nodeRight.from
}
return bubbleUpRange(command, parentLeft, moveRange(range, from, to), state)
return bubbleUpRange(nodeType, parentLeft, moveRange(range, from, to))
}
function spansWholeArgument(
@@ -362,28 +368,26 @@ function spansWholeArgument(
range: SelectionRange
): boolean {
const argument = commandNode?.getChild('TextArgument')?.getChild('LongArg')
const res = Boolean(
return Boolean(
argument && argument.from === range.from && argument.to === range.to
)
return res
}
function bubbleUpRange(
command: string,
nodeType: string | number,
node: SyntaxNode | null,
range: SelectionRange,
state: EditorState
range: SelectionRange
) {
let currentRange = range
for (
let ancestorCommand: SyntaxNode | null = ancestorOfNodeWithType(
node,
'UnknownCommand'
'$ToggleTextFormattingCommand'
);
spansWholeArgument(ancestorCommand, currentRange);
ancestorCommand = ancestorOfNodeWithType(
ancestorCommand.parent,
'UnknownCommand'
'$ToggleTextFormattingCommand'
)
) {
if (!ancestorCommand) {
@@ -394,7 +398,7 @@ function bubbleUpRange(
ancestorCommand.from,
ancestorCommand.to
)
if (isUnknownCommandWithName(ancestorCommand, command, state)) {
if (ancestorCommand.type.is(nodeType)) {
const argumentNode = ancestorCommand
.getChild('TextArgument')
?.getChild('LongArg')
@@ -408,7 +412,9 @@ function bubbleUpRange(
return range
}
export function toggleRanges(command: string) {
export function toggleRanges(command: FormattingCommand) {
const nodeType: FormattingNodeType = formattingCommandMap[command]
/* There are a number of situations we need to handle in this function.
* In the following examples, the selection range is marked within <>
@@ -443,7 +449,7 @@ export function toggleRanges(command: string) {
}
view.dispatch(
view.state.changeByRange(initialRange => {
const range = adjustRangeIfNeeded(command, initialRange, view.state)
const range = adjustRangeIfNeeded(nodeType, initialRange, view.state)
const content = view.state.sliceDoc(range.from, range.to)
const ancestorAtStartOfRange = getParentNode(
@@ -470,25 +476,22 @@ export function toggleRanges(command: string) {
) {
// But handle the exception of case 8
const ancestorAtStartIsWrappingCommand =
ancestorAtStartOfRange &&
isUnknownCommandWithName(
ancestorAtStartOfRange,
command,
view.state
)
ancestorAtStartOfRange?.type.is(nodeType)
const ancestorAtEndIsWrappingCommand =
ancestorAtEndOfRange &&
isUnknownCommandWithName(ancestorAtEndOfRange, command, view.state)
ancestorAtEndOfRange && ancestorAtEndOfRange.type.is(nodeType)
if (
ancestorAtStartIsWrappingCommand &&
ancestorAtEndIsWrappingCommand &&
ancestorAtStartOfRange?.parent?.parent &&
ancestorAtEndOfRange?.parent?.parent
ancestorAtStartOfRange?.parent?.parent?.parent &&
ancestorAtEndOfRange?.parent?.parent?.parent
) {
// Test for case 8
const nextAncestorAtStartOfRange =
ancestorAtStartOfRange.parent.parent
const nextAncestorAtEndOfRange = ancestorAtEndOfRange.parent.parent
ancestorAtStartOfRange.parent.parent.parent
const nextAncestorAtEndOfRange =
ancestorAtEndOfRange.parent.parent.parent
if (nextAncestorAtStartOfRange === nextAncestorAtEndOfRange) {
// Join the two ranges
@@ -539,7 +542,8 @@ export function toggleRanges(command: string) {
if (
ancestorAtEndIsWrappingCommand &&
ancestorAtEndOfRange.parent?.parent === ancestorAtStartOfRange
ancestorAtEndOfRange.parent?.parent?.parent ===
ancestorAtStartOfRange
) {
// Extend to the left. Case 10
const contentUpToCommand = view.state.sliceDoc(
@@ -582,7 +586,9 @@ export function toggleRanges(command: string) {
if (
ancestorAtStartIsWrappingCommand &&
ancestorAtStartOfRange.parent?.parent === ancestorAtEndOfRange
ancestorAtStartOfRange &&
ancestorAtStartOfRange.parent?.parent?.parent ===
ancestorAtEndOfRange
) {
// Extend to the right. Case 9
const contentAfterCommand = view.state.sliceDoc(
@@ -635,15 +641,11 @@ export function toggleRanges(command: string) {
range.empty &&
ancestor &&
range.from === ancestor.from &&
isUnknownCommandWithName(ancestor, command, view.state)
ancestor.type.is(nodeType)
// If we can't find an ancestor node, or if the parent is not an exsting
// \textbf, then we just wrap it in a range. Case 3.
if (
isCursorBeforeAncestor ||
!ancestor ||
!isUnknownCommandWithName(ancestor, command, view.state)
) {
if (isCursorBeforeAncestor || !ancestor?.type.is(nodeType)) {
return wrapRangeInCommand(view.state, range, command)
}