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:
parent
d55b9fb8ef
commit
dbad8e90c9
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -16,12 +16,13 @@ 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
|
||||
Span span.Span `json:"span"` // span of the definition
|
||||
Description string `json:"description"` // description of the denoted object
|
||||
}
|
||||
|
||||
|
@ -29,7 +30,7 @@ type Definition struct {
|
|||
// 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
|
||||
}
|
||||
|
|
|
@ -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":
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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")
|
||||
*/
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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:]...)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
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 err != nil || diag.Kind != packages.TypeError {
|
||||
end = 0
|
||||
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
|
||||
}
|
||||
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{
|
||||
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{
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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] == ':'
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue