← Back to all projects

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 activated
  • deactivate(): 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 .vsix files with vsce
  • 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.json contribution 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

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:

  1. User triggers command → VSCode checks package.json for activation events
  2. If extension not activated, activate() is called
  3. Inside activate(), you register command handlers with vscode.commands.registerCommand(commandId, callback)
  4. Callback executes → show information message with vscode.window.showInformationMessage()
  5. 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

  1. Extension runs in Development Host → You understand the F5 debugging workflow
  2. Command appears in Command Palette → You understand contribution points
  3. Notification and output work → You understand the Window API basics
  4. 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

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:

  1. Create status bar item in activate() with vscode.window.createStatusBarItem(alignment, priority)
  2. Subscribe to onDidChangeActiveTextEditor → when editor changes, recalculate
  3. Subscribe to onDidChangeTextDocument → when content changes, recalculate
  4. 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

  1. Status bar item appears → You understand createStatusBarItem
  2. Count updates on typing → You understand document change events
  3. Count updates on file switch → You understand editor change events
  4. 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() with TextEdit) → 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

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:

  1. Get current active editor
  2. Get cursor position: editor.selection.active
  3. Call editor.edit(editBuilder => editBuilder.insert(position, text))
  4. 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

  1. Quick Pick shows with all options → You understand modal UI APIs
  2. Text inserts at cursor → You understand TextEditor.edit
  3. Tabstops work ($1, $2) → You understand SnippetString
  4. 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 (onDidChangeTreeData event) → 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

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:

  1. Contribution Point in package.json: declare view container and view
  2. TreeDataProvider class: implements getTreeItem(element) and getChildren(element)
  3. TreeItem class: represents each node with label, icon, command, contextValue
  4. Refresh mechanism: fire onDidChangeTreeData event when data changes

Storage:

  • context.workspaceState.get(key) / .update(key, value) for workspace-specific data
  • context.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

  1. View appears in sidebar → You understand view containers and contribution points
  2. Tree populates with items → You understand TreeDataProvider
  3. Clicking navigates to location → You understand TreeItem commands
  4. Data persists across sessions → You understand workspaceState
  5. 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

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

  1. Webview panel opens → You understand createWebviewPanel
  2. Markdown renders as HTML → You understand webview.html
  3. CSS loads correctly → You understand asWebviewUri and CSP
  4. Preview updates on typing → You understand document sync
  5. 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

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

  1. Lightbulb appears on console.log → You understand provider registration
  2. Action replaces code correctly → You understand WorkspaceEdit
  3. Works with selections → You understand range handling
  4. 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

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

  1. Squiggly lines appear → You understand DiagnosticCollection.set
  2. Problems panel populates → You understand Diagnostic structure
  3. Updates in real-time → You understand document event subscriptions
  4. 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

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 lines
  • git 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

  1. Gutter colors appear → You understand TextEditorDecorationType
  2. Git data fetched correctly → You understand child_process
  3. Blame shows on hover → You understand HoverProvider
  4. 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

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 objects
  • repository: named patterns for reuse

Pattern types:

  • match + name: single regex match
  • begin/end + patterns: multi-line constructs
  • include: 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

  1. File recognized as custom language → You understand language contributions
  2. Basic highlighting works → You understand match patterns
  3. Nested constructs highlight → You understand begin/end patterns
  4. Bracket matching works → You understand language configuration
  5. 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

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

  1. Completions appear in menu → You understand CompletionItemProvider
  2. Filtering works → You understand completion matching
  3. Documentation shows on hover → You understand resolveCompletionItem
  4. Only triggers in right context → You understand context detection
  5. 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

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;">&nbsp;</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

  1. Hover popup appears → You understand HoverProvider
  2. Content formats correctly → You understand MarkdownString
  3. Works for your domain → You understand token detection
  4. 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

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:

  1. Server (standalone Node.js process): handles language logic
  2. 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

  1. Server starts with extension → You understand client-server spawning
  2. Diagnostics appear → You understand document sync and validation
  3. Completions work → You understand onCompletion handler
  4. Go-to-definition works → You understand location responses
  5. 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

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

  1. Debug session starts → You understand DAP initialization
  2. Breakpoints set and hit → You understand breakpoint management
  3. Step controls work → You understand execution control
  4. Variables show values → You understand scopes and variables
  5. 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

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

  1. Webview renders React app → You understand bundling and CSP
  2. Data flows from extension → You understand postMessage
  3. Click navigates to file → You understand bidirectional messaging
  4. State persists on reopen → You understand webview state
  5. 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

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

  1. Tests run locally → You understand vscode-test
  2. Tests access VSCode APIs → You understand integration testing
  3. Tests pass in CI → You understand headless execution
  4. Auto-publish works → You understand CD pipeline
  5. 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

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

  1. Extension activates on remote → You understand extensionKind
  2. File operations work remotely → You understand workspace.fs
  3. Commands run on remote → You understand remote execution
  4. UI stays responsive → You understand UI/workspace split
  5. 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

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:

  1. TextMate Grammar (Project 9) for initial highlighting
  2. Language Server (Project 12) for intelligence
  3. Debug Adapter (Project 13) for dry-run debugging
  4. Webview Dashboard (Project 14) for visualization
  5. Test Suite (Project 15) for quality
  6. Remote Support (Project 16) for cloud use

Learning milestones

  1. Syntax highlighting complete → Grammar mastery
  2. All LSP features working → Protocol mastery
  3. Debugging works → DAP mastery
  4. Dashboard visualizes project → UI integration mastery
  5. 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.