Brad Fitzpatrick | 66f0d6e | 2013-07-18 13:51:17 +1000 | [diff] [blame] | 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 ( |
Brad Garcia | a28efa5 | 2014-01-06 09:51:01 -0500 | [diff] [blame] | 8 | "bytes" |
Brad Fitzpatrick | 66f0d6e | 2013-07-18 13:51:17 +1000 | [diff] [blame] | 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 |
Brad Garcia | f3faf8b | 2013-11-21 11:55:42 -0500 | [diff] [blame] | 28 | Idents map[SpotKind][]Ident |
Brad Fitzpatrick | 66f0d6e | 2013-07-18 13:51:17 +1000 | [diff] [blame] | 29 | } |
| 30 | |
| 31 | func (c *Corpus) Lookup(query string) SearchResult { |
Brad Garcia | f3faf8b | 2013-11-21 11:55:42 -0500 | [diff] [blame] | 32 | result := &SearchResult{Query: query} |
Brad Fitzpatrick | 66f0d6e | 2013-07-18 13:51:17 +1000 | [diff] [blame] | 33 | |
| 34 | index, timestamp := c.CurrentIndex() |
| 35 | if index != nil { |
| 36 | // identifier search |
Brad Garcia | 804b965 | 2014-02-20 11:26:05 -0500 | [diff] [blame] | 37 | if r, err := index.Lookup(query); err == nil { |
| 38 | result = r |
| 39 | } else if err != nil && !c.IndexFullText { |
Brad Fitzpatrick | 66f0d6e | 2013-07-18 13:51:17 +1000 | [diff] [blame] | 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() |
Brad Garcia | f3faf8b | 2013-11-21 11:55:42 -0500 | [diff] [blame] | 43 | return *result |
Brad Fitzpatrick | 66f0d6e | 2013-07-18 13:51:17 +1000 | [diff] [blame] | 44 | } |
| 45 | |
| 46 | // full text search |
Brad Garcia | f3faf8b | 2013-11-21 11:55:42 -0500 | [diff] [blame] | 47 | if c.IndexFullText && query != "" { |
Brad Fitzpatrick | 66f0d6e | 2013-07-18 13:51:17 +1000 | [diff] [blame] | 48 | rx, err := regexp.Compile(query) |
| 49 | if err != nil { |
| 50 | result.Alert = "Error in query regular expression: " + err.Error() |
Brad Garcia | f3faf8b | 2013-11-21 11:55:42 -0500 | [diff] [blame] | 51 | return *result |
Brad Fitzpatrick | 66f0d6e | 2013-07-18 13:51:17 +1000 | [diff] [blame] | 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 | |
Brad Garcia | f3faf8b | 2013-11-21 11:55:42 -0500 | [diff] [blame] | 75 | return *result |
Brad Fitzpatrick | 66f0d6e | 2013-07-18 13:51:17 +1000 | [diff] [blame] | 76 | } |
| 77 | |
Brad Garcia | a28efa5 | 2014-01-06 09:51:01 -0500 | [diff] [blame] | 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. |
Brad Fitzpatrick | 66f0d6e | 2013-07-18 13:51:17 +1000 | [diff] [blame] | 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 | |
Brad Fitzpatrick | e5fe289 | 2018-10-11 02:21:09 +0000 | [diff] [blame] | 102 | var contents bytes.Buffer |
Brad Garcia | a28efa5 | 2014-01-06 09:51:01 -0500 | [diff] [blame] | 103 | for _, f := range p.SearchResults { |
| 104 | contents.Write(f(p, result)) |
Brad Garcia | f3faf8b | 2013-11-21 11:55:42 -0500 | [diff] [blame] | 105 | } |
Brad Garcia | a28efa5 | 2014-01-06 09:51:01 -0500 | [diff] [blame] | 106 | |
Brad Fitzpatrick | 66f0d6e | 2013-07-18 13:51:17 +1000 | [diff] [blame] | 107 | var title string |
Brad Garcia | a28efa5 | 2014-01-06 09:51:01 -0500 | [diff] [blame] | 108 | if haveResults := contents.Len() > 0; haveResults { |
Andrew Gerrand | fe74a41 | 2016-02-05 11:04:38 +1100 | [diff] [blame] | 109 | title = fmt.Sprintf(`Results for query: %v`, query) |
Brad Garcia | a28efa5 | 2014-01-06 09:51:01 -0500 | [diff] [blame] | 110 | if !p.Corpus.IndexEnabled { |
| 111 | result.Alert = "" |
| 112 | } |
Brad Fitzpatrick | 66f0d6e | 2013-07-18 13:51:17 +1000 | [diff] [blame] | 113 | } else { |
| 114 | title = fmt.Sprintf(`No results found for query %q`, query) |
| 115 | } |
| 116 | |
Brad Garcia | a28efa5 | 2014-01-06 09:51:01 -0500 | [diff] [blame] | 117 | body := bytes.NewBuffer(applyTemplate(p.SearchHTML, "searchHTML", result)) |
| 118 | body.Write(contents.Bytes()) |
| 119 | |
Brad Fitzpatrick | 66f0d6e | 2013-07-18 13:51:17 +1000 | [diff] [blame] | 120 | p.ServePage(w, Page{ |
| 121 | Title: title, |
| 122 | Tabtitle: query, |
| 123 | Query: query, |
Brad Garcia | a28efa5 | 2014-01-06 09:51:01 -0500 | [diff] [blame] | 124 | Body: body.Bytes(), |
Brad Fitzpatrick | 66f0d6e | 2013-07-18 13:51:17 +1000 | [diff] [blame] | 125 | }) |
| 126 | } |
Brad Fitzpatrick | 80d3823 | 2013-07-19 14:02:03 +1000 | [diff] [blame] | 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 | } |
Brad Garcia | efd232e | 2014-01-29 10:53:45 -0500 | [diff] [blame] | 133 | applyTemplateToResponseWriter(w, p.SearchDescXML, &data) |
Brad Fitzpatrick | 80d3823 | 2013-07-19 14:02:03 +1000 | [diff] [blame] | 134 | } |
Agniva De Sarker | 3e7aa9e | 2018-08-11 13:33:17 +0530 | [diff] [blame] | 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 | } |