diff --git a/src/main.ts b/src/main.ts index cc44066..0294e39 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,29 +5,68 @@ import { AutomaticLinkerSettings, DEFAULT_SETTINGS, } from "./settings"; +import PCancelable from "p-cancelable"; export default class AutomaticLinkerPlugin extends Plugin { settings: AutomaticLinkerSettings; - private allFileNames: string[] = []; + // Holds the currently running modifyLinks task + private currentModifyLinks: PCancelable | null = null; constructor(app: App, pluginManifest: PluginManifest) { super(app, pluginManifest); } + // Cancelable function to modify links in the active file async modifyLinks() { - const activeFile = this.app.workspace.getActiveFile(); - if (!activeFile) return; - const fileContent = await this.app.vault.read(activeFile); + // If a previous task is running, cancel it first. + if (this.currentModifyLinks) { + this.currentModifyLinks.cancel(); + } - const updatedContent = await replaceLinks({ - fileContent, - allFileNames: this.allFileNames, - getFrontMatterInfo, - specialDirs: this.settings.specialDirs, - }); + const cancelableTask = new PCancelable( + async (resolve, reject, onCancel) => { + const activeFile = this.app.workspace.getActiveFile(); + if (!activeFile) { + resolve(); + return; + } - await this.app.vault.modify(activeFile, updatedContent); + // Flag to track cancellation within this task. + let canceled = false; + onCancel(() => { + canceled = true; + }); + + try { + // Read the active file's content + const fileContent = await this.app.vault.read(activeFile); + // If the task was canceled after reading, abort processing. + if (canceled) { + return reject(new PCancelable.CancelError()); + } + // Replace links using the provided utility function. + const updatedContent = await replaceLinks({ + fileContent, + allFileNames: this.allFileNames, + getFrontMatterInfo, + specialDirs: this.settings.specialDirs, + }); + // Check again if the task has been canceled. + if (canceled) { + return reject(new PCancelable.CancelError()); + } + // Modify the active file with the updated content. + await this.app.vault.modify(activeFile, updatedContent); + resolve(); + } catch (error) { + reject(error); + } + }, + ); + + this.currentModifyLinks = cancelableTask; + return cancelableTask; } async onload() { @@ -42,41 +81,46 @@ export default class AutomaticLinkerPlugin extends Plugin { const allFileNames = allMarkdownFiles.map((file) => file.path.replace(/\.md$/, ""), ); - // Sort by descending length so that longer paths match first. + // Sort file names in descending order by length so longer paths match first. allFileNames.sort((a, b) => b.length - a.length); this.allFileNames = allFileNames; }; + // Load markdown file names when the layout is ready. this.app.workspace.onLayoutReady(() => { loadMarkdownFiles(); console.log("Automatic Linker: Loaded all markdown files."); console.log("this.allFileNames.length", this.allFileNames.length); }); this.registerEvent( - this.app.vault.on("delete", () => { - loadMarkdownFiles(); - }), + this.app.vault.on("delete", () => loadMarkdownFiles()), ); this.registerEvent( - this.app.vault.on("create", () => { - loadMarkdownFiles(); - }), + this.app.vault.on("create", () => loadMarkdownFiles()), ); this.registerEvent( - this.app.vault.on("rename", () => { - loadMarkdownFiles(); - }), + this.app.vault.on("rename", () => loadMarkdownFiles()), ); + // Add a command to trigger modifyLinks manually. this.addCommand({ id: "automatic-linker:link-current-file", name: "Link current file", editorCallback: async () => { - await this.modifyLinks(); + try { + await this.modifyLinks(); + } catch (error) { + // If the task was canceled, log that it was canceled. + if (error instanceof PCancelable.CancelError) { + console.log("modifyLinks was canceled"); + } else { + console.error(error); + } + } }, }); - // Override the save command callback if available. + // Optionally, override the default save command to run modifyLinks with debounce. const saveCommandDefinition = (this.app as any)?.commands?.commands?.[ "editor:save-file" ]; @@ -85,11 +129,17 @@ export default class AutomaticLinkerPlugin extends Plugin { // Create a debounced version of modifyLinks with a 300ms delay. const debouncedModifyLinks = debounce(async () => { if (this.settings.formatOnSave) { - await this.modifyLinks(); + try { + await this.modifyLinks(); + } catch (error) { + if (!(error instanceof PCancelable.CancelError)) { + console.error(error); + } + } } }, 300); saveCommandDefinition.callback = async () => { - // Call the debounced modifyLinks function. + // Call the debounced modifyLinks function first. debouncedModifyLinks(); // Then, call the original save function. save(); @@ -114,7 +164,6 @@ export default class AutomaticLinkerPlugin extends Plugin { function debounce any>(func: T, wait: number): T { let timeout: ReturnType; return function (this: any, ...args: any[]) { - // eslint-disable-next-line @typescript-eslint/no-this-alias const context = this; clearTimeout(timeout); timeout = setTimeout(() => func.apply(context, args), wait); diff --git a/tsconfig.json b/tsconfig.json index 5309f4c..0a8b3e3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,6 +11,7 @@ "importHelpers": true, "isolatedModules": true, "strictNullChecks": true, + "allowSyntheticDefaultImports": true, "lib": [ "DOM", "ES5",