From 0c7aeead43c38f219dcaae259f84c339347a5cce Mon Sep 17 00:00:00 2001 From: James Yu Date: Wed, 13 Dec 2023 12:02:59 +0000 Subject: [PATCH] Modularize env and cmd --- src/completion/completer/argument.ts | 221 +++--- src/completion/completer/atsuggestion.ts | 6 +- src/completion/completer/citation.ts | 519 ++++++------ .../completer/commandlib/surround.ts | 4 +- src/completion/completer/completerutils.ts | 6 +- src/completion/completer/documentclass.ts | 6 +- src/completion/completer/environment.ts | 535 +++++++------ src/completion/completer/glossary.ts | 9 +- src/completion/completer/input.ts | 7 +- src/completion/completer/macro.ts | 736 +++++++++--------- src/completion/completer/package.ts | 7 +- src/completion/completer/reference.ts | 17 +- src/completion/index.ts | 9 + src/completion/latex.ts | 89 +-- src/core/cache.ts | 6 +- src/core/commands.ts | 2 +- src/language/definition.ts | 4 +- src/lw.ts | 2 + src/main.ts | 4 +- src/parse/parser/biberlog.ts | 2 +- src/parse/parser/bibtexlog.ts | 2 +- src/preview/hover.ts | 6 +- src/types.ts | 92 ++- test/suites/04_intellisense.test.ts | 22 +- test/suites/11_snippet.test.ts | 4 +- test/suites/utils.ts | 2 +- 26 files changed, 1154 insertions(+), 1165 deletions(-) create mode 100644 src/completion/index.ts diff --git a/src/completion/completer/argument.ts b/src/completion/completer/argument.ts index b1a3f42388..7d60b6fab5 100644 --- a/src/completion/completer/argument.ts +++ b/src/completion/completer/argument.ts @@ -1,135 +1,134 @@ import * as vscode from 'vscode' import { lw } from '../../lw' -import type { IProvider, IProviderArgs } from '../latex' +import { EnvSnippetType } from '../../types' +import type { CompletionArgs, CompletionProvider } from '../../types' import { CmdEnvSuggestion, filterArgumentHint } from './completerutils' -import { EnvSnippetType } from './environment' -export class Argument implements IProvider { +export const provider: CompletionProvider = { from } - provideFrom(result: RegExpMatchArray, args: IProviderArgs) { - if (result[1] === 'usepackage') { - return this.providePackageOptions(args.line) - } - if (result[1] === 'documentclass') { - return this.provideClassOptions(args.line) - } - const index = this.getArgumentIndex(result[2]) - const packages = lw.completer.package.getPackagesIncluded(args.langId) - let candidate: CmdEnvSuggestion | undefined - let environment: string | undefined - if (result[1] === 'begin') { - environment = result[2].match(/{(.*?)}/)?.[1] - } - for (const packageName of Object.keys(packages)) { - if (environment) { - const environments = lw.completer.environment.getEnvFromPkg(packageName, EnvSnippetType.AsCommand) || [] - for (const env of environments) { - if (environment !== env.signature.name) { - continue - } - if (index !== env.keyvalpos + 1) { // Start from one. - continue - } - candidate = env +function from(result: RegExpMatchArray, args: CompletionArgs) { + if (result[1] === 'usepackage') { + return providePackageOptions(args.line) + } + if (result[1] === 'documentclass') { + return provideClassOptions(args.line) + } + const index = getArgumentIndex(result[2]) + const packages = lw.completer.package.getPackagesIncluded(args.langId) + let candidate: CmdEnvSuggestion | undefined + let environment: string | undefined + if (result[1] === 'begin') { + environment = result[2].match(/{(.*?)}/)?.[1] + } + for (const packageName of Object.keys(packages)) { + if (environment) { + const environments = lw.completion.environment.getEnvFromPkg(packageName, EnvSnippetType.AsCommand) || [] + for (const env of environments) { + if (environment !== env.signature.name) { + continue } - } else { - const commands = lw.completer.command.getPackageCmds(packageName) - for (const command of commands) { - if (result[1] !== command.signature.name) { - continue - } - if (index !== command.keyvalpos) { - continue - } - candidate = command - break + if (index !== env.keyvalpos + 1) { // Start from one. + continue } + candidate = env } - if (candidate !== undefined) { + } else { + const commands = lw.completion.macro.getPackageCmds(packageName) + for (const command of commands) { + if (result[1] !== command.signature.name) { + continue + } + if (index !== command.keyvalpos) { + continue + } + candidate = command break } } - const suggestions = candidate?.keyvals?.map(option => { - const item = new vscode.CompletionItem(option, vscode.CompletionItemKind.Constant) - item.insertText = new vscode.SnippetString(option) - return item - }) || [] - - filterArgumentHint(suggestions) - - return suggestions + if (candidate !== undefined) { + break + } } + const suggestions = candidate?.keyvals?.map(option => { + const item = new vscode.CompletionItem(option, vscode.CompletionItemKind.Constant) + item.insertText = new vscode.SnippetString(option) + return item + }) || [] - private providePackageOptions(line: string): vscode.CompletionItem[] { - const regex = /\\usepackage.*{(.*?)}/ - const match = line.match(regex) - if (!match) { - return [] - } - lw.completer.loadPackageData(match[1]) - const suggestions = lw.completer.package.getPackageOptions(match[1]) - .map(option => { - const item = new vscode.CompletionItem(option, vscode.CompletionItemKind.Constant) - item.insertText = new vscode.SnippetString(option) - return item - }) + filterArgumentHint(suggestions) - filterArgumentHint(suggestions) + return suggestions +} - return suggestions +function providePackageOptions(line: string): vscode.CompletionItem[] { + const regex = /\\usepackage.*{(.*?)}/ + const match = line.match(regex) + if (!match) { + return [] } + lw.completer.loadPackageData(match[1]) + const suggestions = lw.completer.package.getPackageOptions(match[1]) + .map(option => { + const item = new vscode.CompletionItem(option, vscode.CompletionItemKind.Constant) + item.insertText = new vscode.SnippetString(option) + return item + }) - private provideClassOptions(line: string): vscode.CompletionItem[] { - const regex = /\\documentclass.*{(.*?)}/s - const match = line.match(regex) - if (!match) { - return [] - } - const isDefaultClass = ['article', 'report', 'book'].includes(match[1]) - lw.completer.loadPackageData(isDefaultClass ? 'latex-document' : `class-${match[1]}`) - const suggestions = lw.completer.package.getPackageOptions(isDefaultClass ? 'latex-document' : `class-${match[1]}`) - .map(option => { - const item = new vscode.CompletionItem(option, vscode.CompletionItemKind.Constant) - item.insertText = new vscode.SnippetString(option) - return item - }) + filterArgumentHint(suggestions) - filterArgumentHint(suggestions) + return suggestions +} - return suggestions +function provideClassOptions(line: string): vscode.CompletionItem[] { + const regex = /\\documentclass.*{(.*?)}/s + const match = line.match(regex) + if (!match) { + return [] } + const isDefaultClass = ['article', 'report', 'book'].includes(match[1]) + lw.completer.loadPackageData(isDefaultClass ? 'latex-document' : `class-${match[1]}`) + const suggestions = lw.completer.package.getPackageOptions(isDefaultClass ? 'latex-document' : `class-${match[1]}`) + .map(option => { + const item = new vscode.CompletionItem(option, vscode.CompletionItemKind.Constant) + item.insertText = new vscode.SnippetString(option) + return item + }) - private getArgumentIndex(argstr: string) { - let argumentIndex = 0 - let curlyLevel = argstr[0] === '{' ? 1 : 0 - let squareLevel = argstr[0] === '[' ? 1 : 0 - for (let index = 1; index < argstr.length; index++) { - if (argstr[index-1] === '\\') { - continue - } - switch (argstr[index]) { - case '{': - curlyLevel++ - break - case '[': - squareLevel++ - break - case '}': - curlyLevel-- - if (curlyLevel === 0 && squareLevel === 0) { - argumentIndex++ - } - break - case ']': - squareLevel-- - if (curlyLevel === 0 && squareLevel === 0) { - argumentIndex++ - } - break - default: - break - } + filterArgumentHint(suggestions) + + return suggestions +} + +function getArgumentIndex(argstr: string) { + let argumentIndex = 0 + let curlyLevel = argstr[0] === '{' ? 1 : 0 + let squareLevel = argstr[0] === '[' ? 1 : 0 + for (let index = 1; index < argstr.length; index++) { + if (argstr[index-1] === '\\') { + continue + } + switch (argstr[index]) { + case '{': + curlyLevel++ + break + case '[': + squareLevel++ + break + case '}': + curlyLevel-- + if (curlyLevel === 0 && squareLevel === 0) { + argumentIndex++ + } + break + case ']': + squareLevel-- + if (curlyLevel === 0 && squareLevel === 0) { + argumentIndex++ + } + break + default: + break } - return argumentIndex } + return argumentIndex } diff --git a/src/completion/completer/atsuggestion.ts b/src/completion/completer/atsuggestion.ts index 9e1721c7e9..f508aee553 100644 --- a/src/completion/completer/atsuggestion.ts +++ b/src/completion/completer/atsuggestion.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode' import * as fs from 'fs' import { lw } from '../../lw' -import type {IProvider, IProviderArgs} from '../latex' +import type { CompletionProvider, CompletionArgs } from '../../types' import {escapeRegExp} from '../../utils/utils' interface AtSuggestionItemEntry { @@ -12,7 +12,7 @@ interface AtSuggestionItemEntry { type DataAtSuggestionJsonType = typeof import('../../../data/at-suggestions.json') -export class AtSuggestion implements IProvider { +export class AtSuggestion implements CompletionProvider { private readonly triggerCharacter: string private readonly escapedTriggerCharacter: string private readonly suggestions: vscode.CompletionItem[] = [] @@ -54,7 +54,7 @@ export class AtSuggestion implements IProvider { }) } - provideFrom(result: RegExpMatchArray, args: IProviderArgs) { + from(result: RegExpMatchArray, args: CompletionArgs) { const suggestions = this.provide(args.line, args.position) // Manually filter suggestions when there are several consecutive trigger characters const reg = new RegExp(this.escapedTriggerCharacter + '{2,}$') diff --git a/src/completion/completer/citation.ts b/src/completion/completer/citation.ts index a94cd30c59..8c3e036fa5 100644 --- a/src/completion/completer/citation.ts +++ b/src/completion/completer/citation.ts @@ -2,67 +2,30 @@ import * as vscode from 'vscode' import * as fs from 'fs' import { bibtexParser } from 'latex-utensils' import { lw } from '../../lw' +import type { CitationField, CitationItem, CompletionArgs, CompletionItem, CompletionProvider } from '../../types' import type { FileCache } from '../../types' -import {trimMultiLineString} from '../../utils/utils' -import {computeFilteringRange} from './completerutils' -import type { IProvider, ICompletionItem, IProviderArgs } from '../latex' +import { trimMultiLineString } from '../../utils/utils' +import { computeFilteringRange } from './completerutils' const logger = lw.log('Intelli', 'Citation') -class Fields extends Map { - - get author() { - return this.get('author') - } - - get journal() { - return this.get('journal') - } - - get journaltitle() { - return this.get('journaltitle') - } - - get title() { - return this.get('title') - } - - get publisher() { - return this.get('publisher') - } - - /** - * Concatenate the values of the fields listed in `selectedFields` - * @param selectedFields an array of field names - * @param prefixWithKeys if true, every field is prefixed by 'Fieldname: ' - * @param joinString the string to use for joining the fields - * @returns a string - */ - join(selectedFields: string[], prefixWithKeys: boolean, joinString: string = ' '): string { - const s: string[] = [] - for (const key of this.keys()) { - if (selectedFields.includes(key)) { - const value = this.get(key) as string - if (prefixWithKeys) { - s.push(key.charAt(0).toUpperCase() + key.slice(1) + ': ' + value) - } else { - s.push(value) - } - } - } - return s.join(joinString) - } - +export const provider: CompletionProvider = { from } +export const citation = { + parse, + browser, + getItem, + parseBibFile } -export interface CiteSuggestion extends ICompletionItem { - key: string, - fields: Fields, - file: string, - position: vscode.Position +const data = { + bibEntries: new Map() } +lw.watcher.bib.onCreate(filePath => parseBibFile(filePath)) +lw.watcher.bib.onChange(filePath => parseBibFile(filePath)) +lw.watcher.bib.onDelete(filePath => removeEntriesInFile(filePath)) + /** * Read the value `intellisense.citation.format` * @param configuration workspace configuration @@ -124,252 +87,262 @@ function parseAbbrevations(ast: bibtexParser.BibtexAst) { return abbreviations } -export class Citation implements IProvider { - /** - * Bib entries in each bib `file`. - */ - private readonly bibEntries = new Map() - - constructor() { - lw.watcher.bib.onCreate(filePath => this.parseBibFile(filePath)) - lw.watcher.bib.onChange(filePath => this.parseBibFile(filePath)) - lw.watcher.bib.onDelete(filePath => this.removeEntriesInFile(filePath)) - } - - provideFrom(_result: RegExpMatchArray, args: IProviderArgs) { - return this.provide(args.uri, args.line, args.position) - } - private provide(uri: vscode.Uri, line: string, position: vscode.Position): ICompletionItem[] { - // Compile the suggestion array to vscode completion array - const configuration = vscode.workspace.getConfiguration('latex-workshop', uri) - const label = configuration.get('intellisense.citation.label') as string - const fields = readCitationFormat(configuration) - const range: vscode.Range | undefined = computeFilteringRange(line, position) - return this.updateAll(this.getIncludedBibs(lw.root.file.path)).map(item => { - // Compile the completion item label - switch(label) { - case 'bibtex key': - default: - item.label = item.key - break - case 'title': - if (item.fields.title) { - item.label = item.fields.title - } - break - case 'authors': - if (item.fields.author) { - item.label = item.fields.author - } - break - } - item.filterText = item.key + ' ' + item.fields.join(fields, false) - item.insertText = item.key - item.range = range - // We need two spaces to ensure md newline - item.documentation = new vscode.MarkdownString( '\n' + item.fields.join(fields, true, ' \n') + '\n\n') - return item - }) - } +function from(_result: RegExpMatchArray, args: CompletionArgs) { + return provide(args.uri, args.line, args.position) +} - browser(args?: IProviderArgs) { - const configuration = vscode.workspace.getConfiguration('latex-workshop', args?.uri) - const label = configuration.get('intellisense.citation.label') as string - const fields = readCitationFormat(configuration, label) - void vscode.window.showQuickPick(this.updateAll(this.getIncludedBibs(lw.root.file.path)).map(item => { - return { - label: item.fields.title ? trimMultiLineString(item.fields.title) : '', - description: item.key, - detail: item.fields.join(fields, true, ', ') - } - }), { - placeHolder: 'Press ENTER to insert citation key at cursor', - matchOnDetail: true, - matchOnDescription: true, - ignoreFocusOut: true - }).then(selected => { - if (!selected) { - return - } - if (vscode.window.activeTextEditor) { - const editor = vscode.window.activeTextEditor - const content = editor.document.getText(new vscode.Range(new vscode.Position(0, 0), editor.selection.start)) - let start = editor.selection.start - if (content.lastIndexOf('\\cite') > content.lastIndexOf('}')) { - const curlyStart = content.lastIndexOf('{') + 1 - const commaStart = content.lastIndexOf(',') + 1 - start = editor.document.positionAt(curlyStart > commaStart ? curlyStart : commaStart) +function provide(uri: vscode.Uri, line: string, position: vscode.Position): CompletionItem[] { + // Compile the suggestion array to vscode completion array + const configuration = vscode.workspace.getConfiguration('latex-workshop', uri) + const label = configuration.get('intellisense.citation.label') as string + const fields = readCitationFormat(configuration) + const range: vscode.Range | undefined = computeFilteringRange(line, position) + return updateAll(getIncludedBibs(lw.root.file.path)).map(item => { + // Compile the completion item label + switch(label) { + case 'bibtex key': + default: + item.label = item.key + break + case 'title': + if (item.fields.title) { + item.label = item.fields.title } - void editor.edit(edit => edit.replace(new vscode.Range(start, editor.selection.end), selected.description || '')) - .then(() => editor.selection = new vscode.Selection(editor.selection.end, editor.selection.end)) - } - }) - } - - getEntry(key: string): CiteSuggestion | undefined { - const suggestions = this.updateAll() - const entry = suggestions.find((elm) => elm.key === key) - return entry - } - - getEntryWithDocumentation(key: string, configurationScope: vscode.ConfigurationScope | undefined): CiteSuggestion | undefined { - const entry = this.getEntry(key) - if (entry && !(entry.detail || entry.documentation)) { - const configuration = vscode.workspace.getConfiguration('latex-workshop', configurationScope) - const fields = readCitationFormat(configuration) - // We need two spaces to ensure md newline - entry.documentation = new vscode.MarkdownString( '\n' + entry.fields.join(fields, true, ' \n') + '\n\n') + break + case 'authors': + if (item.fields.author) { + item.label = item.fields.author + } + break } - return entry - } + item.filterText = item.key + ' ' + item.fields.join(fields, false) + item.insertText = item.key + item.range = range + // We need two spaces to ensure md newline + item.documentation = new vscode.MarkdownString( '\n' + item.fields.join(fields, true, ' \n') + '\n\n') + return item + }) +} - /** - * Returns the array of the paths of `.bib` files referenced from `file`. - * - * @param file The path of a LaTeX file. If `undefined`, the keys of `bibEntries` are used. - * @param visitedTeX Internal use only. - */ - private getIncludedBibs(file?: string, visitedTeX: string[] = []): string[] { - if (file === undefined) { - // Only happens when rootFile is undefined - return Array.from(this.bibEntries.keys()) +function browser(args?: CompletionArgs) { + const configuration = vscode.workspace.getConfiguration('latex-workshop', args?.uri) + const label = configuration.get('intellisense.citation.label') as string + const fields = readCitationFormat(configuration, label) + void vscode.window.showQuickPick(updateAll(getIncludedBibs(lw.root.file.path)).map(item => { + return { + label: item.fields.title ? trimMultiLineString(item.fields.title) : '', + description: item.key, + detail: item.fields.join(fields, true, ', ') } - const cache = lw.cache.get(file) - if (cache === undefined) { - return [] + }), { + placeHolder: 'Press ENTER to insert citation key at cursor', + matchOnDetail: true, + matchOnDescription: true, + ignoreFocusOut: true + }).then(selected => { + if (!selected) { + return } - let bibs = Array.from(cache.bibfiles) - visitedTeX.push(file) - for (const child of cache.children) { - if (visitedTeX.includes(child.filePath)) { - // Already included - continue + if (vscode.window.activeTextEditor) { + const editor = vscode.window.activeTextEditor + const content = editor.document.getText(new vscode.Range(new vscode.Position(0, 0), editor.selection.start)) + let start = editor.selection.start + if (content.lastIndexOf('\\cite') > content.lastIndexOf('}')) { + const curlyStart = content.lastIndexOf('{') + 1 + const commaStart = content.lastIndexOf(',') + 1 + start = editor.document.positionAt(curlyStart > commaStart ? curlyStart : commaStart) } - bibs = Array.from(new Set(bibs.concat(this.getIncludedBibs(child.filePath, visitedTeX)))) + void editor.edit(edit => edit.replace(new vscode.Range(start, editor.selection.end), selected.description || '')) + .then(() => editor.selection = new vscode.Selection(editor.selection.end, editor.selection.end)) } - return bibs + }) +} + +function getRawItem(key: string): CitationItem | undefined { + const suggestions = updateAll() + const entry = suggestions.find((elm) => elm.key === key) + return entry +} + +function getItem(key: string, configurationScope?: vscode.ConfigurationScope): CitationItem | undefined { + const entry = getRawItem(key) + if (entry && !(entry.detail || entry.documentation)) { + const configuration = vscode.workspace.getConfiguration('latex-workshop', configurationScope) + const fields = readCitationFormat(configuration) + // We need two spaces to ensure md newline + entry.documentation = new vscode.MarkdownString( '\n' + entry.fields.join(fields, true, ' \n') + '\n\n') } + return entry +} - /** - * Returns aggregated bib entries from `.bib` files and bibitems defined on LaTeX files included in the root file. - * - * @param bibFiles The array of the paths of `.bib` files. If `undefined`, the keys of `bibEntries` are used. - */ - private updateAll(bibFiles?: string[]): CiteSuggestion[] { - let suggestions: CiteSuggestion[] = [] - // From bib files - if (bibFiles === undefined) { - bibFiles = Array.from(this.bibEntries.keys()) +/** + * Returns the array of the paths of `.bib` files referenced from `file`. + * + * @param file The path of a LaTeX file. If `undefined`, the keys of `bibEntries` are used. + * @param visitedTeX Internal use only. + */ +function getIncludedBibs(file?: string, visitedTeX: string[] = []): string[] { + if (file === undefined) { + // Only happens when rootFile is undefined + return Array.from(data.bibEntries.keys()) + } + const cache = lw.cache.get(file) + if (cache === undefined) { + return [] + } + let bibs = Array.from(cache.bibfiles) + visitedTeX.push(file) + for (const child of cache.children) { + if (visitedTeX.includes(child.filePath)) { + // Already included + continue } - bibFiles.forEach(file => { - const entry = this.bibEntries.get(file) - if (entry) { - suggestions = suggestions.concat(entry) - } - }) - // From caches - lw.cache.getIncludedTeX().forEach(cachedFile => { - const cachedBibs = lw.cache.get(cachedFile)?.elements.bibitem - if (cachedBibs === undefined) { - return - } - suggestions = suggestions.concat(cachedBibs.map(bib => { - return { - ...bib, - key: bib.label, - detail: bib.detail ? bib.detail : '', - file: cachedFile, - fields: new Fields() - } - })) - }) - return suggestions + bibs = Array.from(new Set(bibs.concat(getIncludedBibs(child.filePath, visitedTeX)))) } + return bibs +} - /** - * Parses `.bib` file. The results are stored in this instance. - * - * @param fileName The path of `.bib` file. - */ - async parseBibFile(fileName: string) { - logger.log(`Parsing .bib entries from ${fileName}`) - const configuration = vscode.workspace.getConfiguration('latex-workshop', vscode.Uri.file(fileName)) - if (fs.statSync(fileName).size >= (configuration.get('bibtex.maxFileSize') as number) * 1024 * 1024) { - logger.log(`Bib file is too large, ignoring it: ${fileName}`) - this.bibEntries.delete(fileName) - return +/** + * Returns aggregated bib entries from `.bib` files and bibitems defined on LaTeX files included in the root file. + * + * @param bibFiles The array of the paths of `.bib` files. If `undefined`, the keys of `bibEntries` are used. + */ +function updateAll(bibFiles?: string[]): CitationItem[] { + let suggestions: CitationItem[] = [] + // From bib files + if (bibFiles === undefined) { + bibFiles = Array.from(data.bibEntries.keys()) + } + bibFiles.forEach(file => { + const entry = data.bibEntries.get(file) + if (entry) { + suggestions = suggestions.concat(entry) } - const newEntry: CiteSuggestion[] = [] - const bibtex = fs.readFileSync(fileName).toString() - logger.log(`Parse BibTeX AST from ${fileName} .`) - const ast = await lw.parse.bib(bibtex) - if (ast === undefined) { - logger.log(`Parsed 0 bib entries from ${fileName}.`) - lw.event.fire(lw.event.FileParsed, fileName) + }) + // From caches + lw.cache.getIncludedTeX().forEach(cachedFile => { + const cachedBibs = lw.cache.get(cachedFile)?.elements.bibitem + if (cachedBibs === undefined) { return } - const abbreviations = parseAbbrevations(ast) - ast.content - .filter(bibtexParser.isEntry) - .forEach((entry: bibtexParser.Entry) => { - if (entry.internalKey === undefined) { - return - } - const item: CiteSuggestion = { - key: entry.internalKey, - label: entry.internalKey, - file: fileName, - position: new vscode.Position(entry.location.start.line - 1, entry.location.start.column - 1), - kind: vscode.CompletionItemKind.Reference, - fields: new Fields() - } - entry.content.forEach(field => { - const value = deParenthesis(expandField(abbreviations, field.value)) - item.fields.set(field.name, value) - }) - newEntry.push(item) - }) - this.bibEntries.set(fileName, newEntry) - logger.log(`Parsed ${newEntry.length} bib entries from ${fileName} .`) - void lw.outline.reconstruct() + suggestions = suggestions.concat(cachedBibs) + }) + return suggestions +} + +/** + * Parses `.bib` file. The results are stored in this instance. + * + * @param fileName The path of `.bib` file. + */ +async function parseBibFile(fileName: string) { + logger.log(`Parsing .bib entries from ${fileName}`) + const configuration = vscode.workspace.getConfiguration('latex-workshop', vscode.Uri.file(fileName)) + if (fs.statSync(fileName).size >= (configuration.get('bibtex.maxFileSize') as number) * 1024 * 1024) { + logger.log(`Bib file is too large, ignoring it: ${fileName}`) + data.bibEntries.delete(fileName) + return + } + const newEntry: CitationItem[] = [] + const bibtex = fs.readFileSync(fileName).toString() + logger.log(`Parse BibTeX AST from ${fileName} .`) + const ast = await lw.parse.bib(bibtex) + if (ast === undefined) { + logger.log(`Parsed 0 bib entries from ${fileName}.`) lw.event.fire(lw.event.FileParsed, fileName) + return } + const abbreviations = parseAbbrevations(ast) + ast.content + .filter(bibtexParser.isEntry) + .forEach((entry: bibtexParser.Entry) => { + if (entry.internalKey === undefined) { + return + } + const item: CitationItem = { + key: entry.internalKey, + label: entry.internalKey, + file: fileName, + position: new vscode.Position(entry.location.start.line - 1, entry.location.start.column - 1), + kind: vscode.CompletionItemKind.Reference, + fields: new Fields() + } + entry.content.forEach(field => { + const value = deParenthesis(expandField(abbreviations, field.value)) + item.fields.set(field.name, value) + }) + newEntry.push(item) + }) + data.bibEntries.set(fileName, newEntry) + logger.log(`Parsed ${newEntry.length} bib entries from ${fileName} .`) + void lw.outline.reconstruct() + lw.event.fire(lw.event.FileParsed, fileName) +} + +function removeEntriesInFile(file: string) { + logger.log(`Remove parsed bib entries for ${file}`) + data.bibEntries.delete(file) +} + +/** + * Updates the Manager cache for bibitems with Cache. + * Cache `content` is parsed with regular expressions, + * and the result is used to update the cache bibitem element. + */ +function parse(cache: FileCache) { + cache.elements.bibitem = parseContent(cache.filePath, cache.content) +} - removeEntriesInFile(file: string) { - logger.log(`Remove parsed bib entries for ${file}`) - this.bibEntries.delete(file) +function parseContent(file: string, content: string): CitationItem[] { + const itemReg = /^(?!%).*\\bibitem(?:\[[^[\]{}]*\])?{([^}]*)}/gm + const items: CitationItem[] = [] + while (true) { + const result = itemReg.exec(content) + if (result === null) { + break + } + const postContent = content.substring(result.index + result[0].length, content.indexOf('\n', result.index)).trim() + const positionContent = content.substring(0, result.index).split('\n') + items.push({ + key: result[1], + label: result[1], + file, + kind: vscode.CompletionItemKind.Reference, + detail: `${postContent}\n...`, + fields: new Fields(), + position: new vscode.Position(positionContent.length - 1, positionContent[positionContent.length - 1].length) + }) } + return items +} + +class Fields extends Map implements CitationField { + get author() { return this.get('author') } + get journal() { return this.get('journal') } + get journaltitle() { return this.get('journaltitle') } + get title() { return this.get('title') } + get publisher() { return this.get('publisher') } /** - * Updates the Manager cache for bibitems with Cache. - * Cache `content` is parsed with regular expressions, - * and the result is used to update the cache bibitem element. + * Concatenate the values of the fields listed in `selectedFields` + * @param selectedFields an array of field names + * @param prefixWithKeys if true, every field is prefixed by 'Fieldname: ' + * @param joinString the string to use for joining the fields + * @returns a string */ - parse(cache: FileCache) { - cache.elements.bibitem = this.parseContent(cache.filePath, cache.content) - } - - private parseContent(file: string, content: string): CiteSuggestion[] { - const itemReg = /^(?!%).*\\bibitem(?:\[[^[\]{}]*\])?{([^}]*)}/gm - const items: CiteSuggestion[] = [] - while (true) { - const result = itemReg.exec(content) - if (result === null) { - break + join(selectedFields: string[], prefixWithKeys: boolean, joinString: string = ' '): string { + const s: string[] = [] + for (const key of this.keys()) { + if (selectedFields.includes(key)) { + const value = this.get(key) as string + if (prefixWithKeys) { + s.push(key.charAt(0).toUpperCase() + key.slice(1) + ': ' + value) + } else { + s.push(value) + } } - const postContent = content.substring(result.index + result[0].length, content.indexOf('\n', result.index)).trim() - const positionContent = content.substring(0, result.index).split('\n') - items.push({ - key: result[1], - label: result[1], - file, - kind: vscode.CompletionItemKind.Reference, - detail: `${postContent}\n...`, - fields: new Fields(), - position: new vscode.Position(positionContent.length - 1, positionContent[positionContent.length - 1].length) - }) } - return items + return s.join(joinString) } } diff --git a/src/completion/completer/commandlib/surround.ts b/src/completion/completer/commandlib/surround.ts index bd773f96c6..7501b33138 100644 --- a/src/completion/completer/commandlib/surround.ts +++ b/src/completion/completer/commandlib/surround.ts @@ -1,8 +1,8 @@ import * as vscode from 'vscode' -import type { ICompletionItem } from '../../latex' +import type { CompletionItem } from '../../../types' export class SurroundCommand { - static surround(cmdItems: ICompletionItem[]) { + static surround(cmdItems: CompletionItem[]) { if (!vscode.window.activeTextEditor) { return } diff --git a/src/completion/completer/completerutils.ts b/src/completion/completer/completerutils.ts index f4205b9ff7..198b7434e9 100644 --- a/src/completion/completer/completerutils.ts +++ b/src/completion/completer/completerutils.ts @@ -1,5 +1,5 @@ import * as vscode from 'vscode' -import type { ICompletionItem } from '../latex' +import type { CompletionItem } from '../../types' interface CmdSignature { /** name without leading `\` */ @@ -26,7 +26,7 @@ interface CmdSignature { } } -export class CmdEnvSuggestion extends vscode.CompletionItem implements ICompletionItem { +export class CmdEnvSuggestion extends vscode.CompletionItem implements CompletionItem { label: string package: string keyvals: string[] @@ -65,7 +65,7 @@ export class CmdEnvSuggestion extends vscode.CompletionItem implements ICompleti } } -export function filterNonLetterSuggestions(suggestions: ICompletionItem[], typedText: string, pos: vscode.Position): ICompletionItem[] { +export function filterNonLetterSuggestions(suggestions: CompletionItem[], typedText: string, pos: vscode.Position): CompletionItem[] { if (typedText.match(/[^a-zA-Z]/)) { const exactSuggestion = suggestions.filter(entry => entry.label.startsWith(typedText)) if (exactSuggestion.length > 0) { diff --git a/src/completion/completer/documentclass.ts b/src/completion/completer/documentclass.ts index 1aaadc4d5b..cb6fed7a59 100644 --- a/src/completion/completer/documentclass.ts +++ b/src/completion/completer/documentclass.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode' import * as fs from 'fs' import { lw } from '../../lw' -import type { IProvider } from '../latex' +import type { CompletionProvider } from '../../types' type DataClassnamesJsonType = typeof import('../../../data/classnames.json') @@ -11,7 +11,7 @@ type ClassItemEntry = { documentation: string } -export class DocumentClass implements IProvider { +export class DocumentClass implements CompletionProvider { private readonly suggestions: vscode.CompletionItem[] = [] initialize(classes: {[key: string]: ClassItemEntry}) { @@ -23,7 +23,7 @@ export class DocumentClass implements IProvider { }) } - provideFrom() { + from() { return this.provide() } diff --git a/src/completion/completer/environment.ts b/src/completion/completer/environment.ts index d802f77689..e5b26d06cb 100644 --- a/src/completion/completer/environment.ts +++ b/src/completion/completer/environment.ts @@ -2,335 +2,322 @@ import * as vscode from 'vscode' import * as fs from 'fs' import type * as Ast from '@unified-latex/unified-latex-types' import { lw } from '../../lw' -import type { FileCache } from '../../types' -import type { ICompletionItem, IProvider, IProviderArgs } from '../latex' +import { EnvSnippetType } from '../../types' +import type { CompletionArgs, CompletionItem, CompletionProvider, Environment, FileCache } from '../../types' import { CmdEnvSuggestion, splitSignatureString, filterNonLetterSuggestions, filterArgumentHint } from './completerutils' const logger = lw.log('Intelli', 'Environment') -export type EnvType = { - /** Name of the environment, what comes inside \begin{...} */ - name: string, - /** To be inserted after \begin{..} */ - snippet?: string, - /** The option of package below that activates this env */ - option?: string, - /** Possible options of this env */ - keyvals?: string[], - /** The index of keyval list in package .json file. Should not be used */ - keyvalindex?: number, - /** The index of argument which have the keyvals */ - keyvalpos?: number, - /** The package providing the environment */ - package?: string, - detail?: string +export const provider: CompletionProvider = { from } +export const environment = { + parse, + getDefaultEnvs, + setPackageEnvs, + getEnvFromPkg, + provideEnvsAsCommandInPkg } -function isEnv(obj: any): obj is EnvType { - return (typeof obj.name === 'string') +const data = { + defaultEnvsAsName: [] as CmdEnvSuggestion[], + defaultEnvsAsCommand: [] as CmdEnvSuggestion[], + defaultEnvsForBegin: [] as CmdEnvSuggestion[], + packageEnvs: new Map(), + packageEnvsAsName: new Map(), + packageEnvsAsCommand: new Map(), + packageEnvsForBegin: new Map() } -export enum EnvSnippetType { AsName, AsCommand, ForBegin, } - -export class Environment implements IProvider { - private defaultEnvsAsName: CmdEnvSuggestion[] = [] - private defaultEnvsAsCommand: CmdEnvSuggestion[] = [] - private defaultEnvsForBegin: CmdEnvSuggestion[] = [] - private readonly packageEnvs = new Map() - private readonly packageEnvsAsName = new Map() - private readonly packageEnvsAsCommand = new Map() - private readonly packageEnvsForBegin= new Map() +lw.onConfigChange('intellisense.package.exclude', initialize) +initialize() +function initialize() { + const excludeDefault = (vscode.workspace.getConfiguration('latex-workshop').get('intellisense.package.exclude') as string[]).includes('lw-default') + const envs = excludeDefault ? {} : JSON.parse(fs.readFileSync(`${lw.extensionRoot}/data/environments.json`, {encoding: 'utf8'})) as {[key: string]: Environment} + Object.entries(envs).forEach(([key, env]) => { + env.name = env.name || key + env.snippet = env.snippet || '' + env.detail = key + }) + data.defaultEnvsAsCommand = [] + data.defaultEnvsForBegin = [] + data.defaultEnvsAsName = [] + Object.entries(envs).forEach(([key, env]) => { + data.defaultEnvsAsCommand.push(entryEnvToCompletion(key, env, EnvSnippetType.AsCommand)) + data.defaultEnvsForBegin.push(entryEnvToCompletion(key, env, EnvSnippetType.ForBegin)) + data.defaultEnvsAsName.push(entryEnvToCompletion(key, env, EnvSnippetType.AsName)) + }) + + return data +} - constructor() { - lw.onConfigChange('intellisense.package.exclude', () => this.initialize()) - } +function isEnv(obj: any): obj is Environment { + return (typeof obj.name === 'string') +} - initialize() { - const excludeDefault = (vscode.workspace.getConfiguration('latex-workshop').get('intellisense.package.exclude') as string[]).includes('lw-default') - const envs = excludeDefault ? {} : JSON.parse(fs.readFileSync(`${lw.extensionRoot}/data/environments.json`, {encoding: 'utf8'})) as {[key: string]: EnvType} - Object.entries(envs).forEach(([key, env]) => { - env.name = env.name || key - env.snippet = env.snippet || '' - env.detail = key - }) - this.defaultEnvsAsCommand = [] - this.defaultEnvsForBegin = [] - this.defaultEnvsAsName = [] - Object.entries(envs).forEach(([key, env]) => { - this.defaultEnvsAsCommand.push(this.entryEnvToCompletion(key, env, EnvSnippetType.AsCommand)) - this.defaultEnvsForBegin.push(this.entryEnvToCompletion(key, env, EnvSnippetType.ForBegin)) - this.defaultEnvsAsName.push(this.entryEnvToCompletion(key, env, EnvSnippetType.AsName)) - }) - return this +/** + * This function is called by Command.initialize with type=EnvSnippetType.AsCommand + * to build a `\envname` command for every default environment. + */ +function getDefaultEnvs(type: EnvSnippetType): CmdEnvSuggestion[] { + switch (type) { + case EnvSnippetType.AsName: + return data.defaultEnvsAsName + break + case EnvSnippetType.AsCommand: + return data.defaultEnvsAsCommand + break + case EnvSnippetType.ForBegin: + return data.defaultEnvsForBegin + break + default: + return [] } +} - /** - * This function is called by Command.initialize with type=EnvSnippetType.AsCommand - * to build a `\envname` command for every default environment. - */ - getDefaultEnvs(type: EnvSnippetType): CmdEnvSuggestion[] { - switch (type) { - case EnvSnippetType.AsName: - return this.defaultEnvsAsName - break - case EnvSnippetType.AsCommand: - return this.defaultEnvsAsCommand - break - case EnvSnippetType.ForBegin: - return this.defaultEnvsForBegin - break - default: - return [] - } +function getPackageEnvs(type?: EnvSnippetType): Map { + switch (type) { + case EnvSnippetType.AsName: + return data.packageEnvsAsName + case EnvSnippetType.AsCommand: + return data.packageEnvsAsCommand + case EnvSnippetType.ForBegin: + return data.packageEnvsForBegin + default: + return new Map() } +} - getPackageEnvs(type?: EnvSnippetType): Map { - switch (type) { - case EnvSnippetType.AsName: - return this.packageEnvsAsName - case EnvSnippetType.AsCommand: - return this.packageEnvsAsCommand - case EnvSnippetType.ForBegin: - return this.packageEnvsForBegin - default: - return new Map() - } - } +function from(result: RegExpMatchArray, args: CompletionArgs) { + const suggestions = provide(args.langId, args.line, args.position) + // Commands starting with a non letter character are not filtered properly because of wordPattern definition. + return filterNonLetterSuggestions(suggestions, result[1], args.position) +} - provideFrom(result: RegExpMatchArray, args: IProviderArgs) { - const suggestions = this.provide(args.langId, args.line, args.position) - // Commands starting with a non letter character are not filtered properly because of wordPattern definition. - return filterNonLetterSuggestions(suggestions, result[1], args.position) +function provide(langId: string, line: string, position: vscode.Position): CompletionItem[] { + let snippetType: EnvSnippetType = EnvSnippetType.ForBegin + if (vscode.window.activeTextEditor && vscode.window.activeTextEditor.selections.length > 1 || line.slice(position.character).match(/[a-zA-Z*]*}/)) { + snippetType = EnvSnippetType.AsName } - private provide(langId: string, line: string, position: vscode.Position): ICompletionItem[] { - let snippetType: EnvSnippetType = EnvSnippetType.ForBegin - if (vscode.window.activeTextEditor && vscode.window.activeTextEditor.selections.length > 1 || line.slice(position.character).match(/[a-zA-Z*]*}/)) { - snippetType = EnvSnippetType.AsName - } + // Extract cached envs and add to default ones + const suggestions: CmdEnvSuggestion[] = Array.from(getDefaultEnvs(snippetType)) + const envList: string[] = getDefaultEnvs(snippetType).map(env => env.label) - // Extract cached envs and add to default ones - const suggestions: CmdEnvSuggestion[] = Array.from(this.getDefaultEnvs(snippetType)) - const envList: string[] = this.getDefaultEnvs(snippetType).map(env => env.label) + // Insert package environments + const configuration = vscode.workspace.getConfiguration('latex-workshop') + if (configuration.get('intellisense.package.enabled')) { + const packages = lw.completer.package.getPackagesIncluded(langId) + Object.entries(packages).forEach(([packageName, options]) => { + getEnvFromPkg(packageName, snippetType).forEach(env => { + if (env.option && options && !options.includes(env.option)) { + return + } + if (!envList.includes(env.label)) { + suggestions.push(env) + envList.push(env.label) + } + }) + }) + } - // Insert package environments - const configuration = vscode.workspace.getConfiguration('latex-workshop') - if (configuration.get('intellisense.package.enabled')) { - const packages = lw.completer.package.getPackagesIncluded(langId) - Object.entries(packages).forEach(([packageName, options]) => { - this.getEnvFromPkg(packageName, snippetType).forEach(env => { - if (env.option && options && !options.includes(env.option)) { - return - } - if (!envList.includes(env.label)) { - suggestions.push(env) - envList.push(env.label) + // Insert environments defined in tex + lw.cache.getIncludedTeX().forEach(cachedFile => { + const cachedEnvs = lw.cache.get(cachedFile)?.elements.environment + if (cachedEnvs !== undefined) { + cachedEnvs.forEach(env => { + if (! envList.includes(env.label)) { + if (snippetType === EnvSnippetType.ForBegin) { + env.insertText = new vscode.SnippetString(`${env.label}}\n\t$0\n\\end{${env.label}}`) + } else { + env.insertText = env.label } - }) + suggestions.push(env) + envList.push(env.label) + } }) } + }) - // Insert environments defined in tex - lw.cache.getIncludedTeX().forEach(cachedFile => { - const cachedEnvs = lw.cache.get(cachedFile)?.elements.environment - if (cachedEnvs !== undefined) { - cachedEnvs.forEach(env => { - if (! envList.includes(env.label)) { - if (snippetType === EnvSnippetType.ForBegin) { - env.insertText = new vscode.SnippetString(`${env.label}}\n\t$0\n\\end{${env.label}}`) - } else { - env.insertText = env.label - } - suggestions.push(env) - envList.push(env.label) - } - }) - } - }) + filterArgumentHint(suggestions) - filterArgumentHint(suggestions) + return suggestions +} - return suggestions +/** + * Environments can be inserted using `\envname`. + * This function is called by Command.provide to compute these commands for every package in use. + */ +function provideEnvsAsCommandInPkg(packageName: string, options: string[], suggestions: CmdEnvSuggestion[], defined?: Set) { + defined = defined ?? new Set() + const configuration = vscode.workspace.getConfiguration('latex-workshop') + const useOptionalArgsEntries = configuration.get('intellisense.optionalArgsEntries.enabled') + + if (! configuration.get('intellisense.package.env.enabled')) { + return } - /** - * 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: CmdEnvSuggestion[], defined?: Set) { - defined = defined ?? new Set() - const configuration = vscode.workspace.getConfiguration('latex-workshop') - const useOptionalArgsEntries = configuration.get('intellisense.optionalArgsEntries.enabled') - - if (! configuration.get('intellisense.package.env.enabled')) { - return - } + // Load environments from the package if not already done + const entry = getEnvFromPkg(packageName, EnvSnippetType.AsCommand) + // No environment defined in package + if (!entry || entry.length === 0) { + return + } - // Load environments from the package if not already done - const entry = this.getEnvFromPkg(packageName, EnvSnippetType.AsCommand) - // No environment defined in package - if (!entry || entry.length === 0) { + // Insert env snippets + for (const env of entry) { + if (!useOptionalArgsEntries && env.hasOptionalArgs()) { return } - - // Insert env snippets - for (const env of entry) { - if (!useOptionalArgsEntries && env.hasOptionalArgs()) { + if (!defined.has(env.signatureAsString())) { + if (env.option && options && !options.includes(env.option)) { return } - if (!defined.has(env.signatureAsString())) { - if (env.option && options && !options.includes(env.option)) { - return - } - suggestions.push(env) - defined.add(env.signatureAsString()) - } + suggestions.push(env) + defined.add(env.signatureAsString()) } } +} - parse(cache: FileCache) { - if (cache.ast !== undefined) { - cache.elements.environment = this.parseAst(cache.ast) - } else { - cache.elements.environment = this.parseContent(cache.contentTrimmed) - } +function parse(cache: FileCache) { + if (cache.ast !== undefined) { + cache.elements.environment = parseAst(cache.ast) + } else { + cache.elements.environment = parseContent(cache.contentTrimmed) } +} - private parseAst(node: Ast.Node): CmdEnvSuggestion[] { - let envs: CmdEnvSuggestion[] = [] - if (node.type === 'environment' || node.type === 'mathenv') { - const content = (typeof node.env === 'string') ? node.env : (node.env as unknown as {content: string}).content - const env = new CmdEnvSuggestion(`${content}`, '', [], -1, { name: content, args: '' }, vscode.CompletionItemKind.Module) - env.documentation = '`' + content + '`' - env.filterText = content - envs.push(env) - } +function parseAst(node: Ast.Node): CmdEnvSuggestion[] { + let envs: CmdEnvSuggestion[] = [] + if (node.type === 'environment' || node.type === 'mathenv') { + const content = (typeof node.env === 'string') ? node.env : (node.env as unknown as {content: string}).content + const env = new CmdEnvSuggestion(`${content}`, '', [], -1, { name: content, args: '' }, vscode.CompletionItemKind.Module) + env.documentation = '`' + content + '`' + env.filterText = content + envs.push(env) + } - const parseContent = (content: Ast.Node[]) => { - for (const subNode of content) { - envs = [...envs, ...this.parseAst(subNode)] - } + const parseNodeContent = (content: Ast.Node[]) => { + for (const subNode of content) { + envs = [...envs, ...parseAst(subNode)] } - if (node.type === 'macro' && node.args) { - for (const arg of node.args) { - parseContent(arg.content) - } - } else if ('content' in node && typeof node.content !== 'string') { - parseContent(node.content) + } + if (node.type === 'macro' && node.args) { + for (const arg of node.args) { + parseNodeContent(arg.content) } - - return envs + } else if ('content' in node && typeof node.content !== 'string') { + parseNodeContent(node.content) } - 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] + return envs +} - envs.push(env) - envList.push(result[1]) +function 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 } - return envs - } - - getEnvFromPkg(packageName: string, type: EnvSnippetType): CmdEnvSuggestion[] { - const packageEnvs = this.getPackageEnvs(type) - const entry = packageEnvs.get(packageName) - if (entry !== undefined) { - return entry + 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] - lw.completer.loadPackageData(packageName) - // No package command defined - const pkgEnvs = this.packageEnvs.get(packageName) - if (!pkgEnvs || pkgEnvs.length === 0) { - return [] - } + envs.push(env) + envList.push(result[1]) + } + return envs +} - const newEntry: CmdEnvSuggestion[] = [] - pkgEnvs.forEach(env => { - // \array{} : detail=array{}, name=array. - newEntry.push(this.entryEnvToCompletion(env.detail || env.name, env, type)) - }) - packageEnvs.set(packageName, newEntry) - return newEntry +function getEnvFromPkg(packageName: string, type: EnvSnippetType): CmdEnvSuggestion[] { + const packageEnvs = getPackageEnvs(type) + const entry = packageEnvs.get(packageName) + if (entry !== undefined) { + return entry } - setPackageEnvs(packageName: string, envs: {[key: string]: EnvType}) { - const environments: EnvType[] = [] - Object.entries(envs).forEach(([key, env]) => { - env.package = packageName - if (isEnv(env)) { - environments.push(env) - } else { - logger.log(`Cannot parse intellisense file for ${packageName}`) - logger.log(`Missing field in entry: "${key}": ${JSON.stringify(env)}`) - delete envs[key] - } - }) - this.packageEnvs.set(packageName, environments) + lw.completer.loadPackageData(packageName) + // No package command defined + const pkgEnvs = data.packageEnvs.get(packageName) + if (!pkgEnvs || pkgEnvs.length === 0) { + return [] } - private entryEnvToCompletion(itemKey: string, item: EnvType, type: EnvSnippetType): CmdEnvSuggestion { - const label = item.detail ? item.detail : item.name - const suggestion = new CmdEnvSuggestion( - item.name, - item.package || 'latex', - item.keyvals && typeof(item.keyvals) !== 'number' ? item.keyvals : [], - item.keyvalpos === undefined ? -1 : item.keyvalpos, - splitSignatureString(itemKey), - vscode.CompletionItemKind.Module, - item.option) - suggestion.detail = `\\begin{${item.name}}${item.snippet?.replace(/\$\{\d+:([^$}]*)\}/g, '$1')}\n...\n\\end{${item.name}}` - suggestion.documentation = `Environment ${item.name} .` - if (item.package) { - suggestion.documentation += ` From package: ${item.package}.` - } - suggestion.sortText = label.replace(/^[a-zA-Z]/, c => { - const n = c.match(/[a-z]/) ? c.toUpperCase().charCodeAt(0): c.toLowerCase().charCodeAt(0) - return n !== undefined ? n.toString(16): c - }) + const newEntry: CmdEnvSuggestion[] = [] + pkgEnvs.forEach(env => { + // \array{} : detail=array{}, name=array. + newEntry.push(entryEnvToCompletion(env.detail || env.name, env, type)) + }) + packageEnvs.set(packageName, newEntry) + return newEntry +} - if (type === EnvSnippetType.AsName) { - return suggestion +function setPackageEnvs(packageName: string, envs: {[key: string]: Environment}) { + const environments: Environment[] = [] + Object.entries(envs).forEach(([key, env]) => { + env.package = packageName + if (isEnv(env)) { + environments.push(env) } else { - if (type === EnvSnippetType.AsCommand) { - suggestion.kind = vscode.CompletionItemKind.Snippet - } - const configuration = vscode.workspace.getConfiguration('latex-workshop') - const useTabStops = configuration.get('intellisense.useTabStops.enabled') - const prefix = (type === EnvSnippetType.ForBegin) ? '' : 'begin{' - let snippet: string = item.snippet ? item.snippet : '' - if (item.snippet) { - if (useTabStops) { - snippet = item.snippet.replace(/\$\{(\d+):[^}]*\}/g, '$${$1}') - } - } - if (snippet.match(/\$\{?0\}?/)) { - snippet = snippet.replace(/\$\{?0\}?/, '$${0:$${TM_SELECTED_TEXT}}') - snippet += '\n' - } else { - snippet += '\n\t${0:${TM_SELECTED_TEXT}}\n' - } - if (item.detail) { - suggestion.label = item.detail + logger.log(`Cannot parse intellisense file for ${packageName}`) + logger.log(`Missing field in entry: "${key}": ${JSON.stringify(env)}`) + delete envs[key] + } + }) + data.packageEnvs.set(packageName, environments) +} + +function entryEnvToCompletion(itemKey: string, item: Environment, type: EnvSnippetType): CmdEnvSuggestion { + const label = item.detail ? item.detail : item.name + const suggestion = new CmdEnvSuggestion( + item.name, + item.package || 'latex', + item.keyvals && typeof(item.keyvals) !== 'number' ? item.keyvals : [], + item.keyvalpos === undefined ? -1 : item.keyvalpos, + splitSignatureString(itemKey), + vscode.CompletionItemKind.Module, + item.option) + suggestion.detail = `\\begin{${item.name}}${item.snippet?.replace(/\$\{\d+:([^$}]*)\}/g, '$1')}\n...\n\\end{${item.name}}` + suggestion.documentation = `Environment ${item.name} .` + if (item.package) { + suggestion.documentation += ` From package: ${item.package}.` + } + suggestion.sortText = label.replace(/^[a-zA-Z]/, c => { + const n = c.match(/[a-z]/) ? c.toUpperCase().charCodeAt(0): c.toLowerCase().charCodeAt(0) + return n !== undefined ? n.toString(16): c + }) + + if (type === EnvSnippetType.AsName) { + return suggestion + } else { + if (type === EnvSnippetType.AsCommand) { + suggestion.kind = vscode.CompletionItemKind.Snippet + } + const configuration = vscode.workspace.getConfiguration('latex-workshop') + const useTabStops = configuration.get('intellisense.useTabStops.enabled') + const prefix = (type === EnvSnippetType.ForBegin) ? '' : 'begin{' + let snippet: string = item.snippet ? item.snippet : '' + if (item.snippet) { + if (useTabStops) { + snippet = item.snippet.replace(/\$\{(\d+):[^}]*\}/g, '$${$1}') } - suggestion.filterText = itemKey - suggestion.insertText = new vscode.SnippetString(`${prefix}${item.name}}${snippet}\\end{${item.name}}`) - return suggestion } + if (snippet.match(/\$\{?0\}?/)) { + snippet = snippet.replace(/\$\{?0\}?/, '$${0:$${TM_SELECTED_TEXT}}') + snippet += '\n' + } else { + snippet += '\n\t${0:${TM_SELECTED_TEXT}}\n' + } + if (item.detail) { + suggestion.label = item.detail + } + suggestion.filterText = itemKey + suggestion.insertText = new vscode.SnippetString(`${prefix}${item.name}}${snippet}\\end{${item.name}}`) + return suggestion } - } diff --git a/src/completion/completer/glossary.ts b/src/completion/completer/glossary.ts index a388ce61a5..d356a3763a 100644 --- a/src/completion/completer/glossary.ts +++ b/src/completion/completer/glossary.ts @@ -1,8 +1,7 @@ import * as vscode from 'vscode' import type * as Ast from '@unified-latex/unified-latex-types' import { lw } from '../../lw' -import type { FileCache } from '../../types' -import type { ICompletionItem, IProvider } from '../latex' +import type { CompletionItem, CompletionProvider, FileCache } from '../../types' import { argContentToStr } from '../../utils/parser' import { getLongestBalancedString } from '../../utils/utils' @@ -16,18 +15,18 @@ interface GlossaryEntry { description: string | undefined } -export interface GlossarySuggestion extends ICompletionItem { +export interface GlossarySuggestion extends CompletionItem { type: GlossaryType, filePath: string, position: vscode.Position } -export class Glossary implements IProvider { +export class Glossary implements CompletionProvider { // use object for deduplication private readonly glossaries = new Map() private readonly acronyms = new Map() - provideFrom(result: RegExpMatchArray) { + from(result: RegExpMatchArray) { return this.provide(result) } diff --git a/src/completion/completer/input.ts b/src/completion/completer/input.ts index 118d067f63..f0ce141497 100644 --- a/src/completion/completer/input.ts +++ b/src/completion/completer/input.ts @@ -3,14 +3,13 @@ import * as fs from 'fs' import * as path from 'path' import * as micromatch from 'micromatch' import { lw } from '../../lw' -import type { FileCache } from '../../types' -import type { IProvider, IProviderArgs } from '../latex' +import type { CompletionProvider, CompletionArgs, FileCache } from '../../types' const logger = lw.log('Intelli', 'Input') const ignoreFiles = ['**/.vscode', '**/.vscodeignore', '**/.gitignore'] -abstract class InputAbstract implements IProvider { +abstract class InputAbstract implements CompletionProvider { graphicsPath: Set = new Set() /** @@ -64,7 +63,7 @@ abstract class InputAbstract implements IProvider { this.graphicsPath.clear() } - provideFrom(result: RegExpMatchArray, args: IProviderArgs) { + from(result: RegExpMatchArray, args: CompletionArgs) { const command = result[1] const payload = [...result.slice(2).reverse()] return this.provide(args.uri, args.line, args.position, command, payload) diff --git a/src/completion/completer/macro.ts b/src/completion/completer/macro.ts index 14cb04047e..7e4ba9559a 100644 --- a/src/completion/completer/macro.ts +++ b/src/completion/completer/macro.ts @@ -2,37 +2,69 @@ import * as vscode from 'vscode' import * as fs from 'fs' import type * as Ast from '@unified-latex/unified-latex-types' import { lw } from '../../lw' -import { FileCache } from '../../types' +import { EnvSnippetType } from '../../types' +import type { CompletionArgs, CompletionItem, CompletionProvider, FileCache, Macro, Package } from '../../types' +import { environment } from './environment' -import type { IProvider, ICompletionItem, PkgType, IProviderArgs } from '../latex' import { CmdEnvSuggestion, splitSignatureString, filterNonLetterSuggestions, filterArgumentHint } from './completerutils' -import {SurroundCommand} from './commandlib/surround' -import { Environment, EnvSnippetType } from './environment' - +import { SurroundCommand } from './commandlib/surround' const logger = lw.log('Intelli', 'Command') -type DataUnimathSymbolsJsonType = typeof import('../../../data/unimathsymbols.json') - -export type CmdType = { - /** Name of the command without the leading \ and with argument signature */ - command?: string, - /** Snippet to be inserted after the leading \ */ - snippet?: string, - /** The option of package below that activates this cmd */ - option?: string, - /** Possible options of this env */ - keyvals?: string[], - /** The index of keyval list in package .json file. Should not be used */ - keyvalindex?: number, - /** The index of argument which have the keyvals */ - keyvalpos?: number, - detail?: string, - documentation?: string, - /** The package providing the environment */ - package?: string, - /** The action to be executed after inserting the snippet */ - postAction?: string +export const provider: CompletionProvider = { from } +export const macro = { + parse, + surround, + getPackageCmds, + setPackageCmds, + provideCmdInPkg, + getData +} + +function getData() { return data } +const data = { + definedCmds: new Map(), + defaultCmds: [] as CmdEnvSuggestion[], + defaultSymbols: [] as CmdEnvSuggestion[], + packageCmds: new Map() +} +Object.entries(JSON.parse(fs.readFileSync(`${lw.extensionRoot}/data/unimathsymbols.json`).toString()) as typeof import('../../../data/unimathsymbols.json')) + .forEach(([key, symbol]) => data.defaultSymbols.push(entryCmdToCompletion(key, symbol))) + +lw.onConfigChange(['intellisense.command.user', 'intellisense.package.exclude'], initialize) +initialize() +function initialize() { + const excludeDefault = (vscode.workspace.getConfiguration('latex-workshop').get('intellisense.package.exclude') as string[]).includes('lw-default') + const cmds = excludeDefault ? {} : JSON.parse(fs.readFileSync(`${lw.extensionRoot}/data/commands.json`, {encoding: 'utf8'})) as {[key: string]: Macro} + const maths = excludeDefault ? {} : (JSON.parse(fs.readFileSync(`${lw.extensionRoot}/data/packages/tex.json`, {encoding: 'utf8'})) as Package).macros + Object.assign(maths, cmds) + Object.entries(maths).forEach(([key, cmd]) => { + cmd.macro = key + cmd.snippet = cmd.snippet || key + }) + + const defaultEnvs = environment.getDefaultEnvs(EnvSnippetType.AsCommand) + + const userCmds = vscode.workspace.getConfiguration('latex-workshop').get('intellisense.command.user') as {[key: string]: string} + Object.entries(userCmds).forEach(([key, snippet]) => { + if (maths[key] && snippet !== '') { + maths[key].snippet = snippet + } else if (maths[key] && snippet === '') { + delete maths[key] + } else { + maths[key] = { snippet } + } + }) + + data.defaultCmds = [] + + // Initialize default commands and the ones in `tex.json` + Object.entries(maths).forEach(([key, cmd]) => data.defaultCmds.push(entryCmdToCompletion(key, cmd))) + + // Initialize default env begin-end pairs + defaultEnvs.forEach(cmd => { + data.defaultCmds.push(cmd) + }) } export function isTriggerSuggestNeeded(name: string): boolean { @@ -40,412 +72,360 @@ export function isTriggerSuggestNeeded(name: string): boolean { return reg.test(name) } -function isCmdWithSnippet(obj: any): obj is CmdType { +function isCmdWithSnippet(obj: any): obj is Macro { return (typeof obj.command === 'string') && (typeof obj.snippet === 'string') } -export class Command implements IProvider { - - 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))) +function from(result: RegExpMatchArray, args: CompletionArgs) { + const suggestions = provide(args.langId, args.line, args.position) + // Commands ending with (, { or [ are not filtered properly by vscode intellisense. So we do it by hand. + if (result[0].match(/[({[]$/)) { + const exactSuggestion = suggestions.filter(entry => entry.label === result[0]) + if (exactSuggestion.length > 0) { + return exactSuggestion + } + } + // Commands starting with a non letter character are not filtered properly because of wordPattern definition. + return filterNonLetterSuggestions(suggestions, result[1], args.position) +} - lw.onConfigChange(['intellisense.command.user', 'intellisense.package.exclude'], () => { - this.initialize(lw.completer.environment) +function provide(langId: string, line?: string, position?: vscode.Position): CompletionItem[] { + const configuration = vscode.workspace.getConfiguration('latex-workshop') + const useOptionalArgsEntries = configuration.get('intellisense.optionalArgsEntries.enabled') + let range: vscode.Range | undefined = undefined + if (line && position) { + const startPos = line.lastIndexOf('\\', position.character - 1) + if (startPos >= 0) { + range = new vscode.Range(position.line, startPos + 1, position.line, position.character) + } + } + const suggestions: CmdEnvSuggestion[] = [] + let defined = new Set() + // Insert default commands + data.defaultCmds.forEach(cmd => { + if (!useOptionalArgsEntries && cmd.hasOptionalArgs()) { + return + } + cmd.range = range + suggestions.push(cmd) + defined.add(cmd.signatureAsString()) + }) + + // Insert unimathsymbols + if (configuration.get('intellisense.unimathsymbols.enabled')) { + data.defaultSymbols.forEach(symbol => { + suggestions.push(symbol) + defined.add(symbol.signatureAsString()) }) } - initialize(environment: Environment) { - const excludeDefault = (vscode.workspace.getConfiguration('latex-workshop').get('intellisense.package.exclude') as string[]).includes('lw-default') - const cmds = excludeDefault ? {} : JSON.parse(fs.readFileSync(`${lw.extensionRoot}/data/commands.json`, {encoding: 'utf8'})) as {[key: string]: CmdType} - const maths = excludeDefault ? {} : (JSON.parse(fs.readFileSync(`${lw.extensionRoot}/data/packages/tex.json`, {encoding: 'utf8'})) as PkgType).cmds - Object.assign(maths, cmds) - Object.entries(maths).forEach(([key, cmd]) => { - cmd.command = key - cmd.snippet = cmd.snippet || key + // Insert commands from packages + if ((configuration.get('intellisense.package.enabled'))) { + const packages = lw.completer.package.getPackagesIncluded(langId) + Object.entries(packages).forEach(([packageName, options]) => { + provideCmdInPkg(packageName, options, suggestions) + environment.provideEnvsAsCommandInPkg(packageName, options, suggestions, defined) }) + } - const defaultEnvs = environment.getDefaultEnvs(EnvSnippetType.AsCommand) - - const userCmds = vscode.workspace.getConfiguration('latex-workshop').get('intellisense.command.user') as {[key: string]: string} - Object.entries(userCmds).forEach(([key, snippet]) => { - if (maths[key] && snippet !== '') { - maths[key].snippet = snippet - } else if (maths[key] && snippet === '') { - delete maths[key] - } else { - maths[key] = { snippet } - } - }) + // 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 + defined = new Set(suggestions.map(s => s.signatureAsString())) + lw.cache.getIncludedTeX().forEach(tex => { + const cmds = lw.cache.get(tex)?.elements.command + if (cmds !== undefined) { + cmds.forEach(cmd => { + if (!defined.has(cmd.signatureAsString())) { + cmd.range = range + suggestions.push(cmd) + defined.add(cmd.signatureAsString()) + } + }) + } + }) - this.defaultCmds = [] + filterArgumentHint(suggestions) - // Initialize default commands and the ones in `tex.json` - Object.entries(maths).forEach(([key, cmd]) => this.defaultCmds.push(this.entryCmdToCompletion(key, cmd))) + return suggestions +} - // Initialize default env begin-end pairs - defaultEnvs.forEach(cmd => { - this.defaultCmds.push(cmd) - }) +/** + * Surrounds `content` with a command picked in QuickPick. + * + * @param content A string to be surrounded. If not provided, then we loop over all the selections and surround each of them. + */ +function surround() { + if (!vscode.window.activeTextEditor) { + return } + const editor = vscode.window.activeTextEditor + const cmdItems = provide(editor.document.languageId) + SurroundCommand.surround(cmdItems) +} - 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. - if (result[0].match(/[({[]$/)) { - const exactSuggestion = suggestions.filter(entry => entry.label === result[0]) - if (exactSuggestion.length > 0) { - return exactSuggestion - } +function parse(cache: FileCache) { + // Remove newcommand cmds, because they will be re-insert in the next step + data.definedCmds.forEach((entry,cmd) => { + if (entry.filePath === cache.filePath) { + data.definedCmds.delete(cmd) } - // Commands starting with a non letter character are not filtered properly because of wordPattern definition. - return filterNonLetterSuggestions(suggestions, result[1], args.position) + }) + if (cache.ast !== undefined) { + cache.elements.command = parseAst(cache.ast, cache.filePath) + } else { + cache.elements.command = parseContent(cache.content, cache.filePath) } +} - private provide(langId: string, line?: string, position?: vscode.Position): ICompletionItem[] { - const configuration = vscode.workspace.getConfiguration('latex-workshop') - const useOptionalArgsEntries = configuration.get('intellisense.optionalArgsEntries.enabled') - let range: vscode.Range | undefined = undefined - if (line && position) { - const startPos = line.lastIndexOf('\\', position.character - 1) - if (startPos >= 0) { - range = new vscode.Range(position.line, startPos + 1, position.line, position.character) - } +function parseAst(node: Ast.Node, filePath: string, defined?: Set): CmdEnvSuggestion[] { + defined = defined ?? new Set() + let cmds: CmdEnvSuggestion[] = [] + let found = false + let name = '' + let args = '' + if (node.type === 'macro' && + ['renewcommand', 'newcommand'].includes(node.content) && + node.args?.[2]?.content?.[0]?.type === 'macro') { + // \newcommand{\fix}[3][]{\chdeleted{#2}\chadded[comment={#1}]{#3}} + // \newcommand\WARNING{\textcolor{red}{WARNING}} + found = true + name = node.args[2].content[0].content + 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) } - const suggestions: CmdEnvSuggestion[] = [] - let defined = new Set() - // Insert default commands - this.defaultCmds.forEach(cmd => { - if (!useOptionalArgsEntries && cmd.hasOptionalArgs()) { - return - } - cmd.range = range - suggestions.push(cmd) - defined.add(cmd.signatureAsString()) - }) - - // Insert unimathsymbols - if (configuration.get('intellisense.unimathsymbols.enabled')) { - this.defaultSymbols.forEach(symbol => { - suggestions.push(symbol) - defined.add(symbol.signatureAsString()) - }) + } else if (node.type === 'macro' && + ['DeclarePairedDelimiter', 'DeclarePairedDelimiterX', 'DeclarePairedDelimiterXPP'].includes(node.content) && + node.args?.[0]?.content?.[0]?.type === 'macro') { + // \DeclarePairedDelimiterX\braketzw[2]{\langle}{\rangle}{#1\,\delimsize\vert\,\mathopen{}#2} + found = true + name = node.args[0].content[0].content + if (['DeclarePairedDelimiterX', 'DeclarePairedDelimiterXPP'].includes(node.content) && + node.args?.[1].content?.[0]?.type === 'string' && + parseInt(node.args?.[1].content?.[0].content) > 0) { + args = (node.args?.[2].openMark === '[' ? '[]' : '{}') + '{}'.repeat(parseInt(node.args?.[1].content?.[0].content) - 1) } - - // Insert commands from packages - if ((configuration.get('intellisense.package.enabled'))) { - const packages = lw.completer.package.getPackagesIncluded(langId) - Object.entries(packages).forEach(([packageName, options]) => { - this.provideCmdInPkg(packageName, options, suggestions) - lw.completer.environment.provideEnvsAsCommandInPkg(packageName, options, suggestions, defined) - }) + } else if (node.type === 'macro' && + ['providecommand', 'DeclareMathOperator', 'DeclareRobustCommand'].includes(node.content) && + node.args?.[1]?.content?.[0]?.type === 'macro') { + found = true + name = node.args[1].content[0].content + if (node.args?.[2].content?.[0]?.type === 'string' && + parseInt(node.args?.[2].content?.[0].content) > 0) { + args = (node.args?.[3].openMark === '[' ? '[]' : '{}') + '{}'.repeat(parseInt(node.args?.[2].content?.[0].content) - 1) } - - // 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 - defined = new Set(suggestions.map(s => s.signatureAsString())) - lw.cache.getIncludedTeX().forEach(tex => { - const cmds = lw.cache.get(tex)?.elements.command - if (cmds !== undefined) { - cmds.forEach(cmd => { - if (!defined.has(cmd.signatureAsString())) { - cmd.range = range - suggestions.push(cmd) - defined.add(cmd.signatureAsString()) - } - }) - } - }) - - filterArgumentHint(suggestions) - - return suggestions } - /** - * Surrounds `content` with a command picked in QuickPick. - * - * @param content A string to be surrounded. If not provided, then we loop over all the selections and surround each of them. - */ - surround() { - if (!vscode.window.activeTextEditor) { - return + if (found && !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 + 1) + '}]') + index++ + } + while (argTabs.includes('{}')) { + argTabs = argTabs.replace('{}', '{${' + (index + 1) + '}}') + index++ } - const editor = vscode.window.activeTextEditor - const cmdItems = this.provide(editor.document.languageId) - SurroundCommand.surround(cmdItems) + 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) + data.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()) } - parse(cache: FileCache) { - // Remove newcommand cmds, because they will be re-insert in the next step - this.definedCmds.forEach((entry,cmd) => { - if (entry.filePath === cache.filePath) { - this.definedCmds.delete(cmd) - } - }) - if (cache.ast !== undefined) { - cache.elements.command = this.parseAst(cache.ast, cache.filePath) - } else { - cache.elements.command = this.parseContent(cache.content, cache.filePath) + if ('content' in node && typeof node.content !== 'string') { + for (const subNode of node.content) { + cmds = [...cmds, ...parseAst(subNode, filePath, defined)] } } - private parseAst(node: Ast.Node, filePath: string, defined?: Set): CmdEnvSuggestion[] { - defined = defined ?? new Set() - let cmds: CmdEnvSuggestion[] = [] - let found = false - let name = '' - let args = '' - if (node.type === 'macro' && - ['renewcommand', 'newcommand'].includes(node.content) && - node.args?.[2]?.content?.[0]?.type === 'macro') { - // \newcommand{\fix}[3][]{\chdeleted{#2}\chadded[comment={#1}]{#3}} - // \newcommand\WARNING{\textcolor{red}{WARNING}} - found = true - name = node.args[2].content[0].content - 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) - } - } else if (node.type === 'macro' && - ['DeclarePairedDelimiter', 'DeclarePairedDelimiterX', 'DeclarePairedDelimiterXPP'].includes(node.content) && - node.args?.[0]?.content?.[0]?.type === 'macro') { - // \DeclarePairedDelimiterX\braketzw[2]{\langle}{\rangle}{#1\,\delimsize\vert\,\mathopen{}#2} - found = true - name = node.args[0].content[0].content - if (['DeclarePairedDelimiterX', 'DeclarePairedDelimiterXPP'].includes(node.content) && - node.args?.[1].content?.[0]?.type === 'string' && - parseInt(node.args?.[1].content?.[0].content) > 0) { - args = (node.args?.[2].openMark === '[' ? '[]' : '{}') + '{}'.repeat(parseInt(node.args?.[1].content?.[0].content) - 1) - } - } else if (node.type === 'macro' && - ['providecommand', 'DeclareMathOperator', 'DeclareRobustCommand'].includes(node.content) && - node.args?.[1]?.content?.[0]?.type === 'macro') { - found = true - name = node.args[1].content[0].content - if (node.args?.[2].content?.[0]?.type === 'string' && - parseInt(node.args?.[2].content?.[0].content) > 0) { - args = (node.args?.[3].openMark === '[' ? '[]' : '{}') + '{}'.repeat(parseInt(node.args?.[2].content?.[0].content) - 1) - } + return cmds +} + +function parseContent(content: string, filePath: string): CmdEnvSuggestion[] { + const cmdInPkg: CmdEnvSuggestion[] = [] + const packages = lw.completer.package.getPackagesIncluded('latex-expl3') + Object.entries(packages).forEach(([packageName, options]) => { + provideCmdInPkg(packageName, options, 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 (found && !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 + 1) + '}]') - index++ - } - while (argTabs.includes('{}')) { - argTabs = argTabs.replace('{}', '{${' + (index + 1) + '}}') - index++ - } - cmd.insertText = new vscode.SnippetString(name + argTabs) - cmd.filterText = name - if (isTriggerSuggestNeeded(name)) { - cmd.command = { title: 'Post-Action', command: 'editor.action.triggerSuggest' } + + 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) - 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) - }) - 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 - } - + 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 + } - 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()) + let tabStops = '' + let args = '' + if (result[2]) { + const numArgs = parseInt(result[2]) + for (let i = 1; i <= numArgs; ++i) { + tabStops += '{${' + i + '}}' + args += '{}' } } - 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 - } + 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()) + } - let tabStops = '' - let args = '' - if (result[2]) { - const numArgs = parseInt(result[2]) - for (let i = 1; i <= numArgs; ++i) { - tabStops += '{${' + i + '}}' - args += '{}' - } - } + data.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)) + }) + } - 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()) - } + return cmds +} - 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)) - }) +function entryCmdToCompletion(itemKey: string, item: Macro): CmdEnvSuggestion { + item.macro = item.macro || itemKey + const backslash = item.macro.startsWith(' ') ? '' : '\\' + const suggestion = new CmdEnvSuggestion( + `${backslash}${item.macro}`, + item.package || 'latex', + item.keyvals && typeof(item.keyvals) !== 'number' ? item.keyvals : [], + item.keyvalpos === undefined ? -1 : item.keyvalpos, + splitSignatureString(itemKey), + vscode.CompletionItemKind.Function, + item.option) + + if (item.snippet) { + // Wrap the selected text when there is a single placeholder + if (! (item.snippet.match(/\$\{?2/) || (item.snippet.match(/\$\{?0/) && item.snippet.match(/\$\{?1/)))) { + item.snippet = item.snippet.replace(/\$1|\$\{1\}/, '$${1:$${TM_SELECTED_TEXT}}').replace(/\$\{1:([^$}]+)\}/, '$${1:$${TM_SELECTED_TEXT:$1}}') } - - return cmds + suggestion.insertText = new vscode.SnippetString(item.snippet) + } else { + suggestion.insertText = item.macro + } + suggestion.filterText = itemKey + suggestion.detail = item.detail || `\\${item.snippet?.replace(/\$\{\d+:([^$}]*)\}/g, '$1')}` + suggestion.documentation = item.documentation ? item.documentation : `Command \\${item.macro}.` + if (item.package) { + suggestion.documentation += ` From package: ${item.package}.` + } + suggestion.sortText = item.macro.replace(/^[a-zA-Z]/, c => { + const n = c.match(/[a-z]/) ? c.toUpperCase().charCodeAt(0): c.toLowerCase().charCodeAt(0) + return n !== undefined ? n.toString(16): c + }) + if (item.postAction) { + suggestion.command = { title: 'Post-Action', command: item.postAction } + } else if (isTriggerSuggestNeeded(item.macro)) { + // Automatically trigger completion if the command is for citation, filename, reference or glossary + suggestion.command = { title: 'Post-Action', command: 'editor.action.triggerSuggest' } } + return suggestion +} - private entryCmdToCompletion(itemKey: string, item: CmdType): CmdEnvSuggestion { - item.command = item.command || itemKey - const backslash = item.command.startsWith(' ') ? '' : '\\' - const suggestion = new CmdEnvSuggestion( - `${backslash}${item.command}`, - item.package || 'latex', - item.keyvals && typeof(item.keyvals) !== 'number' ? item.keyvals : [], - item.keyvalpos === undefined ? -1 : item.keyvalpos, - splitSignatureString(itemKey), - vscode.CompletionItemKind.Function, - item.option) - - if (item.snippet) { - // Wrap the selected text when there is a single placeholder - if (! (item.snippet.match(/\$\{?2/) || (item.snippet.match(/\$\{?0/) && item.snippet.match(/\$\{?1/)))) { - item.snippet = item.snippet.replace(/\$1|\$\{1\}/, '$${1:$${TM_SELECTED_TEXT}}').replace(/\$\{1:([^$}]+)\}/, '$${1:$${TM_SELECTED_TEXT:$1}}') - } - suggestion.insertText = new vscode.SnippetString(item.snippet) +function setPackageCmds(packageName: string, cmds: {[key: string]: Macro}) { + const commands: CmdEnvSuggestion[] = [] + Object.entries(cmds).forEach(([key, cmd]) => { + cmd.package = packageName + if (isCmdWithSnippet(cmd)) { + commands.push(entryCmdToCompletion(key, cmd)) } else { - suggestion.insertText = item.command + logger.log(`Cannot parse intellisense file for ${packageName}.`) + logger.log(`Missing field in entry: "${key}": ${JSON.stringify(cmd)}.`) } - suggestion.filterText = itemKey - suggestion.detail = item.detail || `\\${item.snippet?.replace(/\$\{\d+:([^$}]*)\}/g, '$1')}` - suggestion.documentation = item.documentation ? item.documentation : `Command \\${item.command}.` - if (item.package) { - suggestion.documentation += ` From package: ${item.package}.` - } - suggestion.sortText = item.command.replace(/^[a-zA-Z]/, c => { - const n = c.match(/[a-z]/) ? c.toUpperCase().charCodeAt(0): c.toLowerCase().charCodeAt(0) - return n !== undefined ? n.toString(16): c - }) - if (item.postAction) { - suggestion.command = { title: 'Post-Action', command: item.postAction } - } else if (isTriggerSuggestNeeded(item.command)) { - // Automatically trigger completion if the command is for citation, filename, reference or glossary - suggestion.command = { title: 'Post-Action', command: 'editor.action.triggerSuggest' } - } - return suggestion - } + }) + data.packageCmds.set(packageName, commands) +} - setPackageCmds(packageName: string, cmds: {[key: string]: CmdType}) { - const commands: CmdEnvSuggestion[] = [] - Object.entries(cmds).forEach(([key, cmd]) => { - cmd.package = packageName - if (isCmdWithSnippet(cmd)) { - commands.push(this.entryCmdToCompletion(key, cmd)) - } else { - logger.log(`Cannot parse intellisense file for ${packageName}.`) - logger.log(`Missing field in entry: "${key}": ${JSON.stringify(cmd)}.`) - } - }) - this.packageCmds.set(packageName, commands) - } +function getPackageCmds(packageName: string) { + return data.packageCmds.get(packageName) || [] +} - getPackageCmds(packageName: string) { - return this.packageCmds.get(packageName) || [] +function provideCmdInPkg(packageName: string, options: string[], suggestions: CmdEnvSuggestion[]) { + const defined = new Set() + const configuration = vscode.workspace.getConfiguration('latex-workshop') + const useOptionalArgsEntries = configuration.get('intellisense.optionalArgsEntries.enabled') + // Load command in pkg + lw.completer.loadPackageData(packageName) + + // No package command defined + const pkgCmds = data.packageCmds.get(packageName) + if (!pkgCmds || pkgCmds.length === 0) { + return } - provideCmdInPkg(packageName: string, options: string[], suggestions: CmdEnvSuggestion[]) { - const defined = new Set() - const configuration = vscode.workspace.getConfiguration('latex-workshop') - const useOptionalArgsEntries = configuration.get('intellisense.optionalArgsEntries.enabled') - // Load command in pkg - lw.completer.loadPackageData(packageName) - - // No package command defined - const pkgCmds = this.packageCmds.get(packageName) - if (!pkgCmds || pkgCmds.length === 0) { + // Insert commands + pkgCmds.forEach(cmd => { + if (!useOptionalArgsEntries && cmd.hasOptionalArgs()) { return } - - // Insert commands - pkgCmds.forEach(cmd => { - if (!useOptionalArgsEntries && cmd.hasOptionalArgs()) { + if (!defined.has(cmd.signatureAsString())) { + if (cmd.option && options && !options.includes(cmd.option)) { return } - if (!defined.has(cmd.signatureAsString())) { - if (cmd.option && options && !options.includes(cmd.option)) { - return - } - suggestions.push(cmd) - defined.add(cmd.signatureAsString()) - } - }) - } - + suggestions.push(cmd) + defined.add(cmd.signatureAsString()) + } + }) } diff --git a/src/completion/completer/package.ts b/src/completion/completer/package.ts index bc02f061f9..6cfc824d7c 100644 --- a/src/completion/completer/package.ts +++ b/src/completion/completer/package.ts @@ -2,8 +2,7 @@ import * as vscode from 'vscode' import * as fs from 'fs' import type * as Ast from '@unified-latex/unified-latex-types' import { lw } from '../../lw' -import { FileCache } from '../../types' -import type { IProvider } from '../latex' +import type { CompletionProvider, FileCache } from '../../types' import { argContentToStr } from '../../utils/parser' type DataPackagesJsonType = typeof import('../../../data/packagenames.json') @@ -14,7 +13,7 @@ type PackageItemEntry = { documentation: string } -export class Package implements IProvider { +export class Package implements CompletionProvider { private readonly suggestions: vscode.CompletionItem[] = [] private readonly packageDeps: {[packageName: string]: {[key: string]: string[]}} = {} private readonly packageOptions: {[packageName: string]: string[]} = {} @@ -28,7 +27,7 @@ export class Package implements IProvider { }) } - provideFrom() { + from() { return this.provide() } diff --git a/src/completion/completer/reference.ts b/src/completion/completer/reference.ts index 0388273812..621273b1b4 100644 --- a/src/completion/completer/reference.ts +++ b/src/completion/completer/reference.ts @@ -3,13 +3,12 @@ import * as fs from 'fs' import * as path from 'path' import type * as Ast from '@unified-latex/unified-latex-types' import { lw } from '../../lw' -import type { FileCache } from '../../types' +import type { CompletionArgs, CompletionItem, CompletionProvider, FileCache } from '../../types' import { getLongestBalancedString, stripEnvironments } from '../../utils/utils' import { computeFilteringRange } from './completerutils' -import type { IProvider, ICompletionItem, IProviderArgs } from '../latex' import { argContentToStr } from '../../utils/parser' -export interface ReferenceEntry extends ICompletionItem { +export interface ReferenceEntry extends CompletionItem { /** The file that defines the ref. */ file: string, /** The position that defines the ref. */ @@ -27,12 +26,12 @@ export type ReferenceDocType = { prevIndex: ReferenceEntry['prevIndex'] } -export class Reference implements IProvider { +export class Reference implements CompletionProvider { // Here we use an object instead of an array for de-duplication private readonly suggestions = new Map() private prevIndexObj = new Map() - provideFrom(_result: RegExpMatchArray, args: IProviderArgs) { + from(_result: RegExpMatchArray, args: CompletionArgs) { return this.provide(args.line, args.position) } @@ -161,8 +160,8 @@ export class Reference implements IProvider { } } - private parseAst(node: Ast.Node, lines: string[], labelMacros: string[]): ICompletionItem[] { - let refs: ICompletionItem[] = [] + private parseAst(node: Ast.Node, lines: string[], labelMacros: string[]): CompletionItem[] { + let refs: CompletionItem[] = [] if (node.type === 'macro' && ['renewcommand', 'newcommand', 'providecommand', 'DeclareMathOperator', 'renewenvironment', 'newenvironment'].includes(node.content)) { // Do not scan labels inside \newcommand, \newenvironment & co @@ -218,9 +217,9 @@ export class Reference implements IProvider { return refs } - private parseContent(content: string): ICompletionItem[] { + private parseContent(content: string): CompletionItem[] { const refReg = /(?:\\label(?:\[[^[\]{}]*\])?|(?:^|[,\s])label=){([^#\\}]*)}/gm - const refs: ICompletionItem[] = [] + const refs: CompletionItem[] = [] const refList: string[] = [] content = stripEnvironments(content, ['']) while (true) { diff --git a/src/completion/index.ts b/src/completion/index.ts new file mode 100644 index 0000000000..a746ba4237 --- /dev/null +++ b/src/completion/index.ts @@ -0,0 +1,9 @@ +import { citation } from './completer/citation' +import { environment } from './completer/environment' +import { macro } from './completer/macro' + +export const completion = { + citation, + environment, + macro +} diff --git a/src/completion/latex.ts b/src/completion/latex.ts index afe26a977f..d12a39ef59 100644 --- a/src/completion/latex.ts +++ b/src/completion/latex.ts @@ -2,55 +2,26 @@ import * as vscode from 'vscode' import * as fs from 'fs' import * as path from 'path' import { lw } from '../lw' -import { Citation } from './completer/citation' +import { CompletionArgs, CompletionProvider, Package } from '../types' +import { citation, provider as citationProvider } from './completer/citation' import { DocumentClass } from './completer/documentclass' -import { Command } from './completer/command' -import type { CmdType } from './completer/command' -import { Environment } from './completer/environment' -import type { EnvType } from './completer/environment' -import { Argument } from './completer/argument' +import { environment, provider as environmentProvider } from './completer/environment' +import { macro, provider as macroProvider } from './completer/macro' +import { provider as argumentProvider } from './completer/argument' import { AtSuggestion } from './completer/atsuggestion' import { Reference } from './completer/reference' -import { Package } from './completer/package' +import { Package as PackageCompletion } from './completer/package' import { Input, Import, SubImport } from './completer/input' import { Glossary } from './completer/glossary' import type { ReferenceDocType } from './completer/reference' import { escapeRegExp } from '../utils/utils' - const logger = lw.log('Intelli') -export type PkgType = {includes: {[key: string]: string[]}, cmds: {[key: string]: CmdType}, envs: {[key: string]: EnvType}, options: string[], keyvals: string[][]} - -export type IProviderArgs = { - uri: vscode.Uri, - langId: string, - line: string, - position: vscode.Position -} - -export interface IProvider { - /** - * Returns the array of completion items. Should be called only from `Completer.completion`. - */ - provideFrom( - result: RegExpMatchArray, - args: IProviderArgs - ): vscode.CompletionItem[] -} - -export interface ICompletionItem extends vscode.CompletionItem { - label: string -} - export class Completer implements vscode.CompletionItemProvider { - readonly citation: Citation - readonly command: Command readonly documentClass: DocumentClass - readonly environment: Environment - readonly argument: Argument readonly reference: Reference - readonly package: Package + readonly package: PackageCompletion readonly input: Input readonly import: Import readonly subImport: SubImport @@ -59,23 +30,13 @@ export class Completer implements vscode.CompletionItemProvider { private readonly packagesLoaded: string[] = [] constructor() { - this.citation = new Citation() - this.environment = new Environment() // Must be created before command - this.command = new Command() - this.argument = new Argument() this.documentClass = new DocumentClass() this.reference = new Reference() - this.package = new Package() + this.package = new PackageCompletion() this.input = new Input() this.import = new Import() this.subImport = new SubImport() this.glossary = new Glossary() - try { - const environment = this.environment.initialize() - this.command.initialize(environment) - } catch (err) { - logger.log(`Error reading data: ${err}.`) - } } loadPackageData(packageName: string) { @@ -90,12 +51,12 @@ export class Completer implements vscode.CompletionItemProvider { } try { - const packageData = JSON.parse(fs.readFileSync(filePath).toString()) as PkgType + const packageData = JSON.parse(fs.readFileSync(filePath).toString()) as Package this.populatePackageData(packageData) this.package.setPackageDeps(packageName, packageData.includes) - this.command.setPackageCmds(packageName, packageData.cmds) - this.environment.setPackageEnvs(packageName, packageData.envs) + macro.setPackageCmds(packageName, packageData.macros) + environment.setPackageEnvs(packageName, packageData.envs) this.package.setPackageOptions(packageName, packageData.options) this.packagesLoaded.push(packageName) @@ -127,9 +88,9 @@ export class Completer implements vscode.CompletionItemProvider { return } - private populatePackageData(packageData: PkgType) { - Object.entries(packageData.cmds).forEach(([key, cmd]) => { - cmd.command = key + private populatePackageData(packageData: Package) { + Object.entries(packageData.macros).forEach(([key, cmd]) => { + cmd.macro = key cmd.snippet = cmd.snippet || key cmd.keyvals = packageData.keyvals[cmd.keyvalindex ?? -1] }) @@ -157,7 +118,7 @@ export class Completer implements vscode.CompletionItemProvider { }) } - provide(args: IProviderArgs): vscode.CompletionItem[] { + provide(args: CompletionArgs): 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']) { @@ -166,7 +127,7 @@ export class Completer implements vscode.CompletionItemProvider { if (type === 'citation') { const configuration = vscode.workspace.getConfiguration('latex-workshop') if (configuration.get('intellisense.citation.type') as string === 'browser') { - setTimeout(() => this.citation.browser(args), 10) + setTimeout(() => citation.browser(args), 10) return [] } } @@ -221,13 +182,13 @@ export class Completer implements vscode.CompletionItemProvider { } } - completion(type: string, args: IProviderArgs): vscode.CompletionItem[] { + completion(type: string, args: CompletionArgs): vscode.CompletionItem[] { let reg: RegExp | undefined - let provider: IProvider | undefined + let provider: CompletionProvider | undefined switch (type) { case 'citation': reg = /(?:\\[a-zA-Z]*[Cc]ite[a-zA-Z]*\*?(?:\([^[)]*\)){0,2}(?:<[^<>]*>|\[[^[\]]*\]|{[^{}]*})*{([^}]*)$)|(?:\\bibentry{([^}]*)$)/ - provider = this.citation + provider = citationProvider break case 'reference': reg = /(?:\\hyperref\[([^\]]*)(?!\])$)|(?:(?:\\(?!hyper)[a-zA-Z]*ref[a-zA-Z]*\*?(?:\[[^[\]]*\])?){([^}]*)$)|(?:\\[Cc][a-z]*refrange\*?{[^{}]*}{([^}]*)$)/ @@ -235,15 +196,15 @@ export class Completer implements vscode.CompletionItemProvider { break case 'environment': reg = /(?:\\begin|\\end){([^}]*)$/ - provider = this.environment + provider = environmentProvider break case 'command': reg = args.langId === 'latex-expl3' ? /\\([a-zA-Z_@]*(?::[a-zA-Z]*)?)$/ : /\\(\+?[a-zA-Z]*|(?:left|[Bb]ig{1,2}l)?[({[]?)$/ - provider = this.command + provider = macroProvider break case 'argument': reg = args.langId === 'latex-expl3' ? /\\([a-zA-Z_@]*(?::[a-zA-Z]*)?)((?:\[.*?\]|{.*?})*)[[{][^[\]{}]*$/ : /\\(\+?[a-zA-Z]*)((?:\[.*?\]|{.*?})*)[[{][^[\]{}]*$/ - provider = this.argument + provider = argumentProvider break case 'package': reg = /(?:\\usepackage(?:\[[^[\]]*\])*){([^}]*)$/ @@ -285,7 +246,7 @@ export class Completer implements vscode.CompletionItemProvider { const result = lineToPos.match(reg) let suggestions: vscode.CompletionItem[] = [] if (result) { - suggestions = provider.provideFrom(result, args) + suggestions = provider.from(result, args) } return suggestions } @@ -319,13 +280,13 @@ export class AtSuggestionCompleter implements vscode.CompletionItemProvider { }) } - provide(args: IProviderArgs): vscode.CompletionItem[] { + provide(args: CompletionArgs): vscode.CompletionItem[] { const escapedTriggerCharacter = escapeRegExp(this.triggerCharacter) const reg = new RegExp(escapedTriggerCharacter + '[^\\\\s]*$') const result = args.line.substring(0, args.position.character).match(reg) let suggestions: vscode.CompletionItem[] = [] if (result) { - suggestions = this.atSuggestion.provideFrom(result, args) + suggestions = this.atSuggestion.from(result, args) } return suggestions } diff --git a/src/core/cache.ts b/src/core/cache.ts index 173d23a4f2..c00e253437 100644 --- a/src/core/cache.ts +++ b/src/core/cache.ts @@ -361,13 +361,13 @@ function updateChildrenXr(fileCache: FileCache, rootPath: string) { */ function updateElements(fileCache: FileCache) { const start = performance.now() - lw.completer.citation.parse(fileCache) + lw.completion.citation.parse(fileCache) // Package parsing must be before command and environment. lw.completer.package.parse(fileCache) lw.completer.reference.parse(fileCache) lw.completer.glossary.parse(fileCache) - lw.completer.environment.parse(fileCache) - lw.completer.command.parse(fileCache) + lw.completion.environment.parse(fileCache) + lw.completion.macro.parse(fileCache) lw.completer.input.parseGraphicsPath(fileCache) updateBibfiles(fileCache) const elapsed = performance.now() - start diff --git a/src/core/commands.ts b/src/core/commands.ts index 194cf7cc6b..717aa7ecdd 100644 --- a/src/core/commands.ts +++ b/src/core/commands.ts @@ -140,7 +140,7 @@ export function addTexRoot() { export function citation() { logger.log('CITATION command invoked.') - lw.completer.citation.browser() + lw.completion.citation.browser() } export function wordcount() { diff --git a/src/language/definition.ts b/src/language/definition.ts index 273158b007..876767b8e1 100644 --- a/src/language/definition.ts +++ b/src/language/definition.ts @@ -50,7 +50,7 @@ export class DefinitionProvider implements vscode.DefinitionProvider { } if (token.startsWith('\\')) { - const command = lw.completer.command.definedCmds.get(token.slice(1)) + const command = lw.completion.macro.getData().definedCmds.get(token.slice(1)) if (command) { return command.location } @@ -60,7 +60,7 @@ export class DefinitionProvider implements vscode.DefinitionProvider { if (ref) { return new vscode.Location(vscode.Uri.file(ref.file), ref.position) } - const cite = lw.completer.citation.getEntry(token) + const cite = lw.completion.citation.getItem(token) if (cite) { return new vscode.Location(vscode.Uri.file(cite.file), cite.position) } diff --git a/src/lw.ts b/src/lw.ts index bb95499624..88a2ceb629 100644 --- a/src/lw.ts +++ b/src/lw.ts @@ -8,6 +8,7 @@ import type { root } from './core/root' import type { compile } from './compile' import type { preview, server, viewer } from './preview' import type { locate } from './locate' +import type { completion } from './completion' import type { lint } from './lint' import type { outline } from './outline' import type { parse } from './parse' @@ -32,6 +33,7 @@ export const lw = { server: {} as typeof server, preview: {} as typeof preview, locate: {} as typeof locate, + completion: {} as typeof completion, completer: Object.create(null) as Completer, atSuggestionCompleter: Object.create(null) as AtSuggestionCompleter, lint: {} as typeof lint, diff --git a/src/main.ts b/src/main.ts index d4749125c9..6e24673cbd 100644 --- a/src/main.ts +++ b/src/main.ts @@ -25,6 +25,8 @@ lw.viewer = viewer lw.preview = preview import { locate } from './locate' lw.locate = locate +import { completion } from './completion' +lw.completion = completion import { lint } from './lint' lw.lint = lint import { outline } from './outline' @@ -229,7 +231,7 @@ function registerLatexWorkshopCommands(extensionContext: vscode.ExtensionContext vscode.commands.registerCommand('latex-workshop.shortcut.mathsf', () => lw.commands.toggleSelectedKeyword('mathsf')), vscode.commands.registerCommand('latex-workshop.shortcut.mathbb', () => lw.commands.toggleSelectedKeyword('mathbb')), vscode.commands.registerCommand('latex-workshop.shortcut.mathcal', () => lw.commands.toggleSelectedKeyword('mathcal')), - vscode.commands.registerCommand('latex-workshop.surround', () => lw.completer.command.surround()), + vscode.commands.registerCommand('latex-workshop.surround', () => lw.completion.macro.surround()), vscode.commands.registerCommand('latex-workshop.promote-sectioning', () => lw.commands.shiftSectioningLevel('promote')), vscode.commands.registerCommand('latex-workshop.demote-sectioning', () => lw.commands.shiftSectioningLevel('demote')), diff --git a/src/parse/parser/biberlog.ts b/src/parse/parser/biberlog.ts index 7f0d05cb3e..c3c953322b 100644 --- a/src/parse/parser/biberlog.ts +++ b/src/parse/parser/biberlog.ts @@ -110,7 +110,7 @@ function resolveBibFile(filename: string, rootFile: string): string { } function findKeyLocation(key: string): {file: string, line: number} | undefined { - const entry = lw.completer.citation.getEntry(key) + const entry = lw.completion.citation.getItem(key) if (entry) { const file = entry.file const line = entry.position.line + 1 diff --git a/src/parse/parser/bibtexlog.ts b/src/parse/parser/bibtexlog.ts index 22748bafa9..5cbbe578cf 100644 --- a/src/parse/parser/bibtexlog.ts +++ b/src/parse/parser/bibtexlog.ts @@ -114,7 +114,7 @@ function resolveBibFile(filename: string, rootFile: string): string { } function findKeyLocation(key: string): {file: string, line: number} | undefined { - const entry = lw.completer.citation.getEntry(key) + const entry = lw.completion.citation.getItem(key) if (entry) { const file = entry.file const line = entry.position.line + 1 diff --git a/src/preview/hover.ts b/src/preview/hover.ts index ff923cc764..027c382698 100644 --- a/src/preview/hover.ts +++ b/src/preview/hover.ts @@ -53,7 +53,7 @@ class HoverProvider implements vscode.HoverProvider { const hover = await lw.preview.math.onRef(document, position, refData, token, ctoken) return hover } - const cite = lw.completer.citation.getEntryWithDocumentation(token, document.uri) + const cite = lw.completion.citation.getItem(token, document.uri) if (hovCitation && cite) { const range = document.getWordRangeAtPosition(position, /\{.*?\}/) const md = cite.documentation || cite.detail @@ -74,8 +74,8 @@ class HoverProvider implements vscode.HoverProvider { if ((configuration.get('intellisense.package.enabled'))) { const packages = lw.completer.package.getPackagesIncluded('latex-expl3') Object.entries(packages).forEach(([packageName, options]) => { - lw.completer.command.provideCmdInPkg(packageName, options, packageCmds) - lw.completer.environment.provideEnvsAsCommandInPkg(packageName, options, packageCmds) + lw.completion.macro.provideCmdInPkg(packageName, options, packageCmds) + lw.completion.environment.provideEnvsAsCommandInPkg(packageName, options, packageCmds) }) } diff --git a/src/types.ts b/src/types.ts index 4c4a325eda..c3f0b475a3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,9 +1,7 @@ import type * as vscode from 'vscode' import type * as Ast from '@unified-latex/unified-latex-types' import type { CmdEnvSuggestion } from './completion/completer/completerutils' -import type { CiteSuggestion } from './completion/completer/citation' import type { GlossarySuggestion } from './completion/completer/glossary' -import type { ICompletionItem } from './completion/latex' export type FileCache = { /** The raw file path of this Cache. */ @@ -15,13 +13,13 @@ export type FileCache = { /** Completion items */ elements: { /** \ref{} items */ - reference?: ICompletionItem[], + reference?: CompletionItem[], /** \gls items */ glossary?: GlossarySuggestion[], /** \begin{} items */ environment?: CmdEnvSuggestion[], /** \cite{} items from \bibitem definition */ - bibitem?: CiteSuggestion[], + bibitem?: CitationItem[], /** command items */ command?: CmdEnvSuggestion[], /** \usepackage{}, a dictionary whose key is package name and value is the options */ @@ -129,4 +127,88 @@ export type TeXElement = { appendix?: boolean } -export type TeXMathEnv = { texString: string, range: vscode.Range, envname: string } +export type TeXMathEnv = { + texString: string, + range: vscode.Range, + envname: string +} + +export type Package = { + includes: {[key: string]: string[]}, + macros: {[key: string]: Macro}, + envs: {[key: string]: Environment}, + options: string[], + keyvals: string[][] +} + +export type CompletionArgs = { + uri: vscode.Uri, + langId: string, + line: string, + position: vscode.Position +} + +export interface CompletionProvider { + from(result: RegExpMatchArray, args: CompletionArgs): vscode.CompletionItem[] +} + +export interface CompletionItem extends vscode.CompletionItem { + label: string +} + +export interface CitationField extends Map { + author?: string, + journal?: string, + journaltitle?: string, + title?: string, + publisher?: string, + join(selectedFields: string[], prefixWithKeys: boolean, joinString?: string): string +} + +export interface CitationItem extends CompletionItem { + key: string, + fields: CitationField, + file: string, + position: vscode.Position +} + +export enum EnvSnippetType { AsName, AsCommand, ForBegin } + +export type Environment = { + /** Name of the environment, what comes inside \begin{...} */ + name: string, + /** To be inserted after \begin{..} */ + snippet?: string, + /** The option of package below that activates this env */ + option?: string, + /** Possible options of this env */ + keyvals?: string[], + /** The index of keyval list in package .json file. Should not be used */ + keyvalindex?: number, + /** The index of argument which have the keyvals */ + keyvalpos?: number, + /** The package providing the environment */ + package?: string, + detail?: string +} + +export type Macro = { + /** Name of the macro without the leading \ and with argument signature */ + macro?: string, + /** Snippet to be inserted after the leading \ */ + snippet?: string, + /** The option of package below that activates this cmd */ + option?: string, + /** Possible options of this env */ + keyvals?: string[], + /** The index of keyval list in package .json file. Should not be used */ + keyvalindex?: number, + /** The index of argument which have the keyvals */ + keyvalpos?: number, + detail?: string, + documentation?: string, + /** The package providing the environment */ + package?: string, + /** The action to be executed after inserting the snippet */ + postAction?: string +} diff --git a/test/suites/04_intellisense.test.ts b/test/suites/04_intellisense.test.ts index be4b2fc380..7901168dfc 100644 --- a/test/suites/04_intellisense.test.ts +++ b/test/suites/04_intellisense.test.ts @@ -5,10 +5,8 @@ import * as assert from 'assert' import { glob } from 'glob' import { lw } from '../../src/lw' import * as test from './utils' -import { EnvSnippetType, EnvType } from '../../src/completion/completer/environment' -import { CmdType } from '../../src/completion/completer/command' -import { PkgType } from '../../src/completion/latex' -import { isTriggerSuggestNeeded } from '../../src/completion/completer/command' +import { EnvSnippetType, Environment, Macro, Package } from '../../src/types' +import { isTriggerSuggestNeeded } from '../../src/completion/completer/macro' function assertKeys(keys: string[], expected: string[] = [], message: string): void { assert.ok( @@ -41,7 +39,7 @@ suite('Intellisense test suite', () => { test.run('check default environment .json completion file', () => { const file = `${lw.extensionRoot}/data/environments.json` - const envs = JSON.parse(fs.readFileSync(file, {encoding: 'utf8'})) as {[key: string]: EnvType} + const envs = JSON.parse(fs.readFileSync(file, {encoding: 'utf8'})) as {[key: string]: Environment} assert.ok(Object.keys(envs).length > 0) Object.values(envs).forEach(env => { assertKeys( @@ -54,7 +52,7 @@ suite('Intellisense test suite', () => { test.run('check default commands .json completion file', () => { const file = `${lw.extensionRoot}/data/commands.json` - const cmds = JSON.parse(fs.readFileSync(file, {encoding: 'utf8'})) as {[key: string]: CmdType} + const cmds = JSON.parse(fs.readFileSync(file, {encoding: 'utf8'})) as {[key: string]: Macro} assert.ok(Object.keys(cmds).length > 0) Object.values(cmds).forEach(cmd => { assertKeys( @@ -66,19 +64,19 @@ suite('Intellisense test suite', () => { }) test.run('test default envs', () => { - let defaultEnvs = lw.completer.environment.getDefaultEnvs(EnvSnippetType.AsCommand).map(e => e.label) + let defaultEnvs = lw.completion.environment.getDefaultEnvs(EnvSnippetType.AsCommand).map(e => e.label) assert.ok(defaultEnvs.includes('document')) assert.ok(defaultEnvs.includes('align')) - defaultEnvs = lw.completer.environment.getDefaultEnvs(EnvSnippetType.AsName).map(e => e.label) + defaultEnvs = lw.completion.environment.getDefaultEnvs(EnvSnippetType.AsName).map(e => e.label) assert.ok(defaultEnvs.includes('document')) assert.ok(defaultEnvs.includes('align')) - defaultEnvs = lw.completer.environment.getDefaultEnvs(EnvSnippetType.ForBegin).map(e => e.label) + defaultEnvs = lw.completion.environment.getDefaultEnvs(EnvSnippetType.ForBegin).map(e => e.label) assert.ok(defaultEnvs.includes('document')) assert.ok(defaultEnvs.includes('align')) }) test.run('test default cmds', () => { - const defaultCommands = lw.completer.command.defaultCmds.map(e => e.label) + const defaultCommands = lw.completion.macro.getData().defaultCmds.map(e => e.label) assert.ok(defaultCommands.includes('\\begin')) assert.ok(defaultCommands.includes('\\left(')) assert.ok(defaultCommands.includes('\\section{}')) @@ -87,8 +85,8 @@ suite('Intellisense test suite', () => { test.run('check package .json completion file', () => { const files = glob.sync('data/packages/*.json', {cwd: lw.extensionRoot}) files.forEach(file => { - const pkg = JSON.parse(fs.readFileSync(path.join(lw.extensionRoot, file), {encoding: 'utf8'})) as PkgType - Object.values(pkg.cmds).forEach(cmd => { + const pkg = JSON.parse(fs.readFileSync(path.join(lw.extensionRoot, file), {encoding: 'utf8'})) as Package + Object.values(pkg.macros).forEach(cmd => { assertKeys( Object.keys(cmd), ['command', 'snippet', 'option', 'keyvalindex', 'keyvalpos', 'documentation', 'detail'], diff --git a/test/suites/11_snippet.test.ts b/test/suites/11_snippet.test.ts index 3f90144df2..94980f21eb 100644 --- a/test/suites/11_snippet.test.ts +++ b/test/suites/11_snippet.test.ts @@ -3,8 +3,8 @@ import * as path from 'path' import * as assert from 'assert' import * as test from './utils' import { lw } from '../../src/lw' +import type { CompletionItem } from '../../src/types' import { SurroundCommand } from '../../src/completion/completer/commandlib/surround' -import { ICompletionItem } from '../../src/completion/latex' suite('Snippet test suite', () => { test.suite.name = path.basename(__filename).replace('.test.js', '') @@ -26,7 +26,7 @@ suite('Snippet test suite', () => { const active = vscode.window.activeTextEditor assert.ok(active) active.selection = new vscode.Selection(new vscode.Position(2, 0), new vscode.Position(2, 1)) - const items: ICompletionItem[] = [{ + const items: CompletionItem[] = [{ label: '\\fbox{}', detail: '\\fbox{${1:${TM_SELECTED_TEXT:text}}}', documentation: 'Command \\fbox{}.', diff --git a/test/suites/utils.ts b/test/suites/utils.ts index 33b4bef506..0b482bd011 100644 --- a/test/suites/utils.ts +++ b/test/suites/utils.ts @@ -140,7 +140,7 @@ export async function load(fixture: string, files: {src: string, dst: string, ws logger.log('Cache tex and bib.') files.filter(file => file.dst.endsWith('.tex')).forEach(file => lw.cache.add(path.resolve(getWsFixture(fixture, file.ws), file.dst))) const texPromise = files.filter(file => file.dst.endsWith('.tex')).map(file => lw.cache.refreshCache(path.resolve(getWsFixture(fixture, file.ws), file.dst), lw.root.file.path)) - const bibPromise = files.filter(file => file.dst.endsWith('.bib')).map(file => lw.completer.citation.parseBibFile(path.resolve(getWsFixture(fixture, file.ws), file.dst))) + const bibPromise = files.filter(file => file.dst.endsWith('.bib')).map(file => lw.completion.citation.parseBibFile(path.resolve(getWsFixture(fixture, file.ws), file.dst))) await Promise.all([...texPromise, ...bibPromise]) } if (config.open > -1) {