How it is built
Architecture overview
A code editor holds source files in memory, shows them at screen rate, and lets a person change them while the tool understands the code well enough to colour it, complete it, navigate it, debug it and sometimes rewrite it. Its constraint is latency on every keystroke: typing, scrolling and cursor moves must finish within a frame, while everything expensive (parsing, type checking, indexing, search, AI completion) happens somewhere else and arrives later. Examples are VS Code, the JetBrains IDEs, Xcode, Zed and Cursor.
The design that makes this work is a split of three speeds. The editing core (text buffer, undo, selections, layout, rendering) is synchronous, local and never waits on a server. The language layer (incremental parsing, highlighting, folding) is fast enough to follow typing on a background thread, using trees that are updated, not rebuilt. The services layer (language servers, debug adapters, search and symbol indexes, extensions, remote machines) lives in other processes, talks over a message protocol, and may answer slowly or die without taking the editor with it. Every part below is a decision about which of the three speeds a feature belongs to.
Parts:
- Text buffer — store the document so edits, line lookups and snapshots are cheap — Rope,
gap-buffer(no page yet),undo-redo(no page yet), Text position encoding, Three-way merge, Myers diff - Syntax layer — incremental parse, highlight, fold, select — Incremental parsing,
lossless-syntax-trees(no page yet),parser-error-recovery(no page yet), Recursive-descent parsing,syntax-highlighting(no page yet),hash-consing(no page yet),string-interning(no page yet) - Language services — completion, definitions, diagnostics from a separate process — Language Server Protocol, Message sequence numbers,
breakpoints-watchpoints(no page yet) - Indexing and search — files, symbols, text, fuzzy open — Inverted index,
trigram-index(no page yet), Regex engine, Aho-Corasick, SIMD string search, Code symbol indexing, Name resolution,glob-matching(no page yet),fuzzy-finder-scoring(no page yet), Top-k selection, File-system watching - Derived-data engine — recompute only what an edit invalidated —
incremental-computation(no page yet), Topological sort, Swiss table - Rendering — shape text, rasterise, draw only what changed —
text-shaping(no page yet),opentype-font-parsing(no page yet),font-fallback-itemization(no page yet),unicode-bidi-algorithm(no page yet),font-rasterization(no page yet),damage-tracking(no page yet), List virtualization, SIMD programming - Extension host — run third-party code out of the UI process, local or remote — (process boundary plus the protocols above),
protobuf-wire-format(no page yet) - Collaboration and AI — shared editing and repository context for model completions —
operational-transformation(no page yet),repository-context-retrieval(no page yet),fill-in-the-middle(no page yet)
keyboard / mouse
│
▼
┌──────────────────────────── UI process ────────────────────────────┐
│ editing core: rope buffer ── undo ── selections ── layout │
│ │ edit(range, text) │ visible lines only │
│ ▼ ▼ │
│ syntax tree (incremental) ──► highlights shaping ► raster ► GPU │
└───────┬───────────────────────────┬────────────────────────────────┘
│ didChange (versioned) │ commands, events
▼ ▼
language server (LSP) extension host(s) ──► debug adapter (DAP)
diagnostics, completion local / remote / web
▲
│ symbols, refs
index service: file watcher ─► symbol index + text index ─► query (top-k)
How the parts fit together
Text buffer: edit at the cursor, snapshot for everyone else
The buffer must support insertion and deletion anywhere, offset-to-line and line-to-offset conversion, and a cheap consistent snapshot for background readers (highlighting, indexing, a language server request). A Rope, a balanced tree of small chunks with cached byte and newline counts, gives O(log n) for all of these and copy-on-write snapshots; the alternative gap-buffer (no page yet) is O(1) at the cursor and simpler, but a snapshot is a full copy and far-apart edits (multi-cursor, rename) are slow. Ropey is a UTF-8 rope for Rust "designed to be the backing text-buffer for applications such as text editors" (source: Ropey README). The contract is that every consumer holds a snapshot with a version number, and results are applied only if the version still matches.
Positions are the classic bug source. Store byte offsets internally and convert to line and column on demand with a line-start table (Text position encoding). Language servers count columns in UTF-16 code units by default in LSP, so the editor converts at the protocol edge, not in the buffer. Undo (undo-redo, no page yet) is a stack of grouped edit commands that coalesces typing runs; with multiple cursors one user action must be one undo step. Diff and merge use Myers diff for a shortest edit script and Three-way merge when a file changed both on disk and in the buffer.
Syntax layer: trees that follow typing
A regex highlighter is cheap but wrong on nested constructs; a parser-based one is accurate if it survives code that is incomplete, which is most code while someone types. Tree-sitter describes itself as a parser generator and incremental parsing library that builds a concrete syntax tree and efficiently updates it as the source is edited, fast enough to parse on every keystroke, robust to syntax errors, and dependency-free in pure C11 (source: Tree-sitter documentation). That is Incremental parsing: the edit tells the parser which region is damaged, and subtrees outside it are reused. Error recovery (parser-error-recovery, no page yet) keeps a full tree with error nodes, which is the property that lets highlighting and folding keep working mid-edit. A concrete tree that keeps whitespace and comments (lossless-syntax-trees, no page yet) lets refactorings rewrite code without losing formatting; sharing structurally equal nodes (hash-consing, no page yet) and identifiers (string-interning, no page yet) cuts memory. Hand-written servers often use Recursive-descent parsing for its control over error messages. Highlight queries then map nodes to colour spans (syntax-highlighting, no page yet); only the visible range is evaluated.
Language services: a protocol, a process, a version
The Language Server Protocol standardises the conversation between an editor and a language server so one server serves many editors: the protocol defines messages sent using JSON-RPC (source: LSP official page). The costs are real: every request is asynchronous, the server's view of the file must be kept in sync by incremental didChange messages, and answers can arrive after the user moved on. Hence the contract: version every document, cancel superseded requests, apply a result only if the document version matches, and show stale results greyed instead of blocking. Message sequence numbers is the general idea (detect loss, duplicates, reordering); LSP uses request ids and document versions. Debugging uses the Debug Adapter Protocol, which does for debuggers what LSP does for languages (source: DAP official page); the adapter in turn drives breakpoints, watchpoints and single-stepping (breakpoints-watchpoints, no page yet).
Servers can be slow to start and heavy. The editor treats a server as a service that can crash, restart and be restarted lazily.
Indexing and search: files, symbols and text
Three indexes serve three questions. "Open file by name" is a fuzzy subsequence match (fuzzy-finder-scoring, no page yet) over a path list, with a bounded result set via Top-k selection and ignore rules matched by glob-matching (no page yet). "Find in files" can be a streaming scan with SIMD string search and a Regex engine with linear-time guarantees; for very large trees a trigram-index (no page yet) over an Inverted index narrows the candidates before the regex runs, and Aho-Corasick serves many literal patterns at once. "Go to symbol / find references" needs the structure of the code: Code symbol indexing extracts definitions and references per file with stable symbol ids, stored as symbol-to-occurrences, and Name resolution binds uses to declarations with a scope stack. Sourcegraph's SCIP is an open format for precomputed code indexes (see Real implementations).
All three must be kept fresh. File-system watching treats OS change notices as hints, debounces them, and rescans a snapshot when notices overflow; the index is updated per changed file, not rebuilt. Searching unsaved buffers means consulting the editor's snapshots in preference to disk.
Derived-data engine: recompute only what changed
Diagnostics, outlines, symbol tables and type information are functions of other data. An incremental-computation (no page yet) layer records which inputs each derived value read, orders recomputation with Topological sort, and skips values whose inputs are unchanged. This is how a type checker can respond within a keystroke on a large project: the edit changes one file's syntax tree, and only the dependent queries re-run. Lookup tables keyed by identifiers use a fast hash such as a Swiss table.
Rendering: draw the visible, shape the complex
Only visible lines are laid out and drawn (List virtualization); the scrollbar extent is computed from cached line counts in the rope. Text must be shaped, not just mapped to glyphs: OpenType substitution and positioning rules (text-shaping, opentype-font-parsing, both no page yet), fallback fonts per script run (font-fallback-itemization, no page yet) and the bidirectional algorithm for mixed left-to-right and right-to-left text (unicode-bidi-algorithm, no page yet) produce positioned glyphs. Rasterised glyphs go into an atlas (font-rasterization, no page yet) and the GPU draws them as quads. damage-tracking (no page yet) repaints only what changed, which matters on battery and on remote displays. Wide-character width, ligatures and combining marks mean that the cursor is moved by grapheme cluster, not by code point or byte; see Text position encoding.
Extension host: isolate other people's code
VS Code's documentation describes the extension host as the part that runs extensions, with local, web and remote configurations; extensions are loaded lazily by activation events, and the host "prevents extensions from impacting startup performance, slowing down UI operations, modifying the UI" (source: VS Code extension host docs). The contract is a message API in both directions: the UI process exposes a narrow set of capabilities; the extension host calls them asynchronously; a misbehaving extension can hang its own process without freezing typing. For remote development, the same split places the extension host (and language servers) next to the files on the remote machine while the UI stays local, so only small messages cross the network. Messages crossing the boundary use a compact serialisation (protobuf-wire-format, no page yet, is the typical choice for custom services), with versioned schemas so old extensions keep working.
Collaboration and AI
Shared editing needs concurrent edits to converge: operational-transformation (no page yet) transforms edits against each other using the buffer's positions; the alternative is a conflict-free replicated structure. AI completion needs context: repository-context-retrieval (no page yet) selects relevant files and symbols (using the same symbol and text indexes as above) to fit a prompt budget, and fill-in-the-middle prompting (fill-in-the-middle, no page yet) lets a model complete the text between a prefix and a suffix. Treat model output like any other asynchronous service: versioned, cancellable, applied only if the buffer version matches.
Data and control flow
One keystroke:
- The key event reaches the editing core, which applies the edit to the rope and creates a new snapshot with a new version. Only the affected line is relaid out.
- The syntax tree is updated for the damaged region on a worker thread; highlights for visible lines are recomputed and the line is redrawn (a few milliseconds is the goal; the exact budget is the frame time).
- An incremental
didChangegoes to the language server with the new version; pending requests for the older version are cancelled. - When diagnostics or completions arrive for that version, they are applied; if the version moved on, they are dropped.
- The index service learns of the save, re-extracts symbols for the file and updates the symbol and text indexes.
One "find in files":
- The query is parsed; literals are extracted from the regex.
- The text index (if present) returns candidate files; otherwise the walker scans the tree honouring ignore rules.
- Files are searched in parallel with a bounded result count; open buffers are searched from their snapshots.
- Results stream into the panel as they arrive; the search is cancelled when the query changes.
One remote session:
- The UI connects to a small server on the remote machine and starts a remote extension host and language servers there.
- File access, search, indexing and debugging happen remotely; the UI receives results and edits as messages.
- If the link drops, the UI keeps the buffer and reconnects; unsaved edits are not lost because the buffer lives in the UI process.
Scaling
In the order you meet the limits:
- Large files (logs, generated code). Levers: a rope, a size threshold above which parsing and language services are turned off, streaming load.
- Large repositories. Levers: ignore rules, an on-disk index instead of scanning, per-file incremental updates, lazy language server startup per project.
- Slow language servers. Levers: cancellation, versioned results, incremental sync, partial results, and showing last-known diagnostics.
- Extension load. Levers: lazy activation, one extension host per window or per remote, resource limits, and a profiler that names the extension.
- Long lines and complex scripts. Levers: cap layout work per line, cache shaping results per run, virtualise horizontally.
- Remote latency. Levers: put language servers beside the files, send edits rather than files, local echo for typing.
- Memory in long sessions. Levers: share syntax nodes, free snapshots, evict caches (LRU and CLOCK).
Failure modes
| Failure | Symptom | Cause | Fix |
|---|---|---|---|
| Keystroke lag | typing stutters | synchronous work in the UI thread (parsing, a plugin, a long line) | move work off-thread, budget per frame, cap line length |
| Stale or wrong results | completions apply to the wrong text | answer arrives after further edits | version every document, apply only on match, cancel superseded requests |
| Off-by-one cursor or edit | edits land one character off | mixed byte, UTF-16 and code-point positions | one internal unit, convert at the boundary, test with non-ASCII text |
| Server crash loop | features vanish, high CPU | a language server fails on startup or on a file | restart with backoff, disable after N crashes, show status |
| Extension hang | UI freezes or high memory | extension runs in the UI process or leaks | separate extension host, activation events, kill and restart the host |
| Missed file changes | search or index is stale | OS notice overflow or ignored events | treat notices as hints, rescan snapshots on overflow, manual refresh |
| Regex stall | search never finishes | backtracking regex on large input | linear-time engine, timeouts, cancellation |
| Merge clobbers edits | external change overwrites the buffer | no three-way merge with the base | track base version, three-way merge, conflict markers |
| Wrong glyphs or order | broken text for complex scripts | no shaping or bidi handling | shape by script run, fallback fonts, bidi reordering |
| Lost work | crash discards unsaved edits | edits only in memory | periodic backup of dirty buffers, restore on start |
Real implementations
| name | what it is | repo | licence | notes |
|---|---|---|---|---|
| VS Code (Code - OSS) | Electron-based editor with an extension host and LSP/DAP clients | microsoft/vscode | MIT | source layout in the wiki |
| Zed | Rust editor with its own rope crate | zed-industries/zed | per crate; the rope crate is GPL-3.0-or-later (Cargo.toml) | licence differs by crate, check each |
| Helix | modal editor in Rust built on Tree-sitter | helix-editor/helix | MPL-2.0 | — |
| Neovim | extensible Vim fork | neovim/neovim | Apache-2.0, with parts under the Vim licence (per LICENSE.txt) | — |
| Lapce | code editor written in Rust | lapce/lapce | Apache-2.0 (per repo page) | — |
| Tree-sitter | incremental parsing library and parser generator | tree-sitter/tree-sitter | MIT | used by several editors above |
| Ropey | UTF-8 rope for Rust | cessen/ropey | MIT | text-buffer library |
| rust-analyzer | Rust language server | rust-lang/rust-analyzer | Apache-2.0 and MIT | LSP server |
| SCIP | open code-intelligence index format from Sourcegraph | sourcegraph/scip | Apache-2.0 (per repo page) | precomputed symbol index |
Proprietary systems named in the graph: JetBrains IDEs, Xcode, Cursor. They are not described here.
Sources
- Language Server Protocol: https://microsoft.github.io/language-server-protocol/
- Debug Adapter Protocol: https://microsoft.github.io/debug-adapter-protocol/
- Tree-sitter documentation: https://tree-sitter.github.io/tree-sitter/
- VS Code, Extension Host: https://code.visualstudio.com/api/advanced-topics/extension-host
- VS Code, Language Server Extension Guide: https://code.visualstudio.com/api/language-extensions/language-server-extension-guide
- Ropey README: https://github.com/cessen/ropey