gopls/internal/golang: factor the 3 web reports
This change factors the common elements of the
three reports:
- the common CSS and JS, previously constants,
are now assets; only the ad hoc styles
particular to each page remain in the HTML.
- The disconnect banner element is now created on load,
in common.js, so no <div> HTML is required.
- objHTML, sourceLink are factored out.
Also:
- use the same font-families as pkg.go.dev.
- use addEventListener instead of clobbering window.onload.
Change-Id: Ic21cc46fc8d92a94b78aa1faf5b2f3012f539e57
Reviewed-on: https://go-review.googlesource.com/c/tools/+/591355
Auto-Submit: 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>
diff --git a/gopls/internal/golang/assembly.go b/gopls/internal/golang/assembly.go
index ca9f61c..dc5b589 100644
--- a/gopls/internal/golang/assembly.go
+++ b/gopls/internal/golang/assembly.go
@@ -50,7 +50,6 @@
escape := html.EscapeString
// Produce the report.
- // TODO(adonovan): factor with RenderPkgDoc, FreeSymbolsHTML
title := fmt.Sprintf("%s assembly for %s",
escape(snapshot.View().GOARCH()),
escape(symbol))
@@ -59,31 +58,11 @@
<html>
<head>
<meta charset="UTF-8">
- <style>` + pkgDocStyle + `</style>
<title>` + escape(title) + `</title>
- <script type='text/javascript'>
-// httpGET requests a URL for its effects only.
-function httpGET(url) {
- var xhttp = new XMLHttpRequest();
- xhttp.open("GET", url, true);
- xhttp.send();
- return false; // disable usual <a href=...> behavior
-}
-
-// Start a GET /hang request. If it ever completes, the server
-// has disconnected. Show a banner in that case.
-{
- var x = new XMLHttpRequest();
- x.open("GET", "/hang", true);
- x.onloadend = () => {
- document.getElementById("disconnected").style.display = 'block';
- };
- x.send();
-};
- </script>
+ <link rel="stylesheet" href="/assets/common.css">
+ <script src="/assets/common.js"></script>
</head>
<body>
-<div id='disconnected'>Gopls server has terminated. Page is inactive.</div>
<h1>` + title + `</h1>
<p>
<a href='https://go.dev/doc/asm'>A Quick Guide to Go's Assembler</a>
@@ -101,18 +80,6 @@
<pre>
`)
- // sourceLink returns HTML for a link to open a file in the client editor.
- // TODO(adonovan): factor with two other copies.
- sourceLink := func(text, url string) string {
- // The /open URL returns nothing but has the side effect
- // of causing the LSP client to open the requested file.
- // So we use onclick to prevent the browser from navigating.
- // We keep the href attribute as it causes the <a> to render
- // as a link: blue, underlined, with URL hover information.
- return fmt.Sprintf(`<a href="%[1]s" onclick='return httpGET("%[1]s")'>%[2]s</a>`,
- escape(url), text)
- }
-
// insnRx matches an assembly instruction line.
// Submatch groups are: (offset-hex-dec, file-line-column, instruction).
insnRx := regexp.MustCompile(`^(\s+0x[0-9a-f ]+)\(([^)]*)\)\s+(.*)$`)
@@ -145,7 +112,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, web.OpenURL(file, linenum, 1))
+ link = sourceLink(text, web.SrcURL(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 0bf0d9c..6a71c20 100644
--- a/gopls/internal/golang/freesymbols.go
+++ b/gopls/internal/golang/freesymbols.go
@@ -161,40 +161,11 @@
.col-local { color: #0cb7c9 }
li { font-family: monospace; }
p { max-width: 6in; }
-#disconnected {
- position: fixed;
- top: 1em;
- left: 1em;
- display: none; /* initially */
- background-color: white;
- border: thick solid red;
- padding: 2em;
-}
</style>
-<!-- TODO(adonovan): factor with RenderPackageDoc -->
- <script type='text/javascript'>
-// httpGET requests a URL for its effects only.
-function httpGET(url) {
- var xhttp = new XMLHttpRequest();
- xhttp.open("GET", url, true);
- xhttp.send();
- return false; // disable usual <a href=...> behavior
-}
-
-// Start a GET /hang request. If it ever completes, the server
-// has disconnected. Show a banner in that case.
-{
- var x = new XMLHttpRequest();
- x.open("GET", "/hang", true);
- x.onloadend = () => {
- document.getElementById("disconnected").style.display = 'block';
- };
- x.send();
-};
- </script>
+ <script src="/assets/common.js"></script>
+ <link rel="stylesheet" href="/assets/common.css">
</head>
<body>
-<div id='disconnected'>Gopls server has terminated. Page is inactive.</div>
<h1>Free symbols</h1>
<p>
The selected code contains references to these free* symbols:
@@ -219,28 +190,6 @@
}
buf.WriteString("</ul>\n")
- // sourceLink returns HTML for a link to open a file in the client editor.
- // TODO(adonovan): factor with RenderPackageDoc.
- sourceLink := func(text, url string) string {
- // The /open URL returns nothing but has the side effect
- // of causing the LSP client to open the requested file.
- // So we use onclick to prevent the browser from navigating.
- // We keep the href attribute as it causes the <a> to render
- // as a link: blue, underlined, with URL hover information.
- return fmt.Sprintf(`<a href="%[1]s" onclick='return httpGET("%[1]s")'>%[2]s</a>`,
- html.EscapeString(url), text)
- }
-
- // objHTML returns HTML for obj.Name(), possibly as a link.
- // TODO(adonovan): factor with RenderPackageDoc.
- objHTML := func(obj types.Object) string {
- text := obj.Name()
- if posn := safetoken.StartPosition(pkg.FileSet(), obj.Pos()); posn.IsValid() {
- return sourceLink(text, web.OpenURL(posn.Filename, posn.Line, posn.Column))
- }
- return text
- }
-
// -- package and local symbols --
showSymbols := func(scope, title string, symbols []Symbol) {
@@ -253,7 +202,7 @@
if i > 0 {
buf.WriteByte('.')
}
- buf.WriteString(objHTML(obj))
+ buf.WriteString(objHTML(pkg.FileSet(), web, obj))
}
fmt.Fprintf(&buf, " %s</li>\n", html.EscapeString(sym.Type))
}
@@ -452,3 +401,26 @@
ast.Inspect(path[0], visit)
return free
}
+
+// objHTML returns HTML for obj.Name(), possibly marked up as a link
+// to the web server that, when visited, opens the declaration in the
+// client editor.
+func objHTML(fset *token.FileSet, web Web, obj types.Object) string {
+ text := obj.Name()
+ if posn := safetoken.StartPosition(fset, obj.Pos()); posn.IsValid() {
+ url := web.SrcURL(posn.Filename, posn.Line, posn.Column)
+ return sourceLink(text, url)
+ }
+ return text
+}
+
+// sourceLink returns HTML for a link to open a file in the client editor.
+func sourceLink(text, url string) string {
+ // The /src URL returns nothing but has the side effect
+ // of causing the LSP client to open the requested file.
+ // So we use onclick to prevent the browser from navigating.
+ // We keep the href attribute as it causes the <a> to render
+ // as a link: blue, underlined, with URL hover information.
+ return fmt.Sprintf(`<a href="%[1]s" onclick='return httpGET("%[1]s")'>%[2]s</a>`,
+ html.EscapeString(url), text)
+}
diff --git a/gopls/internal/golang/pkgdoc.go b/gopls/internal/golang/pkgdoc.go
index 94e00be..93761d6 100644
--- a/gopls/internal/golang/pkgdoc.go
+++ b/gopls/internal/golang/pkgdoc.go
@@ -17,7 +17,6 @@
// - list promoted methods---we have type information!
// - gather Example tests, following go/doc and pkgsite.
// - add option for doc.AllDecls: show non-exported symbols too.
-// - abbreviate long signatures by replacing parameters 4 onwards with "...".
// - style the <li> bullets in the index as invisible.
// - add push notifications such as didChange -> reload.
// - there appears to be a maximum file size beyond which the
@@ -25,9 +24,9 @@
// - modify JS httpGET function to give a transient visual indication
// when clicking a source link that the editor is being navigated
// (in case it doesn't raise itself, like VS Code).
-// - move this into a new package, golang/pkgdoc, and then
+// - move this into a new package, golang/web, and then
// split out the various helpers without fear of polluting
-// the golang package namespace.
+// the golang package namespace?
// - show "Deprecated" chip when appropriate.
import (
@@ -58,8 +57,8 @@
// PkgURL forms URLs of package or symbol documentation.
PkgURL(viewID string, path PackagePath, fragment string) 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
+ // SrcURL forms URLs that cause the editor to open a file at a specific position.
+ SrcURL(filename string, line, col8 int) protocol.URI
}
// PackageDocHTML formats the package documentation page.
@@ -199,39 +198,40 @@
<html>
<head>
<meta charset="UTF-8">
- <style>` + pkgDocStyle + `</style>
<title>` + title + `</title>
- <script type='text/javascript'>
-// httpGET requests a URL for its effects only.
-function httpGET(url) {
- var xhttp = new XMLHttpRequest();
- xhttp.open("GET", url, true);
- xhttp.send();
- return false; // disable usual <a href=...> behavior
+ <link rel="stylesheet" href="/assets/common.css">
+ <script src="/assets/common.js"></script>
+ <style>
+.lit { color: darkgreen; }
+
+header {
+ position: sticky;
+ top: 0;
+ left: 0;
+ width: 100%;
+ padding: 0.3em;
}
-window.onload = () => {
+#pkgsite { height: 1.5em; }
+
+#hdr-Selector {
+ margin-right: 0.3em;
+ float: right;
+ min-width: 25em;
+ padding: 0.3em;
+}
+ </style>
+ <script type='text/javascript'>
+window.addEventListener('load', function() {
// Hook up the navigation selector.
document.getElementById('hdr-Selector').onchange = (e) => {
window.location.href = e.target.value;
};
-};
-
-// Start a GET /hang request. If it ever completes, the server
-// has disconnected. Show a banner in that case.
-{
- var x = new XMLHttpRequest();
- x.open("GET", "/hang", true);
- x.onloadend = () => {
- document.getElementById("disconnected").style.display = 'block';
- };
- x.send();
-};
+});
</script>
</head>
<body>
<header>
-<div id='disconnected'>Gopls server has terminated. Page is inactive.</div>
<select id='hdr-Selector'>
<optgroup label="Documentation">
<option label="Overview" value="#hdr-Overview"/>
@@ -316,26 +316,6 @@
// -- main element --
- // sourceLink returns HTML for a link to open a file in the client editor.
- sourceLink := func(text, url string) string {
- // The /open URL returns nothing but has the side effect
- // of causing the LSP client to open the requested file.
- // So we use onclick to prevent the browser from navigating.
- // We keep the href attribute as it causes the <a> to render
- // as a link: blue, underlined, with URL hover information.
- return fmt.Sprintf(`<a href="%[1]s" onclick='return httpGET("%[1]s")'>%[2]s</a>`,
- escape(url), escape(text))
- }
-
- // objHTML returns HTML for obj.Name(), possibly as a link.
- objHTML := func(obj types.Object) string {
- text := obj.Name()
- if posn := safetoken.StartPosition(pkg.FileSet(), obj.Pos()); posn.IsValid() {
- return sourceLink(text, web.OpenURL(posn.Filename, posn.Line, posn.Column))
- }
- return text
- }
-
// nodeHTML returns HTML markup for a syntax tree.
// It replaces referring identifiers with links,
// and adds style spans for strings and comments.
@@ -613,7 +593,7 @@
for _, docfn := range funcs {
obj := scope.Lookup(docfn.Name).(*types.Func)
fmt.Fprintf(&buf, "<h3 id='%s'>func %s</h3>\n",
- docfn.Name, objHTML(obj))
+ docfn.Name, objHTML(pkg.FileSet(), web, obj))
// decl: func F(params) results
fmt.Fprintf(&buf, "<pre class='code'>%s</pre>\n",
@@ -631,7 +611,8 @@
tname := scope.Lookup(doctype.Name).(*types.TypeName)
// title and source link
- fmt.Fprintf(&buf, "<h3 id='%s'>type %s</a></h3>\n", doctype.Name, objHTML(tname))
+ fmt.Fprintf(&buf, "<h3 id='%s'>type %s</a></h3>\n",
+ doctype.Name, objHTML(pkg.FileSet(), web, tname))
// declaration
// TODO(adonovan): excise non-exported struct fields somehow.
@@ -652,7 +633,7 @@
method, _, _ := types.LookupFieldOrMethod(tname.Type(), true, tname.Pkg(), docmethod.Name)
fmt.Fprintf(&buf, "<h4 id='%s.%s'>func (%s) %s</h4>\n",
doctype.Name, docmethod.Name,
- doctype.Name, objHTML(method))
+ doctype.Name, objHTML(pkg.FileSet(), web, method))
// decl: func (x T) M(params) results
fmt.Fprintf(&buf, "<pre class='code'>%s</pre>\n",
@@ -668,7 +649,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), web.OpenURL(filename, 1, 1)))
+ sourceLink(filepath.Base(filename), web.SrcURL(filename, 1, 1)))
}
fmt.Fprintf(&buf, "</main>\n")
@@ -693,135 +674,3 @@
}
return slice
}
-
-// (partly taken from pkgsite's typography.css)
-const pkgDocStyle = `
-body {
- font-family: Helvetica, Arial, sans-serif;
- font-size: 1rem;
- line-height: normal;
-}
-
-h1 {
- font-size: 1.5rem;
-}
-
-h2 {
- font-size: 1.375rem;
-}
-
-h3 {
- font-size: 1.25rem;
-}
-
-h4 {
- font-size: 1.125rem;
-}
-
-h5 {
- font-size: 1rem;
-}
-
-h6 {
- font-size: 0.875rem;
-}
-
-h1,
-h2,
-h3,
-h4 {
- font-weight: 600;
- line-height: 1.25em;
- word-break: break-word;
-}
-
-h5,
-h6 {
- font-weight: 500;
- line-height: 1.3em;
- word-break: break-word;
-}
-
-p {
- font-size: 1rem;
- line-height: 1.5rem;
- max-width: 60rem;
-}
-
-strong {
- font-weight: 600;
-}
-
-code,
-pre,
-textarea.code {
- font-family: Consolas, 'Liberation Mono', Menlo, monospace;
- font-size: 0.875rem;
- line-height: 1.5em;
-}
-
-pre,
-textarea.code {
- background-color: #eee;
- border: 3px;
- border-radius: 3px
- color: black;
- overflow-x: auto;
- padding: 0.625rem;
- tab-size: 4;
- white-space: pre;
-}
-
-button,
-input,
-select,
-textarea {
- font: inherit;
-}
-
-a,
-a:link,
-a:visited {
- color: rgb(0, 125, 156);
- text-decoration: none;
-}
-
-a:hover,
-a:focus {
- color: rgb(0, 125, 156);
- text-decoration: underline;
-}
-
-a:hover > * {
- text-decoration: underline;
-}
-
-.lit { color: darkgreen; }
-
-#pkgsite { height: 1.5em; }
-
-header {
- position: sticky;
- top: 0;
- left: 0;
- width: 100%;
- padding: 0.3em;
-}
-
-#hdr-Selector {
- margin-right: 0.3em;
- float: right;
- min-width: 25em;
- padding: 0.3em;
-}
-
-#disconnected {
- position: fixed;
- top: 1em;
- left: 1em;
- display: none; /* initially */
- background-color: white;
- border: thick solid red;
- padding: 2em;
-}
-`
diff --git a/gopls/internal/server/assets/common.css b/gopls/internal/server/assets/common.css
new file mode 100644
index 0000000..16baa4f
--- /dev/null
+++ b/gopls/internal/server/assets/common.css
@@ -0,0 +1,116 @@
+/* Copyright 2024 The Go Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file.
+ */
+
+/* inspired by pkg.go.dev's typography.css */
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji';
+ font-size: 1rem;
+ line-height: normal;
+}
+
+h1 {
+ font-size: 1.5rem;
+}
+
+h2 {
+ font-size: 1.375rem;
+}
+
+h3 {
+ font-size: 1.25rem;
+}
+
+h4 {
+ font-size: 1.125rem;
+}
+
+h5 {
+ font-size: 1rem;
+}
+
+h6 {
+ font-size: 0.875rem;
+}
+
+h1,
+h2,
+h3,
+h4 {
+ font-weight: 600;
+ line-height: 1.25em;
+ word-break: break-word;
+}
+
+h5,
+h6 {
+ font-weight: 500;
+ line-height: 1.3em;
+ word-break: break-word;
+}
+
+p {
+ font-size: 1rem;
+ line-height: 1.5rem;
+ max-width: 60rem;
+}
+
+strong {
+ font-weight: 600;
+}
+
+code,
+pre,
+textarea.code {
+ font-family: Consolas, 'Liberation Mono', Menlo, monospace;
+ font-size: 0.875rem;
+ line-height: 1.5em;
+}
+
+pre,
+textarea.code {
+ background-color: #eee;
+ border: 3px;
+ border-radius: 3px
+ color: black;
+ overflow-x: auto;
+ padding: 0.625rem;
+ tab-size: 4;
+ white-space: pre;
+}
+
+button,
+input,
+select,
+textarea {
+ font: inherit;
+}
+
+a,
+a:link,
+a:visited {
+ color: rgb(0, 125, 156);
+ text-decoration: none;
+}
+
+a:hover,
+a:focus {
+ color: rgb(0, 125, 156);
+ text-decoration: underline;
+}
+
+a:hover > * {
+ text-decoration: underline;
+}
+
+#disconnected {
+ position: fixed;
+ top: 1em;
+ left: 1em;
+ display: none; /* initially */
+ background-color: white;
+ border: thick solid red;
+ padding: 2em;
+}
diff --git a/gopls/internal/server/assets/common.js b/gopls/internal/server/assets/common.js
new file mode 100644
index 0000000..1233456
--- /dev/null
+++ b/gopls/internal/server/assets/common.js
@@ -0,0 +1,28 @@
+// Copyright 2024 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// httpGET requests a URL for its effects only.
+// (It is needed for /open URLs; see objHTML.)
+function httpGET(url) {
+ var x = new XMLHttpRequest();
+ x.open("GET", url, true);
+ x.send();
+ return false; // disable usual <a href=...> behavior
+}
+
+// disconnect banner
+window.addEventListener('load', function() {
+ // Create a hidden <div id='disconnected'> element.
+ var banner = document.createElement("div");
+ banner.id = "disconnected";
+ banner.innerText = "Gopls server has terminated. Page is inactive.";
+ document.body.appendChild(banner);
+
+ // Start a GET /hang request. If it ever completes, the server
+ // has disconnected. Reveal the banner in that case.
+ var x = new XMLHttpRequest();
+ x.open("GET", "/hang", true);
+ x.onloadend = () => { banner.style.display = "block"; };
+ x.send();
+});
diff --git a/gopls/internal/server/server.go b/gopls/internal/server/server.go
index a414942..80e64bb 100644
--- a/gopls/internal/server/server.go
+++ b/gopls/internal/server/server.go
@@ -286,9 +286,9 @@
mux: webMux,
}
- // The /open handler allows the browser to request that the
- // LSP client editor open a file; see web.urlToOpen.
- webMux.HandleFunc("/open", func(w http.ResponseWriter, req *http.Request) {
+ // The /src handler allows the browser to request that the
+ // LSP client editor open a file; see web.SrcURL.
+ webMux.HandleFunc("/src", func(w http.ResponseWriter, req *http.Request) {
if err := req.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
@@ -463,16 +463,16 @@
//go:embed assets/*
var assets embed.FS
-// OpenURL returns an /open URL that, when visited, causes the client
+// SrcURL returns a /src 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) SrcURL(filename string, line, col8 int) protocol.URI {
return w.url(
- "open",
+ "src",
fmt.Sprintf("file=%s&line=%d&col=%d", url.QueryEscape(filename), line, col8),
"")
}
diff --git a/gopls/internal/test/integration/misc/webserver_test.go b/gopls/internal/test/integration/misc/webserver_test.go
index ae89745..0a692ec 100644
--- a/gopls/internal/test/integration/misc/webserver_test.go
+++ b/gopls/internal/test/integration/misc/webserver_test.go
@@ -61,14 +61,14 @@
// (We don't have a DOM or JS interpreter so we have
// to know something of the document internals here.)
rx := regexp.MustCompile(`<h3 id='NewFunc'.*httpGET\("(.*)"\)`)
- openURL := html.UnescapeString(string(rx.FindSubmatch(doc2)[1]))
+ srcURL := html.UnescapeString(string(rx.FindSubmatch(doc2)[1]))
// Fetch the document. Its result isn't important,
// but it must have the side effect of another showDocument
// downcall, this time for a "file:" URL, causing the
// client editor to navigate to the source file.
- t.Log("extracted /open URL", openURL)
- get(t, openURL)
+ t.Log("extracted /src URL", srcURL)
+ get(t, srcURL)
// Check that that shown location is that of NewFunc.
shownSource := shownDocument(t, env, "file:")