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}}