aboutsummaryrefslogtreecommitdiff
path: root/gopls/internal/lsp/work/completion.go
diff options
context:
space:
mode:
Diffstat (limited to 'gopls/internal/lsp/work/completion.go')
-rw-r--r--gopls/internal/lsp/work/completion.go154
1 files changed, 154 insertions, 0 deletions
diff --git a/gopls/internal/lsp/work/completion.go b/gopls/internal/lsp/work/completion.go
new file mode 100644
index 000000000..bcdc2d1f4
--- /dev/null
+++ b/gopls/internal/lsp/work/completion.go
@@ -0,0 +1,154 @@
+// 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"
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+
+ "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/lsp/source"
+ "golang.org/x/tools/internal/event"
+)
+
+func Completion(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, 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, fmt.Errorf("getting go.work file handle: %w", err)
+ }
+ cursor, err := pw.Mapper.PositionOffset(position)
+ if err != nil {
+ return nil, fmt.Errorf("computing cursor offset: %w", err)
+ }
+
+ // Find the use statement the user is in.
+ use, pathStart, _ := usePath(pw, cursor)
+ if use == nil {
+ return &protocol.CompletionList{}, nil
+ }
+ completingFrom := use.Path[:cursor-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, fmt.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
+}