Language Server Protocol

Co to jest protokół serwera językowego?

Obsługa zaawansowanych funkcji edycji, takich jak automatyczne uzupełnianie kodu źródłowego lub Przechodzenie do definicji języka programowania w edytorze lub środowisku IDE, jest tradycyjnie bardzo trudne i czasochłonne. Zwykle wymaga to pisania modelu domeny (skanera, analizatora, narzędzia sprawdzania typów, konstruktora i nie tylko) w języku programowania edytora lub środowiska IDE. Na przykład wtyczka Eclipse CDT, która zapewnia obsługę języka C/C++ w środowisku ECLIPSE IDE, jest napisana w języku Java, ponieważ samo środowisko IDE środowiska Eclipse jest napisane w języku Java. Zgodnie z tym podejściem oznaczałoby to zaimplementowanie modelu domeny C/C++ w języku TypeScript dla programu Visual Studio Code i oddzielnego modelu domeny w języku C# dla programu Visual Studio.

Tworzenie modeli domen specyficznych dla języka jest również znacznie łatwiejsze, jeśli narzędzie programistyczne może ponownie używać istniejących bibliotek specyficznych dla języka. Jednak te biblioteki są zwykle implementowane w samym języku programowania (na przykład dobre modele domeny C/C++ są implementowane w języku C/C++). Integracja biblioteki C/C++ z edytorem napisanym w języku TypeScript jest technicznie możliwa, ale trudna do wykonania.

Serwery językowe

Innym podejściem jest uruchomienie biblioteki we własnym procesie i użycie komunikacji między procesami w celu komunikowania się z nią. Komunikaty wysyłane tam i z powrotem tworzą protokół. Protokół serwera językowego (LSP) to produkt standaryzacji komunikatów wymienianych między narzędziem programistycznym a procesem serwera językowego. Używanie serwerów językowych lub demonów nie jest nowym ani nowatorskim pomysłem. Edytory, takie jak Vim i Emacs, od jakiegoś czasu zapewniają obsługę semantycznego automatycznego uzupełniania. Celem dostawcy LSP było uproszczenie tego rodzaju integracji i zapewnienie przydatnej platformy do uwidaczniania funkcji językowych w różnych narzędziach.

Posiadanie wspólnego protokołu umożliwia integrację funkcji języka programowania z narzędziem programistycznym z minimalnym rozmyciem przez ponowne użycie istniejącej implementacji modelu domeny języka. Zaplecze serwera językowego może być napisane w języku PHP, Python lub Java, a dostawca LSP umożliwia łatwe integrowanie go z różnymi narzędziami. Protokół działa na wspólnym poziomie abstrakcji, dzięki czemu narzędzie może oferować zaawansowane usługi językowe bez konieczności pełnego zrozumienia niuansów specyficznych dla bazowego modelu domeny.

Jak pracować nad uruchomionym dostawcą LSP

LSP ewoluował w czasie i dzisiaj jest w wersji 3.0. Zaczęło się, gdy koncepcja serwera językowego została odebrana przez OmniSharp w celu zapewnienia rozbudowanych funkcji edycji dla języka C#. Początkowo omniSharp używało protokołu HTTP z ładunkiem JSON i zostało zintegrowane z kilkoma edytorami, w tym visual Studio Code.

W tym samym czasie firma Microsoft zaczęła pracować na serwerze języka TypeScript, mając na myśli obsługę języka TypeScript w edytorach, takich jak Emacs i Sublime Text. W tej implementacji edytor komunikuje się za pośrednictwem stdin/stdout z procesem serwera TypeScript i używa ładunku JSON inspirowanego protokołem debugera V8 dla żądań i odpowiedzi. Serwer TypeScript został zintegrowany z wtyczką TypeScript Wzniosłe i programem VS Code na potrzeby rozbudowanej edycji języka TypeScript.

Po zintegrowaniu dwóch różnych serwerów językowych zespół programu VS Code rozpoczął eksplorowanie wspólnego protokołu serwera języka dla edytorów i środowisk IDE. Wspólny protokół umożliwia dostawcy języka tworzenie pojedynczego serwera językowego, który może być używany przez różne środowiska IDE. Użytkownik serwera językowego musi tylko raz zaimplementować po stronie klienta protokołu. Powoduje to sytuację win-win zarówno dla dostawcy języka, jak i konsumenta języka.

Protokół serwera językowego rozpoczął się od protokołu używanego przez serwer TypeScript, rozszerzając go o więcej funkcji językowych inspirowanych interfejsem API języka programu VS Code. Protokół jest wspierany z protokołem JSON-RPC na potrzeby zdalnego wywołania ze względu na prostotę i istniejące biblioteki.

Zespół programu VS Code utworzył prototyp protokołu, implementując kilka serwerów języka linter, które odpowiadają na żądania lint (skanowanie) pliku i zwracają zestaw wykrytych ostrzeżeń i błędów. Celem było lint pliku podczas edycji użytkownika w dokumencie, co oznacza, że podczas sesji edytora będzie wiele żądań linting. Warto zachować działanie serwera, aby nie trzeba było uruchamiać nowego procesu lintingu dla każdej edycji użytkownika. Zaimplementowano kilka serwerów linter, w tym rozszerzenia ESLint i TSLint programu VS Code. Te dwa serwery linter są implementowane w języku TypeScript/JavaScript i uruchamiane w środowisku Node.js. Udostępniają bibliotekę, która implementuje część protokołu klienta i serwera.

Jak działa dostawca LSP

Serwer językowy działa we własnym procesie, a narzędzia, takie jak Visual Studio lub VS Code, komunikują się z serwerem przy użyciu protokołu językowego za pośrednictwem protokołu JSON-RPC. Kolejną zaletą serwera językowego działającego w dedykowanym procesie jest uniknięcie problemów z wydajnością związanych z pojedynczym modelem procesów. Rzeczywisty kanał transportu może być stdio, gniazda, nazwane potoki lub ipc węzła, jeśli zarówno klient, jak i serwer są zapisywane w node.js.

Poniżej przedstawiono przykład sposobu komunikowania się narzędzia i serwera językowego podczas rutynowej sesji edycji:

lsp flow diagram

  • Użytkownik otwiera plik (nazywany dokumentem) w narzędziu: Narzędzie powiadamia serwer językowy o otwarciu dokumentu ('textDocument/didOpen'). Od tej pory prawda o zawartości dokumentu nie znajduje się już w systemie plików, ale jest przechowywana przez narzędzie w pamięci.

  • Użytkownik dokonuje edycji: narzędzie powiadamia serwer o zmianie dokumentu ('textDocument/didChange'), a semantyczne informacje programu są aktualizowane przez serwer językowy. W takim przypadku serwer językowy analizuje te informacje i powiadamia narzędzie o wykrytych błędach i ostrzeżeniach ('textDocument/publishDiagnostics').

  • Użytkownik wykonuje żądanie "Przejdź do definicji" na symbolu w edytorze: narzędzie wysyła żądanie "textDocument/definition" z dwoma parametrami: (1) identyfikator URI dokumentu i (2) położenie tekstu, z którego żądanie Przejdź do definicji zostało zainicjowane na serwerze. Serwer odpowiada za pomocą identyfikatora URI dokumentu i położenia definicji symbolu w dokumencie.

  • Użytkownik zamyka dokument (plik): powiadomienie "textDocument/didClose" jest wysyłane z narzędzia, informując serwer językowy, że dokument nie jest już w pamięci i że bieżąca zawartość jest teraz aktualna w systemie plików.

W tym przykładzie pokazano, jak protokół komunikuje się z serwerem językowym na poziomie funkcji edytora, takich jak "Przejdź do definicji", "Znajdź wszystkie odwołania". Typy danych używane przez protokół to edytor lub środowisko IDE "typy danych", takie jak aktualnie otwarty dokument tekstowy i położenie kursora. Typy danych nie są na poziomie modelu domeny języka programowania, który zwykle zapewnia abstrakcyjne drzewa składni i symbole kompilatora (na przykład rozpoznawane typy, przestrzenie nazw, ...). Znacznie upraszcza to protokół.

Teraz przyjrzyjmy się bardziej szczegółowo żądaniu "textDocument/definition". Poniżej przedstawiono ładunki, które przechodzą między narzędziem klienckim a serwerem językowym dla żądania "Przejdź do definicji" w dokumencie C++.

Jest to żądanie:

{
    "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
        }
    }
}

Jest to odpowiedź:

{
    "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
            }
        }
    }
}

Z perspektywy czasu opisywanie typów danych na poziomie edytora, a nie na poziomie modelu języka programowania, jest jednym z powodów sukcesu protokołu serwera językowego. Znacznie prostsze jest standaryzację identyfikatora URI dokumentu tekstowego lub położenia kursora w porównaniu ze standaryzacją abstrakcyjnego drzewa składni i symboli kompilatora w różnych językach programowania.

Gdy użytkownik pracuje z różnymi językami, program VS Code zwykle uruchamia serwer językowy dla każdego języka programowania. W poniższym przykładzie pokazano sesję, w której użytkownik pracuje w plikach Java i SASS.

java and sass

Funkcje

Nie każdy serwer językowy może obsługiwać wszystkie funkcje zdefiniowane przez protokół. W związku z tym klient i serwer ogłaszają obsługiwany zestaw funkcji za pośrednictwem funkcji "capabilities". Na przykład serwer ogłasza, że może obsłużyć żądanie "textDocument/definition", ale może nie obsługiwać żądania "workspace/symbol". Podobnie klienci mogą ogłosić, że mogą udostępniać powiadomienia "o zapisaniu" przed zapisaniem dokumentu, dzięki czemu serwer może obliczyć zmiany tekstowe w celu automatycznego formatowania edytowanego dokumentu.

Integrowanie serwera językowego

Rzeczywista integracja serwera językowego z określonym narzędziem nie jest definiowana przez protokół serwera językowego i jest pozostawiona do implementatorów narzędzi. Niektóre narzędzia integrują serwery językowe w sposób ogólny, mając rozszerzenie, które może uruchamiać i komunikować się z dowolnym rodzajem serwera językowego. Inne, takie jak VS Code, tworzą rozszerzenie niestandardowe na serwer językowy, dzięki czemu rozszerzenie jest nadal w stanie zapewnić niektóre funkcje języka niestandardowego.

Aby uprościć implementację serwerów językowych i klientów, istnieją biblioteki lub zestawy SDK dla części klienta i serwera. Te biblioteki są udostępniane dla różnych języków. Na przykład istnieje moduł npm klienta języka, który ułatwia integrację serwera językowego z rozszerzeniem programu VS Code i innym modułem npm serwera językowego w celu napisania serwera językowego przy użyciu środowiska Node.js. Jest to bieżąca lista bibliotek pomocy technicznej.

Używanie protokołu Language Server w programie Visual Studio

  • Dodawanie rozszerzenia protokołu serwera językowego — dowiedz się więcej o integracji serwera językowego z programem Visual Studio.