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:
- Understand the Extension Host architecture and process isolation
- Master command registration and the contribution point system
- Work with the TextDocument and TextEditor APIs for code manipulation
- Implement proper resource cleanup with disposables
- 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!');
}] }
} );
package.jsondeclares the command exists (searchable in palette)registerCommand()defines what happens when invoked- 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:
- Registers commands in the Command Palette
- Removes all
console.logstatements from the current file - Shows a count of removed statements
- Adds a status bar item showing log count
- Can be triggered via keyboard shortcut
Deliverables
- Working Extension: Installable via .vsix file
- Multiple Commands: Clean logs, count logs, toggle status bar
- Configuration: User-configurable regex pattern
- Keybinding: Custom keyboard shortcut
- 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.
- Install Yeoman and the generator:
npm install -g yo generator-code
- 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
- Open in VS Code:
cd code-butler
code .
- Explore the generated files:
src/extension.ts: Your code goes herepackage.json: Extension manifest.vscode/launch.json: Debug configuration
Phase 2: Create Your First Command (45 minutes)
Goal: Register a command and show a notification.
- 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() {}
- Update package.json commands:
"contributes": {
"commands": [
{
"command": "code-butler.helloWorld",
"title": "Butler: Hello World"
}
]
}
- Test the extension:
- Press
F5to start debugging - A new VS Code window opens (Extension Development Host)
- Press
Cmd+Shift+P→ type “Butler: Hello World” - You should see the notification!
- Press
Phase 3: Implement Log Cleaning (1 hour)
Goal: Remove console.log statements from the current file.
- 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');
}
}
- 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() {}
- Update package.json:
"contributes": {
"commands": [
{
"command": "code-butler.cleanLogs",
"title": "Butler: Clean Console Logs"
}
]
}
- Test it:
- Press
F5to launch Extension Development Host - Create a test file with console.log statements
- Run “Butler: Clean Console Logs”
- Logs should be removed!
- Press
Phase 4: Add Status Bar Item (45 minutes)
Goal: Show live count of log statements.
- 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();
}
}
- 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.
- 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"
}
}
}
}
-
Read configuration in cleanLogs.ts (already done above)
-
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.
- Install vsce:
npm install -g @vscode/vsce
- 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"
},
...
}
- Package the extension:
vsce package
# Output:
# DONE Packaged: code-butler-0.0.1.vsix
- 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.jsoncommand ID matchesregisterCommand()ID exactly - Check for typos
- Ensure extension compiled (
npm run compile)
Pitfall 2: Extension Not Activating
Problem: No console output, extension seems dead.
Debug:
- Check Output panel → “Log (Extension Host)”
- Check for compilation errors
- Verify
activationEventsor 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
awaitfor 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
-
“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.”
-
“What are activation events?”
Answer: “Activation events tell VS Code when to load an extension. Examples:
onCommand:*loads when a command is invoked,onLanguage:pythonloads when a Python file opens. This lazy loading keeps VS Code fast—unused extensions don’t consume resources.” -
“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.