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)