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 08 — Completion micro-DSL: triggers, item providers, context-aware items

Completion is dynamic suggestion. The user types a character, the editor sends textDocument/completion to the language server, the server inspects the position, computes a list of candidates, returns them. The list depends on the cursor's syntactic context (what came before, what comes after), on the workspace state (what symbols exist, what types they have), and sometimes on external data (a recent file, a recent edit, a pending refactor). Completion is therefore not declarative content — it is a function from cursor-context-plus-workspace-state to a list of items, and the Completion micro-DSL is the surface that lets a DSL author declare those functions per Concept without writing any LSP plumbing.

Concern

The user types @Satisfies( in a .req.ts file. The cursor is now inside a decorator argument list. The expected items are the names of Requirement subclasses declared anywhere in the workspace — BaseRequirement, IsoRequirement, RegulatoryRequirement. The completion list should narrow as the user types more characters; it should sort by relevance (recently used first, then alphabetical); it should include each requirement's documentation as the detail field; it should preview the import statement that will be added if the user selects an item from a different file.

Producing this behaviour from scratch requires writing a completion provider, registering it for the requirements language id, parsing the cursor's surrounding context to recognise the @Satisfies( shape, walking the workspace AST to find Requirement declarations, formatting each as a CompletionItem, handling the additionalTextEdits for the import statement, choosing the right kind so the editor renders the right icon. The Completion micro-DSL collapses all of this into one declarative @Completion decorator over a Concept method.

The Surface

import { Completion } from '@frenchexdev/ide-dsl-completion';
import { Concept, ChildLink, ReferenceLink, Property } from '@frenchexdev/ide-dsl-kernel';

@Concept({ id: 'cmf.req.Feature' })
export class Feature {
  @Property({ type: 'enum', values: ['Low', 'Medium', 'High', 'Critical'] })
  priority!: 'Low' | 'Medium' | 'High' | 'Critical';

  @ReferenceLink({ target: 'cmf.req.Requirement', card: '0..*' })
  @Completion({
    triggers: ['@Satisfies(', ','],
    kind: 'class',
    items: (ctx) => ctx.workspace.allConceptsAssignableTo('cmf.req.Requirement'),
    importsFor: (item) => ({ from: item.module, named: item.name }),
  })
  satisfies!: RequirementRef[];
}

@Completion over a @ReferenceLink field declares: when the user is at one of these triggers in this field's surface form, the candidate items are the result of this items function over the kernel context, and selection should auto-add the relevant import. The ctx argument exposes the kernel APIs the items function may consult: ctx.workspace for cross-file lookup, ctx.cursor for the position, ctx.document for the local file's AST, ctx.partialText for what the user has typed so far. Each is typed; each is read-only; the function returns synchronously or asynchronously a list of items.

The kind field maps to LSP's CompletionItemKind enumeration ('class' corresponds to the class icon in IntelliSense). The importsFor field is optional; when present, selecting an item produces additional text edits that insert the import at the top of the file if it is not already there.

A second form is available for @Property fields with constraints:

@Property({ type: 'enum', values: ['Low', 'Medium', 'High', 'Critical'] })
@Completion({ triggers: ["'", '"'], kind: 'enumMember' })
priority!: 'Low' | 'Medium' | 'High' | 'Critical';

The micro-DSL derives the items automatically from @Property({ values }). No items function is needed; the constraint is the source.

Kernel boundary

Reads:

  • The Structure Model — to enumerate Concepts assignable to a target type, to read enum values, to walk inheritance.
  • The current document AST — to compute the cursor's syntactic context.
  • The workspace AST through the kernel's per-document index — to enumerate cross-file symbols.

Writes:

  • None. Completion is read-only against the kernel.

The boundary is purely consumptive. A completion provider that needs to mutate the AST (auto-insert a placeholder, schedule a refactor) is not a completion provider — it is a code action, owned by the Diagnostics or Refactoring micro-DSL.

Emitted artefacts

The Completion micro-DSL contributes:

// LSP server registration (generated, in server/handlers/completion.generated.ts)
import { CompletionItem, CompletionItemKind, TextDocumentPositionParams } from 'vscode-languageserver';
import { kernel } from '@frenchexdev/ide-dsl-kernel';
import { completionRegistry } from './_registry.generated';

export async function handleCompletion(params: TextDocumentPositionParams): Promise<CompletionItem[]> {
  const ctx = kernel.contextFor(params);
  const candidates = completionRegistry.matching(ctx);
  const items = await Promise.all(candidates.map(c => c.items(ctx)));
  return items.flat().map(toLspCompletionItem);
}

Plus a manifest contribution for trigger characters:

// extension capabilities (generated, embedded in server/main.generated.ts)
{
  "completionProvider": {
    "triggerCharacters": ["@", "(", ",", "'", "\"", "."]
  }
}

Both files carry Banners. The trigger character list is the union of every @Completion({ triggers }) declaration in the workspace; the LSP host (article 20) merges the contributions.

Composition with peers

  • Snippets (article 09) — coexist in the same dropdown when both match. The merge order is fixed by the Completion micro-DSL: snippets that match the prefix appear at the top with a snippet icon; dynamic completion items follow, sorted by the sortText field.
  • Hover (article 10) — uses the same workspace index Completion uses; the index is built once by the kernel and consulted by both. No coupling between Completion and Hover beyond the shared kernel API.
  • Diagnostics (article 11) — does not interact with Completion directly; both are LSP-routed but address orthogonal concerns.
  • LSP host (article 20) — routes textDocument/completion to the merged provider chain.

MPS aspect referent

MPS Editor aspect, code completion sub-aspect. MPS attaches completion menus to cells in the projectional editor; the menu's items are computed by the Concept's behavior methods. We adopt the per-Concept declarative shape and the typed item-computation function; we drop the projectional cell anchor (we use trigger characters instead, because we render text not cells). The mass-production property — one declaration produces completion items for any Concept assignable to the target — survives the translation directly.

Boundary justification

Why not collapse Completion into Snippets? Snippets are static templates with parameter placeholders; the entire body is known at extraction time, no LSP round-trip is needed, and they can be served from contributes.snippets without any server. Completion items are computed at request time against the live workspace; they require an LSP round-trip for each request. The two pay different costs and benefit from different infrastructures. Forcing them into one shape would either pay the LSP cost for snippets (slower) or constrain completion to static data (less useful). Two micro-DSLs, two costs, two surfaces.

Why not in the LSP host? Because the host's job is routing, not authoring. The host knows that a textDocument/completion request should be dispatched to "the Completion micro-DSL's contribution"; it does not know what items to return for any given context. The micro-DSL owns that knowledge; the host owns the dispatch.

Requirements

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

  • triggerCharactersDeclarativeSurfaceShown — the Surface section shows @Completion({ triggers }) over both @ReferenceLink and @Property fields.
  • contextResolutionFromKernelExplained — the Surface section shows the ctx parameter; the Kernel boundary section enumerates the read APIs.
  • performanceConstraintsStated — the article opens with the sub-100ms median target; the LSP host article (20) revisits the per-handler latency budget.
  • boundaryAgainstSnippetsJustified — the Boundary justification section names the static-vs-dynamic and cost-of-LSP-round-trip arguments.

Article 09 picks up with Snippets, the static counterpart.

⬇ Download