Впервые я познакомился с Vim в университете, и с тех пор он был желанным спутником на протяжении большей части моей карьеры разработчика. Работа с программами на Python и Go казалась мне естественной с Vim, и я всегда чувствовал себя продуктивным. А вот Java была другим зверем. Когда появлялась возможность поработать с Java, я сначала пробовал Vim, но возвращался к IntelliJ и плагину IdeaVim, чтобы воспользоваться богатыми возможностями языка, которые открывает полноценная IDE.
К сожалению, у IntelliJ есть свои проблемы. В случайные, а иногда и в неподходящие моменты она просто перестаёт работать, пока не будут восстановлены все кэши, перезагружены проекты и не будет проведено полдня или больше за работой по устранению её неполадок. Пройдя через всю эту песню несколько месяцев назад, и глядя на прогресс в Vim, Neovim, спецификации протокола языкового сервера (Language Server Protocol, LSP) и их различных реализаций, я подумал, что, возможно, пришло время ещё раз взглянуть на использование Neovim в качестве Java IDE.
Возможно ли это? Да. Рекомендую ли я это делать? Возможно. Сошел ли я с ума? Возможно.
Поехали.
Если вы хотите сразу перейти к полной конфигурации, она доступна на Github.
Ландшафт
Для того чтобы превратить Neovim из простого текстового редактора в полноценную IDE, требуется несколько движущихся частей. Поэтому, прежде чем погружаться в заумные конфигурационные опции, стоит потратить немного времени, чтобы разобраться во всех составляющих и понять, как они взаимодействуют друг с другом.
Для меня важны такие фичи IDE, как навигация по коду (перейти к определению, найти ссылки, перейти к реализации), авто-дополнение кода, подсказки по типам и сигнатурам, рефакторинг и отладка. Современные реализации протокола языкового сервера покрывают большинство из этих возможностей, а более новый проект-компаньон - протокол отладочного адаптера - занимается частью отладки.
Все вместе: сервер языка, отладчик и отладочный адаптер взаимодействуют с кодом, как показано на следующей схеме:
![Компоненты IDE на базе Neovim Компоненты IDE на базе Neovim](https://habrastorage.org/getpro/habr/upload_files/863/db5/a34/863db5a3442e87f83d253578b9755dcc.png)
Эта схема хороша тем, что она не является специфичной для Java. Разобравшись с тем, как всё работает на одном языке, можно повторить этот процесс для любого языка, реализующего протокол языкового сервера и протокол отладочного адаптера. Для Java мы используем Eclipse JDT LS в качестве реализации языкового сервера и vscode-java-debug в качестве отладочного адаптера (который использует java-debug).
Приступаем
Neovim встраивает скриптовый движок Lua 5.1 и компилятор LuaJIT в сам редактор. Это означает, что полнофункциональный и высокопроизводительный язык доступен вам в любое время. Это также значительно снижает потребность в поддержке альтернативных языков. Я хотел упростить работу с Neovim, поэтому первое, что я сделал, - это отключил поддержку тех языковых провайдеров, которые я не использую:
-- отключение поддержки языковых провайдеров (оставляя плагины только на Lua и/или Vimscript)
vim.g.loaded_perl_provider = 0
vim.g.loaded_ruby_provider = 0
vim.g.loaded_node_provider = 0
vim.g.loaded_python_provider = 0
vim.g.loaded_python3_provider = 0
Практический эффект от этого изменения в том, что все плагины, которые я выбираю, работают только с нативными для Neovim языками: Vimscript и Lua. Пока что я не нахожу это изменение слишком ограничивающим, но время покажет. Интеграция Lua в Neovim привела к резкому увеличению как качества, так и количества подключаемых модулей, из которых можно выбирать.
Существует множество альтернативных плагинов Neovim, которые работают несколько иначе, и вы можете предпочесть их моим. В проекте Awesome Neovim собраны многие из лучших и наиболее зрелых плагинов.
Наконец, я решил использовать встроенный LSP-клиент Neovim, что сокращает количество необходимых зависимостей. Если для вас лёгкость использования важнее простоты конфигурации, вы можете отдать предпочтение coc.nvim.
Менеджер плагинов
Превращение Neovim в полнофункциональную IDE требует расширения его плагинами. В качестве менеджера плагинов на чистом Lua я выбрал packer.nvim. Для начала работы необходимо клонировать packer в свой packpath
- каталог, в котором ваш Neovim ищет плагины. Как только этот шаг будет выполнен, packer.nvim
будет управлять сам собой, и с этого момента вам не нужно беспокоиться о packpath
. Конфигурация по умолчанию на macOS (и на Linux, прим. перевод.) будет выглядеть следующим образом:
git clone --depth 1 https://github.com/wbthomason/packer.nvim ~/.local/share/nvim/site/pack/packer/start/packer.nvim
Затем можно написать спецификацию плагина на языке Lua. К примеру, отредактируйте файл ~/.config/nvim/lua/plugins.lua
, а затем загрузите его с помощью require('plugins')
в файле init.lua
.
Например, вот содержимое моего файла plugins.lua
:
return require('packer').startup(function(use)
-- Packer can manage itself
use 'wbthomason/packer.nvim'
use 'mfussenegger/nvim-dap'
use 'mfussenegger/nvim-jdtls'
use 'nvim-lua/plenary.nvim'
end)
packer.nvim
достаточно развит, потому что позволяет добавлять плагины и редактировать их конфигурацию в одном месте, но я обнаружил, что проще настраивать более сложные плагины отдельно и позволить Packer лишь заниматься их установкой. Как обычно, выбор за сами, так что не стесняйтесь вносить собственные коррективы по мере необходимости.
После того как у вас есть корректная конфигурация packer.nvim
, в ~/.config/nvim/init.lua
вы можете импортировать её с помощью require('plugins')
. Оттуда выполните команду :PackerInstall
для установки всех плагинов, перечисленных в конфигурации.
Проще всего добавлять по плагину за раз, затем понять, как его настроить, как его использовать и какую функциональность он обеспечивает, а только впоследствии добавлять другие. Таким образом вы точнее контролируете изменения, и не перегружаете себя и конфиги.
Языковой сервер — eclipse.jdt.ls
Основу работы Neovim, как IDE, составляет протокол языкового сервера (LSP). Для включения поддержки языка на уровне IDE нужен работающий языковой сервер. Для Java стандартом де-факто является eclipse.jdt.ls — Eclipse JDT Language Server.
Установить его на macOS можно с помощью Homebrew, обязательно записав место установки (в частности, номер версии):
$ > brew install jdtls
...
==> Pouring jdtls--1.18.0.all.bottle.tar.gz
? /opt/homebrew/Cellar/jdtls/1.18.0: 99 files, 42.8MB
На моей машине место установки — /opt/homebrew/Cellar/jdtls/1.18.0
. Путь до него нам понадобится позже для настройки LSP-клиента.
прим. перевод.:
Linux
Для установки на Linux можно попробовать использовать ваши стандартные системные менеджеры пакетов — ищите по ключам jdtls
и eclipse.jdt.ls
.
Или же просто скачайте стабильный релиз (1.26.0 на момент выхода перевода), разархивируйте, и просто положите, где удобно — в PATH
добавлять не нужно. Так же, как и в случае с macOS, нужно только запомнить путь до его установки.
Windows
Если вы отправились на всю эту авантюру из под Windows, то вам придётся переделывать все пути до файлов, которые вы встретите в этом гайде. Кроме того, готовьтесь чинить из ниоткуда возникающие проблемы на ровном месте, потому что Windows.
А что касается Eclipse JDT LS, то при скачивании и распаковке всё того же стабильного релиза, убедитесь, что вы помещаете его по пути, который не содержит пробелов. ¯_(ツ)_/¯
Клиент языкового сервера — Neovim и nvim-jdtls
Neovim поддерживает протокол языкового сервера (LSP) из коробки, выступая в качестве клиента для серверов LSP и предоставляя фреймворк на языке Lua под названием vim.lsp
для создания расширенных LSP-инструментов. Для начала работы со встроенным клиентом рекомендуется использовать nvim-lspconfig, который предоставляет стандартные конфигурации для многих языков.
Для некоторых языков существуют плагины, поддерживающие более богатую функциональность, чем предоставляет стандартный LSP. Одним из них является Java. nvim-jdtls предоставляет расширения для встроенного клиента, такие как организация импортируемых библиотек, извлечение переменных и генерация кода. И nvim-lspconfig
, и nvim-jdtls
используют встроенный в Neovim клиент, основные отличия заключаются в том, что nvim-jdtls
добавляет некоторые дополнительные обработчики и функциональность, а также упрощает конфигурацию. Одним из преимуществ использования nvim-jdtls
является то, что, запустив его, вы можете использовать те же сочетания клавиш Neovim и фичи клиента, которые вы уже используете для других языков, без необходимости изучать специфические для плагина способы взаимодействия.
На следующей схеме из документации по nvim-jdtls
показано, чем он отличается от nvim-lspconfig
. Обе программы используют уже встроенные в Neovim связки Lua, но устанавливаются и настраиваются несколько по-разному:
┌────────────┐ ┌────────────────┐
│ nvim-jdtls │ │ nvim-lspconfig │
└────────────┘ └────────────────┘
| |
start_or_attach nvim_lsp.jdtls.setup
│ |
│ настройка хука для java filetype │
│ ┌─────────┐ │
└───►│ vim.lsp │◄─────────────────┘
└─────────┘
Настройка nvim-jdtls
может пугать. Следующий пример конфигурации прокомментирован, чтобы показать, как я настраиваю nvim-jdtls
на своей машине для разработки. Большинство опций взяты непосредственно из документации Eclipse JDT LS и специфичны для него:
Сниппет кода на 164 строки
local home = os.getenv('HOME')
local jdtls = require('jdtls')
-- Типы файлов, которые обозначают корень Java-проекта, они будут использоваться jdtls.
local root_markers = {'gradlew', 'mvnw', '.git'}
local root_dir = require('jdtls.setup').find_root(root_markers)
-- jdtls хранит файлы, специфичные для проекта, внутри папки с оным. Если вы работаете с множеством
-- разных проектов, каждый должен будет использовать отдельную папку под такие файлы.
-- Эта переменная используется для конфигурации jdtls на использование названия папки
-- текущего проекта используя root_marker как папку под специфичные файлы проекта.
local workspace_folder = home .. "/.local/share/eclipse/" .. vim.fn.fnamemodify(root_dir, ":p:h:t")
-- Вспомогательная функция для создания сочетаний клавиш
function nnoremap(rhs, lhs, bufopts, desc)
bufopts.desc = desc
vim.keymap.set("n", rhs, lhs, bufopts)
end
-- Функция on_attach используется тут для настройки сочетаний клавиш после того,
-- как языковой сервер подключается к текущему буферу
local on_attach = function(client, bufnr)
-- Стандартные сочетания для LSP клиента Neovim
local bufopts = { noremap=true, silent=true, buffer=bufnr }
nnoremap('gD', vim.lsp.buf.declaration, bufopts, "Go to declaration")
nnoremap('gd', vim.lsp.buf.definition, bufopts, "Go to definition")
nnoremap('gi', vim.lsp.buf.implementation, bufopts, "Go to implementation")
nnoremap('K', vim.lsp.buf.hover, bufopts, "Hover text")
nnoremap('<C-k>', vim.lsp.buf.signature_help, bufopts, "Show signature")
nnoremap('<space>wa', vim.lsp.buf.add_workspace_folder, bufopts, "Add workspace folder")
nnoremap('<space>wr', vim.lsp.buf.remove_workspace_folder, bufopts, "Remove workspace folder")
nnoremap('<space>wl', function()
print(vim.inspect(vim.lsp.buf.list_workspace_folders()))
end, bufopts, "List workspace folders")
nnoremap('<space>D', vim.lsp.buf.type_definition, bufopts, "Go to type definition")
nnoremap('<space>rn', vim.lsp.buf.rename, bufopts, "Rename")
nnoremap('<space>ca', vim.lsp.buf.code_action, bufopts, "Code actions")
vim.keymap.set('v', "<space>ca", "<ESC><CMD>lua vim.lsp.buf.range_code_action()<CR>",
{ noremap=true, silent=true, buffer=bufnr, desc = "Code actions" })
nnoremap('<space>f', function() vim.lsp.buf.format { async = true } end, bufopts, "Format file")
-- Java расширения, предоставленные jdtls
nnoremap("<C-o>", jdtls.organize_imports, bufopts, "Organize imports")
nnoremap("<space>ev", jdtls.extract_variable, bufopts, "Extract variable")
nnoremap("<space>ec", jdtls.extract_constant, bufopts, "Extract constant")
vim.keymap.set('v', "<space>em", [[<ESC><CMD>lua require('jdtls').extract_method(true)<CR>]],
{ noremap=true, silent=true, buffer=bufnr, desc = "Extract method" })
end
local config = {
flags = {
debounce_text_changes = 80,
},
on_attach = on_attach, -- Передаём наши сочетания из on_attach в общие сочетания клавиш конфига
root_dir = root_dir, -- Устанавливаем корневую папку для найденного root_marker
-- Тут вы можете настроить специфичные для eclipse.jdt.ls параметры, которые будут передаваться LSP на его старте.
-- См.: https://github.com/eclipse/eclipse.jdt.ls/wiki/Running-the-JAVA-LS-server-from-the-command-line#initialize-request
-- для полного списка опций
settings = {
java = {
format = {
settings = {
-- Используем гайд по форматированию Java от Google
-- Убедитесь, что вы загрузили файл https://github.com/google/styleguide/blob/gh-pages/eclipse-java-google-style.xml
-- и поместили его в папку ~/.local/share/eclipse, например
url = "/.local/share/eclipse/eclipse-java-google-style.xml",
profile = "GoogleStyle",
},
},
signatureHelp = { enabled = true },
contentProvider = { preferred = 'fernflower' }, -- Используем утилиту fernflower для декомпиляции кода библиотек
-- Указываем опции для авто-дополнения
completion = {
favoriteStaticMembers = {
"org.hamcrest.MatcherAssert.assertThat",
"org.hamcrest.Matchers.*",
"org.hamcrest.CoreMatchers.*",
"org.junit.jupiter.api.Assertions.*",
"java.util.Objects.requireNonNull",
"java.util.Objects.requireNonNullElse",
"org.mockito.Mockito.*"
},
filteredTypes = {
"com.sun.*",
"io.micrometer.shaded.*",
"java.awt.*",
"jdk.*", "sun.*",
},
},
-- Указываем опции для организации импорта из библиотек
sources = {
organizeImports = {
starThreshold = 9999;
staticStarThreshold = 9999;
},
},
-- Параметры кодо-генерации
codeGeneration = {
toString = {
template = "${object.className}{${member.name()}=${member.value}, ${otherMembers}}"
},
hashCodeEquals = {
useJava7Objects = true,
},
useBlocks = true,
},
-- Если вы разрабатываете проекты используя разные версии Java, то нужно сообщить eclipse.jdt.ls местоположения ваших JDK.
-- См.: https://github.com/eclipse/eclipse.jdt.ls/wiki/Running-the-JAVA-LS-server-from-the-command-line#initialize-request
-- И ищете `interface RuntimeOption`.
-- ВАЖНО: Поле `name` НЕ выбирается произвольно, но должно соответствовать одному из элементов в `enum ExecutionEnvironment` по ссылке выше.
configuration = {
runtimes = {
{
name = "JavaSE-17",
path = home .. "/.asdf/installs/java/corretto-17.0.4.9.1", -- прим. перевод.: JDK, которые использует автор. У вас могут быть свои :)
},
{
name = "JavaSE-11",
path = home .. "/.asdf/installs/java/corretto-11.0.16.9.1",
},
{
name = "JavaSE-1.8",
path = home .. "/.asdf/installs/java/corretto-8.352.08.1"
},
}
}
}
},
-- cmd это тот набор аргументов, который будет передан в командной строке для старта jdtls
-- Заметьте, что тот использует Java версии 17 или выше.
-- См.: https://github.com/eclipse/eclipse.jdt.ls#running-from-the-command-line
-- для полного списка опций.
cmd = {
home .. "/.asdf/installs/java/corretto-17.0.4.9.1/bin/java",
'-Declipse.application=org.eclipse.jdt.ls.core.id1',
'-Dosgi.bundles.defaultStartLevel=4',
'-Declipse.product=org.eclipse.jdt.ls.core.product',
'-Dlog.protocol=true',
'-Dlog.level=ALL',
'-Xmx4g',
'--add-modules=ALL-SYSTEM',
'--add-opens', 'java.base/java.util=ALL-UNNAMED',
'--add-opens', 'java.base/java.lang=ALL-UNNAMED',
-- Если вы используете lombok, скачайте jar с ним и поместите его в ~/.local/share/eclipse
'-javaagent:' .. home .. '/.local/share/eclipse/lombok.jar',
-- Следующий jar файл расположен внутри папки, в которую вы установили/распаковали jdtls.
-- ВАЖНО: не забудьте изменить путь до jdtls ниже:
'-jar', vim.fn.glob('/opt/homebrew/Cellar/jdtls/1.18.0/libexec/plugins/org.eclipse.equinox.launcher_*.jar'),
-- Стандартная конфигурация для jdtls также расположена внутри его папки.
-- ВАЖНО: измените путь до jdtls, а также выберите конфиг-папку согласно вашей системе: config_win, config_linux или config_mac:
'-configuration', '/opt/homebrew/Cellar/jdtls/1.18.0/libexec/config_mac',
-- Переиспользуем workspace_folder определённый выше, чтобы хранить специфичные jdtls данные для проекта
'-data', workspace_folder,
},
}
-- Наконец, запускаем jdtls. Эта команда запустит языковой сервер с конфигурацией, которую мы предоставили,
-- настроит сочетания клавиш и закрепит LSP клиент за текущим буфером:
jdtls.start_or_attach(config)
Чтобы запустить jdtls
с использованием этой конфигурации, поместите приведенный выше файл в папку ~/.config/nvim/ftplugin/java.lua
. Neovim будет автоматически выполнять этот код всякий раз, когда в текущий буфер будет загружен файл типа Java. (ftplugin
это сокращение в конфигурации Neovim от filetype plugin).
Хотя конфигурация кажется очень большой, её можно разбить на несколько частей. Во-первых, мы прописываем необходимые сочетания клавиш для LSP-клиента. Затем мы указываем опции, которые должны передаваться jdtls
, и, наконец, задаем команду, которая будет использоваться для запуска jdtls
. Получив эту конфигурацию, мы передаем её в качестве параметра команде jdtls.start_or_attach
, которая запускает языковой сервер или присоединяется к существующему экземпляру, если сервер уже запущен.
С учётом того, что вам удалось корректно настроить и запустить jdtls
, в следующем скринкасте показано, как извлечь метод с помощью jdtls
. Доступные действия с кодом (code actions, прим. перевод.) отображаются с помощью telescope.nvim:
![Извлекаем метод с помощью jdtls Извлекаем метод с помощью jdtls](https://habrastorage.org/getpro/habr/upload_files/510/c3d/d32/510c3dd32c6142b2efb47b3eedd59b9c.gif)
jdtls
Отладка — nvim-dap
Протокол отладочного адаптера (Debug Adapter Protocol, DAP) является проектом-компаньоном для протокола языкового сервера. Идея протокола Debug Adapter Protocol (DAP) заключается в том, чтобы абстрагироваться от того, как поддержка отладки в средствах разработки взаимодействует с отладчиками или средами выполнения. Поскольку отладчики уже существуют для многих языков, DAP работает вместе с адаптером, обеспечивая соответствие существующего отладчика или среды выполнения протоколу отладочного адаптера, а не предполагает написание нового отладчика для соответствия протоколу.
nvim-dap - это реализация клиента DAP. Работая совместно с дебаг-адаптером, nvim-dap
может запускать приложение в режиме отладки, подключаться к работающим приложениям, устанавливать точки останова, перебирать код и проверять состояние приложения.
Для работы nvim-dap
требуется дебаг-адаптер, который выступает в роли посредника между nvim-dap
(клиентом) и дебаггером под конкретный язык. На следующей схеме, взятой из документации по nvim-dap
, показано взаимодействие этих компонентов.
Клиент DAP ----- Адаптер отладки ------- Дебаггер ------ Отлаживаемое приложение
(nvim-dap) | (под язык) | (под язык)
| |
| Коммуникация со спецификой реализации
| Адаптер отладки и отладчик (дебаггер) могут быть одним и тем же процессом
|
Коммуникация через Debug Adapter Protocol (DAP)
Подобно протоколу LSP, протокол DAP требует от нас установки дополнительных компонентов. Видимо, из-за относительной незрелости протокола DAP, этот процесс является более сложным, чем для LSP, к сожалению.
Java Debug Server - это реализация протокола отладочного адаптера, доступная на Github. Реализация основана на интерфейсе Java Debug Interface (JDI). Он работает с языковым сервером Eclipse JDT в качестве плагина для обеспечения отладочной функциональности, оборачивая отладочный сервер в плагине для Eclipse, работающим с jdtls
. Для регистрации java-debug
нам необходимо передать ему расположение jar-файлов в качестве опции инициализации. Для этого необходимо сначала скомпилировать плагин, а затем настроить его.
Компиляция плагина выполняется с помощью Maven:
Клонируете java-debug
Переходите в репо (
cd java-debug
)Запускаете
./mvnw clean install
После этого можно передать расположение jar-файла в качестве аргумента. Конфигурация jdtls
должна быть расширена примерно так:
local bundles = {
vim.fn.glob('<path-to-java-debug>/com.microsoft.java.debug.plugin/target/com.microsoft.java.debug.plugin-*.jar'),
}
local config = {
-- ...
on_attach = on_attach,
init_options = {
bundles = bundles
},
-- ...
}
Затем необходимо уведомить nvim-jdtls
о том, что отладочный адаптер доступен для использования. В функцию on_attach
добавьте require('jdtls').setup_dap()
, чтобы она зарегистрировала адаптер под Java.
config['on_attach'] = function(client, bufnr)
-- С `hotcodereplace = 'auto' отладочный адаптер попробует автоматически применять изменения в коде,
-- которые вы вносите во время отладочной сессии. Эту опцию можно просто убрать, если не нужна.
require('jdtls').setup_dap({ hotcodereplace = 'auto' })
end
nvim-dap
поддерживает подмножество параметров файла launch.json
, используемого для настройки отладочных адаптеров в Visual Studio Code. Для загрузки этого файла, используйте функцию load_launchjs
из модуля dap.ext.vscode
. Следующий код загрузит все конфигурации запуска, доступные в текущем проекте:
require('dap.ext.vscode').load_launchjs()
Напоследок необходимо настроить сочетания клавиш под отладку. Это те, которые использую я, вы можете изменить их в соответствии со своими потребностями:
function nnoremap(rhs, lhs, bufopts, desc)
bufopts.desc = desc
vim.keymap.set("n", rhs, lhs, bufopts)
end
-- nvim-dap
nnoremap("<leader>bb", "<cmd>lua require'dap'.toggle_breakpoint()<cr>", "Set breakpoint")
nnoremap("<leader>bc", "<cmd>lua require'dap'.set_breakpoint(vim.fn.input('Breakpoint condition: '))<cr>", "Set conditional breakpoint")
nnoremap("<leader>bl", "<cmd>lua require'dap'.set_breakpoint(nil, nil, vim.fn.input('Log point message: '))<cr>", "Set log point")
nnoremap('<leader>br', "<cmd>lua require'dap'.clear_breakpoints()<cr>", "Clear breakpoints")
nnoremap('<leader>ba', '<cmd>Telescope dap list_breakpoints<cr>', "List breakpoints")
nnoremap("<leader>dc", "<cmd>lua require'dap'.continue()<cr>", "Continue")
nnoremap("<leader>dj", "<cmd>lua require'dap'.step_over()<cr>", "Step over")
nnoremap("<leader>dk", "<cmd>lua require'dap'.step_into()<cr>", "Step into")
nnoremap("<leader>do", "<cmd>lua require'dap'.step_out()<cr>", "Step out")
nnoremap('<leader>dd', "<cmd>lua require'dap'.disconnect()<cr>", "Disconnect")
nnoremap('<leader>dt', "<cmd>lua require'dap'.terminate()<cr>", "Terminate")
nnoremap("<leader>dr", "<cmd>lua require'dap'.repl.toggle()<cr>", "Open REPL")
nnoremap("<leader>dl", "<cmd>lua require'dap'.run_last()<cr>", "Run last")
nnoremap('<leader>di', function() require"dap.ui.widgets".hover() end, "Variables")
nnoremap('<leader>d?', function() local widgets=require"dap.ui.widgets";widgets.centered_float(widgets.scopes) end, "Scopes")
nnoremap('<leader>df', '<cmd>Telescope dap frames<cr>', "List frames")
nnoremap('<leader>dh', '<cmd>Telescope dap commands<cr>', "List commands")
Увы, но проект java-debug
не поддерживает отладку для тестов, и для этого нам придется установить другой плагин. К счастью, он работает по аналогичной схеме. Чтобы отлаживать тесты, необходимо установить пакеты из vscode-java-test с помощью той же процедуры, которую мы использовали для java-debug
:
Сперва скомпилируйте jar-файлы из проекта:
Клонируете репозиторий
Переходите в репо (
cd vscode-java-test
)Запускаете
npm install
(с заранее установленным Node.js, прим. перевод.)Запускаете
npm run build-plugin
Затем дополните конфигурацию nvim-jdtls
пакетами из vs-code-java-test
:
-- Тут всё так же, как и в прошлой секции про DAP:
local bundles = {
vim.fn.glob("<path-to-java-debug>/com.microsoft.java.debug.plugin/target/com.microsoft.java.debug.plugin-*.jar", 1),
};
-- Это — дополнение для отладки тестов
vim.list_extend(bundles, vim.split(vim.fn.glob("<path-to-vscode-java-test>/server/*.jar", 1), "\n"))
local config = {
-- ...
on_attach = on_attach,
init_options = {
bundles = bundles
},
-- ...
}
Это открывает доступ к двум новым функциям nvim-jdtls
, которые я настраиваю со следующими сочетаниями клавиш:
nnoremap("<leader>vc", jdtls.test_class, bufopts, "Test class (DAP)")
nnoremap("<leader>vm", jdtls.test_nearest_method, bufopts, "Test method (DAP)")
В следующем скринкасте показан запуск и отладка теста с использованием nvim-dap
. После того как точка останова была достигнута, я открываю окно областей видимости, чтобы посмотреть на текущее состояние стека.
![Отладка с помощью nvim-dap Отладка с помощью nvim-dap](https://habrastorage.org/getpro/habr/upload_files/f17/fa6/72f/f17fa672fe8a7ad2c3c237433a8c7ee5.gif)
nvim-dap
Авто-дополнения — nvim-cmp
Следующей функцией, необходимой для создания полноценной среды разработки, является авто-дополнение. Для этого я обратился к плагину для Neovim под названием nvim-cmp. nvim-cmp
работает как базовый плагин, который расширяется источниками авто-дополнений. Источниками могут быть фрагменты кода (сниппеты), символы LSP или слова из текущего буфера.
Для начала установите nvim-cmp
вместе с необходимыми источниками авто-дополнений. На примере ниже я установил nvim-cmp
и используемые мною источники сниппетов и дополнений от LSP:
return require('packer').startup(function(use)
-- ...
use 'hrsh7th/nvim-cmp'
use 'hrsh7th/cmp-nvim-lsp'
use 'hrsh7th/cmp-vsnip'
use 'hrsh7th/vim-vsnip'
-- ...
end)
Языковые серверы предоставляют различные дополнения в зависимости от возможностей клиента. nvim-cmp
поддерживает больше типов авто-дополнений, чем стандартный для Neovim omnifunc
, поэтому мы должны заявлять о доступных вариантах, передаваемых LSP серверу, чтобы он мог их предоставить во время запроса на авто-дополнение. Эти варианты предоставляются с помощью вспомогательной функции require('cmp_nvim_lsp').default_capabilities
, которая может быть добавлена в нашу конфигурацию jdtls
:
-- nvim-cmp поддерживает дополнительные варианты авто-дополнений от LSP,
-- поэтому надо сообщить о них LSP-серверам
local capabilities = vim.lsp.protocol.make_client_capabilities()
capabilities = require('cmp_nvim_lsp').default_capabilities(capabilities)
local config = {
-- ...
capabilities = capabilities,
on_attach = on_attach,
-- ...
}
Затем необходимо настроить сам nvim-cmp
. Следующий фрагмент кода перечисляет те авто-дополнения и плагин для сниппетов, которые мы хотим использовать, и настраивает клавишу Tab для циклического перебора вариантов авто-дополнений и клавишу Enter для выбора определённого:
local cmp = require('cmp')
cmp.setup {
sources = {
{ name = 'nvim_lsp' },
{ name = 'nvim_lsp_signature_help' },
{ name = 'vsnip' },
},
snippet = {
expand = function(args)
vim.fn["vsnip#anonymous"](args.body) -- потому что используем плагин vsnip cmp
end,
},
mapping = cmp.mapping.preset.insert({
['<C-d>'] = cmp.mapping.scroll_docs(-4),
['<C-f>'] = cmp.mapping.scroll_docs(4),
['<C-Space>'] = cmp.mapping.complete(),
['<CR>'] = cmp.mapping.confirm {
behavior = cmp.ConfirmBehavior.Replace,
select = true,
},
['<Tab>'] = cmp.mapping(function(fallback)
if cmp.visible() then
cmp.select_next_item()
else
fallback()
end
end, { 'i', 's' }),
['<S-Tab>'] = cmp.mapping(function(fallback)
if cmp.visible() then
cmp.select_prev_item()
else
fallback()
end
end, { 'i', 's' }),
}),
}
Если же вы хотите отображать маленькие иконки рядом с предлагаемыми дополнениями, установите плагин onsails/lspkind.nvim
и настройте его, добавив блок formatting
в нашу конфигурацию cmp
:
local lspkind = require('lspkind')
cmp.setup {
-- ...
formatting = {
format = lspkind.cmp_format({
mode = 'symbol_text',
maxwidth = 50,
ellipsis_char = '...',
before = function (_, vim_item)
return vim_item
end
})
}
-- ...
}
В приведённом ниже скринкасте я показываю, как nvim-cmp
отображает набор доступных авто-дополнений, предоставляемых протоколом LSP. Маленькие иконки рядом с каждым типом дополнений взяты из lspkind
:
![Авто-дополнения с nvim-cmp Авто-дополнения с nvim-cmp](https://habrastorage.org/getpro/habr/upload_files/b6a/59d/22b/b6a59d22b4a4df48eaaa4fbad3cec0e0.gif)
nvim-cmp
Поиск — telescope-nvim
telescope.nvim - это очень расширяемый fuzzy-поиск по любым спискам. telescope
предоставляет и интерфейс, и функциональность для фильтрации и выбора элементов. Как и nvim-cmp
, telescope
расширяется путём добавления дополнительных источников данных, которые telescope
будет отображать и фильтровать.
Моя конфигурация telescope
использует fzf
для повышения производительности поиска, для этого необходимо установить telescope-fzf-native
, используя следующую конфигурацию:
use {'nvim-telescope/telescope-fzf-native.nvim', run = 'make' }
При работе с Java-проектами стандартная структура проекта приводит к длинным именам каталогов. Чтобы усечь имена каталогов и использовать fzf
для повышения производительности, я использую следующую конфигурацию:
require('telescope').setup({
defaults = {
path_display = {
shorten = {
len = 3, exclude = {1, -1}
},
truncate = true
},
dynamic_preview_title = true,
},
extensions = {
fzf = {
fuzzy = true, -- false активирует только точный поиск
override_generic_sorter = true,
override_file_sorter = true,
case_mode = "smart_case", -- другие варианты: "ignore_case", "respect_case".
-- стандартный режим это "smart_case", когда поиск регистро-независим
-- при вводе букв только в нижнем регистре, но регистро-зависим, если есть хотя бы одна
-- буква в верхнем.
}
}
})
require('telescope').load_extension('fzf')
Я довольно часто пользуюсь telescope
, и для поиска использую сочетания клавиш с префиксом <leader>f
:
-- telescope
nnoremap("<leader>ff", "<cmd>Telescope find_files<cr>", "Find file")
nnoremap("<leader>fg", "<cmd>Telescope live_grep<cr>", "Grep")
nnoremap("<leader>fb", "<cmd>Telescope buffers<cr>", "Find buffer")
nnoremap("<leader>fm", "<cmd>Telescope marks<cr>", "Find mark")
nnoremap("<leader>fr", "<cmd>Telescope lsp_references<cr>", "Find references (LSP)")
nnoremap("<leader>fs", "<cmd>Telescope lsp_document_symbols<cr>", "Find symbols (LSP)")
nnoremap("<leader>fc", "<cmd>Telescope lsp_incoming_calls<cr>", "Find incoming calls (LSP)")
nnoremap("<leader>fo", "<cmd>Telescope lsp_outgoing_calls<cr>", "Find outgoing calls (LSP)")
nnoremap("<leader>fi", "<cmd>Telescope lsp_implementations<cr>", "Find implementations (LSP)")
nnoremap("<leader>fx", "<cmd>Telescope diagnostics bufnr=0<cr>", "Find errors (LSP)")
В этом скринкасте я показываю, как telescope
можно использовать в качестве файлового браузера для быстрого поиска и открытия файла:
![Поиск файла с помощью telescope Поиск файла с помощью telescope](https://habrastorage.org/getpro/habr/upload_files/d72/6f2/9c9/d726f29c941c326eea5ff9588dee4c9b.gif)
telescope
Панель символов — symbols-outline
Ещё одной фичей IDE, которой я пользуюсь, является панель символов. Эта функция обеспечивает иерархическое, древовидное представление символов в текущем файле и их взаимосвязи друг с другом. Для этого я использую относительно простой плагин symbols-outline. Параметры по умолчанию вполне подходят для моего случая, но есть одно небольшое дополнение: автоматическое закрытие панели при любом последующем выделении. Для этого я использую следующую конфигурацию:
require("symbols-outline").setup {
auto_close = true,
}
Приведенные ниже сочетания клавиш также позволяет легко изменять размеры панели с помощью клавиш Ctrl-Shift-Стрелка вправо
и Ctrl-Shift-Стрелка влево
:
-- управление панелью
nnoremap("<C-S-Right>", "<cmd>:vertical resize -1<cr>", "Minimize window")
nnoremap("<C-S-Left>", "<cmd>:vertical resize +1<cr>", "Maximize window")
В этом скринкасте я открываю файл, затем перемещаюсь по символам верхнего уровня с помощью плагина symbols-outline
и выбираю один из них для перехода к нему:
![Панель символов Панель символов](https://habrastorage.org/getpro/habr/upload_files/9e0/4ed/7dd/9e04ed7dd3abe080849474c575ebe47a.gif)
Проводник — nvim-tree
Плагин nvim-tree представляет собой файловый обозреватель, написанный на языке Lua. После установки я использую следующие сочетания клавиш для открытия и закрытия файлового древа:
-- nvim-tree
nnoremap("<leader>nn", "<cmd>NvimTreeToggle<cr>", "Open file browser")
nnoremap("<leader>nf", "<cmd>NvimTreeFindFile<cr>", "Find in file browser")
Я также отключаю стандартный netrw
, поскольку не использую его и поскольку он может конфликтовать с nvim-tree
. Я также настроил панель на автоматическое закрытие при любом последующем выделении и автоматическое изменение размера до нужной ширины:
require("nvim-tree").setup({
disable_netrw = true,
view = {
adaptive_size = true,
float = {
enable = true,
},
},
actions = {
open_file = {
quit_on_open = true,
}
}
})
В приведенном ниже скринкасте показано использование nvim-tree
:
![Файловый браузер с nvim-tree Файловый браузер с nvim-tree](https://habrastorage.org/getpro/habr/upload_files/5d0/c30/507/5d0c30507dc08e95c477aa682aec6437.gif)
nvim-tree
Статусная строка — lualine
lualine — это плагин для statusline
(статусной строки Vim/Neovim, прим. перевод.), написанный на языке Lua. Плагин отображает полезную информацию о текущем файле, такую как тип файла, ветвь в git и кодировку. Единственное изменение, которое я вношу в lualine
, - это установка темы в соответствии с цветовой гаммой моего терминала:
require('lualine').setup {
options = { theme = 'onedark' },
}
Итог
Протокол LSP является отличным фундаментом для разработки построения IDE из редактора. Картину дополняет добавление LSP-клиента в Neovim, а также плагинов для расширения пользовательского интерфейса. Потратив некоторое время на конфигурацию и настройку, описанную в этой статье, я смог превратить Neovim в IDE под Java, ежедневно используя её как рабочую среду.
Чтобы начать свой собственный путь к IDE на базе Neovim, вы можете посмотреть мою полную конфигурацию на Github.
прим. перевод.: С недавних пор (с августа 2023) менеджер плагинов (Packer), используемый в статье, более не поддерживается разработчиками, а сам автор в конфигурации по ссылке стал использовать другой — lazy.nvim. Классная штука, попробуйте и вы :)
ИМХО: В случае Java, использование Vim или NeoVim это скорее отговорка, чтобы поковыряться в конфигах, плагинах и утилитах, чем работа над реальным приростом продуктивности работы в будущем. Учитывая Intellij IDEA Ultimate или Community, и замечательный плагин IdeaVim под обе. Ну да ладно, ведь в конечном счёте в Vim есть не только кривая входа, но и довольно крутая кривая выхода:)
Будет ещё много полезного материала по Java и/или CLI: как переводов, так и самостоятельных статей. Подписывайтесь тут на Хабре и в Телеграме, если ещё не.