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 <rstambler@golang.org>
This commit is contained in:
Ian Cottrell 2019-02-19 21:11:15 -05:00
parent d55b9fb8ef
commit dbad8e90c9
24 changed files with 338 additions and 897 deletions

View File

@ -16,10 +16,10 @@ import (
"golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/packages" "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() v.mcache.mu.Lock()
defer v.mcache.mu.Unlock() defer v.mcache.mu.Unlock()
@ -78,7 +78,7 @@ func (v *View) cachePackage(pkg *Package) {
log.Printf("no token.File for %v", file.Name) log.Printf("no token.File for %v", file.Name)
continue continue
} }
fURI := source.ToURI(tok.Name()) fURI := span.FileURI(tok.Name())
f := v.getFile(fURI) f := v.getFile(fURI)
f.token = tok f.token = tok
f.ast = file f.ast = file
@ -88,7 +88,7 @@ func (v *View) cachePackage(pkg *Package) {
} }
func (v *View) checkMetadata(ctx context.Context, f *File) error { func (v *View) checkMetadata(ctx context.Context, f *File) error {
filename, err := f.URI.Filename() filename, err := f.uri.Filename()
if err != nil { if err != nil {
return err return err
} }
@ -155,7 +155,7 @@ func (v *View) link(pkgPath string, pkg *packages.Package, parent *metadata) *me
m.name = pkg.Name m.name = pkg.Name
m.files = pkg.CompiledGoFiles m.files = pkg.CompiledGoFiles
for _, filename := range m.files { 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 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. // 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 var fAST *ast.File
if f != nil { if f != nil {
fAST = f.ast fAST = f.ast

View File

@ -11,11 +11,12 @@ import (
"io/ioutil" "io/ioutil"
"golang.org/x/tools/internal/lsp/source" "golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/span"
) )
// File holds all the information we know about a file. // File holds all the information we know about a file.
type File struct { type File struct {
URI source.URI uri span.URI
view *View view *View
active bool active bool
content []byte content []byte
@ -26,6 +27,10 @@ type File struct {
imports []*ast.ImportSpec 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. // GetContent returns the contents of the file, reading it from file system if needed.
func (f *File) GetContent(ctx context.Context) []byte { func (f *File) GetContent(ctx context.Context) []byte {
f.view.mu.Lock() f.view.mu.Lock()
@ -47,7 +52,7 @@ func (f *File) GetToken(ctx context.Context) *token.File {
defer f.view.mu.Unlock() defer f.view.mu.Unlock()
if f.token == nil || len(f.view.contentChanges) > 0 { 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 return nil
} }
} }
@ -59,7 +64,7 @@ func (f *File) GetAST(ctx context.Context) *ast.File {
defer f.view.mu.Unlock() defer f.view.mu.Unlock()
if f.ast == nil || len(f.view.contentChanges) > 0 { 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 return nil
} }
} }
@ -71,7 +76,7 @@ func (f *File) GetPackage(ctx context.Context) source.Package {
defer f.view.mu.Unlock() defer f.view.mu.Unlock()
if f.pkg == nil || len(f.view.contentChanges) > 0 { 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 return nil
} }
} }
@ -95,7 +100,7 @@ func (f *File) read(ctx context.Context) {
} }
} }
// We don't know the content yet, so read it. // We don't know the content yet, so read it.
filename, err := f.URI.Filename() filename, err := f.uri.Filename()
if err != nil { if err != nil {
return return
} }

View File

