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>
This commit is contained in:
Rob Findley 2020-09-18 17:26:33 -04:00 committed by Robert Findley
parent 03f0cc1caa
commit 463111b698
6 changed files with 148 additions and 3 deletions

View File

@ -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")

View File

@ -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},
}
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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

View File

@ -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)