|
15 | 15 | package buflsp |
16 | 16 |
|
17 | 17 | import ( |
| 18 | + "net/url" |
18 | 19 | "strings" |
19 | 20 |
|
20 | 21 | "go.lsp.dev/protocol" |
21 | 22 | "go.lsp.dev/uri" |
22 | 23 | ) |
23 | 24 |
|
24 | | -// normalizeURI ensures that URIs are properly percent-encoded for LSP compatibility. |
25 | | -// |
26 | | -// The go.lsp.dev/uri package (which uses Go's net/url) follows RFC 3986 strictly and |
27 | | -// allows '@' unencoded in path components. However, VS Code's LSP client uses the |
28 | | -// microsoft/vscode-uri package which encodes '@' as '%40' everywhere to avoid ambiguity |
29 | | -// with the authority component separator (user@host). |
30 | | -// |
31 | | -// Additionally, on Windows, the package also encodes ':' as '%3A' in drive letter paths |
32 | | -// (e.g., 'file:///d:/path' becomes 'file:///d%3A/path'). |
| 25 | +// FilePathToURI converts a file path to a properly encoded URI. |
| 26 | +func FilePathToURI(path string) protocol.URI { |
| 27 | + return normalizeURI(uri.File(path)) |
| 28 | +} |
| 29 | + |
| 30 | +// normalizeURI encodes a URI to match VS Code's microsoft/vscode-uri behavior. |
33 | 31 | // |
34 | | -// When URIs don't match exactly, LSP operations like go-to-definition fail because |
35 | | -// the client's URI (with %40) doesn't match the server's URI (with @). |
| 32 | +// Go's net/url follows RFC 3986 and permits '@' and ':' unencoded in path |
| 33 | +// components (valid pchar); vscode-uri always encodes them. vscode-uri also |
| 34 | +// lowercases Windows drive letters. When URIs differ, LSP operations like |
| 35 | +// go-to-definition silently fail because the client and server URIs don't match. |
36 | 36 | func normalizeURI(u protocol.URI) protocol.URI { |
37 | | - normalized := strings.ReplaceAll(string(u), "@", "%40") |
| 37 | + str := string(u) |
38 | 38 |
|
39 | | - if after, found := strings.CutPrefix(normalized, "file:///"); found { |
40 | | - normalized = "file:///" + strings.ReplaceAll(after, ":", "%3A") |
| 39 | + after, found := strings.CutPrefix(str, "file:///") |
| 40 | + if !found { |
| 41 | + // Non-file URIs: only encode @. |
| 42 | + return protocol.URI(strings.ReplaceAll(str, "@", "%40")) |
41 | 43 | } |
42 | 44 |
|
43 | | - return protocol.URI(normalized) |
44 | | -} |
| 45 | + segments := strings.Split(after, "/") |
| 46 | + for i, segment := range segments { |
| 47 | + // Decode first to avoid double-encoding already-normalized URIs. |
| 48 | + // PathUnescape only fails on malformed sequences (e.g. %2G); falling |
| 49 | + // back to the raw segment is the best we can do. |
| 50 | + decoded, err := url.PathUnescape(segment) |
| 51 | + if err != nil { |
| 52 | + decoded = segment |
| 53 | + } |
| 54 | + // PathEscape encodes spaces as %20 (not +) and most special chars, |
| 55 | + // but permits '@' and ':' as RFC 3986 pchar. Encode those manually. |
| 56 | + encoded := url.PathEscape(decoded) |
| 57 | + encoded = strings.ReplaceAll(encoded, "@", "%40") |
| 58 | + encoded = strings.ReplaceAll(encoded, ":", "%3A") |
| 59 | + segments[i] = encoded |
| 60 | + } |
45 | 61 |
|
46 | | -// FilePathToURI converts a file path to a properly encoded URI. |
47 | | -func FilePathToURI(path string) protocol.URI { |
48 | | - return normalizeURI(uri.File(path)) |
| 62 | + // vscode-uri lowercases Windows drive letters: C%3A → c%3A. |
| 63 | + // 'A'+32 == 'a' by ASCII identity; segments[0] is e.g. "C%3A" (4 bytes). |
| 64 | + if len(segments[0]) == 4 && |
| 65 | + segments[0][0] >= 'A' && segments[0][0] <= 'Z' && |
| 66 | + segments[0][1:] == "%3A" { |
| 67 | + segments[0] = string(segments[0][0]+32) + "%3A" |
| 68 | + } |
| 69 | + |
| 70 | + return protocol.URI("file:///" + strings.Join(segments, "/")) |
49 | 71 | } |
0 commit comments