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

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


  1. Tony-Sol
    11.09.2023 12:03

    Интересно, насколько стара оригинальная статья, потому что

    В качестве менеджера плагинов на чистом Lua я выбрал packer.nvim

    но в самом коде конфига используется lazy.nvim


    1. novusnota Автор
      11.09.2023 12:03

      Всего лишь прошлогодняя, 2022 г. :)
      Добавил информацию о lazy.nvim под конец перевода


  1. eojysele
    11.09.2023 12:03
    +2

    С недавних пор packer не поддерживается, поэтому лучше использовать другой менеджер плагинов (например, lazy).

    Так же могу порекомендовать удобный плагин для установки языковых серверов - Mason.


    1. novusnota Автор
      11.09.2023 12:03

      Да, хорошее замечание, спасибо :)


  1. 0Bannon
    11.09.2023 12:03

    Vim классный. Потихоньку изучаю.

    Единственеое, что не нравится в IDE типа pycharm - это долгая загрузка, много ОЗУ съедают. А однажды после обновы pycharm bsod выскочил. После этого только vscode или vim использую.


  1. souls_arch
    11.09.2023 12:03
    +1

    Когда скучно жить, много времени на глупости и мало Intellij IDEA, NetBeans, Eclips для Java... Как говорится, у разработчика нет цели, есть только путь.


    1. novusnota Автор
      11.09.2023 12:03

      С одной стороны — да, путь ради пути. А вот с другой стороны только так понимаешь, насколько в Java вне Intellij, NetBeans и Eclipse всё держится на скотче и палках. Иногда — без скотча :)


  1. f1opec
    11.09.2023 12:03

    Еще немного вводит в заблуждение выбор code_actions через telescope. На сколько я понял, этот функционал был выпилен из телескопа