internal/search: fix stdlib grouping

Fix a bug that resulted in net/http/prof not being grouped with
net/http.

The bug arose from a mistake in extracting the top-level directory
from a stdlib package path. After coming across the proposal for
`strings.Cut` (https://golang.org/issue/46336), I was on the fence
about adding a copy to this repo and using it here. Ultimately I
decided that the job was simple enough to do without it.

I was wrong. So this CL adds `Cut` and uses it here. We can
incremently use it in other places where it simplifies things.

Change-Id: I804aea93f3850bba52d9e0edde8ea136746093f8
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/347555
Trust: Jonathan Amsterdam <jba@google.com>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
TryBot-Result: kokoro <noreply+kokoro@google.com>
Reviewed-by: Julie Qiu <julie@golang.org>
diff --git a/internal/postgres/search.go b/internal/postgres/search.go
index 359c113..6cbf43e 100644
--- a/internal/postgres/search.go
+++ b/internal/postgres/search.go
@@ -9,7 +9,6 @@
 	"database/sql"
 	"fmt"
 	"io"
-	"path"
 	"sort"
 	"strings"
 	"time"
@@ -591,11 +590,8 @@
 	// Packages in the standard library are grouped by their top-level
 	// directory, and we can consider them all part of the same major version.
 	if r.ModulePath == stdlib.ModulePath {
-		dir := r.PackagePath
-		if strings.ContainsRune(dir, '/') {
-			dir = path.Dir(dir)
-		}
-		return dir, 1
+		before, _, _ := internal.Cut(r.PackagePath, "/")
+		return before, 1
 	}
 	return internal.SeriesPathAndMajorVersion(r.ModulePath)
 }
diff --git a/internal/postgres/search_test.go b/internal/postgres/search_test.go
index 02a0cba..e942b14 100644
--- a/internal/postgres/search_test.go
+++ b/internal/postgres/search_test.go
@@ -1458,6 +1458,25 @@
 				}},
 			},
 		},
+		{
+			name: "stdlib 2",
+			in: []*SearchResult{
+				{PackagePath: "m1", ModulePath: "m1", Version: "v0.0.0", Score: 9},
+				{PackagePath: "net/http", ModulePath: stdlib.ModulePath, Score: 8},
+				{PackagePath: "encoding/json", ModulePath: stdlib.ModulePath, Score: 7},
+				{PackagePath: "encoding/gob", ModulePath: stdlib.ModulePath, Score: 6},
+				{PackagePath: "net/http/prof", ModulePath: stdlib.ModulePath, Score: 5},
+			},
+			want: []*SearchResult{
+				{PackagePath: "m1", ModulePath: "m1", Version: "v0.0.0", Score: 9},
+				{PackagePath: "net/http", ModulePath: stdlib.ModulePath, Score: 8, SameModule: []*SearchResult{
+					{PackagePath: "net/http/prof", ModulePath: stdlib.ModulePath, Score: 5},
+				}},
+				{PackagePath: "encoding/json", ModulePath: stdlib.ModulePath, Score: 7, SameModule: []*SearchResult{
+					{PackagePath: "encoding/gob", ModulePath: stdlib.ModulePath, Score: 6},
+				}},
+			},
+		},
 	} {
 		t.Run(test.name, func(t *testing.T) {
 			got := groupSearchResults(test.in)
diff --git a/internal/util.go b/internal/util.go
index a803139..eb7f9bc 100644
--- a/internal/util.go
+++ b/internal/util.go
@@ -37,3 +37,18 @@
 	}
 	return lines, nil
 }
+
+// Cut cuts s around the first instance of sep,
+// returning the text before and after sep.
+// The found result reports whether sep appears in s.
+// If sep does not appear in s, cut returns s, "", false.
+//
+// https://golang.org/issue/46336 is an accepted proposal to add this to the
+// standard library. It will presumably land in Go 1.18, so this can be removed
+// when pkgsite moves to that version.
+func Cut(s, sep string) (before, after string, found bool) {
+	if i := strings.Index(s, sep); i >= 0 {
+		return s[:i], s[i+len(sep):], true
+	}
+	return s, "", false
+}