~/vscode-extensions/language-server-protocol/

Language Server Protocol einfach erklärt

Ein Server, viele Editoren: wie das Language Server Protocol Sprachfunktionen vom Editor trennt, welche Nachrichten ausgetauscht werden und wann sich ein eigener Server lohnt.

Das Language Server Protocol (LSP) trennt die Sprachintelligenz vom Editor. Ein Language Server analysiert den Code und beantwortet Fragen wie „Was kann an dieser Stelle stehen?“ oder „Wo ist diese Funktion definiert?“. Der Editor schickt die Fragen und zeigt die Antworten an. Beide sprechen über ein standardisiertes Format aus JSON-Nachrichten miteinander, deshalb funktioniert derselbe Server in VS Code, Neovim, Emacs, Sublime Text, Helix und vielen weiteren Editoren.

Welches Problem LSP löst

Vor LSP musste jede Kombination aus Sprache und Editor einzeln gebaut werden. Zehn Sprachen in zehn Editoren bedeuteten im schlechtesten Fall hundert Integrationen, jede mit eigenen Fehlern. Mit LSP schreibt jede Sprachgemeinschaft einen Server und jeder Editor einen Client. Aus der Multiplikation wird eine Addition.

Microsoft hat das Protokoll im Zusammenhang mit VS Code entwickelt und als offene Spezifikation veröffentlicht. Heute bringen viele Sprachen einen offiziellen oder etablierten Server mit, etwa gopls für Go, rust-analyzer für Rust, clangd für C und C++ oder Pyright für Python.

So sieht die Kommunikation aus

Editor und Server tauschen Nachrichten im Format JSON-RPC aus. Jede Nachricht beginnt mit einem Header, der die Länge angibt, danach folgt der JSON-Inhalt:

Content-Length: 156

{"jsonrpc":"2.0","id":7,"method":"textDocument/definition","params":{"textDocument":{"uri":"file:///projekt/main.go"},"position":{"line":11,"character":8}}}

Der Server antwortet mit derselben id und dem Ergebnis, hier einer Fundstelle mit Datei und Bereich. Es gibt drei Nachrichtenarten:

Art Erwartet Antwort Beispiel
Request ja textDocument/completion, textDocument/hover
Response ist selbst die Antwort Liste von Vorschlägen, Fundstelle
Notification nein textDocument/didChange, textDocument/publishDiagnostics

Als Transportweg dient meist die Standardein- und -ausgabe des Server-Prozesses. Der Editor startet den Server als Kindprozess und schreibt in dessen Eingabe. Sockets und Pipes sind ebenfalls möglich. Positionen werden als Zeile und Zeichen angegeben, beide nullbasiert. Wie Zeichen gezählt werden, handeln Client und Server aus; ohne Absprache gilt die Zählung in UTF-16-Codeeinheiten, was bei Emojis und anderen Sonderzeichen zu verschobenen Markierungen führen kann.

Der Lebenszyklus einer Sitzung

  1. initialize: Der Editor startet den Server und schickt seine Fähigkeiten, das Projektverzeichnis und Einstellungen. Der Server antwortet mit seinen eigenen Fähigkeiten (Capabilities).
  2. initialized: Eine Benachrichtigung bestätigt, dass die Verbindung steht. Ab jetzt laufen normale Anfragen.
  3. Dokumente synchronisieren: Mit didOpen, didChange, didSave und didClose hält der Editor den Server über geöffnete Dateien auf dem Laufenden. Der Server arbeitet mit diesem Stand, nicht mit der Datei auf der Festplatte.
  4. Anfragen und Diagnosen: Der Editor fragt nach Vervollständigung, Hover-Infos oder Referenzen. Der Server schickt Fehler und Warnungen von sich aus per publishDiagnostics.
  5. shutdown und exit: Beim Schließen bittet der Editor den Server, aufzuräumen, und beendet ihn anschließend.

