---
title: LSP and Language Servers
url: https://doc.liz6.com/en/compilers/08-compilation-technology-applications/04-lsp-and-language-servers
locale: en
area: compilers
tags:
- compilers
- compilation-technology-applications
date: 2026-06-30
modified: 2026-07-16
description: IDE completion, jump-to-definition, hover types, refactoring—these code intelligence features all come from the language server, which is essentially a compiler frontend + incremental updates + error-tolerant parsing. LSP exposes lexical/symbol table capabilities to the editor via JSON-RPC.
---

# LSP and Language Servers

> IDE completion, jump-to-definition, hover types, refactoring—these code intelligence features all come from the language server, which is essentially a compiler frontend + incremental updates + error-tolerant parsing. LSP exposes lexical/symbol table capabilities to the editor via JSON-RPC.

## Overview

IDE code intelligence—completion, jump-to-definition, hover type display, refactoring—traditionally required each IDE to write a separate analysis logic for each language it supported (M × N). LSP (Language Server Protocol) solves this using the same strategy as MCP: **provide one language server process per language, with the IDE communicating via JSON-RPC**. For compilation technology, LSP is a direct consumer of compiler frontend data (lexical/syntax/semantic/symbol table)—a language server is essentially a compiler frontend + an incremental update engine. This article covers the core request types of the LSP protocol, how language servers use the compiler frontend techniques from previous chapters to build indexes and respond, and why "being fast enough" is harder to achieve than "being precise enough."

## LSP is not a compiler; it's a compiler frontend with a different goal

What a compiler wants: produce machine code from source code; correctness is paramount; batch processing across a set of files.
What a language server wants: update completion/diagnostics/highlighting in <100ms for every keystroke, provide reasonable results for **incomplete/erroneous code**, and only recompute changed parts.

The latter requirement means a language server cannot simply run the entire compiler pipeline—it must be **incremental** and **tolerant of syntax errors** (source code will inevitably have syntax errors while the user is still typing).

## Core Request Types: All Queries on Symbol Tables + AST

Every LSP request is a query on compiler frontend data structures:

| Request | Corresponding Compiler Frontend | What is Queried |
|------|---------------|-----------|
| `textDocument/completion` | Symbol Table + Type System | What names are visible in the current scope? What are their types and documentation? |
| `textDocument/definition` | Symbol Table | On which line is the definition of this identifier? |
| `textDocument/references` | Symbol Table (Reverse Lookup) | Where is this definition referenced? |
| `textDocument/hover` | Symbol Table + Type System | What is the type of this identifier? What does its documentation comment say? |
| `textDocument/signatureHelp` | Type System | What are the parameter lists and overloads of the current function? Which parameter is currently active? |
| `textDocument/rename` | Symbol Table (All References) | Change the name at all reference sites of this definition to the new name. |
| `textDocument/publishDiagnostics` | Semantic Analysis (Type Checking) | What type errors/unused variables exist in this file? |

**All capabilities stem from the [Symbol Table](/compilers/03-semantic-analysis/01-symbol-tables-and-scopes.md)**. `definition` is a lookup in the symbol table, `references` is the use list of a symbol, `completion` is an enumeration of the symbol table in the current scope, and `hover` is the type and doc string of a symbol. If the symbol table does not maintain a reverse lookup index for "where this name is referenced," `references` and `rename` cannot be efficient—building the symbol table requires bidirectional indexes (from definition to reference, and from reference to definition).

## Indexing: Knowing What Each File Exports Before Opening It

In large projects with thousands of files, when a user opens `main.rs`, the language server cannot parse the entire project. It needs a **persistent index**:

