mirror of
https://github.com/logancyang/obsidian-copilot.git
synced 2026-07-22 07:50:24 +00:00
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>
153 lines
5 KiB
TypeScript
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));
|
|
},
|
|
});
|
|
}
|
|
}
|