Skip to content

Commit 79de8ba

Browse files
committed
refactor: migrate to native LSP API and Neovim 0.12 runtimepath
Replace the autocmd/vim.lsp.start wiring with `vim.lsp.config()` and `vim.lsp.enable()`, shipping `lsp/spring-boot.lua` on the runtimepath so clients compose through the standard |lsp-config-merge| chain instead of a hand-built config. Notable changes: - Drop `spring_boot/ls_config.lua` and `launch.ls_autocmd()`; the implicit `autocmd` option is gone and `setup()` is idempotent, with later calls overriding earlier ones. - Require the language server on start: `setup()` returns the config and no longer attaches to `vim.lsp.start`, and `root_dir` doubles as a gate. - Add `project_filter` for per-workspace opt-out, backed by `has_spring_boot_dependency()`, with predicate results memoized. - Register `spring-boot` client commands locally (instead of the global `init_lsp_commands` handler), so per-client commands win; drop the global variant from the telescope/nvim-lspconfig instructions. - Reshape `config.lua`: `log_level`, `jvm_args`, `jars` and `server` replace `autocmd`/`exploded_ls_jar_data` (now derived), keeping the process language. - Add `:checkhealth spring_boot` (`health.lua`) covering java, LS discovery, extension jars, filter verdict and running clients. - Move `register_*_service` handlers into lazily-bound `M.handlers()` tables in `classpath.lua` and `java_data.lua`, resetting plugin load order. - Tighten `is_application_yml_file`/`is_application_properties_file` to match on the basename, excluding
1 parent eea95b7 commit 79de8ba

11 files changed

Lines changed: 777 additions & 267 deletions

File tree

‎README.md‎

Lines changed: 84 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,31 +10,99 @@
1010
- [x] `Spring` 注解依赖提示补全。
1111
- [x] `Code Action`。
1212

13+
## 要求
14+
15+
- `Neovim 0.12+`。插件在 `runtimepath` 上提供 `lsp/spring-boot.lua`,通过 `vim.lsp.config()` / `vim.lsp.enable()` 接入 LSP。
16+
1317
## 安装
1418

