image


В сообществе фанатов текстового редактора Neovim произошло знаменательное событие — вышла версия 0.5, в которой появилось большое количество нововведений:


  • встроенная поддержка языка Lua;
  • экспериментальная поддержка treesitter;
  • и, наконец, встроенный LSP клиент, позволяющий сделать из простого текстового редактора достойного соперника IDE!

Neovim — это модальный редактор, форк редактора Vim, который ставит своей целью улучшение пользовательского опыта при работе с Vim: «Neovim is built for users who want the good parts of Vim, and more».


Мне нравится Neovim своей гибкостью, благодаря которой его можно превратить в очень мощный инструмент редактирования не только текста, но и кода. Как scala-разработчику мне интересно испытать новый встроенный LSP клиент в применении к любимому языку программирования. В отличие от VSCode и даже Vim + CoC настройка LSP клиента в Neovim несколько более сложная, но при этом крайне гибкая. Данная статья — краткое руководство по настройке Neovim для работы со Scala и краткий обзор возможностей, которые дает связка Neovim + Metals.


Исходный код проекта, использованного в анимациях

file: src/main/scala/example/Animals.scala


package example

trait Animals {
  def say(): String
}

case object Cat extends Animals {
  def say(): String = "meow"
}

case object Dog extends Animals {
  def say(): String = "woof"
}

file: src/test/scala/example/AnimalsTest.scala


package example

class AnimalsTest extends org.scalatest.funsuite.AnyFunSuite {
  test("what does a cat say?") {
    assert(Cat.say() == "meow")
  }
  test("what does a dog say?") {
    assert(Dog.say() == "woof")
  }
}

Тема редактора в анимациях: onehalf.


Что такое LSP?



Прежде всего, давайте разберемся с тем, что такое LSP и почему появление встроенного его клиента столь примечательно для Neovim. LSP, или Language Server Protocol, создан Microsoft для унификации опыта разработки в совершенно разных редакторах. Это мост между языками программирования и редакторами текста. Он позволяет выполнять над кодом операции, за которые мы так любим среды разработки:


  • go-to-definition;
  • find-references;
  • hover;
  • completion;
  • rename;
  • format;
  • refactor.

и всё это в любимом вами редакторе!


LSP подразумевает наличие двух компонентов: сервера, который реализует протокол для конкретного языка, и клиента, который умеет коммуницировать с сервером и выполнять необходимые действия внутри текстового редактора. Таким образом, LSP решает проблему, связанную с добавлением поддержки новых языков программирования в огромное количество редакторов кода. Вместо того, чтобы снова и снова, в редакторе за редактором тратить время и силы на поддержку очередного языка, теперь достаточно реализовать клиент к LSP в редакторе и сервер LSP для нового языка программирования!





Подготавливаем окружение


Реализация серверной части LSP для Scala называется Metals. Metals реализует протокол, но конкретные задачи, такие как форматирование, рефакторинг или компиляция, он делегирует другим инструментам. Например, Metals не занимается сборкой или запуском проектов, эти задачи делегируются другому серверу, реализующему BSP (Build Server Protocol). В качестве BSP сервера может выступать bloop и, начиная с версии 1.4, sbt. Оба инструмента используют компилятор scalac для компилирования scala-файлов в байт код JVM, и/или scalajs для компилирования в JavaScript.



Для полноценной работы Metals нам необходимо установить:


  • JVM — для запуска необходимых инструментов и scala-кода;
  • scalac — для компилирования scala-файлов;
  • bloop — для управления сборкой и запуском scala-проектов;
  • sbt (или любую другую систему сборки, способную сгенерировать конфигурацию для bloop) — для управления зависимостями и генерирования конфигурации для bloop;
  • scalafmt — для форматирования кода;
  • scalafix — для оптимизации импорта и прочего рефакторинга;
  • metals — как реализацию сервера LSP для навигации по коду, рефакторинга, отладки и проч.;
  • neovim — как реализацию клиента LSP для редактирования кода.

Список зависимостей достаточно велик, но, к счастью, для управления зависимостями в Scala существует крайне удобный инструмент: coursier. Он не только берет на себя задачу управления библиотеками, но и способен упростить процесс установки всех необходимых для работы со scala-инструментов:


coursier is a dependency resolver / fetcher à la Maven / Ivy, entirely rewritten from scratch in Scala. It aims at being fast and easy to embed in other contexts. Its core embraces functional programming principles.

Coursier может быть установлен с помощью скрипта:


# скачиваем скрипт
$ curl -fLo cs https://git.io/coursier-cli-"$(uname | tr LD ld)"
# делаем скрипт исполняемым
$ chmod +x cs

Полученный скрипт позволяет установить нужные нам инструменты командой ./cs install. И первое, что нам стоит установить, — непосредственно сам coursier:


# устанавливаем coursier через coursier
$ ./cs install cs
# удаляем скачанный скрипт
$ rm ./cs

