x/tools/gopls: 'go generate' should use $/progress if available

This commit will make the 'go generate' CodeLens command check the client
capabilities to use $/progress to report the progress of the command or otherwise
it will fallback to showing the message box already in place. The $/progress will
give you a play by play update while the message box shows only the beginning and
the end. The gopls logs will have all the details in either case.

Note that the $/progress is an LSP 3.15+ feature and not yet supported in all clients.

Updates golang/go#37680

Change-Id: I5ba37448be8e388f728394795e1bb5f1d50cc30d
Reviewed-on: https://go-review.googlesource.com/c/tools/+/223739
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
Reviewed-by: Heschi Kreinick <heschi@google.com>
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
diff --git a/internal/lsp/generate.go b/internal/lsp/generate.go
index bddf9c2..491077e 100644
--- a/internal/lsp/generate.go
+++ b/internal/lsp/generate.go
@@ -28,7 +28,7 @@
 	defer s.clearInProgress(token)
 
 	er := &eventWriter{ctx: ctx}
-	wc := s.newProgressWriter(ctx, cancel)
+	wc := s.newProgressWriter(ctx, cancel, token)
 	defer wc.Close()
 	args := []string{"-x"}
 	if recursive {
@@ -71,13 +71,16 @@
 // 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 {
+func (s *Server) newProgressWriter(ctx context.Context, cancel func(), token string) io.WriteCloser {
 	var wc interface {
 		io.WriteCloser
 		start()
 	}
-	// TODO(marwan-at-work): add $/progress notifications
-	wc = &messageWriter{cancel, ctx, s.client}
+	if s.supportsWorkDoneProgress {
+		wc = &workDoneWriter{ctx, token, s.client}
+	} else {
+		wc = &messageWriter{ctx, cancel, s.client}
+	}
 	wc.start()
 	return wc
 }
@@ -88,9 +91,11 @@
 // report anything afterwards. This is because each
 // log shows up as a separate window and therefore
 // would be obnoxious to show every incoming line.
+// Request cancellation happens synchronously through
+// the ShowMessageRequest response.
 type messageWriter struct {
-	cancel func()
 	ctx    context.Context
+	cancel func()
 	client protocol.Client
 }
 
@@ -123,3 +128,59 @@
 		Message: "go generate has finished",
 	})
 }
+
+// workDoneWriter implements progressWriter
+// that will send $/progress notifications
+// to the client. Request cancellations
+// happens separately through the
+// window/workDoneProgress/cancel request
+// in which case the given context will be rendered
+// done.
+type workDoneWriter struct {
+	ctx    context.Context
+	token  string
+	client protocol.Client
+}
+
+func (wdw *workDoneWriter) Write(p []byte) (n int, err error) {
+	return len(p), wdw.client.Progress(wdw.ctx, &protocol.ProgressParams{
+		Token: wdw.token,
+		Value: &protocol.WorkDoneProgressReport{
+			Kind:        "report",
+			Cancellable: true,
+			Message:     string(p),
+		},
+	})
+}
+
+func (wdw *workDoneWriter) start() {
+	err := wdw.client.WorkDoneProgressCreate(wdw.ctx, &protocol.WorkDoneProgressCreateParams{
+		Token: wdw.token,
+	})
+	if err != nil {
+		event.Error(wdw.ctx, "generate progress create", err)
+		return
+	}
+	err = wdw.client.Progress(wdw.ctx, &protocol.ProgressParams{
+		Token: wdw.token,
+		Value: &protocol.WorkDoneProgressBegin{
+			Kind:        "begin",
+			Cancellable: true,
+			Message:     "running go generate",
+			Title:       "generate",
+		},
+	})
+	if err != nil {
+		event.Error(wdw.ctx, "generate progress begin", err)
+	}
+}
+
+func (wdw *workDoneWriter) Close() error {
+	return wdw.client.Progress(wdw.ctx, &protocol.ProgressParams{
+		Token: wdw.token,
+		Value: protocol.WorkDoneProgressEnd{
+			Kind:    "end",
+			Message: "finished",
+		},
+	})
+}