# Coding Guidelines ## Indentation Use 4 spaces per indentation level. ## Imports Use `organize imports` to sort imports, and make sure the imports work properly (e.g. imports from `/src/` rather than `/lib/` for *.ts files may break builds). ## Names * [1.](#pascalcase-type) Use PascalCase for `type` names. * [2.](#pascalcase-enum) Use PascalCase for `enum` values. * [3.](#camelcase-fn) Use camelCase for `function` and `method` names. * [4.](#camelcase-var) Use camelCase for `property` names and `local variables`. * [5.](#whole-words-names) Use whole words in names when possible. ```ts // bad const termWdgId = 1; // good const terminalWidgetId = 1; ``` * [6.](#lower-case-names) Use lower-case, dash-separated file names (e.g. `document-provider.ts`). * [7.](#file-name) Name files after the main type it exports. > Why? It should be easy to find a type by a file name. * [7.1](#one-large-class-per-file) Avoid one file with many large classes; put each class in its own file. > Why? It should be easy to find a class by a file name. * [8.](#unique-names) Give unique names to types and files. Use specific names to achieve it. > Why? In order to avoid duplicate records in file and type search. ```ts // bad export interface TitleButton {} // good export interface QuickInputTitleButton {} ``` * [9.](#no_underscore_private) Do not use "_" as a prefix for private properties. Exceptions: * [9.1](#underscore_accessors) Exposing a property through get/set and using underscore for the internal field. * [9.2](#underscore_json) Attaching internal data to user-visible JSON objects. * [10.](#event_names) Names of events follow the `on[Will|Did]VerbNoun?` pattern. The name signals if the event is going to happen (onWill) or already happened (onDid), what happened (verb), and the context (noun) unless obvious from the context. * [11.](#unique-context-keys) Give unique names to keybinding contexts and keys to avoid collisions at runtime. Use specific names to achieve it. ```ts // bad export namespace TerminalSearchKeybindingContext { export const disableSearch = 'hideSearch'; } // good export namespace TerminalSearchKeybindingContext { export const disableSearch = 'terminalHideSearch'; } // bad const terminalFocusKey = this.contextKeyService.createKey('focus', false); // good const terminalFocusKey = this.contextKeyService.createKey('terminalFocus', false); ``` ## Types * [1.](#no-expose-types) Do not export `types` or `functions` unless you need to share it across multiple components, [see as well](#di-function-export). * [2.](#no-global-types) Do not introduce new `types` or `values` to the global namespace. * [3.](#explicit-return-type) Always declare a return type in order to avoid accidental breaking changes because of changes to a method body. ## Interfaces/Symbols * [1.](#interfaces-no-i-prefix) Do not use `I` prefix for interfaces. Use `Impl` suffix for implementation of interfaces with the same name. See [624](https://github.com/theia-ide/theia/issues/624) for the discussion on this. * [2.](#classes-over-interfaces) Use classes instead of interfaces + symbols when possible to avoid boilerplate. ```ts // bad export const TaskDefinitionRegistry = Symbol('TaskDefinitionRegistry'); export interface TaskDefinitionRegistry { register(definition: TaskDefinition): void; } export class TaskDefinitionRegistryImpl implements TaskDefinitionRegistry { register(definition: TaskDefinition): void { } } bind(TaskDefinitionRegistryImpl).toSelf().inSingletonScope(); bind(TaskDefinitionRegistry).toService(TaskDefinitionRegistryImpl); // good export class TaskDefinitionRegistry { register(definition: TaskDefinition): void { } } bind(TaskDefinitionRegistry).toSelf().inSingletonScope(); ``` **Exceptions** * [2.1](#remote-interfaces) Remote services should be declared as an interface + a symbol in order to be used in the frontend and backend. ## Comments * Use JSDoc style comments for `functions`, `interfaces`, `enums`, and `classes` ## Strings * Use 'single quotes' for all strings that aren't [template literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals) ## null and undefined Use `undefined`; do not use `null`. ## Internationalization/Localization * [1.](#nls-localize) Always localize user-facing text with the `nls.localize(key, defaultValue, ...args)` function. > What is user-facing text? Any strings that are hard-coded (not calculated) that could be in any way visible to the user, be it labels for commands and menus, messages/notifications/dialogs, quick-input placeholders or preferences. * [1.1.](#nls-localize-args) Parameters for messages should be passed as the `args` of the `localize` function. They are inserted at the location of the placeholders - in the form of `{\d+}` - in the localized text. E.g. `{0}` will be replaced with the first `arg`, `{1}` with the second, etc. ```ts // bad nls.localize('hello', `Hello there ${name}.`); // good nls.localize('hello', 'Hello there {0}.', name); ``` * [1.2.](#nls-localize-by-default) The `nls.localizeByDefault` function automatically finds the translation key for VS Code's language packs just by using the default value as its argument and translates it into the currently used locale. If the `nls.localizeByDefault` function is not able to find a key for the supplied default value, a warning will be shown in the browser console. If there is no appropriate translation in VSCode, just use the `nls.localize` function with a new key using the syntax `theia//`. ```ts // bad nls.localize('vscode/dialogService/close', 'Close'); // good nls.localizeByDefault('Close'); ``` * [2.](#nls-utilities) Use utility functions where possible: * `Command.toLocalizedCommand` should be used when the label requires a custom localization key (using `nls.localize` internally). * `Command.toDefaultLocalizedCommand` should be used when the label and category already exist in VS Code's language packs (using `nls.localizeByDefault` internally). ```ts // bad command: Command = { label: nls.localize('theia/my-package/myCommand', 'My Custom Label'), originalLabel: 'My Custom Label' }; // good - use toLocalizedCommand with a custom localization key command = Command.toLocalizedCommand( { id: 'my-command-id', label: 'My Custom Label' }, 'theia/my-package/myCommand' ); // good - use toDefaultLocalizedCommand when the label exists in VS Code's language packs command = Command.toDefaultLocalizedCommand( { id: 'my-command-id', label: 'Close Editor' } ); ``` * [3.](#nls-rich-content-markdown) For localizing rich content (HTML), use Markdown instead of HTML strings. > Why? Markdown ensures valid, well-formed HTML and aligns with VS Code's approach for rich content (e.g., in detailed preference descriptions). Theia already supports rendering Markdown to HTML. ```tsx // bad - localizing HTML fragments individually

