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

The Status Bar

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.

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

Architecture Note

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.

Event Listeners

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.

VS Code Concept

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.

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