Learning exercise turning the Getting Started tutorial on Visual Studio Code into a prompt quiz extension.
- VS Code Quick Reference Documentation
Document Navigation:
Use yeoman temporarily.
npx --package yo --package generator-code -- yo codeUse yeoman globally.
npm install --global yo generator-code
yo code .Note
Use yo code . instead of yo code to create in current directory.
Update package.json and src/extension.ts.
`package.json`
{
"name": "exercise-vscode-extension",
"displayName": "exercise-vscode-extension",
"description": "Extension that works as a customizable learning exercise.",
"version": "0.0.1",
"engines": {
"vscode": "^1.106.1"
},
"categories": [
"Other"
],
"activationEvents": [],
"main": "./out/extension.js",
"contributes": {
"commands": [
{
"command": "exercise-vscode-extension.exercise",
"title": "exercise"
}
]
},
"scripts": {
"vscode:prepublish": "npm run compile",
"compile": "tsc -p ./",
"watch": "tsc -watch -p ./",
"pretest": "npm run compile && npm run lint",
"lint": "eslint src",
"test": "vscode-test"
},
"devDependencies": {
"@types/vscode": "^1.106.1",
"@types/mocha": "^10.0.10",
"@types/node": "22.x",
"typescript-eslint": "^8.46.3",
"eslint": "^9.39.1",
"typescript": "^5.9.3",
"@vscode/test-cli": "^0.0.12",
"@vscode/test-electron": "^2.5.2"
}
}`src/extension.ts`
// extension
// Use a 'Hello world' variation to get started.
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
const disposable = vscode.commands.registerCommand("exercise-vscode-extension.exercise", () => {
vscode.window.showInformationMessage("It worked");
});
context.subscriptions.push(disposable);
}
export function deactivate() {}Open src/extension.ts, then press F5 or in command palette (Ctrl+Shift+P) run Debug: Start Debugging.
In the debug window run exercise from the command palette (Ctrl+Shift+P). A window with the text "It worked" will appear.
Three concepts for a VS Code extension:
- Activation Events
onCommandinpackage.jsonascommand.
- Contribution Points
contributes.commandsinpackage.jsonmakes eachcommandnested incommandsavailable from command palette.
- VS Code API
commands.registerCommandbinds function to registered command inpackage.json.
"contributes": {
"commands": [
{
"command": "exercise-vscode-extension.exercise",
"title": "exercise"
}
]
} vscode.commands.registerCommand("command", () => {});Every extension exports two functions:
-
activate(context: vscode.ExtensionContext)- Called when activation event occurs- Register commands, providers, and event handlers
- Subscribe disposables to
context.subscriptions - One-time initialization code
-
deactivate()- Called when extension is disabled/uninstalled- Cleanup resources
- Save state if needed
- Return
Promiseif async cleanup required
.
├── .vscode/
│ ├── launch.json // Debugging configuration
│ └── tasks.json // Build tasks
├── src/
│ ├── extension.ts // Entry point
│ ├── data.json // Quiz data
│ └── test/
│ └── extension.test.ts
├── out/ // Compiled JavaScript (generated)
├── package.json // Extension manifest
├── tsconfig.json // TypeScript config
└── README.md
| API | Purpose | Example |
|---|---|---|
vscode.commands.registerCommand() |
Register command | registerCommand('exercise', () => {}) |
vscode.window.showInputBox() |
Get user text input | await showInputBox({ prompt: '...' }) |
vscode.window.showInformationMessage() |
Show info notification | showInformationMessage('Success!') |
vscode.window.showWarningMessage() |
Show warning notification | showWarningMessage('Incorrect') |
vscode.window.showErrorMessage() |
Show error notification | showErrorMessage('File not found') |
Run tests with:
npm testThe extension includes:
- Unit tests in
src/test/extension.test.ts - Integration tests with VS Code test runner
- ESLint for code quality
The test suite demonstrates essential patterns for VS Code extension testing.
Tests use the Mocha framework with suite() and test() functions:
import * as assert from 'assert';
import * as vscode from 'vscode';
suite('Extension Test Suite', () => {
vscode.window.showInformationMessage('Test Started');
test('Test Name', () => {
assert.strictEqual(x, y);
});
});assert.strictEqual(actual, expected) - Checks strict equality (===)
assert.strictEqual("a", "a"); // Passes
assert.strictEqual(0, [1, 2, 3].indexOf(1)); // PassesParameter order doesn't matter - both work identically:
assert.strictEqual([1, 2, 3].indexOf(1), 0);
assert.strictEqual(0, [1, 2, 3].indexOf(1));assert.ok(condition, message) - Validates boolean condition
assert.ok(2 > 1, "This should not output."); // Passes silently
assert.ok(!(2 < 1), "This would output on failure."); // PassesUse !() to negate conditions when testing for false cases.
The quiz simulation test mocks showInputBox() to automate user interaction:
const originalShowInputBox = vscode.window.showInputBox;
const inputs = ['yes', 'answer1', 'answer2', 'exit'];
let inputIndex = 0;
vscode.window.showInputBox = async (options?: vscode.InputBoxOptions): Promise<string | undefined> => {
if (inputIndex >= inputs.length) {
return undefined;
}
const response = inputs[inputIndex++];
await new Promise(resolve => setTimeout(resolve, 10));
return response;
};
try {
await vscode.commands.executeCommand('exercise-vscode-extension.exercise');
// Verify behavior
} finally {
vscode.window.showInputBox = originalShowInputBox; // Restore
}Key Points:
- Store original function reference
- Replace with mock implementation
- Return predefined sequence of inputs
- Always restore original in
finallyblock
For tests that need to wait for async completion without hardcoded timeouts:
- Use
awaitat end of test.- This pattern eliminates race conditions and hardcoded timeouts.
- Use a helper function, and call at end of test.
Note
Using both will also work.
Using await:
// Before suite - declare tracker
let testCompletionResolve: (() => void) | null = null;
const testCompletionPromise = new Promise<void>((resolve) => {
testCompletionResolve = resolve;
});
// In last test's finally block - signal completion
if (testCompletionResolve) {
testCompletionResolve();
}
// In async IIFE - wait for completion
(async () => {
await testCompletionPromise;
console.log("All tests complete!");
})();Using Helper Function:
// Signal that all tests are complete
if (testCompletionResolve) {
testCompletionResolve();
helperFunction();
}The quiz simulation test demonstrates:
- Loading external JSON data
- Randomly selecting incorrect answers
- Tracking test metrics
- Extended timeouts for long-running tests
test('Quiz simulation with 90% correct answers', async function() {
this.timeout(30000); // 30 seconds for full quiz
const totalQuestions = quizData.quiz.length;
const targetCorrect = Math.floor(totalQuestions * 0.9);
const incorrectCount = totalQuestions - targetCorrect;
// Randomly select which questions to answer incorrectly
const incorrectIndices = new Set<number>();
while (incorrectIndices.size < incorrectCount) {
incorrectIndices.add(Math.floor(Math.random() * totalQuestions));
}
// Build input sequence with correct/incorrect answers
quizData.quiz.forEach((question, index) => {
if (incorrectIndices.has(index)) {
inputSequence.push('wrong answer');
} else {
inputSequence.push(question.answer);
}
});
});Use console output for debugging details visible in the terminal:
console.log(`Quiz Results:`);
console.log(` Total Questions: ${totalQuestions}`);
console.log(` Expected Correct: ${expectedCorrect}`);Use vscode.window.showInformationMessage() for messages visible in the test UI.
For this exercise a helper function was created for dual outputs:
dualLog() Helper Function:
function dualLog(msg: string): void {
if (msg !== "") {
vscode.window.showInformationMessage(msg);
}
console.log(` ${msg}`);
}Execute tests from command palette or terminal:
npm testOr press F5 with test file open to debug tests interactively.
This extension evolves from a simple "Hello World" to an interactive quiz system that teaches VS Code extension development concepts.
The extension uses vscode.window.showInputBox() to create an interactive quiz experience:
const userAnswer = await vscode.window.showInputBox({
prompt: `Question ${currentQuestionIndex + 1} of ${totalQuestions}`,
placeHolder: currentQuestion.question,
ignoreFocusOut: true,
validateInput: (text) => {
if (!text || text.trim() === '') {
return 'Please enter an answer or type "exit" to quit, "hint" for a hint';
}
return null;
}
});Key Learning Points:
showInputBox()returns aPromise<string | undefined>validateInputprovides real-time validationignoreFocusOutkeeps the input box open when clicking elsewhere- Returns
undefinedwhen user cancels (Escape key)
The extension reads quiz data from data.json:
import * as fs from 'fs';
import * as path from 'path';
const dataPath = path.join(__dirname, '..', 'src', 'data.json');
const quizData: QuizData = JSON.parse(fs.readFileSync(dataPath, 'utf8'));Key Learning Points:
__dirnamepoints to the compiledout/folder- Navigate to source files using
path.join() - TypeScript interfaces define data structures
- Node.js
fsmodule for file operations
The quiz implements recursive async functions and user feedback:
const askQuestion = async () => {
if (currentQuestionIndex >= totalQuestions) {
// Quiz completed
return;
}
const userAnswer = await vscode.window.showInputBox({ /* ... */ });
// Handle special commands
if (userAnswer === 'exit') return;
if (userAnswer === 'hint') {
// Show hint and re-ask
await askQuestion();
return;
}
// Check answer and continue
currentQuestionIndex++;
setTimeout(() => askQuestion(), 500);
};Key Learning Points:
- Recursive async/await patterns
- Command handling (
exit,hint) - Progressive disclosure of information
- User feedback with
showInformationMessage()andshowWarningMessage()
Quiz questions are stored in src/data.json:
{
"quiz": [
{
"id": 1,
"question": "What are the two main functions exported by an extension entry file?",
"answer": "activate and deactivate",
"hints": ["One is called when the extension is activated", "One is called when the extension is deactivated"],
"category": "Extension Anatomy"
}
]
}Key Learning Points:
- JSON as configuration/data storage
- TypeScript interfaces for type safety
- Separation of code and data
- Extensible quiz system
- Extension Anatomy - Understand extension structure
- Extension Capabilities - What extensions can do
- VS Code API Reference - Complete API documentation
- Extension Guides - Topic-specific guides
- UX Guidelines - Best practices for UI
Explore the vscode-extension-samples repository for examples:
- Hello World - Basic command registration to get started
- Tree View - Custom explorer views
- Webview and Webview Example - HTML-based UIs in VS Code
- Language Service - Language support such as syntax highlighting
- Snippet Sampler - Snippet guide
- QuickInput Sample - what this exercise kind of started from
To publish your extension to the VS Code Marketplace:
-
Install
vsce(Visual Studio Code Extensions):npm install -g @vscode/vsce
-
Package your extension:
vsce package
-
Publish (requires publisher account):
vsce publish
Learn more: Publishing Extensions
Enhanced the current learning exercise extension, making it viably publishable. The final results are at prompt-quiz.
See LICENSE file for details.
