Project 7: The "Code Butler" Command Extension

Project 7: The “Code Butler” Command Extension

Project Overview

Attribute Value
Difficulty Advanced
Time Estimate 1-2 Weeks
Main Language TypeScript
Alternative Languages JavaScript
Knowledge Area VS Code API / Plugin Architecture
Prerequisites TypeScript, Node.js

Learning Objectives

By completing this project, you will:

  1. Understand the Extension Host architecture and process isolation
  2. Master command registration and the contribution point system
  3. Work with the TextDocument and TextEditor APIs for code manipulation
  4. Implement proper resource cleanup with disposables
  5. Package and distribute a VS Code extension

The Core Question

“If the tool doesn’t do what I want, how do I teach it?”


Deep Theoretical Foundation

Why Build Extensions?

VS Code has 50,000+ extensions, but sometimes you need:

  • Company-specific code generators
  • Custom linters for internal conventions
  • Integration with internal tools
  • Workflows that no existing extension handles

Building extensions teaches you how VS Code works at its core—knowledge that makes you a power user even when using other extensions.

The Extension Host Architecture

VS Code runs as multiple processes:

┌─────────────────────────────────────────────────────────────┐
│                    VS Code Processes                         │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  ┌─────────────────────────────────────────────────────┐    │
│  │           Main Process (Electron)                    │    │
│  │  - Window management                                 │    │
│  │  - File system access                                │    │
│  │  - Native menus                                      │    │
│  └──────────────────────┬──────────────────────────────┘    │
│                         │                                    │
│  ┌──────────────────────┴──────────────────────────────┐    │
│  │       Renderer Process (Chromium)                    │    │
│  │  - Monaco Editor                                     │    │
│  │  - Workbench UI                                      │    │
│  └──────────────────────┬──────────────────────────────┘    │
│                         │                                    │
│  ┌──────────────────────┴──────────────────────────────┐    │
│  │       Extension Host (Node.js)                       │    │
│  │  ┌─────────────────────────────────────────────┐    │    │
│  │  │  Your Extension                             │    │    │
│  │  │  (runs here, isolated from UI)              │    │    │
│  │  └─────────────────────────────────────────────┘    │    │
│  └─────────────────────────────────────────────────────┘    │
│                                                              │
└─────────────────────────────────────────────────────────────┘

Why this matters:

  • If your extension crashes, VS Code stays alive
  • Extensions can’t directly manipulate the DOM
  • Extensions communicate via message passing (secure, stable)

Activation Events: Lazy Loading

Extensions don’t load until needed. Activation events tell VS Code when to load:

Event When
onCommand:myext.cmd When user invokes the command
onLanguage:python When a Python file opens
workspaceContains:**/package.json When workspace has package.json
* Always load (avoid this)

Modern approach: Use empty activationEvents: [] and VS Code infers from contributes.commands.

The Command System

Commands connect user actions to code:

package.json                     extension.ts
─────────────                    ─────────────
"contributes": {                 vscode.commands.registerCommand(
  "commands": [{                   'myext.hello',
    "command": "myext.hello",      () => {
    "title": "Say Hello"             vscode.window.showInfo('Hi!');
  }]                               }
}                                );
  1. package.json declares the command exists (searchable in palette)
  2. registerCommand() defines what happens when invoked
  3. They must match—if not, clicking the command does nothing

TextDocument vs TextEditor

TextDocument TextEditor
The file’s content (model) The view of that content
getText(), line counts Cursor position, selection
One document, multiple editors Each editor has one document
Can exist without editor Always has a document

Editing safely:

// Wrong: Direct modification doesn't work
let text = document.getText();
text = text.replace('foo', 'bar');  // Does nothing!

// Right: Use edit builder
editor.edit(editBuilder => {
  editBuilder.replace(range, newText);
});

Disposables: Resource Management

Every registered command, event listener, and status bar item is a Disposable:

