internal/frontend: support search mode prefixes
When a search query contains a "package: " or "identifier: " prefix,
always default to that search mode, regardless of the `m=<mode>` query
param that is set.
For golang/go#47320
For golang/go#44142
Change-Id: I412772fe264e25a3cb79362eaa4ba992fe273beb
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/341176
Trust: Julie Qiu <julie@golang.org>
Run-TryBot: Julie Qiu <julie@golang.org>
Reviewed-by: Jamal Carvalho <jamal@golang.org>
diff --git a/internal/frontend/paginate.go b/internal/frontend/paginate.go
index 675638b..5114597 100644
--- a/internal/frontend/paginate.go
+++ b/internal/frontend/paginate.go
@@ -43,7 +43,7 @@
// URL constructs a URL that adds limit and mode query parameters to the base
// URL. Passing a zero value omits the parameter.
-func (p pagination) URL(limit int, mode string) string {
+func (p pagination) URL(limit int, mode, q string) string {
newQuery := p.baseURL.Query()
if limit != 0 {
newQuery.Set("limit", strconv.Itoa(limit))
@@ -51,6 +51,9 @@
if mode != "" {
newQuery.Set("m", mode)
}
+ if q != "" {
+ newQuery.Set("q", q)
+ }
p.baseURL.RawQuery = newQuery.Encode()
return p.baseURL.String()
}
diff --git a/internal/frontend/search.go b/internal/frontend/search.go
index 275cf04..615b76a 100644
--- a/internal/frontend/search.go
+++ b/internal/frontend/search.go
@@ -250,6 +250,14 @@
// maxSearchPageSize is the maximum allowed limit for search results.
maxSearchPageSize = 100
+
+ // searchModePackage is the keyword prefix and query param for searching
+ // by packages.
+ searchModePackage = "packages"
+
+ // searchModeSymbol is the keyword prefix and query param for searching
+ // by symbols.
+ searchModeSymbol = "identifiers"
)
// serveSearch applies database data to the search template. Handles endpoint
@@ -266,7 +274,7 @@
}
ctx := r.Context()
- query := searchQuery(r)
+ query, searchSymbols := searchQuery(r)
if !utf8.ValidString(query) {
return &serverError{status: http.StatusBadRequest}
}
@@ -308,14 +316,13 @@
return nil
}
- searchSymbols := shouldSearchSymbols(r)
page, err := fetchSearchPage(ctx, db, query, pageParams, searchSymbols)
if err != nil {
return fmt.Errorf("fetchSearchPage(ctx, db, %q): %v", query, err)
}
page.basePage = s.newBasePage(r, fmt.Sprintf("%s - Search Results", query))
if searchSymbols {
- page.SearchMode = "identifiers"
+ page.SearchMode = searchModeSymbol
}
tmpl := "legacy_search"
@@ -351,14 +358,22 @@
return fmt.Sprintf("/%s", requestedPath)
}
-// searchQuery extracts a search query from the request.
-func searchQuery(r *http.Request) string {
- return strings.TrimSpace(r.FormValue("q"))
-}
+// searchQuery extracts a search query from the request. It also reports
+// whether the search performed should be in symbolSearch mode.
+// See TestSearchQuery for examples.
+func searchQuery(r *http.Request) (q string, searchSymbols bool) {
+ q = strings.TrimSpace(r.FormValue("q"))
+ if !experiment.IsActive(r.Context(), internal.ExperimentSymbolSearch) {
+ return q, false
+ }
-// shouldSearchSymbols reports whether the search mode is to search for symbols.
-func shouldSearchSymbols(r *http.Request) bool {
- return strings.TrimSpace(r.FormValue("m")) == "identifiers"
+ if prefix := searchModeSymbol + ":"; strings.HasPrefix(q, prefix) {
+ return strings.TrimPrefix(q, prefix), true
+ }
+ if prefix := searchModePackage + ":"; strings.HasPrefix(q, prefix) {
+ return strings.TrimPrefix(q, prefix), false
+ }
+ return q, strings.TrimSpace(r.FormValue("m")) == searchModeSymbol
}
// elapsedTime takes a date and returns returns human-readable,
diff --git a/internal/frontend/search_test.go b/internal/frontend/search_test.go
index afdaaa3..50aac8e 100644
--- a/internal/frontend/search_test.go
+++ b/internal/frontend/search_test.go
@@ -6,17 +6,81 @@
import (
"context"
+ "fmt"
+ "net/http/httptest"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"golang.org/x/pkgsite/internal"
+ "golang.org/x/pkgsite/internal/experiment"
"golang.org/x/pkgsite/internal/licenses"
"golang.org/x/pkgsite/internal/postgres"
"golang.org/x/pkgsite/internal/testing/sample"
)
+func TestSearchQuery(t *testing.T) {
+ for _, test := range []struct {
+ name, m, q, wantQuery string
+ wantSearchSymbols bool
+ }{
+ {
+ name: "package: prefix in symbol mode",
+ m: searchModeSymbol,
+ q: fmt.Sprintf("%s:foo", searchModePackage),
+ wantQuery: "foo",
+ wantSearchSymbols: false,
+ },
+ {
+ name: "package: prefix in package mode",
+ m: searchModePackage,
+ q: fmt.Sprintf("%s:foo", searchModePackage),
+ wantQuery: "foo",
+ wantSearchSymbols: false,
+ },
+ {
+ name: "identifier: prefix in symbol mode",
+ m: searchModeSymbol,
+ q: fmt.Sprintf("%s:foo", searchModeSymbol),
+ wantQuery: "foo",
+ wantSearchSymbols: true,
+ },
+ {
+ name: "identifier: prefix in package mode",
+ m: searchModeSymbol,
+ q: fmt.Sprintf("%s:foo", searchModeSymbol),
+ wantQuery: "foo",
+ wantSearchSymbols: true,
+ },
+ {
+ name: "search in package mode",
+ m: searchModePackage,
+ q: "foo",
+ wantQuery: "foo",
+ wantSearchSymbols: false,
+ },
+ {
+ name: "search in symbol mode",
+ m: searchModeSymbol,
+ q: "foo",
+ wantQuery: "foo",
+ wantSearchSymbols: true,
+ },
+ } {
+ t.Run(test.name, func(t *testing.T) {
+ u := fmt.Sprintf("/search?q=%s&m=%s", test.q, test.m)
+ r := httptest.NewRequest("GET", u, nil)
+ r = r.WithContext(experiment.NewContext(r.Context(), internal.ExperimentSymbolSearch))
+ gotQuery, gotSearchSymbols := searchQuery(r)
+ if gotQuery != test.wantQuery || gotSearchSymbols != test.wantSearchSymbols {
+ t.Errorf("searchQuery(%q) = %q, %t; want = %q, %t", u, gotQuery, gotSearchSymbols,
+ test.wantQuery, test.wantSearchSymbols)
+ }
+ })
+ }
+}
+
func TestFetchSearchPage(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
defer cancel()
diff --git a/internal/frontend/server.go b/internal/frontend/server.go
index 583c934..7e9a43c 100644
--- a/internal/frontend/server.go
+++ b/internal/frontend/server.go
@@ -293,9 +293,10 @@
// newBasePage returns a base page for the given request and title.
func (s *Server) newBasePage(r *http.Request, title string) basePage {
+ q, _ := searchQuery(r)
return basePage{
HTMLTitle: title,
- Query: searchQuery(r),
+ Query: q,
Experiments: experiment.FromContext(r.Context()),
DevMode: s.devMode,
AppVersionLabel: s.appVersionLabel,
diff --git a/static/frontend/search/search.tmpl b/static/frontend/search/search.tmpl
index a7d130d..4322d0c 100644
--- a/static/frontend/search/search.tmpl
+++ b/static/frontend/search/search.tmpl
@@ -29,7 +29,7 @@
{{define "search_identifier"}}
<h1>Identifiers matching “{{.Query}}”</h1>
- <a class="SearchResults-instead" href="{{.Pagination.URL .Pagination.Limit "packages"}}">
+ <a class="SearchResults-instead" href="{{.Pagination.URL .Pagination.Limit "packages" .Query}}">
<span>Search instead for </span>
“package: {{.Query}}”
</a>
@@ -82,7 +82,7 @@
{{define "search_package"}}
<h1>Search results for “{{.Query}}”</h1>
- <a class="SearchResults-instead" href="{{.Pagination.URL .Pagination.Limit "identifiers"}}">
+ <a class="SearchResults-instead" href="{{.Pagination.URL .Pagination.Limit "identifiers" .Query}}">
<span class="SearchSnippet-symbolPackagePath">Search instead for </span>
“identifier: {{.Query}}”
</a>
@@ -191,7 +191,7 @@
Show
<select name="limit" class="go-Select js-selectNav">
{{range $p.Limits}}
- <option value="{{$p.URL . $.SearchMode}}"{{if eq . $p.Limit}} selected{{end}}>{{.}}</option>
+ <option value="{{$p.URL . $.SearchMode ""}}"{{if eq . $p.Limit}} selected{{end}}>{{.}}</option>
{{end}}
</select>
results