Contributing to ZMK
July 8, 2026 · View on GitHub
Thank you for your interest in contributing to ZMK (Zodiac Make)! This document provides information about the extension's architecture, development setup, and guidelines for contributors.
Development Setup
Prerequisites
- Node.js (v24 or later)
- npm
- VS Code
Building from Source
# Clone the repository
git clone <repository-url>
cd zmk
# Install dependencies
npm install
# Build the extension
npm run build
# Watch mode for development (auto-rebuild on changes)
npm run watch
# Package extension as VSIX
npm run package
Running and Debugging
- Open the project in VS Code
- Press F5 to launch the Extension Development Host
- Open a Valhalla workspace in the new window
- Set breakpoints in the source code as needed
Source of information
Extension relies on the following sources of information:
compile_commands.json- generated by GN build system, contains compiler commands for each source fileproject.json- generated by GN build system, contains project metadata, targets, and dependenciesargs.gn- GN build arguments, used to determine toolchain and cross-compilation settings.ninja_deps- Ninja build dependency files, used to track file dependencies and rebuild triggers
Architecture
The extension is built using a service-oriented architecture with dependency injection via a custom ServiceContainer. This design provides modularity, testability, and clear separation of concerns.
Core Components
Service Container
The ServiceContainer class (src/services/ServiceContainer.ts) provides a lightweight dependency injection system that manages service lifecycle and dependencies.
Features:
- Instance registration: Pre-created objects registered directly
- Factory registration: Lazy-initialized services created on first access
- Dependency resolution: Automatic resolution of service dependencies
- Singleton pattern: Services are created once and reused
Usage:
// Register a service
container.registerInstance(ISettingsService, settingsService);
container.registerFactory(IBuilderService, () => new BuilderService(container));
// Retrieve a service
const builder = container.get(IBuilderService);
Main Services
The extension is composed of several key services, each with a specific responsibility:
BuilderService
Location: src/services/impl/BuilderService.ts
Responsibilities:
- Executes
gnbbuild commands - Manages build process lifecycle
- Handles build output streaming
- Emits build status events
Key Methods:
build(target?, flags?): Execute build commandcleanBuild(target?, flags?): Clean and rebuilddeepCleanBuild(target?, flags?): Remove build directory and rebuild
ProjectInfoService
Location: src/services/impl/ProjectInfoService.ts
Responsibilities:
- Parses GN's
project.jsonfile - Provides project structure information
- Caches project metadata with mtime-based invalidation
- Maps targets to source files
Key Methods:
getProjectInfo(): Get parsed project informationgetTargets(): Get list of build targetsgetTargetInfo(targetName): Get details for specific target
SettingsService
Location: src/services/impl/SettingsService.ts
Responsibilities:
- Manages extension settings
- Provides access to workspace configuration
- Resolves setting values with variable substitution
- Monitors setting changes
Key Methods:
get<T>(key): Get setting valueset(key, value): Update setting valuegetRootDir(): Get Valhalla root directorygetBuildDir(): Get build output directory
StatusService
Location: src/services/impl/StatusService.ts
Responsibilities:
- Updates VS Code status bar
- Shows current build configuration
- Displays build progress
- Provides quick access to commands
BuildStatusService
Location: src/services/impl/BuildStatusService.ts
Responsibilities:
- Tracks build state (idle, building, success, failure)
- Notifies listeners of build events
- Manages build status transitions
UIService
Location: src/services/impl/UIService.ts
Responsibilities:
- Handles user interactions
- Shows dialogs and prompts
- Manages user configuration flow
ValhallaCppToolsProviderService
Location: src/services/impl/ValhallaCppToolsProviderService.ts
Responsibilities:
- Integrates with VS Code C++ extension
- Implements
CustomConfigurationProviderinterface - Provides IntelliSense configuration per file
- Updates configurations after builds
ConfigTreeProvider
Location: src/services/impl/ConfigTreeDataProvider.ts
Responsibilities:
- Provides tree view for build configurations
- Scans
configs/*.yamlfiles - Highlights current configuration
- Handles configuration selection
TargetTreeProvider
Location: src/services/impl/TargetTreeProvider.ts
Responsibilities:
- Provides tree view for build targets
- Reads targets from
project.json - Organizes targets by GN path hierarchy
- Indicates default target
SourceFileConfigurationItemTreeProvider
Location: src/services/impl/SourceFileConfigurationItemTreeProvider.ts
Responsibilities:
- Shows IntelliSense settings for current file
- Displays include paths (list or tree view)
- Shows preprocessor defines
- Shows compiler settings
ValhallaTaskProvider
Location: src/components/tasks.ts
Responsibilities:
- Provides custom tasks of type
gnb - Creates automatic build tasks
- Monitors task execution
- Updates build status after task completion
Key Data Structures
CompileCommands
Location: src/components/CompileCommands.ts
Parses and caches compile_commands.json to provide source file configuration.
Key Features:
- Extracts include paths from compiler flags (
-I,-isystem, etc.) - Parses preprocessor defines (
-D) - Determines C++ standard from
-std=flag - Infers IntelliSense mode from compiler path
- Supports custom toolchain configurations via
zmk.toolchainsettings - Caches parsed data with mtime-based invalidation
Key Methods:
getConfiguration(sourceFile): Get IntelliSense config for a filerefresh(): Reload compile commands from filehasFile(sourceFile): Check if file is in compile commands
Configuration Resolution:
- Parse compiler command line
- Extract flags and arguments
- Apply toolchain-specific overrides
- Merge with user settings
- Return complete configuration
ProjectInfo
Location: src/components/ProjectInfo.ts
Reads and parses GN's project.json file containing build metadata.
Key Features:
- Maps GN targets to source files
- Resolves target dependencies
- Provides configuration data (defines, include paths, compiler flags)
- Caches parsed data with mtime-based invalidation
- Supports lazy loading of target information
Data Structure:
{
targets: {
[targetName: string]: {
type: string; // executable, shared_library, source_set, etc.
sources: string[];
deps: string[];
configs: string[];
defines: string[];
include_dirs: string[];
}
}
}
ArgsFile
Location: src/components/ArgsFile.ts
Manages GN build arguments from args.gn files.
Key Features:
- Reads
args.gnfiles from build output - Parses toolchain configuration
- Supports pattern-based toolchain selection
- Extracts cross-compilation settings
Parsed Information:
cross_os: Target operating systemcross_cpu: Target CPU architecturecross_abi: Target ABI- Other GN arguments
Implementation Details
Extension Activation Flow
When the extension activates:
-
Service Registration
- All services are registered in the
ServiceContainer - Dependencies between services are established
- All services are registered in the
-
Valhalla Detection
- Searches workspace folders for Valhalla root
- Looks for
gnborgnbcscript - Verifies presence of
configs/directory
-
Initial Build
- Runs minimal build (
emptytarget) - Generates
compile_commands.json - Generates
project.json
- Runs minimal build (
-
Provider Registration
- Registers custom configuration provider with C++ extension (if enabled)
- Registers task provider for
gnbtasks
-
View Registration
- Creates tree views for configurations, targets, and source file settings
- Sets up view event handlers
-
Command Registration
- Registers all extension commands
- Sets up command handlers
Build Process
The build process follows these steps:
-
Command Construction
BuilderServiceconstructsgnbcommand- Adds configuration and target arguments
- Applies additional flags from settings or task definition
-
Process Execution
- Spawns child process with build command
- Sets working directory and environment variables
-
Output Handling
- Streams stdout/stderr to dedicated output channel
- Parses output for errors and warnings
- Updates problem matchers
-
Status Updates
- Updates status bar with build progress
- Emits build events to listeners
- Tracks build state transitions
-
Metadata Refresh
- After successful build, reloads
compile_commands.json - Reloads
project.json - Invalidates caches
- After successful build, reloads
-
IntelliSense Update
- Notifies C++ extension of configuration changes
- Triggers IntelliSense refresh for open files
IntelliSense Configuration Flow
When a C/C++ file is opened or configuration is requested:
-
File Open Event
- VS Code C++ extension calls
CustomConfigurationProvider.canProvideConfiguration() - Extension checks if file exists in
compile_commands.json
- VS Code C++ extension calls
-
Configuration Request
- C++ extension calls
provideConfigurations([files]) - Extension looks up each file in compile commands cache
- C++ extension calls
-
Command Parsing
- Parses compiler command line
- Extracts include paths using regex patterns:
-I<path>- Include directory-isystem <path>- System include-iquote <path>- Quote include
- Extracts defines:
-D<name>[=<value>] - Extracts C++ standard:
-std=c++XX - Identifies compiler path from command
-
Toolchain Selection
- Reads
args.gnto determine toolchain - Matches against
zmk.toolchainpatterns - Applies toolchain-specific settings
- Reads
-
Configuration Merge
- Starts with settings from compile commands
- Applies toolchain configuration overrides
- Merges user settings (
zmk.includeDirs,zmk.defines, etc.) - Returns final configuration
-
Cache Management
- Configurations are cached per file
- Cache invalidated when compile commands file changes
- Cache invalidated when settings change
Project Information Management
Project metadata is managed as follows:
-
project.json Parsing
- Reads JSON file generated by GN
- Parses target definitions
- Extracts target metadata
-
Target Graph
- Builds dependency graph from target deps
- Resolves transitive dependencies
- Supports circular dependency detection
-
Source Mapping
- Maps source files to containing targets
- Handles source files in multiple targets
- Supports generated source files
-
Configuration Extraction
- Extracts per-target settings
- Resolves config inheritance
- Merges target configs
-
Lazy Loading
- Parses target information on-demand
- Caches parsed targets
- Minimizes memory usage for large projects
-
Cache Invalidation
- Monitors
project.jsonmodification time - Reloads when file changes
- Clears dependent caches
- Monitors
Tree View Implementation
Tree views are implemented using VS Code's TreeDataProvider interface:
Configuration Tree
-
Data Source
- Scans
configs/*.yamlfiles - Parses configuration names from filenames
- Scans
-
Tree Structure
- Flat list of configurations
- Current configuration highlighted
-
Refresh Logic
- Manual refresh via command
- Automatic refresh after configuration change
-
User Actions
- Click to select configuration
- Context menu for additional actions
Target Tree
-
Data Source
- Reads targets from
project.json - Filters and sorts target list
- Reads targets from
-
Tree Structure
- Hierarchical organization by GN path
- Nodes for directories and targets
- Shows target type icons
-
Refresh Logic
- Refreshes after build completion
- Manual refresh via command
-
User Actions
- Click to set default target
- Context menu to build specific target
Source File Configuration Tree
-
Data Source
- Reads configuration for active editor file
- Parses include paths and defines
-
Tree Structure
- Root nodes: Includes, Defines, Compiler
- Child nodes for individual items
- Supports list or tree view for includes
-
Refresh Logic
- Updates when active editor changes
- Refreshes after build
-
User Actions
- Toggle between list and tree view
- Copy values to clipboard
Utility Features
Copyright Header Management
The zmk.updateCopyright command:
-
Language Detection
- Determines file language from extension
- Currently supports C/C++ (
//comments)
-
Existing Header Search
- Searches file start for copyright comment
- Detects multi-line comment blocks
-
Template Processing
- Uses template from
zmk.copyrightCommentsetting - Substitutes placeholders:
${developer}: Fromzmk.developersetting${year}: Current year${date}: Current date (YYYY-MM-DD)
- Uses template from
-
Header Insertion
- Inserts at file start if no header exists
- Replaces existing header if found
Bundle Include Path Automation
The zmk.updateBundlesInclude command:
-
Bundle Directory Scan
- Scans
zmk.bundleDirfor subdirectories - Identifies bundle directories
- Scans
-
Bundle Filtering
- Filters out excluded bundles from
zmk.excludeBundles - Checks for
include/subdirectory
- Filters out excluded bundles from
-
Path Generation
- Generates include paths using
${env:zmk.bundleDir}prefix - Creates glob pattern for all bundles
- Generates include paths using
-
Configuration Update
- Reads
c_cpp_properties.json - Removes old bundle paths
- Adds new bundle paths
- Preserves other include paths
- Reads
-
User Confirmation
- Shows diff preview
- Prompts for confirmation
- Writes updated configuration
Project Structure
zmk/
├── src/
│ ├── extension.ts # Extension entry point
│ ├── components/ # Utility components
│ │ ├── ArgsFile.ts # GN args parser
│ │ ├── CompileCommands.ts # compile_commands.json parser
│ │ ├── constants.ts # Constants and enums
│ │ ├── Interactions.ts # User interaction helpers
│ │ ├── LazyCache.ts # Caching utility
│ │ ├── parseTarget.ts # Target name parser
│ │ ├── ProjectInfo.ts # project.json parser
│ │ ├── promise.ts # Promise utilities
│ │ ├── SourceFileConfiguration.ts # IntelliSense config types
│ │ ├── tasks.ts # Task provider
│ │ └── utils.ts # General utilities
│ ├── services/ # Service interfaces
│ │ ├── AppServices.ts # Service type definitions
│ │ ├── IBuilderService.ts
│ │ ├── IBuildStatusService.ts
│ │ ├── IConfigTreeProvider.ts
│ │ ├── IProjectInfoService.ts
│ │ ├── ISettingsService.ts
│ │ ├── ISourceFileConfigurationItemTreeProvider.ts
│ │ ├── IStatusService.ts
│ │ ├── ITargetTreeProvider.ts
│ │ ├── IUIService.ts
│ │ ├── IValhallaCppTools.ts
│ │ ├── IValhallaTaskProvider.ts
│ │ ├── IVirtualDocumentProvider.ts
│ │ ├── ServiceContainer.ts # DI container
│ │ └── impl/ # Service implementations
│ │ ├── BuilderService.ts
│ │ ├── BuildStatusService.ts
│ │ ├── ConfigTreeDataProvider.ts
│ │ ├── ProjectInfoService.ts
│ │ ├── SettingsService.ts
│ │ ├── SourceFileConfigurationItemTreeProvider.ts
│ │ ├── StatusService.ts
│ │ ├── TargetTreeProvider.ts
│ │ ├── UIService.ts
│ │ ├── ValhallaCppToolsProviderService.ts
│ │ └── VirtualDocumentProviderService.ts
│ └── test/
│ └── extension.test.ts # Extension tests
├── config-examples/ # Configuration examples
│ ├── appcloud/ # AppCloud config
│ └── zebra/ # Zebra config
├── images/ # Extension images
├── build.js # Build script
├── eslint.config.mts # ESLint configuration
├── package.json # Extension manifest
├── tsconfig.json # TypeScript configuration
├── CHANGELOG.md # Release notes
├── README.md # User documentation
├── CONTRIBUTING.md # This file
└── LICENSE # License file
Development Workflow
Making Changes
- Create a branch for your changes
- Make your changes in the
src/directory - Test your changes using the Extension Development Host (F5)
- Build the extension with
npm run build - Run linting with
npm run lint - Commit your changes with clear commit messages
Testing
- Manual testing: Use F5 to launch Extension Development Host
- Unit tests: Run
npm test(tests insrc/test/) - Integration testing: Test with real Valhalla workspace
Code Style
- Follow TypeScript best practices
- Use meaningful variable and function names
- Add comments for complex logic
- Keep functions focused and small
- Use interfaces for service contracts
Commit Guidelines
- Use clear, descriptive commit messages
- Reference issues when applicable
- Keep commits focused on a single change
Extension Architecture Patterns
Dependency Injection
Services use constructor injection:
class BuilderService implements IBuilderService {
constructor(
private container: ServiceContainer
) {}
private get settings() {
return this.container.get(ISettingsService);
}
}
Event-Driven Communication
Services communicate via events:
// Service emits event
buildStatusService.onBuildComplete(() => {
// Handle build completion
});
// In BuilderService
private emitBuildComplete() {
this.buildStatusService.notifyBuildComplete();
}
Lazy Initialization
Resources are initialized on-demand:
class LazyCache<T> {
private value?: T;
private loader: () => T;
get(): T {
if (!this.value) {
this.value = this.loader();
}
return this.value;
}
}
Cache Invalidation
Caches are invalidated by file modification:
class CompileCommands {
private cache?: ParsedData;
private mtime?: number;
private async checkCache() {
const stat = await fs.stat(this.filePath);
if (!this.cache || stat.mtimeMs !== this.mtime) {
this.cache = await this.parse();
this.mtime = stat.mtimeMs;
}
}
}
Bundled Ninja
Extension bundles Ninja binaries for Windows, macOS, and Linux. The
binaries are fetched from ninja-runtime project via scripts/prepare-ninja.mjs script.
Binaries are stored in the resources/ninja directory and bundled into the extension package. The extension uses the bundled Ninja binaries to execute build commands in a cross-platform manner.
[!TODO] Should I pick them up from the original repository instead?
see Ninja Releases
References
VS Code Extension Development
C++ Extension Integration
Build System Documentation
TypeScript
Questions?
If you have questions about contributing, please open an issue in the repository or contact the maintainers.
License
By contributing to ZMK, you agree that your contributions will be licensed under the same license as the project. See LICENSE for details.