x/tools/gopls: run go generate through CodeLens

This change adds support for recognizing a //go:generate directive
and offering a CodeLens that will then send a "generate" command to
the server to run "go generate" or "go generate ./...". Because
"go generate" can only be executed per package, there is no need to show
the CodeLens on top of every //go:generate comment. Therefore, only the
top directive will be considered.

The stdout/stderr of the go generate command will be piped to the logger
while stderr will also be sent to the editor as a window/showMessage

The user will only know when the process starts and when it ends so that they wouldn't
get bogged with a large number of message windows popping up. However, they can
check the logs for all the details.

If a user wants to cancel the "go generate" command, they will be able
to do so with a "Cancel" ActionItem that the server will offer to the client

Fixes golang/go#37680

Change-Id: I89a9617521eab20859cb2215db133f34fda856c7
Reviewed-on: https://go-review.googlesource.com/c/tools/+/222247
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/internal/gocommand/invoke.go b/internal/gocommand/invoke.go
index 75d73e7..468db40 100644
--- a/internal/gocommand/invoke.go
+++ b/internal/gocommand/invoke.go
@@ -5,6 +5,7 @@
 	"bytes"
 	"context"
 	"fmt"
+	"io"
 	"os"
 	"os/exec"
 	"strings"
@@ -28,9 +29,27 @@
 	return stdout, friendly
 }
 
-// RunRaw is like Run, but also returns the raw stderr and error for callers
+// RunRaw is like RunPiped, but also returns the raw stderr and error for callers
 // that want to do low-level error handling/recovery.
 func (i *Invocation) RunRaw(ctx context.Context) (stdout *bytes.Buffer, stderr *bytes.Buffer, friendlyError error, rawError error) {
+	stdout = &bytes.Buffer{}
+	stderr = &bytes.Buffer{}
+	rawError = i.RunPiped(ctx, stdout, stderr)
+	if rawError != nil {
+		// Check for 'go' executable not being found.
+		if ee, ok := rawError.(*exec.Error); ok && ee.Err == exec.ErrNotFound {
+			friendlyError = fmt.Errorf("go command required, not found: %v", ee)
+		}
+		if ctx.Err() != nil {
+			friendlyError = ctx.Err()
+		}
+		friendlyError = fmt.Errorf("err: %v: stderr: %s", rawError, stderr)
+	}
+	return
+}
+
+// RunPiped is like Run, but relies on the given stdout/stderr
+func (i *Invocation) RunPiped(ctx context.Context, stdout, stderr io.Writer) error {
 	log := i.Logf
 	if log == nil {
 		log = func(string, ...interface{}) {}
@@ -51,8 +70,6 @@
 		goArgs = append(goArgs, i.Args...)
 	}
 	cmd := exec.Command("go", goArgs...)
-	stdout = &bytes.Buffer{}
-	stderr = &bytes.Buffer{}
 	cmd.Stdout = stdout
 	cmd.Stderr = stderr
 	// On darwin the cwd gets resolved to the real path, which breaks anything that
@@ -66,19 +83,7 @@
 
 	defer func(start time.Time) { log("%s for %v", time.Since(start), cmdDebugStr(cmd)) }(time.Now())
 
-	rawError = runCmdContext(ctx, cmd)
-	friendlyError = rawError
-	if rawError != nil {
-		// Check for 'go' executable not being found.
-		if ee, ok := rawError.(*exec.Error); ok && ee.Err == exec.ErrNotFound {
-			friendlyError = fmt.Errorf("go command required, not found: %v", ee)
-		}
-		if ctx.Err() != nil {
-			friendlyError = ctx.Err()
-		}
-		friendlyError = fmt.Errorf("err: %v: stderr: %s", rawError, stderr)
-	}
-	return
+	return runCmdContext(ctx, cmd)
 }
 
 // runCmdContext is like exec.CommandContext except it sends os.Interrupt
