130 Widgets

Building tools. Learning to build tools. Learning to build learning tools.

8. Extension Lifecycle & Event-Driven Reactivity

How extension.ts ties every module together through activation, commands, and event listeners.

The activate() Function

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:

VS Code Concept

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.

Helper Functions

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°.

The Status Bar

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.

Command Registration

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);
}
Architecture Note

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).

Event Listeners

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:

  1. Theme kind change (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.
  2. Theme name change (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.
  3. Extension settings change (colorIdentity.*) — covers changes from the Settings UI, settings.json edits, or the color picker command.
VS Code Concept

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.

The deactivate() Function

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:

Checkpoint

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.