Прочие варианты установки coursier описаны на сайте проекта.


Хорошая новость заключается в том, что coursier может помочь с установкой всех необходимых нам зависимостей связанных со Scala. Отличная новость заключается в том, что большинство необходимых инструментов можно установить всего одной командой!


$ cs setup --yes

В результате coursier услужливо установит для нас:


  • ammonite;
  • coursier;
  • scala;
  • scalac;
  • sbt;
  • sbtn;
  • scalafmt.

Почти полный набор. Остаётся установить bloop и scalafix:


$ cs install scalafix bloop

И, наконец, Neovim и Metals.


Устанавливаем и настраиваем Neovim



Процесс установки Neovim зависит от вашей системы и подробно описан здесь: Installing-Neovim.


Несмотря на появление поддержки Lua, авторы уверяют, что не собираются отказываться от VimL. Тем не менее в статье я буду описывать процесс настройки редактора именно с помощью Lua. Есть отличное руководство, помогающее начать использовать Lua в Neovim: Getting started using Lua in Neovim. Доступен перевод на русский: Начало работы с Lua в Neovim .


Конфигурация на Lua описывается в файле ~/.config/nvim/init.lua:


# Если после запуска nvim, в подвале его окна будет напечатано Hello World! 
# значит всё установлено корректно и можно приступать к настройке nvim
$ mkdir -p ~/.config/nvim/ && echo 'print("Hello world!")' >> ~/.config/nvim/init.lua

Начнем с установки менеджера плагинов Packer и настройки нескольких глобальных опций:


git clone https://github.com/wbthomason/packer.nvim\
 ~/.local/share/nvim/site/pack/packer/start/packer.nvim

----------------------------------
-- file:  ~/.config/nvim/init.lua
----------------------------------

-- включаем работу с мышью
vim.opt_global.mouse = 'a'

-- включаем подсветку синтаксиса
vim.opt_global.syntax = 'on'

-- делаем цветовую палитру богаче
-- (работает не для всех терминалов, если 
-- будут проблемы с отображением цвета, удалите
-- эту опцию)
vim.opt_global.termguicolors = true

-- используем 2 пробела вместо табуляции
vim.opt_global.tabstop = 2
vim.opt_global.softtabstop = 2
vim.opt_global.shiftwidth = 2

-- включаем менеджмент плагинами
vim.cmd('packadd packer.nvim')
require('packer').startup(function(use)
  -- будем использовать packer, чтобы обновлять packer
  use { 'wbthomason/packer.nvim', opt = true }
  -- новые плагины добавлять сюда:
  -- ...
end)

После добавления очередных плагинов в список необходимо их установить командой :PackerInstall.


Устанавливаем Metals



Рекомендованным клиентом к LSP для Scala всё ещё остается плагин к CoC во многом благодаря простоте настройки и проверке временем. Но сегодня мы рассмотрим процесс настройки Metals для нового, встроенного LSP клиента. У двух проектов существенно отличается взгляд на конфигурацию. Если CoC предоставляет практически законченное решение с автодополнением и прочими приятными возможностями, то LSP клиент в Neovim — это прежде всего api. Поэтому настройка встроенного LSP клиента может показаться сложнее, но в конечном счете это позволяет достичь более выдающихся результатов. Если интересно узнать о разнице в функциональных возможностях плагинов более подробно, советую посмотреть запись:



Вообще для конфигурации нового LSP клиента в neovim предлагается использовать nvim-lspconfig, но в случае co Scala предпочтительнее воспользоваться специальным плагином nvim-metals, так как этот плагин содержит огромное количество преднастроек. Пример минимальной конфигурации для работы с Metals (не забывайте выполнить :PackerInstall для установки nvim-metals):


----------------------------------
-- file:  ~/.config/nvim/init.lua
----------------------------------
require('packer').startup(function(use)
  -- ...
  -- устанавливаем metals
  use { 'scalameta/nvim-metals' }
end)

-- рекомендованная настройка, см README плагина
vim.opt_global.shortmess:remove("F"):append("c")

-- metals_config - таблица с конфигурацией nvim-metals
-- будем заполнять её постепенно и осмысленно
metals_config = require('metals').bare_config

-- при редактировании .scala или .sbt будем запускать/подключаться к metals
vim.cmd('augroup scalametals')
vim.cmd('autocmd!')
vim.cmd('autocmd FileType scala,sbt lua require("metals").initialize_or_attach(metals_config)')
vim.cmd('augroup end')

Несмотря на то, что с такой конфигурацией работать с metals уже возможно, делать это будет крайне неудобно из-за отсутствия горячих клавиш. Переопределение клавиш на Lua выглядит довольно громоздко, поэтому разумно написать пару вспомогательных функций:


