diff --git a/.vscode/launch.json b/.vscode/launch.json index 371468953..589d3d3c7 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -33,7 +33,7 @@ "preLaunchTask": "task-watch-all", "env": { "LATEXWORKSHOP_CI": "1", - "LATEXWORKSHOP_SUITE": "" + "LATEXWORKSHOP_SUITE": "04" } }, { diff --git a/src/components/cacher.ts b/src/components/cacher.ts index 50d7e4e31..61f134f22 100644 --- a/src/components/cacher.ts +++ b/src/components/cacher.ts @@ -233,16 +233,17 @@ export class Cacher { private updateElements(filePath: string, content: string, contentTrimmed: string) { lw.completer.citation.update(filePath, content) const cache = this.get(filePath) + if (cache) { + cache.elements.reference = lw.completer.reference.update(content, cache?.ast) + } if (cache?.luAst) { const nodes = cache.luAst.content const lines = content.split('\n') - lw.completer.reference.update(filePath, nodes, lines) lw.completer.glossary.update(filePath, nodes) lw.completer.environment.update(filePath, nodes, lines) lw.completer.command.update(filePath, nodes) } else { logger.log(`Use RegExp to update elements of ${filePath} .`) - lw.completer.reference.update(filePath, undefined, undefined, contentTrimmed) lw.completer.glossary.update(filePath, undefined, contentTrimmed) lw.completer.environment.update(filePath, undefined, undefined, contentTrimmed) lw.completer.command.update(filePath, undefined, contentTrimmed) diff --git a/src/components/eventbus.ts b/src/components/eventbus.ts index df94cf97d..3df0b47dc 100644 --- a/src/components/eventbus.ts +++ b/src/components/eventbus.ts @@ -54,7 +54,9 @@ export class EventBus { fire(eventName: T, arg: EventArgs[T]): void fire(eventName: EventName): void fire(eventName: EventName, arg?: any): void { - logger.log(eventName + (arg ? `: ${JSON.stringify(arg)}` : '')) + if (eventName !== 'DOCUMENT_CHANGED') { + logger.log(eventName + (arg ? `: ${JSON.stringify(arg)}` : '')) + } this.eventEmitter.emit(eventName, arg) } diff --git a/src/components/parserlib/defs.ts b/src/components/parserlib/defs.ts index 6dd4f0ad7..0da6c24b8 100644 --- a/src/components/parserlib/defs.ts +++ b/src/components/parserlib/defs.ts @@ -15,6 +15,8 @@ const MACROS: MacroInfoRecord = { subimport: { signature: 'm m' }, subinputfrom: { signature: 'm m' }, subincludefrom: { signature: 'm m' }, + // \label{some-label} + linelabel: { signature: 'o m'} } const ENVS: EnvInfoRecord = {} diff --git a/src/providers/completer/reference.ts b/src/providers/completer/reference.ts index 356459557..c96b2f9e9 100644 --- a/src/providers/completer/reference.ts +++ b/src/providers/completer/reference.ts @@ -1,11 +1,12 @@ import * as vscode from 'vscode' import * as fs from 'fs' import * as path from 'path' -import {latexParser} from 'latex-utensils' +import type * as Ast from '@unified-latex/unified-latex-types' import * as lw from '../../lw' -import {stripEnvironments, isNewCommand, isNewEnvironment} from '../../utils/utils' -import {computeFilteringRange} from './completerutils' +import { stripEnvironments } from '../../utils/utils' +import { computeFilteringRange } from './completerutils' import type { IProvider, ICompletionItem, IProviderArgs } from '../completion' +import { argContentToStr } from '../../utils/parser' export interface ReferenceEntry extends ICompletionItem { /** The file that defines the ref. */ @@ -29,7 +30,6 @@ export class Reference implements IProvider { // Here we use an object instead of an array for de-duplication private readonly suggestions = new Map() private prevIndexObj = new Map() - private readonly envsToSkip = ['tikzpicture'] provideFrom(_result: RegExpMatchArray, args: IProviderArgs) { return this.provide(args.line, args.position) @@ -64,27 +64,6 @@ export class Reference implements IProvider { return items } - /** - * Updates the Manager cache for references defined in `file` with `nodes`. - * If `nodes` is `undefined`, `content` is parsed with regular expressions, - * and the result is used to update the cache. - * @param file The path of a LaTeX file. - * @param nodes AST of a LaTeX file. - * @param lines The lines of the content. They are used to generate the documentation of completion items. - * @param content The content of a LaTeX file. - */ - update(file: string, nodes?: latexParser.Node[], lines?: string[], content?: string) { - const cache = lw.cacher.get(file) - if (cache === undefined) { - return - } - if (nodes !== undefined && lines !== undefined) { - cache.elements.reference = this.getRefFromNodeArray(nodes, lines) - } else if (content !== undefined) { - cache.elements.reference = this.getRefFromContent(content) - } - } - getRef(token: string): ReferenceEntry | undefined { this.updateAll() return this.suggestions.get(token) @@ -171,88 +150,64 @@ export class Reference implements IProvider { }) } - // This function will return all references in a node array, including sub-nodes - private getRefFromNodeArray(nodes: latexParser.Node[], lines: string[]): ICompletionItem[] { - let refs: ICompletionItem[] = [] - for (let index = 0; index < nodes.length; ++index) { - if (index < nodes.length - 1) { - // Also pass the next node to handle cases like `label={some-text}` - refs = refs.concat(this.getRefFromNode(nodes[index], lines, nodes[index+1])) - } else { - refs = refs.concat(this.getRefFromNode(nodes[index], lines)) - } + update(content: string, ast?: Ast.Root): ICompletionItem[] | undefined { + const lines = content.split('\n') + if (ast !== undefined) { + return this.parseAst(ast, lines) + } else { + return this.parseContent(content) } - return refs } - // This function will return the reference defined by the node, or all references in `content` - private getRefFromNode(node: latexParser.Node, lines: string[], nextNode?: latexParser.Node): ICompletionItem[] { - const configuration = vscode.workspace.getConfiguration('latex-workshop') - const labelCmdNames = configuration.get('intellisense.label.command') as string[] - const useLabelKeyVal = configuration.get('intellisense.label.keyval') as boolean - const refs: ICompletionItem[] = [] - let label = '' - if (isNewCommand(node) || isNewEnvironment(node) || latexParser.isDefCommand(node)) { + private parseAst(node: Ast.Node, lines: string[]): ICompletionItem[] { + let refs: ICompletionItem[] = [] + if (node.type === 'macro' && + ['renewcommand', 'newcommand', 'providecommand', 'DeclareMathOperator', 'renewenvironment', 'newenvironment'].includes(node.content)) { // Do not scan labels inside \newcommand, \newenvironment & co - return refs + return [] } - if (latexParser.isEnvironment(node) && this.envsToSkip.includes(node.name)) { - return refs + if (node.type === 'environment' && ['tikzpicture'].includes(node.env)) { + return [] } - if (latexParser.isLabelCommand(node) && labelCmdNames.includes(node.name)) { - // \label{some-text} - label = node.label - } else if (latexParser.isCommand(node) && labelCmdNames.includes(node.name) - && node.args.length === 1 - && latexParser.isGroup(node.args[0])) { - // \linelabel{actual_label} - label = latexParser.stringify(node.args[0]).slice(1, -1) - } else if (latexParser.isCommand(node) && labelCmdNames.includes(node.name) - && node.args.length === 2 - && latexParser.isOptionalArg(node.args[0]) - && latexParser.isGroup(node.args[1])) { - // \label[opt_arg]{actual_label} - label = latexParser.stringify(node.args[1]).slice(1, -1) - } else if (latexParser.isTextString(node) && node.content === 'label=' - && useLabelKeyVal && nextNode !== undefined) { - // label={some-text} - label = latexParser.stringify(nextNode).slice(1, -1) + + let label = '' + const configuration = vscode.workspace.getConfiguration('latex-workshop') + const labelMacros = configuration.get('intellisense.label.command') as string[] + if (node.type === 'macro' && labelMacros.includes(node.content)) { + label = argContentToStr(node.args?.[1]?.content || []) + } else if (node.type === 'environment' && ['frame'].includes(node.env)) { + label = argContentToStr(node.args?.[1]?.content || []).split(',').map(arg => arg.trim()).find(arg => arg.startsWith('label='))?.slice(6) ?? '' + if (label.charAt(0) === '{' && label.charAt(label.length - 1) === '}') { + label = label.slice(1, label.length - 1) + } } - if (label !== '' && - (latexParser.isLabelCommand(node) - || latexParser.isCommand(node) - || latexParser.isTextString(node))) { + + if (label !== '' && node.position !== undefined) { refs.push({ label, kind: vscode.CompletionItemKind.Reference, // One row before, four rows after - documentation: lines.slice(node.location.start.line - 2, node.location.end.line + 4).join('\n'), + documentation: lines.slice(node.position.start.line - 2, node.position.end.line + 4).join('\n'), // Here we abuse the definition of range to store the location of the reference definition - range: new vscode.Range(node.location.start.line - 1, node.location.start.column, - node.location.end.line - 1, node.location.end.column) + range: new vscode.Range(node.position.start.line - 1, node.position.start.column - 1, + node.position.end.line - 1, node.position.end.column - 1) }) - return refs } - if (latexParser.hasContentArray(node)) { - return this.getRefFromNodeArray(node.content, lines) - } - if (latexParser.hasArgsArray(node)) { - return this.getRefFromNodeArray(node.args, lines) - } - if (latexParser.isLstlisting(node)) { - const arg = (node as latexParser.Lstlisting).arg - if (arg) { - return this.getRefFromNode(arg, lines) + + if ('content' in node && typeof node.content !== 'string') { + for (const subNode of node.content) { + refs = [...refs, ...this.parseAst(subNode, lines)] } } + return refs } - private getRefFromContent(content: string): ICompletionItem[] { + private parseContent(content: string): ICompletionItem[] { const refReg = /(?:\\label(?:\[[^[\]{}]*\])?|(?:^|[,\s])label=){([^#\\}]*)}/gm const refs: ICompletionItem[] = [] const refList: string[] = [] - content = stripEnvironments(content, this.envsToSkip) + content = stripEnvironments(content, ['']) while (true) { const result = refReg.exec(content) if (result === null) { diff --git a/src/providers/structure.ts b/src/providers/structure.ts index 208e1490b..97c83b369 100644 --- a/src/providers/structure.ts +++ b/src/providers/structure.ts @@ -61,6 +61,7 @@ export class StructureView implements vscode.TreeDataProvider { ev.affectsConfiguration('latex-workshop.view.outline.commands')) { parser.resetUnifiedParser() lw.cacher.allPaths.forEach(filePath => parser.unifiedArgsParse(lw.cacher.get(filePath)?.ast)) + void this.reconstruct() } }) } diff --git a/src/providers/structurelib/latex.ts b/src/providers/structurelib/latex.ts index 7c753ff3f..818939206 100644 --- a/src/providers/structurelib/latex.ts +++ b/src/providers/structurelib/latex.ts @@ -8,6 +8,7 @@ import { InputFileRegExp } from '../../utils/inputfilepath' import { getLogger } from '../../components/logger' import { parser } from '../../components/parser' +import { argContentToStr } from '../../utils/parser' const logger = getLogger('Structure', 'LaTeX') @@ -91,47 +92,6 @@ async function constructFile(filePath: string, config: StructureConfig, structs: structs[filePath] = rootElement.children } -function macroToStr(macro: Ast.Macro): string { - if (macro.content === 'texorpdfstring') { - return (macro.args?.[1].content[0] as Ast.String | undefined)?.content || '' - } - return `\\${macro.content}` + (macro.args?.map(arg => `${arg.openMark}${argContentToStr(arg.content)}${arg.closeMark}`).join('') ?? '') -} - -function envToStr(env: Ast.Environment | Ast.VerbatimEnvironment): string { - return `\\environment{${env.env}}` -} - -function argContentToStr(argContent: Ast.Node[]): string { - return argContent.map(node => { - // Verb - switch (node.type) { - case 'string': - return node.content - case 'whitespace': - case 'parbreak': - case 'comment': - return ' ' - case 'macro': - return macroToStr(node) - case 'environment': - case 'verbatim': - case 'mathenv': - return envToStr(node) - case 'inlinemath': - return `$${argContentToStr(node.content)}$` - case 'displaymath': - return `\\[${argContentToStr(node.content)}\\]` - case 'group': - return argContentToStr(node.content) - case 'verb': - return node.content - default: - return '' - } - }).join('') -} - async function parseNode( node: Ast.Node, rnwSub: ReturnType, diff --git a/src/utils/parser.ts b/src/utils/parser.ts new file mode 100644 index 000000000..04c58aa05 --- /dev/null +++ b/src/utils/parser.ts @@ -0,0 +1,42 @@ +import type * as Ast from '@unified-latex/unified-latex-types' + +function macroToStr(macro: Ast.Macro): string { + if (macro.content === 'texorpdfstring') { + return (macro.args?.[1].content[0] as Ast.String | undefined)?.content || '' + } + return `\\${macro.content}` + (macro.args?.map(arg => `${arg.openMark}${argContentToStr(arg.content)}${arg.closeMark}`).join('') ?? '') +} + +function envToStr(env: Ast.Environment | Ast.VerbatimEnvironment): string { + return `\\environment{${env.env}}` +} + +export function argContentToStr(argContent: Ast.Node[]): string { + return argContent.map(node => { + // Verb + switch (node.type) { + case 'string': + return node.content + case 'whitespace': + case 'parbreak': + case 'comment': + return ' ' + case 'macro': + return macroToStr(node) + case 'environment': + case 'verbatim': + case 'mathenv': + return envToStr(node) + case 'inlinemath': + return `$${argContentToStr(node.content)}$` + case 'displaymath': + return `\\[${argContentToStr(node.content)}\\]` + case 'group': + return argContentToStr(node.content) + case 'verb': + return node.content + default: + return '' + } + }).join('') +} diff --git a/test/fixtures/armory/intellisense/base.tex b/test/fixtures/armory/intellisense/base.tex index 264d9566b..fc149e1f9 100644 --- a/test/fixtures/armory/intellisense/base.tex +++ b/test/fixtures/armory/intellisense/base.tex @@ -10,7 +10,7 @@ main main \begin{align} E = mc^{2} \end{align} -label={eq1} +\begin{frame}[label={frame}]label={trap}\end{frame} \lstinline[showlines]{test} \begin{lstlisting}[print] \end{lstlisting} diff --git a/test/suites/04_intellisense.test.ts b/test/suites/04_intellisense.test.ts index 6e3069162..376f83bf3 100644 --- a/test/suites/04_intellisense.test.ts +++ b/test/suites/04_intellisense.test.ts @@ -230,7 +230,8 @@ suite('Intellisense test suite', () => { ]) let suggestions = test.suggest(8, 5) assert.ok(suggestions.labels.includes('sec1')) - assert.ok(suggestions.labels.includes('eq1')) + assert.ok(suggestions.labels.includes('frame')) + assert.ok(!suggestions.labels.includes('trap')) await vscode.workspace.getConfiguration('latex-workshop').update('intellisense.label.keyval', false) await test.load(fixture, [