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

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

Поддержку расширенных функций редактирования, таких как автозавершение исходного кода или переход к определению, для языка программирования в редакторе или интегрированной среде разработки обычно довольно сложно обеспечить, и это занимает много времени. Обычно для этого требуется написание модели предметной области (средства сканирования, средства синтаксического анализа, средства проверки типов, построителя и др.) на языке программирования редактора или интегрированной среды разработки. Например, подключаемый модуль Eclipse CDT, который обеспечивает поддержку C/C++ в интегрированной среде разработки Eclipse, написан на Java, так как сама среда IDE 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.

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

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

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

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

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

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

lsp flow diagram

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

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

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

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

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

Теперь давайте рассмотрим запрос "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 and sass

Возможности

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

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

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

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

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