Дайджесты новостей
Иллюстрация компонентной архитектуры языкового сервера на Go с автоматическим согласованием возможностей протокола LSP.

go-lsp: типизированная разработка LSP-серверов на Go без рутины JSON-RPC

Любой разработчик, создавший собственный проблемно-ориентированный язык (DSL), корпоративный шаблон конфигураций или кодогенератор, мечтает о богатом пользовательском опыте: чтобы в любимом редакторе код подсвечивался синтаксисом, при наведении курсора всплывала документация, а ошибки подсвечивались красной волнистой линией прямо во время ввода. Однако попытка реализовать сервер по протоколу Language Server Protocol (LSP 3.17) с нуля часто разбивается о стену технической рутины: парсинг заголовков Content-Length в JSON-RPC 2.0, диспетчеризация методов, управление состоянием открытых буферов и согласование возможностей (capabilities negotiation).

Библиотека owenrumney/go-lsp на языке Go берет на себя всю низкоуровневую черную работу, предоставляя разработчику элегантную компонентную архитектуру на основе стандартных интерфейсов.

Интерфейсная модель и автоматическое согласование

Вместо монолитных диспетчеров библиотека предлагает собирать сервер как конструктор. Единственное жесткое требование — реализовать интерфейс LifecycleHandler, отвечающий за методы инициализации (Initialize) и завершения работы (Shutdown). Все остальные функции протокола подключаются реализацией опциональных интерфейсов: HoverHandler, CompletionHandler, DefinitionHandler или FormattingHandler.

Во время фазы рукопожатия библиотека сама инспектирует методы переданной структуры и автоматически формирует блок ServerCapabilities для клиента (VS Code, Neovim, Helix). Разработчик освобожден от риска рассинхронизации: если метод реализован в коде, редактор немедленно узнает о поддержке функции.

Продемонстрируем минимальный сервер, возвращающий документацию при наведении курсора:

package main

import (
    "context"
    "github.com/owenrumney/go-lsp/lsp"
)

type MyDSLHandler struct{}

// Обязательный метод жизненного цикла
func (h *MyDSLHandler) Initialize(ctx context.Context, params *lsp.InitializeParams) (*lsp.InitializeResult, error) {
    return &lsp.InitializeResult{}, nil
}

func (h *MyDSLHandler) Shutdown(ctx context.Context) error {
    return nil
}

// Реализация HoverHandler для всплывающих подсказок
func (h *MyDSLHandler) Hover(ctx context.Context, params *lsp.HoverParams) (*lsp.Hover, error) {
    return &lsp.Hover{
        Contents: lsp.MarkupContent{
            Kind:  lsp.MarkupKindMarkdown,
            Value: "**MyDSL Keyword**: нативная директива оркестрации пайплайна",
        },
    }, nil
}

Запуск сервера и тестирование без редактора

Инициализация и запуск готового процесса сводятся к передаче потоков ввода-вывода в конструктор сервера:

package main

import (
    "os"
    "github.com/owenrumney/go-lsp/server"
)

func main() {
    handler := &MyDSLHandler{}
    srv := server.NewServer(handler)

    // Запуск на стандартных потоках stdin/stdout
    if err := srv.Run(os.Stdin, os.Stdout); err != nil {
        os.Exit(1)
    }
}

Огромным преимуществом библиотеки является встроенный тестовый пакет servertest. С его помощью можно проверять логику подсказок и валидации в стандартных unit-тестах Go через каналы в памяти (net.Pipe()), не запуская реальный процесс редактора.

Автор библиотеки Оуэн Рамни успешно применяет ее в боевых проектах make-ls (сервер для Makefiles) и goreleaser-ls. Библиотека позволяет собрать надежный специализированный языковой сервер за один вечер, опираясь на строгую типизацию Go.