From f053ac4e6b2d568e5c3e0c0f01eccc424084d860 Mon Sep 17 00:00:00 2001 From: James Yu Date: Tue, 19 Dec 2023 13:02:15 +0000 Subject: [PATCH] Fix #4045 Use AST to parse new commands for preview --- src/preview/hover.ts | 4 +- src/preview/math.ts | 8 +- .../math/mathpreviewlib/newcommandfinder.ts | 121 +++++++++--------- 3 files changed, 70 insertions(+), 63 deletions(-) diff --git a/src/preview/hover.ts b/src/preview/hover.ts index c3bcfff74..aff364f59 100644 --- a/src/preview/hover.ts +++ b/src/preview/hover.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode' import { lw } from '../lw' +import { findNewCommand } from './math/mathpreviewlib/newcommandfinder' import { tokenizer, onAPackage } from '../utils/tokenizer' -import { findProjectNewCommand } from '../preview/math/mathpreviewlib/newcommandfinder' import { CmdEnvSuggestion } from '../completion/completer/completerutils' export { @@ -19,7 +19,7 @@ class HoverProvider implements vscode.HoverProvider { if (hov) { const tex = lw.preview.math.findTeX(document, position) if (tex) { - const newCommands = await findProjectNewCommand(ctoken) + const newCommands = await findNewCommand(ctoken) const hover = await lw.preview.math.onTeX(document, tex, newCommands) return hover } diff --git a/src/preview/math.ts b/src/preview/math.ts index 0ddcdf07a..791fbe64c 100644 --- a/src/preview/math.ts +++ b/src/preview/math.ts @@ -9,7 +9,7 @@ import * as utils from '../utils/svg' import { getCurrentThemeLightness } from '../utils/theme' import { renderCursor as renderCursorWorker } from './math/mathpreviewlib/cursorrenderer' import { type ITextDocumentLike, TextDocumentLike } from './math/mathpreviewlib/textdocumentlike' -import { findProjectNewCommand } from './math/mathpreviewlib/newcommandfinder' +import { findNewCommand } from './math/mathpreviewlib/newcommandfinder' import { TeXMathEnvFinder } from './math/mathpreviewlib/texmathenvfinder' import { HoverPreviewOnRefProvider } from './math/mathpreviewlib/hoverpreviewonref' import { MathPreviewUtils } from './math/mathpreviewlib/mathpreviewutils' @@ -80,7 +80,7 @@ async function onRef( if (configuration.get('hover.ref.enabled') as boolean) { const tex = TeXMathEnvFinder.findHoverOnRef(document, position, refData, token) if (tex) { - const newCommands = await findProjectNewCommand(ctoken) + const newCommands = await findNewCommand(ctoken) return HoverPreviewOnRefProvider.provideHoverPreviewOnRef(tex, newCommands, refData, color) } } @@ -103,7 +103,7 @@ function refNumberMessage(refData: Pick): string | } async function generateSVG(tex: TeXMathEnv, newCommandsArg?: string) { - const newCommands: string = newCommandsArg ?? await findProjectNewCommand() + const newCommands: string = newCommandsArg ?? await findNewCommand() const configuration = vscode.workspace.getConfiguration('latex-workshop') const scale = configuration.get('hover.preview.scale') as number const s = MathPreviewUtils.mathjaxify(tex.texString, tex.envname) @@ -142,7 +142,7 @@ function findRef( } async function renderSvgOnRef(tex: TeXMathEnv, refData: Pick, ctoken: vscode.CancellationToken) { - const newCommand = await findProjectNewCommand(ctoken) + const newCommand = await findNewCommand(ctoken) return HoverPreviewOnRefProvider.renderSvgOnRef(tex, newCommand, refData, color) } diff --git a/src/preview/math/mathpreviewlib/newcommandfinder.ts b/src/preview/math/mathpreviewlib/newcommandfinder.ts index 6b5e79456..83a62c47f 100644 --- a/src/preview/math/mathpreviewlib/newcommandfinder.ts +++ b/src/preview/math/mathpreviewlib/newcommandfinder.ts @@ -1,86 +1,93 @@ import * as vscode from 'vscode' import * as path from 'path' +import type * as Ast from '@unified-latex/unified-latex-types' import { lw } from '../../../lw' -import { stripCommentsAndVerbatim } from '../../../utils/utils' const logger = lw.log('Preview', 'Math') -export async function findProjectNewCommand(ctoken?: vscode.CancellationToken): Promise { +export async function findNewCommand(ctoken?: vscode.CancellationToken): Promise { + let newcommand = '' + const filepaths = [] const configuration = vscode.workspace.getConfiguration('latex-workshop') - const newCommandFile = configuration.get('hover.preview.newcommand.newcommandFile') as string - let commandsInConfigFile = '' - if (newCommandFile !== '') { - commandsInConfigFile = await loadNewCommandFromConfigFile(newCommandFile) + const newcommandPath = await resolveNewCommandFile(configuration.get('hover.preview.newcommand.newcommandFile') as string) + if (newcommandPath !== undefined) { + filepaths.push(newcommandPath) + if (lw.cache.get(newcommandPath) === undefined) { + lw.cache.add(newcommandPath) + } } - - if (!configuration.get('hover.preview.newcommand.parseTeXFile.enabled')) { - return commandsInConfigFile + if (configuration.get('hover.preview.newcommand.parseTeXFile.enabled') as boolean) { + lw.cache.getIncludedTeX().forEach(filepath => filepaths.push(filepath)) } - let commands: string[] = [] - for (const tex of lw.cache.getIncludedTeX()) { + for (const filepath of filepaths) { if (ctoken?.isCancellationRequested) { return '' } - await lw.cache.wait(tex) - const content = lw.cache.get(tex)?.content - if (content === undefined) { - continue + await lw.cache.wait(filepath) + const content = lw.cache.get(filepath)?.content + const ast = lw.cache.get(filepath)?.ast + if (content === undefined || ast === undefined) { + logger.log(`Cannot parse the AST of ${filepath} .`) + } else { + newcommand += parseAst(content, ast).join('\n') + '\n' } - commands = commands.concat(findNewCommand(content)) } - return commandsInConfigFile + '\n' + postProcessNewCommands(commands.join('')) + + return newcommand } -function postProcessNewCommands(commands: string): string { - return commands.replace(/\\providecommand/g, '\\newcommand') - .replace(/\\newcommand\*/g, '\\newcommand') - .replace(/\\renewcommand\*/g, '\\renewcommand') - .replace(/\\DeclarePairedDelimiter{(\\[a-zA-Z]+)}{([^{}]*)}{([^{}]*)}/g, '\\newcommand{$1}[2][]{#1$2 #2 #1$3}') +function parseAst(content: string, node: Ast.Node): string[] { + let macros = [] + const args = node.type === 'macro' && node.args + // \newcommand{\fix}[3][]{\chdeleted{#2}\chadded[comment={#1}]{#3}} + // \newcommand\WARNING{\textcolor{red}{WARNING}} + const isNewCommand = node.type === 'macro' && + ['renewcommand', 'newcommand'].includes(node.content) && + node.args?.[2]?.content?.[0]?.type === 'macro' + // \DeclarePairedDelimiterX\braketzw[2]{\langle}{\rangle}{#1\,\delimsize\vert\,\mathopen{}#2} + const isDeclarePairedDelimiter = node.type === 'macro' && + ['DeclarePairedDelimiter', 'DeclarePairedDelimiterX', 'DeclarePairedDelimiterXPP'].includes(node.content) && + node.args?.[0]?.content?.[0]?.type === 'macro' + const isProvideCommand = node.type === 'macro' && + ['providecommand', 'DeclareMathOperator', 'DeclareRobustCommand'].includes(node.content) && + node.args?.[1]?.content?.[0]?.type === 'macro' + if (args && (isNewCommand || isDeclarePairedDelimiter || isProvideCommand)) { + // \newcommand{\fix}[3][]{\chdeleted{#2}\chadded[comment={#1}]{#3}} + // \newcommand\WARNING{\textcolor{red}{WARNING}} + const start = node.position?.start.offset ?? 0 + const lastArg = args[args.length - 1] + const end = lastArg.content[lastArg.content.length - 1].position?.end.offset ?? -1 + macros.push(content.slice(start, end + 1)) + } + + if ('content' in node && typeof node.content !== 'string') { + for (const subNode of node.content) { + macros = [...macros, ...parseAst(content, subNode)] + } + } + return macros } -async function loadNewCommandFromConfigFile(newCommandFile: string) { - let commandsString: string | undefined = '' - if (newCommandFile === '') { - return commandsString +async function resolveNewCommandFile(filepath: string): Promise { + if (filepath === '') { + return undefined } - let newCommandFileAbs: string - if (path.isAbsolute(newCommandFile)) { - newCommandFileAbs = newCommandFile + let filepathAbs: string + if (path.isAbsolute(filepath)) { + filepathAbs = filepath } else { if (lw.root.file.path === undefined) { await lw.root.find() } const rootDir = lw.root.dir.path if (rootDir === undefined) { - logger.log(`Cannot identify the absolute path of new command file ${newCommandFile} without root file.`) - return '' + logger.log(`Cannot identify the absolute path of new command file ${filepath} without root file.`) + return undefined } - newCommandFileAbs = path.join(rootDir, newCommandFile) + filepathAbs = path.join(rootDir, filepath) } - commandsString = lw.file.read(newCommandFileAbs) - if (commandsString === undefined) { - logger.log(`Cannot read file ${newCommandFileAbs}`) - return '' + if (await lw.file.exists(vscode.Uri.file(filepathAbs))) { + return filepathAbs } - commandsString = commandsString.replace(/^\s*$/gm, '') - commandsString = postProcessNewCommands(commandsString) - return commandsString -} - -function findNewCommand(content: string): string[] { - const commands: string[] = [] - const regex = /(\\(?:(?:(?:(?:re)?new|provide)command|DeclareMathOperator)(\*)?{\\[a-zA-Z]+}(?:\[[^[\]{}]*\])*{.*})|\\(?:def\\[a-zA-Z]+(?:#[0-9])*{.*})|\\DeclarePairedDelimiter{\\[a-zA-Z]+}{[^{}]*}{[^{}]*})/gm - const noCommentContent = stripCommentsAndVerbatim(content) - let result: RegExpExecArray | null - do { - result = regex.exec(noCommentContent) - if (result) { - let command = result[1] - if (result[2]) { - command = command.replace('*', '') - } - commands.push(command) - } - } while (result) - return commands + return undefined }