diff --git a/src/compile/build.ts b/src/compile/build.ts index 0585d7bb2..0c4526761 100644 --- a/src/compile/build.ts +++ b/src/compile/build.ts @@ -413,7 +413,7 @@ async function afterSuccessfulBuilt(lastStep: Step, skipped: boolean) { return } lw.viewer.refresh(lw.file.getPdfPath(lastStep.rootFile)) - lw.completer.reference.setNumbersFromAuxFile(lastStep.rootFile) + lw.completion.reference.setNumbersFromAuxFile(lastStep.rootFile) await lw.cache.loadFlsFile(lastStep.rootFile ?? '') const configuration = vscode.workspace.getConfiguration('latex-workshop', vscode.Uri.file(lastStep.rootFile)) // If the PDF viewer is internal, we call SyncTeX in src/components/viewer.ts. diff --git a/src/completion/bibtex.ts b/src/completion/bibtex.ts index e80a6d7af..5fdb2eec8 100644 --- a/src/completion/bibtex.ts +++ b/src/completion/bibtex.ts @@ -8,7 +8,7 @@ const logger = lw.log('Intelli', 'Bib') type DataBibtexJsonType = typeof import('../../data/bibtex-entries.json') type DataBibtexOptionalJsonType = typeof import('../../data/bibtex-optional-entries.json') -export class BibtexCompleter implements vscode.CompletionItemProvider { +export class BibProvider implements vscode.CompletionItemProvider { private scope: vscode.ConfigurationScope | undefined = undefined private readonly entryItems: vscode.CompletionItem[] = [] private readonly optFieldItems = Object.create(null) as { [key: string]: vscode.CompletionItem[] } diff --git a/src/completion/completer/argument.ts b/src/completion/completer/argument.ts index 7d60b6fab..1e71623c5 100644 --- a/src/completion/completer/argument.ts +++ b/src/completion/completer/argument.ts @@ -14,7 +14,7 @@ function from(result: RegExpMatchArray, args: CompletionArgs) { return provideClassOptions(args.line) } const index = getArgumentIndex(result[2]) - const packages = lw.completer.package.getPackagesIncluded(args.langId) + const packages = lw.completion.usepackage.getAll(args.langId) let candidate: CmdEnvSuggestion | undefined let environment: string | undefined if (result[1] === 'begin') { @@ -66,8 +66,8 @@ function providePackageOptions(line: string): vscode.CompletionItem[] { if (!match) { return [] } - lw.completer.loadPackageData(match[1]) - const suggestions = lw.completer.package.getPackageOptions(match[1]) + lw.completion.usepackage.load(match[1]) + const suggestions = lw.completion.usepackage.getOpts(match[1]) .map(option => { const item = new vscode.CompletionItem(option, vscode.CompletionItemKind.Constant) item.insertText = new vscode.SnippetString(option) @@ -86,8 +86,8 @@ function provideClassOptions(line: string): vscode.CompletionItem[] { 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]}`) + lw.completion.usepackage.load(isDefaultClass ? 'latex-document' : `class-${match[1]}`) + const suggestions = lw.completion.usepackage.getOpts(isDefaultClass ? 'latex-document' : `class-${match[1]}`) .map(option => { const item = new vscode.CompletionItem(option, vscode.CompletionItemKind.Constant) item.insertText = new vscode.SnippetString(option) diff --git a/src/completion/completer/atsuggestion.ts b/src/completion/completer/atsuggestion.ts index f508aee55..4b7ab1ec6 100644 --- a/src/completion/completer/atsuggestion.ts +++ b/src/completion/completer/atsuggestion.ts @@ -2,7 +2,18 @@ import * as vscode from 'vscode' import * as fs from 'fs' import { lw } from '../../lw' import type { CompletionProvider, CompletionArgs } from '../../types' -import {escapeRegExp} from '../../utils/utils' +import { escapeRegExp } from '../../utils/utils' + +export const provider: CompletionProvider = { from } +export const atSuggestion = { + initialize +} + +const data = { + triggerCharacter: '', + escapedTriggerCharacter: '', + suggestions: [] as vscode.CompletionItem[] +} interface AtSuggestionItemEntry { prefix: string, @@ -10,73 +21,61 @@ interface AtSuggestionItemEntry { description: string } -type DataAtSuggestionJsonType = typeof import('../../../data/at-suggestions.json') - -export class AtSuggestion implements CompletionProvider { - private readonly triggerCharacter: string - private readonly escapedTriggerCharacter: string - private readonly suggestions: vscode.CompletionItem[] = [] - - constructor(triggerCharacter: string) { - this.triggerCharacter = triggerCharacter - this.escapedTriggerCharacter = escapeRegExp(this.triggerCharacter) - - const allSuggestions: {[key: string]: AtSuggestionItemEntry} = JSON.parse(fs.readFileSync(`${lw.extensionRoot}/data/at-suggestions.json`).toString()) as DataAtSuggestionJsonType - this.initialize(allSuggestions) - lw.onConfigChange('intellisense.atSuggestion.user', () => { - this.initialize(allSuggestions) - }) +lw.onConfigChange(['intellisense.atSuggestion.user'], initialize) +// AtSuggestion is not initialized here, but in AtSuggestionCompleter +function initialize(triggerCharacter?: string) { + if (triggerCharacter) { + data.triggerCharacter = triggerCharacter + data.escapedTriggerCharacter = escapeRegExp(data.triggerCharacter) } + const userSnippets = vscode.workspace.getConfiguration('latex-workshop').get('intellisense.atSuggestion.user') as {[key: string]: string} + data.suggestions.length = 0 + Object.entries(userSnippets).forEach(([prefix, body]) => { + if (body === '') { + return + } + const completionItem = new vscode.CompletionItem(prefix.replace('@', data.triggerCharacter), vscode.CompletionItemKind.Function) + completionItem.insertText = new vscode.SnippetString(body) + completionItem.documentation = 'User defined @suggestion' + completionItem.detail = 'User defined @suggestion' + data.suggestions.push(completionItem) + }) - private initialize(suggestions: {[key: string]: AtSuggestionItemEntry}) { - const userSnippets = vscode.workspace.getConfiguration('latex-workshop').get('intellisense.atSuggestion.user') as {[key: string]: string} - this.suggestions.length = 0 - Object.entries(userSnippets).forEach(([prefix, body]) => { - if (body === '') { - return - } - const completionItem = new vscode.CompletionItem(prefix.replace('@', this.triggerCharacter), vscode.CompletionItemKind.Function) - completionItem.insertText = new vscode.SnippetString(body) - completionItem.documentation = 'User defined @suggestion' - completionItem.detail = 'User defined @suggestion' - this.suggestions.push(completionItem) - }) - - Object.values(suggestions).forEach(item => { - if (item.prefix in userSnippets) { - return - } - const completionItem = new vscode.CompletionItem(item.prefix.replace('@', this.triggerCharacter), vscode.CompletionItemKind.Function) - completionItem.insertText = new vscode.SnippetString(item.body) - completionItem.documentation = new vscode.MarkdownString(item.description) - completionItem.detail = item.description - this.suggestions.push(completionItem) - }) - } + const suggestions: {[key: string]: AtSuggestionItemEntry} = JSON.parse(fs.readFileSync(`${lw.extensionRoot}/data/at-suggestions.json`).toString()) as typeof import('../../../data/at-suggestions.json') + Object.values(suggestions).forEach(item => { + if (item.prefix in userSnippets) { + return + } + const completionItem = new vscode.CompletionItem(item.prefix.replace('@', data.triggerCharacter), vscode.CompletionItemKind.Function) + completionItem.insertText = new vscode.SnippetString(item.body) + completionItem.documentation = new vscode.MarkdownString(item.description) + completionItem.detail = item.description + data.suggestions.push(completionItem) + }) +} - 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,}$') - if (result[0].match(reg)) { - const filteredSuggestions = suggestions.filter(item => item.label === result[0]) - if (filteredSuggestions.length > 0) { - return filteredSuggestions.map(item => { - item.range = new vscode.Range(args.position.translate(undefined, -item.label.toString().length), args.position) - return item - }) - } +function from(result: RegExpMatchArray, args: CompletionArgs) { + const suggestions = provide(args.line, args.position) + // Manually filter suggestions when there are several consecutive trigger characters + const reg = new RegExp(data.escapedTriggerCharacter + '{2,}$') + if (result[0].match(reg)) { + const filteredSuggestions = suggestions.filter(item => item.label === result[0]) + if (filteredSuggestions.length > 0) { + return filteredSuggestions.map(item => { + item.range = new vscode.Range(args.position.translate(undefined, -item.label.toString().length), args.position) + return item + }) } - return suggestions } + return suggestions +} - private provide(line: string, position: vscode.Position): vscode.CompletionItem[] { - let range: vscode.Range | undefined = undefined - const startPos = line.lastIndexOf(this.triggerCharacter, position.character - 1) - if (startPos >= 0) { - range = new vscode.Range(position.line, startPos, position.line, position.character) - } - this.suggestions.forEach(suggestion => {suggestion.range = range}) - return this.suggestions +function provide(line: string, position: vscode.Position): vscode.CompletionItem[] { + let range: vscode.Range | undefined = undefined + const startPos = line.lastIndexOf(data.triggerCharacter, position.character - 1) + if (startPos >= 0) { + range = new vscode.Range(position.line, startPos, position.line, position.character) } + data.suggestions.forEach(suggestion => {suggestion.range = range}) + return data.suggestions } diff --git a/src/completion/completer/class.ts b/src/completion/completer/class.ts new file mode 100644 index 000000000..5d51e70e8 --- /dev/null +++ b/src/completion/completer/class.ts @@ -0,0 +1,33 @@ +import * as vscode from 'vscode' +import * as fs from 'fs' +import { lw } from '../../lw' +import type { CompletionProvider } from '../../types' + +export const provider: CompletionProvider = { from } + +const data = { + suggestions: [] as vscode.CompletionItem[] +} + +type ClassItemEntry = { + command: string, + detail: string, + documentation: string +} + +function initialize(classes: {[key: string]: ClassItemEntry}) { + Object.values(classes).forEach(item => { + const cl = new vscode.CompletionItem(item.command, vscode.CompletionItemKind.Module) + cl.detail = item.detail + cl.documentation = new vscode.MarkdownString(`[${item.documentation}](${item.documentation})`) + data.suggestions.push(cl) + }) +} + +function from(): vscode.CompletionItem[] { + if (data.suggestions.length === 0) { + const allClasses: {[key: string]: ClassItemEntry} = JSON.parse(fs.readFileSync(`${lw.extensionRoot}/data/classnames.json`).toString()) as typeof import('../../../data/classnames.json') + initialize(allClasses) + } + return data.suggestions +} diff --git a/src/completion/completer/documentclass.ts b/src/completion/completer/documentclass.ts deleted file mode 100644 index cb6fed7a5..000000000 --- a/src/completion/completer/documentclass.ts +++ /dev/null @@ -1,37 +0,0 @@ -import * as vscode from 'vscode' -import * as fs from 'fs' -import { lw } from '../../lw' -import type { CompletionProvider } from '../../types' - -type DataClassnamesJsonType = typeof import('../../../data/classnames.json') - -type ClassItemEntry = { - command: string, - detail: string, - documentation: string -} - -export class DocumentClass implements CompletionProvider { - private readonly suggestions: vscode.CompletionItem[] = [] - - initialize(classes: {[key: string]: ClassItemEntry}) { - Object.values(classes).forEach(item => { - const cl = new vscode.CompletionItem(item.command, vscode.CompletionItemKind.Module) - cl.detail = item.detail - cl.documentation = new vscode.MarkdownString(`[${item.documentation}](${item.documentation})`) - this.suggestions.push(cl) - }) - } - - from() { - return this.provide() - } - - private provide(): vscode.CompletionItem[] { - if (this.suggestions.length === 0) { - const allClasses: {[key: string]: ClassItemEntry} = JSON.parse(fs.readFileSync(`${lw.extensionRoot}/data/classnames.json`).toString()) as DataClassnamesJsonType - this.initialize(allClasses) - } - return this.suggestions - } -} diff --git a/src/completion/completer/environment.ts b/src/completion/completer/environment.ts index e5b26d06c..cf46e8247 100644 --- a/src/completion/completer/environment.ts +++ b/src/completion/completer/environment.ts @@ -106,7 +106,7 @@ function provide(langId: string, line: string, position: vscode.Position): Compl // Insert package environments const configuration = vscode.workspace.getConfiguration('latex-workshop') if (configuration.get('intellisense.package.enabled')) { - const packages = lw.completer.package.getPackagesIncluded(langId) + const packages = lw.completion.usepackage.getAll(langId) Object.entries(packages).forEach(([packageName, options]) => { getEnvFromPkg(packageName, snippetType).forEach(env => { if (env.option && options && !options.includes(env.option)) { @@ -241,7 +241,7 @@ function getEnvFromPkg(packageName: string, type: EnvSnippetType): CmdEnvSuggest return entry } - lw.completer.loadPackageData(packageName) + lw.completion.usepackage.load(packageName) // No package command defined const pkgEnvs = data.packageEnvs.get(packageName) if (!pkgEnvs || pkgEnvs.length === 0) { diff --git a/src/completion/completer/glossary.ts b/src/completion/completer/glossary.ts index d356a3763..fda300e98 100644 --- a/src/completion/completer/glossary.ts +++ b/src/completion/completer/glossary.ts @@ -1,13 +1,20 @@ import * as vscode from 'vscode' import type * as Ast from '@unified-latex/unified-latex-types' import { lw } from '../../lw' -import type { CompletionItem, CompletionProvider, FileCache } from '../../types' +import { GlossaryType } from '../../types' +import type { CompletionProvider, FileCache, GlossaryItem } from '../../types' import { argContentToStr } from '../../utils/parser' import { getLongestBalancedString } from '../../utils/utils' -enum GlossaryType { - glossary, - acronym +export const provider: CompletionProvider = { from } +export const glossary = { + parse, + getItem +} + +const data = { + glossaries: new Map(), + acronyms: new Map() } interface GlossaryEntry { @@ -15,192 +22,176 @@ interface GlossaryEntry { description: string | undefined } -export interface GlossarySuggestion extends CompletionItem { - type: GlossaryType, - filePath: string, - position: vscode.Position -} - -export class Glossary implements CompletionProvider { - // use object for deduplication - private readonly glossaries = new Map() - private readonly acronyms = new Map() +function from(result: RegExpMatchArray): vscode.CompletionItem[] { + updateAll() + let suggestions: Map - from(result: RegExpMatchArray) { - return this.provide(result) + if (result[1] && result[1].match(/^ac/i)) { + suggestions = data.acronyms + } else { + suggestions = new Map( [...data.acronyms, ...data.glossaries] ) } - private provide(result: RegExpMatchArray): vscode.CompletionItem[] { - this.updateAll() - let suggestions: Map - - if (result[1] && result[1].match(/^ac/i)) { - suggestions = this.acronyms - } else { - suggestions = new Map( [...this.acronyms, ...this.glossaries] ) - } - - // Compile the suggestion object to array - const items = Array.from(suggestions.values()) - return items - } - - getEntry(token: string): GlossarySuggestion | undefined { - this.updateAll() - return this.glossaries.get(token) || this.acronyms.get(token) - } + // Compile the suggestion object to array + const items = Array.from(suggestions.values()) + return items +} - private updateAll() { - // Extract cached references - const glossaryList: string[] = [] +function getItem(token: string): GlossaryItem | undefined { + updateAll() + return data.glossaries.get(token) || data.acronyms.get(token) +} - lw.cache.getIncludedTeX().forEach(cachedFile => { - const cachedGlossaries = lw.cache.get(cachedFile)?.elements.glossary - if (cachedGlossaries === undefined) { - return - } - cachedGlossaries.forEach(ref => { - if (ref.type === GlossaryType.glossary) { - this.glossaries.set(ref.label, ref) - } else { - this.acronyms.set(ref.label, ref) - } - glossaryList.push(ref.label) - }) - }) +function updateAll() { + // Extract cached references + const glossaryList: string[] = [] - // Remove references that has been deleted - this.glossaries.forEach((_, key) => { - if (!glossaryList.includes(key)) { - this.glossaries.delete(key) - } - }) - this.acronyms.forEach((_, key) => { - if (!glossaryList.includes(key)) { - this.acronyms.delete(key) + lw.cache.getIncludedTeX().forEach(cachedFile => { + const cachedGlossaries = lw.cache.get(cachedFile)?.elements.glossary + if (cachedGlossaries === undefined) { + return + } + cachedGlossaries.forEach(ref => { + if (ref.type === GlossaryType.glossary) { + data.glossaries.set(ref.label, ref) + } else { + data.acronyms.set(ref.label, ref) } + glossaryList.push(ref.label) }) - } + }) - parse(cache: FileCache) { - if (cache.ast !== undefined) { - cache.elements.glossary = this.parseAst(cache.ast, cache.filePath) - } else { - cache.elements.glossary = this.parseContent(cache.content, cache.filePath) + // Remove references that has been deleted + data.glossaries.forEach((_, key) => { + if (!glossaryList.includes(key)) { + data.glossaries.delete(key) } - } + }) + data.acronyms.forEach((_, key) => { + if (!glossaryList.includes(key)) { + data.acronyms.delete(key) + } + }) +} - private parseAst(node: Ast.Node, filePath: string): GlossarySuggestion[] { - let glos: GlossarySuggestion[] = [] - let entry: GlossaryEntry = { label: '', description: '' } - let type: GlossaryType | undefined +function parse(cache: FileCache) { + if (cache.ast !== undefined) { + cache.elements.glossary = parseAst(cache.ast, cache.filePath) + } else { + cache.elements.glossary = parseContent(cache.content, cache.filePath) + } +} - if (node.type === 'macro' && ['newglossaryentry', 'provideglossaryentry'].includes(node.content)) { - type = GlossaryType.glossary - let description = argContentToStr(node.args?.[1]?.content || [], true) - const index = description.indexOf('description=') - if (index >= 0) { - description = description.slice(index + 12) - if (description.charAt(0) === '{') { - description = getLongestBalancedString(description) ?? '' - } else { - description = description.split(',')[0] ?? '' - } +function parseAst(node: Ast.Node, filePath: string): GlossaryItem[] { + let glos: GlossaryItem[] = [] + let entry: GlossaryEntry = { label: '', description: '' } + let type: GlossaryType | undefined + + if (node.type === 'macro' && ['newglossaryentry', 'provideglossaryentry'].includes(node.content)) { + type = GlossaryType.glossary + let description = argContentToStr(node.args?.[1]?.content || [], true) + const index = description.indexOf('description=') + if (index >= 0) { + description = description.slice(index + 12) + if (description.charAt(0) === '{') { + description = getLongestBalancedString(description) ?? '' } else { - description = '' - } - entry = { - label: argContentToStr(node.args?.[0]?.content || []), - description - } - } else if (node.type === 'macro' && ['longnewglossaryentry', 'longprovideglossaryentry', 'newacronym', 'newabbreviation', 'newabbr'].includes(node.content)) { - if (['longnewglossaryentry', 'longprovideglossaryentry'].includes(node.content)) { - type = GlossaryType.glossary - } else { - type = GlossaryType.acronym - } - entry = { - label: argContentToStr(node.args?.[1]?.content || []), - description: argContentToStr(node.args?.[3]?.content || []), + description = description.split(',')[0] ?? '' } + } else { + description = '' } - if (type !== undefined && entry.label && entry.description && node.position !== undefined) { - glos.push({ - type, - filePath, - position: new vscode.Position(node.position.start.line - 1, node.position.start.column - 1), - label: entry.label, - detail: entry.description, - kind: vscode.CompletionItemKind.Reference - }) + entry = { + label: argContentToStr(node.args?.[0]?.content || []), + description } - - const parseContent = (content: Ast.Node[]) => { - for (const subNode of content) { - glos = [...glos, ...this.parseAst(subNode, filePath)] - } + } else if (node.type === 'macro' && ['longnewglossaryentry', 'longprovideglossaryentry', 'newacronym', 'newabbreviation', 'newabbr'].includes(node.content)) { + if (['longnewglossaryentry', 'longprovideglossaryentry'].includes(node.content)) { + type = GlossaryType.glossary + } else { + type = GlossaryType.acronym } - 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) + entry = { + label: argContentToStr(node.args?.[1]?.content || []), + description: argContentToStr(node.args?.[3]?.content || []), } + } + if (type !== undefined && entry.label && entry.description && node.position !== undefined) { + glos.push({ + type, + filePath, + position: new vscode.Position(node.position.start.line - 1, node.position.start.column - 1), + label: entry.label, + detail: entry.description, + kind: vscode.CompletionItemKind.Reference + }) + } - return glos + const parseContentNodes = (content: Ast.Node[]) => { + for (const subNode of content) { + glos = [...glos, ...parseAst(subNode, filePath)] + } + } + if (node.type === 'macro' && node.args) { + for (const arg of node.args) { + parseContentNodes(arg.content) + } + } else if ('content' in node && typeof node.content !== 'string') { + parseContentNodes(node.content) } - private parseContent(content: string, filePath: string): GlossarySuggestion[] { - const glossaries: GlossarySuggestion[] = [] - const glossaryList: string[] = [] + return glos +} - // We assume that the label is always result[1] and use getDescription(result) for the description - const regexes: { - [key: string]: { - regex: RegExp, - type: GlossaryType, - getDescription: (result: RegExpMatchArray) => string - } - } = { - 'glossary': { - regex: /\\(?:provide|new)glossaryentry{([^{}]*)}\s*{(?:(?!description).)*description=(?:([^{},]*)|{([^{}]*))[,}]/gms, - type: GlossaryType.glossary, - getDescription: (result) => { return result[2] ? result[2] : result[3] } - }, - 'longGlossary': { - regex: /\\long(?:provide|new)glossaryentry{([^{}]*)}\s*{[^{}]*}\s*{([^{}]*)}/gms, - type: GlossaryType.glossary, - getDescription: (result) => { return result[2] } - }, - 'acronym': { - regex: /\\newacronym(?:\[[^[\]]*\])?{([^{}]*)}{[^{}]*}{([^{}]*)}/gm, - type: GlossaryType.acronym, - getDescription: (result) => { return result[2] } - } +function parseContent(content: string, filePath: string): GlossaryItem[] { + const glossaries: GlossaryItem[] = [] + const glossaryList: string[] = [] + + // We assume that the label is always result[1] and use getDescription(result) for the description + const regexes: { + [key: string]: { + regex: RegExp, + type: GlossaryType, + getDescription: (result: RegExpMatchArray) => string + } + } = { + 'glossary': { + regex: /\\(?:provide|new)glossaryentry{([^{}]*)}\s*{(?:(?!description).)*description=(?:([^{},]*)|{([^{}]*))[,}]/gms, + type: GlossaryType.glossary, + getDescription: (result) => { return result[2] ? result[2] : result[3] } + }, + 'longGlossary': { + regex: /\\long(?:provide|new)glossaryentry{([^{}]*)}\s*{[^{}]*}\s*{([^{}]*)}/gms, + type: GlossaryType.glossary, + getDescription: (result) => { return result[2] } + }, + 'acronym': { + regex: /\\newacronym(?:\[[^[\]]*\])?{([^{}]*)}{[^{}]*}{([^{}]*)}/gm, + type: GlossaryType.acronym, + getDescription: (result) => { return result[2] } } + } - for(const key in regexes){ - while(true) { - const result = regexes[key].regex.exec(content) - if (result === null) { - break - } - const positionContent = content.substring(0, result.index).split('\n') - if (glossaryList.includes(result[1])) { - continue - } - glossaries.push({ - type: regexes[key].type, - filePath, - position: new vscode.Position(positionContent.length - 1, positionContent[positionContent.length - 1].length), - label: result[1], - detail: regexes[key].getDescription(result), - kind: vscode.CompletionItemKind.Reference - }) + for(const key in regexes){ + while(true) { + const result = regexes[key].regex.exec(content) + if (result === null) { + break + } + const positionContent = content.substring(0, result.index).split('\n') + if (glossaryList.includes(result[1])) { + continue } + glossaries.push({ + type: regexes[key].type, + filePath, + position: new vscode.Position(positionContent.length - 1, positionContent[positionContent.length - 1].length), + label: result[1], + detail: regexes[key].getDescription(result), + kind: vscode.CompletionItemKind.Reference + }) } - - return glossaries } + + return glossaries } diff --git a/src/completion/completer/input.ts b/src/completion/completer/input.ts index f0ce14149..a5fff450a 100644 --- a/src/completion/completer/input.ts +++ b/src/completion/completer/input.ts @@ -10,7 +10,6 @@ const logger = lw.log('Intelli', 'Input') const ignoreFiles = ['**/.vscode', '**/.vscodeignore', '**/.gitignore'] abstract class InputAbstract implements CompletionProvider { - graphicsPath: Set = new Set() /** * Compute the base directory for file completion @@ -44,25 +43,6 @@ abstract class InputAbstract implements CompletionProvider { }) } - /** - * Set the graphics path - */ - parseGraphicsPath(cache: FileCache) { - const regex = /\\graphicspath{[\s\n]*((?:{[^{}]*}[\s\n]*)*)}/g - let result: string[] | null - while (true) { - result = regex.exec(cache.contentTrimmed) - if (result === null) { - break - } - result[1].split(/\{|\}/).filter(s => s.replace(/^\s*$/, '')).forEach(dir => this.graphicsPath.add(dir)) - } - } - - reset() { - this.graphicsPath.clear() - } - from(result: RegExpMatchArray, args: CompletionArgs) { const command = result[1] const payload = [...result.slice(2).reverse()] @@ -131,7 +111,28 @@ abstract class InputAbstract implements CompletionProvider { } } -export class Input extends InputAbstract { +class Input extends InputAbstract { + graphicsPath: Set = new Set() + + /** + * Set the graphics path + */ + parseGraphicsPath(cache: FileCache) { + const regex = /\\graphicspath{[\s\n]*((?:{[^{}]*}[\s\n]*)*)}/g + let result: string[] | null + while (true) { + result = regex.exec(cache.contentTrimmed) + if (result === null) { + break + } + result[1].split(/\{|\}/).filter(s => s.replace(/^\s*$/, '')).forEach(dir => this.graphicsPath.add(dir)) + } + } + + reset() { + this.graphicsPath.clear() + } + provideDirOnly(_importFromDir: string): boolean { return false } @@ -170,7 +171,7 @@ export class Input extends InputAbstract { } } -export class Import extends InputAbstract { +class Import extends InputAbstract { provideDirOnly(importFromDir: string): boolean { return (!importFromDir) } @@ -185,7 +186,7 @@ export class Import extends InputAbstract { } -export class SubImport extends InputAbstract { +class SubImport extends InputAbstract { provideDirOnly(importFromDir: string): boolean { return (!importFromDir) } @@ -199,3 +200,11 @@ export class SubImport extends InputAbstract { } } } + +export const input = new Input() +export const inputProvider: CompletionProvider = input + +const importMacro = new Import() +const subimportMacro = new SubImport() +export const importProvider: CompletionProvider = importMacro +export const subimportProvider: CompletionProvider = subimportMacro diff --git a/src/completion/completer/macro.ts b/src/completion/completer/macro.ts index 2a3f6e2eb..1815b8102 100644 --- a/src/completion/completer/macro.ts +++ b/src/completion/completer/macro.ts @@ -9,7 +9,7 @@ import { environment } from './environment' import { CmdEnvSuggestion, splitSignatureString, filterNonLetterSuggestions, filterArgumentHint } from './completerutils' import { SurroundCommand } from './commandlib/surround' -const logger = lw.log('Intelli', 'Command') +const logger = lw.log('Intelli', 'Macro') export const provider: CompletionProvider = { from } export const macro = { @@ -121,7 +121,7 @@ function provide(langId: string, line?: string, position?: vscode.Position): Com // Insert commands from packages if ((configuration.get('intellisense.package.enabled'))) { - const packages = lw.completer.package.getPackagesIncluded(langId) + const packages = lw.completion.usepackage.getAll(langId) Object.entries(packages).forEach(([packageName, options]) => { provideCmdInPkg(packageName, options, suggestions) environment.provideEnvsAsCommandInPkg(packageName, options, suggestions, defined) @@ -257,7 +257,7 @@ function parseAst(node: Ast.Node, filePath: string, defined?: Set): CmdE function parseContent(content: string, filePath: string): CmdEnvSuggestion[] { const cmdInPkg: CmdEnvSuggestion[] = [] - const packages = lw.completer.package.getPackagesIncluded('latex-expl3') + const packages = lw.completion.usepackage.getAll('latex-expl3') Object.entries(packages).forEach(([packageName, options]) => { provideCmdInPkg(packageName, options, cmdInPkg) }) @@ -407,7 +407,7 @@ function provideCmdInPkg(packageName: string, options: string[], suggestions: Cm const configuration = vscode.workspace.getConfiguration('latex-workshop') const useOptionalArgsEntries = configuration.get('intellisense.optionalArgsEntries.enabled') // Load command in pkg - lw.completer.loadPackageData(packageName) + lw.completion.usepackage.load(packageName) // No package command defined const pkgCmds = data.packageCmds.get(packageName) diff --git a/src/completion/completer/package.ts b/src/completion/completer/package.ts index 6cfc824d7..5b9225ffe 100644 --- a/src/completion/completer/package.ts +++ b/src/completion/completer/package.ts @@ -1,11 +1,30 @@ import * as vscode from 'vscode' 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 { CompletionProvider, FileCache } from '../../types' +import type { CompletionProvider, FileCache, Package } from '../../types' import { argContentToStr } from '../../utils/parser' -type DataPackagesJsonType = typeof import('../../../data/packagenames.json') +const logger = lw.log('Intelli', 'Package') + +export const provider: CompletionProvider = { from } +export const usepackage = { + parse, + load, + getAll, + getDeps, + getOpts, + setDeps, + setOpts +} + +const data = { + loaded: [] as string[], + suggestions: [] as vscode.CompletionItem[], + packageDeps: Object.create(null) as { [packageName: string]: { [key: string]: string[] } }, + packageOptions: Object.create(null) as { [packageName: string]: string[] } +} type PackageItemEntry = { command: string, @@ -13,160 +32,213 @@ type PackageItemEntry = { documentation: string } -export class Package implements CompletionProvider { - private readonly suggestions: vscode.CompletionItem[] = [] - private readonly packageDeps: {[packageName: string]: {[key: string]: string[]}} = {} - private readonly packageOptions: {[packageName: string]: string[]} = {} +function load(packageName: string) { + if (data.loaded.includes(packageName)) { + return + } - initialize(defaultPackages: {[key: string]: PackageItemEntry}) { - Object.values(defaultPackages).forEach(item => { - const pack = new vscode.CompletionItem(item.command, vscode.CompletionItemKind.Module) - pack.detail = item.detail - pack.documentation = new vscode.MarkdownString(`[${item.documentation}](${item.documentation})`) - this.suggestions.push(pack) - }) + const filePath: string | undefined = resolvePackageFile(packageName) + if (filePath === undefined) { + data.loaded.push(packageName) + return } - from() { - return this.provide() + try { + const packageData = JSON.parse(fs.readFileSync(filePath).toString()) as Package + populatePackageData(packageData) + + setDeps(packageName, packageData.includes) + setOpts(packageName, packageData.options) + lw.completion.environment.setPackageEnvs(packageName, packageData.envs) + lw.completion.macro.setPackageCmds(packageName, packageData.macros) + + data.loaded.push(packageName) + } catch (e) { + logger.log(`Cannot parse intellisense file: ${filePath}`) } +} - private provide(): vscode.CompletionItem[] { - if (this.suggestions.length === 0) { - const pkgs: {[key: string]: PackageItemEntry} = JSON.parse(fs.readFileSync(`${lw.extensionRoot}/data/packagenames.json`).toString()) as DataPackagesJsonType - this.initialize(pkgs) +function resolvePackageFile(packageName: string): string | undefined { + const defaultDir = `${lw.extensionRoot}/data/packages/` + const dirs = vscode.workspace.getConfiguration('latex-workshop').get('intellisense.package.dirs') as string[] + dirs.push(defaultDir) + for (const dir of dirs) { + const filePath = path.resolve(dir, `${packageName}.json`) + if (fs.existsSync(filePath)) { + return filePath } - return this.suggestions } - - setPackageDeps(packageName: string, deps: {[key: string]: string[]}) { - this.packageDeps[packageName] = deps + // Many package with names like toppackage-config.sty are just wrappers around + // the general package toppacke.sty and do not define commands on their own. + const indexDash = packageName.lastIndexOf('-') + if (indexDash > - 1) { + const generalPkg = packageName.substring(0, indexDash) + const filePath = path.resolve(defaultDir, `${generalPkg}.json`) + if (fs.existsSync(filePath)) { + return filePath + } } + return +} - setPackageOptions(packageName: string, options: string[]) { - this.packageOptions[packageName] = options - } +function 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] + }) + Object.entries(packageData.envs).forEach(([key, env]) => { + env.detail = key + env.name = env.name || key + env.snippet = env.snippet || '' + env.keyvals = packageData.keyvals[env.keyvalindex ?? -1] + }) +} - getPackageOptions(packageName: string) { - return this.packageOptions[packageName] || [] - } +function initialize(defaultPackages: {[key: string]: PackageItemEntry}) { + Object.values(defaultPackages).forEach(item => { + const pack = new vscode.CompletionItem(item.command, vscode.CompletionItemKind.Module) + pack.detail = item.detail + pack.documentation = new vscode.MarkdownString(`[${item.documentation}](${item.documentation})`) + data.suggestions.push(pack) + }) +} - private getPackageDeps(packageName: string): {[key: string]: string[]} { - return this.packageDeps[packageName] || {} +function from(): vscode.CompletionItem[] { + if (data.suggestions.length === 0) { + const pkgs: {[key: string]: PackageItemEntry} = JSON.parse(fs.readFileSync(`${lw.extensionRoot}/data/packagenames.json`).toString()) as typeof import('../../../data/packagenames.json') + initialize(pkgs) } + return data.suggestions +} - getPackagesIncluded(languageId: string): {[packageName: string]: string[]} { - const packages: {[packageName: string]: string[]} = {} - const config = vscode.workspace.getConfiguration('latex-workshop') - const excluded = config.get('intellisense.package.exclude') as string[] - if (!excluded.includes('lw-default')) { - if (['latex', 'latex-expl3'].includes(languageId)) { - packages['latex-document'] = [] - } - if (languageId === 'latex-expl3') { - packages['expl3'] = [] - } +function setDeps(packageName: string, deps: {[key: string]: string[]}) { + data.packageDeps[packageName] = deps +} + +function setOpts(packageName: string, options: string[]) { + data.packageOptions[packageName] = options +} + +function getOpts(packageName: string) { + return data.packageOptions[packageName] || [] +} + +function getDeps(packageName: string): {[key: string]: string[]} { + return data.packageDeps[packageName] || {} +} + +function getAll(languageId: string): {[packageName: string]: string[]} { + const packages: {[packageName: string]: string[]} = {} + const config = vscode.workspace.getConfiguration('latex-workshop') + const excluded = config.get('intellisense.package.exclude') as string[] + if (!excluded.includes('lw-default')) { + if (['latex', 'latex-expl3'].includes(languageId)) { + packages['latex-document'] = [] + } + if (languageId === 'latex-expl3') { + packages['expl3'] = [] } + } - (config.get('intellisense.package.extra') as string[]) - .filter(packageName => !excluded.includes(packageName)) - .forEach(packageName => packages[packageName] = []) + (config.get('intellisense.package.extra') as string[]) + .filter(packageName => !excluded.includes(packageName)) + .forEach(packageName => packages[packageName] = []) - lw.cache.getIncludedTeX().forEach(tex => { - const included = lw.cache.get(tex)?.elements.package - if (included === undefined) { - return - } - Object.entries(included) - .filter(([packageName, ]) => !excluded.includes(packageName)) - .forEach(([packageName, options]) => packages[packageName] = options) - }) - - while (true) { - let newPackageInserted = false - Object.entries(packages).forEach(([packageName, options]) => Object.keys(this.getPackageDeps(packageName)) - .filter(includeName => !excluded.includes(includeName)) - .forEach(includeName => { - const dependOptions = this.getPackageDeps(packageName)[includeName] - const hasOption = dependOptions.length === 0 - || options.filter(option => dependOptions.includes(option)).length > 0 - if (packages[includeName] === undefined && hasOption) { - packages[includeName] = [] - newPackageInserted = true - } + lw.cache.getIncludedTeX().forEach(tex => { + const included = lw.cache.get(tex)?.elements.package + if (included === undefined) { + return + } + Object.entries(included) + .filter(([packageName, ]) => !excluded.includes(packageName)) + .forEach(([packageName, options]) => packages[packageName] = options) + }) + + while (true) { + let newPackageInserted = false + Object.entries(packages).forEach(([packageName, options]) => Object.keys(getDeps(packageName)) + .filter(includeName => !excluded.includes(includeName)) + .forEach(includeName => { + const dependOptions = getDeps(packageName)[includeName] + const hasOption = dependOptions.length === 0 + || options.filter(option => dependOptions.includes(option)).length > 0 + if (packages[includeName] === undefined && hasOption) { + packages[includeName] = [] + newPackageInserted = true } - )) - if (!newPackageInserted) { - break } + )) + if (!newPackageInserted) { + break } - - return packages } - parse(cache: FileCache) { - if (cache.ast !== undefined) { - cache.elements.package = this.parseAst(cache.ast) - } else { - cache.elements.package = this.parseContent(cache.content) - } + return packages +} + +function parse(cache: FileCache) { + if (cache.ast !== undefined) { + cache.elements.package = parseAst(cache.ast) + } else { + cache.elements.package = parseContent(cache.content) } +} - private parseAst(node: Ast.Node): {[pkgName: string]: string[]} { - const packages = {} - if (node.type === 'macro' && ['usepackage', 'documentclass'].includes(node.content)) { - const options: string[] = argContentToStr(node.args?.[0]?.content || []) - .split(',') - .map(arg => arg.trim()) - const optionsNoTrue = options - .filter(option => option.includes('=true')) - .map(option => option.replace('=true', '')) - - argContentToStr(node.args?.[1]?.content || []) - .split(',') - .map(packageName => this.toPackageObj(packageName.trim(), [...options, ...optionsNoTrue], node)) - .forEach(packageObj => Object.assign(packages, packageObj)) - } else if ('content' in node && typeof node.content !== 'string') { - for (const subNode of node.content) { - Object.assign(packages, this.parseAst(subNode)) - } +function parseAst(node: Ast.Node): {[pkgName: string]: string[]} { + const packages = {} + if (node.type === 'macro' && ['usepackage', 'documentclass'].includes(node.content)) { + const options: string[] = argContentToStr(node.args?.[0]?.content || []) + .split(',') + .map(arg => arg.trim()) + const optionsNoTrue = options + .filter(option => option.includes('=true')) + .map(option => option.replace('=true', '')) + + argContentToStr(node.args?.[1]?.content || []) + .split(',') + .map(packageName => toPackageObj(packageName.trim(), [...options, ...optionsNoTrue], node)) + .forEach(packageObj => Object.assign(packages, packageObj)) + } else if ('content' in node && typeof node.content !== 'string') { + for (const subNode of node.content) { + Object.assign(packages, parseAst(subNode)) } - return packages } + return packages +} - private parseContent(content: string): {[pkgName: string]: string[]} { - const packages = {} - const pkgReg = /\\(?:usepackage|RequirePackage)(\[[^[\]{}]*\])?{(.*?)}/gs - while (true) { - const result = pkgReg.exec(content) - if (result === null) { - break - } - const packageNames = result[2].split(',').map(packageName => packageName.trim()) - const options = (result[1] || '[]').slice(1,-1).replace(/\s*=\s*/g,'=').split(',').map(option => option.trim()) - const optionsNoTrue = options.filter(option => option.includes('=true')).map(option => option.replace('=true', '')) - packageNames - .map(packageName => this.toPackageObj(packageName, [...options, ...optionsNoTrue])) - .forEach(packageObj => Object.assign(packages, packageObj)) +function parseContent(content: string): {[pkgName: string]: string[]} { + const packages = {} + const pkgReg = /\\(?:usepackage|RequirePackage)(\[[^[\]{}]*\])?{(.*?)}/gs + while (true) { + const result = pkgReg.exec(content) + if (result === null) { + break } - return packages + const packageNames = result[2].split(',').map(packageName => packageName.trim()) + const options = (result[1] || '[]').slice(1,-1).replace(/\s*=\s*/g,'=').split(',').map(option => option.trim()) + const optionsNoTrue = options.filter(option => option.includes('=true')).map(option => option.replace('=true', '')) + packageNames + .map(packageName => toPackageObj(packageName, [...options, ...optionsNoTrue])) + .forEach(packageObj => Object.assign(packages, packageObj)) } + return packages +} - private toPackageObj(packageName: string, options: string[], node?: Ast.Node): {[pkgName: string]: string[]} { - packageName = packageName.trim() - if (packageName === '') { - return {} - } - let pkgObj: {[pkgName: string]: string[]} = {} - if (node?.type === 'macro' && node.content === 'documentclass') { - const clsPath = lw.file.kpsewhich([`${packageName}.cls`]) - if (vscode.workspace.getConfiguration('latex-workshop').get('kpsewhich.enabled') as boolean && - clsPath && fs.existsSync(clsPath)) { - pkgObj = this.parseContent(fs.readFileSync(clsPath).toString()) - } - packageName = 'class-' + packageName +function toPackageObj(packageName: string, options: string[], node?: Ast.Node): {[pkgName: string]: string[]} { + packageName = packageName.trim() + if (packageName === '') { + return {} + } + let pkgObj: {[pkgName: string]: string[]} = {} + if (node?.type === 'macro' && node.content === 'documentclass') { + const clsPath = lw.file.kpsewhich([`${packageName}.cls`]) + if (vscode.workspace.getConfiguration('latex-workshop').get('kpsewhich.enabled') as boolean && + clsPath && fs.existsSync(clsPath)) { + pkgObj = parseContent(fs.readFileSync(clsPath).toString()) } - pkgObj[packageName] = options - return pkgObj + packageName = 'class-' + packageName } + pkgObj[packageName] = options + return pkgObj } diff --git a/src/completion/completer/reference.ts b/src/completion/completer/reference.ts index 621273b1b..5899c6d74 100644 --- a/src/completion/completer/reference.ts +++ b/src/completion/completer/reference.ts @@ -3,278 +3,266 @@ 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 { CompletionArgs, CompletionItem, CompletionProvider, FileCache } from '../../types' +import type { CompletionArgs, CompletionItem, CompletionProvider, FileCache, ReferenceDocType, ReferenceEntry } from '../../types' import { getLongestBalancedString, stripEnvironments } from '../../utils/utils' import { computeFilteringRange } from './completerutils' import { argContentToStr } from '../../utils/parser' -export interface ReferenceEntry extends CompletionItem { - /** The file that defines the ref. */ - file: string, - /** The position that defines the ref. */ - position: vscode.Position, - /** Stores the ref number. */ - prevIndex?: {refNumber: string, pageNumber: string} +export const provider: CompletionProvider = { from } +export const reference = { + parse, + getItem, + setNumbersFromAuxFile } -export type ReferenceDocType = { - documentation: ReferenceEntry['documentation'], - file: ReferenceEntry['file'], - position: {line: number, character: number}, - key: string, - label: ReferenceEntry['label'], - prevIndex: ReferenceEntry['prevIndex'] +const data = { + suggestions: new Map(), + prevIndexObj: new Map() } -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() - - from(_result: RegExpMatchArray, args: CompletionArgs) { - return this.provide(args.line, args.position) - } +function from(_result: RegExpMatchArray, args: CompletionArgs) { + return provide(args.line, args.position) +} - private provide(line: string, position: vscode.Position): vscode.CompletionItem[] { - // Compile the suggestion object to array - this.updateAll(line, position) - let keys = [...this.suggestions.keys(), ...this.prevIndexObj.keys()] - keys = Array.from(new Set(keys)) - const items: vscode.CompletionItem[] = [] - for (const key of keys) { - const sug = this.suggestions.get(key) - if (sug) { - const data: ReferenceDocType = { - documentation: sug.documentation, - file: sug.file, - position: { - line: sug.position.line, - character: sug.position.character - }, - key, - label: sug.label, - prevIndex: sug.prevIndex - } - sug.documentation = JSON.stringify(data) - items.push(sug) - } else { - items.push({label: key}) +function provide(line: string, position: vscode.Position): vscode.CompletionItem[] { + // Compile the suggestion object to array + updateAll(line, position) + let keys = [...data.suggestions.keys(), ...data.prevIndexObj.keys()] + keys = Array.from(new Set(keys)) + const items: vscode.CompletionItem[] = [] + for (const key of keys) { + const suggestion = data.suggestions.get(key) + if (suggestion) { + const refDoc: ReferenceDocType = { + documentation: suggestion.documentation, + file: suggestion.file, + position: { + line: suggestion.position.line, + character: suggestion.position.character + }, + key, + label: suggestion.label, + prevIndex: suggestion.prevIndex } + suggestion.documentation = JSON.stringify(refDoc) + items.push(suggestion) + } else { + items.push({label: key}) } - return items } + return items +} - getRef(token: string): ReferenceEntry | undefined { - this.updateAll() - return this.suggestions.get(token) - } +function getItem(token: string): ReferenceEntry | undefined { + updateAll() + return data.suggestions.get(token) +} - private updateAll(line?: string, position?: vscode.Position) { - if (!lw.root.file.path) { - this.suggestions.clear() - return - } +function updateAll(line?: string, position?: vscode.Position) { + if (!lw.root.file.path) { + data.suggestions.clear() + return + } - const included: Set = new Set([lw.root.file.path]) - // Included files may originate from \input or `xr`. If the latter, a - // prefix may be used to ref to the file. The following obj holds them. - const prefixes: {[filePath: string]: string} = {} - while (true) { - // The process adds newly included file recursively, only stops when - // all have been found, i.e., no new ones - const startSize = included.size - included.forEach(cachedFile => { - lw.cache.getIncludedTeX(cachedFile).forEach(includedTeX => { - if (includedTeX === cachedFile) { - return - } - included.add(includedTeX) - // If the file is indeed included by \input, but was - // previously included by `xr`, the possible prefix is - // removed as it can be directly referenced without. - delete prefixes[includedTeX] - }) - const cache = lw.cache.get(cachedFile) - if (!cache) { + const included: Set = new Set([lw.root.file.path]) + // Included files may originate from \input or `xr`. If the latter, a + // prefix may be used to ref to the file. The following obj holds them. + const prefixes: {[filePath: string]: string} = {} + while (true) { + // The process adds newly included file recursively, only stops when + // all have been found, i.e., no new ones + const startSize = included.size + included.forEach(cachedFile => { + lw.cache.getIncludedTeX(cachedFile).forEach(includedTeX => { + if (includedTeX === cachedFile) { return } - Object.keys(cache.external).forEach(external => { - // Don't repeatedly add, no matter previously by \input or - // `xr` - if (included.has(external)) { - return - } - // If the file is included by `xr`, both file path and - // prefix is recorded. - included.add(external) - prefixes[external] = cache.external[external] - }) + included.add(includedTeX) + // If the file is indeed included by \input, but was + // previously included by `xr`, the possible prefix is + // removed as it can be directly referenced without. + delete prefixes[includedTeX] }) - if (included.size === startSize) { - break - } - } - - // Extract cached references - const refList: string[] = [] - let range: vscode.Range | undefined = undefined - if (line && position) { - range = computeFilteringRange(line, position) - } - - included.forEach(cachedFile => { - const cachedRefs = lw.cache.get(cachedFile)?.elements.reference - if (cachedRefs === undefined) { + const cache = lw.cache.get(cachedFile) + if (!cache) { return } - cachedRefs.forEach(ref => { - if (ref.range === undefined) { + Object.keys(cache.external).forEach(external => { + // Don't repeatedly add, no matter previously by \input or + // `xr` + if (included.has(external)) { return } - const label = (cachedFile in prefixes ? prefixes[cachedFile] : '') + ref.label - this.suggestions.set(label, {...ref, - label, - file: cachedFile, - position: 'inserting' in ref.range ? ref.range.inserting.start : ref.range.start, - range, - prevIndex: this.prevIndexObj.get(label) - }) - refList.push(label) + // If the file is included by `xr`, both file path and + // prefix is recorded. + included.add(external) + prefixes[external] = cache.external[external] }) }) - // Remove references that have been deleted - this.suggestions.forEach((_, key) => { - if (!refList.includes(key)) { - this.suggestions.delete(key) - } - }) + if (included.size === startSize) { + break + } } - parse(cache: FileCache) { - if (cache.ast !== undefined) { - const configuration = vscode.workspace.getConfiguration('latex-workshop') - const labelMacros = configuration.get('intellisense.label.command') as string[] - cache.elements.reference = this.parseAst(cache.ast, cache.content.split('\n'), labelMacros) - } else { - cache.elements.reference = this.parseContent(cache.content) - } + // Extract cached references + const refList: string[] = [] + let range: vscode.Range | undefined = undefined + if (line && position) { + range = computeFilteringRange(line, position) } - 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 - return [] + included.forEach(cachedFile => { + const cachedRefs = lw.cache.get(cachedFile)?.elements.reference + if (cachedRefs === undefined) { + return } - if (node.type === 'environment' && ['tikzpicture'].includes(node.env)) { - return [] + cachedRefs.forEach(ref => { + if (ref.range === undefined) { + return + } + const label = (cachedFile in prefixes ? prefixes[cachedFile] : '') + ref.label + data.suggestions.set(label, {...ref, + label, + file: cachedFile, + position: 'inserting' in ref.range ? ref.range.inserting.start : ref.range.start, + range, + prevIndex: data.prevIndexObj.get(label) + }) + refList.push(label) + }) + }) + // Remove references that have been deleted + data.suggestions.forEach((_, key) => { + if (!refList.includes(key)) { + data.suggestions.delete(key) } + }) +} - let label = '' - if (node.type === 'macro' && labelMacros.includes(node.content)) { - label = argContentToStr(node.args?.[1]?.content || []) - } else if (node.type === 'environment') { - label = argContentToStr(node.args?.[1]?.content || []) - const index = label.indexOf('label=') - if (index >= 0) { - label = label.slice(index + 6) - if (label.charAt(0) === '{') { - label = getLongestBalancedString(label) ?? '' - } else { - label = label.split(',')[0] ?? '' - } +function parse(cache: FileCache) { + if (cache.ast !== undefined) { + const configuration = vscode.workspace.getConfiguration('latex-workshop') + const labelMacros = configuration.get('intellisense.label.command') as string[] + cache.elements.reference = parseAst(cache.ast, cache.content.split('\n'), labelMacros) + } else { + cache.elements.reference = parseContent(cache.content) + } +} + +function 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 + return [] + } + if (node.type === 'environment' && ['tikzpicture'].includes(node.env)) { + return [] + } + + let label = '' + if (node.type === 'macro' && labelMacros.includes(node.content)) { + label = argContentToStr(node.args?.[1]?.content || []) + } else if (node.type === 'environment') { + label = argContentToStr(node.args?.[1]?.content || []) + const index = label.indexOf('label=') + if (index >= 0) { + label = label.slice(index + 6) + if (label.charAt(0) === '{') { + label = getLongestBalancedString(label) ?? '' } else { - label = '' + label = label.split(',')[0] ?? '' } + } else { + label = '' } + } - if (label !== '' && node.position !== undefined) { - refs.push({ - label, - kind: vscode.CompletionItemKind.Reference, - // One row before, four rows after - documentation: lines.slice(node.position.start.line - 2, node.position.end.line + 4).join('\n'), - // Here we abuse the definition of range to store the location of the reference definition - range: new vscode.Range(node.position.start.line - 1, node.position.start.column - 1, - node.position.end.line - 1, node.position.end.column - 1) - }) - } + if (label !== '' && node.position !== undefined) { + refs.push({ + label, + kind: vscode.CompletionItemKind.Reference, + // One row before, four rows after + documentation: lines.slice(node.position.start.line - 2, node.position.end.line + 4).join('\n'), + // Here we abuse the definition of range to store the location of the reference definition + range: new vscode.Range(node.position.start.line - 1, node.position.start.column - 1, + node.position.end.line - 1, node.position.end.column - 1) + }) + } - const parseContent = (content: Ast.Node[]) => { - for (const subNode of content) { - refs = [...refs, ...this.parseAst(subNode, lines, labelMacros)] - } + const parseContentNodes = (content: Ast.Node[]) => { + for (const subNode of content) { + refs = [...refs, ...parseAst(subNode, lines, labelMacros)] } - 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) { + parseContentNodes(arg.content) } - - return refs + } else if ('content' in node && typeof node.content !== 'string') { + parseContentNodes(node.content) } - private parseContent(content: string): CompletionItem[] { - const refReg = /(?:\\label(?:\[[^[\]{}]*\])?|(?:^|[,\s])label=){([^#\\}]*)}/gm - const refs: CompletionItem[] = [] - const refList: string[] = [] - content = stripEnvironments(content, ['']) - while (true) { - const result = refReg.exec(content) - if (result === null) { - break - } - if (refList.includes(result[1])) { - continue - } - const prevContent = content.substring(0, content.substring(0, result.index).lastIndexOf('\n') - 1) - const followLength = content.substring(result.index, content.length).split('\n', 4).join('\n').length - const positionContent = content.substring(0, result.index).split('\n') + return refs +} - refs.push({ - label: result[1], - kind: vscode.CompletionItemKind.Reference, - // One row before, four rows after - documentation: content.substring(prevContent.lastIndexOf('\n') + 1, result.index + followLength), - // Here we abuse the definition of range to store the location of the reference definition - range: new vscode.Range(positionContent.length - 1, positionContent[positionContent.length - 1].length, - positionContent.length - 1, positionContent[positionContent.length - 1].length) - }) - refList.push(result[1]) +function parseContent(content: string): CompletionItem[] { + const refReg = /(?:\\label(?:\[[^[\]{}]*\])?|(?:^|[,\s])label=){([^#\\}]*)}/gm + const refs: CompletionItem[] = [] + const refList: string[] = [] + content = stripEnvironments(content, ['']) + while (true) { + const result = refReg.exec(content) + if (result === null) { + break } - return refs - } + if (refList.includes(result[1])) { + continue + } + const prevContent = content.substring(0, content.substring(0, result.index).lastIndexOf('\n') - 1) + const followLength = content.substring(result.index, content.length).split('\n', 4).join('\n').length + const positionContent = content.substring(0, result.index).split('\n') - setNumbersFromAuxFile(rootFile: string) { - const outDir = lw.file.getOutDir(rootFile) - const rootDir = path.dirname(rootFile) - const auxFile = path.resolve(rootDir, path.join(outDir, path.basename(rootFile, '.tex') + '.aux')) - this.suggestions.forEach((entry) => { - entry.prevIndex = undefined + refs.push({ + label: result[1], + kind: vscode.CompletionItemKind.Reference, + // One row before, four rows after + documentation: content.substring(prevContent.lastIndexOf('\n') + 1, result.index + followLength), + // Here we abuse the definition of range to store the location of the reference definition + range: new vscode.Range(positionContent.length - 1, positionContent[positionContent.length - 1].length, + positionContent.length - 1, positionContent[positionContent.length - 1].length) }) - this.prevIndexObj = new Map() - if (!fs.existsSync(auxFile)) { - return + refList.push(result[1]) + } + return refs +} + +function setNumbersFromAuxFile(rootFile: string) { + const outDir = lw.file.getOutDir(rootFile) + const rootDir = path.dirname(rootFile) + const auxFile = path.resolve(rootDir, path.join(outDir, path.basename(rootFile, '.tex') + '.aux')) + data.suggestions.forEach((entry) => { + entry.prevIndex = undefined + }) + data.prevIndexObj = new Map() + if (!fs.existsSync(auxFile)) { + return + } + const newLabelReg = /^\\newlabel\{(.*?)\}\{\{(.*?)\}\{(.*?)\}/gm + const auxContent = fs.readFileSync(auxFile, {encoding: 'utf8'}) + while (true) { + const result = newLabelReg.exec(auxContent) + if (result === null) { + break } - const newLabelReg = /^\\newlabel\{(.*?)\}\{\{(.*?)\}\{(.*?)\}/gm - const auxContent = fs.readFileSync(auxFile, {encoding: 'utf8'}) - while (true) { - const result = newLabelReg.exec(auxContent) - if (result === null) { - break - } - if ( result[1].endsWith('@cref') && this.prevIndexObj.has(result[1].replace('@cref', '')) ) { - // Drop extra \newlabel entries added by cleveref - continue - } - this.prevIndexObj.set(result[1], {refNumber: result[2], pageNumber: result[3]}) - const ent = this.suggestions.get(result[1]) - if (ent) { - ent.prevIndex = {refNumber: result[2], pageNumber: result[3]} - } + if ( result[1].endsWith('@cref') && data.prevIndexObj.has(result[1].replace('@cref', '')) ) { + // Drop extra \newlabel entries added by cleveref + continue + } + data.prevIndexObj.set(result[1], {refNumber: result[2], pageNumber: result[3]}) + const ent = data.suggestions.get(result[1]) + if (ent) { + ent.prevIndex = {refNumber: result[2], pageNumber: result[3]} } } } diff --git a/src/completion/index.ts b/src/completion/index.ts index a746ba423..d2193f4f1 100644 --- a/src/completion/index.ts +++ b/src/completion/index.ts @@ -1,9 +1,23 @@ import { citation } from './completer/citation' import { environment } from './completer/environment' import { macro } from './completer/macro' +import { reference } from './completer/reference' +import { usepackage } from './completer/package' +import { input } from './completer/input' +import { glossary } from './completer/glossary' + +import { Provider, AtProvider } from './latex' +import { BibProvider } from './bibtex' export const completion = { citation, environment, - macro + macro, + reference, + usepackage, + input, + glossary, + provider: new Provider(), + atProvider: new AtProvider(), + bibProvider: new BibProvider() } diff --git a/src/completion/latex.ts b/src/completion/latex.ts index d12a39ef5..73061852e 100644 --- a/src/completion/latex.ts +++ b/src/completion/latex.ts @@ -1,107 +1,22 @@ import * as vscode from 'vscode' -import * as fs from 'fs' -import * as path from 'path' import { lw } from '../lw' -import { CompletionArgs, CompletionProvider, Package } from '../types' +import type { CompletionArgs, CompletionProvider, ReferenceDocType } from '../types' import { citation, provider as citationProvider } from './completer/citation' -import { DocumentClass } from './completer/documentclass' -import { environment, provider as environmentProvider } from './completer/environment' -import { macro, provider as macroProvider } from './completer/macro' +import { provider as environmentProvider } from './completer/environment' +import { 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 as PackageCompletion } from './completer/package' -import { Input, Import, SubImport } from './completer/input' -import { Glossary } from './completer/glossary' -import type { ReferenceDocType } from './completer/reference' +import { provider as classProvider } from './completer/class' +import { provider as referenceProvider } from './completer/reference' +import { provider as packageProvider } from './completer/package' +import { inputProvider, importProvider, subimportProvider } from './completer/input' +import { provider as glossaryProvider } from './completer/glossary' +import { atSuggestion, provider as atProvider } from './completer/atsuggestion' + import { escapeRegExp } from '../utils/utils' const logger = lw.log('Intelli') -export class Completer implements vscode.CompletionItemProvider { - readonly documentClass: DocumentClass - readonly reference: Reference - readonly package: PackageCompletion - readonly input: Input - readonly import: Import - readonly subImport: SubImport - readonly glossary: Glossary - - private readonly packagesLoaded: string[] = [] - - constructor() { - this.documentClass = new DocumentClass() - this.reference = new Reference() - this.package = new PackageCompletion() - this.input = new Input() - this.import = new Import() - this.subImport = new SubImport() - this.glossary = new Glossary() - } - - loadPackageData(packageName: string) { - if (this.packagesLoaded.includes(packageName)) { - return - } - - const filePath: string | undefined = this.resolvePackageFile(packageName) - if (filePath === undefined) { - this.packagesLoaded.push(packageName) - return - } - - try { - const packageData = JSON.parse(fs.readFileSync(filePath).toString()) as Package - this.populatePackageData(packageData) - - this.package.setPackageDeps(packageName, packageData.includes) - macro.setPackageCmds(packageName, packageData.macros) - environment.setPackageEnvs(packageName, packageData.envs) - this.package.setPackageOptions(packageName, packageData.options) - - this.packagesLoaded.push(packageName) - } catch (e) { - logger.log(`Cannot parse intellisense file: ${filePath}`) - } - } - - private resolvePackageFile(packageName: string): string | undefined { - const defaultDir = `${lw.extensionRoot}/data/packages/` - const dirs = vscode.workspace.getConfiguration('latex-workshop').get('intellisense.package.dirs') as string[] - dirs.push(defaultDir) - for (const dir of dirs) { - const filePath = path.resolve(dir, `${packageName}.json`) - if (fs.existsSync(filePath)) { - return filePath - } - } - // Many package with names like toppackage-config.sty are just wrappers around - // the general package toppacke.sty and do not define commands on their own. - const indexDash = packageName.lastIndexOf('-') - if (indexDash > - 1) { - const generalPkg = packageName.substring(0, indexDash) - const filePath = path.resolve(defaultDir, `${generalPkg}.json`) - if (fs.existsSync(filePath)) { - return filePath - } - } - return - } - - 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] - }) - Object.entries(packageData.envs).forEach(([key, env]) => { - env.detail = key - env.name = env.name || key - env.snippet = env.snippet || '' - env.keyvals = packageData.keyvals[env.keyvalindex ?? -1] - }) - } - +export class Provider implements vscode.CompletionItemProvider { provideCompletionItems( document: vscode.TextDocument, position: vscode.Position @@ -192,7 +107,7 @@ export class Completer implements vscode.CompletionItemProvider { break case 'reference': reg = /(?:\\hyperref\[([^\]]*)(?!\])$)|(?:(?:\\(?!hyper)[a-zA-Z]*ref[a-zA-Z]*\*?(?:\[[^[\]]*\])?){([^}]*)$)|(?:\\[Cc][a-z]*refrange\*?{[^{}]*}{([^}]*)$)/ - provider = this.reference + provider = referenceProvider break case 'environment': reg = /(?:\\begin|\\end){([^}]*)$/ @@ -208,31 +123,31 @@ export class Completer implements vscode.CompletionItemProvider { break case 'package': reg = /(?:\\usepackage(?:\[[^[\]]*\])*){([^}]*)$/ - provider = this.package + provider = packageProvider break case 'documentclass': reg = /(?:\\documentclass(?:\[[^[\]]*\])*){([^}]*)$/ - provider = this.documentClass + provider = classProvider break case 'input': reg = /\\(input|include|subfile|subfileinclude|includegraphics|includesvg|lstinputlisting|verbatiminput|loadglsentries|markdownInput)\*?(?:\[[^[\]]*\])*{([^}]*)$/ - provider = this.input + provider = inputProvider break case 'includeonly': reg = /\\(includeonly|excludeonly){(?:{[^}]*},)*(?:[^,]*,)*{?([^},]*)$/ - provider = this.input + provider = inputProvider break case 'import': reg = /\\(import|includefrom|inputfrom)\*?(?:{([^}]*)})?{([^}]*)$/ - provider = this.import + provider = importProvider break case 'subimport': reg = /\\(sub(?:import|includefrom|inputfrom))\*?(?:{([^}]*)})?{([^}]*)$/ - provider = this.subImport + provider = subimportProvider break case 'glossary': reg = /\\(gls(?:pl|text|first|fmt(?:text|short|long)|plural|firstplural|name|symbol|desc|disp|user(?:i|ii|iii|iv|v|vi))?|Acr(?:long|full|short)?(?:pl)?|ac[slf]?p?)(?:\[[^[\]]*\])?{([^}]*)$/i - provider = this.glossary + provider = glossaryProvider break default: // This shouldn't be possible, so mark as error case in log. @@ -252,20 +167,18 @@ export class Completer implements vscode.CompletionItemProvider { } } -export class AtSuggestionCompleter implements vscode.CompletionItemProvider { - private atSuggestion: AtSuggestion - private triggerCharacter: string +export class AtProvider implements vscode.CompletionItemProvider { + private reg: RegExp = new RegExp('@[^\\\\s]*$') constructor() { - const configuration = vscode.workspace.getConfiguration('latex-workshop') - this.triggerCharacter = configuration.get('intellisense.atSuggestion.trigger.latex') as string - this.atSuggestion = new AtSuggestion(this.triggerCharacter) + this.updateTrigger() } updateTrigger() { const configuration = vscode.workspace.getConfiguration('latex-workshop') - this.triggerCharacter = configuration.get('intellisense.atSuggestion.trigger.latex') as string - this.atSuggestion = new AtSuggestion(this.triggerCharacter) + const triggerCharacter = configuration.get('intellisense.atSuggestion.trigger.latex') as string + atSuggestion.initialize(triggerCharacter) + this.reg = new RegExp(escapeRegExp(triggerCharacter) + '[^\\\\s]*$') } provideCompletionItems( @@ -281,12 +194,10 @@ export class AtSuggestionCompleter implements vscode.CompletionItemProvider { } 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) + const result = args.line.substring(0, args.position.character).match(this.reg) let suggestions: vscode.CompletionItem[] = [] if (result) { - suggestions = this.atSuggestion.from(result, args) + suggestions = atProvider.from(result, args) } return suggestions } diff --git a/src/core/cache.ts b/src/core/cache.ts index c00e25343..ef0ad1b70 100644 --- a/src/core/cache.ts +++ b/src/core/cache.ts @@ -363,12 +363,12 @@ function updateElements(fileCache: FileCache) { const start = performance.now() 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.completion.usepackage.parse(fileCache) + lw.completion.reference.parse(fileCache) + lw.completion.glossary.parse(fileCache) lw.completion.environment.parse(fileCache) lw.completion.macro.parse(fileCache) - lw.completer.input.parseGraphicsPath(fileCache) + lw.completion.input.parseGraphicsPath(fileCache) updateBibfiles(fileCache) const elapsed = performance.now() - start logger.log(`Updated elements in ${elapsed.toFixed(2)} ms: ${fileCache.filePath} .`) diff --git a/src/core/root.ts b/src/core/root.ts index 5cef6b142..1134be586 100644 --- a/src/core/root.ts +++ b/src/core/root.ts @@ -66,7 +66,7 @@ async function find(): Promise { lw.event.fire(lw.event.RootFileChanged, rootFilePath) // We also clean the completions from the old project - lw.completer.input.reset() + lw.completion.input.reset() lw.lint.label.reset() lw.cache.reset() lw.cache.add(rootFilePath) diff --git a/src/language/definition.ts b/src/language/definition.ts index 876767b8e..726e98867 100644 --- a/src/language/definition.ts +++ b/src/language/definition.ts @@ -56,7 +56,7 @@ export class DefinitionProvider implements vscode.DefinitionProvider { } return } - const ref = lw.completer.reference.getRef(token) + const ref = lw.completion.reference.getItem(token) if (ref) { return new vscode.Location(vscode.Uri.file(ref.file), ref.position) } @@ -64,7 +64,7 @@ export class DefinitionProvider implements vscode.DefinitionProvider { if (cite) { return new vscode.Location(vscode.Uri.file(cite.file), cite.position) } - const glossary = lw.completer.glossary.getEntry(token) + const glossary = lw.completion.glossary.getItem(token) if (glossary) { return new vscode.Location(vscode.Uri.file(glossary.filePath), glossary.position) } diff --git a/src/lw.ts b/src/lw.ts index 88a2ceb62..83ec5db89 100644 --- a/src/lw.ts +++ b/src/lw.ts @@ -14,7 +14,6 @@ import type { outline } from './outline' import type { parse } from './parse' import type { extra } from './extras' -import type { AtSuggestionCompleter, Completer } from './completion/latex' import type * as commands from './core/commands' /* eslint-disable */ @@ -34,8 +33,6 @@ export const lw = { 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, outline: {} as typeof outline, extra: {} as typeof extra, diff --git a/src/main.ts b/src/main.ts index 053c8ab6b..d502dc54d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,6 +4,8 @@ import { lw } from './lw' lw.extensionRoot = path.resolve(`${__dirname}/../../`) import { log } from './utils/logger' lw.log = log.getLogger +const logger = lw.log('Extension') +logger.log('Initializing LaTeX Workshop.') import { event } from './core/event' lw.event = event import { file } from './core/file' @@ -38,22 +40,15 @@ lw.commands = commander log.initStatusBarItem() -import { BibtexCompleter } from './completion/bibtex' import { DocSymbolProvider } from './language/symbol-document' import { ProjectSymbolProvider } from './language/symbol-project' import { DefinitionProvider } from './language/definition' import { FoldingProvider, WeaveFoldingProvider } from './language/folding' import { SelectionRangeProvider } from './language/selection' -import { AtSuggestionCompleter, Completer } from './completion/latex' - -const logger = lw.log('Extension') - -function initialize() { - lw.completer = new Completer() - lw.atSuggestionCompleter = new AtSuggestionCompleter() +export function activate(extensionContext: vscode.ExtensionContext) { + void vscode.commands.executeCommand('setContext', 'latex-workshop:enabled', true) - logger.log('Initializing LaTeX Workshop.') logger.log(`Extension root: ${lw.extensionRoot}`) logger.log(`$PATH: ${process.env.PATH}`) logger.log(`$SHELL: ${process.env.SHELL}`) @@ -66,13 +61,6 @@ function initialize() { logger.log(`vscode.env.uiKind: ${vscode.env.uiKind}`) log.logConfig() log.logDeprecatedConfig() - logger.log('LaTeX Workshop initialized.') -} - -export function activate(extensionContext: vscode.ExtensionContext) { - void vscode.commands.executeCommand('setContext', 'latex-workshop:enabled', true) - - initialize() lw.onDispose(undefined, extensionContext.subscriptions) @@ -166,6 +154,8 @@ export function activate(extensionContext: vscode.ExtensionContext) { } }) conflictCheck() + + logger.log('LaTeX Workshop initialized.') } function registerLatexWorkshopCommands(extensionContext: vscode.ExtensionContext) { @@ -279,8 +269,8 @@ function registerProviders(extensionContext: vscode.ExtensionContext) { ) extensionContext.subscriptions.push( - vscode.languages.registerCompletionItemProvider({ scheme: 'file', language: 'tex'}, lw.completer, '\\', '{'), - vscode.languages.registerCompletionItemProvider(bibtexSelector, new BibtexCompleter(), '@') + vscode.languages.registerCompletionItemProvider({ scheme: 'file', language: 'tex'}, lw.completion.provider, '\\', '{'), + vscode.languages.registerCompletionItemProvider(bibtexSelector, lw.completion.bibProvider, '@') ) let triggerDisposable: vscode.Disposable | undefined @@ -289,7 +279,7 @@ function registerProviders(extensionContext: vscode.ExtensionContext) { const latexTriggers = ['\\', ','].concat(userTriggersLatex) logger.log(`Trigger characters for intellisense of LaTeX documents: ${JSON.stringify(latexTriggers)}`) - triggerDisposable = vscode.languages.registerCompletionItemProvider(latexDoctexSelector, lw.completer, ...latexTriggers) + triggerDisposable = vscode.languages.registerCompletionItemProvider(latexDoctexSelector, lw.completion.provider, ...latexTriggers) extensionContext.subscriptions.push(triggerDisposable) } registerTrigger() @@ -305,8 +295,8 @@ function registerProviders(extensionContext: vscode.ExtensionContext) { const registerAtSuggestion = () => { const atSuggestionLatexTrigger = vscode.workspace.getConfiguration('latex-workshop').get('intellisense.atSuggestion.trigger.latex') as string if (atSuggestionLatexTrigger !== '') { - lw.atSuggestionCompleter.updateTrigger() - atSuggestionDisposable = vscode.languages.registerCompletionItemProvider(latexDoctexSelector, lw.atSuggestionCompleter, atSuggestionLatexTrigger) + lw.completion.atProvider.updateTrigger() + atSuggestionDisposable = vscode.languages.registerCompletionItemProvider(latexDoctexSelector, lw.completion.atProvider, atSuggestionLatexTrigger) extensionContext.subscriptions.push(atSuggestionDisposable) } } diff --git a/src/preview/graphics.ts b/src/preview/graphics.ts index fd1ac40ac..e1523be09 100644 --- a/src/preview/graphics.ts +++ b/src/preview/graphics.ts @@ -112,7 +112,7 @@ function findFilePath(relPath: string, document: vscode.TextDocument): string | } const activeDir = path.dirname(document.uri.fsPath) - for (const dirPath of lw.completer.input.graphicsPath) { + for (const dirPath of lw.completion.input.graphicsPath) { const filePath = path.resolve(activeDir, dirPath, relPath) if (fs.existsSync(filePath)) { return filePath diff --git a/src/preview/hover.ts b/src/preview/hover.ts index 027c38269..c3bcfff74 100644 --- a/src/preview/hover.ts +++ b/src/preview/hover.ts @@ -48,7 +48,7 @@ class HoverProvider implements vscode.HoverProvider { const ctanLink = new vscode.MarkdownString(`[${ctanUrl}](${ctanUrl})`) return new vscode.Hover([md, mdLink, ctanLink]) } - const refData = lw.completer.reference.getRef(token) + const refData = lw.completion.reference.getItem(token) if (hovReference && refData) { const hover = await lw.preview.math.onRef(document, position, refData, token, ctoken) return hover @@ -72,7 +72,7 @@ class HoverProvider implements vscode.HoverProvider { const packageCmds: CmdEnvSuggestion[] = [] const configuration = vscode.workspace.getConfiguration('latex-workshop') if ((configuration.get('intellisense.package.enabled'))) { - const packages = lw.completer.package.getPackagesIncluded('latex-expl3') + const packages = lw.completion.usepackage.getAll('latex-expl3') Object.entries(packages).forEach(([packageName, options]) => { lw.completion.macro.provideCmdInPkg(packageName, options, packageCmds) lw.completion.environment.provideEnvsAsCommandInPkg(packageName, options, packageCmds) diff --git a/src/preview/math.ts b/src/preview/math.ts index f5565f720..0ddcdf07a 100644 --- a/src/preview/math.ts +++ b/src/preview/math.ts @@ -4,9 +4,8 @@ import * as workerpool from 'workerpool' import type { SupportedExtension } from 'mathjax-full' import type { IMathJaxWorker } from './math/mathjax' import { lw } from '../lw' -import type { TeXMathEnv } from '../types' +import type { ReferenceEntry, TeXMathEnv } from '../types' import * as utils from '../utils/svg' -import type { ReferenceEntry } from '../completion/completer/reference' import { getCurrentThemeLightness } from '../utils/theme' import { renderCursor as renderCursorWorker } from './math/mathpreviewlib/cursorrenderer' import { type ITextDocumentLike, TextDocumentLike } from './math/mathpreviewlib/textdocumentlike' diff --git a/src/preview/math/mathpreviewlib/hoverpreviewonref.ts b/src/preview/math/mathpreviewlib/hoverpreviewonref.ts index cea7bd792..897a0a095 100644 --- a/src/preview/math/mathpreviewlib/hoverpreviewonref.ts +++ b/src/preview/math/mathpreviewlib/hoverpreviewonref.ts @@ -1,8 +1,7 @@ import * as vscode from 'vscode' import { lw } from '../../../lw' -import type { TeXMathEnv } from '../../../types' +import type { ReferenceEntry, TeXMathEnv } from '../../../types' import * as utils from '../../../utils/svg' -import type { ReferenceEntry } from '../../../completion/completer/reference' import { MathPreviewUtils } from './mathpreviewutils' const logger = lw.log('Preview', 'Hover') diff --git a/src/preview/math/mathpreviewlib/texmathenvfinder.ts b/src/preview/math/mathpreviewlib/texmathenvfinder.ts index 823b6b232..6ae33c94a 100644 --- a/src/preview/math/mathpreviewlib/texmathenvfinder.ts +++ b/src/preview/math/mathpreviewlib/texmathenvfinder.ts @@ -1,8 +1,7 @@ import * as vscode from 'vscode' -import type { TeXMathEnv } from '../../../types' +import type { ReferenceEntry, TeXMathEnv } from '../../../types' import * as utils from '../../../utils/utils' import { type ITextDocumentLike, TextDocumentLike } from './textdocumentlike' -import type { ReferenceEntry } from '../../../completion/completer/reference' const ENV_NAMES = [ 'align', 'align\\*', 'alignat', 'alignat\\*', 'aligned', 'alignedat', 'array', 'Bmatrix', 'bmatrix', 'cases', 'CD', 'eqnarray', 'eqnarray\\*', 'equation', 'equation\\*', 'flalign', 'flalign\\*', 'gather', 'gather\\*', 'gathered', 'matrix', 'multline', 'multline\\*', 'pmatrix', 'smallmatrix', 'split', 'subarray', 'Vmatrix', 'vmatrix' diff --git a/src/types.ts b/src/types.ts index c3f0b475a..bd2559c75 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,7 +1,6 @@ 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 { GlossarySuggestion } from './completion/completer/glossary' export type FileCache = { /** The raw file path of this Cache. */ @@ -15,7 +14,7 @@ export type FileCache = { /** \ref{} items */ reference?: CompletionItem[], /** \gls items */ - glossary?: GlossarySuggestion[], + glossary?: GlossaryItem[], /** \begin{} items */ environment?: CmdEnvSuggestion[], /** \cite{} items from \bibitem definition */ @@ -212,3 +211,32 @@ export type Macro = { /** The action to be executed after inserting the snippet */ postAction?: string } + +export interface ReferenceEntry extends CompletionItem { + /** The file that defines the ref. */ + file: string, + /** The position that defines the ref. */ + position: vscode.Position, + /** Stores the ref number. */ + prevIndex?: {refNumber: string, pageNumber: string} +} + +export type ReferenceDocType = { + documentation: ReferenceEntry['documentation'], + file: ReferenceEntry['file'], + position: {line: number, character: number}, + key: string, + label: ReferenceEntry['label'], + prevIndex: ReferenceEntry['prevIndex'] +} + +export enum GlossaryType { + glossary, + acronym +} + +export interface GlossaryItem extends CompletionItem { + type: GlossaryType, + filePath: string, + position: vscode.Position +} diff --git a/test/suites/utils.ts b/test/suites/utils.ts index 0b482bd01..ab4a81770 100644 --- a/test/suites/utils.ts +++ b/test/suites/utils.ts @@ -77,7 +77,7 @@ export async function reset() { await Promise.all(Object.values(lw.cache.promises)) lw.root.file.path = undefined lw.root.subfiles.path = undefined - lw.completer.input.reset() + lw.completion.input.reset() lw.lint.label.reset() lw.cache.reset() glob.sync('**/{**.tex,**.pdf,**.bib}', { cwd: getFixture() }).forEach(file => { try {fs.unlinkSync(path.resolve(getFixture(), file))} catch {} }) @@ -199,7 +199,7 @@ export function suggest(row: number, col: number, isAtSuggestion = false, openFi const lines = lw.cache.get(openFile ?? lw.root.file.path)?.content?.split('\n') ok(lines) logger.log('Get suggestion.') - const items = (isAtSuggestion ? lw.atSuggestionCompleter : lw.completer).provide({ + const items = (isAtSuggestion ? lw.completion.atProvider : lw.completion.provider).provide({ uri: vscode.Uri.file(openFile ?? lw.root.file.path), langId: 'latex', line: lines[row],