diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 48f7db05426..b9ef2580be4 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -64,6 +64,7 @@ import { Editor } from "../../util/editor" import stripAnsi from "strip-ansi" import { Footer } from "./footer.tsx" import { usePromptRef } from "../../context/prompt" +import { Filesystem } from "@/util/filesystem" addDefaultParsers(parsers.parsers) @@ -1414,7 +1415,10 @@ ToolRegistry.register({ return props.input.content }) - const diagnostics = createMemo(() => props.metadata.diagnostics?.[props.input.filePath ?? ""] ?? []) + const diagnostics = createMemo(() => { + const filePath = Filesystem.normalizePath(props.input.filePath ?? "") + return props.metadata.diagnostics?.[filePath] ?? [] + }) return ( <> @@ -1587,7 +1591,8 @@ ToolRegistry.register({ const diffContent = createMemo(() => props.metadata.diff ?? props.permission["diff"]) const diagnostics = createMemo(() => { - const arr = props.metadata.diagnostics?.[props.input.filePath ?? ""] ?? [] + const filePath = Filesystem.normalizePath(props.input.filePath ?? "") + const arr = props.metadata.diagnostics?.[filePath] ?? [] return arr.filter((x) => x.severity === 1).slice(0, 3) }) diff --git a/packages/opencode/src/lsp/client.ts b/packages/opencode/src/lsp/client.ts index ce426cf622d..f06c2c938ea 100644 --- a/packages/opencode/src/lsp/client.ts +++ b/packages/opencode/src/lsp/client.ts @@ -11,6 +11,7 @@ import type { LSPServer } from "./server" import { NamedError } from "@opencode-ai/util/error" import { withTimeout } from "../util/timeout" import { Instance } from "../project/instance" +import { Filesystem } from "../util/filesystem" export namespace LSPClient { const log = Log.create({ service: "lsp.client" }) @@ -47,14 +48,15 @@ export namespace LSPClient { const diagnostics = new Map() connection.onNotification("textDocument/publishDiagnostics", (params) => { - const path = fileURLToPath(params.uri) + const filePath = Filesystem.normalizePath(fileURLToPath(params.uri)) l.info("textDocument/publishDiagnostics", { - path, + path: filePath, + count: params.diagnostics.length, }) - const exists = diagnostics.has(path) - diagnostics.set(path, params.diagnostics) + const exists = diagnostics.has(filePath) + diagnostics.set(filePath, params.diagnostics) if (!exists && input.serverID === "typescript") return - Bus.publish(Event.Diagnostics, { path, serverID: input.serverID }) + Bus.publish(Event.Diagnostics, { path: filePath, serverID: input.serverID }) }) connection.onRequest("window/workDoneProgress/create", (params) => { l.info("window/workDoneProgress/create", params) @@ -181,14 +183,16 @@ export namespace LSPClient { return diagnostics }, async waitForDiagnostics(input: { path: string }) { - input.path = path.isAbsolute(input.path) ? input.path : path.resolve(Instance.directory, input.path) - log.info("waiting for diagnostics", input) + const normalizedPath = Filesystem.normalizePath( + path.isAbsolute(input.path) ? input.path : path.resolve(Instance.directory, input.path), + ) + log.info("waiting for diagnostics", { path: normalizedPath }) let unsub: () => void return await withTimeout( new Promise((resolve) => { unsub = Bus.subscribe(Event.Diagnostics, (event) => { - if (event.properties.path === input.path && event.properties.serverID === result.serverID) { - log.info("got diagnostics", input) + if (event.properties.path === normalizedPath && event.properties.serverID === result.serverID) { + log.info("got diagnostics", { path: normalizedPath }) unsub?.() resolve() } diff --git a/packages/opencode/src/tool/edit.ts b/packages/opencode/src/tool/edit.ts index fdf115ac466..b49bd7abe00 100644 --- a/packages/opencode/src/tool/edit.ts +++ b/packages/opencode/src/tool/edit.ts @@ -140,16 +140,14 @@ export const EditTool = Tool.define("edit", { let output = "" await LSP.touchFile(filePath, true) const diagnostics = await LSP.diagnostics() - for (const [file, issues] of Object.entries(diagnostics)) { - if (issues.length === 0) continue - if (file === filePath) { - const errors = issues.filter((item) => item.severity === 1) - const limited = errors.slice(0, MAX_DIAGNOSTICS_PER_FILE) - const suffix = - errors.length > MAX_DIAGNOSTICS_PER_FILE ? `\n... and ${errors.length - MAX_DIAGNOSTICS_PER_FILE} more` : "" - output += `\nThis file has errors, please fix\n\n${limited.map(LSP.Diagnostic.pretty).join("\n")}${suffix}\n\n` - continue - } + const normalizedFilePath = Filesystem.normalizePath(filePath) + const issues = diagnostics[normalizedFilePath] ?? [] + if (issues.length > 0) { + const errors = issues.filter((item) => item.severity === 1) + const limited = errors.slice(0, MAX_DIAGNOSTICS_PER_FILE) + const suffix = + errors.length > MAX_DIAGNOSTICS_PER_FILE ? `\n... and ${errors.length - MAX_DIAGNOSTICS_PER_FILE} more` : "" + output += `\nThis file has errors, please fix\n\n${limited.map(LSP.Diagnostic.pretty).join("\n")}${suffix}\n\n` } const filediff: Snapshot.FileDiff = { diff --git a/packages/opencode/src/tool/write.ts b/packages/opencode/src/tool/write.ts index 03f2ba891a5..6b8fd3dd111 100644 --- a/packages/opencode/src/tool/write.ts +++ b/packages/opencode/src/tool/write.ts @@ -80,6 +80,7 @@ export const WriteTool = Tool.define("write", { let output = "" await LSP.touchFile(filepath, true) const diagnostics = await LSP.diagnostics() + const normalizedFilepath = Filesystem.normalizePath(filepath) let projectDiagnosticsCount = 0 for (const [file, issues] of Object.entries(diagnostics)) { if (issues.length === 0) continue @@ -87,7 +88,7 @@ export const WriteTool = Tool.define("write", { const limited = sorted.slice(0, MAX_DIAGNOSTICS_PER_FILE) const suffix = issues.length > MAX_DIAGNOSTICS_PER_FILE ? `\n... and ${issues.length - MAX_DIAGNOSTICS_PER_FILE} more` : "" - if (file === filepath) { + if (file === normalizedFilepath) { output += `\nThis file has errors, please fix\n\n${limited.map(LSP.Diagnostic.pretty).join("\n")}${suffix}\n\n` continue } diff --git a/packages/opencode/src/util/filesystem.ts b/packages/opencode/src/util/filesystem.ts index a3dcfc70367..98fbe533de3 100644 --- a/packages/opencode/src/util/filesystem.ts +++ b/packages/opencode/src/util/filesystem.ts @@ -1,7 +1,21 @@ +import { realpathSync } from "fs" import { exists } from "fs/promises" import { dirname, join, relative } from "path" export namespace Filesystem { + /** + * On Windows, normalize a path to its canonical casing using the filesystem. + * This is needed because Windows paths are case-insensitive but LSP servers + * may return paths with different casing than what we send them. + */ + export function normalizePath(p: string): string { + if (process.platform !== "win32") return p + try { + return realpathSync.native(p) + } catch { + return p + } + } export function overlaps(a: string, b: string) { const relA = relative(a, b) const relB = relative(b, a)