|
|
@@ -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)
|
|
|
}
|
|
|
|