emacs + go-mode + lsp + REPL

Некоторое время назад я начал переезжать с spacemacs обратно на самописную конфигурацию(мне так стабильнее и кастомайзить проще). Common LISP и scheme работают почти без настройки, а вот с golang пришлось поплясать.

Цель: настроить среду разработки, которая способна:

  • к автокомплиту
  • выводу документации
  • go to definition

Это минимум, необходимый мне для работы.

Скажу сразу:

  • не всё заработало
  • некоторые вещи тормозят
  • некоторые работают странно

Но жить можно, плюс ко всему - понятно что улучшать.

Прошлое встречается с настоящим

Я использовал следующие пакеты:

  • go-mode
  • company-go
  • go-imports

Эти пакеты основные, дающие следующие жизненно важные фичи:

  • автокомплит по проекту с помощью gocode
  • go to definition с помощью godef
  • поиск и добавление импортов через go-imports

Как многие знают, go 1.11 наконец-то получил что-то похожее на современный менеджер пакетов и теперь можно:

  • фиксировать версии зависимостей встроенными инструментами
  • не использовать переменную окружения GOPATH совсем

Но вместе с этим приятным новшеством стоит заметить, теперь в go есть следующие способы управления зависимостями:

  1. классический go get с использованием GOPATH
  2. вендоринг, подразумевающий хранение всех зависимостей в директории vendor в корне проекта
  3. модули, не требующие GOPATH вообще

Я решил сосредоточиться на поддержке способа #3, поскольку на сегодняшний день не считаю #1 и #2 удачными решениями(кстати, вендоринг никуда не уходит).

Что с поддержкой модулей у имеющихся инструментов:

  • gocode не работает с модулями и мейнтейнер не планирует их поддерживать
  • godef с вендорингом так и не научился работать, модули не поддерживает, но есть надежда
  • go-imports напрямую не зависит от способа управления зависимостями, так что он универсален

Работать с форком gocode мне не очень хочется, равно как и ждать/запиливать поддержку модулей в godef. Выходит нужно найти замену для двух основных инструментов: gocode и godef.

Адаптация

Я сразу решил попробовать то что нам принёс VS Code - Language Server Protocol и его поддержку в emacs.

На сегодняшний день есть как минимум 2 готовых реализации Language Server Protocol для golang:

К тому же команда go готовит собственную реализацию и призывает к сотрудничеству.

Так получилось что первым я попробовал go-langserver, поскольку с самого начала пошел не совсем верной дорогой, поставив lsp-go. В репозитории отсутствовал deprecation notice, так что я даже успел его отрефакторить, прежде чем узнал что он “всё” и вся функциональность теперь поддерживается в lsp-mode.

Но это дало мне возможность оценить легкость поддержки реализаций language server в emacs, вот пример elisp кода, который зарегистрирует language server и будет запускать его при открытии .go файлов:

