summaryrefslogtreecommitdiffhomepage
path: root/internal/lsp/protocol/uri.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/lsp/protocol/uri.go')
-rw-r--r--internal/lsp/protocol/uri.go218
1 files changed, 218 insertions, 0 deletions
diff --git a/internal/lsp/protocol/uri.go b/internal/lsp/protocol/uri.go
new file mode 100644
index 000000000..18fd5ea75
--- /dev/null
+++ b/internal/lsp/protocol/uri.go
@@ -0,0 +1,218 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package protocol
+
+// This file declares URI, DocumentUri, and its methods.
+//
+// For the LSP definition of these types, see
+// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#uri
+
+import (
+ "fmt"
+ "net/url"
+ "path/filepath"
+ "strings"
+ "unicode"
+)
+
+// A DocumentUri is the URI of a client editor document.
+//
+// According to the LSP specification:
+//
+// Care should be taken to handle encoding in URIs. For
+// example, some clients (such as VS Code) may encode colons
+// in drive letters while others do not. The URIs below are
+// both valid, but clients and servers should be consistent
+// with the form they use themselves to ensure the other party
+// doesn’t interpret them as distinct URIs. Clients and
+// servers should not assume that each other are encoding the
+// same way (for example a client encoding colons in drive
+// letters cannot assume server responses will have encoded
+// colons). The same applies to casing of drive letters - one
+// party should not assume the other party will return paths
+// with drive letters cased the same as it.
+//
+// file:///c:/project/readme.md
+// file:///C%3A/project/readme.md
+//
+// This is done during JSON unmarshalling;
+// see [DocumentUri.UnmarshalText] for details.
+type DocumentUri string
+
+// A URI is an arbitrary URL (e.g. https), not necessarily a file.
+type URI = string
+
+// UnmarshalText implements decoding of DocumentUri values.
+//
+// In particular, it implements a systematic correction of various odd
+// features of the definition of DocumentUri in the LSP spec that
+// appear to be workarounds for bugs in VS Code. For example, it may
+// URI-encode the URI itself, so that colon becomes %3A, and it may
+// send file://foo.go URIs that have two slashes (not three) and no
+// hostname.
+//
+// We use UnmarshalText, not UnmarshalJSON, because it is called even
+// for non-addressable values such as keys and values of map[K]V,
+// where there is no pointer of type *K or *V on which to call
+// UnmarshalJSON. (See Go issue #28189 for more detail.)
+//
+// Non-empty DocumentUris are valid "file"-scheme URIs.
+// The empty DocumentUri is valid.
+func (uri *DocumentUri) UnmarshalText(data []byte) (err error) {
+ *uri, err = ParseDocumentUri(string(data))
+ return
+}
+
+// Path returns the file path for the given URI.
+//
+// DocumentUri("").Path() returns the empty string.
+//
+// Path panics if called on a URI that is not a valid filename.
+func (uri DocumentUri) Path() string {
+ filename, err := filename(uri)
+ if err != nil {
+ // e.g. ParseRequestURI failed.
+ //
+ // This can only affect DocumentUris created by
+ // direct string manipulation; all DocumentUris
+ // received from the client pass through
+ // ParseRequestURI, which ensures validity.
+ panic(err)
+ }
+ return filepath.FromSlash(filename)
+}
+
+// Dir returns the URI for the directory containing the receiver.
+func (uri DocumentUri) Dir() DocumentUri {
+ // This function could be more efficiently implemented by avoiding any call
+ // to Path(), but at least consolidates URI manipulation.
+ return URIFromPath(uri.DirPath())
+}
+
+// DirPath returns the file path to the directory containing this URI, which
+// must be a file URI.
+func (uri DocumentUri) DirPath() string {
+ return filepath.Dir(uri.Path())
+}
+
+func filename(uri DocumentUri) (string, error) {
+ if uri == "" {
+ return "", nil
+ }
+
+ // This conservative check for the common case
+ // of a simple non-empty absolute POSIX filename
+ // avoids the allocation of a net.URL.
+ if strings.HasPrefix(string(uri), "file:///") {
+ rest := string(uri)[len("file://"):] // leave one slash
+ for i := range len(rest) {
+ b := rest[i]
+ // Reject these cases:
+ if b < ' ' || b == 0x7f || // control character
+ b == '%' || b == '+' || // URI escape
+ b == ':' || // Windows drive letter
+ b == '@' || b == '&' || b == '?' { // authority or query
+ goto slow
+ }
+ }
+ return rest, nil
+ }
+slow:
+
+ u, err := url.ParseRequestURI(string(uri))
+ if err != nil {
+ return "", err
+ }
+ if u.Scheme != fileScheme {
+ return "", fmt.Errorf("only file URIs are supported, got %q from %q", u.Scheme, uri)
+ }
+ // If the URI is a Windows URI, we trim the leading "/" and uppercase
+ // the drive letter, which will never be case sensitive.
+ if isWindowsDriveURIPath(u.Path) {
+ u.Path = strings.ToUpper(string(u.Path[1])) + u.Path[2:]
+ }
+
+ return u.Path, nil
+}
+
+// ParseDocumentUri interprets a string as a DocumentUri, applying VS
+// Code workarounds; see [DocumentUri.UnmarshalText] for details.
+func ParseDocumentUri(s string) (DocumentUri, error) {
+ if s == "" {
+ return "", nil
+ }
+
+ if !strings.HasPrefix(s, "file://") {
+ return "", fmt.Errorf("DocumentUri scheme is not 'file': %s", s)
+ }
+
+ // VS Code sends URLs with only two slashes,
+ // which are invalid. golang/go#39789.
+ if !strings.HasPrefix(s, "file:///") {
+ s = "file:///" + s[len("file://"):]
+ }
+
+ // Even though the input is a URI, it may not be in canonical form. VS Code
+ // in particular over-escapes :, @, etc. Unescape and re-encode to canonicalize.
+ path, err := url.PathUnescape(s[len("file://"):])
+ if err != nil {
+ return "", err
+ }
+
+ // File URIs from Windows may have lowercase drive letters.
+ // Since drive letters are guaranteed to be case insensitive,
+ // we change them to uppercase to remain consistent.
+ // For example, file:///c:/x/y/z becomes file:///C:/x/y/z.
+ if isWindowsDriveURIPath(path) {
+ path = path[:1] + strings.ToUpper(string(path[1])) + path[2:]
+ }
+ u := url.URL{Scheme: fileScheme, Path: path}
+ return DocumentUri(u.String()), nil
+}
+
+// URIFromPath returns DocumentUri for the supplied file path.
+// Given "", it returns "".
+func URIFromPath(path string) DocumentUri {
+ if path == "" {
+ return ""
+ }
+ if !isWindowsDrivePath(path) {
+ if abs, err := filepath.Abs(path); err == nil {
+ path = abs
+ }
+ }
+ // Check the file path again, in case it became absolute.
+ if isWindowsDrivePath(path) {
+ path = "/" + strings.ToUpper(string(path[0])) + path[1:]
+ }
+ path = filepath.ToSlash(path)
+ u := url.URL{
+ Scheme: fileScheme,
+ Path: path,
+ }
+ return DocumentUri(u.String())
+}
+
+const fileScheme = "file"
+
+// isWindowsDrivePath returns true if the file path is of the form used by
+// Windows. We check if the path begins with a drive letter, followed by a ":".
+// For example: C:/x/y/z.
+func isWindowsDrivePath(path string) bool {
+ if len(path) < 3 {
+ return false
+ }
+ return unicode.IsLetter(rune(path[0])) && path[1] == ':'
+}
+
+// isWindowsDriveURIPath returns true if the file URI is of the format used by
+// Windows URIs. The url.Parse package does not specially handle Windows paths
+// (see golang/go#6027), so we check if the URI path has a drive prefix (e.g. "/C:").
+func isWindowsDriveURIPath(uri string) bool {
+ if len(uri) < 4 {
+ return false
+ }
+ return uri[0] == '/' && unicode.IsLetter(rune(uri[1])) && uri[2] == ':'
+}