internal/lsp: apply go.sum fixes to all modules in multi-module module

When go.sum updates are needed in experimental workspace module mode, we
don't necessarily know which module needs the correction. As a fix,
apply all of these fixes to each module in the multi-module workspace.

The "add dependency" quick fix also seems to be broken, but I'll fix
that in a separate CL.

Fixes golang/go#44097

Change-Id: Id4a6013f2aceb6b902dff3118b027f6cb99eb3c1
Reviewed-on: https://go-review.googlesource.com/c/tools/+/289772
Trust: Rebecca Stambler <rstambler@golang.org>
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Heschi Kreinick <heschi@google.com>
diff --git a/gopls/doc/commands.md b/gopls/doc/commands.md
index 70153df..8c5a06d 100644
--- a/gopls/doc/commands.md
+++ b/gopls/doc/commands.md
@@ -194,7 +194,7 @@
 ```
 {
 	// The file URI.
-	"URI": string,
+	"URIs": []string,
 }
 ```
 
@@ -222,7 +222,7 @@
 ```
 {
 	// The file URI.
-	"URI": string,
+	"URIs": []string,
 }
 ```
 
diff --git a/gopls/doc/generate.go b/gopls/doc/generate.go
index 90014a8..d6bd1ab 100644
--- a/gopls/doc/generate.go
+++ b/gopls/doc/generate.go
@@ -410,6 +410,10 @@
 		return "{ ... }"
 	}
 	under := arg.Type.Underlying()
+	switch u := under.(type) {
+	case *types.Slice:
+		return fmt.Sprintf("[]%s", u.Elem().Underlying().String())
+	}
 	return types.TypeString(under, nil)
 }
 
diff --git a/gopls/internal/regtest/workspace/workspace_test.go b/gopls/internal/regtest/workspace/workspace_test.go
index 98eea27..f3f1a77 100644
--- a/gopls/internal/regtest/workspace/workspace_test.go
+++ b/gopls/internal/regtest/workspace/workspace_test.go
@@ -782,3 +782,61 @@
 		env.Await(env.DiagnosticAtRegexp("include/include.go", `exclude.(X)`))
 	})
 }
+
+// Confirm that a fix for a tidy module will correct all modules in the
+// workspace.
+func TestMultiModule_OneBrokenModule(t *testing.T) {
+	testenv.NeedsGo1Point(t, 15)
+
+	const mod = `
+-- a/go.mod --
+module a.com
+
+go 1.12
+-- a/main.go --
+package main
+-- b/go.mod --
+module b.com
+
+go 1.12
+
+require (
+	example.com v1.2.3
+)
+-- b/go.sum --
+-- b/main.go --
+package b
+
+import "example.com/blah"
+
+func main() {
+	blah.Hello()
+}
+`
+	WithOptions(
+		ProxyFiles(workspaceProxy),
+		Modes(Experimental),
+	).Run(t, mod, func(t *testing.T, env *Env) {
+		params := &protocol.PublishDiagnosticsParams{}
+		env.OpenFile("a/go.mod")
+		env.Await(
+			ReadDiagnostics("a/go.mod", params),
+		)
+		for _, d := range params.Diagnostics {
+			if d.Message != `go.sum is out of sync with go.mod. Please update it by applying the quick fix.` {
+				continue
+			}
+			actions, err := env.GetQuickFixes("a/go.mod", []protocol.Diagnostic{d})
+			if err != nil {
+				t.Fatal(err)
+			}
+			if len(actions) != 2 {
+				t.Fatalf("expected 2 code actions, got %v", len(actions))
+			}
+			env.ApplyQuickFixes("a/go.mod", []protocol.Diagnostic{d})
+		}
+		env.Await(
+			EmptyDiagnostics("a/go.mod"),
+		)
+	})
+}
diff --git a/gopls/internal/regtest/wrappers.go b/gopls/internal/regtest/wrappers.go
index fa9367e..12ac7f9 100644
--- a/gopls/internal/regtest/wrappers.go
+++ b/gopls/internal/regtest/wrappers.go
@@ -200,6 +200,12 @@
 	}
 }
 
