mirror of https://github.com/golang/go.git
157 lines
4.8 KiB
Go
157 lines
4.8 KiB
Go
// 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"
|
|
"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"
|
|
)
|
|
|
|
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, fmt.Errorf("getting go.work file handle: %w", err)
|
|
}
|
|
pos, err := pw.Mapper.Pos(position)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("computing cursor position: %w", err)
|
|
}
|
|
|
|
// Find the use statement the user is in.
|
|
cursor := pos - 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, 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
|
|
}
|