(require 'lsp-mode)
(lsp-register-client
 (make-lsp-client
  :new-connection (lsp-stdio-connection "some-language-server-binary")
  :major-modes '(some-language-emacs-mode)
  :server-id 'some-language-server))

Методом проб и ошибок я пришел к следующей конфигурации для emacs:

Она использует use-package для более удобного управления зависимостями прямо из кода.

(defun config/packages/go ()
  (use-package go-mode
    :ensure t
    :config
    (add-to-list 'auto-mode-alist '("\\.go\\'" . go-mode)))
  (use-package go-tag :ensure t)
  (use-package go-add-tags :ensure t)
  (use-package go-playground :ensure t)
  (use-package go-rename :ensure t)
  (use-package go-stacktracer :ensure t)
  (use-package gore-mode :ensure t)
  (use-package gorepl-mode
    :ensure t
    :config
    (add-hook 'go-mode-hook #'gorepl-mode)))

(defun config/packages/lsp ()
  (use-package lsp-mode :ensure t)
  (use-package lsp-ui
    :ensure t
    :config
    (add-hook 'lsp-mode-hook 'lsp-ui-mode)
    (setq
     lsp-ui-doc-header t
     lsp-ui-doc-include-signature t
     lsp-ui-doc-use-childframe t
     lsp-ui-doc-position 'bottom
     lsp-ui-sideline-enable nil
     lsp-enable-eldoc nil))
  (use-package company-lsp
    :ensure t
    :config
    (add-to-list 'company-backends 'company-lsp)))

(config/packages/lsp)
(config/packages/go)

Данная конфигурация декларирует и вызывает 2 функции, настраивающие поддержку LSP и тулинг вокруг go:

  • config/packages/lsp
  • config/packages/go

Необходимые внешние инструменты:

Что даёт следующие возможности:

  • запускать language server когда это потребуется(не автоматически, дальше напишу почему) через вызов функции lsp
  • иметь автокомплит по проекту(с некоторыми оговорками)
  • добавлять импорты по C-a C-c или функцией go-import-add
  • добавлять/удалять теги у структур с помощью функций go-tag-add, go-tag-remove
  • отправлять буфер или выделенный регион в play.golang.org через функции с префиксом go-play
  • управлять локальным плейграундом через функции с префиксом go-playground
  • производить простейшие рефакторинги переименованием с помощью функции go-rename
  • преобразовывать стектрейсы в список ссылок на строки(к которым можно легко перейти из редактора)
  • использовать REPL через функции с префиксом gorepl или используя указанные хоткеи
  • смотреть документацию по символам под курсором, которая появляется в верхнем правом углу текущего буфера
  • переходить к месту декларации символа под курсором через lsp-find-definition(за префиксом lsp-find есть ещё более специфичные функции)

Теперь про оговорки и о том что не работает:

  • запускать language server стоит руками, потому что его запуск дорог и он способен значительно уменьшить время работы ноутбука от батарейки
  • включать language server приходится отдельно для каждого буфера
  • подсказки lsp-ui с документацией не всегда прибиты к верхней части буфера, не смотря на настройку
  • автокомплит иногда просто не работает без видимых причин
  • порой автокомплит показывает слижком много мусора, не имеющего отношения к инспектируемому символу

В остальном довольно юзабельно.

Вывод

Поддержка LSP в emacs и go всё ещё не без багов, но её уже можно использовать, более того, отсутствие поддержки модулей(vgo) в gocode делает language servers практически безальтернативным вариантом(если исключить возможность перехода на предлагаемый автором форк).

Остаётся следить за обновлениями официального репозитория с тулзами для go и ждать/помогать приблизить улучшение поддержки LSP в экосистеме go & emacs.

Бонус

Для тех кто дочитал до конца выкладываю nix derivation для LSP сервера bingo и расскажу небольшую историю про его “опакечивание”.

Когда я понял что мне нужен language server bingo и не нашел его в nixpgks мне пришлось заняться упаковкой, чтобы я мог использовать его в конфигурации своей рабочей станции. Чтобы запаковывать golang пакеты, поддерживающие vgo одним чуваком из сообщества был написан инструмент vgo2nix, которым я и попробовал воспользоваться, но тут началось довольно весёлое приключение:

Где-то в цепочке зависимостей bingo есть пакет git.apache.org/thrift.git, когда этот URL обрабатывается пакетом golang.org/x/tools/go/vcs то из него пропадает .git, а git.apache.org настроен таким образом чтобы делать редирект git.apache.org/thrift.git -> github.com/apache/thrift и пропажа .git из URL приводит к тому что git.apache.org начинает отдавать 404.

Гошный vcs.RepoRootForImportPath считает что если ему отдали 404 по HTTP то нужно попробовать по SSH. Спорный фолбэк конечно(скорее всего сделан для приватных репозиториев). Пока я пытался отдебажить, какие именно аргументы приходят в vcs.RepoRootForImportPath меня успешно забанили на фаерволе git.apache.org, похоже что у них установлен fail2ban :)

Таким образом я решил добавить поддержку рерайтов в vgo2nix и переписал весь инструмент(патч для HEAD b298f4f). После я смог сгенерировать deps.nix с правильной декларацией зависимостей, ссылающейся напрямую на github вместо git.apache.org.

Патч автору пока не отправлял, пришлось поправить несколько регулярок для парсинга версий в выводе go list -m all и я хочу попозже разобраться, можно ли без них обойтись и как vgo вообще генерирует версии для go пакетов.

Такие дела.