diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index 517a7c4b12..e625030e5c 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -1374,7 +1374,7 @@ func (s *snapshot) getWorkspaceModuleHandle(ctx context.Context) (*workspaceModu 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 @@ func (s *snapshot) getWorkspaceModuleHandle(ctx context.Context) (*workspaceModu 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 02b5ca7da0..82432e1d0a 100644 --- a/internal/lsp/cmd/cmd.go +++ b/internal/lsp/cmd/cmd.go @@ -192,6 +192,7 @@ func (app *Application) featureCommands() []tool.Application { &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 0000000000..1c1151b2ad --- /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 " [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 d4a8ba43e8..06ade14e94 100644 --- a/internal/lsp/command.go +++ b/internal/lsp/command.go @@ -10,8 +10,10 @@ import ( "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 @@ func (s *Server) executeCommand(ctx context.Context, params *protocol.ExecuteCom // 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 @@ func (s *Server) runCommand(ctx context.Context, work *workDone, command *source 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 2bc3c77bab..90cb8ee301 100644 --- a/internal/lsp/source/command.go +++ b/internal/lsp/source/command.go @@ -22,6 +22,10 @@ import ( 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 @@ var Commands = []*Command{ CommandExtractVariable, CommandExtractFunction, CommandToggleDetails, + CommandGenerateGoplsMod, } var ( @@ -135,6 +140,13 @@ var ( 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 d534eb0f21..9e5abcfbae 100644 --- a/internal/lsp/source/view.go +++ b/internal/lsp/source/view.go @@ -94,6 +94,10 @@ type Snapshot interface { // 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)