Overview
All Inkdown plugins extend the Plugin base class, which provides lifecycle methods and utility functions for plugin development.
import { Plugin } from '@inkdown/api';
export default class MyPlugin extends Plugin {
async onload(): Promise<void> {
// Initialize plugin
}
async onunload(): Promise<void> {
// Cleanup
}
}
Class Definition
export abstract class Plugin extends Component {
app: App;
manifest: PluginManifest;
enabled: boolean;
constructor(app: App, manifest: PluginManifest);
abstract onload(): Promise<void>;
abstract onunload(): Promise<void>;
loadData<T = unknown>(): Promise<T | undefined>;
saveData(data: unknown): Promise<void>;
addCommand(command: Command): Command;
addSettingTab(tab: PluginSettingTab): void;
addStatusBarItem(): IStatusBarItem;
registerEditorSuggest<T>(suggest: EditorSuggest<T>): void;
registerMarkdownCodeBlockProcessor(
language: string,
processor: MarkdownCodeBlockProcessor
): void;
registerMarkdownPostProcessor(processor: MarkdownPostProcessor): void;
registerEditorExtension(extension: Extension): void;
registerEvent(eventRef: () => void): void;
registerDomEvent<K extends keyof HTMLElementEventMap>(
el: HTMLElement | Document | Window,
type: K,
callback: (ev: HTMLElementEventMap[K]) => unknown,
options?: boolean | AddEventListenerOptions
): void;
showNotice(message: string, duration?: number): Notice;
getRecentFiles(): string[];
}
Properties
app
Reference to the main application instance. Provides access to all managers and services.
this.app.workspace // File operations
this.app.commandManager // Commands
this.app.metadataCache // File metadata
this.app.editorRegistry // Editors
See App Interface for details.
manifest
The plugin’s manifest metadata loaded from manifest.json.
console.log(this.manifest.id); // "my-plugin"
console.log(this.manifest.version); // "1.0.0"
console.log(this.manifest.author); // "Your Name"
enabled
Whether the plugin is currently enabled.
if (this.enabled) {
// Plugin is active
}
Lifecycle Methods
onload()
onload
() => Promise<void>
required
Called when the plugin is loaded. Initialize your plugin here.
async onload() {
console.log('Plugin loaded');
// Load settings
await this.loadSettings();
// Register commands
this.addCommand({
id: 'my-command',
name: 'My Command',
callback: () => console.log('Command executed'),
});
// Add status bar
const statusBar = this.addStatusBarItem();
statusBar.setText('Ready');
// Register events
this.registerEvent(
this.app.workspace.onFileCreate((file) => {
console.log('File created:', file.path);
})
);
}
Common initialization tasks:
- Load plugin settings
- Register commands
- Create UI elements
- Register event listeners
- Initialize services
onunload()
onunload
() => Promise<void>
required
Called when the plugin is unloaded. Clean up resources here.
async onunload() {
console.log('Plugin unloaded');
// Stop intervals
if (this.updateInterval) {
clearInterval(this.updateInterval);
}
// Close modals
if (this.modal) {
this.modal.close();
}
}
Most cleanup is handled automatically. You only need to clean up resources you manage manually (intervals, external connections, etc.).
Auto-cleanup (handled by framework):
- Registered commands
- Status bar items
- Settings tabs
- Event listeners (registered with
registerEvent)
- Editor extensions
- Markdown processors
Data Persistence
loadData()
loadData
<T>() => Promise<T | undefined>
Load plugin data from disk. Returns undefined if no data exists.
interface MyPluginData {
count: number;
lastUsed: string;
}
async onload() {
const data = await this.loadData<MyPluginData>();
if (data) {
console.log('Count:', data.count);
console.log('Last used:', data.lastUsed);
}
}
Example: Settings Pattern
interface MySettings {
enabled: boolean;
format: string;
}
const DEFAULT_SETTINGS: MySettings = {
enabled: true,
format: 'YYYY-MM-DD',
};
export default class MyPlugin extends Plugin {
settings: MySettings = DEFAULT_SETTINGS;
async onload() {
await this.loadSettings();
}
async loadSettings() {
const data = await this.loadData<MySettings>();
this.settings = { ...DEFAULT_SETTINGS, ...data };
}
}
saveData()
saveData
(data: unknown) => Promise<void>
Save plugin data to disk. Data is serialized as JSON.
async saveSettings() {
await this.saveData(this.settings);
}
// Usage
this.settings.enabled = false;
await this.saveSettings();
Data must be JSON-serializable. Functions, class instances, and circular references will cause errors.
Registration Methods
addCommand()
addCommand
(command: Command) => Command
Register a command that appears in the command palette.
this.addCommand({
id: 'insert-date',
name: 'Insert Current Date',
hotkey: ['Mod', 'd'],
editorCallback: (editor) => {
const date = new Date().toLocaleDateString();
editor.replaceSelection(date);
},
});
See Commands for details.
addSettingTab()
addSettingTab
(tab: PluginSettingTab) => void
Add a settings tab for your plugin.
import { PluginSettingTab } from '@inkdown/api';
class MySettingTab extends PluginSettingTab {
display() {
// Build settings UI
}
}
async onload() {
this.addSettingTab(new MySettingTab(this.app, this));
}
See Settings for details.
addStatusBarItem()
Create a status bar item.
const statusBar = this.addStatusBarItem();
statusBar.setText('Word count: 0');
statusBar.setIcon('file-text');
statusBar.setOnClick(() => {
this.showNotice('Status bar clicked!');
});
See Status Bar for details.
registerEditorSuggest()
registerEditorSuggest
<T>(suggest: EditorSuggest<T>) => void
Register an autocomplete/suggest provider for the editor.
class MyEditorSuggest extends EditorSuggest<string> {
onTrigger(cursor, editor, file) {
// Detect trigger
}
getSuggestions(context) {
return ['suggestion1', 'suggestion2'];
}
selectSuggestion(value, evt) {
// Insert suggestion
}
}
async onload() {
this.registerEditorSuggest(new MyEditorSuggest(this.app));
}
See Editor Suggest for details.
registerMarkdownCodeBlockProcessor()
registerMarkdownCodeBlockProcessor
(language: string, processor: MarkdownCodeBlockProcessor) => void
Process code blocks with a specific language.
this.registerMarkdownCodeBlockProcessor('mermaid', (source, el, ctx) => {
// Render mermaid diagram
el.setText('Diagram: ' + source);
});
See Markdown Processing for details.
registerMarkdownPostProcessor()
registerMarkdownPostProcessor
(processor: MarkdownPostProcessor) => void
Process rendered markdown content.
this.registerMarkdownPostProcessor((el, ctx) => {
// Find and process elements
const links = el.findAll('a');
links.forEach(link => {
link.addClass('external-link');
});
});
See Markdown Processing for details.
registerEditorExtension()
registerEditorExtension
(extension: Extension) => void
Add a CodeMirror extension to the editor.
import { ViewPlugin } from '@codemirror/view';
const myExtension = ViewPlugin.fromClass(class {
constructor(view) {
// Initialize
}
update(update) {
// Handle updates
}
});
this.registerEditorExtension(myExtension);
See Editor Extensions for details.
registerEvent()
registerEvent
(eventRef: () => void) => void
Register an event listener that will be cleaned up automatically.
this.registerEvent(
this.app.workspace.onFileCreate((file) => {
console.log('Created:', file.path);
})
);
this.registerEvent(
this.app.workspace.onFileModify((file) => {
console.log('Modified:', file.path);
})
);
See Events for details.
registerDomEvent()
registerDomEvent
(el: HTMLElement, type: string, callback: Function, options?: AddEventListenerOptions) => void
Register a DOM event listener with automatic cleanup.
const button = document.createElement('button');
this.registerDomEvent(button, 'click', (evt) => {
console.log('Button clicked');
});
// Also works with window and document
this.registerDomEvent(window, 'resize', () => {
console.log('Window resized');
});
Utility Methods
showNotice()
showNotice
(message: string, duration?: number) => Notice
Display a toast notification.
this.showNotice('File saved successfully!');
this.showNotice('Error occurred', 5000); // 5 second duration
Default duration is 3 seconds.
getRecentFiles()
Get list of recently opened file paths.
const recentFiles = this.getRecentFiles();
console.log('Recent files:', recentFiles);
Complete Example
Here’s the Word Count plugin demonstrating multiple features:
import { Plugin, PluginSettingTab, Setting } from '@inkdown/api';
interface WordCountSettings {
showCharCount: boolean;
showWordCount: boolean;
countSpaces: boolean;
}
const DEFAULT_SETTINGS: WordCountSettings = {
showCharCount: false,
showWordCount: true,
countSpaces: false,
};
export default class WordCountPlugin extends Plugin {
settings: WordCountSettings = DEFAULT_SETTINGS;
private statusBarItem: IStatusBarItem | null = null;
private updateInterval: number | null = null;
async onload() {
console.log('WordCountPlugin loaded');
await this.loadSettings();
await this.createStatusBarItem();
this.addSettingTab(new WordCountSettingTab(this.app, this));
this.startUpdating();
}
async onunload() {
console.log('WordCountPlugin unloaded');
this.stopUpdating();
}
async loadSettings() {
const data = await this.loadData<WordCountSettings>();
this.settings = { ...DEFAULT_SETTINGS, ...data };
}
async saveSettings() {
await this.saveData(this.settings);
this.updateWordCount();
}
private async createStatusBarItem() {
this.statusBarItem = this.addStatusBarItem();
this.statusBarItem.addClass('word-count-status');
this.statusBarItem.setIcon('folder-heart');
this.updateWordCount();
}
private startUpdating() {
this.updateInterval = window.setInterval(() => {
this.updateWordCount();
}, 500);
}
private stopUpdating() {
if (this.updateInterval !== null) {
window.clearInterval(this.updateInterval);
this.updateInterval = null;
}
}
private updateWordCount() {
if (!this.statusBarItem) return;
const activeEditorView = this.app.editorRegistry.getActive();
if (!activeEditorView) {
this.statusBarItem.setText('');
return;
}
const content = activeEditorView.state.doc.toString();
const wordCount = this.countWords(content);
const charCount = this.countCharacters(content);
const parts: string[] = [];
if (this.settings.showWordCount) {
parts.push(`${wordCount} ${wordCount === 1 ? 'word' : 'words'}`);
}
if (this.settings.showCharCount) {
parts.push(`${charCount} ${charCount === 1 ? 'char' : 'chars'}`);
}
this.statusBarItem.setText(parts.join(' • '));
}
private countWords(text: string): number {
if (!text || text.trim().length === 0) return 0;
const words = text.split(/\s+/).filter(word => word.length > 0);
return words.length;
}
private countCharacters(text: string): number {
if (!text) return 0;
return this.settings.countSpaces ? text.length : text.replace(/\s/g, '').length;
}
}
class WordCountSettingTab extends PluginSettingTab {
plugin: WordCountPlugin;
constructor(app: App, plugin: WordCountPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display() {
const { containerEl } = this;
new Setting(containerEl).setName('Word Count Settings').setHeading();
new Setting(containerEl)
.setName('Show word count')
.setDesc('Display the number of words in the current note')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.showWordCount)
.onChange(async (value) => {
this.plugin.settings.showWordCount = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Show character count')
.setDesc('Display the number of characters in the current note')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.showCharCount)
.onChange(async (value) => {
this.plugin.settings.showCharCount = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Count spaces in character count')
.setDesc('Include spaces when counting characters')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.countSpaces)
.onChange(async (value) => {
this.plugin.settings.countSpaces = value;
await this.plugin.saveSettings();
})
);
}
}