Recipes

One worked example per contribution point. Each snippet is a complete main.js (or the relevant part of one); pair it with a manifest.json from Getting Started. Full signatures are in the API Reference.

Command palette entry

export default {
  activate(ctx) {
    ctx.commands.register({
      id: "demo.hello",
      title: "Demo: Say Hello",
      run: () => ctx.notify("Hello!"),
    });
  },
};

Status bar item

mount gets a container element; register teardown via registerCleanup.

export default {
  activate(ctx) {
    ctx.ui.addStatusBarItem({
      id: "demo.clock",
      mount(el, registerCleanup) {
        const tick = () => { el.textContent = new Date().toLocaleTimeString(); };
        tick();
        const timer = setInterval(tick, 1000);
        registerCleanup(() => clearInterval(timer));
      },
    });
  },
};
export default {
  activate(ctx) {
    ctx.ui.addSidebarPanel({
      id: "demo.panel",
      title: "Scratch",
      mount(el) {
        el.textContent = "Anything you render here appears under the Outline.";
      },
    });
  },
};

Settings panel with persisted settings

ctx.settings is hydrated before activate, so get is synchronous.

export default {
  activate(ctx) {
    ctx.ui.addSettingsPanel({
      id: "demo.settings",
      mount(el) {
        const input = document.createElement("input");
        input.value = ctx.settings.get("greeting") ?? "Hello";
        input.onchange = () => ctx.settings.set("greeting", input.value);
        el.append(input);
      },
    });
  },
};

Injected styles

Theme tweaks and custom CSS ship as plugins; styles are removed on unload.

export default {
  activate(ctx) {
    ctx.ui.addStyles(`
      .markdown-body h1 { letter-spacing: -0.02em; }
      .markdown-body blockquote { border-left-color: var(--color-accent); }
    `);
  },
};

Fenced code-block renderer

Render ```graphviz blocks (any language tag you register) with your own component. The render value is a React-style function component; return DOM-building code via the host's React (see Bundling for JSX setups), or keep it dependency-free:

export default {
  activate(ctx) {
    ctx.markdown.registerFencedRenderer("rot13", ({ code }) =>
      code.replace(/[a-z]/gi, (c) =>
        String.fromCharCode((c.charCodeAt(0) - (c < "a" ? 65 : 97) + 13) % 26 + (c < "a" ? 65 : 97)),
      ),
    );
  },
};

remark / rehype plugin

Standard unified plugins slot into the pipeline after the built-ins.

function remarkUppercaseHeadings() {
  return (tree) => {
    for (const node of tree.children) {
      if (node.type === "heading") {
        for (const child of node.children) {
          if (child.type === "text") child.value = child.value.toUpperCase();
        }
      }
    }
  };
}

export default {
  activate(ctx) {
    ctx.markdown.registerRemarkPlugin(remarkUppercaseHeadings);
  },
};

Exporter

The host prepares the rendered HTML, asks for a save location, and writes the file; you only turn HTML into contents. Appears in the palette as "Export: Plain HTML…".

export default {
  activate(ctx) {
    ctx.exporters.register({
      id: "demo.plain-html",
      label: "Plain HTML",
      extension: "html",
      build: async (bodyHtml) => `<!doctype html><body>${bodyHtml}</body>`,
    });
  },
};

Translations

Ship your own strings per locale; they deep-merge into the host's i18n.

export default {
  activate(ctx) {
    ctx.registerTranslations("en", "demo", { greet: "Hello" });
    ctx.registerTranslations("de", "demo", { greet: "Hallo" });
    ctx.registerTranslations("fa", "demo", { greet: "سلام" });
  },
};

Workspace reads (permission-gated)

Requires "permissions": ["workspace:read"] in the manifest; the user consents at install.

export default {
  activate(ctx) {
    ctx.commands.register({
      id: "demo.count-files",
      title: "Demo: Count Workspace Files",
      run: async () => {
        const files = await ctx.workspace.listFiles();
        ctx.notify(`${files.length} markdown files in this workspace`);
      },
    });
  },
};

Spell-check dictionary

Add a language to Settings → Editor → Spell Check. Ship the Hunspell files as package assets and read them through ctx.assets; load is lazy, so nothing is read until the user picks the language. No permissions needed: bundled assets are the plugin's own content.

{
  "id": "com.you.dictionary-fa",
  "name": "Persian Dictionary",
  "version": "1.0.0",
  "apiVersion": "0.16.0",
  "main": "main.js",
  "files": ["main.js", "assets/fa.aff", "assets/fa.dic"],
  "permissions": []
}
export default {
  activate(ctx) {
    ctx.spellcheck.registerDictionary({
      language: "fa",
      label: "فارسی (Persian)",
      load: async () => ({
        aff: await ctx.assets.readText("assets/fa.aff"),
        dic: await ctx.assets.readText("assets/fa.dic"),
      }),
    });
  },
};

Sandboxed network plugin

Set "sandbox": true and declare the hosts you call; fetch is fenced to them and the plugin runs in a worker with no DOM. See the API reference for the available ctx subset.

{
  "id": "demo.quote",
  "name": "Quote of the Day",
  "version": "1.0.0",
  "apiVersion": "0.16.0",
  "sandbox": true,
  "permissions": ["network:zenquotes.io"]
}
export default {
  activate(ctx) {
    ctx.commands.register({
      id: "demo.quote",
      title: "Demo: Quote of the Day",
      run: async () => {
        const res = await fetch("https://zenquotes.io/api/today");
        const [q] = await res.json();
        ctx.notify(`${q.q}: ${q.a}`);
      },
    });
  },
};