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 09 — Snippets micro-DSL: prefix, body, scope, parametric placeholders

The Snippets micro-DSL is the static counterpart of Completion (article 08). Where Completion produces dynamic items computed at request time, Snippets produces parametric templates known at build time. The user types a prefix, the editor offers an expansion, the expansion drops into the buffer with placeholders the user can tab through. No LSP round-trip is involved; VSCode loads snippet definitions from a JSON file declared in contributes.snippets and serves them locally.

The boundary against Completion is sharp by design: snippets are cheap (no per-request cost), static (the body is known in advance), and scoped per language (one declaration per language id). Completion items are expensive (per-request server round-trip), dynamic (computed against the live workspace), and contextual (computed at the cursor with kernel context). A DSL author uses snippets for boilerplate (the same shape every time, just with the user's names plugged in) and Completion for symbols (the candidate set depends on what exists in the workspace).

Concern

The user is writing a new requirements file. They want to declare a new Feature. The skeleton is mechanical: an @Satisfies(...) line, a class declaration extending Feature, a typed id, a typed title, a typed priority, an abstract acceptance criterion method. Typing this by hand every time is slow; copy-pasting from another file invites stale ids; building a complete Feature skeleton with placeholders the user fills as they tab through is the right ergonomic. VSCode's snippet system supports this directly; the Snippets micro-DSL produces the JSON.

The Surface

import { Snippet } from '@frenchexdev/ide-dsl-snippets';
import { Concept } from '@frenchexdev/ide-dsl-kernel';

@Concept({ id: 'cmf.req.Feature' })
export class Feature {
  @Snippet({
    prefix: 'feat',
    description: 'New Feature skeleton satisfying one Requirement',
    body: [
      "@Satisfies(${1:RequirementClass})",
      "export abstract class ${2:NameFeature} extends Feature {",
      "  readonly id = '${3:FEAT-ID}';",
      "  readonly title = '${4:Human-readable title}';",
      "  readonly priority = Priority.${5|Low,Medium,High,Critical|};",
      "",
      "  abstract ${6:acName}(): ACResult;",
      "}",
    ],
  })
  static readonly snippetMarker = Symbol();
}

@Concept({ id: 'cmf.req.AcceptanceCriterion' })
export class AcceptanceCriterion {
  @Snippet({
    prefix: 'ac',
    description: 'Add one acceptance criterion method to the current Feature',
    body: 'abstract ${1:acName}(): ACResult;',
  })
  static readonly snippetMarker = Symbol();
}

@Snippet declares: a prefix (what the user types to trigger the snippet), an optional description, a body (an array of strings — one element per line — using VSCode's snippet placeholder grammar). The placeholder grammar is VSCode-standard: ${1:default} is a tab stop with default text, ${1|opt1,opt2,opt3|} is a choice tab stop with a dropdown, $0 is the final cursor position. The micro-DSL's surface does not invent a new grammar; it forwards VSCode's own.

The micro-DSL is decorator-only — there is no separate API. Each @Snippet declaration is colocated on a Concept, on a static class member that exists solely as a marker (the snippetMarker = Symbol() idiom keeps the class non-empty for tooling that warns about empty bodies). The Concept's owning language id is consulted to scope the snippet.

Kernel boundary

Reads:

  • The Concept on which the snippet is declared (to discover the owning language id via the Concept's package).
  • The LanguageRegistry from article 06 (to validate the scope binding).

Writes:

  • None.

The boundary is one of the simplest in the suite. The Snippets micro-DSL takes one piece of metadata (where to scope the snippet) and pairs it with the declaration's body verbatim.

Emitted artefacts

// snippets/requirements.code-snippets (generated)
{
  "Feature skeleton": {
    "prefix": "feat",
    "description": "New Feature skeleton satisfying one Requirement",
    "body": [
      "@Satisfies(${1:RequirementClass})",
      "export abstract class ${2:NameFeature} extends Feature {",
      "  readonly id = '${3:FEAT-ID}';",
      "  readonly title = '${4:Human-readable title}';",
      "  readonly priority = Priority.${5|Low,Medium,High,Critical|};",
      "",
      "  abstract ${6:acName}(): ACResult;",
      "}"
    ]
  },
  "AC method": {
    "prefix": "ac",
    "description": "Add one acceptance criterion method to the current Feature",
    "body": "abstract ${1:acName}(): ACResult;"
  }
}
// package.json contributions (merged)
{
  "contributes": {
    "snippets": [
      {
        "language": "requirements",
        "path": "./snippets/requirements.code-snippets"
      }
    ]
  }
}

The snippets file carries a Banner; the package.json fragment is merged by the Extension host. Multiple Concepts contributing snippets to the same language id produce one merged snippet file per language; the merge is deterministic (alphabetical by snippet name) so regeneration is reproducible.

Composition with peers

  • Language (article 06) — supplies the language id that scopes the snippet file.
  • Completion (article 08) — coexists in the same dropdown when both have items matching the prefix; snippets appear with a snippet icon, completion items follow.
  • No other coupling. Snippets do not interact with Hover, Diagnostics, CodeLens, Refactoring, or any other micro-DSL.

The micro-DSL is one of the most isolated in the suite — a desirable property for a static contribution.

MPS aspect referent

MPS Editor aspect, templates with parameters. MPS authors define cell-based templates whose parameters are filled either at expansion or by query against the AST. We adopt the placeholder grammar (with the explicit non-claim that VSCode's grammar is sufficient for our consumers; we do not need MPS's query parameters because dynamic completion handles those cases). The mass-production angle, again: each Concept can declare snippets without writing JSON or learning the snippet file format.

Boundary justification

Why not collapse Snippets into Completion? Already addressed in article 08's Boundary justification: cost asymmetry (no LSP round-trip vs. one round-trip per request), data shape (static body vs. dynamic items), infrastructure (contributes.snippets vs. server provider). Two micro-DSLs preserve both ergonomics.

Why not derive snippet bodies from the Concept's structure automatically? Tempting — the kernel knows every property of Feature and could generate a "all-fields-populated" snippet. The reason to keep snippets author-provided: the choice of which fields go in the snippet, what placeholder text to use, what default values to suggest is a user-facing curation decision. Auto-generated snippets either include too much (every field always) or too little (only required fields), and never match the author's idiom for the language. Manual @Snippet declarations let the author shape the ergonomics.

Requirements

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

  • snippetSurfaceShownWithPlaceholderGrammar — the Surface section shows @Snippet with ${1:default}, ${1|choices|}, multi-line body.
  • scopeBindingFromKernelLanguageExplained — the Kernel boundary section names the language-id derivation through the Concept's package.
  • emittedSnippetsJsonShape — the Emitted artefacts section shows the .code-snippets file and the contributes.snippets block.
  • boundaryAgainstCompletionJustified — the Boundary justification section names the cost / shape / infrastructure asymmetry.

Article 10 picks up with Hover, the first cross-file LSP-routed micro-DSL.

⬇ Download