-- mode   - режим, для которого определяется комбинация: "n", "i", "v"...
-- keys   - строка с комбинацией клавиш: "qq"
-- action - действие, выполняемое по нажатию комбинации keys: "<cmd>q<cr>"
-- opts   - таблица с дополнительными опциями, на пример: { expr = true }
-- bufnr  - идентификатор буфера. Если указан, клавиши будут назначены 
--          локально для этого буфера
local function map(mode, keys, action, opts, bufnr)
  local options = { noremap = true }
if opts then
    options = vim.tbl_extend("force", options, opts)
  end
  if bufnr then
    vim.api.nvim_buf_set_keymap(bufnr, mode, keys, action, options)
  else
    vim.api.nvim_set_keymap(mode, keys, action, options)
  end
end
-- аналог nnoremap
local function nmap(keys, action, opts)
  map('n', keys, action, opts)
end
-- аналог inoremap
local function imap(keys, action, opts)
  map('i', keys, action, opts)
end

Непосредственно комбинации клавиш — вопрос вкуса, ниже представлен пример моей конфигурации:


-- переход к месту определения символа под курсором:
nmap("gd", "<cmd>lua vim.lsp.buf.definition()<CR>")
-- поиск мест реализации символа под курсором:
nmap("gi", "<cmd>lua vim.lsp.buf.implementation()<CR>")
-- поиск мест, где используется символ под курсором:
nmap("gr", "<cmd>lua vim.lsp.buf.references()<CR>")
-- показать описание символа под курсором:
nmap("K", "<cmd>lua vim.lsp.buf.hover()<CR>")
-- показать сигнатуру метода:
nmap("<c-k>", "<cmd>lua vim.lsp.buf.signature_help()<CR>")
imap("<c-k>", "<cmd>lua vim.lsp.buf.signature_help()<CR>")
-- отформатировать текущий буфер:
nmap("<leader>f", "<cmd>lua vim.lsp.buf.formatting()<CR>")
-- переименовать текущий символ под курсором:
nmap("<space>rn", "<cmd>lua vim.lsp.buf.rename()<CR>")
-- показать все возможные действия в текущем контексте
-- (добавить необходимый импорт, реализовать недостающие методы и тд):
nmap("<space>ca", "<cmd>lua vim.lsp.buf.code_action()<CR>")
-- перейти к месту с ошибкой перед курсором:
nmap("[c", "<cmd>lua vim.lsp.diagnostic.goto_prev { wrap = false }<CR>")
-- перейти к месту с ошибкой после курсора:
nmap("]c", "<cmd>lua vim.lsp.diagnostic.goto_next { wrap = false }<CR>")

-- использовать LSP в качестве источника подсказок (комбинация по умолчанию <c-x><c-o>):
vim.cmd('set omnifunc=v:lua.vim.lsp.omnifunc')

Есть два подхода к конфигурированию горячих клавиш. Первый — глобальное конфигурирование, когда клавишам назначаются команды вне зависимости от того, запущен LSP клиент или нет. Лично я пока предпочитаю этот подход, так как он позволяет быстрее диагностировать ситуации, когда случаются проблемы со стартом клиента. Второй подход подразумевает назначение горячих клавиш в обработчике запуска клиента. Такой подход используются в примерах nvim-lspconfig. В nvim-metals его можно повторить, если определить маппинг в функции on_attach в таблице конфигурации metals_config:


metals_config.on_attach = function(client, bufnr)
  -- используем LSP для подсказок автодополнения
  vim.api.nvim_buf_set_option(bufnr, 'omnifunc', 'v:lua.vim.lsp.omnifunc')

  -- назначаем клавиши локально для буфера bufnr
  local function nmap(keys, action, opts) 
    map('n', keys, action, opts, bufnr)
  end
  local function imap(keys, action, opts) 
    map('i', keys, action, opts, bufnr) 
  end

  nmap("gd", "<cmd>lua vim.lsp.buf.definition()<CR>")
  nmap("gi", "<cmd>lua vim.lsp.buf.implementation()<CR>")
  ...
end

Обратите внимание на то, что при таком подходе комбинации клавиш назначаются локально для буфера. Идентификатор буфера bufnr, для которого происходит назначение, передается в обработчик on_attach плагином Metals.


Пример минимальной конфигурации для работы с metals
local function map(mode, keys, action, opts, bufnr)
  local options = { noremap = true }
if opts then
    options = vim.tbl_extend("force", options, opts)
  end
  if bufnr then
    vim.api.nvim_buf_set_keymap(bufnr, mode, keys, action, options)
  else
    vim.api.nvim_set_keymap(mode, keys, action, options)
  end
end
local function nmap(keys, action, opts)
  map('n', keys, action, opts)
end
local function imap(keys, action, opts)
  map('i', keys, action, opts)
end

vim.opt_global.mouse = 'a'
vim.opt_global.syntax = 'on'
vim.opt_global.tabstop = 2
vim.opt_global.softtabstop = 2
vim.opt_global.shiftwidth = 2
vim.opt_global.shortmess:remove("F"):append("c")

