Layout of src/
Features and activate()
Each feature exports activate(): void | (() => void). It registers actions, tool windows, status items, editor contributions and services, and returns a disposer that undoes all of it. src/features/index.ts lists the features in activation order and activates each inside a try, so one feature that throws doesn’t stop the others. src/workbench/activate.ts runs first and provides the shell services (tool windows, notifications) that features rely on.
A whole feature can be this small:
src/features/logcat/index.ts
lazyComponent) so they don’t weigh on startup; pnpm perf budgets the initial bundle.
Feature isolation
Features never import each other. They communicate through three channels:- Actions:
executeAction("project.dontOpen")runs another feature’s behaviour by its stable id, without importing it. - Contributions: a feature registers into an extension point in
src/workbench/contributions.ts(orsrc/editor/api.ts); whoever renders that point doesn’t know who contributed. - Services: cross-feature services are provided once and looked up by accessor (below).
platform/, ui/, workbench/ or editor/api, not into one feature that the others import.
Actions
Everything a user can invoke is an action with a stable, dotted id (src/platform/actions.ts). Menus, keybindings, toolbar buttons, context menus and the Actions tab of Search Everywhere all resolve to the same registry, so a feature registers its behaviour once:
Run actions with
executeAction(id, arg?). registerAction returns a disposer.
Keymaps and the dispatcher
Default shortcuts live insrc/platform/keymaps/, never on the action. A keymap maps action ids to bindings:
src/platform/keymaps/intellij-macos.ts
intellij-macos (default on macOS), intellij-windows (default on Windows and Linux), vscode-macos, vscode-windows, eclipse-macos, eclipse-windows. When you add an action with a default shortcut, add it to both IntelliJ presets, and to the others where an equivalent exists.
Binding syntax: modifiers Meta Ctrl Alt Shift joined with + to a key (Meta+Shift+O); a space separates chord strokes (Meta+K Meta+C); a doubled modifier is Shift Shift; mouse buttons are Button1…Button5. Keys are matched by physical position (KeyboardEvent.code).
installKeyboardDispatcher listens to keydown in the capture phase so IDE shortcuts win over CodeMirror’s defaults, resolves the stroke against the active keymap plus the user’s overrides, and runs the first enabled action (when and keyWhen). A component that needs a keystroke for itself registers a key claim (registerKeyClaim), e.g. Vim mode for its shortcut conflicts.
Context keys and when clauses
Components publish facts as context keys (src/platform/context.ts); actions and keybindings are enabled by a when expression over them.
||, &&, !, parentheses, and key, key == value, key != value. An empty clause is always true. Clauses are compiled once and cached. Plugin menu items use the same clauses.
Contributions
src/workbench/contributions.ts holds the workbench’s extension points. Each register* function returns a disposer.
Editors are extended through
src/editor/api.ts: registerEditorContribution({ id, extensions }) adds CodeMirror extensions per document. editor/core owns the views; LSP, VCS gutters, breakpoints and inline debug values are all contributions and never import editor internals.
Plugin UI contributions reach these same functions: the backend validates them, and src/features/plugins/projection.ts maps each one onto an existing register* call. See Plugin host.
Services
Services are cross-feature APIs with a single implementation, provided by whoever owns it:
The non-
maybe accessors throw if nothing provided the service, so call them at use time, not at module load.
Talking to the backend
Features call the backend only throughcommands from @/platform/ipc and listen with onIdeEvent; see IPC. Several features also keep an in-memory backend for the plain-browser mode (pnpm dev) that unit and E2E tests use, chosen by isTauri().