mfussenegger / nvim-jdtls

Extensions for the built-in LSP support in Neovim for eclipse.jdt.ls
GNU General Public License v3.0
1.09k stars 62 forks source link

fix: support multi clients #350

Closed fengwk closed 2 years ago

fengwk commented 2 years ago

support multi clients

mfussenegger commented 2 years ago

Could you explain what problem this fixes?

If it is that you're having multiple jdtls clients because you've multiple projects open at the same time, you could add a { bufnr = bufnr } filter to get_active_clients call instead.

fengwk commented 2 years ago

I use workspaces.nvim to record the paths of different projects and open them. There are times when multiple java projects are opened, which can cause multiple jdtls servers to start.

When I used the super implementation, I found that the later opened project used the wrong jdtls client (It belongs to the previous project, and it doesn't work correctly).

I'm not familiar with the api, but I happened to find vim.lsp.buf_get_clients(), and the api worked as I expected.

Here is my configuration, is there another way to solve this problem?

-- 每个java缓冲区需要执行setup函数,首次执行将启动lsp并attach,后续仅attach

local jdtls = require "jdtls"
local utils = require "user.utils"
local lsp_utils = require "user.ide.lsp-utils"

local java_home_preset = {
  java_home_5 = os.getenv("JAVA_HOME_5"),
  java_home_6 = os.getenv("JAVA_HOME_6"),
  java_home_7 = os.getenv("JAVA_HOME_7"),
  java_home_8 = os.getenv("JAVA_HOME_8"),
  java_home_9 = os.getenv("JAVA_HOME_9"),
  java_home_10 = os.getenv("JAVA_HOME_10"),
  java_home_11 = os.getenv("JAVA_HOME_11"),
  java_home_12 = os.getenv("JAVA_HOME_12"),
  java_home_13 = os.getenv("JAVA_HOME_13"),
  java_home_14 = os.getenv("JAVA_HOME_14"),
  java_home_15 = os.getenv("JAVA_HOME_15"),
  java_home_16 = os.getenv("JAVA_HOME_16"),
  java_home_17 = os.getenv("JAVA_HOME_17"),
  java_home_18 = os.getenv("JAVA_HOME_18"),
}

local stdpath_config = vim.fn.stdpath("config")
local stdpath_data = vim.fn.stdpath("data")
local stdpath_cache = vim.fn.stdpath("cache")

local java_home_17 = java_home_preset.java_home_17
local cp_sp = utils.os_name == "win" and ";" or ":"
local cp = "." .. cp_sp .. utils.fs_concat({ java_home_17, "lib", "dt.jar" }) .. cp_sp .. utils.fs_concat({ java_home_17, "lib", "tools.jar" })

local java = utils.fs_concat({ java_home_17, "bin", "java" })
local jdtls_home = utils.fs_concat({ stdpath_data, "mason", "packages", "jdtls" })
local lombok_jar = utils.fs_concat({ jdtls_home, "lombok.jar" })
local launcher_jar = vim.fn.glob(utils.fs_concat({ jdtls_home, "plugins", "org.eclipse.equinox.launcher_*.jar" }))
local config_dir = utils.fs_concat({ jdtls_home, (utils.os_name == "win" and "config_win" or "config_linux") })

local runtimes_preset = {
  {
    name = "J2SE-1.5",
    path = java_home_preset.java_home_5,
  },
  {
    name = "JavaSE-1.6",
    path = java_home_preset.java_home_6,
    sources = utils.fs_concat({ java_home_preset.java_home_6, "src.zip" }),
    javadoc = "https://docs.oracle.com/javase/6/docs/api",
  },
  {
    name = "JavaSE-1.7",
    path = java_home_preset.java_home_7,
    sources = utils.fs_concat({ java_home_preset.java_home_7, "src.zip" }),
    javadoc = "https://docs.oracle.com/javase/7/docs/api",
  },
  {
    name = "JavaSE-1.8",
    path = java_home_preset.java_home_8,
    sources = utils.fs_concat({ java_home_preset.java_home_8, "src.zip" }),
    javadoc = "https://docs.oracle.com/javase/8/docs/api",
    default = true,
  },
  {
    name = "JavaSE-9",
    path = java_home_preset.java_home_9,
    sources = utils.fs_concat({ java_home_preset.java_home_9, "lib", "src.zip" }),
    javadoc = "https://docs.oracle.com/javase/9/docs/api",
  },
  {
    name = "JavaSE-10",
    path = java_home_preset.java_home_10,
    sources = utils.fs_concat({ java_home_preset.java_home_10, "lib", "src.zip" }),
    javadoc = "https://docs.oracle.com/javase/10/docs/api",
  },
  {
    name = "JavaSE-11",
    path = java_home_preset.java_home_11,
    sources = utils.fs_concat({ java_home_preset.java_home_11, "lib", "src.zip" }),
    javadoc = "https://docs.oracle.com/javase/11/docs/api",
  },
  {
    name = "JavaSE-12",
    path = java_home_preset.java_home_12,
    sources = utils.fs_concat({ java_home_preset.java_home_12, "lib", "src.zip" }),
    javadoc = "https://docs.oracle.com/javase/12/docs/api",
  },
  {
    name = "JavaSE-13",
    path = java_home_preset.java_home_13,
    sources = utils.fs_concat({ java_home_preset.java_home_13, "lib", "src.zip" }),
    javadoc = "https://docs.oracle.com/javase/13/docs/api",
  },
  {
    name = "JavaSE-14",
    path = java_home_preset.java_home_14,
    sources = utils.fs_concat({ java_home_preset.java_home_14, "lib", "src.zip" }),
    javadoc = "https://docs.oracle.com/javase/14/docs/api",
  },
  {
    name = "JavaSE-15",
    path = java_home_preset.java_home_15,
    sources = utils.fs_concat({ java_home_preset.java_home_15, "lib", "src.zip" }),
    javadoc = "https://docs.oracle.com/javase/15/docs/api",
  },
  {
    name = "JavaSE-16",
    path = java_home_preset.java_home_16,
    sources = utils.fs_concat({ java_home_preset.java_home_16, "lib", "src.zip" }),
    javadoc = "https://docs.oracle.com/javase/16/docs/api",
  },
  {
    name = "JavaSE-17",
    path = java_home_preset.java_home_17,
    sources = utils.fs_concat({ java_home_preset.java_home_17, "lib", "src.zip" }),
    javadoc = "https://docs.oracle.com/javase/17/docs/api",
  },
  {
    name = "JavaSE-18",
    path = java_home_preset.java_home_18,
    sources = utils.fs_concat({ java_home_preset.java_home_18, "lib", "src.zip" }),
    javadoc = "https://docs.oracle.com/javase/18/docs/api",
  },
  {
    name = "JavaSE-19",
    path = java_home_preset.java_home_19,
    sources = utils.fs_concat({ java_home_preset.java_home_19, "lib", "src.zip" }),
    javadoc = "https://docs.oracle.com/javase/19/docs/api",
  },
}

local function build_runtimes()
  local runtimes = {}
  for _, item in pairs(runtimes_preset) do
    if item.path ~= nil then
      table.insert(runtimes, item)
    end
  end
  return runtimes
end

local M = {}

M.setup = function()

  -- 获取工作目录
  local root_dir = utils.find_root_dir({
    'build.xml', -- Ant
    "mvnw", -- Maven
    'pom.xml', -- Maven
    'settings.gradle', -- Gradle
    'settings.gradle.kts', -- Gradle
    "gradlew", -- Gradle
  })
  local is_single_file = root_dir == nil
  local workspace_dir = root_dir or vim.fn.expand("%:p")

  -- 转义工作目录作为名称
  local workspace_name = string.gsub(workspace_dir, utils.fs_separator, "__")
  if utils.os_name == "win" then
    workspace_name = string.gsub(workspace_name, ":", "++")
  end
  -- 数据目录
  local data_dir = utils.fs_concat({ stdpath_cache, "lsp", "jdtls", workspace_name })

  -- jdtls配置
  local config = {}

  -- jdtls启动命令
  config.cmd = {
    java,
    "-cp", cp,
    "-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",
    "-javaagent:" .. lombok_jar,
    "-jar", launcher_jar,
    "-configuration", config_dir,
    "-data", data_dir,
    "--add-modules=ALL-SYSTEM",
    "--add-opens java.base/java.util=ALL-UNNAMED",
    "--add-opens java.base/java.lang=ALL-UNNAMED",
  }

  -- Use an on_attach function to only map the following keys
  -- after the language server attaches to the current buffer
  config.on_attach = function(client, bufnr)
    lsp_utils.on_attach(client, bufnr)
    -- jdtls特性
    jdtls.setup_dap({ hotcodereplace = "auto" })
    jdtls.setup.add_commands()

    -- https://github.com/mfussenegger/nvim-jdtls#nvim-dap-configuration
    -- 注册用于调试的主类,如果是新增的main方法需要使用:JdtRefreshDebugConfigs命令刷新
    if not is_single_file then
      require("jdtls.dap").setup_dap_main_class_configs()
    end

    -- 注册调试命令
    vim.cmd([[
      command! JdtTestClass lua require'jdtls'.test_class()
      command! JdtTestMethod lua require'jdtls'.test_nearest_method()
      command! JdtRemoteDebug lua require'user.ide.jdtls.command'.remote_debug_by_input()
      command! JdtDebug lua require'user.ide.jdtls.command'.debug()
      command! JdtA lua require'user.ide.jdtls.command'.test()
    ]])

    -- 设置jdt的扩展快捷键
    vim.keymap.set("n", "gp", "<Cmd>lua require'jdtls'.super_implementation()<CR>", { noremap = true, silent = true, buffer = bufnr, desc = "Lsp Super Implementation" })

  end

  config.capabilities = lsp_utils.make_capabilities()

  config.root_dir = workspace_dir

  config.settings = {
    java = {
      signatureHelp = { enabled = true },
      contentProvider = { preferred = "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.*"
        }
      },
      sources = {
        -- 不在import中使用*
        organizeImports = {
          starThreshold = 999,
          staticStarThreshold = 999,
        },
      },
      configuration = {
        -- See https://github.com/eclipse/eclipse.jdt.ls/wiki/Running-the-JAVA-LS-server-from-the-command-line#initialize-request
        -- And search for `interface RuntimeOption`
        -- The `name` is NOT arbitrary, but must match one of the elements from `enum ExecutionEnvironment` in the link above
        runtimes = build_runtimes(),
      },
    },
  }

  -- See https://github.com/mfussenegger/nvim-jdtls#java-debug-installation
  -- debug插件
  local bundles = {
      vim.fn.glob(utils.fs_concat({ stdpath_config, "lua", "user", "ide", "jdtls", "plugins", "java-debug", "com.microsoft.java.debug.plugin-*.jar"})),
  }
  -- 单元测试插件
  vim.list_extend(bundles, vim.split(vim.fn.glob(utils.fs_concat({ stdpath_config, "lua", "user", "ide", "jdtls", "plugins", "vscode-java-test", "*.jar" })), "\n"))

  local extendedClientCapabilities = jdtls.extendedClientCapabilities
  extendedClientCapabilities.resolveAdditionalTextEditsSupport = true
  config.init_options = {
    bundles = bundles,
    extendedClientCapabilities = extendedClientCapabilities,
  }

  -- This starts a new client & server,
  -- or attaches to an existing client & server depending on the `root_dir`.
  require("jdtls").start_or_attach(config)

