diff --git a/package.json b/package.json index 228e9b057..583433ff4 100644 --- a/package.json +++ b/package.json @@ -1382,13 +1382,6 @@ ], "markdownDescription": "The names of the commands to be shown in the outline/structure views. The commands must be called in the form `\\commandname{arg}`." }, - "latex-workshop.view.outline.fastparse.enabled": { - "scope": "window", - "type": "boolean", - "default": true, - "deprecationMessage": "Deprecated: This config has been renamed to `latex-workshop.intellisense.fastparse.enabled`.", - "markdownDeprecationMessage": "**Deprecated**: This config has been renamed to `#latex-workshop.intellisense.fastparse.enabled#`." - }, "latex-workshop.view.outline.floats.enabled": { "scope": "window", "type": "boolean", @@ -1804,12 +1797,6 @@ "default": 1000, "markdownDescription": "The minimal time interval between two consecutive runs of `texcount` in milliseconds when `#latex-workshop.texcount.run#` is set to `onSave`." }, - "latex-workshop.intellisense.fastparse.enabled": { - "scope": "window", - "type": "boolean", - "default": true, - "markdownDescription": "Use fast LaTeX parsing algorithm to build outline/structure. This is done by inherently removing texts and comments before building AST. Enabling this will not tamper the document, but may result in incomplete outline/structure." - }, "latex-workshop.intellisense.update.aggressive.enabled": { "scope": "window", "type": "boolean", @@ -1975,12 +1962,6 @@ ], "markdownDescription": "The name of LaTeX commands that indicates a label definition. The command must accept one mandatory argument of the label reference string, e.g, \\linelabel{ref-str}." }, - "latex-workshop.intellisense.label.keyval": { - "scope": "window", - "type": "boolean", - "default": true, - "markdownDescription": "Scan for labels defined as `label={some tex}` to add to the reference intellisense menu. The braces are mandatory." - }, "latex-workshop.intellisense.unimathsymbols.enabled": { "scope": "window", "type": "boolean", diff --git a/src/components/cacher.ts b/src/components/cacher.ts index 95d107d38..7fd54c2c7 100644 --- a/src/components/cacher.ts +++ b/src/components/cacher.ts @@ -1,7 +1,6 @@ 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 * as eventbus from './eventbus' @@ -20,9 +19,13 @@ import { performance } from 'perf_hooks' const logger = getLogger('Cacher') -interface Cache { +export interface Cache { + /** The raw file path of this Cache. */ + filePath: string, /** Cached content of file. Dirty if opened in vscode, disk otherwise */ - content: string | undefined, + content: string, + /** Cached trimmed content of `content`. */ + contentTrimmed: string, /** Completion items */ elements: { /** \ref{} items */ @@ -50,8 +53,6 @@ interface Cache { /** A dictionary of external documents provided by `\externaldocument` of * `xr` package. The value is its prefix `\externaldocument[prefix]{*}` */ external: {[filePath: string]: string}, - /** The AST of this file, generated by latex-utensils */ - luAst?: latexParser.LatexAst, /** The AST of this file, generated by unified-latex */ ast?: Ast.Root } @@ -101,6 +102,22 @@ export class Cacher { return this.promises[filePath] } + async wait(filePath: string, seconds = 2) { + let waited = 0 + while (!this.promise(filePath) && !this.has(filePath)) { + // Just open vscode, has not cached, wait for a bit? + await new Promise(resolve => setTimeout(resolve, 100)) + waited++ + if (waited >= seconds * 10) { + // Waited for two seconds before starting cache. Really? + logger.log(`Error loading cache: ${filePath} . Forcing.`) + await this.refreshCache(filePath) + break + } + } + return this.promise(filePath) + } + get allPromises() { return Object.values(this.promises) } @@ -126,19 +143,22 @@ export class Cacher { } logger.log(`Caching ${filePath} .`) this.caching++ - const content = lw.lwfs.readFileSyncGracefully(filePath) - this.caches[filePath] = {content, elements: {}, children: [], bibfiles: new Set(), external: {}} - if (content === undefined) { - logger.log(`Cannot read ${filePath} .`) - return - } - const contentTrimmed = utils.stripCommentsAndVerbatim(content) + const content = lw.lwfs.readFileSyncGracefully(filePath) ?? '' + const cache: Cache = { + filePath, + content, + contentTrimmed: utils.stripCommentsAndVerbatim(content), + elements: {}, + children: [], + bibfiles: new Set(), + external: {}} + this.caches[filePath] = cache rootPath = rootPath || lw.manager.rootFile - this.updateChildren(filePath, rootPath, contentTrimmed) + this.updateChildren(cache, rootPath) - this.promises[filePath] = this.updateAST(filePath, content).then(() => { - this.updateElements(filePath, content, contentTrimmed) - this.updateBibfiles(filePath, contentTrimmed) + this.promises[filePath] = this.updateAST(cache).then(() => { + this.updateElements(cache) + this.updateBibfiles(cache) }).finally(() => { logger.log(`Cached ${filePath} .`) this.caching-- @@ -153,40 +173,29 @@ export class Cacher { return this.promises[filePath] } - private async updateAST(filePath: string, content: string) { - const configuration = vscode.workspace.getConfiguration('latex-workshop') - const fastparse = configuration.get('intellisense.fastparse.enabled') as boolean - logger.log('Parse LaTeX AST ' + (fastparse ? 'with fast-parse: ' : ': ') + filePath + ' .') - - let start = performance.now() - const strippedText = utils.stripText(content) - const ast = await parser.parseLatex(fastparse ? strippedText : content) - logger.log(`Parsed LaTeX AST with LU in ${(performance.now() - start).toFixed(2)} ms: ${filePath} .`) - - const cache = this.get(filePath) - if (cache) { - start = performance.now() - cache.ast = parser.unifiedParse(content) - logger.log(`Parsed LaTeX AST in ${(performance.now() - start).toFixed(2)} ms: ${filePath} .`) - } - if (ast && cache) { - cache.luAst = ast - } else { - logger.log(ast === undefined ? 'Failed parsing LaTeX AST.' : `Cannot get cache for ${filePath} .`) - } + private async updateAST(cache: Cache): Promise { + logger.log(`Parse LaTeX AST: ${cache.filePath} .`) + const start = performance.now() + return new Promise((resolve, _) => { + setTimeout(() => { + cache.ast = parser.unifiedParse(cache.content) + logger.log(`Parsed LaTeX AST in ${(performance.now() - start).toFixed(2)} ms: ${cache.filePath} .`) + resolve() + }, 0) + }) } - private updateChildren(filePath: string, rootPath: string | undefined, contentTrimmed: string) { - rootPath = rootPath || filePath - this.updateChildrenInput(filePath, rootPath, contentTrimmed) - this.updateChildrenXr(filePath, rootPath, contentTrimmed) - logger.log(`Updated inputs of ${filePath} .`) + private updateChildren(cache: Cache, rootPath: string | undefined) { + rootPath = rootPath || cache.filePath + this.updateChildrenInput(cache, rootPath) + this.updateChildrenXr(cache, rootPath) + logger.log(`Updated inputs of ${cache.filePath} .`) } - private updateChildrenInput(filePath: string, rootPath: string , contentTrimmed: string) { + private updateChildrenInput(cache: Cache, rootPath: string) { const inputFileRegExp = new InputFileRegExp() while (true) { - const result = inputFileRegExp.exec(contentTrimmed, filePath, rootPath) + const result = inputFileRegExp.exec(cache.contentTrimmed, cache.filePath, rootPath) if (!result) { break } @@ -199,7 +208,7 @@ export class Cacher { index: result.match.index, filePath: result.path }) - logger.log(`Input ${result.path} from ${filePath} .`) + logger.log(`Input ${result.path} from ${cache.filePath} .`) if (this.src.has(result.path)) { continue @@ -209,16 +218,16 @@ export class Cacher { } } - private updateChildrenXr(filePath: string, rootPath: string , contentTrimmed: string) { + private updateChildrenXr(cache: Cache, rootPath: string) { const externalDocRegExp = /\\externaldocument(?:\[(.*?)\])?\{(.*?)\}/g while (true) { - const result = externalDocRegExp.exec(contentTrimmed) + const result = externalDocRegExp.exec(cache.contentTrimmed) if (!result) { break } const texDirs = vscode.workspace.getConfiguration('latex-workshop').get('latex.texDirs') as string[] - const externalPath = utils.resolveFile([path.dirname(filePath), path.dirname(rootPath), ...texDirs], result[2]) + const externalPath = utils.resolveFile([path.dirname(cache.filePath), path.dirname(rootPath), ...texDirs], result[2]) if (!externalPath || !fs.existsSync(externalPath) || path.relative(externalPath, rootPath) === '') { logger.log(`Failed resolving external ${result[2]} . Tried ${externalPath} ` + (externalPath && path.relative(externalPath, rootPath) === '' ? ', which is root.' : '.')) @@ -226,7 +235,7 @@ export class Cacher { } this.caches[rootPath].external[externalPath] = result[1] || '' - logger.log(`External document ${externalPath} from ${filePath} .` + + logger.log(`External document ${externalPath} from ${cache.filePath} .` + (result[1] ? ` Prefix is ${result[1]}`: '')) if (this.src.has(externalPath)) { @@ -237,31 +246,22 @@ export class Cacher { } } - private updateElements(filePath: string, content: string, contentTrimmed: string) { - lw.completer.citation.update(filePath, content) - const cache = this.get(filePath) - 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) - } - lw.duplicateLabels.run(filePath) - logger.log(`Updated elements of ${filePath} .`) + private updateElements(cache: Cache) { + lw.completer.citation.update(cache.filePath, cache.content) + // Package parsing must be before command and environment. + lw.completer.package.parse(cache) + lw.completer.reference.parse(cache) + lw.completer.glossary.parse(cache) + lw.completer.environment.parse(cache) + lw.completer.command.parse(cache) + lw.duplicateLabels.run(cache.filePath) + logger.log(`Updated elements of ${cache.filePath} .`) } - private updateBibfiles(filePath: string, contentTrimmed: string) { + private updateBibfiles(cache: Cache) { const bibReg = /(?:\\(?:bibliography|addbibresource)(?:\[[^[\]{}]*\])?){(.+?)}|(?:\\putbib)\[(.+?)\]/g while (true) { - const result = bibReg.exec(contentTrimmed) + const result = bibReg.exec(cache.contentTrimmed) if (!result) { break } @@ -269,18 +269,18 @@ export class Cacher { const bibs = (result[1] ? result[1] : result[2]).split(',').map(bib => bib.trim()) for (const bib of bibs) { - const bibPath = PathUtils.resolveBibPath(bib, path.dirname(filePath)) + const bibPath = PathUtils.resolveBibPath(bib, path.dirname(cache.filePath)) if (bibPath === undefined) { continue } - this.caches[filePath].bibfiles.add(bibPath) - logger.log(`Bib ${bibPath} from ${filePath} .`) + cache.bibfiles.add(bibPath) + logger.log(`Bib ${bibPath} from ${cache.filePath} .`) if (!this.bib.has(bibPath)) { this.bib.add(bibPath) } } } - logger.log(`Updated bibs of ${filePath} .`) + logger.log(`Updated bibs of ${cache.filePath} .`) } /** diff --git a/src/components/envpair.ts b/src/components/envpair.ts index 3def3ccf2..df42e28b9 100644 --- a/src/components/envpair.ts +++ b/src/components/envpair.ts @@ -1,8 +1,8 @@ import * as vscode from 'vscode' import * as lw from '../lw' import { getLogger } from './logger' -import { parser } from './parser' -import { latexParser } from 'latex-utensils' +import type * as Ast from '@unified-latex/unified-latex-types' +import { argContentToStr } from '../utils/parser' const logger = getLogger('EnvPair') @@ -67,33 +67,32 @@ export class EnvPair { constructor() {} - async buildCommandPairTree(doc: vscode.TextDocument): Promise { - logger.log(`Parse LaTeX AST : ${doc.fileName} .`) - let ast: latexParser.LatexAst | undefined = await parser.parseLatex(doc.getText()) - - if (!ast) { - logger.log('Failed to parse LaTeX AST, fallback to cached AST.') - await lw.cacher.promise(doc.fileName) - ast = lw.cacher.get(doc.fileName)?.luAst - } - - if (!ast) { - logger.log(`Failed to load AST for ${doc.fileName} .`) + async buildCommandPairTree(document: vscode.TextDocument): Promise { + await lw.cacher.wait(document.fileName) + const content = lw.cacher.get(document.fileName)?.content + const ast = lw.cacher.get(document.fileName)?.ast + if (!content || !ast) { + logger.log(`Error loading ${content ? 'AST' : 'content'} during structuring: ${document.fileName} .`) return [] } - logger.log(`Parsed ${ast.content.length} AST items.`) const commandPairs: CommandPair[] = [] let parentPair: CommandPair | undefined = undefined - for (const node of ast.content) { - parentPair = this.buildCommandPairTreeFromNode(doc, node, parentPair, commandPairs) + for (let index = 0; index < ast.content.length; index++) { + const node = ast.content[index] + const next = index === ast.content.length - 1 ? undefined : ast.content[index + 1] + parentPair = this.buildCommandPairTreeFromNode(document, node, next, parentPair, commandPairs) } return commandPairs } - private buildCommandPairTreeFromNode(doc: vscode.TextDocument, node: latexParser.Node, parentCommandPair: CommandPair | undefined, commandPairs: CommandPair[]): CommandPair | undefined { - if (latexParser.isEnvironment(node) || latexParser.isMathEnv(node) || latexParser.isMathEnvAligned(node)) { - const name = node.name + private buildCommandPairTreeFromNode(doc: vscode.TextDocument, node: Ast.Node, next: Ast.Node | undefined, parentCommandPair: CommandPair | undefined, commandPairs: CommandPair[]): CommandPair | undefined { + if (node.position === undefined) { + return parentCommandPair + } + if (node.type === 'environment' || node.type === 'mathenv') { + // The following is necessary as node.env may be Ast.String, bug in upstream (16.06.23) + const name = argContentToStr([node.env as unknown as Ast.Node]) || node.env let currentCommandPair: CommandPair | undefined // If we encounter `\begin{document}`, clear commandPairs if (name === 'document') { @@ -103,8 +102,8 @@ export class EnvPair { } else { const beginName = `\\begin{${name}}` const endName = `\\end{${name}}` - const beginPos = new vscode.Position(node.location.start.line - 1, node.location.start.column - 1) - const endPos = new vscode.Position(node.location.end.line - 1, node.location.end.column - 1) + const beginPos = new vscode.Position(node.position.start.line - 1, node.position.start.column - 1) + const endPos = new vscode.Position(node.position.end.line - 1, node.position.end.column - 1) currentCommandPair = new CommandPair(PairType.ENVIRONMENT, beginName, beginPos, endName, endPos) if (parentCommandPair) { currentCommandPair.parent = parentCommandPair @@ -114,13 +113,15 @@ export class EnvPair { } parentCommandPair = currentCommandPair } - for (const subnode of node.content) { - parentCommandPair = this.buildCommandPairTreeFromNode(doc, subnode, parentCommandPair, commandPairs) + for (let index = 0; index < node.content.length; index++) { + const subnode = node.content[index] + const subnext = index === node.content.length - 1 ? undefined : node.content[index + 1] + parentCommandPair = this.buildCommandPairTreeFromNode(doc, subnode, subnext, parentCommandPair, commandPairs) } parentCommandPair = currentCommandPair?.parent - } else if (latexParser.isDisplayMath(node)) { - const beginPos = new vscode.Position(node.location.start.line - 1, node.location.start.column - 1) - const endPos = new vscode.Position(node.location.end.line - 1, node.location.end.column - 1) + } else if (node.type === 'displaymath') { + const beginPos = new vscode.Position(node.position.start.line - 1, node.position.start.column - 1) + const endPos = new vscode.Position(node.position.end.line - 1, node.position.end.column - 1) if (doc.getText(new vscode.Range(beginPos, beginPos.translate(0, 2))) === '$$') { const currentCommandPair = new CommandPair(PairType.DISPLAYMATH, '$$', beginPos, '$$', endPos) commandPairs.push(currentCommandPair) @@ -128,9 +129,9 @@ export class EnvPair { const currentCommandPair = new CommandPair(PairType.DISPLAYMATH, '\\[', beginPos, '\\]', endPos) commandPairs.push(currentCommandPair) } - } else if (latexParser.isInlienMath(node)) { - const beginPos = new vscode.Position(node.location.start.line - 1, node.location.start.column - 1) - const endPos = new vscode.Position(node.location.end.line - 1, node.location.end.column - 1) + } else if (node.type === 'inlinemath') { + const beginPos = new vscode.Position(node.position.start.line - 1, node.position.start.column - 1) + const endPos = new vscode.Position(node.position.end.line - 1, node.position.end.column - 1) if (doc.getText(new vscode.Range(beginPos, beginPos.translate(0, 1))) === '$') { const currentCommandPair = new CommandPair(PairType.INLINEMATH, '$', beginPos, '$', endPos) commandPairs.push(currentCommandPair) @@ -138,11 +139,11 @@ export class EnvPair { const currentCommandPair = new CommandPair(PairType.INLINEMATH, '\\(', beginPos, '\\)', endPos) commandPairs.push(currentCommandPair) } - } else if (latexParser.isCommand(node)) { - if (node.name === 'begin' && node.args.length > 0 && latexParser.isGroup(node.args[0])) { + } else if (node.type === 'macro') { + if (node.content === 'begin' && next?.type === 'group' && next.content[0]?.type === 'string') { // This is an unbalanced environment - const beginPos = new vscode.Position(node.location.start.line - 1, node.location.start.column - 1) - const envName = latexParser.stringify(node.args[0]).slice(1, -1) + const beginPos = new vscode.Position(node.position.start.line - 1, node.position.start.column - 1) + const envName = next.content[0].content const name = `\\begin{${envName}}` const currentCommandPair = new CommandPair(PairType.ENVIRONMENT, name, beginPos) if (parentCommandPair) { @@ -154,18 +155,18 @@ export class EnvPair { // currentCommandPair becomes the new parent return currentCommandPair } - const name = '\\' + node.name + const name = '\\' + node.content for (const pair of EnvPair.delimiters) { if (pair.type === PairType.COMMAND && name.match(pair.end) && parentCommandPair && parentCommandPair.start.match(pair.start)) { parentCommandPair.end = name - parentCommandPair.endPosition = new vscode.Position(node.location.end.line - 1, node.location.end.column - 1) + parentCommandPair.endPosition = new vscode.Position(node.position.end.line - 1, node.position.end.column - 1) parentCommandPair = parentCommandPair.parent // Do not return after finding an 'end' token as it can also be the start of an other pair. } } for (const pair of EnvPair.delimiters) { if (pair.type === PairType.COMMAND && name.match(pair.start)) { - const beginPos = new vscode.Position(node.location.start.line - 1, node.location.start.column - 1) + const beginPos = new vscode.Position(node.position.start.line - 1, node.position.start.column - 1) const currentCommandPair = new CommandPair(PairType.COMMAND, name, beginPos) if (parentCommandPair) { currentCommandPair.parent = parentCommandPair diff --git a/src/components/logger.ts b/src/components/logger.ts index fb210be07..af3f5c582 100644 --- a/src/components/logger.ts +++ b/src/components/logger.ts @@ -52,7 +52,7 @@ function logTagless(message: string) { return } const date = new Date() - const timestamp = `${date.toLocaleTimeString('en-US', { hour12: false })}.${date.getMilliseconds()}` + const timestamp = `${date.toLocaleTimeString('en-US', { hour12: false })}.${date.getMilliseconds().toString().padStart(3, '0')}` vscode.workspace.workspaceFolders?.forEach(folder => { if (folder.uri.fsPath in PLACEHOLDERS) { return diff --git a/src/components/parserlib/defs.ts b/src/components/parserlib/defs.ts index 6dd4f0ad7..49798bf8e 100644 --- a/src/components/parserlib/defs.ts +++ b/src/components/parserlib/defs.ts @@ -15,6 +15,17 @@ const MACROS: MacroInfoRecord = { subimport: { signature: 'm m' }, subinputfrom: { signature: 'm m' }, subincludefrom: { signature: 'm m' }, + // \label{some-label} + linelabel: { signature: 'o m'}, + // \newglossaryentry{vscode}{name=VSCode, description=Editor} + newglossaryentry: { signature: 'm m'}, + provideglossaryentry: { signature: 'm m'}, + // \newacronym[optional parameters]{lw}{LW}{LaTeX Workshop} + longnewglossaryentry: { signature: 'o m m m'}, + longprovideglossaryentry: { signature: 'o m m m'}, + newacronym: { signature: 'o m m m'}, + newabbreviation: { signature: 'o m m m'}, + newabbr: { signature: 'o m m m'}, } const ENVS: EnvInfoRecord = {} diff --git a/src/providers/completer/command.ts b/src/providers/completer/command.ts index 47d36a001..1d646405a 100644 --- a/src/providers/completer/command.ts +++ b/src/providers/completer/command.ts @@ -1,15 +1,14 @@ import * as vscode from 'vscode' import * as fs from 'fs' -import { latexParser } from 'latex-utensils' +import type * as Ast from '@unified-latex/unified-latex-types' import * as lw from '../../lw' import type { IProvider, ICompletionItem, PkgType, IProviderArgs } from '../completion' -import { CommandFinder, isTriggerSuggestNeeded } from './commandlib/commandfinder' import { CmdEnvSuggestion, splitSignatureString, filterNonLetterSuggestions, filterArgumentHint } from './completerutils' -import { CommandSignatureDuplicationDetector } from './commandlib/commandfinder' import {SurroundCommand} from './commandlib/surround' import { Environment, EnvSnippetType } from './environment' import { getLogger } from '../../components/logger' +import { Cache } from '../../components/cacher' const logger = getLogger('Intelli', 'Command') @@ -36,17 +35,26 @@ export type CmdType = { postAction?: string } +export function isTriggerSuggestNeeded(name: string): boolean { + const reg = /^(?:[a-z]*(cite|ref|input)[a-z]*|begin|bibitem|(sub)?(import|includefrom|inputfrom)|gls(?:pl|text|first|plural|firstplural|name|symbol|desc|user(?:i|ii|iii|iv|v|vi))?|Acr(?:long|full|short)?(?:pl)?|ac[slf]?p?)/i + return reg.test(name) +} + function isCmdWithSnippet(obj: any): obj is CmdType { return (typeof obj.command === 'string') && (typeof obj.snippet === 'string') } export class Command implements IProvider { - private defaultCmds: CmdEnvSuggestion[] = [] - private readonly _defaultSymbols: CmdEnvSuggestion[] = [] + definedCmds = new Map() + defaultCmds: CmdEnvSuggestion[] = [] + private readonly defaultSymbols: CmdEnvSuggestion[] = [] private readonly packageCmds = new Map() constructor() { + const symbols: { [key: string]: CmdType } = JSON.parse(fs.readFileSync(`${lw.extensionRoot}/data/unimathsymbols.json`).toString()) as DataUnimathSymbolsJsonType + Object.entries(symbols).forEach(([key, symbol]) => this.defaultSymbols.push(this.entryCmdToCompletion(key, symbol))) + lw.registerDisposable(vscode.workspace.onDidChangeConfiguration((e: vscode.ConfigurationChangeEvent) => { if (!e.affectsConfiguration('latex-workshop.intellisense.command.user') && !e.affectsConfiguration('latex-workshop.intellisense.package.exclude')) { @@ -90,23 +98,6 @@ export class Command implements IProvider { }) } - get definedCmds() { - return CommandFinder.definedCmds - } - - get defaultSymbols() { - if (this._defaultSymbols.length > 0) { - return this._defaultSymbols - } - const symbols: { [key: string]: CmdType } = JSON.parse(fs.readFileSync(`${lw.extensionRoot}/data/unimathsymbols.json`).toString()) as DataUnimathSymbolsJsonType - Object.entries(symbols).forEach(([key, symbol]) => this._defaultSymbols.push(this.entryCmdToCompletion(key, symbol))) - return this._defaultSymbols - } - - getDefaultCmds(): CmdEnvSuggestion[] { - return this.defaultCmds - } - provideFrom(result: RegExpMatchArray, args: IProviderArgs) { const suggestions = this.provide(args.langId, args.line, args.position) // Commands ending with (, { or [ are not filtered properly by vscode intellisense. So we do it by hand. @@ -131,7 +122,7 @@ export class Command implements IProvider { } } const suggestions: CmdEnvSuggestion[] = [] - const cmdDuplicationDetector = new CommandSignatureDuplicationDetector() + let defined = new Set() // Insert default commands this.defaultCmds.forEach(cmd => { if (!useOptionalArgsEntries && cmd.hasOptionalArgs()) { @@ -139,14 +130,14 @@ export class Command implements IProvider { } cmd.range = range suggestions.push(cmd) - cmdDuplicationDetector.add(cmd) + defined.add(cmd.signatureAsString()) }) // Insert unimathsymbols if (configuration.get('intellisense.unimathsymbols.enabled')) { this.defaultSymbols.forEach(symbol => { suggestions.push(symbol) - cmdDuplicationDetector.add(symbol) + defined.add(symbol.signatureAsString()) }) } @@ -154,22 +145,22 @@ export class Command implements IProvider { if ((configuration.get('intellisense.package.enabled'))) { const packages = lw.completer.package.getPackagesIncluded(langId) Object.entries(packages).forEach(([packageName, options]) => { - this.provideCmdInPkg(packageName, options, suggestions, cmdDuplicationDetector) - lw.completer.environment.provideEnvsAsCommandInPkg(packageName, options, suggestions, cmdDuplicationDetector) + this.provideCmdInPkg(packageName, options, suggestions) + lw.completer.environment.provideEnvsAsCommandInPkg(packageName, options, suggestions, defined) }) } // Start working on commands in tex. To avoid over populating suggestions, we do not include // user defined commands, whose name matches a default command or one provided by a package - const commandSignatureDuplicationDetector = new CommandSignatureDuplicationDetector(suggestions) + defined = new Set(suggestions.map(s => s.signatureAsString())) lw.cacher.getIncludedTeX().forEach(tex => { const cmds = lw.cacher.get(tex)?.elements.command if (cmds !== undefined) { cmds.forEach(cmd => { - if (!commandSignatureDuplicationDetector.has(cmd)) { + if (!defined.has(cmd.signatureAsString())) { cmd.range = range suggestions.push(cmd) - commandSignatureDuplicationDetector.add(cmd) + defined.add(cmd.signatureAsString()) } }) } @@ -194,37 +185,161 @@ export class Command implements IProvider { SurroundCommand.surround(cmdItems) } - /** - * Updates the Manager cache for commands used 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 content The content of a LaTeX file. - */ - update(file: string, nodes?: latexParser.Node[], content?: string) { - // First, we must update the package list - lw.completer.package.updateUsepackage(file, nodes, content) + parse(cache: Cache) { // Remove newcommand cmds, because they will be re-insert in the next step this.definedCmds.forEach((entry,cmd) => { - if (entry.file === file) { + if (entry.filePath === cache.filePath) { this.definedCmds.delete(cmd) } }) - const cache = lw.cacher.get(file) - if (cache === undefined) { - return + if (cache.ast !== undefined) { + cache.elements.command = this.parseAst(cache.ast, cache.filePath) + } else { + cache.elements.command = this.parseContent(cache.content, cache.filePath) + } + } + + private parseAst(node: Ast.Node, filePath: string, defined?: Set): CmdEnvSuggestion[] { + defined = defined ?? new Set() + let cmds: CmdEnvSuggestion[] = [] + if (node.type === 'macro' && + ['renewcommand', 'newcommand', 'providecommand', 'DeclareMathOperator'].includes(node.content) && + node.args?.[2]?.content?.[0]?.type === 'macro') { + // \newcommand{\fix}[3][]{\chdeleted{#2}\chadded[comment={#1}]{#3}} + // \newcommand\WARNING{\textcolor{red}{WARNING}} + const name = node.args[2].content[0].content + let args = '' + if (node.args?.[3].content?.[0]?.type === 'string' && + parseInt(node.args?.[3].content?.[0].content) > 0) { + args = (node.args?.[4].openMark === '[' ? '[]' : '{}') + '{}'.repeat(parseInt(node.args?.[3].content?.[0].content) - 1) + } + if (!defined.has(`${name}${args}`)) { + const cmd = new CmdEnvSuggestion(`\\${name}${args}`, 'user-defined', [], -1, {name, args}, vscode.CompletionItemKind.Function) + cmd.documentation = '`' + name + '`' + let argTabs = args + let index = 0 + while (argTabs.includes('[]')) { + argTabs = argTabs.replace('[]', '[${' + index + '}]') + index++ + } + while (argTabs.includes('{}')) { + argTabs = argTabs.replace('{}', '{${' + index + '}}') + index++ + } + cmd.insertText = new vscode.SnippetString(name + argTabs) + cmd.filterText = name + if (isTriggerSuggestNeeded(name)) { + cmd.command = { title: 'Post-Action', command: 'editor.action.triggerSuggest' } + } + cmds.push(cmd) + this.definedCmds.set(cmd.signatureAsString(), { + filePath, + location: new vscode.Location( + vscode.Uri.file(filePath), + new vscode.Position( + (node.position?.start.line ?? 1) - 1, + (node.position?.start.column ?? 1) - 1)) + }) + defined.add(cmd.signatureAsString()) + } } + + if ('content' in node && typeof node.content !== 'string') { + for (const subNode of node.content) { + cmds = [...cmds, ...this.parseAst(subNode, filePath, defined)] + } + } + + return cmds + } + + private parseContent(content: string, filePath: string): CmdEnvSuggestion[] { const cmdInPkg: CmdEnvSuggestion[] = [] const packages = lw.completer.package.getPackagesIncluded('latex-expl3') Object.entries(packages).forEach(([packageName, options]) => { - this.provideCmdInPkg(packageName, options, cmdInPkg, new CommandSignatureDuplicationDetector()) + this.provideCmdInPkg(packageName, options, cmdInPkg) }) - if (nodes !== undefined) { - cache.elements.command = CommandFinder.getCmdFromNodeArray(file, nodes, cmdInPkg, new CommandSignatureDuplicationDetector()) - } else if (content !== undefined) { - cache.elements.command = CommandFinder.getCmdFromContent(file, content, cmdInPkg) + const cmdReg = /\\([a-zA-Z@_]+(?::[a-zA-Z]*)?\*?)({[^{}]*})?({[^{}]*})?({[^{}]*})?/g + const cmds: CmdEnvSuggestion[] = [] + const defined = new Set() + let explSyntaxOn: boolean = false + while (true) { + const result = cmdReg.exec(content) + if (result === null) { + break + } + if (result[1] === 'ExplSyntaxOn') { + explSyntaxOn = true + continue + } else if (result[1] === 'ExplSyntaxOff') { + explSyntaxOn = false + continue + } + + + if (!explSyntaxOn) { + const len = result[1].search(/[_:]/) + if (len > -1) { + result[1] = result[1].slice(0, len) + } + } + const args = '{}'.repeat(result.length - 1) + const cmd = new CmdEnvSuggestion( + `\\${result[1]}${args}`, + cmdInPkg.find(candidate => candidate.signatureAsString() === result[1] + args)?.package ?? '', + [], + -1, + { name: result[1], args }, + vscode.CompletionItemKind.Function + ) + cmd.documentation = '`' + result[1] + '`' + cmd.insertText = new vscode.SnippetString( + result[1] + (result[2] ? '{${1}}' : '') + (result[3] ? '{${2}}' : '') + (result[4] ? '{${3}}' : '')) + cmd.filterText = result[1] + if (isTriggerSuggestNeeded(result[1])) { + cmd.command = { title: 'Post-Action', command: 'editor.action.triggerSuggest' } + } + if (!defined.has(cmd.signatureAsString())) { + cmds.push(cmd) + defined.add(cmd.signatureAsString()) + } + } + + const newCommandReg = /\\(?:(?:(?:re|provide)?(?:new)?command)|(?:DeclarePairedDelimiter(?:X|XPP)?)|DeclareMathOperator)\*?{?\\(\w+)}?(?:\[([1-9])\])?/g + while (true) { + const result = newCommandReg.exec(content) + if (result === null) { + break + } + + let tabStops = '' + let args = '' + if (result[2]) { + const numArgs = parseInt(result[2]) + for (let i = 1; i <= numArgs; ++i) { + tabStops += '{${' + i + '}}' + args += '{}' + } + } + + const cmd = new CmdEnvSuggestion(`\\${result[1]}${args}`, 'user-defined', [], -1, {name: result[1], args}, vscode.CompletionItemKind.Function) + cmd.documentation = '`' + result[1] + '`' + cmd.insertText = new vscode.SnippetString(result[1] + tabStops) + cmd.filterText = result[1] + if (!defined.has(cmd.signatureAsString())) { + cmds.push(cmd) + defined.add(cmd.signatureAsString()) + } + + this.definedCmds.set(result[1], { + filePath, + location: new vscode.Location( + vscode.Uri.file(filePath), + new vscode.Position(content.substring(0, result.index).split('\n').length - 1, 0)) + }) } + + return cmds } private entryCmdToCompletion(itemKey: string, item: CmdType): CmdEnvSuggestion { @@ -285,7 +400,8 @@ export class Command implements IProvider { return this.packageCmds.get(packageName) || [] } - provideCmdInPkg(packageName: string, options: string[], suggestions: vscode.CompletionItem[], cmdDuplicationDetector: CommandSignatureDuplicationDetector) { + private provideCmdInPkg(packageName: string, options: string[], suggestions: vscode.CompletionItem[]) { + const defined = new Set() const configuration = vscode.workspace.getConfiguration('latex-workshop') const useOptionalArgsEntries = configuration.get('intellisense.optionalArgsEntries.enabled') // Load command in pkg @@ -302,12 +418,12 @@ export class Command implements IProvider { if (!useOptionalArgsEntries && cmd.hasOptionalArgs()) { return } - if (!cmdDuplicationDetector.has(cmd)) { + if (!defined.has(cmd.signatureAsString())) { if (cmd.option && options && !options.includes(cmd.option)) { return } suggestions.push(cmd) - cmdDuplicationDetector.add(cmd) + defined.add(cmd.signatureAsString()) } }) } diff --git a/src/providers/completer/commandlib/commandfinder.ts b/src/providers/completer/commandlib/commandfinder.ts deleted file mode 100644 index 0226b2e33..000000000 --- a/src/providers/completer/commandlib/commandfinder.ts +++ /dev/null @@ -1,282 +0,0 @@ -import * as vscode from 'vscode' -import {latexParser} from 'latex-utensils' -import {CmdEnvSuggestion} from '../completerutils' - - -export function isTriggerSuggestNeeded(name: string): boolean { - const reg = /^(?:[a-z]*(cite|ref|input)[a-z]*|begin|bibitem|(sub)?(import|includefrom|inputfrom)|gls(?:pl|text|first|plural|firstplural|name|symbol|desc|user(?:i|ii|iii|iv|v|vi))?|Acr(?:long|full|short)?(?:pl)?|ac[slf]?p?)/i - return reg.test(name) -} - -export class CommandFinder { - static definedCmds = new Map() - - static getCmdFromNodeArray(file: string, nodes: latexParser.Node[], cmdInPkg: CmdEnvSuggestion[], commandSignatureDuplicationDetector: CommandSignatureDuplicationDetector): CmdEnvSuggestion[] { - let cmds: CmdEnvSuggestion[] = [] - nodes.forEach((node, index) => { - const prev = nodes[index - 1] - const next = nodes[index + 1] - cmds = cmds.concat(CommandFinder.getCmdFromNode(file, node, cmdInPkg, commandSignatureDuplicationDetector, latexParser.isCommand(prev) ? prev : undefined, latexParser.isCommand(next) ? next : undefined)) - }) - return cmds - } - - private static getCmdFromNode(file: string, node: latexParser.Node, cmdInPkg: CmdEnvSuggestion[], commandSignatureDuplicationDetector: CommandSignatureDuplicationDetector, prev?: latexParser.Command, next?: latexParser.Command): CmdEnvSuggestion[] { - const cmds: CmdEnvSuggestion[] = [] - const newCommandDeclarations = ['newcommand', 'renewcommand', 'providecommand', 'DeclareMathOperator', 'DeclarePairedDelimiter', 'DeclarePairedDelimiterX', 'DeclarePairedDelimiterXPP'] - if (latexParser.isDefCommand(node)) { - const name = node.token.slice(1) - const args = CommandFinder.getArgsFromNode(node) - const cmd = new CmdEnvSuggestion(`\\${name}${args}`, '', [], -1, {name, args}, vscode.CompletionItemKind.Function) - if (!commandSignatureDuplicationDetector.has(cmd)) { - cmd.documentation = '`' + name + '`' - cmd.insertText = new vscode.SnippetString(name + CommandFinder.getTabStopsFromNode(node)) - cmd.filterText = name - if (isTriggerSuggestNeeded(name)) { - cmd.command = { title: 'Post-Action', command: 'editor.action.triggerSuggest' } - } - cmds.push(cmd) - commandSignatureDuplicationDetector.add(cmd) - } - } else if (latexParser.isCommand(node) && latexParser.isCommand(prev) && newCommandDeclarations.includes(prev.name.replace(/\*$/, '')) && prev.args.length === 0) { - // We have gone to the WARNING part of \newcommand\WARNING{\textcolor{red}{WARNING}}, already parsed - return cmds - } else if (latexParser.isCommand(node) && newCommandDeclarations.includes(node.name.replace(/\*$/, ''))) { - // \newcommand{\fix}[3][]{\chdeleted{#2}\chadded[comment={#1}]{#3}} - const inside = node.args.length > 0 - && latexParser.isGroup(node.args[0]) - && node.args[0].content.length > 0 - && latexParser.isCommand(node.args[0].content[0]) - // \newcommand\WARNING{\textcolor{red}{WARNING}} - const beside = next - && latexParser.isCommand(next) - && next.args.length > 0 - if (!inside && !beside) { - return cmds - } - const label = ((inside ? node.args[0].content[0] : next) as latexParser.Command).name - let tabStops = '' - let newargs = '' - const argsNode = inside ? node.args : next?.args || [] - const argNumNode = inside ? argsNode[1] : argsNode[0] - if (latexParser.isOptionalArg(argNumNode)) { - const numArgs = parseInt((argNumNode.content[0] as latexParser.TextString).content) - let index = 1 - for (let i = (inside ? 2 : 1); i <= argsNode.length - 1; ++i) { - if (!latexParser.isOptionalArg(argsNode[i])) { - break - } - tabStops += '[${' + index + '}]' - newargs += '[]' - index++ - } - for (; index <= numArgs; ++index) { - tabStops += '{${' + index + '}}' - newargs += '{}' - } - } - const newcmd = new CmdEnvSuggestion(`\\${label}${newargs}`, 'user-defined', [], -1, {name: label, args: newargs}, vscode.CompletionItemKind.Function) - if (!commandSignatureDuplicationDetector.has(newcmd)) { - newcmd.documentation = '`' + label + '`' - newcmd.insertText = new vscode.SnippetString(label + tabStops) - newcmd.filterText = label - if (isTriggerSuggestNeeded(label)) { - newcmd.command = { title: 'Post-Action', command: 'editor.action.triggerSuggest' } - } - cmds.push(newcmd) - CommandFinder.definedCmds.set(newcmd.signatureAsString(), { - file, - location: new vscode.Location( - vscode.Uri.file(file), - new vscode.Position(node.location.start.line - 1, node.location.start.column)) - }) - commandSignatureDuplicationDetector.add(newcmd) - } - } else if (latexParser.isCommand(node)) { - const args = CommandFinder.getArgsFromNode(node) - const cmd = new CmdEnvSuggestion(`\\${node.name}${args}`, '', [], -1, {name: node.name, args}, vscode.CompletionItemKind.Function) - if (!commandSignatureDuplicationDetector.has(cmd) && !newCommandDeclarations.includes(node.name)) { - cmd.package = CommandFinder.whichPackageProvidesCommand(node.name, args, cmdInPkg) - cmd.documentation = '`' + node.name + '`' - cmd.insertText = new vscode.SnippetString(node.name + CommandFinder.getTabStopsFromNode(node)) - if (isTriggerSuggestNeeded(node.name)) { - cmd.command = { title: 'Post-Action', command: 'editor.action.triggerSuggest' } - } - cmds.push(cmd) - commandSignatureDuplicationDetector.add(cmd) - } - } - if (latexParser.hasContentArray(node)) { - return cmds.concat(CommandFinder.getCmdFromNodeArray(file, node.content, cmdInPkg, commandSignatureDuplicationDetector)) - } - return cmds - } - - private static getArgsHelperFromNode(node: latexParser.Node, helper: (i: number) => string): string { - let args = '' - if (!('args' in node)) { - return args - } - let index = 0 - if (latexParser.isCommand(node)) { - node.args.forEach(arg => { - ++index - if (latexParser.isOptionalArg(arg)) { - args += '[' + helper(index) + ']' - } else { - args += '{' + helper(index) + '}' - } - }) - return args - } - if (latexParser.isDefCommand(node)) { - node.args.forEach(arg => { - ++index - if (latexParser.isCommandParameter(arg)) { - args += '{' + helper(index) + '}' - } - }) - return args - } - return args - } - - private static getTabStopsFromNode(node: latexParser.Node): string { - return CommandFinder.getArgsHelperFromNode(node, (i: number) => { return '${' + i + '}' }) - } - - private static getArgsFromNode(node: latexParser.Node): string { - return CommandFinder.getArgsHelperFromNode(node, (_: number) => { return '' }) - } - - - static getCmdFromContent(file: string, content: string, cmdInPkg: CmdEnvSuggestion[]): CmdEnvSuggestion[] { - const cmdReg = /\\([a-zA-Z@_]+(?::[a-zA-Z]*)?\*?)({[^{}]*})?({[^{}]*})?({[^{}]*})?/g - const cmds: CmdEnvSuggestion[] = [] - const commandSignatureDuplicationDetector = new CommandSignatureDuplicationDetector() - let explSyntaxOn: boolean = false - while (true) { - const result = cmdReg.exec(content) - if (result === null) { - break - } - if (result[1] === 'ExplSyntaxOn') { - explSyntaxOn = true - continue - } else if (result[1] === 'ExplSyntaxOff') { - explSyntaxOn = false - continue - } - - - if (!explSyntaxOn) { - const len = result[1].search(/[_:]/) - if (len > -1) { - result[1] = result[1].slice(0, len) - } - } - const args = CommandFinder.getArgsFromRegResult(result) - const cmd = new CmdEnvSuggestion( - `\\${result[1]}${args}`, - CommandFinder.whichPackageProvidesCommand(result[1], args, cmdInPkg), - [], - -1, - { name: result[1], args }, - vscode.CompletionItemKind.Function - ) - cmd.documentation = '`' + result[1] + '`' - cmd.insertText = new vscode.SnippetString(result[1] + CommandFinder.getTabStopsFromRegResult(result)) - cmd.filterText = result[1] - if (isTriggerSuggestNeeded(result[1])) { - cmd.command = { title: 'Post-Action', command: 'editor.action.triggerSuggest' } - } - if (!commandSignatureDuplicationDetector.has(cmd)) { - cmds.push(cmd) - commandSignatureDuplicationDetector.add(cmd) - } - } - - const newCommandReg = /\\(?:(?:(?:re|provide)?(?:new)?command)|(?:DeclarePairedDelimiter(?:X|XPP)?)|DeclareMathOperator)\*?{?\\(\w+)}?(?:\[([1-9])\])?/g - while (true) { - const result = newCommandReg.exec(content) - if (result === null) { - break - } - - let tabStops = '' - let args = '' - if (result[2]) { - const numArgs = parseInt(result[2]) - for (let i = 1; i <= numArgs; ++i) { - tabStops += '{${' + i + '}}' - args += '{}' - } - } - - const cmd = new CmdEnvSuggestion(`\\${result[1]}${args}`, 'user-defined', [], -1, {name: result[1], args}, vscode.CompletionItemKind.Function) - cmd.documentation = '`' + result[1] + '`' - cmd.insertText = new vscode.SnippetString(result[1] + tabStops) - cmd.filterText = result[1] - if (!commandSignatureDuplicationDetector.has(cmd)) { - cmds.push(cmd) - commandSignatureDuplicationDetector.add(cmd) - } - - CommandFinder.definedCmds.set(result[1], { - file, - location: new vscode.Location( - vscode.Uri.file(file), - new vscode.Position(content.substring(0, result.index).split('\n').length - 1, 0)) - }) - } - - return cmds - } - - private static getTabStopsFromRegResult(result: RegExpExecArray): string { - let text = '' - - if (result[2]) { - text += '{${1}}' - } - if (result[3]) { - text += '{${2}}' - } - if (result[4]) { - text += '{${3}}' - } - return text - } - - private static getArgsFromRegResult(result: RegExpExecArray): string { - return '{}'.repeat(result.length - 1) - } - - /** - * Return the name of the package providing cmdName among all the packages - * included in the rootFile. If no package matches, return '' - * - * @param cmdName the name of a command (without the leading '\\') - */ - private static whichPackageProvidesCommand(cmdName: string, args: string, cmdInPkg: CmdEnvSuggestion[]): string { - return cmdInPkg.find(cmd => cmd.signatureAsString() === cmdName + args)?.package ?? '' - } - -} - - -export class CommandSignatureDuplicationDetector { - private readonly cmdSignatureList: Set = new Set() - - constructor(suggestions: CmdEnvSuggestion[] = []) { - this.cmdSignatureList = new Set(suggestions.map(s => s.signatureAsString())) - } - - add(cmd: CmdEnvSuggestion) { - this.cmdSignatureList.add(cmd.signatureAsString()) - } - - has(cmd: CmdEnvSuggestion): boolean { - return this.cmdSignatureList.has(cmd.signatureAsString()) - } -} diff --git a/src/providers/completer/environment.ts b/src/providers/completer/environment.ts index 1a6ec3b5d..145745be7 100644 --- a/src/providers/completer/environment.ts +++ b/src/providers/completer/environment.ts @@ -1,13 +1,13 @@ import * as vscode from 'vscode' import * as fs from 'fs' -import { latexParser } from 'latex-utensils' +import type * as Ast from '@unified-latex/unified-latex-types' import * as lw from '../../lw' import type { ICompletionItem, IProviderArgs } from '../completion' import type { IProvider } from '../completion' -import { CommandSignatureDuplicationDetector } from './commandlib/commandfinder' import { CmdEnvSuggestion, splitSignatureString, filterNonLetterSuggestions, filterArgumentHint } from './completerutils' import { getLogger } from '../../components/logger' +import { Cache } from '../../components/cacher' const logger = getLogger('Intelli', 'Environment') @@ -166,7 +166,8 @@ export class Environment implements IProvider { * Environments can be inserted using `\envname`. * This function is called by Command.provide to compute these commands for every package in use. */ - provideEnvsAsCommandInPkg(packageName: string, options: string[], suggestions: vscode.CompletionItem[], cmdDuplicationDetector: CommandSignatureDuplicationDetector) { + provideEnvsAsCommandInPkg(packageName: string, options: string[], suggestions: vscode.CompletionItem[], defined?: Set) { + defined = defined ?? new Set() const configuration = vscode.workspace.getConfiguration('latex-workshop') const useOptionalArgsEntries = configuration.get('intellisense.optionalArgsEntries.enabled') @@ -182,64 +183,64 @@ export class Environment implements IProvider { } // Insert env snippets - entry.forEach(env => { + for (const env of entry) { if (!useOptionalArgsEntries && env.hasOptionalArgs()) { return } - if (!cmdDuplicationDetector.has(env)) { + if (!defined.has(env.signatureAsString())) { if (env.option && options && !options.includes(env.option)) { return } suggestions.push(env) - cmdDuplicationDetector.add(env) + defined.add(env.signatureAsString()) } - }) + } } - /** - * Updates the Manager cache for environments used 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 content The content of a LaTeX file. - */ - update(file: string, nodes?: latexParser.Node[], lines?: string[], content?: string) { - // First, we must update the package list - lw.completer.package.updateUsepackage(file, nodes, content) - - const cache = lw.cacher.get(file) - if (cache === undefined) { - return - } - if (nodes !== undefined && lines !== undefined) { - cache.elements.environment = this.getEnvFromNodeArray(nodes, lines) - } else if (content !== undefined) { - cache.elements.environment = this.getEnvFromContent(content) + parse(cache: Cache) { + if (cache.ast !== undefined) { + cache.elements.environment = this.parseAst(cache.ast) + } else { + cache.elements.environment = this.parseContent(cache.contentTrimmed) } } - // This function will return all environments in a node array, including sub-nodes - private getEnvFromNodeArray(nodes: latexParser.Node[], lines: string[]): CmdEnvSuggestion[] { + private parseAst(node: Ast.Node): CmdEnvSuggestion[] { let envs: CmdEnvSuggestion[] = [] - for (let index = 0; index < nodes.length; ++index) { - envs = envs.concat(this.getEnvFromNode(nodes[index], lines)) + if (node.type === 'environment' || node.type === 'mathenv') { + const env = new CmdEnvSuggestion(`${node.env}`, '', [], -1, { name: node.env, args: '' }, vscode.CompletionItemKind.Module) + env.documentation = '`' + node.env + '`' + env.filterText = node.env + envs.push(env) + } + + if ('content' in node && typeof node.content !== 'string') { + for (const subNode of node.content) { + envs = [...envs, ...this.parseAst(subNode)] + } } + return envs } - private getEnvFromNode(node: latexParser.Node, lines: string[]): CmdEnvSuggestion[] { - let envs: CmdEnvSuggestion[] = [] - // Here we only check `isEnvironment` which excludes `align*` and `verbatim`. - // Nonetheless, they have already been included in `defaultEnvs`. - if (latexParser.isEnvironment(node)) { - const env = new CmdEnvSuggestion(`${node.name}`, '', [], -1, { name: node.name, args: '' }, vscode.CompletionItemKind.Module) - env.documentation = '`' + node.name + '`' - env.filterText = node.name + private parseContent(content: string): CmdEnvSuggestion[] { + const envReg = /\\begin\s?{([^{}]*)}/g + const envs: CmdEnvSuggestion[] = [] + const envList: string[] = [] + while (true) { + const result = envReg.exec(content) + if (result === null) { + break + } + if (envList.includes(result[1])) { + continue + } + const env = new CmdEnvSuggestion(`${result[1]}`, '', [], -1, { name: result[1], args: '' }, vscode.CompletionItemKind.Module) + env.documentation = '`' + result[1] + '`' + env.filterText = result[1] + envs.push(env) - } - if (latexParser.hasContentArray(node)) { - envs = envs.concat(this.getEnvFromNodeArray(node.content, lines)) + envList.push(result[1]) } return envs } @@ -267,28 +268,6 @@ export class Environment implements IProvider { return newEntry } - private getEnvFromContent(content: string): CmdEnvSuggestion[] { - const envReg = /\\begin\s?{([^{}]*)}/g - const envs: CmdEnvSuggestion[] = [] - const envList: string[] = [] - while (true) { - const result = envReg.exec(content) - if (result === null) { - break - } - if (envList.includes(result[1])) { - continue - } - const env = new CmdEnvSuggestion(`${result[1]}`, '', [], -1, { name: result[1], args: '' }, vscode.CompletionItemKind.Module) - env.documentation = '`' + result[1] + '`' - env.filterText = result[1] - - envs.push(env) - envList.push(result[1]) - } - return envs - } - setPackageEnvs(packageName: string, envs: {[key: string]: EnvType}) { const environments: EnvType[] = [] Object.entries(envs).forEach(([key, env]) => { diff --git a/src/providers/completer/glossary.ts b/src/providers/completer/glossary.ts index c522053be..11c599bfe 100644 --- a/src/providers/completer/glossary.ts +++ b/src/providers/completer/glossary.ts @@ -1,8 +1,11 @@ import * as vscode from 'vscode' -import {latexParser} from 'latex-utensils' +import type * as Ast from '@unified-latex/unified-latex-types' import * as lw from '../../lw' import type { ICompletionItem } from '../completion' import type { IProvider } from '../completion' +import { Cache } from '../../components/cacher' +import { argContentToStr } from '../../utils/parser' +import { getLongestBalancedString } from '../../utils/utils' enum GlossaryType { glossary, @@ -16,7 +19,7 @@ interface GlossaryEntry { export interface GlossarySuggestion extends ICompletionItem { type: GlossaryType, - file: string, + filePath: string, position: vscode.Position } @@ -44,129 +47,9 @@ export class Glossary implements IProvider { return items } - private getGlossaryFromNodeArray(nodes: latexParser.Node[], file: string): GlossarySuggestion[] { - const glossaries: GlossarySuggestion[] = [] - let entry: GlossaryEntry - let type: GlossaryType | undefined - - nodes.forEach(node => { - if (latexParser.isCommand(node) && node.args.length > 0) { - if (['newglossaryentry', 'provideglossaryentry'].includes(node.name)) { - type = GlossaryType.glossary - entry = this.getShortNodeDescription(node) - } else if(['longnewglossaryentry', 'longprovideglossaryentry'].includes(node.name)) { - type = GlossaryType.glossary - entry = this.getLongNodeLabelDescription(node) - } else if(['newacronym', 'newabbreviation', 'newabbr'].includes(node.name)) { - type = GlossaryType.acronym - entry = this.getLongNodeLabelDescription(node) - } - if (type !== undefined && entry.description !== undefined && entry.label !== undefined) { - glossaries.push({ - type, - file, - position: new vscode.Position(node.location.start.line - 1, node.location.start.column - 1), - label: entry.label, - detail: entry.description, - kind: vscode.CompletionItemKind.Reference - }) - } - } - }) - return glossaries - } - - /** - * Parse the description from "long nodes" such as \newacronym and \longnewglossaryentry - * - * Spec: \newacronym[〈key-val list〉]{〈label〉}{〈abbrv〉}{〈description〉} - * - * Fairly straightforward, a \newacronym command takes the form - * \newacronym[optional parameters]{lw}{LW}{LaTeX Workshop} - * - * - * @param node the \newacronym node from the parser - * @return the pair (label, description) - */ - private getLongNodeLabelDescription(node: latexParser.Command): GlossaryEntry { - let description: string | undefined = undefined - let label: string | undefined = undefined - - // We expect 3 arguments + 1 optional argument - if (node.args.length < 3 || node.args.length > 4) { - return {label: undefined, description: undefined} - } - const hasOptionalArg: boolean = latexParser.isOptionalArg(node.args[0]) - - // First arg is optional, we must have 4 arguments - if (hasOptionalArg && node.args.length !== 4) { - return {label: undefined, description: undefined} - } - - const labelNode = hasOptionalArg ? node.args[1] : node.args[0] - const descriptionNode = hasOptionalArg ? node.args[3] : node.args[2] - if (latexParser.isGroup(descriptionNode)) { - description = latexParser.stringify(descriptionNode).slice(1, -1) - } - - if (latexParser.isGroup(labelNode)) { - label = latexParser.stringify(labelNode).slice(1, -1) - } - - return {label, description} - } - - /** - * Parse the description from "short nodes" like \newglossaryentry - * - * Spec: \newglossaryentry{〈label〉}{〈key=value list〉} - * - * Example glossary entries: - * \newglossaryentry{lw}{name={LaTeX Workshop}, description={What this extension is}} - * \newglossaryentry{vscode}{name=VSCode, description=Editor} - * - * Note: descriptions can be single words or a {group of words} - * - * @param node the \newglossaryentry node from the parser - * @returns the value of the description field - */ - private getShortNodeDescription(node: latexParser.Command): GlossaryEntry { - let result: RegExpExecArray | null - let description: string | undefined = undefined - let label: string | undefined = undefined - let lastNodeWasDescription = false - - // We expect 2 arguments - if (node.args.length !== 2) { - return {label: undefined, description: undefined} - } - - // Get label - if (latexParser.isGroup(node.args[0])) { - label = latexParser.stringify(node.args[0]).slice(1, -1) - } - - // Get description - if (latexParser.isGroup(node.args[1])) { - for (const subNode of node.args[1].content) { - if (latexParser.isTextString(subNode)) { - // Description is of the form description=single_word - if ((result = /description=(.*)/.exec(subNode.content)) !== null) { - if (result[1] !== '') { - description = result[1] - break - } - lastNodeWasDescription = true - } - } else if (lastNodeWasDescription && latexParser.isGroup(subNode)) { - // otherwise we have description={group of words} - description = latexParser.stringify(subNode).slice(1, -1) - break - } - } - } - - return {label, description} + getEntry(token: string): GlossarySuggestion | undefined { + this.updateAll() + return this.glossaries.get(token) || this.acronyms.get(token) } private updateAll() { @@ -201,32 +84,69 @@ export class Glossary implements IProvider { }) } - /** - * Update 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 content The content of a LaTeX file. - */ - update(file: string, nodes?: latexParser.Node[], content?: string) { - const cache = lw.cacher.get(file) - if (cache === undefined) { - return - } - if (nodes !== undefined) { - cache.elements.glossary = this.getGlossaryFromNodeArray(nodes, file) - } else if (content !== undefined) { - cache.elements.glossary = this.getGlossaryFromContent(content, file) + parse(cache: Cache) { + if (cache.ast !== undefined) { + cache.elements.glossary = this.parseAst(cache.ast, cache.filePath) + } else { + cache.elements.glossary = this.parseContent(cache.content, cache.filePath) } } - getEntry(token: string): GlossarySuggestion | undefined { - this.updateAll() - return this.glossaries.get(token) || this.acronyms.get(token) + private parseAst(node: Ast.Node, filePath: string): GlossarySuggestion[] { + let glos: GlossarySuggestion[] = [] + let entry: GlossaryEntry = { label: '', description: '' } + let type: GlossaryType | undefined + + if (node.type === 'macro' && ['newglossaryentry', 'provideglossaryentry'].includes(node.content)) { + type = GlossaryType.glossary + let description = argContentToStr(node.args?.[1]?.content || [], true) + const index = description.indexOf('description=') + if (index >= 0) { + description = description.slice(index + 12) + if (description.charAt(0) === '{') { + description = getLongestBalancedString(description) ?? '' + } else { + description = description.split(',')[0] ?? '' + } + } else { + description = '' + } + entry = { + label: argContentToStr(node.args?.[0]?.content || []), + description + } + } else if (node.type === 'macro' && ['longnewglossaryentry', 'longprovideglossaryentry', 'newacronym', 'newabbreviation', 'newabbr'].includes(node.content)) { + if (['longnewglossaryentry', 'longprovideglossaryentry'].includes(node.content)) { + type = GlossaryType.glossary + } else { + type = GlossaryType.acronym + } + entry = { + label: argContentToStr(node.args?.[1]?.content || []), + description: argContentToStr(node.args?.[3]?.content || []), + } + } + if (type !== undefined && entry.label && entry.description && node.position !== undefined) { + glos.push({ + type, + filePath, + position: new vscode.Position(node.position.start.line - 1, node.position.start.column - 1), + label: entry.label, + detail: entry.description, + kind: vscode.CompletionItemKind.Reference + }) + } + + if ('content' in node && typeof node.content !== 'string') { + for (const subNode of node.content) { + glos = [...glos, ...this.parseAst(subNode, filePath)] + } + } + + return glos } - getGlossaryFromContent(content: string, file: string): GlossarySuggestion[] { + private parseContent(content: string, filePath: string): GlossarySuggestion[] { const glossaries: GlossarySuggestion[] = [] const glossaryList: string[] = [] @@ -267,7 +187,7 @@ export class Glossary implements IProvider { } glossaries.push({ type: regexes[key].type, - file, + filePath, position: new vscode.Position(positionContent.length - 1, positionContent[positionContent.length - 1].length), label: result[1], detail: regexes[key].getDescription(result), diff --git a/src/providers/completer/package.ts b/src/providers/completer/package.ts index f850a380b..7d68ce33e 100644 --- a/src/providers/completer/package.ts +++ b/src/providers/completer/package.ts @@ -1,8 +1,10 @@ import * as vscode from 'vscode' import * as fs from 'fs' -import { latexParser } from 'latex-utensils' +import type * as Ast from '@unified-latex/unified-latex-types' import * as lw from '../../lw' import type { IProvider } from '../completion' +import { argContentToStr } from '../../utils/parser' +import { Cache } from '../../components/cacher' type DataPackagesJsonType = typeof import('../../../data/packagenames.json') @@ -103,73 +105,64 @@ export class Package implements IProvider { return packages } - /** - * Updates the cache for packages used 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 content The content of a LaTeX file. - */ - updateUsepackage(file: string, nodes?: latexParser.Node[], content?: string) { - if (nodes !== undefined) { - this.updateUsepackageNodes(file, nodes) - } else if (content !== undefined) { - const pkgReg = /\\usepackage(\[[^[\]{}]*\])?{(.*?)}/gs - - while (true) { - const result = pkgReg.exec(content) - if (result === null) { - break - } - const packages = result[2].split(',').map(packageName => packageName.trim()) - const options = (result[1] || '[]').slice(1,-1).replace(/\s*=\s*/g,'=').split(',').map(option => option.trim()) - const optionsNoTrue = options.filter(option => option.includes('=true')).map(option => option.replace('=true', '')) - packages.forEach(packageName => this.pushUsepackage(file, packageName, [...options, ...optionsNoTrue])) + parse(cache: Cache) { + if (cache.ast !== undefined) { + cache.elements.package = this.parseAst(cache.ast) + } else { + cache.elements.package = this.parseContent(cache.content) + } + } + + private parseAst(node: Ast.Node): {[pkgName: string]: string[]} { + const packages = {} + if (node.type === 'macro' && ['usepackage', 'documentclass'].includes(node.content)) { + const options: string[] = argContentToStr(node.args?.[0]?.content || []) + .split(',') + .map(arg => arg.trim()) + const optionsNoTrue = options + .filter(option => option.includes('=true')) + .map(option => option.replace('=true', '')) + + argContentToStr(node.args?.[1]?.content || []) + .split(',') + .map(packageName => this.toPackageObj(packageName.trim(), [...options, ...optionsNoTrue], node)) + .forEach(packageObj => Object.assign(packages, packageObj)) + } else if ('content' in node && typeof node.content !== 'string') { + for (const subNode of node.content) { + Object.assign(packages, this.parseAst(subNode)) } } + return packages } - private updateUsepackageNodes(file: string, nodes: latexParser.Node[]) { - nodes.forEach(node => { - if ( latexParser.isCommand(node) && (node.name === 'usepackage' || node.name === 'documentclass') ) { - let options: string[] = [] - node.args.forEach(arg => { - if (latexParser.isOptionalArg(arg)) { - options = arg.content.filter(latexParser.isTextString).filter(str => str.content !== ',').map(str => str.content) - const optionsNoTrue = options.filter(option => option.includes('=true')).map(option => option.replace('=true', '')) - options = [...options, ...optionsNoTrue] - return - } - for (const c of arg.content) { - if (!latexParser.isTextString(c)) { - continue - } - c.content.split(',').forEach(packageName => this.pushUsepackage(file, packageName, options, node)) - } - }) - } else { - if (latexParser.hasContentArray(node)) { - this.updateUsepackageNodes(file, node.content) - } + private parseContent(content: string): {[pkgName: string]: string[]} { + const packages = {} + const pkgReg = /\\usepackage(\[[^[\]{}]*\])?{(.*?)}/gs + while (true) { + const result = pkgReg.exec(content) + if (result === null) { + break } - }) + const packageNames = result[2].split(',').map(packageName => packageName.trim()) + const options = (result[1] || '[]').slice(1,-1).replace(/\s*=\s*/g,'=').split(',').map(option => option.trim()) + const optionsNoTrue = options.filter(option => option.includes('=true')).map(option => option.replace('=true', '')) + packageNames + .map(packageName => this.toPackageObj(packageName, [...options, ...optionsNoTrue])) + .forEach(packageObj => Object.assign(packages, packageObj)) + } + return packages } - private pushUsepackage(fileName: string, packageName: string, options: string[], node?: latexParser.Command) { + private toPackageObj(packageName: string, options: string[], node?: Ast.Node): {[pkgName: string]: string[]} { packageName = packageName.trim() if (packageName === '') { - return + return {} } - if (node?.name === 'documentclass') { + if (node?.type === 'macro' && node.content === 'documentclass') { packageName = 'class-' + packageName } - const cache = lw.cacher.get(fileName) - if (cache === undefined) { - return - } - cache.elements.package = cache.elements.package || {} - cache.elements.package[packageName] = options + const pkgObj: {[pkgName: string]: string[]} = {} + pkgObj[packageName] = options + return pkgObj } } diff --git a/src/providers/completer/reference.ts b/src/providers/completer/reference.ts index 356459557..9e36927c8 100644 --- a/src/providers/completer/reference.ts +++ b/src/providers/completer/reference.ts @@ -1,11 +1,13 @@ 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 { getLongestBalancedString, stripEnvironments } from '../../utils/utils' +import { computeFilteringRange } from './completerutils' import type { IProvider, ICompletionItem, IProviderArgs } from '../completion' +import { argContentToStr } from '../../utils/parser' +import { Cache } from '../../components/cacher' export interface ReferenceEntry extends ICompletionItem { /** The file that defines the ref. */ @@ -29,7 +31,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 +65,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 +151,71 @@ 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)) - } + parse(cache: Cache) { + if (cache.ast !== undefined) { + cache.elements.reference = this.parseAst(cache.ast, cache.content.split('\n')) + } else { + cache.elements.reference = this.parseContent(cache.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 || []) + const index = label.indexOf('label=') + if (index >= 0) { + label = label.slice(index + 6) + if (label.charAt(0) === '{') { + label = getLongestBalancedString(label) ?? '' + } else { + label = label.split(',')[0] ?? '' + } + } else { + label = '' + } } - 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) { @@ -308,5 +271,4 @@ export class Reference implements IProvider { } } } - } diff --git a/src/providers/completion.ts b/src/providers/completion.ts index d5c78b67b..45ea1fae0 100644 --- a/src/providers/completion.ts +++ b/src/providers/completion.ts @@ -157,7 +157,7 @@ export class Completer implements vscode.CompletionItemProvider { }) } - provide(args: IProviderArgs): vscode.CompletionItem[] | undefined { + provide(args: IProviderArgs): vscode.CompletionItem[] { // Note that the order of the following array affects the result. // 'command' must be at the last because it matches any commands. for (const type of ['citation', 'reference', 'environment', 'package', 'documentclass', 'input', 'subimport', 'import', 'includeonly', 'glossary', 'argument', 'command']) { @@ -167,13 +167,13 @@ export class Completer implements vscode.CompletionItemProvider { const configuration = vscode.workspace.getConfiguration('latex-workshop') if (configuration.get('intellisense.citation.type') as string === 'browser') { setTimeout(() => this.citation.browser(args), 10) - return + return [] } } return suggestions } } - return + return [] } async resolveCompletionItem(item: vscode.CompletionItem, token: vscode.CancellationToken): Promise { diff --git a/src/providers/definition.ts b/src/providers/definition.ts index ac6a90d88..ccbfc12b3 100644 --- a/src/providers/definition.ts +++ b/src/providers/definition.ts @@ -66,7 +66,7 @@ export class DefinitionProvider implements vscode.DefinitionProvider { } const glossary = lw.completer.glossary.getEntry(token) if (glossary) { - return new vscode.Location(vscode.Uri.file(glossary.file), glossary.position) + return new vscode.Location(vscode.Uri.file(glossary.filePath), glossary.position) } if (vscode.window.activeTextEditor && token.includes('.')) { // We skip graphics files diff --git a/src/providers/hover.ts b/src/providers/hover.ts index 4a825967d..7a04bd65d 100644 --- a/src/providers/hover.ts +++ b/src/providers/hover.ts @@ -1,6 +1,7 @@ import * as vscode from 'vscode' import * as lw from '../lw' import { tokenizer, onAPackage } from './tokenizer' +import { findProjectNewCommand } from './preview/mathpreviewlib/newcommandfinder' export class HoverProvider implements vscode.HoverProvider { public async provideHover(document: vscode.TextDocument, position: vscode.Position, ctoken: vscode.CancellationToken): Promise { @@ -13,7 +14,7 @@ export class HoverProvider implements vscode.HoverProvider { if (hov) { const tex = lw.mathPreview.findHoverOnTex(document, position) if (tex) { - const newCommands = await lw.mathPreview.findProjectNewCommand(ctoken) + const newCommands = await findProjectNewCommand(ctoken) const hover = await lw.mathPreview.provideHoverOnTex(document, tex, newCommands) return hover } diff --git a/src/providers/preview/mathpreview.ts b/src/providers/preview/mathpreview.ts index f65bee05a..d2c20e353 100644 --- a/src/providers/preview/mathpreview.ts +++ b/src/providers/preview/mathpreview.ts @@ -5,7 +5,7 @@ import type { ReferenceEntry } from '../completer/reference' import { getCurrentThemeLightness } from '../../utils/theme' import { CursorRenderer} from './mathpreviewlib/cursorrenderer' import { type ITextDocumentLike, TextDocumentLike } from './mathpreviewlib/textdocumentlike' -import { NewCommandFinder } from './mathpreviewlib/newcommandfinder' +import { findProjectNewCommand } from './mathpreviewlib/newcommandfinder' import { TexMathEnv, TeXMathEnvFinder } from './mathpreviewlib/texmathenvfinder' import { HoverPreviewOnRefProvider } from './mathpreviewlib/hoverpreviewonref' import { MathPreviewUtils } from './mathpreviewlib/mathpreviewutils' @@ -23,10 +23,6 @@ export class MathPreview { MathJaxPool.initialize() } - findProjectNewCommand(ctoken: vscode.CancellationToken): Promise { - return NewCommandFinder.findProjectNewCommand(ctoken) - } - async provideHoverOnTex(document: vscode.TextDocument, tex: TexMathEnv, newCommand: string): Promise { const configuration = vscode.workspace.getConfiguration('latex-workshop') const scale = configuration.get('hover.preview.scale') as number @@ -59,7 +55,7 @@ export class MathPreview { if (configuration.get('hover.ref.enabled') as boolean) { const tex = TeXMathEnvFinder.findHoverOnRef(document, position, refData, token) if (tex) { - const newCommands = await this.findProjectNewCommand(ctoken) + const newCommands = await findProjectNewCommand(ctoken) return HoverPreviewOnRefProvider.provideHoverPreviewOnRef(tex, newCommands, refData, this.color) } } @@ -82,7 +78,7 @@ export class MathPreview { } async generateSVG(tex: TexMathEnv, newCommandsArg?: string) { - const newCommands: string = newCommandsArg ?? await NewCommandFinder.findProjectNewCommand() + const newCommands: string = newCommandsArg ?? await findProjectNewCommand() const configuration = vscode.workspace.getConfiguration('latex-workshop') const scale = configuration.get('hover.preview.scale') as number const s = MathPreviewUtils.mathjaxify(tex.texString, tex.envname) @@ -117,7 +113,7 @@ export class MathPreview { } async renderSvgOnRef(tex: TexMathEnv, refData: Pick, ctoken: vscode.CancellationToken) { - const newCommand = await this.findProjectNewCommand(ctoken) + const newCommand = await findProjectNewCommand(ctoken) return HoverPreviewOnRefProvider.renderSvgOnRef(tex, newCommand, refData, this.color) } diff --git a/src/providers/preview/mathpreviewlib/newcommandfinder.ts b/src/providers/preview/mathpreviewlib/newcommandfinder.ts index 9b0e5fa39..d049585bb 100644 --- a/src/providers/preview/mathpreviewlib/newcommandfinder.ts +++ b/src/providers/preview/mathpreviewlib/newcommandfinder.ts @@ -1,120 +1,87 @@ import * as vscode from 'vscode' -import {latexParser} from 'latex-utensils' -import {stripCommentsAndVerbatim, isNewCommand, NewCommand} from '../../../utils/utils' import * as path from 'path' import * as lw from '../../../lw' +import { stripCommentsAndVerbatim } from '../../../utils/utils' import { getLogger } from '../../../components/logger' -import { parser } from '../../../components/parser' const logger = getLogger('Preview', 'Math') -export class NewCommandFinder { - private static postProcessNewCommands(commands: string): string { - return commands.replace(/\\providecommand/g, '\\newcommand') - .replace(/\\newcommand\*/g, '\\newcommand') - .replace(/\\renewcommand\*/g, '\\renewcommand') - .replace(/\\DeclarePairedDelimiter{(\\[a-zA-Z]+)}{([^{}]*)}{([^{}]*)}/g, '\\newcommand{$1}[2][]{#1$2 #2 #1$3}') +export async function findProjectNewCommand(ctoken?: vscode.CancellationToken): Promise { + const configuration = vscode.workspace.getConfiguration('latex-workshop') + const newCommandFile = configuration.get('hover.preview.newcommand.newcommandFile') as string + let commandsInConfigFile = '' + if (newCommandFile !== '') { + commandsInConfigFile = await loadNewCommandFromConfigFile(newCommandFile) } - private static async loadNewCommandFromConfigFile(newCommandFile: string) { - let commandsString: string | undefined = '' - if (newCommandFile === '') { - return commandsString - } - let newCommandFileAbs: string - if (path.isAbsolute(newCommandFile)) { - newCommandFileAbs = newCommandFile - } else { - if (lw.manager.rootFile === undefined) { - await lw.manager.findRoot() - } - const rootDir = lw.manager.rootDir - if (rootDir === undefined) { - logger.log(`Cannot identify the absolute path of new command file ${newCommandFile} without root file.`) - return '' - } - newCommandFileAbs = path.join(rootDir, newCommandFile) - } - commandsString = lw.lwfs.readFileSyncGracefully(newCommandFileAbs) - if (commandsString === undefined) { - logger.log(`Cannot read file ${newCommandFileAbs}`) + if (!configuration.get('hover.preview.newcommand.parseTeXFile.enabled')) { + return commandsInConfigFile + } + let commands: string[] = [] + for (const tex of lw.cacher.getIncludedTeX()) { + if (ctoken?.isCancellationRequested) { return '' } - commandsString = commandsString.replace(/^\s*$/gm, '') - commandsString = NewCommandFinder.postProcessNewCommands(commandsString) - return commandsString + await lw.cacher.wait(tex) + const content = lw.cacher.get(tex)?.content + if (content === undefined) { + continue + } + commands = commands.concat(findNewCommand(content)) } + return commandsInConfigFile + '\n' + postProcessNewCommands(commands.join('')) +} - static async findProjectNewCommand(ctoken?: vscode.CancellationToken): Promise { - const configuration = vscode.workspace.getConfiguration('latex-workshop') - const newCommandFile = configuration.get('hover.preview.newcommand.newcommandFile') as string - let commandsInConfigFile = '' - if (newCommandFile !== '') { - commandsInConfigFile = await NewCommandFinder.loadNewCommandFromConfigFile(newCommandFile) - } +function postProcessNewCommands(commands: string): string { + return commands.replace(/\\providecommand/g, '\\newcommand') + .replace(/\\newcommand\*/g, '\\newcommand') + .replace(/\\renewcommand\*/g, '\\renewcommand') + .replace(/\\DeclarePairedDelimiter{(\\[a-zA-Z]+)}{([^{}]*)}{([^{}]*)}/g, '\\newcommand{$1}[2][]{#1$2 #2 #1$3}') +} - if (!configuration.get('hover.preview.newcommand.parseTeXFile.enabled')) { - return commandsInConfigFile +async function loadNewCommandFromConfigFile(newCommandFile: string) { + let commandsString: string | undefined = '' + if (newCommandFile === '') { + return commandsString + } + let newCommandFileAbs: string + if (path.isAbsolute(newCommandFile)) { + newCommandFileAbs = newCommandFile + } else { + if (lw.manager.rootFile === undefined) { + await lw.manager.findRoot() } - let commands: string[] = [] - let exceeded = false - setTimeout( () => { exceeded = true }, 5000) - for (const tex of lw.cacher.getIncludedTeX()) { - if (ctoken?.isCancellationRequested) { - return '' - } - if (exceeded) { - logger.log('Timeout error when parsing preambles in findProjectNewCommand.') - throw new Error('Timeout Error in findProjectNewCommand') - } - const content = lw.cacher.get(tex)?.content - if (content === undefined) { - continue - } - commands = commands.concat(await NewCommandFinder.findNewCommand(content)) + const rootDir = lw.manager.rootDir + if (rootDir === undefined) { + logger.log(`Cannot identify the absolute path of new command file ${newCommandFile} without root file.`) + return '' } - return commandsInConfigFile + '\n' + NewCommandFinder.postProcessNewCommands(commands.join('')) + newCommandFileAbs = path.join(rootDir, newCommandFile) + } + commandsString = lw.lwfs.readFileSyncGracefully(newCommandFileAbs) + if (commandsString === undefined) { + logger.log(`Cannot read file ${newCommandFileAbs}`) + return '' } + commandsString = commandsString.replace(/^\s*$/gm, '') + commandsString = postProcessNewCommands(commandsString) + return commandsString +} - static async findNewCommand(content: string): Promise { - let commands: string[] = [] - logger.log('Parse LaTeX preamble AST.') - const ast = await parser.parseLatexPreamble(content) - if (ast !== undefined) { - logger.log(`Parsed ${ast.content.length} AST items.`) - ast.content.forEach(node => { - if (isNewCommand(node) && node.args.length > 0) { - node.name = node.name.replace(/\*$/, '') as NewCommand['name'] - const s = latexParser.stringify(node) - commands.push(s) - } else if (latexParser.isDefCommand(node)) { - const s = latexParser.stringify(node) - commands.push(s) - } else if (latexParser.isCommand(node) && node.name === 'DeclarePairedDelimiter' && node.args.length === 3) { - const name = latexParser.stringify(node.args[0]) - const leftDelim = latexParser.stringify(node.args[1]).slice(1, -1) - const rightDelim = latexParser.stringify(node.args[2]).slice(1, -1) - const s = `\\newcommand${name}[2][]{#1${leftDelim} #2 #1${rightDelim}}` - commands.push(s) - } - }) - } else { - logger.log('Failed parsing preamble AST, fallback to regex.') - commands = [] - const regex = /(\\(?:(?:(?:(?:re)?new|provide)command|DeclareMathOperator)(\*)?{\\[a-zA-Z]+}(?:\[[^[\]{}]*\])*{.*})|\\(?:def\\[a-zA-Z]+(?:#[0-9])*{.*})|\\DeclarePairedDelimiter{\\[a-zA-Z]+}{[^{}]*}{[^{}]*})/gm - const noCommentContent = stripCommentsAndVerbatim(content) - let result: RegExpExecArray | null - do { - result = regex.exec(noCommentContent) - if (result) { - let command = result[1] - if (result[2]) { - command = command.replace('*', '') - } - commands.push(command) - } - } while (result) +function findNewCommand(content: string): string[] { + const commands: string[] = [] + const regex = /(\\(?:(?:(?:(?:re)?new|provide)command|DeclareMathOperator)(\*)?{\\[a-zA-Z]+}(?:\[[^[\]{}]*\])*{.*})|\\(?:def\\[a-zA-Z]+(?:#[0-9])*{.*})|\\DeclarePairedDelimiter{\\[a-zA-Z]+}{[^{}]*}{[^{}]*})/gm + const noCommentContent = stripCommentsAndVerbatim(content) + let result: RegExpExecArray | null + do { + result = regex.exec(noCommentContent) + if (result) { + let command = result[1] + if (result[2]) { + command = command.replace('*', '') + } + commands.push(command) } - return commands - } + } while (result) + return commands } diff --git a/src/providers/selection.ts b/src/providers/selection.ts index d25657d1e..4dcc94d48 100644 --- a/src/providers/selection.ts +++ b/src/providers/selection.ts @@ -1,245 +1,63 @@ import * as vscode from 'vscode' -import { latexParser } from 'latex-utensils' +import type * as Ast from '@unified-latex/unified-latex-types' import * as lw from '../lw' -import { parser } from '../components/parser' import { getLogger } from '../components/logger' const logger = getLogger('Selection') -interface ILuRange { - start: ILuPos, - end: ILuPos -} - -class LuRange implements ILuRange { - readonly start: LuPos - readonly end: LuPos - - constructor(arg: {start: ILuPos, end: ILuPos}) { - this.start = LuPos.from(arg.start) - this.end = LuPos.from(arg.end) - } - - contains(pos: ILuPos): boolean { - return this.start.isBeforeOrEqual(pos) && this.end.isAfterOrEqual(pos) - } -} - -interface ILuPos { - readonly line: number, - readonly column: number -} - -interface IContent { - content: latexParser.Node[], - contentLuRange: LuRange, - startSep: latexParser.Node | undefined, - endSep: latexParser.Node | undefined -} - -class LuPos implements ILuPos { - - static from(loc: ILuPos) { - return new LuPos(loc.line, loc.column) - } - - constructor( - readonly line: number, - readonly column: number - ) {} - - isAfter(other: ILuPos): boolean { - return this.line > other.line || ( this.line === other.line && this.column > other.column ) - } - - isAfterOrEqual(other: ILuPos): boolean { - return this.line > other.line || ( this.line === other.line && this.column >= other.column ) - } - - isBefore(other: ILuPos): boolean { - return this.line < other.line || ( this.line === other.line && this.column < other.column ) - } - - isBeforeOrEqual(other: ILuPos): boolean { - return this.line < other.line || ( this.line === other.line && this.column <= other.column ) +function findNode(position: vscode.Position, node: Ast.Node, stack: Ast.Node[] = [ node ]): Ast.Node[] { + if ('content' in node && typeof node.content !== 'string') { + for (const child of node.content) { + if (child.position === undefined) { + continue + } + if (child.position.start.line > position.line + 1 || + child.position.end.line < position.line + 1) { + continue + } + if (child.position.start.line === position.line + 1 && + child.position.start.column > position.character + 1) { + continue + } + if (child.position.end.line === position.line + 1 && + child.position.end.column < position.character + 1) { + continue + } + stack.push(child) + findNode(position, child, stack) + break + } } + return stack } -function toVscodeRange(loc: ILuRange): vscode.Range { - return new vscode.Range( - loc.start.line - 1, loc.start.column - 1, - loc.end.line - 1, loc.end.column - 1 - ) -} - -function toLatexUtensilPosition(pos: vscode.Position): LuPos { - return new LuPos(pos.line + 1, pos.character + 1) +function nodeStackToSelectionRange(stack: Ast.Node[]): vscode.SelectionRange { + const last = stack[stack.length - 1] + const parent: Ast.Node | undefined = stack[stack.length - 2] + return new vscode.SelectionRange( + new vscode.Range( + (last.position?.start.line || 1) - 1, (last.position?.start.column || 1) - 1, + (last.position?.end.line || 1) - 1, (last.position?.end.column || 1) - 1 + ), parent ? nodeStackToSelectionRange(stack.slice(0, -1)) : undefined) } export class SelectionRangeProvider implements vscode.SelectionRangeProvider { async provideSelectionRanges(document: vscode.TextDocument, positions: vscode.Position[]) { - const content = document.getText() - logger.log(`Parse LaTeX AST : ${document.fileName} .`) - const latexAst = lw.cacher.get(document.fileName)?.luAst || await parser.parseLatex(content, { enableMathCharacterLocation: true }) - if (!latexAst) { - logger.log(`Failed to parse AST for ${document.fileName} .`) + await lw.cacher.wait(document.fileName) + const content = lw.cacher.get(document.fileName)?.content + const ast = lw.cacher.get(document.fileName)?.ast + if (!content || !ast) { + logger.log(`Error loading ${content ? 'AST' : 'content'} during structuring: ${document.fileName} .`) return [] } + const ret: vscode.SelectionRange[] = [] - positions.forEach(pos => { - const lupos = toLatexUtensilPosition(pos) - const result = latexParser.findNodeAt( - latexAst.content, - lupos - ) - const selectionRange = this.resultToSelectionRange(lupos, result) - if (selectionRange) { - ret.push(selectionRange) - } + positions.forEach(position => { + const nodeStack = findNode(position, ast) + const selectionRange = nodeStackToSelectionRange(nodeStack) + ret.push(selectionRange) }) return ret } - - private getInnerContentLuRange(node: latexParser.Node): LuRange | undefined { - if (latexParser.isEnvironment(node) || latexParser.isMathEnv(node) || latexParser.isMathEnvAligned(node)) { - return new LuRange({ - start: { - line: node.location.start.line, - column: node.location.start.column + '\\begin{}'.length + node.name.length - }, - end: { - line: node.location.end.line, - column: node.location.end.column - '\\end{}'.length - node.name.length - } - }) - } else if (latexParser.isGroup(node) || latexParser.isInlienMath(node)) { - return new LuRange({ - start: { - line: node.location.start.line, - column: node.location.start.column + 1 - }, - end: { - line: node.location.end.line, - column: node.location.end.column - 1 - } - }) - } else if (latexParser.isLabelCommand(node)) { - return new LuRange({ - start: { - line: node.location.start.line, - column: node.location.start.column + '\\{'.length + node.name.length - }, - end: { - line: node.location.end.line, - column: node.location.end.column - '}'.length - } - }) - } else if (latexParser.isMathDelimiters(node)) { - return new LuRange({ - start: { - line: node.location.start.line, - column: node.location.start.column + node.left.length + node.lcommand.length - }, - end: { - line: node.location.end.line, - column: node.location.end.column - node.right.length - node.rcommand.length - } - }) - } - return - } - - private findInnerContentIncludingPos( - lupos: LuPos, - content: latexParser.Node[], - sepNodes: latexParser.Node[], - innerContentRange: LuRange | undefined - ): IContent | undefined { - const startSep = Array.from(sepNodes).reverse().find((node) => node.location && lupos.isAfterOrEqual(node.location.end)) - const endSep = sepNodes.find((node) => node.location && lupos.isBeforeOrEqual(node.location.start)) - const startSepPos = startSep?.location ? LuPos.from(startSep.location.end) : innerContentRange?.start - const endSepPos = endSep?.location ? LuPos.from(endSep.location.start) : innerContentRange?.end - if (!startSepPos || !endSepPos) { - return - } - const innerContent = content.filter((node) => { - return node.location && startSepPos.isBeforeOrEqual(node.location.start) && endSepPos.isAfterOrEqual(node.location.end) - }) - return { - content: innerContent, - contentLuRange: new LuRange({ - start: startSepPos, - end: endSepPos - }), - startSep, - endSep - } - } - - private resultToSelectionRange( - lupos: LuPos, - findNodeAtResult: ReturnType - ): vscode.SelectionRange | undefined { - if (!findNodeAtResult) { - return - } - const curNode = findNodeAtResult.node - const parentNode = findNodeAtResult.parent - const parentSelectionRange = parentNode ? this.resultToSelectionRange(lupos, parentNode) : undefined - if (!curNode.location) { - return parentSelectionRange - } - const curRange = toVscodeRange(curNode.location) - let curSelectionRange = new vscode.SelectionRange(curRange, parentSelectionRange) - let innerContentLuRange = this.getInnerContentLuRange(curNode) - if (innerContentLuRange) { - if (!innerContentLuRange.contains(lupos)) { - return curSelectionRange - } - const newCurRange = toVscodeRange(innerContentLuRange) - curSelectionRange = new vscode.SelectionRange(newCurRange, curSelectionRange) - } - if (latexParser.hasContentArray(curNode)) { - let innerContent = curNode.content - let newInnerContent: IContent | undefined - if (latexParser.isEnvironment(curNode) && (curNode.name === 'itemize' || curNode.name === 'enumerate')) { - let itemNodes = curNode.content.filter(latexParser.isCommand) - itemNodes = itemNodes.filter((node) => node.name === 'item') - newInnerContent = this.findInnerContentIncludingPos(lupos, innerContent, itemNodes, innerContentLuRange) - if (newInnerContent) { - innerContent = newInnerContent.content - innerContentLuRange = newInnerContent.contentLuRange - let newContentRange = toVscodeRange(innerContentLuRange) - if (newInnerContent.startSep?.location) { - const start = LuPos.from(newInnerContent.startSep.location.start) - newContentRange = toVscodeRange({start, end:innerContentLuRange.end}) - } - curSelectionRange = new vscode.SelectionRange(newContentRange, curSelectionRange) - } - } - const linebreaksNodes = innerContent.filter(latexParser.isLinebreak) - newInnerContent = this.findInnerContentIncludingPos(lupos, innerContent, linebreaksNodes, innerContentLuRange) - if (newInnerContent) { - innerContent = newInnerContent.content - innerContentLuRange = newInnerContent.contentLuRange - let newContentRange = toVscodeRange(innerContentLuRange) - if (newInnerContent.endSep?.location) { - const end = LuPos.from(newInnerContent.endSep.location.end) - newContentRange = toVscodeRange({start: innerContentLuRange.start, end}) - } - curSelectionRange = new vscode.SelectionRange(newContentRange, curSelectionRange) - } - const alignmentTabNodes = innerContent.filter(latexParser.isAlignmentTab) - newInnerContent = this.findInnerContentIncludingPos(lupos, innerContent, alignmentTabNodes, innerContentLuRange) - if (newInnerContent) { - // curContent = newContent.innerContent - innerContentLuRange = newInnerContent.contentLuRange - const newContentRange = toVscodeRange(innerContentLuRange) - curSelectionRange = new vscode.SelectionRange(newContentRange, curSelectionRange) - } - } - return curSelectionRange - } - } diff --git a/src/providers/structure.ts b/src/providers/structure.ts index b271330a5..939f54863 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 44c16040d..859b57202 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') @@ -50,7 +51,7 @@ export async function construct(filePath: string | undefined = undefined, subFil } async function constructFile(filePath: string, config: StructureConfig, structs: FileStructureCache): Promise { - if (structs[filePath]) { + if (structs[filePath] !== undefined) { return } const openEditor: vscode.TextDocument | undefined = vscode.workspace.textDocuments.filter(document => document.fileName === path.normalize(filePath))?.[0] @@ -60,19 +61,7 @@ async function constructFile(filePath: string, config: StructureConfig, structs: content = openEditor.getText() ast = parser.unifiedParse(content) } else { - let waited = 0 - while (!lw.cacher.promise(filePath) && !lw.cacher.has(filePath)) { - // Just open vscode, has not cached, wait for a bit? - await new Promise(resolve => setTimeout(resolve, 100)) - waited++ - if (waited >= 20) { - // Waited for two seconds before starting cache. Really? - logger.log(`Error loading cache during structuring: ${filePath} . Forcing.`) - await lw.cacher.refreshCache(filePath) - break - } - } - await lw.cacher.promise(filePath) + await lw.cacher.wait(filePath) content = lw.cacher.get(filePath)?.content ast = lw.cacher.get(filePath)?.ast } @@ -86,52 +75,11 @@ async function constructFile(filePath: string, config: StructureConfig, structs: // Parse each base-level node. If the node has contents, that function // will be called recursively. const rootElement = { children: [] } - for (const node of ast.content) { - await parseNode(node, rnwSub, rootElement, filePath, config, 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 || '' + for (const node of ast.content) { + await parseNode(node, rnwSub, rootElement, filePath, config, structs) } - 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( @@ -274,15 +222,18 @@ async function parseNode( } } -function insertSubFile(structs: FileStructureCache, struct?: TeXElement[]): TeXElement[] { +function insertSubFile(structs: FileStructureCache, struct?: TeXElement[], traversed?: string[]): TeXElement[] { if (lw.manager.rootFile === undefined) { return [] } struct = struct ?? structs[lw.manager.rootFile] ?? [] + traversed = traversed ?? [lw.manager.rootFile] let elements: TeXElement[] = [] for (const element of struct) { - if (element.type === TeXElementType.SubFile && structs[element.label]) { - elements = [...elements, ...insertSubFile(structs, structs[element.label])] + if (element.type === TeXElementType.SubFile + && structs[element.label] + && !traversed.includes(element.label)) { + elements = [...elements, ...insertSubFile(structs, structs[element.label], [...traversed, element.label])] continue } if (element.children.length > 0) { diff --git a/src/utils/parser.ts b/src/utils/parser.ts new file mode 100644 index 000000000..0d7bbefb0 --- /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[], preserveCurlyBrace: boolean = false): 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 preserveCurlyBrace ? `{${argContentToStr(node.content)}}` : argContentToStr(node.content) + case 'verb': + return node.content + default: + return '' + } + }).join('') +} diff --git a/src/utils/utils.ts b/src/utils/utils.ts index d0308e134..1637afab5 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,7 +1,6 @@ import * as vscode from 'vscode' import * as path from 'path' import * as fs from 'fs' -import {latexParser} from 'latex-utensils' export function sleep(ms: number) { return new Promise(resolve => setTimeout(resolve, ms)) @@ -120,7 +119,7 @@ export function trimMultiLineString(text: string): string { * * @param s A string to be searched. */ -function getLongestBalancedString(s: string, bracket: 'curly' | 'square'='curly'): string | undefined { +export function getLongestBalancedString(s: string, bracket: 'curly' | 'square'='curly'): string | undefined { const bracketStack: ('{' | '[' | '(')[] = [] const opener = bracket === 'curly' ? '{' : '[' @@ -288,33 +287,3 @@ export function replaceArgumentPlaceholders(rootFile: string, tmpDir: string): ( return expandPlaceHolders(arg).replace(/%OUTDIR%/g, outDir).replace(/%OUTDIR_W32%/g, outDirW32) } } - -export type NewCommand = { - kind: 'command', - name: 'renewcommand|newcommand|providecommand|DeclareMathOperator|renewcommand*|newcommand*|providecommand*|DeclareMathOperator*', - args: (latexParser.OptionalArg | latexParser.Group)[], - location: latexParser.Location -} - -export function isNewCommand(node: latexParser.Node | undefined): node is NewCommand { - const regex = /^(renewcommand|newcommand|providecommand|DeclareMathOperator)(\*)?$/ - if (latexParser.isCommand(node) && node.name.match(regex)) { - return true - } - return false -} - -type NewEnvironment = { - kind: 'command', - name: 'renewenvironment|newenvironment|renewenvironment*|newenvironment*', - args: (latexParser.OptionalArg | latexParser.Group)[], - location: latexParser.Location -} - -export function isNewEnvironment(node: latexParser.Node | undefined): node is NewEnvironment { - const regex = /^(renewenvironment|newenvironment)(\*)?$/ - if (latexParser.isCommand(node) && node.name.match(regex)) { - return true - } - return false -} 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 @@ \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/fixtures/armory/intellisense/newcommand.tex b/test/fixtures/armory/intellisense/newcommand.tex index 150b875a9..0b22350ea 100644 --- a/test/fixtures/armory/intellisense/newcommand.tex +++ b/test/fixtures/armory/intellisense/newcommand.tex @@ -1,10 +1,8 @@ \documentclass[10pt]{article} \newcommand\WARNING{\textcolor{red}{WARNING}} \newcommand\FIXME[1]{\textcolor{red}{FIX:}\textcolor{red}{#1}} -\newcommand\FIXME[2][]{\textcolor{red}{FIX:}\textcolor{red}{#1}} +\newcommand\FIXMETOO[2][]{\textcolor{red}{FIX:}\textcolor{red}{#1}} \newcommand{\fix}[3][]{\chdeleted{#2}\chadded[comment={#1}]{#3}} \begin{document} -\fakecommand -\fakecommand{arg} -\fakecommand[opt]{arg} +\WARNING \end{document} diff --git a/test/suites/04_intellisense.test.ts b/test/suites/04_intellisense.test.ts index 6e3069162..22343a9da 100644 --- a/test/suites/04_intellisense.test.ts +++ b/test/suites/04_intellisense.test.ts @@ -8,7 +8,7 @@ import * as test from './utils' import { EnvSnippetType, EnvType } from '../../src/providers/completer/environment' import { CmdType } from '../../src/providers/completer/command' import { PkgType } from '../../src/providers/completion' -import { isTriggerSuggestNeeded } from '../../src/providers/completer/commandlib/commandfinder' +import { isTriggerSuggestNeeded } from '../../src/providers/completer/command' function assertKeys(keys: string[], expected: string[] = [], message: string): void { assert.ok( @@ -34,7 +34,6 @@ suite('Intellisense test suite', () => { await vscode.workspace.getConfiguration('latex-workshop').update('intellisense.citation.label', undefined) await vscode.workspace.getConfiguration('latex-workshop').update('intellisense.citation.format', undefined) await vscode.workspace.getConfiguration('latex-workshop').update('intellisense.label.command', undefined) - await vscode.workspace.getConfiguration('latex-workshop').update('intellisense.label.keyval', undefined) await vscode.workspace.getConfiguration('latex-workshop').update('intellisense.argumentHint.enabled', undefined) await vscode.workspace.getConfiguration('latex-workshop').update('intellisense.command.user', undefined) await vscode.workspace.getConfiguration('latex-workshop').update('intellisense.package.exclude', undefined) @@ -79,7 +78,7 @@ suite('Intellisense test suite', () => { }) test.run('test default cmds', () => { - const defaultCommands = lw.completer.command.getDefaultCmds().map(e => e.label) + const defaultCommands = lw.completer.command.defaultCmds.map(e => e.label) assert.ok(defaultCommands.includes('\\begin')) assert.ok(defaultCommands.includes('\\left(')) assert.ok(defaultCommands.includes('\\section{}')) @@ -155,11 +154,8 @@ suite('Intellisense test suite', () => { const suggestions = test.suggest(0, 1) assert.ok(suggestions.labels.includes('\\WARNING')) assert.ok(suggestions.labels.includes('\\FIXME{}')) - assert.ok(suggestions.labels.includes('\\FIXME[]{}')) + assert.ok(suggestions.labels.includes('\\FIXMETOO[]{}')) assert.ok(suggestions.labels.includes('\\fix[]{}{}')) - assert.ok(suggestions.labels.includes('\\fakecommand')) - assert.ok(suggestions.labels.includes('\\fakecommand{}')) - assert.ok(suggestions.labels.includes('\\fakecommand[]{}')) }) test.run('command intellisense with config `intellisense.argumentHint.enabled`', async (fixture: string) => { @@ -222,24 +218,15 @@ suite('Intellisense test suite', () => { assert.ok(suggestions.labels.includes('\\overline{}')) }) - test.run('reference intellisense and config intellisense.label.keyval', async (fixture: string) => { - await vscode.workspace.getConfiguration('latex-workshop').update('intellisense.label.keyval', true) + test.run('reference intellisense', async (fixture: string) => { await test.load(fixture, [ {src: 'intellisense/base.tex', dst: 'main.tex'}, {src: 'intellisense/sub.tex', dst: 'sub/s.tex'} ]) - let suggestions = test.suggest(8, 5) + const suggestions = test.suggest(8, 5) assert.ok(suggestions.labels.includes('sec1')) - assert.ok(suggestions.labels.includes('eq1')) - - await vscode.workspace.getConfiguration('latex-workshop').update('intellisense.label.keyval', false) - await test.load(fixture, [ - {src: 'intellisense/base.tex', dst: 'main.tex'}, - {src: 'intellisense/sub.tex', dst: 'sub/s.tex'} - ]) - 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')) }) test.run('reference intellisense and config intellisense.label.command', async (fixture: string) => {