vim.cmd('packadd packer.nvim')
require('packer').startup(function(use)
  use { 'wbthomason/packer.nvim', opt = true }
  use { 'scalameta/nvim-metals' }
end)

vim.cmd('augroup scalametals')
vim.cmd('autocmd!')
vim.cmd('autocmd FileType scala,sbt lua require("metals").initialize_or_attach({})')
vim.cmd('augroup end')

vim.cmd('set omnifunc=v:lua.vim.lsp.omnifunc')

nmap("gd", "<cmd>lua vim.lsp.buf.definition()<CR>")
nmap("gi", "<cmd>lua vim.lsp.buf.implementation()<CR>")
nmap("gr", "<cmd>lua vim.lsp.buf.references()<CR>")
nmap("K", "<cmd>lua vim.lsp.buf.hover()<CR>")
nmap("<c-k>", "<cmd>lua vim.lsp.buf.signature_help()<CR>")
imap("<c-k>", "<cmd>lua vim.lsp.buf.signature_help()<CR>")
nmap("<leader>f", "<cmd>lua vim.lsp.buf.formatting()<CR>")
nmap("<space>rn", "<cmd>lua vim.lsp.buf.rename()<CR>")
nmap("<space>ca", "<cmd>lua vim.lsp.buf.code_action()<CR>")
nmap("[c", "<cmd>lua vim.lsp.diagnostic.goto_prev { wrap = false }<CR>")
nmap("]c", "<cmd>lua vim.lsp.diagnostic.goto_next { wrap = false }<CR>")

Собираем обратную связь


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



Если функциональность Metals не работает, начинать диагностику стоит с проверки состояния LSP клиента в Neovim. Если у вас установлен плагин nvim-lspconfig (для Metals он не нужен, но для других языков полезен, как минимум удобно иметь настроенный LSP для Lua), то сделать это можно командой: :LspInfo, для сбора более подробной информации можно включить логи: lua vim.lsp.set_log_level("debug").



Следующим шагом диагностики, не требующим дополнительных плагинов, является запуск :MetalsRunDoctor — встроенной утилиты для диагностики Metals:



Metals может слать сообщения о ходе текущих операций, которые можно отображать в строке состояния. Для этого надо сделать две вещи: включить отправку сообщений в конфигурации Metals и настроить строку состояния для их отображения. Справиться с первой задачей нам поможет таблица настроек metals_config, которую мы передаем при запуске Metals:


metals_config.init_options.statusBarProvider = "on"

Настройка строки состояния зависит от используемых для её отображения плагинов. Здесь в качестве примера я покажу, как можно настроить стоковую строку вообще без плагинов:


function InitStatusline()
  local background_color = '%#CursorColumn#'
  local current_file = '%t'
  local metals_status = '%{metals#status()}'
  local separator = '%='
  return background_color .. current_file .. separator .. metals_status
end
vim.opt_global.statusline = InitStatusline()


И, наконец, самый эффективный способ диагностики состояния Metals — чтение лог файла .metals/metals.log. Для нашего удобства nvim-metals позволяет открыть логи не покидая текстового редактора командой :MetalsToggleLogs. Эта команда буквально запускает tail во встроенном терминале nvim:



Но лично я предпочитаю держать отдельный терминал с запущенной tail -f .metals/metals.log.


Клиент или сервер LSP не единственные звенья, нарушающие работу всей цепочки компонентов. BSP сервера подводят не реже. В частности, в случае с bloop, иногда бывает полезным запустить компиляцию проекта в отдельном терминале напрямую и посмотреть на результат. Не часто, но случается, что bloop зависает, и его приходится перезапускать: bloop exit.


nvim-metals — больше чем конфигурация LSP клиента!


Пришло время рассказать, почему рекомендуется конфигурировать Metals именно с помощью nvim-metals.


Tree View Protocol


Начнем с того, что Metals поддерживает Tree View Protocol — расширение протокола LSP, позволяющее строить дерево проекта. И nvim-metals реализует частичную поддержку этого протокола. В нашем распоряжении две функции:


  • toggle_tree_view() — для отображение и/или скрытия дерева проекта,
  • reveal_in_tree() — для отображения текущего элемента в дереве проекта


Как правило, мне достаточно только второй функции:


nmap("<leader>tr", '<cmd>lua require("metals.tvp").reveal_in_tree()<CR>')

Подробнее о конфигурировании этого расширения можно почитать в документации: :help metals-tvp-module.


Debug Adapter Protocol


Еще одна важная функциональность, доступная в nvim-metals, — поддержка протокола DAP, который позволяет запускать и отлаживать приложения и тесты прямо в Neovim!



Пример конфигурации DAP:


----------------------------------
-- file:  ~/.config/nvim/init.lua
----------------------------------
require('packer').startup(function(use)
  ...
  -- устанавливаем плагин для отладки:
  use { 'mfussenegger/nvim-dap' }
end)