Die Synchronisierung kann vollständig oder inkrementell erfolgen. Bei der vollständigen Variante schickt der Editor nach jeder Änderung den ganzen Dateiinhalt, bei der inkrementellen nur die geänderten Bereiche. Für große Dateien ist inkrementell deutlich effizienter, verlangt vom Server aber sorgfältige Buchführung.

Capabilities: Was kann der Server?

LSP ist bewusst modular. In der Antwort auf initialize meldet der Server, welche Funktionen er anbietet. Häufige Bausteine sind:

  • Diagnosen: Fehler, Warnungen und Hinweise, die der Editor unterstreicht.
  • Vervollständigung: Vorschläge beim Tippen, optional mit Details und Dokumentation.
  • Hover: Typ und Beschreibung beim Überfahren eines Symbols.
  • Definition, Referenzen, Implementierungen: Navigation im Code.
  • Umbenennen: projektweite, sichere Umbenennung eines Symbols.
  • Formatierung: ganze Datei oder Auswahl.
  • Code Actions: schnelle Korrekturen wie „fehlenden Import hinzufügen“.
  • Semantic Tokens: Informationen für eine feinere Hervorhebung.
  • Inlay Hints: eingeblendete Typen oder Parameternamen.

Ein Server muss nicht alles können. Ein kleiner Server für eine Konfigurationssprache bietet vielleicht nur Diagnosen und Vervollständigung an und ist trotzdem nützlich.

LSP in VS Code: Client-Extension und Server

In VS Code besteht eine LSP-Integration aus zwei Teilen: einer normalen Extension, die den Server startet und als Client auftritt, und dem Server selbst. Microsoft stellt dafür die npm-Pakete vscode-languageclient für die Extension und vscode-languageserver für einen Server in Node.js bereit. Wie Sie eine Extension anlegen, zeigt der Artikel VS-Code-Extension erstellen mit Yeoman.

// extension.ts: Client-Seite in der VS-Code-Extension
import * as path from "node:path";
import { ExtensionContext } from "vscode";
import { LanguageClient, ServerOptions, TransportKind } from "vscode-languageclient/node";

let client: LanguageClient;

export function activate(context: ExtensionContext) {
  const modul = context.asAbsolutePath(path.join("server", "out", "server.js"));
  const serverOptions: ServerOptions = {
    run: { module: modul, transport: TransportKind.ipc },
    debug: { module: modul, transport: TransportKind.ipc },
  };
  client = new LanguageClient(
    "beispielSprache",
    "Beispiel Language Server",
    serverOptions,
    { documentSelector: [{ scheme: "file", language: "beispiel" }] },
  );
  client.start();
}

export function deactivate() {
  return client?.stop();
}

Auf der Server-Seite registrieren Sie Handler für die Anfragen, die Ihr Server beantworten soll, und schicken Diagnosen, sobald sich ein Dokument ändert. Der Vorteil dieser Aufteilung: Der Server läuft in einem eigenen Prozess. Hängt eine Analyse, bleibt der Editor bedienbar.

LSP in anderen Editoren und IDEs

Neovim enthält einen eingebauten LSP-Client, Emacs bringt mit Eglot ebenfalls einen mit, Sublime Text und Helix unterstützen LSP direkt oder über Pakete. Sie müssen dort meist nur angeben, welcher Server für welchen Dateityp gestartet werden soll.

JetBrains-IDEs haben eine eigene, tiefere Code-Analyse auf Basis des Syntaxbaums (PSI). Für Sprachen, die dort nicht nativ unterstützt werden, bietet die IntelliJ Platform inzwischen eine Schnittstelle, über die Plugins einen vorhandenen Language Server einbinden können. Wie Plugins für diese Plattform grundsätzlich aufgebaut sind, beschreibt JetBrains-Plugin entwickeln mit dem IntelliJ Platform SDK. Wer nur einen Server anbinden will, kommt so ohne vollständige Sprachintegration aus; wer die volle Tiefe der IDE nutzen möchte, setzt weiterhin auf eine native Implementierung.

Wann sich ein eigener Language Server lohnt

