internal/lsp: use protocol.Range in completion items
This change switches Completion to use protocol positions instead of
token.Pos.
Change-Id: I012ce03c9316d8363938dd0156f485982b7e04fe
Reviewed-on: https://go-review.googlesource.com/c/tools/+/190600
Reviewed-by: Ian Cottrell <iancottrell@google.com>
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
diff --git a/internal/lsp/cmd/check_test.go b/internal/lsp/cmd/check_test.go
index 7f39ee8..d918f0d 100644
--- a/internal/lsp/cmd/check_test.go
+++ b/internal/lsp/cmd/check_test.go
@@ -52,7 +52,6 @@
got[l] = struct{}{}
}
for _, diag := range want {
- // TODO: This is a hack, fix this.
expect := fmt.Sprintf("%v:%v:%v: %v", diag.URI.Filename(), diag.Range.Start.Line+1, diag.Range.Start.Character+1, diag.Message)
if diag.Range.Start.Character == 0 {
expect = fmt.Sprintf("%v:%v: %v", diag.URI.Filename(), diag.Range.Start.Line+1, diag.Message)
diff --git a/internal/lsp/code_action.go b/internal/lsp/code_action.go
index 3491e36..2c6f065 100644
--- a/internal/lsp/code_action.go
+++ b/internal/lsp/code_action.go
@@ -133,14 +133,14 @@
return nil, nil, err
}
// Convert all source edits to protocol edits.
- pEdits, err := ToProtocolEdits(m, edits)
+ pEdits, err := source.ToProtocolEdits(m, edits)
if err != nil {
return nil, nil, err
}
pEditsPerFix := make([]*protocolImportFix, len(editsPerFix))
for i, fix := range editsPerFix {
- pEdits, err := ToProtocolEdits(m, fix.Edits)
+ pEdits, err := source.ToProtocolEdits(m, fix.Edits)
if err != nil {
return nil, nil, err
}
@@ -241,7 +241,7 @@
if err != nil {
return nil, err
}
- edits, err := ToProtocolEdits(m, ca.Edits)
+ edits, err := source.ToProtocolEdits(m, ca.Edits)
if err != nil {
return nil, err
}
diff --git a/internal/lsp/completion.go b/internal/lsp/completion.go
index bbfe73b..899cf6f 100644
--- a/internal/lsp/completion.go
+++ b/internal/lsp/completion.go
@@ -23,64 +23,42 @@
if err != nil {
return nil, err
}
- m, err := getMapper(ctx, f)
- if err != nil {
- return nil, err
- }
- spn, err := m.PointSpan(params.Position)
- if err != nil {
- return nil, err
- }
- rng, err := spn.Range(m.Converter)
- if err != nil {
- return nil, err
- }
- candidates, surrounding, err := source.Completion(ctx, view, f, rng.Start, source.CompletionOptions{
+ candidates, surrounding, err := source.Completion(ctx, view, f, params.Position, source.CompletionOptions{
DeepComplete: s.useDeepCompletions,
WantDocumentaton: s.wantCompletionDocumentation,
WantFullDocumentation: s.hoverKind == fullDocumentation,
WantUnimported: s.wantUnimportedCompletions,
})
if err != nil {
- log.Print(ctx, "no completions found", tag.Of("At", rng), tag.Of("Failure", err))
+ log.Print(ctx, "no completions found", tag.Of("At", params.Position), tag.Of("Failure", err))
}
- return &protocol.CompletionList{
- // When using deep completions/fuzzy matching, report results as incomplete so
- // client fetches updated completions after every key stroke.
- IsIncomplete: s.useDeepCompletions,
- Items: s.toProtocolCompletionItems(ctx, view, m, candidates, params.Position, surrounding),
- }, nil
-}
-
-func (s *Server) toProtocolCompletionItems(ctx context.Context, view source.View, m *protocol.ColumnMapper, candidates []source.CompletionItem, pos protocol.Position, surrounding *source.Selection) []protocol.CompletionItem {
+ if candidates == nil {
+ return &protocol.CompletionList{
+ Items: []protocol.CompletionItem{},
+ }, nil
+ }
+ // We might need to adjust the position to account for the prefix.
+ rng, err := surrounding.Range()
+ if err != nil {
+ return nil, err
+ }
// Sort the candidates by score, since that is not supported by LSP yet.
sort.SliceStable(candidates, func(i, j int) bool {
return candidates[i].Score > candidates[j].Score
})
- // We might need to adjust the position to account for the prefix.
- insertionRange := protocol.Range{
- Start: pos,
- End: pos,
- }
- if surrounding != nil {
- spn, err := surrounding.Range.Span()
- if err != nil {
- log.Print(ctx, "failed to get span for surrounding position: %s:%v:%v: %v", tag.Of("Position", pos), tag.Of("Failure", err))
- } else {
- rng, err := m.Range(spn)
- if err != nil {
- log.Print(ctx, "failed to convert surrounding position", tag.Of("Position", pos), tag.Of("Failure", err))
- } else {
- insertionRange = rng
- }
- }
- }
+ return &protocol.CompletionList{
+ // When using deep completions/fuzzy matching, report results as incomplete so
+ // client fetches updated completions after every key stroke.
+ IsIncomplete: s.useDeepCompletions,
+ Items: s.toProtocolCompletionItems(candidates, rng),
+ }, nil
+}
+func (s *Server) toProtocolCompletionItems(candidates []source.CompletionItem, rng protocol.Range) []protocol.CompletionItem {
var (
items = make([]protocol.CompletionItem, 0, len(candidates))
numDeepCompletionsSeen int
)
-
for i, candidate := range candidates {
// Limit the number of deep completions to not overwhelm the user in cases
// with dozens of deep completion matches.
@@ -97,21 +75,16 @@
if s.insertTextFormat == protocol.SnippetTextFormat {
insertText = candidate.Snippet(s.usePlaceholders)
}
- addlEdits, err := ToProtocolEdits(m, candidate.AdditionalTextEdits)
- if err != nil {
- log.Error(ctx, "failed to convert to protocol edits", err)
- continue
- }
item := protocol.CompletionItem{
Label: candidate.Label,
Detail: candidate.Detail,
Kind: toProtocolCompletionItemKind(candidate.Kind),
TextEdit: &protocol.TextEdit{
NewText: insertText,
- Range: insertionRange,
+ Range: rng,
},
InsertTextFormat: s.insertTextFormat,
- AdditionalTextEdits: addlEdits,
+ AdditionalTextEdits: candidate.AdditionalTextEdits,
// This is a hack so that the client sorts completion results in the order
// according to their score. This can be removed upon the resolution of
// https://github.com/Microsoft/language-server-protocol/issues/348.
diff --git a/internal/lsp/format.go b/internal/lsp/format.go
index 95c9fd3..d819afd 100644
--- a/internal/lsp/format.go
+++ b/internal/lsp/format.go
@@ -25,7 +25,7 @@
if err != nil {
return nil, err
}
- return ToProtocolEdits(m, edits)
+ return source.ToProtocolEdits(m, edits)
}
func spanToRange(ctx context.Context, view source.View, spn span.Span) (source.GoFile, *protocol.ColumnMapper, span.Range, error) {
@@ -52,24 +52,6 @@
return f, m, rng, nil
}
-func ToProtocolEdits(m *protocol.ColumnMapper, edits []diff.TextEdit) ([]protocol.TextEdit, error) {
- if edits == nil {
- return nil, nil
- }
- result := make([]protocol.TextEdit, len(edits))
- for i, edit := range edits {
- rng, err := m.Range(edit.Span)
- if err != nil {
- return nil, err
- }
- result[i] = protocol.TextEdit{
- Range: rng,
- NewText: edit.NewText,
- }
- }
- return result, nil
-}
-
func FromProtocolEdits(m *protocol.ColumnMapper, edits []protocol.TextEdit) ([]diff.TextEdit, error) {
if edits == nil {
return nil, nil
diff --git a/internal/lsp/rename.go b/internal/lsp/rename.go
index 8fb5e2c..0aca1e2 100644
--- a/internal/lsp/rename.go
+++ b/internal/lsp/rename.go
@@ -49,7 +49,7 @@
if err != nil {
return nil, err
}
- protocolEdits, err := ToProtocolEdits(m, textEdits)
+ protocolEdits, err := source.ToProtocolEdits(m, textEdits)
if err != nil {
return nil, err
}
diff --git a/internal/lsp/source/completion.go b/internal/lsp/source/completion.go
index d6d6c28..ebde3a2 100644
--- a/internal/lsp/source/completion.go
+++ b/internal/lsp/source/completion.go
@@ -13,8 +13,8 @@
"golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/internal/imports"
- "golang.org/x/tools/internal/lsp/diff"
"golang.org/x/tools/internal/lsp/fuzzy"
+ "golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/snippet"
"golang.org/x/tools/internal/span"
"golang.org/x/tools/internal/telemetry/log"
@@ -43,7 +43,7 @@
// Additional text edits should be used to change text unrelated to the current cursor position
// (for example adding an import statement at the top of the file if the completion item will
// insert an unqualified type).
- AdditionalTextEdits []diff.TextEdit
+ AdditionalTextEdits []protocol.TextEdit
// Depth is how many levels were searched to find this completion.
// For example when completing "foo<>", "fooBar" is depth 0, and
@@ -199,6 +199,9 @@
// expensive and can be called many times for the same type while searching
// for deep completions.
methodSetCache map[methodSetKey]*types.MethodSet
+
+ // mapper converts the positions in the file from which the completion originated.
+ mapper *protocol.ColumnMapper
}
type compLitInfo struct {
@@ -229,13 +232,13 @@
// A Selection represents the cursor position and surrounding identifier.
type Selection struct {
- Content string
- Range span.Range
- Cursor token.Pos
+ content string
+ cursor token.Pos
+ mappedRange
}
func (p Selection) Prefix() string {
- return p.Content[:p.Cursor-p.Range.Start]
+ return p.content[:p.cursor-p.spanRange.Start]
}
func (c *completer) setSurrounding(ident *ast.Ident) {
@@ -246,9 +249,12 @@
return
}
c.surrounding = &Selection{
- Content: ident.Name,
- Range: span.NewRange(c.view.Session().Cache().FileSet(), ident.Pos(), ident.End()),
- Cursor: c.pos,
+ content: ident.Name,
+ cursor: c.pos,
+ mappedRange: mappedRange{
+ spanRange: span.NewRange(c.view.Session().Cache().FileSet(), ident.Pos(), ident.End()),
+ m: c.mapper,
+ },
}
// Fuzzy matching shares the "useDeepCompletions" config flag, so if deep completions
@@ -260,6 +266,20 @@
}
}
+func (c *completer) getSurrounding() *Selection {
+ if c.surrounding == nil {
+ c.surrounding = &Selection{
+ content: "",
+ cursor: c.pos,
+ mappedRange: mappedRange{
+ spanRange: span.NewRange(c.view.Session().Cache().FileSet(), c.pos, c.pos),
+ m: c.mapper,
+ },
+ }
+ }
+ return c.surrounding
+}
+
// found adds a candidate completion. We will also search through the object's
// members for more candidates.
func (c *completer) found(obj types.Object, score float64, imp *imports.ImportInfo) {
@@ -351,27 +371,51 @@
// The selection is computed based on the preceding identifier and can be used by
// the client to score the quality of the completion. For instance, some clients
// may tolerate imperfect matches as valid completion results, since users may make typos.
-func Completion(ctx context.Context, view View, f GoFile, pos token.Pos, opts CompletionOptions) ([]CompletionItem, *Selection, error) {
+func Completion(ctx context.Context, view View, f GoFile, pos protocol.Position, opts CompletionOptions) ([]CompletionItem, *Selection, error) {
ctx, done := trace.StartSpan(ctx, "source.Completion")
defer done()
- file, err := f.GetAST(ctx, ParseFull)
+ pkg, err := f.GetPackage(ctx)
+ if err != nil {
+ return nil, nil, err
+ }
+ var ph ParseGoHandle
+ for _, h := range pkg.GetHandles() {
+ if h.File().Identity().URI == f.URI() {
+ ph = h
+ }
+ }
+ file, err := ph.Cached(ctx)
if file == nil {
return nil, nil, err
}
- pkg, err := f.GetPackage(ctx)
+ data, _, err := ph.File().Read(ctx)
+ if err != nil {
+ return nil, nil, err
+ }
+ fset := view.Session().Cache().FileSet()
+ tok := fset.File(file.Pos())
+ if tok == nil {
+ return nil, nil, errors.Errorf("no token.File for %s", f.URI())
+ }
+ m := protocol.NewColumnMapper(f.URI(), f.URI().Filename(), fset, tok, data)
+ spn, err := m.PointSpan(pos)
+ if err != nil {
+ return nil, nil, err
+ }
+ rng, err := spn.Range(m.Converter)
if err != nil {
return nil, nil, err
}
// Completion is based on what precedes the cursor.
// Find the path to the position before pos.
- path, _ := astutil.PathEnclosingInterval(file, pos-1, pos-1)
+ path, _ := astutil.PathEnclosingInterval(file, rng.Start-1, rng.Start-1)
if path == nil {
return nil, nil, errors.Errorf("cannot find node enclosing position")
}
// Skip completion inside comments.
for _, g := range file.Comments {
- if g.Pos() <= pos && pos <= g.End() {
+ if g.Pos() <= rng.Start && rng.Start <= g.End() {
return nil, nil, nil
}
}
@@ -380,7 +424,7 @@
return nil, nil, nil
}
- clInfo := enclosingCompositeLiteral(path, pos, pkg.GetTypesInfo())
+ clInfo := enclosingCompositeLiteral(path, rng.Start, pkg.GetTypesInfo())
c := &completer{
types: pkg.GetTypes(),
info: pkg.GetTypesInfo(),
@@ -390,14 +434,15 @@
filename: f.URI().Filename(),
file: file,
path: path,
- pos: pos,
+ pos: rng.Start,
seen: make(map[types.Object]bool),
- enclosingFunction: enclosingFunction(path, pos, pkg.GetTypesInfo()),
+ enclosingFunction: enclosingFunction(path, rng.Start, pkg.GetTypesInfo()),
enclosingCompositeLiteral: clInfo,
opts: opts,
// default to a matcher that always matches
matcher: prefixMatcher(""),
methodSetCache: make(map[methodSetKey]*types.MethodSet),
+ mapper: m,
}
c.deepState.enabled = opts.DeepComplete
@@ -414,7 +459,7 @@
if err := c.structLiteralFieldName(); err != nil {
return nil, nil, err
}
- return c.items, c.surrounding, nil
+ return c.items, c.getSurrounding(), nil
}
switch n := path[0].(type) {
@@ -424,7 +469,7 @@
if err := c.selector(sel); err != nil {
return nil, nil, err
}
- return c.items, c.surrounding, nil
+ return c.items, c.getSurrounding(), nil
}
// reject defining identifiers
if obj, ok := pkg.GetTypesInfo().Defs[n]; ok {
@@ -472,7 +517,7 @@
}
}
- return c.items, c.surrounding, nil
+ return c.items, c.getSurrounding(), nil
}
func (c *completer) wantStructFieldCompletions() bool {
diff --git a/internal/lsp/source/completion_format.go b/internal/lsp/source/completion_format.go
index 55e8675..0ceaf5a 100644
--- a/internal/lsp/source/completion_format.go
+++ b/internal/lsp/source/completion_format.go
@@ -13,7 +13,7 @@
"go/types"
"strings"
- "golang.org/x/tools/internal/lsp/diff"
+ "golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/snippet"
"golang.org/x/tools/internal/span"
"golang.org/x/tools/internal/telemetry/log"
@@ -36,7 +36,7 @@
kind CompletionItemKind
plainSnippet *snippet.Builder
placeholderSnippet *snippet.Builder
- addlEdits []diff.TextEdit
+ protocolEdits []protocol.TextEdit
)
// expandFuncCall mutates the completion label, detail, and snippets
@@ -94,14 +94,18 @@
if err != nil {
return CompletionItem{}, err
}
- addlEdits = append(addlEdits, edit...)
+ addlEdits, err := ToProtocolEdits(c.mapper, edit)
+ if err != nil {
+ return CompletionItem{}, err
+ }
+ protocolEdits = append(protocolEdits, addlEdits...)
}
detail = strings.TrimPrefix(detail, "untyped ")
item := CompletionItem{
Label: label,
InsertText: insert,
- AdditionalTextEdits: addlEdits,
+ AdditionalTextEdits: protocolEdits,
Detail: detail,
Kind: kind,
Score: cand.score,
diff --git a/internal/lsp/source/format.go b/internal/lsp/source/format.go
index a1448cd..ae49558 100644
--- a/internal/lsp/source/format.go
+++ b/internal/lsp/source/format.go
@@ -14,6 +14,7 @@
"golang.org/x/tools/go/packages"
"golang.org/x/tools/internal/imports"
"golang.org/x/tools/internal/lsp/diff"
+ "golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/span"
"golang.org/x/tools/internal/telemetry/log"
"golang.org/x/tools/internal/telemetry/trace"
@@ -234,3 +235,21 @@
}
return diff.ComputeEdits(file.URI(), string(data), formatted)
}
+
+func ToProtocolEdits(m *protocol.ColumnMapper, edits []diff.TextEdit) ([]protocol.TextEdit, error) {
+ if edits == nil {
+ return nil, nil
+ }
+ result := make([]protocol.TextEdit, len(edits))
+ for i, edit := range edits {
+ rng, err := m.Range(edit.Span)
+ if err != nil {
+ return nil, err
+ }
+ result[i] = protocol.TextEdit{
+ Range: rng,
+ NewText: edit.NewText,
+ }
+ }
+ return result, nil
+}
diff --git a/internal/lsp/source/source_test.go b/internal/lsp/source/source_test.go
index 854edaa..4b1ea4c 100644
--- a/internal/lsp/source/source_test.go
+++ b/internal/lsp/source/source_test.go
@@ -18,6 +18,7 @@
"golang.org/x/tools/internal/lsp/cache"
"golang.org/x/tools/internal/lsp/diff"
"golang.org/x/tools/internal/lsp/fuzzy"
+ "golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/lsp/tests"
"golang.org/x/tools/internal/span"
@@ -87,14 +88,12 @@
if err != nil {
t.Fatalf("failed for %v: %v", src, err)
}
- tok, err := f.(source.GoFile).GetToken(ctx)
- if err != nil {
- t.Fatalf("failed to get token for %s: %v", src.URI(), err)
- }
- pos := tok.Pos(src.Start().Offset())
deepComplete := strings.Contains(string(src.URI()), "deepcomplete")
unimported := strings.Contains(string(src.URI()), "unimported")
- list, surrounding, err := source.Completion(ctx, r.view, f.(source.GoFile), pos, source.CompletionOptions{
+ list, surrounding, err := source.Completion(ctx, r.view, f.(source.GoFile), protocol.Position{
+ Line: float64(src.Start().Line() - 1),
+ Character: float64(src.Start().Column() - 1),
+ }, source.CompletionOptions{
DeepComplete: deepComplete,
WantDocumentaton: true,
WantUnimported: unimported,
@@ -144,12 +143,10 @@
if err != nil {
t.Fatalf("failed for %v: %v", src, err)
}
- tok, err := f.(source.GoFile).GetToken(ctx)
- if err != nil {
- t.Fatalf("failed to get token for %s: %v", src.URI(), err)
- }
- pos := tok.Pos(src.Start().Offset())
- list, _, err := source.Completion(ctx, r.view, f.(source.GoFile), pos, source.CompletionOptions{
+ list, _, err := source.Completion(ctx, r.view, f.(source.GoFile), protocol.Position{
+ Line: float64(src.Start().Line() - 1),
+ Character: float64(src.Start().Column() - 1),
+ }, source.CompletionOptions{
DeepComplete: strings.Contains(string(src.URI()), "deepcomplete"),
})
if err != nil {
diff --git a/internal/lsp/source/util.go b/internal/lsp/source/util.go
index ad25225..6ca1a4e 100644
--- a/internal/lsp/source/util.go
+++ b/internal/lsp/source/util.go
@@ -14,9 +14,38 @@
"regexp"
"strings"
+ "golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/span"
)
+type mappedRange struct {
+ spanRange span.Range
+ m *protocol.ColumnMapper
+
+ // protocolRange is the result of converting the spanRange using the mapper.
+ // It is computed on-demand.
+ protocolRange *protocol.Range
+}
+
+func (s mappedRange) Range() (protocol.Range, error) {
+ if s.protocolRange == nil {
+ spn, err := s.spanRange.Span()
+ if err != nil {
+ return protocol.Range{}, err
+ }
+ prng, err := s.m.Range(spn)
+ if err != nil {
+ return protocol.Range{}, err
+ }
+ s.protocolRange = &prng
+ }
+ return *s.protocolRange, nil
+}
+
+func (s mappedRange) URI() span.URI {
+ return s.m.URI
+}
+
func IsGenerated(ctx context.Context, view View, uri span.URI) bool {
f, err := view.GetFile(ctx, uri)
if err != nil {