-- создаем конфигурации запуска приложения и тестов:
require("dap").configurations.scala = {
  {
    type = "scala",
    request = "launch",
    name = "Run",
    metals = {
      runType = "run",
    },
  },
  {
    type = "scala",
    request = "launch",
    name = "Test File",
    metals = {
      runType = "testFile",
    },
  },
}

-- запуск/продолжение исполнения приложения:
nmap("<leader>dc", "<cmd>lua require('dap').continue()<CR>")
-- скрыть/отобразить окно с выводом отладчика:
nmap("<leader>dr", "<cmd>lua require('dap').repl.toggle()<CR>")
-- показать значения переменных в текущем контексте:
nmap("<leader>ds", "<cmd>lua require('dap.ui.variables').scopes()<CR>")
-- установить/снять точку остановки:
nmap("<leader>dt", "<cmd>lua require('dap').toggle_breakpoint()<CR>")
-- выполнить шаг без захода в функцию:
nmap("<leader>do", "<cmd>lua require('dap').step_over()<CR>")
-- выполнить шаг с заходом в функцию:
nmap("<leader>di", "<cmd>lua require('dap').step_into()<CR>")
-- выйти из функции:
nmap('<leader>du', "<cmd>lua require('dap').step_out()<CR>")
-- прервать исполнение:
nmap('<leader>dst', "<cmd>lua require('dap').stop()<CR>")

Прочие полезные возможности и команды Metals


Одной статьи не хватит, чтобы охватить всю функциональность Metals, поэтому здесь я упомяну лишь несколько наиболее интересных, на мой взгляд команд и возможностей:


  • Поддержка Ammonite скриптов :MetalsAmmoniteStart, :MetalsAmmoniteStop;
  • Анализ стектрейса :MetalsAnalyzeStacktrace;
  • Создание нового scala-файла :MetalsNewScalaFile;
  • Создание нового scala-проекта с помощью g8 :MetalsNewScalaProject;

Более подробно о доступных командах можно почитать в документации: :help metals-commands.


Делаем мир UI лучше


В начале статьи я упоминал, что одно из преимуществ встроенного LSP клиента заключается в его гибкости в плане конфигурирования. Сейчас я покажу это на примерах.


Compe


Начнем с того, что вызвать автодополнение по Ctrl+X Ctrl+O, мягко говоря, не очень удобно. Чтобы улучшить опыт работы с автодополнением для nvim уже написано несколько плагинов, наиболее популярным из которых является nvim-compe:


----------------------------------
-- file:  ~/.config/nvim/init.lua
----------------------------------

-- настраиваем поведение автодополнения:
-- menueone - показывает всплывающее окно, 
--            даже когда есть всего один вариант;
-- noinsert - не вставлять текст пока пользователь
--            не выберет один из предложенных вариантов;
vim.opt_global.completeopt = { "menuone", "noinsert" }

require('packer').startup(function(use)
  ...
  -- устанавливаем compe
  use { 'hrsh7th/nvim-compe' }
end)

-- минимальная настройка плагина:
require('compe').setup {
  -- включаем плагин
  enabled = true;
  -- показываем всплывающее окно автоматически
  autocomplete = true;
  -- настраиваем источники подсказок
  source = {
    path = true;
    buffer = true;
    nvim_lsp = true;
  }
}

-- показывать варианты по нажатию Ctrl + Space
imap("<c-space>", "compe#complete()", { expr = true })
-- переход к следующему варианту
imap("<Tab>", 'pumvisible() ? "\\<C-n>" : "\\<Tab>"', { expr = true })
-- переход к предыдущему варианту
imap("<S-Tab>", 'pumvisible() ? "\\<C-p>" : "\\<Tab>"', { expr = true })
-- вставить текущий вариант
imap("<CR>", 'compe#confirm("\\<cr>")', { expr = true })

Подсветка текущего символа под курсором


Чтобы было проще ориентироваться в текущем файле с кодом, можно включить подсветку символа под курсором. Это позволит быстрее находить места, в которых он используется:



В интернете можно найти достаточно много вопросов о том, почему данная функциональность не работает. И ещё больше советов о том, как её починить. Большинство советов сводится к тому, чтобы определять настройки подсветки в правильной последовательности относительно настройки LSP клиента. Проверенное решение — определять настройку подсветки в обработчике запуска LSP клиента. В случае с Metals, функция, вызываемая в момент запуска клиента, определяется в таблице настроек metals_config и называется on_attach (мы уже встречали её ранее):


metals_config.on_attach = function(client, bufnr)
  ...
  vim.cmd([[hi! link LspReferenceText CursorColumn]])
  vim.cmd([[hi! link LspReferenceRead CursorColumn]])
  vim.cmd([[hi! link LspReferenceWrite CursorColumn]])
  vim.cmd([[autocmd CursorHold  <buffer> lua vim.lsp.buf.document_highlight()]])
  vim.cmd([[autocmd CursorHoldI <buffer> lua vim.lsp.buf.document_highlight()]])
  vim.cmd([[autocmd CursorMoved <buffer> lua vim.lsp.buf.clear_references()]])