Ein eigener Server ist sinnvoll, wenn Sie eine Sprache, ein Konfigurationsformat oder eine domänenspezifische Sprache (DSL) in mehreren Editoren unterstützen wollen. Typische Kandidaten sind interne Konfigurationsdateien, Vorlagensprachen oder Abfragesprachen eines Produkts.

Weniger sinnvoll ist ein Server, wenn Sie nur Syntaxfarben oder ein paar Snippets brauchen. Das lösen eine Grammatik und eine Snippet-Datei in einer gewöhnlichen Extension schneller. Auch wenn Ihre Funktion stark an die Oberfläche eines bestimmten Editors gebunden ist, etwa eine eigene Seitenleiste, gehört sie in eine Extension und nicht in das Protokoll.

Typische Stolpersteine

  • Ausgaben auf stdout: Läuft der Transport über die Standardausgabe, zerstört jedes console.log im Server das Protokoll. Protokollieren Sie über die dafür vorgesehenen Logging-Funktionen oder auf stderr.
  • Veralteter Dokumentstand: Der Server muss mit dem Inhalt arbeiten, den der Editor geschickt hat, nicht mit der gespeicherten Datei. Sonst passen Positionen nicht.
  • Zeichenzählung: Unterschiedliche Kodierungen von Positionen führen zu verrutschten Unterstreichungen in Zeilen mit Umlauten oder Emojis. Testen Sie solche Zeilen gezielt.
  • Zu langsame Antworten: Vervollständigung muss schnell sein. Teure Analysen gehören in den Hintergrund, Ergebnisse in einen Cache.
  • Abbrüche ignoriert: Editoren brechen veraltete Anfragen ab, sobald weitergetippt wird. Ein Server, der Abbrüche beachtet, spart Rechenzeit.

Verwandte Begriffe

Neben LSP gibt es weitere Protokolle nach demselben Muster. Das Debug Adapter Protocol standardisiert die Anbindung von Debuggern, das Language Server Index Format (LSIF) beschreibt vorab berechnete Code-Navigationsdaten, etwa für Code-Browser im Web. Begriffe wie JSON-RPC, Extension Host oder Capability erklärt außerdem unser Glossar.

faq

Fragen und Antworten

Brauche ich für LSP eine bestimmte Programmiersprache?
Nein. Ein Language Server ist ein eigenständiges Programm und kann in jeder Sprache geschrieben sein, die JSON lesen und schreiben kann. Viele Server sind in der Sprache geschrieben, die sie unterstützen, etwa gopls in Go oder rust-analyzer in Rust. Für TypeScript und Node.js gibt es fertige Bibliotheken von Microsoft, die viel Protokollarbeit abnehmen.
Was ist der Unterschied zwischen LSP und Syntax-Highlighting?
Einfaches Syntax-Highlighting erledigt der Editor selbst, meist über Grammatiken wie TextMate oder Parser wie Tree-sitter. Es färbt Text nach Mustern, ohne den Code zu verstehen. Ein Language Server kennt dagegen Typen, Importe und Projektstruktur. Über sogenannte Semantic Tokens kann er die Hervorhebung zusätzlich verfeinern, etwa Parameter anders färben als lokale Variablen.
Unterstützt jeder Editor alle LSP-Funktionen?
Nein. Beim Start tauschen Editor und Server ihre Fähigkeiten aus. Der Server bietet nur an, was er kann, und der Editor nutzt nur, was er kennt. Grundfunktionen wie Diagnosen, Vervollständigung und Sprung zur Definition beherrschen praktisch alle LSP-Clients, seltenere Funktionen fehlen in manchen Editoren.
Gibt es etwas Vergleichbares für Debugger?
Ja. Das Debug Adapter Protocol (DAP) verfolgt dieselbe Idee für Debugger: Ein Debug-Adapter übersetzt zwischen Editor und einem konkreten Debugger. Auch DAP stammt aus dem VS-Code-Umfeld und wird inzwischen von weiteren Editoren unterstützt.