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-16 09:40:44 +01:00 committed by Copybot
parent 07aa9e2768
commit 9cf94e57d3
8 changed files with 159 additions and 42 deletions

View file

@ -26,6 +26,7 @@ import ReactDOM from 'react-dom'
import { SplitTestProvider } from '@/shared/context/split-test-context'
import SplitTestBadge from '@/shared/components/split-test-badge'
import { nodeHasError } from '../utils/tree-operations/common'
import { documentEnvironments } from '../languages/latex/document-environments'
const REPOSITION_EVENT = 'editor:repositionMathTooltips'
@ -150,8 +151,17 @@ const buildTooltipContent = (
element.textContent = math.content
let definitions = ''
const commandState = state.field(documentCommands, false)
const environmentState = state.field(documentEnvironments, false)
if (environmentState?.items) {
for (const environment of environmentState.items) {
if (environment.type === 'definition') {
definitions += `${environment.raw}\n`
}
}
}
const commandState = state.field(documentCommands, false)
if (commandState?.items) {
for (const command of commandState.items) {
if (command.type === 'definition' && command.raw) {

View file

@ -22,7 +22,10 @@ export function customCommandCompletions(
const items = countCommandUsage(context)
for (const item of items.values()) {
if (!existingCommands.has(commandNameFromLabel(item.label))) {
if (
!existingCommands.has(commandNameFromLabel(item.label)) &&
!item.ignoreInAutoComplete
) {
output.push({
type: 'cmd',
label: item.label,
@ -42,7 +45,12 @@ const countCommandUsage = (context: CompletionContext) => {
const result = new Map<
string,
{ label: string; snippet: string; count: number }
{
label: string
snippet: string
count: number
ignoreInAutoComplete?: boolean
}
>()
const commandListProjection = context.state.field(documentCommands)
@ -58,7 +66,12 @@ const countCommandUsage = (context: CompletionContext) => {
const label = buildLabel(command)
const snippet = buildSnippet(command)
const item = result.get(label) || { label, snippet, count: 0 }
const item = result.get(label) || {
label,
snippet,
count: 0,
ignoreInAutoComplete: command.ignoreInAutocomplete,
}
item.count++
result.set(label, item)
}
@ -69,15 +82,15 @@ const countCommandUsage = (context: CompletionContext) => {
const buildLabel = (command: Command): string => {
return [
`${command.title}`,
'[]'.repeat(command.optionalArgCount),
'{}'.repeat(command.requiredArgCount),
'[]'.repeat(command.optionalArgCount ?? 0),
'{}'.repeat(command.requiredArgCount ?? 0),
].join('')
}
const buildSnippet = (command: Command): string => {
return [
`${command.title}`,
'[#{}]'.repeat(command.optionalArgCount),
'{#{}}'.repeat(command.requiredArgCount),
'[#{}]'.repeat(command.optionalArgCount ?? 0),
'{#{}}'.repeat(command.requiredArgCount ?? 0),
].join('')
}

View file

@ -1,8 +1,8 @@
import { customBeginCompletion } from './environments'
import { CompletionContext } from '@codemirror/autocomplete'
import { documentEnvironmentNames } from '../document-environment-names'
import { documentEnvironments } from '../document-environments'
import { ProjectionResult } from '../../../utils/tree-operations/projection'
import { EnvironmentName } from '../../../utils/tree-operations/environments'
import { Environment } from '../../../utils/tree-operations/environments'
/**
* Environments from the current doc
@ -25,8 +25,8 @@ export function customEnvironmentCompletions(context: CompletionContext) {
export const findEnvironmentsInDoc = (context: CompletionContext) => {
const result = new Set<string>()
const environmentNamesProjection: ProjectionResult<EnvironmentName> =
context.state.field(documentEnvironmentNames)
const environmentNamesProjection: ProjectionResult<Environment> =
context.state.field(documentEnvironments)
if (!environmentNamesProjection || !environmentNamesProjection.items) {
return result
}

View file

@ -1,8 +1,8 @@
import {
EnvironmentName,
Environment,
enterNode,
} from '../../utils/tree-operations/environments'
import { makeProjectionStateField } from '../../utils/projection-state-field'
export const documentEnvironmentNames =
makeProjectionStateField<EnvironmentName>(enterNode)
export const documentEnvironments =
makeProjectionStateField<Environment>(enterNode)

View file

@ -15,7 +15,7 @@ import { documentCommands } from './document-commands'
import importOverleafModules from '../../../../../macros/import-overleaf-module.macro'
import { documentOutline } from './document-outline'
import { LaTeXLanguage } from './latex-language'
import { documentEnvironmentNames } from './document-environment-names'
import { documentEnvironments } from './document-environments'
import {
figureModal,
figureModalPasteHandler,
@ -36,7 +36,7 @@ export const latex = () => {
shortcuts(),
documentOutline,
documentCommands,
documentEnvironmentNames,
documentEnvironments,
latexIndentService(),
linting(),
metadata(),

View file

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

View file

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

View file

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