mirror of
https://github.com/golang/go
synced 2024-11-18 16:44:43 -07:00
internal/lsp: support running go mod tidy
as a code action
This changes adds basic support for running `go mod tidy` as a code action when a user opens a go.mod file. When we have a command available like `go mod tidy -check`, we will be able to return edits as part of the codeAction. For now, we execute the command directly. This change also required a few modifications to our handling of file kinds so that we could distinguish between a Go file and a go.mod file. Change-Id: I343079b8886724b67f90a314e45639545a34f21e Reviewed-on: https://go-review.googlesource.com/c/tools/+/196322 Run-TryBot: Rebecca Stambler <rstambler@golang.org> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Ian Cottrell <iancottrell@google.com>
This commit is contained in:
parent
c426260dee
commit
1081e67f6b
2
internal/lsp/cache/builtin.go
vendored
2
internal/lsp/cache/builtin.go
vendored
@ -40,7 +40,7 @@ func (view *view) buildBuiltinPackage(ctx context.Context) error {
|
|||||||
pkg := pkgs[0]
|
pkg := pkgs[0]
|
||||||
files := make(map[string]*ast.File)
|
files := make(map[string]*ast.File)
|
||||||
for _, filename := range pkg.GoFiles {
|
for _, filename := range pkg.GoFiles {
|
||||||
fh := view.session.GetFile(span.FileURI(filename))
|
fh := view.session.GetFile(span.FileURI(filename), source.Go)
|
||||||
ph := view.session.cache.ParseGoHandle(fh, source.ParseFull)
|
ph := view.session.cache.ParseGoHandle(fh, source.ParseFull)
|
||||||
view.builtin.files = append(view.builtin.files, ph)
|
view.builtin.files = append(view.builtin.files, ph)
|
||||||
file, _, _, err := ph.Parse(ctx)
|
file, _, _, err := ph.Parse(ctx)
|
||||||
|
8
internal/lsp/cache/cache.go
vendored
8
internal/lsp/cache/cache.go
vendored
@ -54,8 +54,8 @@ type fileData struct {
|
|||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cache) GetFile(uri span.URI) source.FileHandle {
|
func (c *cache) GetFile(uri span.URI, kind source.FileKind) source.FileHandle {
|
||||||
underlying := c.fs.GetFile(uri)
|
underlying := c.fs.GetFile(uri, kind)
|
||||||
key := fileKey{
|
key := fileKey{
|
||||||
identity: underlying.Identity(),
|
identity: underlying.Identity(),
|
||||||
}
|
}
|
||||||
@ -96,10 +96,6 @@ func (h *fileHandle) Identity() source.FileIdentity {
|
|||||||
return h.underlying.Identity()
|
return h.underlying.Identity()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *fileHandle) Kind() source.FileKind {
|
|
||||||
return h.underlying.Kind()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *fileHandle) Read(ctx context.Context) ([]byte, string, error) {
|
func (h *fileHandle) Read(ctx context.Context) ([]byte, string, error) {
|
||||||
v := h.handle.Get(ctx)
|
v := h.handle.Get(ctx)
|
||||||
if v == nil {
|
if v == nil {
|
||||||
|
10
internal/lsp/cache/external.go
vendored
10
internal/lsp/cache/external.go
vendored
@ -27,7 +27,7 @@ type nativeFileHandle struct {
|
|||||||
identity source.FileIdentity
|
identity source.FileIdentity
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fs *nativeFileSystem) GetFile(uri span.URI) source.FileHandle {
|
func (fs *nativeFileSystem) GetFile(uri span.URI, kind source.FileKind) source.FileHandle {
|
||||||
version := "DOES NOT EXIST"
|
version := "DOES NOT EXIST"
|
||||||
if fi, err := os.Stat(uri.Filename()); err == nil {
|
if fi, err := os.Stat(uri.Filename()); err == nil {
|
||||||
version = fi.ModTime().String()
|
version = fi.ModTime().String()
|
||||||
@ -37,6 +37,7 @@ func (fs *nativeFileSystem) GetFile(uri span.URI) source.FileHandle {
|
|||||||
identity: source.FileIdentity{
|
identity: source.FileIdentity{
|
||||||
URI: uri,
|
URI: uri,
|
||||||
Version: version,
|
Version: version,
|
||||||
|
Kind: kind,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -49,17 +50,12 @@ func (h *nativeFileHandle) Identity() source.FileIdentity {
|
|||||||
return h.identity
|
return h.identity
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *nativeFileHandle) Kind() source.FileKind {
|
|
||||||
// TODO: How should we determine the file kind?
|
|
||||||
return source.Go
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *nativeFileHandle) Read(ctx context.Context) ([]byte, string, error) {
|
func (h *nativeFileHandle) Read(ctx context.Context) ([]byte, string, error) {
|
||||||
ctx, done := trace.StartSpan(ctx, "cache.nativeFileHandle.Read", telemetry.File.Of(h.identity.URI.Filename()))
|
ctx, done := trace.StartSpan(ctx, "cache.nativeFileHandle.Read", telemetry.File.Of(h.identity.URI.Filename()))
|
||||||
defer done()
|
defer done()
|
||||||
ioLimit <- struct{}{}
|
ioLimit <- struct{}{}
|
||||||
defer func() { <-ioLimit }()
|
defer func() { <-ioLimit }()
|
||||||
//TODO: this should fail if the version is not the same as the handle
|
// TODO: this should fail if the version is not the same as the handle
|
||||||
data, err := ioutil.ReadFile(h.identity.URI.Filename())
|
data, err := ioutil.ReadFile(h.identity.URI.Filename())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
|
2
internal/lsp/cache/file.go
vendored
2
internal/lsp/cache/file.go
vendored
@ -59,7 +59,7 @@ func (f *fileBase) Handle(ctx context.Context) source.FileHandle {
|
|||||||
defer f.handleMu.Unlock()
|
defer f.handleMu.Unlock()
|
||||||
|
|
||||||
if f.handle == nil {
|
if f.handle == nil {
|
||||||
f.handle = f.view.Session().GetFile(f.URI())
|
f.handle = f.view.session.GetFile(f.URI(), f.kind)
|
||||||
}
|
}
|
||||||
return f.handle
|
return f.handle
|
||||||
}
|
}
|
||||||
|
2
internal/lsp/cache/gofile.go
vendored
2
internal/lsp/cache/gofile.go
vendored
@ -101,7 +101,7 @@ func (v *view) reverseDeps(ctx context.Context, seen map[packageID]struct{}, res
|
|||||||
}
|
}
|
||||||
for _, uri := range m.files {
|
for _, uri := range m.files {
|
||||||
// Call unlocked version of getFile since we hold the lock on the view.
|
// Call unlocked version of getFile since we hold the lock on the view.
|
||||||
if f, err := v.getFile(ctx, uri); err == nil && v.session.IsOpen(uri) {
|
if f, err := v.getFile(ctx, uri, source.Go); err == nil && v.session.IsOpen(uri) {
|
||||||
results[f.(*goFile)] = struct{}{}
|
results[f.(*goFile)] = struct{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
2
internal/lsp/cache/load.go
vendored
2
internal/lsp/cache/load.go
vendored
@ -245,7 +245,7 @@ func (v *view) link(ctx context.Context, g *importGraph) error {
|
|||||||
m.files = append(m.files, span.FileURI(filename))
|
m.files = append(m.files, span.FileURI(filename))
|
||||||
|
|
||||||
// Call the unlocked version of getFile since we are holding the view's mutex.
|
// Call the unlocked version of getFile since we are holding the view's mutex.
|
||||||
f, err := v.getFile(ctx, span.FileURI(filename))
|
f, err := v.getFile(ctx, span.FileURI(filename), source.Go)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(ctx, "no file", err, telemetry.File.Of(filename))
|
log.Error(ctx, "no file", err, telemetry.File.Of(filename))
|
||||||
continue
|
continue
|
||||||
|
11
internal/lsp/cache/modfile.go
vendored
11
internal/lsp/cache/modfile.go
vendored
@ -4,22 +4,11 @@
|
|||||||
|
|
||||||
package cache
|
package cache
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"go/token"
|
|
||||||
|
|
||||||
errors "golang.org/x/xerrors"
|
|
||||||
)
|
|
||||||
|
|
||||||
// modFile holds all of the information we know about a mod file.
|
// modFile holds all of the information we know about a mod file.
|
||||||
type modFile struct {
|
type modFile struct {
|
||||||
fileBase
|
fileBase
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*modFile) GetToken(context.Context) (*token.File, error) {
|
|
||||||
return nil, errors.Errorf("GetToken: not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*modFile) setContent(content []byte) {}
|
func (*modFile) setContent(content []byte) {}
|
||||||
func (*modFile) filename() string { return "" }
|
func (*modFile) filename() string { return "" }
|
||||||
func (*modFile) isActive() bool { return false }
|
func (*modFile) isActive() bool { return false }
|
||||||
|
23
internal/lsp/cache/session.go
vendored
23
internal/lsp/cache/session.go
vendored
@ -193,7 +193,7 @@ func (s *session) removeView(ctx context.Context, view *view) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Propagate the language ID through to the view.
|
// TODO: Propagate the language ID through to the view.
|
||||||
func (s *session) DidOpen(ctx context.Context, uri span.URI, _ source.FileKind, text []byte) {
|
func (s *session) DidOpen(ctx context.Context, uri span.URI, kind source.FileKind, text []byte) {
|
||||||
ctx = telemetry.File.With(ctx, uri)
|
ctx = telemetry.File.With(ctx, uri)
|
||||||
|
|
||||||
// Files with _ prefixes are ignored.
|
// Files with _ prefixes are ignored.
|
||||||
@ -211,7 +211,7 @@ func (s *session) DidOpen(ctx context.Context, uri span.URI, _ source.FileKind,
|
|||||||
|
|
||||||
// Read the file on disk and compare it to the text provided.
|
// Read the file on disk and compare it to the text provided.
|
||||||
// If it is the same as on disk, we can avoid sending it as an overlay to go/packages.
|
// If it is the same as on disk, we can avoid sending it as an overlay to go/packages.
|
||||||
s.openOverlay(ctx, uri, text)
|
s.openOverlay(ctx, uri, kind, text)
|
||||||
|
|
||||||
// Mark the file as just opened so that we know to re-run packages.Load on it.
|
// Mark the file as just opened so that we know to re-run packages.Load on it.
|
||||||
// We do this because we may not be aware of all of the packages the file belongs to.
|
// We do this because we may not be aware of all of the packages the file belongs to.
|
||||||
@ -254,15 +254,15 @@ func (s *session) IsOpen(uri span.URI) bool {
|
|||||||
return open
|
return open
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *session) GetFile(uri span.URI) source.FileHandle {
|
func (s *session) GetFile(uri span.URI, kind source.FileKind) source.FileHandle {
|
||||||
if overlay := s.readOverlay(uri); overlay != nil {
|
if overlay := s.readOverlay(uri); overlay != nil {
|
||||||
return overlay
|
return overlay
|
||||||
}
|
}
|
||||||
// Fall back to the cache-level file system.
|
// Fall back to the cache-level file system.
|
||||||
return s.Cache().GetFile(uri)
|
return s.cache.GetFile(uri, kind)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *session) SetOverlay(uri span.URI, data []byte) bool {
|
func (s *session) SetOverlay(uri span.URI, kind source.FileKind, data []byte) bool {
|
||||||
s.overlayMu.Lock()
|
s.overlayMu.Lock()
|
||||||
defer func() {
|
defer func() {
|
||||||
s.overlayMu.Unlock()
|
s.overlayMu.Unlock()
|
||||||
@ -280,6 +280,7 @@ func (s *session) SetOverlay(uri span.URI, data []byte) bool {
|
|||||||
s.overlays[uri] = &overlay{
|
s.overlays[uri] = &overlay{
|
||||||
session: s,
|
session: s,
|
||||||
uri: uri,
|
uri: uri,
|
||||||
|
kind: kind,
|
||||||
data: data,
|
data: data,
|
||||||
hash: hashContents(data),
|
hash: hashContents(data),
|
||||||
unchanged: o == nil,
|
unchanged: o == nil,
|
||||||
@ -289,7 +290,7 @@ func (s *session) SetOverlay(uri span.URI, data []byte) bool {
|
|||||||
|
|
||||||
// openOverlay adds the file content to the overlay.
|
// openOverlay adds the file content to the overlay.
|
||||||
// It also checks if the provided content is equivalent to the file's content on disk.
|
// It also checks if the provided content is equivalent to the file's content on disk.
|
||||||
func (s *session) openOverlay(ctx context.Context, uri span.URI, data []byte) {
|
func (s *session) openOverlay(ctx context.Context, uri span.URI, kind source.FileKind, data []byte) {
|
||||||
s.overlayMu.Lock()
|
s.overlayMu.Lock()
|
||||||
defer func() {
|
defer func() {
|
||||||
s.overlayMu.Unlock()
|
s.overlayMu.Unlock()
|
||||||
@ -298,11 +299,12 @@ func (s *session) openOverlay(ctx context.Context, uri span.URI, data []byte) {
|
|||||||
s.overlays[uri] = &overlay{
|
s.overlays[uri] = &overlay{
|
||||||
session: s,
|
session: s,
|
||||||
uri: uri,
|
uri: uri,
|
||||||
|
kind: kind,
|
||||||
data: data,
|
data: data,
|
||||||
hash: hashContents(data),
|
hash: hashContents(data),
|
||||||
unchanged: true,
|
unchanged: true,
|
||||||
}
|
}
|
||||||
_, hash, err := s.cache.GetFile(uri).Read(ctx)
|
_, hash, err := s.cache.GetFile(uri, kind).Read(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(ctx, "failed to read", err, telemetry.File)
|
log.Error(ctx, "failed to read", err, telemetry.File)
|
||||||
return
|
return
|
||||||
@ -356,14 +358,9 @@ func (o *overlay) Identity() source.FileIdentity {
|
|||||||
return source.FileIdentity{
|
return source.FileIdentity{
|
||||||
URI: o.uri,
|
URI: o.uri,
|
||||||
Version: o.hash,
|
Version: o.hash,
|
||||||
|
Kind: o.kind,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *overlay) Kind() source.FileKind {
|
|
||||||
// TODO: Determine the file kind using textDocument.languageId.
|
|
||||||
return source.Go
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *overlay) Read(ctx context.Context) ([]byte, string, error) {
|
func (o *overlay) Read(ctx context.Context) ([]byte, string, error) {
|
||||||
return o.data, o.hash, nil
|
return o.data, o.hash, nil
|
||||||
}
|
}
|
||||||
|
11
internal/lsp/cache/sumfile.go
vendored
11
internal/lsp/cache/sumfile.go
vendored
@ -4,22 +4,11 @@
|
|||||||
|
|
||||||
package cache
|
package cache
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"go/token"
|
|
||||||
|
|
||||||
errors "golang.org/x/xerrors"
|
|
||||||
)
|
|
||||||
|
|
||||||
// sumFile holds all of the information we know about a sum file.
|
// sumFile holds all of the information we know about a sum file.
|
||||||
type sumFile struct {
|
type sumFile struct {
|
||||||
fileBase
|
fileBase
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*sumFile) GetToken(context.Context) (*token.File, error) {
|
|
||||||
return nil, errors.Errorf("GetToken: not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*sumFile) setContent(content []byte) {}
|
func (*sumFile) setContent(content []byte) {}
|
||||||
func (*sumFile) filename() string { return "" }
|
func (*sumFile) filename() string { return "" }
|
||||||
func (*sumFile) isActive() bool { return false }
|
func (*sumFile) isActive() bool { return false }
|
||||||
|
31
internal/lsp/cache/view.go
vendored
31
internal/lsp/cache/view.go
vendored
@ -12,7 +12,6 @@ import (
|
|||||||
"go/types"
|
"go/types"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
@ -229,7 +228,7 @@ func (v *view) modFilesChanged() bool {
|
|||||||
// and modules included by a replace directive. Return true if
|
// and modules included by a replace directive. Return true if
|
||||||
// any of these file versions do not match.
|
// any of these file versions do not match.
|
||||||
for filename, version := range v.modFileVersions {
|
for filename, version := range v.modFileVersions {
|
||||||
if version != v.fileVersion(filename) {
|
if version != v.fileVersion(filename, source.Mod) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -248,14 +247,14 @@ func (v *view) storeModFileVersions() {
|
|||||||
// and modules included by a replace directive in the resolver.
|
// and modules included by a replace directive in the resolver.
|
||||||
for _, mod := range r.ModsByModPath {
|
for _, mod := range r.ModsByModPath {
|
||||||
if (mod.Main || mod.Replace != nil) && mod.GoMod != "" {
|
if (mod.Main || mod.Replace != nil) && mod.GoMod != "" {
|
||||||
v.modFileVersions[mod.GoMod] = v.fileVersion(mod.GoMod)
|
v.modFileVersions[mod.GoMod] = v.fileVersion(mod.GoMod, source.Mod)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *view) fileVersion(filename string) string {
|
func (v *view) fileVersion(filename string, kind source.FileKind) string {
|
||||||
uri := span.FileURI(filename)
|
uri := span.FileURI(filename)
|
||||||
f := v.session.GetFile(uri)
|
f := v.session.GetFile(uri, kind)
|
||||||
return f.Identity().Version
|
return f.Identity().Version
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -315,7 +314,8 @@ func (v *view) SetContent(ctx context.Context, uri span.URI, content []byte) (bo
|
|||||||
v.backgroundCtx, v.cancel = context.WithCancel(v.baseCtx)
|
v.backgroundCtx, v.cancel = context.WithCancel(v.baseCtx)
|
||||||
|
|
||||||
if !v.Ignore(uri) {
|
if !v.Ignore(uri) {
|
||||||
return v.session.SetOverlay(uri, content), nil
|
kind := source.DetectLanguage("", uri.Filename())
|
||||||
|
return v.session.SetOverlay(uri, kind, content), nil
|
||||||
}
|
}
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
@ -425,32 +425,33 @@ func (v *view) GetFile(ctx context.Context, uri span.URI) (source.File, error) {
|
|||||||
v.mu.Lock()
|
v.mu.Lock()
|
||||||
defer v.mu.Unlock()
|
defer v.mu.Unlock()
|
||||||
|
|
||||||
return v.getFile(ctx, uri)
|
// TODO(rstambler): Should there be a version that provides a kind explicitly?
|
||||||
|
kind := source.DetectLanguage("", uri.Filename())
|
||||||
|
return v.getFile(ctx, uri, kind)
|
||||||
}
|
}
|
||||||
|
|
||||||
// getFile is the unlocked internal implementation of GetFile.
|
// getFile is the unlocked internal implementation of GetFile.
|
||||||
func (v *view) getFile(ctx context.Context, uri span.URI) (viewFile, error) {
|
func (v *view) getFile(ctx context.Context, uri span.URI, kind source.FileKind) (viewFile, error) {
|
||||||
if f, err := v.findFile(uri); err != nil {
|
if f, err := v.findFile(uri); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if f != nil {
|
} else if f != nil {
|
||||||
return f, nil
|
return f, nil
|
||||||
}
|
}
|
||||||
filename := uri.Filename()
|
|
||||||
var f viewFile
|
var f viewFile
|
||||||
switch ext := filepath.Ext(filename); ext {
|
switch kind {
|
||||||
case ".mod":
|
case source.Mod:
|
||||||
f = &modFile{
|
f = &modFile{
|
||||||
fileBase: fileBase{
|
fileBase: fileBase{
|
||||||
view: v,
|
view: v,
|
||||||
fname: filename,
|
fname: uri.Filename(),
|
||||||
kind: source.Mod,
|
kind: source.Mod,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
case ".sum":
|
case source.Sum:
|
||||||
f = &sumFile{
|
f = &sumFile{
|
||||||
fileBase: fileBase{
|
fileBase: fileBase{
|
||||||
view: v,
|
view: v,
|
||||||
fname: filename,
|
fname: uri.Filename(),
|
||||||
kind: source.Sum,
|
kind: source.Sum,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -459,7 +460,7 @@ func (v *view) getFile(ctx context.Context, uri span.URI) (viewFile, error) {
|
|||||||
f = &goFile{
|
f = &goFile{
|
||||||
fileBase: fileBase{
|
fileBase: fileBase{
|
||||||
view: v,
|
view: v,
|
||||||
fname: filename,
|
fname: uri.Filename(),
|
||||||
kind: source.Go,
|
kind: source.Go,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -19,24 +19,6 @@ import (
|
|||||||
errors "golang.org/x/xerrors"
|
errors "golang.org/x/xerrors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Server) getSupportedCodeActions() []protocol.CodeActionKind {
|
|
||||||
allCodeActionKinds := make(map[protocol.CodeActionKind]struct{})
|
|
||||||
for _, kinds := range s.session.Options().SupportedCodeActions {
|
|
||||||
for kind := range kinds {
|
|
||||||
allCodeActionKinds[kind] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var result []protocol.CodeActionKind
|
|
||||||
for kind := range allCodeActionKinds {
|
|
||||||
result = append(result, kind)
|
|
||||||
}
|
|
||||||
sort.Slice(result, func(i, j int) bool {
|
|
||||||
return result[i] < result[j]
|
|
||||||
})
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionParams) ([]protocol.CodeAction, error) {
|
func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionParams) ([]protocol.CodeAction, error) {
|
||||||
uri := span.NewURI(params.TextDocument.URI)
|
uri := span.NewURI(params.TextDocument.URI)
|
||||||
view := s.session.ViewOf(uri)
|
view := s.session.ViewOf(uri)
|
||||||
@ -46,7 +28,7 @@ func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionPara
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Determine the supported actions for this file kind.
|
// Determine the supported actions for this file kind.
|
||||||
fileKind := f.Handle(ctx).Kind()
|
fileKind := f.Handle(ctx).Identity().Kind
|
||||||
supportedCodeActions, ok := view.Options().SupportedCodeActions[fileKind]
|
supportedCodeActions, ok := view.Options().SupportedCodeActions[fileKind]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("no supported code actions for %v file kind", fileKind)
|
return nil, fmt.Errorf("no supported code actions for %v file kind", fileKind)
|
||||||
@ -68,63 +50,95 @@ func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionPara
|
|||||||
}
|
}
|
||||||
|
|
||||||
var codeActions []protocol.CodeAction
|
var codeActions []protocol.CodeAction
|
||||||
|
switch fileKind {
|
||||||
edits, editsPerFix, err := source.AllImportsFixes(ctx, view, f)
|
case source.Mod:
|
||||||
if err != nil {
|
if !wanted[protocol.SourceOrganizeImports] {
|
||||||
return nil, err
|
return nil, nil
|
||||||
}
|
|
||||||
|
|
||||||
// If the user wants to see quickfixes.
|
|
||||||
if wanted[protocol.QuickFix] {
|
|
||||||
// First, add the quick fixes reported by go/analysis.
|
|
||||||
gof, ok := f.(source.GoFile)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("%s is not a Go file", f.URI())
|
|
||||||
}
|
}
|
||||||
qf, err := quickFixes(ctx, view, gof)
|
|
||||||
if err != nil {
|
|
||||||
log.Error(ctx, "quick fixes failed", err, telemetry.File.Of(uri))
|
|
||||||
}
|
|
||||||
codeActions = append(codeActions, qf...)
|
|
||||||
|
|
||||||
// If we also have diagnostics for missing imports, we can associate them with quick fixes.
|
|
||||||
if findImportErrors(params.Context.Diagnostics) {
|
|
||||||
// Separate this into a set of codeActions per diagnostic, where
|
|
||||||
// each action is the addition, removal, or renaming of one import.
|
|
||||||
for _, importFix := range editsPerFix {
|
|
||||||
// Get the diagnostics this fix would affect.
|
|
||||||
if fixDiagnostics := importDiagnostics(importFix.Fix, params.Context.Diagnostics); len(fixDiagnostics) > 0 {
|
|
||||||
codeActions = append(codeActions, protocol.CodeAction{
|
|
||||||
Title: importFixTitle(importFix.Fix),
|
|
||||||
Kind: protocol.QuickFix,
|
|
||||||
Edit: &protocol.WorkspaceEdit{
|
|
||||||
Changes: &map[string][]protocol.TextEdit{
|
|
||||||
string(uri): importFix.Edits,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Diagnostics: fixDiagnostics,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the results of import organization as source.OrganizeImports.
|
|
||||||
if wanted[protocol.SourceOrganizeImports] {
|
|
||||||
codeActions = append(codeActions, protocol.CodeAction{
|
codeActions = append(codeActions, protocol.CodeAction{
|
||||||
Title: "Organize Imports",
|
Title: "Tidy",
|
||||||
Kind: protocol.SourceOrganizeImports,
|
Kind: protocol.SourceOrganizeImports,
|
||||||
Edit: &protocol.WorkspaceEdit{
|
Command: &protocol.Command{
|
||||||
Changes: &map[string][]protocol.TextEdit{
|
Title: "Tidy",
|
||||||
string(uri): edits,
|
Command: "tidy",
|
||||||
|
Arguments: []interface{}{
|
||||||
|
f.URI(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
case source.Go:
|
||||||
|
gof, ok := f.(source.GoFile)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.Errorf("%s is not a Go file", f.URI())
|
||||||
|
}
|
||||||
|
edits, editsPerFix, err := source.AllImportsFixes(ctx, view, gof)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if wanted[protocol.QuickFix] {
|
||||||
|
// First, add the quick fixes reported by go/analysis.
|
||||||
|
qf, err := quickFixes(ctx, view, gof)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(ctx, "quick fixes failed", err, telemetry.File.Of(uri))
|
||||||
|
}
|
||||||
|
codeActions = append(codeActions, qf...)
|
||||||
|
|
||||||
|
// If we also have diagnostics for missing imports, we can associate them with quick fixes.
|
||||||
|
if findImportErrors(params.Context.Diagnostics) {
|
||||||
|
// Separate this into a set of codeActions per diagnostic, where
|
||||||
|
// each action is the addition, removal, or renaming of one import.
|
||||||
|
for _, importFix := range editsPerFix {
|
||||||
|
// Get the diagnostics this fix would affect.
|
||||||
|
if fixDiagnostics := importDiagnostics(importFix.Fix, params.Context.Diagnostics); len(fixDiagnostics) > 0 {
|
||||||
|
codeActions = append(codeActions, protocol.CodeAction{
|
||||||
|
Title: importFixTitle(importFix.Fix),
|
||||||
|
Kind: protocol.QuickFix,
|
||||||
|
Edit: &protocol.WorkspaceEdit{
|
||||||
|
Changes: &map[string][]protocol.TextEdit{
|
||||||
|
string(uri): importFix.Edits,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Diagnostics: fixDiagnostics,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if wanted[protocol.SourceOrganizeImports] {
|
||||||
|
codeActions = append(codeActions, protocol.CodeAction{
|
||||||
|
Title: "Organize Imports",
|
||||||
|
Kind: protocol.SourceOrganizeImports,
|
||||||
|
Edit: &protocol.WorkspaceEdit{
|
||||||
|
Changes: &map[string][]protocol.TextEdit{
|
||||||
|
string(uri): edits,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// Unsupported file kind for a code action.
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
return codeActions, nil
|
return codeActions, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) getSupportedCodeActions() []protocol.CodeActionKind {
|
||||||
|
allCodeActionKinds := make(map[protocol.CodeActionKind]struct{})
|
||||||
|
for _, kinds := range s.session.Options().SupportedCodeActions {
|
||||||
|
for kind := range kinds {
|
||||||
|
allCodeActionKinds[kind] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var result []protocol.CodeActionKind
|
||||||
|
for kind := range allCodeActionKinds {
|
||||||
|
result = append(result, kind)
|
||||||
|
}
|
||||||
|
sort.Slice(result, func(i, j int) bool {
|
||||||
|
return result[i] < result[j]
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
type protocolImportFix struct {
|
type protocolImportFix struct {
|
||||||
fix *imports.ImportFix
|
fix *imports.ImportFix
|
||||||
edits []protocol.TextEdit
|
edits []protocol.TextEdit
|
||||||
@ -189,9 +203,7 @@ func importDiagnostics(fix *imports.ImportFix, diagnostics []protocol.Diagnostic
|
|||||||
results = append(results, diagnostic)
|
results = append(results, diagnostic)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
|
34
internal/lsp/command.go
Normal file
34
internal/lsp/command.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package lsp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"golang.org/x/tools/internal/lsp/protocol"
|
||||||
|
"golang.org/x/tools/internal/lsp/source"
|
||||||
|
"golang.org/x/tools/internal/span"
|
||||||
|
errors "golang.org/x/xerrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Server) executeCommand(ctx context.Context, params *protocol.ExecuteCommandParams) (interface{}, error) {
|
||||||
|
switch params.Command {
|
||||||
|
case "tidy":
|
||||||
|
if len(params.Arguments) == 0 || len(params.Arguments) > 1 {
|
||||||
|
return nil, errors.Errorf("expected one file URI for call to `go mod tidy`, got %v", params.Arguments)
|
||||||
|
}
|
||||||
|
// Confirm that this action is being taken on a go.mod file.
|
||||||
|
uri := span.NewURI(params.Arguments[0].(string))
|
||||||
|
view := s.session.ViewOf(uri)
|
||||||
|
f, err := view.GetFile(ctx, uri)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, ok := f.(source.ModFile); !ok {
|
||||||
|
return nil, errors.Errorf("%s is not a mod file", uri)
|
||||||
|
}
|
||||||
|
// Run go.mod tidy on the view.
|
||||||
|
if err := source.ModTidy(ctx, view); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
@ -82,12 +82,15 @@ func (s *Server) initialize(ctx context.Context, params *protocol.ParamInitia) (
|
|||||||
DefinitionProvider: true,
|
DefinitionProvider: true,
|
||||||
DocumentFormattingProvider: true,
|
DocumentFormattingProvider: true,
|
||||||
DocumentSymbolProvider: true,
|
DocumentSymbolProvider: true,
|
||||||
FoldingRangeProvider: true,
|
ExecuteCommandProvider: &protocol.ExecuteCommandOptions{
|
||||||
HoverProvider: true,
|
Commands: options.SupportedCommands,
|
||||||
DocumentHighlightProvider: true,
|
},
|
||||||
DocumentLinkProvider: &protocol.DocumentLinkOptions{},
|
FoldingRangeProvider: true,
|
||||||
ReferencesProvider: true,
|
HoverProvider: true,
|
||||||
RenameProvider: renameOpts,
|
DocumentHighlightProvider: true,
|
||||||
|
DocumentLinkProvider: &protocol.DocumentLinkOptions{},
|
||||||
|
ReferencesProvider: true,
|
||||||
|
RenameProvider: renameOpts,
|
||||||
SignatureHelpProvider: &protocol.SignatureHelpOptions{
|
SignatureHelpProvider: &protocol.SignatureHelpOptions{
|
||||||
TriggerCharacters: []string{"(", ","},
|
TriggerCharacters: []string{"(", ","},
|
||||||
},
|
},
|
||||||
|
@ -67,7 +67,7 @@ func testLSP(t *testing.T, exporter packagestest.Exporter) {
|
|||||||
options.Env = data.Config.Env
|
options.Env = data.Config.Env
|
||||||
session.NewView(ctx, viewName, span.FileURI(data.Config.Dir), options)
|
session.NewView(ctx, viewName, span.FileURI(data.Config.Dir), options)
|
||||||
for filename, content := range data.Config.Overlay {
|
for filename, content := range data.Config.Overlay {
|
||||||
session.SetOverlay(span.FileURI(filename), content)
|
session.SetOverlay(span.FileURI(filename), source.DetectLanguage("", filename), content)
|
||||||
}
|
}
|
||||||
|
|
||||||
r := &runner{
|
r := &runner{
|
||||||
|
@ -124,8 +124,8 @@ func (s *Server) Symbol(context.Context, *protocol.WorkspaceSymbolParams) ([]pro
|
|||||||
return nil, notImplemented("Symbol")
|
return nil, notImplemented("Symbol")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) ExecuteCommand(context.Context, *protocol.ExecuteCommandParams) (interface{}, error) {
|
func (s *Server) ExecuteCommand(ctx context.Context, params *protocol.ExecuteCommandParams) (interface{}, error) {
|
||||||
return nil, notImplemented("ExecuteCommand")
|
return s.executeCommand(ctx, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Text Synchronization
|
// Text Synchronization
|
||||||
|
@ -9,7 +9,6 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"go/format"
|
"go/format"
|
||||||
"log"
|
|
||||||
|
|
||||||
"golang.org/x/tools/go/packages"
|
"golang.org/x/tools/go/packages"
|
||||||
"golang.org/x/tools/internal/imports"
|
"golang.org/x/tools/internal/imports"
|
||||||
@ -150,15 +149,11 @@ type ImportFix struct {
|
|||||||
// In addition to returning the result of applying all edits,
|
// In addition to returning the result of applying all edits,
|
||||||
// it returns a list of fixes that could be applied to the file, with the
|
// it returns a list of fixes that could be applied to the file, with the
|
||||||
// corresponding TextEdits that would be needed to apply that fix.
|
// corresponding TextEdits that would be needed to apply that fix.
|
||||||
func AllImportsFixes(ctx context.Context, view View, f File) (edits []protocol.TextEdit, editsPerFix []*ImportFix, err error) {
|
func AllImportsFixes(ctx context.Context, view View, f GoFile) (edits []protocol.TextEdit, editsPerFix []*ImportFix, err error) {
|
||||||
ctx, done := trace.StartSpan(ctx, "source.AllImportsFixes")
|
ctx, done := trace.StartSpan(ctx, "source.AllImportsFixes")
|
||||||
defer done()
|
defer done()
|
||||||
|
|
||||||
gof, ok := f.(GoFile)
|
cphs, err := f.CheckPackageHandles(ctx)
|
||||||
if !ok {
|
|
||||||
return nil, nil, errors.Errorf("no imports fixes for non-Go files: %v", err)
|
|
||||||
}
|
|
||||||
cphs, err := gof.CheckPackageHandles(ctx)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
@ -279,7 +274,6 @@ func hasParseErrors(pkg Package, uri span.URI) bool {
|
|||||||
func hasListErrors(errors []packages.Error) bool {
|
func hasListErrors(errors []packages.Error) bool {
|
||||||
for _, err := range errors {
|
for _, err := range errors {
|
||||||
if err.Kind == packages.ListError {
|
if err.Kind == packages.ListError {
|
||||||
log.Printf("LIST ERROR: %v", err)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,9 +26,14 @@ var (
|
|||||||
protocol.SourceOrganizeImports: true,
|
protocol.SourceOrganizeImports: true,
|
||||||
protocol.QuickFix: true,
|
protocol.QuickFix: true,
|
||||||
},
|
},
|
||||||
Mod: {},
|
Mod: {
|
||||||
|
protocol.SourceOrganizeImports: true,
|
||||||
|
},
|
||||||
Sum: {},
|
Sum: {},
|
||||||
},
|
},
|
||||||
|
SupportedCommands: []string{
|
||||||
|
"tidy", // for go.mod files
|
||||||
|
},
|
||||||
Completion: CompletionOptions{
|
Completion: CompletionOptions{
|
||||||
Documentation: true,
|
Documentation: true,
|
||||||
Deep: true,
|
Deep: true,
|
||||||
@ -39,7 +44,6 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Options struct {
|
type Options struct {
|
||||||
|
|
||||||
// Env is the current set of environment overrides on this view.
|
// Env is the current set of environment overrides on this view.
|
||||||
Env []string
|
Env []string
|
||||||
|
|
||||||
@ -59,6 +63,8 @@ type Options struct {
|
|||||||
|
|
||||||
SupportedCodeActions map[FileKind]map[protocol.CodeActionKind]bool
|
SupportedCodeActions map[FileKind]map[protocol.CodeActionKind]bool
|
||||||
|
|
||||||
|
SupportedCommands []string
|
||||||
|
|
||||||
// TODO: Remove the option once we are certain there are no issues here.
|
// TODO: Remove the option once we are certain there are no issues here.
|
||||||
TextDocumentSyncKind protocol.TextDocumentSyncKind
|
TextDocumentSyncKind protocol.TextDocumentSyncKind
|
||||||
|
|
||||||
|
@ -57,7 +57,7 @@ func testSource(t *testing.T, exporter packagestest.Exporter) {
|
|||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
}
|
}
|
||||||
for filename, content := range data.Config.Overlay {
|
for filename, content := range data.Config.Overlay {
|
||||||
session.SetOverlay(span.FileURI(filename), content)
|
session.SetOverlay(span.FileURI(filename), source.DetectLanguage("", filename), content)
|
||||||
}
|
}
|
||||||
tests.Run(t, r, data)
|
tests.Run(t, r, data)
|
||||||
}
|
}
|
||||||
|
19
internal/lsp/source/tidy.go
Normal file
19
internal/lsp/source/tidy.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package source
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ModTidy(ctx context.Context, view View) error {
|
||||||
|
cfg := view.Config(ctx)
|
||||||
|
|
||||||
|
// Running `go mod tidy` modifies the file on disk directly.
|
||||||
|
// Ideally, we should return modules that could possibly be removed
|
||||||
|
// and apply each action as an edit.
|
||||||
|
//
|
||||||
|
// TODO(rstambler): This will be possible when golang/go#27005 is resolved.
|
||||||
|
if _, err := invokeGo(ctx, view.Folder().Filename(), cfg.Env, "mod", "tidy"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
@ -22,10 +22,11 @@ import (
|
|||||||
type FileIdentity struct {
|
type FileIdentity struct {
|
||||||
URI span.URI
|
URI span.URI
|
||||||
Version string
|
Version string
|
||||||
|
Kind FileKind
|
||||||
}
|
}
|
||||||
|
|
||||||
func (identity FileIdentity) String() string {
|
func (identity FileIdentity) String() string {
|
||||||
return fmt.Sprintf("%s%s", identity.URI, identity.Version)
|
return fmt.Sprintf("%s%s%s", identity.URI, identity.Version, identity.Kind)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FileHandle represents a handle to a specific version of a single file from
|
// FileHandle represents a handle to a specific version of a single file from
|
||||||
@ -37,9 +38,6 @@ type FileHandle interface {
|
|||||||
// Identity returns the FileIdentity for the file.
|
// Identity returns the FileIdentity for the file.
|
||||||
Identity() FileIdentity
|
Identity() FileIdentity
|
||||||
|
|
||||||
// Kind returns the FileKind for the file.
|
|
||||||
Kind() FileKind
|
|
||||||
|
|
||||||
// Read reads the contents of a file and returns it along with its hash value.
|
// Read reads the contents of a file and returns it along with its hash value.
|
||||||
// If the file is not available, returns a nil slice and an error.
|
// If the file is not available, returns a nil slice and an error.
|
||||||
Read(ctx context.Context) ([]byte, string, error)
|
Read(ctx context.Context) ([]byte, string, error)
|
||||||
@ -48,7 +46,7 @@ type FileHandle interface {
|
|||||||
// FileSystem is the interface to something that provides file contents.
|
// FileSystem is the interface to something that provides file contents.
|
||||||
type FileSystem interface {
|
type FileSystem interface {
|
||||||
// GetFile returns a handle for the specified file.
|
// GetFile returns a handle for the specified file.
|
||||||
GetFile(uri span.URI) FileHandle
|
GetFile(uri span.URI, kind FileKind) FileHandle
|
||||||
}
|
}
|
||||||
|
|
||||||
// FileKind describes the kind of the file in question.
|
// FileKind describes the kind of the file in question.
|
||||||
@ -59,6 +57,7 @@ const (
|
|||||||
Go = FileKind(iota)
|
Go = FileKind(iota)
|
||||||
Mod
|
Mod
|
||||||
Sum
|
Sum
|
||||||
|
UnknownKind
|
||||||
)
|
)
|
||||||
|
|
||||||
// TokenHandle represents a handle to the *token.File for a file.
|
// TokenHandle represents a handle to the *token.File for a file.
|
||||||
@ -189,7 +188,7 @@ type Session interface {
|
|||||||
IsOpen(uri span.URI) bool
|
IsOpen(uri span.URI) bool
|
||||||
|
|
||||||
// Called to set the effective contents of a file from this session.
|
// Called to set the effective contents of a file from this session.
|
||||||
SetOverlay(uri span.URI, data []byte) (wasFirstChange bool)
|
SetOverlay(uri span.URI, kind FileKind, data []byte) (wasFirstChange bool)
|
||||||
|
|
||||||
// DidChangeOutOfBand is called when a file under the root folder
|
// DidChangeOutOfBand is called when a file under the root folder
|
||||||
// changes. The file is not necessarily open in the editor.
|
// changes. The file is not necessarily open in the editor.
|
||||||
|
@ -96,7 +96,7 @@ func fullChange(changes []protocol.TextDocumentContentChangeEvent) (string, bool
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) applyChanges(ctx context.Context, uri span.URI, changes []protocol.TextDocumentContentChangeEvent) (string, error) {
|
func (s *Server) applyChanges(ctx context.Context, uri span.URI, changes []protocol.TextDocumentContentChangeEvent) (string, error) {
|
||||||
content, _, err := s.session.GetFile(uri).Read(ctx)
|
content, _, err := s.session.GetFile(uri, source.UnknownKind).Read(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "file not found (%v)", err)
|
return "", jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "file not found (%v)", err)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user