gopls/internal/golang: Web, an abstraction of server.web

This change consolidates the two func types, PosURL and PkgURL,
into an interface, Web, that aligns with the server.web
implementation.

Also, strength-reduce PkgURL to require only a viewID,
not a view (as we did for freesymbolsURL in CL 591157).

Change-Id: Ic48e0d5808257934c56b31126fd4880ee88c7a33
Reviewed-on: https://go-review.googlesource.com/c/tools/+/591318
Commit-Queue: Alan Donovan <adonovan@google.com>
Reviewed-by: Robert Findley <rfindley@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Auto-Submit: Alan Donovan <adonovan@google.com>
diff --git a/gopls/internal/golang/assembly.go b/gopls/internal/golang/assembly.go
index cbb9512..ca9f61c 100644
--- a/gopls/internal/golang/assembly.go
+++ b/gopls/internal/golang/assembly.go
@@ -30,7 +30,7 @@
 // TODO(adonovan):
 // - display a "Compiling..." message as a cold build can be slow.
 // - cross-link jumps and block labels, like github.com/aclements/objbrowse.
-func AssemblyHTML(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, symbol string, posURL PosURLFunc) ([]byte, error) {
+func AssemblyHTML(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, symbol string, web Web) ([]byte, error) {
 	// Compile the package with -S, and capture its stderr stream.
 	inv, cleanupInvocation, err := snapshot.GoCommandInvocation(false, &gocommand.Invocation{
 		Verb:       "build",
@@ -145,7 +145,7 @@
 			if file, linenum, ok := cutLast(parts[2], ":"); ok && !strings.HasPrefix(file, "<") {
 				if linenum, err := strconv.Atoi(linenum); err == nil {
 					text := fmt.Sprintf("L%04d", linenum)
-					link = sourceLink(text, posURL(file, linenum, 1))
+					link = sourceLink(text, web.OpenURL(file, linenum, 1))
 				}
 			}
 			fmt.Fprintf(&buf, "%s\t%s\t%s", escape(parts[1]), link, escape(parts[3]))
diff --git a/gopls/internal/golang/freesymbols.go b/gopls/internal/golang/freesymbols.go
index 4333a85..0bf0d9c 100644
--- a/gopls/internal/golang/freesymbols.go
+++ b/gopls/internal/golang/freesymbols.go
@@ -27,7 +27,7 @@
 
 // FreeSymbolsHTML returns an HTML document containing the report of
 // free symbols referenced by the selection.
-func FreeSymbolsHTML(pkg *cache.Package, pgf *parsego.File, start, end token.Pos, posURL PosURLFunc, pkgURL PkgURLFunc) []byte {
+func FreeSymbolsHTML(viewID string, pkg *cache.Package, pgf *parsego.File, start, end token.Pos, web Web) []byte {
 
 	// Compute free references.
 	refs := freeRefs(pkg.Types(), pkg.TypesInfo(), pgf.File, start, end)
@@ -210,7 +210,7 @@
 	fmt.Fprintf(&buf, "<ul>\n")
 	for _, imp := range model.Imported {
 		fmt.Fprintf(&buf, "<li>import \"<a href='%s'>%s</a>\" // for %s</li>\n",
-			pkgURL(imp.Path, ""),
+			web.PkgURL(viewID, imp.Path, ""),
 			html.EscapeString(string(imp.Path)),
 			strings.Join(imp.Symbols, ", "))
 	}
@@ -236,7 +236,7 @@
 	objHTML := func(obj types.Object) string {
 		text := obj.Name()
 		if posn := safetoken.StartPosition(pkg.FileSet(), obj.Pos()); posn.IsValid() {
-			return sourceLink(text, posURL(posn.Filename, posn.Line, posn.Column))
+			return sourceLink(text, web.OpenURL(posn.Filename, posn.Line, posn.Column))
 		}
 		return text
 	}
diff --git a/gopls/internal/golang/pkgdoc.go b/gopls/internal/golang/pkgdoc.go
index c605d29..94e00be 100644
--- a/gopls/internal/golang/pkgdoc.go
+++ b/gopls/internal/golang/pkgdoc.go
@@ -53,24 +53,21 @@
 	"golang.org/x/tools/internal/typesinternal"
 )
 
-// TODO(adonovan): factor these two functions into an interface.
-type (
-	// A PkgURLFunc forms URLs of package or symbol documentation.
-	PkgURLFunc = func(path PackagePath, fragment string) protocol.URI
+// Web is an abstraction of gopls' web server.
+type Web interface {
+	// PkgURL forms URLs of package or symbol documentation.
+	PkgURL(viewID string, path PackagePath, fragment string) protocol.URI
 
-	// A PosURLFunc forms URLs that cause the editor to navigate to a position.
-	PosURLFunc = func(filename string, line, col8 int) protocol.URI
-)
+	// OpenURL forms URLs that cause the editor to open a file at a specific position.
+	OpenURL(filename string, line, col8 int) protocol.URI
+}
 
 // PackageDocHTML formats the package documentation page.
 //
 // The posURL function returns a URL that when visited, has the side
 // effect of causing gopls to direct the client editor to navigate to
 // the specified file/line/column position, in UTF-8 coordinates.
-//
-// The pkgURL function returns a URL for the documentation of the
-// specified package and symbol.
-func PackageDocHTML(pkg *cache.Package, posURL PosURLFunc, pkgURL PkgURLFunc) ([]byte, error) {
+func PackageDocHTML(viewID string, pkg *cache.Package, web Web) ([]byte, error) {
 	// We can't use doc.NewFromFiles (even with doc.PreserveAST
 	// mode) as it calls ast.NewPackage which assumes that each
 	// ast.File has an ast.Scope and resolves identifiers to
@@ -143,7 +140,7 @@
 			if link.Recv != "" {
 				fragment = link.Recv + "." + link.Name
 			}
-			return pkgURL(path, fragment)
+			return web.PkgURL(viewID, path, fragment)
 		}
 		parser := docpkg.Parser()
 		parser.LookupPackage = func(name string) (importPath string, ok bool) {
@@ -334,7 +331,7 @@
 	objHTML := func(obj types.Object) string {
 		text := obj.Name()
 		if posn := safetoken.StartPosition(pkg.FileSet(), obj.Pos()); posn.IsValid() {
-			return sourceLink(text, posURL(posn.Filename, posn.Line, posn.Column))
+			return sourceLink(text, web.OpenURL(posn.Filename, posn.Line, posn.Column))
 		}
 		return text
 	}
@@ -350,7 +347,7 @@
 				// imported package name?
 				if pkgname, ok := obj.(*types.PkgName); ok {
 					// TODO(adonovan): do this for Defs of PkgName too.
-					return pkgURL(PackagePath(pkgname.Imported().Path()), "")
+					return web.PkgURL(viewID, PackagePath(pkgname.Imported().Path()), "")
 				}
 
 				// package-level symbol?
@@ -358,7 +355,7 @@
 					if obj.Pkg() == pkg.Types() {
 						return "#" + obj.Name() // intra-package ref
 					} else {
-						return pkgURL(PackagePath(obj.Pkg().Path()), obj.Name())
+						return web.PkgURL(viewID, PackagePath(obj.Pkg().Path()), obj.Name())
 					}
 				}
 
@@ -369,7 +366,7 @@
 						_, named := typesinternal.ReceiverNamed(sig.Recv())
 						if named != nil {
 							fragment := named.Obj().Name() + "." + fn.Name()
-							return pkgURL(PackagePath(fn.Pkg().Path()), fragment)
+							return web.PkgURL(viewID, PackagePath(fn.Pkg().Path()), fragment)
 						}
 					}
 					return ""
@@ -671,7 +668,7 @@
 	fmt.Fprintf(&buf, "<h2 id='hdr-SourceFiles'>Source files</h2>\n")
 	for _, filename := range docpkg.Filenames {
 		fmt.Fprintf(&buf, "<div class='comment'>%s</div>\n",
-			sourceLink(filepath.Base(filename), posURL(filename, 1, 1)))
+			sourceLink(filepath.Base(filename), web.OpenURL(filename, 1, 1)))
 	}
 
 	fmt.Fprintf(&buf, "</main>\n")
diff --git a/gopls/internal/server/command.go b/gopls/internal/server/command.go
index 201a96d..75f5134 100644
--- a/gopls/internal/server/command.go
+++ b/gopls/internal/server/command.go
@@ -571,7 +571,7 @@
 		}
 
 		// Direct the client to open the /pkg page.
-		url := web.pkgURL(deps.snapshot.View(), pkgpath, fragment)
+		url := web.PkgURL(deps.snapshot.View().ID(), pkgpath, fragment)
 		openClientBrowser(ctx, c.s.client, url)
 
 		return nil
diff --git a/gopls/internal/server/server.go b/gopls/internal/server/server.go
index 58747bb..a414942 100644
--- a/gopls/internal/server/server.go
+++ b/gopls/internal/server/server.go
@@ -347,10 +347,7 @@
 			http.Error(w, err.Error(), http.StatusInternalServerError)
 			return
 		}
-		pkgURL := func(path golang.PackagePath, fragment string) protocol.URI {
-			return web.pkgURL(view, path, fragment)
-		}
-		content, err := golang.PackageDocHTML(pkgs[0], web.openURL, pkgURL)
+		content, err := golang.PackageDocHTML(view.ID(), pkgs[0], web)
 		if err != nil {
 			http.Error(w, err.Error(), http.StatusInternalServerError)
 			return
@@ -405,10 +402,7 @@
 		}
 
 		// Produce report.
-		pkgURL := func(path golang.PackagePath, fragment string) protocol.URI {
-			return web.pkgURL(view, path, fragment)
-		}
-		html := golang.FreeSymbolsHTML(pkg, pgf, start, end, web.openURL, pkgURL)
+		html := golang.FreeSymbolsHTML(view.ID(), pkg, pgf, start, end, web)
 		w.Write(html)
 	})
 
