Ref suggestions now use unified-latex

This commit is contained in:
James Yu 2023-05-27 00:56:51 +08:00
parent f6fb7927f6
commit 2c98d3b8a8
10 changed files with 96 additions and 132 deletions

2
.vscode/launch.json vendored
View File

@ -33,7 +33,7 @@
"preLaunchTask": "task-watch-all",
"env": {
"LATEXWORKSHOP_CI": "1",
"LATEXWORKSHOP_SUITE": ""
"LATEXWORKSHOP_SUITE": "04"
}
},
{

View File

@ -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)

View File

@ -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)
}

View File

@ -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 = {}

View File

@ -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) {

View File

@ -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()
}
})
}

View File

@ -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
View 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('')
}

View File

@ -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}

View File

@ -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, [