]> Cypherpunks repositories - gostls13.git/commitdiff
code search for godoc:
authorRobert Griesemer <gri@golang.org>
Tue, 27 Oct 2009 17:34:31 +0000 (10:34 -0700)
committerRobert Griesemer <gri@golang.org>
Tue, 27 Oct 2009 17:34:31 +0000 (10:34 -0700)
- added goroutine to automatically index in the background
- added handler for search requests
- added search box to top-level godoc template
- added search.html template for the display of search results
- changes to spec.go because of name conflicts
- added extra styles to style.css (for shorter .html files)

R=rsc
http://go/go-review/1014011

doc/style.css
lib/godoc/godoc.html
lib/godoc/search.html [new file with mode: 0644]
src/cmd/godoc/Makefile
src/cmd/godoc/godoc.go
src/cmd/godoc/spec.go

index cd344df9cf5289839ec95a027132dfe76141cbc5..84c29c077b34f888113c6c5bf582173bd48bbbf4 100644 (file)
@@ -151,10 +151,54 @@ div#linkList li.navhead {
 /* ------------------------------------------------------------------------- */
 /* Styles used by go/printer Styler implementations. */
 
+a.noline {
+  text-decoration: none;
+}
+
 span.comment {
   color: #0000a0;
 }
 
 span.highlight {
-  background-color: #00ff00;
+  background-color: #81F781;
+}
+
+
+/* ------------------------------------------------------------------------- */
+/* Styles used by infoClassFmt */
+
+a.import {
+  text-decoration: none;
+  background-color: #D8D8D8;
+}
+
+a.const {
+  text-decoration: none;
+  background-color: #F5A9A9;
+}
+
+a.type {
+  text-decoration: none;
+  background-color: #F2F5A9;
+}
+
+a.var {
+  text-decoration: none;
+  background-color: #A9F5A9;
+}
+
+a.func {
+  text-decoration: none;
+  background-color: #A9D0F5;
+}
+
+a.method {
+  text-decoration: none;
+  background-color: #D0A9F5;
+}
+
+a.use {
+  text-decoration: none;
+  color: #FFFFFF;
+  background-color: #5858FA;
 }
index ddde999a1649b1b7957c6543d2d68c5a0bfdb6a9..b77a1301bb7e8f7c4efe854099a7a8480012fb14 100644 (file)
@@ -4,7 +4,7 @@
 <head>
 
   <meta http-equiv="content-type" content="text/html; charset=utf-8">
-  <title>{title}</title>
+  <title>{Title}</title>
 
   <link rel="stylesheet" type="text/css" href="/doc/style.css">
   <script type="text/javascript" src="/doc/godocs.js"></script>
     <li><a href="/doc/go_lang_faq.html">Language Design FAQ</a></li>
     <li><a href="/doc/go_for_cpp_programmers.html">Go for C++ Programmers</a></li>
 
+    <li class="blank">&nbsp;</li>
+    <li class="navhead">How To</li>
+    <li><a href="/doc/install.html">Install Go</a></li>
+    <li><a href="/doc/contribute.html">Contribute code</a></li>
+
     <li class="blank">&nbsp;</li>
     <li class="navhead">Programming</li>
     <li><a href="/pkg">Package documentation</a></li>
 
     <li class="blank">&nbsp;</li>
-    <li class="navhead">How To</li>
-    <li><a href="/doc/install.html">Install Go</a></li>
-    <li><a href="/doc/contribute.html">Contribute code</a></li>
+    <li class="navhead">Go code search</li>
+    <form method="GET" action="/search" class="search">
+    <input name="q" value="{Query}" size="25" />
+    <input type="submit" value="Go" />
 
     <li class="blank">&nbsp;</li>
     <li class="navhead">Last update</li>
-       <li>{timestamp}</li>
+       <li>{Timestamp}</li>
   </ul>
 </div>
 
 <div id="content">
-  <h1>{title}</h1>
+  <h1>{Title}</h1>
 
   <!-- The Table of Contents is automatically inserted in this <div>.
        Do not delete this <div>. -->
   <div id="nav"></div>
 
-  {content}
+  {Content}
 </div>
 
 <div id="footer">
