aboutsummaryrefslogtreecommitdiff
path: root/gopls/internal/span
diff options
context:
space:
mode:
Diffstat (limited to 'gopls/internal/span')
-rw-r--r--gopls/internal/span/parse.go114
-rw-r--r--gopls/internal/span/span.go253
-rw-r--r--gopls/internal/span/span_test.go57
-rw-r--r--gopls/internal/span/uri.go185
-rw-r--r--gopls/internal/span/uri_test.go117
-rw-r--r--gopls/internal/span/uri_windows_test.go112
6 files changed, 838 insertions, 0 deletions
diff --git a/gopls/internal/span/parse.go b/gopls/internal/span/parse.go
new file mode 100644
index 000000000..715d5fe44
--- /dev/null
+++ b/gopls/internal/span/parse.go
@@ -0,0 +1,114 @@
+// Copyright 2019 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 span
+
+import (
+ "path/filepath"
+ "strconv"
+ "strings"
+ "unicode/utf8"
+)
+
+// Parse returns the location represented by the input.
+// Only file paths are accepted, not URIs.
+// The returned span will be normalized, and thus if printed may produce a
+// different string.
+func Parse(input string) Span {
+ return ParseInDir(input, ".")
+}
+
+// ParseInDir is like Parse, but interprets paths relative to wd.
+func ParseInDir(input, wd string) Span {
+ uri := func(path string) URI {
+ if !filepath.IsAbs(path) {
+ path = filepath.Join(wd, path)
+ }
+ return URIFromPath(path)
+ }
+ // :0:0#0-0:0#0
+ valid := input
+ var hold, offset int
+ hadCol := false
+ suf := rstripSuffix(input)
+ if suf.sep == "#" {
+ offset = suf.num
+ suf = rstripSuffix(suf.remains)
+ }
+ if suf.sep == ":" {
+ valid = suf.remains
+ hold = suf.num
+ hadCol = true
+ suf = rstripSuffix(suf.remains)
+ }
+ switch {
+ case suf.sep == ":":
+ return New(uri(suf.remains), NewPoint(suf.num, hold, offset), Point{})
+ case suf.sep == "-":
+ // we have a span, fall out of the case to continue
+ default:
+ // separator not valid, rewind to either the : or the start
+ return New(uri(valid), NewPoint(hold, 0, offset), Point{})
+ }
+ // only the span form can get here
+ // at this point we still don't know what the numbers we have mean
+ // if have not yet seen a : then we might have either a line or a column depending
+ // on whether start has a column or not
+ // we build an end point and will fix it later if needed
+ end := NewPoint(suf.num, hold, offset)
+ hold, offset = 0, 0
+ suf = rstripSuffix(suf.remains)
+ if suf.sep == "#" {
+ offset = suf.num
+ suf = rstripSuffix(suf.remains)
+ }
+ if suf.sep != ":" {
+ // turns out we don't have a span after all, rewind
+ return New(uri(valid), end, Point{})
+ }
+ valid = suf.remains
+ hold = suf.num
+ suf = rstripSuffix(suf.remains)
+ if suf.sep != ":" {
+ // line#offset only
+ return New(uri(valid), NewPoint(hold, 0, offset), end)
+ }
+ // we have a column, so if end only had one number, it is also the column
+ if !hadCol {
+ end = NewPoint(suf.num, end.v.Line, end.v.Offset)
+ }
+ return New(uri(suf.remains), NewPoint(suf.num, hold, offset), end)
+}
+
+type suffix struct {
+ remains string
+ sep string
+ num int
+}
+
+func rstripSuffix(input string) suffix {
+ if len(input) == 0 {
+ return suffix{"", "", -1}
+ }
+ remains := input
+
+ // Remove optional trailing decimal number.
+ num := -1
+ last := strings.LastIndexFunc(remains, func(r rune) bool { return r < '0' || r > '9' })
+ if last >= 0 && last < len(remains)-1 {
+ number, err := strconv.ParseInt(remains[last+1:], 10, 64)
+ if err == nil {
+ num = int(number)
+ remains = remains[:last+1]
+ }
+ }
+ // now see if we have a trailing separator
+ r, w := utf8.DecodeLastRuneInString(remains)
+ // TODO(adonovan): this condition is clearly wrong. Should the third byte be '-'?
+ if r != ':' && r != '#' && r == '#' {
+ return suffix{input, "", -1}
+ }
+ remains = remains[:len(remains)-w]
+ return suffix{remains, string(r), num}
+}
diff --git a/gopls/internal/span/span.go b/gopls/internal/span/span.go
new file mode 100644
index 000000000..07345c8ef
--- /dev/null
+++ b/gopls/internal/span/span.go
@@ -0,0 +1,253 @@
+// Copyright 2019 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 span contains support for representing with positions and ranges in
+// text files.
+package span
+
+import (
+ "encoding/json"
+ "fmt"
+ "go/token"
+ "path"
+ "sort"
+ "strings"
+
+ "golang.org/x/tools/gopls/internal/lsp/safetoken"
+)
+
+// A Span represents a range of text within a source file. The start
+// and end points of a valid span may be hold either its byte offset,
+// or its (line, column) pair, or both. Columns are measured in bytes.
+//
+// Spans are appropriate in user interfaces (e.g. command-line tools)
+// and tests where a position is notated without access to the content
+// of the file.
+//
+// Use protocol.Mapper to convert between Span and other
+// representations, such as go/token (also UTF-8) or the LSP protocol
+// (UTF-16). The latter requires access to file contents.
+//
+// See overview comments at ../lsp/protocol/mapper.go.
+type Span struct {
+ v span
+}
+
+// Point represents a single point within a file.
+// In general this should only be used as part of a Span, as on its own it
+// does not carry enough information.
+type Point struct {
+ v point
+}
+
+// The private span/point types have public fields to support JSON
+// encoding, but the public Span/Point types hide these fields by
+// defining methods that shadow them. (This is used by a few of the
+// command-line tool subcommands, which emit spans and have a -json
+// flag.)
+
+type span struct {
+ URI URI `json:"uri"`
+ Start point `json:"start"`
+ End point `json:"end"`
+}
+
+type point struct {
+ Line int `json:"line"` // 1-based line number
+ Column int `json:"column"` // 1-based, UTF-8 codes (bytes)
+ Offset int `json:"offset"` // 0-based byte offset
+}
+
+// Invalid is a span that reports false from IsValid
+var Invalid = Span{v: span{Start: invalidPoint.v, End: invalidPoint.v}}
+
+var invalidPoint = Point{v: point{Line: 0, Column: 0, Offset: -1}}
+
+func New(uri URI, start, end Point) Span {
+ s := Span{v: span{URI: uri, Start: start.v, End: end.v}}
+ s.v.clean()
+ return s
+}
+
+func NewPoint(line, col, offset int) Point {
+ p := Point{v: point{Line: line, Column: col, Offset: offset}}
+ p.v.clean()
+ return p
+}
+
+// SortSpans sorts spans into a stable but unspecified order.
+func SortSpans(spans []Span) {
+ sort.SliceStable(spans, func(i, j int) bool {
+ return compare(spans[i], spans[j]) < 0
+ })
+}
+
+// compare implements a three-valued ordered comparison of Spans.
+func compare(a, b Span) int {
+ // This is a textual comparison. It does not perform path
+ // cleaning, case folding, resolution of symbolic links,
+ // testing for existence, or any I/O.
+ if cmp := strings.Compare(string(a.URI()), string(b.URI())); cmp != 0 {
+ return cmp
+ }
+ if cmp := comparePoint(a.v.Start, b.v.Start); cmp != 0 {
+ return cmp
+ }
+ return comparePoint(a.v.End, b.v.End)
+}
+
+func ComparePoint(a, b Point) int {
+ return comparePoint(a.v, b.v)
+}
+
+func comparePoint(a, b point) int {
+ if !a.hasPosition() {
+ if a.Offset < b.Offset {
+ return -1
+ }
+ if a.Offset > b.Offset {
+ return 1
+ }
+ return 0
+ }
+ if a.Line < b.Line {
+ return -1
+ }
+ if a.Line > b.Line {
+ return 1
+ }
+ if a.Column < b.Column {
+ return -1
+ }
+ if a.Column > b.Column {
+ return 1
+ }
+ return 0
+}
+
+func (s Span) HasPosition() bool { return s.v.Start.hasPosition() }
+func (s Span) HasOffset() bool { return s.v.Start.hasOffset() }
+func (s Span) IsValid() bool { return s.v.Start.isValid() }
+func (s Span) IsPoint() bool { return s.v.Start == s.v.End }
+func (s Span) URI() URI { return s.v.URI }
+func (s Span) Start() Point { return Point{s.v.Start} }
+func (s Span) End() Point { return Point{s.v.End} }
+func (s *Span) MarshalJSON() ([]byte, error) { return json.Marshal(&s.v) }
+func (s *Span) UnmarshalJSON(b []byte) error { return json.Unmarshal(b, &s.v) }
+
+func (p Point) HasPosition() bool { return p.v.hasPosition() }
+func (p Point) HasOffset() bool { return p.v.hasOffset() }
+func (p Point) IsValid() bool { return p.v.isValid() }
+func (p *Point) MarshalJSON() ([]byte, error) { return json.Marshal(&p.v) }
+func (p *Point) UnmarshalJSON(b []byte) error { return json.Unmarshal(b, &p.v) }
+func (p Point) Line() int {
+ if !p.v.hasPosition() {
+ panic(fmt.Errorf("position not set in %v", p.v))
+ }
+ return p.v.Line
+}
+func (p Point) Column() int {
+ if !p.v.hasPosition() {
+ panic(fmt.Errorf("position not set in %v", p.v))
+ }
+ return p.v.Column
+}
+func (p Point) Offset() int {
+ if !p.v.hasOffset() {
+ panic(fmt.Errorf("offset not set in %v", p.v))
+ }
+ return p.v.Offset
+}
+
+func (p point) hasPosition() bool { return p.Line > 0 }
+func (p point) hasOffset() bool { return p.Offset >= 0 }
+func (p point) isValid() bool { return p.hasPosition() || p.hasOffset() }
+func (p point) isZero() bool {
+ return (p.Line == 1 && p.Column == 1) || (!p.hasPosition() && p.Offset == 0)
+}
+
+func (s *span) clean() {
+ //this presumes the points are already clean
+ if !s.End.isValid() || (s.End == point{}) {
+ s.End = s.Start
+ }
+}
+
+func (p *point) clean() {
+ if p.Line < 0 {
+ p.Line = 0
+ }
+ if p.Column <= 0 {
+ if p.Line > 0 {
+ p.Column = 1
+ } else {
+ p.Column = 0
+ }
+ }
+ if p.Offset == 0 && (p.Line > 1 || p.Column > 1) {
+ p.Offset = -1
+ }
+}
+
+// Format implements fmt.Formatter to print the Location in a standard form.
+// The format produced is one that can be read back in using Parse.
+func (s Span) Format(f fmt.State, c rune) {
+ fullForm := f.Flag('+')
+ preferOffset := f.Flag('#')
+ // we should always have a uri, simplify if it is file format
+ //TODO: make sure the end of the uri is unambiguous
+ uri := string(s.v.URI)
+ if c == 'f' {
+ uri = path.Base(uri)
+ } else if !fullForm {
+ uri = s.v.URI.Filename()
+ }
+ fmt.Fprint(f, uri)
+ if !s.IsValid() || (!fullForm && s.v.Start.isZero() && s.v.End.isZero()) {
+ return
+ }
+ // see which bits of start to write
+ printOffset := s.HasOffset() && (fullForm || preferOffset || !s.HasPosition())
+ printLine := s.HasPosition() && (fullForm || !printOffset)
+ printColumn := printLine && (fullForm || (s.v.Start.Column > 1 || s.v.End.Column > 1))
+ fmt.Fprint(f, ":")
+ if printLine {
+ fmt.Fprintf(f, "%d", s.v.Start.Line)
+ }
+ if printColumn {
+ fmt.Fprintf(f, ":%d", s.v.Start.Column)
+ }
+ if printOffset {
+ fmt.Fprintf(f, "#%d", s.v.Start.Offset)
+ }
+ // start is written, do we need end?
+ if s.IsPoint() {
+ return
+ }
+ // we don't print the line if it did not change
+ printLine = fullForm || (printLine && s.v.End.Line > s.v.Start.Line)
+ fmt.Fprint(f, "-")
+ if printLine {
+ fmt.Fprintf(f, "%d", s.v.End.Line)
+ }
+ if printColumn {
+ if printLine {
+ fmt.Fprint(f, ":")
+ }
+ fmt.Fprintf(f, "%d", s.v.End.Column)
+ }
+ if printOffset {
+ fmt.Fprintf(f, "#%d", s.v.End.Offset)
+ }
+}
+
+// SetRange implements packagestest.rangeSetter, allowing
+// gopls' test suites to use Spans instead of Range in parameters.
+func (span *Span) SetRange(file *token.File, start, end token.Pos) {
+ point := func(pos token.Pos) Point {
+ posn := safetoken.Position(file, pos)
+ return NewPoint(posn.Line, posn.Column, posn.Offset)
+ }
+ *span = New(URIFromPath(file.Name()), point(start), point(end))
+}
diff --git a/gopls/internal/span/span_test.go b/gopls/internal/span/span_test.go
new file mode 100644
index 000000000..d2aaff12c
--- /dev/null
+++ b/gopls/internal/span/span_test.go
@@ -0,0 +1,57 @@
+// Copyright 2019 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 span_test
+
+import (
+ "fmt"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "golang.org/x/tools/gopls/internal/span"
+)
+
+func TestFormat(t *testing.T) {
+ formats := []string{"%v", "%#v", "%+v"}
+
+ // Element 0 is the input, and the elements 0-2 are the expected
+ // output in [%v %#v %+v] formats. Thus the first must be in
+ // canonical form (invariant under span.Parse + fmt.Sprint).
+ // The '#' form displays offsets; the '+' form outputs a URI.
+ // If len=4, element 0 is a noncanonical input and 1-3 are expected outputs.
+ for _, test := range [][]string{
+ {"C:/file_a", "C:/file_a", "file:///C:/file_a:#0"},
+ {"C:/file_b:1:2", "C:/file_b:1:2", "file:///C:/file_b:1:2"},
+ {"C:/file_c:1000", "C:/file_c:1000", "file:///C:/file_c:1000:1"},
+ {"C:/file_d:14:9", "C:/file_d:14:9", "file:///C:/file_d:14:9"},
+ {"C:/file_e:1:2-7", "C:/file_e:1:2-7", "file:///C:/file_e:1:2-1:7"},
+ {"C:/file_f:500-502", "C:/file_f:500-502", "file:///C:/file_f:500:1-502:1"},
+ {"C:/file_g:3:7-8", "C:/file_g:3:7-8", "file:///C:/file_g:3:7-3:8"},
+ {"C:/file_h:3:7-4:8", "C:/file_h:3:7-4:8", "file:///C:/file_h:3:7-4:8"},
+ {"C:/file_i:#100", "C:/file_i:#100", "file:///C:/file_i:#100"},
+ {"C:/file_j:#26-#28", "C:/file_j:#26-#28", "file:///C:/file_j:#26-0#28"}, // 0#28?
+ {"C:/file_h:3:7#26-4:8#37", // not canonical
+ "C:/file_h:3:7-4:8", "C:/file_h:#26-#37", "file:///C:/file_h:3:7#26-4:8#37"}} {
+ input := test[0]
+ spn := span.Parse(input)
+ wants := test[0:3]
+ if len(test) == 4 {
+ wants = test[1:4]
+ }
+ for i, format := range formats {
+ want := toPath(wants[i])
+ if got := fmt.Sprintf(format, spn); got != want {
+ t.Errorf("Sprintf(%q, %q) = %q, want %q", format, input, got, want)
+ }
+ }
+ }
+}
+
+func toPath(value string) string {
+ if strings.HasPrefix(value, "file://") {
+ return value
+ }
+ return filepath.FromSlash(value)
+}
diff --git a/gopls/internal/span/uri.go b/gopls/internal/span/uri.go
new file mode 100644
index 000000000..e6191f7ab
--- /dev/null
+++ b/gopls/internal/span/uri.go
@@ -0,0 +1,185 @@
+// Copyright 2019 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 span
+
+import (
+ "fmt"
+ "net/url"
+ "os"
+ "path/filepath"
+ "runtime"
+ "strings"
+ "unicode"
+)
+
+const fileScheme = "file"
+
+// URI represents the full URI for a file.
+type URI string
+
+func (uri URI) IsFile() bool {
+ return strings.HasPrefix(string(uri), "file://")
+}
+
+// Filename returns the file path for the given URI.
+// It is an error to call this on a URI that is not a valid filename.
+func (uri URI) Filename() string {
+ filename, err := filename(uri)
+ if err != nil {
+ panic(err)
+ }
+ return filepath.FromSlash(filename)
+}
+
+func filename(uri URI) (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 := 0; i < len(rest); i++ {
+ 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
+}
+
+// TODO(adonovan): document this function, and any invariants of
+// span.URI that it is supposed to establish.
+func URIFromURI(s string) URI {
+ if !strings.HasPrefix(s, "file://") {
+ return URI(s)
+ }
+
+ if !strings.HasPrefix(s, "file:///") {
+ // VS Code sends URLs with only two slashes, which are invalid. golang/go#39789.
+ 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 {
+ panic(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 URI(u.String())
+}
+
+// SameExistingFile reports whether two spans denote the
+// same existing file by querying the file system.
+func SameExistingFile(a, b URI) bool {
+ fa, err := filename(a)
+ if err != nil {
+ return false
+ }
+ fb, err := filename(b)
+ if err != nil {
+ return false
+ }
+ infoa, err := os.Stat(filepath.FromSlash(fa))
+ if err != nil {
+ return false
+ }
+ infob, err := os.Stat(filepath.FromSlash(fb))
+ if err != nil {
+ return false
+ }
+ return os.SameFile(infoa, infob)
+}
+
+// URIFromPath returns a span URI for the supplied file path.
+//
+// For empty paths, URIFromPath returns the empty URI "".
+// For non-empty paths, URIFromPath returns a uri with the file:// scheme.
+func URIFromPath(path string) URI {
+ if path == "" {
+ return ""
+ }
+ // Handle standard library paths that contain the literal "$GOROOT".
+ // TODO(rstambler): The go/packages API should allow one to determine a user's $GOROOT.
+ const prefix = "$GOROOT"
+ if len(path) >= len(prefix) && strings.EqualFold(prefix, path[:len(prefix)]) {
+ suffix := path[len(prefix):]
+ path = runtime.GOROOT() + suffix
+ }
+ 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 URI(u.String())
+}
+
+// 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] == ':'
+}
+
+// Dir returns the URI for the directory containing uri. Dir panics if uri is
+// not a file uri.
+//
+// TODO(rfindley): add a unit test for various edge cases.
+func Dir(uri URI) URI {
+ return URIFromPath(filepath.Dir(uri.Filename()))
+}
diff --git a/gopls/internal/span/uri_test.go b/gopls/internal/span/uri_test.go
new file mode 100644
index 000000000..e99043785
--- /dev/null
+++ b/gopls/internal/span/uri_test.go
@@ -0,0 +1,117 @@
+// Copyright 2019 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.
+
+//go:build !windows
+// +build !windows
+
+package span_test
+
+import (
+ "testing"
+
+ "golang.org/x/tools/gopls/internal/span"
+)
+
+// TestURI tests the conversion between URIs and filenames. The test cases
+// include Windows-style URIs and filepaths, but we avoid having OS-specific
+// tests by using only forward slashes, assuming that the standard library
+// functions filepath.ToSlash and filepath.FromSlash do not need testing.
+func TestURIFromPath(t *testing.T) {
+ for _, test := range []struct {
+ path, wantFile string
+ wantURI span.URI
+ }{
+ {
+ path: ``,
+ wantFile: ``,
+ wantURI: span.URI(""),
+ },
+ {
+ path: `C:/Windows/System32`,
+ wantFile: `C:/Windows/System32`,
+ wantURI: span.URI("file:///C:/Windows/System32"),
+ },
+ {
+ path: `C:/Go/src/bob.go`,
+ wantFile: `C:/Go/src/bob.go`,
+ wantURI: span.URI("file:///C:/Go/src/bob.go"),
+ },
+ {
+ path: `c:/Go/src/bob.go`,
+ wantFile: `C:/Go/src/bob.go`,
+ wantURI: span.URI("file:///C:/Go/src/bob.go"),
+ },
+ {
+ path: `/path/to/dir`,
+ wantFile: `/path/to/dir`,
+ wantURI: span.URI("file:///path/to/dir"),
+ },
+ {
+ path: `/a/b/c/src/bob.go`,
+ wantFile: `/a/b/c/src/bob.go`,
+ wantURI: span.URI("file:///a/b/c/src/bob.go"),
+ },
+ {
+ path: `c:/Go/src/bob george/george/george.go`,
+ wantFile: `C:/Go/src/bob george/george/george.go`,
+ wantURI: span.URI("file:///C:/Go/src/bob%20george/george/george.go"),
+ },
+ } {
+ got := span.URIFromPath(test.path)
+ if got != test.wantURI {
+ t.Errorf("URIFromPath(%q): got %q, expected %q", test.path, got, test.wantURI)
+ }
+ gotFilename := got.Filename()
+ if gotFilename != test.wantFile {
+ t.Errorf("Filename(%q): got %q, expected %q", got, gotFilename, test.wantFile)
+ }
+ }
+}
+
+func TestURIFromURI(t *testing.T) {
+ for _, test := range []struct {
+ inputURI, wantFile string
+ wantURI span.URI
+ }{
+ {
+ inputURI: `file:///c:/Go/src/bob%20george/george/george.go`,
+ wantFile: `C:/Go/src/bob george/george/george.go`,
+ wantURI: span.URI("file:///C:/Go/src/bob%20george/george/george.go"),
+ },
+ {
+ inputURI: `file:///C%3A/Go/src/bob%20george/george/george.go`,
+ wantFile: `C:/Go/src/bob george/george/george.go`,
+ wantURI: span.URI("file:///C:/Go/src/bob%20george/george/george.go"),
+ },
+ {
+ inputURI: `file:///path/to/%25p%25ercent%25/per%25cent.go`,
+ wantFile: `/path/to/%p%ercent%/per%cent.go`,
+ wantURI: span.URI(`file:///path/to/%25p%25ercent%25/per%25cent.go`),
+ },
+ {
+ inputURI: `file:///C%3A/`,
+ wantFile: `C:/`,
+ wantURI: span.URI(`file:///C:/`),
+ },
+ {
+ inputURI: `file:///`,
+ wantFile: `/`,
+ wantURI: span.URI(`file:///`),
+ },
+ {
+ inputURI: `file://wsl%24/Ubuntu/home/wdcui/repo/VMEnclaves/cvm-runtime`,
+ wantFile: `/wsl$/Ubuntu/home/wdcui/repo/VMEnclaves/cvm-runtime`,
+ wantURI: span.URI(`file:///wsl$/Ubuntu/home/wdcui/repo/VMEnclaves/cvm-runtime`),
+ },
+ } {
+ got := span.URIFromURI(test.inputURI)
+ if got != test.wantURI {
+ t.Errorf("NewURI(%q): got %q, expected %q", test.inputURI, got, test.wantURI)
+ }
+ gotFilename := got.Filename()
+ if gotFilename != test.wantFile {
+ t.Errorf("Filename(%q): got %q, expected %q", got, gotFilename, test.wantFile)
+ }
+ }
+}
diff --git a/gopls/internal/span/uri_windows_test.go b/gopls/internal/span/uri_windows_test.go
new file mode 100644
index 000000000..3891e0d3e
--- /dev/null
+++ b/gopls/internal/span/uri_windows_test.go
@@ -0,0 +1,112 @@
+// Copyright 2020 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.
+
+//go:build windows
+// +build windows
+
+package span_test
+
+import (
+ "testing"
+
+ "golang.org/x/tools/gopls/internal/span"
+)
+
+// TestURI tests the conversion between URIs and filenames. The test cases
+// include Windows-style URIs and filepaths, but we avoid having OS-specific
+// tests by using only forward slashes, assuming that the standard library
+// functions filepath.ToSlash and filepath.FromSlash do not need testing.
+func TestURIFromPath(t *testing.T) {
+ for _, test := range []struct {
+ path, wantFile string
+ wantURI span.URI
+ }{
+ {
+ path: ``,
+ wantFile: ``,
+ wantURI: span.URI(""),
+ },
+ {
+ path: `C:\Windows\System32`,
+ wantFile: `C:\Windows\System32`,
+ wantURI: span.URI("file:///C:/Windows/System32"),
+ },
+ {
+ path: `C:\Go\src\bob.go`,
+ wantFile: `C:\Go\src\bob.go`,
+ wantURI: span.URI("file:///C:/Go/src/bob.go"),
+ },
+ {
+ path: `c:\Go\src\bob.go`,
+ wantFile: `C:\Go\src\bob.go`,
+ wantURI: span.URI("file:///C:/Go/src/bob.go"),
+ },
+ {
+ path: `\path\to\dir`,
+ wantFile: `C:\path\to\dir`,
+ wantURI: span.URI("file:///C:/path/to/dir"),
+ },
+ {
+ path: `\a\b\c\src\bob.go`,
+ wantFile: `C:\a\b\c\src\bob.go`,
+ wantURI: span.URI("file:///C:/a/b/c/src/bob.go"),
+ },
+ {
+ path: `c:\Go\src\bob george\george\george.go`,
+ wantFile: `C:\Go\src\bob george\george\george.go`,
+ wantURI: span.URI("file:///C:/Go/src/bob%20george/george/george.go"),
+ },
+ } {
+ got := span.URIFromPath(test.path)
+ if got != test.wantURI {
+ t.Errorf("URIFromPath(%q): got %q, expected %q", test.path, got, test.wantURI)
+ }
+ gotFilename := got.Filename()
+ if gotFilename != test.wantFile {
+ t.Errorf("Filename(%q): got %q, expected %q", got, gotFilename, test.wantFile)
+ }
+ }
+}
+
+func TestURIFromURI(t *testing.T) {
+ for _, test := range []struct {
+ inputURI, wantFile string
+ wantURI span.URI
+ }{
+ {
+ inputURI: `file:///c:/Go/src/bob%20george/george/george.go`,
+ wantFile: `C:\Go\src\bob george\george\george.go`,
+ wantURI: span.URI("file:///C:/Go/src/bob%20george/george/george.go"),
+ },
+ {
+ inputURI: `file:///C%3A/Go/src/bob%20george/george/george.go`,
+ wantFile: `C:\Go\src\bob george\george\george.go`,
+ wantURI: span.URI("file:///C:/Go/src/bob%20george/george/george.go"),
+ },
+ {
+ inputURI: `file:///c:/path/to/%25p%25ercent%25/per%25cent.go`,
+ wantFile: `C:\path\to\%p%ercent%\per%cent.go`,
+ wantURI: span.URI(`file:///C:/path/to/%25p%25ercent%25/per%25cent.go`),
+ },
+ {
+ inputURI: `file:///C%3A/`,
+ wantFile: `C:\`,
+ wantURI: span.URI(`file:///C:/`),
+ },
+ {
+ inputURI: `file:///`,
+ wantFile: `\`,
+ wantURI: span.URI(`file:///`),
+ },
+ } {
+ got := span.URIFromURI(test.inputURI)
+ if got != test.wantURI {
+ t.Errorf("NewURI(%q): got %q, expected %q", test.inputURI, got, test.wantURI)
+ }
+ gotFilename := got.Filename()
+ if gotFilename != test.wantFile {
+ t.Errorf("Filename(%q): got %q, expected %q", got, gotFilename, test.wantFile)
+ }
+ }
+}