export function activate(context: vscode.ExtensionContext) {
  // Good: Push to subscriptions for auto-cleanup
  let cmd = vscode.commands.registerCommand('myext.cmd', () => {});
  context.subscriptions.push(cmd);

  // Bad: Memory leak!
  vscode.commands.registerCommand('myext.leak', () => {});
  // Not pushed, never cleaned up
}

When the extension deactivates, VS Code calls dispose() on everything in context.subscriptions.


Project Specification

What You’re Building

A “Code Butler” extension that:

  1. Registers commands in the Command Palette
  2. Removes all console.log statements from the current file
  3. Shows a count of removed statements
  4. Adds a status bar item showing log count
  5. Can be triggered via keyboard shortcut

Deliverables

  1. Working Extension: Installable via .vsix file
  2. Multiple Commands: Clean logs, count logs, toggle status bar
  3. Configuration: User-configurable regex pattern
  4. Keybinding: Custom keyboard shortcut
  5. Documentation: README with usage instructions

Success Criteria

  • Command appears in Command Palette
  • Removes console.log statements correctly
  • Shows notification with count
  • Status bar shows live count
  • Works on TypeScript and JavaScript files
  • Can be installed via .vsix

Solution Architecture

Project Structure

code-butler/
├── src/
│   ├── extension.ts       # Main entry point
│   ├── commands/
│   │   ├── cleanLogs.ts   # Clean logs command
│   │   └── countLogs.ts   # Count logs command
│   └── statusBar.ts       # Status bar management
├── package.json           # Extension manifest
├── tsconfig.json          # TypeScript config
├── .vscode/
│   └── launch.json        # Debug configuration
└── README.md

Extension Manifest (package.json)

{
  "name": "code-butler",
  "displayName": "Code Butler",
  "description": "Automate repetitive code cleanup tasks",
  "version": "0.0.1",
  "engines": {
    "vscode": "^1.85.0"
  },
  "categories": ["Other"],
  "activationEvents": [],
  "main": "./out/extension.js",
  "contributes": {
    "commands": [
      {
        "command": "code-butler.cleanLogs",
        "title": "Butler: Clean Console Logs"
      },
      {
        "command": "code-butler.countLogs",
        "title": "Butler: Count Console Logs"
      }
    ],
    "keybindings": [
      {
        "command": "code-butler.cleanLogs",
        "key": "ctrl+shift+l",
        "mac": "cmd+shift+l",
        "when": "editorTextFocus"
      }
    ],
    "configuration": {
      "title": "Code Butler",
      "properties": {
        "codeButler.logPattern": {
          "type": "string",
          "default": "console\\.log\\(.*?\\);?\\n?",
          "description": "Regex pattern to match log statements"
        }
      }
    }
  }
}

Phased Implementation Guide

Phase 1: Scaffold the Extension (30 minutes)

Goal: Create the extension boilerplate.

  1. Install Yeoman and the generator:
npm install -g yo generator-code
  1. Generate the extension:
yo code

? What type of extension do you want to create? New Extension (TypeScript)
? What's the name of your extension? Code Butler
? What's the identifier of your extension? code-butler
? What's the description of your extension? Automate code cleanup
? Initialize a git repository? Yes
? Which package manager to use? npm
  1. Open in VS Code:
cd code-butler
code .
  1. Explore the generated files:
    • src/extension.ts: Your code goes here
    • package.json: Extension manifest
    • .vscode/launch.json: Debug configuration

Phase 2: Create Your First Command (45 minutes)

Goal: Register a command and show a notification.

  1. Edit src/extension.ts:
import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {
    console.log('Code Butler is now active!');

    // Register the command
    let disposable = vscode.commands.registerCommand(
        'code-butler.helloWorld',
        () => {
            vscode.window.showInformationMessage('Hello from Code Butler!');
        }
    );

    // Add to subscriptions for cleanup
    context.subscriptions.push(disposable);
}

export function deactivate() {}
  1. Update package.json commands:
