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
# 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 (refresh, selection, watch, navigation)
│ ├── mediators/ # Cross-cutting coordinators
│ └── emitter.ts # Event system (framework-free)
│
├── features/ # Feature modules
│ ├── tree-view/ # Sidebar tree navigation
│ │ └── view-model/ # MVVM presentation logic
│ ├── editor/ # Editor integration (hover, CodeLens)
│ └── preview/ # Webview preview panel
│ ├── 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
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:
import { useApplicationStore } from './application-store';
// In ViewModels or components
const model = useApplicationStore(state => state.calmModel);
const setModel = useApplicationStore(state => state.setCalmModel);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:
tree-data-provider.ts- VSCode TreeDataProviderview-model/tree-view-model.ts- Business logic (framework-free)
- 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-architectures/- Sample CALM files for testing
# 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:
interface ApplicationStore {
myNewState: string;
setMyNewState: (value: string) => void;
}
export const useApplicationStore = create<ApplicationStore>((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 shared- 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 --workspace calm-plugins/vscode # Creates .vsix file
# 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