Skip to content

Commit a16ae90

Browse files
committed
Merge remote-tracking branch 'origin/main' into feat/tools_paths_over_auto_install
2 parents 0ed9503 + dcf18d8 commit a16ae90

31 files changed

Lines changed: 1018 additions & 217 deletions

.github/workflows/test.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ on:
99
jobs:
1010
test:
1111
runs-on: ${{ matrix.os }}
12+
timeout-minutes: 40
1213
strategy:
14+
fail-fast: false
1315
matrix:
1416
os: [ubuntu-latest, macos-latest, windows-latest]
1517
nvim-versions: ["stable", "nightly"]

.luacheckrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ globals = {
55
'vim.bo',
66
'vim.opt',
77
'vim.lsp',
8+
'vim.ui',
89
}
910
read_globals = {
1011
'vim',

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
11
# Changelog
22

3+
## [4.1.2](https://github.com/nvim-java/nvim-java/compare/v4.1.1...v4.1.2) (2026-07-03)
4+
5+
6+
### Bug Fixes
7+
8+
* change jdtls download url from milestones to snapshots ([#499](https://github.com/nvim-java/nvim-java/issues/499)) ([3f071ca](https://github.com/nvim-java/nvim-java/commit/3f071caa984347880baacf46993728cbdc87c43b))
9+
* **classpath:** fix bug when generated sources are skipped by jdtls ([#489](https://github.com/nvim-java/nvim-java/issues/489)) ([116339b](https://github.com/nvim-java/nvim-java/commit/116339b9a9b6d9d4563b2afc38fe51ba6e1a1df8))
10+
11+
## [4.1.1](https://github.com/nvim-java/nvim-java/compare/v4.1.0...v4.1.1) (2026-06-12)
12+
13+
14+
### Bug Fixes
15+
16+
* **dap:** skip enrich_config for attach mode ([#494](https://github.com/nvim-java/nvim-java/issues/494)) ([ccd76f9](https://github.com/nvim-java/nvim-java/commit/ccd76f9eee604bc88e287d0daee764bf4277cd6b))
17+
* **refactor:** use method-call syntax for Action:rename ([#493](https://github.com/nvim-java/nvim-java/issues/493)) ([bb12076](https://github.com/nvim-java/nvim-java/commit/bb120763d717eb375db1fe9c5d2034855aa1514c))
18+
319
## [4.1.0](https://github.com/nvim-java/nvim-java/compare/v4.0.4...v4.1.0) (2026-02-04)
420

521

CLAUDE.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -76,22 +76,22 @@ plugin/java.lua # User command registration
7676

7777
```
7878
tests/
79-
├── assets/ # Test fixtures and assets (e.g., HelloWorld.java)
80-
├── constants/ # Test constants (e.g., capabilities.lua)
79+
├── fixtures/ # Test projects (e.g., demo/ - Maven project with JUnit tests)
8180
├── utils/ # Test utilities and config files
8281
│ ├── lsp-utils.lua # LSP test helpers
82+
│ ├── project.lua # Fixture project + wait/assert helpers
8383
│ ├── prepare-config.lua # Lazy.nvim test setup
84-
│ └── test-config.lua # Manual test setup
84+
│ └── test-config.lua # Minimal init for plenary child nvim
8585
└── specs/ # Test specifications
86-
├── lsp_spec.lua # All LSP-related tests
87-
└── pkgm_spec.lua # All pkgm-related tests
86+
└── integration_spec.lua # End-to-end plugin feature tests
8887
```
8988

9089
**Test Guidelines:**
91-
- Group related tests in single spec file (e.g., all pkgm tests in `pkgm_spec.lua`)
90+
- Tests are end-to-end: real Neovim + real JDTLS against fixture Maven project
91+
- Fixture projects are copied to a temp dir at runtime (root markers prefer `.git`, so they must run outside the repo)
92+
- Spec `it` blocks run sequentially and share JDTLS session state; order matters
9293
- Extract reusable logic to `utils/` to keep test steps clean
93-
- Store test data/fixtures in `assets/`
94-
- Store constants (capabilities, expected values) in `constants/`
94+
- Store test projects in `fixtures/`
9595

9696
## Code Patterns
9797

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ TESTS_ROOT=tests
33
TESTS_DIR?=${TESTS_ROOT}/specs
44
PREPARE_CONFIG=${TESTS_ROOT}/utils/prepare-config.lua
55
TEST_CONFIG=${TESTS_ROOT}/utils/test-config.lua
6-
TEST_TIMEOUT?=60000
6+
TEST_TIMEOUT?=600000
77

88
.PHONY: test tests lint format all
99

lua/java-dap/setup.lua

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,14 @@ end
4141
function Setup:enrich_config(config)
4242
config = vim.deepcopy(config)
4343

44+
-- Attach configs don't need enriching — the JVM is already running and
45+
-- chose its own main class, classpath, and java executable. Without this
46+
-- short-circuit, the `assert(main, ...)` below fires because attach
47+
-- configs (correctly) have no mainClass.
48+
if config.request == 'attach' then
49+
return config
50+
end
51+
4452
-- skip enriching if already enriched
4553
if config.mainClass and config.projectName and config.modulePaths and config.classPaths and config.javaExec then
4654
return config

lua/java-refactor/client-command-handlers.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ local M = {
1818
---@param params java-refactor.RenameAction[]
1919
[ClientCommand.RENAME_COMMAND] = function(params)
2020
run('Failed to rename the symbol', function(action)
21-
action.rename(params)
21+
action:rename(params)
2222
end)
2323
end,
2424

lua/java.lua

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ function M.setup(custom_config)
7878
----------------------------------------------------------------------
7979
-- init --
8080
----------------------------------------------------------------------
81+
if config.experimental.fix_generated_sources then
82+
require('java.experimental.fix-generated-sources').patch(vim.fn.getcwd())
83+
end
84+
8185
require('java.startup.lsp_setup').setup(config)
8286
require('java.startup.decompile-watcher').setup()
8387
require('java-refactor').setup()

lua/java/config.lua

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ local V = jdtls_version_map[JDTLS_VERSION]
2727
---@field java_debug_adapter { enable: boolean, version: string, path: string|nil, auto_install: boolean }
2828
---@field spring_boot_tools { enable: boolean, version: string, path: string|nil, auto_install: boolean }
2929
---@field jdk { auto_install: boolean, version: string, path: string|nil }
30+
---@field experimental { fix_generated_sources: boolean }
3031
---@field log java-core.Log2Config
3132

3233
---@class java.PartialConfig
@@ -37,6 +38,7 @@ local V = jdtls_version_map[JDTLS_VERSION]
3738
---@field java_debug_adapter? { enable?: boolean, version?: string, path?: string, auto_install?: boolean }
3839
---@field spring_boot_tools? { enable?: boolean, version?: string, path?: string, auto_install?: boolean }
3940
---@field jdk? { auto_install?: boolean, version?: string, path?: string }
41+
---@field experimental? { fix_generated_sources?: boolean }
4042
---@field log? java-core.PartialLog2Config
4143

4244
---@type java.Config
@@ -46,6 +48,10 @@ local config = {
4648
nvim_jdtls_conflict = true,
4749
},
4850

51+
experimental = {
52+
fix_generated_sources = false,
53+
},
54+
4955
jdtls = {
5056
version = JDTLS_VERSION,
5157
path = nil,
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
local path_utils = require('java-core.utils.path')
2+
local log = require('java-core.utils.log2')
3+
4+
local M = {}
5+
6+
---Normalize path separators to `/` for .classpath operations.
7+
---Eclipse .classpath files always use `/` regardless of OS.
8+
---@param p string
9+
---@return string
10+
local function classpath_normalize(p)
11+
return p:gsub('\\', '/')
12+
end
13+
14+
---Set or update the `excluding` attribute on a classpathentry line.
15+
---Parses any existing exclusions, merges with the new set (union),
16+
---and writes the sorted result back.
17+
---@param line string
18+
---@param exclusions string[]
19+
---@return string
20+
local function set_exclusions(line, exclusions)
21+
-- Parse existing exclusions from the line
22+
local existing = {}
23+
local existing_match = line:match('excluding="([^"]+)"')
24+
if existing_match then
25+
for entry in existing_match:gmatch('[^|]+') do
26+
existing[entry] = true
27+
end
28+
end
29+
30+
-- Merge: union existing + new (new keys take no precedence — it's a set union)
31+
local merged = vim.deepcopy(existing)
32+
for _, excl in ipairs(exclusions) do
33+
merged[excl] = true
34+
end
35+
36+
local merged_list = vim.tbl_keys(merged)
37+
table.sort(merged_list)
38+
local exclusion_value = table.concat(merged_list, '|')
39+
40+
if existing_match then
41+
return line:gsub('excluding="[^"]+"', 'excluding="' .. exclusion_value .. '"', 1)
42+
end
43+
44+
return line:gsub('<classpathentry ', '<classpathentry excluding="' .. exclusion_value .. '" ', 1)
45+
end
46+
47+
---Get the relative path from root. Result is normalized to `/`
48+
---for .classpath compatibility.
49+
---@param root string
50+
---@param absolute_path string
51+
---@return string
52+
local function get_relative_path(root, absolute_path)
53+
-- vim.fs.find always joins children with '/' regardless of OS, so
54+
-- normalize both sides before stripping the prefix.
55+
local normalized_root = classpath_normalize(root)
56+
local normalized_path = classpath_normalize(absolute_path)
57+
local prefix = normalized_root .. '/'
58+
if normalized_path:sub(1, #prefix) == prefix then
59+
return normalized_path:sub(#prefix + 1)
60+
end
61+
return normalized_path
62+
end
63+
64+
---@param lines string[]
65+
---@param entry_path string
66+
---@return boolean
67+
local function has_classpath_entry(lines, entry_path)
68+
local pattern = 'path="' .. entry_path .. '"'
69+
for _, line in ipairs(lines) do
70+
if line:find(pattern, 1, true) then
71+
return true
72+
end
73+
end
74+
return false
75+
end
76+
77+
---Find the index of the target/generated-sources classpathentry.
78+
---Assumes .classpath uses double or single quotes and one tag per line.
79+
---@param lines string[]
80+
---@return integer|nil
81+
local function find_generated_sources_entry(lines)
82+
for index, line in ipairs(lines) do
83+
if line:find('path="target/generated%-sources"') or line:find("path='target/generated%-sources'") then
84+
return index
85+
end
86+
end
87+
return nil
88+
end
89+
90+
---Insert a new classpathentry before the <classpathentry kind="output"> line.
91+
---@param lines string[]
92+
---@param entry_path string
93+
local function add_classpath_entry(lines, entry_path)
94+
local entry = {
95+
'\t<classpathentry kind="src" output="target/classes" path="' .. entry_path .. '">',
96+
'\t\t<attributes>',
97+
'\t\t\t<attribute name="optional" value="true"/>',
98+
'\t\t\t<attribute name="maven.pomderived" value="true"/>',
99+
'\t\t</attributes>',
100+
'\t</classpathentry>',
101+
}
102+
103+
local output_index = #lines + 1
104+
for index, line in ipairs(lines) do
105+
if line:find('<classpathentry kind="output"') then
106+
output_index = index
107+
break
108+
end
109+
end
110+
111+
for offset = #entry, 1, -1 do
112+
table.insert(lines, output_index, entry[offset])
113+
end
114+
end
115+
116+
---Find generated source roots under target/generated-sources that
117+
---match the pattern `*/src/{segment}/java`.
118+
---@param module_root string
119+
---@return string[]
120+
local function get_generated_source_roots(module_root)
121+
local generated_root = path_utils.join(module_root, 'target', 'generated-sources')
122+
if not vim.uv.fs_stat(generated_root) then
123+
return {}
124+
end
125+
126+
-- vim.fs.find always yields '/'-separated paths regardless of OS, so
127+
-- match against a '/'-normalized substring even on Windows.
128+
local target_gen_src = 'target/generated-sources'
129+
local java_roots = vim.fs.find(function(name, generated_path)
130+
return name == 'java' and classpath_normalize(generated_path):find(target_gen_src, 1, true) ~= nil
131+
end, {
132+
path = generated_root,
133+
type = 'directory',
134+
limit = 500,
135+
})
136+
137+
local source_roots = {}
138+
local seen_roots = {}
139+
for _, java_root in ipairs(java_roots) do
140+
local relative_to_generated_root = get_relative_path(generated_root, java_root)
141+
-- Split on / since get_relative_path normalizes
142+
local segments = vim.split(relative_to_generated_root, '/', { plain = true })
143+
local segment_count = #segments
144+
145+
if segment_count >= 3 and segments[segment_count] == 'java' and segments[segment_count - 2] == 'src' then
146+
local source_root = get_relative_path(module_root, java_root)
147+
if not seen_roots[source_root] then
148+
seen_roots[source_root] = true
149+
table.insert(source_roots, source_root)
150+
end
151+
end
152+
end
153+
154+
table.sort(source_roots)
155+
return source_roots
156+
end
157+
158+
---Build exclusion list for the parent target/generated-sources entry.
159+
---Always seeds `annotations/` — Eclipse/Maven convention excludes the
160+
---annotations subdirectory from generated-sources to avoid processing
161+
---compiled annotation processor output as source.
162+
---@param source_roots string[]
163+
---@return string[]
164+
local function get_generated_source_exclusions(source_roots)
165+
local exclusions = { ['annotations/'] = true }
166+
-- Normalize prefix to / for comparison with source_roots (which are /-normalized)
167+
local generated_root_prefix = classpath_normalize(path_utils.join('target', 'generated-sources')) .. '/'
168+
169+
for _, source_root in ipairs(source_roots) do
170+
if source_root:sub(1, #generated_root_prefix) == generated_root_prefix then
171+
local relative_to_generated_root = source_root:sub(#generated_root_prefix + 1)
172+
local first_segment = vim.split(relative_to_generated_root, '/', { plain = true })[1]
173+
if first_segment then
174+
exclusions[first_segment .. '/'] = true
175+
end
176+
end
177+
end
178+
179+
local ordered_exclusions = vim.tbl_keys(exclusions)
180+
table.sort(ordered_exclusions)
181+
return ordered_exclusions
182+
end
183+
184+
---Patch a single .classpath file to include generated source roots
185+
---and exclude them from the parent generated-sources entry.
186+
---Safe to re-run — idempotent (has_classpath_entry + set_exclusions
187+
---no-op when already applied).
188+
---@param classpath_file string
189+
---@return boolean changed
190+
local function patch_module_classpath(classpath_file)
191+
local module_root = vim.fs.dirname(classpath_file)
192+
local lines = vim.fn.readfile(classpath_file)
193+
local generated_sources_entry_index = find_generated_sources_entry(lines)
194+
if not generated_sources_entry_index then
195+
return false
196+
end
197+
198+
local file_changed = false
199+
local source_roots = get_generated_source_roots(module_root)
200+
local line = lines[generated_sources_entry_index]
201+
local patched = set_exclusions(line, get_generated_source_exclusions(source_roots))
202+
203+
if patched ~= line then
204+
lines[generated_sources_entry_index] = patched
205+
file_changed = true
206+
end
207+
208+
for _, source_root in ipairs(source_roots) do
209+
if not has_classpath_entry(lines, source_root) then
210+
add_classpath_entry(lines, source_root)
211+
file_changed = true
212+
end
213+
end
214+
215+
if file_changed then
216+
log.info('nvim-java: patching .classpath with generated sources', classpath_file)
217+
vim.fn.writefile(lines, classpath_file)
218+
end
219+
220+
return file_changed
221+
end
222+
223+
---Patch all .classpath files under `root` to include generated source roots.
224+
---This is an experimental workaround for JDTLS import failures caused by
225+
---nested generated sources under target/generated-sources.
226+
---@param root string # Project root directory
227+
---@return boolean changed # true if any .classpath file was modified
228+
function M.patch(root)
229+
local changed = false
230+
local start = vim.uv.hrtime()
231+
local count = 0
232+
local LIMIT = 500
233+
234+
for _, file in ipairs(vim.fs.find('.classpath', { path = root, type = 'file', limit = LIMIT })) do
235+
if patch_module_classpath(file) then
236+
changed = true
237+
end
238+
count = count + 1
239+
end
240+
241+
if count >= LIMIT then
242+
log.warn(
243+
'nvim-java: fix_generated_sources hit .classpath find limit (' .. LIMIT .. ') — some files may be missed'
244+
)
245+
end
246+
247+
local elapsed = (vim.uv.hrtime() - start) / 1e6
248+
log.debug(('nvim-java: fix_generated_sources scanned %d .classpath files in %.2fms'):format(count, elapsed))
249+
250+
return changed
251+
end
252+
253+
return M

0 commit comments

Comments
 (0)