Visual Studio Code is built with a layered architecture using TypeScript, web APIs and Electron, combining web technologies with native app capabilities. The codebase is organized into key architectural layers:
src/: Main TypeScript source code with unit tests insrc/vs/*/test/foldersbuild/: Build scripts and CI/CD toolsextensions/: Built-in extensions that ship with VS Codetest/: Integration tests and test infrastructurescripts/: Development and build scriptsresources/: Static resources (icons, themes, etc.)out/: Compiled JavaScript output (generated during build)
src/vs/base/- Foundation utilities and cross-platform abstractionssrc/vs/platform/- Platform services and dependency injection infrastructuresrc/vs/editor/- Text editor implementation with language services, syntax highlighting, and editing featuressrc/vs/workbench/- Main application workbench for web and desktopworkbench/browser/- Core workbench UI components (parts, layout, actions)workbench/services/- Service implementationsworkbench/contrib/- Feature contributions (git, debug, search, terminal, etc.)workbench/api/- Extension host and VS Code API implementation
src/vs/code/- Electron main process specific implementationsrc/vs/server/- Server specific implementationsrc/vs/sessions/- Agent sessions window, a dedicated workbench layer for agentic workflows (sits alongsidevs/workbench, may import from it but not vice versa)
The core architecture follows these principles:
- Layered architecture - from
base,platform,editor, toworkbench - Dependency injection - Services are injected through constructor parameters
- If non-service parameters are needed, they need to come before the service parameters
- Contribution model - Features contribute to registries and extension points
- Cross-platform compatibility - Abstractions separate platform-specific code
The extensions/ directory contains first-party extensions that ship with VS Code:
- Language support -
typescript-language-features/,html-language-features/,css-language-features/, etc. - Core features -
git/,debug-auto-launch/,emmet/,markdown-language-features/ - Themes -
theme-*folders for default color themes - Development tools -
extension-editing/,vscode-api-tests/
Each extension follows the standard VS Code extension structure with package.json, TypeScript sources, and contribution points to extend the workbench through the Extension API.
- Semantic search first: Use file search for general concepts
- Grep for exact strings: Use grep for error messages or specific function names
- Follow imports: Check what files import the problematic module
- Check test files: Often reveal usage patterns and expected behavior
Choose validation based on the scope and risk of the change. Large-scale builds and typechecking can be slow, and consume significant resources, so minimize their use. Prefer existing editor or watch-task diagnostics and the smallest targeted tests that cover the changed behavior. Do not start build or watch tasks, run broad type checks, or make type checking a prerequisite for targeted tests solely as a completion ritual.
When running in a VS Code editor window with a workspace folder, use the VS Code task tools for build and watch workflows: inspect the existing task output first, and run an existing task instead of invoking its equivalent shell command. Do not start a duplicate build or watch process when the workspace task already provides current diagnostics. Agents window chats and isolated worktree sessions may not have access to the editor's workspace tasks; use the repository commands directly in those contexts.
Run a targeted type check or build when you are not fully confident in the change, and the change is broad or cross-cutting, it affects build or type configuration, or another validation step reports a compilation problem. When task tools are unavailable or no suitable task exists, useful commands include:
pnpm run typecheck-clientfor the main sources undersrc/pnpm run gulp compile-extensionsfor built-in extensionspnpm run typecheckfrom thebuildfolder for build tooling
Development compile tasks already type-check their inputs. Do not run pnpm run typecheck-client immediately before pnpm run compile or pnpm run compile-client; choose the command that covers the required validation. When tests only need fresh client output, use pnpm run build-fast --client-only. It supports cold builds and file-granular incremental updates without starting a watcher or building extensions. Use pnpm run build-fast when extension and Copilot outputs are also needed. The client and extension JavaScript lanes transpile rather than type-check; extension media retains its existing type checks. Run explicit type checks when the change warrants them. Reserve pnpm run transpile-client for an intentional clean client rebuild.
Reuse successful validation while its relevant inputs remain unchanged, including when moving into review, commit, or PR creation. Batch related test selectors in one invocation. Prefer pnpm run eslint <changed files> for targeted linting; this wrapper caches results by file content. The repository installs dependencies with pnpm (the packageManager field of package.json pins the version); never install them with npm or yarn. Install dependencies only when dependency inputs change or validation identifies missing dependencies; run pnpm install for a needed refresh, which takes a few seconds when nothing changed. Never share or symlink node_modules between worktrees.
Use scripts/test.sh (or scripts\test.bat on Windows) for unit tests and scripts/test-integration.sh (or scripts\test-integration.bat on Windows) for integration tests. Add a targeted selector such as --grep whenever possible. Run pnpm run valid-layers-check only when a change may affect module layering.
- Do not hard-wrap Markdown prose to a fixed line width. Keep each paragraph on one source line and let the editor or renderer wrap it naturally. Preserve structural line breaks in lists, tables, code blocks, and intentional Markdown hard breaks.
We use tabs, not spaces.
- Use PascalCase for
typenames - Use PascalCase for
enumvalues - Use camelCase for
functionandmethodnames - Use camelCase for
propertynames andlocal variables - Use whole words in names when possible
- Do not export
typesorfunctionsunless you need to share it across multiple components - Do not introduce new
typesorvaluesto the global namespace
- Use JSDoc style comments for
functions,interfaces,enums, andclasses
- Use "double quotes" for strings shown to the user that need to be externalized (localized)
- Use 'single quotes' otherwise
- All strings visible to the user need to be externalized using the
vs/nlsmodule - Externalized strings must not use string concatenation. Use placeholders instead (
{0}).
- Use title-style capitalization for command labels, buttons and menu items (each word is capitalized).
- Don't capitalize prepositions of four or fewer letters unless it's the first or last word (e.g. "in", "with", "for").
- When creating, editing, or reviewing any visual surface, reason in design terms, not pixels: name the feeling (Calm, Focused, Consistent, Delightful), find the principle it breaks, then reach for the move (token/tier/ramp) that restores it. Describe a bug by its role/tier/ramp (e.g. "this overlay is rounded at the control tier"), not its number.
- See the
design-philosophyskill for the full Values→Principles→Moves vocabulary, worked examples, and feedback guidance, and design-tokens.instructions.md for the token reference.
- Use arrow functions
=>over anonymous function expressions - Only surround arrow function parameters when necessary. For example,
(x) => x + xis wrong but the following are correct:
x => x + x
(x, y) => x + y
<T>(x: T, y: T) => x === y- Always surround loop and conditional bodies with curly braces
- Open curly braces always go on the same line as whatever necessitates them
- Parenthesized constructs should have no surrounding whitespace. A single space follows commas, colons, and semicolons in those constructs. For example:
for (let i = 0, n = str.length; i < 10; i++) {
if (x < 10) {
foo();
}
}
function f(x: number, y: string): void { }- Whenever possible, in top-level scopes, use
export function x(…) {…}instead ofexport const x = (…) => {…}. One advantage of using thefunctionkeyword is that the stack trace shows a good name when debugging.
- All files must include Microsoft copyright header
- Prefer
asyncandawaitoverPromiseandthencalls - All user facing messages must be localized using the applicable localization framework (for example
nls.localize()method) - Don't add tests to the wrong test suite (e.g., adding to end of file instead of inside relevant suite)
- Look for existing test patterns before creating new structures
- Use
describeandtestconsistently with existing patterns - Prefer regex capture groups with names over numbered capture groups.
- If you create any temporary new files, scripts, or helper files for iteration, clean up these files by removing them at the end of the task
- Never duplicate imports. Always reuse existing imports if they are present.
- When removing an import, do not leave behind blank lines where the import was. Ensure the surrounding code remains compact.
- Do not use
anyorunknownas the type for variables, parameters, or return values unless absolutely necessary. If they need type annotations, they should have proper types or interfaces defined. - When adding file watching, prefer correlated file watchers (via fileService.createWatcher) to shared ones.
- When adding tooltips to UI elements, prefer the use of IHoverService service.
- Do not duplicate code. Always look for existing utility functions, helpers, or patterns in the codebase before implementing new functionality. Reuse and extend existing code whenever possible.
- You MUST deal with disposables by registering them immediately after creation for later disposal. Use helpers such as
DisposableStore,MutableDisposableorDisposableMap. Do NOT register a disposable to the containing class if the object is created within a method that is called repeatedly to avoid leaks. Instead, return anIDisposablefrom such method and let the caller register it. - You MUST NOT use storage keys of another component only to make changes to that component. You MUST come up with proper API to change another component.
- Use
IEditorServiceto open editors instead ofIEditorGroupsService.activeGroup.openEditorto ensure that the editor opening logic is properly followed and to avoid bypassing important features such asrevealIfOpenedorpreserveFocus. - Avoid using
bind(),call()andapply()solely to controlthisor partially apply arguments; prefer arrow functions or closures to capture the necessary context, and use these methods only when required by an API or interoperability. - Avoid using events to drive control flow between components. Instead, prefer direct method calls or service interactions to ensure clearer dependencies and easier traceability of logic. Events should be reserved for broadcasting state changes or notifications rather than orchestrating behavior across components.
- Service dependencies MUST be declared in constructors and MUST NOT be accessed through the
IInstantiationServiceat any other point in time.
This repository is public, and so is everything written in it: pull requests, issues, comments, reviews, commit messages, code, tests and documentation. The instructions above cover the code; this section covers the text around it.
- Don't include confidential information: credentials or tokens, personal or customer data, links to internal-only tools and dashboards, or data from internal sources such as telemetry queries, experiment scorecards and cost reports.
- Anything derived from internal data is confidential too: counts of users, devices, sessions or requests; rates, shares and deltas; statistical results; cost, token usage and capacity figures; and screenshots of queries or dashboards. Telemetry event and property names, and the shape of a query, are fine.
- When internal data informs a change, describe the conclusion without numbers, for example "the error is common on Windows".
- A pull request, commit or issue that ships, changes, graduates or removes an experiment describes only the code change. Don't explain the change with the experiment, its rollout or its results, even without numbers, such as "the experiment showed no regression" or "the experiment is shipping". Link its tracking issue instead.
- Numbers you measured yourself, for example in a local run, a test or a benchmark, are fine as long as they aren't cost figures. So are planned settings such as a rollout percentage.
- Check the text of a pull request, issue, comment or commit message against these rules before you publish it. Commit messages can't be changed after they are merged.
- Minimize the amount of assertions in tests. Prefer one snapshot-style
assert.deepStrictEqualover multiple precise assertions, as they are much more difficult to understand and to update. - Do not stub a global object (e.g.
(mainWindow as any).ResizeObserver = ...) or useanycasts to install fakes in tests. Instead, make the dependency injectable: add an optional constructor parameter on the production class that defaults to the real implementation (e.g.dom.DisposableResizeObserver'sresizeObserverCtoroption, which defaults totargetWindow.ResizeObserver), and have the test pass a fake that implements the real interface.