From dbad8e90c91bdd4d0527ece950a56895f51d84b7 Mon Sep 17 00:00:00 2001 From: Ian Cottrell Date: Tue, 19 Feb 2019 21:11:15 -0500 Subject: [PATCH] internal/lsp: convert to the new location library This rationalises all the position handling and conversion code out. Fixes golang/go#29149 Change-Id: I2814f3e8ba769924bc70f35df9e5bf4d97d064de Reviewed-on: https://go-review.googlesource.com/c/tools/+/166884 Reviewed-by: Rebecca Stambler --- internal/lsp/cache/check.go | 12 +- internal/lsp/cache/file.go | 15 ++- internal/lsp/cache/view.go | 25 +++-- internal/lsp/cmd/definition.go | 28 +++-- internal/lsp/cmd/definition_test.go | 17 ++- internal/lsp/cmd/location.go | 165 ---------------------------- internal/lsp/cmd/testdata/a/a.go | 14 +-- internal/lsp/cmd/testdata/b/b.go | 8 +- internal/lsp/diagnostics.go | 29 ++--- internal/lsp/format.go | 52 ++++----- internal/lsp/imports.go | 23 ++-- internal/lsp/lsp_test.go | 85 ++++++++------ internal/lsp/position.go | 139 ----------------------- internal/lsp/protocol/printers.go | 30 ----- internal/lsp/protocol/span.go | 79 +++++++++++++ internal/lsp/server.go | 153 ++++++++------------------ internal/lsp/source/definition.go | 53 ++------- internal/lsp/source/diagnostics.go | 112 +++++-------------- internal/lsp/source/format.go | 32 ++---- internal/lsp/source/uri.go | 89 --------------- internal/lsp/source/uri_test.go | 52 --------- internal/lsp/source/view.go | 19 +--- internal/span/token.go | 2 +- internal/span/utf16.go | 2 +- 24 files changed, 338 insertions(+), 897 deletions(-) delete mode 100644 internal/lsp/cmd/location.go delete mode 100644 internal/lsp/position.go create mode 100644 internal/lsp/protocol/span.go delete mode 100644 internal/lsp/source/uri.go delete mode 100644 internal/lsp/source/uri_test.go diff --git a/internal/lsp/cache/check.go b/internal/lsp/cache/check.go index db3a1708..7b58c630 100644 --- a/internal/lsp/cache/check.go +++ b/internal/lsp/cache/check.go @@ -16,10 +16,10 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/packages" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/internal/span" ) -func (v *View) parse(ctx context.Context, uri source.URI) error { +func (v *View) parse(ctx context.Context, uri span.URI) error { v.mcache.mu.Lock() defer v.mcache.mu.Unlock() @@ -78,7 +78,7 @@ func (v *View) cachePackage(pkg *Package) { log.Printf("no token.File for %v", file.Name) continue } - fURI := source.ToURI(tok.Name()) + fURI := span.FileURI(tok.Name()) f := v.getFile(fURI) f.token = tok f.ast = file @@ -88,7 +88,7 @@ func (v *View) cachePackage(pkg *Package) { } func (v *View) checkMetadata(ctx context.Context, f *File) error { - filename, err := f.URI.Filename() + filename, err := f.uri.Filename() if err != nil { return err } @@ -155,7 +155,7 @@ func (v *View) link(pkgPath string, pkg *packages.Package, parent *metadata) *me m.name = pkg.Name m.files = pkg.CompiledGoFiles for _, filename := range m.files { - if f, ok := v.files[source.ToURI(filename)]; ok { + if f, ok := v.files[span.FileURI(filename)]; ok { f.meta = m } } @@ -319,7 +319,7 @@ func (v *View) parseFiles(filenames []string) ([]*ast.File, []error) { } // First, check if we have already cached an AST for this file. - f := v.files[source.ToURI(filename)] + f := v.files[span.FileURI(filename)] var fAST *ast.File if f != nil { fAST = f.ast diff --git a/internal/lsp/cache/file.go b/internal/lsp/cache/file.go index 4747c66f..8b3bceb8 100644 --- a/internal/lsp/cache/file.go +++ b/internal/lsp/cache/file.go @@ -11,11 +11,12 @@ import ( "io/ioutil" "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/internal/span" ) // File holds all the information we know about a file. type File struct { - URI source.URI + uri span.URI view *View active bool content []byte @@ -26,6 +27,10 @@ type File struct { imports []*ast.ImportSpec } +func (f *File) URI() span.URI { + return f.uri +} + // GetContent returns the contents of the file, reading it from file system if needed. func (f *File) GetContent(ctx context.Context) []byte { f.view.mu.Lock() @@ -47,7 +52,7 @@ func (f *File) GetToken(ctx context.Context) *token.File { defer f.view.mu.Unlock() if f.token == nil || len(f.view.contentChanges) > 0 { - if err := f.view.parse(ctx, f.URI); err != nil { + if err := f.view.parse(ctx, f.uri); err != nil { return nil } } @@ -59,7 +64,7 @@ func (f *File) GetAST(ctx context.Context) *ast.File { defer f.view.mu.Unlock() if f.ast == nil || len(f.view.contentChanges) > 0 { - if err := f.view.parse(ctx, f.URI); err != nil { + if err := f.view.parse(ctx, f.uri); err != nil { return nil } } @@ -71,7 +76,7 @@ func (f *File) GetPackage(ctx context.Context) source.Package { defer f.view.mu.Unlock() if f.pkg == nil || len(f.view.contentChanges) > 0 { - if err := f.view.parse(ctx, f.URI); err != nil { + if err := f.view.parse(ctx, f.uri); err != nil { return nil } } @@ -95,7 +100,7 @@ func (f *File) read(ctx context.Context) { } } // We don't know the content yet, so read it. - filename, err := f.URI.Filename() + filename, err := f.uri.Filename() if err != nil { return } diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go index 2dba8d83..1817bd1a 100644 --- a/internal/lsp/cache/view.go +++ b/internal/lsp/cache/view.go @@ -11,6 +11,7 @@ import ( "golang.org/x/tools/go/packages" "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/internal/span" ) type View struct { @@ -30,14 +31,14 @@ type View struct { Config packages.Config // files caches information for opened files in a view. - files map[source.URI]*File + files map[span.URI]*File // contentChanges saves the content changes for a given state of the view. // When type information is requested by the view, all of the dirty changes // are applied, potentially invalidating some data in the caches. The // closures in the dirty slice assume that their caller is holding the // view's mutex. - contentChanges map[source.URI]func() + contentChanges map[span.URI]func() // mcache caches metadata for the packages of the opened files in a view. mcache *metadataCache @@ -74,8 +75,8 @@ func NewView(config *packages.Config) *View { backgroundCtx: ctx, cancel: cancel, Config: *config, - files: make(map[source.URI]*File), - contentChanges: make(map[source.URI]func()), + files: make(map[span.URI]*File), + contentChanges: make(map[span.URI]func()), mcache: &metadataCache{ packages: make(map[string]*metadata), }, @@ -97,7 +98,7 @@ func (v *View) FileSet() *token.FileSet { } // SetContent sets the overlay contents for a file. -func (v *View) SetContent(ctx context.Context, uri source.URI, content []byte) error { +func (v *View) SetContent(ctx context.Context, uri span.URI, content []byte) error { v.mu.Lock() defer v.mu.Unlock() @@ -134,7 +135,7 @@ func (v *View) applyContentChanges(ctx context.Context) error { // setContent applies a content update for a given file. It assumes that the // caller is holding the view's mutex. -func (v *View) applyContentChange(uri source.URI, content []byte) { +func (v *View) applyContentChange(uri span.URI, content []byte) { f := v.getFile(uri) f.content = content @@ -151,14 +152,14 @@ func (v *View) applyContentChange(uri source.URI, content []byte) { case f.active && content == nil: // The file was active, so we need to forget its content. f.active = false - if filename, err := f.URI.Filename(); err == nil { + if filename, err := f.uri.Filename(); err == nil { delete(f.view.Config.Overlay, filename) } f.content = nil case content != nil: // This is an active overlay, so we update the map. f.active = true - if filename, err := f.URI.Filename(); err == nil { + if filename, err := f.uri.Filename(); err == nil { f.view.Config.Overlay[filename] = f.content } } @@ -178,7 +179,7 @@ func (v *View) remove(pkgPath string) { // All of the files in the package may also be holding a pointer to the // invalidated package. for _, filename := range m.files { - if f, ok := v.files[source.ToURI(filename)]; ok { + if f, ok := v.files[span.FileURI(filename)]; ok { f.pkg = nil } } @@ -187,7 +188,7 @@ func (v *View) remove(pkgPath string) { // GetFile returns a File for the given URI. It will always succeed because it // adds the file to the managed set if needed. -func (v *View) GetFile(ctx context.Context, uri source.URI) (source.File, error) { +func (v *View) GetFile(ctx context.Context, uri span.URI) (source.File, error) { v.mu.Lock() defer v.mu.Unlock() @@ -199,11 +200,11 @@ func (v *View) GetFile(ctx context.Context, uri source.URI) (source.File, error) } // getFile is the unlocked internal implementation of GetFile. -func (v *View) getFile(uri source.URI) *File { +func (v *View) getFile(uri span.URI) *File { f, found := v.files[uri] if !found { f = &File{ - URI: uri, + uri: uri, view: v, } v.files[uri] = f diff --git a/internal/lsp/cmd/definition.go b/internal/lsp/cmd/definition.go index 62889a91..ea7bab88 100644 --- a/internal/lsp/cmd/definition.go +++ b/internal/lsp/cmd/definition.go @@ -16,20 +16,21 @@ import ( guru "golang.org/x/tools/cmd/guru/serial" "golang.org/x/tools/internal/lsp/cache" "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/tool" ) // A Definition is the result of a 'definition' query. type Definition struct { - Location Location `json:"location"` // location of the definition - Description string `json:"description"` // description of the denoted object + Span span.Span `json:"span"` // span of the definition + Description string `json:"description"` // description of the denoted object } // This constant is printed in the help, and then used in a test to verify the // help is still valid. // It should be the byte offset in this file of the "Set" in "flag.FlagSet" from // the DetailedHelp method below. -const exampleOffset = 1277 +const exampleOffset = 1311 // definition implements the definition noun for the query command. type definition struct { @@ -57,11 +58,8 @@ func (d *definition) Run(ctx context.Context, args ...string) error { return tool.CommandLineErrorf("definition expects 1 argument") } view := cache.NewView(&d.query.app.Config) - from, err := parseLocation(args[0]) - if err != nil { - return err - } - f, err := view.GetFile(ctx, source.ToURI(from.Filename)) + from := span.Parse(args[0]) + f, err := view.GetFile(ctx, from.URI) if err != nil { return err } @@ -72,10 +70,10 @@ func (d *definition) Run(ctx context.Context, args ...string) error { } ident, err := source.Identifier(ctx, view, f, pos) if err != nil { - return err + return fmt.Errorf("%v: %v", from, err) } if ident == nil { - return fmt.Errorf("not an identifier") + return fmt.Errorf("%v: not an identifier", from) } var result interface{} switch d.query.Emulate { @@ -96,7 +94,7 @@ func (d *definition) Run(ctx context.Context, args ...string) error { } switch d := result.(type) { case *Definition: - fmt.Printf("%v: defined here as %s", d.Location, d.Description) + fmt.Printf("%v: defined here as %s", d.Span, d.Description) case *guru.Definition: fmt.Printf("%s: defined here as %s", d.ObjPos, d.Desc) default: @@ -111,16 +109,16 @@ func buildDefinition(ctx context.Context, view source.View, ident *source.Identi return nil, err } return &Definition{ - Location: newLocation(view.FileSet(), ident.Declaration.Range), + Span: ident.Declaration.Range.Span(), Description: content, }, nil } func buildGuruDefinition(ctx context.Context, view source.View, ident *source.IdentifierInfo) (*guru.Definition, error) { - loc := newLocation(view.FileSet(), ident.Declaration.Range) + spn := ident.Declaration.Range.Span() pkg := ident.File.GetPackage(ctx) // guru does not support ranges - loc.End = loc.Start + spn.End = span.Point{} // Behavior that attempts to match the expected output for guru. For an example // of the format, see the associated definition tests. buf := &bytes.Buffer{} @@ -170,7 +168,7 @@ func buildGuruDefinition(ctx context.Context, view source.View, ident *source.Id fmt.Fprint(buf, suffix) } return &guru.Definition{ - ObjPos: fmt.Sprint(loc), + ObjPos: fmt.Sprint(spn), Desc: buf.String(), }, nil } diff --git a/internal/lsp/cmd/definition_test.go b/internal/lsp/cmd/definition_test.go index d02fab2c..c6c7467d 100644 --- a/internal/lsp/cmd/definition_test.go +++ b/internal/lsp/cmd/definition_test.go @@ -20,6 +20,7 @@ import ( "golang.org/x/tools/go/packages/packagestest" "golang.org/x/tools/internal/lsp/cmd" + "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/tool" ) @@ -33,7 +34,7 @@ func TestDefinitionHelpExample(t *testing.T) { } thisFile := filepath.Join(dir, "definition.go") args := []string{"query", "definition", fmt.Sprintf("%v:#%v", thisFile, cmd.ExampleOffset)} - expect := regexp.MustCompile(`^[\w/\\:_]+flag[/\\]flag.go:\d+:\d+,\d+:\d+: defined here as type flag.FlagSet struct{.*}$`) + expect := regexp.MustCompile(`^[\w/\\:_]+flag[/\\]flag.go:\d+:\d+-\d+: defined here as type flag.FlagSet struct{.*}$`) got := captureStdOut(t, func() { tool.Main(context.Background(), &cmd.Application{}, args) }) @@ -58,14 +59,14 @@ func TestDefinition(t *testing.T) { } args = append(args, "definition") f := fset.File(src) - loc := cmd.Location{ - Filename: f.Name(), - Start: cmd.Position{ + spn := span.Span{ + URI: span.FileURI(f.Name()), + Start: span.Point{ Offset: f.Offset(src), }, } - loc.End = loc.Start - args = append(args, fmt.Sprint(loc)) + spn.End = spn.Start + args = append(args, fmt.Sprint(spn)) app := &cmd.Application{} app.Config = *exported.Config got := captureStdOut(t, func() { @@ -80,6 +81,10 @@ func TestDefinition(t *testing.T) { case "efile": qfile := strconv.Quote(start.Filename) return qfile[1 : len(qfile)-1] + case "euri": + uri := span.FileURI(start.Filename) + quri := strconv.Quote(string(uri)) + return quri[1 : len(quri)-1] case "line": return fmt.Sprint(start.Line) case "col": diff --git a/internal/lsp/cmd/location.go b/internal/lsp/cmd/location.go deleted file mode 100644 index 101046e0..00000000 --- a/internal/lsp/cmd/location.go +++ /dev/null @@ -1,165 +0,0 @@ -// 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 cmd - -import ( - "fmt" - "go/token" - "path/filepath" - "regexp" - "strconv" - - "golang.org/x/tools/internal/lsp/source" -) - -type Location struct { - Filename string `json:"file"` - Start Position `json:"start"` - End Position `json:"end"` -} - -type Position struct { - Line int `json:"line"` - Column int `json:"column"` - Offset int `json:"offset"` -} - -func newLocation(fset *token.FileSet, r source.Range) Location { - start := fset.Position(r.Start) - end := fset.Position(r.End) - // it should not be possible the following line to fail - filename, _ := source.ToURI(start.Filename).Filename() - return Location{ - Filename: filename, - Start: Position{ - Line: start.Line, - Column: start.Column, - Offset: fset.File(r.Start).Offset(r.Start), - }, - End: Position{ - Line: end.Line, - Column: end.Column, - Offset: fset.File(r.End).Offset(r.End), - }, - } -} - -var posRe = regexp.MustCompile( - `(?P.*):(?P(?P\d+):(?P\d)+|#(?P\d+))(?P:(?P\d+):(?P\d+)|#(?P\d+))?$`) - -const ( - posReAll = iota - posReFile - posReStart - posReSLine - posReSCol - posReSOff - posReEnd - posReELine - posReECol - posReEOff -) - -func init() { - names := posRe.SubexpNames() - // verify all our submatch offsets are correct - for name, index := range map[string]int{ - "file": posReFile, - "start": posReStart, - "sline": posReSLine, - "scol": posReSCol, - "soff": posReSOff, - "end": posReEnd, - "eline": posReELine, - "ecol": posReECol, - "eoff": posReEOff, - } { - if names[index] == name { - continue - } - // try to find it - for test := range names { - if names[test] == name { - panic(fmt.Errorf("Index for %s incorrect, wanted %v have %v", name, index, test)) - } - } - panic(fmt.Errorf("Subexp %s does not exist", name)) - } -} - -// parseLocation parses a string of the form "file:pos" or -// file:start,end" where pos, start, end match either a byte offset in the -// form #%d or a line and column in the form %d,%d. -func parseLocation(value string) (Location, error) { - var loc Location - m := posRe.FindStringSubmatch(value) - if m == nil { - return loc, fmt.Errorf("bad location syntax %q", value) - } - loc.Filename = m[posReFile] - if !filepath.IsAbs(loc.Filename) { - loc.Filename, _ = filepath.Abs(loc.Filename) // ignore error - } - if m[posReSLine] != "" { - v, err := strconv.ParseInt(m[posReSLine], 10, 32) - if err != nil { - return loc, err - } - loc.Start.Line = int(v) - v, err = strconv.ParseInt(m[posReSCol], 10, 32) - if err != nil { - return loc, err - } - loc.Start.Column = int(v) - } else { - v, err := strconv.ParseInt(m[posReSOff], 10, 32) - if err != nil { - return loc, err - } - loc.Start.Offset = int(v) - } - if m[posReEnd] == "" { - loc.End = loc.Start - } else { - if m[posReELine] != "" { - v, err := strconv.ParseInt(m[posReELine], 10, 32) - if err != nil { - return loc, err - } - loc.End.Line = int(v) - v, err = strconv.ParseInt(m[posReECol], 10, 32) - if err != nil { - return loc, err - } - loc.End.Column = int(v) - } else { - v, err := strconv.ParseInt(m[posReEOff], 10, 32) - if err != nil { - return loc, err - } - loc.End.Offset = int(v) - } - } - return loc, nil -} - -func (l Location) Format(f fmt.State, c rune) { - // we should always have a filename - fmt.Fprint(f, l.Filename) - // are we in line:column format or #offset format - fmt.Fprintf(f, ":%v", l.Start) - if l.End != l.Start { - fmt.Fprintf(f, ",%v", l.End) - } -} - -func (p Position) Format(f fmt.State, c rune) { - // are we in line:column format or #offset format - if p.Line > 0 { - fmt.Fprintf(f, "%d:%d", p.Line, p.Column) - return - } - fmt.Fprintf(f, "#%d", p.Offset) -} diff --git a/internal/lsp/cmd/testdata/a/a.go b/internal/lsp/cmd/testdata/a/a.go index 23364341..d4516aec 100644 --- a/internal/lsp/cmd/testdata/a/a.go +++ b/internal/lsp/cmd/testdata/a/a.go @@ -23,19 +23,19 @@ func useThings() { } /*@ -definition(aStructType, "", Thing, "$file:$line:$col,$eline:$ecol: defined here as type Thing struct{Member string}") +definition(aStructType, "", Thing, "$file:$line:$col-$ecol: defined here as type Thing struct{Member string}") definition(aStructType, "-emulate=guru", Thing, "$file:$line:$col: defined here as type Thing") -definition(aMember, "", Member, "$file:$line:$col,$eline:$ecol: defined here as field Member string") +definition(aMember, "", Member, "$file:$line:$col-$ecol: defined here as field Member string") definition(aMember, "-emulate=guru", Member, "$file:$line:$col: defined here as field Member string") -definition(aVar, "", Other, "$file:$line:$col,$eline:$ecol: defined here as var Other Thing") +definition(aVar, "", Other, "$file:$line:$col-$ecol: defined here as var Other Thing") definition(aVar, "-emulate=guru", Other, "$file:$line:$col: defined here as var Other") -definition(aFunc, "", Things, "$file:$line:$col,$eline:$ecol: defined here as func Things(val []string) []Thing") +definition(aFunc, "", Things, "$file:$line:$col-$ecol: defined here as func Things(val []string) []Thing") definition(aFunc, "-emulate=guru", Things, "$file:$line:$col: defined here as func Things(val []string) []Thing") -definition(aMethod, "", Method, "$file:$line:$col,$eline:$ecol: defined here as func (Thing).Method(i int) string") +definition(aMethod, "", Method, "$file:$line:$col-$ecol: defined here as func (Thing).Method(i int) string") definition(aMethod, "-emulate=guru", Method, "$file:$line:$col: defined here as func (Thing).Method(i int) string") //param @@ -46,8 +46,8 @@ definition(aMethod, "-emulate=guru", Method, "$file:$line:$col: defined here as // JSON tests definition(aStructType, "-json", Thing, `{ - "location": { - "file": "$efile", + "span": { + "uri": "$euri", "start": { "line": $line, "column": $col, diff --git a/internal/lsp/cmd/testdata/b/b.go b/internal/lsp/cmd/testdata/b/b.go index bd197de6..9f1f975f 100644 --- a/internal/lsp/cmd/testdata/b/b.go +++ b/internal/lsp/cmd/testdata/b/b.go @@ -12,15 +12,15 @@ func useThings() { } /*@ -definition(bStructType, "", Thing, "$file:$line:$col,$eline:$ecol: defined here as type a.Thing struct{Member string}") +definition(bStructType, "", Thing, "$file:$line:$col-$ecol: defined here as type a.Thing struct{Member string}") definition(bStructType, "-emulate=guru", Thing, "$file:$line:$col: defined here as type golang.org/fake/a.Thing") -definition(bMember, "", Member, "$file:$line:$col,$eline:$ecol: defined here as field Member string") +definition(bMember, "", Member, "$file:$line:$col-$ecol: defined here as field Member string") definition(bMember, "-emulate=guru", Member, "$file:$line:$col: defined here as field Member string") -definition(bVar, "", Other, "$file:$line:$col,$eline:$ecol: defined here as var a.Other a.Thing") +definition(bVar, "", Other, "$file:$line:$col-$ecol: defined here as var a.Other a.Thing") definition(bVar, "-emulate=guru", Other, "$file:$line:$col: defined here as var golang.org/fake/a.Other") -definition(bFunc, "", Things, "$file:$line:$col,$eline:$ecol: defined here as func a.Things(val []string) []a.Thing") +definition(bFunc, "", Things, "$file:$line:$col-$ecol: defined here as func a.Things(val []string) []a.Thing") definition(bFunc, "-emulate=guru", Things, "$file:$line:$col: defined here as func golang.org/fake/a.Things(val []string) []golang.org/fake/a.Thing") */ diff --git a/internal/lsp/diagnostics.go b/internal/lsp/diagnostics.go index 558b8766..e46abf62 100644 --- a/internal/lsp/diagnostics.go +++ b/internal/lsp/diagnostics.go @@ -10,42 +10,45 @@ import ( "golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/internal/span" ) -func (s *server) cacheAndDiagnose(ctx context.Context, uri string, content string) { - sourceURI, err := fromProtocolURI(uri) - if err != nil { - return // handle error? - } - if err := s.setContent(ctx, sourceURI, []byte(content)); err != nil { - return // handle error? +func (s *server) cacheAndDiagnose(ctx context.Context, uri span.URI, content string) error { + if err := s.setContent(ctx, uri, []byte(content)); err != nil { + return err } go func() { ctx := s.view.BackgroundContext() if ctx.Err() != nil { return } - reports, err := source.Diagnostics(ctx, s.view, sourceURI) + reports, err := source.Diagnostics(ctx, s.view, uri) if err != nil { return // handle error? } - for filename, diagnostics := range reports { + for uri, diagnostics := range reports { s.client.PublishDiagnostics(ctx, &protocol.PublishDiagnosticsParams{ - URI: string(source.ToURI(filename)), Diagnostics: toProtocolDiagnostics(ctx, s.view, diagnostics), + URI: protocol.NewURI(uri), }) } }() + return nil } -func (s *server) setContent(ctx context.Context, uri source.URI, content []byte) error { +func (s *server) setContent(ctx context.Context, uri span.URI, content []byte) error { return s.view.SetContent(ctx, uri, content) } func toProtocolDiagnostics(ctx context.Context, v source.View, diagnostics []source.Diagnostic) []protocol.Diagnostic { reports := []protocol.Diagnostic{} for _, diag := range diagnostics { - tok := v.FileSet().File(diag.Start) + _, m, err := newColumnMap(ctx, v, diag.Span.URI) + if err != nil { + //TODO: if we can't find the file we cannot map + //the diagnostic, but also this should never happen + continue + } src := diag.Source if src == "" { src = "LSP" @@ -59,7 +62,7 @@ func toProtocolDiagnostics(ctx context.Context, v source.View, diagnostics []sou } reports = append(reports, protocol.Diagnostic{ Message: diag.Message, - Range: toProtocolRange(tok, diag.Range), + Range: m.Range(diag.Span), Severity: severity, Source: src, }) diff --git a/internal/lsp/format.go b/internal/lsp/format.go index 0642ce39..fc239752 100644 --- a/internal/lsp/format.go +++ b/internal/lsp/format.go @@ -5,56 +5,46 @@ import ( "golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/internal/span" ) // formatRange formats a document with a given range. -func formatRange(ctx context.Context, v source.View, uri string, rng *protocol.Range) ([]protocol.TextEdit, error) { - sourceURI, err := fromProtocolURI(uri) +func formatRange(ctx context.Context, v source.View, s span.Span) ([]protocol.TextEdit, error) { + f, m, err := newColumnMap(ctx, v, s.URI) if err != nil { return nil, err } - f, err := v.GetFile(ctx, sourceURI) + rng := s.Range(m.Converter) + if rng.Start == rng.End { + // if we have a single point, then assume the rest of the file + rng.End = f.GetToken(ctx).Pos(f.GetToken(ctx).Size()) + } + edits, err := source.Format(ctx, f, rng) if err != nil { return nil, err } - tok := f.GetToken(ctx) - var r source.Range - if rng == nil { - r.Start = tok.Pos(0) - r.End = tok.Pos(tok.Size()) - } else { - r = fromProtocolRange(tok, *rng) - } - edits, err := source.Format(ctx, f, r) - if err != nil { - return nil, err - } - return toProtocolEdits(ctx, f, edits), nil + return toProtocolEdits(m, edits), nil } -func toProtocolEdits(ctx context.Context, f source.File, edits []source.TextEdit) []protocol.TextEdit { +func toProtocolEdits(m *protocol.ColumnMapper, edits []source.TextEdit) []protocol.TextEdit { if edits == nil { return nil } - tok := f.GetToken(ctx) - content := f.GetContent(ctx) - // When a file ends with an empty line, the newline character is counted - // as part of the previous line. This causes the formatter to insert - // another unnecessary newline on each formatting. We handle this case by - // checking if the file already ends with a newline character. - hasExtraNewline := content[len(content)-1] == '\n' result := make([]protocol.TextEdit, len(edits)) for i, edit := range edits { - rng := toProtocolRange(tok, edit.Range) - // If the edit ends at the end of the file, add the extra line. - if hasExtraNewline && tok.Offset(edit.Range.End) == len(content) { - rng.End.Line++ - rng.End.Character = 0 - } result[i] = protocol.TextEdit{ - Range: rng, + Range: m.Range(edit.Span), NewText: edit.NewText, } } return result } + +func newColumnMap(ctx context.Context, v source.View, uri span.URI) (source.File, *protocol.ColumnMapper, error) { + f, err := v.GetFile(ctx, uri) + if err != nil { + return nil, nil, err + } + m := protocol.NewColumnMapper(f.URI(), f.GetFileSet(ctx), f.GetToken(ctx), f.GetContent(ctx)) + return f, m, nil +} diff --git a/internal/lsp/imports.go b/internal/lsp/imports.go index ca712f2d..21fb98e1 100644 --- a/internal/lsp/imports.go +++ b/internal/lsp/imports.go @@ -9,25 +9,22 @@ import ( "golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/internal/span" ) -func organizeImports(ctx context.Context, v source.View, uri string) ([]protocol.TextEdit, error) { - sourceURI, err := fromProtocolURI(uri) +func organizeImports(ctx context.Context, v source.View, s span.Span) ([]protocol.TextEdit, error) { + f, m, err := newColumnMap(ctx, v, s.URI) if err != nil { return nil, err } - f, err := v.GetFile(ctx, sourceURI) + rng := s.Range(m.Converter) + if rng.Start == rng.End { + // if we have a single point, then assume the rest of the file + rng.End = f.GetToken(ctx).Pos(f.GetToken(ctx).Size()) + } + edits, err := source.Imports(ctx, f, rng) if err != nil { return nil, err } - tok := f.GetToken(ctx) - r := source.Range{ - Start: tok.Pos(0), - End: tok.Pos(tok.Size()), - } - edits, err := source.Imports(ctx, f, r) - if err != nil { - return nil, err - } - return toProtocolEdits(ctx, f, edits), nil + return toProtocolEdits(m, edits), nil } diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go index 79bd6a9b..21af2a9d 100644 --- a/internal/lsp/lsp_test.go +++ b/internal/lsp/lsp_test.go @@ -20,6 +20,7 @@ import ( "golang.org/x/tools/internal/lsp/cache" "golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/internal/span" ) // TODO(rstambler): Remove this once Go 1.12 is released as we end support for @@ -147,7 +148,7 @@ func testLSP(t *testing.T, exporter packagestest.Exporter) { }) } -type diagnostics map[string][]protocol.Diagnostic +type diagnostics map[span.URI][]protocol.Diagnostic type completionItems map[token.Pos]*protocol.CompletionItem type completions map[token.Position][]token.Pos type formats map[string]string @@ -156,14 +157,14 @@ type definitions map[protocol.Location]protocol.Location func (d diagnostics) test(t *testing.T, v source.View) int { count := 0 ctx := context.Background() - for filename, want := range d { - sourceDiagnostics, err := source.Diagnostics(context.Background(), v, source.ToURI(filename)) + for uri, want := range d { + sourceDiagnostics, err := source.Diagnostics(context.Background(), v, uri) if err != nil { t.Fatal(err) } - got := toProtocolDiagnostics(ctx, v, sourceDiagnostics[filename]) + got := toProtocolDiagnostics(ctx, v, sourceDiagnostics[uri]) sorted(got) - if diff := diffDiagnostics(filename, want, got); diff != "" { + if diff := diffDiagnostics(uri, want, got); diff != "" { t.Error(diff) } count += len(want) @@ -171,10 +172,10 @@ func (d diagnostics) test(t *testing.T, v source.View) int { return count } -func (d diagnostics) collect(fset *token.FileSet, rng packagestest.Range, msgSource, msg string) { - f := fset.File(rng.Start) - if _, ok := d[f.Name()]; !ok { - d[f.Name()] = []protocol.Diagnostic{} +func (d diagnostics) collect(e *packagestest.Exported, fset *token.FileSet, rng packagestest.Range, msgSource, msg string) { + spn, m := testLocation(e, fset, rng) + if _, ok := d[spn.URI]; !ok { + d[spn.URI] = []protocol.Diagnostic{} } // If a file has an empty diagnostic message, return. This allows us to // avoid testing diagnostics in files that may have a lot of them. @@ -182,21 +183,21 @@ func (d diagnostics) collect(fset *token.FileSet, rng packagestest.Range, msgSou return } severity := protocol.SeverityError - if strings.Contains(f.Name(), "analyzer") { + if strings.Contains(string(spn.URI), "analyzer") { severity = protocol.SeverityWarning } want := protocol.Diagnostic{ - Range: toProtocolRange(f, source.Range(rng)), + Range: m.Range(spn), Severity: severity, Source: msgSource, Message: msg, } - d[f.Name()] = append(d[f.Name()], want) + d[spn.URI] = append(d[spn.URI], want) } // diffDiagnostics prints the diff between expected and actual diagnostics test // results. -func diffDiagnostics(filename string, want, got []protocol.Diagnostic) string { +func diffDiagnostics(uri span.URI, want, got []protocol.Diagnostic) string { if len(got) != len(want) { goto Failed } @@ -209,7 +210,7 @@ func diffDiagnostics(filename string, want, got []protocol.Diagnostic) string { goto Failed } // Special case for diagnostics on parse errors. - if strings.Contains(filename, "noparse") { + if strings.Contains(string(uri), "noparse") { if g.Range.Start != g.Range.End || w.Range.Start != g.Range.End { goto Failed } @@ -228,7 +229,7 @@ func diffDiagnostics(filename string, want, got []protocol.Diagnostic) string { return "" Failed: msg := &bytes.Buffer{} - fmt.Fprintf(msg, "diagnostics failed for %s:\nexpected:\n", filename) + fmt.Fprintf(msg, "diagnostics failed for %s:\nexpected:\n", uri) for _, d := range want { fmt.Fprintf(msg, " %v\n", d) } @@ -248,7 +249,7 @@ func (c completions) test(t *testing.T, exported *packagestest.Exported, s *serv list, err := s.Completion(context.Background(), &protocol.CompletionParams{ TextDocumentPositionParams: protocol.TextDocumentPositionParams{ TextDocument: protocol.TextDocumentIdentifier{ - URI: string(source.ToURI(src.Filename)), + URI: protocol.NewURI(span.FileURI(src.Filename)), }, Position: protocol.Position{ Line: float64(src.Line - 1), @@ -361,10 +362,12 @@ Failed: } func (f formats) test(t *testing.T, s *server) { + ctx := context.Background() for filename, gofmted := range f { + uri := span.FileURI(filename) edits, err := s.Formatting(context.Background(), &protocol.DocumentFormattingParams{ TextDocument: protocol.TextDocumentIdentifier{ - URI: string(source.ToURI(filename)), + URI: protocol.NewURI(uri), }, }) if err != nil { @@ -373,11 +376,11 @@ func (f formats) test(t *testing.T, s *server) { } continue } - f, err := s.view.GetFile(context.Background(), source.ToURI(filename)) + f, m, err := newColumnMap(ctx, s.view, uri) if err != nil { t.Error(err) } - buf, err := applyEdits(f.GetContent(context.Background()), edits) + buf, err := applyEdits(m, f.GetContent(context.Background()), edits) if err != nil { t.Error(err) } @@ -412,7 +415,7 @@ func (d definitions) test(t *testing.T, s *server, typ bool) { locs, err = s.Definition(context.Background(), params) } if err != nil { - t.Fatalf("failed for %s: %v", src, err) + t.Fatalf("failed for %v: %v", src, err) } if len(locs) != 1 { t.Errorf("got %d locations for definition, expected 1", len(locs)) @@ -423,9 +426,21 @@ func (d definitions) test(t *testing.T, s *server, typ bool) { } } -func (d definitions) collect(fset *token.FileSet, src, target packagestest.Range) { - loc := toProtocolLocation(fset, source.Range(src)) - d[loc] = toProtocolLocation(fset, source.Range(target)) +func (d definitions) collect(e *packagestest.Exported, fset *token.FileSet, src, target packagestest.Range) { + sSrc, mSrc := testLocation(e, fset, src) + sTarget, mTarget := testLocation(e, fset, target) + d[mSrc.Location(sSrc)] = mTarget.Location(sTarget) +} + +func testLocation(e *packagestest.Exported, fset *token.FileSet, rng packagestest.Range) (span.Span, *protocol.ColumnMapper) { + spn := span.NewRange(fset, rng.Start, rng.End).Span() + f := fset.File(rng.Start) + content, err := e.FileContents(f.Name()) + if err != nil { + return spn, nil + } + m := protocol.NewColumnMapper(spn.URI, fset, f, content) + return spn, m } func TestBytesOffset(t *testing.T) { @@ -450,27 +465,31 @@ func TestBytesOffset(t *testing.T) { {text: "aaa\nbbb\n\n", pos: protocol.Position{Line: 2, Character: 0}, want: 8}, } - for _, test := range tests { - got := bytesOffset([]byte(test.text), test.pos) - if got != test.want { - t.Errorf("want %d for %q(Line:%d,Character:%d), but got %d", test.want, test.text, int(test.pos.Line), int(test.pos.Character), got) + for i, test := range tests { + fname := fmt.Sprintf("test %d", i) + fset := token.NewFileSet() + f := fset.AddFile(fname, -1, len(test.text)) + f.SetLinesForContent([]byte(test.text)) + mapper := protocol.NewColumnMapper(span.FileURI(fname), fset, f, []byte(test.text)) + got := mapper.Point(test.pos) + if got.Offset != test.want { + t.Errorf("want %d for %q(Line:%d,Character:%d), but got %d", test.want, test.text, int(test.pos.Line), int(test.pos.Character), got.Offset) } } } -func applyEdits(content []byte, edits []protocol.TextEdit) ([]byte, error) { +func applyEdits(m *protocol.ColumnMapper, content []byte, edits []protocol.TextEdit) ([]byte, error) { prev := 0 result := make([]byte, 0, len(content)) for _, edit := range edits { - start := bytesOffset(content, edit.Range.Start) - end := bytesOffset(content, edit.Range.End) - if start > prev { - result = append(result, content[prev:start]...) + spn := m.RangeSpan(edit.Range).Clean(nil) + if spn.Start.Offset > prev { + result = append(result, content[prev:spn.Start.Offset]...) } if len(edit.NewText) > 0 { result = append(result, []byte(edit.NewText)...) } - prev = end + prev = spn.End.Offset } if prev < len(content) { result = append(result, content[prev:]...) diff --git a/internal/lsp/position.go b/internal/lsp/position.go deleted file mode 100644 index 087ddbcb..00000000 --- a/internal/lsp/position.go +++ /dev/null @@ -1,139 +0,0 @@ -// Copyright 2018 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 lsp - -import ( - "context" - "go/token" - "net/url" - - "golang.org/x/tools/internal/lsp/cache" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" -) - -// fromProtocolURI converts a string to a source.URI. -// TODO(rstambler): Add logic here to support Windows. -func fromProtocolURI(uri string) (source.URI, error) { - unescaped, err := url.PathUnescape(string(uri)) - if err != nil { - return "", err - } - return source.URI(unescaped), nil -} - -// fromProtocolLocation converts from a protocol location to a source range. -// It will return an error if the file of the location was not valid. -// It uses fromProtocolRange to convert the start and end positions. -func fromProtocolLocation(ctx context.Context, v *cache.View, loc protocol.Location) (source.Range, error) { - sourceURI, err := fromProtocolURI(loc.URI) - if err != nil { - return source.Range{}, err - } - f, err := v.GetFile(ctx, sourceURI) - if err != nil { - return source.Range{}, err - } - tok := f.GetToken(ctx) - return fromProtocolRange(tok, loc.Range), nil -} - -// toProtocolLocation converts from a source range back to a protocol location. -func toProtocolLocation(fset *token.FileSet, r source.Range) protocol.Location { - tok := fset.File(r.Start) - uri := source.ToURI(tok.Name()) - return protocol.Location{ - URI: string(uri), - Range: toProtocolRange(tok, r), - } -} - -// fromProtocolRange converts a protocol range to a source range. -// It uses fromProtocolPosition to convert the start and end positions, which -// requires the token file the positions belongs to. -func fromProtocolRange(f *token.File, r protocol.Range) source.Range { - start := fromProtocolPosition(f, r.Start) - var end token.Pos - switch { - case r.End == r.Start: - end = start - case r.End.Line < 0: - end = token.NoPos - default: - end = fromProtocolPosition(f, r.End) - } - return source.Range{ - Start: start, - End: end, - } -} - -// toProtocolRange converts from a source range back to a protocol range. -func toProtocolRange(f *token.File, r source.Range) protocol.Range { - return protocol.Range{ - Start: toProtocolPosition(f, r.Start), - End: toProtocolPosition(f, r.End), - } -} - -// fromProtocolPosition converts a protocol position (0-based line and column -// number) to a token.Pos (byte offset value). -// It requires the token file the pos belongs to in order to do this. -func fromProtocolPosition(f *token.File, pos protocol.Position) token.Pos { - line := lineStart(f, int(pos.Line)+1) - return line + token.Pos(pos.Character) // TODO: this is wrong, bytes not characters -} - -// toProtocolPosition converts from a token pos (byte offset) to a protocol -// position (0-based line and column number) -// It requires the token file the pos belongs to in order to do this. -func toProtocolPosition(f *token.File, pos token.Pos) protocol.Position { - if !pos.IsValid() { - return protocol.Position{Line: -1.0, Character: -1.0} - } - p := f.Position(pos) - return protocol.Position{ - Line: float64(p.Line - 1), - Character: float64(p.Column - 1), - } -} - -// fromTokenPosition converts a token.Position (1-based line and column -// number) to a token.Pos (byte offset value). -// It requires the token file the pos belongs to in order to do this. -func fromTokenPosition(f *token.File, pos token.Position) token.Pos { - line := lineStart(f, pos.Line) - return line + token.Pos(pos.Column-1) // TODO: this is wrong, bytes not characters -} - -// this functionality was borrowed from the analysisutil package -func lineStart(f *token.File, line int) token.Pos { - // Use binary search to find the start offset of this line. - // - // TODO(rstambler): eventually replace this function with the - // simpler and more efficient (*go/token.File).LineStart, added - // in go1.12. - - min := 0 // inclusive - max := f.Size() // exclusive - for { - offset := (min + max) / 2 - pos := f.Pos(offset) - posn := f.Position(pos) - if posn.Line == line { - return pos - (token.Pos(posn.Column) - 1) - } - - if min+1 >= max { - return token.NoPos - } - - if posn.Line < line { - min = offset - } else { - max = offset - } - } -} diff --git a/internal/lsp/protocol/printers.go b/internal/lsp/protocol/printers.go index 7b16cc9f..33423e32 100644 --- a/internal/lsp/protocol/printers.go +++ b/internal/lsp/protocol/printers.go @@ -16,28 +16,6 @@ import ( "fmt" ) -func (p Position) Format(f fmt.State, c rune) { - fmt.Fprintf(f, "%d", int(p.Line)+1) - if p.Character >= 0 { - fmt.Fprintf(f, ":%d", int(p.Character)+1) - } -} - -func (r Range) Format(f fmt.State, c rune) { - switch { - case r.Start == r.End || r.End.Line < 0: - fmt.Fprintf(f, "%v", r.Start) - case r.End.Line == r.Start.Line: - fmt.Fprintf(f, "%v¦%d", r.Start, int(r.End.Character)+1) - default: - fmt.Fprintf(f, "%v¦%v", r.Start, r.End) - } -} - -func (l Location) Format(f fmt.State, c rune) { - fmt.Fprintf(f, "%s:%v", l.URI, l.Range) -} - func (s DiagnosticSeverity) Format(f fmt.State, c rune) { switch s { case SeverityError: @@ -51,14 +29,6 @@ func (s DiagnosticSeverity) Format(f fmt.State, c rune) { } } -func (d Diagnostic) Format(f fmt.State, c rune) { - fmt.Fprintf(f, "%v:%v from %v at %v: %v", d.Severity, d.Code, d.Source, d.Range, d.Message) -} - -func (i CompletionItem) Format(f fmt.State, c rune) { - fmt.Fprintf(f, "%v %v %v", i.Label, i.Detail, CompletionItemKind(i.Kind)) -} - func (k CompletionItemKind) Format(f fmt.State, c rune) { switch k { case StructCompletion: diff --git a/internal/lsp/protocol/span.go b/internal/lsp/protocol/span.go new file mode 100644 index 00000000..52a95409 --- /dev/null +++ b/internal/lsp/protocol/span.go @@ -0,0 +1,79 @@ +// Copyright 2018 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. + +// this file contains protocol<->span converters + +package protocol + +import ( + "go/token" + "golang.org/x/tools/internal/span" +) + +type ColumnMapper struct { + URI span.URI + Converter *span.TokenConverter + Content []byte +} + +func NewURI(uri span.URI) string { + return string(uri) +} + +func NewColumnMapper(uri span.URI, fset *token.FileSet, f *token.File, content []byte) *ColumnMapper { + return &ColumnMapper{ + URI: uri, + Converter: span.NewTokenConverter(fset, f), + Content: content, + } +} + +func (m *ColumnMapper) Location(s span.Span) Location { + return Location{ + URI: NewURI(s.URI), + Range: m.Range(s), + } +} + +func (m *ColumnMapper) Range(s span.Span) Range { + return Range{ + Start: m.Position(s.Start), + End: m.Position(s.End), + } +} + +func (m *ColumnMapper) Position(p span.Point) Position { + chr := span.ToUTF16Column(m.Converter, p, m.Content) + return Position{ + Line: float64(p.Line - 1), + Character: float64(chr - 1), + } +} + +func (m *ColumnMapper) Span(l Location) span.Span { + return span.Span{ + URI: m.URI, + Start: m.Point(l.Range.Start), + End: m.Point(l.Range.End), + }.Clean(m.Converter) +} + +func (m *ColumnMapper) RangeSpan(r Range) span.Span { + return span.Span{ + URI: m.URI, + Start: m.Point(r.Start), + End: m.Point(r.End), + }.Clean(m.Converter) +} + +func (m *ColumnMapper) PointSpan(p Position) span.Span { + return span.Span{ + URI: m.URI, + Start: m.Point(p), + }.Clean(m.Converter) +} + +func (m *ColumnMapper) Point(p Position) span.Point { + return span.FromUTF16Column(m.Converter, int(p.Line)+1, int(p.Character)+1, m.Content) +} diff --git a/internal/lsp/server.go b/internal/lsp/server.go index f9a22346..ea753b2c 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -14,13 +14,13 @@ import ( "net" "os" "sync" - "unicode/utf8" "golang.org/x/tools/go/packages" "golang.org/x/tools/internal/jsonrpc2" "golang.org/x/tools/internal/lsp/cache" "golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/internal/span" ) // RunServer starts an LSP server on the supplied stream, and waits until the @@ -90,15 +90,11 @@ func (s *server) Initialize(ctx context.Context, params *protocol.InitializePara } s.signatureHelpEnabled = true - var rootURI string + var rootURI span.URI if params.RootURI != "" { - rootURI = params.RootURI + rootURI = span.URI(params.RootURI) } - sourceURI, err := fromProtocolURI(rootURI) - if err != nil { - return nil, err - } - rootPath, err := sourceURI.Filename() + rootPath, err := rootURI.Filename() if err != nil { return nil, err } @@ -188,38 +184,7 @@ func (s *server) ExecuteCommand(context.Context, *protocol.ExecuteCommandParams) } func (s *server) DidOpen(ctx context.Context, params *protocol.DidOpenTextDocumentParams) error { - s.cacheAndDiagnose(ctx, params.TextDocument.URI, params.TextDocument.Text) - return nil -} - -func bytesOffset(content []byte, pos protocol.Position) int { - var line, char, offset int - - for { - if line == int(pos.Line) && char == int(pos.Character) { - return offset - } - if len(content) == 0 { - return -1 - } - - r, size := utf8.DecodeRune(content) - char++ - // The offsets are based on a UTF-16 string representation. - // So the rune should be checked twice for two code units in UTF-16. - if r >= 0x10000 { - if line == int(pos.Line) && char == int(pos.Character) { - return offset - } - char++ - } - offset += size - content = content[size:] - if r == '\n' { - line++ - char = 0 - } - } + return s.cacheAndDiagnose(ctx, span.URI(params.TextDocument.URI), params.TextDocument.Text) } func (s *server) applyChanges(ctx context.Context, params *protocol.DidChangeTextDocumentParams) (string, error) { @@ -232,30 +197,23 @@ func (s *server) applyChanges(ctx context.Context, params *protocol.DidChangeTex return change.Text, nil } - sourceURI, err := fromProtocolURI(params.TextDocument.URI) - if err != nil { - return "", err - } - - file, err := s.view.GetFile(ctx, sourceURI) + file, m, err := newColumnMap(ctx, s.view, span.URI(params.TextDocument.URI)) if err != nil { return "", jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "file not found") } - content := file.GetContent(ctx) for _, change := range params.ContentChanges { - start := bytesOffset(content, change.Range.Start) - if start == -1 { + spn := m.RangeSpan(*change.Range).Clean(nil) + if spn.Start.Offset <= 0 { return "", jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "invalid range for content change") } - end := bytesOffset(content, change.Range.End) - if end == -1 { + if spn.End.Offset <= spn.Start.Offset { return "", jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "invalid range for content change") } var buf bytes.Buffer - buf.Write(content[:start]) + buf.Write(content[:spn.Start.Offset]) buf.WriteString(change.Text) - buf.Write(content[end:]) + buf.Write(content[spn.End.Offset:]) content = buf.Bytes() } return string(content), nil @@ -282,8 +240,7 @@ func (s *server) DidChange(ctx context.Context, params *protocol.DidChangeTextDo } text = change.Text } - s.cacheAndDiagnose(ctx, params.TextDocument.URI, text) - return nil + return s.cacheAndDiagnose(ctx, span.URI(params.TextDocument.URI), text) } func (s *server) WillSave(context.Context, *protocol.WillSaveTextDocumentParams) error { @@ -299,26 +256,17 @@ func (s *server) DidSave(context.Context, *protocol.DidSaveTextDocumentParams) e } func (s *server) DidClose(ctx context.Context, params *protocol.DidCloseTextDocumentParams) error { - sourceURI, err := fromProtocolURI(params.TextDocument.URI) - if err != nil { - return err - } - s.setContent(ctx, sourceURI, nil) + s.setContent(ctx, span.URI(params.TextDocument.URI), nil) return nil } func (s *server) Completion(ctx context.Context, params *protocol.CompletionParams) (*protocol.CompletionList, error) { - sourceURI, err := fromProtocolURI(params.TextDocument.URI) + f, m, err := newColumnMap(ctx, s.view, span.URI(params.TextDocument.URI)) if err != nil { return nil, err } - f, err := s.view.GetFile(ctx, sourceURI) - if err != nil { - return nil, err - } - tok := f.GetToken(ctx) - pos := fromProtocolPosition(tok, params.Position) - items, prefix, err := source.Completion(ctx, f, pos) + spn := m.PointSpan(params.Position) + items, prefix, err := source.Completion(ctx, f, spn.Range(m.Converter).Start) if err != nil { return nil, err } @@ -333,17 +281,12 @@ func (s *server) CompletionResolve(context.Context, *protocol.CompletionItem) (* } func (s *server) Hover(ctx context.Context, params *protocol.TextDocumentPositionParams) (*protocol.Hover, error) { - sourceURI, err := fromProtocolURI(params.TextDocument.URI) + f, m, err := newColumnMap(ctx, s.view, span.URI(params.TextDocument.URI)) if err != nil { return nil, err } - f, err := s.view.GetFile(ctx, sourceURI) - if err != nil { - return nil, err - } - tok := f.GetToken(ctx) - pos := fromProtocolPosition(tok, params.Position) - ident, err := source.Identifier(ctx, s.view, f, pos) + spn := m.PointSpan(params.Position) + ident, err := source.Identifier(ctx, s.view, f, spn.Range(m.Converter).Start) if err != nil { return nil, err } @@ -352,28 +295,23 @@ func (s *server) Hover(ctx context.Context, params *protocol.TextDocumentPositio return nil, err } markdown := "```go\n" + content + "\n```" - x := toProtocolRange(tok, ident.Range) + rng := m.Range(ident.Range.Span()) return &protocol.Hover{ Contents: protocol.MarkupContent{ Kind: protocol.Markdown, Value: markdown, }, - Range: &x, + Range: &rng, }, nil } func (s *server) SignatureHelp(ctx context.Context, params *protocol.TextDocumentPositionParams) (*protocol.SignatureHelp, error) { - sourceURI, err := fromProtocolURI(params.TextDocument.URI) + f, m, err := newColumnMap(ctx, s.view, span.URI(params.TextDocument.URI)) if err != nil { return nil, err } - f, err := s.view.GetFile(ctx, sourceURI) - if err != nil { - return nil, err - } - tok := f.GetToken(ctx) - pos := fromProtocolPosition(tok, params.Position) - info, err := source.SignatureHelp(ctx, f, pos) + spn := m.PointSpan(params.Position) + info, err := source.SignatureHelp(ctx, f, spn.Range(m.Converter).Start) if err != nil { return nil, err } @@ -381,39 +319,29 @@ func (s *server) SignatureHelp(ctx context.Context, params *protocol.TextDocumen } func (s *server) Definition(ctx context.Context, params *protocol.TextDocumentPositionParams) ([]protocol.Location, error) { - sourceURI, err := fromProtocolURI(params.TextDocument.URI) + f, m, err := newColumnMap(ctx, s.view, span.URI(params.TextDocument.URI)) if err != nil { return nil, err } - f, err := s.view.GetFile(ctx, sourceURI) + spn := m.PointSpan(params.Position) + ident, err := source.Identifier(ctx, s.view, f, spn.Range(m.Converter).Start) if err != nil { return nil, err } - tok := f.GetToken(ctx) - pos := fromProtocolPosition(tok, params.Position) - ident, err := source.Identifier(ctx, s.view, f, pos) - if err != nil { - return nil, err - } - return []protocol.Location{toProtocolLocation(s.view.FileSet(), ident.Declaration.Range)}, nil + return []protocol.Location{m.Location(ident.Declaration.Range.Span())}, nil } func (s *server) TypeDefinition(ctx context.Context, params *protocol.TextDocumentPositionParams) ([]protocol.Location, error) { - sourceURI, err := fromProtocolURI(params.TextDocument.URI) + f, m, err := newColumnMap(ctx, s.view, span.URI(params.TextDocument.URI)) if err != nil { return nil, err } - f, err := s.view.GetFile(ctx, sourceURI) + spn := m.PointSpan(params.Position) + ident, err := source.Identifier(ctx, s.view, f, spn.Range(m.Converter).Start) if err != nil { return nil, err } - tok := f.GetToken(ctx) - pos := fromProtocolPosition(tok, params.Position) - ident, err := source.Identifier(ctx, s.view, f, pos) - if err != nil { - return nil, err - } - return []protocol.Location{toProtocolLocation(s.view.FileSet(), ident.Type.Range)}, nil + return []protocol.Location{m.Location(ident.Type.Range.Span())}, nil } func (s *server) Implementation(context.Context, *protocol.TextDocumentPositionParams) ([]protocol.Location, error) { @@ -433,7 +361,12 @@ func (s *server) DocumentSymbol(context.Context, *protocol.DocumentSymbolParams) } func (s *server) CodeAction(ctx context.Context, params *protocol.CodeActionParams) ([]protocol.CodeAction, error) { - edits, err := organizeImports(ctx, s.view, params.TextDocument.URI) + _, m, err := newColumnMap(ctx, s.view, span.URI(params.TextDocument.URI)) + if err != nil { + return nil, err + } + spn := m.RangeSpan(params.Range) + edits, err := organizeImports(ctx, s.view, spn) if err != nil { return nil, err } @@ -475,11 +408,17 @@ func (s *server) ColorPresentation(context.Context, *protocol.ColorPresentationP } func (s *server) Formatting(ctx context.Context, params *protocol.DocumentFormattingParams) ([]protocol.TextEdit, error) { - return formatRange(ctx, s.view, params.TextDocument.URI, nil) + spn := span.Span{URI: span.URI(params.TextDocument.URI)} + return formatRange(ctx, s.view, spn) } func (s *server) RangeFormatting(ctx context.Context, params *protocol.DocumentRangeFormattingParams) ([]protocol.TextEdit, error) { - return formatRange(ctx, s.view, params.TextDocument.URI, ¶ms.Range) + _, m, err := newColumnMap(ctx, s.view, span.URI(params.TextDocument.URI)) + if err != nil { + return nil, err + } + spn := m.RangeSpan(params.Range) + return formatRange(ctx, s.view, spn) } func (s *server) OnTypeFormatting(context.Context, *protocol.DocumentOnTypeFormattingParams) ([]protocol.TextEdit, error) { diff --git a/internal/lsp/source/definition.go b/internal/lsp/source/definition.go index edffa049..5b862683 100644 --- a/internal/lsp/source/definition.go +++ b/internal/lsp/source/definition.go @@ -12,19 +12,20 @@ import ( "go/types" "golang.org/x/tools/go/ast/astutil" + "golang.org/x/tools/internal/span" ) // IdentifierInfo holds information about an identifier in Go source. type IdentifierInfo struct { Name string - Range Range + Range span.Range File File Type struct { - Range Range + Range span.Range Object types.Object } Declaration struct { - Range Range + Range span.Range Object types.Object } @@ -83,7 +84,7 @@ func identifier(ctx context.Context, v View, f File, pos token.Pos) (*Identifier } } result.Name = result.ident.Name - result.Range = Range{Start: result.ident.Pos(), End: result.ident.End()} + result.Range = span.NewRange(v.FileSet(), result.ident.Pos(), result.ident.End()) result.Declaration.Object = pkg.GetTypesInfo().ObjectOf(result.ident) if result.Declaration.Object == nil { return nil, fmt.Errorf("no object for ident %v", result.Name) @@ -125,48 +126,10 @@ func typeToObject(typ types.Type) types.Object { } } -func objToRange(ctx context.Context, v View, obj types.Object) (Range, error) { +func objToRange(ctx context.Context, v View, obj types.Object) (span.Range, error) { p := obj.Pos() if !p.IsValid() { - return Range{}, fmt.Errorf("invalid position for %v", obj.Name()) - } - return Range{ - Start: p, - End: p + token.Pos(identifierLen(obj.Name())), - }, nil -} - -// TODO: This needs to be fixed to address golang.org/issue/29149. -func identifierLen(ident string) int { - return len([]byte(ident)) -} - -// this functionality was borrowed from the analysisutil package -func lineStart(f *token.File, line int) token.Pos { - // Use binary search to find the start offset of this line. - // - // TODO(rstambler): eventually replace this function with the - // simpler and more efficient (*go/token.File).LineStart, added - // in go1.12. - - min := 0 // inclusive - max := f.Size() // exclusive - for { - offset := (min + max) / 2 - pos := f.Pos(offset) - posn := f.Position(pos) - if posn.Line == line { - return pos - (token.Pos(posn.Column) - 1) - } - - if min+1 >= max { - return token.NoPos - } - - if posn.Line < line { - min = offset - } else { - max = offset - } + return span.Range{}, fmt.Errorf("invalid position for %v", obj.Name()) } + return span.NewRange(v.FileSet(), p, p+token.Pos(len(obj.Name()))), nil } diff --git a/internal/lsp/source/diagnostics.go b/internal/lsp/source/diagnostics.go index 98e42074..f3945506 100644 --- a/internal/lsp/source/diagnostics.go +++ b/internal/lsp/source/diagnostics.go @@ -8,9 +8,6 @@ import ( "bytes" "context" "fmt" - "go/token" - "strconv" - "strings" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/asmdecl" @@ -35,12 +32,12 @@ import ( "golang.org/x/tools/go/analysis/passes/unreachable" "golang.org/x/tools/go/analysis/passes/unsafeptr" "golang.org/x/tools/go/analysis/passes/unusedresult" - "golang.org/x/tools/go/packages" + "golang.org/x/tools/internal/span" ) type Diagnostic struct { - Range + span.Span Message string Source string Severity DiagnosticSeverity @@ -53,16 +50,16 @@ const ( SeverityError ) -func Diagnostics(ctx context.Context, v View, uri URI) (map[string][]Diagnostic, error) { +func Diagnostics(ctx context.Context, v View, uri span.URI) (map[span.URI][]Diagnostic, error) { f, err := v.GetFile(ctx, uri) if err != nil { return nil, err } pkg := f.GetPackage(ctx) // Prepare the reports we will send for this package. - reports := make(map[string][]Diagnostic) + reports := make(map[span.URI][]Diagnostic) for _, filename := range pkg.GetFilenames() { - reports[filename] = []Diagnostic{} + reports[span.FileURI(filename)] = []Diagnostic{} } var parseErrors, typeErrors []packages.Error for _, err := range pkg.GetErrors() { @@ -82,35 +79,27 @@ func Diagnostics(ctx context.Context, v View, uri URI) (map[string][]Diagnostic, diags = parseErrors } for _, diag := range diags { - pos := errorPos(diag) - diagFile, err := v.GetFile(ctx, ToURI(pos.Filename)) - if err != nil { - continue - } - diagTok := diagFile.GetToken(ctx) - end, err := identifierEnd(diagFile.GetContent(ctx), pos.Line, pos.Column) - // Don't set a range if it's anything other than a type error. - if err != nil || diag.Kind != packages.TypeError { - end = 0 - } - startPos := fromTokenPosition(diagTok, pos.Line, pos.Column) - if !startPos.IsValid() { - continue - } - endPos := fromTokenPosition(diagTok, pos.Line, pos.Column+end) - if !endPos.IsValid() { - continue + spn := span.Parse(diag.Pos) + if spn.IsPoint() && diag.Kind == packages.TypeError { + // Don't set a range if it's anything other than a type error. + if diagFile, err := v.GetFile(ctx, spn.URI); err == nil { + content := diagFile.GetContent(ctx) + c := span.NewTokenConverter(diagFile.GetFileSet(ctx), diagFile.GetToken(ctx)) + s := spn.CleanOffset(c) + if end := bytes.IndexAny(content[s.Start.Offset:], " \n,():;[]"); end > 0 { + spn.End = s.Start + spn.End.Column += end + spn.End.Offset += end + } + } } diagnostic := Diagnostic{ - Range: Range{ - Start: startPos, - End: endPos, - }, + Span: spn, Message: diag.Msg, Severity: SeverityError, } - if _, ok := reports[pos.Filename]; ok { - reports[pos.Filename] = append(reports[pos.Filename], diagnostic) + if _, ok := reports[spn.URI]; ok { + reports[spn.URI] = append(reports[spn.URI], diagnostic) } } if len(diags) > 0 { @@ -118,14 +107,16 @@ func Diagnostics(ctx context.Context, v View, uri URI) (map[string][]Diagnostic, } // Type checking and parsing succeeded. Run analyses. runAnalyses(ctx, v, pkg, func(a *analysis.Analyzer, diag analysis.Diagnostic) { - pos := v.FileSet().Position(diag.Pos) + r := span.NewRange(v.FileSet(), diag.Pos, 0) + s := r.Span() category := a.Name if diag.Category != "" { category += "." + category } - reports[pos.Filename] = append(reports[pos.Filename], Diagnostic{ + + reports[s.URI] = append(reports[s.URI], Diagnostic{ Source: category, - Range: Range{Start: diag.Pos, End: diag.Pos}, + Span: s, Message: fmt.Sprintf(diag.Message), Severity: SeverityWarning, }) @@ -134,57 +125,6 @@ func Diagnostics(ctx context.Context, v View, uri URI) (map[string][]Diagnostic, return reports, nil } -// fromTokenPosition converts a token.Position (1-based line and column -// number) to a token.Pos (byte offset value). This requires the token.File -// to which the token.Pos belongs. -func fromTokenPosition(f *token.File, line, col int) token.Pos { - linePos := lineStart(f, line) - // TODO: This is incorrect, as pos.Column represents bytes, not characters. - // This needs to be handled to address golang.org/issue/29149. - return linePos + token.Pos(col-1) -} - -func errorPos(pkgErr packages.Error) token.Position { - remainder1, first, hasLine := chop(pkgErr.Pos) - remainder2, second, hasColumn := chop(remainder1) - var pos token.Position - if hasLine && hasColumn { - pos.Filename = remainder2 - pos.Line = second - pos.Column = first - } else if hasLine { - pos.Filename = remainder1 - pos.Line = first - } - return pos -} - -func chop(text string) (remainder string, value int, ok bool) { - i := strings.LastIndex(text, ":") - if i < 0 { - return text, 0, false - } - v, err := strconv.ParseInt(text[i+1:], 10, 64) - if err != nil { - return text, 0, false - } - return text[:i], int(v), true -} - -// identifierEnd returns the length of an identifier within a string, -// given the starting line and column numbers of the identifier. -func identifierEnd(content []byte, l, c int) (int, error) { - lines := bytes.Split(content, []byte("\n")) - if len(lines) < l { - return 0, fmt.Errorf("invalid line number: got %v, but only %v lines", l, len(lines)) - } - line := lines[l-1] - if len(line) < c { - return 0, fmt.Errorf("invalid column number: got %v, but the length of the line is %v", c, len(line)) - } - return bytes.IndexAny(line[c-1:], " \n,():;[]"), nil -} - func runAnalyses(ctx context.Context, v View, pkg Package, report func(a *analysis.Analyzer, diag analysis.Diagnostic)) error { // the traditional vet suite: analyzers := []*analysis.Analyzer{ diff --git a/internal/lsp/source/format.go b/internal/lsp/source/format.go index d1a89dda..785ab9d9 100644 --- a/internal/lsp/source/format.go +++ b/internal/lsp/source/format.go @@ -11,16 +11,16 @@ import ( "fmt" "go/ast" "go/format" - "go/token" "strings" "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/imports" "golang.org/x/tools/internal/lsp/diff" + "golang.org/x/tools/internal/span" ) // Format formats a file with a given range. -func Format(ctx context.Context, f File, rng Range) ([]TextEdit, error) { +func Format(ctx context.Context, f File, rng span.Range) ([]TextEdit, error) { fAST := f.GetAST(ctx) path, exact := astutil.PathEnclosingInterval(fAST, rng.Start, rng.End) if !exact || len(path) == 0 { @@ -56,7 +56,7 @@ func Format(ctx context.Context, f File, rng Range) ([]TextEdit, error) { } // Imports formats a file using the goimports tool. -func Imports(ctx context.Context, f File, rng Range) ([]TextEdit, error) { +func Imports(ctx context.Context, f File, rng span.Range) ([]TextEdit, error) { formatted, err := imports.Process(f.GetToken(ctx).Name(), f.GetContent(ctx), nil) if err != nil { return nil, err @@ -66,35 +66,19 @@ func Imports(ctx context.Context, f File, rng Range) ([]TextEdit, error) { func computeTextEdits(ctx context.Context, file File, formatted string) (edits []TextEdit) { u := strings.SplitAfter(string(file.GetContent(ctx)), "\n") - tok := file.GetToken(ctx) f := strings.SplitAfter(formatted, "\n") for _, op := range diff.Operations(u, f) { - start := lineStart(tok, op.I1+1) - if start == token.NoPos && op.I1 == len(u) { - start = tok.Pos(tok.Size()) - } - end := lineStart(tok, op.I2+1) - if end == token.NoPos && op.I2 == len(u) { - end = tok.Pos(tok.Size()) + s := span.Span{ + Start: span.Point{Line: op.I1 + 1}, + End: span.Point{Line: op.I2 + 1}, } switch op.Kind { case diff.Delete: // Delete: unformatted[i1:i2] is deleted. - edits = append(edits, TextEdit{ - Range: Range{ - Start: start, - End: end, - }, - }) + edits = append(edits, TextEdit{Span: s}) case diff.Insert: // Insert: formatted[j1:j2] is inserted at unformatted[i1:i1]. - edits = append(edits, TextEdit{ - Range: Range{ - Start: start, - End: start, - }, - NewText: op.Content, - }) + edits = append(edits, TextEdit{Span: s, NewText: op.Content}) } } return edits diff --git a/internal/lsp/source/uri.go b/internal/lsp/source/uri.go deleted file mode 100644 index d630e704..00000000 --- a/internal/lsp/source/uri.go +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright 2018 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 source - -import ( - "fmt" - "net/url" - "path/filepath" - "runtime" - "strings" - "unicode" -) - -const fileScheme = "file" - -// URI represents the full URI for a file. -type URI string - -// Filename gets the file path for the URI. -// It will return an error if the uri is not valid, or if the URI was not -// a file URI -func (uri URI) Filename() (string, error) { - filename, err := filename(uri) - if err != nil { - return "", err - } - return filepath.FromSlash(filename), nil -} - -func filename(uri URI) (string, error) { - u, err := url.ParseRequestURI(string(uri)) - if err != nil { - return "", err - } - if u.Scheme != fileScheme { - return "", fmt.Errorf("only file URIs are supported, got %v", u.Scheme) - } - if isWindowsDriveURI(u.Path) { - u.Path = u.Path[1:] - } - return u.Path, nil -} - -// ToURI returns a protocol URI for the supplied path. -// It will always have the file scheme. -func ToURI(path string) URI { - u := toURI(path) - u.Path = filepath.ToSlash(u.Path) - return URI(u.String()) -} - -func toURI(path string) *url.URL { - // 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) { - path = "/" + path - } - return &url.URL{ - Scheme: fileScheme, - Path: path, - } -} - -// 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 ":". -func isWindowsDrivePath(path string) bool { - if len(path) < 4 { - return false - } - return unicode.IsLetter(rune(path[0])) && path[1] == ':' -} - -// isWindowsDriveURI 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 https://github.com/golang/go/issues/6027). We check if the URI path has -// a drive prefix (e.g. "/C:"). If so, we trim the leading "/". -func isWindowsDriveURI(uri string) bool { - if len(uri) < 4 { - return false - } - return uri[0] == '/' && unicode.IsLetter(rune(uri[1])) && uri[2] == ':' -} diff --git a/internal/lsp/source/uri_test.go b/internal/lsp/source/uri_test.go deleted file mode 100644 index e3d1e7d8..00000000 --- a/internal/lsp/source/uri_test.go +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2018 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 source - -import ( - "testing" -) - -// 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 TestURI(t *testing.T) { - for _, tt := range []struct { - uri URI - filename string - }{ - { - uri: URI(`file:///C:/Windows/System32`), - filename: `C:/Windows/System32`, - }, - { - uri: URI(`file:///C:/Go/src/bob.go`), - filename: `C:/Go/src/bob.go`, - }, - { - uri: URI(`file:///c:/Go/src/bob.go`), - filename: `c:/Go/src/bob.go`, - }, - { - uri: URI(`file:///path/to/dir`), - filename: `/path/to/dir`, - }, - { - uri: URI(`file:///a/b/c/src/bob.go`), - filename: `/a/b/c/src/bob.go`, - }, - } { - if string(tt.uri) != toURI(tt.filename).String() { - t.Errorf("ToURI: expected %s, got %s", tt.uri, ToURI(tt.filename)) - } - filename, err := filename(tt.uri) - if err != nil { - t.Fatal(err) - } - if tt.filename != filename { - t.Errorf("Filename: expected %s, got %s", tt.filename, filename) - } - } -} diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go index c0e6f01c..1930734c 100644 --- a/internal/lsp/source/view.go +++ b/internal/lsp/source/view.go @@ -12,14 +12,15 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/packages" + "golang.org/x/tools/internal/span" ) // View abstracts the underlying architecture of the package using the source // package. The view provides access to files and their contents, so the source // package does not directly access the file system. type View interface { - GetFile(ctx context.Context, uri URI) (File, error) - SetContent(ctx context.Context, uri URI, content []byte) error + GetFile(ctx context.Context, uri span.URI) (File, error) + SetContent(ctx context.Context, uri span.URI, content []byte) error FileSet() *token.FileSet } @@ -28,6 +29,7 @@ type View interface { // building blocks for most queries. Users of the source package can abstract // the loading of packages into their own caching systems. type File interface { + URI() span.URI GetAST(ctx context.Context) *ast.File GetFileSet(ctx context.Context) *token.FileSet GetPackage(ctx context.Context) Package @@ -46,18 +48,9 @@ type Package interface { GetActionGraph(ctx context.Context, a *analysis.Analyzer) (*Action, error) } -// Range represents a start and end position. -// Because Range is based purely on two token.Pos entries, it is not self -// contained. You need access to a token.FileSet to regain the file -// information. -type Range struct { - Start token.Pos - End token.Pos -} - // TextEdit represents a change to a section of a document. -// The text within the specified range should be replaced by the supplied new text. +// The text within the specified span should be replaced by the supplied new text. type TextEdit struct { - Range Range + Span span.Span NewText string } diff --git a/internal/span/token.go b/internal/span/token.go index c03ebc4e..fb428094 100644 --- a/internal/span/token.go +++ b/internal/span/token.go @@ -43,7 +43,7 @@ func NewTokenConverter(fset *token.FileSet, f *token.File) *TokenConverter { // NewContentConverter returns an implementation of Coords and Offsets for the // given file content. -func NewContentConverter(filename string, content []byte) Converter { +func NewContentConverter(filename string, content []byte) *TokenConverter { fset := token.NewFileSet() f := fset.AddFile(filename, -1, len(content)) f.SetLinesForContent(content) diff --git a/internal/span/utf16.go b/internal/span/utf16.go index 94339993..4cc16a17 100644 --- a/internal/span/utf16.go +++ b/internal/span/utf16.go @@ -14,7 +14,7 @@ import ( // This is used to convert from the native (always in bytes) column // representation and the utf16 counts used by some editors. func ToUTF16Column(offsets Offsets, p Point, content []byte) int { - if content == nil { + if content == nil || p.Column < 1 { return -1 } // make sure we have a valid offset