diff --git a/internal/lsp/command.go b/internal/lsp/command.go
index 2c00b7e..6c5f51f 100644
--- a/internal/lsp/command.go
+++ b/internal/lsp/command.go
@@ -1,3 +1,7 @@
+// Copyright 2020 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 lsp
 
 import (
@@ -7,11 +11,18 @@
 	"golang.org/x/tools/internal/gocommand"
 	"golang.org/x/tools/internal/lsp/protocol"
 	"golang.org/x/tools/internal/lsp/source"
+	"golang.org/x/tools/internal/xcontext"
 	errors "golang.org/x/xerrors"
 )
 
 func (s *Server) executeCommand(ctx context.Context, params *protocol.ExecuteCommandParams) (interface{}, error) {
 	switch params.Command {
+	case "generate":
+		dir, recursive, err := getGenerateRequest(params.Arguments)
+		if err != nil {
+			return nil, err
+		}
+		go s.runGenerate(xcontext.Detach(ctx), dir, recursive)
 	case "tidy":
 		if len(params.Arguments) == 0 || len(params.Arguments) > 1 {
 			return nil, errors.Errorf("expected one file URI for call to `go mod tidy`, got %v", params.Arguments)
@@ -54,3 +65,18 @@
 	}
 	return nil, nil
 }
+
+func getGenerateRequest(args []interface{}) (string, bool, error) {
+	if len(args) != 2 {
+		return "", false, errors.Errorf("expected exactly 2 arguments but got %d", len(args))
+	}
+	dir, ok := args[0].(string)
+	if !ok {
+		return "", false, errors.Errorf("expected dir to be a string value but got %T", args[0])
+	}
+	recursive, ok := args[1].(bool)
+	if !ok {
+		return "", false, errors.Errorf("expected recursive to be a boolean but got %T", args[1])
+	}
+	return dir, recursive, nil
+}
diff --git a/internal/lsp/generate.go b/internal/lsp/generate.go
new file mode 100644
index 0000000..bddf9c2
--- /dev/null
+++ b/internal/lsp/generate.go
@@ -0,0 +1,125 @@
+// Copyright 2020 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 lsp
+
+import (
+	"context"
+	"io"
+	"log"
+	"math/rand"
+	"strconv"
+
+	"golang.org/x/tools/internal/gocommand"
+	"golang.org/x/tools/internal/lsp/protocol"
+	"golang.org/x/tools/internal/telemetry/event"
+	errors "golang.org/x/xerrors"
+)
+
+func (s *Server) runGenerate(ctx context.Context, dir string, recursive bool) {
+	ctx, cancel := context.WithCancel(ctx)
+	defer cancel()
+
+	token := strconv.FormatInt(rand.Int63(), 10)
+	s.inProgressMu.Lock()
+	s.inProgress[token] = cancel
+	s.inProgressMu.Unlock()
+	defer s.clearInProgress(token)
+
+	er := &eventWriter{ctx: ctx}
+	wc := s.newProgressWriter(ctx, cancel)
+	defer wc.Close()
+	args := []string{"-x"}
+	if recursive {
+		args = append(args, "./...")
+	}
+	inv := &gocommand.Invocation{
+		Verb:       "generate",
+		Args:       args,
+		Env:        s.session.Options().Env,
+		WorkingDir: dir,
+	}
+	stderr := io.MultiWriter(er, wc)
+	err := inv.RunPiped(ctx, er, stderr)
+	if err != nil && !errors.Is(ctx.Err(), context.Canceled) {
+		log.Printf("generate: command error: %v", err)
+		s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
+			Type:    protocol.Error,
+			Message: "go generate exited with an error, check gopls logs",
+		})
+	}
+}
+
+// eventWriter writes every incoming []byte to
+// event.Print with the operation=generate tag
+// to distinguish its logs from others.
+type eventWriter struct {
+	ctx context.Context
+}
+
+func (ew *eventWriter) Write(p []byte) (n int, err error) {
+	event.Print(ew.ctx, string(p), event.Tag{
+		Key: &event.Key{
+			Name: "operation",
+		},
+		Value: "generate",
+	})
+	return len(p), nil
+}
+
+// newProgressWriter returns an io.WriterCloser that can be used
+// to report progress on the "go generate" command based on the
+// client capabilities.
+func (s *Server) newProgressWriter(ctx context.Context, cancel func()) io.WriteCloser {
+	var wc interface {
+		io.WriteCloser
+		start()
+	}
+	// TODO(marwan-at-work): add $/progress notifications
+	wc = &messageWriter{cancel, ctx, s.client}
+	wc.start()
+	return wc
+}
+
+// messageWriter implements progressWriter
+// and only tells the user that "go generate"
+// has started through window/showMessage but does not
+// report anything afterwards. This is because each
+// log shows up as a separate window and therefore
+// would be obnoxious to show every incoming line.
+type messageWriter struct {
+	cancel func()
+	ctx    context.Context
+	client protocol.Client
+}
+
+func (lw *messageWriter) Write(p []byte) (n int, err error) {
+	return len(p), nil
+}
+
+func (lw *messageWriter) start() {
+	go func() {
+		msg, err := lw.client.ShowMessageRequest(lw.ctx, &protocol.ShowMessageRequestParams{
+			Type:    protocol.Log,
+			Message: "go generate has started, check logs for progress",
+			Actions: []protocol.MessageActionItem{{
+				Title: "Cancel",
+			}},
+		})
+		if err != nil {
+			event.Error(lw.ctx, "error sending initial generate msg", err)
+			return
+		}
+		if msg != nil && msg.Title == "Cancel" {
+			lw.cancel()
+		}
+	}()
+}
+
+func (lw *messageWriter) Close() error {
+	return lw.client.ShowMessage(lw.ctx, &protocol.ShowMessageParams{
+		Type:    protocol.Info,
+		Message: "go generate has finished",
+	})
+}
diff --git a/internal/lsp/server.go b/internal/lsp/server.go
index b8e997b..27b431a 100644
--- a/internal/lsp/server.go
+++ b/internal/lsp/server.go
@@ -101,11 +101,18 @@
 }
 
 func (s *Server) codeLens(ctx context.Context, params *protocol.CodeLensParams) ([]protocol.CodeLens, error) {
-	snapshot, fh, ok, err := s.beginFileRequest(params.TextDocument.URI, source.Mod)
+	snapshot, fh, ok, err := s.beginFileRequest(params.TextDocument.URI, source.UnknownKind)
 	if !ok {
 		return nil, err
 	}
-	return mod.CodeLens(ctx, snapshot, fh.Identity().URI)
+	switch fh.Identity().Kind {
+	case source.Mod:
+		return mod.CodeLens(ctx, snapshot, fh.Identity().URI)
+	case source.Go:
+		return source.CodeLens(ctx, snapshot, fh)
+	}
+	// Unsupported file kind for a code action.
+	return nil, nil
 }
 
 func (s *Server) nonstandardRequest(ctx context.Context, method string, params interface{}) (interface{}, error) {
@@ -154,6 +161,12 @@
 	return nil
 }
 
