Skip to content

Concepts

Four ideas explain almost everything vsceasy does.

Each feature type has a directory. A file in it is the feature.

DirectoryAPIBecomes…
panels/definePanelwebview panel + auto <prefix>.open<Name> cmd
commands/defineCommandpalette command + keybindings
menus/defineMenuactivity-bar container + tree view
treeViews/defineTreeViewdata-driven view inside a menu container
subpanels/defineSubpanelinline webview section inside a menu
statusBars/defineStatusBarstatus bar item
jobs/defineJobscheduled / event-triggered task
completions/defineCompletionIntelliSense provider
inlineCompletions/defineInlineCompletionghost text at the cursor
hovers/defineHoverhover panel (markdown)
typingGuards/defineTypingGuardkeystroke / paste / delete interception
decorations/defineDecorationeditor overlays
terminals/defineTerminalcaptured exec + visible terminal

The last seven act on the editor itself — see Editor surface.

src/panels/dashboard.ts
import { definePanel } from '../shared/vsceasy';
export default definePanel({
title: 'Dashboard',
});

scripts/gen.ts scans those directories and writes two things:

  • src/extension/_registry.ts — a typed registry of everything on disk.
  • package.json#contributes — commands, keybindings, viewsContainers, views, all kept in sync with the files.

Run it with bun run gen. Generators run it for you after writing files.

gen owns commands, keybindings, viewsContainers and views. Anything else VS Code contributes — languages, grammars, snippets, themes, iconThemes, configuration — goes in an optional contributes.extra.json at the project root, which gen deep-merges in on every run. See contributes.extra.json.

flowchart LR
  DIRS["convention dirs"] --> GEN["gen.ts"]
  GEN --> REG["_registry.ts"]
  GEN --> CONTRIB["contributes"]

The webview talks to the extension through one typed interface.

src/shared/api.ts
export interface DashboardApi {
getStats(): Promise<{ total: number }>;
}
src/panels/dashboard.ts
export default definePanel<DashboardApi>({
title: 'Dashboard',
rpc: (vscode) => ({
async getStats() {
return { total: 42 };
},
}),
});
src/webview/panels/dashboard/App.tsx
import { connectWebview } from '../../../shared/vsceasy/client';
import type { DashboardApi } from '../../../shared/api';
const api = connectWebview<DashboardApi>();
const stats = await api.getStats(); // typed, no postMessage

See Typed RPC for the full story.

extension.ts is a one-liner. bootstrap(registry) registers everything from the generated registry on activate, so you rarely touch activation events.

src/extension/extension.ts
import { bootstrap } from '../shared/vsceasy';
import { registry } from './_registry';
export const activate = bootstrap(registry, { onActivate: [/* initDb, … */] });

onActivate hooks run once on activate — wire initDb(context), initSecrets(context), and similar there.