| // Copyright 2025 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 completion |
| |
| // unimported completion is invoked when the user types something like 'foo.xx', |
| // foo is known to be a package name not yet imported in the current file, and |
| // xx (or whatever the user has typed) is interpreted as a hint (pattern) for the |
| // member of foo that the user is looking for. |
| // |
| // This code looks for a suitable completion in a number of places. A 'suitable |
| // completion' is an exported symbol (so a type, const, var, or func) from package |
| // foo, which, after converting everything to lower case, has the pattern as a |
| // subsequence. |
| // |
| // The code looks for a suitable completion in |
| // 1. the imports of some other file of the current package, |
| // 2. the standard library, |
| // 3. the imports of some other file in the current workspace, |
| // 4. the module cache. |
| // It stops at the first success. |
| |
| import ( |
| "context" |
| "fmt" |
| "go/ast" |
| "go/printer" |
| "go/token" |
| "path" |
| "slices" |
| "strings" |
| |
| "golang.org/x/tools/gopls/internal/cache/metadata" |
| "golang.org/x/tools/gopls/internal/golang/completion/snippet" |
| "golang.org/x/tools/gopls/internal/protocol" |
| "golang.org/x/tools/gopls/internal/util/bug" |
| "golang.org/x/tools/internal/imports" |
| "golang.org/x/tools/internal/modindex" |
| "golang.org/x/tools/internal/stdlib" |
| "golang.org/x/tools/internal/versions" |
| ) |
| |
| func (c *completer) unimported(ctx context.Context, pkgname metadata.PackageName, prefix string) { |
| wsIDs, ourIDs := c.findPackageIDs(pkgname) |
| stdpkgs := c.stdlibPkgs(pkgname) |
| if len(ourIDs) > 0 { |
| // use the one in the current package, if possible |
| items := c.pkgIDmatches(ctx, ourIDs, pkgname, prefix) |
| if c.scoreList(items) { |
| return |
| } |
| } |
| // do the stdlib next. |
| items := c.stdlibMatches(stdpkgs, pkgname, prefix) |
| if c.scoreList(items) { |
| return |
| } |
| |
| // look in the rest of the workspace |
| items = c.pkgIDmatches(ctx, wsIDs, pkgname, prefix) |
| if c.scoreList(items) { |
| return |
| } |
| |
| // look in the module cache, for the last chance |
| items, err := c.modcacheMatches(pkgname, prefix) |
| if err == nil { |
| c.scoreList(items) |
| } |
| } |
| |
| // find all the packageIDs for packages in the workspace that have the desired name |
| // thisPkgIDs contains the ones known to the current package, wsIDs contains the others |
| func (c *completer) findPackageIDs(pkgname metadata.PackageName) (wsIDs, thisPkgIDs []metadata.PackageID) { |
| g := c.snapshot.MetadataGraph() |
| for pid, pkg := range c.snapshot.MetadataGraph().Packages { |
| if pkg.Name != pkgname { |
| continue |
| } |
| imports := g.ImportedBy[pid] |
| // Metadata is not canonical: it may be held onto by a package. Therefore, |
| // we must compare by ID. |
| thisPkg := func(mp *metadata.Package) bool { return mp.ID == c.pkg.Metadata().ID } |
| if slices.ContainsFunc(imports, thisPkg) { |
| thisPkgIDs = append(thisPkgIDs, pid) |
| } else { |
| wsIDs = append(wsIDs, pid) |
| } |
| } |
| return |
| } |
| |
| // find all the stdlib packages that have the desired name |
| func (c *completer) stdlibPkgs(pkgname metadata.PackageName) []metadata.PackagePath { |
| var pkgs []metadata.PackagePath // stlib packages that match pkg |
| for pkgpath := range stdlib.PackageSymbols { |
| v := metadata.PackageName(path.Base(pkgpath)) |
| if v == pkgname { |
| pkgs = append(pkgs, metadata.PackagePath(pkgpath)) |
| } else if imports.WithoutVersion(string(pkgpath)) == string(pkgname) { |
| pkgs = append(pkgs, metadata.PackagePath(pkgpath)) |
| } |
| } |
| return pkgs |
| } |
| |
| // return CompletionItems for all matching symbols in the packages in ids. |
| func (c *completer) pkgIDmatches(ctx context.Context, ids []metadata.PackageID, pkgname metadata.PackageName, prefix string) []CompletionItem { |
| pattern := strings.ToLower(prefix) |
| allpkgsyms, err := c.snapshot.Symbols(ctx, ids...) |
| if err != nil { |
| return nil // would if be worth retrying the ids one by one? |
| } |
| if len(allpkgsyms) != len(ids) { |
| bug.Reportf("Symbols returned %d values for %d pkgIDs", len(allpkgsyms), len(ids)) |
| return nil |
| } |
| var got []CompletionItem |
| for i, pkgID := range ids { |
| pkg := c.snapshot.MetadataGraph().Packages[pkgID] |
| if pkg == nil { |
| bug.Reportf("no metadata for %s", pkgID) |
| continue // something changed underfoot, otherwise can't happen |
| } |
| pkgsyms := allpkgsyms[i] |
| pkgfname := pkgsyms.Files[0].Path() |
| if !imports.CanUse(c.filename, pkgfname) { |
| // avoid unusable internal, etc |
| continue |
| } |
| // are any of these any good? |
| for np, asym := range pkgsyms.Symbols { |
| for _, sym := range asym { |
| if !token.IsExported(sym.Name) { |
| continue |
| } |
| if !usefulCompletion(sym.Name, pattern) { |
| // for json.U, the existing code finds InvalidUTF8Error |
| continue |
| } |
| var params []string |
| var kind protocol.CompletionItemKind |
| var detail string |
| switch sym.Kind { |
| case protocol.Function: |
| foundURI := pkgsyms.Files[np] |
| fh := c.snapshot.FindFile(foundURI) |
| pgf, err := c.snapshot.ParseGo(ctx, fh, 0) |
| if err == nil { |
| params = funcParams(pgf.File, sym.Name) |
| } |
| kind = protocol.FunctionCompletion |
| detail = fmt.Sprintf("func (from %q)", pkg.PkgPath) |
| case protocol.Variable, protocol.Struct: |
| kind = protocol.VariableCompletion |
| detail = fmt.Sprintf("var (from %q)", pkg.PkgPath) |
| case protocol.Constant: |
| kind = protocol.ConstantCompletion |
| detail = fmt.Sprintf("const (from %q)", pkg.PkgPath) |
| default: |
| continue |
| } |
| got = c.appendNewItem(got, sym.Name, |
| detail, |
| pkg.PkgPath, |
| kind, |
| pkgname, params) |
| } |
| } |
| } |
| return got |
| } |
| |
| // return CompletionItems for all the matches in packages in pkgs. |
| func (c *completer) stdlibMatches(pkgs []metadata.PackagePath, pkg metadata.PackageName, prefix string) []CompletionItem { |
| // check for deprecated symbols someday |
| got := make([]CompletionItem, 0) |
| pattern := strings.ToLower(prefix) |
| // avoid non-determinacy, especially for marker tests |
| slices.Sort(pkgs) |
| for _, candpkg := range pkgs { |
| if std, ok := stdlib.PackageSymbols[string(candpkg)]; ok { |
| for _, sym := range std { |
| if !usefulCompletion(sym.Name, pattern) { |
| continue |
| } |
| if !versions.AtLeast(c.goversion, sym.Version.String()) { |
| continue |
| } |
| var kind protocol.CompletionItemKind |
| var detail string |
| var params []string |
| switch sym.Kind { |
| case stdlib.Func: |
| params = parseSignature(sym.Signature) |
| kind = protocol.FunctionCompletion |
| detail = fmt.Sprintf("func (from %q)", candpkg) |
| case stdlib.Const: |
| kind = protocol.ConstantCompletion |
| detail = fmt.Sprintf("const (from %q)", candpkg) |
| case stdlib.Var: |
| kind = protocol.VariableCompletion |
| detail = fmt.Sprintf("var (from %q)", candpkg) |
| case stdlib.Type: |
| kind = protocol.VariableCompletion |
| detail = fmt.Sprintf("type (from %q)", candpkg) |
| default: |
| continue |
| } |
| got = c.appendNewItem(got, sym.Name, |
| detail, |
| candpkg, |
| kind, |
| pkg, params) |
| } |
| } |
| } |
| return got |
| } |
| |
| func (c *completer) modcacheMatches(pkg metadata.PackageName, prefix string) ([]CompletionItem, error) { |
| ix, err := c.snapshot.View().ModcacheIndex() |
| if err != nil { |
| return nil, err |
| } |
| // retrieve everything and let usefulCompletion() and the matcher sort them out |
| cands := ix.Lookup(string(pkg), "", true) |
| lx := len(cands) |
| got := make([]CompletionItem, 0, lx) |
| pattern := strings.ToLower(prefix) |
| for _, cand := range cands { |
| if !usefulCompletion(cand.Name, pattern) { |
| continue |
| } |
| var params []string |
| var kind protocol.CompletionItemKind |
| var detail string |
| switch cand.Type { |
| case modindex.Func: |
| for _, f := range cand.Sig { |
| params = append(params, fmt.Sprintf("%s %s", f.Arg, f.Type)) |
| } |
| kind = protocol.FunctionCompletion |
| detail = fmt.Sprintf("func (from %s)", cand.ImportPath) |
| case modindex.Var: |
| kind = protocol.VariableCompletion |
| detail = fmt.Sprintf("var (from %s)", cand.ImportPath) |
| case modindex.Const: |
| kind = protocol.ConstantCompletion |
| detail = fmt.Sprintf("const (from %s)", cand.ImportPath) |
| case modindex.Type: // might be a type alias |
| kind = protocol.VariableCompletion |
| detail = fmt.Sprintf("type (from %s)", cand.ImportPath) |
| default: |
| continue |
| } |
| got = c.appendNewItem(got, cand.Name, |
| detail, |
| metadata.PackagePath(cand.ImportPath), |
| kind, |
| pkg, params) |
| } |
| return got, nil |
| } |
| |
| func (c *completer) appendNewItem(got []CompletionItem, name, detail string, path metadata.PackagePath, kind protocol.CompletionItemKind, pkg metadata.PackageName, params []string) []CompletionItem { |
| item := CompletionItem{ |
| Label: name, |
| Detail: detail, |
| InsertText: name, |
| Kind: kind, |
| } |
| imp := importInfo{ |
| importPath: string(path), |
| name: string(pkg), |
| } |
| if imports.ImportPathToAssumedName(string(path)) == string(pkg) { |
| imp.name = "" |
| } |
| item.AdditionalTextEdits, _ = c.importEdits(&imp) |
| if params != nil { |
| var sn snippet.Builder |
| c.functionCallSnippet(name, nil, params, &sn) |
| item.snippet = &sn |
| } |
| got = append(got, item) |
| return got |
| } |
| |
| // score the list. Return true if any item is added to c.items |
| func (c *completer) scoreList(items []CompletionItem) bool { |
| ret := false |
| for _, item := range items { |
| item.Score = float64(c.matcher.Score(item.Label)) |
| if item.Score > 0 { |
| c.items = append(c.items, item) |
| ret = true |
| } |
| } |
| return ret |
| } |
| |
| // pattern is always the result of strings.ToLower |
| func usefulCompletion(name, pattern string) bool { |
| // this travesty comes from foo.(type) somehow. see issue59096.txt |
| if pattern == "_" { |
| return true |
| } |
| // convert both to lower case, and then the runes in the pattern have to occur, in order, |
| // in the name |
| cand := strings.ToLower(name) |
| for _, r := range pattern { |
| ix := strings.IndexRune(cand, r) |
| if ix < 0 { |
| return false |
| } |
| cand = cand[ix+1:] |
| } |
| return true |
| } |
| |
| // return a printed version of the function arguments for snippets |
| func funcParams(f *ast.File, fname string) []string { |
| var params []string |
| setParams := func(list *ast.FieldList) { |
| if list == nil { |
| return |
| } |
| var cfg printer.Config // slight overkill |
| param := func(name string, typ ast.Expr) { |
| var buf strings.Builder |
| buf.WriteString(name) |
| buf.WriteByte(' ') |
| cfg.Fprint(&buf, token.NewFileSet(), typ) // ignore error |
| params = append(params, buf.String()) |
| } |
| |
| for _, field := range list.List { |
| if field.Names != nil { |
| for _, name := range field.Names { |
| param(name.Name, field.Type) |
| } |
| } else { |
| param("_", field.Type) |
| } |
| } |
| } |
| for _, n := range f.Decls { |
| switch x := n.(type) { |
| case *ast.FuncDecl: |
| if x.Recv == nil && x.Name.Name == fname { |
| setParams(x.Type.Params) |
| } |
| } |
| } |
| return params |
| } |
| |
| // extract the formal parameters from the signature. |
| // func[M1 ~map[K]V, M2 ~map[K]V, K comparable, V any](dst M1, src M2) -> []{"dst M1", "src M2"} |
| // func[K comparable, V any](seq iter.Seq2[K, V]) map[K]V -> []{"seq iter.Seq2[K, V]"} |
| // func(args ...any) *Logger -> []{"args ...any"} |
| // func[M ~map[K]V, K comparable, V any](m M, del func(K, V) bool) -> []{"m M", "del func(K, V) bool"} |
| func parseSignature(sig string) []string { |
| var level int // nesting level of delimiters |
| var processing bool // are we doing the params |
| var last int // start of current parameter |
| var params []string |
| for i := range len(sig) { |
| switch sig[i] { |
| case '[', '{': |
| level++ |
| case ']', '}': |
| level-- |
| case '(': |
| level++ |
| if level == 1 { |
| processing = true |
| last = i + 1 |
| } |
| case ')': |
| level-- |
| if level == 0 && processing { // done |
| if i > last { |
| params = append(params, strings.TrimSpace(sig[last:i])) |
| } |
| return params |
| } |
| case ',': |
| if level == 1 && processing { |
| params = append(params, strings.TrimSpace(sig[last:i])) |
| last = i + 1 |
| } |
| } |
| } |
| return nil |
| } |