internal/lsp: add a command to generate the gopls.mod file

Wire up a command to generate a gopls.mod file for a multi-module
workspace. In the future, this can actually be used to manage the
workspace, but for now the file is just generated, not actually used.

For golang/go#32394

Change-Id: I8a53da8ac9337bde132c7d8ca8557467f368fc24
Reviewed-on: https://go-review.googlesource.com/c/tools/+/256042
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/cache/snapshot.go b/internal/lsp/cache/snapshot.go
index 517a7c4..e625030e 100644
--- a/internal/lsp/cache/snapshot.go
+++ b/internal/lsp/cache/snapshot.go
@@ -1374,7 +1374,7 @@
 	h := s.generation.Bind(key, func(ctx context.Context, arg memoize.Arg) interface{} {
 		s := arg.(*snapshot)
 		data := &workspaceModuleData{}
-		data.file, data.err = s.buildWorkspaceModule(ctx)
+		data.file, data.err = s.BuildWorkspaceModFile(ctx)
 		return data
 	})
 	wsModule = &workspaceModuleHandle{
@@ -1386,9 +1386,9 @@
 	return s.workspaceModuleHandle, nil
 }
 
-// buildWorkspaceModule generates a workspace module given the modules in the
+// BuildWorkspaceModFile generates a workspace module given the modules in the
 // the workspace.
-func (s *snapshot) buildWorkspaceModule(ctx context.Context) (*modfile.File, error) {
+func (s *snapshot) BuildWorkspaceModFile(ctx context.Context) (*modfile.File, error) {
 	file := &modfile.File{}
 	file.AddModuleStmt("gopls-workspace")
 
diff --git a/internal/lsp/cmd/cmd.go b/internal/lsp/cmd/cmd.go
index 02b5ca7..82432e1 100644
--- a/internal/lsp/cmd/cmd.go
+++ b/internal/lsp/cmd/cmd.go
@@ -192,6 +192,7 @@
 		&signature{app: app},
 		&suggestedFix{app: app},
 		&symbols{app: app},
+		&workspace{app: app},
 		&workspaceSymbol{app: app},
 	}
 }
diff --git a/internal/lsp/cmd/workspace.go b/internal/lsp/cmd/workspace.go
new file mode 100644
index 0000000..1c1151b
--- /dev/null
+++ b/internal/lsp/cmd/workspace.go
@@ -0,0 +1,90 @@
+// 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 cmd
+
+import (
+	"context"
+	"flag"
+	"fmt"
+
+	"golang.org/x/tools/internal/lsp/protocol"
+	"golang.org/x/tools/internal/lsp/source"
+	"golang.org/x/tools/internal/tool"
+)
+
+// workspace is a top-level command for working with the gopls workspace. This
+// is experimental and subject to change. The idea is that subcommands could be
+// used for manipulating the workspace mod file, rather than editing it
+// manually.
+type workspace struct {
+	app *Application
+}
+
+func (w *workspace) subCommands() []tool.Application {
+	return []tool.Application{
+		&generateWorkspaceMod{app: w.app},
+	}
+}
+
+func (w *workspace) Name() string  { return "workspace" }
+func (w *workspace) Usage() string { return "<subcommand> [args...]" }
+func (w *workspace) ShortHelp() string {
+	return "manage the gopls workspace (experimental: under development)"
+}
+
+func (w *workspace) DetailedHelp(f *flag.FlagSet) {
+	fmt.Fprint(f.Output(), "\nsubcommands:\n")
+	for _, c := range w.subCommands() {
+		fmt.Fprintf(f.Output(), "  %s: %s\n", c.Name(), c.ShortHelp())
+	}
+	f.PrintDefaults()
+}
+
+func (w *workspace) Run(ctx context.Context, args ...string) error {
+	if len(args) == 0 {
+		return tool.CommandLineErrorf("must provide subcommand to %q", w.Name())
+	}
+	command, args := args[0], args[1:]
+	for _, c := range w.subCommands() {
+		if c.Name() == command {
+			return tool.Run(ctx, c, args)
+		}
+	}
+	return tool.CommandLineErrorf("unknown command %v", command)
+}
+
+// generateWorkspaceMod (re)generates the gopls.mod file for the current
+// workspace.
+type generateWorkspaceMod struct {
+	app *Application
+}
+
+func (c *generateWorkspaceMod) Name() string  { return "generate" }
+func (c *generateWorkspaceMod) Usage() string { return "" }
+func (c *generateWorkspaceMod) ShortHelp() string {
+	return "generate a gopls.mod file for a workspace"
+}
+
+func (c *generateWorkspaceMod) DetailedHelp(f *flag.FlagSet) {
+	f.PrintDefaults()
+}
+
+func (c *generateWorkspaceMod) Run(ctx context.Context, args ...string) error {
+	origOptions := c.app.options
+	c.app.options = func(opts *source.Options) {
+		origOptions(opts)
+		opts.ExperimentalWorkspaceModule = true
+	}
+	conn, err := c.app.connect(ctx)
+	if err != nil {
+		return err
+	}
+	defer conn.terminate(ctx)
+	params := &protocol.ExecuteCommandParams{Command: source.CommandGenerateGoplsMod.Name}
+	if _, err := conn.ExecuteCommand(ctx, params); err != nil {
+		return fmt.Errorf("executing server command: %v", err)
+	}
+	return nil
+}
diff --git a/internal/lsp/command.go b/internal/lsp/command.go
index d4a8ba4..06ade14 100644
--- a/internal/lsp/command.go
+++ b/internal/lsp/command.go
@@ -10,8 +10,10 @@
 	"encoding/json"
 	"fmt"
 	"io"
