Поделиться через


Протокол сервера языка

Что такое протокол сервера языка?

Поддержка расширенных функций редактирования, таких как автозавершение исходного кода или переход к определению языка программирования в редакторе или интегрированной среде разработки, традиционно является очень сложным и трудоемким. Обычно это требует написания модели домена (сканера, средства синтаксического анализа, средства проверки типов, построителя и многого другого) на языке программирования редактора или интегрированной среды разработки. Например, подключаемый модуль Eclipse CDT, который обеспечивает поддержку C/C++ в интегрированной среде разработки Eclipse, записывается на Java, так как сама интегрированная среда разработки Eclipse написана на Java. Следуя этому подходу, это означает реализацию модели домена C/C++ в TypeScript для Visual Studio Code и отдельную модель домена в C# для Visual Studio.

Создание моделей предметных областей для конкретного языка также гораздо проще, если средство разработки может повторно использовать существующие библиотеки, относящиеся к языку. Однако эти библиотеки обычно реализуются на самом языке программирования (например, хорошие модели домена C/C++ реализуются в C/C++). Интеграция библиотеки C/C++ в редактор, написанный в TypeScript, технически возможна, но трудно сделать.

Языковые серверы

Другой подход заключается в том, чтобы запустить библиотеку в собственном процессе и использовать взаимодействие между процессами для общения с ней. Сообщения, отправляемые обратно и вперед, формируют протокол. Протокол сервера языка (LSP) — это продукт стандартизации сообщений, обменивающихся между средством разработки и процессом сервера языка. Использование языковых серверов или демонов не является новой или новой идеей. Редакторы, такие как Vim и Emacs, делают это в течение некоторого времени, чтобы обеспечить поддержку семантического автозавершения. Цель LSP заключалась в упрощении таких интеграций и предоставлении полезной платформы для предоставления возможностей языка различным инструментам.

Наличие общего протокола позволяет интегрировать функции языка программирования в средство разработки с минимальным суетом, повторно используя существующую реализацию модели предметной области языка. Серверная часть языка может быть написана на PHP, Python или Java, а LSP позволяет легко интегрировать её в различные инструменты. Протокол работает на общем уровне абстракции, чтобы средство может предлагать расширенные языковые службы, не требуя полного понимания нюансов, характерных для базовой модели предметной области.

Как началась работа над LSP

LSP развивался со временем, и сегодня он находится в версии 3.0. Она началась, когда концепция языкового сервера была выбрана OmniSharp, чтобы обеспечить широкие возможности редактирования для C#. Изначально OmniSharp использовал протокол HTTP с полезными данными JSON и был интегрирован в несколько редакторов, включая Visual Studio Code.

Примерно в то же время корпорация Майкрософт начала работать на сервере языка TypeScript с идеей поддержки TypeScript в редакторах, таких как Emacs и Sublime Text. В этой реализации редактор взаимодействует через stdin/stdout с серверным процессом TypeScript и использует JSON-полезную нагрузку, основанную на протоколе отладчика V8 для запросов и ответов. Сервер TypeScript был интегрирован в подключаемый модуль TypeScript Sublime и VS Code для расширенного редактирования TypeScript.

После интеграции двух разных языковых серверов команда Visual Studio Code начала изучать общий протокол языкового сервера для редакторов и IDE. Общий протокол позволяет поставщику языков создавать один языковой сервер, который может использоваться различными IDE. Потребитель сервера языка должен реализовать только клиентскую сторону протокола один раз. Это приводит к выигрышной ситуации как для поставщика языка, так и для потребителя языка.

Протокол сервера языка начался с протокола, используемого сервером TypeScript, расширяя его с дополнительными языковыми функциями, вдохновленными API языка VS Code. Протокол поддерживается JSON-RPC для удаленного вызова из-за простоты и существующих библиотек.

Команда VS Code создала прототип протокола, реализуя несколько языковых серверов lint, которые отвечают на запросы для выполнения lint-анализа (сканирования) файла и возвращают набор обнаруженных предупреждений и ошибок. Цель заключалась в том, чтобы проверять файл по мере редактирования пользователем документа, что означает, что во время сеанса редактора будет много запросов на линтинг. Имеет смысл удерживать сервер в рабочем состоянии, чтобы новый процесс линтинга не запускался для каждого редактирования пользователя. Было реализовано несколько линтер-серверов, включая расширения для VS Code: ESLint и TSLint. Эти два сервера linter реализованы в TypeScript/JavaScript и выполняются в Node.js. Они совместно используют библиотеку, которая реализует клиент и серверную часть протокола.

