## Obsidian Plugin Development - Technical Reference ### Table of Contents - [Getting Started](#getting-started) - [Plugin Anatomy](#plugin-anatomy) - [Core API](#core-api) - [User Interface](#user-interface) - [Editor Extensions](#editor-extensions) - [Data Persistence](#data-persistence) - [Event System](#event-system) - [Best Practices](#best-practices) --- ### Getting Started #### Prerequisites - Node.js v16+ (`node --version`) - TypeScript knowledge - Obsidian app installed #### Initial Setup ```bash # Clone the sample plugin git clone https://github.com/obsidianmd/obsidian-sample-plugin.git cd obsidian-sample-plugin # Install dependencies npm install # Start development watch mode npm run dev ``` #### Project Structure ```sh plugin-folder/ ├── main.ts # Main plugin code ├── manifest.json # Plugin metadata ├── package.json # Dependencies ├── tsconfig.json # TypeScript config ├── esbuild.config.mjs # Build configuration └── styles.css # Optional styling ``` #### manifest.json Example ```json { "id": "my-plugin", "name": "My Plugin", "version": "1.0.0", "minAppVersion": "0.15.0", "description": "Plugin description", "author": "Your Name", "authorUrl": "https://yoursite.com", "isDesktopOnly": false } ``` --- ### Plugin Anatomy #### Basic Plugin Class ```typescript import { Plugin } from "obsidian"; export default class MyPlugin extends Plugin { // Called when plugin is loaded async onload() { console.log("Loading plugin"); // Load settings await this.loadSettings(); // Register components this.addRibbonIcon("dice", "My Plugin", () => { console.log("Ribbon clicked"); }); } // Called when plugin is disabled onunload() { console.log("Unloading plugin"); } } ``` #### Plugin Lifecycle Methods | Method | Purpose | When Called | | ---------------- | ----------------- | -------------------------- | | `onload()` | Initialize plugin | Plugin enabled/app starts | | `onunload()` | Cleanup resources | Plugin disabled/app closes | | `loadSettings()` | Load saved data | User-defined | | `saveSettings()` | Save plugin data | User-defined | | --- | --- | --- | ### Core API #### App Interface The global `app` object provides access to all major interfaces. ```typescript // Access core modules this.app.vault; // File operations this.app.workspace; // UI/panes this.app.metadataCache; // Cached metadata ``` #### Vault Operations ```typescript import { TFile, TFolder } from "obsidian"; // Read file const file = this.app.vault.getAbstractFileByPath("path/to/file.md") as TFile; const content = await this.app.vault.read(file); // Write file await this.app.vault.modify(file, "new content"); // Create file await this.app.vault.create("path/to/newfile.md", "initial content"); // Delete file await this.app.vault.delete(file); // Rename file await this.app.vault.rename(file, "new-name.md"); // Get all markdown files const files = this.app.vault.getMarkdownFiles(); // Process file await this.app.vault.process(file, (data) => { // Modify data return data.replace(/old/g, "new"); }); ``` #### Workspace Operations ```typescript import { MarkdownView } from "obsidian"; // Get active view const view = this.app.workspace.getActiveViewOfType(MarkdownView); // Get active file const file = this.app.workspace.getActiveFile(); // Open file in new leaf const leaf = this.app.workspace.getLeaf("tab"); await leaf.openFile(file); // Split workspace const newLeaf = this.app.workspace.getLeaf("split"); // Access editor if (view) { const editor = view.editor; const cursor = editor.getCursor(); const selection = editor.getSelection(); editor.replaceSelection("new text"); } ``` #### MetadataCache ```typescript // Get file cache const cache = this.app.metadataCache.getFileCache(file); // Access metadata if (cache) { const headings = cache.headings; // All headings const links = cache.links; // Internal links const embeds = cache.embeds; // Embedded files const tags = cache.tags; // Tags const frontmatter = cache.frontmatter; // YAML frontmatter } // Get first link destination const dest = this.app.metadataCache.getFirstLinkpathDest("note-name", "source-file.md"); // Listen for cache changes this.registerEvent( this.app.metadataCache.on("changed", (file) => { console.log("Cache updated for:", file.path); }), ); // Wait for all files to be indexed this.registerEvent( this.app.metadataCache.on("resolved", () => { console.log("All files indexed"); }), ); ``` --- ### User Interface #### Commands ```typescript // Simple command this.addCommand({ id: "my-command", name: "My Command", callback: () => { console.log("Command executed"); }, }); // Command with hotkey this.addCommand({ id: "hotkey-command", name: "Command with Hotkey", hotkeys: [ { modifiers: ["Mod", "Shift"], key: "k", }, ], callback: () => { console.log("Hotkey pressed"); }, }); // Editor command this.addCommand({ id: "editor-command", name: "Editor Command", editorCallback: (editor, view) => { const selection = editor.getSelection(); editor.replaceSelection(selection.toUpperCase()); }, }); // Conditional command this.addCommand({ id: "conditional-command", name: "Conditional Command", checkCallback: (checking) => { const view = this.app.workspace.getActiveViewOfType(MarkdownView); if (checking) { return view !== null; // Show only when markdown view active } // Execute command console.log("Command executed"); }, }); ``` #### Ribbon Actions ```typescript // Add ribbon icon const ribbonIcon = this.addRibbonIcon( "dice", // Icon name (lucide icons) "Roll Dice", // Tooltip (evt: MouseEvent) => { new Notice("Rolling dice!"); }, ); // Add class to ribbon icon ribbonIcon.addClass("my-plugin-ribbon-class"); // Remove on cleanup (automatic via plugin lifecycle) ``` #### Settings Tab ```typescript import { App, PluginSettingTab, Setting } from "obsidian"; interface MyPluginSettings { mySetting: string; enableFeature: boolean; numberValue: number; } const DEFAULT_SETTINGS: MyPluginSettings = { mySetting: "default", enableFeature: true, numberValue: 42, }; class MySettingTab extends PluginSettingTab { plugin: MyPlugin; constructor(app: App, plugin: MyPlugin) { super(app, plugin); this.plugin = plugin; } display(): void { const { containerEl } = this; containerEl.empty(); containerEl.createEl("h2", { text: "My Plugin Settings" }); // Text input new Setting(containerEl) .setName("Setting name") .setDesc("Setting description") .addText((text) => text .setPlaceholder("Enter value") .setValue(this.plugin.settings.mySetting) .onChange(async (value) => { this.plugin.settings.mySetting = value; await this.plugin.saveSettings(); }), ); // Toggle new Setting(containerEl) .setName("Enable feature") .setDesc("Turn feature on or off") .addToggle((toggle) => toggle.setValue(this.plugin.settings.enableFeature).onChange(async (value) => { this.plugin.settings.enableFeature = value; await this.plugin.saveSettings(); }), ); // Slider new Setting(containerEl) .setName("Number value") .setDesc("Select a number") .addSlider((slider) => slider .setLimits(0, 100, 1) .setValue(this.plugin.settings.numberValue) .setDynamicTooltip() .onChange(async (value) => { this.plugin.settings.numberValue = value; await this.plugin.saveSettings(); }), ); // Dropdown new Setting(containerEl) .setName("Choose option") .setDesc("Select from dropdown") .addDropdown((dropdown) => dropdown .addOption("option1", "Option 1") .addOption("option2", "Option 2") .setValue(this.plugin.settings.mySetting) .onChange(async (value) => { this.plugin.settings.mySetting = value; await this.plugin.saveSettings(); }), ); // Button new Setting(containerEl) .setName("Action button") .setDesc("Click to perform action") .addButton((button) => button.setButtonText("Do Something").onClick(async () => { new Notice("Button clicked!"); }), ); } } // In plugin onload: this.addSettingTab(new MySettingTab(this.app, this)); ``` #### Modals ```typescript import { Modal, Notice } from "obsidian"; // Basic Modal class MyModal extends Modal { onOpen() { const { contentEl } = this; contentEl.createEl("h2", { text: "My Modal" }); contentEl.createEl("p", { text: "Modal content here" }); } onClose() { const { contentEl } = this; contentEl.empty(); } } // Open modal new MyModal(this.app).open(); // Modal with input class InputModal extends Modal { result: string; onSubmit: (result: string) => void; constructor(app: App, onSubmit: (result: string) => void) { super(app); this.onSubmit = onSubmit; } onOpen() { const { contentEl } = this; contentEl.createEl("h2", { text: "Enter value" }); new Setting(contentEl).setName("Value").addText((text) => text.onChange((value) => { this.result = value; }), ); new Setting(contentEl).addButton((btn) => btn .setButtonText("Submit") .setCta() .onClick(() => { this.close(); this.onSubmit(this.result); }), ); } onClose() { const { contentEl } = this; contentEl.empty(); } } // Use input modal new InputModal(this.app, (result) => { new Notice(`You entered: ${result}`); }).open(); // SuggestModal - show suggestions class MySuggestModal extends SuggestModal<string> { getSuggestions(query: string): string[] { const items = ["apple", "banana", "cherry", "date", "elderberry"]; return items.filter((item) => item.toLowerCase().includes(query.toLowerCase())); } renderSuggestion(item: string, el: HTMLElement) { el.createEl("div", { text: item }); } onChooseSuggestion(item: string, evt: MouseEvent | KeyboardEvent) { new Notice(`Selected: ${item}`); } } // FuzzySuggestModal - fuzzy search class MyFuzzySuggestModal extends FuzzySuggestModal<TFile> { getItems(): TFile[] { return this.app.vault.getMarkdownFiles(); } getItemText(item: TFile): string { return item.basename; } onChooseItem(item: TFile, evt: MouseEvent | KeyboardEvent) { new Notice(`Selected: ${item.basename}`); this.app.workspace.openLinkText(item.path, "", false); } } ``` #### Notices ```typescript // Simple notice new Notice("Something happened!"); // Notice with duration (ms) new Notice("This will stay for 10 seconds", 10000); // Notice with fragment const frag = document.createDocumentFragment(); frag.createEl("span", { text: "Bold: " }); frag.createEl("strong", { text: "Important!" }); new Notice(frag); ``` #### Custom Views ```typescript import { ItemView, WorkspaceLeaf } from 'obsidian'; const VIEW_TYPE_EXAMPLE = 'example-view'; class ExampleView extends ItemView { constructor(leaf: WorkspaceLeaf) { super(leaf); } getViewType() { return VIEW_TYPE_EXAMPLE; } getDisplayText() { return 'Example view'; } // Icon from lucide icons getIcon() { return 'dice'; } async onOpen() { const container = this.containerEl.children[1]; container.empty(); container.createEl('h2', { text: 'Custom View' }); container.createEl('p', { text: 'This is a custom view!' }); } async onClose() { // Cleanup } } // Register view in plugin onload this.registerView( VIEW_TYPE_EXAMPLE, (leaf) => new ExampleView(leaf) ); // Add command to open view this.addCommand({ id: 'open-example-view', name: 'Open Example View', callback: () => { this.activateView(); } }); // Method to activate view async activateView() { const { workspace } = this.app; let leaf: WorkspaceLeaf | null = null; const leaves = workspace.getLeavesOfType(VIEW_TYPE_EXAMPLE); if (leaves.length > 0) { // View already exists, reveal it leaf = leaves[0]; } else { // Create new leaf in right sidebar leaf = workspace.getRightLeaf(false); await leaf.setViewState({ type: VIEW_TYPE_EXAMPLE, active: true, }); } // Reveal the leaf workspace.revealLeaf(leaf); } ``` #### Status Bar ```typescript // Add status bar item const statusBarItem = this.addStatusBarItem(); statusBarItem.setText("Status: Ready"); // Update status bar statusBarItem.setText("Status: Processing..."); // Style status bar statusBarItem.addClass("my-status-bar-class"); ``` --- ### Editor Extensions #### CodeMirror 6 Integration Obsidian uses CodeMirror 6 for the editor. Editor extensions are CM6 extensions. ```typescript import { EditorView, Decoration, DecorationSet, ViewPlugin, ViewUpdate } from "@codemirror/view"; import { syntaxTree } from "@codemirror/language"; // Register editor extension this.registerEditorExtension([ ViewPlugin.fromClass( class { decorations: DecorationSet; constructor(view: EditorView) { this.decorations = this.buildDecorations(view); } update(update: ViewUpdate) { if (update.docChanged || update.viewportChanged) { this.decorations = this.buildDecorations(update.view); } } buildDecorations(view: EditorView): DecorationSet { const builder = new RangeSetBuilder<Decoration>(); for (const { from, to } of view.visibleRanges) { syntaxTree(view.state).iterate({ from, to, enter(node) { // Add decorations based on syntax if (node.name === "strong") { builder.add(node.from, node.to, Decoration.mark({ class: "cm-strong" })); } }, }); } return builder.finish(); } }, { decorations: (v) => v.decorations, }, ), ]); ``` #### Editor State Fields ```typescript import { StateField, StateEffect } from "@codemirror/state"; // Define state effect const highlightEffect = StateEffect.define<{ from: number; to: number }>(); // Define state field const highlightField = StateField.define<DecorationSet>({ create() { return Decoration.none; }, update(decorations, tr) { decorations = decorations.map(tr.changes); for (const effect of tr.effects) { if (effect.is(highlightEffect)) { decorations = decorations.update({ add: [ Decoration.mark({ class: "cm-highlight", }).range(effect.value.from, effect.value.to), ], }); } } return decorations; }, provide: (f) => EditorView.decorations.from(f), }); // Register extension this.registerEditorExtension([highlightField]); // Dispatch effect const view = this.app.workspace.getActiveViewOfType(MarkdownView); if (view) { view.editor.cm.dispatch({ effects: highlightEffect.of({ from: 0, to: 10 }), }); } ``` #### Editor Commands ```typescript // Add editor command this.addCommand({ id: "transform-text", name: "Transform Selected Text", editorCallback: (editor: Editor, view: MarkdownView) => { const selection = editor.getSelection(); // Transform text const transformed = selection.toUpperCase(); // Replace selection editor.replaceSelection(transformed); // Or use transaction for more control editor.transaction({ changes: [ { from: editor.getCursor("from"), to: editor.getCursor("to"), text: transformed, }, ], selection: { anchor: editor.getCursor("from").offset, }, }); }, }); ``` --- ### Data Persistence #### Plugin Settings ```typescript interface MyPluginSettings { apiKey: string; features: string[]; lastSync: number; } const DEFAULT_SETTINGS: MyPluginSettings = { apiKey: "", features: [], lastSync: 0, }; export default class MyPlugin extends Plugin { settings: MyPluginSettings; async onload() { await this.loadSettings(); } async loadSettings() { this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); } async saveSettings() { await this.saveData(this.settings); } } ``` #### Data Storage Location Plugin data is stored in `.obsidian/plugins/[plugin-id]/data.json` #### Complex Data Structures ```typescript interface TaskData { id: string; title: string; completed: boolean; created: number; } interface PluginData { version: number; tasks: Map<string, TaskData>; cache: Record<string, any>; } async loadSettings() { const data = await this.loadData(); // Handle data migration if (!data || data.version < 2) { this.settings = this.migrateData(data); } else { this.settings = data; } // Convert plain objects to Maps if needed if (this.settings.tasks) { this.settings.tasks = new Map(Object.entries(this.settings.tasks)); } } async saveSettings() { // Convert Maps to plain objects for JSON serialization const dataToSave = { ...this.settings, tasks: this.settings.tasks ? Object.fromEntries(this.settings.tasks) : {} }; await this.saveData(dataToSave); } migrateData(oldData: any): PluginData { // Migration logic return { version: 2, tasks: new Map(), cache: oldData?.cache || {} }; } ``` #### Best Practices - **DO use** `loadData()` and `saveData()` for persistence - **DON'T use** `localStorage` - has compatibility issues on mobile - **DO** implement data migration for version updates - **DO** debounce frequent saves to reduce I/O ```typescript import { debounce } from "obsidian"; // Debounced save debouncedSave = debounce( async () => { await this.saveData(this.settings); }, 2000, // Wait 2 seconds after last change true, // Leading edge ); // Use debounced save this.settings.value = newValue; this.debouncedSave(); ``` --- ### Event System #### Core Event Types ```typescript // Vault events this.registerEvent( this.app.vault.on("create", (file) => { console.log("File created:", file.path); }), ); this.registerEvent( this.app.vault.on("modify", (file) => { console.log("File modified:", file.path); }), ); this.registerEvent( this.app.vault.on("delete", (file) => { console.log("File deleted:", file.path); }), ); this.registerEvent( this.app.vault.on("rename", (file, oldPath) => { console.log(`File renamed from ${oldPath} to ${file.path}`); }), ); // Workspace events this.registerEvent( this.app.workspace.on("file-open", (file) => { console.log("File opened:", file?.path); }), ); this.registerEvent( this.app.workspace.on("active-leaf-change", (leaf) => { console.log("Active leaf changed"); }), ); this.registerEvent( this.app.workspace.on("layout-change", () => { console.log("Layout changed"); }), ); this.registerEvent( this.app.workspace.on("editor-change", (editor, info) => { console.log("Editor changed"); }), ); // MetadataCache events this.registerEvent( this.app.metadataCache.on("changed", (file, data, cache) => { console.log("Metadata changed for:", file.path); }), ); this.registerEvent( this.app.metadataCache.on("resolved", () => { console.log("All metadata resolved"); }), ); ``` #### DOM Events ```typescript // Register DOM event (auto-cleanup) this.registerDomEvent(document, "click", (evt: MouseEvent) => { console.log("Document clicked", evt); }); // Window events this.registerDomEvent(window, "resize", (evt: UIEvent) => { console.log("Window resized"); }); // Element events const element = document.querySelector(".my-element"); if (element) { this.registerDomEvent(element, "click", (evt: MouseEvent) => { console.log("Element clicked"); }); } ``` #### Intervals ```typescript // Register interval (auto-cleanup) this.registerInterval( window.setInterval( () => { console.log("Periodic task"); }, 5 * 60 * 1000, ), // Every 5 minutes ); ``` #### Custom Events ```typescript import { Events } from "obsidian"; class MyEventEmitter extends Events { trigger(name: string, ...data: any[]) { super.trigger(name, ...data); } } // In plugin const emitter = new MyEventEmitter(); // Subscribe to event this.registerEvent( emitter.on("custom-event", (data) => { console.log("Custom event:", data); }), ); // Emit event emitter.trigger("custom-event", { foo: "bar" }); ``` #### Event Timing ```typescript // Wait for workspace ready this.app.workspace.onLayoutReady(() => { console.log("Workspace layout ready"); // Safe to access DOM elements now }); // Execute on metadata cache resolved this.registerEvent( this.app.metadataCache.on("resolved", () => { console.log("All files indexed, safe to process metadata"); }), ); ``` --- ### Best Practices #### Performance Optimization ```typescript // ❌ Bad: Synchronous file reads const files = this.app.vault.getMarkdownFiles(); for (const file of files) { const content = await this.app.vault.read(file); // Sequential // Process content } // ✅ Good: Parallel file reads const files = this.app.vault.getMarkdownFiles(); const contents = await Promise.all( files.map(file => this.app.vault.read(file)) ); // ❌ Bad: Processing all files on load async onload() { const files = this.app.vault.getMarkdownFiles(); await this.processAllFiles(files); // Slow startup } // ✅ Good: Lazy loading async onload() { // Quick startup this.app.workspace.onLayoutReady(() => { // Process files in background this.processFilesInBackground(); }); } // ❌ Bad: No caching getFileMetadata(file: TFile) { return this.app.metadataCache.getFileCache(file); // Called frequently } // ✅ Good: Cache results private metadataCache = new Map<string, CachedMetadata>(); getFileMetadata(file: TFile) { if (!this.metadataCache.has(file.path)) { const cache = this.app.metadataCache.getFileCache(file); this.metadataCache.set(file.path, cache); } return this.metadataCache.get(file.path); } ``` #### Error Handling ```typescript // ❌ Bad: Unhandled errors async loadSettings() { this.settings = await this.loadData(); } // ✅ Good: Proper error handling async loadSettings() { try { const data = await this.loadData(); this.settings = Object.assign({}, DEFAULT_SETTINGS, data); } catch (error) { console.error('Failed to load settings:', error); new Notice('Failed to load plugin settings'); this.settings = DEFAULT_SETTINGS; } } // ❌ Bad: Silent failures async processFile(file: TFile) { const content = await this.app.vault.read(file); // Process content } // ✅ Good: User feedback async processFile(file: TFile) { try { const content = await this.app.vault.read(file); // Process content new Notice(`Processed ${file.basename}`); } catch (error) { console.error(`Failed to process ${file.path}:`, error); new Notice(`Failed to process ${file.basename}`, 5000); } } ``` #### Security ```typescript // ❌ Bad: XSS vulnerability const userInput = await getUserInput(); containerEl.innerHTML = userInput; // Dangerous! // ✅ Good: Use DOM methods const userInput = await getUserInput(); containerEl.createEl("div", { text: userInput }); // Safe // ❌ Bad: Command injection const filename = file.basename; exec(`cat ${filename}`); // Dangerous! // ✅ Good: Use Vault API const content = await this.app.vault.read(file); // Safe // ❌ Bad: Storing sensitive data this.settings.apiKey = userApiKey; await this.saveSettings(); // Stored in plain text! // ✅ Good: Warn users new Setting(containerEl) .setName("API Key") .setDesc("⚠️ Stored in plain text in vault/.obsidian folder") .addText((text) => text .setPlaceholder("Enter API key") .setValue(this.settings.apiKey) .onChange(async (value) => { this.settings.apiKey = value; await this.saveSettings(); }), ); ``` #### Memory Management ```typescript // ❌ Bad: Memory leaks class MyPlugin extends Plugin { private data: LargeDataStructure; async onload() { this.data = await loadLargeData(); // Never cleaned up } } // ✅ Good: Proper cleanup class MyPlugin extends Plugin { private data: LargeDataStructure | null = null; async onload() { this.data = await loadLargeData(); } onunload() { this.data = null; // Allow garbage collection } } // ✅ Good: Use WeakMap for cache private cache = new WeakMap<TFile, ProcessedData>(); getProcessedData(file: TFile): ProcessedData { if (!this.cache.has(file)) { this.cache.set(file, this.processFile(file)); } return this.cache.get(file)!; } // When file is deleted, entry is automatically removed ``` #### Mobile Compatibility ```typescript // Check if running on mobile if (Platform.isMobile) { // Mobile-specific code new Notice("Running on mobile"); } else { // Desktop-specific code new Notice("Running on desktop"); } // Disable plugin on mobile if (Platform.isMobile && this.manifest.isDesktopOnly) { return; } // Responsive UI if (Platform.isMobileApp) { // Simplify UI for mobile this.addCommand({ id: "mobile-action", name: "Mobile Action", mobileOnly: true, callback: () => { // Mobile implementation }, }); } ``` #### TypeScript Best Practices ```typescript // ✅ Use interfaces for settings interface MySettings { feature: boolean; value: number; } // ✅ Type your callbacks this.addCommand({ id: "typed-command", name: "Typed Command", callback: (): void => { // Implementation }, }); // ✅ Use type guards function isMarkdownView(view: View): view is MarkdownView { return view.getViewType() === "markdown"; } const view = this.app.workspace.getActiveViewOfType(View); if (view && isMarkdownView(view)) { // TypeScript knows view is MarkdownView const editor = view.editor; } // ✅ Use enums for constants enum ViewType { EXAMPLE = "example-view", ANOTHER = "another-view", } this.registerView(ViewType.EXAMPLE, (leaf) => new ExampleView(leaf)); ``` #### Testing & Debugging ```typescript // Add debug logging const DEBUG = false; function log(...args: any[]) { if (DEBUG) { console.log('[MyPlugin]', ...args); } } // Measure performance async processLargeDataset() { console.time('processLargeDataset'); // ... processing console.timeEnd('processLargeDataset'); } // Validate data function validateSettings(settings: any): settings is MySettings { return ( typeof settings.feature === 'boolean' && typeof settings.value === 'number' && settings.value >= 0 && settings.value <= 100 ); } async loadSettings() { const data = await this.loadData(); if (!validateSettings(data)) { console.warn('Invalid settings, using defaults'); this.settings = DEFAULT_SETTINGS; } else { this.settings = data; } } ``` #### Plugin Distribution ```typescript // Update version in manifest.json { "version": "1.0.0", "minAppVersion": "0.15.0" } // Update versions.json for compatibility { "1.0.0": "0.15.0", "0.9.0": "0.14.0" } // Create GitHub release with: // - main.js (compiled code) // - manifest.json // - styles.css (if applicable) // Release checklist: // [ ] Update version in manifest.json // [ ] Update versions.json // [ ] Update CHANGELOG.md // [ ] Test on desktop and mobile // [ ] Run `npm run build` // [ ] Create GitHub release // [ ] Attach build artifacts ``` --- ### Common Patterns #### File Processing Pipeline ```typescript async processAllFiles() { const files = this.app.vault.getMarkdownFiles(); const batchSize = 10; for (let i = 0; i < files.length; i += batchSize) { const batch = files.slice(i, i + batchSize); await Promise.all(batch.map(file => this.processFile(file))); // Update progress new Notice(`Processed ${Math.min(i + batchSize, files.length)}/${files.length} files`); } } async processFile(file: TFile) { const content = await this.app.vault.read(file); const cache = this.app.metadataCache.getFileCache(file); // Process content and metadata const processed = this.transform(content, cache); if (processed !== content) { await this.app.vault.modify(file, processed); } } ``` #### Modal with Form Validation ```typescript class ValidationModal extends Modal { private email: string = ""; private age: number = 0; onOpen() { const { contentEl } = this; contentEl.createEl("h2", { text: "Enter Details" }); // Email input new Setting(contentEl) .setName("Email") .addText((text) => text.setPlaceholder("[email protected]").onChange((value) => (this.email = value))); // Age input new Setting(contentEl) .setName("Age") .addText((text) => text.setPlaceholder("18").onChange((value) => (this.age = parseInt(value) || 0))); // Submit button new Setting(contentEl).addButton((btn) => btn .setButtonText("Submit") .setCta() .onClick(() => { if (this.validate()) { this.onSubmit(); this.close(); } }), ); } validate(): boolean { if (!this.email.includes("@")) { new Notice("Invalid email address"); return false; } if (this.age < 18 || this.age > 120) { new Notice("Age must be between 18 and 120"); return false; } return true; } onSubmit() { new Notice(`Submitted: ${this.email}, ${this.age}`); // Process form data } onClose() { const { contentEl } = this; contentEl.empty(); } } ``` #### Context Menu Integration ```typescript // Add context menu item this.registerEvent( this.app.workspace.on("file-menu", (menu, file) => { menu.addItem((item) => { item .setTitle("Process File") .setIcon("dice") .onClick(async () => { await this.processFile(file); new Notice(`Processed ${file.basename}`); }); }); }), ); // Editor context menu this.registerEvent( this.app.workspace.on("editor-menu", (menu, editor, view) => { menu.addItem((item) => { item .setTitle("Transform Selection") .setIcon("wand") .onClick(() => { const selection = editor.getSelection(); editor.replaceSelection(selection.toUpperCase()); }); }); }), ); ``` --- ### Resources #### Official Documentation - [Obsidian Developer Docs](https://docs.obsidian.md/) - [Obsidian API Repository](https://github.com/obsidianmd/obsidian-api) - [Sample Plugin](https://github.com/obsidianmd/obsidian-sample-plugin) #### Community Resources - [Obsidian Plugin Developer Docs (archived)](https://marcusolsson.github.io/obsidian-plugin-docs/) - [API Reference (DeepWiki)](https://deepwiki.com/obsidianmd/obsidian-api) - [Obsidian Forum - Developers](https://forum.obsidian.md/c/developers-api/) #### Type Definitions ```bash # Install Obsidian API types npm install -D obsidian # Install CodeMirror types npm install -D @codemirror/state @codemirror/view ``` #### Useful Libraries - [obsidian-dataview](https://github.com/blacksmithgu/obsidian-dataview) - Query vault data - [obsidian-daily-notes-interface](https://github.com/liamcain/obsidian-daily-notes-interface) - Daily notes utilities - [obsidian-calendar-ui](https://github.com/liamcain/obsidian-calendar-ui) - Calendar components --- ### Quick Reference #### Plugin Lifecycle ```typescript onload() → loadSettings() → register components → ready unload() → cleanup → save data → done ``` #### Core APIs ```typescript this.app.vault; // File operations this.app.workspace; // UI/layout this.app.metadataCache; // Cached metadata ``` #### Registration Methods ```typescript this.addCommand(); // Add command this.addRibbonIcon(); // Add ribbon button this.addSettingTab(); // Add settings tab this.addStatusBarItem(); // Add status bar this.registerView(); // Register custom view this.registerEditorExtension(); // Register CM6 extension this.registerEvent(); // Register event listener this.registerDomEvent(); // Register DOM event this.registerInterval(); // Register interval ``` #### Common Imports ```typescript import { App, Plugin, PluginSettingTab, Setting, Notice, Modal, TFile, TFolder, MarkdownView, ItemView, WorkspaceLeaf, Editor, SuggestModal, FuzzySuggestModal, debounce, Platform, } from "obsidian"; ``` --- _Last Updated: 2025_ _Based on Obsidian API documentation and community resources_