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/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

View File

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

View File

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

View File

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

View File

@ -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":

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(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,

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(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")
*/

View File

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

View File

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

View File

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

View File

@ -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:]...)

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"
)
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:

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"
"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, &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) {

View File

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

View File

@ -8,9 +8,6 @@ import (
"bytes"
"context"
"fmt"
"go/token"
"strconv"
"strings"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/asmdecl"
@ -35,12 +32,12 @@ import (
"golang.org/x/tools/go/analysis/passes/unreachable"
"golang.org/x/tools/go/analysis/passes/unsafeptr"
"golang.org/x/tools/go/analysis/passes/unusedresult"
"golang.org/x/tools/go/packages"
"golang.org/x/tools/internal/span"
)
type Diagnostic struct {
Range
span.Span
Message string
Source string
Severity DiagnosticSeverity
@ -53,16 +50,16 @@ const (
SeverityError
)
func Diagnostics(ctx context.Context, v View, uri URI) (map[string][]Diagnostic, error) {
func Diagnostics(ctx context.Context, v View, uri span.URI) (map[span.URI][]Diagnostic, error) {
f, err := v.GetFile(ctx, uri)
if err != nil {
return nil, err
}
pkg := f.GetPackage(ctx)
// Prepare the reports we will send for this package.
reports := make(map[string][]Diagnostic)
reports := make(map[span.URI][]Diagnostic)
for _, filename := range pkg.GetFilenames() {
reports[filename] = []Diagnostic{}
reports[span.FileURI(filename)] = []Diagnostic{}
}
var parseErrors, typeErrors []packages.Error
for _, err := range pkg.GetErrors() {
@ -82,35 +79,27 @@ func Diagnostics(ctx context.Context, v View, uri URI) (map[string][]Diagnostic,
diags = parseErrors
}
for _, diag := range diags {
pos := errorPos(diag)
diagFile, err := v.GetFile(ctx, ToURI(pos.Filename))
if err != nil {
continue
}
diagTok := diagFile.GetToken(ctx)
end, err := identifierEnd(diagFile.GetContent(ctx), pos.Line, pos.Column)
// Don't set a range if it's anything other than a type error.
if err != nil || diag.Kind != packages.TypeError {
end = 0
}
startPos := fromTokenPosition(diagTok, pos.Line, pos.Column)
if !startPos.IsValid() {
continue
}
endPos := fromTokenPosition(diagTok, pos.Line, pos.Column+end)
if !endPos.IsValid() {
continue
spn := span.Parse(diag.Pos)
if spn.IsPoint() && diag.Kind == packages.TypeError {
// Don't set a range if it's anything other than a type error.
if diagFile, err := v.GetFile(ctx, spn.URI); err == nil {
content := diagFile.GetContent(ctx)
c := span.NewTokenConverter(diagFile.GetFileSet(ctx), diagFile.GetToken(ctx))
s := spn.CleanOffset(c)
if end := bytes.IndexAny(content[s.Start.Offset:], " \n,():;[]"); end > 0 {
spn.End = s.Start
spn.End.Column += end
spn.End.Offset += end
}
}
}
diagnostic := Diagnostic{
Range: Range{
Start: startPos,
End: endPos,
},
Span: spn,
Message: diag.Msg,
Severity: SeverityError,
}
if _, ok := reports[pos.Filename]; ok {
reports[pos.Filename] = append(reports[pos.Filename], diagnostic)
if _, ok := reports[spn.URI]; ok {
reports[spn.URI] = append(reports[spn.URI], diagnostic)
}
}
if len(diags) > 0 {
@ -118,14 +107,16 @@ func Diagnostics(ctx context.Context, v View, uri URI) (map[string][]Diagnostic,
}
// Type checking and parsing succeeded. Run analyses.
runAnalyses(ctx, v, pkg, func(a *analysis.Analyzer, diag analysis.Diagnostic) {
pos := v.FileSet().Position(diag.Pos)
r := span.NewRange(v.FileSet(), diag.Pos, 0)
s := r.Span()
category := a.Name
if diag.Category != "" {
category += "." + category
}
reports[pos.Filename] = append(reports[pos.Filename], Diagnostic{
reports[s.URI] = append(reports[s.URI], Diagnostic{
Source: category,
Range: Range{Start: diag.Pos, End: diag.Pos},
Span: s,
Message: fmt.Sprintf(diag.Message),
Severity: SeverityWarning,
})
@ -134,57 +125,6 @@ func Diagnostics(ctx context.Context, v View, uri URI) (map[string][]Diagnostic,
return reports, nil
}
// fromTokenPosition converts a token.Position (1-based line and column
// number) to a token.Pos (byte offset value). This requires the token.File
// to which the token.Pos belongs.
func fromTokenPosition(f *token.File, line, col int) token.Pos {
linePos := lineStart(f, line)
// TODO: This is incorrect, as pos.Column represents bytes, not characters.
// This needs to be handled to address golang.org/issue/29149.
return linePos + token.Pos(col-1)
}
func errorPos(pkgErr packages.Error) token.Position {
remainder1, first, hasLine := chop(pkgErr.Pos)
remainder2, second, hasColumn := chop(remainder1)
var pos token.Position
if hasLine && hasColumn {
pos.Filename = remainder2
pos.Line = second
pos.Column = first
} else if hasLine {
pos.Filename = remainder1
pos.Line = first
}
return pos
}
func chop(text string) (remainder string, value int, ok bool) {
i := strings.LastIndex(text, ":")
if i < 0 {
return text, 0, false
}
v, err := strconv.ParseInt(text[i+1:], 10, 64)
if err != nil {
return text, 0, false
}
return text[:i], int(v), true
}
// identifierEnd returns the length of an identifier within a string,
// given the starting line and column numbers of the identifier.
func identifierEnd(content []byte, l, c int) (int, error) {
lines := bytes.Split(content, []byte("\n"))
if len(lines) < l {
return 0, fmt.Errorf("invalid line number: got %v, but only %v lines", l, len(lines))
}
line := lines[l-1]
if len(line) < c {
return 0, fmt.Errorf("invalid column number: got %v, but the length of the line is %v", c, len(line))
}
return bytes.IndexAny(line[c-1:], " \n,():;[]"), nil
}
func runAnalyses(ctx context.Context, v View, pkg Package, report func(a *analysis.Analyzer, diag analysis.Diagnostic)) error {
// the traditional vet suite:
analyzers := []*analysis.Analyzer{

View File

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

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

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
// 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)

View File

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