end

Подсветка текущего символа происходит при наступлении события CursorHold. По умолчанию это событие происходит по истечении 4 секунд после последнего перемещения курсора. Стоит сократить это значение (обратите внимание на то, что эта настройка также влияет на частоту записи в swap-файл):


vim.opt_global.updatetime = 300

Better Quickfix Window


Quickfix window часто используется во многих фичах LSP, таких как переход к месту использования символа или поиск реализаций. Поэтому улучшение поведения самого quickfix window — довольно привлекательная идея. Плагин nvim-bqf позволяет выполнить предварительный просмотр найденного места в всплывающем окне:


default nvim-bqf
original with nvin-saga

LSP Saga


Следующий плагин целиком посвящен непосредственно улучшению пользовательского опыта работы с LSP: lspsaga. Вообще плагин предлагает альтернативное оформление для довольного большого набора функциональностей, но я предпочитаю пользоваться следующими:


  • Показать описание символа под курсором:
    default lspsaga
    original with nvim-saga



  • Показать сигнатуру метода:
    default lspsaga
    original with nvim-saga



  • Переименовать символ под курсором:
    default lspsaga
    original with nvim-saga



  • Показать список доступных действий:
    default lspsaga
    original with nvim-saga



  • Переход к предыдущей/следующей ошибке:
    default lspsaga
    original with nvim-saga



Соответствующие функции заменяются в конфигурации горячих клавиш:


nmap("K", "<cmd><nmap("K", "<cmd>lua require('lspsaga.hover').render_hover_doc()<CR>")
nmap("<c-k>", "<cmd>lua require('lspsaga.signaturehelp').signature_help()<CR>")
imap("<c-k>", "<cmd>lua require('lspsaga.signaturehelp').signature_help()<CR>")
nmap("<space>rn", "<cmd>lua require('lspsaga.rename').rename()<CR>")
nmap("<space>ca", "<cmd>lua require('lspsaga.codeaction').code_action()<CR>")
nmap("[c", "<cmd>lua require('lspsaga.diagnostic').lsp_jump_diagnostic_prev()<CR>")
nmap("]c", "<cmd>lua require('lspsaga.diagnostic').lsp_jump_diagnostic_next()<CR>")

Telescope


Я не могу в контексте Neovim не рассказать про Telescope. Этот плагин унифицирует опыт поиска чего бы то ни было в Neovim, а nvim-metals, в свою очередь, имеет встроенную интеграцию с Telescope. Следующая команда открывает окно поиска команд для Metals:


:Telescope metals commands


Если привязать команду к горячим клавишам, она оказывается довольно удобной в повседневном использовании.


nmap(";m", "<cmd>Telescope metals commands<CR>")

Еще удобно добавить директорию target в список исключений при поиске файлов:


local ignore_patterns = "{ file_ignore_patterns = { 'target/' } }"
nmap(";f", "<cmd>lua require('telescope.builtin').find_files(" .. ignore_patterns .. ")<CR>")

find_files find_files(ignore_patterns)
without filter with filter

На правах заключения


Наконец, я приведу список всех упомянутых плагинов и конечный вариант конфигурации:



Конечный вариант конфигурации
------------------------------------------------------------
-- Utilities
------------------------------------------------------------
-- {{{
local function map(mode, keys, action, opts, bufnr)
  local options = { noremap = true }
  if opts then
    options = vim.tbl_extend("force", options, opts)
  end
  if bufnr then
    vim.api.nvim_buf_set_keymap(bufnr, mode, keys, action, options)
  else
    vim.api.nvim_set_keymap(mode, keys, action, options)
  end
end
-- equivalent of nnoremap
local function nmap(keys, action, opts)
  map('n', keys, action, opts)
end
-- equivalent of inoremap
local function imap(keys, action, opts)
  map('i', keys, action, opts)
end
-- }}}

------------------------------------------------------------
-- Global options
------------------------------------------------------------
-- {{{
-- turn on mouse:
vim.opt_global.mouse = 'a'
-- turns on syntax highlight
vim.opt_global.syntax = 'on'
-- a 'size' of <Tab> in spaces
vim.opt_global.tabstop = 2
vim.opt_global.softtabstop = 2
-- number of spaces to use for each step of (auto)indent
vim.opt_global.shiftwidth = 2
-- a comma separated list of options for Insert mode completion
-- menuone  - use a popup menu to show the possible completions,
--            also when there is only one match;
-- noinsert - do not insert any text for a match until the user
--            selects a match from the menu;
vim.opt_global.completeopt = { "menuone", "noinsert" }
-- needed for correct work of the metals
vim.opt_global.shortmess:remove("F"):append("c")
-- speedup used for the CursorHold autocommand event
vim.opt_global.updatetime = 300
-- }}}

------------------------------------------------------------
-- Plugins
------------------------------------------------------------
-- {{{
vim.cmd([[packadd packer.nvim]])
require("packer").startup(function(use)
  -- packer can manage itself
  use { 'wbthomason/packer.nvim', opt = true }
  -- autocompletion
  use { "hrsh7th/nvim-compe" }
  -- dap
  use { 'mfussenegger/nvim-dap' }
  -- better quickfix
  use {  'kevinhwang91/nvim-bqf' }
  -- scala metals
  use { 'neovim/nvim-lspconfig' }
  use { 'scalameta/nvim-metals' }
  -- for better ui
  use { 'glepnir/lspsaga.nvim' }
  -- telescope
  use {
    'nvim-telescope/telescope.nvim',
    requires = {{'nvim-lua/popup.nvim'}, {'nvim-lua/plenary.nvim'}}
  }
end)
-- }}}

------------------------------------------------------------
-- DAP (Debug Adapter Protocol)
------------------------------------------------------------
-- {{{
require("dap").configurations.scala = {
  {
    type = "scala",
    request = "launch",
    name = "Run",
    metals = {
      runType = "run",
    },
  },
  {
    type = "scala",
    request = "launch",
    name = "Test File",
    metals = {
      runType = "testFile",
    },
  },
}
-- }}}

------------------------------------------------------------
-- Metals
------------------------------------------------------------
--{{{
metals_config = require'metals'.bare_config
metals_config.init_options.statusBarProvider = "on"
metals_config.root_patterns = { "build.sbt", "build.sc", ".git", ".bloop" }
metals_config.settings = {
   showImplicitArguments = true,
   showInferredType = true,
}

metals_config.handlers["textDocument/publishDiagnostics"] = vim.lsp.with(
  vim.lsp.diagnostic.on_publish_diagnostics, {
    virtual_text = {
      prefix = "",
    }
  }
)

metals_config.tvp = {
  panel_alignment = "right"
}

metals_config.on_attach = function(client, bufnr)
  -- turn on dap support
  require('metals').setup_dap()

  vim.api.nvim_buf_set_option(bufnr, 'omnifunc', 'v:lua.vim.lsp.omnifunc')

  -- buffer local mapping
  local function nmap(keys, action, opts) map('n', keys, action, opts, bufnr) end
  local function imap(keys, action, opts) map('i', keys, action, opts, bufnr) end

  -- go to definition
  nmap('gd', "<cmd>lua vim.lsp.buf.definition()<CR>")
  -- go to implementation
  nmap('gi', "<cmd>lua vim.lsp.buf.implementation()<CR>")
  -- show all places where the symbol under cursor is used
  nmap('gr', "<cmd>lua vim.lsp.buf.references()<CR>")
  -- show description of the symbol under cursor
  nmap('K', "<cmd>lua require('lspsaga.hover').render_hover_doc()<CR>")
  -- show signature of current method
  nmap('<c-k>', "<cmd>lua require('lspsaga.signaturehelp').signature_help()<CR>")
  imap('<c-k>', "<cmd>lua require('lspsaga.signaturehelp').signature_help()<CR>")
  -- format current buffer
  nmap('<leader>f', '<cmd>lua vim.lsp.buf.formatting()<CR>')
  -- renave the symbol under cursor
  nmap('<space>rn', '<cmd>lua require("lspsaga.rename").rename()<CR>')
  -- show all possible actions
  nmap('<space>ca', '<cmd>lua require("lspsaga.codeaction").code_action()<CR>')
  -- go to error before cursor
  nmap('[c', '<cmd>lua require("lspsaga.diagnostic").lsp_jump_diagnostic_prev()<CR>')
  -- go to error after cursor
  nmap(']c', '<cmd>lua require("lspsaga.diagnostic").lsp_jump_diagnostic_next()<CR>')
  -- show current buffer in the tree view
  nmap('<leader>tr', '<cmd>lua require("metals.tvp").reveal_in_tree()<CR>')

  -- DAP
  nmap('<leader>dc', '<cmd>lua require("dap").continue()<CR>')
  nmap('<leader>dl', '<cmd>lua require("dap").run_last()<CR>')
  nmap('<leader>dr', '<cmd>lua require("dap").repl.toggle()<CR>')
  nmap('<leader>ds', '<cmd>lua require("dap.ui.variables").scopes()<CR>')
  nmap('<leader>dt', '<cmd>lua require("dap").toggle_breakpoint()<CR>')
  nmap('<leader>do', '<cmd>lua require("dap").step_over()<CR>')
  nmap('<leader>di', '<cmd>lua require("dap").step_into()<CR>')
  nmap('<leader>du', '<cmd>lua require("dap").step_out()<CR>')
  nmap('<leader>dst', '<cmd>lua require("dap").stop()<CR>')

  -- Need for symbol highlights to work correctly
  vim.cmd([[hi! link LspReferenceText CursorColumn]])
  vim.cmd([[hi! link LspReferenceRead CursorColumn]])
  vim.cmd([[hi! link LspReferenceWrite CursorColumn]])
  vim.cmd([[autocmd CursorHold  <buffer> lua vim.lsp.buf.document_highlight()]])
  vim.cmd([[autocmd CursorHoldI <buffer> lua vim.lsp.buf.document_highlight()]])
  vim.cmd([[autocmd CursorMoved <buffer> lua vim.lsp.buf.clear_references()]])
end

-- Run metals on open scala file
vim.cmd([[augroup scalametals]])
vim.cmd([[autocmd!]])
vim.cmd([[autocmd FileType scala,sbt lua require("metals").initialize_or_attach(metals_config)]])
vim.cmd([[augroup end]])
--}}}

------------------------------------------------------------
-- Autocompletion
------------------------------------------------------------
--{{{
require'compe'.setup {
  enabled = true;
  autocomplete = true;
  source = {
    path = true;
    buffer = true;
    nvim_lsp = true;
  }
}
--}}}

------------------------------------------------------------
-- Statusline configuration
------------------------------------------------------------
-- {{{
function InitStatusline()
  local background_color = '%#CursorColumn#'
  local current_file = '%t'
  local metals_status = '%{metals#status()}'
  local separator = '%='
  return background_color .. current_file .. separator .. metals_status
end
vim.opt_global.statusline = InitStatusline()
--}}}

------------------------------------------------------------
-- Keymapping
------------------------------------------------------------
-- {{{

-- select next option in autocompletion
imap("<c-space>", "compe#complete()", { expr = true })
imap("<S-Tab>", 'pumvisible() ? "\\<C-p>" : "\\<Tab>"', { expr = true })
-- select next option in autocompletion
imap("<Tab>", 'pumvisible() ? "\\<C-n>" : "\\<Tab>"', { expr = true })
-- make choice by pressing <Enter>
imap("<CR>", "compe#confirm({ 'keys': '<CR>', 'select': v:true })", { expr = true })

-- find files
local ignore_patterns = "{ file_ignore_patterns = { 'target/' } }"
nmap(";f", "<cmd>lua require('telescope.builtin').find_files(" .. ignore_patterns .. ")<CR>")
nmap(";F", "<cmd>lua require('telescope.builtin').find_files({ hidden = true })<CR>")
-- find buffers
nmap(";e", "<cmd>lua require('telescope.builtin').live_grep()<CR>")
-- find recent files
nmap(";o", "<cmd>lua require('telescope.builtin').oldfiles()<cr>")
-- find metals commands
nmap("<leader>m", "<cmd>Telescope metals commands<CR>")
-- }}}

Несмотря на то, что в начале надо потратить время, чтобы разобраться с новым инструментом, вопреки тому, что на сегодня набор функций для рефакторинга у Metals довольно скудный, у Neovim есть что предложить scala-разработчику. Это более бережное отношение к ресурсам вашей системы, корректные диагностические сообщения прямиком от компилятора (!), высокая скорость редактирования кода (да, знаю что это субъективно, но если вы прочитали статью целиком, скорее всего вы сторонник этого утверждения :) ), одинаковый пользовательский опыт (что немаловажно — бесплатно) при работе с любым языком, для которого реализована поддержка LSP. И наконец, огромное количество плагинов, которые способны ещё сильнее сократить отличие от IDE.


