diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts index b0755b8b563..d32d8f17d93 100644 --- a/packages/opencode/src/lsp/server.ts +++ b/packages/opencode/src/lsp/server.ts @@ -50,6 +50,20 @@ export namespace LSPServer { } } + const MarkerRoot = (patterns: string[]): RootFunction => { + return async (file) => { + const files = Filesystem.up({ + targets: patterns, + start: path.dirname(file), + stop: Instance.directory, + }) + const first = await files.next() + await files.return() + if (!first.value) return undefined + return path.dirname(first.value) + } + } + export interface Info { id: string extensions: string[] @@ -1129,7 +1143,15 @@ export namespace LSPServer { export const JDTLS: Info = { id: "jdtls", - root: NearestRoot(["pom.xml", "build.gradle", "build.gradle.kts", ".project", ".classpath"]), + root: async (file) => { + const settingsRoot = await MarkerRoot(["settings.gradle.kts", "settings.gradle"])(file) + if (settingsRoot) return settingsRoot + + const wrapperRoot = await MarkerRoot(["gradlew", "gradlew.bat"])(file) + if (wrapperRoot) return wrapperRoot + + return NearestRoot(["build.gradle.kts", "build.gradle", "pom.xml", ".project", ".classpath"])(file) + }, extensions: [".java"], async spawn(root) { const java = Bun.which("java") diff --git a/packages/opencode/test/lsp/server-root.test.ts b/packages/opencode/test/lsp/server-root.test.ts new file mode 100644 index 00000000000..5079a63d038 --- /dev/null +++ b/packages/opencode/test/lsp/server-root.test.ts @@ -0,0 +1,83 @@ +import { describe, expect, test } from "bun:test" +import * as fs from "fs/promises" +import path from "path" +import { LSPServer } from "../../src/lsp/server" +import { Instance } from "../../src/project/instance" +import { tmpdir } from "../fixture/fixture" + +describe("lsp.server roots", () => { + test("jdtls prefers settings.gradle root", async () => { + await using tmp = await tmpdir() + const root = tmp.path + const module = path.join(root, "services/api") + const file = path.join(module, "src/main/java/App.java") + + await fs.mkdir(path.dirname(file), { recursive: true }) + await Bun.write(path.join(root, "settings.gradle"), "") + await Bun.write(path.join(module, "build.gradle"), "") + await Bun.write(file, "class App {}") + + const result = await Instance.provide({ + directory: root, + fn: () => LSPServer.JDTLS.root(file), + }) + + expect(result).toBe(root) + }) + + test("jdtls uses gradle wrapper root when settings is missing", async () => { + await using tmp = await tmpdir() + const root = tmp.path + const module = path.join(root, "services/api") + const file = path.join(module, "src/main/java/App.java") + + await fs.mkdir(path.dirname(file), { recursive: true }) + await Bun.write(path.join(root, "gradlew"), "") + await Bun.write(path.join(module, "build.gradle"), "") + await Bun.write(file, "class App {}") + + const result = await Instance.provide({ + directory: root, + fn: () => LSPServer.JDTLS.root(file), + }) + + expect(result).toBe(root) + }) + + test("jdtls falls back to nearest build file", async () => { + await using tmp = await tmpdir() + const root = tmp.path + const module = path.join(root, "services/api") + const file = path.join(module, "src/main/java/App.java") + + await fs.mkdir(path.dirname(file), { recursive: true }) + await Bun.write(path.join(module, "build.gradle"), "") + await Bun.write(file, "class App {}") + + const result = await Instance.provide({ + directory: root, + fn: () => LSPServer.JDTLS.root(file), + }) + + expect(result).toBe(module) + }) + + test("jdtls supports kotlin gradle markers", async () => { + await using tmp = await tmpdir() + const root = tmp.path + const module = path.join(root, "services/api") + const file = path.join(module, "src/main/java/App.java") + + await fs.mkdir(path.dirname(file), { recursive: true }) + await Bun.write(path.join(root, "settings.gradle.kts"), "") + await Bun.write(path.join(module, "build.gradle.kts"), "") + await Bun.write(file, "class App {}") + + const result = await Instance.provide({ + directory: root, + fn: () => LSPServer.JDTLS.root(file), + }) + + expect(result).toBe(root) + }) +})