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 14 — Views micro-DSL: tree views, locations, view containers

The Views micro-DSL declares workspace-level UI panels — the kind of tree that lives in the explorer sidebar or in a custom activity-bar view container. A Requirements DSL gets a "Requirements" tree showing every Epic with its Features and Stories underneath. A Workflow DSL gets a "Workflows" tree showing every workflow with its stages. The micro-DSL produces the contributes.views and contributes.viewsContainers entries plus the registered TreeDataProvider implementations; refreshes are driven by the kernel EditLog so the tree updates whenever a relevant PatchBus operation happens.

Views and the Custom Editor host (article 21) are the two UI-surface micro-DSLs. The boundary, named in Boundary justification below: Views shows the workspace; the Custom Editor host shows the document. Both render trees; both react to EditLog events; their scopes do not overlap.

Concern

The author wants a "Requirements" tree in the activity bar. The tree's roots are Epics. Each Epic shows Features as children; each Feature shows Acceptance Criteria; each AC shows whether it is satisfied. Inline actions on a Feature: "Add AC", "Run satisfying tests". The tree should refresh when the user edits any .req.ts file — adding a Feature should add a tree node without manual refresh.

Hand-writing the tree requires implementing TreeDataProvider, walking the workspace, building a typed tree of items, exposing the inline actions, wiring onDidChangeTreeData to a workspace-watcher event source. The Views micro-DSL collapses these into @View plus @TreeProvider decorators with kernel-aware defaults.

The Surface

import { View, TreeProvider, TreeItem, InlineAction } from '@frenchexdev/ide-dsl-views';
import { Concept, ChildLink } from '@frenchexdev/ide-dsl-kernel';

@View({
  id: 'requirements.tree',
  name: 'Requirements',
  location: { container: 'requirements', icon: 'lucide:layers-3' },
})
export class RequirementsTree {
  @TreeProvider({ rootConcept: 'cmf.req.Epic' })
  static rootProvider(ctx) {
    return ctx.workspace.allConceptsOfType('cmf.req.Epic');
  }

  @TreeProvider({ childrenOf: 'cmf.req.Epic', via: 'features' })
  static featuresOfEpic(epic) {
    return epic.features;
  }

  @TreeProvider({ childrenOf: 'cmf.req.Feature', via: 'acceptance' })
  static acsOfFeature(feature) {
    return feature.acceptance;
  }

  @TreeItem({ for: 'cmf.req.Feature' })
  static itemForFeature(feature) {
    return {
      label: `${feature.id}${feature.title}`,
      icon: 'lucide:flag',
      tooltip: feature.title,
      contextValue: 'requirements.feature',
    };
  }

  @InlineAction({ for: 'cmf.req.Feature', command: 'requirements.runSatisfyingTests', icon: 'lucide:play' })
  static runTestsAction = Symbol();
}

@View declares the panel's id, name, and location (which container, which icon). @TreeProvider declares hierarchy: a root provider returns the top-level items; per-Concept child providers walk @ChildLink declarations. @TreeItem customises the rendering of each item (label, icon, tooltip). @InlineAction adds an action button to a tree item, referencing a command id from the Commands micro-DSL.

The micro-DSL is heavily kernel-aware: every @TreeProvider({ childrenOf: 'X', via: 'linkName' }) consults the Structure Model to validate that X has a @ChildLink named linkName and uses the kernel's typed AST navigation to walk it. Authors who declare a non-existent link receive a build-time error.

Kernel boundary

Reads:

  • The Structure Model — for @ChildLink walks and validation.
  • The workspace AST — for tree population.
  • The LanguageRegistry — for filtering items by language.
  • The CommandRegistry — for inline action references.

Writes:

  • None directly. Inline actions invoke commands; the commands mutate through PatchBus.

The kernel exposes a walkChildLinks helper used by both Views and Symbols (article 16); both micro-DSLs walk the same shape but for different purposes (Views for the visible tree, Symbols for the searchable index).

The EditLog subscription is straightforward: every insertChild, removeChild, or setProperty on a relevant property triggers onDidChangeTreeData for the affected subtree. The Views micro-DSL aggregates EditLog deltas in 100ms windows to avoid thrashing the tree on large refactors.

Emitted artefacts

// package.json contributions (merged)
{
  "contributes": {
    "viewsContainers": {
      "activitybar": [
        {
          "id": "requirements",
          "title": "Requirements",
          "icon": "$(layers)"
        }
      ]
    },
    "views": {
      "requirements": [
        {
          "id": "requirements.tree",
          "name": "Requirements",
          "icon": "$(layers)"
        }
      ]
    },
    "menus": {
      "view/item/context": [
        {
          "command": "requirements.runSatisfyingTests",
          "when": "view == requirements.tree && viewItem == requirements.feature",
          "group": "inline"
        }
      ]
    }
  }
}
// extension.ts (excerpt, generated)
import * as vscode from 'vscode';
import { viewProviderRegistry, kernel } from './_registry.generated';

export function activate(ctx: vscode.ExtensionContext) {
  for (const [id, providerFactory] of viewProviderRegistry.entries()) {
    const provider = providerFactory(kernel);
    ctx.subscriptions.push(vscode.window.registerTreeDataProvider(id, provider));
    kernel.onEditLogAppend(() => provider.refresh());
  }
}

Composition with peers

  • Commands (article 13) — every inline action references a command id.
  • Symbols (article 16) — both walk Concept hierarchy; Symbols is for searchable navigation, Views is for browsable navigation.
  • Custom Editor host (article 21) — orthogonal scope (workspace vs. document).
  • Extension host — registers the TreeDataProviders.

MPS aspect referent

MPS' Logical View and the Project Tree in the MPS workbench. MPS exposes a project-wide tree of language modules, models, roots, and per-Concept structure. We translate the same idea to a VSCode activity-bar tree: the tree is browsable, hierarchical, refreshes on workspace changes. We do not adopt MPS' multi-perspective tree (Logical / Physical / Inheritance) — VSCode does not have the chrome for it — and instead allow multiple @View declarations to coexist, each with its own perspective.

Boundary justification

Why not in the Custom Editor host? Scope. Views aggregates state from the entire workspace; the Custom Editor host renders one document at a time. A user opens a Custom Editor on feature-checkout.req.ts and sees that file's projections; they consult the Views tree to see every requirements file's structure. Same data, different scopes, different micro-DSLs.

Why not in Symbols? Both walk hierarchy. The distinction: Symbols answers structural-navigation queries against the LSP (workspace/symbol, textDocument/documentSymbol) and is consumed primarily through the editor's "Go to symbol" UI; Views provides a persistent panel the user can browse. Symbols is for the keyboard-driven user; Views is for the browse-driven user.

Requirements

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

  • treeProviderDeclarativeSurface — the Surface section shows @TreeProvider with rootConcept, childrenOf, via.
  • viewLocationContributionExplained — the Surface and Emitted artefacts sections show the viewsContainers and views blocks.
  • refreshSemanticsFromKernelEditLogStated — the Kernel boundary section names the EditLog subscription and the 100ms aggregation window.
  • boundaryAgainstCustomEditorJustified — the Boundary justification section names the workspace-vs-document scope argument.

Article 15 picks up with Formatter, the deterministic-pretty-print micro-DSL.

⬇ Download