<svg viewBox="0 0 720 380" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,'Source Han Sans CN','Microsoft YaHei',sans-serif" role="img" aria-label="Three-step lifecycle and index content of persistent indexing">
  <rect width="720" height="380" fill="#ffffff"/>
  <text x="360" y="28" text-anchor="middle" font-size="17" font-weight="700" fill="#1f2933">Persistent Index: Startup Load, Incremental Update, On-Demand Query</text>
  <defs><marker id="idxah" markerWidth="10" markerHeight="8" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#475569"/></marker></defs>

  <rect x="10" y="90" width="60" height="30" rx="15" fill="#e2e8f0"/>
  <text x="40" y="109" text-anchor="middle" font-size="11" fill="#475569">Startup</text>
  <line x1="70" y1="105" x2="88" y2="105" stroke="#475569" stroke-width="1.6" marker-end="url(#idxah)"/>

  <rect x="90" y="68" width="170" height="74" rx="8" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="175" y="90" text-anchor="middle" font-size="13" font-weight="700" fill="#3730a3">① Load Existing Index</text>
  <text x="175" y="108" text-anchor="middle" font-size="11" fill="#3730a3">From previously stored results</text>
  <text x="175" y="124" text-anchor="middle" font-size="11" fill="#3730a3">Deserialize and load</text>
  <line x1="260" y1="105" x2="288" y2="105" stroke="#475569" stroke-width="1.6" marker-end="url(#idxah)"/>

  <rect x="288" y="68" width="170" height="74" rx="8" fill="#e0e7ff" stroke="#c7d2fe"/>
  <text x="373" y="90" text-anchor="middle" font-size="13" font-weight="700" fill="#3730a3">② Incremental Update</text>
  <text x="373" y="108" text-anchor="middle" font-size="11" fill="#3730a3">Re-parse files</text>
  <text x="373" y="124" text-anchor="middle" font-size="11" fill="#3730a3">that changed since last close</text>
  <line x1="458" y1="105" x2="486" y2="105" stroke="#475569" stroke-width="1.6" marker-end="url(#idxah)"/>

  <rect x="486" y="68" width="170" height="74" rx="8" fill="#f0fdfa" stroke="#99f6e4"/>
  <text x="571" y="90" text-anchor="middle" font-size="13" font-weight="700" fill="#115e59">③ Query Response</text>
  <text x="571" y="108" text-anchor="middle" font-size="11" fill="#115e59">Open new file: query external symbols</text>
  <text x="571" y="124" text-anchor="middle" font-size="11" fill="#115e59">Only parse that file in memory</text>

  <line x1="60" y1="160" x2="660" y2="160" stroke="#e2e8f0" stroke-width="1"/>
  <text x="60" y="186" font-size="13" font-weight="700" fill="#1f2933">What the Index Stores</text>

  <rect x="60" y="200" width="92" height="24" rx="5" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="106" y="216" text-anchor="middle" font-size="11" font-weight="700" fill="#3730a3">Exported Symbols</text>
  <text x="166" y="217" font-size="12" fill="#475569">Symbols exported (pub) by each file: name, location, type signature, documentation</text>

  <rect x="60" y="234" width="92" height="24" rx="5" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="106" y="250" text-anchor="middle" font-size="11" font-weight="700" fill="#3730a3">Import List</text>
  <text x="166" y="251" font-size="12" fill="#475569">Import (use/import) list for each file, to quickly check "who references this symbol"</text>

  <rect x="60" y="268" width="92" height="24" rx="5" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="106" y="284" text-anchor="middle" font-size="11" font-weight="700" fill="#3730a3">Dependency Graph</text>
  <text x="166" y="285" font-size="12" fill="#475569">Symbol dependency graph: A calls B → file containing A depends on definition file of B</text>

  <rect x="60" y="306" width="600" height="56" rx="8" fill="#f0fdfa" stroke="#99f6e4"/>
  <text x="76" y="326" font-size="12.5" fill="#115e59">The index allows the language server to avoid parsing the entire project when opening a file—only incrementally update changed files,</text>
  <text x="76" y="348" font-size="12.5" fill="#115e59">while unaffected files are resolved via direct table lookup. This is key to <100ms response times.</text>
</svg>

rust-analyzer uses Salsa (an incremental computation framework) to maintain this index. Any change only recomputes affected files, while unaffected files are retrieved directly from the cache. This is the key to keeping IDE response times under <100ms—not full recomputation, but incremental updates.

## Error-Tolerant Parsing: Producing Results on Syntactically Invalid Input

When a user is typing (`let x = some_struct.`), the source code is inevitably incomplete—the parser sees the dot after `some_struct`, expects a field name, but encounters EOF. A compiler would stop here with an error. A language server **cannot stop; it must recover from the error and provide completion for what might follow the `.`**.

This requires the error recovery mechanism described in [AST Design and Error Recovery](/compilers/02-syntax-analysis/03-ast-design-and-error-recovery.md) to be robust: upon seeing an incomplete expression, the parser should still construct an AST node for it (marked as an error). Higher-level semantic analysis can then perform partial type inference on this "AST with errors"—extracting the field list from the type information of `some_struct` and returning it as completion candidates.

```
User Input: let x = some_struct.
Parser Output: Expr::Field { object: "some_struct", field: <Error> }
Semantic Analysis: Look up type of some_struct in symbol table → StructFoo { a: i32, b: String }
Completion: [a: i32, b: String]
```

Without error recovery, there is no completion—these two are **a pair** in a language server. The only distinction is whether "the current token is a complete expression" or "the current token is a syntax error." The latter requires the AST node produced by the parser to mark "the field is missing," allowing semantic analysis to know that the user expects completion at that position.

## Semantic Tokens: Another Way to Implement Syntax Highlighting

