package lsp import ( "net/url" "path/filepath" "strings" ) // PathToURI turns a file path into the file:// URI the protocol names // documents by. // // The path is made canonical first — absolute, with symbolic links resolved — // since a language server has no idea what the editor's working directory is, // and some resolve links themselves: see CanonicalPath. // // lsp.PathToURI("main.go") // file:///home/you/project/main.go func PathToURI(path string) string { absolute := filepath.ToSlash(CanonicalPath(path)) if !strings.HasPrefix(absolute, "/") { // A Windows path such as C:/x becomes /C:/x, which is what the URI // form of a drive letter looks like. absolute = "/" + absolute } uri := url.URL{Scheme: "file", Path: absolute} return uri.String() } // CanonicalPath is the one spelling of a file this package names it by: // absolute, with every symbolic link resolved. // // Resolving the links is not tidiness. moon-lsp works a package's files out // from disk and canonicalises what it finds, so a document announced under // another spelling of the same path is, to it, a file that belongs to no // package: it answers no completion about the buffer's types, and it publishes // the file's diagnostics under the spelling it knows — which the editor then // cannot match to any open buffer. On macOS every temporary directory is such // a spelling: /var is a link to /private/var, and the Turbo MoonBit suite, // green on Linux, failed both of those ways the first time it ran on a Mac. // // A file that is not on disk yet — a buffer being saved under a new name — has // nothing to resolve, so its deepest existing directory is resolved instead and // the rest of the path put back on. A path that cannot be made absolute is // returned as it is, which degrades to the behaviour there was before. func CanonicalPath(path string) string { absolute, err := filepath.Abs(path) if err != nil { return path } if resolved, err := filepath.EvalSymlinks(absolute); err == nil { return resolved } dir, rest := filepath.Dir(absolute), filepath.Base(absolute) for dir != filepath.Dir(dir) { if resolved, err := filepath.EvalSymlinks(dir); err == nil { return filepath.Join(resolved, rest) } dir, rest = filepath.Dir(dir), filepath.Join(filepath.Base(dir), rest) } return absolute } // URIToPath turns a file:// URI back into a path. Anything that is not a file // URI comes back unchanged, since there is nothing better to do with it. func URIToPath(uri string) string { parsed, err := url.Parse(uri) if err != nil || parsed.Scheme != "file" { return uri } path := parsed.Path // Undo the leading slash added in front of a Windows drive letter. if len(path) > 2 && path[0] == '/' && path[2] == ':' { path = path[1:] } return filepath.FromSlash(path) }