diff --git a/gopls/doc/commands.md b/gopls/doc/commands.md index 97e18527f7..b70b0afec8 100644 --- a/gopls/doc/commands.md +++ b/gopls/doc/commands.md @@ -142,6 +142,37 @@ Args: } ``` +### **List imports of a file and its package** +Identifier: `gopls.list_imports` + +Retrieve a list of imports in the given Go file, and the package it +belongs to. + +Args: + +``` +{ + // The file URI. + "URI": string, +} +``` + +Result: + +``` +{ + // Imports is a list of imports in the requested file. + "Imports": []{ + "Path": string, + "Name": string, + }, + // PackageImports is a list of all imports in the requested file's package. + "PackageImports": []{ + "Path": string, + }, +} +``` + ### **List known packages** Identifier: `gopls.list_known_packages` diff --git a/gopls/internal/regtest/misc/add_import_test.go b/gopls/internal/regtest/misc/add_import_test.go deleted file mode 100644 index 8eb96cf00b..0000000000 --- a/gopls/internal/regtest/misc/add_import_test.go +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2021 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 misc - -import ( - "testing" - - "golang.org/x/tools/internal/lsp/command" - "golang.org/x/tools/internal/lsp/protocol" - . "golang.org/x/tools/internal/lsp/regtest" - "golang.org/x/tools/internal/lsp/tests" -) - -func TestAddImport(t *testing.T) { - const before = `package main - -import "fmt" - -func main() { - fmt.Println("hello world") -} -` - - const want = `package main - -import ( - "bytes" - "fmt" -) - -func main() { - fmt.Println("hello world") -} -` - - Run(t, "", func(t *testing.T, env *Env) { - env.CreateBuffer("main.go", before) - cmd, err := command.NewAddImportCommand("Add Import", command.AddImportArgs{ - URI: protocol.URIFromSpanURI(env.Sandbox.Workdir.URI("main.go").SpanURI()), - ImportPath: "bytes", - }) - if err != nil { - t.Fatal(err) - } - _, err = env.Editor.ExecuteCommand(env.Ctx, &protocol.ExecuteCommandParams{ - Command: "gopls.add_import", - Arguments: cmd.Arguments, - }) - if err != nil { - t.Fatal(err) - } - got := env.Editor.BufferText("main.go") - if got != want { - t.Fatalf("gopls.add_import failed\n%s", tests.Diff(t, want, got)) - } - }) -} diff --git a/gopls/internal/regtest/misc/import_test.go b/gopls/internal/regtest/misc/import_test.go new file mode 100644 index 0000000000..d5b6bcf43f --- /dev/null +++ b/gopls/internal/regtest/misc/import_test.go @@ -0,0 +1,133 @@ +// Copyright 2021 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 misc + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "golang.org/x/tools/internal/lsp/command" + "golang.org/x/tools/internal/lsp/protocol" + . "golang.org/x/tools/internal/lsp/regtest" + "golang.org/x/tools/internal/lsp/tests" +) + +func TestAddImport(t *testing.T) { + const before = `package main + +import "fmt" + +func main() { + fmt.Println("hello world") +} +` + + const want = `package main + +import ( + "bytes" + "fmt" +) + +func main() { + fmt.Println("hello world") +} +` + + Run(t, "", func(t *testing.T, env *Env) { + env.CreateBuffer("main.go", before) + cmd, err := command.NewAddImportCommand("Add Import", command.AddImportArgs{ + URI: env.Sandbox.Workdir.URI("main.go"), + ImportPath: "bytes", + }) + if err != nil { + t.Fatal(err) + } + env.ExecuteCommand(&protocol.ExecuteCommandParams{ + Command: "gopls.add_import", + Arguments: cmd.Arguments, + }, nil) + got := env.Editor.BufferText("main.go") + if got != want { + t.Fatalf("gopls.add_import failed\n%s", tests.Diff(t, want, got)) + } + }) +} + +func TestListImports(t *testing.T) { + const files = ` +-- go.mod -- +module mod.com + +go 1.12 +-- foo.go -- +package foo +const C = 1 +-- import_strings_test.go -- +package foo +import ( + x "strings" + "testing" +) + +func TestFoo(t *testing.T) {} +-- import_testing_test.go -- +package foo + +import "testing" + +func TestFoo2(t *testing.T) {} +` + tests := []struct { + filename string + want command.ListImportsResult + }{ + { + filename: "import_strings_test.go", + want: command.ListImportsResult{ + Imports: []command.FileImport{ + {Name: "x", Path: "strings"}, + {Path: "testing"}, + }, + PackageImports: []command.PackageImport{ + {Path: "strings"}, + {Path: "testing"}, + }, + }, + }, + { + filename: "import_testing_test.go", + want: command.ListImportsResult{ + Imports: []command.FileImport{ + {Path: "testing"}, + }, + PackageImports: []command.PackageImport{ + {Path: "strings"}, + {Path: "testing"}, + }, + }, + }, + } + + Run(t, files, func(t *testing.T, env *Env) { + for _, tt := range tests { + cmd, err := command.NewListImportsCommand("List Imports", command.URIArg{ + URI: env.Sandbox.Workdir.URI(tt.filename), + }) + if err != nil { + t.Fatal(err) + } + var result command.ListImportsResult + env.ExecuteCommand(&protocol.ExecuteCommandParams{ + Command: command.ListImports.ID(), + Arguments: cmd.Arguments, + }, &result) + if diff := cmp.Diff(tt.want, result); diff != "" { + t.Errorf("unexpected list imports result for %q (-want +got):\n%s", tt.filename, diff) + } + } + + }) +} diff --git a/internal/lsp/command.go b/internal/lsp/command.go index 153af443b0..d8f9d2ce4f 100644 --- a/internal/lsp/command.go +++ b/internal/lsp/command.go @@ -13,9 +13,11 @@ import ( "io/ioutil" "os" "path/filepath" + "sort" "strings" "golang.org/x/mod/modfile" + "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/internal/lsp/command" @@ -681,6 +683,48 @@ func (c *commandHandler) ListKnownPackages(ctx context.Context, args command.URI }) return result, err } + +func (c *commandHandler) ListImports(ctx context.Context, args command.URIArg) (command.ListImportsResult, error) { + var result command.ListImportsResult + err := c.run(ctx, commandConfig{ + forURI: args.URI, + }, func(ctx context.Context, deps commandDeps) error { + pkg, err := deps.snapshot.PackageForFile(ctx, args.URI.SpanURI(), source.TypecheckWorkspace, source.NarrowestPackage) + if err != nil { + return err + } + pgf, err := pkg.File(args.URI.SpanURI()) + if err != nil { + return err + } + for _, group := range astutil.Imports(deps.snapshot.FileSet(), pgf.File) { + for _, imp := range group { + if imp.Path == nil { + continue + } + var name string + if imp.Name != nil { + name = imp.Name.Name + } + result.Imports = append(result.Imports, command.FileImport{ + Path: source.ImportPath(imp), + Name: name, + }) + } + } + for _, imp := range pkg.Imports() { + result.PackageImports = append(result.PackageImports, command.PackageImport{ + Path: imp.PkgPath(), // This might be the vendored path under GOPATH vendoring, in which case it's a bug. + }) + } + sort.Slice(result.PackageImports, func(i, j int) bool { + return result.PackageImports[i].Path < result.PackageImports[j].Path + }) + return nil + }) + return result, err +} + func (c *commandHandler) AddImport(ctx context.Context, args command.AddImportArgs) error { return c.run(ctx, commandConfig{ progress: "Adding import", diff --git a/internal/lsp/command/command_gen.go b/internal/lsp/command/command_gen.go index f872c7fc69..c814bfe58c 100644 --- a/internal/lsp/command/command_gen.go +++ b/internal/lsp/command/command_gen.go @@ -27,6 +27,7 @@ const ( Generate Command = "generate" GenerateGoplsMod Command = "generate_gopls_mod" GoGetPackage Command = "go_get_package" + ListImports Command = "list_imports" ListKnownPackages Command = "list_known_packages" RegenerateCgo Command = "regenerate_cgo" RemoveDependency Command = "remove_dependency" @@ -49,6 +50,7 @@ var Commands = []Command{ Generate, GenerateGoplsMod, GoGetPackage, + ListImports, ListKnownPackages, RegenerateCgo, RemoveDependency, @@ -112,6 +114,12 @@ func Dispatch(ctx context.Context, params *protocol.ExecuteCommandParams, s Inte return nil, err } return nil, s.GoGetPackage(ctx, a0) + case "gopls.list_imports": + var a0 URIArg + if err := UnmarshalArgs(params.Arguments, &a0); err != nil { + return nil, err + } + return s.ListImports(ctx, a0) case "gopls.list_known_packages": var a0 URIArg if err := UnmarshalArgs(params.Arguments, &a0); err != nil { @@ -280,6 +288,18 @@ func NewGoGetPackageCommand(title string, a0 GoGetPackageArgs) (protocol.Command }, nil } +func NewListImportsCommand(title string, a0 URIArg) (protocol.Command, error) { + args, err := MarshalArgs(a0) + if err != nil { + return protocol.Command{}, err + } + return protocol.Command{ + Title: title, + Command: "gopls.list_imports", + Arguments: args, + }, nil +} + func NewListKnownPackagesCommand(title string, a0 URIArg) (protocol.Command, error) { args, err := MarshalArgs(a0) if err != nil { diff --git a/internal/lsp/command/interface.go b/internal/lsp/command/interface.go index d80e675b2a..d5f520dd76 100644 --- a/internal/lsp/command/interface.go +++ b/internal/lsp/command/interface.go @@ -120,6 +120,12 @@ type Interface interface { // Retrieve a list of packages that are importable from the given URI. ListKnownPackages(context.Context, URIArg) (ListKnownPackagesResult, error) + // ListImports: List imports of a file and its package + // + // Retrieve a list of imports in the given Go file, and the package it + // belongs to. + ListImports(context.Context, URIArg) (ListImportsResult, error) + // AddImport: Add an import // // Ask the server to add an import path to a given Go file. The method will @@ -224,6 +230,26 @@ type ListKnownPackagesResult struct { Packages []string } +type ListImportsResult struct { + // Imports is a list of imports in the requested file. + Imports []FileImport + + // PackageImports is a list of all imports in the requested file's package. + PackageImports []PackageImport +} + +type FileImport struct { + // Path is the import path of the import. + Path string + // Name is the name of the import, e.g. `foo` in `import foo "strings"`. + Name string +} + +type PackageImport struct { + // Path is the import path of the import. + Path string +} + type WorkspaceMetadataArgs struct { } diff --git a/internal/lsp/source/api_json.go b/internal/lsp/source/api_json.go index cf5f97888e..4e8e9df14a 100755 --- a/internal/lsp/source/api_json.go +++ b/internal/lsp/source/api_json.go @@ -622,6 +622,13 @@ var GeneratedAPIJSON = &APIJSON{ Doc: "Runs `go get` to fetch a package.", ArgDoc: "{\n\t// Any document URI within the relevant module.\n\t\"URI\": string,\n\t// The package to go get.\n\t\"Pkg\": string,\n\t\"AddRequire\": bool,\n}", }, + { + Command: "gopls.list_imports", + Title: "List imports of a file and its package", + Doc: "Retrieve a list of imports in the given Go file, and the package it\nbelongs to.", + ArgDoc: "{\n\t// The file URI.\n\t\"URI\": string,\n}", + ResultDoc: "{\n\t// Imports is a list of imports in the requested file.\n\t\"Imports\": []{\n\t\t\"Path\": string,\n\t\t\"Name\": string,\n\t},\n\t// PackageImports is a list of all imports in the requested file's package.\n\t\"PackageImports\": []{\n\t\t\"Path\": string,\n\t},\n}", + }, { Command: "gopls.list_known_packages", Title: "List known packages",