VSCODE EXTENSION DEVELOPMENT PROJECTS
Learning VSCode Extension Development: From Basics to Mastery
Core Concept Analysis
To truly understand VSCode extension development, you need to grasp these fundamental building blocks:
1. Extension Architecture
- Extension Host: The isolated process where your extension runs, separate from the main VSCode UI
- Extension Manifest (
package.json): Declarative configuration defining what your extension contributes - Contribution Points: Static declarations (commands, menus, keybindings, views) that extend VSCode
- Activation Events: Triggers that tell VSCode when to load your extension (lazy loading)
2. The Extension Lifecycle
activate(context): Called when your extension is first activateddeactivate(): Called when extension is unloaded (cleanup)- Extension Context: Provides subscriptions, storage, secrets, and extension paths
3. Core APIs
- Commands: Actions users can trigger via Command Palette, keybindings, or menus
- Editor/Document API: Interacting with text documents, selections, and edits
- Workspace API: Access to files, folders, configuration, and workspace state
- Window API: Status bar, notifications, input boxes, quick picks, and views
4. Advanced Concepts
- Language Server Protocol (LSP): Standardized protocol for language intelligence features
- Debug Adapter Protocol (DAP): Protocol for integrating debuggers
- Webviews: Custom HTML/CSS/JS UIs embedded in VSCode
- Tree Views: Hierarchical data displays in the sidebar
- Virtual Documents: Providing content for non-file URIs
5. Distribution & Quality
- Testing: Integration tests running in Extension Development Host
- Packaging: Creating
.vsixfiles withvsce - Publishing: VS Marketplace and Open VSX Registry
- CI/CD: Automated testing and publishing pipelines
Project-Based Learning Path
The following projects are ordered to progressively build your understanding, starting from simple command-based extensions to complex language servers and debugging integrations.
Project 1: Hello World Command Extension
File: VSCODE_EXTENSION_DEVELOPMENT_PROJECTS.md Main Programming Language: TypeScript Alternative Programming Languages: JavaScript Coolness Level: Level 1: Pure Corporate Snoozefest Business Potential: 1. The “Resume Gold” Difficulty: Level 1: Beginner Knowledge Area: Extension Architecture / Commands Software or Tool: VSCode Extension API Main Book: “Visual Studio Code: End-to-End Editing and Debugging Tools for Web Developers” by Bruce Johnson
What you’ll build
A simple extension that registers a command, displays a notification, and writes to the output channel.
Why it teaches VSCode Extensions
This is your foundation. You’ll understand the extension lifecycle (activate/deactivate), how commands are registered and invoked, and how the package.json manifest declares contribution points. Every VSCode extension builds on these fundamentals.
Core challenges you’ll face
- Setting up the development environment (Node.js, Yeoman generator, TypeScript) → maps to tooling setup
- Understanding
package.jsoncontribution points (commands, activationEvents) → maps to extension manifest - Registering commands programmatically (using
vscode.commands.registerCommand) → maps to Commands API - Managing disposables (pushing to
context.subscriptions) → maps to resource cleanup - Debugging with Extension Development Host (F5 workflow) → maps to development cycle
Key Concepts
- Extension Manifest Structure: Extension Anatomy - VS Code Docs
- Activation Events: Activation Events Reference - VS Code Docs
- Commands API: Commands API - VS Code Docs
- Disposables Pattern: Extension Anatomy - VS Code Docs
Difficulty: Beginner Time estimate: Weekend Prerequisites: Basic TypeScript/JavaScript, familiarity with VSCode as a user
Real world outcome
1. Open Command Palette (Cmd/Ctrl+Shift+P)
2. Type "Hello World"
3. See a notification appear: "Hello from your first extension!"
4. See a message written to the Output panel under "My Extension" channel
Implementation Hints
Start by running npx --package yo --package generator-code -- yo code to scaffold your project. Choose TypeScript. The generator creates a src/extension.ts with a basic activate function.
The flow is:
- User triggers command → VSCode checks
package.jsonfor activation events - If extension not activated,
activate()is called - Inside
activate(), you register command handlers withvscode.commands.registerCommand(commandId, callback) - Callback executes → show information message with
vscode.window.showInformationMessage() - Create output channel with
vscode.window.createOutputChannel()→ append lines to it
Pseudo code:
function activate(context):
outputChannel = createOutputChannel("My Extension")
disposable = registerCommand("myext.helloWorld", () => {
showInformationMessage("Hello from your first extension!")
outputChannel.appendLine("Command executed at: " + currentTime)
outputChannel.show()
})
context.subscriptions.push(disposable, outputChannel)
Learning milestones
- Extension runs in Development Host → You understand the F5 debugging workflow
- Command appears in Command Palette → You understand contribution points
- Notification and output work → You understand the Window API basics
- Clean unload without errors → You understand disposables and lifecycle
Project 2: Word Counter Status Bar Extension
File: VSCODE_EXTENSION_DEVELOPMENT_PROJECTS.md Main Programming Language: TypeScript Alternative Programming Languages: JavaScript Coolness Level: Level 2: Practical but Forgettable Business Potential: 2. The “Micro-SaaS / Pro Tool” Difficulty: Level 1: Beginner Knowledge Area: Status Bar / Document Events Software or Tool: VSCode Extension API Main Book: “Visual Studio Code Distilled” by Alessandro Del Sole
What you’ll build
A status bar item that displays the word count of the current document, updating in real-time as you type.
Why it teaches VSCode Extensions
You’ll learn event-driven programming in VSCode—subscribing to document changes, editor switches, and selection changes. The status bar is a key UI extension point, and this project teaches you to respond reactively to user actions.
Core challenges you’ll face
- Creating and positioning status bar items (alignment, priority) → maps to Status Bar API
- Subscribing to document change events (
onDidChangeTextDocument) → maps to event subscriptions - Subscribing to editor change events (
onDidChangeActiveTextEditor) → maps to workspace events - Efficiently counting words (without blocking UI) → maps to performance considerations
- Handling edge cases (no active editor, binary files) → maps to defensive programming
Key Concepts
- Status Bar Items: Status Bar - UX Guidelines - VS Code Docs
- Document Events: Workspace API - onDidChangeTextDocument
- TextEditor API: TextEditor - VS Code API
Difficulty: Beginner Time estimate: Weekend Prerequisites: Project 1 completed, basic event-driven programming concepts
Real world outcome
1. Open any text file in VSCode
2. Look at the status bar (bottom right or left)
3. See "Words: 42" updating as you type
4. Switch files → count updates to reflect new file
5. Close all editors → status bar item hides gracefully
Implementation Hints
Status bar items have properties: text, tooltip, command, alignment (left/right), and priority (position order).
The flow:
- Create status bar item in
activate()withvscode.window.createStatusBarItem(alignment, priority) - Subscribe to
onDidChangeActiveTextEditor→ when editor changes, recalculate - Subscribe to
onDidChangeTextDocument→ when content changes, recalculate - Word counting: split document text by whitespace, filter empty strings, count
Pseudo code:
function activate(context):
statusBarItem = createStatusBarItem(StatusBarAlignment.Right, 100)
statusBarItem.command = "myext.showWordCount" // clicking shows detail
function updateWordCount():
editor = getActiveTextEditor()
if not editor:
statusBarItem.hide()
return
text = editor.document.getText()
wordCount = text.split(/\s+/).filter(word => word.length > 0).length
statusBarItem.text = "Words: " + wordCount
statusBarItem.show()
context.subscriptions.push(
statusBarItem,
onDidChangeActiveTextEditor(() => updateWordCount()),
onDidChangeTextDocument(() => updateWordCount())
)
updateWordCount() // initial call
Learning milestones
- Status bar item appears → You understand createStatusBarItem
- Count updates on typing → You understand document change events
- Count updates on file switch → You understand editor change events
- No errors on edge cases → You handle null/undefined defensively
Project 3: Snippet Inserter with Quick Pick
File: VSCODE_EXTENSION_DEVELOPMENT_PROJECTS.md Main Programming Language: TypeScript Alternative Programming Languages: JavaScript Coolness Level: Level 2: Practical but Forgettable Business Potential: 2. The “Micro-SaaS / Pro Tool” Difficulty: Level 2: Intermediate Knowledge Area: Quick Pick / Text Editing Software or Tool: VSCode Extension API Main Book: “Visual Studio Code: End-to-End Editing and Debugging Tools for Web Developers” by Bruce Johnson
What you’ll build
An extension that shows a quick pick menu of code snippets, and inserts the selected snippet at the cursor position with proper indentation.
Why it teaches VSCode Extensions
This project introduces you to VSCode’s modal UI components (Quick Pick, Input Box) and the critical TextEditor edit API. You’ll learn how to programmatically modify document content—the bread and butter of productivity extensions.
Core challenges you’ll face
- Creating quick pick items with descriptions (labels, descriptions, detail) → maps to Quick Pick API
- Performing text edits at cursor position (
editor.edit()withTextEdit) → maps to Edit API - Preserving indentation (detecting and applying current line indent) → maps to text manipulation
- Supporting multi-cursor (inserting at all cursor positions) → maps to Selection API
- Handling undo/redo correctly (edit batching) → maps to transactional edits
Key Concepts
- Quick Pick API: Quick Pick - VS Code Docs
- TextEditor.edit(): TextEditor.edit - VS Code API
- Position and Range: Position - VS Code API
- Snippet Syntax: Snippet Syntax - VS Code Docs
Difficulty: Intermediate Time estimate: Weekend Prerequisites: Projects 1-2, understanding of text positions
Real world outcome
1. Trigger command "Insert Snippet" from Command Palette
2. Quick Pick appears with options: "Console Log", "Try-Catch", "Arrow Function"
3. Select "Console Log"
4. "console.log();" is inserted at cursor position
5. Cursor is positioned inside the parentheses, ready to type
Implementation Hints
Quick Pick items are objects with label, description, and detail. You can attach arbitrary data. The showQuickPick function returns a promise with the selected item.
For text insertion:
- Get current active editor
- Get cursor position:
editor.selection.active - Call
editor.edit(editBuilder => editBuilder.insert(position, text)) - For snippet-style insertion with tabstops, use
editor.insertSnippet(new SnippetString(...))
Pseudo code:
function insertSnippetCommand():
editor = getActiveTextEditor()
if not editor: return
snippets = [
{ label: "Console Log", snippet: "console.log($1);$0" },
{ label: "Try-Catch", snippet: "try {\n\t$1\n} catch (error) {\n\t$2\n}$0" },
{ label: "Arrow Function", snippet: "const $1 = ($2) => {\n\t$3\n};$0" }
]
selected = await showQuickPick(snippets, { placeHolder: "Select a snippet" })
if not selected: return
// Use SnippetString for tabstops support
snippetString = new SnippetString(selected.snippet)
editor.insertSnippet(snippetString)
Learning milestones
- Quick Pick shows with all options → You understand modal UI APIs
- Text inserts at cursor → You understand TextEditor.edit
- Tabstops work ($1, $2) → You understand SnippetString
- Works with multi-cursor → You understand Selection array
Project 4: File Bookmark Manager
File: VSCODE_EXTENSION_DEVELOPMENT_PROJECTS.md Main Programming Language: TypeScript Alternative Programming Languages: JavaScript Coolness Level: Level 3: Genuinely Clever Business Potential: 2. The “Micro-SaaS / Pro Tool” Difficulty: Level 2: Intermediate Knowledge Area: Workspace Storage / Tree View Software or Tool: VSCode Extension API Main Book: “Visual Studio Code Distilled” by Alessandro Del Sole
What you’ll build
An extension with a sidebar tree view that lets users bookmark files and lines, persisting across sessions, with quick navigation.
Why it teaches VSCode Extensions
This project introduces the Tree View API—one of VSCode’s most powerful UI components. You’ll learn to provide hierarchical data, handle tree item actions, and persist extension state using workspace storage. This is how tools like GitLens display their complex UIs.
Core challenges you’ll face
- Implementing TreeDataProvider (getChildren, getTreeItem) → maps to Tree View API
- Refreshing tree view on data changes (
onDidChangeTreeDataevent) → maps to reactive updates - Persisting data with workspaceState (globalState vs workspaceState) → maps to extension storage
- Navigating to file and line (
vscode.window.showTextDocument,revealRange) → maps to document navigation - Decorating tree items (icons, context values, commands) → maps to tree item customization
Key Concepts
- Tree View API: Tree View API - VS Code Docs
- TreeDataProvider: TreeDataProvider - VS Code API
- ExtensionContext Storage: Extension Context - globalState/workspaceState
- View Containers: View Container - VS Code Docs
Difficulty: Intermediate Time estimate: 1-2 weeks Prerequisites: Projects 1-3, TypeScript classes, async/await
Real world outcome
1. Open the "Bookmarks" view in the sidebar (custom icon in Activity Bar)
2. Right-click on any line in editor → "Add Bookmark"
3. Bookmark appears in tree: "main.ts:42 - function processData"
4. Click bookmark → jumps to that file and line
5. Close VSCode, reopen → bookmarks still there
6. Right-click bookmark → "Remove Bookmark"
Implementation Hints
Tree View requires:
- Contribution Point in
package.json: declare view container and view - TreeDataProvider class: implements
getTreeItem(element)andgetChildren(element) - TreeItem class: represents each node with label, icon, command, contextValue
- Refresh mechanism: fire
onDidChangeTreeDataevent when data changes
Storage:
context.workspaceState.get(key)/.update(key, value)for workspace-specific datacontext.globalState.get(key)/.update(key, value)for cross-workspace data
Pseudo code:
class BookmarkProvider implements TreeDataProvider<Bookmark>:
private bookmarks: Bookmark[] = []
private _onDidChangeTreeData = new EventEmitter()
onDidChangeTreeData = this._onDidChangeTreeData.event
constructor(private context: ExtensionContext):
this.bookmarks = context.workspaceState.get("bookmarks", [])
getTreeItem(bookmark: Bookmark): TreeItem:
item = new TreeItem(bookmark.label)
item.tooltip = bookmark.filePath + ":" + bookmark.line
item.iconPath = new ThemeIcon("bookmark")
item.command = { command: "bookmarks.goto", arguments: [bookmark] }
item.contextValue = "bookmark" // for context menu
return item
getChildren(element?): Bookmark[]:
if not element:
return this.bookmarks // root level
return [] // bookmarks have no children
addBookmark(filePath, line, label):
this.bookmarks.push({ filePath, line, label })
this.context.workspaceState.update("bookmarks", this.bookmarks)
this._onDidChangeTreeData.fire()
removeBookmark(bookmark):
this.bookmarks = this.bookmarks.filter(b => b !== bookmark)
this.context.workspaceState.update("bookmarks", this.bookmarks)
this._onDidChangeTreeData.fire()
Learning milestones
- View appears in sidebar → You understand view containers and contribution points
- Tree populates with items → You understand TreeDataProvider
- Clicking navigates to location → You understand TreeItem commands
- Data persists across sessions → You understand workspaceState
- Context menu actions work → You understand contextValue and menu contributions
Project 5: Markdown Preview with Custom Styles
File: VSCODE_EXTENSION_DEVELOPMENT_PROJECTS.md Main Programming Language: TypeScript Alternative Programming Languages: JavaScript Coolness Level: Level 3: Genuinely Clever Business Potential: 2. The “Micro-SaaS / Pro Tool” Difficulty: Level 2: Intermediate Knowledge Area: Webview / HTML Rendering Software or Tool: VSCode Extension API Main Book: “Visual Studio Code: End-to-End Editing and Debugging Tools for Web Developers” by Bruce Johnson
What you’ll build
A live markdown preview panel using webviews that renders markdown with custom CSS themes and updates in real-time as you type.
Why it teaches VSCode Extensions
Webviews unlock unlimited UI possibilities in VSCode—you’re rendering full HTML/CSS/JS. This project teaches content security policies, message passing between extension and webview, and handling the webview lifecycle. Many popular extensions (Mermaid previews, Kanban boards) use webviews.
Core challenges you’ll face
- Creating and managing webview panels (createWebviewPanel, reveal, dispose) → maps to Webview API
- Setting Content Security Policy (preventing XSS, allowing styles) → maps to security
- Passing messages extension ↔ webview (postMessage, onDidReceiveMessage) → maps to IPC
- Updating webview on document changes (efficient re-rendering) → maps to reactive webviews
- Loading local resources (asWebviewUri for CSS/JS/images) → maps to resource loading
Key Concepts
- Webview API: Webview API - VS Code Docs
- Content Security Policy: Webview Security - VS Code Docs
- Message Passing: Webview Messages - VS Code Docs
- Webview Persistence: Webview Persistence - VS Code Docs
Difficulty: Intermediate Time estimate: 1-2 weeks Prerequisites: Projects 1-3, basic HTML/CSS, understanding of markdown
Real world outcome
1. Open a .md file
2. Run "Markdown Preview: Open Preview" command
3. A side panel opens showing rendered HTML with your custom theme
4. Type in the markdown file → preview updates live
5. Click a link in preview → opens in browser
6. Select theme from dropdown → preview re-styles immediately
Implementation Hints
Webviews are created with vscode.window.createWebviewPanel(viewType, title, column, options). The options include enableScripts, localResourceRoots, and retainContextWhenHidden.
Key pattern: Extension generates HTML string and sets webview.html. To update, regenerate and reassign.
For markdown rendering, use a library like marked (bundled with your extension). For real-time updates, debounce the document change events.
Pseudo code:
function openPreview():
editor = getActiveTextEditor()
if not editor or not isMarkdownFile(editor.document): return
panel = createWebviewPanel(
"markdownPreview",
"Markdown Preview",
ViewColumn.Beside,
{ enableScripts: true, localResourceRoots: [extensionUri] }
)
function updatePreview():
markdownText = editor.document.getText()
htmlContent = marked.parse(markdownText)
cssUri = panel.webview.asWebviewUri(join(extensionUri, "styles", "preview.css"))
panel.webview.html = `
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Security-Policy"
content="default-src 'none'; style-src ${panel.webview.cspSource};">
<link href="${cssUri}" rel="stylesheet">
</head>
<body>
${htmlContent}
</body>
</html>
`
context.subscriptions.push(
panel,
onDidChangeTextDocument(e => {
if e.document === editor.document:
debounce(updatePreview, 300)()
})
)
updatePreview()
Learning milestones
- Webview panel opens → You understand createWebviewPanel
- Markdown renders as HTML → You understand webview.html
- CSS loads correctly → You understand asWebviewUri and CSP
- Preview updates on typing → You understand document sync
- State persists when hidden → You understand retainContextWhenHidden
Project 6: Code Action Provider (Quick Fixes)
File: VSCODE_EXTENSION_DEVELOPMENT_PROJECTS.md Main Programming Language: TypeScript Alternative Programming Languages: JavaScript Coolness Level: Level 3: Genuinely Clever Business Potential: 3. The “Service & Support” Model Difficulty: Level 2: Intermediate Knowledge Area: Code Actions / Diagnostics Software or Tool: VSCode Extension API Main Book: “Language Implementation Patterns” by Terence Parr
What you’ll build
An extension that provides “quick fix” code actions for a specific pattern—like converting console.log to a proper logger call, or wrapping error-prone code in try-catch.
Why it teaches VSCode Extensions
Code Actions are the lightbulb menu that appears when you hover over code. Understanding this API is crucial for building intelligent coding assistants. You’ll learn how VSCode’s language features work: diagnostics (squiggly lines), code actions (fixes), and how they connect.
Core challenges you’ll face
- Implementing CodeActionProvider (provideCodeActions method) → maps to provider pattern
- Analyzing code at cursor/selection range (getting text, parsing) → maps to code analysis
- Creating WorkspaceEdit (replacing text across files) → maps to bulk edits
- Associating actions with diagnostics (quick fixes for specific errors) → maps to diagnostics integration
- Providing multiple action kinds (quickfix, refactor, source) → maps to action categories
Key Concepts
- Code Action Provider: Code Actions - VS Code Docs
- WorkspaceEdit: WorkspaceEdit - VS Code API
- DiagnosticCollection: Diagnostics - VS Code Docs
- Code Action Kinds: CodeActionKind - VS Code API
Difficulty: Intermediate Time estimate: 1-2 weeks Prerequisites: Projects 1-4, regex basics, understanding of AST concepts
Real world outcome
1. Open a JavaScript file with console.log("test")
2. Yellow squiggly appears under console.log
3. Click the lightbulb (or Cmd+.)
4. See action: "Replace with logger.debug()"
5. Click action → code transforms to: logger.debug("test")
6. If logger import missing, action adds import statement too
Implementation Hints
Code Action Providers register for specific document selectors (language, scheme). When the lightbulb appears, VSCode calls your provideCodeActions(document, range, context).
The context contains diagnostics if you want to provide fixes for specific errors. Return an array of CodeAction objects, each with a title, kind, and either edit (WorkspaceEdit) or command.
Pseudo code:
class ConsoleLogFixer implements CodeActionProvider:
static metadata = {
providedCodeActionKinds: [CodeActionKind.QuickFix]
}
provideCodeActions(document, range, context): CodeAction[]:
actions = []
// Find console.log on the affected lines
for lineNum in range.start.line to range.end.line:
line = document.lineAt(lineNum)
match = line.text.match(/console\.log\((.*)\)/)
if match:
action = new CodeAction(
"Replace with logger.debug()",
CodeActionKind.QuickFix
)
edit = new WorkspaceEdit()
loggerCall = "logger.debug(" + match[1] + ")"
edit.replace(
document.uri,
new Range(lineNum, match.index, lineNum, match.index + match[0].length),
loggerCall
)
action.edit = edit
action.isPreferred = true // shows in auto-fix
actions.push(action)
return actions
// Register in activate()
languages.registerCodeActionsProvider(
{ language: "javascript" },
new ConsoleLogFixer(),
ConsoleLogFixer.metadata
)
Learning milestones
- Lightbulb appears on console.log → You understand provider registration
- Action replaces code correctly → You understand WorkspaceEdit
- Works with selections → You understand range handling
- Adds missing imports → You understand multi-location edits
Project 7: Custom Diagnostic Provider (Linter)
File: VSCODE_EXTENSION_DEVELOPMENT_PROJECTS.md Main Programming Language: TypeScript Alternative Programming Languages: JavaScript Coolness Level: Level 3: Genuinely Clever Business Potential: 3. The “Service & Support” Model Difficulty: Level 2: Intermediate Knowledge Area: Diagnostics / Code Analysis Software or Tool: VSCode Extension API Main Book: “Compilers: Principles and Practice” by Parag H. Dave
What you’ll build
A linter extension that analyzes code for custom rules (like “TODO comments must have assignee”, “magic numbers not allowed”, “function too long”) and shows warnings/errors with squiggly lines.
Why it teaches VSCode Extensions
Diagnostics are the foundation of intelligent editors—the squiggly lines under errors, warnings in the Problems panel. Building a linter teaches you to analyze code, produce structured diagnostics, and integrate with VSCode’s error reporting system.
Core challenges you’ll face
- Creating DiagnosticCollection (managing diagnostics lifecycle) → maps to diagnostics API
- Analyzing documents for patterns (regex, line-by-line, AST) → maps to static analysis
- Producing rich diagnostics (severity, message, range, code, relatedInformation) → maps to error reporting
- Updating diagnostics on changes (debouncing, incremental updates) → maps to performance
- Clearing diagnostics appropriately (on file close, on fix) → maps to state management
Key Concepts
- Diagnostics API: Diagnostics - VS Code Docs
- DiagnosticCollection: DiagnosticCollection - VS Code API
- Diagnostic Severity: DiagnosticSeverity - VS Code API
- Related Information: DiagnosticRelatedInformation - VS Code API
Difficulty: Intermediate Time estimate: 1-2 weeks Prerequisites: Projects 1-6, regex, basic parsing concepts
Real world outcome
1. Open a JavaScript file with: const x = 42; // magic number
2. See yellow squiggly under "42"
3. Hover shows: "Warning: Magic number detected. Consider using a named constant."
4. Problems panel shows the warning with file path and line
5. Write: // TODO fix this later
6. See error: "TODO must have assignee: // TODO(@username) ..."
7. Fix the issue → squiggly disappears immediately
Implementation Hints
Diagnostics are managed through a DiagnosticCollection created with vscode.languages.createDiagnosticCollection("myLinter"). You set diagnostics per URI: collection.set(uri, diagnostics).
Each Diagnostic has: range, message, severity (Error, Warning, Info, Hint), source (your linter name), code (rule ID).
Pattern: Subscribe to document open/change events → analyze → set diagnostics.
Pseudo code:
class MyLinter:
private collection: DiagnosticCollection
constructor():
this.collection = languages.createDiagnosticCollection("my-linter")
analyzeDocument(document: TextDocument):
if document.languageId !== "javascript": return
diagnostics = []
for lineNum in 0 to document.lineCount:
line = document.lineAt(lineNum)
// Rule: Magic numbers
for match in line.text.matchAll(/\b\d{2,}\b/g):
diagnostic = new Diagnostic(
new Range(lineNum, match.index, lineNum, match.index + match[0].length),
"Magic number detected. Use a named constant.",
DiagnosticSeverity.Warning
)
diagnostic.source = "my-linter"
diagnostic.code = "no-magic-numbers"
diagnostics.push(diagnostic)
// Rule: TODO format
todoMatch = line.text.match(/\/\/\s*TODO(?!.*@\w+)/)
if todoMatch:
diagnostic = new Diagnostic(
new Range(lineNum, todoMatch.index, lineNum, line.text.length),
"TODO must have assignee: // TODO(@username)",
DiagnosticSeverity.Error
)
diagnostic.source = "my-linter"
diagnostic.code = "todo-format"
diagnostics.push(diagnostic)
this.collection.set(document.uri, diagnostics)
clearDiagnostics(uri: Uri):
this.collection.delete(uri)
// In activate():
linter = new MyLinter()
workspace.onDidOpenTextDocument(doc => linter.analyzeDocument(doc))
workspace.onDidChangeTextDocument(e => linter.analyzeDocument(e.document))
workspace.onDidCloseTextDocument(doc => linter.clearDiagnostics(doc.uri))
Learning milestones
- Squiggly lines appear → You understand DiagnosticCollection.set
- Problems panel populates → You understand Diagnostic structure
- Updates in real-time → You understand document event subscriptions
- Clears on file close → You understand lifecycle management
Project 8: Git Diff Decoration Extension
File: VSCODE_EXTENSION_DEVELOPMENT_PROJECTS.md Main Programming Language: TypeScript Alternative Programming Languages: JavaScript Coolness Level: Level 3: Genuinely Clever Business Potential: 2. The “Micro-SaaS / Pro Tool” Difficulty: Level 3: Advanced Knowledge Area: Decorations / Source Control Software or Tool: Git, VSCode Extension API Main Book: “Pro Git” by Scott Chacon (free online)
What you’ll build
An extension that shows inline git blame information and highlights modified/added/deleted lines with colored gutters, similar to GitLens but simpler.
Why it teaches VSCode Extensions
Decorations are how extensions visually annotate code—colored backgrounds, gutter icons, inline text. This project combines decorations with spawning external processes (git), parsing output, and efficiently updating decorations on scroll/edit. It’s a masterclass in extension performance.
Core challenges you’ll face
- Creating TextEditorDecorationType (colors, borders, gutter icons) → maps to Decoration API
- Spawning child processes (calling git commands) → maps to Node.js child_process
- Parsing git output (blame, diff –numstat) → maps to text parsing
- Efficient decoration updates (visible ranges, caching) → maps to performance optimization
- Handling non-git files gracefully → maps to error handling
Key Concepts
- Decoration Types: Decorations - VS Code Docs
- setDecorations: TextEditor.setDecorations - VS Code API
- Child Process: Node.js child_process
- Visible Ranges: TextEditor.visibleRanges - VS Code API
Difficulty: Advanced Time estimate: 2-3 weeks Prerequisites: Projects 1-7, git command line, child process spawning
Real world outcome
1. Open a file in a git repository
2. Modified lines show orange gutter marker
3. Added lines show green gutter marker
4. Deleted lines show red triangle in gutter
5. Hover over any line → see git blame: "John Doe, 3 days ago: Fixed bug"
6. Scrolling and typing remain smooth (no lag)
Implementation Hints
Create decoration types once in activate() with styles. Then use editor.setDecorations(decorationType, ranges) to apply.
For git data:
git diff --numstat HEAD -- file.txt→ shows added/removed linesgit blame --line-porcelain file.txt→ shows blame per line
Cache git data per file. Invalidate on save. Only re-query visible range for blame (optimization).
Pseudo code:
// Decoration types (create once)
const addedLineDecoration = createTextEditorDecorationType({
gutterIconPath: greenDotPath,
overviewRulerColor: "green"
})
const modifiedLineDecoration = createTextEditorDecorationType({
gutterIconPath: orangeDotPath,
overviewRulerColor: "orange"
})
async function updateDecorations(editor: TextEditor):
filePath = editor.document.uri.fsPath
// Check if in git repo
if not isInGitRepo(filePath): return
// Get diff info
diffOutput = await exec("git diff --numstat HEAD -- " + filePath)
modifiedLines = parseDiffOutput(diffOutput)
// Get blame for visible range (optimization)
visibleRange = editor.visibleRanges[0]
blameOutput = await exec(`git blame -L ${visibleRange.start.line + 1},${visibleRange.end.line + 1} --line-porcelain ` + filePath)
blameData = parseBlameOutput(blameOutput)
// Apply decorations
addedRanges = modifiedLines.added.map(line => new Range(line, 0, line, 0))
modifiedRanges = modifiedLines.modified.map(line => new Range(line, 0, line, 0))
editor.setDecorations(addedLineDecoration, addedRanges)
editor.setDecorations(modifiedLineDecoration, modifiedRanges)
// Store blame data for hover provider
blameCache.set(filePath, blameData)
// Register hover provider for blame info
languages.registerHoverProvider("*", {
provideHover(document, position):
blame = blameCache.get(document.uri.fsPath)?.[position.line]
if blame:
return new Hover(`${blame.author}, ${blame.date}: ${blame.summary}`)
return null
})
Learning milestones
- Gutter colors appear → You understand TextEditorDecorationType
- Git data fetched correctly → You understand child_process
- Blame shows on hover → You understand HoverProvider
- Performance stays smooth → You understand caching and visible ranges
Project 9: Custom Language Syntax Highlighting
File: VSCODE_EXTENSION_DEVELOPMENT_PROJECTS.md Main Programming Language: TypeScript + JSON (TextMate Grammar) Alternative Programming Languages: JSON only (declarative) Coolness Level: Level 3: Genuinely Clever Business Potential: 3. The “Service & Support” Model Difficulty: Level 3: Advanced Knowledge Area: Language Grammars / TextMate Software or Tool: TextMate Grammars Main Book: “Language Implementation Patterns” by Terence Parr
What you’ll build
Complete syntax highlighting for a custom configuration language or DSL (like .env files with sections, or a custom template language), using TextMate grammars.
Why it teaches VSCode Extensions
Syntax highlighting is the first thing users notice about language support. TextMate grammars are regex-based and declarative—understanding them teaches you how VSCode tokenizes text. This is foundational knowledge before building Language Servers.
Core challenges you’ll face
- Writing TextMate grammar JSON (patterns, captures, repository) → maps to grammar specification
- Understanding scope naming (keyword.control, string.quoted.double) → maps to semantic tokens
- Handling nested constructs (begin/end patterns, includes) → maps to recursive tokenization
- Testing grammars (scope inspector, grammar debugging) → maps to grammar debugging
- Language configuration (brackets, comments, folding) → maps to editor integration
Key Concepts
- Syntax Highlighting Guide: Syntax Highlighting - VS Code Docs
- TextMate Grammars: TextMate Language Grammars
- Language Configuration: Language Configuration - VS Code Docs
- Scope Naming: Scope Naming - TextMate
Difficulty: Advanced Time estimate: 1-2 weeks Prerequisites: Regex mastery, understanding of tokenization
Real world outcome
1. Create a file with .myconfig extension
2. Syntax highlighting automatically applies:
- Keywords in blue: [section], @include
- Strings in green: "quoted values"
- Comments in gray: # This is a comment
- Variables in orange: ${variable}
3. Bracket matching works for { } and [ ]
4. Comment toggling (Cmd+/) uses # prefix
5. Folding works on sections
Implementation Hints
TextMate grammars are JSON/PLIST files. Key structure:
scopeName: unique identifier like “source.myconfig”patterns: array of pattern objectsrepository: named patterns for reuse
Pattern types:
match+name: single regex matchbegin/end+patterns: multi-line constructsinclude: reference repository or other grammars
Pseudo grammar structure:
{
"scopeName": "source.myconfig",
"patterns": [
{ "include": "#comments" },
{ "include": "#sections" },
{ "include": "#key-value" }
],
"repository": {
"comments": {
"match": "#.*$",
"name": "comment.line.number-sign.myconfig"
},
"sections": {
"begin": "\\[",
"end": "\\]",
"beginCaptures": { "0": { "name": "punctuation.section.begin.myconfig" } },
"endCaptures": { "0": { "name": "punctuation.section.end.myconfig" } },
"patterns": [
{ "match": "[\\w-]+", "name": "entity.name.section.myconfig" }
]
},
"key-value": {
"match": "^(\\w+)\\s*(=)\\s*(.*)$",
"captures": {
"1": { "name": "variable.other.key.myconfig" },
"2": { "name": "keyword.operator.assignment.myconfig" },
"3": { "patterns": [{ "include": "#values" }] }
}
},
"values": {
"patterns": [
{ "match": "\"[^\"]*\"", "name": "string.quoted.double.myconfig" },
{ "match": "\\$\\{\\w+\\}", "name": "variable.other.interpolation.myconfig" }
]
}
}
}
Language configuration (language-configuration.json):
{
"comments": { "lineComment": "#" },
"brackets": [["[", "]"], ["{", "}"]],
"autoClosingPairs": [
{ "open": "[", "close": "]" },
{ "open": "\"", "close": "\"" }
]
}
Learning milestones
- File recognized as custom language → You understand language contributions
- Basic highlighting works → You understand match patterns
- Nested constructs highlight → You understand begin/end patterns
- Bracket matching works → You understand language configuration
- Scopes visible in Inspector → You understand debugging grammars
Project 10: Autocomplete Provider (IntelliSense)
File: VSCODE_EXTENSION_DEVELOPMENT_PROJECTS.md Main Programming Language: TypeScript Alternative Programming Languages: JavaScript Coolness Level: Level 3: Genuinely Clever Business Potential: 3. The “Service & Support” Model Difficulty: Level 3: Advanced Knowledge Area: Completion / Language Features Software or Tool: VSCode Extension API Main Book: “Language Implementation Patterns” by Terence Parr
What you’ll build
An autocomplete extension for a specific domain—like CSS class names from your project, environment variable names, or API endpoint paths—providing rich completions with documentation.
Why it teaches VSCode Extensions
CompletionItemProvider is one of the most-used extension APIs. Understanding how completions work—triggering, filtering, sorting, commit characters—lets you build extensions that dramatically speed up coding. This is the foundation for AI coding assistants.
Core challenges you’ll face
- Implementing CompletionItemProvider (provideCompletionItems, resolveCompletionItem) → maps to completion API
- Contextual completions (only trigger in right places) → maps to context detection
- Lazy loading completion details (resolveCompletionItem for docs) → maps to performance
- Completion item properties (kind, detail, documentation, insertText, sortText) → maps to rich completions
- Trigger characters (completing after specific keys) → maps to trigger configuration
Key Concepts
- Completion Provider: Completions - VS Code Docs
- CompletionItem: CompletionItem - VS Code API
- CompletionItemKind: CompletionItemKind - VS Code API
- Trigger Characters: CompletionItemProvider - triggerCharacters
Difficulty: Advanced Time estimate: 1-2 weeks Prerequisites: Projects 1-9, understanding of completion UX
Real world outcome
1. Open an HTML file in a project with Tailwind CSS
2. Type class=" and start typing "bg-"
3. Autocomplete shows: bg-red-500, bg-blue-500, etc.
4. Each item shows color preview in detail
5. Select item → inserts with correct cursor position
6. Hover over suggestion → see Tailwind documentation
Alternative: Environment variables completion
1. Open any file, type process.env.
2. Autocomplete shows all .env variables: DATABASE_URL, API_KEY
3. Each shows current value (masked if secret)
Implementation Hints
Register with languages.registerCompletionItemProvider(selector, provider, ...triggerCharacters). The provideCompletionItems(document, position, token, context) method returns CompletionItem[] or CompletionList.
For CSS class completion, scan project for class definitions. Cache results. Filter based on current word.
Pseudo code:
class CSSClassCompletionProvider implements CompletionItemProvider:
private classCache: Map<string, string[]> = new Map()
async provideCompletionItems(document, position, token, context):
// Only complete inside class="..."
lineText = document.lineAt(position).text
beforeCursor = lineText.substring(0, position.character)
if not isInsideClassAttribute(beforeCursor):
return []
// Get current word being typed
wordRange = document.getWordRangeAtPosition(position)
currentWord = wordRange ? document.getText(wordRange) : ""
// Get all CSS classes from project
classes = await this.getAllClasses()
return classes
.filter(cls => cls.startsWith(currentWord))
.map(cls => {
item = new CompletionItem(cls, CompletionItemKind.Value)
item.detail = "CSS Class"
item.sortText = "0" + cls // prioritize exact matches
return item
})
async resolveCompletionItem(item, token):
// Lazy load documentation
if isTailwindClass(item.label):
item.documentation = await fetchTailwindDocs(item.label)
return item
private async getAllClasses():
if this.classCache.has("all"):
return this.classCache.get("all")
// Scan CSS files for class definitions
cssFiles = await workspace.findFiles("**/*.css")
classes = []
for file in cssFiles:
content = await workspace.fs.readFile(file)
// Parse .className patterns
matches = content.toString().matchAll(/\.([a-zA-Z][\w-]*)/g)
classes.push(...matches.map(m => m[1]))
this.classCache.set("all", [...new Set(classes)])
return this.classCache.get("all")
// Register with trigger character
languages.registerCompletionItemProvider(
{ language: "html" },
new CSSClassCompletionProvider(),
'"', "'" // Trigger after opening quote in class=""
)
Learning milestones
- Completions appear in menu → You understand CompletionItemProvider
- Filtering works → You understand completion matching
- Documentation shows on hover → You understand resolveCompletionItem
- Only triggers in right context → You understand context detection
- Performance stays fast → You understand caching strategies
Project 11: Hover Information Provider
File: VSCODE_EXTENSION_DEVELOPMENT_PROJECTS.md Main Programming Language: TypeScript Alternative Programming Languages: JavaScript Coolness Level: Level 3: Genuinely Clever Business Potential: 2. The “Micro-SaaS / Pro Tool” Difficulty: Level 2: Intermediate Knowledge Area: Hover / Documentation Software or Tool: VSCode Extension API Main Book: “Language Implementation Patterns” by Terence Parr
What you’ll build
A hover provider that shows rich information when hovering over specific tokens—like showing color previews for hex codes, documentation for custom functions, or API response schemas for endpoint paths.
Why it teaches VSCode Extensions
Hover providers deliver contextual information without interrupting flow. This project teaches you to detect what’s under the cursor, fetch relevant data, and format it with Markdown. Combined with completions and diagnostics, hovers complete the basic IDE experience.
Core challenges you’ll face
- Implementing HoverProvider (provideHover method) → maps to hover API
- Detecting token under cursor (word at position, pattern matching) → maps to text analysis
- Formatting with MarkdownString (code blocks, links, images) → maps to markdown rendering
- Multiple hover sources (combining data from different places) → maps to data aggregation
- Async hover data (fetching from APIs or files) → maps to async patterns
Key Concepts
- Hover Provider: Hovers - VS Code Docs
- MarkdownString: MarkdownString - VS Code API
- Document Position Utilities: document.getWordRangeAtPosition
Difficulty: Intermediate Time estimate: 1 week Prerequisites: Projects 1-5, Markdown syntax
Real world outcome
1. Open a CSS file with: color: #3498db;
2. Hover over #3498db
3. Popup shows:
- Color preview square
- RGB: 52, 152, 219
- HSL: 204°, 70%, 53%
- "Click to copy"
Alternative: API documentation
1. Open file with: fetch("/api/users")
2. Hover over "/api/users"
3. Popup shows:
- GET /api/users
- Response schema
- Example response JSON
Implementation Hints
HoverProvider is simple: implement provideHover(document, position, token) returning Hover or null. A Hover contains contents (MarkdownString or array) and optional range.
For color preview, use MarkdownString with supportHtml = true and inline SVG or data URIs.
Pseudo code:
class ColorHoverProvider implements HoverProvider:
provideHover(document, position, token):
// Get word at position with custom pattern for hex colors
range = document.getWordRangeAtPosition(position, /#[0-9a-fA-F]{3,8}\b/)
if not range:
return null
hexColor = document.getText(range)
rgb = hexToRgb(hexColor)
hsl = rgbToHsl(rgb)
md = new MarkdownString()
md.supportHtml = true
// Color swatch using inline SVG
md.appendMarkdown(`<span style="background:${hexColor};padding:0 20px;"> </span>\n\n`)
md.appendMarkdown(`**Hex:** \`${hexColor}\`\n\n`)
md.appendMarkdown(`**RGB:** ${rgb.r}, ${rgb.g}, ${rgb.b}\n\n`)
md.appendMarkdown(`**HSL:** ${hsl.h}°, ${hsl.s}%, ${hsl.l}%\n\n`)
md.appendMarkdown(`[Copy to clipboard](command:colorHover.copy?${encodeURIComponent(hexColor)})`)
md.isTrusted = true // Allow command links
return new Hover(md, range)
// Register for CSS files
languages.registerHoverProvider(
{ language: "css" },
new ColorHoverProvider()
)
Learning milestones
- Hover popup appears → You understand HoverProvider
- Content formats correctly → You understand MarkdownString
- Works for your domain → You understand token detection
- Commands work in hover → You understand trusted markdown
Project 12: Simple Language Server (LSP)
File: VSCODE_EXTENSION_DEVELOPMENT_PROJECTS.md Main Programming Language: TypeScript Alternative Programming Languages: JavaScript, Python, Rust, Go Coolness Level: Level 4: Hardcore Tech Flex Business Potential: 4. The “Open Core” Infrastructure Difficulty: Level 4: Expert Knowledge Area: Language Server Protocol Software or Tool: LSP, vscode-languageserver Main Book: “Language Implementation Patterns” by Terence Parr
What you’ll build
A Language Server for a simple custom language (like a config format, template language, or DSL) providing diagnostics, completions, hover, and go-to-definition.
Why it teaches VSCode Extensions
The Language Server Protocol is how professional IDE features are built. It separates the language intelligence (server) from the editor (client). Once you build an LSP server, it works in any LSP-compatible editor (VSCode, Vim, Emacs, Sublime). This is the pinnacle of language tooling.
Core challenges you’ll face
- Understanding LSP architecture (client/server, JSON-RPC, capabilities) → maps to protocol design
- Implementing server lifecycle (initialize, initialized, shutdown) → maps to LSP lifecycle
- Document synchronization (full vs incremental, text document manager) → maps to state sync
- Providing language features (textDocument/completion, textDocument/hover) → maps to LSP methods
- Client extension integration (activating server, forwarding requests) → maps to client-server coordination
Key Concepts
- LSP Specification: Official LSP Spec
- VSCode LSP Guide: Language Server Extension Guide - VS Code Docs
- vscode-languageserver: GitHub - vscode-languageserver-node
- JSON-RPC: JSON-RPC 2.0 Specification
Difficulty: Expert Time estimate: 1 month Prerequisites: All previous projects, understanding of client-server architecture
Real world outcome
For a custom config language (.myconf):
1. Open .myconf file - language server starts
2. Type invalid syntax → red squiggle with error message
3. Type "server." → autocomplete shows: host, port, timeout
4. Hover over "timeout" → shows "Connection timeout in milliseconds (default: 5000)"
5. Cmd+click on "include: ./other.myconf" → jumps to that file
6. Rename symbol → renames across all files
7. Works in Neovim, Sublime Text too (same server!)
Implementation Hints
LSP extensions have two parts:
- Server (standalone Node.js process): handles language logic
- Client (extension code): spawns server, forwards messages
Use vscode-languageserver and vscode-languageclient packages.
Server structure:
- Create connection: createConnection(ProposedFeatures.all)
- Create document manager: TextDocuments<TextDocument>
- Register capability handlers: connection.onCompletion(), connection.onHover()
- Listen: connection.listen()
Pseudo code for server:
// server.ts
import { createConnection, TextDocuments, ProposedFeatures } from 'vscode-languageserver/node'
import { TextDocument } from 'vscode-languageserver-textdocument'
const connection = createConnection(ProposedFeatures.all)
const documents = new TextDocuments(TextDocument)
connection.onInitialize((params) => {
return {
capabilities: {
textDocumentSync: TextDocumentSyncKind.Incremental,
completionProvider: { triggerCharacters: ['.'] },
hoverProvider: true,
definitionProvider: true
}
}
})
documents.onDidChangeContent((change) => {
// Validate document, send diagnostics
const diagnostics = validateDocument(change.document)
connection.sendDiagnostics({ uri: change.document.uri, diagnostics })
})
connection.onCompletion((params) => {
const document = documents.get(params.textDocument.uri)
// Return completion items based on cursor position
return getCompletions(document, params.position)
})
connection.onHover((params) => {
const document = documents.get(params.textDocument.uri)
const word = getWordAtPosition(document, params.position)
if (isConfigKey(word)) {
return { contents: getDocumentation(word) }
}
return null
})
documents.listen(connection)
connection.listen()
Pseudo code for client extension:
// extension.ts
import { LanguageClient, TransportKind } from 'vscode-languageclient/node'
let client: LanguageClient
export function activate(context: ExtensionContext) {
const serverModule = context.asAbsolutePath('server/out/server.js')
const serverOptions = {
run: { module: serverModule, transport: TransportKind.ipc },
debug: { module: serverModule, transport: TransportKind.ipc }
}
const clientOptions = {
documentSelector: [{ scheme: 'file', language: 'myconf' }]
}
client = new LanguageClient('myconf-lsp', 'MyConf Language Server', serverOptions, clientOptions)
client.start()
}
export function deactivate() {
return client?.stop()
}
Learning milestones
- Server starts with extension → You understand client-server spawning
- Diagnostics appear → You understand document sync and validation
- Completions work → You understand onCompletion handler
- Go-to-definition works → You understand location responses
- Works in another editor → You understand LSP portability
Project 13: Custom Debug Adapter
File: VSCODE_EXTENSION_DEVELOPMENT_PROJECTS.md Main Programming Language: TypeScript Alternative Programming Languages: JavaScript, Python Coolness Level: Level 5: Pure Magic Business Potential: 4. The “Open Core” Infrastructure Difficulty: Level 5: Master Knowledge Area: Debug Adapter Protocol Software or Tool: DAP, vscode-debugadapter Main Book: “The Art of Debugging” by Norman Matloff
What you’ll build
A Debug Adapter for a custom runtime or scripting language, allowing users to set breakpoints, step through code, inspect variables, and view the call stack.
Why it teaches VSCode Extensions
The Debug Adapter Protocol is the standard for integrating debuggers. Building one teaches you process control, debugging concepts (breakpoints, stepping, scopes), and protocol design. This is expert-level systems programming with immediate visual feedback.
Core challenges you’ll face
- Understanding DAP architecture (launch vs attach, request/response) → maps to protocol understanding
- Implementing debug session lifecycle (initialize, launch, terminate) → maps to session management
- Managing breakpoints (setBreakpoints, validating, dynamic changes) → maps to breakpoint logic
- Controlling execution (continue, next, stepIn, pause) → maps to execution control
- Providing variable inspection (scopes, variables, evaluate) → maps to runtime introspection
Key Concepts
- DAP Specification: Debug Adapter Protocol
- VSCode Debugger Guide: Debugger Extension - VS Code Docs
- vscode-debugadapter: GitHub - vscode-debugadapter-node
- Mock Debug Sample: VSCode Mock Debug
Difficulty: Master Time estimate: 1 month+ Prerequisites: All previous projects, understanding of debugger concepts, process control
Real world outcome
For a custom scripting language (e.g., simplified JavaScript interpreter):
1. Set breakpoint on line 5 (click gutter → red dot)
2. Press F5 (Start Debugging)
3. Execution pauses at line 5 (line highlighted)
4. Variables panel shows local variables: x = 42, name = "test"
5. Call Stack shows: main → processData → calculate
6. Step Over (F10) → moves to line 6
7. Step Into (F11) → enters function call
8. Hover over variable in editor → shows value
9. Debug Console: type "x + 10" → shows 52
Implementation Hints
Debug Adapters are standalone processes communicating via DAP (JSON over stdin/stdout). VSCode spawns your adapter when debugging starts.
Use @vscode/debugadapter library for TypeScript. Extend DebugSession class and implement request handlers.
Pseudo code:
import { DebugSession, InitializedEvent, StoppedEvent, Thread, StackFrame, Scope, Variable } from '@vscode/debugadapter'
class MyDebugSession extends DebugSession {
private runtime: MyRuntime // Your language runtime
protected initializeRequest(response, args) {
response.body = {
supportsConfigurationDoneRequest: true,
supportsEvaluateForHovers: true,
supportsStepInTargetsRequest: true
}
this.sendResponse(response)
this.sendEvent(new InitializedEvent())
}
protected launchRequest(response, args) {
this.runtime = new MyRuntime()
this.runtime.on('stopOnBreakpoint', () => {
this.sendEvent(new StoppedEvent('breakpoint', 1))
})
this.runtime.start(args.program)
this.sendResponse(response)
}
protected setBreakPointsRequest(response, args) {
const path = args.source.path
const breakpoints = args.breakpoints.map(bp => {
const verified = this.runtime.setBreakpoint(path, bp.line)
return { verified, line: bp.line }
})
response.body = { breakpoints }
this.sendResponse(response)
}
protected threadsRequest(response) {
response.body = { threads: [new Thread(1, "main")] }
this.sendResponse(response)
}
protected stackTraceRequest(response, args) {
const frames = this.runtime.getStackFrames().map((f, i) =>
new StackFrame(i, f.name, { name: f.file, path: f.file }, f.line)
)
response.body = { stackFrames: frames, totalFrames: frames.length }
this.sendResponse(response)
}
protected scopesRequest(response, args) {
response.body = {
scopes: [
new Scope("Local", 1, false),
new Scope("Global", 2, true)
]
}
this.sendResponse(response)
}
protected variablesRequest(response, args) {
const variables = this.runtime.getVariables(args.variablesReference)
.map(v => new Variable(v.name, String(v.value)))
response.body = { variables }
this.sendResponse(response)
}
protected continueRequest(response, args) {
this.runtime.continue()
this.sendResponse(response)
}
protected nextRequest(response, args) {
this.runtime.stepOver()
this.sendResponse(response)
}
}
DebugSession.run(MyDebugSession)
Learning milestones
- Debug session starts → You understand DAP initialization
- Breakpoints set and hit → You understand breakpoint management
- Step controls work → You understand execution control
- Variables show values → You understand scopes and variables
- Evaluate expressions work → You understand runtime introspection
Project 14: Webview-Based Dashboard Extension
File: VSCODE_EXTENSION_DEVELOPMENT_PROJECTS.md Main Programming Language: TypeScript + React/Svelte/Vue Alternative Programming Languages: TypeScript + vanilla JS Coolness Level: Level 4: Hardcore Tech Flex Business Potential: 3. The “Service & Support” Model Difficulty: Level 3: Advanced Knowledge Area: Webview / SPA Integration Software or Tool: VSCode Webview, React/Svelte Main Book: “Visual Studio Code: End-to-End Editing and Debugging Tools for Web Developers” by Bruce Johnson
What you’ll build
A full-featured dashboard webview with a modern JS framework (React/Svelte/Vue) showing project analytics, task management, or API testing—with state persistence and bidirectional communication.
Why it teaches VSCode Extensions
This project teaches you to build complex UIs within VSCode. You’ll learn to bundle frontend code with webpack/esbuild, handle message passing between extension and webview, manage webview state across sessions, and work within Content Security Policy constraints.
Core challenges you’ll face
- Bundling frontend framework for webview (webpack/esbuild config) → maps to build tooling
- Bidirectional message passing (postMessage, onDidReceiveMessage) → maps to IPC patterns
- State persistence (webview state, extension state sync) → maps to state management
- Theming support (using VSCode CSS variables) → maps to theme integration
- Content Security Policy (nonces, trusted sources) → maps to security
Key Concepts
- Webview with React: Webview UI Toolkit Samples
- Webview State: Webview Persistence - VS Code Docs
- CSS Theme Variables: Color Theme - VS Code Docs
- Bundling for Webviews: Bundling Extensions - VS Code Docs
Difficulty: Advanced Time estimate: 2-3 weeks Prerequisites: Projects 1-11, React/Svelte/Vue experience, webpack/esbuild
Real world outcome
Project Analytics Dashboard:
1. Open Command Palette → "Open Project Dashboard"
2. Webview opens showing:
- Code coverage chart (interactive)
- Recent commits list
- TODO/FIXME summary
- Performance metrics
3. Click on a TODO → navigates to file:line in editor
4. Switch VSCode theme → dashboard updates colors
5. Close and reopen dashboard → state preserved
6. Click "Refresh" button → fetches latest data
Implementation Hints
Structure:
extension/
├── src/
│ ├── extension.ts # VSCode extension
│ └── webview/
│ ├── index.tsx # React app entry
│ ├── App.tsx # Main component
│ └── vscode.d.ts # acquireVsCodeApi types
├── webpack.config.js # Bundle webview separately
Key pattern: Extension creates webview with bundled HTML. HTML loads bundled JS. JS calls acquireVsCodeApi() to get messaging API.
Pseudo code for extension:
// extension.ts
function openDashboard(context: ExtensionContext) {
const panel = window.createWebviewPanel(
'projectDashboard',
'Project Dashboard',
ViewColumn.One,
{
enableScripts: true,
retainContextWhenHidden: true,
localResourceRoots: [Uri.joinPath(context.extensionUri, 'dist')]
}
)
const scriptUri = panel.webview.asWebviewUri(
Uri.joinPath(context.extensionUri, 'dist', 'webview.js')
)
const nonce = getNonce()
panel.webview.html = `
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Security-Policy"
content="default-src 'none'; script-src 'nonce-${nonce}'; style-src ${panel.webview.cspSource};">
<link href="${styleUri}" rel="stylesheet">
</head>
<body>
<div id="root"></div>
<script nonce="${nonce}" src="${scriptUri}"></script>
</body>
</html>
`
// Handle messages from webview
panel.webview.onDidReceiveMessage(async (message) => {
switch (message.command) {
case 'openFile':
const doc = await workspace.openTextDocument(message.path)
await window.showTextDocument(doc)
break
case 'refresh':
const data = await collectProjectData()
panel.webview.postMessage({ command: 'update', data })
break
}
})
// Send initial data
collectProjectData().then(data => {
panel.webview.postMessage({ command: 'init', data })
})
}
Pseudo code for webview (React):
// App.tsx
const vscode = acquireVsCodeApi()
function App() {
const [data, setData] = useState(vscode.getState()?.data || null)
useEffect(() => {
window.addEventListener('message', (event) => {
const message = event.data
if (message.command === 'init' || message.command === 'update') {
setData(message.data)
vscode.setState({ data: message.data })
}
})
}, [])
const handleTodoClick = (filePath: string, line: number) => {
vscode.postMessage({ command: 'openFile', path: filePath, line })
}
return (
<div className="dashboard">
<CoverageChart data={data?.coverage} />
<CommitsList commits={data?.commits} />
<TodoList todos={data?.todos} onTodoClick={handleTodoClick} />
</div>
)
}
Learning milestones
- Webview renders React app → You understand bundling and CSP
- Data flows from extension → You understand postMessage
- Click navigates to file → You understand bidirectional messaging
- State persists on reopen → You understand webview state
- Theme changes apply → You understand CSS variable theming
Project 15: Extension Test Suite
File: VSCODE_EXTENSION_DEVELOPMENT_PROJECTS.md Main Programming Language: TypeScript Alternative Programming Languages: JavaScript Coolness Level: Level 2: Practical but Forgettable Business Potential: 3. The “Service & Support” Model Difficulty: Level 3: Advanced Knowledge Area: Testing / CI-CD Software or Tool: vscode-test, Mocha Main Book: “Test Driven Development” by Kent Beck
What you’ll build
A comprehensive test suite for a VSCode extension covering unit tests, integration tests (running in Extension Development Host), and E2E tests with CI/CD pipeline.
Why it teaches VSCode Extensions
Testing extensions is uniquely challenging—many features only work inside VSCode. This project teaches you the testing infrastructure, mocking strategies, and CI/CD setup specific to VSCode extensions. Professional extensions require automated testing.
Core challenges you’ll face
- Setting up vscode-test (test CLI, test runner configuration) → maps to test infrastructure
- Writing integration tests (accessing VSCode APIs in tests) → maps to integration testing
- Mocking VSCode APIs (for unit tests without VSCode) → maps to mocking strategies
- Running tests in CI (headless, xvfb on Linux) → maps to CI configuration
- Publishing automation (vsce publish in pipeline) → maps to CD pipeline
Key Concepts
- Testing Extensions: Testing Extensions - VS Code Docs
- Test CLI: @vscode/test-cli
- CI Setup: Continuous Integration - VS Code Docs
- Extension Test Runner: Extension Test Runner
Difficulty: Advanced Time estimate: 1-2 weeks Prerequisites: All previous projects, testing experience, CI/CD basics
Real world outcome
1. Run npm test → tests execute in headless VSCode
2. See output:
✓ Command registration works
✓ Status bar shows word count
✓ Diagnostics appear for invalid code
✓ Completion items provided
✓ Webview opens correctly
3. Push to GitHub → Actions run tests automatically
4. Create tag → extension auto-publishes to Marketplace
5. See coverage report: 87% coverage
Implementation Hints
Test structure:
extension/
├── src/
│ └── extension.ts
├── src/test/
│ ├── suite/
│ │ ├── index.ts # Test runner setup
│ │ └── extension.test.ts
│ └── runTest.ts # Test launcher
├── .vscode-test.mjs # Test CLI config
└── .github/workflows/ci.yml
Integration tests run inside VSCode, accessing real APIs. For unit tests of pure logic, you can run outside VSCode.
Pseudo code for test:
// extension.test.ts
import * as assert from 'assert'
import * as vscode from 'vscode'
suite('Extension Test Suite', () => {
vscode.window.showInformationMessage('Starting tests')
test('Extension should activate', async () => {
const ext = vscode.extensions.getExtension('publisher.myextension')
assert.ok(ext)
await ext.activate()
assert.strictEqual(ext.isActive, true)
})
test('Command should be registered', async () => {
const commands = await vscode.commands.getCommands()
assert.ok(commands.includes('myext.helloWorld'))
})
test('Status bar should show word count', async () => {
const doc = await vscode.workspace.openTextDocument({
content: 'hello world test',
language: 'plaintext'
})
await vscode.window.showTextDocument(doc)
// Wait for extension to update
await sleep(100)
// Access status bar item (via exposed API or check workspace state)
const state = await vscode.commands.executeCommand('myext.getWordCount')
assert.strictEqual(state, 3)
})
test('Diagnostics should report errors', async () => {
const doc = await vscode.workspace.openTextDocument({
content: 'invalid syntax here',
language: 'myconfig'
})
await vscode.window.showTextDocument(doc)
// Wait for diagnostics
await waitForDiagnostics(doc.uri)
const diagnostics = vscode.languages.getDiagnostics(doc.uri)
assert.ok(diagnostics.length > 0)
assert.strictEqual(diagnostics[0].severity, vscode.DiagnosticSeverity.Error)
})
})
GitHub Actions CI:
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: xvfb-run -a npm test
publish:
if: startsWith(github.ref, 'refs/tags/')
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci
- run: npx vsce publish -p ${{ secrets.VSCE_PAT }}
Learning milestones
- Tests run locally → You understand vscode-test
- Tests access VSCode APIs → You understand integration testing
- Tests pass in CI → You understand headless execution
- Auto-publish works → You understand CD pipeline
- Coverage is measured → You understand test coverage
Project 16: Remote Development Extension
File: VSCODE_EXTENSION_DEVELOPMENT_PROJECTS.md Main Programming Language: TypeScript Alternative Programming Languages: JavaScript Coolness Level: Level 4: Hardcore Tech Flex Business Potential: 4. The “Open Core” Infrastructure Difficulty: Level 4: Expert Knowledge Area: Remote Development / Extension Host Software or Tool: VSCode Remote Development Main Book: “Distributed Systems” by Maarten van Steen
What you’ll build
An extension that works correctly in Remote Development scenarios (SSH, Containers, WSL, Codespaces), with proper UI/workspace separation and remote-aware file operations.
Why it teaches VSCode Extensions
Remote Development splits VSCode into UI (local) and workspace (remote) extension hosts. Understanding this architecture is crucial for modern extension development. Many extensions break in remote scenarios because they conflate UI and workspace operations.
Core challenges you’ll face
- Understanding UI vs Workspace extensions (extensionKind in package.json) → maps to extension architecture
- Handling remote file URIs (vscode-remote:// schemes) → maps to URI handling
- Spawning remote processes (commands run on remote, not local) → maps to remote execution
- Forwarding ports (exposing remote services locally) → maps to networking
- Testing remote scenarios (Dev Containers, SSH) → maps to remote testing
Key Concepts
- Remote Development: Supporting Remote Development - VS Code Docs
- Extension Kinds: Extension Manifest - extensionKind
- Remote URI Handling: Virtual File Systems - VS Code Docs
Difficulty: Expert Time estimate: 2-3 weeks Prerequisites: All previous projects, understanding of remote development
Real world outcome
1. Connect to Remote SSH host
2. Your extension works identically:
- Commands execute (on remote)
- File operations work (on remote filesystem)
- Tree views populate (with remote data)
- Terminal commands run (in remote shell)
3. UI-only features stay local:
- Webviews render locally
- Notifications show locally
- Status bar appears locally
4. Works in GitHub Codespaces too
Implementation Hints
Key is extensionKind in package.json:
"ui": Runs in local UI extension host (webviews, notifications)"workspace": Runs in remote workspace extension host (file access, terminals)["ui", "workspace"]: Can run in either, VSCode decides based on capabilities needed
For hybrid extensions, you may need two extensions or careful capability detection.
Pseudo code for remote-aware extension:
// Check if running remotely
function isRemote(): boolean {
return vscode.env.remoteName !== undefined
}
// Get workspace folder (works locally and remotely)
function getWorkspaceRoot(): Uri | undefined {
return vscode.workspace.workspaceFolders?.[0]?.uri
// Returns vscode-remote://ssh-remote+host/path/to/folder for SSH
}
// Read file (works with remote URIs)
async function readConfig(): Promise<Config> {
const root = getWorkspaceRoot()
const configUri = Uri.joinPath(root, '.myconfig')
// workspace.fs works with remote URIs automatically
const content = await workspace.fs.readFile(configUri)
return JSON.parse(content.toString())
}
// Spawn process (runs on remote in remote scenarios)
async function runBuild() {
// This terminal runs on the remote host
const terminal = vscode.window.createTerminal('Build')
terminal.sendText('npm run build')
terminal.show()
}
// For UI-only operations, explicitly run locally
async function showDashboard() {
// Webview panels are always local (UI side)
const panel = vscode.window.createWebviewPanel(...)
// But data fetching should go through workspace extension
// Use commands to communicate between UI and workspace
const data = await vscode.commands.executeCommand('myext.getData')
updateWebview(panel, data)
}
package.json:
{
"extensionKind": ["workspace"],
"capabilities": {
"virtualWorkspaces": true,
"untrustedWorkspaces": {
"supported": "limited",
"description": "Some features require trust"
}
}
}
Learning milestones
- Extension activates on remote → You understand extensionKind
- File operations work remotely → You understand workspace.fs
- Commands run on remote → You understand remote execution
- UI stays responsive → You understand UI/workspace split
- Works in Codespaces → You understand cloud development
Project Comparison Table
| Project | Difficulty | Time | Depth of Understanding | Fun Factor | Business Potential |
|---|---|---|---|---|---|
| 1. Hello World Command | Beginner | Weekend | ⭐⭐ | ⭐⭐ | Resume Gold |
| 2. Word Counter Status Bar | Beginner | Weekend | ⭐⭐⭐ | ⭐⭐ | Micro-SaaS |
| 3. Snippet Inserter | Intermediate | Weekend | ⭐⭐⭐ | ⭐⭐⭐ | Micro-SaaS |
| 4. File Bookmark Manager | Intermediate | 1-2 weeks | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | Micro-SaaS |
| 5. Markdown Preview | Intermediate | 1-2 weeks | ⭐⭐⭐⭐ | ⭐⭐⭐ | Micro-SaaS |
| 6. Code Action Provider | Intermediate | 1-2 weeks | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | Service & Support |
| 7. Custom Linter | Intermediate | 1-2 weeks | ⭐⭐⭐⭐ | ⭐⭐⭐ | Service & Support |
| 8. Git Diff Decorations | Advanced | 2-3 weeks | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | Micro-SaaS |
| 9. Syntax Highlighting | Advanced | 1-2 weeks | ⭐⭐⭐⭐ | ⭐⭐⭐ | Service & Support |
| 10. Autocomplete Provider | Advanced | 1-2 weeks | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | Service & Support |
| 11. Hover Provider | Intermediate | 1 week | ⭐⭐⭐ | ⭐⭐⭐ | Micro-SaaS |
| 12. Language Server (LSP) | Expert | 1 month | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | Open Core |
| 13. Debug Adapter | Master | 1 month+ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | Open Core |
| 14. Webview Dashboard | Advanced | 2-3 weeks | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | Service & Support |
| 15. Extension Test Suite | Advanced | 1-2 weeks | ⭐⭐⭐⭐ | ⭐⭐ | Service & Support |
| 16. Remote Development | Expert | 2-3 weeks | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | Open Core |
Recommended Learning Path
For Beginners (New to VSCode Extensions)
Start with Projects 1-3 in order. This gives you:
- Extension architecture fundamentals
- Commands, status bar, and UI components
- Text editing and quick picks
Time: 2-3 weekends
For Intermediate Developers
After completing 1-3, tackle Projects 4, 5, 6, 7 in any order:
- Tree Views (data display)
- Webviews (custom UI)
- Code Actions (intelligent editing)
- Diagnostics (error reporting)
Time: 4-6 weeks
For Advanced Developers
Projects 8-11 cover the “polish” features:
- Decorations for visual feedback
- Syntax highlighting
- IntelliSense (completions, hovers)
Time: 4-6 weeks
For Expert Level
Projects 12-16 are the professional-grade features:
- Language Server Protocol
- Debug Adapter Protocol
- Advanced webviews
- Testing and CI/CD
- Remote development
Time: 2-3 months
Final Capstone Project: Full-Stack IDE for a Custom Language
File: VSCODE_EXTENSION_DEVELOPMENT_PROJECTS.md Main Programming Language: TypeScript Alternative Programming Languages: Rust (for LSP server), Go Coolness Level: Level 5: Pure Magic Business Potential: 5. The “Industry Disruptor” Difficulty: Level 5: Master Knowledge Area: Full IDE Implementation Software or Tool: LSP, DAP, TextMate, Webview Main Book: “Engineering a Compiler” by Keith D. Cooper
What you’ll build
A complete IDE experience for a custom programming language or DSL, combining everything you’ve learned: syntax highlighting, semantic tokens, completions, hover, go-to-definition, diagnostics, code actions, debugging, project-level features, and a rich documentation webview.
Why it teaches VSCode Extensions
This is the culmination of your learning. You’ll integrate all the pieces—language analysis, LSP, DAP, and UI—into a cohesive product. This is what companies build when they need first-class tooling for their internal languages.
Core challenges you’ll face
- Language design (grammar, semantics, type system) → maps to language theory
- Parser/analyzer implementation (AST, symbol table, type checking) → maps to compiler construction
- LSP feature completeness (all major capabilities) → maps to protocol mastery
- DAP integration (stepping, breakpoints, inspection) → maps to debugger integration
- Project-level features (workspace symbols, project-wide rename) → maps to scalable analysis
Key Concepts
- All previous project concepts combined
- Compiler Design: “Engineering a Compiler” by Keith D. Cooper
- Language Design: “Language Implementation Patterns” by Terence Parr
- Parser Generators: Tree-sitter or ANTLR
Difficulty: Master Time estimate: 3-6 months Prerequisites: All 16 previous projects completed
Real world outcome
For a custom configuration language (like Terraform but simplified):
1. Create new .infra file - syntax highlighting works
2. Type incomplete code - real-time error highlighting
3. Type "resource." - completions show: aws_instance, aws_bucket, etc.
4. Hover over "aws_instance" - shows full documentation
5. Cmd+click on "module.vpc" - jumps to module definition
6. F2 on variable name - renames across all files
7. F5 to "dry-run" - debug adapter shows execution plan step by step
8. Errors panel shows - "Missing required field: region"
9. Lightbulb offers - "Add region field with default value"
10. Sidebar webview shows - project structure, resource graph, cost estimates
Implementation Hints
Architecture:
myinfra-extension/
├── client/ # VSCode extension (client)
│ ├── src/extension.ts
│ └── webview/ # Project dashboard
├── server/ # Language Server
│ ├── src/server.ts
│ ├── src/parser.ts # Parse .infra files
│ ├── src/analyzer.ts # Semantic analysis
│ ├── src/completions.ts # Completion logic
│ └── src/diagnostics.ts # Error reporting
├── debugger/ # Debug Adapter
│ ├── src/debugAdapter.ts
│ └── src/runtime.ts # Infra execution simulation
├── syntaxes/ # TextMate grammar
│ └── infra.tmLanguage.json
└── schemas/ # Resource type definitions
└── aws.json
This project ties together:
- TextMate Grammar (Project 9) for initial highlighting
- Language Server (Project 12) for intelligence
- Debug Adapter (Project 13) for dry-run debugging
- Webview Dashboard (Project 14) for visualization
- Test Suite (Project 15) for quality
- Remote Support (Project 16) for cloud use
Learning milestones
- Syntax highlighting complete → Grammar mastery
- All LSP features working → Protocol mastery
- Debugging works → DAP mastery
- Dashboard visualizes project → UI integration mastery
- Published to marketplace → Professional extension development mastery
Essential Resources
Official Documentation
Books
- “Visual Studio Code: End-to-End Editing and Debugging Tools” by Bruce Johnson
- “Visual Studio Code Distilled” by Alessandro Del Sole
- “Language Implementation Patterns” by Terence Parr
- “Engineering a Compiler” by Keith D. Cooper
Online Resources
Community
Summary
| # | Project | Main Language |
|---|---|---|
| 1 | Hello World Command Extension | TypeScript |
| 2 | Word Counter Status Bar Extension | TypeScript |
| 3 | Snippet Inserter with Quick Pick | TypeScript |
| 4 | File Bookmark Manager | TypeScript |
| 5 | Markdown Preview with Custom Styles | TypeScript |
| 6 | Code Action Provider (Quick Fixes) | TypeScript |
| 7 | Custom Diagnostic Provider (Linter) | TypeScript |
| 8 | Git Diff Decoration Extension | TypeScript |
| 9 | Custom Language Syntax Highlighting | TypeScript + JSON (TextMate) |
| 10 | Autocomplete Provider (IntelliSense) | TypeScript |
| 11 | Hover Information Provider | TypeScript |
| 12 | Simple Language Server (LSP) | TypeScript |
| 13 | Custom Debug Adapter | TypeScript |
| 14 | Webview-Based Dashboard Extension | TypeScript + React/Svelte |
| 15 | Extension Test Suite | TypeScript |
| 16 | Remote Development Extension | TypeScript |
| Final | Full-Stack IDE for Custom Language | TypeScript (+ Rust for perf) |
This learning path will take you from “I’ve never built an extension” to “I can build professional IDE-grade tooling.” Each project builds on the previous ones, ensuring you understand not just the APIs but the underlying concepts that make great extensions.