## 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_