Sprachserverprotokoll

Was ist das Sprachserverprotokoll?

Die Unterstützung umfangreicher Bearbeitungsfunktionen wie die automatische Vervollständigung von Quellcode oder Gehe zur Definition für eine Programmiersprache in einem Editor oder einer IDE ist traditionell sehr anspruchsvoll und zeitaufwändig. Normalerweise ist es erforderlich, ein Domänenmodell (einen Scanner, einen Parser, eine Typprüfung, einen Generator und mehr) in der Programmiersprache des Editors oder der IDE zu schreiben. Beispielsweise wird das Eclipse CDT-Plug-In, das Unterstützung für C/C++ in der Eclipse-IDE bereitstellt, in Java geschrieben, da die Eclipse-IDE selbst in Java geschrieben wird. Nach diesem Ansatz würde es bedeuten, ein C/C++-Domänenmodell in TypeScript für Visual Studio Code und ein separates Domänenmodell in C# für Visual Studio zu implementieren.

Die Erstellung sprachspezifischer Domänenmodelle ist auch viel einfacher, wenn ein Entwicklungstool vorhandene sprachspezifische Bibliotheken wiederverwenden kann. Diese Bibliotheken werden jedoch in der Regel in der Programmiersprache selbst implementiert (z. B. gute C/C++-Domänenmodelle werden in C/C++implementiert). Die Integration einer C/C++-Bibliothek in einen in TypeScript geschriebenen Editor ist technisch möglich, aber schwer zu realisieren.

Sprachserver

Ein anderer Ansatz besteht darin, die Bibliothek in einem eigenen Prozess laufen zu lassen und die Kommunikation zwischen den Prozessen zu nutzen, um mit der Bibliothek zu kommunizieren. Die gesendeten Nachrichten bilden ein Protokoll. Das Sprachserverprotokoll (LSP) ist das Produkt der Standardisierung der zwischen einem Entwicklungstool und einem Sprachserverprozess ausgetauschten Nachrichten. Die Verwendung von Sprachservern oder Dämonen ist keine neue oder neuartige Idee. Editoren wie Vim und Emacs haben dies seit einiger Zeit durchgeführt, um semantische Unterstützung für die automatische Fertigstellung bereitzustellen. Das Ziel der LSP war es, diese Art von Integrationen zu vereinfachen und einen nützlichen Rahmen zu schaffen, um Sprachfunktionen für eine Vielzahl von Tools verfügbar zu machen.

Das Gemeinsame Protokoll ermöglicht die Integration von Programmiersprachenfeatures in ein Entwicklungstool mit minimalem Aufwand, indem eine vorhandene Implementierung des Domänenmodells der Sprache erneut verwendet wird. Ein Sprachserver-Back-End kann in PHP, Python oder Java geschrieben werden und lässt sich mit dem LSP leicht in eine Vielzahl von Tools integrieren. Das Protokoll funktioniert auf einer gemeinsamen Abstraktionsebene, sodass ein Tool umfangreiche Sprachdienste anbieten kann, ohne die Nuancen, die für das zugrunde liegende Domänenmodell spezifisch sind, vollständig zu verstehen.

Wie die Arbeit am LSP begann

LSP hat sich im Laufe der Zeit weiterentwickelt und liegt heute in der Version 3.0 vor. Es begann damit, dass das Konzept eines Sprachenservers von OmniSharp aufgegriffen wurde, um umfangreiche Bearbeitungsfeatures für C# bereitzustellen. Zunächst verwendete OmniSharp das HTTP-Protokoll mit einer JSON-Nutzlast und wurde in mehrere Editoren integriert, einschließlich Visual Studio Code.

Rund um die gleiche Zeit begann Microsoft mit der Arbeit an einem TypeScript-Sprachserver, mit der Idee, TypeScript in Editoren wie Emacs und Sublime Text zu unterstützen. In dieser Implementierung kommuniziert ein Editor über stdin/stdout mit dem TypeScript-Serverprozess und verwendet eine JSON-Nutzlast, die vom V8-Debugger-Protokoll für Anfragen und Antworten inspiriert ist. Der TypeScript-Server wurde in das TypeScript-Sublime-Plug-In und VS Code integriert, um eine umfassende TypeScript-Bearbeitung zu ermöglichen.

