internal/lsp: always show errors from running commands

Some failure modes for suggested fixes were not displaying a failure
message. Refactor so that all errors result in a message.

Fixes golang/go#41413

Change-Id: I939d707626c2bd93c19e28f2197f4f1c66c6947c
Reviewed-on: https://go-review.googlesource.com/c/tools/+/255128
Run-TryBot: Robert Findley <rfindley@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Trust: Robert Findley <rfindley@google.com>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/internal/lsp/command.go b/internal/lsp/command.go
index c53d4dc..d4a8ba4 100644
--- a/internal/lsp/command.go
+++ b/internal/lsp/command.go
@@ -42,6 +42,10 @@
 	if !match {
 		return nil, fmt.Errorf("%s is not a supported command", command.Name)
 	}
+	title := command.Title
+	if title == "" {
+		title = command.Name
+	}
 	// Some commands require that all files are saved to disk. If we detect
 	// unsaved files, warn the user instead of running the commands.
 	unsaved := false
@@ -55,50 +59,19 @@
 		switch params.Command {
 		case source.CommandTest.Name, source.CommandGenerate.Name, source.CommandToggleDetails.Name:
 			// TODO(PJW): for Toggle, not an error if it is being disabled
-			err := fmt.Errorf("cannot run command %s: unsaved files in the view", params.Command)
-			s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
-				Type:    protocol.Error,
-				Message: err.Error(),
-			})
+			err := errors.New("unsaved files in the view")
+			s.showCommandError(ctx, title, err)
 			return nil, err
 		}
 	}
 	// If the command has a suggested fix function available, use it and apply
 	// the edits to the workspace.
 	if command.IsSuggestedFix() {
-		var uri protocol.DocumentURI
-		var rng protocol.Range
-		if err := source.UnmarshalArgs(params.Arguments, &uri, &rng); err != nil {
-			return nil, err
-		}
-		snapshot, fh, ok, release, err := s.beginFileRequest(ctx, uri, source.Go)
-		defer release()
-		if !ok {
-			return nil, err
-		}
-		edits, err := command.SuggestedFix(ctx, snapshot, fh, rng)
+		err := s.runSuggestedFixCommand(ctx, command, params.Arguments)
 		if err != nil {
-			return nil, err
+			s.showCommandError(ctx, title, err)
 		}
-		r, err := s.client.ApplyEdit(ctx, &protocol.ApplyWorkspaceEditParams{
-			Edit: protocol.WorkspaceEdit{
-				DocumentChanges: edits,
-			},
-		})
-		if err != nil {
-			return nil, err
-		}
-		if !r.Applied {
-			return nil, s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
-				Type:    protocol.Error,
-				Message: fmt.Sprintf("%s failed: %v", params.Command, r.FailureReason),
-			})
-		}
-		return nil, nil
-	}
-	title := command.Title
-	if title == "" {
-		title = command.Name
+		return nil, err
 	}
 	ctx, cancel := context.WithCancel(xcontext.Detach(ctx))
 	// Start progress prior to spinning off a goroutine specifically so that
@@ -117,12 +90,7 @@
 			work.end(title + ": failed")
 			// Show a message when work completes with error, because the progress end
 			// message is typically dismissed immediately by LSP clients.
-			if err := s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
-				Type:    protocol.Error,
-				Message: fmt.Sprintf("%s: An error occurred: %v", title, err),
-			}); err != nil {
-				event.Error(ctx, title+": failed to show message", err)
-			}
+			s.showCommandError(ctx, title, err)
 		default:
 			work.end(command.Name + ": completed")
 		}
@@ -130,6 +98,46 @@
 	return nil, nil
 }
 
+func (s *Server) runSuggestedFixCommand(ctx context.Context, command *source.Command, args []json.RawMessage) error {
+	var uri protocol.DocumentURI
+	var rng protocol.Range
+	if err := source.UnmarshalArgs(args, &uri, &rng); err != nil {
+		return err
+	}
+	snapshot, fh, ok, release, err := s.beginFileRequest(ctx, uri, source.Go)
+	defer release()
+	if !ok {
+		return err
+	}
+	edits, err := command.SuggestedFix(ctx, snapshot, fh, rng)
+	if err != nil {
+		return err
+	}
+	r, err := s.client.ApplyEdit(ctx, &protocol.ApplyWorkspaceEditParams{
+		Edit: protocol.WorkspaceEdit{
+			DocumentChanges: edits,
+		},
+	})
+	if err != nil {
+		return err
+	}
+	if !r.Applied {
+		return errors.New(r.FailureReason)
+	}
+	return nil
+}
+
+func (s *Server) showCommandError(ctx context.Context, title string, err error) {
+	// Command error messages should not be cancelable.
+	ctx = xcontext.Detach(ctx)
+	if err := s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
+		Type:    protocol.Error,
+		Message: fmt.Sprintf("%s failed: %v", title, err),
+	}); err != nil {
+		event.Error(ctx, title+": failed to show message", err)
+	}
+}
+
 func (s *Server) runCommand(ctx context.Context, work *workDone, command *source.Command, args []json.RawMessage) error {
 	switch command {
 	case source.CommandTest: