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 getEffectiveHue(): number {
const config = readConfig();
// In harmonized mode with a stored offset, recompute from the
// current theme's base hue so the color adapts on theme change.
if (config.colorMode === 'harmonized'
&& config.harmonyOffset !== null) {
const baseHue = extractThemeBaseHue();
return ((baseHue + config.harmonyOffset) % 360 + 360) % 360;
}
if (config.hueOverride !== null) {
return config.hueOverride;
}
const name = getWorkspaceName();
return name ? hashToHue(name) : 0;
}
getEffectiveHue resolves the hue through a priority chain. The key addition for
harmonized mode: if a harmonyOffset is stored, the effective hue is computed as
themeBaseHue + offset. This is what makes colors adapt automatically — the
offset (e.g., +25° for analogous) stays fixed, but the base hue changes when the theme changes.
The modular arithmetic ((baseHue + offset) % 360 + 360) % 360 handles negative
offsets correctly — an offset of -120° from base 90° gives 330°, not -30°.
function updateStatusBar(): void {
const config = readConfig();
if (!config.enabled) {
statusBarItem.text = '$(circle-slash) ColorIdentity';
statusBarItem.tooltip = 'ColorIdentity is disabled';
} else {
const hue = getEffectiveHue();
const isHarmony = config.colorMode === 'harmonized'
&& config.harmonyOffset !== null;
const isAuto = config.hueOverride === null && !isHarmony;
const source = isHarmony ? 'harmony' : isAuto ? 'auto' : 'override';
statusBarItem.text = `$(symbol-color) ColorIdentity`;
statusBarItem.tooltip =
`Hue: ${hue}° (${source}) — Click to change`;
}
}
The status bar now reports three possible sources: auto (hash-derived),
override (manual hue), or harmony (offset from theme base).
This helps the user understand why a particular color was chosen.
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) { ... }
const currentHue = getEffectiveHue();
const themeKind = vscode.window.activeColorTheme.kind;
const themeProfile = getThemeProfile(themeKind);
const config = readConfig();
const result = await showColorPicker(
currentHue, themeProfile, swatchDir, config.colorMode
);
if (result === undefined) {
return; // dismissed
}
await persistColorSelection(result.hue, result.harmonyOffset);
// Config change listener will auto-apply
})
);
The key change from the original: persistColorSelection writes both
hueOverride and harmonyOffset to workspace settings. In harmonized
mode, the offset is what enables theme-adaptive colors — the absolute hue is stored as
a fallback for when the theme’s base hue can’t be detected.
async function persistColorSelection(
hue: number | null,
harmonyOffset?: number
): Promise<void> {
const cfg = vscode.workspace.getConfiguration('colorIdentity');
await cfg.update('hueOverride', hue,
vscode.ConfigurationTarget.Workspace);
await cfg.update('harmonyOffset', harmonyOffset ?? null,
vscode.ConfigurationTarget.Workspace);
}
This is event-driven design at work. The command doesn’t apply colors directly —
it writes settings and lets the config change listener do the rest. 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. The harmonyOffset is
the key innovation — it decouples “what the user chose” (a relationship)
from “what gets applied” (an absolute hue recomputed per theme).
Two event listeners make the extension reactive:
// Re-apply when the theme kind changes (e.g., Dark → Light)
context.subscriptions.push(
vscode.window.onDidChangeActiveColorTheme(() => {
clearSwatchCache(swatchDir);
setTimeout(() => applyIdentityColors(), 250);
})
);
// Re-apply when configuration changes — covers both our settings
// AND the workbench color theme (switching between themes of the
// same kind, e.g., Dark Modern → Dracula).
context.subscriptions.push(
vscode.workspace.onDidChangeConfiguration((e) => {
if (e.affectsConfiguration('workbench.colorTheme')) {
clearSwatchCache(swatchDir);
setTimeout(() => applyIdentityColors(), 250);
} else if (e.affectsConfiguration('colorIdentity')) {
const config = readConfig();
if (config.enabled) {
applyIdentityColors();
} else {
resetColors();
updateStatusBar();
}
}
})
);
There are now three event-driven triggers:
onDidChangeActiveColorTheme) — fires when
the theme kind changes (e.g., Dark → Light). A 250ms delay lets the
workbench.colorTheme config settle before we read the new theme name.
workbench.colorTheme config) — fires when
switching between themes of the same kind (e.g., Dark Modern → Dracula). Without
this, the onDidChangeActiveColorTheme event wouldn’t fire because the kind
didn’t change.
colorIdentity.*) — covers changes
from the Settings UI, settings.json edits, or the color picker command.
The setTimeout delay is important. When the user switches themes, VS Code fires
events before the workbench.colorTheme config is fully updated.
extractThemeBaseHue reads that config to infer the theme’s dominant hue
via name-matching heuristics. Without the delay, it would read the old theme name
and compute the wrong base hue.
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 seven modules are now accounted for: types define the contracts, the theme analyzer detects the active theme’s hue, the color generator derives palettes with harmony-aware adjustments, the applier writes them to settings, the picker provides a dual-mode UI, the swatch generator creates icons, and the extension entry point wires it all together with commands, event listeners, and theme-change reactivity. The final section covers packaging and publishing.