Skip to content

Commit bea56fe

Browse files
areyouoknexxeln
andauthored
fix(lsp): resolve JDTLS root to topmost pom.xml in Java Maven multi-module projects (anomalyco#28761)
Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com>
1 parent 685a894 commit bea56fe

2 files changed

Lines changed: 535 additions & 17 deletions

File tree

packages/opencode/src/lsp/server.ts

Lines changed: 73 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,30 @@ const NearestRoot = (includePatterns: string[], excludePatterns?: string[]): Roo
5555
}
5656
}
5757

58+
const StrictNearestRoot = (includePatterns: string[], excludePatterns?: string[]): RootFunction => {
59+
return async (file, ctx) => {
60+
if (excludePatterns) {
61+
const excludedFiles = Filesystem.up({
62+
targets: excludePatterns,
63+
start: path.dirname(file),
64+
stop: ctx.directory,
65+
})
66+
const excluded = await excludedFiles.next()
67+
await excludedFiles.return()
68+
if (excluded.value) return undefined
69+
}
70+
const files = Filesystem.up({
71+
targets: includePatterns,
72+
start: path.dirname(file),
73+
stop: ctx.directory,
74+
})
75+
const first = await files.next()
76+
await files.return()
77+
if (!first.value) return undefined
78+
return path.dirname(first.value)
79+
}
80+
}
81+
5882
export interface Info {
5983
id: string
6084
extensions: string[]
@@ -1173,31 +1197,63 @@ export const Astro: Info = {
11731197
},
11741198
}
11751199

1200+
function isModuleOf(pomContent: string, modulePath: string): boolean {
1201+
const normalized = modulePath.replace(/\\/g, "/").replace(/\/$/, "")
1202+
if (!normalized) return false
1203+
const modulesBlocks = pomContent.match(/<modules>([\s\S]*?)<\/modules>/g) ?? []
1204+
for (const block of modulesBlocks) {
1205+
const stripped = block.replace(/<!--[\s\S]*?-->/g, "")
1206+
for (const m of stripped.matchAll(/<module>\s*([^<]+?)\s*<\/module>/g)) {
1207+
const decl = m[1].replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/$/, "")
1208+
if (decl === normalized) return true
1209+
}
1210+
}
1211+
return false
1212+
}
1213+
11761214
export const JDTLS: Info = {
11771215
id: "jdtls",
11781216
root: async (file, ctx) => {
1179-
// Without exclusions, NearestRoot defaults to instance directory so we can't
1180-
// distinguish between a) no project found and b) project found at instance dir.
1181-
// So we can't choose the root from (potential) monorepo markers first.
1182-
// Look for potential subproject markers first while excluding potential monorepo markers.
11831217
const settingsMarkers = ["settings.gradle", "settings.gradle.kts"]
11841218
const gradleMarkers = ["gradlew", "gradlew.bat"]
1185-
const exclusionsForMonorepos = gradleMarkers.concat(settingsMarkers)
1186-
1187-
const [projectRoot, wrapperRoot, settingsRoot] = await Promise.all([
1188-
NearestRoot(["pom.xml", "build.gradle", "build.gradle.kts", ".project", ".classpath"], exclusionsForMonorepos)(
1189-
file,
1190-
ctx,
1191-
),
1192-
NearestRoot(gradleMarkers, settingsMarkers)(file, ctx),
1193-
NearestRoot(settingsMarkers)(file, ctx),
1219+
// 1. Gradle (unchanged from original logic)
1220+
const [wrapperRoot, settingsRoot] = await Promise.all([
1221+
StrictNearestRoot(gradleMarkers, settingsMarkers)(file, ctx),
1222+
StrictNearestRoot(settingsMarkers)(file, ctx),
11941223
])
1195-
1196-
// If projectRoot is undefined we know we are in a monorepo or no project at all.
1197-
// So can safely fall through to the other roots
1198-
if (projectRoot) return projectRoot
11991224
if (wrapperRoot) return wrapperRoot
12001225
if (settingsRoot) return settingsRoot
1226+
1227+
// 2. Gradle single-project fallback (build.gradle without settings.gradle)
1228+
const buildRoot = await StrictNearestRoot(["build.gradle", "build.gradle.kts"])(file, ctx)
1229+
if (buildRoot) return buildRoot
1230+
1231+
// 3. Maven: walk up pom.xml chain verifying <module> relationships
1232+
const pomFiles = await Filesystem.findUp(
1233+
"pom.xml",
1234+
path.dirname(file),
1235+
ctx.directory,
1236+
)
1237+
if (pomFiles.length > 0) {
1238+
let root = path.dirname(pomFiles[0])
1239+
for (let i = 1; i < pomFiles.length; i++) {
1240+
const parentDir = path.dirname(pomFiles[i])
1241+
const rel = path.relative(parentDir, root)
1242+
const content = await fs.readFile(pomFiles[i], "utf-8").catch(() => null)
1243+
if (content && isModuleOf(content, rel)) {
1244+
root = parentDir
1245+
} else {
1246+
break
1247+
}
1248+
}
1249+
return root
1250+
}
1251+
1252+
// 4. Eclipse native project fallback
1253+
const eclipseRoot = await StrictNearestRoot([".project", ".classpath"])(file, ctx)
1254+
if (eclipseRoot) return eclipseRoot
1255+
1256+
return undefined
12011257
},
12021258
extensions: [".java"],
12031259
async spawn(root, _ctx, flags) {

0 commit comments

Comments
 (0)