diff --git a/lib/godoc/search.html b/lib/godoc/search.html
new file mode 100644 (file)
index 0000000..419a9f8
--- /dev/null
@@ -0,0 +1,65 @@
+<!--
+       Copyright 2009 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.
+-->
+
+{.section Accurate}
+{.or}
+       <p>
+       <span class="alert" style="font-size:120%">Indexing in progress - result may be inaccurate</span>
+       </p>
+{.end}
+{.section Alt}
+       <p>
+       <span class="alert" style="font-size:120%">Did you mean: </span>
+       {.repeated section Alts}
+               <a href="search?q={@|html}" style="font-size:120%">{@|html}</a>
+       {.end}
+       </p>
+{.end}
+{.section Hit}
+       {.section Decls}
+               <h2>Package-level declarations</h2>
+               {.repeated section @}
+                       <h3>package {Pak.Name|html}</h3>
+                       {.repeated section Files}
+                               {.repeated section Infos}
+                                       <a href="{File.Path|html}?h={Query|html}#L{@|infoLine}">{File.Path|html}:{@|infoLine}</a>
+                                       <pre>{@|infoSnippet}</pre>
+                               {.end}
+                       {.end}
+               {.end}
+       {.end}
+       {.section Others}
+               <h2>Local declarations and uses</h2>
+               <p>
+               Legend:
+               {.repeated section Legend}
+                       <a class="{@}">{@}</a>
+               {.end}
+               </p>
+               {.repeated section @}
+                       <h3>package {Pak.Name|html}</h3>
+                       <table border="0" cellspacing="2">
+                       {.repeated section Files}
+                               <tr>
+                               <td valign="top">
+                                       <a href="{File.Path|html}?h={Query|html}" class="noline">{File.Path|html}:</a>
+                               </td>
+                               <td>
+                               {.repeated section Infos}
+                                       <a href="{File.Path|html}?h={Query|html}#L{@|infoLine}" class="{@|infoClass}">{@|infoLine}</a>
+                               {.end}
+                               </td>
+                               </tr>
+                       {.end}
+                       </table>
+               {.end}
+       {.end}
+{.or}
+       <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}
index 00463a5ea56e00389310ac2276ad56e5e117d4d4..cbaa5b3cda60b82e2ef8c716681d5e07a74e991c 100644 (file)
@@ -7,6 +7,8 @@ include $(GOROOT)/src/Make.$(GOARCH)
 TARG=godoc
 GOFILES=\
        godoc.go\
+       index.go\
+       snippet.go\
        spec.go\
 
 include $(GOROOT)/src/Make.cmd
index 64e2607a909182889d5c80662a8bbf3ce441af04..168c816f92a48ded1ac5712a21200ea45adf89a3 100644 (file)
 package main
 
 import (
-                       "bytes";
-                       "container/vector";
-                       "flag";
-                       "fmt";
-                       "go/ast";
-                       "go/doc";
-                       "go/parser";
-                       "go/printer";
-                       "go/scanner";
-                       "go/token";
-                       "http";
-                       "io";
-                       "log";
-                       "os";
-       pathutil        "path";
-                       "sort";
-                       "strings";
-                       "sync";
-                       "template";
-                       "time";
+       "bytes";
+       "container/vector";
+       "flag";
+       "fmt";
+       "go/ast";
+       "go/doc";
+       "go/parser";
+       "go/printer";
+       "go/scanner";
+       "go/token";
+       "http";
+       "io";
+       "log";
+       "os";
+       pathutil "path";
+       "sort";
+       "strings";
+       "sync";
+       "template";
+       "time";
 )
 
 
@@ -59,9 +59,9 @@ const Pkg = "/pkg/"   // name for auto-generated package documentation tree
 // An RWValue wraps a value and permits mutually exclusive
 // access to it and records the time the value was last set.
 type RWValue struct {
-       mutex   sync.RWMutex;
-       value   interface{};
-       timestamp       int64;  // time of last set(), in seconds since epoch
+       mutex           sync.RWMutex;
+       value           interface{};
+       timestamp       int64;  // time of last set(), in seconds since epoch
 }
 
 
@@ -138,7 +138,12 @@ func init() {
 func isGoFile(dir *os.Dir) bool {
        return dir.IsRegular() &&
                !strings.HasPrefix(dir.Name, ".") &&    // ignore .files
-               pathutil.Ext(dir.Name) == ".go" &&
+               pathutil.Ext(dir.Name) == ".go";
+}
+
+
+func isPkgFile(dir *os.Dir) bool {
+       return isGoFile(dir) &&
                !strings.HasSuffix(dir.Name, "_test.go");       // ignore test files
 }
 
@@ -231,7 +236,7 @@ func (s *Styler) LineTag(line int) (text []byte, tag printer.HtmlTag) {
 }
 
 
-func (s *Styler) Comment(c *ast.Comment, line []byte)  (text []byte, tag printer.HtmlTag) {
+func (s *Styler) Comment(c *ast.Comment, line []byte) (text []byte, tag printer.HtmlTag) {
        text = line;
        // minimal syntax-coloring of comments for now - people will want more
        // (don't do anything more until there's a button to turn it on/off)
@@ -240,13 +245,13 @@ func (s *Styler) Comment(c *ast.Comment, line []byte)  (text []byte, tag printer
 }
 
 
-func (s *Styler) BasicLit(x *ast.BasicLit)  (text []byte, tag printer.HtmlTag) {
+func (s *Styler) BasicLit(x *ast.BasicLit) (text []byte, tag printer.HtmlTag) {
        text = x.Value;
        return;
 }
 
 
-func (s *Styler) Ident(id *ast.Ident)  (text []byte, tag printer.HtmlTag) {
+func (s *Styler) Ident(id *ast.Ident) (text []byte, tag printer.HtmlTag) {
        text = strings.Bytes(id.Value);
        if s.highlight == id.Value {
                tag = printer.HtmlTag{"<span class=highlight>", "</span>"};
@@ -255,23 +260,22 @@ func (s *Styler) Ident(id *ast.Ident)  (text []byte, tag printer.HtmlTag) {
 }
 
 
-func (s *Styler) Token(tok token.Token)  (text []byte, tag printer.HtmlTag) {
+func (s *Styler) Token(tok token.Token) (text []byte, tag printer.HtmlTag) {
        text = strings.Bytes(tok.String());
        return;
 }
 
 
-
 // ----------------------------------------------------------------------------
 // Templates
 
 // Write an AST-node to w; optionally html-escaped.
-func writeNode(w io.Writer, node interface{}, html bool, style printer.Styler) {
+func writeNode(w io.Writer, node interface{}, html bool, styler printer.Styler) {
        mode := printer.UseSpaces;
        if html {
                mode |= printer.GenHTML;
        }
-       (&printer.Config{mode, *tabwidth, style}).Fprint(w, node);
+       (&printer.Config{mode, *tabwidth, styler}).Fprint(w, node);
 }
 
 
@@ -344,11 +348,55 @@ func linkFmt(w io.Writer, x interface{}, format string) {
 }
 
 
+var infoClasses = [nKinds]string{
+       "import",       // ImportDecl
+       "const",        // ConstDecl
+       "type", // TypeDecl
+       "var",  // VarDecl
+       "func", // FuncDecl
+       "method",       // MethodDecl
+       "use",  // Use
+}
+
+
+// Template formatter for "infoClass" format.
+func infoClassFmt(w io.Writer, x interface{}, format string) {
+       fmt.Fprintf(w, infoClasses[x.(SpotInfo).Kind()]);
+}
+
+
+// Template formatter for "infoLine" format.
+func infoLineFmt(w io.Writer, x interface{}, format string) {
+       info := x.(SpotInfo);
+       line := info.Lori();
+       if info.IsIndex() {
+               index, _ := searchIndex.get();
+               line = index.(*Index).Snippet(line).Line;
+       }
+       fmt.Fprintf(w, "%d", line);
+}
+
+
+// Template formatter for "infoSnippet" format.
+func infoSnippetFmt(w io.Writer, x interface{}, format string) {
+       info := x.(SpotInfo);
+       text := `<span class="alert">no snippet text available</span>`;
+       if info.IsIndex() {
+               index, _ := searchIndex.get();
+               text = index.(*Index).Snippet(info.Lori()).Text;
+       }
+       fmt.Fprintf(w, "%s", text);
+}
+
+
 var fmap = template.FormatterMap{
        "": textFmt,
        "html": htmlFmt,
        "html-comment": htmlCommentFmt,
        "link": linkFmt,
+       "infoClass": infoClassFmt,
+       "infoLine": infoLineFmt,
+       "infoSnippet": infoSnippetFmt,
 }
 
 
@@ -371,7 +419,8 @@ var (
        packageHtml,
        packageText,
        parseerrorHtml,
-       parseerrorText *template.Template;
+       parseerrorText,
+       searchHtml *template.Template;
 )
 
 func readTemplates() {
@@ -382,24 +431,27 @@ func readTemplates() {
        packageText = readTemplate("package.txt");
        parseerrorHtml = readTemplate("parseerror.html");
        parseerrorText = readTemplate("parseerror.txt");
+       searchHtml = readTemplate("search.html");
 }
 
 
 // ----------------------------------------------------------------------------
 // Generic HTML wrapper
 
-func servePage(c *http.Conn, title, content interface{}) {
+func servePage(c *http.Conn, title, query string, content []byte) {
        type Data struct {
-               title           interface{};
-               timestamp       string;
-               content         interface{};
+               Title           string;
+               Timestamp       string;
+               Query           string;
+               Content         []byte;
        }
 
        _, ts := syncTime.get();
        d := Data{
-               title: title,
-               timestamp: time.SecondsToLocalTime(ts).String(),
-               content: content,
+               Title: title,
+               Timestamp: time.SecondsToLocalTime(ts).String(),
+               Query: query,
+               Content: content,
        };
 
        if err := godocHtml.Execute(&d, c); err != nil {
@@ -451,7 +503,7 @@ func serveHtmlDoc(c *http.Conn, r *http.Request, filename string) {
        }
 
        title := commentText(src);
-       servePage(c, title, src);
+       servePage(c, title, "", src);
 }
 
 
@@ -461,11 +513,11 @@ func serveParseErrors(c *http.Conn, errors *parseErrors) {
        if err := parseerrorHtml.Execute(errors, &buf); err != nil {
                log.Stderrf("parseerrorHtml.Execute: %s", err);
        }
-       servePage(c, "Parse errors in source file " + errors.filename, buf.Bytes());
+       servePage(c, "Parse errors in source file " + errors.filename, "", buf.Bytes());
 }
 
 
-func serveGoSource(c *http.Conn, filename string, style printer.Styler) {
+func serveGoSource(c *http.Conn, filename string, styler printer.Styler) {
        path := pathutil.Join(goroot, filename);
        prog, errors := parse(path, parser.ParseComments);
        if errors != nil {
@@ -475,10 +527,10 @@ func serveGoSource(c *http.Conn, filename string, style printer.Styler) {
 
        var buf bytes.Buffer;
        fmt.Fprintln(&buf, "<pre>");
-       writeNode(&buf, prog, true, style);
+       writeNode(&buf, prog, true, styler);
        fmt.Fprintln(&buf, "</pre>");
 
-       servePage(c, "Source file " + filename, buf.Bytes());
+       servePage(c, "Source file " + filename, "", buf.Bytes());
 }
 
 
@@ -560,7 +612,7 @@ func getPageInfo(path string) PageInfo {
        var subdirlist vector.Vector;
        subdirlist.Init(0);
        filter := func(d *os.Dir) bool {
-               if isGoFile(d) {
+               if isPkgFile(d) {
                        // Some directories contain main packages: Only accept
                        // files that belong to the expected package so that
                        // parser.ParsePackage doesn't return "multiple packages
@@ -634,7 +686,48 @@ func servePkg(c *http.Conn, r *http.Request) {
                title = "Package " + info.PDoc.PackageName;
        }
 
-       servePage(c, title, buf.Bytes());
+       servePage(c, title, "", buf.Bytes());
+}
+
+
+// ----------------------------------------------------------------------------
+// Search
+
+var searchIndex RWValue
+
+type SearchResult struct {
+       Query           string;
+       Hit             *LookupResult;
+       Alt             *AltWords;
+       Accurate        bool;
+       Legend          []string;
+}
+
+func search(c *http.Conn, r *http.Request) {
+       query := r.FormValue("q");
+       var result SearchResult;
+
+       if index, timestamp := searchIndex.get(); index != nil {
+               result.Query = query;
+               result.Hit, result.Alt = index.(*Index).Lookup(query);
+               _, ts := syncTime.get();
+               result.Accurate = timestamp >= ts;
+               result.Legend = &infoClasses;
+       }
+
+       var buf bytes.Buffer;
+       if err := searchHtml.Execute(result, &buf); err != nil {
+               log.Stderrf("searchHtml.Execute: %s", err);
+       }
+
+       var title string;
+       if result.Hit != nil {
+               title = fmt.Sprintf(`Results for query %q`, query);
+       } else {
+               title = fmt.Sprintf(`No results found for query %q`, query);
+       }
+
+       servePage(c, title, query, buf.Bytes());
 }
 
 
@@ -754,6 +847,7 @@ func main() {
                if *syncCmd != "" {
                        http.Handle("/debug/sync", http.HandlerFunc(dosync));
                }
+               http.Handle("/search", http.HandlerFunc(search));
                http.Handle("/", http.HandlerFunc(serveFile));
 
                // The server may have been restarted; always wait 1sec to
@@ -776,6 +870,29 @@ func main() {
                        }();
                }
 
+               // Start indexing goroutine.
+               go func() {
+                       for {
+                               _, ts := syncTime.get();
+                               if _, timestamp := searchIndex.get(); timestamp < ts {
+                                       // index possibly out of date - make a new one
+                                       // (could use a channel to send an explicit signal
+                                       // from the sync goroutine, but this solution is
+                                       // more decoupled, trivial, and works well enough)
+                                       start := time.Nanoseconds();
+                                       index := NewIndex(".");
+                                       stop := time.Nanoseconds();
+                                       searchIndex.set(index);
+                                       if *verbose {
+                                               secs := float64((stop-start)/1e6)/1e3;
+                                               nwords, nspots := index.Size();
+                                               log.Stderrf("index updated (%gs, %d unique words, %d spots)", secs, nwords, nspots);
+                                       }
+                               }
+                               time.Sleep(1*60e9);     // try once a minute
+                       }
+               }();
+
                // Start http server.
                if err := http.ListenAndServe(*httpaddr, handler); err != nil {
                        log.Exitf("ListenAndServe %s: %v", *httpaddr, err);
index e942195174d2632aecf659b7baff074cb1657721..ee9ff4e0489eba9b773503034753f560dce43ab0 100644 (file)
@@ -49,7 +49,7 @@ func (p *ebnfParser) next() {
 
 
 func (p *ebnfParser) Error(pos token.Position, msg string) {
-       fmt.Fprintf(p.out, "<font color=red>error: %s</font>", msg);
+       fmt.Fprintf(p.out, `<span class="alert">error: %s</span>`, msg);
 }
 
 
@@ -83,7 +83,7 @@ func (p *ebnfParser) parseIdentifier(def bool) {
        if def {
                fmt.Fprintf(p.out, `<a id="%s">%s</a>`, name, name);
        } else {
-               fmt.Fprintf(p.out, `<a href="#%s" style="text-decoration: none;">%s</a>`, name, name);
+               fmt.Fprintf(p.out, `<a href="#%s" class="noline">%s</a>`, name, name);
        }
        p.prev += len(name);    // skip identifier when calling flush
 }
@@ -165,8 +165,8 @@ func (p *ebnfParser) parse(out io.Writer, src []byte) {
 
 // Markers around EBNF sections
 var (
-       open    = strings.Bytes(`<pre class="ebnf">`);
-       close   = strings.Bytes(`</pre>`);
+       openTag         = strings.Bytes(`<pre class="ebnf">`);
+       closeTag        = strings.Bytes(`</pre>`);
 )
 
 
@@ -175,14 +175,14 @@ func linkify(out io.Writer, src []byte) {
                n := len(src);
 
                // i: beginning of EBNF text (or end of source)
-               i := bytes.Index(src, open);
+               i := bytes.Index(src, openTag);
                if i < 0 {
-                       i = n-len(open);
+                       i = n-len(openTag);
                }
-               i += len(open);
+               i += len(openTag);
 
                // j: end of EBNF text (or end of source)
-               j := bytes.Index(src[i:n], close);      // close marker
+               j := bytes.Index(src[i:n], closeTag);   // close marker
                if j < 0 {
                        j = n-i;
                }