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 есть следующие способы управления зависимостями:
- классический
go get
с использованиемGOPATH
- вендоринг, подразумевающий хранение всех зависимостей в директории
vendor
в корне проекта - модули, не требующие
GOPATH
вообще
Я решил сосредоточиться на поддержке способа #3, поскольку на сегодняшний день не считаю #1 и #2 удачными решениями(кстати, вендоринг никуда не уходит).
Что с поддержкой модулей у имеющихся инструментов:
gocode
не работает с модулями и мейнтейнер не планирует их поддерживатьgodef
с вендорингом так и не научился работать, модули не поддерживает, но есть надеждаgo-imports
напрямую не зависит от способа управления зависимостями, так что он универсален
Работать с форком gocode
мне не очень хочется, равно как и ждать/запиливать поддержку модулей в godef
. Выходит нужно найти замену для двух основных инструментов: gocode
и godef
.
Адаптация
Я сразу решил попробовать то что нам принёс VS Code - Language Server Protocol и его поддержку в emacs.
На сегодняшний день есть как минимум 2 готовых реализации Language Server Protocol для golang:
- go-langserver, который пока не поддерживает модули
- bingo, поддерживающий модули
К тому же команда 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
Необходимые внешние инструменты:
gomodifytags
из пакета github.com/fatih/gomodifytags для работыgo-tag
иgo-add-tags
gorename
из пакета golang.org/x/tools для работыgo-rename
gore
из пакета github.com/motemen/gore для работыgore-mode
иgorepl-mode
Что даёт следующие возможности:
- запускать 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 пакетов.
Такие дела.