Skip to content

Commit ee31f70

Browse files
committed
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 <[email protected]> Run-TryBot: Michael Matloob <[email protected]> Reviewed-by: Robert Findley <[email protected]> gopls-CI: kokoro <[email protected]> TryBot-Result: Gopher Robot <[email protected]>
1 parent 622cf7b commit ee31f70

File tree

4 files changed

+231
-18
lines changed

4 files changed

+231
-18
lines changed

gopls/internal/regtest/completion/completion_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,3 +599,48 @@ func BenchmarkFoo()
599599
}
600600
})
601601
}
602+
603+
func TestGoWorkCompletion(t *testing.T) {
604+
const files = `
605+
-- go.work --
606+
go 1.18
607+
608+
use ./a
609+
use ./a/ba
610+
use ./a/b/
611+
use ./dir/foo
612+
use ./dir/foobar/
613+
-- a/go.mod --
614+
-- go.mod --
615+
-- a/bar/go.mod --
616+
-- a/b/c/d/e/f/go.mod --
617+
-- dir/bar --
618+
-- dir/foobar/go.mod --
619+
`
620+
621+
Run(t, files, func(t *testing.T, env *Env) {
622+
env.OpenFile("go.work")
623+
624+
tests := []struct {
625+
re string
626+
want []string
627+
}{
628+
{`use ()\.`, []string{".", "./a", "./a/bar", "./dir/foobar"}},
629+
{`use \.()`, []string{"", "/a", "/a/bar", "/dir/foobar"}},
630+
{`use \./()`, []string{"a", "a/bar", "dir/foobar"}},
631+
{`use ./a()`, []string{"", "/b/c/d/e/f", "/bar"}},
632+
{`use ./a/b()`, []string{"/c/d/e/f", "ar"}},
633+
{`use ./a/b/()`, []string{`c/d/e/f`}},
634+
{`use ./a/ba()`, []string{"r"}},
635+
{`use ./dir/foo()`, []string{"bar"}},
636+
{`use ./dir/foobar/()`, []string{}},
637+
}
638+
for _, tt := range tests {
639+
completions := env.Completion("go.work", env.RegexpSearch("go.work", tt.re))
640+
diff := compareCompletionResults(tt.want, completions.Items)
641+
if diff != "" {
642+
t.Errorf("%s: %s", tt.re, diff)
643+
}
644+
}
645+
})
646+
}

internal/lsp/completion.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"golang.org/x/tools/internal/lsp/source"
1717
"golang.org/x/tools/internal/lsp/source/completion"
1818
"golang.org/x/tools/internal/lsp/template"
19+
"golang.org/x/tools/internal/lsp/work"
1920
"golang.org/x/tools/internal/span"
2021
)
2122

