From ff30247f344b16c0bd8b84187cfa310f773bb7cd Mon Sep 17 00:00:00 2001 From: Kalman Bekesi Date: Tue, 29 Oct 2019 08:41:12 +0000 Subject: [PATCH] tools/gopls: add command line support for imports This adds support for calling import from the gopls command line, e.g. $ gopls imports -w ~/tmp/foo/main.go Optional arguments are: -w, which writes the changes back to the original file; and -d, which prints a unified diff to stdout With no arguments, the changed file is printed to stdout. Updates golang/go#32875 Change-Id: I12f980d977fe12c16e51b024c9dd28c33ba6c002 GitHub-Last-Rev: c3fdd90e25204e7a12a94e9dfde389b7674e7e6d GitHub-Pull-Request: golang/tools#176 Reviewed-on: https://go-review.googlesource.com/c/tools/+/202624 Run-TryBot: Rebecca Stambler Reviewed-by: Rebecca Stambler --- internal/lsp/cmd/cmd.go | 1 + internal/lsp/cmd/imports.go | 97 ++++++++++++++++++++++++++++++++ internal/lsp/cmd/test/cmdtest.go | 4 -- internal/lsp/cmd/test/imports.go | 32 +++++++++++ 4 files changed, 130 insertions(+), 4 deletions(-) create mode 100644 internal/lsp/cmd/imports.go create mode 100644 internal/lsp/cmd/test/imports.go diff --git a/internal/lsp/cmd/cmd.go b/internal/lsp/cmd/cmd.go index 60b307d77c..d77cecfd35 100644 --- a/internal/lsp/cmd/cmd.go +++ b/internal/lsp/cmd/cmd.go @@ -143,6 +143,7 @@ func (app *Application) commands() []tool.Application { &bug{}, &check{app: app}, &format{app: app}, + &imports{app: app}, &query{app: app}, &references{app: app}, &rename{app: app}, diff --git a/internal/lsp/cmd/imports.go b/internal/lsp/cmd/imports.go new file mode 100644 index 0000000000..19531df41f --- /dev/null +++ b/internal/lsp/cmd/imports.go @@ -0,0 +1,97 @@ +// Copyright 2019 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" + "io/ioutil" + + "golang.org/x/tools/internal/lsp/diff" + "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/internal/span" + "golang.org/x/tools/internal/tool" + errors "golang.org/x/xerrors" +) + +// imports implements the import verb for gopls. +type imports struct { + Diff bool `flag:"d" help:"display diffs instead of rewriting files"` + Write bool `flag:"w" help:"write result to (source) file instead of stdout"` + + app *Application +} + +func (t *imports) Name() string { return "imports" } +func (t *imports) Usage() string { return "" } +func (t *imports) ShortHelp() string { return "updates import statements" } +func (t *imports) DetailedHelp(f *flag.FlagSet) { + fmt.Fprintf(f.Output(), ` +Example: update imports statements in a file: + +  $ gopls imports -w internal/lsp/cmd/check.go + +gopls imports flags are: +`) + f.PrintDefaults() +} + +// Run performs diagnostic checks on the file specified and either; +// - if -w is specified, updates the file in place; +// - if -d is specified, prints out unified diffs of the changes; or +// - otherwise, prints the new versions to stdout. +func (t *imports) Run(ctx context.Context, args ...string) error { + if len(args) != 1 { + return tool.CommandLineErrorf("imports expects 1 argument") + } + conn, err := t.app.connect(ctx) + if err != nil { + return err + } + defer conn.terminate(ctx) + + from := span.Parse(args[0]) + uri := from.URI() + file := conn.AddFile(ctx, uri) + if file.err != nil { + return file.err + } + actions, err := conn.CodeAction(ctx, &protocol.CodeActionParams{ + TextDocument: protocol.TextDocumentIdentifier{ + URI: protocol.NewURI(uri), + }, + }) + if err != nil { + return errors.Errorf("%v: %v", from, err) + } + var edits []protocol.TextEdit + for _, a := range actions { + if a.Title == "Organize Imports" { + edits = (*a.Edit.Changes)[string(uri)] + } + } + sedits, err := source.FromProtocolEdits(file.mapper, edits) + if err != nil { + return errors.Errorf("%v: %v", edits, err) + } + + newContent := diff.ApplyEdits(string(file.mapper.Content), sedits) + + filename := file.uri.Filename() + switch { + case t.Write: + if len(edits) > 0 { + ioutil.WriteFile(filename, []byte(newContent), 0644) + } + case t.Diff: + diffs := diff.ToUnified(filename+".orig", filename, string(file.mapper.Content), sedits) + fmt.Print(diffs) + default: + fmt.Print(string(newContent)) + } + return nil +} diff --git a/internal/lsp/cmd/test/cmdtest.go b/internal/lsp/cmd/test/cmdtest.go index 02579aef9c..708fb8efa7 100644 --- a/internal/lsp/cmd/test/cmdtest.go +++ b/internal/lsp/cmd/test/cmdtest.go @@ -90,10 +90,6 @@ func (r *runner) Link(t *testing.T, uri span.URI, wantLinks []tests.Link) { //TODO: add command line link tests when it works } -func (r *runner) Import(t *testing.T, spn span.Span) { - //TODO: add command line imports tests when it works -} - func (r *runner) SuggestedFix(t *testing.T, spn span.Span) { //TODO: add suggested fix tests when it works } diff --git a/internal/lsp/cmd/test/imports.go b/internal/lsp/cmd/test/imports.go new file mode 100644 index 0000000000..9432a6c6e0 --- /dev/null +++ b/internal/lsp/cmd/test/imports.go @@ -0,0 +1,32 @@ +// Copyright 2019 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 cmdtest + +import ( + "os/exec" + "testing" + + "golang.org/x/tools/internal/lsp/cmd" + "golang.org/x/tools/internal/span" + "golang.org/x/tools/internal/tool" +) + +func (r *runner) Import(t *testing.T, spn span.Span) { + uri := spn.URI() + filename := uri.Filename() + args := []string{"imports", filename} + app := cmd.New("gopls-test", r.data.Config.Dir, r.data.Exported.Config.Env, r.options) + got := CaptureStdOut(t, func() { + _ = tool.Run(r.ctx, app, args) + }) + want := string(r.data.Golden("goimports", filename, func() ([]byte, error) { + cmd := exec.Command("goimports", filename) + out, _ := cmd.Output() // ignore error, sometimes we have intentionally ungofmt-able files + return out, nil + })) + if want != got { + t.Errorf("imports failed for %s, expected:\n%v\ngot:\n%v", filename, want, got) + } +}