1519
- `lazy.nvim`
1620
```lua
17-
-- 使用 `autocmd` 方式启动(默认)
1821
-- 默认使用 mason 或 ~/.vscode/extensions/vmware.vscode-spring-boot-x.xx.x 中的 jar
1922
{
2023
"JavaHello/spring-boot.nvim",
21-
ft = {"java", "yaml", "jproperties"},
24+
ft = { "java", "yaml", "jproperties" },
2225
dependencies = {
2326
"mfussenegger/nvim-jdtls", -- or nvim-java, nvim-lspconfig
2427
"ibhagwan/fzf-lua", -- 可选,用于符号选择等UI功能。也可以使用其他选择器(例如 telescope.nvim)。
2528
},
2629
---@type bootls.Config
2730
opts = {}
2831
},
29-
3032
```
31-
- [Visual Studio Code](https://code.visualstudio.com/) 中安装[VScode Spring Boot](https://marketplace.visualstudio.com/items?itemName=vmware.vscode-spring-boot)(可选的)
33+
34+
插件加载时会自动调用 `vim.lsp.enable("spring-boot")`,无需再手动配置 LSP。
35+
36+
如果你希望自己控制启动时机,设置 `opts = { auto_enable = false }`,然后自行调用 `vim.lsp.enable("spring-boot")`。
37+
38+
- [Visual Studio Code](https://code.visualstudio.com/) 中安装 [VScode Spring Boot](https://marketplace.visualstudio.com/items?itemName=vmware.vscode-spring-boot)(可选的)
39+
40+
## 配置
41+
42+
`opts` 中可用的字段:
43+
44+
| 字段 | 类型 | 说明 |
45+
| --- | --- | --- |
46+
| `ls_path` | `string?` | 语言服务器 jar(或解压后的目录)路径。默认依次从 mason-registry、vscode 扩展目录查找。 |
47+
| `java_cmd` | `string?` | `java` 可执行文件路径。默认 `$JAVA_HOME/bin/java`,否则 `java`。 |
48+
| `log_file` | `string\|fun(root_dir: string?): string?` | 服务端日志文件。默认不记录日志(`/dev/null`)。 |
49+
| `log_level` | `string?` | 根日志级别,默认 `warn`。 |
50+
| `jvm_args` | `string[]?` | 追加的 JVM 参数。 |
51+
| `jars` | `string[]?` | 显式指定 jdtls 扩展 jar,跳过自动查找。 |
52+
| `project_filter` | `fun(root_dir: string): boolean?` | 决定某个工作区是否启动客户端。见[按需启动](#按需启动)。 |
53+
| `jdtls_name` | `string?` | jdtls 客户端名,默认 `jdtls`。 |
54+
| `server` | `vim.lsp.ClientConfig?` | 额外合并进 `spring-boot` 客户端配置的字段,优先级最高。 |
55+
| `auto_enable` | `boolean?` | 是否在 `setup()` 中自动 `vim.lsp.enable("spring-boot")`,默认 `true`。 |
56+
57+
例如把服务端日志留到文件里排查问题:
58+
59+
```lua
60+
opts = {
61+
log_file = function(root_dir)
62+
return vim.fs.joinpath(vim.fn.stdpath("log"), "spring-boot-ls.log")
63+
end,
64+
log_level = "debug",
65+
}
66+
```
67+
68+
## 按需启动
69+
70+
默认情况下,只要找到了语言服务器,`java` 文件和 `application*.yml` / `application*.properties` 就会启动客户端。如果只想在真正的 Spring Boot 工程里启动,加一个项目级判断:
71+
72+
```lua
73+
opts = {
74+
project_filter = function(root_dir)
75+
return require("spring_boot.util").has_spring_boot_dependency(root_dir)
76+
end,
77+
}
78+
```
79+
80+
`project_filter` 在插件找到工作区根目录、并通过内置的 `application.yml` / `.properties` 文件名判断之后调用,返回 `false` 就不启动。判断期间抛错会当作 `true` 处理并给出警告,避免谓词写错时静默失去补全。
81+
82+
`has_spring_boot_dependency(root_dir)` 检查 `pom.xml` / `build.gradle` / `build.gradle.kts`,识别 `spring-boot-starter-*`、`spring-boot-starter-parent` 以及 Gradle 的 `org.springframework.boot` 插件。多模块工程的聚合根 pom 常常只列 `<modules>`,因此会广度优先往下找子模块(最多 3 层、最多读 50 个构建文件,`target/`、`build/` 等输出目录跳过)。
83+
84+
判断结果按工作区缓存(`root_dir` 在每次 `FileType` 事件都会执行,而谓词可能要读构建文件)。改动 pom 之后重新调用一次 `setup()` 即可清空缓存重新评估。
85+
86+
## 覆盖 LSP 配置
87+
88+
`spring-boot` 客户端配置按 `vim.lsp.config()` 的[合并链](https://neovim.io/doc/user/lsp.html#lsp-config-merge)解析:先插件自带的 `lsp/spring-boot.lua`,再 `after/lsp/spring-boot.lua`,最后 `opts.server`。因此可以像配置其他 LSP 一样覆盖任意字段:
89+
90+
```lua
91+
-- ~/.config/nvim/after/lsp/spring-boot.lua
92+
return {
93+
on_attach = function(client, bufnr)
94+
-- ...
95+
end,
96+
}
97+
```
3298

3399
## `jdtls` 配置
34100

101+
Spring Boot 的 `Java` 相关能力(依赖提示、`Code Action` 等)由 jdtls 的扩展 jar 提供。
102+
35103
### 选项 1: 使用 `nvim-jdtls`
36104

37-
详细配置参考[nvim-jdtls](https://github.com/mfussenegger/nvim-jdtls)项目
105+
详细配置参考 [nvim-jdtls](https://github.com/mfussenegger/nvim-jdtls) 项目
38106

39107
```lua
40108
local jdtls_config = {
@@ -47,8 +115,6 @@ vim.list_extend(jdtls_config.bundles, require("spring_boot").java_extensions())
47115
### 选项 2: 使用 `nvim-lspconfig`
48116

49117
```lua
50-
-- 添加全局命令处理器
51-
require('spring_boot').init_lsp_commands()
52118
-- 添加 spring-boot jdtls 扩展 jar 包
53119
require("lspconfig").jdtls.setup {
54120
init_options = {
@@ -67,9 +133,18 @@ require("lspconfig").jdtls.setup {
67133
:FzfLua lsp_live_workspace_symbols
68134
```
69135
- 如果您正在使用 `telescope.nvim`:
70-
```vim
71-
:lua require'telescope.builtin'.lsp_workspace_symbols{}
136+
```lua
137+
require'telescope.builtin'.lsp_workspace_symbols{}
72138
```
73139
*(注意:具体命令可能因您选择的选取器及其配置而异。请确保您的选取器已配置为处理 LSP 符号。)*
74140
![lsp_live_workspace_symbols](https://github.com/JavaHello/javahello.github.io/raw/refs/heads/master/content/posts/nvim-lean/images/spring-boot.png)
75141
142+
## 排查问题
143+
144+
```vim
145+
:checkhealth spring_boot
146+
```
147+
148+
会依次检查 `java` 可执行文件、语言服务器路径(及 mason / vscode 扩展两个来源)、jdtls 扩展 jar、`$MASON`、配置是否已启用,以及当前运行中的客户端及其 `root_dir`。
149+
150+
服务端自身的日志默认丢弃,排查启动失败时建议先按上面的例子设置 `log_file` 和 `log_level = "debug"`。

‎README_en.md‎

Lines changed: 83 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,104 @@
22

33
# Spring Boot Nvim
44

5-
Adapted from [VSCode Spring Boot](https://marketplace.visualstudio.com/items?itemName=vmware.vscode-spring-boot) extension, integrating some of its features into `Neovim`.
5+
Adapted from the [VSCode Spring Boot](https://marketplace.visualstudio.com/items?itemName=vmware.vscode-spring-boot) extension, integrating some of its features into `Neovim`.
66

77
- [x] Find Beans using Spring annotations
88
- [x] Discover Web Endpoints
99
- [x] Code completion hints and navigation for `application.properties`/`application.yml`
1010
- [x] Dependency hints/completion for Spring annotations
1111
- [x] Code Actions
1212

13+
## Requirements
14+
15+
- `Neovim 0.12+`. The plugin ships `lsp/spring-boot.lua` on the runtimepath and plugs into LSP through `vim.lsp.config()` / `vim.lsp.enable()`.
16+
1317
## Installation
1418

1519
- `lazy.nvim`
1620
```lua
17-
-- Using autocmd launch (default)
1821
-- Default uses jars from mason or ~/.vscode/extensions/vmware.vscode-spring-boot-x.x.x
1922
{
2023
"JavaHello/spring-boot.nvim",
21-
ft = {"java", "yaml", "jproperties"},
24+
ft = { "java", "yaml", "jproperties" },
2225
dependencies = {
2326
"mfussenegger/nvim-jdtls", -- or nvim-java, nvim-lspconfig
2427
"ibhagwan/fzf-lua", -- optional, for UI features like symbol picking. Other pickers (e.g., telescope.nvim) can also be used.
2528
},
2629
---@type bootls.Config
2730
opts = {}
2831
},
29-
3032
```
33+
34+
The plugin calls `vim.lsp.enable("spring-boot")` for you when it loads, so no extra LSP wiring is needed.
35+
36+
To control when the client starts yourself, pass `opts = { auto_enable = false }` and call `vim.lsp.enable("spring-boot")` on your own.
37+
3138
- Install [VSCode Spring Boot](https://marketplace.visualstudio.com/items?itemName=vmware.vscode-spring-boot) in [Visual Studio Code](https://code.visualstudio.com/) (optional)
3239

40+
## Configuration
41+
42+
Fields accepted in `opts`:
43+
44+
| Field | Type | Description |
45+
| --- | --- | --- |
46+
| `ls_path` | `string?` | Path to the language server jar (or an exploded directory). Discovered from mason-registry, then the vscode extension directory. |
47+
| `java_cmd` | `string?` | Path to the `java` executable. Defaults to `$JAVA_HOME/bin/java`, then `java`. |
48+
| `log_file` | `string\|fun(root_dir: string?): string?` | The server's log file. Logging is disabled by default (`/dev/null`). |
49+
| `log_level` | `string?` | Root logging level, defaults to `warn`. |
50+
| `jvm_args` | `string[]?` | Extra JVM arguments. |
51+
| `jars` | `string[]?` | Explicit jdtls extension jars, skipping discovery. |
52+
| `project_filter` | `fun(root_dir: string): boolean?` | Decides whether the client starts in a given workspace. See [Starting on demand](#starting-on-demand). |
53+
| `jdtls_name` | `string?` | Name of the jdtls client, defaults to `jdtls`. |
54+
| `server` | `vim.lsp.ClientConfig?` | Extra fields merged into the `spring-boot` client config, at the highest priority. |
55+
| `auto_enable` | `boolean?` | Whether `setup()` calls `vim.lsp.enable("spring-boot")`, defaults to `true`. |
56+
57+
For example, to keep the server log around when debugging:
58+
59+
```lua
60+
opts = {
61+
log_file = function(root_dir)
62+
return vim.fs.joinpath(vim.fn.stdpath("log"), "spring-boot-ls.log")
63+
end,
64+
log_level = "debug",
65+
}
66+
```
67+
68+
## Starting on demand
69+
70+
By default the client starts for `java` files and for `application*.yml` / `application*.properties` as soon as the language server is found. To only start it inside actual Spring Boot projects, add a per-workspace check:
71+
72+
```lua
73+
opts = {
74+
project_filter = function(root_dir)
75+
return require("spring_boot.util").has_spring_boot_dependency(root_dir)
76+
end,
77+
}
78+
```
79+
80+
`project_filter` is called once the workspace root has been found and the built-in `application.yml` / `.properties` filename check has passed; returning `false` keeps the client from starting. A predicate that raises is treated as `true` and warns, so a mistake there never silently costs you completion.
81+
82+
`has_spring_boot_dependency(root_dir)` reads `pom.xml` / `build.gradle` / `build.gradle.kts` and recognises `spring-boot-starter-*`, `spring-boot-starter-parent` and the Gradle `org.springframework.boot` plugin. Because an aggregator `pom.xml` in a multi-module build often only lists `<modules>`, submodules are searched too, breadth-first (at most 3 levels deep and 50 build files read, skipping output directories such as `target/` and `build/`).
83+
84+
Verdicts are memoized per workspace — `root_dir` runs on every `FileType` event and the predicate may read build files. Call `setup()` again to clear the cache and re-evaluate, for example after adding the dependency.
85+
86+
## Overriding the LSP config
87+
88+
The `spring-boot` client config resolves through the [config merge chain](https://neovim.io/doc/user/lsp.html#lsp-config-merge): the `lsp/spring-boot.lua` shipped by the plugin, then `after/lsp/spring-boot.lua`, then `opts.server`. Any field can therefore be overridden like any other LSP config:
89+
90+
```lua
91+
-- ~/.config/nvim/after/lsp/spring-boot.lua
92+
return {
93+
on_attach = function(client, bufnr)
94+
-- ...
95+
end,
96+
}
97+
```
98+
3399
## `jdtls` Configuration
34100

101+
The Java-side features (dependency hints, code actions, …) come from jdtls extension jars.
102+
35103
### Option 1: Using `nvim-jdtls`
36104

37105
Refer to [nvim-jdtls](https://github.com/mfussenegger/nvim-jdtls) for detailed configuration
@@ -47,8 +115,6 @@ vim.list_extend(jdtls_config.bundles, require("spring_boot").java_extensions())
47115
### Option 2: Using `nvim-lspconfig`
48116

49117
```lua
50-
-- Add global command handlers
51-
require('spring_boot').init_lsp_commands()
52118
-- Add spring-boot jdtls extension jars
53119
require("lspconfig").jdtls.setup {
54120
init_options = {
@@ -67,9 +133,18 @@ require("lspconfig").jdtls.setup {
67133
:FzfLua lsp_live_workspace_symbols
68134
```
69135
- If you are using `telescope.nvim`:
70-
```vim
71-
:lua require'telescope.builtin'.lsp_workspace_symbols{}
136+
```lua
137+
require'telescope.builtin'.lsp_workspace_symbols{}
72138
```
73139
*(Note: The exact command may vary depending on your chosen picker and its configuration. Ensure your picker is set up to handle LSP symbols.)*
74140
![lsp_live_workspace_symbols](https://github.com/JavaHello/javahello.github.io/raw/refs/heads/master/content/posts/nvim-lean/images/spring-boot.png)
75141
142+
## Troubleshooting
143+
144+
```vim
145+
:checkhealth spring_boot
146+
```
147+
148+
Reports the `java` executable, the resolved language server path (plus both discovery sources: mason and the vscode extension), the jdtls extension jars, `$MASON`, whether the config is enabled, and the running clients with their `root_dir`.
149+
150+
The server's own log is discarded by default; when debugging a failed start, set `log_file` and `log_level = "debug"` as shown above.

‎lsp/spring-boot.lua‎

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
---@brief
2+
---
3+
--- The Spring Boot language server, taken from the
4+
--- [vscode-spring-boot](https://marketplace.visualstudio.com/items?itemName=vmware.vscode-spring-boot)
5+
--- extension.
6+
---
7+
--- Configure it with |spring_boot.setup()|, which also enables it unless
8+
--- `auto_enable` is false, or enable it yourself with: >lua
9+
--- vim.lsp.enable("spring-boot")
10+
--- <
11+
--- Any field can be overridden from `after/lsp/spring-boot.lua`.
12+
---
13+
--- NOTE: `vim.lsp.config` loads this file with `loadfile()`, possibly before
14+
--- the plugin is on the runtimepath. It therefore must not `require()`
15+
--- anything at load time — every `require` below lives inside the function
16+
--- that needs it.
17+
18+
--- Binds a handler provided by `lua/spring_boot/<mod>.lua` lazily, so that
19+
--- loading this file never loads the plugin.
20+
---@param mod string
21+
---@param method string
22+
---@return lsp.Handler
23+
local function handler(mod, method)
24+
return function(err, result, ctx, config)
25+
return require("spring_boot." .. mod).handlers()[method](err, result, ctx, config)
26+
end
27+
end
28+
29+
---@type vim.lsp.Config
30+
return {
31+
-- The client name is part of the public interface, keep it "spring-boot".
32+
filetypes = { "java", "yaml", "jproperties" },
33+
34+
-- Only the delta: the client deep-merges this over
35+
-- |vim.lsp.protocol.make_client_capabilities()|.
36+
capabilities = {
37+
workspace = {
38+
executeCommand = { value = true },
39+
},
40+
},
41+
42+
init_options = {
43+
enableJdtClasspath = false,
44+
},
45+
46+
settings = {},
47+
48+
-- Gating lives here, not in `filetypes`: the server only applies to
49+
-- application.yml / application.properties (and to java files).
50+
root_dir = function(bufnr, on_dir)
51+
require("spring_boot.launch").root_dir(bufnr, on_dir)
52+
end,
53+
54+
-- A function, so the server is resolved when one actually starts, and so
55+
-- that overriding `cmd` replaces it wholesale instead of merging into it.
56+
cmd = function(dispatchers, config)
57+
local opts = require("spring_boot.config")
58+
local cmd = require("spring_boot.launch").bootls_cmd(opts, config.root_dir)
59+
if not cmd then
60+
-- Returning nil would fail later with an unhelpful "attempt to index a
61+
-- nil value (local 'rpc')"; raising surfaces a warning instead.
62+
error("Spring Boot LS is not installed. Run :checkhealth spring_boot")
63+
end
64+
return vim.lsp.rpc.start(cmd, dispatchers)
65+
end,
66+
67+
-- The workspace folder is only known per buffer, so it cannot be declared
68+
-- statically in `init_options`.
69+
before_init = function(params, config)
70+
if type(params.initializationOptions) ~= "table" then
71+
params.initializationOptions = {}
72+
end
73+
params.initializationOptions.workspaceFolders = config.root_dir
74+
end,
75+
76+
get_language_id = function(bufnr, filetype)
77+
return require("spring_boot.util").language_id(bufnr, filetype)
78+
end,
79+
80+
on_init = function(client, init_result)
81+
require("spring_boot.util").boot_ls_init(client, init_result)
82+
end,
83+
84+
handlers = {
85+
-- The server asking the editor to run a command, e.g. to enable classpath
86+
-- listening. Handled on this client, so no global handler is needed.
87+
["workspace/executeClientCommand"] = function(err, result, ctx, config)
88+
return require("spring_boot").execute_client_command(err, result, ctx, config)
89+
end,
90+
["sts/highlight"] = function() end,
91+
["sts/moveCursor"] = function()
92+
-- TODO: move cursor
93+
return { applied = true }
94+
end,
95+
["sts/addClasspathListener"] = handler("classpath", "sts/addClasspathListener"),
96+
["sts/removeClasspathListener"] = handler("classpath", "sts/removeClasspathListener"),
97+
["sts/javaType"] = handler("java_data", "sts/javaType"),
98+
["sts/javadocHoverLink"] = handler("java_data", "sts/javadocHoverLink"),
99+
["sts/javaLocation"] = handler("java_data", "sts/javaLocation"),
100+
["sts/javadoc"] = handler("java_data", "sts/javadoc"),
101+
["sts/javaSearchTypes"] = handler("java_data", "sts/javaSearchTypes"),
102+
["sts/javaSearchPackages"] = handler("java_data", "sts/javaSearchPackages"),
103+
["sts/javaSubTypes"] = handler("java_data", "sts/javaSubTypes"),
104+
["sts/javaSuperTypes"] = handler("java_data", "sts/javaSuperTypes"),
105+
["sts/javaCodeComplete"] = handler("java_data", "sts/javaCodeComplete"),
106+
["sts/project/gav"] = handler("java_data", "sts/project/gav"),
107+
},
108+
109+
commands = {
110+
["vscode-spring-boot.ls.start"] = function()
111+
require("spring_boot.util").boot_execute_command("sts.vscode-spring-boot.enableClasspathListening", { true })
112+
end,
113+
},
114+
}

0 commit comments

Comments
 (0)