天天看點

使用 Neovim 作為 Java IDE

我第一次學習 Vim 是在大學,從那時起,它一直是我軟體工程職業生涯的主要夥伴。 使用 Vim 使用 Python 和 Go 程式感覺很自然,而且我總是感覺很有效率。 然而,Java 始終是一個難以馴服的野獸。 每當有機會使用 Java 時,我都會不可避免地嘗試 Vim 一段時間,但會回過頭來使用 IntelliJ 和 IdeaVim 插件,以利用功能齊全的 IDE 提供的豐富語言功能。

不幸的是,IntelliJ 有它自己的問題。 在随機的,有時是不合時宜的時候,它會停止運作,直到重建所有緩存,重新附加元件目,并完成半天或更長時間的故障排除以使 IDE 正常工作。 直到幾個月前,看到了 Vim、Neovim 和語言伺服器協定規範和實作的進步,我想也許是時候重新審視一下使用 Neovim 作為 Java IDE 了。

是否可以? 是的。 我會推薦它嗎? 或許。 我瘋了嗎? 大概。

我們開始吧。

概覽

将 Neovim 從一個簡單的文本編輯器推向一個全功能的 IDE 需要一些部件,是以最好花一分鐘時間先了解所有涉及的部件以及它們如何互相互動,然後再深入了解一些神秘的配置 選項。

我從 IDE 中尋找的功能是代碼導航(轉到定義、查找參考、轉到實作)、代碼完成、簽名提示、重構和調試。 語言伺服器協定的現代實作涵蓋了大多數這些情況,而一個更新的配套項目——調試擴充卡協定——處理調試。

語言伺服器、調試器和調試擴充卡一起與您的代碼互動,如下圖所示。

使用 Neovim 作為 Java IDE

基于 Vim 的 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           

這一變化的實際效果意味着我選擇的所有插件都是原生的 vimscript 和 Lua。 到目前為止,我還沒有發現這種變化的局限,但時間會證明一切。 将 Lua 內建到 Neovim 中導緻可供選擇的插件在品質和數量上都出現爆炸式增長。

有許多替代的 Neovim 插件,它們的工作方式略有不同,您可能更喜歡我的設定。 Awesome Neovim 項目收集了許多最好和最成熟的插件。

最後,我選擇使用 neovim 内置的 LSP 用戶端,這減少了所需的依賴項數量。 如果您優先考慮易用性而不是簡單性,您可能更喜歡 coc.nvim。

插件管理器

将 Neovim 變成功能齊全的 IDE 需要使用插件對其進行擴充。 我選擇 packer.nvim 作為我的純 Lua 插件管理器。 要開始,您需要将 packer 克隆到您的包路徑,這是您的 Neovim 安裝找到包的目錄。 完成此步驟後,packer.nvim 将自行管理,從此時起您無需擔心 packpath。 macOS 上的預設配置是這樣的:

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,然後在 init.lua 檔案中使用 require('plugins') 加載該規範。

例如,這是我的 plugins.lua 檔案的内容:

return require('packer').startup(function(use)
  -- Packer 能自行管理
  use 'wbthomason/packer.nvim'

  use 'mfussenegger/nvim-dap'
  use 'mfussenegger/nvim-jdtls'
  use 'nvim-lua/plenary.nvim'
end)
           

Packer 非常複雜,允許您指定依賴項和設定插件作為插件規範的一部分,但我發現單獨設定更複雜的插件并讓 packer 簡單地處理安裝更簡單。 像往常一樣,使用您自己的判斷并根據需要調整。

一旦你有一個有效的packer配置,在 ~/.config/nvim/init.lua 你可以使用 require('plugins') 導入規範。 從那裡執行 :PackerInstall 指令來安裝您在規範中列出的任何插件。

如果您正在通過本指南将 Noevim 設定為 Java IDE,最簡單的方法是一次添加一個插件,了解如何配置它、如何使用它以及它提供的功能,然後添加更多插件 . 通過這種方式,您可以更好地了解您對 Neovim 環境所做的更改,而不會不知所措。

語言服務 — eclipse.jdt.ls

Neovim IDE 體驗的核心是由語言伺服器協定提供的。 要啟用對語言的類似 IDE 的支援,需要運作語言伺服器。 對于 Java,事實上的标準是 eclipse.jdt.ls — Eclipse JDT 語言伺服器。

您可以在 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 用戶端。

語言服務用戶端- Neovim 和 nvim-jdtls

Neovim 開箱即用地支援語言伺服器協定 (LSP),充當 LSP 伺服器的用戶端,并包含一個名為 vim.lsp 的 Lua 架構,用于建構增強的 LSP 工具。 開始使用内置用戶端的一般建議是使用 nvim-lspconfig,它為許多不同的語言提供預設配置。

