Используем Neovim в качестве IDE для Java

—

от автора

Впервые я познакомился с 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

Эта схема хороша тем, что она не является специфичной для 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

Отладка — 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

Авто-дополнения — 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

Поиск — 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

Панель символов — 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

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


ссылка на оригинал статьи https://habr.com/ru/articles/760234/