logancyang_obsidian-copilot/src/commands/customCommandRegister.ts
Zero Liu 500bc347a0
chore(eslint): enable no-unsafe-member-access; fix 124 violations (#2438)
Enables @typescript-eslint/no-unsafe-member-access globally. Tests and 41
heavy source files (>5 violations each) are exempted via per-file overrides
with current violation counts annotated for follow-up PRs. Fixes 124
violations across 51 source files using narrow type assertions, instanceof
Error guards, and proper structural types instead of blanket any casts.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 22:35:38 -07:00

153 lines
5 KiB
TypeScript

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 "lodash.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));
},
});
}
}