Skip to content

Commit

Permalink
Fix #4045 Use AST to parse new commands for preview
Browse files Browse the repository at this point in the history
  • Loading branch information
James-Yu committed Dec 19, 2023
1 parent 6664026 commit f053ac4
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 63 deletions.
4 changes: 2 additions & 2 deletions src/preview/hover.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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
}
Expand Down
8 changes: 4 additions & 4 deletions src/preview/math.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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)
}
}
Expand All @@ -103,7 +103,7 @@ function refNumberMessage(refData: Pick<ReferenceEntry, 'prevIndex'>): 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)
Expand Down Expand Up @@ -142,7 +142,7 @@ function findRef(
}

async function renderSvgOnRef(tex: TeXMathEnv, refData: Pick<ReferenceEntry, 'label' | 'prevIndex'>, ctoken: vscode.CancellationToken) {
const newCommand = await findProjectNewCommand(ctoken)
const newCommand = await findNewCommand(ctoken)
return HoverPreviewOnRefProvider.renderSvgOnRef(tex, newCommand, refData, color)
}

Expand Down
121 changes: 64 additions & 57 deletions src/preview/math/mathpreviewlib/newcommandfinder.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
export async function findNewCommand(ctoken?: vscode.CancellationToken): Promise<string> {
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<string | undefined> {
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
}

0 comments on commit f053ac4

Please sign in to comment.