Впервые я познакомился с Vim в университете, и с тех пор он был желанным спутником на протяжении большей части моей карьеры разработчика. Работа с программами на Python и Go казалась мне естественной с Vim, и я всегда чувствовал себя продуктивным. А вот Java была другим зверем. Когда появлялась возможность поработать с Java, я сначала пробовал Vim, но возвращался к IntelliJ и плагину IdeaVim, чтобы воспользоваться богатыми возможностями языка, которые открывает полноценная IDE.
К сожалению, у IntelliJ есть свои проблемы. В случайные, а иногда и в неподходящие моменты она просто перестаёт работать, пока не будут восстановлены все кэши, перезагружены проекты и не будет проведено полдня или больше за работой по устранению её неполадок. Пройдя через всю эту песню несколько месяцев назад, и глядя на прогресс в Vim, Neovim, спецификации протокола языкового сервера (Language Server Protocol, LSP) и их различных реализаций, я подумал, что, возможно, пришло время ещё раз взглянуть на использование Neovim в качестве Java IDE.
Возможно ли это? Да. Рекомендую ли я это делать? Возможно. Сошел ли я с ума? Возможно.
Поехали.
Если вы хотите сразу перейти к полной конфигурации, она доступна на Github.
Ландшафт
Для того чтобы превратить Neovim из простого текстового редактора в полноценную IDE, требуется несколько движущихся частей. Поэтому, прежде чем погружаться в заумные конфигурационные опции, стоит потратить немного времени, чтобы разобраться во всех составляющих и понять, как они взаимодействуют друг с другом.
Для меня важны такие фичи IDE, как навигация по коду (перейти к определению, найти ссылки, перейти к реализации), авто-дополнение кода, подсказки по типам и сигнатурам, рефакторинг и отладка. Современные реализации протокола языкового сервера покрывают большинство из этих возможностей, а более новый проект-компаньон - протокол отладочного адаптера - занимается частью отладки.
Все вместе: сервер языка, отладчик и отладочный адаптер взаимодействуют с кодом, как показано на следующей схеме:

Эта схема хороша тем, что она не является специфичной для 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Отладка — 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-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Поиск — 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Панель символов — 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 и выбираю один из них для перехода к нему:

Проводник — 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Статусная строка — 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: как переводов, так и самостоятельных статей. Подписывайтесь тут на Хабре и в Телеграме, если ещё не.