某些語言具有支援更豐富的 LSP 功能的插件。 Java 就是其中之一。 nvim-jdtls 為内置的 LSP 用戶端提供擴充,例如組織導入、提取變量和代碼生成。 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
     │                              |
     │                             setup java filetype hook
     │    ┌─────────┐                  │
     └───►│ vim.lsp │◄─────────────────┘
          └─────────┘
           

配置 nvim-jdtls 可能會令人生畏。 以下示例配置被注釋以顯示我如何在我的開發機器上設定 nvim-jdtls。 大多數選項直接來自 Eclipse JDTLS 文檔并且特定于 jdtls。

local home = os.getenv('HOME')
local jdtls = require('jdtls')

-- 表示 Java 項目根目錄的檔案類型。 eclipse 将使用它來确定什麼構成工作區
local root_markers = {'gradlew', 'mvnw', '.git'}
local root_dir = require('jdtls.setup').find_root(root_markers)

--eclipse.jdt.ls 将項目特定資料存儲在一個檔案夾中。 如果您正在處理多個不同的項目,每個項目都必須使用專用的資料目錄。
-- 此變量用于配置 eclipse,以使用使用 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)
  -- Regular Neovim LSP client keymappings
  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")

  -- jdtls 提供的 Java 擴充
  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,  -- We pass our on_attach keybindings to the configuration map
  root_dir = root_dir, -- Set the root directory to our found root_marker
  -- 這裡可以配置eclipse.jdt.ls具體設定
  -- 這些由 eclipse.jdt.ls 項目定義,并在啟動時傳遞給 eclipse。
  settings = {
    java = {
      format = {
        settings = {
          -- 使用 Google Java 樣式指南進行格式化
          -- 要使用,請確定從 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反編譯庫代碼
      -- Specify any completion options
      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 使用您的 Java 版本的 JDK 的位置
      -- `name` 不是任意的,必須與上面連結中的 `enum ExecutionEnvironment` 中的元素之一比對
      configuration = {
        runtimes = {
          {
            name = "JavaSE-17",
            path = home .. "/.asdf/installs/java/corretto-17.0.4.9.1",
          },
          {
            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的東西。
  -- 注意eclipse.jdt.ls必須用17以上的Java版本啟動
  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,請下載下傳 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 的地方。 這将需要根據您的環境進行更新
    '-configuration', '/opt/homebrew/Cellar/jdtls/1.18.0/libexec/config_mac',

    -- 使用上面定義的 workspace_folder 來存儲這個項目的資料
    '-data', workspace_folder,
  },
}

-- 最後,啟動jdtls。 這将使用我們指定的配置運作語言伺服器,設定鍵映射,并将 LSP 用戶端附加到目前緩沖區
jdtls.start_or_attach(config)
           

要使用此配置啟動 jdtls,請将上面的檔案放在檔案夾 .config\nvim\ftplugin\java.lua 中。 每當将 Java 類型的檔案加載到目前緩沖區中時,Neovim 将自動執行此代碼。 (ftplugin 是檔案類型插件的簡寫)。

雖然配置看起來很多,但可以分解成幾個部分。 首先,我們為 LSP 用戶端建立所需的鍵映射。 然後我們指定要傳遞給 eclipse.jdt.ls 的選項,最後,我們設定用于啟動 eclipse.jdt.ls 的指令。 一旦我們獲得該配置,我們将其作為參數傳遞給 jdtls.start_or_attach,它将啟動語言伺服器或附加到現有的運作執行個體(如果伺服器已經啟動)。

假設您能夠啟動并運作 jdtls,以下截屏視訊顯示了如何使用 jdtls 提取方法。 可用的代碼操作是使用 telescope.nvim 呈現的。

調試— nvim-dap

調試擴充卡協定 (DAP) 是語言伺服器協定的配套項目。 調試擴充卡協定 (DAP) 背後的想法是抽象出開發工具的調試支援如何與調試器或運作時通信。 因為許多語言已經存在調試器,是以 DAP 與擴充卡一起工作以将現有調試器或運作時與調試擴充卡協定相比對,而不是假設需要編寫新的調試器來比對協定。

nvim-dap 是一個 DAP 用戶端實作。 與調試擴充卡一起工作,nvim-dap 可以啟動應用程式進行調試、附加到正在運作的應用程式、設定斷點、逐漸執行代碼以及檢查應用程式的狀态。

nvim-dap 需要一個調試擴充卡作為 nvim-dap(用戶端)和特定語言調試器之間的促進者。 下圖來自 nvim-dap 文檔,顯示了這些部分如何互動。

DAP-Client ----- Debug Adapter ------- Debugger ------ Debugee
(nvim-dap)  |   (per language)  |   (per language)    (your app)
            |                   |
            |        Implementation specific communication
            |        Debug adapter and debugger could be the same process
            |
     Communication via the Debug Adapter Protocol
           

與 LSP 協定一樣,DAP 協定需要我們安裝額外的元件。 不幸的是,也許由于 DAP 協定相對不成熟,該過程比 LSP 伺服器涉及更多。

Java 調試伺服器是 Github 上可用的調試擴充卡協定的實作。 該實作基于 Java 調試接口 (JDI)。 它作為插件與 Eclipse JDT 語言伺服器一起工作,通過将調試伺服器包裝在與 jdtls 一起工作的 Eclipse 插件中來提供調試功能。 要将 java-debug 注冊為 Eclipse 插件,我們需要将 jar 檔案的位置作為初始化選項傳遞給 Eclipse。 這需要首先編譯插件,然後配置 Eclipse 以使用插件。

編譯插件是通過 Maven 完成的:

  • 克隆 java-debug
  • 導航到克隆的存儲庫(cd java-debug)
  • 運作 ./mvnw 全新安裝

完成後,您可以将 jar 檔案的位置作為 Eclipse 的配置選項傳遞。 您的 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 支援用于在 Visual Studio Code 中配置調試擴充卡的 launch.json 檔案格式的子集。 要加載 launch.json 檔案,請使用 dap.ext.vscode 子產品中的 load_launchjs 函數。 以下代碼将加載目前項目中可用的啟動配置:

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 項目不支援調試測試,我們需要為此設定另一個插件。 值得慶幸的是,它遵循類似的過程。 為了能夠調試測試,有必要使用我們用于 java-debug 的相同過程從 vscode-java-test 安裝包:

首先從項目建構 jar 檔案。

  • 克隆存儲庫
  • 導航到檔案夾(cd vscode-java-test)
  • 運作 npm 安裝
  • 運作 npm run build-plugin

然後,擴充 nvim-jdtls 配置中的包以包含來自 vs-code-java-test 的包:

-- 這個 bundle 定義和上一節(java-debug 安裝)一樣
local bundles = {
  vim.fn.glob("<path-to-java-debug>/com.microsoft.java.debug.plugin/target/com.microsoft.java.debug.plugin-*.jar", 1),
};

-- This is the new part
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-cmp

建立全功能 IDE 體驗所需的下一個功能是代碼完成。 為此,我求助于 Neovim 的通用補全插件 nvim-cmp。 nvim-cmp 作為一個核心插件使用補全源進行擴充。 源可以是代碼片段、LSP 符号或來自目前緩沖區的單詞。

要開始使用 nvim-cmp,首先安裝 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 支援更多類型的完成候選,是以我們必須通告發送到伺服器的可用功能,以便它可以在完成請求期間提供這些候選。 這些功能是通過輔助函數 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 插件,并通過在我們的 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。

查找— telescope-nvim

telescope.nvim 是一個高度可擴充的清單模糊查找器。 telescope 提供用于從清單中過濾和選擇項目的界面和功能。 像 nvim-cmp 一樣,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 will only do exact matching
      override_generic_sorter = true,  -- override the generic sorter
      override_file_sorter = true,     -- override the file sorter
      case_mode = "smart_case",        -- or "ignore_case" or "respect_case"
                                       -- the default case_mode is "smart_case"
    }
  }
})
require('telescope').load_extension('fzf')
           

我大量使用了telescope,并使用一組以 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用作檔案浏覽器來快速查找和打開檔案。

檔案結構 — 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")
           

在這個截屏視訊中,我打開一個檔案,然後使用大綱插件浏覽頂級符号,然後選擇一個跳轉到。

檔案浏覽— 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 被用作檔案浏覽器。

狀态欄- lualine

lualine 是一個用 Lua 編寫的狀态行插件。 statusline 插件顯示有關目前檔案的有用資訊,例如檔案類型、git 分支和編碼。 我對 lualine 所做的唯一更改是将主題設定為與我的終端顔色主題相比對:

require('lualine').setup {
  options = { theme = 'onedark' },
}
           

最終結果

語言伺服器協定為開發全功能 IDE 提供了極好的支柱。 在 Neovim 中添加 LSP 用戶端,以及有助于使用者界面的插件,完成了我的目标。 在花了一些時間進行我在這篇文章中描述的配置和設定之後,我已經能夠将 Neovim 變成我用作日常工作環境的 Java IDE。