Конечный выбор всегда за вами, но согласитесь, хорошо, когда есть из чего его делать. В общем...


Комментарии (6)


  1. Shtucer
    24.08.2021 12:52

    Поставить nvim и первым делом...

    > -- включаем работу с мышью

    забавно...


    1. Akon32
      24.08.2021 13:25

      Меня больше позабавила необходимость реализации map / imap / nmap.


  1. test_rt
    24.08.2021 13:05
    +1

    Полезная статья! Спасибо!


  1. Akon32
    24.08.2021 13:39
    +2

    Спасибо за статью (в последнее время нечасто встречаются статьи такого уровня).

    Что касается neovim, после прочтения статьи он кажется крайне сложным в установке. Необходимость установки 100500 плагинов, компонентов, да ещё и написания сотни-другой строк макросов - это никуда не годится с точки зрения пользователя редактора. Для сравнения, в VSCode Rust как-то работает после установки 1-2 плагинов, в Idea (rust и scala) - аналогично (может, чуть сложнее). ЕМНИП, и в VSCode, и в Idea используется LSP.

    Хотелось бы видеть в neovim поддержку языка, добавляемую установкой одного плагина, нажатием одной кнопки, а не это вот.


    1. dokwork Автор
      24.08.2021 13:54
      +1

      есть проект, который ставит своей целью подготовить конфигурацию, максимально приближающую neovim к IDE из коробки: LunarVim, но я не пробовал им пользоваться, так что не могу судить о качестве