Принцип работы LSP

Сервер языка работает в собственном процессе, а такие инструменты, как Visual Studio или VS Code, взаимодействуют с сервером с использованием языкового протокола через JSON-RPC. Еще одним преимуществом сервера языка, работающего в выделенном процессе, является то, что проблемы с производительностью, связанные с одной моделью процессов, избегаются. Фактический транспортный канал может быть stdio, сокеты, именованные каналы или IPC Node.js, если клиент и сервер написаны на Node.js.

Ниже приведен пример того, как средство и сервер языка взаимодействуют во время обычного сеанса редактирования:

Схема потока lsp

  • Пользователь открывает файл (который называется документом) в инструменте: средство уведомляет сервер языка о том, что документ открыт ("textDocument/didOpen"). Теперь правда о содержимом документа больше не находится в файловой системе, но хранится средством в памяти.

  • Пользователь вносит изменения: средство уведомляет сервер об изменении документа (textDocument/didChange), а семантическая информация программы обновляется сервером языка. В процессе языковой сервер анализирует эти сведения и уведомляет инструмент об обнаруженных ошибках и предупреждениях (textDocument/publishDiagnostics).

  • Пользователь выполняет "Перейти к определению" на символе в редакторе: инструмент отправляет запрос "textDocument/definition" с двумя параметрами: (1) URI документа и (2) позиция текста, из которой был инициирован запрос "Перейти к определению", на сервер. Сервер отвечает с помощью URI документа и позиции определения символа внутри документа.

  • Пользователь закрывает документ (файл): уведомление textDocument/didClose отправляется из средства, уведомляя сервер языка о том, что документ больше не в памяти, и что текущее содержимое теперь актуально в файловой системе.

В этом примере показано, как протокол взаимодействует с языковым сервером на уровне функций редактора, таких как "Перейти к определению", "Найти все ссылки". Типы данных, используемые протоколом, представляют собой «типы данных» редактора или интегрированной среды разработки, такие как открытый текстовый документ и положение курсора. Типы данных не находятся на уровне модели домена языка программирования, которая обычно предоставляет абстрактные деревья синтаксиса и символы компилятора (например, разрешенные типы, пространства имен, ...). Это значительно упрощает протокол.

Теперь рассмотрим запрос textDocument/definition более подробно. Ниже перечислены данные, которые передаются между клиентским инструментом и языковым сервером для запроса «Перейти к определению» в документе C++.

Это запрос:

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

Это ответ:

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

В ретроспективе описание типов данных на уровне редактора, а не на уровне языковой модели программирования является одной из причин успеха протокола сервера языка. Гораздо проще стандартизировать URI текстового документа или позицию курсора по сравнению с стандартизацией абстрактного дерева синтаксиса и символов компилятора на разных языках программирования.

Когда пользователь работает с различными языками, VS Code обычно запускает языковой сервер для каждого языка программирования. В приведенном ниже примере показано сеанс, в котором пользователь работает с файлами Java и SASS.

java и sass

Capabilities

Не каждый языковой сервер может поддерживать все функции, определенные протоколом. Поэтому клиент и сервер объявляют о поддерживаемых функциях с помощью "возможностей". Например, сервер объявляет, что может обрабатывать запрос textDocument/definition, но может не обрабатывать запрос "workspace/symbol". Аналогичным образом клиенты могут сообщить, что они могут предоставлять уведомления "о сохранении" перед сохранением документа, чтобы сервер мог вычислить текстовые изменения для автоматического форматирования редактируемого документа.

Интеграция языкового сервера

Фактическая интеграция сервера языка с определенным инструментом не определяется протоколом сервера языка и остается для разработчиков инструментов. Некоторые средства интегрируют языковые серверы универсально, имея расширение, которое может запускать и взаимодействовать с любым языковым сервером. Другие, такие как VS Code, создают пользовательское расширение для каждого сервера языка, чтобы расширение по-прежнему могло предоставлять некоторые функции пользовательского языка.

Чтобы упростить реализацию языковых серверов и клиентов, существуют библиотеки или пакеты SDK для частей клиента и сервера. Эти библиотеки предоставляются для разных языков. Например, существует модуль npm клиента языка для упрощения интеграции языкового сервера с расширением VS Code и другой модуль npm для разработки языкового сервера с использованием Node.js. Это текущий список библиотек поддержки.

Использование протокола сервера языка в Visual Studio