The editor itself adds CodeMirror language modes for immediate highlighting (
ui-mode in a language definition), before any backend answer arrives.
Language definitions
Every language is a TOML file inlanguages/ (embedded at build time; users can add more in their config directory). It names detection, comments, the tree-sitter grammar, the editor mode, and the language servers and debugger to start:
languages/rust.toml
[[language-servers]] (for example vtsls and eslint for TypeScript). ide-tools has a registry of installable servers keyed by the same name, and installs them into the app’s managed tools directory without sudo.
Language servers
ide-lsp is a language server manager:
- Lazy start. A server starts per
(server name, workspace root)when a document of a matching language opens. The root comes fromroot-markers. - Document sync. The backend keeps the authoritative copy of each open document (
ide-text). The editor sends change sets tagged with their base version; the backend applies them and forwardsdidChange. A stale change set is rejected withoutOfSyncand the editor resyncs. - IDE types only. Every request takes IDE coordinates (UTF-16, 0-based) and returns types from
ide_lsp::types, neverlsp-types, so the IPC surface stays stable whatever the server sends. - Capabilities. A request a server doesn’t support returns
unavailablewith a message such as “jdtls doesn’t support inline completion”. The editor backs off per document and per feature (src/editor/lsp/availability.ts), so a server lacking one capability doesn’t switch off the others. - Trust. Servers start only in trusted projects: they execute project build scripts.
java/: java-usages (semantic usages with read/write access, configuration navigation), kotlin-bridge (Kotlin libraries on the Java classpath), and a patched Lombok agent. They are embedded by ide-sdk / ide-tools. When the java-usages command is missing or fails, find usages falls back to standard LSP references.
The IDE’s own index
ide-index indexes the project’s files and their symbols in the background. Symbols come from tree-sitter tags (ide-language), so the index works for every language with a bundled grammar, without a language server. It is persisted in redb and queried with nucleo’s fuzzy matcher.
Indexing never blocks: queries answer from whatever is indexed so far, and IdeEvent::IndexState tells the UI when indexing completes (the “dumb mode” indicator).
Tree-sitter syntax
ide-language bundles the tree-sitter grammars and keeps one syntax tree per open document: parsed lazily on the first query, re-parsed incrementally after edits, dropped on close. On top of it:
- outline (Structure), folding, symbol tags, and highlighting for languages that have no CodeMirror mode;
ide-syntax, the structure service: node and expression lookup, syntax errors, scopes and locals, identifier occurrences, structural ranges.
certain: false, exact: false, candidate lists) rather than guessed.
ide-analysis (Locate Duplicates, structural search and replace) and ide-spell (identifiers, comments and strings) also work on these trees.
How results are combined
LSP instead of a PSI-like model
OpenIDEA does not have a unified, IDE-owned semantic model of the code (what IntelliJ calls PSI). Semantic knowledge — types, resolution, references — comes from each language’s server. The IDE’s own layers (index, tree-sitter structure) are syntactic. What that means in practice:- Breadth comes from existing servers. Supporting a language is mostly a language definition plus a server the IDE can install, not a parser and resolver written inside the IDE.
- Depth is bounded by the server. Find usages, rename, refactorings, inspections and quick fixes are as good as the server’s implementation of the LSP request. Where a server lacks a feature, the IDE says so (
unavailable) instead of inventing an answer. - No cross-language semantic model. Refactorings that need to understand several languages at once, or structural search that matches on resolved types, are not possible on this architecture. The roadmap states this scope, and each language page lists its limits.
- Extensions fill gaps per language. When a server can be extended (JDT LS bundles above) or a plugin adds providers, the IDE goes further for that language without a general semantic model.
- Syntactic fallbacks are conservative. The tree-sitter layer never guesses semantics; it marks what it can’t be sure of.
Testing it
The language smoke tests (crates/ide-host/tests/language_smoke.rs) run the everyday checklist — completion, go to definition, find usages, hover, diagnostics with a quick fix, an intention, rename, formatting, parameter info, the Structure view, auto-import, Optimize Imports and moving a file — against each language’s fixtures/ project with its real server. See Smoke tests.