gopls/internal/golang: add "Show assembly of f" code action
This CL adds a new code action to display the Go assembly
code produced for the enclosing function, and all its
nested functions. Each instruction is marked up with a
link that causes cursor navigation in the editor,
using the magic of LSP's showDocument.
Each page load causes a recompile. (A cold build may
take a few seconds in a large project.)
The architecture is determined by the view: for most
files it will be the default GOARCH, but for arch-tagged
files it will be the appropriate one.
+ Test
+ Release note
Fixes golang/go#67478
Change-Id: I51285215e9b27c510076c64eeb7b7ae3899f8a59
Reviewed-on: https://go-review.googlesource.com/c/tools/+/588395
Reviewed-by: Robert Findley <rfindley@google.com>
Auto-Submit: Alan Donovan <adonovan@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/gopls/doc/commands.md b/gopls/doc/commands.md
index 69c3af8..357e349 100644
--- a/gopls/doc/commands.md
+++ b/gopls/doc/commands.md
@@ -140,6 +140,20 @@
}
```
+## `gopls.assembly`: **Show disassembly of current function.**
+
+This command opens a web-based disassembly listing of the
+specified function symbol (plus any nested lambdas and defers).
+The machine architecture is determined by the view.
+
+Args:
+
+```
+string,
+string,
+string
+```
+
## `gopls.change_signature`: **Perform a "change signature" refactoring**
This command is experimental, currently only supporting parameter removal.
diff --git a/gopls/doc/release/v0.16.0.md b/gopls/doc/release/v0.16.0.md
index dc004c4..ab54ae2 100644
--- a/gopls/doc/release/v0.16.0.md
+++ b/gopls/doc/release/v0.16.0.md
@@ -88,6 +88,31 @@
```
TODO(dominikh/go-mode.el#436): add both of these to go-mode.el.
+### Show assembly
+
+Gopls offers a third web-based code action, "Show assembly for f",
+which displays an assembly listing of the function declaration
+enclosing the selected code, plus any nested functions (function
+literals, deferred calls).
+It invokes the compiler to generate the report.
+Reloading the page will update the report.
+
+The machine architecture is determined by the "view" or build
+configuration that gopls selects for the current file.
+This is usually the same as your machine's GOARCH unless you are
+working in a file with `go:build` tags for a different architecture.
+
+- TODO screenshot
+
+Gopls cannot yet display assembly for generic functions: generic
+functions are not fully compiled until they are instantiated, but any
+function declaration enclosing the selection cannot be an instantiated
+generic function.
+<!-- Clearly the ideal UX for generic functions is to use the function
+ symbol under the cursor, e.g. Vector[string], rather than the
+ enclosing function; but computing the name of the linker symbol
+ remains a challenge. -->
+
### `unusedwrite` analyzer
The new
diff --git a/gopls/internal/doc/api.json b/gopls/internal/doc/api.json
index e170385..555c56a 100644
--- a/gopls/internal/doc/api.json
+++ b/gopls/internal/doc/api.json
@@ -961,6 +961,13 @@
"ResultDoc": "{\n\t// Holds changes to existing resources.\n\t\"changes\": map[golang.org/x/tools/gopls/internal/protocol.DocumentURI][]golang.org/x/tools/gopls/internal/protocol.TextEdit,\n\t// Depending on the client capability `workspace.workspaceEdit.resourceOperations` document changes\n\t// are either an array of `TextDocumentEdit`s to express changes to n different text documents\n\t// where each text document edit addresses a specific version of a text document. Or it can contain\n\t// above `TextDocumentEdit`s mixed with create, rename and delete file / folder operations.\n\t//\n\t// Whether a client supports versioned document edits is expressed via\n\t// `workspace.workspaceEdit.documentChanges` client capability.\n\t//\n\t// If a client neither supports `documentChanges` nor `workspace.workspaceEdit.resourceOperations` then\n\t// only plain `TextEdit`s using the `changes` property are supported.\n\t\"documentChanges\": []{\n\t\t\"TextDocumentEdit\": {\n\t\t\t\"textDocument\": { ... },\n\t\t\t\"edits\": { ... },\n\t\t},\n\t\t\"CreateFile\": {\n\t\t\t\"kind\": string,\n\t\t\t\"uri\": string,\n\t\t\t\"options\": { ... },\n\t\t\t\"ResourceOperation\": { ... },\n\t\t},\n\t\t\"RenameFile\": {\n\t\t\t\"kind\": string,\n\t\t\t\"oldUri\": string,\n\t\t\t\"newUri\": string,\n\t\t\t\"options\": { ... },\n\t\t\t\"ResourceOperation\": { ... },\n\t\t},\n\t\t\"DeleteFile\": {\n\t\t\t\"kind\": string,\n\t\t\t\"uri\": string,\n\t\t\t\"options\": { ... },\n\t\t\t\"ResourceOperation\": { ... },\n\t\t},\n\t},\n\t// A map of change annotations that can be referenced in `AnnotatedTextEdit`s or create, rename and\n\t// delete file / folder operations.\n\t//\n\t// Whether clients honor this property depends on the client capability `workspace.changeAnnotationSupport`.\n\t//\n\t// @since 3.16.0\n\t\"changeAnnotations\": map[string]golang.org/x/tools/gopls/internal/protocol.ChangeAnnotation,\n}"
},
{
+ "Command": "gopls.assembly",
+ "Title": "Show disassembly of current function.",
+ "Doc": "This command opens a web-based disassembly listing of the\nspecified function symbol (plus any nested lambdas and defers).\nThe machine architecture is determined by the view.",
+ "ArgDoc": "string,\nstring,\nstring",
+ "ResultDoc": ""
+ },
+ {
"Command": "gopls.change_signature",
"Title": "Perform a \"change signature\" refactoring",
"Doc": "This command is experimental, currently only supporting parameter removal.\nIts signature will certainly change in the future (pun intended).",
diff --git a/gopls/internal/golang/assembly.go b/gopls/internal/golang/assembly.go
new file mode 100644
index 0000000..cbb9512
--- /dev/null
+++ b/gopls/internal/golang/assembly.go
@@ -0,0 +1,166 @@
+// 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.
+
+package golang
+
+// This file produces the "Show GOARCH assembly of f" HTML report.
+//
+// See also:
+// - ./codeaction.go - computes the symbol and offers the CodeAction command.
+// - ../server/command.go - handles the command by opening a web page.
+// - ../server/server.go - handles the HTTP request and calls this function.
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "html"
+ "path/filepath"
+ "regexp"
+ "strconv"
+ "strings"
+
+ "golang.org/x/tools/gopls/internal/cache"
+ "golang.org/x/tools/internal/gocommand"
+)
+
+// AssemblyHTML returns an HTML document containing an assembly listing of the selected function.
+//
+// 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) {
+ // Compile the package with -S, and capture its stderr stream.
+ inv, cleanupInvocation, err := snapshot.GoCommandInvocation(false, &gocommand.Invocation{
+ Verb: "build",
+ Args: []string{"-gcflags=-S", "."},
+ WorkingDir: filepath.Dir(pkg.Metadata().CompiledGoFiles[0].Path()),
+ })
+ if err != nil {
+ return nil, err // e.g. failed to write overlays (rare)
+ }
+ defer cleanupInvocation()
+ _, stderr, err, _ := snapshot.View().GoCommandRunner().RunRaw(ctx, *inv)
+ if err != nil {
+ return nil, err // e.g. won't compile
+ }
+ content := stderr.String()
+
+ 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))
+ var buf bytes.Buffer
+ buf.WriteString(`<!DOCTYPE html>
+<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>
+</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>
+</p>
+<p>
+ Experimental. <a href='https://github.com/golang/go/issues/67478'>Contributions welcome!</a>
+</p>
+<p>
+ Click on a source line marker <code>L1234</code> to navigate your editor there.
+ (Beware: <a href='https://github.com/microsoft/vscode/issues/207634'>#207634</a>)
+</p>
+<p>
+ Reload the page to recompile.
+</p>
+<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+(.*)$`)
+
+ // Parse the functions of interest out of the listing.
+ // Each function is of the form:
+ //
+ // symbol STEXT k=v...
+ // 0x0000 00000 (/file.go:123) NOP...
+ // ...
+ //
+ // Allow matches of symbol, symbol.func1, symbol.deferwrap, etc.
+ on := false
+ for _, line := range strings.Split(content, "\n") {
+ // start of function symbol?
+ if strings.Contains(line, " STEXT ") {
+ on = strings.HasPrefix(line, symbol) &&
+ (line[len(symbol)] == ' ' || line[len(symbol)] == '.')
+ }
+ if !on {
+ continue // within uninteresting symbol
+ }
+
+ // In lines of the form
+ // "\t0x0000 00000 (/file.go:123) NOP..."
+ // replace the "(/file.go:123)" portion with an "L0123" source link.
+ // Skip filenames of the form "<foo>".
+ if parts := insnRx.FindStringSubmatch(line); parts != nil {
+ link := " " // if unknown
+ 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))
+ }
+ }
+ fmt.Fprintf(&buf, "%s\t%s\t%s", escape(parts[1]), link, escape(parts[3]))
+ } else {
+ buf.WriteString(escape(line))
+ }
+ buf.WriteByte('\n')
+ }
+ return buf.Bytes(), nil
+}
+
+// cutLast is the "last" analogue of [strings.Cut].
+func cutLast(s, sep string) (before, after string, ok bool) {
+ if i := strings.LastIndex(s, sep); i >= 0 {
+ return s[:i], s[i+len(sep):], true
+ }
+ return s, "", false
+}
diff --git a/gopls/internal/golang/codeaction.go b/gopls/internal/golang/codeaction.go
index 2208822..07f777e 100644
--- a/gopls/internal/golang/codeaction.go
+++ b/gopls/internal/golang/codeaction.go
@@ -9,8 +9,10 @@
"encoding/json"
"fmt"
"go/ast"
+ "go/types"
"strings"
+ "golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/gopls/internal/analysis/fillstruct"
"golang.org/x/tools/gopls/internal/analysis/fillswitch"
"golang.org/x/tools/gopls/internal/cache"
@@ -24,6 +26,7 @@
"golang.org/x/tools/gopls/internal/util/slices"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/imports"
+ "golang.org/x/tools/internal/typesinternal"
)
// CodeActions returns all wanted code actions (edits and other
@@ -118,7 +121,7 @@
if err != nil {
return nil, err
}
- // For implementation, see commandHandler.showFreeSymbols.
+ // For implementation, see commandHandler.FreeSymbols.
actions = append(actions, protocol.CodeAction{
Title: cmd.Title,
Kind: protocol.GoFreeSymbols,
@@ -130,6 +133,7 @@
// Code actions requiring type information.
if want[protocol.RefactorRewrite] ||
want[protocol.RefactorInline] ||
+ want[protocol.GoAssembly] ||
want[protocol.GoTest] {
pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI())
if err != nil {
@@ -160,6 +164,14 @@
}
actions = append(actions, fixes...)
}
+
+ if want[protocol.GoAssembly] {
+ fixes, err := getGoAssemblyAction(snapshot.View(), pkg, pgf, rng)
+ if err != nil {
+ return nil, err
+ }
+ actions = append(actions, fixes...)
+ }
}
return actions, nil
}
@@ -521,3 +533,81 @@
Command: &cmd,
}}, nil
}
+
+// getGoAssemblyAction returns any "Show assembly for f" code actions for the selection.
+func getGoAssemblyAction(view *cache.View, pkg *cache.Package, pgf *parsego.File, rng protocol.Range) ([]protocol.CodeAction, error) {
+ start, end, err := pgf.RangePos(rng)
+ if err != nil {
+ return nil, err
+ }
+
+ // Find the enclosing toplevel function or method,
+ // and compute its symbol name (e.g. "pkgpath.(T).method").
+ // The report will show this method and all its nested
+ // functions (FuncLit, defers, etc).
+ //
+ // TODO(adonovan): this is no good for generics, since they
+ // will always be uninstantiated when they enclose the cursor.
+ // Instead, we need to query the func symbol under the cursor,
+ // rather than the enclosing function. It may be an explicitly
+ // or implicitly instantiated generic, and it may be defined
+ // in another package, though we would still need to compile
+ // the current package to see its assembly. The challenge,
+ // however, is that computing the linker name for a generic
+ // symbol is quite tricky. Talk with the compiler team for
+ // ideas.
+ //
+ // TODO(adonovan): think about a smoother UX for jumping
+ // directly to (say) a lambda of interest.
+ // Perhaps we could scroll to STEXT for the innermost
+ // enclosing nested function?
+ var actions []protocol.CodeAction
+ path, _ := astutil.PathEnclosingInterval(pgf.File, start, end)
+ if len(path) >= 2 { // [... FuncDecl File]
+ if decl, ok := path[len(path)-2].(*ast.FuncDecl); ok {
+ if fn, ok := pkg.TypesInfo().Defs[decl.Name].(*types.Func); ok {
+ sig := fn.Type().(*types.Signature)
+
+ // Compute the linker symbol of the enclosing function.
+ var sym strings.Builder
+ if fn.Pkg().Name() == "main" {
+ sym.WriteString("main")
+ } else {
+ sym.WriteString(fn.Pkg().Path())
+ }
+ sym.WriteString(".")
+ if sig.Recv() != nil {
+ if isPtr, named := typesinternal.ReceiverNamed(sig.Recv()); named != nil {
+ sym.WriteString("(")
+ if isPtr {
+ sym.WriteString("*")
+ }
+ sym.WriteString(named.Obj().Name())
+ sym.WriteString(").")
+ }
+ }
+ sym.WriteString(fn.Name())
+
+ if fn.Name() != "_" && // blank functions are not compiled
+ (fn.Name() != "init" || sig.Recv() != nil) && // init functions aren't linker functions
+ sig.TypeParams() == nil && sig.RecvTypeParams() == nil { // generic => no assembly
+ cmd, err := command.NewAssemblyCommand(
+ fmt.Sprintf("Show %s assembly for %s", view.GOARCH(), decl.Name),
+ view.ID(),
+ string(pkg.Metadata().ID),
+ sym.String())
+ if err != nil {
+ return nil, err
+ }
+ // For handler, see commandHandler.Assembly.
+ actions = append(actions, protocol.CodeAction{
+ Title: cmd.Title,
+ Kind: protocol.GoAssembly,
+ Command: &cmd,
+ })
+ }
+ }
+ }
+ }
+ return actions, nil
+}
diff --git a/gopls/internal/protocol/codeactionkind.go b/gopls/internal/protocol/codeactionkind.go
index f3d953f..d493b45 100644
--- a/gopls/internal/protocol/codeactionkind.go
+++ b/gopls/internal/protocol/codeactionkind.go
@@ -74,9 +74,10 @@
// instead of == for CodeActionKinds throughout gopls.
// See golang/go#40438 for related discussion.
const (
- GoTest CodeActionKind = "goTest"
+ GoAssembly CodeActionKind = "source.assembly"
GoDoc CodeActionKind = "source.doc"
GoFreeSymbols CodeActionKind = "source.freesymbols"
+ GoTest CodeActionKind = "goTest" // TODO(adonovan): rename "source.test"
)
// CodeActionUnknownTrigger indicates that the trigger for a
diff --git a/gopls/internal/protocol/command/command_gen.go b/gopls/internal/protocol/command/command_gen.go
index 5e267af..2ba73db 100644
--- a/gopls/internal/protocol/command/command_gen.go
+++ b/gopls/internal/protocol/command/command_gen.go
@@ -28,6 +28,7 @@
AddImport Command = "gopls.add_import"
AddTelemetryCounters Command = "gopls.add_telemetry_counters"
ApplyFix Command = "gopls.apply_fix"
+ Assembly Command = "gopls.assembly"
ChangeSignature Command = "gopls.change_signature"
CheckUpgrades Command = "gopls.check_upgrades"
DiagnoseFiles Command = "gopls.diagnose_files"
@@ -66,6 +67,7 @@
AddImport,
AddTelemetryCounters,
ApplyFix,
+ Assembly,
ChangeSignature,
CheckUpgrades,
DiagnoseFiles,
@@ -125,6 +127,14 @@
return nil, err
}
return s.ApplyFix(ctx, a0)
+ case Assembly:
+ var a0 string
+ var a1 string
+ var a2 string
+ if err := UnmarshalArgs(params.Arguments, &a0, &a1, &a2); err != nil {
+ return nil, err
+ }
+ return nil, s.Assembly(ctx, a0, a1, a2)
case ChangeSignature:
var a0 ChangeSignatureArgs
if err := UnmarshalArgs(params.Arguments, &a0); err != nil {
@@ -350,6 +360,18 @@
}, nil
}
+func NewAssemblyCommand(title string, a0 string, a1 string, a2 string) (protocol.Command, error) {
+ args, err := MarshalArgs(a0, a1, a2)
+ if err != nil {
+ return protocol.Command{}, err
+ }
+ return protocol.Command{
+ Title: title,
+ Command: Assembly.String(),
+ Arguments: args,
+ }, nil
+}
+
func NewChangeSignatureCommand(title string, a0 ChangeSignatureArgs) (protocol.Command, error) {
args, err := MarshalArgs(a0)
if err != nil {
diff --git a/gopls/internal/protocol/command/interface.go b/gopls/internal/protocol/command/interface.go
index e8d6d7d..9f76add 100644
--- a/gopls/internal/protocol/command/interface.go
+++ b/gopls/internal/protocol/command/interface.go
@@ -252,6 +252,13 @@
// block of code depends on, perhaps as a precursor to
// extracting it into a separate function.
FreeSymbols(context.Context, protocol.DocumentURI, protocol.Range) error
+
+ // Assembly: Show disassembly of current function.
+ //
+ // This command opens a web-based disassembly listing of the
+ // specified function symbol (plus any nested lambdas and defers).
+ // The machine architecture is determined by the view.
+ Assembly(_ context.Context, viewID, packageID, symbol string) error
}
type RunTestsArgs struct {
diff --git a/gopls/internal/server/code_action.go b/gopls/internal/server/code_action.go
index 45f230e..7252aa9 100644
--- a/gopls/internal/server/code_action.go
+++ b/gopls/internal/server/code_action.go
@@ -139,7 +139,10 @@
if golang.IsGenerated(ctx, snapshot, uri) {
actions = slices.DeleteFunc(actions, func(a protocol.CodeAction) bool {
switch a.Kind {
- case protocol.GoTest, protocol.GoDoc, protocol.GoFreeSymbols:
+ case protocol.GoTest,
+ protocol.GoDoc,
+ protocol.GoFreeSymbols,
+ protocol.GoAssembly:
return false // read-only query
}
return true // potential write operation
diff --git a/gopls/internal/server/command.go b/gopls/internal/server/command.go
index 0a64d1c..04febab 100644
--- a/gopls/internal/server/command.go
+++ b/gopls/internal/server/command.go
@@ -104,7 +104,7 @@
// be populated, depending on which configuration is set. See comments in-line
// for details.
type commandDeps struct {
- snapshot *cache.Snapshot // present if cfg.forURI was set
+ snapshot *cache.Snapshot // present if cfg.forURI or forView was set
fh file.Handle // present if cfg.forURI was set
work *progress.WorkDone // present if cfg.progress was set
}
@@ -1466,6 +1466,9 @@
}
func (c *commandHandler) FreeSymbols(ctx context.Context, uri protocol.DocumentURI, rng protocol.Range) error {
+ // TODO(adonovan): simplify, following Assembly, by putting the
+ // viewID in the command so that c.run isn't necessary.
+ // (freesymbolsURL needs only a viewID, not a view.)
return c.run(ctx, commandConfig{
forURI: uri,
}, func(ctx context.Context, deps commandDeps) error {
@@ -1481,3 +1484,13 @@
return nil
})
}
+
+func (c *commandHandler) Assembly(ctx context.Context, viewID, packageID, symbol string) error {
+ web, err := c.s.getWeb()
+ if err != nil {
+ return err
+ }
+ url := web.assemblyURL(viewID, packageID, symbol)
+ openClientBrowser(ctx, c.s.client, url)
+ return nil
+}
diff --git a/gopls/internal/server/server.go b/gopls/internal/server/server.go
index 3418215..2c0c34a 100644
--- a/gopls/internal/server/server.go
+++ b/gopls/internal/server/server.go
@@ -205,6 +205,8 @@
//
// open?file=%s&line=%d&col=%d - open a file
// pkg/PKGPATH?view=%s - show doc for package in a given view
+// assembly?pkg=%s&view=%s&symbol=%s - show assembly of specified func symbol
+// freesymbols?file=%s&range=%d:%d:%d:%d:&view=%s - show report of free symbols
type web struct {
server *http.Server
addr url.URL // "http://127.0.0.1:PORT/gopls/SECRET"
@@ -315,7 +317,7 @@
// Get snapshot of specified view.
view, err := s.session.View(req.Form.Get("view"))
if err != nil {
- http.Error(w, err.Error(), http.StatusBadRequest)
+ http.Error(w, err.Error(), http.StatusNotFound)
return
}
snapshot, release, err := view.Snapshot()
@@ -410,6 +412,55 @@
w.Write(html)
})
+ // The /assembly?pkg=...&view=...&symbol=... handler shows
+ // the assembly of the current function.
+ webMux.HandleFunc("/assembly", func(w http.ResponseWriter, req *http.Request) {
+ ctx := req.Context()
+ if err := req.ParseForm(); err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+
+ // Get parameters.
+ var (
+ viewID = req.Form.Get("view")
+ pkgID = metadata.PackageID(req.Form.Get("pkg"))
+ symbol = req.Form.Get("symbol")
+ )
+ if viewID == "" || pkgID == "" || symbol == "" {
+ http.Error(w, "/assembly requires view, pkg, symbol", http.StatusBadRequest)
+ return
+ }
+
+ // Get snapshot of specified view.
+ view, err := s.session.View(viewID)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusNotFound)
+ return
+ }
+ snapshot, release, err := view.Snapshot()
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ defer release()
+
+ pkgs, err := snapshot.TypeCheck(ctx, pkgID)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ pkg := pkgs[0]
+
+ // Produce report.
+ html, err := golang.AssemblyHTML(ctx, snapshot, pkg, symbol, web.openURL)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ w.Write(html)
+ })
+
return web, nil
}
@@ -456,6 +507,17 @@
"")
}
+// assemblyURL returns the URL of an assembly listing of the specified function symbol.
+func (w *web) assemblyURL(viewID, packageID, symbol string) protocol.URI {
+ return w.url(
+ "assembly",
+ fmt.Sprintf("view=%s&pkg=%s&symbol=%s",
+ url.QueryEscape(viewID),
+ url.QueryEscape(packageID),
+ url.QueryEscape(symbol)),
+ "")
+}
+
// url returns a URL by joining a relative path, an (encoded) query,
// and an (unencoded) fragment onto the authenticated base URL of the
// web server.
diff --git a/gopls/internal/settings/default.go b/gopls/internal/settings/default.go
index e280c75..fec64e3 100644
--- a/gopls/internal/settings/default.go
+++ b/gopls/internal/settings/default.go
@@ -49,6 +49,7 @@
protocol.RefactorRewrite: true,
protocol.RefactorInline: true,
protocol.RefactorExtract: true,
+ protocol.GoAssembly: true,
protocol.GoDoc: true,
protocol.GoFreeSymbols: true,
},
diff --git a/gopls/internal/test/integration/misc/codeactions_test.go b/gopls/internal/test/integration/misc/codeactions_test.go
index 1a169c6..09629b4 100644
--- a/gopls/internal/test/integration/misc/codeactions_test.go
+++ b/gopls/internal/test/integration/misc/codeactions_test.go
@@ -23,12 +23,12 @@
module example.com
go 1.19
--- src.go --
+-- src/a.go --
package a
func f() { g() }
func g() {}
--- gen.go --
+-- gen/a.go --
// Code generated by hand; DO NOT EDIT.
package a
@@ -62,12 +62,14 @@
}
}
- check("src.go",
+ check("src/a.go",
+ protocol.GoAssembly,
protocol.GoDoc,
protocol.GoFreeSymbols,
protocol.RefactorExtract,
protocol.RefactorInline)
- check("gen.go",
+ check("gen/a.go",
+ protocol.GoAssembly,
protocol.GoDoc,
protocol.GoFreeSymbols)
})
diff --git a/gopls/internal/test/integration/misc/webserver_test.go b/gopls/internal/test/integration/misc/webserver_test.go
index 851cc68..22f65ec 100644
--- a/gopls/internal/test/integration/misc/webserver_test.go
+++ b/gopls/internal/test/integration/misc/webserver_test.go
@@ -9,12 +9,14 @@
"io"
"net/http"
"regexp"
+ "runtime"
"strings"
"testing"
"golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/protocol/command"
. "golang.org/x/tools/gopls/internal/test/integration"
+ "golang.org/x/tools/internal/testenv"
)
// TestWebServer exercises the web server created on demand
@@ -295,6 +297,81 @@
})
}
+// TestAssembly is a basic test of the web-based assembly listing.
+func TestAssembly(t *testing.T) {
+ testenv.NeedsGo1Point(t, 22) // for up-to-date assembly listing
+
+ const files = `
+-- go.mod --
+module example.com
+
+-- a/a.go --
+package a
+
+func f() {
+ println("hello")
+ defer println("world")
+}
+
+func g() {
+ println("goodbye")
+}
+`
+ Run(t, files, func(t *testing.T, env *Env) {
+ env.OpenFile("a/a.go")
+
+ // Invoke the "Show assembly" code action to start the server.
+ loc := env.RegexpSearch("a/a.go", "println")
+ actions, err := env.Editor.CodeAction(env.Ctx, loc, nil)
+ if err != nil {
+ t.Fatalf("CodeAction: %v", err)
+ }
+ const wantTitle = "Show " + runtime.GOARCH + " assembly for f"
+ var action *protocol.CodeAction
+ for _, a := range actions {
+ if a.Title == wantTitle {
+ action = &a
+ break
+ }
+ }
+ if action == nil {
+ t.Fatalf("can't find action with Title %s, only %#v",
+ wantTitle, actions)
+ }
+
+ // Execute the command.
+ // Its side effect should be a single showDocument request.
+ params := &protocol.ExecuteCommandParams{
+ Command: action.Command.Command,
+ Arguments: action.Command.Arguments,
+ }
+ var result command.DebuggingResult
+ env.ExecuteCommand(params, &result)
+ doc := shownDocument(t, env, "http:")
+ if doc == nil {
+ t.Fatalf("no showDocument call had 'file:' prefix")
+ }
+ t.Log("showDocument(package doc) URL:", doc.URI)
+
+ // Get the report and do some minimal checks for sensible results.
+ // Use only portable instructions below!
+ report := get(t, doc.URI)
+ checkMatch(t, true, report, `TEXT.*example.com/a.f`)
+ checkMatch(t, true, report, `CALL runtime.printlock`)
+ checkMatch(t, true, report, `CALL runtime.printstring`)
+ checkMatch(t, true, report, `CALL runtime.printunlock`)
+ checkMatch(t, true, report, `CALL example.com/a.f.deferwrap1`)
+ checkMatch(t, true, report, `RET`)
+ checkMatch(t, true, report, `CALL runtime.morestack_noctxt`)
+
+ // Nested functions are also shown.
+ checkMatch(t, true, report, `TEXT.*example.com/a.f.deferwrap1`)
+
+ // But other functions are not.
+ checkMatch(t, false, report, `TEXT.*example.com/a.g`)
+ })
+}
+
// shownDocument returns the first shown document matching the URI prefix.
// It may be nil.
//