This guide helps AI assistants work efficiently with the CALM VSCode extension codebase.
- Language: TypeScript 5.8+
- Framework: VSCode Extension API 1.88+
- State Management: Zustand (Redux-like store)
- Build Tool: tsup (esbuild-based)
- Test Framework: Vitest
- Architecture Pattern: MVVM + Hexagonal + Mediator
- UI Components: Native VSCode API (TreeView, Webview, Commands)
IMPORTANT: Always run npm commands from the repository root using workspaces, not from within this package directory.
# Development (from repository root)
npm run build --workspace calm-plugins/vscode # Build extension
npm run watch --workspace calm-plugins/vscode # Watch mode (no auto-reload in VSCode)
npm test --workspace calm-plugins/vscode # Run Vitest tests
npm run lint --workspace calm-plugins/vscode # ESLint check
npm run lint-fix --workspace calm-plugins/vscode # Auto-fix linting issues
npm run package --workspace calm-plugins/vscode # Create .vsix package for distribution
# Convenience scripts (run the build:shared prerequisite for you)
npm run test:vscode # build:shared + test the extension
npm run package:vscode # build:shared + package the extension
# Testing Extension in VSCode
# 1. Open the repository root in VSCode (File → Open Folder)
# 2. Use the "calm-plugin: watch" task or run: npm run watch --workspace calm-plugins/vscode
# 3. Press F5 (or Run → Start Debugging) to launch Extension Development Host
# 4. In the new Extension Development Host window, open a CALM JSON file to activate extensionMVVM (Model-View-ViewModel)
View (VSCode UI) <--> ViewModel (Framework-free) <--> Model (Zustand Store)
Hexagonal Architecture (Ports & Adapters)
src/core/
├── ports/ # Interfaces (dependency inversion)
├── services/ # Core business logic
└── mediators/ # Cross-cutting orchestration
Mediator Pattern
- Coordinates between services without tight coupling
- Examples: RefreshService, SelectionService, WatchService
src/
├── extension.ts # VSCode entry point (activate/deactivate)
├── calm-extension-controller.ts # Main orchestrator (wires dependencies)
├── application-store.ts # Zustand global state
│
├── core/ # Framework-free business logic
│ ├── ports/ # Interfaces for dependency inversion
│ ├── services/ # Core services (model, config, navigation, diagnostics, calm-schema-registry, logging)
│ ├── mediators/ # Cross-cutting coordinators (refresh, selection, watch, store-reaction)
│ └── emitter.ts # Event system (framework-free)
│
├── features/ # Feature modules
│ ├── tree-view/ # Sidebar tree navigation
│ │ ├── tree-view-factory.ts # Wires view + view-model
│ │ ├── view/ # VSCode-specific view (tree-view.ts, tree-item.ts)
│ │ └── view-model/ # MVVM presentation logic (framework-free)
│ ├── editor/ # Editor integration (hover, CodeLens)
│ └── preview/ # Webview preview panel
│ ├── webview/ # Webview internals (mermaid-renderer, pan-zoom-manager,
│ │ # diagram-controls, panel.view)
│ ├── docify-tab/ # Documentation generation
│ ├── model-tab/ # Model data display
│ └── template-tab/ # Template processing & live mode
│
├── commands/ # VSCode command handlers
├── models/ # CALM model parsing & indexing
└── cli/ # CLI integration (deprecated, being replaced)
- Framework Isolation: ViewModels have NO
vscodeimports - Dependency Inversion: Core depends on ports, not VSCode
- Single Store: All state in
application-store.ts - Mediator Coordination: Services don't call each other directly
Store Location: src/application-store.ts
The store is created via the createApplicationStore() factory (using Zustand's
subscribeWithSelector middleware), not a module-level create() singleton. The
extension controller owns the instance and injects it where needed.
interface ApplicationStore {
calmModel: CalmModel | null;
selectedNode: string | null;
isLoading: boolean;
// ... other state
// Actions
setCalmModel: (model: CalmModel) => void;
setSelectedNode: (nodeId: string | null) => void;
// ... other actions
}Usage (illustrative — the real store is a per-instance object created by the factory):
import { createApplicationStore } from './application-store';
const store = createApplicationStore();
// Read with a selector
const model = store.getState().calmModel;
// React to changes (subscribeWithSelector)
store.subscribe(state => state.calmModel, model => { /* ... */ });ViewModel Example:
// src/features/tree-view/view-model/tree-view-model.ts
export class TreeViewModel {
// NO vscode imports!
constructor(
private store: ApplicationStore,
private emitter: Emitter
) {}
getTreeData(): TreeNode[] {
const model = this.store.getState().calmModel;
return this.transformToTree(model);
}
}View (VSCode Specific):
// src/features/tree-view/tree-data-provider.ts
export class CalmTreeDataProvider implements vscode.TreeDataProvider {
constructor(private viewModel: TreeViewModel) {}
getChildren(element?: TreeItem): TreeItem[] {
return this.viewModel.getTreeData().map(toTreeItem);
}
}Mediators coordinate between services:
// src/core/mediators/store-reaction-mediator.ts
export class StoreReactionMediator {
constructor(
private store: ApplicationStore,
private refreshService: RefreshService,
private selectionService: SelectionService
) {
// React to store changes
this.store.subscribe(
state => state.calmModel,
model => this.refreshService.refreshAll()
);
}
}- Purpose: Handles navigation between CALM documents via
detailed-architecturereferences. - Key Logic: Uses
DocumentLoaderfrom@finos/calm-sharedto resolve URLs/relative paths to local files based oncalm.urlMapping. - Integration: Called by
SelectionServicewhen a node with details is clicked.
- Purpose: Sidebar navigation of CALM model structure
- Location:
src/features/tree-view/ - Key Files:
view/tree-view.ts- VSCode TreeDataProvider implementationview/tree-item.ts- VSCode TreeItem wrappertree-view-factory.ts- Wires the view and view-model togetherview-model/tree-view-model.ts- Business logic (framework-free)
- Purpose: Real-time CALM document validation with Problems panel integration
- Location:
src/features/validation/ - Key Files:
validation-service.ts- Main service, handles document events and diagnosticsvalidation-service.spec.ts- Unit tests
- Behavior:
- Validates on document open, save, and editor activation
- Clears diagnostics when editor tab is closed
- Uses content-based detection (checks
$schemafield for known CALM schemas) - Produces precise line numbers for error positioning using shared enrichment logic
- Dependencies: Uses
runValidation,enrichWithDocumentPositionsfrom@finos/calm-shared
- Purpose: Multi-tab preview (Model, Docify, Template)
- Location:
src/features/preview/ - Tabs:
- Model Tab: Display CALM JSON in formatted view
- Docify Tab: Generate documentation websites
- Template Tab: Live template processing with Handlebars
- Hover Providers: Show info on hover
- CodeLens: Inline commands in editor
- Location:
src/features/editor/
*.spec.ts- Unit tests alongside sourcetest_fixtures/- Sample CALM files for testing (architecture/,navigable-architecture/)
# From repository root (preferred)
npm test --workspace calm-plugins/vscode # All tests
npm test --workspace calm-plugins/vscode -- --watch # Watch mode
npm test --workspace calm-plugins/vscode -- <file> # Specific test fileViewModels are framework-free, so they're easy to unit test:
import { TreeViewModel } from './tree-view-model';
describe('TreeViewModel', () => {
it('transforms model to tree', () => {
const store = createMockStore();
const vm = new TreeViewModel(store, mockEmitter);
const tree = vm.getTreeData();
expect(tree).toHaveLength(3);
});
});- Register in
package.json:
{
"contributes": {
"commands": [{
"command": "calm.myCommand",
"title": "CALM: My Command"
}]
}
}- Create handler in
src/commands/:
export function registerMyCommand(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('calm.myCommand', () => {
// Implementation
})
);
}- Register in
src/extension.ts:
import { registerMyCommand } from './commands/my-command';
export function activate(context: vscode.ExtensionContext) {
registerMyCommand(context);
}- Update
src/application-store.ts(add to the interface and thecreateApplicationStorefactory):
interface ApplicationStore {
myNewState: string;
setMyNewState: (value: string) => void;
}
export function createApplicationStore(): ApplicationStoreApi {
return createStore<ApplicationStore>()(
subscribeWithSelector((set) => ({
myNewState: '',
setMyNewState: (value) => set({ myNewState: value }),
}))
);
}- Create in appropriate feature folder (framework-free!):
// src/features/my-feature/view-model/my-view-model.ts
export class MyViewModel {
constructor(
private store: ApplicationStore,
private emitter: Emitter
) {}
// Methods that work with store, NO vscode imports
}- Create VSCode View:
// src/features/my-feature/my-view.ts
import * as vscode from 'vscode';
import { MyViewModel } from './view-model/my-view-model';
export class MyView {
constructor(private viewModel: MyViewModel) {}
// VSCode-specific implementation
}- Create tab component in
src/features/preview/my-tab/ - Update
src/features/preview/preview-panel.tsto include new tab - Add HTML template if needed
vscode-plugin depends on:
├── calm-models (via ../../calm-models)
├── calm-widgets (via ../../calm-widgets)
└── shared (via ../../shared)
Important: Build dependencies first:
# From repository root (always use workspaces)
npm run build:shared # Builds models, widgets, shared
# Or build individual packages:
npm run build --workspace calm-models
npm run build --workspace calm-widgets
npm run build --workspace sharedThe extension bundles CALM schemas, widgets, and template bundles into dist/ at build
time so validation, rendering, and templating work without network access or reaching into
sibling workspaces at runtime.
The postbuild step does two things:
copyfilescopies CALM schemas from the repo root:calm/release/**/meta/*→dist/calm/release/...calm/draft/**/meta/*→dist/calm/draft/...
scripts/copy-calm-assets.jscopies the remaining runtime assets:- widgets from
calm-widgets/dist/cli/widgets(falls back tocalm-widgets/src/widgets) →dist/widgets/ - template bundles from
shared/dist/template-bundles→dist/template-bundles/
- widgets from
Schemas are indexed by their $id field for lookup when validating documents.
CalmSchemaRegistry (src/core/services/calm-schema-registry.ts) manages schema discovery:
- Loads bundled schemas from
dist/calm/ - Loads additional schemas from folders configured in
calm.schemas.additionalFolders - Provides
isKnownCalmSchema(url)to check if a schema URL is available locally
When new CALM schema versions are released:
- Add the schema files to
calm/{release|draft}/{version}/meta/ - Rebuild the extension - schemas are automatically copied and indexed
- Importing vscode in ViewModels: ViewModels must be framework-free!
- Direct Service Calls: Use mediators for cross-cutting concerns
- Store Mutations: Always use store actions, never mutate directly
- Extension Not Activating: Check
activationEventsin package.json - Webview Not Updating: Remember to postMessage from webview to extension
- toCanonicalSchema adds undefined values: When using
toCanonicalSchema()from calm-models, ALL optional properties are added withundefinedvalues. Code checking for property existence must check for truthy values, not just key existence. See calm-widgets/AGENTS.md for details. - URL Mapping: To test multi-document navigation, you likely need to configure
calm.urlMappingin.vscode/settings.jsonto point to a mapping file (e.g.calm-mapping.json) in the workspace root.
- Open this folder in VSCode
- Set breakpoints in TypeScript source
- Press F5 (or Run → Start Debugging)
- Extension Development Host window opens
- Open a CALM file to trigger activation
- In Extension Development Host:
Ctrl+Shift+P - Run: "Developer: Open Webview Developer Tools"
- Use browser devtools to debug webview
package.json- Extension manifest, commands, viewstsconfig.json- TypeScript compiler optionstsup.config.ts- Build configurationvitest.config.mts- Test configurationeslint.config.mjs- Linting rules
# From repository root
npm run package:vscode # build:shared + creates .vsix file
# (or, if dependencies are already built: npm run package --workspace calm-plugins/vscode)
# Then publish to VS Code Marketplace via GitHub Actions- DEVELOPER.md - Detailed architecture guide with diagrams
- README.md - User-facing documentation
- VSCode Extension API - Official docs
- Root README - Monorepo overview