@@ -32,6 +33,12 @@ func (s *Server) completion(ctx context.Context, params *protocol.CompletionPara
3233
candidates, surrounding, err = completion.Completion(ctx, snapshot, fh, params.Position, params.Context)
3334
case source.Mod:
3435
candidates, surrounding = nil, nil
36+
case source.Work:
37+
cl, err := work.Completion(ctx, snapshot, fh, params.Position)
38+
if err != nil {
39+
break
40+
}
41+
return cl, nil
3542
case source.Tmpl:
3643
var cl *protocol.CompletionList
3744
cl, err = template.Completion(ctx, snapshot, fh, params.Position, params.Context)

internal/lsp/work/completion.go

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
// Copyright 2022 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package work
6+
7+
import (
8+
"context"
9+
"go/token"
10+
"os"
11+
"path/filepath"
12+
"sort"
13+
"strings"
14+
15+
"golang.org/x/tools/internal/event"
16+
"golang.org/x/tools/internal/lsp/protocol"
17+
"golang.org/x/tools/internal/lsp/source"
18+
errors "golang.org/x/xerrors"
19+
)
20+
21+
func Completion(ctx context.Context, snapshot source.Snapshot, fh source.VersionedFileHandle, position protocol.Position) (*protocol.CompletionList, error) {
22+
ctx, done := event.Start(ctx, "work.Completion")
23+
defer done()
24+
25+
// Get the position of the cursor.
26+
pw, err := snapshot.ParseWork(ctx, fh)
27+
if err != nil {
28+
return nil, errors.Errorf("getting go.work file handle: %w", err)
29+
}
30+
spn, err := pw.Mapper.PointSpan(position)
31+
if err != nil {
32+
return nil, errors.Errorf("computing cursor position: %w", err)
33+
}
34+
rng, err := spn.Range(pw.Mapper.Converter)
35+
if err != nil {
36+
return nil, errors.Errorf("computing range: %w", err)
37+
}
38+
39+
// Find the use statement the user is in.
40+
cursor := rng.Start - 1
41+
use, pathStart, _ := usePath(pw, cursor)
42+
if use == nil {
43+
return &protocol.CompletionList{}, nil
44+
}
45+
completingFrom := use.Path[:cursor-token.Pos(pathStart)]
46+
47+
// We're going to find the completions of the user input
48+
// (completingFrom) by doing a walk on the innermost directory
49+
// of the given path, and comparing the found paths to make sure
50+
// that they match the component of the path after the
51+
// innermost directory.
52+
//
53+
// We'll maintain two paths when doing this: pathPrefixSlash
54+
// is essentially the path the user typed in, and pathPrefixAbs
55+
// is the path made absolute from the go.work directory.
56+
57+
pathPrefixSlash := completingFrom
58+
pathPrefixAbs := filepath.FromSlash(pathPrefixSlash)
59+
if !filepath.IsAbs(pathPrefixAbs) {
60+
pathPrefixAbs = filepath.Join(filepath.Dir(pw.URI.Filename()), pathPrefixAbs)
61+
}
62+
63+
// pathPrefixDir is the directory that will be walked to find matches.
64+
// If pathPrefixSlash is not explicitly a directory boundary (is either equivalent to "." or
65+
// ends in a separator) we need to examine its parent directory to find sibling files that
66+
// match.
67+
depthBound := 5
68+
pathPrefixDir, pathPrefixBase := pathPrefixAbs, ""
69+
pathPrefixSlashDir := pathPrefixSlash
70+
if filepath.Clean(pathPrefixSlash) != "." && !strings.HasSuffix(pathPrefixSlash, "/") {
71+
depthBound++
72+
pathPrefixDir, pathPrefixBase = filepath.Split(pathPrefixAbs)
73+
pathPrefixSlashDir = dirNonClean(pathPrefixSlash)
74+
}
75+
76+
var completions []string
77+
// Stop traversing deeper once we've hit 10k files to try to stay generally under 100ms.
78+
const numSeenBound = 10000
79+
var numSeen int
80+
stopWalking := errors.New("hit numSeenBound")
81+
err = filepath.Walk(pathPrefixDir, func(wpath string, info os.FileInfo, err error) error {
82+
if numSeen > numSeenBound {
83+
// Stop traversing if we hit bound.
84+
return stopWalking
85+
}
86+
numSeen++
87+
88+
// rel is the path relative to pathPrefixDir.
89+
// Make sure that it has pathPrefixBase as a prefix
90+
// otherwise it won't match the beginning of the
91+
// base component of the path the user typed in.
92+
rel := strings.TrimPrefix(wpath[len(pathPrefixDir):], string(filepath.Separator))
93+
if info.IsDir() && wpath != pathPrefixDir && !strings.HasPrefix(rel, pathPrefixBase) {
94+
return filepath.SkipDir
95+
}
96+
97+
// Check for a match (a module directory).
98+
if filepath.Base(rel) == "go.mod" {
99+
relDir := strings.TrimSuffix(dirNonClean(rel), string(os.PathSeparator))
100+
completionPath := join(pathPrefixSlashDir, filepath.ToSlash(relDir))
101+
102+
if !strings.HasPrefix(completionPath, completingFrom) {
103+
return nil
104+
}
105+
if strings.HasSuffix(completionPath, "/") {
106+
// Don't suggest paths that end in "/". This happens
107+
// when the input is a path that ends in "/" and
108+
// the completion is empty.
109+
return nil
110+
}
111+
completion := completionPath[len(completingFrom):]
112+
if completingFrom == "" && !strings.HasPrefix(completion, "./") {
113+
// Bias towards "./" prefixes.
114+
completion = join(".", completion)
115+
}
116+
117+
completions = append(completions, completion)
118+
}
119+
120+
if depth := strings.Count(rel, string(filepath.Separator)); depth >= depthBound {
121+
return filepath.SkipDir
122+
}
123+
return nil
124+
})
125+
if err != nil && !errors.Is(err, stopWalking) {
126+
return nil, errors.Errorf("walking to find completions: %w", err)
127+
}
128+
129+
sort.Strings(completions)
130+
131+
var items []protocol.CompletionItem
132+
for _, c := range completions {
133+
items = append(items, protocol.CompletionItem{
134+
Label: c,
135+
InsertText: c,
136+
})
137+
}
138+
return &protocol.CompletionList{Items: items}, nil
139+
}
140+
141+
// dirNonClean is filepath.Dir, without the Clean at the end.
142+
func dirNonClean(path string) string {
143+
vol := filepath.VolumeName(path)
144+
i := len(path) - 1
145+
for i >= len(vol) && !os.IsPathSeparator(path[i]) {
146+
i--
147+
}
148+
return path[len(vol) : i+1]
149+
}
150+
151+
func join(a, b string) string {
152+
if a == "" {
153+
return b
154+
}
155+
if b == "" {
156+
return a
157+
}
158+
return strings.TrimSuffix(a, "/") + "/" + b
159+
}

internal/lsp/work/hover.go

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -41,24 +41,7 @@ func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle,
4141

4242
// Confirm that the cursor is inside a use statement, and then find
4343
// the position of the use statement's directory path.
44-
var use *modfile.Use
45-
var pathStart, pathEnd int
46-
for _, u := range pw.File.Use {
47-
dep := []byte(u.Path)
48-
s, e := u.Syntax.Start.Byte, u.Syntax.End.Byte
49-
i := bytes.Index(pw.Mapper.Content[s:e], dep)
50-
if i == -1 {
51-
// This should not happen.
52-
continue
53-
}
54-
// Shift the start position to the location of the
55-
// module directory within the use statement.
56-
pathStart, pathEnd = s+i, s+i+len(dep)
57-
if token.Pos(pathStart) <= hoverRng.Start && hoverRng.Start <= token.Pos(pathEnd) {
58-
use = u
59-
break
60-
}
61-
}
44+
use, pathStart, pathEnd := usePath(pw, hoverRng.Start)
6245

6346
// The cursor position is not on a use statement.
6447
if use == nil {
@@ -87,3 +70,22 @@ func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle,
8770
Range: rng,
8871
}, nil
8972
}
73+
74+
func usePath(pw *source.ParsedWorkFile, pos token.Pos) (use *modfile.Use, pathStart, pathEnd int) {
75+
for _, u := range pw.File.Use {
76+
path := []byte(u.Path)
77+
s, e := u.Syntax.Start.Byte, u.Syntax.End.Byte
78+
i := bytes.Index(pw.Mapper.Content[s:e], path)
79+
if i == -1 {
80+
// This should not happen.
81+
continue
82+
}
83+
// Shift the start position to the location of the
84+
// module directory within the use statement.
85+
pathStart, pathEnd = s+i, s+i+len(path)
86+
if token.Pos(pathStart) <= pos && pos <= token.Pos(pathEnd) {
87+
return u, pathStart, pathEnd
88+
}
89+
}
90+
return nil, 0, 0
91+
}

0 commit comments

Comments
 (0)