Nachdem sie zwei verschiedene Sprachserver integriert haben, begann das VS Code-Team, ein gemeinsames Sprachserverprotokoll für Editoren und IDEs zu erkunden. Ein gemeinsames Protokoll ermöglicht es einem Sprachanbieter, einen einzigen Sprachserver zu erstellen, der von verschiedenen IDEs genutzt werden kann. Ein Sprachserver-Verbraucher muss nur einmal die Clientseite des Protokolls implementieren. Dies führt zu einer Win-Win-Situation sowohl für den Sprachanbieter als auch für den Sprachkonsumenten.

Das Sprachserverprotokoll begann mit dem Protokoll, das vom TypeScript-Server verwendet wird, und erweiterte es um weitere Sprachfunktionen, die von der VS Code-Sprach-API inspiriert sind. Das Protokoll wird aufgrund seiner Einfachheit und der vorhandenen Bibliotheken mit JSON-RPC für Remoteaufrufe unterstützt.

Das VS Code-Team hat das Protokoll prototypisch getestet, indem es mehrere Linter-Sprachserver implementiert hat, die auf Anfragen zum Linting (Scannen) einer Datei reagieren und eine Reihe von erkannten Warnungen und Fehlern zurückgeben. Ziel war es, eine Datei zu linsen, während der Benutzer ein Dokument bearbeitet, was bedeutet, dass es während einer Editor-Sitzung viele Linting-Anforderungen geben wird. Es ist sinnvoll, einen Server auf dem laufenden zu halten, damit für jeden Benutzer kein neuer Lintingprozess gestartet werden muss. Mehrere Linter-Server wurden implementiert, einschließlich DER ESLint- und TSLint-Erweiterungen von VS Code. Diese beiden Linter-Server werden sowohl in TypeScript/JavaScript implementiert als auch auf Node.js ausgeführt. Sie teilen eine Bibliothek, die den Client- und Serverteil des Protokolls implementiert.

Funktionsweise des LSP

Ein Sprachserver wird in einem eigenen Prozess ausgeführt, und Tools wie Visual Studio oder VS Code kommunizieren mit dem Server mithilfe des Sprachprotokolls über JSON-RPC. Ein weiterer Vorteil des Sprachservers, der in einem eigenen Prozess arbeitet, ist, dass Leistungsprobleme im Zusammenhang mit einem einzelnen Prozessmodell vermieden werden. Der eigentliche Transportkanal kann entweder stdio, Sockets, Named Pipes oder Node ipc sein, wenn sowohl der Client als auch der Server in Node.js geschrieben sind.

Nachfolgend finden Sie ein Beispiel dafür, wie ein Tool und ein Sprachserver während einer routinemäßigen Bearbeitungssitzung kommunizieren:

lsp flow diagram

  • Der Benutzer öffnet eine Datei (als Dokument bezeichnet) im Tool: Das Tool benachrichtigt den Sprachserver, dass ein Dokument geöffnet ist ('textDocument/didOpen'). Von nun an befindet sich die Wahrheit über den Inhalt des Dokuments nicht mehr im Dateisystem, sondern wird vom Programm im Speicher aufbewahrt.

  • Der Benutzer erstellt Bearbeitungen: Das Tool benachrichtigt den Server über die Dokumentänderung ('textDocument/didChange'), und die semantischen Informationen des Programms werden vom Sprachserver aktualisiert. Wenn dies geschieht, analysiert der Sprachserver diese Informationen und benachrichtigt das Tool über die erkannten Fehler und Warnungen ('textDocument/publishDiagnostics').

  • Der Benutzer führt "Gehe zu Definition" auf einem Symbol im Editor aus: Das Tool sendet eine Anforderung "textDocument/Definition" mit zwei Parametern: (1) den Dokument-URI und (2) die Textposition, an der die Anforderung "Gehe zur Definition" an den Server initiiert wurde. Der Server antwortet mit dem URI-Dokument und der Position der Definition des Symbols innerhalb des Dokuments.

  • Der Benutzer schließt das Dokument (Datei): Eine "textDocument/didClose"-Benachrichtigung wird vom Tool gesendet, informiert den Sprachserver, dass das Dokument jetzt nicht mehr im Arbeitsspeicher ist und dass der aktuelle Inhalt jetzt auf dem Dateisystem aktuell ist.

