diff --git a/README.md b/README.md index 37d6d1c..e9e4094 100644 --- a/README.md +++ b/README.md @@ -12,36 +12,30 @@ ## 安装 -- `lazy.nvim` +- `lazy.nvim` 使用 `` ```lua + -- 使用 `autocmd` 方式启动(默认) + -- 默认使用 mason 或 ~/.vscode/extensions/vmware.vscode-spring-boot-x.xx.x 中的 jar { "JavaHello/spring-boot.nvim", - ft = "java", + ft = {"java", "yaml", "jproperties"}, dependencies = { "mfussenegger/nvim-jdtls", -- or nvim-java, nvim-lspconfig "ibhagwan/fzf-lua", -- 可选 }, - init = function() - vim.g.spring_boot = { - jdt_extensions_path = nil, -- 默认使用 ~/.vscode/extensions/vmware.vscode-spring-boot-x.xx.x - jdt_extensions_jars = { - "io.projectreactor.reactor-core.jar", - "org.reactivestreams.reactive-streams.jar", - "jdt-ls-commons.jar", - "jdt-ls-extension.jar", - "sts-gradle-tooling.jar", - }, - } - end, - config = function() - require("spring_boot").setup { - ls_path = nil, -- 默认使用 ~/.vscode/extensions/vmware.vscode-spring-boot-x.xx.x - jdtls_name = "jdtls", - log_file = nil, - java_cmd = nil, - } - end, + ---@type bootls.Config + opts = {} }, + + -- 使用 `ftplugin` 或自定义 方式启动 + { + "JavaHello/spring-boot.nvim", + lazy = true, + dependencies = { + "mfussenegger/nvim-jdtls", -- or nvim-java, nvim-lspconfig + }, + config = false + } ``` - [Visual Studio Code](https://code.visualstudio.com/) 中安装[VScode Spring Boot](https://marketplace.visualstudio.com/items?itemName=vmware.vscode-spring-boot)(可选的) diff --git a/lua/spring_boot.lua b/lua/spring_boot.lua index 6e20558..311ee22 100644 --- a/lua/spring_boot.lua +++ b/lua/spring_boot.lua @@ -1,15 +1,22 @@ -local jdt_ext_jars = { +local jdt_extensions_jars = { "io.projectreactor.reactor-core.jar", "org.reactivestreams.reactive-streams.jar", "jdt-ls-commons.jar", "jdt-ls-extension.jar", "sts-gradle-tooling.jar", } -vim.g.spring_boot = { - autocmd = true, + +local spring_boot = { jdt_extensions_path = nil, -- https://github.com/spring-projects/sts4/blob/7d3d91ecfa6087ae2d0e0f595da61ce8f52fed96/vscode-extensions/vscode-spring-boot/package.json#L33 - jdt_extensions_jars = jdt_ext_jars, + jdt_expanded_extensions_jars = {}, + is_bundle_jar = function(path) + for _, jar in ipairs(jdt_extensions_jars) do + if vim.endswith(path, jar) then + return true + end + end + end, } local M = {} @@ -43,30 +50,103 @@ M.init_lsp_commands = function() end end +M.get_ls_from_mason = function() + local result = M.get_from_mason_registry("vscode-spring-boot-tools", "vscode-spring-boot-tools/language-server.jar") + if #result > 0 then + return result[1] + end + return nil +end + +M.get_from_mason_registry = function(package_name, key_prefix) + local success, mason_registry = pcall(require, "mason-registry") + local result = {} + if success then + mason_registry.refresh() + local mason_package = mason_registry.get_package(package_name) + if mason_package == nil then + return result + end + if mason_package:is_installed() then + local install_path = mason_package:get_install_path() + mason_package:get_receipt():if_present(function(recipe) + for key, value in pairs(recipe.links.share) do + if key:sub(1, #key_prefix) == key_prefix then + table.insert(result, install_path .. "/" .. value) + end + end + end) + end + end + return result +end + local initialized = false + +---@param opts bootls.Config M.setup = function(opts) if initialized then return end - M._config = opts - require("spring_boot.launch").setup() initialized = true + opts = vim.tbl_deep_extend("keep", opts or {}, require("spring_boot.config")) + if not opts.ls_path then + opts.ls_path = M.get_ls_from_mason() -- get ls from mason-registry + if opts.ls_path then + spring_boot.jdt_expanded_extensions_jars = + M.get_from_mason_registry("vscode-spring-boot-tools", "vscode-spring-boot-tools/jdtls/") + end + end + if not opts.ls_path then + -- try to find ls on standard installation path of vscode + opts.ls_path = require("spring_boot.vscode").find_one("/vmware.vscode-spring-boot-*/language-server") + end + if opts.ls_path then + if vim.fn.isdirectory(opts.ls_path .. "/BOOT-INF") ~= 0 then + -- it's an exploded jar + opts.exploded_ls_jar_data = true + else + -- it's a single jar + local server_jar = vim.split(vim.fn.glob(opts.ls_path .. "/spring-boot-language-server*.jar"), "\n") + if #server_jar > 0 then + opts.ls_path = server_jar[1] + end + end + else + -- all possibilities finding the language server failed + vim.notify("Spring Boot LS is not installed", vim.log.levels.WARN) + return + end + if vim.fn.isdirectory(opts.ls_path .. "/BOOT-INF") ~= 0 then + -- a path was given in opts + opts.exploded_ls_jar_data = true + else + opts.exploded_ls_jar_data = false + end + M.init_lsp_commands() + + if opts.autocmd then + require("spring_boot.launch").ls_autocmd(opts) + end + return opts end M.java_extensions = function() - local bundles = {} - local function bundle_jar(path) - for _, jar in ipairs(vim.g.spring_boot.jdt_extensions_jars or jdt_ext_jars) do - if vim.endswith(path, jar) then - return true - end + if spring_boot.jdt_expanded_extensions_jars and #spring_boot.jdt_expanded_extensions_jars > 0 then + return spring_boot.jdt_expanded_extensions_jars + end + local bundles = M.get_from_mason_registry("vscode-spring-boot-tools", "vscode-spring-boot-tools/jdtls/") + if #bundles > 0 then + for _, v in pairs(bundles) do + table.insert(spring_boot.jdt_expanded_extensions_jars, v) end + return bundles end - local spring_boot_path = vim.g.spring_boot.jdt_extensions_path + local spring_boot_path = spring_boot.jdt_extensions_path or require("spring_boot.vscode").find_one("/vmware.vscode-spring-boot-*/jars") if spring_boot_path then for _, bundle in ipairs(vim.split(vim.fn.glob(spring_boot_path .. "/*.jar"), "\n")) do - if bundle_jar(bundle) then + if spring_boot.is_bundle_jar(bundle) then table.insert(bundles, bundle) end end diff --git a/lua/spring_boot/config.lua b/lua/spring_boot/config.lua index 0b38a03..63c58b4 100644 --- a/lua/spring_boot/config.lua +++ b/lua/spring_boot/config.lua @@ -5,6 +5,7 @@ ---@field log_file? string|function The path to the spring boot ls log file. ---@field server vim.lsp.ClientConfig The language server configuration. ---@field exploded_ls_jar_data boolean The exploded language server jar data. +---@field autocmd boolean autimatically setup autocmd in neovim ---@type bootls.Config local M = { @@ -16,13 +17,7 @@ local M = { server = { cmd = {}, }, + autocmd = true, } -local function init() - local spring_boot = require("spring_boot") - M = vim.tbl_deep_extend("keep", spring_boot._config or {}, M) - spring_boot._config = nil -end -init() - return M diff --git a/lua/spring_boot/launch.lua b/lua/spring_boot/launch.lua index 82f8ada..bb8a6fc 100644 --- a/lua/spring_boot/launch.lua +++ b/lua/spring_boot/launch.lua @@ -1,177 +1,98 @@ -local M = {} -local config = require("spring_boot.config") -local vscode = require("spring_boot.vscode") local classpath = require("spring_boot.classpath") local java_data = require("spring_boot.java_data") +local ls_config = require("spring_boot.ls_config") local util = require("spring_boot.util") +local uv = vim.uv or vim.loop +local M = {} -local root_dir = function() - local ok, jdtls = pcall(require, "jdtls.setup") - if ok then - return jdtls.find_root({ ".git", "mvnw", "gradlew" }) or vim.loop.cwd() - end - return vim.loop.cwd() -end - ----@param opts bootls.Config -local bootls_path = function(opts) - if opts.ls_path then - return opts.ls_path - end - local bls = vscode.find_one("/vmware.vscode-spring-boot-*/language-server") - if bls then - return bls - end -end - -M.enable_classpath_listening = function() - util.boot_execute_command("sts.vscode-spring-boot.enableClasspathListening", { true }) +M.root_dir = function() + return vim.fs.root(0, { ".git", "mvnw", "gradlew" }) or uv.cwd() end ---@param opts bootls.Config -local logfile = function(opts, rt_dir) +M.logfile = function(opts) local lf if opts.log_file ~= nil then if type(opts.log_file) == "function" then - lf = opts.log_file(rt_dir) + lf = opts.log_file(opts.server.root_dir) elseif type(opts.log_file) == "string" then - lf = opts.log_file == "" and nil or opts.log_file + lf = opts.log_file end end return lf or "/dev/null" end ----@param opts bootls.Config -local function bootls_cmd(opts, rt_dir, java_cmd) - local boot_path = bootls_path(opts) - if not boot_path then - vim.notify("Spring Boot LS is not installed", vim.log.levels.WARN) - return - end - if opts.exploded_ls_jar_data then +M.bootls_cmd = function(config) + if config.exploded_ls_jar_data then local boot_classpath = {} - table.insert(boot_classpath, boot_path .. "/BOOT-INF/classes") - table.insert(boot_classpath, boot_path .. "/BOOT-INF/lib/*") + table.insert(boot_classpath, config.ls_path .. "/BOOT-INF/classes") + table.insert(boot_classpath, config.ls_path .. "/BOOT-INF/lib/*") - local cmd = { - java_cmd or util.java_bin(), + return { + config.java_cmd or util.java_bin(), "-XX:TieredStopAtLevel=1", "-Xmx1G", "-XX:+UseZGC", "-cp", table.concat(boot_classpath, util.is_win and ";" or ":"), "-Dsts.lsp.client=vscode", - "-Dsts.log.file=" .. logfile(opts, rt_dir), - "-Dspring.config.location=file:" .. boot_path .. "/BOOT-INF/classes/application.properties", + "-Dsts.log.file=" .. M.logfile(config), + "-Dspring.config.location=file:" .. config.ls_path .. "/BOOT-INF/classes/application.properties", -- "-Dlogging.level.org.springframework=DEBUG", "org.springframework.ide.vscode.boot.app.BootLanguageServerBootApp", } - return cmd else - local server_jar = vim.split(vim.fn.glob(boot_path .. "/spring-boot-language-server*.jar"), "\n") - if #server_jar == 0 then - vim.notify("Spring Boot LS jar not found", vim.log.levels.WARN) - return - end - local cmd = { - java_cmd or util.java_bin(), + return { + config.java_cmd or util.java_bin(), "-XX:TieredStopAtLevel=1", "-Xmx1G", "-XX:+UseZGC", "-Dsts.lsp.client=vscode", - "-Dsts.log.file=" .. logfile(opts, rt_dir), + "-Dsts.log.file=" .. M.logfile(config), "-jar", - server_jar[1], + config.ls_path, } - return cmd end end -M.ls_config = { - name = "spring-boot", - filetypes = { "java", "yaml", "jproperties" }, - -- root_dir = root_dir, - init_options = { - -- workspaceFolders = root_dir, - enableJdtClasspath = false, - }, - settings = { - spring_boot = {}, - }, - handlers = {}, - commands = {}, - get_language_id = function(bufnr, filetype) - if filetype == "yaml" then - local filename = vim.api.nvim_buf_get_name(bufnr) - if util.is_application_yml_file(filename) then - return "spring-boot-properties-yaml" - end - elseif filetype == "jproperties" then - local filename = vim.api.nvim_buf_get_name(bufnr) - if util.is_application_properties_file(filename) then - return "spring-boot-properties" - end - end - return filetype - end, -} -classpath.register_classpath_service(M.ls_config) -java_data.register_java_data_service(M.ls_config) - -vim.lsp.commands["vscode-spring-boot.ls.start"] = function(_, _, _) - M.enable_classpath_listening() - return {} -end - -M.ls_config.handlers["sts/highlight"] = function() end -M.ls_config.handlers["sts/moveCursor"] = function(err, result, ctx, config) - -- TODO: move cursor - return { applied = true } -end -M._update_config = false -M.update_config = function(opts) - if M._update_config then - return - end - M._update_config = true - M.ls_config = vim.tbl_deep_extend("keep", M.ls_config, opts.server) - local capabilities = M.ls_config.capabilities or vim.lsp.protocol.make_client_capabilities() - capabilities.workspace = { - executeCommand = { value = true }, - } - M.ls_config.capabilities = capabilities - if not M.ls_config.root_dir then - M.ls_config.root_dir = root_dir() - end - M.ls_config.cmd = (M.ls_config.cmd and #M.ls_config.cmd > 0) and M.ls_config.cmd - or bootls_cmd(opts, M.ls_config.root_dir, opts.java_cmd) - if not M.ls_config.cmd then - return +--- 使用 ftplugin 启动时,调用此方法 +---@param opts bootls.Config +---@return vim.lsp.ClientConfig +M.update_ls_config = function(opts) + local client_config = vim.tbl_deep_extend("keep", opts.server, ls_config) + if not client_config.root_dir then + client_config.root_dir = M.root_dir() end - M.ls_config.init_options.workspaceFolders = M.ls_config.root_dir - local on_init = M.ls_config.on_init - M.ls_config.on_init = function(client, ctx) - util.boot_ls_init(client, ctx) - if on_init then - on_init(client, ctx) + if not client_config.cmd or #client_config.cmd == 0 then + if not opts.ls_path then + vim.notify("Spring Boot LS is not installed", vim.log.levels.WARN) + return {} end + client_config.cmd = M.bootls_cmd(opts) end -end + client_config.init_options.workspaceFolders = client_config.root_dir -M.setup = function(_) - M.update_config(config) - if vim.g.spring_boot.autocmd then - local group = vim.api.nvim_create_augroup("spring_boot_ls", { clear = true }) - vim.api.nvim_create_autocmd({ "FileType" }, { - group = group, - pattern = { "java", "yaml", "jproperties" }, - desc = "Spring Boot Language Server", - callback = function(_) - M.start(M.ls_config) - end, - }) + classpath.register_classpath_service(client_config) + java_data.register_java_data_service(client_config) + vim.lsp.commands["vscode-spring-boot.ls.start"] = function(_, _, _) + util.boot_execute_command("sts.vscode-spring-boot.enableClasspathListening", { true }) end + return client_config end + +M.ls_autocmd = function(opts) + local current_ls_config = M.update_ls_config(opts) + local group = vim.api.nvim_create_augroup("spring_boot_ls", { clear = true }) + vim.api.nvim_create_autocmd({ "FileType" }, { + group = group, + pattern = { "java", "yaml", "jproperties" }, + desc = "Spring Boot Language Server", + callback = function(_) + M.start(current_ls_config) + end, + }) +end + M.start = function(opts) local buf = vim.api.nvim_get_current_buf() local filename = vim.uri_from_bufnr(buf) diff --git a/lua/spring_boot/ls_config.lua b/lua/spring_boot/ls_config.lua new file mode 100644 index 0000000..4c938e5 --- /dev/null +++ b/lua/spring_boot/ls_config.lua @@ -0,0 +1,44 @@ +local util = require("spring_boot.util") + +local M = { + name = "spring-boot", + filetypes = { "java", "yaml", "jproperties" }, + root_dir = nil, + init_options = { + workspaceFolders = nil, + enableJdtClasspath = false, + }, + settings = {}, + handlers = { + ["sts/highlight"] = function() end, + ["sts/moveCursor"] = function(err, result, ctx, config) + -- TODO: move cursor + return { applied = true } + end, + }, + commands = {}, + get_language_id = function(bufnr, filetype) + if filetype == "yaml" then + local filename = vim.api.nvim_buf_get_name(bufnr) + if util.is_application_yml_file(filename) then + return "spring-boot-properties-yaml" + end + elseif filetype == "jproperties" then + local filename = vim.api.nvim_buf_get_name(bufnr) + if util.is_application_properties_file(filename) then + return "spring-boot-properties" + end + end + return filetype + end, + capabilities = vim.tbl_deep_extend("force", vim.lsp.protocol.make_client_capabilities(), { + workspace = { + executeCommand = { value = true }, + }, + }), + on_init = function(client, ctx) + util.boot_ls_init(client, ctx) + end, +} + +return M diff --git a/lua/spring_boot/util.lua b/lua/spring_boot/util.lua index abef340..89a3ab1 100644 --- a/lua/spring_boot/util.lua +++ b/lua/spring_boot/util.lua @@ -32,7 +32,7 @@ M.get_client = function(name) if clients and #clients > 0 then return clients[1] end - vim.notify("Client not found: " .. name, vim.log.levels.ERROR) + -- vim.notify("Client not found: " .. name, vim.log.levels.ERROR) return nil end