import { getCommandId, isCustomCommandFile, loadAllCustomCommands, parseCustomCommandFile, getNextCustomCommandOrder, ensureCommandFrontmatter, hasOrderFrontmatter, } from "@/commands/customCommandUtils"; import { Editor, Plugin, TFile, Vault } from "obsidian"; import { CustomCommandChatModal } from "@/commands/CustomCommandChatModal"; import { debounce } from "@/utils/debounce"; import { CustomCommand } from "@/commands/type"; import { deleteCachedCommand, getCachedCustomCommands, isFileWritePending, updateCachedCommand, } from "@/commands/state"; import { CustomCommandManager } from "@/commands/customCommandManager"; import { logError } from "@/logger"; /** This manager is used to register custom commands as obsidian commands */ export class CustomCommandRegister { private plugin: Plugin; private vault: Vault; constructor(plugin: Plugin, vault: Vault) { this.plugin = plugin; this.vault = vault; this.initializeEventListeners(); } async initialize() { await loadAllCustomCommands(); this.registerCommands(); } /** * Register all custom commands found in the custom commands folder. * Synchronous: iterates cached commands and registers each. */ private registerCommands() { const commands = getCachedCustomCommands(); commands.forEach((command) => { this.registerCommand(command); }); } /** * Clean up resources used by the cache */ cleanup() { this.vault.off("create", this.handleFileCreation); this.vault.off("delete", this.handleFileDeletion); this.vault.off("rename", this.handleFileRename); this.vault.off("modify", this.handleFileModify); } private initializeEventListeners() { this.vault.on("create", this.handleFileCreation); this.vault.on("delete", this.handleFileDeletion); this.vault.on("rename", this.handleFileRename); this.vault.on("modify", this.handleFileModify); } private handleFileModify = debounce( async (file: TFile) => { if (!isCustomCommandFile(file) || isFileWritePending(file.path)) { return; } const customCommand = await parseCustomCommandFile(file); this.registerCommand(customCommand); updateCachedCommand(customCommand, customCommand.title); }, 1000, { // We cannot use leading: true because frontmatter is not updated // immediately when modify event is triggered. leading: false, trailing: true, } ); // Note: This function is called when obsidian starts up. private handleFileCreation = async (file: TFile) => { if (!isCustomCommandFile(file) || isFileWritePending(file.path)) { return; } try { let customCommand = await parseCustomCommandFile(file); if (!hasOrderFrontmatter(file)) { // Compute the correct order for the new command const newOrder = getNextCustomCommandOrder(); customCommand = { ...customCommand, order: newOrder }; } await ensureCommandFrontmatter(file, customCommand); updateCachedCommand(customCommand, customCommand.title); this.registerCommand(customCommand); } catch (error) { logError(`Error processing custom command creation: ${file.path}`, error); } }; private handleFileDeletion = async (file: TFile) => { if (!isCustomCommandFile(file) || isFileWritePending(file.path)) { return; } const commandId = getCommandId(file.basename); (this.plugin as unknown as { removeCommand: (id: string) => void }).removeCommand(commandId); deleteCachedCommand(file.basename); }; private handleFileRename = async (file: TFile, oldPath: string) => { if (isFileWritePending(file.path)) { return; } // Remove the old command const oldFilename = oldPath.split("/").pop()?.replace(/\.md$/, ""); if (oldFilename) { const oldCommandId = getCommandId(oldFilename); (this.plugin as unknown as { removeCommand: (id: string) => void }).removeCommand( oldCommandId ); deleteCachedCommand(oldFilename); } // Register the new command if it's still a custom command file if (isCustomCommandFile(file)) { const parsedCommand = await parseCustomCommandFile(file); this.registerCommand(parsedCommand); updateCachedCommand(parsedCommand, parsedCommand.title); await ensureCommandFrontmatter(file, parsedCommand); } }; private registerCommand(customCommand: CustomCommand) { const commandId = getCommandId(customCommand.title); (this.plugin as unknown as { removeCommand: (id: string) => void }).removeCommand(commandId); this.plugin.addCommand({ id: commandId, name: customCommand.title, editorCallback: (editor: Editor) => { new CustomCommandChatModal(this.plugin.app, { selectedText: editor.getSelection(), command: customCommand, }).open(); void CustomCommandManager.getInstance() .recordUsage(customCommand) .catch((err) => logError("recordUsage failed", err)); }, }); } }