@@ -453,7 +447,7 @@
 		pkg := pkgs[0]
 
 		// Produce report.
-		html, err := golang.AssemblyHTML(ctx, snapshot, pkg, symbol, web.openURL)
+		html, err := golang.AssemblyHTML(ctx, snapshot, pkg, symbol, web)
 		if err != nil {
 			http.Error(w, err.Error(), http.StatusInternalServerError)
 			return
@@ -469,26 +463,26 @@
 //go:embed assets/*
 var assets embed.FS
 
-// openURL returns an /open URL that, when visited, causes the client
+// OpenURL returns an /open URL that, when visited, causes the client
 // editor to open the specified file/line/column (in 1-based UTF-8
 // coordinates).
 //
 // (Rendering may generate hundreds of positions across files of many
 // packages, so don't convert to LSP coordinates yet: wait until the
 // URL is opened.)
-func (w *web) openURL(filename string, line, col8 int) protocol.URI {
+func (w *web) OpenURL(filename string, line, col8 int) protocol.URI {
 	return w.url(
 		"open",
 		fmt.Sprintf("file=%s&line=%d&col=%d", url.QueryEscape(filename), line, col8),
 		"")
 }
 
-// pkgURL returns a /pkg URL for the documentation of the specified package.
+// PkgURL returns a /pkg URL for the documentation of the specified package.
 // The optional fragment must be of the form "Println" or "Buffer.WriteString".
-func (w *web) pkgURL(v *cache.View, path golang.PackagePath, fragment string) protocol.URI {
+func (w *web) PkgURL(viewID string, path golang.PackagePath, fragment string) protocol.URI {
 	return w.url(
 		"pkg/"+string(path),
-		"view="+url.QueryEscape(v.ID()),
+		"view="+url.QueryEscape(viewID),
 		fragment)
 }