2019-02-12 11:13:49 -07:00
|
|
|
// 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 span
|
|
|
|
|
|
|
|
import (
|
2019-03-15 11:19:43 -06:00
|
|
|
"fmt"
|
2019-02-12 11:13:49 -07:00
|
|
|
"go/token"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Range represents a source code range in token.Pos form.
|
|
|
|
// It also carries the FileSet that produced the positions, so that it is
|
|
|
|
// self contained.
|
|
|
|
type Range struct {
|
|
|
|
FileSet *token.FileSet
|
|
|
|
Start token.Pos
|
|
|
|
End token.Pos
|
|
|
|
}
|
|
|
|
|
|
|
|
// TokenConverter is a Converter backed by a token file set and file.
|
internal/lsp: improve signatureHelp in various cases
- show signature for function calls whose function expression is not
an object (e.g. the second call in foo()()). since the function name
is not available, we use the generic "func"
- only provide signature help when the position is on or within the
call expression parens. this is consistent with the one other lsp
server i tried (java). this improves the gopls experience in emacs
where lsp-mode is constantly calling "hover" and
"signatureHelp" ("hover" should be preferred unless you are inside
the function params list)
- use the entire signature type string as the label since that includes
the return values, which are useful to see
- don't qualify the function name with its package. it looks funny to
see "bytes.Cap()" as the help when you are in a call
to (*bytes.Buffer).Cap(). it could be useful to include invocant
type info, but leave it out for now since signature help is meant to
focus on the function parameters.
- don't turn variadic args "foo ...int" into "foo []int" for the
parameter information (i.e. maintain it as "foo ...int")
- when determining active parameter, count the space before a
parameter name as being part of that parameter (e.g. the space
before "b" in "func(a int, b int)")
- handle variadic params when determining the active param (i.e.
highlight "foo(a int, *b ...string*)" on signature help for final
param in `foo(123, "a", "b", "c")`
- don't generate an extra space in formatParams() for unnamed
arguments
I also tweaked the signatureHelp server log message to include the
error message itself, and populated the server's logger in lsp_test.go
to aid in development.
Fixes golang/go#31448
Change-Id: Iefe0e1e3c531d17197c0fa997b949174475a276c
GitHub-Last-Rev: 5c0b8ebd87a8c05d5d8f519ea096f94e89c77e2c
GitHub-Pull-Request: golang/tools#82
Reviewed-on: https://go-review.googlesource.com/c/tools/+/172439
Run-TryBot: Ian Cottrell <iancottrell@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
2019-04-16 22:54:13 -06:00
|
|
|
// It uses the file set methods to work out the conversions, which
|
|
|
|
// makes it fast and does not require the file contents.
|
2019-02-12 11:13:49 -07:00
|
|
|
type TokenConverter struct {
|
|
|
|
fset *token.FileSet
|
|
|
|
file *token.File
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewRange creates a new Range from a FileSet and two positions.
|
|
|
|
// To represent a point pass a 0 as the end pos.
|
|
|
|
func NewRange(fset *token.FileSet, start, end token.Pos) Range {
|
|
|
|
return Range{
|
|
|
|
FileSet: fset,
|
|
|
|
Start: start,
|
|
|
|
End: end,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-15 11:19:43 -06:00
|
|
|
// NewTokenConverter returns an implementation of Converter backed by a
|
2019-02-12 11:13:49 -07:00
|
|
|
// token.File.
|
|
|
|
func NewTokenConverter(fset *token.FileSet, f *token.File) *TokenConverter {
|
|
|
|
return &TokenConverter{fset: fset, file: f}
|
|
|
|
}
|
|
|
|
|
2019-03-15 11:19:43 -06:00
|
|
|
// NewContentConverter returns an implementation of Converter for the
|
2019-02-12 11:13:49 -07:00
|
|
|
// given file content.
|
2019-02-19 19:11:15 -07:00
|
|
|
func NewContentConverter(filename string, content []byte) *TokenConverter {
|
2019-02-12 11:13:49 -07:00
|
|
|
fset := token.NewFileSet()
|
|
|
|
f := fset.AddFile(filename, -1, len(content))
|
|
|
|
f.SetLinesForContent(content)
|
|
|
|
return &TokenConverter{fset: fset, file: f}
|
|
|
|
}
|
|
|
|
|
|
|
|
// IsPoint returns true if the range represents a single point.
|
|
|
|
func (r Range) IsPoint() bool {
|
|
|
|
return r.Start == r.End
|
|
|
|
}
|
|
|
|
|
|
|
|
// Span converts a Range to a Span that represents the Range.
|
|
|
|
// It will fill in all the members of the Span, calculating the line and column
|
|
|
|
// information.
|
2019-03-15 11:19:43 -06:00
|
|
|
func (r Range) Span() (Span, error) {
|
2019-02-12 11:13:49 -07:00
|
|
|
f := r.FileSet.File(r.Start)
|
2019-03-15 11:19:43 -06:00
|
|
|
if f == nil {
|
|
|
|
return Span{}, fmt.Errorf("file not found in FileSet")
|
|
|
|
}
|
2019-11-20 11:59:03 -07:00
|
|
|
s := Span{}
|
2019-03-26 19:02:11 -06:00
|
|
|
var err error
|
|
|
|
s.v.Start.Offset, err = offset(f, r.Start)
|
|
|
|
if err != nil {
|
|
|
|
return Span{}, err
|
2019-03-26 17:48:42 -06:00
|
|
|
}
|
2019-02-12 11:13:49 -07:00
|
|
|
if r.End.IsValid() {
|
2019-03-26 19:02:11 -06:00
|
|
|
s.v.End.Offset, err = offset(f, r.End)
|
|
|
|
if err != nil {
|
|
|
|
return Span{}, err
|
|
|
|
}
|
2019-02-12 11:13:49 -07:00
|
|
|
}
|
2019-11-20 11:59:03 -07:00
|
|
|
// In the presence of line directives, a single File can have sections from
|
|
|
|
// multiple file names.
|
|
|
|
filename := f.Position(r.Start).Filename
|
|
|
|
if r.End.IsValid() {
|
|
|
|
if endFilename := f.Position(r.End).Filename; filename != endFilename {
|
|
|
|
return Span{}, fmt.Errorf("span begins in file %q but ends in %q", filename, endFilename)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
s.v.URI = FileURI(filename)
|
|
|
|
|
2019-03-15 11:19:43 -06:00
|
|
|
s.v.Start.clean()
|
|
|
|
s.v.End.clean()
|
|
|
|
s.v.clean()
|
2019-02-12 11:13:49 -07:00
|
|
|
converter := NewTokenConverter(r.FileSet, f)
|
2019-03-15 11:19:43 -06:00
|
|
|
return s.WithPosition(converter)
|
2019-02-12 11:13:49 -07:00
|
|
|
}
|
|
|
|
|
2019-03-26 19:02:11 -06:00
|
|
|
// offset is a copy of the Offset function in go/token, but with the adjustment
|
|
|
|
// that it does not panic on invalid positions.
|
|
|
|
func offset(f *token.File, pos token.Pos) (int, error) {
|
|
|
|
if int(pos) < f.Base() || int(pos) > f.Base()+f.Size() {
|
|
|
|
return 0, fmt.Errorf("invalid pos")
|
|
|
|
}
|
|
|
|
return int(pos) - f.Base(), nil
|
|
|
|
}
|
|
|
|
|
2019-02-12 11:13:49 -07:00
|
|
|
// Range converts a Span to a Range that represents the Span for the supplied
|
|
|
|
// File.
|
2019-03-15 11:19:43 -06:00
|
|
|
func (s Span) Range(converter *TokenConverter) (Range, error) {
|
|
|
|
s, err := s.WithOffset(converter)
|
|
|
|
if err != nil {
|
|
|
|
return Range{}, err
|
|
|
|
}
|
2019-06-27 10:39:54 -06:00
|
|
|
// go/token will panic if the offset is larger than the file's size,
|
|
|
|
// so check here to avoid panicking.
|
|
|
|
if s.Start().Offset() > converter.file.Size() {
|
2019-10-07 11:52:07 -06:00
|
|
|
return Range{}, fmt.Errorf("start offset %v is past the end of the file %v", s.Start(), converter.file.Size())
|
2019-06-27 10:39:54 -06:00
|
|
|
}
|
|
|
|
if s.End().Offset() > converter.file.Size() {
|
2019-10-07 11:52:07 -06:00
|
|
|
return Range{}, fmt.Errorf("end offset %v is past the end of the file %v", s.End(), converter.file.Size())
|
2019-06-27 10:39:54 -06:00
|
|
|
}
|
2019-02-12 11:13:49 -07:00
|
|
|
return Range{
|
|
|
|
FileSet: converter.fset,
|
2019-03-15 11:19:43 -06:00
|
|
|
Start: converter.file.Pos(s.Start().Offset()),
|
|
|
|
End: converter.file.Pos(s.End().Offset()),
|
|
|
|
}, nil
|
2019-02-12 11:13:49 -07:00
|
|
|
}
|
|
|
|
|
2019-03-15 11:19:43 -06:00
|
|
|
func (l *TokenConverter) ToPosition(offset int) (int, int, error) {
|
2019-06-27 10:39:54 -06:00
|
|
|
if offset > l.file.Size() {
|
2019-10-07 11:52:07 -06:00
|
|
|
return 0, 0, fmt.Errorf("offset %v is past the end of the file %v", offset, l.file.Size())
|
2019-06-27 10:39:54 -06:00
|
|
|
}
|
2019-02-12 11:13:49 -07:00
|
|
|
pos := l.file.Pos(offset)
|
|
|
|
p := l.fset.Position(pos)
|
2019-10-07 11:52:07 -06:00
|
|
|
if offset == l.file.Size() {
|
|
|
|
return p.Line + 1, 1, nil
|
|
|
|
}
|
2019-03-15 11:19:43 -06:00
|
|
|
return p.Line, p.Column, nil
|
2019-02-12 11:13:49 -07:00
|
|
|
}
|
|
|
|
|
2019-03-15 11:19:43 -06:00
|
|
|
func (l *TokenConverter) ToOffset(line, col int) (int, error) {
|
2019-02-12 11:13:49 -07:00
|
|
|
if line < 0 {
|
2019-03-15 11:19:43 -06:00
|
|
|
return -1, fmt.Errorf("line is not valid")
|
2019-02-12 11:13:49 -07:00
|
|
|
}
|
|
|
|
lineMax := l.file.LineCount() + 1
|
|
|
|
if line > lineMax {
|
2019-10-07 11:52:07 -06:00
|
|
|
return -1, fmt.Errorf("line is beyond end of file %v", lineMax)
|
2019-02-12 11:13:49 -07:00
|
|
|
} else if line == lineMax {
|
|
|
|
if col > 1 {
|
2019-03-15 11:19:43 -06:00
|
|
|
return -1, fmt.Errorf("column is beyond end of file")
|
2019-02-12 11:13:49 -07:00
|
|
|
}
|
|
|
|
// at the end of the file, allowing for a trailing eol
|
2019-03-15 11:19:43 -06:00
|
|
|
return l.file.Size(), nil
|
2019-02-12 11:13:49 -07:00
|
|
|
}
|
|
|
|
pos := lineStart(l.file, line)
|
|
|
|
if !pos.IsValid() {
|
2019-03-15 11:19:43 -06:00
|
|
|
return -1, fmt.Errorf("line is not in file")
|
2019-02-12 11:13:49 -07:00
|
|
|
}
|
|
|
|
// we assume that column is in bytes here, and that the first byte of a
|
|
|
|
// line is at column 1
|
|
|
|
pos += token.Pos(col - 1)
|
2019-03-26 19:02:11 -06:00
|
|
|
return offset(l.file, pos)
|
2019-02-12 11:13:49 -07:00
|
|
|
}
|