In diesem Beispiel wird veranschaulicht, wie das Protokoll mit dem Sprachserver auf Der Ebene der Editorfeatures wie "Gehe zur Definition" kommuniziert, "Alle Verweise suchen". Die vom Protokoll verwendeten Datentypen sind Editor- oder IDE-'Datentypen' wie das aktuell geöffnete Textdokument und die Position des Cursors. Die Datentypen befinden sich nicht auf der Ebene eines Domänenmodells einer Programmiersprache, das normalerweise abstrakte Syntaxbäume und Compilersymbole (z.B. aufgelöste Typen, Namespaces, ...) bereitstellen würde. Dies vereinfacht das Protokoll erheblich.

Betrachten wir nun die Anforderung 'textDocument/definition' genauer. Nachfolgend finden Sie die Nutzdaten, die zwischen dem Clienttool und dem Sprachserver für die Anforderung "Gehe zur Definition" in einem C++-Dokument gehen.

Dies ist die Anforderung:

{
    "jsonrpc": "2.0",
    "id" : 1,
    "method": "textDocument/definition",
    "params": {
        "textDocument": {
            "uri": "file:///p%3A/mseng/VSCode/Playgrounds/cpp/use.cpp"
        },
        "position": {
            "line": 3,
            "character": 12
        }
    }
}

Dies ist die Antwort:

{
    "jsonrpc": "2.0",
    "id": "1",
    "result": {
        "uri": "file:///p%3A/mseng/VSCode/Playgrounds/cpp/provide.cpp",
        "range": {
            "start": {
                "line": 0,
                "character": 4
            },
            "end": {
                "line": 0,
                "character": 11
            }
        }
    }
}

Rückblickend ist die Beschreibung der Datentypen auf der Ebene des Editors und nicht auf der Ebene des Programmiersprachenmodells einer der Gründe für den Erfolg des Sprachserverprotokolls. Es ist viel einfacher, einen Textdokument-URI oder eine Cursorposition zu standardisieren, als einen abstrakten Syntaxbaum und Compilersymbole in verschiedenen Programmiersprachen zu standardisieren.

Wenn ein Benutzer mit verschiedenen Sprachen arbeitet, startet VS Code normalerweise einen Sprachserver für jede Programmiersprache. Das folgende Beispiel zeigt eine Sitzung, in der der Benutzer auf Java- und SASS-Dateien arbeitet.

java and sass

Capabilities

Nicht jeder Sprachserver kann alle vom Protokoll definierten Features unterstützen. Daher geben der Client und der Server ihre unterstützten Features durch "Funktionen" bekannt. Ein Beispiel: Ein Server meldet, dass er die Anforderung 'textDocument/Definition' bearbeiten kann, aber möglicherweise nicht die Anforderung 'Arbeitsbereich/Symbol'. Ebenso können Clients ankündigen, dass sie in der Lage sind, vor dem Speichern eines Dokuments Benachrichtigungen über das bevorstehende Speichern zu übermitteln, so dass ein Server Textänderungen berechnen kann, um das bearbeitete Dokument automatisch zu formatieren.

Integrieren eines Sprachservers

Die tatsächliche Integration eines Sprachservers in ein bestimmtes Tool ist nicht durch das Sprachserverprotokoll definiert und wird den Tool-Implementierern überlassen. Einige Tools integrieren Sprachserver allgemein, indem sie eine Erweiterung haben, die mit jeder Art von Sprachserver beginnen und sprechen kann. Andere, z.B. VS Code, erstellen eine benutzerdefinierte Erweiterung pro Sprachserver, sodass eine Erweiterung weiterhin einige benutzerdefinierte Sprachfeatures bereitstellen kann.

Um die Implementierung von Sprachservern und Clients zu vereinfachen, gibt es Bibliotheken oder SDKs für die Client- und Serverteile. Diese Bibliotheken werden für verschiedene Sprachen bereitgestellt. Beispielsweise gibt es ein Sprachclient-npm-Modul, um die Integration eines Sprachservers in eine VS-Codeerweiterung und ein anderes Sprachserver npm-Modul zu erleichtern, um einen Sprachserver mithilfe von Node.js zu schreiben. Dies ist die aktuelle Liste der Supportbibliotheken.

Verwenden des Sprachserverprotokolls in Visual Studio