Skip to main content
Welcome. This site supports keyboard navigation and screen readers. Press ? at any time for keyboard shortcuts. Press [ to focus the sidebar, ] to focus the content. High-contrast themes are available via the toolbar.
serard@dev00:~/cv

Part 13 — Commands micro-DSL: id, title, when-clauses, keybindings, palette

Commands are the action surface of VSCode. Anything the user can invoke — from the command palette, from a keybinding, from a CodeLens click, from a tree-view inline action, from a context menu — is a command. Commands are identified by stable string ids; they have human titles; they have optional when clauses that gate visibility against context keys. The Commands micro-DSL declares these per-DSL, generates the contributes.commands and contributes.keybindings entries, and registers the runtime handlers that the extension's entry point wires into VSCode's command registry.

Commands sit at a load-bearing position in the dependency graph. CodeLens (article 12) references command ids. Views (article 14) menus reference command ids. The Custom Editor host (article 21) registers commands that mutate the projection state. The Generator micro-DSL (article 19) exposes ide-dsl.regenerate as a command. None of these consumers own command implementations — they reference ids — and the Commands micro-DSL is the single place the implementations live.

Concern

The author wants to ship a "Run satisfying tests for this Feature" command. The command should appear in the command palette under a sensible category (Requirements: Run Satisfying Tests); it should have a default keybinding (Cmd+Shift+R T) when the focused editor is a .req.ts file; it should be invocable from a CodeLens above the Feature declaration; clicking should run the relevant test files and surface the results. Hand-writing the command requires populating package.json's contributes.commands, populating contributes.keybindings, registering the implementation in extension.ts, exposing the command id stably so other contributions reference it. The Commands micro-DSL collapses this into one @Command decorator.

The Surface

import { Command, Keybinding } from '@frenchexdev/ide-dsl-commands';
import { Concept } from '@frenchexdev/ide-dsl-kernel';

@Concept({ id: 'cmf.req.Feature' })
export class Feature {
  @Command({
    id: 'requirements.runSatisfyingTests',
    title: 'Run Satisfying Tests',
    category: 'Requirements',
    when: 'editorLangId == requirements',
  })
  @Keybinding({ key: 'cmd+shift+r t', mac: 'cmd+shift+r t', win: 'ctrl+shift+r t' })
  static async runSatisfyingTests(self: Feature, ctx) {
    const testFiles = await ctx.workspace.findReferences(self.id, '@FeatureTest');
    return ctx.tasks.runTestsAt(testFiles.map(r => r.uri));
  }

  @Command({
    id: 'requirements.regenerate',
    title: 'Regenerate Compliance Report',
    category: 'Requirements',
  })
  static async regenerate(_self: Feature, ctx) {
    return ctx.invoke('ide-dsl.regenerate', { generators: ['cmf.req.compliance'] });
  }
}

@Command declares: a stable id (kebab-cased, namespaced by DSL), a human title, an optional category (groups the palette entry under a header), an optional when clause (using VSCode's standard context-key expression language). @Keybinding is optional; when present, it generates one contributes.keybindings entry with platform-specific overrides.

The decorated method is the implementation. It receives the bound Concept instance (when the command is invoked from a context-aware surface like CodeLens) plus a kernel context exposing the workspace index, task runner, and ctx.invoke for chaining other commands. The static-method idiom keeps the implementation colocated with the metadata.

Kernel boundary

Reads:

  • The Concept on which the command is declared (for context binding when invoked from CodeLens or Views).
  • The workspace AST through the kernel context — for whatever the command's logic needs.

Writes:

  • Through PatchBus, when the command's implementation mutates the AST. (The regenerate example above does not — it invokes another command — but a requirements.markFeatureCompleted command would mutate.)

The kernel exposes a typed CommandRegistry containing every declared command id and its arity; CodeLens, Views, and other consumers query this registry at extraction time to validate id references at build time rather than fail silently at click time.

Emitted artefacts

// package.json contributions (merged)
{
  "contributes": {
    "commands": [
      {
        "command": "requirements.runSatisfyingTests",
        "title": "Run Satisfying Tests",
        "category": "Requirements"
      },
      {
        "command": "requirements.regenerate",
        "title": "Regenerate Compliance Report",
        "category": "Requirements"
      }
    ],
    "keybindings": [
      {
        "command": "requirements.runSatisfyingTests",
        "key": "cmd+shift+r t",
        "mac": "cmd+shift+r t",
        "win": "ctrl+shift+r t",
        "when": "editorLangId == requirements"
      }
    ],
    "menus": {
      "commandPalette": [
        {
          "command": "requirements.runSatisfyingTests",
          "when": "editorLangId == requirements"
        }
      ]
    }
  },
  "activationEvents": [
    "onCommand:requirements.runSatisfyingTests",
    "onCommand:requirements.regenerate"
  ]
}
// extension.ts (excerpt, generated)
import * as vscode from 'vscode';
import { commandRegistry } from './_registry.generated';

export function activate(ctx: vscode.ExtensionContext) {
  for (const [id, handler] of commandRegistry.entries()) {
    ctx.subscriptions.push(vscode.commands.registerCommand(id, handler));
  }
}

Activation events are derived from the command ids so the extension activates lazily on first command invocation.

Composition with peers

  • CodeLens (article 12) — references command ids; the CommandRegistry validates the references at build time.
  • Views (article 14) — context-menu and inline-action contributions reference command ids similarly.
  • Custom Editor host (article 21) — registers projection-specific commands (toggle projection, switch projection kind) through the same micro-DSL.
  • Generator (article 19) — exposes ide-dsl.regenerate as a command.

MPS aspect referent

MPS Actions aspect, plus the Intentions aspect for context-conditional surfacing. We adopt the per-Concept declarative shape and the keybinding metadata; we drop the MPS-specific intention condition language (we use VSCode's when clauses instead, because that is what the host platform speaks).

Boundary justification

Why not collapse Commands and CodeLens? Already addressed: Commands owns implementations; CodeLens owns inline display. Collapsing would force every command to declare display metadata.

Why not in the Extension host? Because commands are DSL-specificrequirements.runSatisfyingTests is a Requirements DSL command; workflow.advanceStage is a Workflow DSL command. The Extension host is a generic packager; pushing per-DSL commands into it would force the host to know every DSL's surface. Keeping commands per-DSL preserves the extension-host's neutrality.

Requirements

FEAT-MICRODSL-13 in assets/features.ts:

  • commandDeclarativeSurface — the Surface section shows @Command({ id, title, category, when }).
  • whenClauseSemanticsExplained — the Surface section names VSCode's context-key language; the Emitted artefacts section shows the when propagation into the command palette and keybindings.
  • keybindingDerivationStated@Keybinding is shown alongside @Command; the merge into contributes.keybindings is explicit in the generated JSON.
  • boundaryAgainstCodeLensJustified — the Boundary justification section names the implementation-vs-display split.

Article 14 picks up with Views — the workspace-level UI panel surface that also references commands.

⬇ Download