Traditional highlighting uses regex matching (like Tree-sitter's `highlights.scm`), but regex doesn't understand semantics—`foo` is a variable definition in `let foo = ...`, a function call in `foo()`, and a function declaration in `fn foo()`. LSP's `textDocument/semanticTokens/full` assigns a **semantic category** (variable, function, keyword, type, comment...) to **each token**, and the IDE colors them based on these categories.

The language server determines the semantic category for each token on top of the AST + symbol table—this requires the tokens produced by the lexer to carry sufficient information (is it an identifier? Does reverse lookup in the symbol table reveal it as a variable, function, or type?), or for semantic analysis to annotate them additionally.

## Architecture: Single Process, Multi-threading, Incremental Scheduling

The typical architecture of industrial language servers (rust-analyzer, clangd):

<svg viewBox="0 0 720 300" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,'Source Han Sans CN','Microsoft YaHei',sans-serif" role="img" aria-label="Language server single-process multi-threaded scheduling architecture">
  <rect width="720" height="300" fill="#ffffff"/>
  <text x="360" y="28" text-anchor="middle" font-size="17" font-weight="700" fill="#1f2933">Language Server Architecture: Single Process, Multi-threading, Incremental Scheduling</text>
  <defs><marker id="archah" markerWidth="10" markerHeight="8" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#475569"/></marker></defs>

  <rect x="40" y="50" width="190" height="80" rx="8" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="135" y="74" text-anchor="middle" font-size="13" font-weight="700" fill="#3730a3">Main Thread (IO)</text>
  <text x="135" y="94" text-anchor="middle" font-size="11" fill="#3730a3">Receive JSON-RPC requests</text>
  <text x="135" y="112" text-anchor="middle" font-size="11" fill="#3730a3">→ Send to scheduler</text>
  <line x1="230" y1="90" x2="258" y2="90" stroke="#475569" stroke-width="1.6" marker-end="url(#archah)"/>

  <rect x="260" y="50" width="190" height="80" rx="8" fill="#e0e7ff" stroke="#c7d2fe"/>
  <text x="355" y="74" text-anchor="middle" font-size="13" font-weight="700" fill="#3730a3">Scheduler</text>
  <text x="355" y="94" text-anchor="middle" font-size="11" fill="#3730a3">Request queue, coalesce requests for the same file (debounce)</text>
  <text x="355" y="112" text-anchor="middle" font-size="11" fill="#3730a3">→ Assign to worker threads</text>
  <line x1="450" y1="90" x2="478" y2="90" stroke="#475569" stroke-width="1.6" marker-end="url(#archah)"/>

  <rect x="480" y="50" width="190" height="80" rx="8" fill="#f0fdfa" stroke="#99f6e4"/>
  <text x="575" y="74" text-anchor="middle" font-size="13" font-weight="700" fill="#115e59">Worker Threads</text>
  <text x="575" y="94" text-anchor="middle" font-size="11" fill="#115e59">Parse file → Update index</text>
  <text x="575" y="112" text-anchor="middle" font-size="11" fill="#115e59">→ Produce response</text>

  <rect x="40" y="150" width="310" height="60" rx="8" fill="#ffedd5" stroke="#f97316"/>
  <text x="56" y="172" font-size="12" font-weight="700" fill="#9a3412">Debounce</text>
  <text x="56" y="190" font-size="11" fill="#c2410c">User types 10 characters within 500ms,</text>
  <text x="56" y="204" font-size="11" fill="#c2410c">do not re-parse character by character—wait 50ms of no input before parsing</text>

  <rect x="370" y="150" width="310" height="60" rx="8" fill="#ffedd5" stroke="#f97316"/>
  <text x="386" y="172" font-size="12" font-weight="700" fill="#9a3412">Cancellation</text>
  <text x="386" y="190" font-size="11" fill="#c2410c">Previous parse not finished, new input arrived,</text>
  <text x="386" y="204" font-size="11" fill="#c2410c">discard previous work using $/cancelRequest</text>

  <rect x="40" y="224" width="640" height="56" rx="8" fill="#f0fdfa" stroke="#99f6e4"/>
  <text x="56" y="246" font-size="12.5" fill="#115e59">Debounce merges "continuous input" into "parse only after the last input"; cancellation discards stale intermediate results—</text>
  <text x="56" y="266" font-size="12.5" fill="#115e59">both are key to stable <100ms response times. It's not full recomputation, but doing only the necessary one.</text>
</svg>

Debounce and cancellation are lifelines for performance—when a user types quickly, `on_type` frequency is ~100ms per character. If every character triggered a full file re-parse, the language server would 100% freeze. Debounce merges "N consecutive inputs" into "parse after the last one," and cancellation discards outdated intermediate results.

## References

- **LSP 3.17 specification**: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/
- **rust-analyzer**: https://github.com/rust-lang/rust-analyzer — Architecture documentation and source code for the Rust LSP server
- **Clangd**: https://clangd.llvm.org — C/C++ LSP server based on the Clang frontend

*Keywords: LSP, Language Server Protocol, JSON-RPC, completion, go-to-definition, find-references, hover, diagnostics, semantic tokens, incremental parsing, index, debounce, cancellation, error-tolerant parsing, symbol table reverse lookup, rust-analyzer, clangd*
