Include \def, \let, \newenvironment and \renewenvironment commands in the math preview (#20197)

* Include \def commands in command definitions

* Allow command name to be in CtrlSym

* Pull nodeIntersectsChange check out of branches

* Fix \def handling

* Add handling for \newenvironment and \renewenvironment

* Allow \def to have optional arguments

* :x
Revert "Add handling for \newenvironment and \renewenvironment"

This reverts commit a70d3a0a13ed552daf3b761893e3f8609f0b0fc8.

* Add let commands to defintions

* Add environment names to math preview

* Ignore let command definitions in autocomplete

* Move nodeIntersectsChange check back into each block

* Add childOfNodeWithType utility and use to get command names

* commandNameText -> commandName

* Only include environment definitions

* Rename documentEnvironmentNames to documentEnvironments

* EnvironmentName -> Environment

* Format

GitOrigin-RevId: 9c5d701423ae786e5ff91960b4bcd94cd35d21c8
This commit is contained in:
David
2024-09-17 08:04:53 +00:00
committed by Copybot
parent 07aa9e2768
commit 9cf94e57d3
8 changed files with 159 additions and 42 deletions
@@ -1,6 +1,6 @@
import { EditorState } from '@codemirror/state'
import { SyntaxNode, SyntaxNodeRef } from '@lezer/common'
import { getOptionalArgumentText } from './common'
import { childOfNodeWithType, getOptionalArgumentText } from './common'
import { NodeIntersectsChangeFn, ProjectionItem } from './projection'
/**
@@ -8,15 +8,34 @@ import { NodeIntersectsChangeFn, ProjectionItem } from './projection'
*/
export class Command extends ProjectionItem {
readonly title: string = ''
readonly optionalArgCount: number = 0
readonly requiredArgCount: number = 0
readonly optionalArgCount: number | undefined = 0
readonly requiredArgCount: number | undefined = 0
readonly type: 'usage' | 'definition' = 'usage'
readonly raw: string | undefined = undefined
readonly ignoreInAutocomplete?: boolean = false
}
const getCommandName = (
node: SyntaxNode,
state: EditorState,
childTypes: string[]
): string | null => {
const child = childOfNodeWithType(node, ...childTypes)
if (child) {
const commandName = state.doc.sliceString(child.from, child.to)
if (commandName.length > 0) {
return commandName
}
}
return null
}
/**
* Extracts Command instances from the syntax tree.
* `\newcommand` and `\renewcommand` are treated specially
* `\newcommand`, `\renewcommand`, `\newenvironment`, `\renewenvironment`
* and `\def` are treated specially.
*/
export const enterNode = (
state: EditorState,
@@ -29,19 +48,13 @@ export const enterNode = (
// This should already be in `items`
return
}
let commandName = node.node.getChild('LiteralArgContent')
if (!commandName) {
commandName = node.node.getChild('Csname')
}
if (!commandName) {
return
}
const commandNameText = state.doc.sliceString(
commandName.from,
commandName.to
)
if (commandNameText.length < 1) {
const commandName = getCommandName(node.node, state, [
'LiteralArgContent',
'Csname',
])
if (commandName === null) {
return
}
@@ -66,7 +79,7 @@ export const enterNode = (
items.push({
line: state.doc.lineAt(node.from).number,
title: commandNameText,
title: commandName,
from: node.from,
to: node.to,
optionalArgCount: commandDefinitionHasOptionalArgument ? 1 : 0,
@@ -74,12 +87,66 @@ export const enterNode = (
type: 'definition',
raw: state.sliceDoc(node.from, node.to),
})
} else if (node.type.is('Def')) {
if (!nodeIntersectsChange(node.node)) {
// This should already be in `items`
return
}
const commandName = getCommandName(node.node, state, ['Csname', 'CtrlSym'])
if (commandName === null) {
return
}
const requiredArgCount = node.node.getChildren('MacroParameter').length
const optionalArgCount = node.node.getChildren(
'OptionalMacroParameter'
).length
items.push({
line: state.doc.lineAt(node.from).number,
title: commandName,
from: node.from,
to: node.to,
optionalArgCount,
requiredArgCount,
type: 'definition',
raw: state.sliceDoc(node.from, node.to),
})
} else if (node.type.is('Let')) {
if (!nodeIntersectsChange(node.node)) {
// This should already be in `items`
return
}
const commandName = getCommandName(node.node, state, ['Csname'])
if (commandName === null) {
return
}
items.push({
line: state.doc.lineAt(node.from).number,
title: commandName,
from: node.from,
to: node.to,
ignoreInAutocomplete: true, // Ignoring since we don't know the argument counts
optionalArgCount: undefined,
requiredArgCount: undefined,
type: 'definition',
raw: state.sliceDoc(node.from, node.to),
})
} else if (
node.type.is('UnknownCommand') ||
node.type.is('KnownCommand') ||
node.type.is('MathUnknownCommand') ||
node.type.is('DefinitionFragmentUnknownCommand')
) {
if (!nodeIntersectsChange(node.node)) {
// This should already be in `items`
return
}
let commandNode: SyntaxNode | null = node.node
if (node.type.is('KnownCommand')) {
// KnownCommands are defined as
@@ -96,10 +163,7 @@ export const enterNode = (
if (!commandNode) {
return
}
if (!nodeIntersectsChange(node.node)) {
// This should already be in `items`
return
}
const ctrlSeq = commandNode.getChild('$CtrlSeq')
if (!ctrlSeq) {
return
@@ -78,3 +78,27 @@ export const nodeHasError = (node: SyntaxNode): boolean => {
return hasError
}
export const childOfNodeWithType = (
node: SyntaxNode,
...types: (string | number)[]
): SyntaxNode | null => {
let childOfType: SyntaxNode | null = null
node.cursor().iterate(child => {
if (childOfType !== null) {
return false
}
for (const type of types) {
if (child.type.is(type)) {
childOfType = child.node
return false
}
}
return true
})
return childOfType
}
@@ -7,14 +7,16 @@ import { FigureData } from '../../extensions/figure-modal'
const HUNDRED_MS = 100
export class EnvironmentName extends ProjectionItem {
export class Environment extends ProjectionItem {
readonly title: string = ''
readonly type: 'usage' | 'definition' = 'usage'
readonly raw: string = ''
}
export const enterNode = (
state: EditorState,
node: SyntaxNodeRef,
items: EnvironmentName[],
items: Environment[],
nodeIntersectsChange: NodeIntersectsChangeFn
): any => {
if (node.type.is('EnvNameGroup')) {
@@ -38,11 +40,13 @@ export const enterNode = (
return false
}
const thisEnvironmentName = {
const thisEnvironmentName: Environment = {
title: envNameText,
from: envNameNode.from,
to: envNameNode.to,
line: state.doc.lineAt(envNameNode.from).number,
type: 'usage',
raw: state.sliceDoc(node.from, node.to),
}
items.push(thisEnvironmentName)
@@ -65,11 +69,13 @@ export const enterNode = (
return
}
const thisEnvironmentName = {
const thisEnvironmentName: Environment = {
title: envNameText,
from: envNameNode.from,
to: envNameNode.to,
line: state.doc.lineAt(envNameNode.from).number,
type: 'definition',
raw: state.sliceDoc(node.from, node.to),
}
items.push(thisEnvironmentName)