From f3faf8b6e0a07c06fcb63c6225176bfabf2d01e2 Mon Sep 17 00:00:00 2001
From: Brad Garcia
Date: Thu, 21 Nov 2013 11:55:42 -0500
Subject: [PATCH] godoc: add search results that point to documentation instead
of source. Add explicit options to Corpus to control search indexing of
documentation, Go source code, and full-text.
R=bradfitz, r
CC=golang-dev
https://golang.org/cl/24190043
---
cmd/godoc/main.go | 4 +
godoc/corpus.go | 29 +++-
godoc/godoc.go | 6 +
godoc/index.go | 350 +++++++++++++++++++++++++++++----------
godoc/index_test.go | 67 ++++++++
godoc/search.go | 27 ++-
godoc/spot.go | 17 ++
godoc/static/search.html | 15 ++
godoc/static/search.txt | 7 +
godoc/static/static.go | 22 +++
10 files changed, 447 insertions(+), 97 deletions(-)
diff --git a/cmd/godoc/main.go b/cmd/godoc/main.go
index 544347a7ad..c9065784f4 100644
--- a/cmd/godoc/main.go
+++ b/cmd/godoc/main.go
@@ -190,7 +190,11 @@ func main() {
corpus := godoc.NewCorpus(fs)
corpus.Verbose = *verbose
+ corpus.MaxResults = *maxResults
corpus.IndexEnabled = *indexEnabled && httpMode
+ if *maxResults == 0 {
+ corpus.IndexFullText = false
+ }
corpus.IndexFiles = *indexFiles
corpus.IndexThrottle = *indexThrottle
if *writeIndex {
diff --git a/godoc/corpus.go b/godoc/corpus.go
index d8d19e57bd..6efc05d4f1 100644
--- a/godoc/corpus.go
+++ b/godoc/corpus.go
@@ -45,8 +45,25 @@ type Corpus struct {
// built once.
IndexInterval time.Duration
+ // IndexDocs enables indexing of Go documentation.
+ // This will produce search results for exported types, functions,
+ // methods, variables, and constants, and will link to the godoc
+ // documentation for those identifiers.
+ IndexDocs bool
+
+ // IndexGoCode enables indexing of Go source code.
+ // This will produce search results for internal and external identifiers
+ // and will link to both declarations and uses of those identifiers in
+ // source code.
+ IndexGoCode bool
+
+ // IndexFullText enables full-text indexing.
+ // This will provide search results for any matching text in any file that
+ // is indexed, including non-Go files (see whitelisted in index.go).
+ // Regexp searching is supported via full-text indexing.
+ IndexFullText bool
+
// MaxResults optionally specifies the maximum results for indexing.
- // The default is 1000.
MaxResults int
// SummarizePackage optionally specifies a function to
@@ -85,14 +102,18 @@ type Corpus struct {
}
// NewCorpus returns a new Corpus from a filesystem.
-// Set any options on Corpus before calling the Corpus.Init method.
+// The returned corpus has all indexing enabled and MaxResults set to 1000.
+// Change or set any options on Corpus before calling the Corpus.Init method.
func NewCorpus(fs vfs.FileSystem) *Corpus {
c := &Corpus{
fs: fs,
refreshMetadataSignal: make(chan bool, 1),
- MaxResults: 1000,
- IndexEnabled: true,
+ MaxResults: 1000,
+ IndexEnabled: true,
+ IndexDocs: true,
+ IndexGoCode: true,
+ IndexFullText: true,
}
return c
}
diff --git a/godoc/godoc.go b/godoc/godoc.go
index d71498db93..eee477c2f6 100644
--- a/godoc/godoc.go
+++ b/godoc/godoc.go
@@ -79,6 +79,7 @@ func (p *Presentation) initFuncMap() {
"pkgLink": pkgLinkFunc,
"srcLink": srcLinkFunc,
"posLink_url": newPosLink_urlFunc(srcPosLinkFunc),
+ "docLink": docLinkFunc,
// formatting of Examples
"example_html": p.example_htmlFunc,
@@ -297,6 +298,11 @@ func srcLinkFunc(s string) string {
return pathpkg.Clean("/" + s)
}
+func docLinkFunc(s string, ident string) string {
+ s = strings.TrimPrefix(s, "/src")
+ return pathpkg.Clean("/"+s) + "/#" + ident
+}
+
func (p *Presentation) example_textFunc(info *PageInfo, funcName, indent string) string {
if !p.ShowExamples {
return ""
diff --git a/godoc/index.go b/godoc/index.go
index f7f45ea6f0..5b73e0033b 100644
--- a/godoc/index.go
+++ b/godoc/index.go
@@ -44,6 +44,7 @@ import (
"errors"
"fmt"
"go/ast"
+ "go/doc"
"go/parser"
"go/token"
"index/suffixarray"
@@ -348,13 +349,44 @@ func (a *AltWords) filter(s string) *AltWords {
return nil
}
+// Ident stores information about external identifiers in order to create
+// links to package documentation.
+type Ident struct {
+ Path string // e.g. "net/http"
+ Package string // e.g. "http"
+ Name string // e.g. "NewRequest"
+ Doc string // e.g. "NewRequest returns a new Request..."
+}
+
+type byPackage []Ident
+
+func (s byPackage) Len() int { return len(s) }
+func (s byPackage) Less(i, j int) bool {
+ if s[i].Package == s[j].Package {
+ return s[i].Path < s[j].Path
+ }
+ return s[i].Package < s[j].Package
+}
+func (s byPackage) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
+
+// Filter creates a new Ident list where the results match the given
+// package name.
+func (s byPackage) filter(pakname string) []Ident {
+ if s == nil {
+ return nil
+ }
+ var res []Ident
+ for _, i := range s {
+ if i.Package == pakname {
+ res = append(res, i)
+ }
+ }
+ return res
+}
+
// ----------------------------------------------------------------------------
// Indexer
-// Adjust these flags as seems best.
-const includeMainPackages = true
-const includeTestFiles = true
-
type IndexResult struct {
Decls RunList // package-level declarations (with snippets)
Others RunList // all other occurrences
@@ -393,6 +425,7 @@ type Indexer struct {
packagePath map[string]map[string]bool // "template" => "text/template" => true
exports map[string]map[string]SpotKind // "net/http" => "ListenAndServe" => FuncDecl
curPkgExports map[string]SpotKind
+ idents map[SpotKind]map[string][]Ident // kind => name => list of Idents
}
func (x *Indexer) intern(s string) string {
@@ -441,9 +474,11 @@ func (x *Indexer) visitIdent(kind SpotKind, id *ast.Ident) {
}
if kind == Use || x.decl == nil {
- // not a declaration or no snippet required
- info := makeSpotInfo(kind, x.current.Line(id.Pos()), false)
- lists.Others = append(lists.Others, Spot{x.file, info})
+ if x.c.IndexGoCode {
+ // not a declaration or no snippet required
+ info := makeSpotInfo(kind, x.current.Line(id.Pos()), false)
+ lists.Others = append(lists.Others, Spot{x.file, info})
+ }
} else {
// a declaration with snippet
index := x.addSnippet(NewSnippet(x.fset, x.decl, id))
@@ -554,16 +589,6 @@ func (x *Indexer) Visit(node ast.Node) ast.Visitor {
return nil
}
-func pkgName(filename string) string {
- // use a new file set each time in order to not pollute the indexer's
- // file set (which must stay in sync with the concatenated source code)
- file, err := parser.ParseFile(token.NewFileSet(), filename, nil, parser.PackageClauseOnly)
- if err != nil || file == nil {
- return ""
- }
- return file.Name.Name
-}
-
// addFile adds a file to the index if possible and returns the file set file
// and the file's AST if it was successfully parsed as a Go file. If addFile
// failed (that is, if the file was not added), it returns file == nil.
@@ -668,26 +693,132 @@ func isWhitelisted(filename string) bool {
return whitelisted[key]
}
-func (x *Indexer) visitFile(dirname string, fi os.FileInfo, fulltextIndex bool) {
- if fi.IsDir() {
+func (x *Indexer) indexDocs(dirname string, filename string, astFile *ast.File) {
+ pkgName := astFile.Name.Name
+ if pkgName == "main" {
+ return
+ }
+ astPkg := ast.Package{
+ Name: pkgName,
+ Files: map[string]*ast.File{
+ filename: astFile,
+ },
+ }
+ var m doc.Mode
+ docPkg := doc.New(&astPkg, pathpkg.Clean(dirname), m)
+ addIdent := func(sk SpotKind, name string, docstr string) {
+ if x.idents[sk] == nil {
+ x.idents[sk] = make(map[string][]Ident)
+ }
+ x.idents[sk][name] = append(x.idents[sk][name], Ident{
+ Path: pathpkg.Clean(dirname),
+ Package: pkgName,
+ Name: name,
+ Doc: doc.Synopsis(docstr),
+ })
+ }
+ for _, c := range docPkg.Consts {
+ for _, name := range c.Names {
+ addIdent(ConstDecl, name, c.Doc)
+ }
+ }
+ for _, t := range docPkg.Types {
+ addIdent(TypeDecl, t.Name, t.Doc)
+ for _, c := range t.Consts {
+ for _, name := range c.Names {
+ addIdent(ConstDecl, name, c.Doc)
+ }
+ }
+ for _, v := range t.Vars {
+ for _, name := range v.Names {
+ addIdent(VarDecl, name, v.Doc)
+ }
+ }
+ for _, f := range t.Funcs {
+ addIdent(FuncDecl, f.Name, f.Doc)
+ }
+ for _, f := range t.Methods {
+ addIdent(MethodDecl, f.Name, f.Doc)
+ // Change the name of methods to be ".".
+ // They will still be indexed as .
+ idents := x.idents[MethodDecl][f.Name]
+ idents[len(idents)-1].Name = t.Name + "." + f.Name
+ }
+ }
+ for _, v := range docPkg.Vars {
+ for _, name := range v.Names {
+ addIdent(VarDecl, name, v.Doc)
+ }
+ }
+ for _, f := range docPkg.Funcs {
+ addIdent(FuncDecl, f.Name, f.Doc)
+ }
+}
+
+func (x *Indexer) indexGoFile(dirname string, filename string, file *token.File, astFile *ast.File) {
+ pkgName := astFile.Name.Name
+
+ if x.c.IndexGoCode {
+ x.current = file
+ pak := x.lookupPackage(dirname, pkgName)
+ x.file = &File{filename, pak}
+ ast.Walk(x, astFile)
+ }
+
+ if x.c.IndexDocs {
+ // Test files are already filtered out in visitFile if IndexGoCode and
+ // IndexFullText are false. Otherwise, check here.
+ isTestFile := (x.c.IndexGoCode || x.c.IndexFullText) &&
+ (strings.HasSuffix(filename, "_test.go") || strings.HasPrefix(dirname, "test/"))
+ if !isTestFile {
+ x.indexDocs(dirname, filename, astFile)
+ }
+ }
+
+ ppKey := x.intern(pkgName)
+ if _, ok := x.packagePath[ppKey]; !ok {
+ x.packagePath[ppKey] = make(map[string]bool)
+ }
+ pkgPath := x.intern(strings.TrimPrefix(dirname, "/src/pkg/"))
+ x.packagePath[ppKey][pkgPath] = true
+
+ // Merge in exported symbols found walking this file into
+ // the map for that package.
+ if len(x.curPkgExports) > 0 {
+ dest, ok := x.exports[pkgPath]
+ if !ok {
+ dest = make(map[string]SpotKind)
+ x.exports[pkgPath] = dest
+ }
+ for k, v := range x.curPkgExports {
+ dest[k] = v
+ }
+ }
+}
+
+func (x *Indexer) visitFile(dirname string, fi os.FileInfo) {
+ if fi.IsDir() || !x.c.IndexEnabled {
return
}
filename := pathpkg.Join(dirname, fi.Name())
- goFile := false
+ goFile := isGoFile(fi)
switch {
- case isGoFile(fi):
- if !includeTestFiles && (!isPkgFile(fi) || strings.HasPrefix(filename, "test/")) {
+ case x.c.IndexFullText:
+ if !isWhitelisted(fi.Name()) {
return
}
- if !includeMainPackages && pkgName(filename) == "main" {
+ case x.c.IndexGoCode:
+ if !goFile {
+ return
+ }
+ case x.c.IndexDocs:
+ if !goFile ||
+ strings.HasSuffix(fi.Name(), "_test.go") ||
+ strings.HasPrefix(dirname, "test/") {
return
}
- goFile = true
-
- case !fulltextIndex || !isWhitelisted(fi.Name()):
- return
}
x.fsOpenGate <- true
@@ -711,31 +842,7 @@ func (x *Indexer) visitFile(dirname string, fi os.FileInfo, fulltextIndex bool)
}
if fast != nil {
- // we've got a Go file to index
- x.current = file
- pak := x.lookupPackage(dirname, fast.Name.Name)
- x.file = &File{fi.Name(), pak}
- ast.Walk(x, fast)
-
- ppKey := x.intern(fast.Name.Name)
- if _, ok := x.packagePath[ppKey]; !ok {
- x.packagePath[ppKey] = make(map[string]bool)
- }
- pkgPath := x.intern(strings.TrimPrefix(dirname, "/src/pkg/"))
- x.packagePath[ppKey][pkgPath] = true
-
- // Merge in exported symbols found walking this file into
- // the map for that package.
- if len(x.curPkgExports) > 0 {
- dest, ok := x.exports[pkgPath]
- if !ok {
- dest = make(map[string]SpotKind)
- x.exports[pkgPath] = dest
- }
- for k, v := range x.curPkgExports {
- dest[k] = v
- }
- }
+ x.indexGoFile(dirname, fi.Name(), file, fast)
}
// update statistics
@@ -744,6 +851,28 @@ func (x *Indexer) visitFile(dirname string, fi os.FileInfo, fulltextIndex bool)
x.stats.Lines += file.LineCount()
}
+// indexOptions contains information that affects the contents of an index.
+type indexOptions struct {
+ // Docs provides documentation search results.
+ // It is only consulted if IndexEnabled is true.
+ // The default values is true.
+ Docs bool
+
+ // GoCode provides Go source code search results.
+ // It is only consulted if IndexEnabled is true.
+ // The default values is true.
+ GoCode bool
+
+ // FullText provides search results from all files.
+ // It is only consulted if IndexEnabled is true.
+ // The default values is true.
+ FullText bool
+
+ // MaxResults optionally specifies the maximum results for indexing.
+ // The default is 1000.
+ MaxResults int
+}
+
// ----------------------------------------------------------------------------
// Index
@@ -762,6 +891,8 @@ type Index struct {
importCount map[string]int // package path ("net/http") => count
packagePath map[string]map[string]bool // "template" => "text/template" => true
exports map[string]map[string]SpotKind // "net/http" => "ListenAndServe" => FuncDecl
+ idents map[SpotKind]map[string][]Ident
+ opts indexOptions
}
func canonical(w string) string { return strings.ToLower(w) }
@@ -774,12 +905,18 @@ const (
maxOpenDirs = 50
)
-// NewIndex creates a new index for the .go files
-// in the directories given by dirnames.
-// The throttle parameter specifies a value between 0.0 and 1.0 that controls
-// artificial sleeping. If 0.0, the indexer always sleeps. If 1.0, the indexer
-// never sleeps.
-func NewIndex(c *Corpus, dirnames <-chan string, fulltextIndex bool, throttle float64) *Index {
+func (c *Corpus) throttle() float64 {
+ if c.IndexThrottle <= 0 {
+ return 0.9
+ }
+ if c.IndexThrottle > 1.0 {
+ return 1.0
+ }
+ return c.IndexThrottle
+}
+
+// NewIndex creates a new index for the .go files provided by the corpus.
+func (c *Corpus) NewIndex() *Index {
// initialize Indexer
// (use some reasonably sized maps to start)
x := &Indexer{
@@ -789,16 +926,17 @@ func NewIndex(c *Corpus, dirnames <-chan string, fulltextIndex bool, throttle fl
strings: make(map[string]string),
packages: make(map[Pak]*Pak, 256),
words: make(map[string]*IndexResult, 8192),
- throttle: util.NewThrottle(throttle, 100*time.Millisecond), // run at least 0.1s at a time
+ throttle: util.NewThrottle(c.throttle(), 100*time.Millisecond), // run at least 0.1s at a time
importCount: make(map[string]int),
packagePath: make(map[string]map[string]bool),
exports: make(map[string]map[string]SpotKind),
+ idents: make(map[SpotKind]map[string][]Ident, 4),
}
// index all files in the directories given by dirnames
var wg sync.WaitGroup // outstanding ReadDir + visitFile
dirGate := make(chan bool, maxOpenDirs)
- for dirname := range dirnames {
+ for dirname := range c.fsDirnames() {
if c.IndexDirectory != nil && !c.IndexDirectory(dirname) {
continue
}
@@ -817,14 +955,14 @@ func NewIndex(c *Corpus, dirnames <-chan string, fulltextIndex bool, throttle fl
wg.Add(1)
go func(fi os.FileInfo) {
defer wg.Done()
- x.visitFile(dirname, fi, fulltextIndex)
+ x.visitFile(dirname, fi)
}(fi)
}
}(dirname)
}
wg.Wait()
- if !fulltextIndex {
+ if !c.IndexFullText {
// the file set, the current file, and the sources are
// not needed after indexing if no text index is built -
// help GC and clear them
@@ -863,10 +1001,16 @@ func NewIndex(c *Corpus, dirnames <-chan string, fulltextIndex bool, throttle fl
// create text index
var suffixes *suffixarray.Index
- if fulltextIndex {
+ if c.IndexFullText {
suffixes = suffixarray.New(x.sources.Bytes())
}
+ for _, idMap := range x.idents {
+ for _, ir := range idMap {
+ sort.Sort(byPackage(ir))
+ }
+ }
+
return &Index{
fset: x.fset,
suffixes: suffixes,
@@ -877,12 +1021,19 @@ func NewIndex(c *Corpus, dirnames <-chan string, fulltextIndex bool, throttle fl
importCount: x.importCount,
packagePath: x.packagePath,
exports: x.exports,
+ idents: x.idents,
+ opts: indexOptions{
+ Docs: x.c.IndexDocs,
+ GoCode: x.c.IndexGoCode,
+ FullText: x.c.IndexFullText,
+ MaxResults: x.c.MaxResults,
+ },
}
}
var ErrFileIndexVersion = errors.New("file index version out of date")
-const fileIndexVersion = 2
+const fileIndexVersion = 3
// fileIndex is the subset of Index that's gob-encoded for use by
// Index.Write and Index.Read.
@@ -896,6 +1047,8 @@ type fileIndex struct {
ImportCount map[string]int
PackagePath map[string]map[string]bool
Exports map[string]map[string]SpotKind
+ Idents map[SpotKind]map[string][]Ident
+ Opts indexOptions
}
func (x *fileIndex) Write(w io.Writer) error {
@@ -923,6 +1076,8 @@ func (x *Index) WriteTo(w io.Writer) (n int64, err error) {
ImportCount: x.importCount,
PackagePath: x.packagePath,
Exports: x.exports,
+ Idents: x.idents,
+ Opts: x.opts,
}
if err := fx.Write(w); err != nil {
return 0, err
@@ -941,7 +1096,7 @@ func (x *Index) WriteTo(w io.Writer) (n int64, err error) {
return n, nil
}
-// Read reads the index from r into x; x must not be nil.
+// ReadFrom reads the index from r into x; x must not be nil.
// If r does not also implement io.ByteReader, it will be wrapped in a bufio.Reader.
// If the index is from an old version, the error is ErrFileIndexVersion.
func (x *Index) ReadFrom(r io.Reader) (n int64, err error) {
@@ -964,7 +1119,8 @@ func (x *Index) ReadFrom(r io.Reader) (n int64, err error) {
x.importCount = fx.ImportCount
x.packagePath = fx.PackagePath
x.exports = fx.Exports
-
+ x.idents = fx.Idents
+ x.opts = fx.Opts
if fx.Fulltext {
x.fset = token.NewFileSet()
decode := func(x interface{}) error {
@@ -1003,6 +1159,12 @@ func (x *Index) Exports() map[string]map[string]SpotKind {
return x.exports
}
+// Idents returns a map from identifier type to exported
+// symbol name to the list of identifiers matching that name.
+func (x *Index) Idents() map[SpotKind]map[string][]Ident {
+ return x.idents
+}
+
func (x *Index) lookupWord(w string) (match *LookupResult, alt *AltWords) {
match = x.words[w]
alt = x.alts[canonical(w)]
@@ -1027,47 +1189,55 @@ func isIdentifier(s string) bool {
}
// For a given query, which is either a single identifier or a qualified
-// identifier, Lookup returns a list of packages, a LookupResult, and a
-// list of alternative spellings, if any. Any and all results may be nil.
-// If the query syntax is wrong, an error is reported.
-func (x *Index) Lookup(query string) (paks HitList, match *LookupResult, alt *AltWords, err error) {
+// identifier, Lookup returns a SearchResult containing packages, a LookupResult, a
+// list of alternative spellings, and identifiers, if any. Any and all results
+// may be nil. If the query syntax is wrong, an error is reported.
+func (x *Index) Lookup(query string) (*SearchResult, error) {
ss := strings.Split(query, ".")
// check query syntax
for _, s := range ss {
if !isIdentifier(s) {
- err = errors.New("all query parts must be identifiers")
- return
+ return nil, errors.New("all query parts must be identifiers")
}
}
-
+ rslt := &SearchResult{
+ Query: query,
+ Idents: make(map[SpotKind][]Ident, 5),
+ }
// handle simple and qualified identifiers
switch len(ss) {
case 1:
ident := ss[0]
- match, alt = x.lookupWord(ident)
- if match != nil {
+ rslt.Hit, rslt.Alt = x.lookupWord(ident)
+ if rslt.Hit != nil {
// found a match - filter packages with same name
// for the list of packages called ident, if any
- paks = match.Others.filter(ident)
+ rslt.Pak = rslt.Hit.Others.filter(ident)
+ }
+ for k, v := range x.idents {
+ rslt.Idents[k] = v[ident]
}
case 2:
pakname, ident := ss[0], ss[1]
- match, alt = x.lookupWord(ident)
- if match != nil {
+ rslt.Hit, rslt.Alt = x.lookupWord(ident)
+ if rslt.Hit != nil {
// found a match - filter by package name
// (no paks - package names are not qualified)
- decls := match.Decls.filter(pakname)
- others := match.Others.filter(pakname)
- match = &LookupResult{decls, others}
+ decls := rslt.Hit.Decls.filter(pakname)
+ others := rslt.Hit.Others.filter(pakname)
+ rslt.Hit = &LookupResult{decls, others}
+ }
+ for k, v := range x.idents {
+ rslt.Idents[k] = byPackage(v[ident]).filter(pakname)
}
default:
- err = errors.New("query is not a (qualified) identifier")
+ return nil, errors.New("query is not a (qualified) identifier")
}
- return
+ return rslt, nil
}
func (x *Index) Snippet(i int) *Snippet {
@@ -1213,6 +1383,15 @@ func (c *Corpus) fsDirnames() <-chan string {
return ch
}
+// CompatibleWith reports whether the Index x is compatible with the corpus
+// indexing options set in c.
+func (x *Index) CompatibleWith(c *Corpus) bool {
+ return x.opts.Docs == c.IndexDocs &&
+ x.opts.GoCode == c.IndexGoCode &&
+ x.opts.FullText == c.IndexFullText &&
+ x.opts.MaxResults == c.MaxResults
+}
+
func (c *Corpus) readIndex(filenames string) error {
matches, err := filepath.Glob(filenames)
if err != nil {
@@ -1234,6 +1413,9 @@ func (c *Corpus) readIndex(filenames string) error {
if _, err := x.ReadFrom(io.MultiReader(files...)); err != nil {
return err
}
+ if !x.CompatibleWith(c) {
+ return fmt.Errorf("index file options are incompatible: %v", x.opts)
+ }
c.searchIndex.Set(x)
return nil
}
@@ -1249,7 +1431,7 @@ func (c *Corpus) UpdateIndex() {
} else if throttle > 1.0 {
throttle = 1.0
}
- index := NewIndex(c, c.fsDirnames(), c.MaxResults > 0, throttle)
+ index := c.NewIndex()
stop := time.Now()
c.searchIndex.Set(index)
if c.Verbose {
diff --git a/godoc/index_test.go b/godoc/index_test.go
index 66a1ba54e4..d245b30e23 100644
--- a/godoc/index_test.go
+++ b/godoc/index_test.go
@@ -7,6 +7,7 @@ package godoc
import (
"bytes"
"reflect"
+ "sort"
"strings"
"testing"
@@ -131,4 +132,70 @@ func testIndex(t *testing.T, ix *Index) {
}; !reflect.DeepEqual(got, want) {
t.Errorf("Exports = %v; want %v", got, want)
}
+
+ if got, want := ix.Idents(), map[SpotKind]map[string][]Ident{
+ ConstDecl: map[string][]Ident{
+ "Pi": []Ident{{"/src/pkg/foo", "foo", "Pi", ""}},
+ },
+ VarDecl: map[string][]Ident{
+ "Foos": []Ident{{"/src/pkg/foo", "foo", "Foos", ""}},
+ },
+ TypeDecl: map[string][]Ident{
+ "Foo": []Ident{{"/src/pkg/foo", "foo", "Foo", "Foo is stuff."}},
+ },
+ FuncDecl: map[string][]Ident{
+ "New": []Ident{{"/src/pkg/foo", "foo", "New", ""}},
+ "X": []Ident{{"/src/pkg/other/bar", "bar", "X", ""}},
+ },
+ }; !reflect.DeepEqual(got, want) {
+ t.Errorf("Idents = %v; want %v", got, want)
+ }
+}
+
+func TestIdentResultSort(t *testing.T) {
+ for _, tc := range []struct {
+ ir []Ident
+ exp []Ident
+ }{
+ {
+ ir: []Ident{
+ {"/a/b/pkg2", "pkg2", "MyFunc2", ""},
+ {"/b/d/pkg3", "pkg3", "MyFunc3", ""},
+ {"/a/b/pkg1", "pkg1", "MyFunc1", ""},
+ },
+ exp: []Ident{
+ {"/a/b/pkg1", "pkg1", "MyFunc1", ""},
+ {"/a/b/pkg2", "pkg2", "MyFunc2", ""},
+ {"/b/d/pkg3", "pkg3", "MyFunc3", ""},
+ },
+ },
+ } {
+ if sort.Sort(byPackage(tc.ir)); !reflect.DeepEqual(tc.ir, tc.exp) {
+ t.Errorf("got: %v, want %v", tc.ir, tc.exp)
+ }
+ }
+}
+
+func TestIdentPackageFilter(t *testing.T) {
+ for _, tc := range []struct {
+ ir []Ident
+ pak string
+ exp []Ident
+ }{
+ {
+ ir: []Ident{
+ {"/a/b/pkg2", "pkg2", "MyFunc2", ""},
+ {"/b/d/pkg3", "pkg3", "MyFunc3", ""},
+ {"/a/b/pkg1", "pkg1", "MyFunc1", ""},
+ },
+ pak: "pkg2",
+ exp: []Ident{
+ {"/a/b/pkg2", "pkg2", "MyFunc2", ""},
+ },
+ },
+ } {
+ if res := byPackage(tc.ir).filter(tc.pak); !reflect.DeepEqual(res, tc.exp) {
+ t.Errorf("got: %v, want %v", res, tc.exp)
+ }
+ }
}
diff --git a/godoc/search.go b/godoc/search.go
index 63a4f1f9e3..f9c0b20160 100644
--- a/godoc/search.go
+++ b/godoc/search.go
@@ -25,30 +25,30 @@ type SearchResult struct {
Found int // number of textual occurrences found
Textual []FileLines // textual matches of Query
Complete bool // true if all textual occurrences of Query are reported
+ Idents map[SpotKind][]Ident
}
func (c *Corpus) Lookup(query string) SearchResult {
- var result SearchResult
- result.Query = query
+ result := &SearchResult{Query: query}
index, timestamp := c.CurrentIndex()
if index != nil {
// identifier search
var err error
- result.Pak, result.Hit, result.Alt, err = index.Lookup(query)
- if err != nil && c.MaxResults <= 0 {
+ result, err = index.Lookup(query)
+ if err != nil && !c.IndexFullText {
// ignore the error if full text search is enabled
// since the query may be a valid regular expression
result.Alert = "Error in query string: " + err.Error()
- return result
+ return *result
}
// full text search
- if c.MaxResults > 0 && query != "" {
+ if c.IndexFullText && query != "" {
rx, err := regexp.Compile(query)
if err != nil {
result.Alert = "Error in query regular expression: " + err.Error()
- return result
+ return *result
}
// If we get maxResults+1 results we know that there are more than
// maxResults results and thus the result may be incomplete (to be
@@ -72,7 +72,7 @@ func (c *Corpus) Lookup(query string) SearchResult {
result.Alert = "Search index disabled: no results available"
}
- return result
+ return *result
}
func (p *Presentation) HandleSearch(w http.ResponseWriter, r *http.Request) {
@@ -84,8 +84,17 @@ func (p *Presentation) HandleSearch(w http.ResponseWriter, r *http.Request) {
return
}
+ haveResults := result.Hit != nil || len(result.Textual) > 0
+ if !haveResults {
+ for _, ir := range result.Idents {
+ if ir != nil {
+ haveResults = true
+ break
+ }
+ }
+ }
var title string
- if result.Hit != nil || len(result.Textual) > 0 {
+ if haveResults {
title = fmt.Sprintf(`Results for query %q`, query)
} else {
title = fmt.Sprintf(`No results found for query %q`, query)
diff --git a/godoc/spot.go b/godoc/spot.go
index 42aecd34dc..95ffa4b8ce 100644
--- a/godoc/spot.go
+++ b/godoc/spot.go
@@ -34,6 +34,23 @@ const (
nKinds
)
+var (
+ // These must match the SpotKind values above.
+ name = []string{
+ "Packages",
+ "Imports",
+ "Constants",
+ "Types",
+ "Variables",
+ "Functions",
+ "Methods",
+ "Uses",
+ "Unknown",
+ }
+)
+
+func (x SpotKind) Name() string { return name[x] }
+
func init() {
// sanity check: if nKinds is too large, the SpotInfo
// accessor functions may need to be updated
diff --git a/godoc/static/search.html b/godoc/static/search.html
index 5b54d71267..5e9093f6e3 100644
--- a/godoc/static/search.html
+++ b/godoc/static/search.html
@@ -28,6 +28,21 @@
{{end}}
+{{range $key, $val := .Idents}}
+ {{if $val}}
+ {{$key.Name}}
+ {{range $val}}
+ {{$pkg_html := pkgLink .Path | html}}
+ {{$doc_html := docLink .Path .Name| html}}
+ {{html .Package}}.{{.Name}}
+ {{if .Doc}}
+ {{comment_html .Doc}}
+ {{else}}
+ No documentation available
+ {{end}}
+ {{end}}
+ {{end}}
+{{end}}
{{with .Hit}}
{{with .Decls}}
Package-level declarations
diff --git a/godoc/static/search.txt b/godoc/static/search.txt
index 5251a388e0..15c1941b45 100644
--- a/godoc/static/search.txt
+++ b/godoc/static/search.txt
@@ -22,6 +22,13 @@ QUERY
---------------------------------------
+*/}}{{range $key, $val := .Idents}}{{if $val}}{{$key.Name}}
+{{range $val.Idents}} {{.Path}}.{{.Name}}
+{{end}}
+{{end}}{{end}}{{/* .Idents */}}{{/*
+
+---------------------------------------
+
*/}}{{with .Hit}}{{with .Decls}}PACKAGE-LEVEL DECLARATIONS
{{range .}}package {{.Pak.Name}}
diff --git a/godoc/static/static.go b/godoc/static/static.go
index 69bc9b1d12..98ec848904 100644
--- a/godoc/static/static.go
+++ b/godoc/static/static.go
@@ -1391,6 +1391,21 @@ function PlaygroundOutput(el) {
{{end}}
+{{range $key, $val := .Idents}}
+ {{if $val}}
+ {{$key.Name}}
+ {{range $val}}
+ {{$pkg_html := pkgLink .Path | html}}
+ {{$doc_html := docLink .Path .Name| html}}
+ {{html .Package}}.{{.Name}}
+ {{if .Doc}}
+ {{comment_html .Doc}}
+ {{else}}
+ No documentation available
+ {{end}}
+ {{end}}
+ {{end}}
+{{end}}
{{with .Hit}}
{{with .Decls}}
Package-level declarations
@@ -1495,6 +1510,13 @@ function PlaygroundOutput(el) {
---------------------------------------
+*/}}{{range $key, $val := .Idents}}{{if $val}}{{$key.Name}}
+{{range $val.Idents}} {{.Path}}.{{.Name}}
+{{end}}
+{{end}}{{end}}{{/* .Idents */}}{{/*
+
+---------------------------------------
+
*/}}{{with .Hit}}{{with .Decls}}PACKAGE-LEVEL DECLARATIONS
{{range .}}package {{.Pak.Name}}