internal/frontend: default capitalized query to symbol search

If a query contains one word, and that word is capitalized, assume the
user is searching for a symbol and not a package.

For golang/go#44142

Change-Id: I220245ab5c941fcf1651a733a9fdc5aa423a6307
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/346114
Trust: Julie Qiu <julie@golang.org>
Run-TryBot: Julie Qiu <julie@golang.org>
TryBot-Result: kokoro <noreply+kokoro@google.com>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
diff --git a/internal/frontend/search.go b/internal/frontend/search.go
index f6902d0..d70ec24 100644
--- a/internal/frontend/search.go
+++ b/internal/frontend/search.go
@@ -14,9 +14,11 @@
 	"sort"
 	"strings"
 	"time"
+	"unicode"
 	"unicode/utf8"
 
 	"github.com/google/safehtml/template"
+	"golang.org/x/mod/semver"
 	"golang.org/x/pkgsite/internal"
 	"golang.org/x/pkgsite/internal/derrors"
 	"golang.org/x/pkgsite/internal/experiment"
@@ -407,18 +409,34 @@
 	return q, mode == searchModeSymbol
 }
 
-// shouldDefaultToSymbolSearch reports whether the search query is of the form
-// <package>.<symbol>. The <symbol> component should not be a top-level domain
-// that is in our database.
+// shouldDefaultToSymbolSearch reports whether the symbol search mode should
+// default to symbol search mode based on the input.
 func shouldDefaultToSymbolSearch(q string) bool {
-	if len(strings.Fields(q)) != 1 || !strings.ContainsAny(q, ".") {
+	if len(strings.Fields(q)) != 1 {
 		return false
 	}
 	if internal.IsGoPkgInPathElement(q) {
 		return false
 	}
 	parts := strings.Split(q, ".")
-	return !internal.TopLevelDomains[parts[len(parts)-1]]
+	if len(parts) > 1 {
+		if len(parts) == 2 && semver.IsValid(parts[1]) {
+			// The q has the format <text>.<semver> which is likely a
+			// gopkg.in host, such as yaml.v2. Default to package search.
+			return false
+		}
+		return !internal.TopLevelDomains[parts[len(parts)-1]]
+	}
+	// If a user searches for "Unmarshal", assume that they are searching for
+	// the symbol name "Unmarshal", not the package unmarshal.
+	return isCapitalized(q)
+}
+
+func isCapitalized(s string) bool {
+	if len(s) == 0 {
+		return false
+	}
+	return unicode.IsUpper(rune(s[0]))
 }
 
 // elapsedTime takes a date and returns returns human-readable,
diff --git a/internal/frontend/search_test.go b/internal/frontend/search_test.go
index 3e86bf2..415119b 100644
--- a/internal/frontend/search_test.go
+++ b/internal/frontend/search_test.go
@@ -411,6 +411,7 @@
 		{"sql.DB.Begin", true},
 		{"yaml.v2", false},
 		{"gopkg.in", false},
+		{"Unmarshal", true},
 	} {
 		t.Run(test.q, func(t *testing.T) {
 			got := shouldDefaultToSymbolSearch(test.q)
diff --git a/tests/search/scripts/default.txt b/tests/search/scripts/default.txt
index 92f7132..87b0af7 100644
--- a/tests/search/scripts/default.txt
+++ b/tests/search/scripts/default.txt
@@ -43,3 +43,11 @@
 keyboard shorcut "package:"
 [] package:Foo
 gopkg.in/foo.v1
+
+capital letter one word defaults to symbol mode
+[symbol] Float
+Float math/big
+
+non-TLD word.word defaults to symbol mode
+[symbol] big.Float
+Float math/big