mirror of https://github.com/golang/go.git
internal/lsp: add completion for use directives
For golang/go#50930 Change-Id: I9def58e9406ee735c93e988de336dbfee37e6c95 Reviewed-on: https://go-review.googlesource.com/c/tools/+/390054 Trust: Michael Matloob <matloob@golang.org> Run-TryBot: Michael Matloob <matloob@golang.org> Reviewed-by: Robert Findley <rfindley@google.com> gopls-CI: kokoro <noreply+kokoro@google.com> TryBot-Result: Gopher Robot <gobot@golang.org>
This commit is contained in:
parent
622cf7b338
commit
ee31f70645
|
|
@ -599,3 +599,48 @@ func BenchmarkFoo()
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGoWorkCompletion(t *testing.T) {
|
||||
const files = `
|
||||
-- go.work --
|
||||
go 1.18
|
||||
|
||||
use ./a
|
||||
use ./a/ba
|
||||
use ./a/b/
|
||||
use ./dir/foo
|
||||
use ./dir/foobar/
|
||||
-- a/go.mod --
|
||||
-- go.mod --
|
||||
-- a/bar/go.mod --
|
||||
-- a/b/c/d/e/f/go.mod --
|
||||
-- dir/bar --
|
||||
-- dir/foobar/go.mod --
|
||||
`
|
||||
|
||||
Run(t, files, func(t *testing.T, env *Env) {
|
||||
env.OpenFile("go.work")
|
||||
|
||||
tests := []struct {
|
||||
re string
|
||||
want []string
|
||||
}{
|
||||
{`use ()\.`, []string{".", "./a", "./a/bar", "./dir/foobar"}},
|
||||
{`use \.()`, []string{"", "/a", "/a/bar", "/dir/foobar"}},
|
||||
{`use \./()`, []string{"a", "a/bar", "dir/foobar"}},
|
||||
{`use ./a()`, []string{"", "/b/c/d/e/f", "/bar"}},
|
||||
{`use ./a/b()`, []string{"/c/d/e/f", "ar"}},
|
||||
{`use ./a/b/()`, []string{`c/d/e/f`}},
|
||||
{`use ./a/ba()`, []string{"r"}},
|
||||
{`use ./dir/foo()`, []string{"bar"}},
|
||||
{`use ./dir/foobar/()`, []string{}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
completions := env.Completion("go.work", env.RegexpSearch("go.work", tt.re))
|
||||
diff := compareCompletionResults(tt.want, completions.Items)
|
||||
if diff != "" {
|
||||
t.Errorf("%s: %s", tt.re, diff)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import (
|
|||
"golang.org/x/tools/internal/lsp/source"
|
||||
"golang.org/x/tools/internal/lsp/source/completion"
|
||||
"golang.org/x/tools/internal/lsp/template"
|
||||
"golang.org/x/tools/internal/lsp/work"
|
||||
"golang.org/x/tools/internal/span"
|
||||
)
|
||||
|
||||
|
|
@ -32,6 +33,12 @@ func (s *Server) completion(ctx context.Context, params *protocol.CompletionPara
|
|||
candidates, surrounding, err = completion.Completion(ctx, snapshot, fh, params.Position, params.Context)
|
||||
case source.Mod:
|
||||
candidates, surrounding = nil, nil
|
||||
case source.Work:
|
||||
cl, err := work.Completion(ctx, snapshot, fh, params.Position)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
return cl, nil
|
||||
case source.Tmpl:
|
||||
var cl *protocol.CompletionList
|
||||
cl, err = template.Completion(ctx, snapshot, fh, params.Position, params.Context)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,159 @@
|
|||
// Copyright 2022 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 work
|
||||
|
||||
import (
|
||||
"context"
|
||||
"go/token"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/tools/internal/event"
|
||||
"golang.org/x/tools/internal/lsp/protocol"
|
||||
"golang.org/x/tools/internal/lsp/source"
|
||||
errors "golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
func Completion(ctx context.Context, snapshot source.Snapshot, fh source.VersionedFileHandle, position protocol.Position) (*protocol.CompletionList, error) {
|
||||
ctx, done := event.Start(ctx, "work.Completion")
|
||||
defer done()
|
||||
|
||||
// Get the position of the cursor.
|
||||
pw, err := snapshot.ParseWork(ctx, fh)
|
||||
if err != nil {
|
||||
return nil, errors.Errorf("getting go.work file handle: %w", err)
|
||||
}
|
||||
spn, err := pw.Mapper.PointSpan(position)
|
||||
if err != nil {
|
||||
return nil, errors.Errorf("computing cursor position: %w", err)
|
||||
}
|
||||
rng, err := spn.Range(pw.Mapper.Converter)
|
||||
if err != nil {
|
||||
return nil, errors.Errorf("computing range: %w", err)
|
||||
}
|
||||
|
||||
// Find the use statement the user is in.
|
||||
cursor := rng.Start - 1
|
||||
use, pathStart, _ := usePath(pw, cursor)
|
||||
if use == nil {
|
||||
return &protocol.CompletionList{}, nil
|
||||
}
|
||||
completingFrom := use.Path[:cursor-token.Pos(pathStart)]
|
||||
|
||||
// We're going to find the completions of the user input
|
||||
// (completingFrom) by doing a walk on the innermost directory
|
||||
// of the given path, and comparing the found paths to make sure
|
||||
// that they match the component of the path after the
|
||||
// innermost directory.
|
||||
//
|
||||
// We'll maintain two paths when doing this: pathPrefixSlash
|
||||
// is essentially the path the user typed in, and pathPrefixAbs
|
||||
// is the path made absolute from the go.work directory.
|
||||
|
||||
pathPrefixSlash := completingFrom
|
||||
pathPrefixAbs := filepath.FromSlash(pathPrefixSlash)
|
||||
if !filepath.IsAbs(pathPrefixAbs) {
|
||||
pathPrefixAbs = filepath.Join(filepath.Dir(pw.URI.Filename()), pathPrefixAbs)
|
||||
}
|
||||
|
||||
// pathPrefixDir is the directory that will be walked to find matches.
|
||||
// If pathPrefixSlash is not explicitly a directory boundary (is either equivalent to "." or
|
||||
// ends in a separator) we need to examine its parent directory to find sibling files that
|
||||
// match.
|
||||
depthBound := 5
|
||||
pathPrefixDir, pathPrefixBase := pathPrefixAbs, ""
|
||||
pathPrefixSlashDir := pathPrefixSlash
|
||||
if filepath.Clean(pathPrefixSlash) != "." && !strings.HasSuffix(pathPrefixSlash, "/") {
|
||||
depthBound++
|
||||
pathPrefixDir, pathPrefixBase = filepath.Split(pathPrefixAbs)
|
||||
pathPrefixSlashDir = dirNonClean(pathPrefixSlash)
|
||||
}
|
||||
|
||||
var completions []string
|
||||
// Stop traversing deeper once we've hit 10k files to try to stay generally under 100ms.
|
||||
const numSeenBound = 10000
|
||||
var numSeen int
|
||||
stopWalking := errors.New("hit numSeenBound")
|
||||
err = filepath.Walk(pathPrefixDir, func(wpath string, info os.FileInfo, err error) error {
|
||||
if numSeen > numSeenBound {
|
||||
// Stop traversing if we hit bound.
|
||||
return stopWalking
|
||||
}
|
||||
numSeen++
|
||||
|
||||
// rel is the path relative to pathPrefixDir.
|
||||
// Make sure that it has pathPrefixBase as a prefix
|
||||
// otherwise it won't match the beginning of the
|
||||
// base component of the path the user typed in.
|
||||
rel := strings.TrimPrefix(wpath[len(pathPrefixDir):], string(filepath.Separator))
|
||||
if info.IsDir() && wpath != pathPrefixDir && !strings.HasPrefix(rel, pathPrefixBase) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
|
||||
// Check for a match (a module directory).
|
||||
if filepath.Base(rel) == "go.mod" {
|
||||
relDir := strings.TrimSuffix(dirNonClean(rel), string(os.PathSeparator))
|
||||
completionPath := join(pathPrefixSlashDir, filepath.ToSlash(relDir))
|
||||
|
||||
if !strings.HasPrefix(completionPath, completingFrom) {
|
||||
return nil
|
||||
}
|
||||
if strings.HasSuffix(completionPath, "/") {
|
||||
// Don't suggest paths that end in "/". This happens
|
||||
// when the input is a path that ends in "/" and
|
||||
// the completion is empty.
|
||||
return nil
|
||||
}
|
||||
completion := completionPath[len(completingFrom):]
|
||||
if completingFrom == "" && !strings.HasPrefix(completion, "./") {
|
||||
// Bias towards "./" prefixes.
|
||||
completion = join(".", completion)
|
||||
}
|
||||
|
||||
completions = append(completions, completion)
|
||||
}
|
||||
|
||||
if depth := strings.Count(rel, string(filepath.Separator)); depth >= depthBound {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil && !errors.Is(err, stopWalking) {
|
||||
return nil, errors.Errorf("walking to find completions: %w", err)
|
||||
}
|
||||
|
||||
sort.Strings(completions)
|
||||
|
||||
var items []protocol.CompletionItem
|
||||
for _, c := range completions {
|
||||
items = append(items, protocol.CompletionItem{
|
||||
Label: c,
|
||||
InsertText: c,
|
||||
})
|
||||
}
|
||||
return &protocol.CompletionList{Items: items}, nil
|
||||
}
|
||||
|
||||
// dirNonClean is filepath.Dir, without the Clean at the end.
|
||||
func dirNonClean(path string) string {
|
||||
vol := filepath.VolumeName(path)
|
||||
i := len(path) - 1
|
||||
for i >= len(vol) && !os.IsPathSeparator(path[i]) {
|
||||
i--
|
||||
}
|
||||
return path[len(vol) : i+1]
|
||||
}
|
||||
|
||||
func join(a, b string) string {
|
||||
if a == "" {
|
||||
return b
|
||||
}
|
||||
if b == "" {
|
||||
return a
|
||||
}
|
||||
return strings.TrimSuffix(a, "/") + "/" + b
|
||||
}
|
||||
|
|
@ -41,24 +41,7 @@ func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle,
|
|||
|
||||
// Confirm that the cursor is inside a use statement, and then find
|
||||
// the position of the use statement's directory path.
|
||||
var use *modfile.Use
|
||||
var pathStart, pathEnd int
|
||||
for _, u := range pw.File.Use {
|
||||
dep := []byte(u.Path)
|
||||
s, e := u.Syntax.Start.Byte, u.Syntax.End.Byte
|
||||
i := bytes.Index(pw.Mapper.Content[s:e], dep)
|
||||
if i == -1 {
|
||||
// This should not happen.
|
||||
continue
|
||||
}
|
||||
// Shift the start position to the location of the
|
||||
// module directory within the use statement.
|
||||
pathStart, pathEnd = s+i, s+i+len(dep)
|
||||
if token.Pos(pathStart) <= hoverRng.Start && hoverRng.Start <= token.Pos(pathEnd) {
|
||||
use = u
|
||||
break
|
||||
}
|
||||
}
|
||||
use, pathStart, pathEnd := usePath(pw, hoverRng.Start)
|
||||
|
||||
// The cursor position is not on a use statement.
|
||||
if use == nil {
|
||||
|
|
@ -87,3 +70,22 @@ func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle,
|
|||
Range: rng,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func usePath(pw *source.ParsedWorkFile, pos token.Pos) (use *modfile.Use, pathStart, pathEnd int) {
|
||||
for _, u := range pw.File.Use {
|
||||
path := []byte(u.Path)
|
||||
s, e := u.Syntax.Start.Byte, u.Syntax.End.Byte
|
||||
i := bytes.Index(pw.Mapper.Content[s:e], path)
|
||||
if i == -1 {
|
||||
// This should not happen.
|
||||
continue
|
||||
}
|
||||
// Shift the start position to the location of the
|
||||
// module directory within the use statement.
|
||||
pathStart, pathEnd = s+i, s+i+len(path)
|
||||
if token.Pos(pathStart) <= pos && pos <= token.Pos(pathEnd) {
|
||||
return u, pathStart, pathEnd
|
||||
}
|
||||
}
|
||||
return nil, 0, 0
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue