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