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