+	"io/ioutil"
 	"log"
 	"path"
+	"path/filepath"
 
 	"golang.org/x/tools/internal/event"
 	"golang.org/x/tools/internal/lsp/protocol"
@@ -79,6 +81,9 @@
 	// matters for regtests, where having a continuous thread of work is
 	// convenient for assertions.
 	work := s.progress.start(ctx, title, "Running...", params.WorkDoneToken, cancel)
+	if command.Synchronous {
+		return nil, s.runCommand(ctx, work, command, params.Arguments)
+	}
 	go func() {
 		defer cancel()
 		err := s.runCommand(ctx, work, command, params.Arguments)
@@ -217,6 +222,39 @@
 		snapshot, release := sv.Snapshot(ctx)
 		defer release()
 		s.diagnoseSnapshot(snapshot)
+	case source.CommandGenerateGoplsMod:
+		var v source.View
+		if len(args) == 0 {
+			views := s.session.Views()
+			if len(views) != 1 {
+				return fmt.Errorf("cannot resolve view: have %d views", len(views))
+			}
+			v = views[0]
+		} else {
+			var uri protocol.DocumentURI
+			if err := source.UnmarshalArgs(args, &uri); err != nil {
+				return err
+			}
+			var err error
+			v, err = s.session.ViewOf(uri.SpanURI())
+			if err != nil {
+				return err
+			}
+		}
+		snapshot, release := v.Snapshot(ctx)
+		defer release()
+		modFile, err := snapshot.BuildWorkspaceModFile(ctx)
+		if err != nil {
+			return errors.Errorf("getting workspace mod file: %w", err)
+		}
+		content, err := modFile.Format()
+		if err != nil {
+			return errors.Errorf("formatting mod file: %w", err)
+		}
+		filename := filepath.Join(v.Folder().Filename(), "gopls.mod")
+		if err := ioutil.WriteFile(filename, content, 0644); err != nil {
+			return errors.Errorf("writing mod file: %w", err)
+		}
 	default:
 		return fmt.Errorf("unsupported command: %s", command.Name)
 	}
diff --git a/internal/lsp/source/command.go b/internal/lsp/source/command.go
index 2bc3c77..90cb8ee 100644
--- a/internal/lsp/source/command.go
+++ b/internal/lsp/source/command.go
@@ -22,6 +22,10 @@
 type Command struct {
 	Name, Title string
 
+	// Synchronous controls whether the command executes synchronously within the
+	// ExecuteCommand request (applying suggested fixes is always synchronous).
+	Synchronous bool
+
 	// appliesFn is an optional field to indicate whether or not a command can
 	// be applied to the given inputs. If it returns false, we should not
 	// suggest this command for these inputs.
@@ -55,6 +59,7 @@
 	CommandExtractVariable,
 	CommandExtractFunction,
 	CommandToggleDetails,
+	CommandGenerateGoplsMod,
 }
 
 var (
@@ -135,6 +140,13 @@
 			return ok
 		},
 	}
+
+	// CommandGenerateGoplsMod (re)generates the gopls.mod file.
+	CommandGenerateGoplsMod = &Command{
+		Name:        "generate_gopls_mod",
+		Title:       "Generate gopls.mod",
+		Synchronous: true,
+	}
 )
 
 // Applies reports whether the command c implements a suggested fix that is
diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go
index d534eb0..9e5abcf 100644
--- a/internal/lsp/source/view.go
+++ b/internal/lsp/source/view.go
@@ -94,6 +94,10 @@
 	// the given go.mod file.
 	ModTidy(ctx context.Context, fh FileHandle) (*TidiedModule, error)
 
+	// BuildWorkspaceModFile builds the contents of mod file to be used for
+	// multi-module workspace.
+	BuildWorkspaceModFile(ctx context.Context) (*modfile.File, error)
+
 	// BuiltinPackage returns information about the special builtin package.
 	BuiltinPackage(ctx context.Context) (*BuiltinPackage, error)