@ -11,6 +11,7 @@ import (
"golang.org/x/tools/go/packages" "golang.org/x/tools/go/packages"
"golang.org/x/tools/internal/lsp/source" "golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/span"
) )
type View struct { type View struct {
@ -30,14 +31,14 @@ type View struct {
Config packages.Config Config packages.Config
// files caches information for opened files in a view. // 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. // 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 // When type information is requested by the view, all of the dirty changes
// are applied, potentially invalidating some data in the caches. The // are applied, potentially invalidating some data in the caches. The
// closures in the dirty slice assume that their caller is holding the // closures in the dirty slice assume that their caller is holding the
// view's mutex. // 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 caches metadata for the packages of the opened files in a view.
mcache *metadataCache mcache *metadataCache
@ -74,8 +75,8 @@ func NewView(config *packages.Config) *View {
backgroundCtx: ctx, backgroundCtx: ctx,
cancel: cancel, cancel: cancel,
Config: *config, Config: *config,
files: make(map[source.URI]*File), files: make(map[span.URI]*File),
contentChanges: make(map[source.URI]func()), contentChanges: make(map[span.URI]func()),
mcache: &metadataCache{ mcache: &metadataCache{
packages: make(map[string]*metadata), packages: make(map[string]*metadata),
}, },
@ -97,7 +98,7 @@ func (v *View) FileSet() *token.FileSet {
} }
// SetContent sets the overlay contents for a file. // 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() v.mu.Lock()
defer v.mu.Unlock() 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 // setContent applies a content update for a given file. It assumes that the
// caller is holding the view's mutex. // 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 := v.getFile(uri)
f.content = content f.content = content
@ -151,14 +152,14 @@ func (v *View) applyContentChange(uri source.URI, content []byte) {
case f.active && content == nil: case f.active && content == nil:
// The file was active, so we need to forget its content. // The file was active, so we need to forget its content.
f.active = false 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) delete(f.view.Config.Overlay, filename)
} }
f.content = nil f.content = nil
case content != nil: case content != nil:
// This is an active overlay, so we update the map. // This is an active overlay, so we update the map.
f.active = true 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 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 // All of the files in the package may also be holding a pointer to the
// invalidated package. // invalidated package.
for _, filename := range m.files { 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 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 // GetFile returns a File for the given URI. It will always succeed because it
// adds the file to the managed set if needed. // 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() v.mu.Lock()
defer v.mu.Unlock() 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. // 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] f, found := v.files[uri]
if !found { if !found {
f = &File{ f = &File{
URI: uri, uri: uri,
view: v, view: v,
} }
v.files[uri] = f v.files[uri] = f

View File

@ -16,20 +16,21 @@ import (
guru "golang.org/x/tools/cmd/guru/serial" guru "golang.org/x/tools/cmd/guru/serial"
"golang.org/x/tools/internal/lsp/cache" "golang.org/x/tools/internal/lsp/cache"
"golang.org/x/tools/internal/lsp/source" "golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/span"
"golang.org/x/tools/internal/tool" "golang.org/x/tools/internal/tool"
) )
// A Definition is the result of a 'definition' query. // A Definition is the result of a 'definition' query.
type Definition struct { type Definition struct {
Location Location `json:"location"` // location of the definition Span span.Span `json:"span"` // span of the definition
Description string `json:"description"` // description of the denoted object 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 // This constant is printed in the help, and then used in a test to verify the
// help is still valid. // help is still valid.
// It should be the byte offset in this file of the "Set" in "flag.FlagSet" from // It should be the byte offset in this file of the "Set" in "flag.FlagSet" from
// the DetailedHelp method below. // the DetailedHelp method below.
const exampleOffset = 1277 const exampleOffset = 1311
// definition implements the definition noun for the query command. // definition implements the definition noun for the query command.
type definition struct { type definition struct {
@ -57,11 +58,8 @@ func (d *definition) Run(ctx context.Context, args ...string) error {
return tool.CommandLineErrorf("definition expects 1 argument") return tool.CommandLineErrorf("definition expects 1 argument")
} }
view := cache.NewView(&d.query.app.Config) view := cache.NewView(&d.query.app.Config)
from, err := parseLocation(args[0]) from := span.Parse(args[0])
if err != nil { f, err := view.GetFile(ctx, from.URI)
return err
}
f, err := view.GetFile(ctx, source.ToURI(from.Filename))
if err != nil { if err != nil {
return err return err
} }
@ -72,10 +70,10 @@ func (d *definition) Run(ctx context.Context, args ...string) error {
} }
ident, err := source.Identifier(ctx, view, f, pos) ident, err := source.Identifier(ctx, view, f, pos)
if err != nil { if err != nil {
return err return fmt.Errorf("%v: %v", from, err)
} }
if ident == nil { if ident == nil {
return fmt.Errorf("not an identifier") return fmt.Errorf("%v: not an identifier", from)
} }
var result interface{} var result interface{}
switch d.query.Emulate { switch d.query.Emulate {
@ -96,7 +94,7 @@ func (d *definition) Run(ctx context.Context, args ...string) error {
} }
switch d := result.(type) { switch d := result.(type) {
case *Definition: 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: case *guru.Definition:
fmt.Printf("%s: defined here as %s", d.ObjPos, d.Desc) fmt.Printf("%s: defined here as %s", d.ObjPos, d.Desc)
default: default:
@ -111,16 +109,16 @@ func buildDefinition(ctx context.Context, view source.View, ident *source.Identi
return nil, err return nil, err
} }
return &Definition{ return &Definition{
Location: newLocation(view.FileSet(), ident.Declaration.Range), Span: ident.Declaration.Range.Span(),
Description: content, Description: content,
}, nil }, nil
} }
func buildGuruDefinition(ctx context.Context, view source.View, ident *source.IdentifierInfo) (*guru.Definition, error) { 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) pkg := ident.File.GetPackage(ctx)
// guru does not support ranges // 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 // Behavior that attempts to match the expected output for guru. For an example
// of the format, see the associated definition tests. // of the format, see the associated definition tests.
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
@ -170,7 +168,7 @@ func buildGuruDefinition(ctx context.Context, view source.View, ident *source.Id
fmt.Fprint(buf, suffix) fmt.Fprint(buf, suffix)
} }
return &guru.Definition{ return &guru.Definition{
ObjPos: fmt.Sprint(loc), ObjPos: fmt.Sprint(spn),
Desc: buf.String(), Desc: buf.String(),
}, nil }, nil
} }

View File

@ -20,6 +20,7 @@ import (
"golang.org/x/tools/go/packages/packagestest" "golang.org/x/tools/go/packages/packagestest"
"golang.org/x/tools/internal/lsp/cmd" "golang.org/x/tools/internal/lsp/cmd"
"golang.org/x/tools/internal/span"
"golang.org/x/tools/internal/tool" "golang.org/x/tools/internal/tool"
) )
@ -33,7 +34,7 @@ func TestDefinitionHelpExample(t *testing.T) {
} }
thisFile := filepath.Join(dir, "definition.go") thisFile := filepath.Join(dir, "definition.go")
args := []string{"query", "definition", fmt.Sprintf("%v:#%v", thisFile, cmd.ExampleOffset)} 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() { got := captureStdOut(t, func() {
tool.Main(context.Background(), &cmd.Application{}, args) tool.Main(context.Background(), &cmd.Application{}, args)
}) })
@ -58,14 +59,14 @@ func TestDefinition(t *testing.T) {
} }
args = append(args, "definition") args = append(args, "definition")
f := fset.File(src) f := fset.File(src)
loc := cmd.Location{ spn := span.Span{
Filename: f.Name(), URI: span.FileURI(f.Name()),
Start: cmd.Position{ Start: span.Point{
Offset: f.Offset(src), Offset: f.Offset(src),
}, },
} }
loc.End = loc.Start spn.End = spn.Start
args = append(args, fmt.Sprint(loc)) args = append(args, fmt.Sprint(spn))
app := &cmd.Application{} app := &cmd.Application{}
app.Config = *exported.Config app.Config = *exported.Config
got := captureStdOut(t, func() { got := captureStdOut(t, func() {
@ -80,6 +81,10 @@ func TestDefinition(t *testing.T) {
case "efile": case "efile":
qfile := strconv.Quote(start.Filename) qfile := strconv.Quote(start.Filename)
return qfile[1 : len(qfile)-1] 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": case "line":
return fmt.Sprint(start.Line) return fmt.Sprint(start.Line)
case "col": case "col":

View File

@ -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<file>.*):(?P<start>(?P<sline>\d+):(?P<scol>\d)+|#(?P<soff>\d+))(?P<end>:(?P<eline>\d+):(?P<ecol>\d+)|#(?P<eoff>\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)
}

View File

@ -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(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(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(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(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") definition(aMethod, "-emulate=guru", Method, "$file:$line:$col: defined here as func (Thing).Method(i int) string")
//param //param
@ -46,8 +46,8 @@ definition(aMethod, "-emulate=guru", Method, "$file:$line:$col: defined here as
// JSON tests // JSON tests
definition(aStructType, "-json", Thing, `{ definition(aStructType, "-json", Thing, `{
"location": { "span": {
"file": "$efile", "uri": "$euri",
"start": { "start": {
"line": $line, "line": $line,
"column": $col, "column": $col,

View File

@ -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(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(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(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") definition(bFunc, "-emulate=guru", Things, "$file:$line:$col: defined here as func golang.org/fake/a.Things(val []string) []golang.org/fake/a.Thing")
*/ */

View File

@ -10,42 +10,45 @@ import (
"golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source" "golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/span"
) )
func (s *server) cacheAndDiagnose(ctx context.Context, uri string, content string) { func (s *server) cacheAndDiagnose(ctx context.Context, uri span.URI, content string) error {
sourceURI, err := fromProtocolURI(uri) if err := s.setContent(ctx, uri, []byte(content)); err != nil {
if err != nil { return err
return // handle error?
}
if err := s.setContent(ctx, sourceURI, []byte(content)); err != nil {
return // handle error?
} }
go func() { go func() {
ctx := s.view.BackgroundContext() ctx := s.view.BackgroundContext()
if ctx.Err() != nil { if ctx.Err() != nil {
return return
} }
reports, err := source.Diagnostics(ctx, s.view, sourceURI) reports, err := source.Diagnostics(ctx, s.view, uri)
if err != nil { if err != nil {
return // handle error? return // handle error?
} }
for filename, diagnostics := range reports { for uri, diagnostics := range reports {
s.client.PublishDiagnostics(ctx, &protocol.PublishDiagnosticsParams{ s.client.PublishDiagnostics(ctx, &protocol.PublishDiagnosticsParams{
URI: string(source.ToURI(filename)),
Diagnostics: toProtocolDiagnostics(ctx, s.view, diagnostics), 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) return s.view.SetContent(ctx, uri, content)
} }
func toProtocolDiagnostics(ctx context.Context, v source.View, diagnostics []source.Diagnostic) []protocol.Diagnostic { func toProtocolDiagnostics(ctx context.Context, v source.View, diagnostics []source.Diagnostic) []protocol.Diagnostic {
reports := []protocol.Diagnostic{} reports := []protocol.Diagnostic{}
for _, diag := range diagnostics { 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 src := diag.Source
if src == "" { if src == "" {
src = "LSP" src = "LSP"
@ -59,7 +62,7 @@ func toProtocolDiagnostics(ctx context.Context, v source.View, diagnostics []sou
} }
reports = append(reports, protocol.Diagnostic{ reports = append(reports, protocol.Diagnostic{
Message: diag.Message, Message: diag.Message,
Range: toProtocolRange(tok, diag.Range), Range: m.Range(diag.Span),
Severity: severity, Severity: severity,
Source: src, Source: src,
}) })

View File

@ -5,56 +5,46 @@ import (
"golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source" "golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/span"
) )
// formatRange formats a document with a given range. // formatRange formats a document with a given range.
func formatRange(ctx context.Context, v source.View, uri string, rng *protocol.Range) ([]protocol.TextEdit, error) { func formatRange(ctx context.Context, v source.View, s span.Span) ([]protocol.TextEdit, error) {
sourceURI, err := fromProtocolURI(uri) f, m, err := newColumnMap(ctx, v, s.URI)
if err != nil { if err != nil {
return nil, err 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 { if err != nil {
return nil, err return nil, err
} }
tok := f.GetToken(ctx) return toProtocolEdits(m, edits), nil
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
} }
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 { if edits == nil {
return 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)) result := make([]protocol.TextEdit, len(edits))
for i, edit := range 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{ result[i] = protocol.TextEdit{
Range: rng, Range: m.Range(edit.Span),
NewText: edit.NewText, NewText: edit.NewText,
} }
} }
return result 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
}

View File

@ -9,25 +9,22 @@ import (
"golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source" "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) { func organizeImports(ctx context.Context, v source.View, s span.Span) ([]protocol.TextEdit, error) {
sourceURI, err := fromProtocolURI(uri) f, m, err := newColumnMap(ctx, v, s.URI)
if err != nil { if err != nil {
return nil, err 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 { if err != nil {
return nil, err return nil, err
} }
tok := f.GetToken(ctx) return toProtocolEdits(m, edits), nil
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
} }

View File

@ -20,6 +20,7 @@ import (
"golang.org/x/tools/internal/lsp/cache" "golang.org/x/tools/internal/lsp/cache"
"golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source" "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 // 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 completionItems map[token.Pos]*protocol.CompletionItem
type completions map[token.Position][]token.Pos type completions map[token.Position][]token.Pos
type formats map[string]string 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 { func (d diagnostics) test(t *testing.T, v source.View) int {
count := 0 count := 0
ctx := context.Background() ctx := context.Background()
for filename, want := range d { for uri, want := range d {
sourceDiagnostics, err := source.Diagnostics(context.Background(), v, source.ToURI(filename)) sourceDiagnostics, err := source.Diagnostics(context.Background(), v, uri)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
got := toProtocolDiagnostics(ctx, v, sourceDiagnostics[filename]) got := toProtocolDiagnostics(ctx, v, sourceDiagnostics[uri])
sorted(got) sorted(got)
if diff := diffDiagnostics(filename, want, got); diff != "" { if diff := diffDiagnostics(uri, want, got); diff != "" {
t.Error(diff) t.Error(diff)
} }
count += len(want) count += len(want)
@ -171,10 +172,10 @@ func (d diagnostics) test(t *testing.T, v source.View) int {
return count return count
} }
func (d diagnostics) collect(fset *token.FileSet, rng packagestest.Range, msgSource, msg string) { func (d diagnostics) collect(e *packagestest.Exported, fset *token.FileSet, rng packagestest.Range, msgSource, msg string) {
f := fset.File(rng.Start) spn, m := testLocation(e, fset, rng)
if _, ok := d[f.Name()]; !ok { if _, ok := d[spn.URI]; !ok {
d[f.Name()] = []protocol.Diagnostic{} d[spn.URI] = []protocol.Diagnostic{}
} }
// If a file has an empty diagnostic message, return. This allows us to // 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. // 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 return
} }
severity := protocol.SeverityError severity := protocol.SeverityError
if strings.Contains(f.Name(), "analyzer") { if strings.Contains(string(spn.URI), "analyzer") {
severity = protocol.SeverityWarning severity = protocol.SeverityWarning
} }
want := protocol.Diagnostic{ want := protocol.Diagnostic{
Range: toProtocolRange(f, source.Range(rng)), Range: m.Range(spn),
Severity: severity, Severity: severity,
Source: msgSource, Source: msgSource,
Message: msg, 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 // diffDiagnostics prints the diff between expected and actual diagnostics test
// results. // 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) { if len(got) != len(want) {
goto Failed goto Failed
} }
@ -209,7 +210,7 @@ func diffDiagnostics(filename string, want, got []protocol.Diagnostic) string {
goto Failed goto Failed
} }
// Special case for diagnostics on parse errors. // 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 { if g.Range.Start != g.Range.End || w.Range.Start != g.Range.End {
goto Failed goto Failed
} }
@ -228,7 +229,7 @@ func diffDiagnostics(filename string, want, got []protocol.Diagnostic) string {
return "" return ""
Failed: Failed:
msg := &bytes.Buffer{} 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 { for _, d := range want {
fmt.Fprintf(msg, " %v\n", d) 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{ list, err := s.Completion(context.Background(), &protocol.CompletionParams{
TextDocumentPositionParams: protocol.TextDocumentPositionParams{ TextDocumentPositionParams: protocol.TextDocumentPositionParams{
TextDocument: protocol.TextDocumentIdentifier{ TextDocument: protocol.TextDocumentIdentifier{
URI: string(source.ToURI(src.Filename)), URI: protocol.NewURI(span.FileURI(src.Filename)),
}, },
Position: protocol.Position{ Position: protocol.Position{
Line: float64(src.Line - 1), Line: float64(src.Line - 1),
@ -361,10 +362,12 @@ Failed:
} }
func (f formats) test(t *testing.T, s *server) { func (f formats) test(t *testing.T, s *server) {
ctx := context.Background()
for filename, gofmted := range f { for filename, gofmted := range f {
uri := span.FileURI(filename)
edits, err := s.Formatting(context.Background(), &protocol.DocumentFormattingParams{ edits, err := s.Formatting(context.Background(), &protocol.DocumentFormattingParams{
TextDocument: protocol.TextDocumentIdentifier{ TextDocument: protocol.TextDocumentIdentifier{
URI: string(source.ToURI(filename)), URI: protocol.NewURI(uri),
}, },
}) })
if err != nil { if err != nil {
@ -373,11 +376,11 @@ func (f formats) test(t *testing.T, s *server) {
} }
continue continue
} }
f, err := s.view.GetFile(context.Background(), source.ToURI(filename)) f, m, err := newColumnMap(ctx, s.view, uri)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
buf, err := applyEdits(f.GetContent(context.Background()), edits) buf, err := applyEdits(m, f.GetContent(context.Background()), edits)
if err != nil { if err != nil {
t.Error(err) 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) locs, err = s.Definition(context.Background(), params)
} }
if err != nil { if err != nil {
t.Fatalf("failed for %s: %v", src, err) t.Fatalf("failed for %v: %v", src, err)
} }
if len(locs) != 1 { if len(locs) != 1 {
t.Errorf("got %d locations for definition, expected 1", len(locs)) 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) { func (d definitions) collect(e *packagestest.Exported, fset *token.FileSet, src, target packagestest.Range) {
loc := toProtocolLocation(fset, source.Range(src)) sSrc, mSrc := testLocation(e, fset, src)
d[loc] = toProtocolLocation(fset, source.Range(target)) 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) { 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}, {text: "aaa\nbbb\n\n", pos: protocol.Position{Line: 2, Character: 0}, want: 8},
} }
for _, test := range tests { for i, test := range tests {
got := bytesOffset([]byte(test.text), test.pos) fname := fmt.Sprintf("test %d", i)
if got != test.want { fset := token.NewFileSet()
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) 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 prev := 0
result := make([]byte, 0, len(content)) result := make([]byte, 0, len(content))
for _, edit := range edits { for _, edit := range edits {
start := bytesOffset(content, edit.Range.Start) spn := m.RangeSpan(edit.Range).Clean(nil)
end := bytesOffset(content, edit.Range.End) if spn.Start.Offset > prev {
if start > prev { result = append(result, content[prev:spn.Start.Offset]...)
result = append(result, content[prev:start]...)
} }
if len(edit.NewText) > 0 { if len(edit.NewText) > 0 {
result = append(result, []byte(edit.NewText)...) result = append(result, []byte(edit.NewText)...)
} }
prev = end prev = spn.End.Offset
} }
if prev < len(content) { if prev < len(content) {
result = append(result, content[prev:]...) result = append(result, content[prev:]...)

View File

@ -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
}
}
}

View File

@ -16,28 +16,6 @@ import (
"fmt" "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) { func (s DiagnosticSeverity) Format(f fmt.State, c rune) {
switch s { switch s {
case SeverityError: 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) { func (k CompletionItemKind) Format(f fmt.State, c rune) {
switch k { switch k {
case StructCompletion: case StructCompletion:

View File

@ -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)
}

View File

@ -14,13 +14,13 @@ import (
"net" "net"
"os" "os"
"sync" "sync"
"unicode/utf8"
"golang.org/x/tools/go/packages" "golang.org/x/tools/go/packages"
"golang.org/x/tools/internal/jsonrpc2" "golang.org/x/tools/internal/jsonrpc2"
"golang.org/x/tools/internal/lsp/cache" "golang.org/x/tools/internal/lsp/cache"
"golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source" "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 // 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 s.signatureHelpEnabled = true
var rootURI string var rootURI span.URI
if params.RootURI != "" { if params.RootURI != "" {
rootURI = params.RootURI rootURI = span.URI(params.RootURI)
} }
sourceURI, err := fromProtocolURI(rootURI) rootPath, err := rootURI.Filename()
if err != nil {
return nil, err
}
rootPath, err := sourceURI.Filename()
if err != nil { if err != nil {
return nil, err 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 { func (s *server) DidOpen(ctx context.Context, params *protocol.DidOpenTextDocumentParams) error {
s.cacheAndDiagnose(ctx, params.TextDocument.URI, params.TextDocument.Text) return s.cacheAndDiagnose(ctx, span.URI(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
}
}
} }
func (s *server) applyChanges(ctx context.Context, params *protocol.DidChangeTextDocumentParams) (string, error) { 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 return change.Text, nil
} }
sourceURI, err := fromProtocolURI(params.TextDocument.URI) file, m, err := newColumnMap(ctx, s.view, span.URI(params.TextDocument.URI))
if err != nil {
return "", err
}
file, err := s.view.GetFile(ctx, sourceURI)
if err != nil { if err != nil {
return "", jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "file not found") return "", jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "file not found")
} }
content := file.GetContent(ctx) content := file.GetContent(ctx)
for _, change := range params.ContentChanges { for _, change := range params.ContentChanges {
start := bytesOffset(content, change.Range.Start) spn := m.RangeSpan(*change.Range).Clean(nil)
if start == -1 { if spn.Start.Offset <= 0 {
return "", jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "invalid range for content change") return "", jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "invalid range for content change")
} }
end := bytesOffset(content, change.Range.End) if spn.End.Offset <= spn.Start.Offset {
if end == -1 {
return "", jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "invalid range for content change") return "", jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "invalid range for content change")
} }
var buf bytes.Buffer var buf bytes.Buffer
buf.Write(content[:start]) buf.Write(content[:spn.Start.Offset])
buf.WriteString(change.Text) buf.WriteString(change.Text)
buf.Write(content[end:]) buf.Write(content[spn.End.Offset:])
content = buf.Bytes() content = buf.Bytes()
} }
return string(content), nil return string(content), nil
@ -282,8 +240,7 @@ func (s *server) DidChange(ctx context.Context, params *protocol.DidChangeTextDo
} }
text = change.Text text = change.Text
} }
s.cacheAndDiagnose(ctx, params.TextDocument.URI, text) return s.cacheAndDiagnose(ctx, span.URI(params.TextDocument.URI), text)
return nil
} }
func (s *server) WillSave(context.Context, *protocol.WillSaveTextDocumentParams) error { 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 { func (s *server) DidClose(ctx context.Context, params *protocol.DidCloseTextDocumentParams) error {
sourceURI, err := fromProtocolURI(params.TextDocument.URI) s.setContent(ctx, span.URI(params.TextDocument.URI), nil)
if err != nil {
return err
}
s.setContent(ctx, sourceURI, nil)
return nil return nil
} }
func (s *server) Completion(ctx context.Context, params *protocol.CompletionParams) (*protocol.CompletionList, error) { 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 { if err != nil {
return nil, err return nil, err
} }
f, err := s.view.GetFile(ctx, sourceURI) spn := m.PointSpan(params.Position)
if err != nil { items, prefix, err := source.Completion(ctx, f, spn.Range(m.Converter).Start)
return nil, err
}
tok := f.GetToken(ctx)
pos := fromProtocolPosition(tok, params.Position)
items, prefix, err := source.Completion(ctx, f, pos)
if err != nil { if err != nil {
return nil, err 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) { 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 { if err != nil {
return nil, err return nil, err
} }
f, err := s.view.GetFile(ctx, sourceURI) spn := m.PointSpan(params.Position)
if err != nil { ident, err := source.Identifier(ctx, s.view, f, spn.Range(m.Converter).Start)
return nil, err
}
tok := f.GetToken(ctx)
pos := fromProtocolPosition(tok, params.Position)
ident, err := source.Identifier(ctx, s.view, f, pos)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -352,28 +295,23 @@ func (s *server) Hover(ctx context.Context, params *protocol.TextDocumentPositio
return nil, err return nil, err
} }
markdown := "```go\n" + content + "\n```" markdown := "```go\n" + content + "\n```"
x := toProtocolRange(tok, ident.Range) rng := m.Range(ident.Range.Span())
return &protocol.Hover{ return &protocol.Hover{
Contents: protocol.MarkupContent{ Contents: protocol.MarkupContent{
Kind: protocol.Markdown, Kind: protocol.Markdown,
Value: markdown, Value: markdown,
}, },
Range: &x, Range: &rng,
}, nil }, nil
} }
func (s *server) SignatureHelp(ctx context.Context, params *protocol.TextDocumentPositionParams) (*protocol.SignatureHelp, error) { 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 { if err != nil {
return nil, err return nil, err
} }
f, err := s.view.GetFile(ctx, sourceURI) spn := m.PointSpan(params.Position)
if err != nil { info, err := source.SignatureHelp(ctx, f, spn.Range(m.Converter).Start)
return nil, err
}
tok := f.GetToken(ctx)
pos := fromProtocolPosition(tok, params.Position)
info, err := source.SignatureHelp(ctx, f, pos)
if err != nil { if err != nil {
return nil, err 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) { 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 { if err != nil {
return nil, err 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 { if err != nil {
return nil, err return nil, err
} }
tok := f.GetToken(ctx) return []protocol.Location{m.Location(ident.Declaration.Range.Span())}, nil
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
} }
func (s *server) TypeDefinition(ctx context.Context, params *protocol.TextDocumentPositionParams) ([]protocol.Location, error) { 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 { if err != nil {
return nil, err 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 { if err != nil {
return nil, err return nil, err
} }
tok := f.GetToken(ctx) return []protocol.Location{m.Location(ident.Type.Range.Span())}, nil
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
} }
func (s *server) Implementation(context.Context, *protocol.TextDocumentPositionParams) ([]protocol.Location, error) { 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) { 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 { if err != nil {
return nil, err 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) { 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) { func (s *server) RangeFormatting(ctx context.Context, params *protocol.DocumentRangeFormattingParams) ([]protocol.TextEdit, error) {
return formatRange(ctx, s.view, params.TextDocument.URI, &params.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) { func (s *server) OnTypeFormatting(context.Context, *protocol.DocumentOnTypeFormattingParams) ([]protocol.TextEdit, error) {

View File

@ -12,19 +12,20 @@ import (
"go/types" "go/types"
"golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/internal/span"
) )
// IdentifierInfo holds information about an identifier in Go source. // IdentifierInfo holds information about an identifier in Go source.
type IdentifierInfo struct { type IdentifierInfo struct {
Name string Name string
Range Range Range span.Range
File File File File
Type struct { Type struct {
Range Range Range span.Range
Object types.Object Object types.Object
} }
Declaration struct { Declaration struct {
Range Range Range span.Range
Object types.Object 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.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) result.Declaration.Object = pkg.GetTypesInfo().ObjectOf(result.ident)
if result.Declaration.Object == nil { if result.Declaration.Object == nil {
return nil, fmt.Errorf("no object for ident %v", result.Name) 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() p := obj.Pos()
if !p.IsValid() { if !p.IsValid() {
return Range{}, fmt.Errorf("invalid position for %v", obj.Name()) return span.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.NewRange(v.FileSet(), p, p+token.Pos(len(obj.Name()))), nil
} }

View File

@ -8,9 +8,6 @@ import (
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
"go/token"
"strconv"
"strings"
"golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/asmdecl" "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/unreachable"
"golang.org/x/tools/go/analysis/passes/unsafeptr" "golang.org/x/tools/go/analysis/passes/unsafeptr"
"golang.org/x/tools/go/analysis/passes/unusedresult" "golang.org/x/tools/go/analysis/passes/unusedresult"
"golang.org/x/tools/go/packages" "golang.org/x/tools/go/packages"
"golang.org/x/tools/internal/span"
) )
type Diagnostic struct { type Diagnostic struct {
Range span.Span
Message string Message string
Source string Source string
Severity DiagnosticSeverity Severity DiagnosticSeverity
@ -53,16 +50,16 @@ const (
SeverityError 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) f, err := v.GetFile(ctx, uri)
if err != nil { if err != nil {
return nil, err return nil, err
} }
pkg := f.GetPackage(ctx) pkg := f.GetPackage(ctx)
// Prepare the reports we will send for this package. // 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() { for _, filename := range pkg.GetFilenames() {
reports[filename] = []Diagnostic{} reports[span.FileURI(filename)] = []Diagnostic{}
} }
var parseErrors, typeErrors []packages.Error var parseErrors, typeErrors []packages.Error
for _, err := range pkg.GetErrors() { for _, err := range pkg.GetErrors() {
@ -82,35 +79,27 @@ func Diagnostics(ctx context.Context, v View, uri URI) (map[string][]Diagnostic,
diags = parseErrors diags = parseErrors
} }
for _, diag := range diags { for _, diag := range diags {
pos := errorPos(diag) spn := span.Parse(diag.Pos)
diagFile, err := v.GetFile(ctx, ToURI(pos.Filename)) if spn.IsPoint() && diag.Kind == packages.TypeError {
if err != nil { // Don't set a range if it's anything other than a type error.
continue if diagFile, err := v.GetFile(ctx, spn.URI); err == nil {
} content := diagFile.GetContent(ctx)
diagTok := diagFile.GetToken(ctx) c := span.NewTokenConverter(diagFile.GetFileSet(ctx), diagFile.GetToken(ctx))
end, err := identifierEnd(diagFile.GetContent(ctx), pos.Line, pos.Column) s := spn.CleanOffset(c)
// Don't set a range if it's anything other than a type error. if end := bytes.IndexAny(content[s.Start.Offset:], " \n,():;[]"); end > 0 {
if err != nil || diag.Kind != packages.TypeError { spn.End = s.Start
end = 0 spn.End.Column += end
} spn.End.Offset += end
startPos := fromTokenPosition(diagTok, pos.Line, pos.Column) }
if !startPos.IsValid() { }
continue
}
endPos := fromTokenPosition(diagTok, pos.Line, pos.Column+end)
if !endPos.IsValid() {
continue
} }
diagnostic := Diagnostic{ diagnostic := Diagnostic{
Range: Range{ Span: spn,
Start: startPos,
End: endPos,
},
Message: diag.Msg, Message: diag.Msg,
Severity: SeverityError, Severity: SeverityError,
} }
if _, ok := reports[pos.Filename]; ok { if _, ok := reports[spn.URI]; ok {
reports[pos.Filename] = append(reports[pos.Filename], diagnostic) reports[spn.URI] = append(reports[spn.URI], diagnostic)
} }
} }
if len(diags) > 0 { 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. // Type checking and parsing succeeded. Run analyses.
runAnalyses(ctx, v, pkg, func(a *analysis.Analyzer, diag analysis.Diagnostic) { 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 category := a.Name
if diag.Category != "" { if diag.Category != "" {
category += "." + category category += "." + category
} }
reports[pos.Filename] = append(reports[pos.Filename], Diagnostic{
reports[s.URI] = append(reports[s.URI], Diagnostic{
Source: category, Source: category,
Range: Range{Start: diag.Pos, End: diag.Pos}, Span: s,
Message: fmt.Sprintf(diag.Message), Message: fmt.Sprintf(diag.Message),
Severity: SeverityWarning, Severity: SeverityWarning,
}) })
@ -134,57 +125,6 @@ func Diagnostics(ctx context.Context, v View, uri URI) (map[string][]Diagnostic,
return reports, nil 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 { func runAnalyses(ctx context.Context, v View, pkg Package, report func(a *analysis.Analyzer, diag analysis.Diagnostic)) error {
// the traditional vet suite: // the traditional vet suite:
analyzers := []*analysis.Analyzer{ analyzers := []*analysis.Analyzer{

View File

@ -11,16 +11,16 @@ import (
"fmt" "fmt"
"go/ast" "go/ast"
"go/format" "go/format"
"go/token"
"strings" "strings"
"golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/imports" "golang.org/x/tools/imports"
"golang.org/x/tools/internal/lsp/diff" "golang.org/x/tools/internal/lsp/diff"
"golang.org/x/tools/internal/span"
) )
// Format formats a file with a given range. // 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) fAST := f.GetAST(ctx)
path, exact := astutil.PathEnclosingInterval(fAST, rng.Start, rng.End) path, exact := astutil.PathEnclosingInterval(fAST, rng.Start, rng.End)
if !exact || len(path) == 0 { 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. // 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) formatted, err := imports.Process(f.GetToken(ctx).Name(), f.GetContent(ctx), nil)
if err != nil { if err != nil {
return nil, err 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) { func computeTextEdits(ctx context.Context, file File, formatted string) (edits []TextEdit) {
u := strings.SplitAfter(string(file.GetContent(ctx)), "\n") u := strings.SplitAfter(string(file.GetContent(ctx)), "\n")
tok := file.GetToken(ctx)
f := strings.SplitAfter(formatted, "\n") f := strings.SplitAfter(formatted, "\n")
for _, op := range diff.Operations(u, f) { for _, op := range diff.Operations(u, f) {
start := lineStart(tok, op.I1+1) s := span.Span{
if start == token.NoPos && op.I1 == len(u) { Start: span.Point{Line: op.I1 + 1},
start = tok.Pos(tok.Size()) End: span.Point{Line: op.I2 + 1},
}
end := lineStart(tok, op.I2+1)
if end == token.NoPos && op.I2 == len(u) {
end = tok.Pos(tok.Size())
} }
switch op.Kind { switch op.Kind {
case diff.Delete: case diff.Delete:
// Delete: unformatted[i1:i2] is deleted. // Delete: unformatted[i1:i2] is deleted.
edits = append(edits, TextEdit{ edits = append(edits, TextEdit{Span: s})
Range: Range{
Start: start,
End: end,
},
})
case diff.Insert: case diff.Insert:
// Insert: formatted[j1:j2] is inserted at unformatted[i1:i1]. // Insert: formatted[j1:j2] is inserted at unformatted[i1:i1].
edits = append(edits, TextEdit{ edits = append(edits, TextEdit{Span: s, NewText: op.Content})
Range: Range{
Start: start,
End: start,
},
NewText: op.Content,
})
} }
} }
return edits return edits

View File

@ -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] == ':'
}

View File

@ -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)
}
}
}

View File

@ -12,14 +12,15 @@ import (
"golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/packages" "golang.org/x/tools/go/packages"
"golang.org/x/tools/internal/span"
) )
// View abstracts the underlying architecture of the package using the source // 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. The view provides access to files and their contents, so the source
// package does not directly access the file system. // package does not directly access the file system.
type View interface { type View interface {
GetFile(ctx context.Context, uri URI) (File, error) GetFile(ctx context.Context, uri span.URI) (File, error)
SetContent(ctx context.Context, uri URI, content []byte) error SetContent(ctx context.Context, uri span.URI, content []byte) error
FileSet() *token.FileSet FileSet() *token.FileSet
} }
@ -28,6 +29,7 @@ type View interface {
// building blocks for most queries. Users of the source package can abstract // building blocks for most queries. Users of the source package can abstract
// the loading of packages into their own caching systems. // the loading of packages into their own caching systems.
type File interface { type File interface {
URI() span.URI
GetAST(ctx context.Context) *ast.File GetAST(ctx context.Context) *ast.File
GetFileSet(ctx context.Context) *token.FileSet GetFileSet(ctx context.Context) *token.FileSet
GetPackage(ctx context.Context) Package GetPackage(ctx context.Context) Package
@ -46,18 +48,9 @@ type Package interface {
GetActionGraph(ctx context.Context, a *analysis.Analyzer) (*Action, error) 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. // 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 { type TextEdit struct {
Range Range Span span.Span
NewText string NewText string
} }

View File

@ -43,7 +43,7 @@ func NewTokenConverter(fset *token.FileSet, f *token.File) *TokenConverter {
// NewContentConverter returns an implementation of Coords and Offsets for the // NewContentConverter returns an implementation of Coords and Offsets for the
// given file content. // given file content.
func NewContentConverter(filename string, content []byte) Converter { func NewContentConverter(filename string, content []byte) *TokenConverter {
fset := token.NewFileSet() fset := token.NewFileSet()
f := fset.AddFile(filename, -1, len(content)) f := fset.AddFile(filename, -1, len(content))
f.SetLinesForContent(content) f.SetLinesForContent(content)

View File

@ -14,7 +14,7 @@ import (
// This is used to convert from the native (always in bytes) column // This is used to convert from the native (always in bytes) column
// representation and the utf16 counts used by some editors. // representation and the utf16 counts used by some editors.
func ToUTF16Column(offsets Offsets, p Point, content []byte) int { func ToUTF16Column(offsets Offsets, p Point, content []byte) int {
if content == nil { if content == nil || p.Column < 1 {
return -1 return -1
} }
// make sure we have a valid offset // make sure we have a valid offset