"contributes": {
  "commands": [
    {
      "command": "code-butler.helloWorld",
      "title": "Butler: Hello World"
    }
  ]
}
  1. Test the extension:
    • Press F5 to start debugging
    • A new VS Code window opens (Extension Development Host)
    • Press Cmd+Shift+P → type “Butler: Hello World”
    • You should see the notification!

Phase 3: Implement Log Cleaning (1 hour)

Goal: Remove console.log statements from the current file.

  1. Create src/commands/cleanLogs.ts:
import * as vscode from 'vscode';

export async function cleanLogs(): Promise<void> {
    const editor = vscode.window.activeTextEditor;

    if (!editor) {
        vscode.window.showErrorMessage('No active editor found');
        return;
    }

    const document = editor.document;
    const text = document.getText();

    // Get pattern from configuration
    const config = vscode.workspace.getConfiguration('codeButler');
    const pattern = config.get<string>('logPattern', 'console\\.log\\(.*?\\);?\\n?');
    const regex = new RegExp(pattern, 'g');

    // Count matches before removal
    const matches = text.match(regex);
    const count = matches ? matches.length : 0;

    if (count === 0) {
        vscode.window.showInformationMessage('No console.log statements found');
        return;
    }

    // Remove log statements
    const newText = text.replace(regex, '');

    // Apply the edit
    const success = await editor.edit(editBuilder => {
        const fullRange = new vscode.Range(
            document.positionAt(0),
            document.positionAt(text.length)
        );
        editBuilder.replace(fullRange, newText);
    });

    if (success) {
        vscode.window.showInformationMessage(
            `Code Butler: Removed ${count} console.log statement(s)!`
        );
    } else {
        vscode.window.showErrorMessage('Failed to apply edits');
    }
}
  1. Update src/extension.ts:
import * as vscode from 'vscode';
import { cleanLogs } from './commands/cleanLogs';

export function activate(context: vscode.ExtensionContext) {
    console.log('Code Butler is now active!');

    let cleanCommand = vscode.commands.registerCommand(
        'code-butler.cleanLogs',
        cleanLogs
    );

    context.subscriptions.push(cleanCommand);
}

export function deactivate() {}
  1. Update package.json:
"contributes": {
  "commands": [
    {
      "command": "code-butler.cleanLogs",
      "title": "Butler: Clean Console Logs"
    }
  ]
}
  1. Test it:
    • Press F5 to launch Extension Development Host
    • Create a test file with console.log statements
    • Run “Butler: Clean Console Logs”
    • Logs should be removed!

Phase 4: Add Status Bar Item (45 minutes)

Goal: Show live count of log statements.

  1. Create src/statusBar.ts:
import * as vscode from 'vscode';

let statusBarItem: vscode.StatusBarItem;

export function createStatusBar(context: vscode.ExtensionContext): void {
    statusBarItem = vscode.window.createStatusBarItem(
        vscode.StatusBarAlignment.Right,
        100
    );
    statusBarItem.command = 'code-butler.cleanLogs';
    statusBarItem.tooltip = 'Click to clean console.log statements';

    context.subscriptions.push(statusBarItem);

    // Update on editor change
    context.subscriptions.push(
        vscode.window.onDidChangeActiveTextEditor(updateStatusBar)
    );

    // Update on text change
    context.subscriptions.push(
        vscode.workspace.onDidChangeTextDocument(updateStatusBar)
    );

    // Initial update
    updateStatusBar();
}