{nls.localize('key1', 'Title')}

{nls.localize('key2', 'First paragraph.')}

{nls.localize('key3', 'Second paragraph.')}

// bad - using dangerouslySetInnerHTML with HTML strings
Title

First paragraph.

Second paragraph.

`)) }} /> // good - using MarkdownRenderer import { MarkdownRenderer } from '@theia/core/lib/browser/markdown-rendering/markdown-renderer'; import { MarkdownString } from '@theia/core/lib/common/markdown-rendering/markdown-string'; @injectable() export class MyService { @inject(MarkdownRenderer) protected readonly markdownRenderer: MarkdownRenderer; renderWelcome(): HTMLElement { const markdownContent = nls.localize('theia/mypackage/welcomeMessage', ` # Welcome to My Feature This feature provides the following capabilities: - **Feature A**: Description of feature A - **Feature B**: Description of feature B Learn more in the [documentation](https://theia-ide.org/docs/). `); const rendered = this.markdownRenderer.render(new MarkdownString(markdownContent)); return rendered.element; } } ``` > [!NOTE] > When Markdown is not suitable and HTML must be used, ensure content is sanitized with `DOMPurify.sanitize()` before rendering with `dangerouslySetInnerHTML`. ## Style * Use arrow functions `=>` over anonymous function expressions. * Only surround arrow function parameters when necessary. For example, `(x) => x + x` is wrong, but the following are correct: ```javascript x => x + x (x,y) => x + y (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: ```javascript for (var i = 0, n = str.length; i < 10; i++) { } if (x < 10) { } function f(x: number, y: string): void { } ``` * Use a single declaration per variable statement
(i.e. use `var x = 1; var y = 2;` over `var x = 1, y = 2;`). * `else` goes on the line of the closing curly brace. ## Dependency Injection * [1.](#property-injection) Use property injection over construction injection. Adding new dependencies via the construction injection is a breaking change. * [2.](#post-construct) Use a method decorated with `postConstruct` rather than the constructor to initialize an object, for example to register event listeners. ```ts @injectable() export class MyComponent { @inject(ApplicationShell) protected readonly shell: ApplicationShell; @postConstruct() protected init(): void { this.shell.activeChanged.connect(() => this.doSomething()); } } ``` * [3.](#singleton-scope) Make sure to add `inSingletonScope` for singleton instances, otherwise a new instance will be created on each injection request. ```ts // bad bind(CommandContribution).to(LoggerFrontendContribution); // good bind(CommandContribution).to(LoggerFrontendContribution).inSingletonScope(); ``` * [4.](#di-function-export) Don't export functions, convert them into class methods. Functions cannot be overridden to change their behavior or work around a bug. ```ts // bad export function createWebSocket(url: string): WebSocket { ... } // good @injectable() export class WebSocketProvider { protected createWebSocket(url: string): WebSocket { ... } } @injectable() export class MyWebSocketProvider extends WebSocketProvider { protected createWebSocket(url: string): WebSocket { // create a web socket with custom options } } ``` **Exceptions** * [4.1](#di-convenient-function-export) Convenient functions which are based on the stable API can be exported in the corresponding namespace. In this case clients: * can customize behaviour via exchanging the API implementation * have a choice to use convenient functions or an API directly ```ts export namespace MonacoEditor { // convenient function to get a Monaco editor based on the editor manager API export function getCurrent(manager: EditorManager): MonacoEditor | undefined { return get(manager.currentEditor); } ... } ``` * [4.2](#di-json-function-export) The special case of [4.1](#di-convenient-function-export) is functions on a JSON type. JSON types are not supposed to be *implementable*, but only *instantiable*. They cannot have functions to avoid serialization issues. ```ts export interface CompositeTreeNode extends TreeNode { children: ReadonlyArray; // bad - JSON types should not have functions getFirstChild(): TreeNode | undefined; } // good - JSON types can have corresponding namespaces with functions export namespace CompositeTreeNode { export function getFirstChild(parent: CompositeTreeNode): TreeNode | undefined { return parent.children[0]; } ... } // bad - JSON types should not be implemented export class MyCompositeTreeNode implements CompositeTreeNode { ... } // good - JSON types can be extended export interface MyCompositeTreeNode extends CompositeTreeNode { ... } ``` * [4.3](#di-auxiliary-function-export) Auxiliary functions which are called from the customizable context can be exported in the corresponding namespace. ```ts @injectable() export class DirtyDiffModel { // This method can be overridden. Subclasses have access to `DirtyDiffModel.documentContentLines`. protected handleDocumentChanged(document: TextEditorDocument): void { this.currentContent = DirtyDiffModel.documentContentLines(document); this.update(); } } export namespace DirtyDiffModel { // the auxiliary function export function documentContentLines(document: TextEditorDocument): ContentLines { ... } } ``` * [5.](#di-factory-over-new) In injectable classes, avoid direct constructor calls for objects that are reasonable to customize. Use an injected factory instead. > Why? A factory can be rebound by adopters to customize the created instance. If a manager directly creates `new XYZImpl()`, adopters have to subclass and rebind the whole manager just to replace the created object. Direct construction is still fine for implementation details, for example for simple values, collections, and helper objects like `new Map()`. ```ts // bad @injectable() export class MyManager { protected createModel(): MyModelImpl { return new MyModelImpl(); } } // good export const MyModelFactory = Symbol('MyModelFactory'); export type MyModelFactory = () => MyModel; @injectable() export class MyManager { @inject(MyModelFactory) protected readonly modelFactory: MyModelFactory; protected createModel(): MyModel { return this.modelFactory(); } } ``` For classes that need runtime parameters, pass an options object to the factory. The following simplified example is inspired by the process task runner: ```ts export const TaskProcessOptions = Symbol('TaskProcessOptions'); export interface TaskProcessOptions { label: string; process: Process; } @injectable() export class ProcessTask { constructor(@inject(TaskProcessOptions) protected readonly options: TaskProcessOptions) { } } export const TaskFactory = Symbol('TaskFactory'); export type TaskFactory = (options: TaskProcessOptions) => ProcessTask; @injectable() export class ProcessTaskRunner { @inject(TaskFactory) protected readonly taskFactory: TaskFactory; protected createTask(task: TaskConfiguration, process: Process): ProcessTask { return this.taskFactory({ label: task.label, process }); } } bind(ProcessTask).toSelf(); bind(TaskFactory).toFactory(ctx => (options: TaskProcessOptions) => { const child = new Container({ defaultScope: 'Singleton' }); child.parent = ctx.container; child.bind(TaskProcessOptions).toConstantValue(options); return child.get(ProcessTask); }); ``` * [6.](#no-multi-inject) Don't use InversifyJS's `@multiInject`, use Theia's utility `ContributionProvider` to inject multiple instances. > Why? > > * `ContributionProvider` is a documented way to introduce contribution points. See `Contribution-Points`: > * If nothing is bound to an identifier, multi-inject resolves to `undefined`, not an empty array. `ContributionProvider` provides an empty array. > * Multi-inject does not guarantee the same instances are injected if an extender does not use `inSingletonScope`. `ContributionProvider` caches instances to ensure uniqueness. > * `ContributionProvider` supports filtering. See `ContributionFilterRegistry`. * [7.](#bind-root-contribution-provider) Use `bindRootContributionProvider` instead of `bindContributionProvider` when binding contribution providers in the main (root) container. `bindContributionProvider` captures a reference to whichever container first resolves the provider. If that container is a child (e.g. created for a widget or a transient binding), the provider will permanently retain that child container and everything cached in it, causing a memory leak. `bindRootContributionProvider` avoids this by walking to the root container before constructing the provider, ensuring that only the long-lived root container is retained. ```ts // bad — risks retaining a child container reference bindContributionProvider(bind, MyContribution); // good — always resolves against the root container bindRootContributionProvider(bind, MyContribution); ``` `bindContributionProvider` is still appropriate when the contributions themselves are scoped to a child container rather than the main application container, for example: * **Connection-scoped containers** created by `ConnectionContainerModule.create(...)`, where services are bound per-connection. * **Test containers**, where a standalone container is constructed for a unit test. See: ## CSS * [1.](#css-use-lower-case-with-dashes) Use the `lower-case-with-dashes` format. * [2.](#css-prefix-global-classes) Prefix classes with `theia` when used as global classes. * [3.](#no-styles-in-code) Do not define styles in code. Introduce proper CSS classes. > Why? It is not possible to play with such styles in the dev tools without recompiling the code. CSS classes can be edited in the dev tools. ## Theming * [1.](#theming-no-css-color-variables) Do not introduce CSS color variables. Implement `ColorContribution` and use `ColorRegistry.register` to register new colors. * [2.](#theming-no-css-color-values) Do not introduce hard-coded color values in CSS. Instead, refer to [VS Code colors](https://code.visualstudio.com/api/references/theme-color) in CSS by prefixing them with `--theia` and replacing all dots with dashes. For example `widget.shadow` color can be referred to in CSS with `var(--theia-widget-shadow)`. * [3.](#theming-derive-colors-from-vscode) Always derive new colors from existing [VS Code colors](https://code.visualstudio.com/api/references/theme-color). New colors can be derived from an existing color by plain reference, e.g. `dark: 'widget.shadow'`, or transformation, e.g. `dark: Color.lighten('widget.shadow', 0.4)`. > Why? Otherwise, there is no guarantee that new colors will fit well into new VSCode color themes. * [4.](#theming-theia-colors) Apply different color values only in concrete Theia themes, see [Light (Theia)](https://github.com/eclipse-theia/theia/blob/master/packages/monaco/data/monaco-themes/vscode/light_theia.json), [Dark (Theia)](https://github.com/eclipse-theia/theia/blob/master/packages/monaco/data/monaco-themes/vscode/dark_theia.json) and [High Contrast (Theia)](https://github.com/eclipse-theia/theia/blob/master/packages/monaco/data/monaco-themes/vscode/hc_theia.json) themes. * [5.](#theming-variable-naming) Names of variable follow the `object.property` pattern. ```ts // bad 'button.secondary.foreground' 'button.secondary.disabled.foreground' // good 'secondaryButton.foreground' 'secondaryButton.disabledForeground' ``` ## React * [1.](#no-bind-fn-in-event-handlers) Do not bind functions in event handlers. * Extract a React component if you want to pass state to an event handler function. > Why? Because doing so creates a new instance of the event handler function on each render and breaks React element caching leading to re-rendering and bad performance. ```ts // bad class MyWidget extends ReactWidget { render(): React.ReactNode { return
; } protected onClickDiv(): void { // do stuff } } // bad class MyWidget extends ReactWidget { render(): React.ReactNode { return
this.onClickDiv()} />; } protected onClickDiv(): void { // do stuff } } // very bad class MyWidget extends ReactWidget { render(): React.ReactNode { return
; } protected onClickDiv(): void { // do stuff, no `this` access } } // good class MyWidget extends ReactWidget { render(): React.ReactNode { return
} protected onClickDiv = () => { // do stuff, can access `this` } } ``` ## URI/Path * [1.](#uri-over-path) Pass URIs between frontend and backend, never paths. URIs should be sent as strings in JSON-RPC services, e.g. `RemoteFileSystemServer` accepts strings, not URIs. > Why? Frontend and backend can have different operating systems leading to incompatibilities between paths. URIs are normalized in order to be OS-agnostic. * [2.](#frontend-fs-path) Use `FileService.fsPath` to get a path on the frontend from a URI. * [3.](#backend-fs-path) Use `FileUri.fsPath` to get a path on the backend from a URI. Never use it on the frontend. * [4.](#explicit-uri-scheme) Always define an explicit scheme for a URI. > Why? A URI without scheme will fall back to `file` scheme for now; in the future it will lead to a runtime error. * [5.](#frontend-path) Use `Path` Theia API to manipulate paths on the frontend. Don't use Node.js APIs like `path` module. Also see [the code organization guideline](code-organization.md). * [6.](#backend-fs) On the backend, use Node.js APIS to manipulate the file system, like `fs` and `fs-extra` modules. > Why? `FileService` is to expose file system capabilities to the frontend only. It's aligned with expectations and requirements on the frontend. Using it on the backend is not possible. * [7.](#use-long-name) Use `LabelProvider.getLongName(uri)` to get a system-wide human-readable representation of a full path. Don't use `uri.toString()` or `uri.path.toString()`. * [8.](#use-short-name) Use `LabelProvider.getName(uri)` to get a system-wide human-readable representation of a simple file name. * [9.](#use-icon) Use `LabelProvider.getIcon(uri)` to get a system-wide file icon. * [10.](#uri-no-string-manipulation) Don't use `string` to manipulate URIs and paths. Use `URI` and `Path` capabilities instead, like `join`, `resolve` and `relative`. > Why? Because object representation can handle corner cases properly, like trailing separators. ```ts // bad uriString + '/' + pathString // good new URI(uriString).join(pathString) // bad pathString.substring(absolutePathString.length + 1) // good new Path(absolutePathString).relative(pathString) ``` ## Logging * [1.](#use-named-loggers) Whenever you need to log and you are within an Inversify context, inject a named logger ```ts // bad @inject(ILogger) protected readonly logger: ILogger; // without a named logger this is the same call as 'console.info' this.logger.info(``); // good @inject(ILogger) @named('my-logger') this.logger.info(``) ``` > Why? The log level of all named loggers can be separately configured and even changed at runtime. This is useful for filtering logs to only the use cases the developer is interested in. See [here](https://github.com/eclipse-theia/theia/tree/master/packages/core#logging-configuration) for more information. * [2.](#naming-loggers) Use the following convention when naming loggers: `[optional-purpose]package-name:class-name#optional-suffix` ```ts // bad @inject(ILogger) @named('MyClass') protected readonly logger: ILogger; // good @inject(ILogger) @named('remote:BackendRemoteServiceImpl'); ``` > Following this convention allows to conveniently configure log levels for purpose, packages and concrete classes. ## Workspace Trust Theia supports [Workspace Trust](https://theia-ide.org/docs/workspace_trust/) to protect users from potentially harmful code in untrusted repositories. Features that execute code or load external content from the workspace must be gated behind workspace trust. * [1.](#workspace-trust-check) Features that execute workspace-provided code (tasks, debug configurations, scripts) or load workspace-provided content (prompt templates, configuration files that influence behavior) must check workspace trust before proceeding. Use `WorkspaceTrustService` to check or request trust. ```ts @inject(WorkspaceTrustService) protected readonly workspaceTrustService: WorkspaceTrustService; // Check current trust state if (!this.workspaceTrustService.isWorkspaceTrusted()) { return; // disable the feature } // Or request trust from the user (shows dialog if not yet decided) const trusted = await this.workspaceTrustService.requestWorkspaceTrust(); if (!trusted) { return; } ``` * [2.](#workspace-trust-restrictions) When a feature is restricted in untrusted workspaces, register a `WorkspaceRestrictionContribution` so that the Restricted Mode status bar tooltip can inform the user about what is disabled. ```ts bind(WorkspaceRestrictionContribution).toDynamicValue((): WorkspaceRestrictionContribution => ({ getRestrictions(): WorkspaceRestriction[] { return [{ label: nls.localize('theia/myPackage/restricted', 'My Feature is disabled in Restricted Mode') }]; } })).inSingletonScope(); ``` * [3.](#workspace-trust-context-key) Use the `isWorkspaceTrusted` context key when workspace trust should control menu or command visibility. ## "To Do" Tags There are situations where we can't properly implement some functionality at the time we merge a PR. In those cases, it is sometimes good practice to leave an indication that something needs to be fixed later in the code. This can be done by putting a "tag" string in a comment. This allows us to find the places we need to fix again later. Currently, we use two "standard" tags in Theia: * `@stubbed` This tag is used in VS Code API implementations. Sometimes we need an implementation of an API in order for VS Code extensions to start up correctly, but we can't provide a proper implementation of the underlying feature at this time. This might be because a certain feature has no corresponding UI in Theia or because we do not have the resources to provide a proper implementation. Using the `@stubbed` tag in a JSDoc comment will mark the element as "stubbed" on the [API status page](https://eclipse-theia.github.io/vscode-theia-comparator/status.html) * `@monaco-uplift` Use this tag when some functionality can be added or needs to be fixed when we move to a newer version of the monaco editor. If you know which minimum version of Monaco we need, you can add that as a reminder.