diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..e4d360099 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.ftl linguist-detectable=false \ No newline at end of file diff --git a/package.json b/package.json index f506a06c9..2a9144129 100644 --- a/package.json +++ b/package.json @@ -359,8 +359,8 @@ "category": "LaTeX Workshop" }, { - "command": "latex-workshop.kill", - "title": "Kill LaTeX compiler process", + "command": "latex-workshop.terminate", + "title": "Terminate LaTeX compiler process", "category": "LaTeX Workshop", "enablement": "!virtualWorkspace" }, diff --git a/src/core/commands.ts b/src/commands.ts similarity index 89% rename from src/core/commands.ts rename to src/commands.ts index a2bf1b493..ab50d5a88 100644 --- a/src/core/commands.ts +++ b/src/commands.ts @@ -1,47 +1,16 @@ import * as vscode from 'vscode' import * as path from 'path' -import * as lw from '../lw' -import { getSurroundingCommandRange, stripText } from '../utils/utils' -import { getLogger } from '../utils/logging/logger' -import { parser } from '../parse/parser' +import * as lw from './lw' +import { getSurroundingCommandRange, stripText } from './utils/utils' +import { getLogger } from './utils/logging/logger' +import { parser } from './parse/parser' +import { extension } from './extension' const logger = getLogger('Commander') export async function build(skipSelection: boolean = false, rootFile: string | undefined = undefined, languageId: string | undefined = undefined, recipe: string | undefined = undefined) { logger.log('BUILD command invoked.') - if (!vscode.window.activeTextEditor) { - logger.log('Cannot start to build because the active editor is undefined.') - return - } - logger.log(`The document of the active editor: ${vscode.window.activeTextEditor.document.uri.toString(true)}`) - logger.log(`The languageId of the document: ${vscode.window.activeTextEditor.document.languageId}`) - const workspace = rootFile ? vscode.Uri.file(rootFile) : vscode.window.activeTextEditor.document.uri - const configuration = vscode.workspace.getConfiguration('latex-workshop', workspace) - const externalBuildCommand = configuration.get('latex.external.build.command') as string - const externalBuildArgs = configuration.get('latex.external.build.args') as string[] - if (rootFile === undefined && lw.manager.hasTexId(vscode.window.activeTextEditor.document.languageId)) { - rootFile = await lw.manager.findRoot() - languageId = lw.manager.rootFileLanguageId - } - if (externalBuildCommand) { - const pwd = path.dirname(rootFile ? rootFile : vscode.window.activeTextEditor.document.fileName) - await lw.builder.buildExternal(externalBuildCommand, externalBuildArgs, pwd, rootFile) - return - } - if (rootFile === undefined || languageId === undefined) { - logger.log('Cannot find LaTeX root file. See https://github.com/James-Yu/LaTeX-Workshop/wiki/Compile#the-root-file') - return - } - let pickedRootFile: string | undefined = rootFile - if (!skipSelection && lw.manager.localRootFile) { - // We are using the subfile package - pickedRootFile = await quickPickRootFile(rootFile, lw.manager.localRootFile, 'build') - if (! pickedRootFile) { - return - } - } - logger.log(`Building root file: ${pickedRootFile}`) - await lw.builder.build(pickedRootFile, languageId, recipe) + await extension.compile.build(skipSelection, rootFile, languageId, recipe) } export async function revealOutputDir() { @@ -114,9 +83,9 @@ export function refresh() { lw.viewer.refreshExistingViewer() } -export function kill() { - logger.log('KILL command invoked.') - lw.builder.kill() +export function terminate() { + logger.log('TERMINATE command invoked.') + extension.compile.terminate() } export function synctex() { @@ -478,7 +447,8 @@ export function texdocUsepackages() { } export async function saveActive() { - await lw.builder.saveActive() + extension.compile.lastBuildTime = Date.now() + await vscode.window.activeTextEditor?.document.save() } export function openMathPreviewPanel() { diff --git a/src/compile/build.ts b/src/compile/build.ts index bd3d9803f..998b90946 100644 --- a/src/compile/build.ts +++ b/src/compile/build.ts @@ -1,844 +1,335 @@ import * as vscode from 'vscode' import * as path from 'path' -import * as fs from 'fs' -import * as cp from 'child_process' import * as cs from 'cross-spawn' import * as lw from '../lw' -import { replaceArgumentPlaceholders } from '../utils/utils' import { AutoBuildInitiated, AutoCleaned, BuildDone } from '../core/event-bus' +import { rootFile as pickRootFile } from '../utils/quick-pick' import { getLogger } from '../utils/logging/logger' import { parser } from '../parse/parser' +import { BIB_MAGIC_PROGRAM_NAME, MAGIC_PROGRAM_ARGS_SUFFIX, MAX_PRINT_LINE, TEX_MAGIC_PROGRAM_NAME, build as buildRecipe } from './recipe' +import { build as buildExternal } from './external' +import { queue } from './queue' -const logger = getLogger('Builder') +import { extension } from '../extension' +import type { Watcher } from '../core/watcher' +import type { ProcessEnv, Step } from '.' -const enum BuildEvents { - never = 'never', - onSave = 'onSave', - onFileChange = 'onFileChange' -} +const logger = getLogger('Build') -export class Builder { - private lastBuild: number = 0 - private prevLangId: string | undefined - private prevRecipe: Recipe | undefined - private building: boolean = false - private outputPDFPath: string = '' - private process: cp.ChildProcessWithoutNullStreams | undefined +export { + initialize, + autoBuild, + build, +} - private readonly isMiktex: boolean = false - private readonly stepQueue: BuildToolQueue = new BuildToolQueue() - private readonly TEX_MAGIC_PROGRAM_NAME = 'TEX_MAGIC_PROGRAM_NAME' - private readonly BIB_MAGIC_PROGRAM_NAME = 'BIB_MAGIC_PROGRAM_NAME' - private readonly MAGIC_PROGRAM_ARGS_SUFFIX = '_WITH_ARGS' - private readonly MAX_PRINT_LINE = '10000' +function initialize(src: Watcher, bib: Watcher) { + src.onChange(filePath => autoBuild(filePath, 'onFileChange')) + bib.onChange(filePath => autoBuild(filePath, 'onFileChange', true)) +} - constructor() { - lw.cacher.src.onChange(filePath => this.buildOnFileChanged(filePath)) - lw.cacher.bib.onChange(filePath => this.buildOnFileChanged(filePath, true)) - // Check if pdflatex is available, and is MikTeX distro - try { - const pdflatexVersion = cp.execSync('pdflatex --version') - if (pdflatexVersion.toString().match(/MiKTeX/)) { - this.isMiktex = true - logger.log('pdflatex is provided by MiKTeX.') - } - } catch (e) { - logger.log('Cannot run pdflatex to determine if we are using MiKTeX.') - } +function autoBuild(file: string, type: 'onFileChange' | 'onSave', bibChanged: boolean = false) { + const configuration = vscode.workspace.getConfiguration('latex-workshop', vscode.Uri.file(file)) + if (configuration.get('latex.autoBuild.run') as string !== type) { + return } - - /** - * Terminate current process of LaTeX building. OS-specific (pkill for linux - * and macos, taskkill for win) kill command is first called with process - * pid. No matter whether it succeeded, `kill()` of `child_process` is later - * called to "double kill". Also, all subsequent tools in queue are cleared, - * including ones in the current recipe and (if available) those from the - * cached recipe to be executed. - */ - kill() { - if (this.process === undefined) { - logger.log('LaTeX build process to kill is not found.') - return - } - const pid = this.process.pid - try { - logger.log(`Kill child processes of the current process with PID ${pid}.`) - if (process.platform === 'linux' || process.platform === 'darwin') { - cp.execSync(`pkill -P ${pid}`, { timeout: 1000 }) - } else if (process.platform === 'win32') { - cp.execSync(`taskkill /F /T /PID ${pid}`, { timeout: 1000 }) - } - } catch (e) { - logger.logError('Failed killing child processes of the current process.', e) - } finally { - this.stepQueue.clear() - this.process.kill() - logger.log(`Killed the current process with PID ${pid}`) - } + logger.log('Auto build started' + (type === 'onFileChange' ? 'detecting the change of a file' : 'on saving file') + `: ${file} .`) + lw.eventBus.fire(AutoBuildInitiated, {type, file}) + if (!canAutoBuild()) { + logger.log('Autobuild temporarily disabled.') + return } - - buildOnFileChanged(file: string, bibChanged: boolean = false) { - const configuration = vscode.workspace.getConfiguration('latex-workshop', vscode.Uri.file(file)) - if (configuration.get('latex.autoBuild.run') as string !== BuildEvents.onFileChange) { - return - } - logger.log(`Auto build started detecting the change of a file: ${file} .`) - lw.eventBus.fire(AutoBuildInitiated, {type: 'onChange', file}) - return this.invokeBuild(file, bibChanged) + if (!bibChanged && lw.manager.localRootFile && configuration.get('latex.rootFile.useSubFile')) { + return build(true, lw.manager.localRootFile, lw.manager.rootFileLanguageId) + } else { + return build(true, lw.manager.rootFile, lw.manager.rootFileLanguageId) } +} - buildOnSaveIfEnabled(file: string) { - const configuration = vscode.workspace.getConfiguration('latex-workshop', vscode.Uri.file(file)) - if (configuration.get('latex.autoBuild.run') as string !== BuildEvents.onSave) { - return - } - logger.log(`Auto build started on saving file: ${file} .`) - lw.eventBus.fire(AutoBuildInitiated, {type: 'onSave', file}) - return this.invokeBuild(file, false) +/** + * This function determines whether an auto-build on save or on change can be + * triggered. There are two conditions that this function should take care of: + * 1. Defined `latex.autoBuild.interval` config, 2. Unwanted auto-build + * triggered by the `saveAll()` in another previous building process. + */ +function canAutoBuild(): boolean { + const configuration = vscode.workspace.getConfiguration('latex-workshop', lw.manager.rootFile ? vscode.Uri.file(lw.manager.rootFile) : undefined) + if (Date.now() - extension.compile.lastBuildTime < (configuration.get('latex.autoBuild.interval', 1000) as number)) { + return false } + return true +} - private invokeBuild(file: string, bibChanged: boolean ) { - if (!lw.builder.canAutoBuild()) { - logger.log('Autobuild temporarily disabled.') - return - } - const configuration = vscode.workspace.getConfiguration('latex-workshop', vscode.Uri.file(file)) - if (!bibChanged && lw.manager.localRootFile && configuration.get('latex.rootFile.useSubFile')) { - return lw.commander.build(true, lw.manager.localRootFile, lw.manager.rootFileLanguageId) - } else { - return lw.commander.build(true, lw.manager.rootFile, lw.manager.rootFileLanguageId) - } - } - - /** - * Build LaTeX project using external command. This function creates a - * {@link Tool} containing the external command info and adds it to the - * queue. After that, this function tries to initiate a {@link buildLoop} if - * there is no one running. - * - * @param command The external command to be executed. - * @param args The arguments to {@link command}. - * @param pwd The current working directory. This argument will be overrided - * if there are workspace folders. If so, the root of the first workspace - * folder is used as the current working directory. - * @param rootFile Path to the root LaTeX file. - */ - async buildExternal(command: string, args: string[], pwd: string, rootFile?: string) { - if (this.building) { - void logger.showErrorMessageWithCompilerLogButton('Please wait for the current build to finish.') - return - } - - this.lastBuild = Date.now() - - await vscode.workspace.saveAll() - - const workspaceFolder = vscode.workspace.workspaceFolders?.[0] - const cwd = workspaceFolder?.uri.fsPath || pwd - if (rootFile !== undefined) { - args = args.map(replaceArgumentPlaceholders(rootFile, lw.manager.tmpDir)) - } - const tool: Tool = { name: command, command, args } - - this.stepQueue.add(tool, rootFile, 'External', Date.now(), true, cwd) - - this.outputPDFPath = rootFile ? lw.manager.tex2pdf(rootFile) : '' - - await this.buildLoop() +async function build(skipSelection: boolean = false, rootFile: string | undefined = undefined, languageId: string | undefined = undefined, recipe: string | undefined = undefined) { + if (!vscode.window.activeTextEditor) { + logger.log('Cannot start to build because the active editor is undefined.') + return } - - /** - * Build LaTeX project using the recipe system. This function creates - * {@link Tool}s containing the tool info and adds them to the queue. After - * that, this function tries to initiate a {@link buildLoop} if there is no - * one running. - * - * @param rootFile Path to the root LaTeX file. - * @param langId The language ID of the root file. This argument is used to - * determine whether the previous recipe can be applied to this root file. - * @param recipeName The name of recipe to be used. If `undefined`, the - * builder tries to determine on its own, in {@link createBuildTools}. This - * parameter is given only when RECIPE command is invoked. For all other - * cases, it should be `undefined`. - */ - async build(rootFile: string, langId: string, recipeName?: string) { - logger.log(`Build root file ${rootFile}`) - - this.lastBuild = Date.now() - - await vscode.workspace.saveAll() - - this.createOutputSubFolders(rootFile) - - const tools = this.createBuildTools(rootFile, langId, recipeName) - - if (tools === undefined) { - logger.log('Invalid toolchain.') - return - } - const timestamp = Date.now() - tools.forEach(tool => this.stepQueue.add(tool, rootFile, recipeName || 'Build', timestamp)) - - this.outputPDFPath = lw.manager.tex2pdf(rootFile) - - await this.buildLoop() + logger.log(`The document of the active editor: ${vscode.window.activeTextEditor.document.uri.toString(true)}`) + logger.log(`The languageId of the document: ${vscode.window.activeTextEditor.document.languageId}`) + const workspace = rootFile ? vscode.Uri.file(rootFile) : vscode.window.activeTextEditor.document.uri + const configuration = vscode.workspace.getConfiguration('latex-workshop', workspace) + const externalBuildCommand = configuration.get('latex.external.build.command') as string + const externalBuildArgs = configuration.get('latex.external.build.args') as string[] + if (rootFile === undefined && lw.manager.hasTexId(vscode.window.activeTextEditor.document.languageId)) { + rootFile = await lw.manager.findRoot() + languageId = lw.manager.rootFileLanguageId } - - /** - * This function determines whether an auto-build on save or on change can - * be triggered. There are two conditions that this function should take - * care of: 1. Defined `latex.autoBuild.interval` config, 2. Unwanted - * auto-build triggered by the `saveAll()` in another previous building - * process. - * @returns Whether auto build can be triggered now. - */ - canAutoBuild() { - const configuration = vscode.workspace.getConfiguration('latex-workshop', lw.manager.rootFile ? vscode.Uri.file(lw.manager.rootFile) : undefined) - if (Date.now() - this.lastBuild < (configuration.get('latex.autoBuild.interval', 1000) as number)) { - return false - } - return true + if (externalBuildCommand) { + const pwd = path.dirname(rootFile ? rootFile : vscode.window.activeTextEditor.document.fileName) + await buildExternal(externalBuildCommand, externalBuildArgs, pwd, buildLoop, rootFile) + return } - - async saveActive() { - this.lastBuild = Date.now() - await vscode.window.activeTextEditor?.document.save() + if (rootFile === undefined || languageId === undefined) { + logger.log('Cannot find LaTeX root file. See https://github.com/James-Yu/LaTeX-Workshop/wiki/Compile#the-root-file') + return } - - isOutputPDF(pdfPath: string) { - return path.relative(pdfPath, this.outputPDFPath) !== '' - } - - /** - * This function returns if there is another {@link buildLoop} function/loop - * running. If not, this function iterates through the - * {@link BuildToolQueue} and execute each {@link Tool} one by one. During - * this process, the {@link Tool}s in {@link BuildToolQueue} can be - * dynamically added or removed, handled by {@link BuildToolQueue}. - */ - private async buildLoop() { - if (this.building) { + let pickedRootFile: string | undefined = rootFile + if (!skipSelection && lw.manager.localRootFile) { + // We are using the subfile package + pickedRootFile = await pickRootFile(rootFile, lw.manager.localRootFile, 'build') + if (! pickedRootFile) { return } - // Stop watching the PDF file to avoid reloading the PDF viewer twice. - // The builder will be responsible for refreshing the viewer. - this.building = true - let skipped = true - while (true) { - const step = this.stepQueue.getStep() - if (step === undefined) { - break - } - const env = this.spawnProcess(step) - const success = await this.monitorProcess(step, env) - skipped = skipped && !(step.isExternal || !step.isSkipped) - if (success && this.stepQueue.isLastStep(step)) { - await this.afterSuccessfulBuilt(step, skipped) - } - } - this.building = false } + logger.log(`Building root file: ${pickedRootFile}`) + await buildRecipe(pickedRootFile, languageId, buildLoop, recipe) +} - /** - * Spawns a `child_process` for the {@link step}. This function first - * creates the environment variables needed for the {@link step}. Then a - * process is spawned according to the nature of the {@link step}: 1) is a - * magic command (tex or bib), 2) is a recipe tool, or 3) is an external - * command. After spawned, the process is stored as a class property, and - * the io handling is performed in {@link monitorProcess}. - * - * @param step The {@link Step} to be executed. - * @returns The process environment passed to the spawned process. - */ - private spawnProcess(step: Step): ProcessEnv { - const configuration = vscode.workspace.getConfiguration('latex-workshop', step.rootFile ? vscode.Uri.file(step.rootFile) : undefined) - if (step.index === 0 || configuration.get('latex.build.clearLog.everyRecipeStep.enabled') as boolean) { - logger.clearCompilerMessage() - } - logger.refreshStatus('sync~spin', 'statusBar.foreground', undefined, undefined, ' ' + this.stepQueue.getStepString(step)) - logger.logCommand(`Recipe step ${step.index + 1}`, step.command, step.args) - logger.log(`env: ${JSON.stringify(step.env)}`) - logger.log(`root: ${step.rootFile}`) - - const env = Object.create(null) as ProcessEnv - Object.entries(process.env).forEach(([key, value]) => env[key] = value) - Object.entries(step.env ?? {}).forEach(([key, value]) => env[key] = value) - env['max_print_line'] = this.MAX_PRINT_LINE - - if (!step.isExternal && - (step.name.startsWith(this.TEX_MAGIC_PROGRAM_NAME) || - step.name.startsWith(this.BIB_MAGIC_PROGRAM_NAME))) { - logger.log(`cwd: ${path.dirname(step.rootFile)}`) - - const args = step.args - if (args && !step.name.endsWith(this.MAGIC_PROGRAM_ARGS_SUFFIX)) { - // All optional arguments are given as a unique string (% !TeX options) if any, so we use {shell: true} - this.process = cs.spawn(`${step.command} ${args[0]}`, [], {cwd: path.dirname(step.rootFile), env, shell: true}) - } else { - this.process = cs.spawn(step.command, args, {cwd: path.dirname(step.rootFile), env}) - } - } else if (!step.isExternal) { - let cwd = path.dirname(step.rootFile) - if (step.command === 'latexmk' && step.rootFile === lw.manager.localRootFile && lw.manager.rootDir) { - cwd = lw.manager.rootDir - } - logger.log(`cwd: ${cwd}`) - this.process = cs.spawn(step.command, step.args, {cwd, env}) - } else { - logger.log(`cwd: ${step.cwd}`) - this.process = cs.spawn(step.command, step.args, {cwd: step.cwd}) - } - logger.log(`LaTeX build process spawned with PID ${this.process.pid}.`) - return env +/** + * This function returns if there is another {@link buildLoop} function/loop + * running. If not, this function iterates through the + * {@link BuildToolQueue} and execute each {@link Tool} one by one. During + * this process, the {@link Tool}s in {@link BuildToolQueue} can be + * dynamically added or removed, handled by {@link BuildToolQueue}. + */ +async function buildLoop() { + if (extension.compile.compiling) { + return } - /** - * Monitors the output and termination of the tool process. This function - * monitors the `stdout` and `stderr` channels to log and parse the output - * messages. This function also **waits** for `error` or `exit` signal of - * the process. The former indicates an unexpected error, e.g., killed by - * user or ENOENT, and the latter is the typical exit of the process, - * successfully built or not. If the build is unsuccessful (code != 0), this - * function considers the four different cases: 1) tool of a recipe, not - * terminated by user, is not a retry and should retry, 2) tool of a recipe, - * not terminated by user, is a retry or should not retry, 3) unsuccessful - * external command, won't retry regardless of the retry config, and 4) - * terminated by user. In the first case, a retry {@link Tool} is created - * and added to the {@link BuildToolQueue} based on {@link step}. In the - * latter three, all subsequent tools in queue are cleared, including ones - * in the current recipe and (if available) those from the cached recipe to - * be executed. - * - * @param step The {@link Step} of process whose io is monitored. - * @param env The process environment passed to the spawned process. - * @return Whether the step is successfully executed. - */ - private async monitorProcess(step: Step, env: ProcessEnv): Promise { - if (this.process === undefined) { - return false + extension.compile.compiling = true + extension.compile.lastBuildTime = Date.now() + // Stop watching the PDF file to avoid reloading the PDF viewer twice. + // The builder will be responsible for refreshing the viewer. + let skipped = true + while (true) { + const step = queue.getStep() + if (step === undefined) { + break + } + lw.manager.compiledRootFile = step.rootFile + const env = spawnProcess(step) + const success = await monitorProcess(step, env) + skipped = skipped && !(step.isExternal || !step.isSkipped) + if (success && queue.isLastStep(step)) { + await afterSuccessfulBuilt(step, skipped) } - let stdout = '' - this.process.stdout.on('data', (msg: Buffer | string) => { - stdout += msg - logger.logCompiler(msg.toString()) - }) - - let stderr = '' - this.process.stderr.on('data', (msg: Buffer | string) => { - stderr += msg - logger.logCompiler(msg.toString()) - }) - - const result: boolean = await new Promise(resolve => { - if (this.process === undefined) { - resolve(false) - return - } - this.process.on('error', err => { - logger.logError(`LaTeX fatal error on PID ${this.process?.pid}.`, err) - logger.log(`Does the executable exist? $PATH: ${env['PATH']}, $Path: ${env['Path']}, $SHELL: ${process.env.SHELL}`) - logger.log(`${stderr}`) - logger.refreshStatus('x', 'errorForeground', undefined, 'error') - void logger.showErrorMessageWithExtensionLogButton(`Recipe terminated with fatal error: ${err.message}.`) - this.process = undefined - this.stepQueue.clear() - resolve(false) - }) - - this.process.on('exit', async (code, signal) => { - const isSkipped = parser.parseLog(stdout, step.rootFile) - if (!step.isExternal) { - step.isSkipped = isSkipped - } - - if (!step.isExternal && code === 0) { - logger.log(`Finished a step in recipe with PID ${this.process?.pid}.`) - this.process = undefined - resolve(true) - return - } else if (code === 0) { - logger.log(`Successfully built document with PID ${this.process?.pid}.`) - logger.refreshStatus('check', 'statusBar.foreground', 'Build succeeded.') - this.process = undefined - resolve(true) - return - } - - if (!step.isExternal) { - logger.log(`Recipe returns with error code ${code}/${signal} on PID ${this.process?.pid}.`) - logger.log(`Does the executable exist? $PATH: ${env['PATH']}, $Path: ${env['Path']}, $SHELL: ${process.env.SHELL}`) - logger.log(`${stderr}`) - } - - const configuration = vscode.workspace.getConfiguration('latex-workshop', step.rootFile ? vscode.Uri.file(step.rootFile) : undefined) - if (!step.isExternal && signal !== 'SIGTERM' && !step.isRetry && configuration.get('latex.autoBuild.cleanAndRetry.enabled')) { - // Recipe, not terminated by user, is not retry and should retry - step.isRetry = true - logger.refreshStatus('x', 'errorForeground', 'Recipe terminated with error. Retry building the project.', 'warning') - logger.log('Cleaning auxiliary files and retrying build after toolchain error.') - - this.stepQueue.prepend(step) - await lw.cleaner.clean(step.rootFile) - lw.eventBus.fire(AutoCleaned) - } else if (!step.isExternal && signal !== 'SIGTERM') { - // Recipe, not terminated by user, is retry or should not retry - logger.refreshStatus('x', 'errorForeground') - if (['onFailed', 'onBuilt'].includes(configuration.get('latex.autoClean.run') as string)) { - await lw.cleaner.clean(step.rootFile) - lw.eventBus.fire(AutoCleaned) - } - void logger.showErrorMessageWithCompilerLogButton('Recipe terminated with error.') - this.stepQueue.clear() - } else if (step.isExternal) { - // External command - logger.log(`Build returns with error: ${code}/${signal} on PID ${this.process?.pid}.`) - logger.refreshStatus('x', 'errorForeground', undefined, 'warning') - void logger.showErrorMessageWithCompilerLogButton('Build terminated with error.') - this.stepQueue.clear() - } else { - // Terminated by user - logger.refreshStatus('x', 'errorForeground') - this.stepQueue.clear() - } - this.process = undefined - resolve(false) - }) - }) - - return result } - - /** - * Some follow-up operations after successfully finishing a recipe. - * Primarily concerning PDF refreshing and file cleaning. The execution is - * covered in {@link buildLoop}. - * - * @param lastStep The last {@link Step} in the recipe. - * @param skipped Whether the **whole** building process is skipped by - * latexmk. - */ - private async afterSuccessfulBuilt(lastStep: Step, skipped: boolean) { - if (lastStep.rootFile === undefined) { - // This only happens when the step is an external command. - lw.viewer.refreshExistingViewer() - return - } - logger.log(`Successfully built ${lastStep.rootFile} .`) - logger.refreshStatus('check', 'statusBar.foreground', 'Recipe succeeded.') - lw.eventBus.fire(BuildDone) - if (!lastStep.isExternal && skipped) { - return - } - lw.viewer.refreshExistingViewer(lw.manager.tex2pdf(lastStep.rootFile)) - lw.completer.reference.setNumbersFromAuxFile(lastStep.rootFile) - await lw.cacher.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. - if (configuration.get('view.pdf.viewer') === 'external' && configuration.get('synctex.afterBuild.enabled')) { - const pdfFile = lw.manager.tex2pdf(lastStep.rootFile) - logger.log('SyncTex after build invoked.') - lw.locator.syncTeX(undefined, undefined, pdfFile) - } - if (['onSucceeded', 'onBuilt'].includes(configuration.get('latex.autoClean.run') as string)) { - logger.log('Auto Clean invoked.') - await lw.cleaner.clean(lastStep.rootFile) - lw.eventBus.fire(AutoCleaned) - } + extension.compile.compiling = false +} +/** + * Spawns a `child_process` for the {@link step}. This function first + * creates the environment variables needed for the {@link step}. Then a + * process is spawned according to the nature of the {@link step}: 1) is a + * magic command (tex or bib), 2) is a recipe tool, or 3) is an external + * command. After spawned, the process is stored as a class property, and + * the io handling is performed in {@link monitorProcess}. + * + * @param step The {@link Step} to be executed. + * @returns The process environment passed to the spawned process. + */ +function spawnProcess(step: Step): ProcessEnv { + const configuration = vscode.workspace.getConfiguration('latex-workshop', step.rootFile ? vscode.Uri.file(step.rootFile) : undefined) + if (step.index === 0 || configuration.get('latex.build.clearLog.everyRecipeStep.enabled') as boolean) { + logger.clearCompilerMessage() } - - /** - * Given an optional recipe, create the corresponding {@link Tool}s. - */ - private createBuildTools(rootFile: string, langId: string, recipeName?: string): Tool[] | undefined { - let buildTools: Tool[] = [] - - const configuration = vscode.workspace.getConfiguration('latex-workshop', vscode.Uri.file(rootFile)) - const magic = this.findMagicComments(rootFile) - - if (recipeName === undefined && magic.tex && !configuration.get('latex.build.forceRecipeUsage')) { - buildTools = this.createBuildMagic(rootFile, magic.tex, magic.bib) + logger.refreshStatus('sync~spin', 'statusBar.foreground', undefined, undefined, ' ' + queue.getStepString(step)) + logger.logCommand(`Recipe step ${step.index + 1}`, step.command, step.args) + logger.log(`env: ${JSON.stringify(step.env)}`) + logger.log(`root: ${step.rootFile}`) + + const env = Object.create(null) as ProcessEnv + Object.entries(process.env).forEach(([key, value]) => env[key] = value) + Object.entries(step.env ?? {}).forEach(([key, value]) => env[key] = value) + env['max_print_line'] = MAX_PRINT_LINE + + if (!step.isExternal && + (step.name.startsWith(TEX_MAGIC_PROGRAM_NAME) || + step.name.startsWith(BIB_MAGIC_PROGRAM_NAME))) { + logger.log(`cwd: ${path.dirname(step.rootFile)}`) + + const args = step.args + if (args && !step.name.endsWith(MAGIC_PROGRAM_ARGS_SUFFIX)) { + // All optional arguments are given as a unique string (% !TeX options) if any, so we use {shell: true} + extension.compile.process = cs.spawn(`${step.command} ${args[0]}`, [], {cwd: path.dirname(step.rootFile), env, shell: true}) } else { - const recipe = this.findRecipe(rootFile, langId, recipeName || magic.recipe) - if (recipe === undefined) { - return - } - logger.log(`Preparing to run recipe: ${recipe.name}.`) - this.prevRecipe = recipe - this.prevLangId = langId - const tools = configuration.get('latex.tools') as Tool[] - recipe.tools.forEach(tool => { - if (typeof tool === 'string') { - const candidates = tools.filter(candidate => candidate.name === tool) - if (candidates.length < 1) { - logger.log(`Skipping undefined tool ${tool} in recipe ${recipe.name}.`) - void logger.showErrorMessage(`Skipping undefined tool "${tool}" in recipe "${recipe.name}."`) - } else { - buildTools.push(candidates[0]) - } - } else { - buildTools.push(tool) - } - }) - logger.log(`Prepared ${buildTools.length} tools.`) - } - if (buildTools.length < 1) { - return - } - - // Use JSON.parse and JSON.stringify for a deep copy. - buildTools = JSON.parse(JSON.stringify(buildTools)) as Tool[] - - this.populateTools(rootFile, buildTools) - - return buildTools + extension.compile.process = cs.spawn(step.command, args, {cwd: path.dirname(step.rootFile), env}) + } + } else if (!step.isExternal) { + let cwd = path.dirname(step.rootFile) + if (step.command === 'latexmk' && step.rootFile === lw.manager.localRootFile && lw.manager.rootDir) { + cwd = lw.manager.rootDir + } + logger.log(`cwd: ${cwd}`) + extension.compile.process = cs.spawn(step.command, step.args, {cwd, env}) + } else { + logger.log(`cwd: ${step.cwd}`) + extension.compile.process = cs.spawn(step.command, step.args, {cwd: step.cwd}) } + logger.log(`LaTeX build process spawned with PID ${extension.compile.process.pid}.`) + return env +} - /** - * Expand the bare {@link Tool} with docker and argument placeholder - * strings. - */ - private populateTools(rootFile: string, buildTools: Tool[]): Tool[] { - const configuration = vscode.workspace.getConfiguration('latex-workshop', vscode.Uri.file(rootFile)) - const docker = configuration.get('docker.enabled') - - buildTools.forEach(tool => { - if (docker) { - switch (tool.command) { - case 'latexmk': - logger.log('Use Docker to invoke the command.') - if (process.platform === 'win32') { - tool.command = path.resolve(lw.extensionRoot, './scripts/latexmk.bat') - } else { - tool.command = path.resolve(lw.extensionRoot, './scripts/latexmk') - fs.chmodSync(tool.command, 0o755) - } - break - default: - logger.log(`Do not use Docker to invoke the command: ${tool.command}.`) - break - } - } - tool.args = tool.args?.map(replaceArgumentPlaceholders(rootFile, lw.manager.tmpDir)) - const env = tool.env ?? {} - Object.entries(env).forEach(([key, value]) => { - env[key] = value && replaceArgumentPlaceholders(rootFile, lw.manager.tmpDir)(value) - }) - if (configuration.get('latex.option.maxPrintLine.enabled')) { - tool.args = tool.args ?? [] - const isLuaLatex = tool.args.includes('-lualatex') || - tool.args.includes('-pdflua') || - tool.args.includes('-pdflualatex') || - tool.args.includes('--lualatex') || - tool.args.includes('--pdflua') || - tool.args.includes('--pdflualatex') - if (this.isMiktex && ((tool.command === 'latexmk' && !isLuaLatex) || tool.command === 'pdflatex')) { - tool.args.unshift('--max-print-line=' + this.MAX_PRINT_LINE) - } - } - }) - return buildTools +/** + * Monitors the output and termination of the tool process. This function + * monitors the `stdout` and `stderr` channels to log and parse the output + * messages. This function also **waits** for `error` or `exit` signal of + * the process. The former indicates an unexpected error, e.g., killed by + * user or ENOENT, and the latter is the typical exit of the process, + * successfully built or not. If the build is unsuccessful (code != 0), this + * function considers the four different cases: 1) tool of a recipe, not + * terminated by user, is not a retry and should retry, 2) tool of a recipe, + * not terminated by user, is a retry or should not retry, 3) unsuccessful + * external command, won't retry regardless of the retry config, and 4) + * terminated by user. In the first case, a retry {@link Tool} is created + * and added to the {@link BuildToolQueue} based on {@link step}. In the + * latter three, all subsequent tools in queue are cleared, including ones + * in the current recipe and (if available) those from the cached recipe to + * be executed. + * + * @param step The {@link Step} of process whose io is monitored. + * @param env The process environment passed to the spawned process. + * @return Whether the step is successfully executed. + */ +async function monitorProcess(step: Step, env: ProcessEnv): Promise { + if (extension.compile.process === undefined) { + return false } - - /** - * @param recipeName This recipe name may come from user selection of RECIPE - * command, or from the %! LW recipe magic command. - */ - private findRecipe(rootFile: string, langId: string, recipeName?: string): Recipe | undefined { - const configuration = vscode.workspace.getConfiguration('latex-workshop', vscode.Uri.file(rootFile)) - - const recipes = configuration.get('latex.recipes') as Recipe[] - const defaultRecipeName = configuration.get('latex.recipe.default') as string - - if (recipes.length < 1) { - logger.log('No recipes defined.') - void logger.showErrorMessage('[Builder] No recipes defined.') + let stdout = '' + extension.compile.process.stdout.on('data', (msg: Buffer | string) => { + stdout += msg + logger.logCompiler(msg.toString()) + }) + + let stderr = '' + extension.compile.process.stderr.on('data', (msg: Buffer | string) => { + stderr += msg + logger.logCompiler(msg.toString()) + }) + + const result: boolean = await new Promise(resolve => { + if (extension.compile.process === undefined) { + resolve(false) return } - if (this.prevLangId !== langId) { - this.prevRecipe = undefined - } - let recipe: Recipe | undefined - // Find recipe according to the given name - if (recipeName === undefined && !['first', 'lastUsed'].includes(defaultRecipeName)) { - recipeName = defaultRecipeName - } - if (recipeName) { - const candidates = recipes.filter(candidate => candidate.name === recipeName) - if (candidates.length < 1) { - logger.log(`Failed to resolve build recipe: ${recipeName}.`) - void logger.showErrorMessage(`[Builder] Failed to resolve build recipe: ${recipeName}.`) - } - recipe = candidates[0] - } - // Find default recipe of last used - if (recipe === undefined && defaultRecipeName === 'lastUsed') { - recipe = this.prevRecipe - } - // If still not found, fallback to 'first' - if (recipe === undefined) { - let candidates: Recipe[] = recipes - if (langId === 'rsweave') { - candidates = recipes.filter(candidate => candidate.name.toLowerCase().match('rnw|rsweave')) - } else if (langId === 'jlweave') { - candidates = recipes.filter(candidate => candidate.name.toLowerCase().match('jnw|jlweave|weave.jl')) - } else if (langId === 'pweave') { - candidates = recipes.filter(candidate => candidate.name.toLowerCase().match('pnw|pweave')) - } - if (candidates.length < 1) { - logger.log(`Failed to resolve build recipe: ${recipeName}.`) - void logger.showErrorMessage(`Failed to resolve build recipe: ${recipeName}.`) - } - recipe = candidates[0] - } - return recipe - } - - private createBuildMagic(rootFile: string, magicTex: Tool, magicBib?: Tool): Tool[] { - const configuration = vscode.workspace.getConfiguration('latex-workshop', vscode.Uri.file(rootFile)) - - if (!magicTex.args) { - magicTex.args = configuration.get('latex.magic.args') as string[] - magicTex.name = this.TEX_MAGIC_PROGRAM_NAME + this.MAGIC_PROGRAM_ARGS_SUFFIX - } - if (magicBib) { - if (!magicBib.args) { - magicBib.args = configuration.get('latex.magic.bib.args') as string[] - magicBib.name = this.BIB_MAGIC_PROGRAM_NAME + this.MAGIC_PROGRAM_ARGS_SUFFIX - } - return [magicTex, magicBib, magicTex, magicTex] - } else { - return [magicTex] - } - } + extension.compile.process.on('error', err => { + logger.logError(`LaTeX fatal error on PID ${extension.compile.process?.pid}.`, err) + logger.log(`Does the executable exist? $PATH: ${env['PATH']}, $Path: ${env['Path']}, $SHELL: ${process.env.SHELL}`) + logger.log(`${stderr}`) + logger.refreshStatus('x', 'errorForeground', undefined, 'error') + void logger.showErrorMessageWithExtensionLogButton(`Recipe terminated with fatal error: ${err.message}.`) + extension.compile.process = undefined + queue.clear() + resolve(false) + }) - private findMagicComments(rootFile: string): {tex?: Tool, bib?: Tool, recipe?: string} { - const regexTex = /^(?:%\s*!\s*T[Ee]X\s(?:TS-)?program\s*=\s*([^\s]*)$)/m - const regexBib = /^(?:%\s*!\s*BIB\s(?:TS-)?program\s*=\s*([^\s]*)$)/m - const regexTexOptions = /^(?:%\s*!\s*T[Ee]X\s(?:TS-)?options\s*=\s*(.*)$)/m - const regexBibOptions = /^(?:%\s*!\s*BIB\s(?:TS-)?options\s*=\s*(.*)$)/m - const regexRecipe = /^(?:%\s*!\s*LW\srecipe\s*=\s*(.*)$)/m - let content = '' - for (const line of fs.readFileSync(rootFile).toString().split('\n')) { - if (line.startsWith('%') || line.trim().length === 0) { - content += line + '\n' - } else { - break + extension.compile.process.on('exit', async (code, signal) => { + const isSkipped = parser.parseLog(stdout, step.rootFile) + if (!step.isExternal) { + step.isSkipped = isSkipped } - } - const tex = content.match(regexTex) - let texCommand: Tool | undefined = undefined - if (tex) { - texCommand = { - name: this.TEX_MAGIC_PROGRAM_NAME, - command: tex[1] - } - logger.log(`Found TeX program by magic comment: ${texCommand.command}.`) - const res = content.match(regexTexOptions) - if (res) { - texCommand.args = [res[1]] - logger.log(`Found TeX options by magic comment: ${texCommand.args}.`) + if (!step.isExternal && code === 0) { + logger.log(`Finished a step in recipe with PID ${extension.compile.process?.pid}.`) + extension.compile.process = undefined + resolve(true) + return + } else if (code === 0) { + logger.log(`Successfully built document with PID ${extension.compile.process?.pid}.`) + logger.refreshStatus('check', 'statusBar.foreground', 'Build succeeded.') + extension.compile.process = undefined + resolve(true) + return } - } - const bib = content.match(regexBib) - let bibCommand: Tool | undefined = undefined - if (bib) { - bibCommand = { - name: this.BIB_MAGIC_PROGRAM_NAME, - command: bib[1] - } - logger.log(`Found BIB program by magic comment: ${bibCommand.command}.`) - const res = content.match(regexBibOptions) - if (res) { - bibCommand.args = [res[1]] - logger.log(`Found BIB options by magic comment: ${bibCommand.args}.`) + if (!step.isExternal) { + logger.log(`Recipe returns with error code ${code}/${signal} on PID ${extension.compile.process?.pid}.`) + logger.log(`Does the executable exist? $PATH: ${env['PATH']}, $Path: ${env['Path']}, $SHELL: ${process.env.SHELL}`) + logger.log(`${stderr}`) } - } - - const recipe = content.match(regexRecipe) - if (recipe && recipe[1]) { - logger.log(`Found LW recipe '${recipe[1]}' by magic comment: ${recipe}.`) - } - return {tex: texCommand, bib: bibCommand, recipe: recipe?.[1]} - } - - /** - * Create sub directories of output directory This was supposed to create - * the outputDir as latexmk does not take care of it (neither does any of - * latex command). If the output directory does not exist, the latex - * commands simply fail. - */ - private createOutputSubFolders(rootFile: string) { - const rootDir = path.dirname(rootFile) - let outDir = lw.manager.getOutDir(rootFile) - if (!path.isAbsolute(outDir)) { - outDir = path.resolve(rootDir, outDir) - } - logger.log(`outDir: ${outDir} .`) - lw.cacher.getIncludedTeX(rootFile).forEach(file => { - const relativePath = path.dirname(file.replace(rootDir, '.')) - const fullOutDir = path.resolve(outDir, relativePath) - // To avoid issues when fullOutDir is the root dir - // Using fs.mkdir() on the root directory even with recursion will result in an error - try { - if (! (fs.existsSync(fullOutDir) && fs.statSync(fullOutDir).isDirectory())) { - fs.mkdirSync(fullOutDir, { recursive: true }) - } - } catch (e) { - if (e instanceof Error) { - // #4048 - logger.log(`Unexpected Error: ${e.name}: ${e.message} .`) - } else { - logger.log('Unexpected Error: please see the console log of the Developer Tools of VS Code.') - logger.refreshStatus('x', 'errorForeground') - throw(e) + const configuration = vscode.workspace.getConfiguration('latex-workshop', step.rootFile ? vscode.Uri.file(step.rootFile) : undefined) + if (!step.isExternal && signal !== 'SIGTERM' && !step.isRetry && configuration.get('latex.autoBuild.cleanAndRetry.enabled')) { + // Recipe, not terminated by user, is not retry and should retry + step.isRetry = true + logger.refreshStatus('x', 'errorForeground', 'Recipe terminated with error. Retry building the project.', 'warning') + logger.log('Cleaning auxiliary files and retrying build after toolchain error.') + + queue.prepend(step) + await lw.cleaner.clean(step.rootFile) + lw.eventBus.fire(AutoCleaned) + } else if (!step.isExternal && signal !== 'SIGTERM') { + // Recipe, not terminated by user, is retry or should not retry + logger.refreshStatus('x', 'errorForeground') + if (['onFailed', 'onBuilt'].includes(configuration.get('latex.autoClean.run') as string)) { + await lw.cleaner.clean(step.rootFile) + lw.eventBus.fire(AutoCleaned) } + void logger.showErrorMessageWithCompilerLogButton('Recipe terminated with error.') + queue.clear() + } else if (step.isExternal) { + // External command + logger.log(`Build returns with error: ${code}/${signal} on PID ${extension.compile.process?.pid}.`) + logger.refreshStatus('x', 'errorForeground', undefined, 'warning') + void logger.showErrorMessageWithCompilerLogButton('Build terminated with error.') + queue.clear() + } else { + // Terminated by user + logger.refreshStatus('x', 'errorForeground') + queue.clear() } + extension.compile.process = undefined + resolve(false) }) - } -} - -class BuildToolQueue { - /** - * The {@link Step}s in the current recipe. - */ - private steps: Step[] = [] - /** - * The {@link Step}s in the next recipe to be executed after the current - * ones. - */ - private nextSteps: Step[] = [] - - constructor() {} - - /** - * Add a {@link Tool} to the queue. The input {@link tool} is first wrapped - * to be a {@link RecipeStep} or {@link ExternalStep} with additional - * information, according to the nature {@link isExternal}. Then the wrapped - * {@link Step} is added to the current {@link steps} if they belongs to the - * same recipe, determined by the same {@link timestamp}, or added to the - * {@link nextSteps} for later execution. - * - * @param tool The {@link Tool} to be added to the queue. - * @param rootFile Path to the root LaTeX file. - * @param recipeName The name of the recipe which the {@link tool} belongs - * to. - * @param timestamp The timestamp when the recipe is called. - * @param isExternal Whether the {@link tool} is an external command. - * @param cwd The current working directory if the {@link tool} is an - * external command. - */ - add(tool: Tool, rootFile: string | undefined, recipeName: string, timestamp: number, isExternal: boolean = false, cwd?: string) { - let step: RecipeStep | ExternalStep - if (!isExternal && rootFile !== undefined) { - step = tool as RecipeStep - step.rootFile = rootFile - step.recipeName = recipeName - step.timestamp = timestamp - step.isRetry = false - step.isExternal = false - step.isSkipped = false - } else { - step = tool as ExternalStep - step.recipeName = 'External' - step.timestamp = timestamp - step.isExternal = true - step.cwd = cwd || '' - } - if (this.steps.length === 0 || step.timestamp === this.steps[0].timestamp) { - step.index = (this.steps[this.steps.length - 1]?.index ?? -1) + 1 - this.steps.push(step) - } else if (this.nextSteps.length === 0 || step.timestamp === this.nextSteps[0].timestamp){ - step.index = (this.nextSteps[this.nextSteps.length - 1]?.index ?? -1) + 1 - this.nextSteps.push(step) - } else { - step.index = 0 - this.nextSteps = [ step ] - } - } + }) - prepend(step: Step) { - this.steps.unshift(step) - } + return result +} - clear() { - this.nextSteps = [] - this.steps = [] +/** + * Some follow-up operations after successfully finishing a recipe. + * Primarily concerning PDF refreshing and file cleaning. The execution is + * covered in {@link buildLoop}. + * + * @param lastStep The last {@link Step} in the recipe. + * @param skipped Whether the **whole** building process is skipped by + * latexmk. + */ +async function afterSuccessfulBuilt(lastStep: Step, skipped: boolean) { + if (lastStep.rootFile === undefined) { + // This only happens when the step is an external command. + lw.viewer.refreshExistingViewer() + return } - - isLastStep(step: Step) { - return this.steps.length === 0 || this.steps[0].timestamp !== step.timestamp + logger.log(`Successfully built ${lastStep.rootFile} .`) + logger.refreshStatus('check', 'statusBar.foreground', 'Recipe succeeded.') + lw.eventBus.fire(BuildDone) + if (!lastStep.isExternal && skipped) { + return } - - getStepString(step: Step): string { - let stepString: string - if (step.timestamp !== this.steps[0]?.timestamp && step.index === 0) { - stepString = step.recipeName - } else if (step.timestamp === this.steps[0]?.timestamp) { - stepString = `${step.recipeName}: ${step.index + 1}/${this.steps[this.steps.length - 1].index + 1} (${step.name})` - } else { - stepString = `${step.recipeName}: ${step.index + 1}/${step.index + 1} (${step.name})` - } - if(step.rootFile) { - const rootFileUri = vscode.Uri.file(step.rootFile) - const configuration = vscode.workspace.getConfiguration('latex-workshop', rootFileUri) - const showFilename = configuration.get('latex.build.rootfileInStatus', false) - if(showFilename) { - const relPath = vscode.workspace.asRelativePath(step.rootFile) - stepString = `${relPath}: ${stepString}` - } - } - return stepString + lw.viewer.refreshExistingViewer(lw.manager.tex2pdf(lastStep.rootFile)) + lw.completer.reference.setNumbersFromAuxFile(lastStep.rootFile) + extension.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. + if (configuration.get('view.pdf.viewer') === 'external' && configuration.get('synctex.afterBuild.enabled')) { + const pdfFile = lw.manager.tex2pdf(lastStep.rootFile) + logger.log('SyncTex after build invoked.') + lw.locator.syncTeX(undefined, undefined, pdfFile) } - - getStep(): Step | undefined { - let step: Step | undefined - if (this.steps.length > 0) { - step = this.steps.shift() - } else if (this.nextSteps.length > 0) { - this.steps = this.nextSteps - this.nextSteps = [] - step = this.steps.shift() - } - return step + if (['onSucceeded', 'onBuilt'].includes(configuration.get('latex.autoClean.run') as string)) { + logger.log('Auto Clean invoked.') + await lw.cleaner.clean(lastStep.rootFile) + lw.eventBus.fire(AutoCleaned) } } - -interface ProcessEnv { - [key: string]: string | undefined -} - -interface Tool { - name: string, - command: string, - args?: string[], - env?: ProcessEnv -} - -interface Recipe { - name: string, - tools: (string | Tool)[] -} - -interface RecipeStep extends Tool { - rootFile: string, - recipeName: string, - timestamp: number, - index: number, - isExternal: false, - isRetry: boolean, - isSkipped: boolean -} - -interface ExternalStep extends Tool { - rootFile?: string, - recipeName: 'External', - timestamp: number, - index: number, - isExternal: true, - cwd: string -} - -type Step = RecipeStep | ExternalStep diff --git a/src/compile/external.ts b/src/compile/external.ts new file mode 100644 index 000000000..4e9988331 --- /dev/null +++ b/src/compile/external.ts @@ -0,0 +1,43 @@ +import vscode from 'vscode' +import * as lw from '../lw' +import type { Tool } from '.' +import { getLogger } from '../utils/logging/logger' +import { replaceArgumentPlaceholders } from '../utils/utils' +import { queue } from './queue' + +import { extension } from '../extension' + +const logger = getLogger('Build', 'External') + +/** + * Build LaTeX project using external command. This function creates a + * {@link Tool} containing the external command info and adds it to the + * queue. After that, this function tries to initiate a {@link buildLoop} if + * there is no one running. + * + * @param command The external command to be executed. + * @param args The arguments to {@link command}. + * @param pwd The current working directory. This argument will be overrided + * if there are workspace folders. If so, the root of the first workspace + * folder is used as the current working directory. + * @param rootFile Path to the root LaTeX file. + */ +export async function build(command: string, args: string[], pwd: string, buildLoop: () => Promise, rootFile?: string) { + if (extension.compile.compiling) { + void logger.showErrorMessageWithCompilerLogButton('Please wait for the current build to finish.') + return + } + + await vscode.workspace.saveAll() + + const workspaceFolder = vscode.workspace.workspaceFolders?.[0] + const cwd = workspaceFolder?.uri.fsPath || pwd + if (rootFile !== undefined) { + args = args.map(replaceArgumentPlaceholders(rootFile, lw.manager.tmpDir)) + } + const tool: Tool = { name: command, command, args } + + queue.add(tool, rootFile, 'External', Date.now(), true, cwd) + + await buildLoop() +} \ No newline at end of file diff --git a/src/compile/index.ts b/src/compile/index.ts new file mode 100644 index 000000000..2fec6d187 --- /dev/null +++ b/src/compile/index.ts @@ -0,0 +1,63 @@ +import type { ChildProcessWithoutNullStreams } from 'child_process' +import { initialize, build, autoBuild } from './build' +import { terminate } from './terminate' + +export const compile = { + initialize, + build, + autoBuild, + terminate, + compiling: false, + lastBuildTime: 0, + compiledPDFPath: '', + process: undefined as ChildProcessWithoutNullStreams | undefined +} + +export type StepQueue = { + /** + * The {@link Step}s in the current recipe. + */ + steps: Step[] + /** + * The {@link Step}s in the next recipe to be executed after the current + * ones. + */ + nextSteps: Step[] +} + +export type ProcessEnv = { + [key: string]: string | undefined +} + +export type Tool = { + name: string + command: string + args?: string[] + env?: ProcessEnv +} + +export type Recipe = { + name: string + tools: (string | Tool)[] +} + +export type RecipeStep = Tool & { + rootFile: string + recipeName: string + timestamp: number + index: number + isExternal: false + isRetry: boolean + isSkipped: boolean +} + +export type ExternalStep = Tool & { + rootFile?: string + recipeName: 'External' + timestamp: number + index: number + isExternal: true + cwd: string +} + +export type Step = RecipeStep | ExternalStep diff --git a/src/compile/queue.ts b/src/compile/queue.ts new file mode 100644 index 000000000..72a4a5d8a --- /dev/null +++ b/src/compile/queue.ts @@ -0,0 +1,105 @@ +import vscode from 'vscode' +import type { ExternalStep, RecipeStep, Step, StepQueue, Tool } from '.' + +const stepQueue: StepQueue = { steps: [], nextSteps: [] } + +/** + * Add a {@link Tool} to the queue. The input {@link tool} is first wrapped + * to be a {@link RecipeStep} or {@link ExternalStep} with additional + * information, according to the nature {@link isExternal}. Then the wrapped + * {@link Step} is added to the current {@link steps} if they belongs to the + * same recipe, determined by the same {@link timestamp}, or added to the + * {@link nextSteps} for later execution. + * + * @param tool The {@link Tool} to be added to the queue. + * @param rootFile Path to the root LaTeX file. + * @param recipeName The name of the recipe which the {@link tool} belongs + * to. + * @param timestamp The timestamp when the recipe is called. + * @param isExternal Whether the {@link tool} is an external command. + * @param cwd The current working directory if the {@link tool} is an + * external command. + */ +function add(tool: Tool, rootFile: string | undefined, recipeName: string, timestamp: number, isExternal: boolean = false, cwd?: string) { + let step: Step + if (!isExternal && rootFile !== undefined) { + step = tool as RecipeStep + step.rootFile = rootFile + step.recipeName = recipeName + step.timestamp = timestamp + step.isRetry = false + step.isExternal = false + step.isSkipped = false + } else { + step = tool as ExternalStep + step.recipeName = 'External' + step.timestamp = timestamp + step.isExternal = true + step.cwd = cwd || '' + } + if (stepQueue.steps.length === 0 || step.timestamp === stepQueue.steps[0].timestamp) { + step.index = (stepQueue.steps[stepQueue.steps.length - 1]?.index ?? -1) + 1 + stepQueue.steps.push(step) + } else if (stepQueue.nextSteps.length === 0 || step.timestamp === stepQueue.nextSteps[0].timestamp){ + step.index = (stepQueue.nextSteps[stepQueue.nextSteps.length - 1]?.index ?? -1) + 1 + stepQueue.nextSteps.push(step) + } else { + step.index = 0 + stepQueue.nextSteps = [ step ] + } +} + +function prepend(step: Step) { + stepQueue.steps.unshift(step) +} + +function clear() { + stepQueue.nextSteps = [] + stepQueue.steps = [] +} + +function isLastStep(step: Step) { + return stepQueue.steps.length === 0 || stepQueue.steps[0].timestamp !== step.timestamp +} + +function getStepString(step: Step): string { + let stepString: string + if (step.timestamp !== stepQueue.steps[0]?.timestamp && step.index === 0) { + stepString = step.recipeName + } else if (step.timestamp === stepQueue.steps[0]?.timestamp) { + stepString = `${step.recipeName}: ${step.index + 1}/${stepQueue.steps[stepQueue.steps.length - 1].index + 1} (${step.name})` + } else { + stepString = `${step.recipeName}: ${step.index + 1}/${step.index + 1} (${step.name})` + } + if(step.rootFile) { + const rootFileUri = vscode.Uri.file(step.rootFile) + const configuration = vscode.workspace.getConfiguration('latex-workshop', rootFileUri) + const showFilename = configuration.get('latex.build.rootfileInStatus', false) + if(showFilename) { + const relPath = vscode.workspace.asRelativePath(step.rootFile) + stepString = `${relPath}: ${stepString}` + } + } + return stepString +} + +function getStep(): Step | undefined { + let step: Step | undefined + if (stepQueue.steps.length > 0) { + step = stepQueue.steps.shift() + } else if (stepQueue.nextSteps.length > 0) { + stepQueue.steps = stepQueue.nextSteps + stepQueue.nextSteps = [] + step = stepQueue.steps.shift() + } + return step +} + +export const queue = { + add, + prepend, + clear, + isLastStep, + getStep, + getStepString +} \ No newline at end of file diff --git a/src/compile/recipe.ts b/src/compile/recipe.ts new file mode 100644 index 000000000..e34743037 --- /dev/null +++ b/src/compile/recipe.ts @@ -0,0 +1,331 @@ +import vscode from 'vscode' +import path from 'path' +import fs from 'fs' +import * as cp from 'child_process' +import * as lw from '../lw' +import { getLogger } from '../utils/logging/logger' +import { replaceArgumentPlaceholders } from '../utils/utils' +import { queue } from './queue' +import type { Recipe, Tool } from '.' +import { extension } from '../extension' + +const logger = getLogger('Build', 'Recipe') + +export const TEX_MAGIC_PROGRAM_NAME = 'TEX_MAGIC_PROGRAM_NAME' +export const BIB_MAGIC_PROGRAM_NAME = 'BIB_MAGIC_PROGRAM_NAME' +export const MAGIC_PROGRAM_ARGS_SUFFIX = '_WITH_ARGS' +export const MAX_PRINT_LINE = '10000' + +let prevRecipe: Recipe | undefined = undefined +let prevLangId = '' + +/** + * Build LaTeX project using the recipe system. This function creates + * {@link Tool}s containing the tool info and adds them to the queue. After + * that, this function tries to initiate a {@link buildLoop} if there is no + * one running. + * + * @param rootFile Path to the root LaTeX file. + * @param langId The language ID of the root file. This argument is used to + * determine whether the previous recipe can be applied to this root file. + * @param recipeName The name of recipe to be used. If `undefined`, the + * builder tries to determine on its own, in {@link createBuildTools}. This + * parameter is given only when RECIPE command is invoked. For all other + * cases, it should be `undefined`. + */ +export async function build(rootFile: string, langId: string, buildLoop: () => Promise, recipeName?: string) { + logger.log(`Build root file ${rootFile}`) + + await vscode.workspace.saveAll() + + createOutputSubFolders(rootFile) + + const tools = createBuildTools(rootFile, langId, recipeName) + + if (tools === undefined) { + logger.log('Invalid toolchain.') + + extension.compile.compiling = false + return + } + const timestamp = Date.now() + tools.forEach(tool => queue.add(tool, rootFile, recipeName || 'Build', timestamp)) + + await buildLoop() +} + +/** + * Create sub directories of output directory This was supposed to create + * the outputDir as latexmk does not take care of it (neither does any of + * latex command). If the output directory does not exist, the latex + * commands simply fail. + */ +function createOutputSubFolders(rootFile: string) { + const rootDir = path.dirname(rootFile) + let outDir = lw.manager.getOutDir(rootFile) + if (!path.isAbsolute(outDir)) { + outDir = path.resolve(rootDir, outDir) + } + logger.log(`outDir: ${outDir} .`) + extension.cache.getIncludedTeX(rootFile).forEach(file => { + const relativePath = path.dirname(file.replace(rootDir, '.')) + const fullOutDir = path.resolve(outDir, relativePath) + // To avoid issues when fullOutDir is the root dir + // Using fs.mkdir() on the root directory even with recursion will result in an error + try { + if (! (fs.existsSync(fullOutDir) && fs.statSync(fullOutDir).isDirectory())) { + fs.mkdirSync(fullOutDir, { recursive: true }) + } + } catch (e) { + if (e instanceof Error) { + // #4048 + logger.log(`Unexpected Error: ${e.name}: ${e.message} .`) + } else { + logger.log('Unexpected Error: please see the console log of the Developer Tools of VS Code.') + logger.refreshStatus('x', 'errorForeground') + throw(e) + } + } + }) +} + +/** + * Given an optional recipe, create the corresponding {@link Tool}s. + */ +function createBuildTools(rootFile: string, langId: string, recipeName?: string): Tool[] | undefined { + let buildTools: Tool[] = [] + + const configuration = vscode.workspace.getConfiguration('latex-workshop', vscode.Uri.file(rootFile)) + const magic = findMagicComments(rootFile) + + if (recipeName === undefined && magic.tex && !configuration.get('latex.build.forceRecipeUsage')) { + buildTools = createBuildMagic(rootFile, magic.tex, magic.bib) + } else { + const recipe = findRecipe(rootFile, langId, recipeName || magic.recipe) + if (recipe === undefined) { + return + } + logger.log(`Preparing to run recipe: ${recipe.name}.`) + prevRecipe = recipe + prevLangId = langId + const tools = configuration.get('latex.tools') as Tool[] + recipe.tools.forEach(tool => { + if (typeof tool === 'string') { + const candidates = tools.filter(candidate => candidate.name === tool) + if (candidates.length < 1) { + logger.log(`Skipping undefined tool ${tool} in recipe ${recipe.name}.`) + void logger.showErrorMessage(`Skipping undefined tool "${tool}" in recipe "${recipe.name}."`) + } else { + buildTools.push(candidates[0]) + } + } else { + buildTools.push(tool) + } + }) + logger.log(`Prepared ${buildTools.length} tools.`) + } + if (buildTools.length < 1) { + return + } + + // Use JSON.parse and JSON.stringify for a deep copy. + buildTools = JSON.parse(JSON.stringify(buildTools)) as Tool[] + + populateTools(rootFile, buildTools) + + return buildTools +} + +function findMagicComments(rootFile: string): {tex?: Tool, bib?: Tool, recipe?: string} { + const regexTex = /^(?:%\s*!\s*T[Ee]X\s(?:TS-)?program\s*=\s*([^\s]*)$)/m + const regexBib = /^(?:%\s*!\s*BIB\s(?:TS-)?program\s*=\s*([^\s]*)$)/m + const regexTexOptions = /^(?:%\s*!\s*T[Ee]X\s(?:TS-)?options\s*=\s*(.*)$)/m + const regexBibOptions = /^(?:%\s*!\s*BIB\s(?:TS-)?options\s*=\s*(.*)$)/m + const regexRecipe = /^(?:%\s*!\s*LW\srecipe\s*=\s*(.*)$)/m + let content = '' + for (const line of fs.readFileSync(rootFile).toString().split('\n')) { + if (line.startsWith('%') || line.trim().length === 0) { + content += line + '\n' + } else { + break + } + } + + const tex = content.match(regexTex) + let texCommand: Tool | undefined = undefined + if (tex) { + texCommand = { + name: TEX_MAGIC_PROGRAM_NAME, + command: tex[1] + } + logger.log(`Found TeX program by magic comment: ${texCommand.command}.`) + const res = content.match(regexTexOptions) + if (res) { + texCommand.args = [res[1]] + logger.log(`Found TeX options by magic comment: ${texCommand.args}.`) + } + } + + const bib = content.match(regexBib) + let bibCommand: Tool | undefined = undefined + if (bib) { + bibCommand = { + name: BIB_MAGIC_PROGRAM_NAME, + command: bib[1] + } + logger.log(`Found BIB program by magic comment: ${bibCommand.command}.`) + const res = content.match(regexBibOptions) + if (res) { + bibCommand.args = [res[1]] + logger.log(`Found BIB options by magic comment: ${bibCommand.args}.`) + } + } + + const recipe = content.match(regexRecipe) + if (recipe && recipe[1]) { + logger.log(`Found LW recipe '${recipe[1]}' by magic comment: ${recipe}.`) + } + + return {tex: texCommand, bib: bibCommand, recipe: recipe?.[1]} +} + +function createBuildMagic(rootFile: string, magicTex: Tool, magicBib?: Tool): Tool[] { + const configuration = vscode.workspace.getConfiguration('latex-workshop', vscode.Uri.file(rootFile)) + + if (!magicTex.args) { + magicTex.args = configuration.get('latex.magic.args') as string[] + magicTex.name = TEX_MAGIC_PROGRAM_NAME + MAGIC_PROGRAM_ARGS_SUFFIX + } + if (magicBib) { + if (!magicBib.args) { + magicBib.args = configuration.get('latex.magic.bib.args') as string[] + magicBib.name = BIB_MAGIC_PROGRAM_NAME + MAGIC_PROGRAM_ARGS_SUFFIX + } + return [magicTex, magicBib, magicTex, magicTex] + } else { + return [magicTex] + } +} + + +/** + * @param recipeName This recipe name may come from user selection of RECIPE + * command, or from the %! LW recipe magic command. + */ +function findRecipe(rootFile: string, langId: string, recipeName?: string): Recipe | undefined { + const configuration = vscode.workspace.getConfiguration('latex-workshop', vscode.Uri.file(rootFile)) + + const recipes = configuration.get('latex.recipes') as Recipe[] + const defaultRecipeName = configuration.get('latex.recipe.default') as string + + if (recipes.length < 1) { + logger.log('No recipes defined.') + void logger.showErrorMessage('[Builder] No recipes defined.') + return + } + if (prevLangId !== langId) { + prevRecipe = undefined + } + let recipe: Recipe | undefined + // Find recipe according to the given name + if (recipeName === undefined && !['first', 'lastUsed'].includes(defaultRecipeName)) { + recipeName = defaultRecipeName + } + if (recipeName) { + const candidates = recipes.filter(candidate => candidate.name === recipeName) + if (candidates.length < 1) { + logger.log(`Failed to resolve build recipe: ${recipeName}.`) + void logger.showErrorMessage(`[Builder] Failed to resolve build recipe: ${recipeName}.`) + } + recipe = candidates[0] + } + // Find default recipe of last used + if (recipe === undefined && defaultRecipeName === 'lastUsed') { + recipe = prevRecipe + } + // If still not found, fallback to 'first' + if (recipe === undefined) { + let candidates: Recipe[] = recipes + if (langId === 'rsweave') { + candidates = recipes.filter(candidate => candidate.name.toLowerCase().match('rnw|rsweave')) + } else if (langId === 'jlweave') { + candidates = recipes.filter(candidate => candidate.name.toLowerCase().match('jnw|jlweave|weave.jl')) + } else if (langId === 'pweave') { + candidates = recipes.filter(candidate => candidate.name.toLowerCase().match('pnw|pweave')) + } + if (candidates.length < 1) { + logger.log(`Failed to resolve build recipe: ${recipeName}.`) + void logger.showErrorMessage(`Failed to resolve build recipe: ${recipeName}.`) + } + recipe = candidates[0] + } + return recipe +} + +/** + * Expand the bare {@link Tool} with docker and argument placeholder + * strings. + */ +function populateTools(rootFile: string, buildTools: Tool[]): Tool[] { + const configuration = vscode.workspace.getConfiguration('latex-workshop', vscode.Uri.file(rootFile)) + const docker = configuration.get('docker.enabled') + + buildTools.forEach(tool => { + if (docker) { + switch (tool.command) { + case 'latexmk': + logger.log('Use Docker to invoke the command.') + if (process.platform === 'win32') { + tool.command = path.resolve(lw.extensionRoot, './scripts/latexmk.bat') + } else { + tool.command = path.resolve(lw.extensionRoot, './scripts/latexmk') + fs.chmodSync(tool.command, 0o755) + } + break + default: + logger.log(`Do not use Docker to invoke the command: ${tool.command}.`) + break + } + } + tool.args = tool.args?.map(replaceArgumentPlaceholders(rootFile, lw.manager.tmpDir)) + const env = tool.env ?? {} + Object.entries(env).forEach(([key, value]) => { + env[key] = value && replaceArgumentPlaceholders(rootFile, lw.manager.tmpDir)(value) + }) + if (configuration.get('latex.option.maxPrintLine.enabled')) { + tool.args = tool.args ?? [] + const isLuaLatex = tool.args.includes('-lualatex') || + tool.args.includes('-pdflua') || + tool.args.includes('-pdflualatex') || + tool.args.includes('--lualatex') || + tool.args.includes('--pdflua') || + tool.args.includes('--pdflualatex') + if (isMikTeX() && ((tool.command === 'latexmk' && !isLuaLatex) || tool.command === 'pdflatex')) { + tool.args.unshift('--max-print-line=' + MAX_PRINT_LINE) + } + } + }) + return buildTools +} + +let _isMikTeX: boolean +/** + * Whether latex toolchain compilers are provided by MikTeX. This function uses + * a cache variable `_isMikTeX`. + */ +function isMikTeX(): boolean { + if (_isMikTeX === undefined) { + try { + if (cp.execSync('pdflatex --version').toString().match(/MiKTeX/)) { + _isMikTeX = true + logger.log('`pdflatex` is provided by MiKTeX.') + } else { + _isMikTeX = false + } + } catch (e) { + logger.log('Cannot run `pdflatex` to determine if we are using MiKTeX.') + _isMikTeX = false + } + } + return _isMikTeX +} \ No newline at end of file diff --git a/src/compile/terminate.ts b/src/compile/terminate.ts new file mode 100644 index 000000000..130ee5c11 --- /dev/null +++ b/src/compile/terminate.ts @@ -0,0 +1,36 @@ +import * as cp from 'child_process' +import { getLogger } from '../utils/logging/logger' +import { queue } from './queue' +import { extension } from '../extension' + +const logger = getLogger('Build', 'Recipe') + +/** + * Terminate current process of LaTeX building. OS-specific (pkill for linux + * and macos, taskkill for win) kill command is first called with process + * pid. No matter whether it succeeded, `kill()` of `child_process` is later + * called to "double kill". Also, all subsequent tools in queue are cleared, + * including ones in the current recipe and (if available) those from the + * cached recipe to be executed. + */ +export function terminate() { + if (extension.compile.process === undefined) { + logger.log('LaTeX build process to kill is not found.') + return + } + const pid = extension.compile.process.pid + try { + logger.log(`Kill child processes of the current process with PID ${pid}.`) + if (process.platform === 'linux' || process.platform === 'darwin') { + cp.execSync(`pkill -P ${pid}`, { timeout: 1000 }) + } else if (process.platform === 'win32') { + cp.execSync(`taskkill /F /T /PID ${pid}`, { timeout: 1000 }) + } + } catch (e) { + logger.logError('Failed killing child processes of the current process.', e) + } finally { + queue.clear() + extension.compile.process.kill() + logger.log(`Killed the current process with PID ${pid}`) + } +} \ No newline at end of file diff --git a/src/completion/completer/citation.ts b/src/completion/completer/citation.ts index 11ff6b6a7..da46fbe22 100644 --- a/src/completion/completer/citation.ts +++ b/src/completion/completer/citation.ts @@ -1,14 +1,15 @@ import * as vscode from 'vscode' import * as fs from 'fs' -import {bibtexParser} from 'latex-utensils' +import { bibtexParser } from 'latex-utensils' import * as lw from '../../lw' +import type { FileCache } from '../../types' import * as eventbus from '../../core/event-bus' -import {trimMultiLineString} from '../../utils/utils' -import {computeFilteringRange} from './completerutils' +import { trimMultiLineString } from '../../utils/utils' +import { computeFilteringRange } from './completerutils' import type { IProvider, ICompletionItem, IProviderArgs } from '../latex' import { getLogger } from '../../utils/logging/logger' import { parser } from '../../parse/parser' -import { Cache } from '../../core/cache' +import { extension } from '../../extension' const logger = getLogger('Intelli', 'Citation') @@ -132,10 +133,12 @@ export class Citation implements IProvider { */ private readonly bibEntries = new Map() - constructor() { - lw.cacher.bib.onCreate(filePath => this.parseBibFile(filePath)) - lw.cacher.bib.onChange(filePath => this.parseBibFile(filePath)) - lw.cacher.bib.onDelete(filePath => this.removeEntriesInFile(filePath)) + constructor() {} + + initialize() { + extension.watcher.bib.onCreate(filePath => this.parseBibFile(filePath)) + extension.watcher.bib.onChange(filePath => this.parseBibFile(filePath)) + extension.watcher.bib.onDelete(filePath => this.removeEntriesInFile(filePath)) } provideFrom(_result: RegExpMatchArray, args: IProviderArgs) { @@ -229,21 +232,21 @@ export class Citation implements IProvider { /** * Returns the array of the paths of `.bib` files referenced from `file`. * - * @param file The path of a LaTeX file. If `undefined`, the keys of `bibEntries` are used. + * @param filePath The path of a LaTeX file. If `undefined`, the keys of `bibEntries` are used. * @param visitedTeX Internal use only. */ - private getIncludedBibs(file?: string, visitedTeX: string[] = []): string[] { - if (file === undefined) { + private getIncludedBibs(filePath?: string, visitedTeX: string[] = []): string[] { + if (filePath === undefined) { // Only happens when rootFile is undefined return Array.from(this.bibEntries.keys()) } - const cache = lw.cacher.get(file) - if (cache === undefined) { + const fileCache = extension.cache.get(filePath) + if (fileCache === undefined) { return [] } - let bibs = Array.from(cache.bibfiles) - visitedTeX.push(file) - for (const child of cache.children) { + let bibs = Array.from(fileCache.bibfiles) + visitedTeX.push(filePath) + for (const child of fileCache.children) { if (visitedTeX.includes(child.filePath)) { // Already included continue @@ -271,17 +274,17 @@ export class Citation implements IProvider { } }) // From caches - lw.cacher.getIncludedTeX().forEach(cachedFile => { - const cachedBibs = lw.cacher.get(cachedFile)?.elements.bibitem - if (cachedBibs === undefined) { + extension.cache.getIncludedTeX().forEach(filePath => { + const bibCache = extension.cache.get(filePath)?.elements.bibitem + if (bibCache === undefined) { return } - suggestions = suggestions.concat(cachedBibs.map(bib => { + suggestions = suggestions.concat(bibCache.map(bib => { return { ...bib, key: bib.label, detail: bib.detail ? bib.detail : '', - file: cachedFile, + file: filePath, fields: new Fields() } })) @@ -348,7 +351,7 @@ export class Citation implements IProvider { * Cache `content` is parsed with regular expressions, * and the result is used to update the cache bibitem element. */ - parse(cache: Cache) { + parse(cache: FileCache) { cache.elements.bibitem = this.parseContent(cache.filePath, cache.content) } diff --git a/src/completion/completer/command.ts b/src/completion/completer/command.ts index 8b862f9f3..c87784ccb 100644 --- a/src/completion/completer/command.ts +++ b/src/completion/completer/command.ts @@ -8,7 +8,8 @@ import {SurroundCommand} from './commandlib/surround' import { Environment, EnvSnippetType } from './environment' import { getLogger } from '../../utils/logging/logger' -import { Cache } from '../../core/cache' +import type { FileCache } from '../../types' +import { extension } from '../../extension' const logger = getLogger('Intelli', 'Command') @@ -153,10 +154,10 @@ export class Command implements IProvider { // Start working on commands in tex. To avoid over populating suggestions, we do not include // user defined commands, whose name matches a default command or one provided by a package defined = new Set(suggestions.map(s => s.signatureAsString())) - lw.cacher.getIncludedTeX().forEach(tex => { - const cmds = lw.cacher.get(tex)?.elements.command - if (cmds !== undefined) { - cmds.forEach(cmd => { + extension.cache.getIncludedTeX().forEach(filePath => { + const cmdCache = extension.cache.get(filePath)?.elements.command + if (cmdCache !== undefined) { + cmdCache.forEach(cmd => { if (!defined.has(cmd.signatureAsString())) { cmd.range = range suggestions.push(cmd) @@ -185,7 +186,7 @@ export class Command implements IProvider { SurroundCommand.surround(cmdItems) } - parse(cache: Cache) { + parse(cache: FileCache) { // Remove newcommand cmds, because they will be re-insert in the next step this.definedCmds.forEach((entry,cmd) => { if (entry.filePath === cache.filePath) { diff --git a/src/completion/completer/environment.ts b/src/completion/completer/environment.ts index ad0ffffb8..201149f26 100644 --- a/src/completion/completer/environment.ts +++ b/src/completion/completer/environment.ts @@ -6,7 +6,8 @@ import type { ICompletionItem, IProvider, IProviderArgs } from '../latex' import { CmdEnvSuggestion, splitSignatureString, filterNonLetterSuggestions, filterArgumentHint } from './completerutils' import { getLogger } from '../../utils/logging/logger' -import { Cache } from '../../core/cache' +import type { FileCache } from '../../types' +import { extension } from '../../extension' const logger = getLogger('Intelli', 'Environment') @@ -139,10 +140,10 @@ export class Environment implements IProvider { } // Insert environments defined in tex - lw.cacher.getIncludedTeX().forEach(cachedFile => { - const cachedEnvs = lw.cacher.get(cachedFile)?.elements.environment - if (cachedEnvs !== undefined) { - cachedEnvs.forEach(env => { + extension.cache.getIncludedTeX().forEach(filePath => { + const envCache = extension.cache.get(filePath)?.elements.environment + if (envCache !== undefined) { + envCache.forEach(env => { if (! envList.includes(env.label)) { if (snippetType === EnvSnippetType.ForBegin) { env.insertText = new vscode.SnippetString(`${env.label}}\n\t$0\n\\end{${env.label}}`) @@ -196,7 +197,7 @@ export class Environment implements IProvider { } } - parse(cache: Cache) { + parse(cache: FileCache) { if (cache.ast !== undefined) { cache.elements.environment = this.parseAst(cache.ast) } else { diff --git a/src/completion/completer/glossary.ts b/src/completion/completer/glossary.ts index c4792be1d..9bfb79703 100644 --- a/src/completion/completer/glossary.ts +++ b/src/completion/completer/glossary.ts @@ -1,10 +1,10 @@ import * as vscode from 'vscode' import type * as Ast from '@unified-latex/unified-latex-types' -import * as lw from '../../lw' import type { ICompletionItem, IProvider } from '../latex' -import { Cache } from '../../core/cache' import { argContentToStr } from '../../utils/parser' import { getLongestBalancedString } from '../../utils/utils' +import type { FileCache } from '../../types' +import { extension } from '../../extension' enum GlossaryType { glossary, @@ -55,18 +55,18 @@ export class Glossary implements IProvider { // Extract cached references const glossaryList: string[] = [] - lw.cacher.getIncludedTeX().forEach(cachedFile => { - const cachedGlossaries = lw.cacher.get(cachedFile)?.elements.glossary - if (cachedGlossaries === undefined) { + extension.cache.getIncludedTeX().forEach(filePath => { + const glsCache = extension.cache.get(filePath)?.elements.glossary + if (glsCache === undefined) { return } - cachedGlossaries.forEach(ref => { - if (ref.type === GlossaryType.glossary) { - this.glossaries.set(ref.label, ref) + glsCache.forEach(gls => { + if (gls.type === GlossaryType.glossary) { + this.glossaries.set(gls.label, gls) } else { - this.acronyms.set(ref.label, ref) + this.acronyms.set(gls.label, gls) } - glossaryList.push(ref.label) + glossaryList.push(gls.label) }) }) @@ -83,7 +83,7 @@ export class Glossary implements IProvider { }) } - parse(cache: Cache) { + parse(cache: FileCache) { if (cache.ast !== undefined) { cache.elements.glossary = this.parseAst(cache.ast, cache.filePath) } else { diff --git a/src/completion/completer/input.ts b/src/completion/completer/input.ts index 068801d5d..0b4c0f89f 100644 --- a/src/completion/completer/input.ts +++ b/src/completion/completer/input.ts @@ -6,7 +6,7 @@ import * as lw from '../../lw' import type { IProvider, IProviderArgs } from '../latex' import { getLogger } from '../../utils/logging/logger' -import type { Cache } from '../../core/cache' +import { type FileCache } from '../../types' const logger = getLogger('Intelli', 'Input') @@ -50,7 +50,7 @@ abstract class InputAbstract implements IProvider { /** * Set the graphics path */ - parseGraphicsPath(cache: Cache) { + parseGraphicsPath(cache: FileCache) { const regex = /\\graphicspath{[\s\n]*((?:{[^{}]*}[\s\n]*)*)}/g let result: string[] | null while (true) { diff --git a/src/completion/completer/package.ts b/src/completion/completer/package.ts index 1d188fb10..1949c0e20 100644 --- a/src/completion/completer/package.ts +++ b/src/completion/completer/package.ts @@ -4,8 +4,9 @@ import type * as Ast from '@unified-latex/unified-latex-types' import * as lw from '../../lw' import type { IProvider } from '../latex' import { argContentToStr } from '../../utils/parser' -import { Cache } from '../../core/cache' import { kpsewhich } from '../../core/cacherlib/pathutils' +import type { FileCache } from '../../types' +import { extension } from '../../extension' type DataPackagesJsonType = typeof import('../../../data/packagenames.json') @@ -74,12 +75,12 @@ export class Package implements IProvider { .filter(packageName => !excluded.includes(packageName)) .forEach(packageName => packages[packageName] = []) - lw.cacher.getIncludedTeX().forEach(tex => { - const included = lw.cacher.get(tex)?.elements.package - if (included === undefined) { + extension.cache.getIncludedTeX().forEach(filePath => { + const pkgCache = extension.cache.get(filePath)?.elements.package + if (pkgCache === undefined) { return } - Object.entries(included) + Object.entries(pkgCache) .filter(([packageName, ]) => !excluded.includes(packageName)) .forEach(([packageName, options]) => packages[packageName] = options) }) @@ -106,7 +107,7 @@ export class Package implements IProvider { return packages } - parse(cache: Cache) { + parse(cache: FileCache) { if (cache.ast !== undefined) { cache.elements.package = this.parseAst(cache.ast) } else { diff --git a/src/completion/completer/reference.ts b/src/completion/completer/reference.ts index 9ef8547ed..b11471a88 100644 --- a/src/completion/completer/reference.ts +++ b/src/completion/completer/reference.ts @@ -7,7 +7,8 @@ import { getLongestBalancedString, stripEnvironments } from '../../utils/utils' import { computeFilteringRange } from './completerutils' import type { IProvider, ICompletionItem, IProviderArgs } from '../latex' import { argContentToStr } from '../../utils/parser' -import { Cache } from '../../core/cache' +import type { FileCache } from '../../types' +import { extension } from '../../extension' export interface ReferenceEntry extends ICompletionItem { /** The file that defines the ref. */ @@ -84,9 +85,9 @@ export class Reference implements IProvider { // 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.cacher.getIncludedTeX(cachedFile).forEach(includedTeX => { - if (includedTeX === cachedFile) { + included.forEach(filePath => { + extension.cache.getIncludedTeX(filePath).forEach(includedTeX => { + if (includedTeX === filePath) { return } included.add(includedTeX) @@ -95,11 +96,11 @@ export class Reference implements IProvider { // removed as it can be directly referenced without. delete prefixes[includedTeX] }) - const cache = lw.cacher.get(cachedFile) - if (!cache) { + const fileCache = extension.cache.get(filePath) + if (!fileCache) { return } - Object.keys(cache.external).forEach(external => { + Object.keys(fileCache.external).forEach(external => { // Don't repeatedly add, no matter previously by \input or // `xr` if (included.has(external)) { @@ -108,7 +109,7 @@ export class Reference implements IProvider { // If the file is included by `xr`, both file path and // prefix is recorded. included.add(external) - prefixes[external] = cache.external[external] + prefixes[external] = fileCache.external[external] }) }) if (included.size === startSize) { @@ -123,8 +124,8 @@ export class Reference implements IProvider { range = computeFilteringRange(line, position) } - included.forEach(cachedFile => { - const cachedRefs = lw.cacher.get(cachedFile)?.elements.reference + included.forEach(filePath => { + const cachedRefs = extension.cache.get(filePath)?.elements.reference if (cachedRefs === undefined) { return } @@ -132,10 +133,10 @@ export class Reference implements IProvider { if (ref.range === undefined) { return } - const label = (cachedFile in prefixes ? prefixes[cachedFile] : '') + ref.label + const label = (filePath in prefixes ? prefixes[filePath] : '') + ref.label this.suggestions.set(label, {...ref, label, - file: cachedFile, + file: filePath, position: 'inserting' in ref.range ? ref.range.inserting.start : ref.range.start, range, prevIndex: this.prevIndexObj.get(label) @@ -151,7 +152,7 @@ export class Reference implements IProvider { }) } - parse(cache: Cache) { + parse(cache: FileCache) { if (cache.ast !== undefined) { const configuration = vscode.workspace.getConfiguration('latex-workshop') const labelMacros = configuration.get('intellisense.label.command') as string[] diff --git a/src/core/cache.ts b/src/core/cache.ts index 5c3a878ae..d10b76684 100644 --- a/src/core/cache.ts +++ b/src/core/cache.ts @@ -1,472 +1,358 @@ 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 * as lw from '../lw' import * as eventbus from './event-bus' -import * as utils from '../utils/utils' -import type { CmdEnvSuggestion } from '../completion/completer/completerutils' -import type { CiteSuggestion } from '../completion/completer/citation' -import type { GlossarySuggestion } from '../completion/completer/glossary' -import type { ICompletionItem } from '../completion/latex' -import { InputFileRegExp } from '../utils/inputfilepath' import * as CacherUtils from './cacherlib/cacherutils' import * as PathUtils from './cacherlib/pathutils' -import { Watcher } from './cacherlib/watcher' +import * as utils from '../utils/utils' +import { InputFileRegExp } from '../utils/inputfilepath' import { getLogger } from '../utils/logging/logger' import { parser } from '../parse/parser' import { performance } from 'perf_hooks' +import { extension } from '../extension' +import type { FileCache } from '../types' +import type { Watcher } from './watcher' +import { getIncludedTeX, getIncludedBib, getTeXChildren, getFlsChildren } from './project' + const logger = getLogger('Cacher') -export interface Cache { - /** The raw file path of this Cache. */ - filePath: string, - /** Cached content of file. Dirty if opened in vscode, disk otherwise */ - content: string, - /** Cached trimmed content of `content`. */ - contentTrimmed: string, - /** Completion items */ - elements: { - /** \ref{} items */ - reference?: ICompletionItem[], - /** \gls items */ - glossary?: GlossarySuggestion[], - /** \begin{} items */ - environment?: CmdEnvSuggestion[], - /** \cite{} items from \bibitem definition */ - bibitem?: CiteSuggestion[], - /** command items */ - command?: CmdEnvSuggestion[], - /** \usepackage{}, a dictionary whose key is package name and value is the options */ - package?: {[packageName: string]: string[]} - }, - /** The sub-files of the LaTeX file. They should be tex or plain files */ - children: { - /** The index of character sub-content is inserted */ - index: number, - /** The path of the sub-file */ - filePath: string - }[], - /** The array of the paths of `.bib` files referenced from the LaTeX file */ - bibfiles: Set, - /** A dictionary of external documents provided by `\externaldocument` of - * `xr` package. The value is its prefix `\externaldocument[prefix]{*}` */ - external: {[filePath: string]: string}, - /** The AST of this file, generated by unified-latex */ - ast?: Ast.Root +const caches: {[filePath: string]: FileCache | undefined} = {} +const promises: {[filePath: string]: Promise | undefined} = {} + +export const cache = { + initialize, + add, + get, + paths, + promises, + getIncludedTeX, + getIncludedBib, + getTeXChildren, + getFlsChildren, + wait, + reset, + refreshCache, + refreshCacheAggressive, + loadFlsFile } -export class Cacher { - private readonly caches: {[filePath: string]: Cache} = {} - readonly src: Watcher = new Watcher() - readonly pdf: Watcher = new Watcher('.pdf') - readonly bib: Watcher = new Watcher('.bib') - private caching = 0 - private promises: {[filePath: string]: Promise} = {} - - constructor() { - this.src.onChange((filePath: string) => { - if (CacherUtils.canCache(filePath)) { - void this.refreshCache(filePath) - } - }) - this.src.onDelete((filePath: string) => { - if (filePath in this.caches) { - delete this.caches[filePath] - logger.log(`Removed ${filePath} .`) - } - }) - } +export const testUtils = { + caches +} - add(filePath: string) { - if (CacherUtils.isExcluded(filePath)) { - logger.log(`Ignored ${filePath} .`) - return +function initialize(watcher: Watcher) { + watcher.onChange((filePath: string) => { + if (CacherUtils.canCache(filePath)) { + void refreshCache(filePath) } - if (!this.src.has(filePath)) { - logger.log(`Adding ${filePath} .`) - this.src.add(filePath) + }) + watcher.onDelete((filePath: string) => { + if (filePath in caches) { + delete caches[filePath] + logger.log(`Removed ${filePath} .`) } - } + }) +} - has(filePath: string) { - return this.caches[filePath] !== undefined +function add(filePath: string) { + if (CacherUtils.isExcluded(filePath)) { + logger.log(`Ignored ${filePath} .`) + return } - - get(filePath: string): Cache | undefined { - return this.caches[filePath] + if (!extension.watcher.src.has(filePath)) { + logger.log(`Adding ${filePath} .`) + extension.watcher.src.add(filePath) } +} - promise(filePath: string): Promise | undefined { - return this.promises[filePath] - } +function get(filePath: string) { + return caches[filePath] +} - async wait(filePath: string, seconds = 2) { - let waited = 0 - while (!this.promise(filePath) && !this.has(filePath)) { - // Just open vscode, has not cached, wait for a bit? - await new Promise(resolve => setTimeout(resolve, 100)) - waited++ - if (waited >= seconds * 10) { - // Waited for two seconds before starting cache. Really? - logger.log(`Error loading cache: ${filePath} . Forcing.`) - await this.refreshCache(filePath) - break - } +function paths() { + return Object.keys(caches) +} + +async function wait(filePath: string, seconds = 2) { + let waited = 0 + while (promises[filePath] === undefined && caches[filePath] === undefined) { + // Just open vscode, has not cached, wait for a bit? + await new Promise(resolve => setTimeout(resolve, 100)) + waited++ + if (waited >= seconds * 10) { + // Waited for two seconds before starting cache. Really? + logger.log(`Error loading cache: ${filePath} . Forcing.`) + await refreshCache(filePath) + break } - return this.promise(filePath) } + return promises[filePath] +} - get allPromises() { - return Object.values(this.promises) - } +function reset() { + extension.watcher.src.reset() + extension.watcher.bib.reset() + extension.watcher.pdf.reset() + Object.keys(caches).forEach(filePath => delete caches[filePath]) +} - get allPaths() { - return Object.keys(this.caches) +let cachingFilesCount = 0 +async function refreshCache(filePath: string, rootPath?: string) { + if (CacherUtils.isExcluded(filePath)) { + logger.log(`Ignored ${filePath} .`) + return } - - reset() { - this.src.reset() - this.bib.reset() - this.pdf.reset() - Object.keys(this.caches).forEach(filePath => delete this.caches[filePath]) + if (!CacherUtils.canCache(filePath)) { + return } - - async refreshCache(filePath: string, rootPath?: string) { - if (CacherUtils.isExcluded(filePath)) { - logger.log(`Ignored ${filePath} .`) - return - } - if (!CacherUtils.canCache(filePath)) { - return + logger.log(`Caching ${filePath} .`) + cachingFilesCount++ + const openEditor: vscode.TextDocument | undefined = vscode.workspace.textDocuments.filter(document => document.fileName === path.normalize(filePath))?.[0] + const content = openEditor?.isDirty ? openEditor.getText() : (lw.lwfs.readFileSyncGracefully(filePath) ?? '') + const cache: FileCache = { + filePath, + content, + contentTrimmed: utils.stripCommentsAndVerbatim(content), + elements: {}, + children: [], + bibfiles: new Set(), + external: {}} + caches[filePath] = cache + rootPath = rootPath || lw.manager.rootFile + updateChildren(cache, rootPath) + + promises[filePath] = updateAST(cache).then(() => { + updateElements(cache) + }).finally(() => { + lw.dupLabelDetector.run() + cachingFilesCount-- + delete promises[filePath] + lw.eventBus.fire(eventbus.FileParsed, filePath) + + if (cachingFilesCount === 0) { + void lw.structureViewer.reconstruct() } - logger.log(`Caching ${filePath} .`) - this.caching++ - const openEditor: vscode.TextDocument | undefined = vscode.workspace.textDocuments.filter(document => document.fileName === path.normalize(filePath))?.[0] - const content = openEditor?.isDirty ? openEditor.getText() : (lw.lwfs.readFileSyncGracefully(filePath) ?? '') - const cache: Cache = { - filePath, - content, - contentTrimmed: utils.stripCommentsAndVerbatim(content), - elements: {}, - children: [], - bibfiles: new Set(), - external: {}} - this.caches[filePath] = cache - rootPath = rootPath || lw.manager.rootFile - this.updateChildren(cache, rootPath) - - this.promises[filePath] = this.updateAST(cache).then(() => { - this.updateElements(cache) - }).finally(() => { - lw.dupLabelDetector.run() - this.caching-- - delete this.promises[filePath] - lw.eventBus.fire(eventbus.FileParsed, filePath) - - if (this.caching === 0) { - void lw.structureViewer.reconstruct() - } - }) + }) - return this.promises[filePath] - } + return promises[filePath] +} - private async updateAST(cache: Cache): Promise { - logger.log(`Parse LaTeX AST: ${cache.filePath} .`) - cache.ast = await parser.parseLaTeX(cache.content) - logger.log(`Parsed LaTeX AST: ${cache.filePath} .`) +let updateCompleter: NodeJS.Timeout +function refreshCacheAggressive(filePath: string) { + if (caches[filePath] === undefined) { + return } - - private updateChildren(cache: Cache, rootPath: string | undefined) { - rootPath = rootPath || cache.filePath - this.updateChildrenInput(cache, rootPath) - this.updateChildrenXr(cache, rootPath) - logger.log(`Updated inputs of ${cache.filePath} .`) + const configuration = vscode.workspace.getConfiguration('latex-workshop') + if (configuration.get('intellisense.update.aggressive.enabled')) { + if (updateCompleter) { + clearTimeout(updateCompleter) + } + updateCompleter = setTimeout(() => { + void refreshCache(filePath, lw.manager.rootFile).then(async () => { + await loadFlsFile(lw.manager.rootFile || filePath) + }) + }, configuration.get('intellisense.update.delay', 1000)) } +} - private updateChildrenInput(cache: Cache, rootPath: string) { - const inputFileRegExp = new InputFileRegExp() - while (true) { - const result = inputFileRegExp.exec(cache.contentTrimmed, cache.filePath, rootPath) - if (!result) { - break - } - - if (!fs.existsSync(result.path) || path.relative(result.path, rootPath) === '') { - continue - } +async function updateAST(cache: FileCache): Promise { + logger.log(`Parse LaTeX AST: ${cache.filePath} .`) + cache.ast = await parser.parseLaTeX(cache.content) + logger.log(`Parsed LaTeX AST: ${cache.filePath} .`) +} - cache.children.push({ - index: result.match.index, - filePath: result.path - }) - logger.log(`Input ${result.path} from ${cache.filePath} .`) +function updateChildren(cache: FileCache, rootPath: string | undefined) { + rootPath = rootPath || cache.filePath + updateChildrenInput(cache, rootPath) + updateChildrenXr(cache, rootPath) + logger.log(`Updated inputs of ${cache.filePath} .`) +} - if (this.src.has(result.path)) { - continue - } - this.add(result.path) - void this.refreshCache(result.path, rootPath) +function updateChildrenInput(cache: FileCache, rootPath: string) { + const inputFileRegExp = new InputFileRegExp() + while (true) { + const result = inputFileRegExp.exec(cache.contentTrimmed, cache.filePath, rootPath) + if (!result) { + break } - } - private updateChildrenXr(cache: Cache, rootPath: string) { - const externalDocRegExp = /\\externaldocument(?:\[(.*?)\])?\{(.*?)\}/g - while (true) { - const result = externalDocRegExp.exec(cache.contentTrimmed) - if (!result) { - break - } - - const texDirs = vscode.workspace.getConfiguration('latex-workshop').get('latex.texDirs') as string[] - const externalPath = utils.resolveFile([path.dirname(cache.filePath), path.dirname(rootPath), ...texDirs], result[2]) - if (!externalPath || !fs.existsSync(externalPath) || path.relative(externalPath, rootPath) === '') { - logger.log(`Failed resolving external ${result[2]} . Tried ${externalPath} ` + - (externalPath && path.relative(externalPath, rootPath) === '' ? ', which is root.' : '.')) - continue - } + if (!fs.existsSync(result.path) || path.relative(result.path, rootPath) === '') { + continue + } - this.caches[rootPath].external[externalPath] = result[1] || '' - logger.log(`External document ${externalPath} from ${cache.filePath} .` + - (result[1] ? ` Prefix is ${result[1]}`: '')) + cache.children.push({ + index: result.match.index, + filePath: result.path + }) + logger.log(`Input ${result.path} from ${cache.filePath} .`) - if (this.src.has(externalPath)) { - continue - } - this.add(externalPath) - void this.refreshCache(externalPath, externalPath) + if (extension.watcher.src.has(result.path)) { + continue } + add(result.path) + void refreshCache(result.path, rootPath) } +} - private updateElements(cache: Cache) { - const start = performance.now() - lw.completer.citation.parse(cache) - // Package parsing must be before command and environment. - lw.completer.package.parse(cache) - lw.completer.reference.parse(cache) - lw.completer.glossary.parse(cache) - lw.completer.environment.parse(cache) - lw.completer.command.parse(cache) - lw.completer.input.parseGraphicsPath(cache) - this.updateBibfiles(cache) - const elapsed = performance.now() - start - logger.log(`Updated elements in ${elapsed.toFixed(2)} ms: ${cache.filePath} .`) - } +function updateChildrenXr(cache: FileCache, rootPath: string) { + const externalDocRegExp = /\\externaldocument(?:\[(.*?)\])?\{(.*?)\}/g + while (true) { + const result = externalDocRegExp.exec(cache.contentTrimmed) + if (!result) { + break + } - private updateBibfiles(cache: Cache) { - const bibReg = /(?:\\(?:bibliography|addbibresource)(?:\[[^[\]{}]*\])?){([\s\S]+?)}|(?:\\putbib)\[(.+?)\]/gm - while (true) { - const result = bibReg.exec(cache.contentTrimmed) - if (!result) { - break - } + const texDirs = vscode.workspace.getConfiguration('latex-workshop').get('latex.texDirs') as string[] + const externalPath = utils.resolveFile([path.dirname(cache.filePath), path.dirname(rootPath), ...texDirs], result[2]) + if (!externalPath || !fs.existsSync(externalPath) || path.relative(externalPath, rootPath) === '') { + logger.log(`Failed resolving external ${result[2]} . Tried ${externalPath} ` + + (externalPath && path.relative(externalPath, rootPath) === '' ? ', which is root.' : '.')) + continue + } - const bibs = (result[1] ? result[1] : result[2]).split(',').map(bib => bib.trim()) + const rootCache = caches[rootPath] + if (rootCache !== undefined) { + rootCache.external[externalPath] = result[1] || '' + logger.log(`External document ${externalPath} from ${cache.filePath} .` + (result[1] ? ` Prefix is ${result[1]}`: '')) + } - for (const bib of bibs) { - const bibPaths = PathUtils.resolveBibPath(bib, path.dirname(cache.filePath)) - for (const bibPath of bibPaths) { - cache.bibfiles.add(bibPath) - logger.log(`Bib ${bibPath} from ${cache.filePath} .`) - if (!this.bib.has(bibPath)) { - this.bib.add(bibPath) - } - } - } + if (extension.watcher.src.has(externalPath)) { + continue } + add(externalPath) + void refreshCache(externalPath, externalPath) } +} - /** - * Parses the content of a `.fls` file attached to the given `srcFile`. - * All `INPUT` files are considered as subfiles/non-tex files included in `srcFile`, - * and all `OUTPUT` files will be checked if they are `.aux` files. - * If so, the `.aux` files are parsed for any possible `.bib` files. - * - * This function is called after a successful build, when looking for the root file, - * and to compute the cachedContent tree. - * - * @param filePath The path of a LaTeX file. - */ - async loadFlsFile(filePath: string) { - const flsPath = PathUtils.getFlsFilePath(filePath) - if (flsPath === undefined) { - return - } - logger.log(`Parsing .fls ${flsPath} .`) - const rootDir = path.dirname(filePath) - const outDir = lw.manager.getOutDir(filePath) - const ioFiles = CacherUtils.parseFlsContent(fs.readFileSync(flsPath).toString(), rootDir) - - for (const inputFile of ioFiles.input) { - // Drop files that are also listed as OUTPUT or should be ignored - if (ioFiles.output.includes(inputFile) || - CacherUtils.isExcluded(inputFile) || - !fs.existsSync(inputFile)) { - continue - } - if (inputFile === filePath || this.src.has(inputFile)) { - // Drop the current rootFile often listed as INPUT - // Drop any file that is already watched as it is handled by - // onWatchedFileChange. - continue - } - if (path.extname(inputFile) === '.tex') { - if (!this.has(filePath)) { - logger.log(`Cache not finished on ${filePath} when parsing fls, try re-cache.`) - await this.refreshCache(filePath) - } - // It might be possible that `filePath` is excluded from caching. - if (this.has(filePath)) { - // Parse tex files as imported subfiles. - this.caches[filePath].children.push({ - index: Number.MAX_VALUE, - filePath: inputFile - }) - this.add(inputFile) - logger.log(`Found ${inputFile} from .fls ${flsPath} , caching.`) - void this.refreshCache(inputFile, filePath) - } else { - logger.log(`Cache not finished on ${filePath} when parsing fls.`) - } - } else if (!this.src.has(inputFile)) { - // Watch non-tex files. - this.add(inputFile) - } - } +function updateElements(cache: FileCache) { + const start = performance.now() + lw.completer.citation.parse(cache) + // Package parsing must be before command and environment. + lw.completer.package.parse(cache) + lw.completer.reference.parse(cache) + lw.completer.glossary.parse(cache) + lw.completer.environment.parse(cache) + lw.completer.command.parse(cache) + lw.completer.input.parseGraphicsPath(cache) + updateBibfiles(cache) + const elapsed = performance.now() - start + logger.log(`Updated elements in ${elapsed.toFixed(2)} ms: ${cache.filePath} .`) +} - for (const outputFile of ioFiles.output) { - if (path.extname(outputFile) === '.aux' && fs.existsSync(outputFile)) { - logger.log(`Found .aux ${filePath} from .fls ${flsPath} , parsing.`) - this.parseAuxFile(outputFile, path.dirname(outputFile).replace(outDir, rootDir)) - logger.log(`Parsed .aux ${filePath} .`) - } +function updateBibfiles(cache: FileCache) { + const bibReg = /(?:\\(?:bibliography|addbibresource)(?:\[[^[\]{}]*\])?){([\s\S]+?)}|(?:\\putbib)\[(.+?)\]/gm + while (true) { + const result = bibReg.exec(cache.contentTrimmed) + if (!result) { + break } - logger.log(`Parsed .fls ${flsPath} .`) - } - private parseAuxFile(filePath: string, srcDir: string) { - const content = fs.readFileSync(filePath).toString() - const regex = /^\\bibdata{(.*)}$/gm - while (true) { - const result = regex.exec(content) - if (!result) { - return - } - const bibs = (result[1] ? result[1] : result[2]).split(',').map((bib) => { - return bib.trim() - }) - for (const bib of bibs) { - const bibPaths = PathUtils.resolveBibPath(bib, srcDir) - for (const bibPath of bibPaths) { - if (lw.manager.rootFile && !this.get(lw.manager.rootFile)?.bibfiles.has(bibPath)) { - this.get(lw.manager.rootFile)?.bibfiles.add(bibPath) - logger.log(`Found .bib ${bibPath} from .aux ${filePath} .`) - } - if (!this.bib.has(bibPath)) { - this.bib.add(bibPath) - } + const bibs = (result[1] ? result[1] : result[2]).split(',').map(bib => bib.trim()) + + for (const bib of bibs) { + const bibPaths = PathUtils.resolveBibPath(bib, path.dirname(cache.filePath)) + for (const bibPath of bibPaths) { + cache.bibfiles.add(bibPath) + logger.log(`Bib ${bibPath} from ${cache.filePath} .`) + if (!extension.watcher.bib.has(bibPath)) { + extension.watcher.bib.add(bibPath) } } } } +} - /** - * Return a string array which holds all imported bib files - * from the given tex `file`. If `file` is `undefined`, traces from the - * root file, or return empty array if the root file is `undefined` - * - * @param file The path of a LaTeX file - */ - getIncludedBib(file?: string, includedBib: string[] = []): string[] { - file = file ?? lw.manager.rootFile - if (file === undefined) { - return [] +/** + * Parses the content of a `.fls` file attached to the given `srcFile`. + * All `INPUT` files are considered as subfiles/non-tex files included in `srcFile`, + * and all `OUTPUT` files will be checked if they are `.aux` files. + * If so, the `.aux` files are parsed for any possible `.bib` files. + * + * This function is called after a successful build, when looking for the root file, + * and to compute the cachedContent tree. + * + * @param filePath The path of a LaTeX file. + */ +async function loadFlsFile(filePath: string) { + const flsPath = PathUtils.getFlsFilePath(filePath) + if (flsPath === undefined) { + return + } + logger.log(`Parsing .fls ${flsPath} .`) + const rootDir = path.dirname(filePath) + const outDir = lw.manager.getOutDir(filePath) + const ioFiles = CacherUtils.parseFlsContent(fs.readFileSync(flsPath).toString(), rootDir) + + for (const inputFile of ioFiles.input) { + // Drop files that are also listed as OUTPUT or should be ignored + if (ioFiles.output.includes(inputFile) || + CacherUtils.isExcluded(inputFile) || + !fs.existsSync(inputFile)) { + continue } - if (!this.has(file)) { - return [] + if (inputFile === filePath || extension.watcher.src.has(inputFile)) { + // Drop the current rootFile often listed as INPUT + // Drop any file that is already watched as it is handled by + // onWatchedFileChange. + continue } - const checkedTeX = [ file ] - const cache = this.get(file) - if (cache) { - includedBib.push(...cache.bibfiles) - for (const child of cache.children) { - if (checkedTeX.includes(child.filePath)) { - // Already parsed - continue - } - this.getIncludedBib(child.filePath, includedBib) + if (path.extname(inputFile) === '.tex') { + if (caches[filePath] === undefined) { + logger.log(`Cache not finished on ${filePath} when parsing fls, try re-cache.`) + await refreshCache(filePath) + } + // It might be possible that `filePath` is excluded from caching. + const cache = caches[filePath] + if (cache !== undefined) { + // Parse tex files as imported subfiles. + cache.children.push({ + index: Number.MAX_VALUE, + filePath: inputFile + }) + add(inputFile) + logger.log(`Found ${inputFile} from .fls ${flsPath} , caching.`) + void refreshCache(inputFile, filePath) + } else { + logger.log(`Cache not finished on ${filePath} when parsing fls.`) } + } else if (!extension.watcher.src.has(inputFile)) { + // Watch non-tex files. + add(inputFile) } - // Make sure to return an array with unique entries - return Array.from(new Set(includedBib)) } - /** - * Return a string array which holds all imported tex files - * from the given `file` including the `file` itself. - * If `file` is `undefined`, trace from the * root file, - * or return empty array if the root file is `undefined` - * - * @param file The path of a LaTeX file - */ - getIncludedTeX(file?: string, includedTeX: string[] = [], cachedOnly: boolean = true): string[] { - file = file ?? lw.manager.rootFile - if (file === undefined) { - return [] - } - if (cachedOnly && !this.has(file)) { - return [] - } - includedTeX.push(file) - if (!this.has(file)) { - return [] + for (const outputFile of ioFiles.output) { + if (path.extname(outputFile) === '.aux' && fs.existsSync(outputFile)) { + logger.log(`Found .aux ${filePath} from .fls ${flsPath} , parsing.`) + parseAuxFile(outputFile, path.dirname(outputFile).replace(outDir, rootDir)) + logger.log(`Parsed .aux ${filePath} .`) } - const cache = this.get(file) - if (cache) { - for (const child of cache.children) { - if (includedTeX.includes(child.filePath)) { - // Already included - continue - } - this.getIncludedTeX(child.filePath, includedTeX, cachedOnly) - } - } - return includedTeX } + logger.log(`Parsed .fls ${flsPath} .`) +} - /** - * Return the list of files (recursively) included in `file` - * - * @param filePath The file in which children are recursively computed - * @param basePath The file currently considered as the rootFile - * @param children The list of already computed children - */ - async getTeXChildren(filePath: string, basePath: string, children: string[]) { - if (!this.has(filePath)) { - logger.log(`Cache not finished on ${filePath} when getting its children.`) - await this.refreshCache(filePath, basePath) +function parseAuxFile(filePath: string, srcDir: string) { + const content = fs.readFileSync(filePath).toString() + const regex = /^\\bibdata{(.*)}$/gm + while (true) { + const result = regex.exec(content) + if (!result) { + return } - - this.get(filePath)?.children.forEach(async child => { - if (children.includes(child.filePath)) { - // Already included - return + const bibs = (result[1] ? result[1] : result[2]).split(',').map((bib) => { return bib.trim() }) + for (const bib of bibs) { + const bibPaths = PathUtils.resolveBibPath(bib, srcDir) + for (const bibPath of bibPaths) { + if (lw.manager.rootFile && !caches[lw.manager.rootFile]?.bibfiles.has(bibPath)) { + caches[lw.manager.rootFile]?.bibfiles.add(bibPath) + logger.log(`Found .bib ${bibPath} from .aux ${filePath} .`) + } + if (!extension.watcher.bib.has(bibPath)) { + extension.watcher.bib.add(bibPath) + } } - children.push(child.filePath) - await this.getTeXChildren(child.filePath, basePath, children) - }) - return children - } - - getFlsChildren(texFile: string) { - const flsFile = PathUtils.getFlsFilePath(texFile) - if (flsFile === undefined) { - return [] } - const rootDir = path.dirname(texFile) - const ioFiles = CacherUtils.parseFlsContent(fs.readFileSync(flsFile).toString(), rootDir) - return ioFiles.input } } diff --git a/src/core/event-bus.ts b/src/core/event-bus.ts index 909840e4f..fcd690d4d 100644 --- a/src/core/event-bus.ts +++ b/src/core/event-bus.ts @@ -21,7 +21,7 @@ export const StructureUpdated = 'STRUCTURE_UPDATED' export const AutoCleaned = 'AUTO_CLEANED' export type EventArgs = { - [AutoBuildInitiated]: {type: 'onChange' | 'onSave', file: string}, + [AutoBuildInitiated]: {type: 'onFileChange' | 'onSave', file: string}, [RootFileChanged]: string, [FileParsed]: string, [ViewerStatusChanged]: PdfViewerState, diff --git a/src/core/project.ts b/src/core/project.ts new file mode 100644 index 000000000..441cb7e18 --- /dev/null +++ b/src/core/project.ts @@ -0,0 +1,112 @@ +import * as path from 'path' +import * as fs from 'fs' +import * as lw from '../lw' +import * as CacherUtils from './cacherlib/cacherutils' +import * as PathUtils from './cacherlib/pathutils' +import { getLogger } from '../utils/logging/logger' + +import { extension } from '../extension' + +const logger = getLogger('Project') + +export { + getIncludedBib, + getIncludedTeX, + getTeXChildren, + getFlsChildren +} + +/** + * Return a string array which holds all imported bib files + * from the given tex `file`. If `file` is `undefined`, traces from the + * root file, or return empty array if the root file is `undefined` + * + * @param filePath The path of a LaTeX file + */ +function getIncludedBib(filePath?: string, includedBib: string[] = []): string[] { + filePath = filePath ?? lw.manager.rootFile + if (filePath === undefined) { + return [] + } + const fileCache = extension.cache.get(filePath) + if (fileCache === undefined) { + return [] + } + const checkedTeX = [ filePath ] + includedBib.push(...fileCache.bibfiles) + for (const child of fileCache.children) { + if (checkedTeX.includes(child.filePath)) { + // Already parsed + continue + } + getIncludedBib(child.filePath, includedBib) + } + // Make sure to return an array with unique entries + return Array.from(new Set(includedBib)) +} + +/** + * Return a string array which holds all imported tex files + * from the given `file` including the `file` itself. + * If `file` is `undefined`, trace from the * root file, + * or return empty array if the root file is `undefined` + * + * @param filePath The path of a LaTeX file + */ +function getIncludedTeX(filePath?: string, includedTeX: string[] = [], cachedOnly: boolean = true): string[] { + filePath = filePath ?? lw.manager.rootFile + if (filePath === undefined) { + return [] + } + const fileCache = extension.cache.get(filePath) + if (cachedOnly && fileCache === undefined) { + return [] + } + includedTeX.push(filePath) + if (fileCache === undefined) { + return [] + } + for (const child of fileCache.children) { + if (includedTeX.includes(child.filePath)) { + // Already included + continue + } + getIncludedTeX(child.filePath, includedTeX, cachedOnly) + } + return includedTeX +} + +/** + * Return the list of files (recursively) included in `file` + * + * @param filePath The file in which children are recursively computed + * @param basePath The file currently considered as the rootFile + * @param children The list of already computed children + */ +function getTeXChildren(filePath: string, basePath: string, children: string[]) { + const fileCache = extension.cache.get(filePath) + if (fileCache === undefined) { + logger.log(`Cache not finished on ${filePath} when getting its children.`) + return [] + } + + fileCache.children.forEach(async child => { + if (children.includes(child.filePath)) { + // Already included + return + } + children.push(child.filePath) + getTeXChildren(child.filePath, basePath, children) + }) + return children +} + +function getFlsChildren(texFile: string) { + const flsFile = PathUtils.getFlsFilePath(texFile) + if (flsFile === undefined) { + return [] + } + const rootDir = path.dirname(texFile) + const ioFiles = CacherUtils.parseFlsContent(fs.readFileSync(flsFile).toString(), rootDir) + return ioFiles.input +} diff --git a/src/core/root-file.ts b/src/core/root-file.ts index 1472af593..9d2d74401 100644 --- a/src/core/root-file.ts +++ b/src/core/root-file.ts @@ -8,6 +8,8 @@ import * as lw from '../lw' import * as eventbus from './event-bus' import { getLogger } from '../utils/logging/logger' +import { extension } from '../extension' + const logger = getLogger('Manager') type RootFileType = { @@ -36,15 +38,10 @@ export class Manager { private _rootFileLanguageId: string | undefined private _rootFile: RootFileType | undefined readonly tmpDir: string + compiledRootFile?: string constructor() { this.registerSetEnvVar() - lw.cacher.src.onDelete(filePath => { - if (filePath === this.rootFile) { - this.rootFile = undefined - void this.findRoot() - } - }) // Create temp folder try { @@ -62,6 +59,15 @@ export class Manager { } } + initialize() { + extension.watcher.src.onDelete(filePath => { + if (filePath === this.rootFile) { + this.rootFile = undefined + void this.findRoot() + } + }) + } + /** * Returns the output directory developed according to the input tex path * and 'latex.outDir' config. If `texPath` is `undefined`, the default root @@ -304,12 +310,12 @@ export class Manager { // We also clean the completions from the old project lw.completer.input.reset() lw.dupLabelDetector.reset() - lw.cacher.src.reset() - lw.cacher.add(rootFile) - void lw.cacher.refreshCache(rootFile).then(async () => { + extension.cache.reset() + extension.cache.add(rootFile) + void extension.cache.refreshCache(rootFile).then(async () => { // We need to parse the fls to discover file dependencies when defined by TeX macro // It happens a lot with subfiles, https://tex.stackexchange.com/questions/289450/path-of-figures-in-different-directories-with-subfile-latex - await lw.cacher.loadFlsFile(rootFile) + await extension.cache.loadFlsFile(rootFile) }) } else { logger.log(`Keep using the same root file: ${this.rootFile}`) @@ -374,7 +380,7 @@ export class Manager { logger.log(`The active document cannot be used as the root file: ${vscode.window.activeTextEditor.document.uri.toString(true)}`) return } - if (lw.cacher.getIncludedTeX().includes(vscode.window.activeTextEditor.document.fileName)) { + if (extension.cache.getIncludedTeX().includes(vscode.window.activeTextEditor.document.fileName)) { return this.rootFile } return @@ -441,7 +447,7 @@ export class Manager { logger.log(`Skip the file: ${file.toString(true)}`) continue } - const flsChildren = lw.cacher.getFlsChildren(file.fsPath) + const flsChildren = extension.cache.getFlsChildren(file.fsPath) if (vscode.window.activeTextEditor && flsChildren.includes(vscode.window.activeTextEditor.document.fileName)) { logger.log(`Found root file from '.fls': ${file.fsPath}`) return file.fsPath @@ -450,7 +456,7 @@ export class Manager { const result = content.match(this.rootIndictor) if (result) { // Can be a root - const children = await lw.cacher.getTeXChildren(file.fsPath, file.fsPath, []) + const children = await extension.cache.getTeXChildren(file.fsPath, file.fsPath, []) if (vscode.window.activeTextEditor && children.includes(vscode.window.activeTextEditor.document.fileName)) { logger.log(`Found root file from parent: ${file.fsPath}`) return file.fsPath diff --git a/src/core/cacherlib/watcher.ts b/src/core/watcher.ts similarity index 93% rename from src/core/cacherlib/watcher.ts rename to src/core/watcher.ts index 005b59598..3710fce59 100644 --- a/src/core/cacherlib/watcher.ts +++ b/src/core/watcher.ts @@ -1,10 +1,10 @@ import * as vscode from 'vscode' import * as fs from 'fs' import * as path from 'path' -import * as lw from '../../lw' -import * as eventbus from '../event-bus' -import { getLogger } from '../../utils/logging/logger' -import { isBinary } from '../root-file' +import * as lw from '../lw' +import * as eventbus from './event-bus' +import { getLogger } from '../utils/logging/logger' +import { isBinary } from './root-file' const logger = getLogger('Cacher', 'Watcher') @@ -15,7 +15,7 @@ export class Watcher { private readonly onDeleteHandlers: Set<(filePath: string) => void> = new Set() private readonly polling: {[filePath: string]: {time: number, size: number}} = {} - constructor(private readonly fileExt: string = '.*') {} + constructor(readonly fileExt: '.*' | '.bib' | '.pdf' = '.*') {} onCreate(handler: (filePath: string) => void) { this.onCreateHandlers.add(handler) @@ -130,3 +130,9 @@ export class Watcher { logger.log('Reset.') } } + +export const watcher = { + src: new Watcher(), + pdf: new Watcher('.pdf'), + bib: new Watcher('.bib') +} diff --git a/src/extension.ts b/src/extension.ts new file mode 100644 index 000000000..5493d4b82 --- /dev/null +++ b/src/extension.ts @@ -0,0 +1,9 @@ +import type { compile } from './compile' +import type { cache } from './core/cache' +import type { watcher } from './core/watcher' + +export const extension = { + cache: {} as typeof cache, + watcher: {} as typeof watcher, + compile: {} as typeof compile +} diff --git a/src/extras/texdoc.ts b/src/extras/texdoc.ts index 46714cd50..a9ca4e1b4 100644 --- a/src/extras/texdoc.ts +++ b/src/extras/texdoc.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode' import * as cs from 'cross-spawn' -import * as lw from '../lw' import { getLogger } from '../utils/logging/logger' +import { extension } from '../extension' const logger = getLogger('TeXDoc') @@ -62,9 +62,9 @@ export class TeXDoc { texdocUsepackages() { const names: Set = new Set() - for (const tex of lw.cacher.getIncludedTeX()) { - const content = lw.cacher.get(tex) - const pkgs = content && content.elements.package + for (const filePath of extension.cache.getIncludedTeX()) { + const fileCache = extension.cache.get(filePath) + const pkgs = fileCache && fileCache.elements.package if (!pkgs) { continue } diff --git a/src/language/selection.ts b/src/language/selection.ts index 3b4990e5e..fdac0f69b 100644 --- a/src/language/selection.ts +++ b/src/language/selection.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode' import type * as Ast from '@unified-latex/unified-latex-types' -import * as lw from '../lw' import { getLogger } from '../utils/logging/logger' +import { extension } from '../extension' const logger = getLogger('Selection') @@ -69,9 +69,9 @@ function nodeStackToSelectionRange(stack: Ast.Node[]): vscode.SelectionRange { export class SelectionRangeProvider implements vscode.SelectionRangeProvider { async provideSelectionRanges(document: vscode.TextDocument, positions: vscode.Position[]) { - await lw.cacher.wait(document.fileName) - const content = lw.cacher.get(document.fileName)?.content - const ast = lw.cacher.get(document.fileName)?.ast + await extension.cache.wait(document.fileName) + const content = extension.cache.get(document.fileName)?.content + const ast = extension.cache.get(document.fileName)?.ast if (!content || !ast) { logger.log(`Error loading ${content ? 'AST' : 'content'} during structuring: ${document.fileName} .`) return [] diff --git a/src/lint/duplicate-label.ts b/src/lint/duplicate-label.ts index f90683122..1a3fabf57 100644 --- a/src/lint/duplicate-label.ts +++ b/src/lint/duplicate-label.ts @@ -1,6 +1,6 @@ import * as vscode from 'vscode' import * as path from 'path' -import * as lw from '../lw' +import { extension } from '../extension' const duplicatedLabelsDiagnostics = vscode.languages.createDiagnosticCollection('Duplicate Labels') @@ -14,12 +14,12 @@ export const dupLabelDetector = { */ function computeDuplicates(): string[] { const labelsCount = new Map() - lw.cacher.getIncludedTeX().forEach(cachedFile => { - const cachedRefs = lw.cacher.get(cachedFile)?.elements.reference - if (cachedRefs === undefined) { + extension.cache.getIncludedTeX().forEach(filePath => { + const refCache = extension.cache.get(filePath)?.elements.reference + if (refCache === undefined) { return } - cachedRefs.forEach(ref => { + refCache.forEach(ref => { if (ref.range === undefined) { return } @@ -56,23 +56,23 @@ function showDiagnostics(duplicates: string[]) { } const diagsCollection = Object.create(null) as { [key: string]: vscode.Diagnostic[] } - lw.cacher.getIncludedTeX().forEach(cachedFile => { - const cachedRefs = lw.cacher.get(cachedFile)?.elements.reference - if (cachedRefs === undefined) { + extension.cache.getIncludedTeX().forEach(filePath => { + const refCache = extension.cache.get(filePath)?.elements.reference + if (refCache === undefined) { return } - cachedRefs.forEach(ref => { + refCache.forEach(ref => { if (ref.range === undefined) { return } if (duplicates.includes(ref.label)) { - if (! (cachedFile in diagsCollection)) { - diagsCollection[cachedFile] = [] + if (! (filePath in diagsCollection)) { + diagsCollection[filePath] = [] } const range = ref.range instanceof vscode.Range ? ref.range : ref.range.inserting const diag = new vscode.Diagnostic(range, `Duplicate label ${ref.label}`, vscode.DiagnosticSeverity.Warning) diag.source = 'DuplicateLabels' - diagsCollection[cachedFile].push(diag) + diagsCollection[filePath].push(diag) } }) }) diff --git a/src/locate/environment.ts b/src/locate/environment.ts index 6e36d2193..23c19e81a 100644 --- a/src/locate/environment.ts +++ b/src/locate/environment.ts @@ -176,6 +176,16 @@ export class EnvPair { return currentCommandPair } } + // #4063 + if (node.content === 'item' && node.args) { + for (let argIndex = 0; argIndex < node.args.length; argIndex++) { + for (let index = 0; index < node.args[argIndex].content.length; index++) { + const subnode = node.args[argIndex].content[index] + const subnext = index === node.args[argIndex].content.length - 1 ? undefined : node.args[argIndex].content[index + 1] + parentCommandPair = this.buildCommandPairTreeFromNode(doc, subnode, subnext, parentCommandPair, commandPairs) + } + } + } } return parentCommandPair } diff --git a/src/locate/synctex.ts b/src/locate/synctex.ts index 17202b751..894942d58 100644 --- a/src/locate/synctex.ts +++ b/src/locate/synctex.ts @@ -8,6 +8,7 @@ import { replaceArgumentPlaceholders } from '../utils/utils' import { isSameRealPath } from '../utils/pathnormalize' import type { ClientRequest } from '../../types/latex-workshop-protocol-types' import { getLogger } from '../utils/logging/logger' +import { extension } from '../extension' const logger = getLogger('Locator') @@ -307,14 +308,14 @@ export class Locator { // kpathsea/SyncTeX follow symlinks. // see http://tex.stackexchange.com/questions/25578/why-is-synctex-in-tl-2011-so-fussy-about-filenames. // We compare the return of symlink with the files list in the texFileTree and try to pickup the correct one. - for (const ed of lw.cacher.allPaths) { + for (const filePath of extension.cache.paths()) { try { - if (isSameRealPath(record.input, ed)) { - record.input = ed + if (isSameRealPath(record.input, filePath)) { + record.input = filePath break } } catch(e) { - logger.logError(`Backward SyncTeX failed on isSameRealPath() with ${record.input} and ${ed} .`, e) + logger.logError(`Backward SyncTeX failed on isSameRealPath() with ${record.input} and ${filePath} .`, e) } } diff --git a/src/lw.ts b/src/lw.ts index 1ed90400e..4b6b0e514 100644 --- a/src/lw.ts +++ b/src/lw.ts @@ -1,7 +1,5 @@ import vscode from 'vscode' import path from 'path' -import { Builder } from './compile/build' -import { Cacher } from './core/cache' import { Cleaner } from './extras/cleaner' import { LaTeXCommanderTreeView } from './extras/activity-bar' import { Configuration } from './utils/logging/log-config' @@ -28,6 +26,7 @@ import { StructureView } from './outline/project' import { getLogger } from './utils/logging/logger' import { TeXDoc } from './extras/texdoc' import { MathJaxPool } from './preview/math/mathjaxpool' +import { extension } from './extension' let disposables: { dispose(): any }[] = [] let context: vscode.ExtensionContext @@ -41,16 +40,18 @@ export function registerDisposable(...items: vscode.Disposable[]) { } } -export * as commander from './core/commands' +export * as commander from './commands' + +export function setViewer(v: Viewer) { + viewer = v +} export const extensionRoot = path.resolve(`${__dirname}/../../`) export const eventBus = new EventBus() export const configuration = new Configuration() export const lwfs = new LwFileSystem() -export const cacher = new Cacher() export const manager = new Manager() -export const builder = new Builder() -export const viewer = new Viewer() +export let viewer: Viewer export const server = new Server() export const locator = new Locator() export const completer = new Completer() @@ -81,7 +82,7 @@ export function init(extensionContext: vscode.ExtensionContext) { logger.log('LaTeX Workshop initialized.') return { dispose: async () => { - cacher.reset() + extension.cache.reset() server.dispose() await parser.dispose() MathJaxPool.dispose() diff --git a/src/main.ts b/src/main.ts index 32dcb27fa..afe47f06b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,5 @@ import * as vscode from 'vscode' -import * as lw from './lw' -import { pdfViewerHookProvider, pdfViewerPanelSerializer } from './preview/viewer' +import { Viewer, pdfViewerHookProvider, pdfViewerPanelSerializer } from './preview/viewer' import { MathPreviewPanelSerializer } from './extras/math-preview-panel' import { BibtexCompleter } from './completion/bibtex' import { HoverProvider } from './preview/hover' @@ -14,11 +13,36 @@ import { bibtexFormat, bibtexFormatterProvider } from './lint/bibtex-formatter' import { getLogger } from './utils/logging/logger' import { DocumentChanged } from './core/event-bus' +import { extension } from './extension' +import { watcher } from './core/watcher' +import { cache } from './core/cache' +import { compile } from './compile' +extension.watcher = watcher +extension.cache = cache +extension.compile = compile + +import * as lw from './lw' + const logger = getLogger('Extension') +function initialize() { + extension.watcher = watcher + extension.cache = cache + extension.compile = compile + + cache.initialize(watcher.src) + lw.manager.initialize() + lw.completer.citation.initialize() + + lw.setViewer(new Viewer()) + lw.viewer.initialize() +} + export function activate(extensionContext: vscode.ExtensionContext) { void vscode.commands.executeCommand('setContext', 'latex-workshop:enabled', true) + initialize() + const lwDisposable = lw.init(extensionContext) lw.registerDisposable(lwDisposable) @@ -29,11 +53,11 @@ export function activate(extensionContext: vscode.ExtensionContext) { return } if (lw.manager.hasTexId(e.languageId) || - lw.cacher.getIncludedTeX(lw.manager.rootFile, [], false).includes(e.fileName) || - lw.cacher.getIncludedBib().includes(e.fileName)) { + extension.cache.getIncludedTeX(lw.manager.rootFile, [], false).includes(e.fileName) || + extension.cache.getIncludedBib().includes(e.fileName)) { logger.log(`onDidSaveTextDocument triggered: ${e.uri.toString(true)}`) lw.linter.lintRootFileIfEnabled() - void lw.builder.buildOnSaveIfEnabled(e.fileName) + void extension.compile.autoBuild(e.fileName, 'onSave') lw.counter.countOnSaveIfEnabled(e.fileName) } })) @@ -65,7 +89,6 @@ export function activate(extensionContext: vscode.ExtensionContext) { } })) - let updateCompleter: NodeJS.Timeout lw.registerDisposable(vscode.workspace.onDidChangeTextDocument((e: vscode.TextDocumentChangeEvent) => { if (lw.lwfs.isVirtualUri(e.document.uri)){ return @@ -77,21 +100,7 @@ export function activate(extensionContext: vscode.ExtensionContext) { } lw.eventBus.fire(DocumentChanged) lw.linter.lintActiveFileIfEnabledAfterInterval(e.document) - if (!lw.cacher.has(e.document.fileName)) { - return - } - const configuration = vscode.workspace.getConfiguration('latex-workshop') - if (configuration.get('intellisense.update.aggressive.enabled')) { - if (updateCompleter) { - clearTimeout(updateCompleter) - } - updateCompleter = setTimeout(() => { - const file = e.document.uri.fsPath - void lw.cacher.refreshCache(file, lw.manager.rootFile).then(async () => { - await lw.cacher.loadFlsFile(lw.manager.rootFile || file) - }) - }, configuration.get('intellisense.update.delay', 1000)) - } + cache.refreshCacheAggressive(e.document.fileName) })) lw.registerDisposable(vscode.window.onDidChangeTextEditorSelection((e: vscode.TextEditorSelectionChangeEvent) => { @@ -124,7 +133,7 @@ function registerLatexWorkshopCommands() { vscode.commands.registerCommand('latex-workshop.tab', () => lw.commander.view('tab')), vscode.commands.registerCommand('latex-workshop.viewInBrowser', () => lw.commander.view('browser')), vscode.commands.registerCommand('latex-workshop.viewExternal', () => lw.commander.view('external')), - vscode.commands.registerCommand('latex-workshop.kill', () => lw.commander.kill()), + vscode.commands.registerCommand('latex-workshop.terminate', () => lw.commander.terminate()), vscode.commands.registerCommand('latex-workshop.synctex', () => lw.commander.synctex()), vscode.commands.registerCommand('latex-workshop.texdoc', (packageName: string | undefined) => lw.commander.texdoc(packageName)), vscode.commands.registerCommand('latex-workshop.texdocUsepackages', () => lw.commander.texdocUsepackages()), diff --git a/src/outline/project.ts b/src/outline/project.ts index 274f1b99e..63f797539 100644 --- a/src/outline/project.ts +++ b/src/outline/project.ts @@ -7,6 +7,7 @@ import { construct as constructDocTeX } from './structurelib/doctex' import { getLogger } from '../utils/logging/logger' import { parser } from '../parse/parser' +import { extension } from '../extension' const logger = getLogger('Structure') @@ -68,8 +69,8 @@ export class StructureView implements vscode.TreeDataProvider { if (ev.affectsConfiguration('latex-workshop.view.outline.sections') || ev.affectsConfiguration('latex-workshop.view.outline.commands')) { await parser.reset() - lw.cacher.allPaths.forEach(async filePath => { - const ast = lw.cacher.get(filePath)?.ast + extension.cache.paths().forEach(async filePath => { + const ast = extension.cache.get(filePath)?.ast if (ast) { await parser.parseArgs(ast) } diff --git a/src/outline/structurelib/latex.ts b/src/outline/structurelib/latex.ts index 0c7754381..a27910faf 100644 --- a/src/outline/structurelib/latex.ts +++ b/src/outline/structurelib/latex.ts @@ -8,6 +8,7 @@ import { InputFileRegExp } from '../../utils/inputfilepath' import { getLogger } from '../../utils/logging/logger' import { argContentToStr } from '../../utils/parser' +import { extension } from '../../extension' const logger = getLogger('Structure', 'LaTeX') @@ -56,9 +57,9 @@ async function constructFile(filePath: string, config: StructureConfig, structs: if (structs[filePath] !== undefined) { return } - await lw.cacher.wait(filePath) - const content = lw.cacher.get(filePath)?.content - const ast = lw.cacher.get(filePath)?.ast + await extension.cache.wait(filePath) + const content = extension.cache.get(filePath)?.content + const ast = extension.cache.get(filePath)?.ast if (!content || !ast) { logger.log(`Error loading ${content ? 'AST' : 'content'} during structuring: ${filePath} .`) return diff --git a/src/parse/parserlib/biberlog.ts b/src/parse/parserlib/biberlog.ts index 56a3947e4..df13d5a08 100644 --- a/src/parse/parserlib/biberlog.ts +++ b/src/parse/parserlib/biberlog.ts @@ -3,6 +3,7 @@ import * as lw from '../../lw' import { type IParser, type LogEntry, showCompilerDiagnostics } from './parserutils' import { getLogger } from '../../utils/logging/logger' +import { extension } from '../../extension' const logger = getLogger('Parser', 'BiberLog') @@ -97,10 +98,7 @@ function pushLog(type: string, file: string, message: string, line: number, excl } function resolveBibFile(filename: string, rootFile: string): string { - if (!lw.cacher.get(rootFile)) { - return filename - } - const bibFiles = lw.cacher.getIncludedBib(rootFile) + const bibFiles = extension.cache.getIncludedBib(rootFile) for (const bib of bibFiles) { if (bib.endsWith(filename)) { return bib diff --git a/src/parse/parserlib/bibtexlog.ts b/src/parse/parserlib/bibtexlog.ts index ed872bad9..fa12e7db8 100644 --- a/src/parse/parserlib/bibtexlog.ts +++ b/src/parse/parserlib/bibtexlog.ts @@ -3,6 +3,7 @@ import * as lw from '../../lw' import { type IParser, type LogEntry, showCompilerDiagnostics } from './parserutils' import { getLogger } from '../../utils/logging/logger' +import { extension } from '../../extension' const logger = getLogger('Parser', 'BibTeXLog') @@ -87,13 +88,9 @@ function pushLog(type: string, file: string, message: string, line: number, excl function resolveAuxFile(filename: string, rootFile: string): string { filename = filename.replace(/\.aux$/, '.tex') - if (!lw.cacher.get(rootFile)) { - return filename - } - const texFiles = lw.cacher.getIncludedTeX(rootFile) - for (const tex of texFiles) { - if (tex.endsWith(filename)) { - return tex + for (const filePath of extension.cache.getIncludedTeX(rootFile)) { + if (filePath.endsWith(filename)) { + return filePath } } logger.log(`Cannot resolve file ${filename} .`) @@ -101,10 +98,7 @@ function resolveAuxFile(filename: string, rootFile: string): string { } function resolveBibFile(filename: string, rootFile: string): string { - if (!lw.cacher.get(rootFile)) { - return filename - } - const bibFiles = lw.cacher.getIncludedBib(rootFile) + const bibFiles = extension.cache.getIncludedBib(rootFile) for (const bib of bibFiles) { if (bib.endsWith(filename)) { return bib diff --git a/src/parse/parserlib/parserutils.ts b/src/parse/parserlib/parserutils.ts index bd6be4cf4..de1a891d3 100644 --- a/src/parse/parserlib/parserutils.ts +++ b/src/parse/parserlib/parserutils.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode' import * as fs from 'fs' -import * as lw from '../../lw' import { convertFilenameEncoding } from '../../utils/convertfilename' +import { extension } from '../../extension' export interface IParser { showLog(): void, @@ -14,8 +14,8 @@ function getErrorPosition(item: LogEntry): {start: number, end: number} | undefi if (!item.errorPosText) { return } - const content = lw.cacher.get(item.file)?.content - if (!content) { + const content = extension.cache.get(item.file)?.content + if (content === undefined) { return } // Try to find the errorPosText in the respective line of the document diff --git a/src/preview/hover.ts b/src/preview/hover.ts index 6abdeb33d..d580f49ce 100644 --- a/src/preview/hover.ts +++ b/src/preview/hover.ts @@ -3,6 +3,7 @@ import * as lw from '../lw' import { tokenizer, onAPackage } from '../utils/tokenizer' import { findProjectNewCommand } from '../preview/math/mathpreviewlib/newcommandfinder' import { CmdEnvSuggestion } from '../completion/completer/completerutils' +import { extension } from '../extension' export class HoverProvider implements vscode.HoverProvider { public async provideHover(document: vscode.TextDocument, position: vscode.Position, ctoken: vscode.CancellationToken): Promise { @@ -92,8 +93,8 @@ export class HoverProvider implements vscode.HoverProvider { packageCmds.forEach(checkCmd) - lw.cacher.getIncludedTeX().forEach(cachedFile => { - lw.cacher.get(cachedFile)?.elements.command?.forEach(checkCmd) + extension.cache.getIncludedTeX().forEach(filePath => { + extension.cache.get(filePath)?.elements.command?.forEach(checkCmd) }) let pkgLink = '' diff --git a/src/preview/math/mathpreviewlib/newcommandfinder.ts b/src/preview/math/mathpreviewlib/newcommandfinder.ts index 02214859b..cedb5a342 100644 --- a/src/preview/math/mathpreviewlib/newcommandfinder.ts +++ b/src/preview/math/mathpreviewlib/newcommandfinder.ts @@ -3,6 +3,7 @@ import * as path from 'path' import * as lw from '../../../lw' import { stripCommentsAndVerbatim } from '../../../utils/utils' import { getLogger } from '../../../utils/logging/logger' +import { extension } from '../../../extension' const logger = getLogger('Preview', 'Math') @@ -18,12 +19,12 @@ export async function findProjectNewCommand(ctoken?: vscode.CancellationToken): return commandsInConfigFile } let commands: string[] = [] - for (const tex of lw.cacher.getIncludedTeX()) { + for (const filePath of extension.cache.getIncludedTeX()) { if (ctoken?.isCancellationRequested) { return '' } - await lw.cacher.wait(tex) - const content = lw.cacher.get(tex)?.content + await extension.cache.wait(filePath) + const content = extension.cache.get(filePath)?.content if (content === undefined) { continue } diff --git a/src/preview/viewer.ts b/src/preview/viewer.ts index 0e2568450..ec1701edc 100644 --- a/src/preview/viewer.ts +++ b/src/preview/viewer.ts @@ -13,6 +13,7 @@ import { viewerManager } from './viewerlib/pdfviewermanager' import { ViewerPageLoaded } from '../core/event-bus' import { getLogger } from '../utils/logging/logger' import { moveActiveEditor } from '../utils/webview' +import { extension } from '../extension' const logger = getLogger('Viewer') @@ -23,11 +24,6 @@ export { pdfViewerPanelSerializer } from './viewerlib/pdfviewerpanel' export class Viewer { constructor() { - lw.cacher.pdf.onChange(pdfPath => { - if (lw.builder.isOutputPDF(pdfPath)) { - this.refreshExistingViewer(pdfPath) - } - }) lw.registerDisposable(vscode.workspace.onDidChangeConfiguration((e: vscode.ConfigurationChangeEvent) => { if (e.affectsConfiguration('latex-workshop.view.pdf.invertMode.enabled') || e.affectsConfiguration('latex-workshop.view.pdf.invert') || @@ -50,6 +46,14 @@ export class Viewer { })) } + initialize() { + extension.watcher.pdf.onChange(pdfPath => { + if (path.relative(pdfPath, lw.manager.compiledRootFile ? lw.manager.tex2pdf(lw.manager.compiledRootFile) : '') !== '') { + this.refreshExistingViewer(pdfPath) + } + }) + } + reloadExistingViewer(): void { viewerManager.clientMap.forEach(clientSet => { clientSet.forEach(client => { @@ -128,7 +132,7 @@ export class Viewer { } const pdfFileUri = vscode.Uri.file(pdfFile) viewerManager.createClientSet(pdfFileUri) - lw.cacher.pdf.add(pdfFileUri.fsPath) + extension.watcher.pdf.add(pdfFileUri.fsPath) try { logger.log(`Serving PDF file at ${url}`) await vscode.env.openExternal(vscode.Uri.parse(url, true)) diff --git a/src/preview/viewerlib/pdfviewermanager.ts b/src/preview/viewerlib/pdfviewermanager.ts index bf39ecf06..08f35022c 100644 --- a/src/preview/viewerlib/pdfviewermanager.ts +++ b/src/preview/viewerlib/pdfviewermanager.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode' -import * as lw from '../../lw' import type { Client } from './client' import type { PdfViewerPanel } from './pdfviewerpanel' +import { extension } from '../../extension' class PdfViewerManager { private readonly webviewPanelMap = new Map>() @@ -50,7 +50,7 @@ class PdfViewerManager { initiatePdfViewerPanel(pdfPanel: PdfViewerPanel): PdfViewerPanel | undefined { const pdfFileUri = pdfPanel.pdfFileUri - lw.cacher.pdf.add(pdfFileUri.fsPath) + extension.watcher.pdf.add(pdfFileUri.fsPath) this.createClientSet(pdfFileUri) const panelSet = this.getPanelSet(pdfFileUri) if (!panelSet) { diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 000000000..b40161567 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,37 @@ +export type FileCache = { + /** The raw file path of this Cache. */ + filePath: string, + /** Cached content of file. Dirty if opened in vscode, disk otherwise */ + content: string, + /** Cached trimmed content of `content`. */ + contentTrimmed: string, + /** Completion items */ + elements: { + /** \ref{} items */ + reference?: import('./completion/latex').ICompletionItem[], + /** \gls items */ + glossary?: import('./completion/completer/glossary').GlossarySuggestion[], + /** \begin{} items */ + environment?: import('./completion/completer/completerutils').CmdEnvSuggestion[], + /** \cite{} items from \bibitem definition */ + bibitem?: import('./completion/completer/citation').CiteSuggestion[], + /** command items */ + command?: import('./completion/completer/completerutils').CmdEnvSuggestion[], + /** \usepackage{}, a dictionary whose key is package name and value is the options */ + package?: {[packageName: string]: string[]} + }, + /** The sub-files of the LaTeX file. They should be tex or plain files */ + children: { + /** The index of character sub-content is inserted */ + index: number, + /** The path of the sub-file */ + filePath: string + }[], + /** The array of the paths of `.bib` files referenced from the LaTeX file */ + bibfiles: Set, + /** A dictionary of external documents provided by `\externaldocument` of + * `xr` package. The value is its prefix `\externaldocument[prefix]{*}` */ + external: {[filePath: string]: string}, + /** The AST of this file, generated by unified-latex */ + ast?: import('@unified-latex/unified-latex-types').Root +} \ No newline at end of file diff --git a/src/utils/quick-pick.ts b/src/utils/quick-pick.ts new file mode 100644 index 000000000..609b1282a --- /dev/null +++ b/src/utils/quick-pick.ts @@ -0,0 +1,36 @@ +import vscode from 'vscode' + +export async function rootFile(rootFile: string, localRootFile: string, verb: string): Promise { + const configuration = vscode.workspace.getConfiguration('latex-workshop', vscode.Uri.file(rootFile)) + const doNotPrompt = configuration.get('latex.rootFile.doNotPrompt') as boolean + if (doNotPrompt) { + if (configuration.get('latex.rootFile.useSubFile')) { + return localRootFile + } else { + return rootFile + } + } + const pickedRootFile = await vscode.window.showQuickPick([{ + label: 'Default root file', + description: `Path: ${rootFile}` + }, { + label: 'Subfiles package root file', + description: `Path: ${localRootFile}` + }], { + placeHolder: `Subfiles package detected. Which file to ${verb}?`, + matchOnDescription: true + }).then( selected => { + if (!selected) { + return + } + switch (selected.label) { + case 'Default root file': + return rootFile + case 'Subfiles package root file': + return localRootFile + default: + return + } + }) + return pickedRootFile +} \ No newline at end of file diff --git a/test/suites/02_autobuild.test.ts b/test/suites/02_autobuild.test.ts index 921ccd84b..78250af24 100644 --- a/test/suites/02_autobuild.test.ts +++ b/test/suites/02_autobuild.test.ts @@ -36,7 +36,7 @@ suite('Auto-build test suite', () => { {src: 'base.tex', dst: 'main.tex'} ]) const { type } = await test.auto(fixture, 'main.tex') - assert.strictEqual(type, 'onChange') + assert.strictEqual(type, 'onFileChange') }) test.run('auto build with subfiles and onFileChange', async (fixture: string) => { @@ -47,7 +47,7 @@ suite('Auto-build test suite', () => { {src: 'subfile_sub.tex', dst: 'sub/s.tex'} ], {local: 1}) const { type } = await test.auto(fixture, 'sub/s.tex') - assert.strictEqual(type, 'onChange') + assert.strictEqual(type, 'onFileChange') }) test.run('auto build with import and onFileChange', async (fixture: string) => { @@ -57,7 +57,7 @@ suite('Auto-build test suite', () => { {src: 'plain.tex', dst: 'sub/subsub/sss/sss.tex'} ], {local: 1}) const { type } = await test.auto(fixture, 'sub/subsub/sss/sss.tex') - assert.strictEqual(type, 'onChange') + assert.strictEqual(type, 'onFileChange') }) test.run('auto build with input and onFileChange', async (fixture: string) => { @@ -66,7 +66,7 @@ suite('Auto-build test suite', () => { {src: 'plain.tex', dst: 'sub/s.tex'} ]) const { type } = await test.auto(fixture, 'sub/s.tex') - assert.strictEqual(type, 'onChange') + assert.strictEqual(type, 'onFileChange') }) test.run('auto build when editing bib', async (fixture: string) => { @@ -75,7 +75,7 @@ suite('Auto-build test suite', () => { {src: 'base.bib', dst: 'bib.bib'} ]) const { type } = await test.auto(fixture, 'bib.bib') - assert.strictEqual(type, 'onChange') + assert.strictEqual(type, 'onFileChange') }) test.run('auto build with input whose path uses a macro', async (fixture: string) => { @@ -84,7 +84,7 @@ suite('Auto-build test suite', () => { {src: 'plain.tex', dst: 'sub/s.tex'} ]) const { type } = await test.auto(fixture, 'sub/s.tex') - assert.strictEqual(type, 'onChange') + assert.strictEqual(type, 'onFileChange') }) test.run('auto build with watch.files.ignore', async (fixture: string) => { @@ -94,7 +94,7 @@ suite('Auto-build test suite', () => { {src: 'plain.tex', dst: 'sub/s.tex'} ]) const { type } = await test.auto(fixture, 'sub/s.tex', true) - assert.strictEqual(type, 'onChange') + assert.strictEqual(type, 'onFileChange') }) test.run('auto build with subfiles and onSave', async (fixture: string) => { @@ -115,6 +115,6 @@ suite('Auto-build test suite', () => { {src: 'build/markdown_sub.md', dst: 'sub.md'} ]) const { type } = await test.auto(fixture, 'sub.md') - assert.strictEqual(type, 'onChange') + assert.strictEqual(type, 'onFileChange') }) }) diff --git a/test/suites/03_findroot.test.ts b/test/suites/03_findroot.test.ts index a7fd85574..e3c0529eb 100644 --- a/test/suites/03_findroot.test.ts +++ b/test/suites/03_findroot.test.ts @@ -1,8 +1,8 @@ import * as vscode from 'vscode' import * as path from 'path' import * as assert from 'assert' -import * as lw from '../../src/lw' import * as test from './utils' +import { extension } from '../../src/extension' suite('Find root file test suite', () => { test.suite.name = path.basename(__filename).replace('.test.js', '') @@ -110,7 +110,7 @@ suite('Find root file test suite', () => { ], {root: -1, skipCache: true}) const roots = await test.find(fixture, 'alt.tex') assert.strictEqual(roots.root, path.join(fixture, 'main.tex')) - const includedTeX = lw.cacher.getIncludedTeX() + const includedTeX = extension.cache.getIncludedTeX() assert.ok(includedTeX) assert.ok(includedTeX.includes(path.resolve(fixture, 'main.tex'))) assert.ok(includedTeX.includes(path.resolve(fixture, 'alt.tex'))) diff --git a/test/suites/utils.ts b/test/suites/utils.ts index cce9e250a..79915e5e6 100644 --- a/test/suites/utils.ts +++ b/test/suites/utils.ts @@ -8,6 +8,7 @@ import * as lw from '../../src/lw' import { AutoBuildInitiated, DocumentChanged, EventArgs, ViewerPageLoaded, ViewerStatusChanged } from '../../src/core/event-bus' import type { EventName } from '../../src/core/event-bus' import { getCachedLog, getLogger, resetCachedLog } from '../../src/utils/logging/logger' +import { extension } from '../../src/extension' let testIndex = 0 const logger = getLogger('Test') @@ -75,12 +76,12 @@ export function sleep(ms: number) { export async function reset() { await vscode.commands.executeCommand('workbench.action.closeAllEditors') - await Promise.all(lw.cacher.allPromises) + await Promise.all(Object.values(extension.cache.promises).filter(promise => promise)) lw.manager.rootFile = undefined lw.manager.localRootFile = undefined lw.completer.input.reset() lw.dupLabelDetector.reset() - lw.cacher.reset() + extension.cache.reset() glob.sync('**/{**.tex,**.pdf,**.bib}', { cwd: getFixture() }).forEach(file => { try {fs.unlinkSync(path.resolve(getFixture(), file))} catch {} }) } @@ -137,8 +138,8 @@ export async function load(fixture: string, files: {src: string, dst: string, ws } if (!config.skipCache) { logger.log('Cache tex and bib.') - files.filter(file => file.dst.endsWith('.tex')).forEach(file => lw.cacher.add(path.resolve(getWsFixture(fixture, file.ws), file.dst))) - const texPromise = files.filter(file => file.dst.endsWith('.tex')).map(file => lw.cacher.refreshCache(path.resolve(getWsFixture(fixture, file.ws), file.dst), lw.manager.rootFile)) + files.filter(file => file.dst.endsWith('.tex')).forEach(file => extension.cache.add(path.resolve(getWsFixture(fixture, file.ws), file.dst))) + const texPromise = files.filter(file => file.dst.endsWith('.tex')).map(file => extension.cache.refreshCache(path.resolve(getWsFixture(fixture, file.ws), file.dst), lw.manager.rootFile)) const bibPromise = files.filter(file => file.dst.endsWith('.bib')).map(file => lw.completer.citation.parseBibFile(path.resolve(getWsFixture(fixture, file.ws), file.dst))) await Promise.all([...texPromise, ...bibPromise]) } @@ -169,7 +170,7 @@ export async function build(fixture: string, openFile: string, ws?: string, acti await (action ?? lw.commander.build)() } -export async function auto(fixture: string, editFile: string, noBuild = false, save = false, ws?: string): Promise<{type: 'onChange' | 'onSave', file: string}> { +export async function auto(fixture: string, editFile: string, noBuild = false, save = false, ws?: string): Promise<{type: 'onFileChange' | 'onSave', file: string}> { const done = wait(AutoBuildInitiated) if (save) { logger.log(`Save ${editFile}.`) @@ -183,7 +184,7 @@ export async function auto(fixture: string, editFile: string, noBuild = false, s if (noBuild) { await sleep(500) strictEqual(getCachedLog().CACHED_EXTLOG.filter(line => line.includes('[Builder]')).filter(line => line.includes(editFile)).length, 0) - return {type: 'onChange', file: ''} + return {type: 'onFileChange', file: ''} } logger.log('Wait for auto-build.') const result = await Promise.any([done, sleep(3000)]) as EventArgs[typeof AutoBuildInitiated] @@ -195,7 +196,7 @@ export async function auto(fixture: string, editFile: string, noBuild = false, s export function suggest(row: number, col: number, isAtSuggestion = false, openFile?: string): {items: vscode.CompletionItem[], labels: string[]} { ok(lw.manager.rootFile) - const lines = lw.cacher.get(openFile ?? lw.manager.rootFile)?.content?.split('\n') + const lines = extension.cache.get(openFile ?? lw.manager.rootFile)?.content?.split('\n') ok(lines) logger.log('Get suggestion.') const items = (isAtSuggestion ? lw.atSuggestionCompleter : lw.completer).provide({