function updateStatusBar(): void {
    const editor = vscode.window.activeTextEditor;

    if (!editor) {
        statusBarItem.hide();
        return;
    }

    const text = editor.document.getText();
    const matches = text.match(/console\.log\(/g);
    const count = matches ? matches.length : 0;

    if (count > 0) {
        statusBarItem.text = `$(bug) ${count} logs`;
        statusBarItem.backgroundColor = new vscode.ThemeColor(
            'statusBarItem.warningBackground'
        );
        statusBarItem.show();
    } else {
        statusBarItem.hide();
    }
}
  1. Update src/extension.ts:
import * as vscode from 'vscode';
import { cleanLogs } from './commands/cleanLogs';
import { createStatusBar } from './statusBar';

export function activate(context: vscode.ExtensionContext) {
    console.log('Code Butler is now active!');

    // Register commands
    let cleanCommand = vscode.commands.registerCommand(
        'code-butler.cleanLogs',
        cleanLogs
    );
    context.subscriptions.push(cleanCommand);

    // Create status bar
    createStatusBar(context);
}

export function deactivate() {}

Phase 5: Add Configuration (30 minutes)

Goal: Let users customize the regex pattern.

  1. Update package.json with configuration:
"contributes": {
  "configuration": {
    "title": "Code Butler",
    "properties": {
      "codeButler.logPattern": {
        "type": "string",
        "default": "console\\.log\\(.*?\\);?\\n?",
        "description": "Regex pattern to match log statements"
      },
      "codeButler.showStatusBar": {
        "type": "boolean",
        "default": true,
        "description": "Show log count in status bar"
      }
    }
  }
}
  1. Read configuration in cleanLogs.ts (already done above)

  2. Add keybinding:

"keybindings": [
  {
    "command": "code-butler.cleanLogs",
    "key": "ctrl+shift+l",
    "mac": "cmd+shift+l",
    "when": "editorTextFocus"
  }
]

Phase 6: Package the Extension (30 minutes)

Goal: Create an installable .vsix file.

  1. Install vsce:
npm install -g @vscode/vsce
  1. Add required fields to package.json:
{
  "name": "code-butler",
  "displayName": "Code Butler",
  "description": "Automate code cleanup tasks",
  "version": "0.0.1",
  "publisher": "your-publisher-name",
  "repository": {
    "type": "git",
    "url": "https://github.com/you/code-butler"
  },
  "engines": {
    "vscode": "^1.85.0"
  },
  ...
}
  1. Package the extension:
vsce package

# Output:
# DONE  Packaged: code-butler-0.0.1.vsix
  1. Install locally:
code --install-extension code-butler-0.0.1.vsix

Complete Extension Code

src/extension.ts

import * as vscode from 'vscode';
import { cleanLogs, countLogs } from './commands/cleanLogs';
import { createStatusBar } from './statusBar';

export function activate(context: vscode.ExtensionContext) {
    console.log('Code Butler activated');

    // Register commands
    context.subscriptions.push(
        vscode.commands.registerCommand('code-butler.cleanLogs', cleanLogs),
        vscode.commands.registerCommand('code-butler.countLogs', countLogs)
    );

    // Create status bar
    createStatusBar(context);
}

export function deactivate() {
    console.log('Code Butler deactivated');
}

src/commands/cleanLogs.ts

import * as vscode from 'vscode';

function getLogPattern(): RegExp {
    const config = vscode.workspace.getConfiguration('codeButler');
    const pattern = config.get<string>('logPattern', 'console\\.log\\(.*?\\);?\\n?');
    return new RegExp(pattern, 'g');
}

export async function cleanLogs(): Promise<void> {
    const editor = vscode.window.activeTextEditor;
    if (!editor) {
        vscode.window.showErrorMessage('No active editor');
        return;
    }

    const document = editor.document;
    const text = document.getText();
    const regex = getLogPattern();
    const matches = text.match(regex);
    const count = matches?.length ?? 0;

    if (count === 0) {
        vscode.window.showInformationMessage('No logs found');
        return;
    }

    const newText = text.replace(regex, '');

    await editor.edit(editBuilder => {
        const fullRange = new vscode.Range(
            document.positionAt(0),
            document.positionAt(text.length)
        );
        editBuilder.replace(fullRange, newText);
    });

    vscode.window.showInformationMessage(`Removed ${count} log(s)`);
}

export function countLogs(): void {
    const editor = vscode.window.activeTextEditor;
    if (!editor) {
        vscode.window.showErrorMessage('No active editor');
        return;
    }

    const text = editor.document.getText();
    const matches = text.match(/console\.log\(/g);
    const count = matches?.length ?? 0;

    vscode.window.showInformationMessage(`Found ${count} console.log statements`);
}

Testing Strategy

Manual Testing Checklist

  • Extension activates without errors
  • Command appears in Command Palette
  • Logs are correctly removed
  • Notification shows correct count
  • Status bar updates on text change
  • Keybinding works
  • Configuration changes take effect
  • Works in both .js and .ts files

Automated Testing

Create src/test/suite/extension.test.ts:

import * as assert from 'assert';
import * as vscode from 'vscode';

suite('Extension Test Suite', () => {
    vscode.window.showInformationMessage('Start all tests.');

    test('Extension should be present', () => {
        assert.ok(vscode.extensions.getExtension('your-publisher.code-butler'));
    });

    test('Should register commands', async () => {
        const commands = await vscode.commands.getCommands();
        assert.ok(commands.includes('code-butler.cleanLogs'));
    });
});

Common Pitfalls and Debugging

Pitfall 1: Command Not Found

Problem: “Command ‘code-butler.cleanLogs’ not found”

Solution:

  • Ensure package.json command ID matches registerCommand() ID exactly
  • Check for typos
  • Ensure extension compiled (npm run compile)

Pitfall 2: Extension Not Activating

Problem: No console output, extension seems dead.

Debug:

  1. Check Output panel → “Log (Extension Host)”
  2. Check for compilation errors
  3. Verify activationEvents or empty array

Pitfall 3: Edits Don’t Apply

Problem: editor.edit() returns false.

Solution:

  • Ensure document is not read-only
  • Check if another edit is in progress
  • Use await for edit operations

Pitfall 4: Memory Leaks

Problem: Extension gets slower over time.

Solution:

  • Push ALL disposables to context.subscriptions
  • Don’t create new event listeners on every command invoke

Interview Questions

  1. “How does VS Code ensure a crashing extension doesn’t crash the editor?”

    Answer: “Extensions run in a separate Extension Host process. If an extension crashes with an infinite loop or uncaught exception, the Extension Host restarts, but the VS Code UI stays responsive. This process isolation protects users.”

  2. “What are activation events?”

    Answer: “Activation events tell VS Code when to load an extension. Examples: onCommand:* loads when a command is invoked, onLanguage:python loads when a Python file opens. This lazy loading keeps VS Code fast—unused extensions don’t consume resources.”

  3. “How do you safely edit a document in a VS Code extension?”

    Answer: “You must use editor.edit() with an edit builder. Direct text manipulation doesn’t work because TextDocument is read-only. The edit builder queues changes and applies them atomically. I always await the result to handle failures.”


Resources

Essential Reading

Resource Topic
VS Code Extension API Official reference
Programming TypeScript (Cherny) TypeScript for extensions
Node.js Design Patterns (Casciaro) Async patterns, events

Official Documentation


Self-Assessment Checklist

  • I can scaffold an extension with Yeoman
  • I understand the Extension Host architecture
  • I can register commands and contribute to package.json
  • I can read and write TextDocument content
  • I understand disposables and proper cleanup
  • I can create status bar items
  • I can read extension configuration
  • I can package and install a .vsix file
  • I can debug an extension with breakpoints

Previous: P06-focus-mode-workspace-profiles.md


Congratulations!

You’ve completed all 7 VS Code Mastery projects! You now understand:

  • Keyboard navigation for flow-state coding
  • Debugging with professional-grade tools
  • Task automation for seamless workflows
  • Snippets for rapid code generation
  • Dev Containers for reproducible environments
  • Profiles for context-specific optimization
  • Extension development to extend VS Code itself

You’ve transformed VS Code from a tool you use into a tool you control.