perf: use PCancelable for cancelable tasks

This commit is contained in:
Kodai Nakamura 2025-02-03 10:28:36 +09:00
parent 8ae51e813f
commit 717580efcf
2 changed files with 76 additions and 26 deletions

View file

@ -5,29 +5,68 @@ import {
AutomaticLinkerSettings, AutomaticLinkerSettings,
DEFAULT_SETTINGS, DEFAULT_SETTINGS,
} from "./settings"; } from "./settings";
import PCancelable from "p-cancelable";
export default class AutomaticLinkerPlugin extends Plugin { export default class AutomaticLinkerPlugin extends Plugin {
settings: AutomaticLinkerSettings; settings: AutomaticLinkerSettings;
private allFileNames: string[] = []; private allFileNames: string[] = [];
// Holds the currently running modifyLinks task
private currentModifyLinks: PCancelable<void> | null = null;
constructor(app: App, pluginManifest: PluginManifest) { constructor(app: App, pluginManifest: PluginManifest) {
super(app, pluginManifest); super(app, pluginManifest);
} }
// Cancelable function to modify links in the active file
async modifyLinks() { async modifyLinks() {
const activeFile = this.app.workspace.getActiveFile(); // If a previous task is running, cancel it first.
if (!activeFile) return; if (this.currentModifyLinks) {
const fileContent = await this.app.vault.read(activeFile); this.currentModifyLinks.cancel();
}
const updatedContent = await replaceLinks({ const cancelableTask = new PCancelable<void>(
fileContent, async (resolve, reject, onCancel) => {
allFileNames: this.allFileNames, const activeFile = this.app.workspace.getActiveFile();
getFrontMatterInfo, if (!activeFile) {
specialDirs: this.settings.specialDirs, 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() { async onload() {
@ -42,41 +81,46 @@ export default class AutomaticLinkerPlugin extends Plugin {
const allFileNames = allMarkdownFiles.map((file) => const allFileNames = allMarkdownFiles.map((file) =>
file.path.replace(/\.md$/, ""), 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); allFileNames.sort((a, b) => b.length - a.length);
this.allFileNames = allFileNames; this.allFileNames = allFileNames;
}; };
// Load markdown file names when the layout is ready.
this.app.workspace.onLayoutReady(() => { this.app.workspace.onLayoutReady(() => {
loadMarkdownFiles(); loadMarkdownFiles();
console.log("Automatic Linker: Loaded all markdown files."); console.log("Automatic Linker: Loaded all markdown files.");
console.log("this.allFileNames.length", this.allFileNames.length); console.log("this.allFileNames.length", this.allFileNames.length);
}); });
this.registerEvent( this.registerEvent(
this.app.vault.on("delete", () => { this.app.vault.on("delete", () => loadMarkdownFiles()),
loadMarkdownFiles();
}),
); );
this.registerEvent( this.registerEvent(
this.app.vault.on("create", () => { this.app.vault.on("create", () => loadMarkdownFiles()),
loadMarkdownFiles();
}),
); );
this.registerEvent( this.registerEvent(
this.app.vault.on("rename", () => { this.app.vault.on("rename", () => loadMarkdownFiles()),
loadMarkdownFiles();
}),
); );
// Add a command to trigger modifyLinks manually.
this.addCommand({ this.addCommand({
id: "automatic-linker:link-current-file", id: "automatic-linker:link-current-file",
name: "Link current file", name: "Link current file",
editorCallback: async () => { 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?.[ const saveCommandDefinition = (this.app as any)?.commands?.commands?.[
"editor:save-file" "editor:save-file"
]; ];
@ -85,11 +129,17 @@ export default class AutomaticLinkerPlugin extends Plugin {
// Create a debounced version of modifyLinks with a 300ms delay. // Create a debounced version of modifyLinks with a 300ms delay.
const debouncedModifyLinks = debounce(async () => { const debouncedModifyLinks = debounce(async () => {
if (this.settings.formatOnSave) { if (this.settings.formatOnSave) {
await this.modifyLinks(); try {
await this.modifyLinks();
} catch (error) {
if (!(error instanceof PCancelable.CancelError)) {
console.error(error);
}
}
} }
}, 300); }, 300);
saveCommandDefinition.callback = async () => { saveCommandDefinition.callback = async () => {
// Call the debounced modifyLinks function. // Call the debounced modifyLinks function first.
debouncedModifyLinks(); debouncedModifyLinks();
// Then, call the original save function. // Then, call the original save function.
save(); save();
@ -114,7 +164,6 @@ export default class AutomaticLinkerPlugin extends Plugin {
function debounce<T extends (...args: any[]) => any>(func: T, wait: number): T { function debounce<T extends (...args: any[]) => any>(func: T, wait: number): T {
let timeout: ReturnType<typeof setTimeout>; let timeout: ReturnType<typeof setTimeout>;
return function (this: any, ...args: any[]) { return function (this: any, ...args: any[]) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const context = this; const context = this;
clearTimeout(timeout); clearTimeout(timeout);
timeout = setTimeout(() => func.apply(context, args), wait); timeout = setTimeout(() => func.apply(context, args), wait);

View file

@ -11,6 +11,7 @@
"importHelpers": true, "importHelpers": true,
"isolatedModules": true, "isolatedModules": true,
"strictNullChecks": true, "strictNullChecks": true,
"allowSyntheticDefaultImports": true,
"lib": [ "lib": [
"DOM", "DOM",
"ES5", "ES5",