blob: 01a39871369202d1a6c0f0f9cef2a2df54ba8dcb [file] [log] [blame]
// Copyright 2019 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 complete defines a Completion type that is used in auto-completion,
// along with Encode and Decode methods that can be used for storing this type
// in redis.
package complete
import (
"fmt"
"strconv"
"strings"
"golang.org/x/pkgsite/internal/derrors"
)
const keySep = "|"
// Redis keys for completion sorted sets ("indexes"). They are in this package
// so that they can be accessed by both worker and frontend.
const (
KeyPrefix = "completions"
PopularKey = KeyPrefix + "Popular"
RemainingKey = KeyPrefix + "Rest"
)
// Completion holds package data from an auto-completion match.
type Completion struct {
// Suffix is the path suffix that matched the compltion input, e.g. a query
// for "error" would match the suffix "errors" of "github.com/pkg/errors".
Suffix string
// ModulePath is the module path of the completion match. We may support
// matches of the same path in different modules.
ModulePath string
// Version is the module version of the completion entry.
Version string
// PackagePath is the full import path.
PackagePath string
// Importers is the number of importers of this package. It is used for
// sorting completion results.
Importers int
}
// Encode string-encodes a completion for storing in the completion index.
func (c Completion) Encode() string {
return strings.Join(c.keyData(), keySep)
}
func (c Completion) keyData() []string {
var suffix string
if strings.HasPrefix(c.PackagePath, c.ModulePath) {
suffix = strings.TrimPrefix(c.PackagePath, c.ModulePath)
suffix = "/" + strings.Trim(suffix, "/")
} else {
// In the case of the standard library, ModulePath will not be a prefix of
// PackagePath.
suffix = c.PackagePath
}
return []string{
c.Suffix,
strings.TrimRight(c.ModulePath, "/"),
c.Version,
suffix,
// It's important that importers is last in this key, since it is the only
// datum that changes. By having it last, we reserve the ability to
// selectively update this entry by deleting the prefix corresponding to
// the values above.
strconv.Itoa(c.Importers),
}
}
// Decode parses a completion entry from the completions index.
func Decode(entry string) (_ *Completion, err error) {
defer derrors.Wrap(&err, "complete.Decode(%q)", entry)
parts := strings.Split(entry, "|")
if len(parts) != 5 {
return nil, fmt.Errorf("got %d parts, want 5", len(parts))
}
c := &Completion{
Suffix: parts[0],
ModulePath: parts[1],
Version: parts[2],
}
suffix := parts[3]
if strings.HasPrefix(suffix, "/") {
c.PackagePath = strings.Trim(c.ModulePath+suffix, "/")
} else {
c.PackagePath = suffix
}
importers, err := strconv.Atoi(parts[4])
if err != nil {
return nil, fmt.Errorf("error parsing importers: %v", err)
}
c.Importers = importers
return c, nil
}
// PathCompletions generates completion entries for all possible suffixes of
// partial.PackagePath.
func PathCompletions(partial Completion) []*Completion {
suffs := pathSuffixes(partial.PackagePath)
var cs []*Completion
for _, pref := range suffs {
var next = partial
next.Suffix = pref
cs = append(cs, &next)
}
return cs
}
// pathSuffixes returns a slice of all path suffixes of a '/'-separated path,
// including the full path itself. i.e.
// pathSuffixes("foo/bar") = []string{"foo/bar", "bar"}
func pathSuffixes(path string) []string {
path = strings.ToLower(path)
var prefs []string
for len(path) > 0 {
prefs = append(prefs, path)
i := strings.Index(path, "/")
if i < 0 {
break
}
path = path[i+1:]
}
return prefs
}