+func (s *Server) clearInProgress(token string) {
+	s.inProgressMu.Lock()
+	delete(s.inProgress, token)
+	s.inProgressMu.Unlock()
+}
+
 func notImplemented(method string) *jsonrpc2.Error {
 	return jsonrpc2.NewErrorf(jsonrpc2.CodeMethodNotFound, "method %q not yet implemented", method)
 }
diff --git a/internal/lsp/source/code_lens.go b/internal/lsp/source/code_lens.go
new file mode 100644
index 0000000..6bdda9e
--- /dev/null
+++ b/internal/lsp/source/code_lens.go
@@ -0,0 +1,55 @@
+// Copyright 2020 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 source
+
+import (
+	"context"
+	"go/token"
+	"path/filepath"
+	"strings"
+
+	"golang.org/x/tools/internal/lsp/protocol"
+)
+
+func CodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.CodeLens, error) {
+	f, _, m, _, err := snapshot.View().Session().Cache().ParseGoHandle(fh, ParseFull).Parse(ctx)
+	if err != nil {
+		return nil, err
+	}
+	const ggDirective = "//go:generate"
+	for _, c := range f.Comments {
+		for _, l := range c.List {
+			if !strings.HasPrefix(l.Text, ggDirective) {
+				continue
+			}
+			fset := snapshot.View().Session().Cache().FileSet()
+			rng, err := newMappedRange(fset, m, l.Pos(), l.Pos()+token.Pos(len(ggDirective))).Range()
+			if err != nil {
+				return nil, err
+			}
+			dir := filepath.Dir(fh.Identity().URI.Filename())
+			return []protocol.CodeLens{
+				{
+					Range: rng,
+					Command: protocol.Command{
+						Title:     "run go generate",
+						Command:   "generate",
+						Arguments: []interface{}{dir, false},
+					},
+				},
+				{
+					Range: rng,
+					Command: protocol.Command{
+						Title:     "run go generate ./...",
+						Command:   "generate",
+						Arguments: []interface{}{dir, true},
+					},
+				},
+			}, nil
+
+		}
+	}
+	return nil, nil
+}
diff --git a/internal/lsp/source/options.go b/internal/lsp/source/options.go
index b0c5b0a..5c1eb17 100644
--- a/internal/lsp/source/options.go
+++ b/internal/lsp/source/options.go
@@ -69,6 +69,7 @@
 			SupportedCommands: []string{
 				"tidy",               // for go.mod files
 				"upgrade.dependency", // for go.mod dependency upgrades
+				"generate",           // for "go generate" commands
 			},
 		},
 		UserOptions: UserOptions{
diff --git a/internal/lsp/source/source_test.go b/internal/lsp/source/source_test.go
index 46b3371..e7358ce 100644
--- a/internal/lsp/source/source_test.go
+++ b/internal/lsp/source/source_test.go
@@ -895,7 +895,17 @@
 }
 
 func (r *runner) CodeLens(t *testing.T, uri span.URI, want []protocol.CodeLens) {
-	// This is a pure LSP feature, no source level functionality to be tested.
+	fh, err := r.view.Snapshot().GetFile(uri)
+	if err != nil {
+		t.Fatal(err)
+	}
+	got, err := source.CodeLens(r.ctx, r.view.Snapshot(), fh)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if diff := tests.DiffCodeLens(uri, want, got); diff != "" {
+		t.Error(diff)
+	}
 }
 
 func spanToRange(data *tests.Data, spn span.Span) (*protocol.ColumnMapper, protocol.Range, error) {
diff --git a/internal/lsp/testdata/lsp/primarymod/generate/generate.go b/internal/lsp/testdata/lsp/primarymod/generate/generate.go
new file mode 100644
index 0000000..ae5e90d
--- /dev/null
+++ b/internal/lsp/testdata/lsp/primarymod/generate/generate.go
@@ -0,0 +1,4 @@
+package generate
+
+//go:generate echo Hi //@ codelens("//go:generate", "run go generate", "generate"), codelens("//go:generate", "run go generate ./...", "generate")
+//go:generate echo I shall have no CodeLens
diff --git a/internal/lsp/testdata/lsp/summary.txt.golden b/internal/lsp/testdata/lsp/summary.txt.golden
index c44224a..9dc1e2a 100644
--- a/internal/lsp/testdata/lsp/summary.txt.golden
+++ b/internal/lsp/testdata/lsp/summary.txt.golden
@@ -1,5 +1,5 @@
 -- summary --
-CodeLensCount = 0
+CodeLensCount = 2
 CompletionsCount = 237
 CompletionSnippetCount = 74
 UnimportedCompletionsCount = 11