Skip to main content

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

app
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

manifest
PluginManifest
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

enabled
boolean
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()

addStatusBarItem
() => IStatusBarItem
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()

getRecentFiles
() => string[]
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();
        })
      );
  }
}