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>
This commit is contained in:
Alan Donovan 2024-06-07 14:14:47 -04:00 коммит произвёл Gopher Robot
Родитель f41a407b04
Коммит 413a491271
7 изменённых файлов: 214 добавлений и 282 удалений

Просмотреть файл

@ -50,7 +50,6 @@ func AssemblyHTML(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Pack
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 @@ func AssemblyHTML(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Pack
<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 @@ function httpGET(url) {
<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 @@ function httpGET(url) {
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]))

Просмотреть файл

@ -161,40 +161,11 @@ func FreeSymbolsHTML(viewID string, pkg *cache.Package, pgf *parsego.File, start
.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 @@ function httpGET(url) {
}
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 @@ function httpGET(url) {
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 @@ func freeRefs(pkg *types.Package, info *types.Info, file *ast.File, start, end t
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)
}

Просмотреть файл

@ -17,7 +17,6 @@ package golang
// - 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 @@ package golang
// - 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 @@ type Web interface {
// 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 @@ func PackageDocHTML(viewID string, pkg *cache.Package, web Web) ([]byte, error)
<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 @@ window.onload = () => {
// -- 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 @@ window.onload = () => {
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 @@ window.onload = () => {
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 @@ window.onload = () => {
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 @@ window.onload = () => {
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 @@ func typesSeqToSlice[T any](seq typesSeq[T]) []T {
}
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;
}
`

Просмотреть файл

@ -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;
}

Просмотреть файл

@ -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();
});

Просмотреть файл

@ -286,9 +286,9 @@ func (s *server) initWeb() (*web, error) {
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 @@ func (s *server) initWeb() (*web, error) {
//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),
"")
}

Просмотреть файл

@ -61,14 +61,14 @@ func (G[T]) F(int, int, int, int, int, int, int, ...int) {}
// (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:")