Skip to content

fix(lsp): Windows-correct file:// URI conversion (jdtls no-op on Windows) - #92

Merged
zzet merged 1 commit into
zzet:mainfrom
avfirsov:pr/lsp-windows-file-uri
Jun 15, 2026
Merged

fix(lsp): Windows-correct file:// URI conversion (jdtls no-op on Windows)#92
zzet merged 1 commit into
zzet:mainfrom
avfirsov:pr/lsp-windows-file-uri

Conversation

@avfirsov

Copy link
Copy Markdown
Contributor

What

Centralises filesystem-path ↔ file:// URI conversion in a new leaf package internal/lspuri and routes all six previously hand-rolled sites through it, fixing a Windows-only bug that made the LSP layer (jdtls etc.) a silent no-op.

The bug

The naive "file://" + path is correct on POSIX — an absolute path already starts with /, so file:// + /repo/X.java = file:///repo/X.java (the required three slashes). On Windows it breaks:

  • Outbound: C:\repo\Main.javafile://C:\repo\Main.java — the drive letter lands in the URI authority and backslashes aren't URI separators, so the server can't match the document we didOpen.
  • Inbound: the server returns file:///C:/repo/Main.java; the old uriToPath took url.Parse(...).Path = /C:/repo/Main.java and did strings.HasPrefix(absPath, repoRoot) against C:\repoalways false → the result is silently dropped.

Net effect on native Windows: jdtls is detected and initialize succeeds, but every didOpen + definition/references/implementation round-trip no-ops — zero Java edges get enriched (ast_inferredlsp_resolved), with no error logged. POSIX/macOS were unaffected, so it went unnoticed.

Fix

New internal/lspuri:

  • PathToURIC:\repo\Main.javafile:///C:/repo/Main.java; /repo/Main.javafile:///repo/Main.java (ToSlash, drive leading-slash, %xx-safe via net/url).
  • URIToAbsPath — inverse, strips the spurious leading slash before a drive letter, FromSlash, decodes %xx.
  • URIToRepoRel — uses filepath.Rel (case-insensitive on Windows) instead of a string prefix, so drive-casing / separators never cause a false miss.

Routed through it (all had the same bug class):

  • internal/semantic/lsp/provider.gopathToURI, uriToAbsPath, uriToPath
  • internal/semantic/lsp/resolver_helper.gouriToAbsLocalPath
  • internal/mcp/diagnostics.gopathToFileURI
  • internal/mcp/tools_simulate.gonormaliseEditURI (now also decodes %xx and drops the host via url)

Tests

internal/lspuri unit tests exercise the drive-letter / separator logic via pure helpers, so the Windows behaviour is verified on the Linux/macOS CI runners (Windows only builds in CI, it doesn't run the suite). go build ./..., go vet, internal/lspuri and internal/mcp suites pass. (Pre-existing unrelated failure TestPassiveProvider_DialFailsNoFallback reproduces on main without this change — it needs a live LSP server.)

Follow-up (not in this PR)

If jdtls still under-resolves after this, the next suspects are async readiness (no wait for jdtls workspace-import to finish before querying) and Java-project recognition (no pom.xml/build.gradle ⇒ no classpath).

…n Windows)

The naive `"file://" + path` is correct on POSIX (an absolute path
already starts with /, giving three slashes) but broken on Windows:
C:\repo\Main.java becomes file://C:\repo\Main.java — the drive lands
in the URI authority and backslashes are not separators. Inbound, the
returned URI's path is /C:/repo/Main.java and a strings.HasPrefix against
the C:\repo repoRoot always fails, so every LSP result is silently
dropped.

Net effect on native Windows: jdtls (and any LSP server) is detected and
initialized, but every textDocument/didOpen + definition/references/
implementation round-trip no-ops — zero Java edges get enriched
(ast_inferred -> lsp_resolved). POSIX was unaffected, so it went unnoticed.

Centralise all path<->file-URI conversion in a new leaf package
internal/lspuri (PathToURI / URIToAbsPath / URIToRepoRel), correct for
both POSIX and Windows (drive-letter leading-slash, ToSlash/FromSlash,
filepath.Rel instead of string prefix, %xx decode). Route all six
previously-buggy sites through it:
- internal/semantic/lsp/provider.go: pathToURI, uriToAbsPath, uriToPath
- internal/semantic/lsp/resolver_helper.go: uriToAbsLocalPath
- internal/mcp/diagnostics.go: pathToFileURI
- internal/mcp/tools_simulate.go: normaliseEditURI

The drive-letter / separator logic is unit-tested via pure helpers so the
Windows path is exercised on the Linux/macOS CI runners (Windows only
builds in CI).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@zzet
zzet merged commit 51d976b into zzet:main Jun 15, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants