Учебное пособие по протоколу языкового сервера: от VSCode до Vim
Опубликовано: 2022-03-11Главным артефактом всей вашей работы, скорее всего, являются простые текстовые файлы. Так почему бы вам не использовать Блокнот для их создания?
Подсветка синтаксиса и автоматическое форматирование — это лишь верхушка айсберга. Как насчет линтинга, завершения кода и полуавтоматического рефакторинга? Все это очень веские причины для использования «настоящего» редактора кода. Они жизненно важны для нашей повседневной жизни, но понимаем ли мы, как они работают?
В этом руководстве по протоколу языкового сервера мы немного рассмотрим эти вопросы и выясним, что заставляет работать наши текстовые редакторы. В конце вместе мы реализуем базовый языковой сервер вместе с примерами клиентов для VSCode, Sublime Text 3 и Vim.
Компиляторы и языковые службы
Мы пока пропустим подсветку синтаксиса и форматирование, которые обрабатываются статическим анализом — интересная тема сама по себе — и сосредоточимся на основной обратной связи, которую мы получаем от этих инструментов. Есть две основные категории: компиляторы и языковые службы.
Компиляторы берут ваш исходный код и выдают другую форму. Если код не соответствует правилам языка, компилятор вернет ошибки. Эти вполне знакомы. Проблема в том, что это обычно довольно медленно и ограничено по объему. Как насчет того, чтобы предложить помощь, пока вы еще создаете код?
Это то, что предоставляют языковые услуги. Они могут дать вам представление о вашей кодовой базе, пока она еще находится в работе, и, вероятно, намного быстрее, чем компилировать весь проект.
Объем этих услуг разнообразен. Это может быть что-то простое, например, возврат списка всех символов в проекте, или что-то сложное, например, возврат шагов для рефакторинга кода. Эти услуги являются основной причиной, по которой мы используем наши редакторы кода. Если бы мы просто хотели скомпилировать и увидеть ошибки, мы могли бы сделать это несколькими нажатиями клавиш. Лингвистические службы дают нам больше информации и очень быстро.
Ставка на текстовый редактор для программирования
Обратите внимание, что мы еще не вызывали определенные текстовые редакторы. Объясним почему на примере.
Скажем, вы разработали новый язык программирования под названием Lapine. Это красивый язык, и компилятор выдает потрясающие сообщения об ошибках, подобные Elm. Кроме того, вы можете предоставить автодополнение кода, справочные материалы, справку по рефакторингу и диагностику.
Какой редактор кода/текста вы поддерживаете в первую очередь? А что после этого? Вам предстоит тяжелая битва за то, чтобы заставить людей принять его, поэтому вы хотите сделать его как можно проще. Вы не хотите выбрать неправильный редактор и упустить пользователей. Что, если вы будете держаться подальше от редакторов кода и сосредоточитесь на своей специальности — языке и его функциях?
Языковые серверы
Введите языковые серверы . Это инструменты, которые общаются с языковыми клиентами и предоставляют информацию, о которой мы упоминали. Они не зависят от текстовых редакторов по причинам, которые мы только что описали в нашей гипотетической ситуации.
Как обычно, нам нужен еще один уровень абстракции. Они обещают разорвать тесную связь языковых инструментов и редакторов кода. Создатели языков могут один раз обернуть свои функции на сервере, а редакторы кода/текста могут добавить небольшие расширения, чтобы превратиться в клиентов. Это победа для всех. Однако, чтобы облегчить это, нам нужно договориться о том, как эти клиенты и серверы будут взаимодействовать.
К счастью для нас, это не гипотетически. Microsoft уже начала с определения протокола языкового сервера.
Как и в случае с большинством великих идей, она возникла из необходимости, а не из предвидения. Многие редакторы кода уже начали добавлять поддержку различных языковых функций; некоторые функции переданы сторонним инструментам, некоторые сделаны скрытно в редакторах. Возникли проблемы с масштабируемостью, и Microsoft взяла на себя инициативу по разделению. Да, Microsoft проложила путь для переноса этих функций из редакторов кода, вместо того, чтобы копить их в VSCode. Они могли бы продолжать создавать свой редактор, блокируя пользователей, но они освободили их.
Протокол языкового сервера
Протокол языкового сервера (LSP) был определен в 2016 году, чтобы помочь разделить языковые инструменты и редакторы. На нем все еще много отпечатков пальцев VSCode, но это важный шаг в направлении агностицизма редактора. Давайте немного изучим протокол.
Клиенты и серверы — редакторы кода и языковые инструменты — общаются с помощью простых текстовых сообщений. Эти сообщения имеют HTTP-подобные заголовки, содержимое JSON-RPC и могут исходить как от клиента, так и от сервера. Протокол JSON-RPC определяет запросы, ответы и уведомления, а также несколько основных правил для них. Ключевой особенностью является то, что он разработан для асинхронной работы, поэтому клиенты/серверы могут обрабатывать сообщения не по порядку и с определенной степенью параллелизма.
Короче говоря, JSON-RPC позволяет клиенту запросить другую программу для запуска метода с параметрами и возврата результата или ошибки. LSP основывается на этом и определяет доступные методы, ожидаемые структуры данных и еще несколько правил для транзакций. Например, есть процесс рукопожатия, когда клиент запускает сервер.
Сервер имеет состояние и предназначен только для обработки одного клиента за раз. Однако нет явных ограничений на общение, поэтому языковой сервер может работать на другом компьютере, чем клиент. Однако на практике это было бы довольно медленно для обратной связи в реальном времени. Языковые серверы и клиенты работают с одними и теми же файлами и довольно болтливы.
LSP имеет приличное количество документации, если вы знаете, что искать. Как уже упоминалось, многое из этого написано в контексте VSCode, хотя идеи имеют гораздо более широкое применение. Например, вся спецификация протокола написана на TypeScript. Чтобы помочь исследователям, незнакомым с VSCode и TypeScript, вот учебник.
Типы сообщений LSP
В протоколе языкового сервера определено множество групп сообщений. Их можно условно разделить на «админ» и «языковые функции». Сообщения администратора содержат сообщения, используемые при рукопожатии клиент/сервер, открытии/изменении файлов и т. д. Важно отметить, что именно здесь клиенты и серверы совместно используют функции, которые они обрабатывают. Конечно, разные языки и инструменты предлагают разные функции. Это также позволяет постепенное внедрение. Langserver.org называет полдюжины ключевых функций, которые должны поддерживать клиенты и серверы, по крайней мере одна из которых необходима для включения в список.
Языковые особенности — это то, что нас больше всего интересует. Из них есть одна, на которую стоит обратить особое внимание: диагностическое сообщение. Диагностика — одна из ключевых функций. Когда вы открываете файл, в основном предполагается, что он запустится. Ваш редактор должен сообщить вам, если с файлом что-то не так. Как это происходит с LSP:
- Клиент открывает файл и отправляет
textDocument/didOpen
на сервер. - Сервер анализирует файл и отправляет уведомление
textDocument/publishDiagnostics
. - Клиент анализирует результаты и отображает индикаторы ошибок в редакторе.
Это пассивный способ получить информацию от ваших языковых служб. Более активным примером может быть поиск всех ссылок на символ под вашим курсором. Это будет выглядеть примерно так:
- Клиент отправляет
textDocument/references
на сервер, указывая расположение в файле. - Сервер вычисляет символ, находит ссылки в этом и других файлах и отвечает списком.
- Клиент отображает ссылки на пользователя.
Инструмент черного списка
Конечно, мы могли бы углубиться в особенности протокола Language Server, но давайте оставим это для разработчиков клиента. Чтобы закрепить идею разделения редактора и языковых инструментов, мы будем играть роль создателя инструментов.
Мы будем упрощать и вместо создания нового языка и функций будем придерживаться диагностики. Диагностика хорошо подходит: это просто предупреждения о содержимом файла. Линтер возвращает диагностику. Сделаем что-то подобное.
Мы создадим инструмент, который уведомит нас о словах, которых мы хотели бы избежать. Затем мы предоставим эту функциональность паре различных текстовых редакторов.
Языковой сервер
Во-первых, инструмент. Мы встроим это прямо в языковой сервер. Для простоты это будет приложение Node.js, хотя мы могли бы сделать это с любой технологией, способной использовать потоки для чтения и записи.
Вот логика. Учитывая некоторый текст, этот метод возвращает массив совпадающих слов из черного списка и индексы, в которых они были найдены.
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 { 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()
Здесь мы используем vscode-languageserver
. Название вводит в заблуждение, так как оно определенно может работать вне VSCode. Это один из многих «отпечатков пальцев», которые вы видите в происхождении LSP. vscode-languageserver
заботится о протоколе нижнего уровня и позволяет вам сосредоточиться на вариантах использования. Этот фрагмент запускает соединение и связывает его с менеджером документов. Когда клиент подключается к серверу, сервер сообщает ему, что он хотел бы получать уведомления об открываемых текстовых документах.

Мы могли бы остановиться здесь. Это полноценный, хотя и бесполезный LSP-сервер. Вместо этого давайте ответим на изменения документа некоторой диагностической информацией.
documents.onDidChangeContent(change => { connection.sendDiagnostics({ uri: change.document.uri, diagnostics: getDiagnostics(change.document), }) })
Наконец, мы соединяем точки между измененным документом, нашей логикой и диагностическим ответом.
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', })
Наша диагностическая полезная нагрузка будет результатом прохождения текста документа через нашу функцию, а затем сопоставления с форматом, ожидаемым клиентом.
Этот скрипт создаст все это для вас.
curl -o- https://raw.githubusercontent.com/reergymerej/lsp-article-resources/revision-for-6.0.0/blacklist-server-install.sh | bash
Примечание. Если вас не устраивает, что исполняемые файлы добавляются на ваш компьютер незнакомыми людьми, проверьте источник. Он создает проект, загружает index.js
и npm link
его для вас.
Полный исходный код сервера
Окончательный источник blacklist-server
:
#!/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()
Учебное пособие по протоколу языкового сервера: время для тест-драйва
После link
проекта попробуйте запустить сервер, указав stdio
в качестве транспортного механизма:
blacklist-server --stdio
Теперь он прослушивает на stdio
сообщения LSP, о которых мы говорили ранее. Мы могли бы предоставить их вручную, но давайте вместо этого создадим клиент.
Языковой клиент: VSCode
Поскольку эта технология возникла в VSCode, кажется уместным начать с нее. Мы создадим расширение, которое создаст клиент LSP и подключит его к только что созданному серверу.
Существует несколько способов создать расширение VSCode, в том числе с помощью Yeoman и соответствующего генератора generator-code
. Однако для простоты давайте сделаем базовый пример.
Склонируем шаблон и установим его зависимости:
git clone [email protected]:reergymerej/standalone-vscode-ext.git blacklist-vscode cd blacklist-vscode npm i # or yarn
Откройте каталог blacklist-vscode
в VSCode.
Нажмите F5, чтобы запустить другой экземпляр VSCode для отладки расширения.
В «консоли отладки» первого экземпляра VSCode вы увидите текст «Смотрите, ма. Расширение!"
Теперь у нас есть базовое расширение VSCode, работающее без всяких наворотов. Давайте сделаем его клиентом LSP. Закройте оба экземпляра VSCode и из каталога blacklist-vscode
запустите:
npm i vscode-languageclient
Замените extension.js на:
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()) }, }
При этом используется пакет vscode-languageclient
для создания клиента LSP в VSCode. В отличие от vscode-languageserver
, это тесно связано с VSCode. Короче говоря, то, что мы делаем в этом расширении, — это создание клиента и указание ему использовать сервер, который мы создали на предыдущих шагах. Не обращая внимания на особенности расширения VSCode, мы видим, что мы говорим ему использовать этот LSP-клиент для простых текстовых файлов.
Чтобы протестировать его, откройте каталог blacklist-vscode
в VSCode. Нажмите F5, чтобы запустить другой экземпляр для отладки расширения.
В новом экземпляре VSCode создайте обычный текстовый файл и сохраните его. Введите «foo» или «bar» и немного подождите. Вы увидите предупреждения о том, что они занесены в черный список.
Вот и все! Нам не пришлось воссоздавать какую-либо нашу логику, просто согласовать клиент и сервер.
Давайте сделаем это снова для другого редактора, на этот раз для Sublime Text 3. Процесс будет очень похож и немного проще.
Языковой клиент: Sublime Text 3
Сначала откройте ST3 и откройте палитру команд. Нам нужна структура, чтобы сделать редактор клиентом LSP. Введите «Управление пакетами: установить пакет» и нажмите Enter. Найдите пакет «LSP» и установите его. После завершения у нас есть возможность указать клиентов LSP. Есть много пресетов, но мы не будем их использовать. Мы создали свои.
Снова откройте палитру команд. Найдите «Настройки: Настройки LSP» и нажмите Enter. Это откроет файл конфигурации LSP.sublime-settings
для пакета LSP. Чтобы добавить собственный клиент, используйте приведенную ниже конфигурацию.
{ "clients": { "blacklister": { "command": [ "blacklist-server", "--stdio" ], "enabled": true, "languages": [ { "syntaxes": [ "Plain text" ] } ] } }, "log_debug": true }
Это может показаться знакомым по расширению VSCode. Мы определили клиент, сказали ему работать с текстовыми файлами и указали языковой сервер.
Сохраните настройки, затем создайте и сохраните обычный текстовый файл. Введите «foo» или «bar» и подождите. Опять же, вы увидите предупреждения о том, что они занесены в черный список. Обработка — то, как сообщения отображаются в редакторе — отличается. Тем не менее, наши функции одинаковы. На этот раз мы почти ничего не сделали, чтобы добавить поддержку редактору.
Язык «Клиент»: Vim
Если вы все еще не уверены, что такое разделение обязанностей упрощает совместное использование функций текстовыми редакторами, вот шаги по добавлению той же функциональности в Vim через Coc.
Откройте Vim и введите :CocConfig
, затем добавьте:
"languageserver": { "blacklister": { "command": "blacklist-server", "args": ["--stdio"], "filetypes": ["text"] } }
Сделанный.
Разделение клиент-сервер позволяет языкам и языковым службам процветать
Отделение ответственности языковых служб от текстовых редакторов, в которых они используются, — это, безусловно, победа. Это позволяет создателям языковых функций сосредоточиться на своей специальности, а создателям редакторов делать то же самое. Это довольно новая идея, но распространение распространяется.
Теперь, когда у вас есть основа для работы, возможно, вы сможете найти проект и помочь продвинуть эту идею. Война флейма редакторов никогда не закончится, но это нормально. Пока языковые возможности могут существовать вне определенных редакторов, вы можете использовать любой редактор, который вам нравится.
Будучи золотым партнером Microsoft, Toptal — это ваша элитная сеть экспертов Microsoft. Создавайте высокопроизводительные команды с нужными вам экспертами - где угодно и именно тогда, когда они вам нужны!