end

return M
fengwk commented 2 years ago

Based on your description, I checked the api again, should I use vim.lsp.buf_get_clients({bufnr=0}) instead of vim.lsp.buf_get_clients() ?

get_active_clients({filter})                    *vim.lsp.get_active_clients()*
    Get active clients.

    Parameters:  
        {filter}  (table|nil) A table with key-value pairs used to filter the
                  returned clients. The available keys are:
                  • id (number): Only return clients with the given id
                  • bufnr (number): Only return clients attached to this
                    buffer
                  • name (string): Only return clients with the given name

    Return:  
        (table) List of |vim.lsp.client| objects
mfussenegger commented 2 years ago

Based on your description, I checked the api again, should I use vim.lsp.buf_get_clients({bufnr=0}) instead of vim.lsp.buf_get_clients() ?

Yes I think something like this should also work as fix:

diff --git a/lua/jdtls.lua b/lua/jdtls.lua
index 97c2328..6323b90 100644
--- a/lua/jdtls.lua
+++ b/lua/jdtls.lua
@@ -36,11 +36,11 @@ function M.start_or_attach(config)
 end

 local request = function(bufnr, method, params, handler)
   local client = nil
-  for _, c in pairs(vim.lsp.get_active_clients()) do
+  for _, c in pairs(vim.lsp.get_active_clients({ bufnr = bufnr })) do
     if c.name == 'jdtls' then
       client = c
       break
     end
   end
fengwk commented 2 years ago

Yes, I modified the commit and it works fine.

mfussenegger commented 2 years ago

Thanks, merged.