a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
122 lines
3.9 KiB
Go
122 lines
3.9 KiB
Go
// Package lspuri converts between filesystem paths and file:// URIs
|
|
// correctly on both POSIX and Windows.
|
|
//
|
|
// PURPOSE — the naive `"file://" + path` concatenation is correct on
|
|
// POSIX (an absolute path already starts with `/`, yielding the required
|
|
// three slashes) but BROKEN on Windows: `C:\repo\Main.java` becomes
|
|
// `file://C:\repo\Main.java` — the drive letter lands in the authority,
|
|
// backslashes are not URI separators, and the round-trip back to a path
|
|
// drops the drive. The net effect on Windows is a silent no-op: an LSP
|
|
// server (jdtls, etc.) cannot match the document we open, and the URIs it
|
|
// returns fail to map back to a repo path, so every definition/reference/
|
|
// implementation result is discarded and no edge is ever enriched.
|
|
//
|
|
// RATIONALE — centralise the conversion so didOpen, position requests,
|
|
// and result fold-back all agree on URI shape, and so the Windows-specific
|
|
// drive-letter / separator handling lives (and is tested) in exactly one
|
|
// place rather than being re-derived at each call site.
|
|
//
|
|
// KEYWORDS — file-uri, windows, drive-letter, lsp, path-conversion
|
|
package lspuri
|
|
|
|
import (
|
|
"net/url"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// PathToURI converts a filesystem path to a file:// URI.
|
|
//
|
|
// POSIX: /repo/Main.java -> file:///repo/Main.java
|
|
// Windows: C:\repo\Main.java -> file:///C:/repo/Main.java
|
|
//
|
|
// Relative paths are made absolute first. Returns "" for an empty path.
|
|
func PathToURI(path string) string {
|
|
if path == "" {
|
|
return ""
|
|
}
|
|
abs, err := filepath.Abs(path)
|
|
if err != nil {
|
|
abs = path
|
|
}
|
|
return slashAbsToURI(filepath.ToSlash(abs))
|
|
}
|
|
|
|
// URIToAbsPath converts a file:// URI to an OS-native absolute path.
|
|
//
|
|
// file:///C:/repo/Main.java -> C:\repo\Main.java (Windows) / C:/repo/Main.java (POSIX runtime)
|
|
// file:///repo/Main.java -> /repo/Main.java
|
|
//
|
|
// Returns "" for non-file or unparseable URIs. URL-encoded octets
|
|
// (e.g. %20) are decoded.
|
|
func URIToAbsPath(uri string) string {
|
|
slash := uriToSlashAbs(uri)
|
|
if slash == "" {
|
|
return ""
|
|
}
|
|
return filepath.FromSlash(slash)
|
|
}
|
|
|
|
// URIToRepoRel converts a file:// URI to a forward-slash path relative to
|
|
// repoRoot, or "" when the URI is outside repoRoot or unparseable.
|
|
//
|
|
// Uses filepath.Rel (case-insensitive on Windows) rather than a string
|
|
// prefix so drive-letter casing and separator differences never cause a
|
|
// false miss — the failure mode that made LSP fold-back silently drop
|
|
// every result on Windows.
|
|
func URIToRepoRel(uri, repoRoot string) string {
|
|
abs := URIToAbsPath(uri)
|
|
if abs == "" || repoRoot == "" {
|
|
return ""
|
|
}
|
|
rel, err := filepath.Rel(repoRoot, abs)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
|
|
return "" // outside the repo
|
|
}
|
|
return filepath.ToSlash(rel)
|
|
}
|
|
|
|
// slashAbsToURI builds a file URI from a forward-slash absolute path.
|
|
// Pure (no filepath / OS dependency) so it is deterministically testable
|
|
// for both POSIX ("/repo/x") and Windows-drive ("C:/repo/x") shapes.
|
|
func slashAbsToURI(slashAbs string) string {
|
|
if slashAbs == "" {
|
|
return ""
|
|
}
|
|
if !strings.HasPrefix(slashAbs, "/") {
|
|
slashAbs = "/" + slashAbs // C:/repo -> /C:/repo so the URI gets file:///C:/repo
|
|
}
|
|
return (&url.URL{Scheme: "file", Path: slashAbs}).String()
|
|
}
|
|
|
|
// uriToSlashAbs extracts the forward-slash absolute path from a file URI.
|
|
// Pure; testable for both shapes. Strips the spurious leading slash that
|
|
// precedes a Windows drive letter (/C:/.. -> C:/..).
|
|
func uriToSlashAbs(uri string) string {
|
|
if uri == "" {
|
|
return ""
|
|
}
|
|
parsed, err := url.Parse(uri)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
if parsed.Scheme != "" && parsed.Scheme != "file" {
|
|
return ""
|
|
}
|
|
p := parsed.Path
|
|
if p == "" {
|
|
p = parsed.Opaque // tolerate `file:relative` shapes
|
|
}
|
|
if len(p) >= 3 && p[0] == '/' && isDriveLetter(p[1]) && p[2] == ':' {
|
|
p = p[1:] // /C:/repo -> C:/repo
|
|
}
|
|
return p
|
|
}
|
|
|
|
func isDriveLetter(c byte) bool {
|
|
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')
|
|
}
|