1 | // Copyright 2009 The Go Authors. All rights reserved. |
---|---|
2 | // Use of this source code is governed by a BSD-style |
3 | // license that can be found in the LICENSE file. |
4 | |
5 | package godoc |
6 | |
7 | import ( |
8 | "bytes" |
9 | "fmt" |
10 | "net/http" |
11 | "regexp" |
12 | "strings" |
13 | ) |
14 | |
15 | type SearchResult struct { |
16 | Query string |
17 | Alert string // error or warning message |
18 | |
19 | // identifier matches |
20 | Pak HitList // packages matching Query |
21 | Hit *LookupResult // identifier matches of Query |
22 | Alt *AltWords // alternative identifiers to look for |
23 | |
24 | // textual matches |
25 | Found int // number of textual occurrences found |
26 | Textual []FileLines // textual matches of Query |
27 | Complete bool // true if all textual occurrences of Query are reported |
28 | Idents map[SpotKind][]Ident |
29 | } |
30 | |
31 | func (c *Corpus) Lookup(query string) SearchResult { |
32 | result := &SearchResult{Query: query} |
33 | |
34 | index, timestamp := c.CurrentIndex() |
35 | if index != nil { |
36 | // identifier search |
37 | if r, err := index.Lookup(query); err == nil { |
38 | result = r |
39 | } else if err != nil && !c.IndexFullText { |
40 | // ignore the error if full text search is enabled |
41 | // since the query may be a valid regular expression |
42 | result.Alert = "Error in query string: " + err.Error() |
43 | return *result |
44 | } |
45 | |
46 | // full text search |
47 | if c.IndexFullText && query != "" { |
48 | rx, err := regexp.Compile(query) |
49 | if err != nil { |
50 | result.Alert = "Error in query regular expression: " + err.Error() |
51 | return *result |
52 | } |
53 | // If we get maxResults+1 results we know that there are more than |
54 | // maxResults results and thus the result may be incomplete (to be |
55 | // precise, we should remove one result from the result set, but |
56 | // nobody is going to count the results on the result page). |
57 | result.Found, result.Textual = index.LookupRegexp(rx, c.MaxResults+1) |
58 | result.Complete = result.Found <= c.MaxResults |
59 | if !result.Complete { |
60 | result.Found-- // since we looked for maxResults+1 |
61 | } |
62 | } |
63 | } |
64 | |
65 | // is the result accurate? |
66 | if c.IndexEnabled { |
67 | if ts := c.FSModifiedTime(); timestamp.Before(ts) { |
68 | // The index is older than the latest file system change under godoc's observation. |
69 | result.Alert = "Indexing in progress: result may be inaccurate" |
70 | } |
71 | } else { |
72 | result.Alert = "Search index disabled: no results available" |
73 | } |
74 | |
75 | return *result |
76 | } |
77 | |
78 | // SearchResultDoc optionally specifies a function returning an HTML body |
79 | // displaying search results matching godoc documentation. |
80 | func (p *Presentation) SearchResultDoc(result SearchResult) []byte { |
81 | return applyTemplate(p.SearchDocHTML, "searchDocHTML", result) |
82 | } |
83 | |
84 | // SearchResultCode optionally specifies a function returning an HTML body |
85 | // displaying search results matching source code. |
86 | func (p *Presentation) SearchResultCode(result SearchResult) []byte { |
87 | return applyTemplate(p.SearchCodeHTML, "searchCodeHTML", result) |
88 | } |
89 | |
90 | // SearchResultTxt optionally specifies a function returning an HTML body |
91 | // displaying search results of textual matches. |
92 | func (p *Presentation) SearchResultTxt(result SearchResult) []byte { |
93 | return applyTemplate(p.SearchTxtHTML, "searchTxtHTML", result) |
94 | } |
95 | |
96 | // HandleSearch obtains results for the requested search and returns a page |
97 | // to display them. |
98 | func (p *Presentation) HandleSearch(w http.ResponseWriter, r *http.Request) { |
99 | query := strings.TrimSpace(r.FormValue("q")) |
100 | result := p.Corpus.Lookup(query) |
101 | |
102 | var contents bytes.Buffer |
103 | for _, f := range p.SearchResults { |
104 | contents.Write(f(p, result)) |
105 | } |
106 | |
107 | var title string |
108 | if haveResults := contents.Len() > 0; haveResults { |
109 | title = fmt.Sprintf(`Results for query: %v`, query) |
110 | if !p.Corpus.IndexEnabled { |
111 | result.Alert = "" |
112 | } |
113 | } else { |
114 | title = fmt.Sprintf(`No results found for query %q`, query) |
115 | } |
116 | |
117 | body := bytes.NewBuffer(applyTemplate(p.SearchHTML, "searchHTML", result)) |
118 | body.Write(contents.Bytes()) |
119 | |
120 | p.ServePage(w, Page{ |
121 | Title: title, |
122 | Tabtitle: query, |
123 | Query: query, |
124 | Body: body.Bytes(), |
125 | }) |
126 | } |
127 | |
128 | func (p *Presentation) serveSearchDesc(w http.ResponseWriter, r *http.Request) { |
129 | w.Header().Set("Content-Type", "application/opensearchdescription+xml") |
130 | data := map[string]interface{}{ |
131 | "BaseURL": fmt.Sprintf("http://%s", r.Host), |
132 | } |
133 | applyTemplateToResponseWriter(w, p.SearchDescXML, &data) |
134 | } |
135 | |
136 | // tocColCount returns the no. of columns |
137 | // to split the toc table to. |
138 | func tocColCount(result SearchResult) int { |
139 | tocLen := tocLen(result) |
140 | colCount := 0 |
141 | // Simple heuristic based on visual aesthetic in manual testing. |
142 | switch { |
143 | case tocLen <= 10: |
144 | colCount = 1 |
145 | case tocLen <= 20: |
146 | colCount = 2 |
147 | case tocLen <= 80: |
148 | colCount = 3 |
149 | default: |
150 | colCount = 4 |
151 | } |
152 | return colCount |
153 | } |
154 | |
155 | // tocLen calculates the no. of items in the toc table |
156 | // by going through various fields in the SearchResult |
157 | // that is rendered in the UI. |
158 | func tocLen(result SearchResult) int { |
159 | tocLen := 0 |
160 | for _, val := range result.Idents { |
161 | if len(val) != 0 { |
162 | tocLen++ |
163 | } |
164 | } |
165 | // If no identifiers, then just one item for the header text "Package <result.Query>". |
166 | // See searchcode.html for further details. |
167 | if len(result.Idents) == 0 { |
168 | tocLen++ |
169 | } |
170 | if result.Hit != nil { |
171 | if len(result.Hit.Decls) > 0 { |
172 | tocLen += len(result.Hit.Decls) |
173 | // We need one extra item for the header text "Package-level declarations". |
174 | tocLen++ |
175 | } |
176 | if len(result.Hit.Others) > 0 { |
177 | tocLen += len(result.Hit.Others) |
178 | // We need one extra item for the header text "Local declarations and uses". |
179 | tocLen++ |
180 | } |
181 | } |
182 | // For "textual occurrences". |
183 | tocLen++ |
184 | return tocLen |
185 | } |
186 |
Members