+// GetQuickFixes returns the available quick fix code actions.
+func (e *Env) GetQuickFixes(path string, diagnostics []protocol.Diagnostic) ([]protocol.CodeAction, error) {
+	e.T.Helper()
+	return e.Editor.GetQuickFixes(e.Ctx, path, nil, diagnostics)
+}
+
 // Hover in the editor, calling t.Fatal on any error.
 func (e *Env) Hover(name string, pos fake.Pos) (*protocol.MarkupContent, fake.Pos) {
 	e.T.Helper()
diff --git a/internal/lsp/cache/mod.go b/internal/lsp/cache/mod.go
index 99f9a22..9112bfd 100644
--- a/internal/lsp/cache/mod.go
+++ b/internal/lsp/cache/mod.go
@@ -214,17 +214,23 @@
 
 // extractGoCommandError tries to parse errors that come from the go command
 // and shape them into go.mod diagnostics.
-func (s *snapshot) extractGoCommandErrors(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, goCmdError string) []*source.Diagnostic {
+func (s *snapshot) extractGoCommandErrors(ctx context.Context, snapshot source.Snapshot, goCmdError string) []*source.Diagnostic {
 	var srcErrs []*source.Diagnostic
-	if srcErr := s.parseModError(ctx, fh, goCmdError); srcErr != nil {
-		srcErrs = append(srcErrs, srcErr)
+	if srcErr := s.parseModError(ctx, goCmdError); srcErr != nil {
+		srcErrs = append(srcErrs, srcErr...)
 	}
-	// If the error message contains a position, use that. Don't pass a file
-	// handle in, as it might not be the file associated with the error.
 	if srcErr := extractErrorWithPosition(ctx, goCmdError, s); srcErr != nil {
 		srcErrs = append(srcErrs, srcErr)
-	} else if srcErr := s.matchErrorToModule(ctx, fh, goCmdError); srcErr != nil {
-		srcErrs = append(srcErrs, srcErr)
+	} else {
+		for _, uri := range s.ModFiles() {
+			fh, err := s.GetFile(ctx, uri)
+			if err != nil {
+				continue
+			}
+			if srcErr := s.matchErrorToModule(ctx, fh, goCmdError); srcErr != nil {
+				srcErrs = append(srcErrs, srcErr)
+			}
+		}
 	}
 	return srcErrs
 }
diff --git a/internal/lsp/cache/mod_tidy.go b/internal/lsp/cache/mod_tidy.go
index e8a8547..1b7ef52 100644
--- a/internal/lsp/cache/mod_tidy.go
+++ b/internal/lsp/cache/mod_tidy.go
@@ -151,8 +151,7 @@
 
 	return mth.tidy(ctx, s)
 }
-
-func (s *snapshot) parseModError(ctx context.Context, fh source.FileHandle, errText string) *source.Diagnostic {
+func (s *snapshot) parseModError(ctx context.Context, errText string) []*source.Diagnostic {
 	// Match on common error messages. This is really hacky, but I'm not sure
 	// of any better way. This can be removed when golang/go#39164 is resolved.
 	isInconsistentVendor := strings.Contains(errText, "inconsistent vendoring")
@@ -162,58 +161,78 @@
 		return nil
 	}
 
-	pmf, err := s.ParseMod(ctx, fh)
-	if err != nil {
-		return nil
-	}
-	if pmf.File.Module == nil || pmf.File.Module.Syntax == nil {
-		return nil
-	}
-	rng, err := rangeFromPositions(pmf.Mapper, pmf.File.Module.Syntax.Start, pmf.File.Module.Syntax.End)
-	if err != nil {
-		return nil
-	}
-	// TODO(rFindley): we shouldn't really need to call three constructors here.
-	//                 Reconsider this.
-	args := command.URIArg{protocol.URIFromSpanURI(fh.URI())}
 	switch {
 	case isInconsistentVendor:
+		uri := s.ModFiles()[0]
+		args := command.URIArg{URI: protocol.URIFromSpanURI(uri)}
+		rng, err := s.uriToModDecl(ctx, uri)
+		if err != nil {
+			return nil
+		}
 		cmd, err := command.NewVendorCommand("Run go mod vendor", args)
 		if err != nil {
 			return nil
 		}
-		return &source.Diagnostic{
-			URI:      fh.URI(),
+		return []*source.Diagnostic{{
+			URI:      uri,
 			Range:    rng,
 			Severity: protocol.SeverityError,
 			Source:   source.ListError,
 			Message: `Inconsistent vendoring detected. Please re-run "go mod vendor".
 See https://github.com/golang/go/issues/39164 for more detail on this issue.`,
 			SuggestedFixes: []source.SuggestedFix{source.SuggestedFixFromCommand(cmd)},
-		}
+		}}
 
 	case isGoSumUpdates:
-		tidyCmd, err := command.NewTidyCommand("Run go mod tidy", args)
-		if err != nil {
-			return nil
+		var args []protocol.DocumentURI
+		for _, uri := range s.ModFiles() {
+			args = append(args, protocol.URIFromSpanURI(uri))
 		}
-		updateCmd, err := command.NewUpdateGoSumCommand("Update go.sum", args)
-		if err != nil {
-			return nil
+		var diagnostics []*source.Diagnostic
+		for _, uri := range s.ModFiles() {
+			rng, err := s.uriToModDecl(ctx, uri)
+			if err != nil {
+				return nil
+			}
+			tidyCmd, err := command.NewTidyCommand("Run go mod tidy", command.URIArgs{URIs: args})
+			if err != nil {
+				return nil
+			}
+			updateCmd, err := command.NewUpdateGoSumCommand("Update go.sum", command.URIArgs{URIs: args})
+			if err != nil {
+				return nil
+			}
+			diagnostics = append(diagnostics, &source.Diagnostic{
+				URI:      uri,
+				Range:    rng,
+				Severity: protocol.SeverityError,
+				Source:   source.ListError,
+				Message:  `go.sum is out of sync with go.mod. Please update it by applying the quick fix.`,
+				SuggestedFixes: []source.SuggestedFix{
+					source.SuggestedFixFromCommand(tidyCmd),
+					source.SuggestedFixFromCommand(updateCmd),
+				},
+			})
 		}
-		return &source.Diagnostic{
-			URI:      fh.URI(),
-			Range:    rng,
-			Severity: protocol.SeverityError,
-			Source:   source.ListError,
-			Message:  `go.sum is out of sync with go.mod. Please update it or run "go mod tidy".`,
-			SuggestedFixes: []source.SuggestedFix{
-				source.SuggestedFixFromCommand(tidyCmd),
-				source.SuggestedFixFromCommand(updateCmd),
-			},
-		}
+		return diagnostics
+	default:
+		return nil
 	}
-	return nil
+}
+
+func (s *snapshot) uriToModDecl(ctx context.Context, uri span.URI) (protocol.Range, error) {
+	fh, err := s.GetFile(ctx, uri)
+	if err != nil {
+		return protocol.Range{}, nil
+	}
+	pmf, err := s.ParseMod(ctx, fh)
+	if err != nil {
+		return protocol.Range{}, nil
+	}
+	if pmf.File.Module == nil || pmf.File.Module.Syntax == nil {
+		return protocol.Range{}, nil
+	}
+	return rangeFromPositions(pmf.Mapper, pmf.File.Module.Syntax.Start, pmf.File.Module.Syntax.End)
 }
 
 func hashImports(ctx context.Context, wsPackages []source.Package) (string, error) {
diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go
index 019a7e6..2c90492 100644
--- a/internal/lsp/cache/snapshot.go
+++ b/internal/lsp/cache/snapshot.go
@@ -1032,14 +1032,7 @@
 	}
 	criticalErr := &source.CriticalError{
 		MainError: loadErr,
-	}
-	// Attempt to place diagnostics in the relevant go.mod files, if any.
-	for _, uri := range s.ModFiles() {
-		fh, err := s.GetFile(ctx, uri)
-		if err != nil {
-			continue
-		}
-		criticalErr.DiagList = append(criticalErr.DiagList, s.extractGoCommandErrors(ctx, s, fh, loadErr.Error())...)
+		DiagList:  s.extractGoCommandErrors(ctx, s, loadErr.Error()),
 	}
 	return criticalErr
 }
diff --git a/internal/lsp/code_action.go b/internal/lsp/code_action.go
index 1ab91fb..6c41dc3 100644
--- a/internal/lsp/code_action.go
+++ b/internal/lsp/code_action.go
@@ -553,6 +553,7 @@
 	}
 	return quickFixes, nil
 }
+
 func sameDiagnostic(pd protocol.Diagnostic, sd *source.Diagnostic) bool {
 	return pd.Message == sd.Message && protocol.CompareRange(pd.Range, sd.Range) == 0 && pd.Source == string(sd.Source)
 }
diff --git a/internal/lsp/command.go b/internal/lsp/command.go
index 487e182..a959d1c 100644
--- a/internal/lsp/command.go
+++ b/internal/lsp/command.go
@@ -184,23 +184,41 @@
 
 // TODO(rFindley): UpdateGoSum, Tidy, and Vendor could probably all be one command.
 
-func (c *commandHandler) UpdateGoSum(ctx context.Context, args command.URIArg) error {
+func (c *commandHandler) UpdateGoSum(ctx context.Context, args command.URIArgs) error {
 	return c.run(ctx, commandConfig{
 		requireSave: true,
 		progress:    "Updating go.sum",
-		forURI:      args.URI,
 	}, func(ctx context.Context, deps commandDeps) error {
-		return runSimpleGoCommand(ctx, deps.snapshot, source.UpdateUserModFile|source.AllowNetwork, args.URI.SpanURI(), "list", []string{"all"})
+		for _, uri := range args.URIs {
+			snapshot, fh, ok, release, err := c.s.beginFileRequest(ctx, uri, source.UnknownKind)
+			defer release()
+			if !ok {
+				return err
+			}
+			if err := runSimpleGoCommand(ctx, snapshot, source.UpdateUserModFile|source.AllowNetwork, fh.URI(), "list", []string{"all"}); err != nil {
+				return err
+			}
+		}
+		return nil
 	})
 }
 
-func (c *commandHandler) Tidy(ctx context.Context, args command.URIArg) error {
+func (c *commandHandler) Tidy(ctx context.Context, args command.URIArgs) error {
 	return c.run(ctx, commandConfig{
 		requireSave: true,
 		progress:    "Running go mod tidy",
-		forURI:      args.URI,
 	}, func(ctx context.Context, deps commandDeps) error {
-		return runSimpleGoCommand(ctx, deps.snapshot, source.UpdateUserModFile|source.AllowNetwork, args.URI.SpanURI(), "mod", []string{"tidy"})
+		for _, uri := range args.URIs {
+			snapshot, fh, ok, release, err := c.s.beginFileRequest(ctx, uri, source.UnknownKind)
+			defer release()
+			if !ok {
+				return err
+			}
+			if err := runSimpleGoCommand(ctx, snapshot, source.UpdateUserModFile|source.AllowNetwork, fh.URI(), "mod", []string{"tidy"}); err != nil {
+				return err
+			}
+		}
+		return nil
 	})
 }
 
diff --git a/internal/lsp/command/command_gen.go b/internal/lsp/command/command_gen.go
index 7fda594..3e7280c 100644
--- a/internal/lsp/command/command_gen.go
+++ b/internal/lsp/command/command_gen.go
@@ -137,7 +137,7 @@
 		err := s.Test(ctx, a0, a1, a2)
 		return nil, err
 	case "gopls.tidy":
-		var a0 URIArg
+		var a0 URIArgs
 		if err := UnmarshalArgs(params.Arguments, &a0); err != nil {
 			return nil, err
 		}
@@ -151,7 +151,7 @@
 		err := s.ToggleGCDetails(ctx, a0)
 		return nil, err
 	case "gopls.update_go_sum":
-		var a0 URIArg
+		var a0 URIArgs
 		if err := UnmarshalArgs(params.Arguments, &a0); err != nil {
 			return nil, err
 		}
@@ -307,7 +307,7 @@
 	}, nil
 }
 
-func NewTidyCommand(title string, a0 URIArg) (protocol.Command, error) {
+func NewTidyCommand(title string, a0 URIArgs) (protocol.Command, error) {
 	args, err := MarshalArgs(a0)
 	if err != nil {
 		return protocol.Command{}, err
@@ -331,7 +331,7 @@
 	}, nil
 }
 
-func NewUpdateGoSumCommand(title string, a0 URIArg) (protocol.Command, error) {
+func NewUpdateGoSumCommand(title string, a0 URIArgs) (protocol.Command, error) {
 	args, err := MarshalArgs(a0)
 	if err != nil {
 		return protocol.Command{}, err
diff --git a/internal/lsp/command/interface.go b/internal/lsp/command/interface.go
index 9de4b32..3d258a8 100644
--- a/internal/lsp/command/interface.go
+++ b/internal/lsp/command/interface.go
@@ -61,7 +61,7 @@
 	// Tidy: Run go mod tidy
 	//
 	// Runs `go mod tidy` for a module.
-	Tidy(context.Context, URIArg) error
+	Tidy(context.Context, URIArgs) error
 
 	// Vendor: Run go mod vendor
 	//
@@ -71,7 +71,7 @@
 	// UpdateGoSum: Update go.sum
 	//
 	// Updates the go.sum file for a module.
-	UpdateGoSum(context.Context, URIArg) error
+	UpdateGoSum(context.Context, URIArgs) error
 
 	// CheckUpgrades: Check for upgrades
 	//
@@ -151,6 +151,11 @@
 	URI protocol.DocumentURI
 }
 
+type URIArgs struct {
+	// The file URI.
+	URIs []protocol.DocumentURI
+}
+
 type CheckUpgradesArgs struct {
 	// The go.mod file URI.
 	URI protocol.DocumentURI
diff --git a/internal/lsp/fake/editor.go b/internal/lsp/fake/editor.go
index ea77ffa..0764e6e 100644
--- a/internal/lsp/fake/editor.go
+++ b/internal/lsp/fake/editor.go
@@ -754,22 +754,15 @@
 	return e.codeAction(ctx, path, rng, diagnostics, protocol.QuickFix, protocol.SourceFixAll)
 }
 
+// GetQuickFixes returns the available quick fix code actions.
+func (e *Editor) GetQuickFixes(ctx context.Context, path string, rng *protocol.Range, diagnostics []protocol.Diagnostic) ([]protocol.CodeAction, error) {
+	return e.getCodeActions(ctx, path, rng, diagnostics, protocol.QuickFix, protocol.SourceFixAll)
+}
+
 func (e *Editor) codeAction(ctx context.Context, path string, rng *protocol.Range, diagnostics []protocol.Diagnostic, only ...protocol.CodeActionKind) error {
-	if e.Server == nil {
-		return nil
-	}
-	params := &protocol.CodeActionParams{}
-	params.TextDocument.URI = e.sandbox.Workdir.URI(path)
-	params.Context.Only = only
-	if diagnostics != nil {
-		params.Context.Diagnostics = diagnostics
-	}
-	if rng != nil {
-		params.Range = *rng
-	}
-	actions, err := e.Server.CodeAction(ctx, params)
+	actions, err := e.getCodeActions(ctx, path, rng, diagnostics, only...)
 	if err != nil {
-		return errors.Errorf("textDocument/codeAction: %w", err)
+		return err
 	}
 	for _, action := range actions {
 		if action.Title == "" {
@@ -814,6 +807,22 @@
 	return nil
 }
 
+func (e *Editor) getCodeActions(ctx context.Context, path string, rng *protocol.Range, diagnostics []protocol.Diagnostic, only ...protocol.CodeActionKind) ([]protocol.CodeAction, error) {
+	if e.Server == nil {
+		return nil, nil
+	}
+	params := &protocol.CodeActionParams{}
+	params.TextDocument.URI = e.sandbox.Workdir.URI(path)
+	params.Context.Only = only
+	if diagnostics != nil {
+		params.Context.Diagnostics = diagnostics
+	}
+	if rng != nil {
+		params.Range = *rng
+	}
+	return e.Server.CodeAction(ctx, params)
+}
+
 func (e *Editor) ExecuteCommand(ctx context.Context, params *protocol.ExecuteCommandParams) (interface{}, error) {
 	if e.Server == nil {
 		return nil, nil
diff --git a/internal/lsp/mod/code_lens.go b/internal/lsp/mod/code_lens.go
index f66a3e1..1598ed5 100644
--- a/internal/lsp/mod/code_lens.go
+++ b/internal/lsp/mod/code_lens.go
@@ -86,7 +86,7 @@
 		return nil, nil
 	}
 	uri := protocol.URIFromSpanURI(fh.URI())
-	cmd, err := command.NewTidyCommand("Run go mod tidy", command.URIArg{URI: uri})
+	cmd, err := command.NewTidyCommand("Run go mod tidy", command.URIArgs{URIs: []protocol.DocumentURI{uri}})
 	if err != nil {
 		return nil, err
 	}
diff --git a/internal/lsp/mod/diagnostics.go b/internal/lsp/mod/diagnostics.go
index 041bf72..e515ada 100644
--- a/internal/lsp/mod/diagnostics.go
+++ b/internal/lsp/mod/diagnostics.go
@@ -105,7 +105,12 @@
 		event.Error(ctx, "diagnosing go.mod", err)
 	}
 	if err == nil {
-		diagnostics = append(diagnostics, tidied.Diagnostics...)
+		for _, d := range tidied.Diagnostics {
+			if d.URI != fh.URI() {
+				continue
+			}
+			diagnostics = append(diagnostics, d)
+		}
 	}
 	return diagnostics, nil
 }
diff --git a/internal/lsp/source/api_json.go b/internal/lsp/source/api_json.go
index dc9ce6f..1942cf3 100755
--- a/internal/lsp/source/api_json.go
+++ b/internal/lsp/source/api_json.go
@@ -742,7 +742,7 @@
 			Command: "gopls.tidy",
 			Title:   "Run go mod tidy",
 			Doc:     "Runs `go mod tidy` for a module.",
-			ArgDoc:  "{\n\t// The file URI.\n\t\"URI\": string,\n}",
+			ArgDoc:  "{\n\t// The file URI.\n\t\"URIs\": []string,\n}",
 		},
 		{
 			Command: "gopls.toggle_gc_details",
@@ -754,7 +754,7 @@
 			Command: "gopls.update_go_sum",
 			Title:   "Update go.sum",
 			Doc:     "Updates the go.sum file for a module.",
-			ArgDoc:  "{\n\t// The file URI.\n\t\"URI\": string,\n}",
+			ArgDoc:  "{\n\t// The file URI.\n\t\"URIs\": []string,\n}",
 		},
 		{
 			Command: "gopls.upgrade_dependency",