Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion packages/opencode/src/lsp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
Expand Down Expand Up @@ -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")
Expand Down
83 changes: 83 additions & 0 deletions packages/opencode/test/lsp/server-root.test.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})