refactor: remove ambient instance reads from lsp (#23023)
This commit is contained in:
@@ -11,7 +11,6 @@ import z from "zod"
|
|||||||
import type * as LSPServer from "./server"
|
import type * as LSPServer from "./server"
|
||||||
import { NamedError } from "@opencode-ai/shared/util/error"
|
import { NamedError } from "@opencode-ai/shared/util/error"
|
||||||
import { withTimeout } from "../util/timeout"
|
import { withTimeout } from "../util/timeout"
|
||||||
import { Instance } from "../project/instance"
|
|
||||||
import { Filesystem } from "../util"
|
import { Filesystem } from "../util"
|
||||||
|
|
||||||
const DIAGNOSTICS_DEBOUNCE_MS = 150
|
const DIAGNOSTICS_DEBOUNCE_MS = 150
|
||||||
@@ -39,7 +38,7 @@ export const Event = {
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function create(input: { serverID: string; server: LSPServer.Handle; root: string }) {
|
export async function create(input: { serverID: string; server: LSPServer.Handle; root: string; directory: string }) {
|
||||||
const l = log.clone().tag("serverID", input.serverID)
|
const l = log.clone().tag("serverID", input.serverID)
|
||||||
l.info("starting client")
|
l.info("starting client")
|
||||||
|
|
||||||
@@ -145,33 +144,33 @@ export async function create(input: { serverID: string; server: LSPServer.Handle
|
|||||||
return connection
|
return connection
|
||||||
},
|
},
|
||||||
notify: {
|
notify: {
|
||||||
async open(input: { path: string }) {
|
async open(request: { path: string }) {
|
||||||
input.path = path.isAbsolute(input.path) ? input.path : path.resolve(Instance.directory, input.path)
|
request.path = path.isAbsolute(request.path) ? request.path : path.resolve(input.directory, request.path)
|
||||||
const text = await Filesystem.readText(input.path)
|
const text = await Filesystem.readText(request.path)
|
||||||
const extension = path.extname(input.path)
|
const extension = path.extname(request.path)
|
||||||
const languageId = LANGUAGE_EXTENSIONS[extension] ?? "plaintext"
|
const languageId = LANGUAGE_EXTENSIONS[extension] ?? "plaintext"
|
||||||
|
|
||||||
const version = files[input.path]
|
const version = files[request.path]
|
||||||
if (version !== undefined) {
|
if (version !== undefined) {
|
||||||
log.info("workspace/didChangeWatchedFiles", input)
|
log.info("workspace/didChangeWatchedFiles", request)
|
||||||
await connection.sendNotification("workspace/didChangeWatchedFiles", {
|
await connection.sendNotification("workspace/didChangeWatchedFiles", {
|
||||||
changes: [
|
changes: [
|
||||||
{
|
{
|
||||||
uri: pathToFileURL(input.path).href,
|
uri: pathToFileURL(request.path).href,
|
||||||
type: 2, // Changed
|
type: 2, // Changed
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
const next = version + 1
|
const next = version + 1
|
||||||
files[input.path] = next
|
files[request.path] = next
|
||||||
log.info("textDocument/didChange", {
|
log.info("textDocument/didChange", {
|
||||||
path: input.path,
|
path: request.path,
|
||||||
version: next,
|
version: next,
|
||||||
})
|
})
|
||||||
await connection.sendNotification("textDocument/didChange", {
|
await connection.sendNotification("textDocument/didChange", {
|
||||||
textDocument: {
|
textDocument: {
|
||||||
uri: pathToFileURL(input.path).href,
|
uri: pathToFileURL(request.path).href,
|
||||||
version: next,
|
version: next,
|
||||||
},
|
},
|
||||||
contentChanges: [{ text }],
|
contentChanges: [{ text }],
|
||||||
@@ -179,36 +178,36 @@ export async function create(input: { serverID: string; server: LSPServer.Handle
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("workspace/didChangeWatchedFiles", input)
|
log.info("workspace/didChangeWatchedFiles", request)
|
||||||
await connection.sendNotification("workspace/didChangeWatchedFiles", {
|
await connection.sendNotification("workspace/didChangeWatchedFiles", {
|
||||||
changes: [
|
changes: [
|
||||||
{
|
{
|
||||||
uri: pathToFileURL(input.path).href,
|
uri: pathToFileURL(request.path).href,
|
||||||
type: 1, // Created
|
type: 1, // Created
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
log.info("textDocument/didOpen", input)
|
log.info("textDocument/didOpen", request)
|
||||||
diagnostics.delete(input.path)
|
diagnostics.delete(request.path)
|
||||||
await connection.sendNotification("textDocument/didOpen", {
|
await connection.sendNotification("textDocument/didOpen", {
|
||||||
textDocument: {
|
textDocument: {
|
||||||
uri: pathToFileURL(input.path).href,
|
uri: pathToFileURL(request.path).href,
|
||||||
languageId,
|
languageId,
|
||||||
version: 0,
|
version: 0,
|
||||||
text,
|
text,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
files[input.path] = 0
|
files[request.path] = 0
|
||||||
return
|
return
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
get diagnostics() {
|
get diagnostics() {
|
||||||
return diagnostics
|
return diagnostics
|
||||||
},
|
},
|
||||||
async waitForDiagnostics(input: { path: string }) {
|
async waitForDiagnostics(request: { path: string }) {
|
||||||
const normalizedPath = Filesystem.normalizePath(
|
const normalizedPath = Filesystem.normalizePath(
|
||||||
path.isAbsolute(input.path) ? input.path : path.resolve(Instance.directory, input.path),
|
path.isAbsolute(request.path) ? request.path : path.resolve(input.directory, request.path),
|
||||||
)
|
)
|
||||||
log.info("waiting for diagnostics", { path: normalizedPath })
|
log.info("waiting for diagnostics", { path: normalizedPath })
|
||||||
let unsub: () => void
|
let unsub: () => void
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ import { pathToFileURL, fileURLToPath } from "url"
|
|||||||
import * as LSPServer from "./server"
|
import * as LSPServer from "./server"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { Config } from "../config"
|
import { Config } from "../config"
|
||||||
import { Instance } from "../project/instance"
|
|
||||||
import { Flag } from "@/flag/flag"
|
import { Flag } from "@/flag/flag"
|
||||||
import { Process } from "../util"
|
import { Process } from "../util"
|
||||||
import { spawn as lspspawn } from "./launch"
|
import { spawn as lspspawn } from "./launch"
|
||||||
import { Effect, Layer, Context } from "effect"
|
import { Effect, Layer, Context } from "effect"
|
||||||
import { InstanceState } from "@/effect"
|
import { InstanceState } from "@/effect"
|
||||||
|
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
||||||
|
|
||||||
const log = Log.create({ service: "lsp" })
|
const log = Log.create({ service: "lsp" })
|
||||||
|
|
||||||
@@ -162,7 +162,7 @@ export const layer = Layer.effect(
|
|||||||
const config = yield* Config.Service
|
const config = yield* Config.Service
|
||||||
|
|
||||||
const state = yield* InstanceState.make<State>(
|
const state = yield* InstanceState.make<State>(
|
||||||
Effect.fn("LSP.state")(function* () {
|
Effect.fn("LSP.state")(function* (ctx) {
|
||||||
const cfg = yield* config.get()
|
const cfg = yield* config.get()
|
||||||
|
|
||||||
const servers: Record<string, LSPServer.Info> = {}
|
const servers: Record<string, LSPServer.Info> = {}
|
||||||
@@ -187,7 +187,7 @@ export const layer = Layer.effect(
|
|||||||
servers[name] = {
|
servers[name] = {
|
||||||
...existing,
|
...existing,
|
||||||
id: name,
|
id: name,
|
||||||
root: existing?.root ?? (async () => Instance.directory),
|
root: existing?.root ?? (async (_file, ctx) => ctx.directory),
|
||||||
extensions: item.extensions ?? existing?.extensions ?? [],
|
extensions: item.extensions ?? existing?.extensions ?? [],
|
||||||
spawn: async (root) => ({
|
spawn: async (root) => ({
|
||||||
process: lspspawn(item.command[0], item.command.slice(1), {
|
process: lspspawn(item.command[0], item.command.slice(1), {
|
||||||
@@ -225,7 +225,10 @@ export const layer = Layer.effect(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const getClients = Effect.fnUntraced(function* (file: string) {
|
const getClients = Effect.fnUntraced(function* (file: string) {
|
||||||
if (!Instance.containsPath(file)) return [] as LSPClient.Info[]
|
const ctx = yield* InstanceState.context
|
||||||
|
if (!AppFileSystem.contains(ctx.directory, file) && (ctx.worktree === "/" || !AppFileSystem.contains(ctx.worktree, file))) {
|
||||||
|
return [] as LSPClient.Info[]
|
||||||
|
}
|
||||||
const s = yield* InstanceState.get(state)
|
const s = yield* InstanceState.get(state)
|
||||||
return yield* Effect.promise(async () => {
|
return yield* Effect.promise(async () => {
|
||||||
const extension = path.parse(file).ext || file
|
const extension = path.parse(file).ext || file
|
||||||
@@ -233,7 +236,7 @@ export const layer = Layer.effect(
|
|||||||
|
|
||||||
async function schedule(server: LSPServer.Info, root: string, key: string) {
|
async function schedule(server: LSPServer.Info, root: string, key: string) {
|
||||||
const handle = await server
|
const handle = await server
|
||||||
.spawn(root)
|
.spawn(root, ctx)
|
||||||
.then((value) => {
|
.then((value) => {
|
||||||
if (!value) s.broken.add(key)
|
if (!value) s.broken.add(key)
|
||||||
return value
|
return value
|
||||||
@@ -251,6 +254,7 @@ export const layer = Layer.effect(
|
|||||||
serverID: server.id,
|
serverID: server.id,
|
||||||
server: handle,
|
server: handle,
|
||||||
root,
|
root,
|
||||||
|
directory: ctx.directory,
|
||||||
}).catch(async (err) => {
|
}).catch(async (err) => {
|
||||||
s.broken.add(key)
|
s.broken.add(key)
|
||||||
await Process.stop(handle.process)
|
await Process.stop(handle.process)
|
||||||
@@ -273,7 +277,7 @@ export const layer = Layer.effect(
|
|||||||
for (const server of Object.values(s.servers)) {
|
for (const server of Object.values(s.servers)) {
|
||||||
if (server.extensions.length && !server.extensions.includes(extension)) continue
|
if (server.extensions.length && !server.extensions.includes(extension)) continue
|
||||||
|
|
||||||
const root = await server.root(file)
|
const root = await server.root(file, ctx)
|
||||||
if (!root) continue
|
if (!root) continue
|
||||||
if (s.broken.has(root + server.id)) continue
|
if (s.broken.has(root + server.id)) continue
|
||||||
|
|
||||||
@@ -326,13 +330,14 @@ export const layer = Layer.effect(
|
|||||||
})
|
})
|
||||||
|
|
||||||
const status = Effect.fn("LSP.status")(function* () {
|
const status = Effect.fn("LSP.status")(function* () {
|
||||||
|
const ctx = yield* InstanceState.context
|
||||||
const s = yield* InstanceState.get(state)
|
const s = yield* InstanceState.get(state)
|
||||||
const result: Status[] = []
|
const result: Status[] = []
|
||||||
for (const client of s.clients) {
|
for (const client of s.clients) {
|
||||||
result.push({
|
result.push({
|
||||||
id: client.serverID,
|
id: client.serverID,
|
||||||
name: s.servers[client.serverID].id,
|
name: s.servers[client.serverID].id,
|
||||||
root: path.relative(Instance.directory, client.root),
|
root: path.relative(ctx.directory, client.root),
|
||||||
status: "connected",
|
status: "connected",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -340,12 +345,13 @@ export const layer = Layer.effect(
|
|||||||
})
|
})
|
||||||
|
|
||||||
const hasClients = Effect.fn("LSP.hasClients")(function* (file: string) {
|
const hasClients = Effect.fn("LSP.hasClients")(function* (file: string) {
|
||||||
|
const ctx = yield* InstanceState.context
|
||||||
const s = yield* InstanceState.get(state)
|
const s = yield* InstanceState.get(state)
|
||||||
return yield* Effect.promise(async () => {
|
return yield* Effect.promise(async () => {
|
||||||
const extension = path.parse(file).ext || file
|
const extension = path.parse(file).ext || file
|
||||||
for (const server of Object.values(s.servers)) {
|
for (const server of Object.values(s.servers)) {
|
||||||
if (server.extensions.length && !server.extensions.includes(extension)) continue
|
if (server.extensions.length && !server.extensions.includes(extension)) continue
|
||||||
const root = await server.root(file)
|
const root = await server.root(file, ctx)
|
||||||
if (!root) continue
|
if (!root) continue
|
||||||
if (s.broken.has(root + server.id)) continue
|
if (s.broken.has(root + server.id)) continue
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Log } from "../util"
|
|||||||
import { text } from "node:stream/consumers"
|
import { text } from "node:stream/consumers"
|
||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import { Filesystem } from "../util"
|
import { Filesystem } from "../util"
|
||||||
import { Instance } from "../project/instance"
|
import type { InstanceContext } from "../project/instance"
|
||||||
import { Flag } from "../flag/flag"
|
import { Flag } from "../flag/flag"
|
||||||
import { Archive } from "../util"
|
import { Archive } from "../util"
|
||||||
import { Process } from "../util"
|
import { Process } from "../util"
|
||||||
@@ -29,15 +29,15 @@ export interface Handle {
|
|||||||
initialization?: Record<string, any>
|
initialization?: Record<string, any>
|
||||||
}
|
}
|
||||||
|
|
||||||
type RootFunction = (file: string) => Promise<string | undefined>
|
type RootFunction = (file: string, ctx: InstanceContext) => Promise<string | undefined>
|
||||||
|
|
||||||
const NearestRoot = (includePatterns: string[], excludePatterns?: string[]): RootFunction => {
|
const NearestRoot = (includePatterns: string[], excludePatterns?: string[]): RootFunction => {
|
||||||
return async (file) => {
|
return async (file, ctx) => {
|
||||||
if (excludePatterns) {
|
if (excludePatterns) {
|
||||||
const excludedFiles = Filesystem.up({
|
const excludedFiles = Filesystem.up({
|
||||||
targets: excludePatterns,
|
targets: excludePatterns,
|
||||||
start: path.dirname(file),
|
start: path.dirname(file),
|
||||||
stop: Instance.directory,
|
stop: ctx.directory,
|
||||||
})
|
})
|
||||||
const excluded = await excludedFiles.next()
|
const excluded = await excludedFiles.next()
|
||||||
await excludedFiles.return()
|
await excludedFiles.return()
|
||||||
@@ -46,11 +46,11 @@ const NearestRoot = (includePatterns: string[], excludePatterns?: string[]): Roo
|
|||||||
const files = Filesystem.up({
|
const files = Filesystem.up({
|
||||||
targets: includePatterns,
|
targets: includePatterns,
|
||||||
start: path.dirname(file),
|
start: path.dirname(file),
|
||||||
stop: Instance.directory,
|
stop: ctx.directory,
|
||||||
})
|
})
|
||||||
const first = await files.next()
|
const first = await files.next()
|
||||||
await files.return()
|
await files.return()
|
||||||
if (!first.value) return Instance.directory
|
if (!first.value) return ctx.directory
|
||||||
return path.dirname(first.value)
|
return path.dirname(first.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -60,16 +60,16 @@ export interface Info {
|
|||||||
extensions: string[]
|
extensions: string[]
|
||||||
global?: boolean
|
global?: boolean
|
||||||
root: RootFunction
|
root: RootFunction
|
||||||
spawn(root: string): Promise<Handle | undefined>
|
spawn(root: string, ctx: InstanceContext): Promise<Handle | undefined>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Deno: Info = {
|
export const Deno: Info = {
|
||||||
id: "deno",
|
id: "deno",
|
||||||
root: async (file) => {
|
root: async (file, ctx) => {
|
||||||
const files = Filesystem.up({
|
const files = Filesystem.up({
|
||||||
targets: ["deno.json", "deno.jsonc"],
|
targets: ["deno.json", "deno.jsonc"],
|
||||||
start: path.dirname(file),
|
start: path.dirname(file),
|
||||||
stop: Instance.directory,
|
stop: ctx.directory,
|
||||||
})
|
})
|
||||||
const first = await files.next()
|
const first = await files.next()
|
||||||
await files.return()
|
await files.return()
|
||||||
@@ -98,8 +98,8 @@ export const Typescript: Info = {
|
|||||||
["deno.json", "deno.jsonc"],
|
["deno.json", "deno.jsonc"],
|
||||||
),
|
),
|
||||||
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"],
|
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"],
|
||||||
async spawn(root) {
|
async spawn(root, ctx) {
|
||||||
const tsserver = Module.resolve("typescript/lib/tsserver.js", Instance.directory)
|
const tsserver = Module.resolve("typescript/lib/tsserver.js", ctx.directory)
|
||||||
log.info("typescript server", { tsserver })
|
log.info("typescript server", { tsserver })
|
||||||
if (!tsserver) return
|
if (!tsserver) return
|
||||||
const bin = await Npm.which("typescript-language-server")
|
const bin = await Npm.which("typescript-language-server")
|
||||||
@@ -154,8 +154,8 @@ export const ESLint: Info = {
|
|||||||
id: "eslint",
|
id: "eslint",
|
||||||
root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
|
root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
|
||||||
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts", ".vue"],
|
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts", ".vue"],
|
||||||
async spawn(root) {
|
async spawn(root, ctx) {
|
||||||
const eslint = Module.resolve("eslint", Instance.directory)
|
const eslint = Module.resolve("eslint", ctx.directory)
|
||||||
if (!eslint) return
|
if (!eslint) return
|
||||||
log.info("spawning eslint server")
|
log.info("spawning eslint server")
|
||||||
const serverPath = path.join(Global.Path.bin, "vscode-eslint", "server", "out", "eslintServer.js")
|
const serverPath = path.join(Global.Path.bin, "vscode-eslint", "server", "out", "eslintServer.js")
|
||||||
@@ -219,7 +219,7 @@ export const Oxlint: Info = {
|
|||||||
"package.json",
|
"package.json",
|
||||||
]),
|
]),
|
||||||
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts", ".vue", ".astro", ".svelte"],
|
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts", ".vue", ".astro", ".svelte"],
|
||||||
async spawn(root) {
|
async spawn(root, ctx) {
|
||||||
const ext = process.platform === "win32" ? ".cmd" : ""
|
const ext = process.platform === "win32" ? ".cmd" : ""
|
||||||
|
|
||||||
const serverTarget = path.join("node_modules", ".bin", "oxc_language_server" + ext)
|
const serverTarget = path.join("node_modules", ".bin", "oxc_language_server" + ext)
|
||||||
@@ -232,7 +232,7 @@ export const Oxlint: Info = {
|
|||||||
const candidates = Filesystem.up({
|
const candidates = Filesystem.up({
|
||||||
targets: [target],
|
targets: [target],
|
||||||
start: root,
|
start: root,
|
||||||
stop: Instance.worktree,
|
stop: ctx.worktree,
|
||||||
})
|
})
|
||||||
const first = await candidates.next()
|
const first = await candidates.next()
|
||||||
await candidates.return()
|
await candidates.return()
|
||||||
@@ -344,10 +344,10 @@ export const Biome: Info = {
|
|||||||
|
|
||||||
export const Gopls: Info = {
|
export const Gopls: Info = {
|
||||||
id: "gopls",
|
id: "gopls",
|
||||||
root: async (file) => {
|
root: async (file, ctx) => {
|
||||||
const work = await NearestRoot(["go.work"])(file)
|
const work = await NearestRoot(["go.work"])(file, ctx)
|
||||||
if (work) return work
|
if (work) return work
|
||||||
return NearestRoot(["go.mod", "go.sum"])(file)
|
return NearestRoot(["go.mod", "go.sum"])(file, ctx)
|
||||||
},
|
},
|
||||||
extensions: [".go"],
|
extensions: [".go"],
|
||||||
async spawn(root) {
|
async spawn(root) {
|
||||||
@@ -810,8 +810,8 @@ export const SourceKit: Info = {
|
|||||||
|
|
||||||
export const RustAnalyzer: Info = {
|
export const RustAnalyzer: Info = {
|
||||||
id: "rust",
|
id: "rust",
|
||||||
root: async (root) => {
|
root: async (file, ctx) => {
|
||||||
const crateRoot = await NearestRoot(["Cargo.toml", "Cargo.lock"])(root)
|
const crateRoot = await NearestRoot(["Cargo.toml", "Cargo.lock"])(file, ctx)
|
||||||
if (crateRoot === undefined) {
|
if (crateRoot === undefined) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
@@ -834,7 +834,7 @@ export const RustAnalyzer: Info = {
|
|||||||
currentDir = parentDir
|
currentDir = parentDir
|
||||||
|
|
||||||
// Stop if we've gone above the app root
|
// Stop if we've gone above the app root
|
||||||
if (!currentDir.startsWith(Instance.worktree)) break
|
if (!currentDir.startsWith(ctx.worktree)) break
|
||||||
}
|
}
|
||||||
|
|
||||||
return crateRoot
|
return crateRoot
|
||||||
@@ -1031,8 +1031,8 @@ export const Astro: Info = {
|
|||||||
id: "astro",
|
id: "astro",
|
||||||
extensions: [".astro"],
|
extensions: [".astro"],
|
||||||
root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
|
root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
|
||||||
async spawn(root) {
|
async spawn(root, ctx) {
|
||||||
const tsserver = Module.resolve("typescript/lib/tsserver.js", Instance.directory)
|
const tsserver = Module.resolve("typescript/lib/tsserver.js", ctx.directory)
|
||||||
if (!tsserver) {
|
if (!tsserver) {
|
||||||
log.info("typescript not found, required for Astro language server")
|
log.info("typescript not found, required for Astro language server")
|
||||||
return
|
return
|
||||||
@@ -1067,7 +1067,7 @@ export const Astro: Info = {
|
|||||||
|
|
||||||
export const JDTLS: Info = {
|
export const JDTLS: Info = {
|
||||||
id: "jdtls",
|
id: "jdtls",
|
||||||
root: async (file) => {
|
root: async (file, ctx) => {
|
||||||
// Without exclusions, NearestRoot defaults to instance directory so we can't
|
// Without exclusions, NearestRoot defaults to instance directory so we can't
|
||||||
// distinguish between a) no project found and b) project found at instance dir.
|
// distinguish between a) no project found and b) project found at instance dir.
|
||||||
// So we can't choose the root from (potential) monorepo markers first.
|
// So we can't choose the root from (potential) monorepo markers first.
|
||||||
@@ -1080,9 +1080,9 @@ export const JDTLS: Info = {
|
|||||||
NearestRoot(
|
NearestRoot(
|
||||||
["pom.xml", "build.gradle", "build.gradle.kts", ".project", ".classpath"],
|
["pom.xml", "build.gradle", "build.gradle.kts", ".project", ".classpath"],
|
||||||
exclusionsForMonorepos,
|
exclusionsForMonorepos,
|
||||||
)(file),
|
)(file, ctx),
|
||||||
NearestRoot(gradleMarkers, settingsMarkers)(file),
|
NearestRoot(gradleMarkers, settingsMarkers)(file, ctx),
|
||||||
NearestRoot(settingsMarkers)(file),
|
NearestRoot(settingsMarkers)(file, ctx),
|
||||||
])
|
])
|
||||||
|
|
||||||
// If projectRoot is undefined we know we are in a monorepo or no project at all.
|
// If projectRoot is undefined we know we are in a monorepo or no project at all.
|
||||||
@@ -1189,18 +1189,18 @@ export const JDTLS: Info = {
|
|||||||
export const KotlinLS: Info = {
|
export const KotlinLS: Info = {
|
||||||
id: "kotlin-ls",
|
id: "kotlin-ls",
|
||||||
extensions: [".kt", ".kts"],
|
extensions: [".kt", ".kts"],
|
||||||
root: async (file) => {
|
root: async (file, ctx) => {
|
||||||
// 1) Nearest Gradle root (multi-project or included build)
|
// 1) Nearest Gradle root (multi-project or included build)
|
||||||
const settingsRoot = await NearestRoot(["settings.gradle.kts", "settings.gradle"])(file)
|
const settingsRoot = await NearestRoot(["settings.gradle.kts", "settings.gradle"])(file, ctx)
|
||||||
if (settingsRoot) return settingsRoot
|
if (settingsRoot) return settingsRoot
|
||||||
// 2) Gradle wrapper (strong root signal)
|
// 2) Gradle wrapper (strong root signal)
|
||||||
const wrapperRoot = await NearestRoot(["gradlew", "gradlew.bat"])(file)
|
const wrapperRoot = await NearestRoot(["gradlew", "gradlew.bat"])(file, ctx)
|
||||||
if (wrapperRoot) return wrapperRoot
|
if (wrapperRoot) return wrapperRoot
|
||||||
// 3) Single-project or module-level build
|
// 3) Single-project or module-level build
|
||||||
const buildRoot = await NearestRoot(["build.gradle.kts", "build.gradle"])(file)
|
const buildRoot = await NearestRoot(["build.gradle.kts", "build.gradle"])(file, ctx)
|
||||||
if (buildRoot) return buildRoot
|
if (buildRoot) return buildRoot
|
||||||
// 4) Maven fallback
|
// 4) Maven fallback
|
||||||
return NearestRoot(["pom.xml"])(file)
|
return NearestRoot(["pom.xml"])(file, ctx)
|
||||||
},
|
},
|
||||||
async spawn(root) {
|
async spawn(root) {
|
||||||
const distPath = path.join(Global.Path.bin, "kotlin-ls")
|
const distPath = path.join(Global.Path.bin, "kotlin-ls")
|
||||||
@@ -1539,7 +1539,7 @@ export const Ocaml: Info = {
|
|||||||
export const BashLS: Info = {
|
export const BashLS: Info = {
|
||||||
id: "bash",
|
id: "bash",
|
||||||
extensions: [".sh", ".bash", ".zsh", ".ksh"],
|
extensions: [".sh", ".bash", ".zsh", ".ksh"],
|
||||||
root: async () => Instance.directory,
|
root: async (_file, ctx) => ctx.directory,
|
||||||
async spawn(root) {
|
async spawn(root) {
|
||||||
let binary = which("bash-language-server")
|
let binary = which("bash-language-server")
|
||||||
const args: string[] = []
|
const args: string[] = []
|
||||||
@@ -1734,7 +1734,7 @@ export const TexLab: Info = {
|
|||||||
export const DockerfileLS: Info = {
|
export const DockerfileLS: Info = {
|
||||||
id: "dockerfile",
|
id: "dockerfile",
|
||||||
extensions: [".dockerfile", "Dockerfile"],
|
extensions: [".dockerfile", "Dockerfile"],
|
||||||
root: async () => Instance.directory,
|
root: async (_file, ctx) => ctx.directory,
|
||||||
async spawn(root) {
|
async spawn(root) {
|
||||||
let binary = which("docker-langserver")
|
let binary = which("docker-langserver")
|
||||||
const args: string[] = []
|
const args: string[] = []
|
||||||
@@ -1799,16 +1799,16 @@ export const Clojure: Info = {
|
|||||||
export const Nixd: Info = {
|
export const Nixd: Info = {
|
||||||
id: "nixd",
|
id: "nixd",
|
||||||
extensions: [".nix"],
|
extensions: [".nix"],
|
||||||
root: async (file) => {
|
root: async (file, ctx) => {
|
||||||
// First, look for flake.nix - the most reliable Nix project root indicator
|
// First, look for flake.nix - the most reliable Nix project root indicator
|
||||||
const flakeRoot = await NearestRoot(["flake.nix"])(file)
|
const flakeRoot = await NearestRoot(["flake.nix"])(file, ctx)
|
||||||
if (flakeRoot && flakeRoot !== Instance.directory) return flakeRoot
|
if (flakeRoot && flakeRoot !== ctx.directory) return flakeRoot
|
||||||
|
|
||||||
// If no flake.nix, fall back to git repository root
|
// If no flake.nix, fall back to git repository root
|
||||||
if (Instance.worktree && Instance.worktree !== Instance.directory) return Instance.worktree
|
if (ctx.worktree && ctx.worktree !== ctx.directory) return ctx.worktree
|
||||||
|
|
||||||
// Finally, use the instance directory as fallback
|
// Finally, use the instance directory as fallback
|
||||||
return Instance.directory
|
return ctx.directory
|
||||||
},
|
},
|
||||||
async spawn(root) {
|
async spawn(root) {
|
||||||
const nixd = which("nixd")
|
const nixd = which("nixd")
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ describe("LSPClient interop", () => {
|
|||||||
serverID: "fake",
|
serverID: "fake",
|
||||||
server: handle as unknown as LSPServer.Handle,
|
server: handle as unknown as LSPServer.Handle,
|
||||||
root: process.cwd(),
|
root: process.cwd(),
|
||||||
|
directory: process.cwd(),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -55,6 +56,7 @@ describe("LSPClient interop", () => {
|
|||||||
serverID: "fake",
|
serverID: "fake",
|
||||||
server: handle as unknown as LSPServer.Handle,
|
server: handle as unknown as LSPServer.Handle,
|
||||||
root: process.cwd(),
|
root: process.cwd(),
|
||||||
|
directory: process.cwd(),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -79,6 +81,7 @@ describe("LSPClient interop", () => {
|
|||||||
serverID: "fake",
|
serverID: "fake",
|
||||||
server: handle as unknown as LSPServer.Handle,
|
server: handle as unknown as LSPServer.Handle,
|
||||||
root: process.cwd(),
|
root: process.cwd(),
|
||||||
|
directory: process.cwd(),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user