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:")