2018-11-07 13:21:31 -07:00
|
|
|
// 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.
|
|
|
|
|
2018-11-05 19:23:02 -07:00
|
|
|
package lsp
|
|
|
|
|
|
|
|
import (
|
2020-09-04 11:36:45 -06:00
|
|
|
"bytes"
|
2019-04-04 17:33:08 -06:00
|
|
|
"context"
|
2018-11-20 14:05:10 -07:00
|
|
|
"fmt"
|
2019-12-23 21:06:34 -07:00
|
|
|
"strings"
|
2018-11-05 19:23:02 -07:00
|
|
|
|
2020-04-17 07:32:56 -06:00
|
|
|
"golang.org/x/tools/internal/event"
|
2020-03-10 21:25:10 -06:00
|
|
|
"golang.org/x/tools/internal/lsp/debug/tag"
|
2018-11-05 19:23:02 -07:00
|
|
|
"golang.org/x/tools/internal/lsp/protocol"
|
|
|
|
"golang.org/x/tools/internal/lsp/source"
|
2020-09-04 13:37:47 -06:00
|
|
|
"golang.org/x/tools/internal/lsp/source/completion"
|
2020-08-28 16:18:58 -06:00
|
|
|
"golang.org/x/tools/internal/span"
|
2018-11-05 19:23:02 -07:00
|
|
|
)
|
|
|
|
|
2019-04-04 17:33:08 -06:00
|
|
|
func (s *Server) completion(ctx context.Context, params *protocol.CompletionParams) (*protocol.CompletionList, error) {
|
2020-07-02 16:34:10 -06:00
|
|
|
snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.UnknownKind)
|
|
|
|
defer release()
|
2020-02-13 11:46:49 -07:00
|
|
|
if !ok {
|
2019-08-16 11:49:17 -06:00
|
|
|
return nil, err
|
|
|
|
}
|
2020-09-04 13:37:47 -06:00
|
|
|
var candidates []completion.CompletionItem
|
|
|
|
var surrounding *completion.Selection
|
internal/lsp: read files eagerly
We use file identities pervasively throughout gopls. Prior to this
change, the identity is the modification date of an unopened file, or
the hash of an opened file. That means that opening a file changes its
identity, which causes unnecessary churn in the cache.
Unfortunately, there isn't an easy way to fix this. Changing the
cache key to something else, such as the modification time, means that
we won't unify cache entries if a change is made and then undone. The
approach here is to read files eagerly in GetFile, so that we know their
hashes immediately. That resolves the churn, but means that we do a ton
of file IO at startup.
Incidental changes:
Remove the FileSystem interface; there was only one implementation and
it added a fair amount of cruft. We have many other places that assume
os.Stat and such work.
Add direct accessors to FileHandle for URI, Kind, and Version. Most uses
of (FileHandle).Identity were for stuff that we derive solely from the
URI, and this helped me disentangle them. It is a *ton* of churn,
though. I can revert it if you want.
Change-Id: Ia2133bc527f71daf81c9d674951726a232ca5bc9
Reviewed-on: https://go-review.googlesource.com/c/tools/+/237037
Run-TryBot: Heschi Kreinick <heschi@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
2020-06-08 13:21:24 -06:00
|
|
|
switch fh.Kind() {
|
2019-12-10 09:51:34 -07:00
|
|
|
case source.Go:
|
2020-09-04 13:37:47 -06:00
|
|
|
candidates, surrounding, err = completion.Completion(ctx, snapshot, fh, params.Position, params.Context.TriggerCharacter)
|
2019-12-10 09:51:34 -07:00
|
|
|
case source.Mod:
|
|
|
|
candidates, surrounding = nil, nil
|
|
|
|
}
|
2019-04-04 17:33:08 -06:00
|
|
|
if err != nil {
|
2020-03-30 10:45:15 -06:00
|
|
|
event.Error(ctx, "no completions found", err, tag.Position.Of(params.Position))
|
2019-08-16 15:05:40 -06:00
|
|
|
}
|
|
|
|
if candidates == nil {
|
|
|
|
return &protocol.CompletionList{
|
|
|
|
Items: []protocol.CompletionItem{},
|
|
|
|
}, nil
|
2019-05-13 14:49:29 -06:00
|
|
|
}
|
2019-08-16 15:05:40 -06:00
|
|
|
// We might need to adjust the position to account for the prefix.
|
|
|
|
rng, err := surrounding.Range()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2020-08-28 16:18:58 -06:00
|
|
|
|
|
|
|
// internal/span treats end of file as the beginning of the next line, even
|
|
|
|
// when it's not newline-terminated. We correct for that behaviour here if
|
|
|
|
// end of file is not newline-terminated. See golang/go#41029.
|
|
|
|
src, err := fh.Read()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2020-09-04 11:36:45 -06:00
|
|
|
numLines := len(bytes.Split(src, []byte("\n")))
|
2020-08-28 16:18:58 -06:00
|
|
|
tok := snapshot.FileSet().File(surrounding.Start())
|
2020-09-04 11:36:45 -06:00
|
|
|
eof := tok.Pos(tok.Size())
|
2020-08-28 16:18:58 -06:00
|
|
|
|
|
|
|
// For newline-terminated files, the line count reported by go/token should
|
|
|
|
// be lower than the actual number of lines we see when splitting by \n. If
|
|
|
|
// they're the same, the file isn't newline-terminated.
|
2020-09-04 11:36:45 -06:00
|
|
|
if tok.Size() > 0 && tok.LineCount() == numLines {
|
|
|
|
// Get the span for the last character in the file-1. This is
|
|
|
|
// technically incorrect, but will get span to point to the previous
|
|
|
|
// line.
|
|
|
|
spn, err := span.NewRange(snapshot.FileSet(), eof-1, eof-1).Span()
|
2020-08-28 16:18:58 -06:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
m := &protocol.ColumnMapper{
|
|
|
|
URI: fh.URI(),
|
2020-09-04 11:36:45 -06:00
|
|
|
Converter: span.NewContentConverter(fh.URI().Filename(), src),
|
|
|
|
Content: src,
|
2020-08-28 16:18:58 -06:00
|
|
|
}
|
|
|
|
eofRng, err := m.Range(spn)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2020-09-04 11:36:45 -06:00
|
|
|
// Instead of using the computed range, correct for our earlier
|
|
|
|
// position adjustment by adding 1 to the column, not the line number.
|
|
|
|
pos := protocol.Position{
|
|
|
|
Line: eofRng.Start.Line,
|
2020-08-28 16:18:58 -06:00
|
|
|
Character: eofRng.Start.Character + 1,
|
|
|
|
}
|
2020-09-04 11:36:45 -06:00
|
|
|
if surrounding.Start() >= eof {
|
|
|
|
rng.Start = pos
|
2020-08-28 16:18:58 -06:00
|
|
|
}
|
2020-09-04 11:36:45 -06:00
|
|
|
if surrounding.End() >= eof {
|
|
|
|
rng.End = pos
|
2020-08-21 15:16:29 -06:00
|
|
|
}
|
|
|
|
}
|
2019-10-22 22:16:21 -06:00
|
|
|
|
|
|
|
// When using deep completions/fuzzy matching, report results as incomplete so
|
|
|
|
// client fetches updated completions after every key stroke.
|
2020-02-13 11:46:49 -07:00
|
|
|
options := snapshot.View().Options()
|
2019-12-29 00:22:12 -07:00
|
|
|
incompleteResults := options.DeepCompletion || options.Matcher == source.Fuzzy
|
2019-10-22 22:16:21 -06:00
|
|
|
|
|
|
|
items := toProtocolCompletionItems(candidates, rng, options)
|
|
|
|
|
2019-04-04 17:33:08 -06:00
|
|
|
return &protocol.CompletionList{
|
2019-10-22 22:16:21 -06:00
|
|
|
IsIncomplete: incompleteResults,
|
|
|
|
Items: items,
|
2019-04-04 17:33:08 -06:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2020-09-12 22:26:21 -06:00
|
|
|
func toProtocolCompletionItems(candidates []completion.CompletionItem, rng protocol.Range, options *source.Options) []protocol.CompletionItem {
|
internal/lsp: add fuzzy completion matching
Make use of the existing fuzzy matcher to perform server side fuzzy
completion matching. Previously the server did exact prefix matching
for completion candidates and left fancy filtering to the
client. Having the server do fuzzy matching has two main benefits:
- Deep completions now update as you type. The completion candidates
returned to the client are marked "incomplete", causing the client
to refresh the candidates after every keystroke. This lets the
server pick the most relevant set of deep completion candidates.
- All editors get fuzzy matching for free. VSCode has fuzzy matching
out of the box, but some editors either don't provide it, or it can
be difficult to set up.
I modified the fuzzy matcher to allow matches where the input doesn't
match the final segment of the candidate. For example, previously "ab"
would not match "abc.def" because the "b" in "ab" did not match the
final segment "def". I can see how this is useful when the text
matching happens in a vacuum and candidate's final segment is the most
specific part. But, in our case, we have various other methods to
order candidates, so we don't want to exclude them just because the
final segment doesn't match. For example, if we know our candidate
needs to be type "context.Context" and "foo.ctx" is of the right type,
we want to suggest "foo.ctx" as soon as the user starts inputting
"foo", even though "foo" doesn't match "ctx" at all.
Note that fuzzy matching is behind the "useDeepCompletions" config
flag for the time being.
Change-Id: Ic7674f0cf885af770c30daef472f2e3c5ac4db78
Reviewed-on: https://go-review.googlesource.com/c/tools/+/190099
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
2019-08-13 14:45:19 -06:00
|
|
|
var (
|
|
|
|
items = make([]protocol.CompletionItem, 0, len(candidates))
|
|
|
|
numDeepCompletionsSeen int
|
|
|
|
)
|
2018-12-10 22:52:31 -07:00
|
|
|
for i, candidate := range candidates {
|
2019-06-27 11:50:01 -06:00
|
|
|
// Limit the number of deep completions to not overwhelm the user in cases
|
|
|
|
// with dozens of deep completion matches.
|
|
|
|
if candidate.Depth > 0 {
|
2019-12-29 00:22:12 -07:00
|
|
|
if !options.DeepCompletion {
|
2019-06-27 11:50:01 -06:00
|
|
|
continue
|
|
|
|
}
|
2020-09-04 13:37:47 -06:00
|
|
|
if numDeepCompletionsSeen >= completion.MaxDeepCompletions {
|
2019-06-27 11:50:01 -06:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
numDeepCompletionsSeen++
|
|
|
|
}
|
2019-04-29 17:47:54 -06:00
|
|
|
insertText := candidate.InsertText
|
2019-09-05 22:17:36 -06:00
|
|
|
if options.InsertTextFormat == protocol.SnippetTextFormat {
|
2019-09-04 11:23:14 -06:00
|
|
|
insertText = candidate.Snippet()
|
2019-04-28 21:19:54 -06:00
|
|
|
}
|
2019-09-04 11:23:14 -06:00
|
|
|
|
internal/lsp: add literal completion candidates
Add support for literal completion candidates such as "[]int{}" or
"make([]int, 0)". We support both named and unnamed types. I used the
existing type matching logic, so, for example, if the expected type is
an interface, we will suggest literal candidates that implement the
interface.
The literal candidates have a lower score than normal matching
candidates, so they shouldn't be disruptive in cases where you don't
want a literal candidate.
This commit adds support for slice, array, struct, map, and channel
literal candidates since they are pretty similar. Functions will be
supported in a subsequent commit.
I also added support for setting a snippet's final tab stop. This is
useful if you want the cursor to end up somewhere other than the
character after the snippet.
Change-Id: Id3b74260fff4d61703989b422267021b00cec005
Reviewed-on: https://go-review.googlesource.com/c/tools/+/193698
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
2019-09-04 17:12:37 -06:00
|
|
|
// This can happen if the client has snippets disabled but the
|
|
|
|
// candidate only supports snippet insertion.
|
|
|
|
if insertText == "" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2018-12-10 22:52:31 -07:00
|
|
|
item := protocol.CompletionItem{
|
2019-03-13 12:25:56 -06:00
|
|
|
Label: candidate.Label,
|
|
|
|
Detail: candidate.Detail,
|
2019-09-24 22:46:57 -06:00
|
|
|
Kind: candidate.Kind,
|
2019-12-04 11:36:18 -07:00
|
|
|
TextEdit: &protocol.TextEdit{
|
2018-11-21 21:51:01 -07:00
|
|
|
NewText: insertText,
|
2019-08-16 15:05:40 -06:00
|
|
|
Range: rng,
|
2018-11-21 21:51:01 -07:00
|
|
|
},
|
2019-09-05 22:17:36 -06:00
|
|
|
InsertTextFormat: options.InsertTextFormat,
|
2019-08-16 15:05:40 -06:00
|
|
|
AdditionalTextEdits: candidate.AdditionalTextEdits,
|
2018-11-21 21:51:01 -07:00
|
|
|
// 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.
|
2019-12-23 21:06:34 -07:00
|
|
|
SortText: fmt.Sprintf("%05d", i),
|
|
|
|
|
2019-12-27 09:57:52 -07:00
|
|
|
// Trim operators (VSCode doesn't like weird characters in
|
|
|
|
// filterText).
|
|
|
|
FilterText: strings.TrimLeft(candidate.InsertText, "&*"),
|
2019-12-23 21:06:34 -07:00
|
|
|
|
2019-07-02 15:31:31 -06:00
|
|
|
Preselect: i == 0,
|
|
|
|
Documentation: candidate.Documentation,
|
2018-11-20 16:18:33 -07:00
|
|
|
}
|
2018-12-10 22:52:31 -07:00
|
|
|
items = append(items, item)
|
2018-11-07 18:57:08 -07:00
|
|
|
}
|
2018-12-10 22:52:31 -07:00
|
|
|
return items
|
2018-11-07 18:57:08 -07:00
|
|
|
}
|