mirror of
https://github.com/overleaf/overleaf.git
synced 2025-03-22 02:04:31 +00:00
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:
parent
07aa9e2768
commit
9cf94e57d3
8 changed files with 159 additions and 42 deletions
|
@ -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) {
|
||||
|
|
|
@ -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('')
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
|
@ -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(),
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue