Samouczek protokołu serwera języka: od VSCode do Vima
Opublikowany: 2022-03-11Głównym artefaktem całej twojej pracy są najprawdopodobniej zwykłe pliki tekstowe. Dlaczego więc nie używasz Notatnika do ich tworzenia?
Podświetlanie składni i automatyczne formatowanie to tylko wierzchołek góry lodowej. A co z lintingiem, uzupełnianiem kodu i półautomatyczną refaktoryzacją? To wszystko są bardzo dobre powody, aby użyć „prawdziwego” edytora kodu. Są one niezbędne na co dzień, ale czy rozumiemy, jak one działają?
W tym samouczku dotyczącym protokołu Language Server Protocol omówimy nieco te pytania i dowiemy się, co sprawia, że nasze edytory tekstu działają. Na koniec wspólnie zaimplementujemy podstawowy serwer językowy wraz z przykładowymi klientami dla VSCode, Sublime Text 3 i Vim.
Kompilatory a usługi językowe
Na razie pominiemy wyróżnianie składni i formatowanie, które jest obsługiwane przez analizę statyczną — interesujący temat sam w sobie — i skoncentrujemy się na głównych informacjach zwrotnych, jakie otrzymujemy z tych narzędzi. Istnieją dwie główne kategorie: kompilatory i usługi językowe.
Kompilatory pobierają twój kod źródłowy i wyrzucają inną formę. Jeśli kod nie jest zgodny z regułami języka, kompilator zwróci błędy. Są dość znajome. Problem z tym polega na tym, że jest to zwykle dość powolne i ograniczone w zakresie. Co powiesz na zaoferowanie pomocy podczas tworzenia kodu?
To właśnie zapewniają usługi językowe. Mogą dać ci wgląd w twoją bazę kodu, gdy jest jeszcze w pracy, i prawdopodobnie znacznie szybciej niż kompilacja całego projektu.
Zakres tych usług jest zróżnicowany. Może to być coś tak prostego, jak zwrócenie listy wszystkich symboli w projekcie, lub coś złożonego, jak zwrócenie kroków do kodu refaktoryzacji. Te usługi są głównym powodem, dla którego korzystamy z naszych edytorów kodu. Gdybyśmy chcieli tylko skompilować i zobaczyć błędy, moglibyśmy to zrobić za pomocą kilku naciśnięć klawiszy. Usługi językowe dają nam więcej informacji i to bardzo szybko.
Postawienie na edytor tekstu do programowania
Zauważ, że nie wywołaliśmy jeszcze konkretnych edytorów tekstu. Wyjaśnijmy dlaczego na przykładzie.
Załóżmy, że opracowałeś nowy język programowania o nazwie Lapine. To piękny język, a kompilator daje wspaniałe komunikaty o błędach, przypominające Elm. Dodatkowo możesz zapewnić uzupełnianie kodu, referencje, pomoc w refaktoryzacji i diagnostykę.
Który edytor kodu/tekstu obsługujesz jako pierwszy? A co po tym? Toczysz ciężką walkę o to, aby ludzie ją adoptowali, więc chcesz, aby było to jak najłatwiejsze. Nie chcesz wybierać złego edytora i tracić użytkowników. A co, jeśli zachowasz dystans od edytorów kodu i skupisz się na swojej specjalizacji — języku i jego funkcjach?
Serwery językowe
Wpisz serwery językowe . Są to narzędzia, które rozmawiają z klientami językowymi i dostarczają spostrzeżeń, o których wspomnieliśmy. Są niezależni od edytorów tekstu z powodów, które właśnie opisaliśmy w naszej hipotetycznej sytuacji.
Jak zwykle kolejna warstwa abstrakcji jest właśnie tym, czego potrzebujemy. Obiecują one zerwać ścisłe połączenie narzędzi językowych i edytorów kodu. Twórcy języka mogą raz umieścić swoje funkcje na serwerze, a edytorzy kodu/tekstu mogą dodawać małe rozszerzenia, aby zamienić się w klientów. To wygrana dla wszystkich. Aby to ułatwić, musimy jednak uzgodnić, w jaki sposób ci klienci i serwery będą się komunikować.
Na szczęście dla nas to nie jest hipotetyczne. Microsoft zaczął już od zdefiniowania protokołu Language Server.
Podobnie jak w przypadku większości wspaniałych pomysłów, wyrósł z konieczności, a nie z przewidywania. Wielu edytorów kodu zaczęło już dodawać obsługę różnych funkcji językowych; niektóre funkcje są zlecane na zewnątrz narzędziom stron trzecich, niektóre są wykonywane pod maską w edytorach. Pojawiły się problemy ze skalowalnością, a Microsoft objął prowadzenie w dzieleniu rzeczy. Tak, firma Microsoft utorowała drogę do przeniesienia tych funkcji z edytorów kodu zamiast gromadzenia ich w programie VSCode. Mogli dalej budować swój edytor, blokując użytkowników, ale uwalniali ich.
Protokół serwera języka
Protokół Language Server Protocol (LSP) został zdefiniowany w 2016 roku, aby pomóc w rozdzieleniu narzędzi językowych i edytorów. Wciąż jest na nim wiele odcisków palców VSCode, ale jest to duży krok w kierunku agnostycyzmu edytorów. Przyjrzyjmy się trochę protokołowi.
Klienci i serwery — pomyśl o edytorach kodu i narzędziach językowych — komunikują się za pomocą prostych wiadomości tekstowych. Te wiadomości mają nagłówki podobne do HTTP, treść JSON-RPC i mogą pochodzić z klienta lub serwera. Protokół JSON-RPC definiuje żądania, odpowiedzi i powiadomienia oraz kilka podstawowych zasad wokół nich. Kluczową cechą jest to, że został zaprojektowany do pracy asynchronicznej, dzięki czemu klienci/serwery mogą radzić sobie z komunikatami niewłaściwie uporządkowanymi iz pewnym stopniem równoległości.
Krótko mówiąc, JSON-RPC pozwala klientowi zażądać od innego programu uruchomienia metody z parametrami i zwrócenia wyniku lub błędu. LSP opiera się na tym i definiuje dostępne metody, oczekiwane struktury danych i kilka innych reguł dotyczących transakcji. Na przykład, gdy klient uruchamia serwer, następuje proces uzgadniania.
Serwer jest stanowy i jest przeznaczony do obsługi tylko jednego klienta na raz. Nie ma jednak wyraźnych ograniczeń w komunikacji, więc serwer języka może działać na innej maszynie niż klient. W praktyce byłoby to jednak dość powolne w przypadku informacji zwrotnych w czasie rzeczywistym. Serwery językowe i klienci pracują z tymi samymi plikami i są dość rozmowni.
LSP ma przyzwoitą ilość dokumentacji, gdy już wiesz, czego szukać. Jak wspomniano, większość z nich jest napisana w kontekście VSCode, chociaż pomysły mają znacznie szersze zastosowanie. Na przykład specyfikacja protokołu jest napisana w języku TypeScript. Aby pomóc odkrywcom niezaznajomionym z VSCode i TypeScript, oto elementarz.
Rodzaje wiadomości LSP
Istnieje wiele grup wiadomości zdefiniowanych w protokole Language Server. Można je z grubsza podzielić na „administratora” i „funkcje językowe”. Wiadomości administracyjne zawierają te używane w uzgadnianiu klient/serwer, otwieraniu/zmianie plików itp. Co ważne, jest to miejsce, w którym klienci i serwery dzielą się funkcjami, które obsługują. Z pewnością różne języki i narzędzia oferują różne funkcje. Pozwala to również na stopniową adopcję. Langserver.org wymienia pół tuzina kluczowych funkcji, które klienty i serwery powinny obsługiwać, z których przynajmniej jedna jest wymagana do sporządzenia listy.
Najbardziej interesują nas funkcje językowe. Spośród nich jest jedna, którą należy wskazać konkretnie: komunikat diagnostyczny. Diagnostyka to jedna z kluczowych funkcji. Kiedy otwierasz plik, w większości zakłada się, że to się uruchomi. Twój edytor powinien powiedzieć ci, czy coś jest nie tak z plikiem. Sposób, w jaki to się dzieje w przypadku LSP, jest następujący:
- Klient otwiera plik i wysyła
textDocument/didOpen
do serwera. - Serwer analizuje plik i wysyła powiadomienie
textDocument/publishDiagnostics
. - Klient analizuje wyniki i wyświetla w edytorze wskaźniki błędów.
Jest to pasywny sposób na uzyskanie wglądu w swoje usługi językowe. Bardziej aktywnym przykładem byłoby znalezienie wszystkich odniesień do symbolu pod kursorem. To wyglądałoby mniej więcej tak:
- Klient wysyła
textDocument/references
do serwera, określając lokalizację w pliku. - Serwer odnajduje symbol, lokalizuje odniesienia w tym i innych plikach i odpowiada listą.
- Klient wyświetla odniesienia do użytkownika.
Narzędzie czarnej listy
Z pewnością moglibyśmy zagłębić się w szczegóły protokołu Language Server, ale zostawmy to implementatorom klienta. Aby scementować ideę rozdzielenia edytora i narzędzia językowego, wcielimy się w rolę twórcy narzędzia.
Zachowamy prostotę i zamiast tworzyć nowy język i funkcje, pozostaniemy przy diagnostyce. Diagnostyka jest odpowiednia: to tylko ostrzeżenia dotyczące zawartości pliku. Linter zwraca diagnostykę. Zrobimy coś podobnego.
Stworzymy narzędzie do powiadamiania nas o słowach, których chcielibyśmy uniknąć. Następnie udostępnimy tę funkcję kilku różnym edytorom tekstu.
Serwer językowy
Po pierwsze, narzędzie. Umieścimy to bezpośrednio na serwerze językowym. Dla uproszczenia będzie to aplikacja Node.js, chociaż możemy to zrobić za pomocą dowolnej technologii, która może używać strumieni do czytania i pisania.
Oto logika. Biorąc pod uwagę jakiś tekst, ta metoda zwraca tablicę pasujących słów z czarnej listy i indeksów, w których zostały znalezione.
const getBlacklisted = (text) => { const blacklist = [ 'foo', 'bar', 'baz', ] const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi') const results = [] while ((matches = regex.exec(text)) && results.length < 100) { results.push({ value: matches[0], index: matches.index, }) } return results }
Teraz zróbmy z tego serwer.
const { TextDocuments, createConnection, } = require('vscode-languageserver') const {TextDocument} = require('vscode-languageserver-textdocument') const connection = createConnection() const documents = new TextDocuments(TextDocument) connection.onInitialize(() => ({ capabilities: { textDocumentSync: documents.syncKind, }, })) documents.listen(connection) connection.listen()
Tutaj używamy vscode-languageserver
. Nazwa wprowadza w błąd, ponieważ z pewnością może działać poza VSCode. To jeden z wielu „odcisków palców”, jakie można zobaczyć na temat początków LSP. vscode-languageserver
zajmuje się protokołem niższego poziomu i pozwala skupić się na przypadkach użycia. Ten fragment kodu uruchamia połączenie i wiąże go z menedżerem dokumentów. Gdy klient łączy się z serwerem, serwer poinformuje go, że chciałby być powiadamiany o otwieraniu dokumentów tekstowych.

Moglibyśmy się tutaj zatrzymać. Jest to w pełni działający, choć bezcelowy serwer LSP. Zamiast tego zareagujmy na zmiany w dokumencie, podając pewne informacje diagnostyczne.
documents.onDidChangeContent(change => { connection.sendDiagnostics({ uri: change.document.uri, diagnostics: getDiagnostics(change.document), }) })
Na koniec łączymy kropki między zmienionym dokumentem, naszą logiką i odpowiedzią diagnostyczną.
const getDiagnostics = (textDocument) => getBlacklisted(textDocument.getText()) .map(blacklistToDiagnostic(textDocument)) const { DiagnosticSeverity, } = require('vscode-languageserver') const blacklistToDiagnostic = (textDocument) => ({ index, value }) => ({ severity: DiagnosticSeverity.Warning, range: { start: textDocument.positionAt(index), end: textDocument.positionAt(index + value.length), }, message: `${value} is blacklisted.`, source: 'Blacklister', })
Nasz ładunek diagnostyczny będzie wynikiem uruchomienia tekstu dokumentu przez naszą funkcję, a następnie zmapowania do formatu oczekiwanego przez klienta.
Ten skrypt stworzy to wszystko za Ciebie.
curl -o- https://raw.githubusercontent.com/reergymerej/lsp-article-resources/revision-for-6.0.0/blacklist-server-install.sh | bash
Uwaga: Jeśli nie czujesz się komfortowo, gdy nieznajomi dodają pliki wykonywalne do twojego komputera, sprawdź źródło. Tworzy projekt, pobiera index.js
i npm link
to za Ciebie.
Kompletne źródło serwera
Ostateczne źródło blacklist-server
to:
#!/usr/bin/env node const { DiagnosticSeverity, TextDocuments, createConnection, } = require('vscode-languageserver') const {TextDocument} = require('vscode-languageserver-textdocument') const getBlacklisted = (text) => { const blacklist = [ 'foo', 'bar', 'baz', ] const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi') const results = [] while ((matches = regex.exec(text)) && results.length < 100) { results.push({ value: matches[0], index: matches.index, }) } return results } const blacklistToDiagnostic = (textDocument) => ({ index, value }) => ({ severity: DiagnosticSeverity.Warning, range: { start: textDocument.positionAt(index), end: textDocument.positionAt(index + value.length), }, message: `${value} is blacklisted.`, source: 'Blacklister', }) const getDiagnostics = (textDocument) => getBlacklisted(textDocument.getText()) .map(blacklistToDiagnostic(textDocument)) const connection = createConnection() const documents = new TextDocuments(TextDocument) connection.onInitialize(() => ({ capabilities: { textDocumentSync: documents.syncKind, }, })) documents.onDidChangeContent(change => { connection.sendDiagnostics({ uri: change.document.uri, diagnostics: getDiagnostics(change.document), }) }) documents.listen(connection) connection.listen()
Samouczek dotyczący protokołu serwera językowego: czas na jazdę próbną
Po link
projektu spróbuj uruchomić serwer, określając stdio
jako mechanizm transportu:
blacklist-server --stdio
Nasłuchuje teraz na stdio
wiadomości LSP, o których mówiliśmy wcześniej. Moglibyśmy podać je ręcznie, ale zamiast tego utwórzmy klienta.
Klient językowy: VSCode
Ponieważ ta technologia wywodzi się z VSCode, wydaje się, że warto zacząć od tego. Stworzymy rozszerzenie, które utworzy klienta LSP i połączy go z właśnie utworzonym serwerem.
Istnieje wiele sposobów na utworzenie rozszerzenia VSCode, w tym użycie Yeoman i odpowiedniego generatora, generator-code
. Jednak dla uproszczenia zróbmy przykład kadłubków.
Sklonujmy schemat i zainstalujmy jego zależności:
git clone [email protected]:reergymerej/standalone-vscode-ext.git blacklist-vscode cd blacklist-vscode npm i # or yarn
Otwórz katalog blacklist-vscode
w programie VSCode.
Naciśnij klawisz F5, aby uruchomić inne wystąpienie programu VSCode, debugując rozszerzenie.
W „konsoli debugowania” pierwszej instancji programu VSCode zobaczysz tekst „Spójrz, mamo. Rozszerzenie!"
Mamy teraz podstawowe rozszerzenie VSCode działające bez wszystkich dzwonków i gwizdków. Zróbmy z niego klienta LSP. Zamknij oba wystąpienia VSCode i z poziomu katalogu blacklist-vscode
uruchom:
npm i vscode-languageclient
Zastąp plik extension.js następującym:
const { LanguageClient } = require('vscode-languageclient') module.exports = { activate(context) { const executable = { command: 'blacklist-server', args: ['--stdio'], } const serverOptions = { run: executable, debug: executable, } const clientOptions = { documentSelector: [{ scheme: 'file', language: 'plaintext', }], } const client = new LanguageClient( 'blacklist-extension-id', 'Blacklister', serverOptions, clientOptions ) context.subscriptions.push(client.start()) }, }
Używa pakietu vscode-languageclient
do tworzenia klienta LSP w programie VSCode. W przeciwieństwie do vscode-languageserver
, jest to ściśle powiązane z programem VSCode. Krótko mówiąc, w tym rozszerzeniu tworzymy klienta i mówimy mu, aby korzystał z serwera, który utworzyliśmy w poprzednich krokach. Przeglądając specyfikę rozszerzenia VSCode, widzimy, że mówimy mu, aby używał tego klienta LSP do plików tekstowych.
Aby przetestować go, otwórz katalog blacklist-vscode
w programie VSCode. Naciśnij klawisz F5, aby uruchomić inną instancję, debugując rozszerzenie.
W nowym wystąpieniu VSCode utwórz zwykły plik tekstowy i zapisz go. Wpisz „foo” lub „bar” i poczekaj chwilę. Zobaczysz ostrzeżenia, że są one na czarnej liście.
Otóż to! Nie musieliśmy odtwarzać żadnej naszej logiki, wystarczyło koordynować klienta i serwer.
Zróbmy to jeszcze raz dla innego edytora, tym razem Sublime Text 3. Proces będzie podobny i nieco łatwiejszy.
Klient językowy: Sublime Text 3
Najpierw otwórz ST3 i otwórz paletę poleceń. Potrzebujemy frameworka, aby edytor stał się klientem LSP. Wpisz „Kontrola pakietu: zainstaluj pakiet” i naciśnij Enter. Znajdź pakiet „LSP” i zainstaluj go. Po zakończeniu mamy możliwość określenia klientów LSP. Istnieje wiele gotowych ustawień, ale nie zamierzamy ich używać. Stworzyliśmy własne.
Ponownie otwórz paletę poleceń. Znajdź „Preferencje: Ustawienia LSP” i naciśnij Enter. Spowoduje to otwarcie pliku konfiguracyjnego LSP.sublime-settings
dla pakietu LSP. Aby dodać klienta niestandardowego, użyj poniższej konfiguracji.
{ "clients": { "blacklister": { "command": [ "blacklist-server", "--stdio" ], "enabled": true, "languages": [ { "syntaxes": [ "Plain text" ] } ] } }, "log_debug": true }
Może to wyglądać znajomo z rozszerzenia VSCode. Zdefiniowaliśmy klienta, nakazaliśmy mu pracować na plikach tekstowych i określiliśmy serwer językowy.
Zapisz ustawienia, a następnie utwórz i zapisz zwykły plik tekstowy. Wpisz „foo” lub „bar” i poczekaj. Ponownie zobaczysz ostrzeżenia, że są one na czarnej liście. Sposób traktowania — sposób wyświetlania komunikatów w edytorze — jest inny. Jednak nasza funkcjonalność jest taka sama. Tym razem prawie nic nie zrobiliśmy, aby dodać wsparcie do edytora.
Język „Klient”: Vim
Jeśli nadal nie jesteś przekonany, że to rozdzielenie problemów ułatwia udostępnianie funkcji w edytorach tekstu, oto kroki, aby dodać tę samą funkcjonalność do Vima przez Coc.
Otwórz Vima i wpisz :CocConfig
, a następnie dodaj:
"languageserver": { "blacklister": { "command": "blacklist-server", "args": ["--stdio"], "filetypes": ["text"] } }
Gotowy.
Separacja klient-serwer pozwala prosperować językom i usługom językowym
Oddzielenie odpowiedzialności za usługi językowe od edytorów tekstu, w których są one używane, jest oczywiście wygraną. Pozwala twórcom funkcji językowych skupić się na swojej specjalizacji, a twórcom edytorów zrobić to samo. To całkiem nowy pomysł, ale adopcja się rozprzestrzenia.
Teraz, gdy masz już podstawę do pracy, może uda Ci się znaleźć projekt i pomóc w rozwinięciu tego pomysłu. Ognista wojna redaktorów nigdy się nie skończy, ale to OK. Dopóki umiejętności językowe mogą istnieć poza określonymi edytorami, możesz używać dowolnego edytora.
Jako Złoty Partner Microsoft, Toptal jest Twoją elitarną siecią ekspertów Microsoft. Twórz wysoko wydajne zespoły z ekspertami, których potrzebujesz - w dowolnym miejscu i dokładnie wtedy, gdy ich potrzebujesz!