Building tools. Learning to build tools. Learning to build learning tools.
How extension.ts ties every module together through activation, commands, and event listeners.
extension.ts is the entry point. When VS Code activates the extension (after startup
finishes, as declared in package.json), it calls activate(context).
The context parameter provides storage paths and a subscription list for cleanup.
export function activate(context: vscode.ExtensionContext) {
// Set up swatch cache directory in extension's global storage
swatchDir = path.join(context.globalStorageUri.fsPath, 'swatches');
// Status bar item — click to open color picker
statusBarItem = vscode.window.createStatusBarItem(
vscode.StatusBarAlignment.Left,
50
);
statusBarItem.command = 'colorIdentity.chooseColor';
statusBarItem.show();
context.subscriptions.push(statusBarItem);
// Apply colors on startup
applyIdentityColors();
}
The first thing activate does is set up infrastructure:
context.globalStorageUri gives the extension
a private directory that persists across sessions. The swatch cache lives in a swatches
subdirectory inside it.
command property makes it trigger the color picker when clicked.
context.subscriptions.push(statusBarItem) registers the status bar item for
automatic cleanup. When the extension deactivates, VS Code disposes everything in the
subscriptions array. This prevents memory leaks and dangling UI elements. Every disposable
resource (status bar items, event listeners, command registrations) should be pushed here.
Before looking at the commands, let’s examine the helpers they share:
function getWorkspaceName(): string | undefined {
const folders = vscode.workspace.workspaceFolders;
if (folders && folders.length > 0) {
return folders[0].name;
}
return undefined;
}
function getEffectiveHue(): number {
const config = readConfig();
if (config.hueOverride !== null) {
return config.hueOverride;
}
const name = getWorkspaceName();
return name ? hashToHue(name) : 0;
}
getWorkspaceName reads the first workspace folder’s name. In a multi-root
workspace, it uses the first folder — a reasonable default. Returning undefined
when no workspace is open lets callers guard against that case.
getEffectiveHue resolves the hue, giving priority to the override. This is called
by the status bar updater and the color picker to show the current state.
function updateStatusBar(): void {
const config = readConfig();
if (!config.enabled) {
statusBarItem.text = '$(circle-slash) Color Identity';
statusBarItem.tooltip = 'Color Identity is disabled';
} else {
const hue = getEffectiveHue();
const isAuto = config.hueOverride === null;
statusBarItem.text = `$(symbol-color) Color Identity`;
statusBarItem.tooltip =
`Hue: ${hue}° (${isAuto ? 'auto' : 'override'}) — Click to change`;
}
}
The status bar provides at-a-glance information: is the extension enabled? What’s the
current hue? Is it auto-derived or overridden? The $(symbol-color) and
$(circle-slash) are Codicon icons that give visual weight to the indicator.
Each of the four commands declared in package.json gets a handler registered
in activate. Here’s the “choose color” command:
context.subscriptions.push(
vscode.commands.registerCommand('colorIdentity.chooseColor', async () => {
const workspaceName = getWorkspaceName();
if (!workspaceName) {
vscode.window.showWarningMessage(
'Color Identity: No workspace folder is open.'
);
return;
}
const currentHue = getEffectiveHue();
const themeKind = vscode.window.activeColorTheme.kind;
const themeProfile = getThemeProfile(themeKind);
const result = await showColorPicker(
currentHue, themeProfile, swatchDir
);
if (result === undefined) {
return; // dismissed
}
await setHueOverride(result.hue);
// Config change listener will auto-apply
})
);
The pattern: guard against invalid state (no workspace), gather current state, show the UI, and write the result. Notice the comment “Config change listener will auto-apply” — writing the hue override triggers a configuration change event, which the extension handles separately. This avoids duplicating the color application logic.
This is event-driven design at work. The command doesn’t apply colors directly —
it writes a setting and lets the config change listener do the rest. The benefit: every path
that changes a setting (the command, the Settings UI, manually editing settings.json)
triggers the same listener, so the extension always stays in sync regardless of how the
change was made.
Two event listeners make the extension reactive:
// Re-apply when the user changes their color theme
context.subscriptions.push(
vscode.window.onDidChangeActiveColorTheme(() => {
clearSwatchCache(swatchDir); // swatches are theme-specific
applyIdentityColors();
})
);
// Re-apply when our configuration changes
context.subscriptions.push(
vscode.workspace.onDidChangeConfiguration((e) => {
if (e.affectsConfiguration('colorIdentity')) {
const config = readConfig();
if (config.enabled) {
applyIdentityColors();
} else {
resetColors();
updateStatusBar();
}
}
})
);
The theme change listener fires when the user switches between dark and light themes (or any theme at all). It clears the swatch cache (because swatches encode theme-specific colors) and re-applies colors with the new theme profile.
The configuration change listener fires when any setting under the
colorIdentity namespace changes. It checks whether the extension is still enabled:
if yes, re-apply; if no, reset all colors and update the status bar.
e.affectsConfiguration('colorIdentity') is a performance guard. VS Code fires
the onDidChangeConfiguration event for any configuration change —
not just yours. Without this check, the extension would re-apply colors every time the user
changed their font size or any other unrelated setting.
export function deactivate() {
// Nothing to clean up — colors persist in workspace settings intentionally
}
This is a deliberate choice. When the extension deactivates (or is uninstalled), the colors
stay in the workspace’s settings.json. This is the right behavior
because:
All six modules are now accounted for: types define the contracts, the color generator derives palettes, the applier writes them to settings, the picker provides UI, the swatch generator creates icons, and the extension entry point wires it all together with commands and event listeners. The final section covers packaging and publishing.