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
@@ -1,7 +1,6 @@
import { ensureSyntaxTree, syntaxTree } from '@codemirror/language'
import { EditorSelection, EditorState, SelectionRange } from '@codemirror/state'
import { SyntaxNode, Tree } from '@lezer/common'
import { isUnknownCommandWithName } from './common'
import { ListEnvironment } from '../../lezer-latex/latex.terms.mjs'
const HUNDRED_MS = 100
@@ -125,14 +124,15 @@ export const lastAncestorAtEndPosition = (
node: SyntaxNode | null | undefined,
to: number
): SyntaxNode | null => {
for (let ancestor = node; ancestor; ancestor = ancestor.parent) {
if (ancestor.parent?.to === to) {
continue
} else if (ancestor.to === to) {
return ancestor
}
let lastAncestor: SyntaxNode | null = null
for (
let ancestor = node;
ancestor && ancestor.to === to;
ancestor = ancestor.parent
) {
lastAncestor = ancestor
}
return null
return lastAncestor
}
export const descendantsOfNodeWithType = (
@@ -226,37 +226,6 @@ export const commonAncestor = (
return null
}
export const withinFormattingCommand = (state: EditorState) => {
const tree = syntaxTree(state)
return (command: string): boolean => {
const isFormattedText = (range: SelectionRange): boolean => {
const nodeLeft = tree.resolveInner(range.from, -1)
const formattingCommandLeft = matchingAncestor(nodeLeft, node =>
isUnknownCommandWithName(node, command, state)
)
if (!formattingCommandLeft) {
return false
}
// We need to check the other end of the selection, and ensure that they
// share a common formatting command ancestor
const nodeRight = tree.resolveInner(range.to, 1)
const ancestor = commonAncestor(formattingCommandLeft, nodeRight)
if (!ancestor) {
return false
}
const formattingAncestor = matchingAncestor(ancestor, node =>
isUnknownCommandWithName(node, command, state)
)
return Boolean(formattingAncestor)
}
return state.selection.ranges.every(isFormattedText)
}
}
export type ListEnvironmentName = 'itemize' | 'enumerate' | 'description'
export const listDepthForNode = (node: SyntaxNode) => {
@@ -58,35 +58,6 @@ export const getOptionalArgumentText = (
): string | undefined => {
const shortArgNode = optionalArgumentNode.getChild('ShortOptionalArg')
if (shortArgNode) {
const shortArgNodeText = state.doc.sliceString(
shortArgNode.from,
shortArgNode.to
)
return shortArgNodeText
return state.doc.sliceString(shortArgNode.from, shortArgNode.to)
}
}
export const resolveNodeAtPos = (
state: EditorState,
pos: number,
side?: -1 | 0 | 1
) => ensureSyntaxTree(state, pos, HUNDRED_MS)?.resolveInner(pos, side) ?? null
export const isUnknownCommandWithName = (
node: SyntaxNode,
command: string,
state: EditorState
): boolean => {
if (!node.type.is('UnknownCommand')) {
return false
}
const commandNameNode = node.getChild('CtrlSeq')
if (!commandNameNode) {
return false
}
const commandName = state.doc.sliceString(
commandNameNode.from,
commandNameNode.to
)
return commandName === command
}
@@ -0,0 +1,50 @@
import { EditorState, SelectionRange } from '@codemirror/state'
import { syntaxTree } from '@codemirror/language'
import {
commonAncestor,
matchingAncestor,
} from '@/features/source-editor/utils/tree-operations/ancestors'
export type FormattingCommand = '\\textbf' | '\\textit'
export type FormattingNodeType = string | number
export const formattingCommandMap: Record<
FormattingCommand,
FormattingNodeType
> = {
'\\textbf': 'TextBoldCommand',
'\\textit': 'TextItalicCommand',
}
export const withinFormattingCommand = (state: EditorState) => {
const tree = syntaxTree(state)
return (command: FormattingCommand): boolean => {
const nodeType = formattingCommandMap[command]
const isFormattedText = (range: SelectionRange): boolean => {
const nodeLeft = tree.resolveInner(range.from, -1)
const formattingCommandLeft = matchingAncestor(nodeLeft, node =>
node.type.is(nodeType)
)
if (!formattingCommandLeft) {
return false
}
// We need to check the other end of the selection, and ensure that they
// share a common formatting command ancestor
const nodeRight = tree.resolveInner(range.to, 1)
const ancestor = commonAncestor(formattingCommandLeft, nodeRight)
if (!ancestor) {
return false
}
const formattingAncestor = matchingAncestor(ancestor, node =>
node.type.is(nodeType)
)
return Boolean(formattingAncestor)
}
return state.selection.ranges.every(isFormattedText)
}
}