Commit 5e54e936 authored by Robert Griesemer's avatar Robert Griesemer

godoc: improved textual search

- improved search result page
- clicking on files shows highlighted search phrase
  (.go files loose their godoc-specific formatting and
  highlighting in this mode - a better solution is in
  the works)
- support for textual results
- fixed bug with non-URL escaped URL parameter (Query)

R=rsc, adg
CC=golang-dev
https://golang.org/cl/3585041
parent 40ff071e
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
{.section Accurate} {.section Accurate}
{.or} {.or}
<p> <p>
<span class="alert" style="font-size:120%">Indexing in progress - result may be inaccurate</span> <span class="alert" style="font-size:120%">Indexing in progress - result may be inaccurate.</span>
</p> </p>
{.end} {.end}
{.section Alt} {.section Alt}
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
{.repeated section Files} {.repeated section Files}
{.repeated section Groups} {.repeated section Groups}
{.repeated section Infos} {.repeated section Infos}
<a href="/{File.Path|url-src}?h={Query|html-esc}#L{@|infoLine}">{File.Path|url-src}:{@|infoLine}</a> <a href="/{File.Path|url-src}?h={Query|urlquery-esc}#L{@|infoLine}">{File.Path|url-src}:{@|infoLine}</a>
<pre>{@|infoSnippet}</pre> <pre>{@|infoSnippet}</pre>
{.end} {.end}
{.end} {.end}
...@@ -38,7 +38,7 @@ ...@@ -38,7 +38,7 @@
{.repeated section @} {.repeated section @}
<h3 id="Local_{Pak.Path|url-pkg}">package <a href="/{Pak.Path|url-pkg}">{Pak.Name|html-esc}</a></h3> <h3 id="Local_{Pak.Path|url-pkg}">package <a href="/{Pak.Path|url-pkg}">{Pak.Name|html-esc}</a></h3>
{.repeated section Files} {.repeated section Files}
<a href="/{File.Path|url-src}?h={Query|html-esc}">{File.Path|url-src}</a> <a href="/{File.Path|url-src}?h={Query|urlquery-esc}">{File.Path|url-src}</a>
<table class="layout"> <table class="layout">
{.repeated section Groups} {.repeated section Groups}
<tr> <tr>
...@@ -47,7 +47,7 @@ ...@@ -47,7 +47,7 @@
<td align="left" width="4"></td> <td align="left" width="4"></td>
<td> <td>
{.repeated section Infos} {.repeated section Infos}
<a href="/{File.Path|url-src}?h={Query|html-esc}#L{@|infoLine}">{@|infoLine}</a> <a href="/{File.Path|url-src}?h={Query|urlquery-esc}#L{@|infoLine}">{@|infoLine}</a>
{.end} {.end}
</td> </td>
</tr> </tr>
...@@ -57,36 +57,32 @@ ...@@ -57,36 +57,32 @@
{.end} {.end}
{.end} {.end}
{.end} {.end}
{.section Illegal}
<p>
<span class="alert" style="font-size:120%">Illegal query syntax</span>
</p>
<p>
A legal query is a single identifier (such as <a href="search?q=ToLower">ToLower</a>)
or a qualified identifier (such as <a href="search?q=math.Sin">math.Sin</a>).
</p>
{.end}
{.section Textual} {.section Textual}
<h2 id="Textual">Textual occurences</h2> {.section Complete}
<h2 id="Textual">{Found|html-esc} textual occurences</h2>
{.or}
<h2 id="Textual">More than {Found|html-esc} textual occurences</h2>
<p>
<span class="alert" style="font-size:120%">Not all files or lines containing {Query|html-esc} are shown.</span>
</p>
{.end}
<p>
<table class="layout"> <table class="layout">
<tr>
<th align=left>File</th>
<th align=left>Occurences</th>
<th align=left>Lines</th>
</tr>
{.repeated section @} {.repeated section @}
<tr> <tr>
<td> <td align="left" valign="top">
<a href="/{Filename|url-src}?h={Query|html-esc}">{Filename|url-src}</a>: <a href="/{Filename|url-src}?g={Query|urlquery-esc}">{Filename|url-src}</a>:
</td> </td>
{Lines|linelist} <td align="left" width="4"></td>
<th align="left" valign="top">{Lines|numlines}</th>
<td align="left" width="4"></td>
<td align="left">{Lines Complete|linelist}</td>
</tr> </tr>
{.end} {.end}
{.section Complete}
{.or}
<tr><td align="left">...</td></tr>
{.end}
</table> </table>
{.end}
{.section Complete}
{.or}
<p>
<span class="alert" style="font-size:120%">Incomplete list of results</span>
</p> </p>
{.end} {.end}
QUERY QUERY
{Query} {Query}
{.section Accurate} {.section Accurate}
{.or} {.or}
...@@ -45,9 +45,18 @@ package {Pak.Name} ...@@ -45,9 +45,18 @@ package {Pak.Name}
{.end} {.end}
{.end} {.end}
{.end} {.end}
{.section Illegal} {.section Textual}
ILLEGAL QUERY SYNTAX {.section Complete}
{Found} TEXTUAL OCCURENCES
{.or}
MORE THAN {Found} TEXTUAL OCCURENCES
{.end}
A legal query is a single identifier (such as ToLower) {.repeated section @}
or a qualified identifier (such as math.Sin). {Lines|numlines} {Filename|url-src}
{.end}
{.section Complete}
{.or}
... ...
{.end}
{.end} {.end}
...@@ -361,7 +361,7 @@ func writeObjInfo(w io.Writer, fset *token.FileSet, obj *ast.Object) { ...@@ -361,7 +361,7 @@ func writeObjInfo(w io.Writer, fset *token.FileSet, obj *ast.Object) {
// for 0 <= i < s.idcount. // for 0 <= i < s.idcount.
func (s *Styler) idList(fset *token.FileSet) []byte { func (s *Styler) idList(fset *token.FileSet) []byte {
var buf bytes.Buffer var buf bytes.Buffer
fmt.Fprintln(&buf, "[") buf.WriteString("[\n")
if s.idcount > 0 { if s.idcount > 0 {
// invert objmap: create an array [id]obj from map[obj]id // invert objmap: create an array [id]obj from map[obj]id
...@@ -382,7 +382,7 @@ func (s *Styler) idList(fset *token.FileSet) []byte { ...@@ -382,7 +382,7 @@ func (s *Styler) idList(fset *token.FileSet) []byte {
} }
} }
fmt.Fprintln(&buf, "]") buf.WriteString("]\n")
return buf.Bytes() return buf.Bytes()
} }
...@@ -600,7 +600,15 @@ func textFmt(w io.Writer, format string, x ...interface{}) { ...@@ -600,7 +600,15 @@ func textFmt(w io.Writer, format string, x ...interface{}) {
} }
// Template formatter for the various "url-xxx" formats. // Template formatter for "urlquery-esc" format.
func urlQueryEscFmt(w io.Writer, format string, x ...interface{}) {
var buf bytes.Buffer
writeAny(&buf, fileset(x), false, x[0])
template.HTMLEscape(w, []byte(http.URLEscape(string(buf.Bytes()))))
}
// Template formatter for the various "url-xxx" formats excluding url-esc.
func urlFmt(w io.Writer, format string, x ...interface{}) { func urlFmt(w io.Writer, format string, x ...interface{}) {
var path string var path string
var line int var line int
...@@ -737,21 +745,30 @@ func localnameFmt(w io.Writer, format string, x ...interface{}) { ...@@ -737,21 +745,30 @@ func localnameFmt(w io.Writer, format string, x ...interface{}) {
} }
// Template formatter for "numlines" format.
func numlinesFmt(w io.Writer, format string, x ...interface{}) {
list := x[0].([]int)
fmt.Fprintf(w, "%d", len(list))
}
// Template formatter for "linelist" format. // Template formatter for "linelist" format.
func linelistFmt(w io.Writer, format string, x ...interface{}) { func linelistFmt(w io.Writer, format string, x ...interface{}) {
const max = 20 // show at most this many lines
list := x[0].([]int) list := x[0].([]int)
// print number of occurences complete := x[1].(bool)
fmt.Fprintf(w, "<td>%d</td>", len(list))
// print actual lines const max = 100 // show at most this many lines
// TODO(gri) should sort them if len(list) > max {
for i, line := range list { list = list[0:max]
if i < max { complete = false
fmt.Fprintf(w, "<td>%d</td>", line) }
} else { sort.SortInts(list)
fmt.Fprint(w, "<td>...</td>")
break for _, line := range list {
} fmt.Fprintf(w, " %d", line)
}
if !complete {
fmt.Fprintf(w, " ...")
} }
} }
...@@ -761,6 +778,7 @@ var fmap = template.FormatterMap{ ...@@ -761,6 +778,7 @@ var fmap = template.FormatterMap{
"html": htmlFmt, "html": htmlFmt,
"html-esc": htmlEscFmt, "html-esc": htmlEscFmt,
"html-comment": htmlCommentFmt, "html-comment": htmlCommentFmt,
"urlquery-esc": urlQueryEscFmt,
"url-pkg": urlFmt, "url-pkg": urlFmt,
"url-src": urlFmt, "url-src": urlFmt,
"url-pos": urlFmt, "url-pos": urlFmt,
...@@ -771,6 +789,7 @@ var fmap = template.FormatterMap{ ...@@ -771,6 +789,7 @@ var fmap = template.FormatterMap{
"time": timeFmt, "time": timeFmt,
"dir/": dirslashFmt, "dir/": dirslashFmt,
"localname": localnameFmt, "localname": localnameFmt,
"numlines": numlinesFmt,
"linelist": linelistFmt, "linelist": linelistFmt,
} }
...@@ -998,6 +1017,35 @@ func isTextFile(path string) bool { ...@@ -998,6 +1017,35 @@ func isTextFile(path string) bool {
} }
// HTMLSubst replaces all occurences of f in s with r and HTML-escapes
// everything else in s (but not r). The result is written to w.
//
func HTMLSubst(w io.Writer, s, f, r []byte) {
for {
i := bytes.Index(s, f)
if i < 0 {
break
}
template.HTMLEscape(w, s[0:i])
w.Write(r)
s = s[i+len(f):]
}
template.HTMLEscape(w, s)
}
// highlight highlights all occurrences of h in s and writes the
// HTML-escaped result to w.
//
func highlight(w io.Writer, s, h []byte) {
var r bytes.Buffer
r.WriteString(`<span class="highlight">`)
template.HTMLEscape(&r, h)
r.WriteString(`</span>`)
HTMLSubst(w, s, h, r.Bytes())
}
func serveTextFile(w http.ResponseWriter, r *http.Request, abspath, relpath string) { func serveTextFile(w http.ResponseWriter, r *http.Request, abspath, relpath string) {
src, err := ioutil.ReadFile(abspath) src, err := ioutil.ReadFile(abspath)
if err != nil { if err != nil {
...@@ -1007,9 +1055,14 @@ func serveTextFile(w http.ResponseWriter, r *http.Request, abspath, relpath stri ...@@ -1007,9 +1055,14 @@ func serveTextFile(w http.ResponseWriter, r *http.Request, abspath, relpath stri
} }
var buf bytes.Buffer var buf bytes.Buffer
fmt.Fprintln(&buf, "<pre>") buf.WriteString("<pre>\n")
template.HTMLEscape(&buf, src) g := r.FormValue("g")
fmt.Fprintln(&buf, "</pre>") if g != "" {
highlight(&buf, src, []byte(g))
} else {
template.HTMLEscape(&buf, src)
}
buf.WriteString("</pre>\n")
servePage(w, "Text file "+relpath, "", "", buf.Bytes()) servePage(w, "Text file "+relpath, "", "", buf.Bytes())
} }
...@@ -1066,6 +1119,10 @@ func serveFile(w http.ResponseWriter, r *http.Request) { ...@@ -1066,6 +1119,10 @@ func serveFile(w http.ResponseWriter, r *http.Request) {
return return
case ".go": case ".go":
if r.FormValue("g") != "" {
serveTextFile(w, r, abspath, relpath)
return
}
serveGoSource(w, r, abspath, relpath) serveGoSource(w, r, abspath, relpath)
return return
} }
...@@ -1332,7 +1389,7 @@ type SearchResult struct { ...@@ -1332,7 +1389,7 @@ type SearchResult struct {
Query string Query string
Hit *LookupResult // identifier occurences of Query Hit *LookupResult // identifier occurences of Query
Alt *AltWords // alternative identifiers to look for Alt *AltWords // alternative identifiers to look for
Illegal bool // true if Query for identifier search has incorrect syntax Found int // number of textual occurences found
Textual []Positions // textual occurences of Query Textual []Positions // textual occurences of Query
Complete bool // true if all textual occurences of Query are reported Complete bool // true if all textual occurences of Query are reported
Accurate bool // true if the index is not older than the indexed files Accurate bool // true if the index is not older than the indexed files
...@@ -1343,10 +1400,10 @@ func lookup(query string) (result SearchResult) { ...@@ -1343,10 +1400,10 @@ func lookup(query string) (result SearchResult) {
result.Query = query result.Query = query
if index, timestamp := searchIndex.get(); index != nil { if index, timestamp := searchIndex.get(); index != nil {
index := index.(*Index) index := index.(*Index)
result.Hit, result.Alt, result.Illegal = index.Lookup(query) result.Hit, result.Alt, _ = index.Lookup(query)
// TODO(gri) should max be a flag? // TODO(gri) should max be a flag?
const max = 5000 // show at most this many fulltext results const max = 5000 // show at most this many fulltext results
result.Textual, result.Complete = index.LookupString(query, max) result.Found, result.Textual, result.Complete = index.LookupString(query, max)
_, ts := fsModified.get() _, ts := fsModified.get()
result.Accurate = timestamp >= ts result.Accurate = timestamp >= ts
} }
...@@ -1440,8 +1497,8 @@ func indexer() { ...@@ -1440,8 +1497,8 @@ func indexer() {
if *verbose { if *verbose {
secs := float64((stop-start)/1e6) / 1e3 secs := float64((stop-start)/1e6) / 1e3
stats := index.Stats() stats := index.Stats()
log.Printf("index updated (%gs, %d bytes of source, %d files, %d unique words, %d spots)", log.Printf("index updated (%gs, %d bytes of source, %d files, %d lines, %d unique words, %d spots)",
secs, stats.Bytes, stats.Files, stats.Words, stats.Spots) secs, stats.Bytes, stats.Files, stats.Lines, stats.Words, stats.Spots)
} }
log.Printf("before GC: bytes = %d footprint = %d\n", runtime.MemStats.HeapAlloc, runtime.MemStats.Sys) log.Printf("before GC: bytes = %d footprint = %d\n", runtime.MemStats.HeapAlloc, runtime.MemStats.Sys)
runtime.GC() runtime.GC()
......
...@@ -443,6 +443,7 @@ type IndexResult struct { ...@@ -443,6 +443,7 @@ type IndexResult struct {
type Statistics struct { type Statistics struct {
Bytes int // total size of indexed source files Bytes int // total size of indexed source files
Files int // number of indexed source files Files int // number of indexed source files
Lines int // number of lines (all files)
Words int // number of different identifiers Words int // number of different identifiers
Spots int // number of identifier occurences Spots int // number of identifier occurences
} }
...@@ -699,6 +700,7 @@ func (x *Indexer) visitFile(dirname string, f *os.FileInfo) { ...@@ -699,6 +700,7 @@ func (x *Indexer) visitFile(dirname string, f *os.FileInfo) {
// (count real file size as opposed to using the padded x.sources.Len()) // (count real file size as opposed to using the padded x.sources.Len())
x.stats.Bytes += x.current.Size() x.stats.Bytes += x.current.Size()
x.stats.Files++ x.stats.Files++
x.stats.Lines += x.current.LineCount()
} }
...@@ -891,13 +893,13 @@ type Positions struct { ...@@ -891,13 +893,13 @@ type Positions struct {
} }
// LookupString returns a list of positions where a string s is found // LookupString returns the number and list of positions where a string
// in the full text index and whether the result is complete or not. // s is found in the full text index and whether the result is complete
// At most n positions (filename and line) are returned. The result is // or not. At most n positions (filename and line) are returned (and thus
// not complete if the index is not present or there are more than n // found <= n). The result is incomplete if the index is not present or
// occurrences of s. // if there are more than n occurrences of s.
// //
func (x *Index) LookupString(s string, n int) (result []Positions, complete bool) { func (x *Index) LookupString(s string, n int) (found int, result []Positions, complete bool) {
if x.suffixes == nil { if x.suffixes == nil {
return return
} }
...@@ -908,6 +910,7 @@ func (x *Index) LookupString(s string, n int) (result []Positions, complete bool ...@@ -908,6 +910,7 @@ func (x *Index) LookupString(s string, n int) (result []Positions, complete bool
} else { } else {
offsets = offsets[0:n] offsets = offsets[0:n]
} }
found = len(offsets)
// compute file names and lines and sort the list by filename // compute file names and lines and sort the list by filename
list := make(positionList, len(offsets)) list := make(positionList, len(offsets))
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment