1
0
mirror of https://github.com/golang/go synced 2024-11-18 08:54:45 -07:00

internal/lsp: add Regenerate Cgo code lens

Now that we support authoring cgo packages better, we need a way to
regenerate the C definitions. Doing it automatically is very difficult;
in particular, referencing a new symbol from the C package may require
regeneration, but we don't want to do that for every typo.

For now, give the user a button and make them push it. We attach a
code lens to the import "C" line. This is vulnerable to the usual
user-didn't-save glitches.

Updates golang/go#35721.

Change-Id: Iaa3540a9a12bbd8705e7f0e43ad0be1b22e87067
Reviewed-on: https://go-review.googlesource.com/c/tools/+/234103
Run-TryBot: Heschi Kreinick <heschi@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Robert Findley <rfindley@google.com>
This commit is contained in:
Heschi Kreinick 2020-05-14 14:02:48 -04:00
parent 444c5ef18e
commit d3bf790afa
11 changed files with 169 additions and 62 deletions

View File

@ -297,7 +297,11 @@ func (s *Session) DidModifyFiles(ctx context.Context, changes []source.FileModif
if err != nil {
return nil, err
}
forceReloadMetadata := false
for _, c := range changes {
if c.Action == source.InvalidateMetadata {
forceReloadMetadata = true
}
// Do nothing if the file is open in the editor and we receive
// an on-disk action. The editor is the source of truth.
if s.isOpen(c.URI) && c.OnDisk {
@ -330,7 +334,7 @@ func (s *Session) DidModifyFiles(ctx context.Context, changes []source.FileModif
}
var snapshots []source.Snapshot
for view, uris := range views {
snapshots = append(snapshots, view.invalidateContent(ctx, uris))
snapshots = append(snapshots, view.invalidateContent(ctx, uris, forceReloadMetadata))
}
return snapshots, nil
}
@ -348,8 +352,8 @@ func (s *Session) updateOverlays(ctx context.Context, changes []source.FileModif
defer s.overlayMu.Unlock()
for _, c := range changes {
// Don't update overlays for on-disk changes.
if c.OnDisk {
// Don't update overlays for on-disk changes or metadata invalidations.
if c.OnDisk || c.Action == source.InvalidateMetadata {
continue
}

View File

@ -664,7 +664,7 @@ func contains(views []*view, view *view) bool {
return false
}
func (s *snapshot) clone(ctx context.Context, withoutURIs map[span.URI]source.FileHandle) *snapshot {
func (s *snapshot) clone(ctx context.Context, withoutURIs map[span.URI]source.FileHandle, forceReloadMetadata bool) *snapshot {
s.mu.Lock()
defer s.mu.Unlock()
@ -714,7 +714,7 @@ func (s *snapshot) clone(ctx context.Context, withoutURIs map[span.URI]source.Fi
// Check if the file's package name or imports have changed,
// and if so, invalidate this file's packages' metadata.
invalidateMetadata := s.shouldInvalidateMetadata(ctx, originalFH, currentFH)
invalidateMetadata := forceReloadMetadata || s.shouldInvalidateMetadata(ctx, originalFH, currentFH)
// Invalidate the previous modTidyHandle if any of the files have been
// saved or if any of the metadata has been invalidated.

View File

@ -586,7 +586,7 @@ func (v *view) awaitInitialized(ctx context.Context) {
// invalidateContent invalidates the content of a Go file,
// including any position and type information that depends on it.
// It returns true if we were already tracking the given file, false otherwise.
func (v *view) invalidateContent(ctx context.Context, uris map[span.URI]source.FileHandle) source.Snapshot {
func (v *view) invalidateContent(ctx context.Context, uris map[span.URI]source.FileHandle, forceReloadMetadata bool) source.Snapshot {
// Detach the context so that content invalidation cannot be canceled.
ctx = xcontext.Detach(ctx)
@ -601,7 +601,7 @@ func (v *view) invalidateContent(ctx context.Context, uris map[span.URI]source.F
v.snapshotMu.Lock()
defer v.snapshotMu.Unlock()
v.snapshot = v.snapshot.clone(ctx, uris)
v.snapshot = v.snapshot.clone(ctx, uris, forceReloadMetadata)
return v.snapshot
}

View File

@ -49,6 +49,13 @@ func (s *Server) executeCommand(ctx context.Context, params *protocol.ExecuteCom
return nil, err
}
go s.runGenerate(xcontext.Detach(ctx), dir, recursive)
case source.CommandRegenerateCgo:
mod := source.FileModification{
URI: protocol.DocumentURI(params.Arguments[0].(string)).SpanURI(),
Action: source.InvalidateMetadata,
}
_, err := s.didModifyFiles(ctx, []source.FileModification{mod}, FromRegenerateCgo)
return nil, err
case source.CommandTidy:
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)

View File

@ -22,9 +22,9 @@ import (
type Editor struct {
Config EditorConfig
// server, client, and sandbox are concurrency safe and written only
// Server, client, and sandbox are concurrency safe and written only
// at construction time, so do not require synchronization.
server protocol.Server
Server protocol.Server
client *Client
sandbox *Sandbox
@ -81,7 +81,7 @@ func NewEditor(ws *Sandbox, config EditorConfig) *Editor {
// It returns the editor, so that it may be called as follows:
// editor, err := NewEditor(s).Connect(ctx, conn)
func (e *Editor) Connect(ctx context.Context, conn *jsonrpc2.Conn, hooks ClientHooks) (*Editor, error) {
e.server = protocol.ServerDispatcher(conn)
e.Server = protocol.ServerDispatcher(conn)
e.client = &Client{editor: e, hooks: hooks}
go conn.Run(ctx,
protocol.Handlers(
@ -96,8 +96,8 @@ func (e *Editor) Connect(ctx context.Context, conn *jsonrpc2.Conn, hooks ClientH
// Shutdown issues the 'shutdown' LSP notification.
func (e *Editor) Shutdown(ctx context.Context) error {
if e.server != nil {
if err := e.server.Shutdown(ctx); err != nil {
if e.Server != nil {
if err := e.Server.Shutdown(ctx); err != nil {
return fmt.Errorf("Shutdown: %w", err)
}
}
@ -106,10 +106,10 @@ func (e *Editor) Shutdown(ctx context.Context) error {
// Exit issues the 'exit' LSP notification.
func (e *Editor) Exit(ctx context.Context) error {
if e.server != nil {
if e.Server != nil {
// Not all LSP clients issue the exit RPC, but we do so here to ensure that
// we gracefully handle it on multi-session servers.
if err := e.server.Exit(ctx); err != nil {
if err := e.Server.Exit(ctx); err != nil {
return fmt.Errorf("Exit: %w", err)
}
}
@ -158,8 +158,8 @@ func (e *Editor) initialize(ctx context.Context) error {
params.Trace = "messages"
// TODO: support workspace folders.
if e.server != nil {
resp, err := e.server.Initialize(ctx, params)
if e.Server != nil {
resp, err := e.Server.Initialize(ctx, params)
if err != nil {
return fmt.Errorf("initialize: %w", err)
}
@ -167,7 +167,7 @@ func (e *Editor) initialize(ctx context.Context) error {
e.serverCapabilities = resp.Capabilities
e.mu.Unlock()
if err := e.server.Initialized(ctx, &protocol.InitializedParams{}); err != nil {
if err := e.Server.Initialized(ctx, &protocol.InitializedParams{}); err != nil {
return fmt.Errorf("initialized: %w", err)
}
}
@ -176,14 +176,14 @@ func (e *Editor) initialize(ctx context.Context) error {
}
func (e *Editor) onFileChanges(ctx context.Context, evts []FileEvent) {
if e.server == nil {
if e.Server == nil {
return
}
var lspevts []protocol.FileEvent
for _, evt := range evts {
lspevts = append(lspevts, evt.ProtocolEvent)
}
e.server.DidChangeWatchedFiles(ctx, &protocol.DidChangeWatchedFilesParams{
e.Server.DidChangeWatchedFiles(ctx, &protocol.DidChangeWatchedFilesParams{
Changes: lspevts,
})
}
@ -200,8 +200,8 @@ func (e *Editor) OpenFile(ctx context.Context, path string) error {
item := textDocumentItem(e.sandbox.Workdir, buf)
e.mu.Unlock()
if e.server != nil {
if err := e.server.DidOpen(ctx, &protocol.DidOpenTextDocumentParams{
if e.Server != nil {
if err := e.Server.DidOpen(ctx, &protocol.DidOpenTextDocumentParams{
TextDocument: item,
}); err != nil {
return fmt.Errorf("DidOpen: %w", err)
@ -242,8 +242,8 @@ func (e *Editor) CreateBuffer(ctx context.Context, path, content string) error {
item := textDocumentItem(e.sandbox.Workdir, buf)
e.mu.Unlock()
if e.server != nil {
if err := e.server.DidOpen(ctx, &protocol.DidOpenTextDocumentParams{
if e.Server != nil {
if err := e.Server.DidOpen(ctx, &protocol.DidOpenTextDocumentParams{
TextDocument: item,
}); err != nil {
return fmt.Errorf("DidOpen: %w", err)
@ -263,8 +263,8 @@ func (e *Editor) CloseBuffer(ctx context.Context, path string) error {
delete(e.buffers, path)
e.mu.Unlock()
if e.server != nil {
if err := e.server.DidClose(ctx, &protocol.DidCloseTextDocumentParams{
if e.Server != nil {
if err := e.Server.DidClose(ctx, &protocol.DidCloseTextDocumentParams{
TextDocument: e.textDocumentIdentifier(path),
}); err != nil {
return fmt.Errorf("DidClose: %w", err)
@ -307,8 +307,8 @@ func (e *Editor) SaveBufferWithoutActions(ctx context.Context, path string) erro
e.mu.Unlock()
docID := e.textDocumentIdentifier(buf.path)
if e.server != nil {
if err := e.server.WillSave(ctx, &protocol.WillSaveTextDocumentParams{
if e.Server != nil {
if err := e.Server.WillSave(ctx, &protocol.WillSaveTextDocumentParams{
TextDocument: docID,
Reason: protocol.Manual,
}); err != nil {
@ -318,7 +318,7 @@ func (e *Editor) SaveBufferWithoutActions(ctx context.Context, path string) erro
if err := e.sandbox.Workdir.WriteFile(ctx, path, content); err != nil {
return fmt.Errorf("writing %q: %w", path, err)
}
if e.server != nil {
if e.Server != nil {
params := &protocol.DidSaveTextDocumentParams{
TextDocument: protocol.VersionedTextDocumentIdentifier{
Version: float64(buf.version),
@ -328,7 +328,7 @@ func (e *Editor) SaveBufferWithoutActions(ctx context.Context, path string) erro
if includeText {
params.Text = &content
}
if err := e.server.DidSave(ctx, params); err != nil {
if err := e.Server.DidSave(ctx, params); err != nil {
return fmt.Errorf("DidSave: %w", err)
}
}
@ -496,8 +496,8 @@ func (e *Editor) editBufferLocked(ctx context.Context, path string, edits []Edit
},
ContentChanges: evts,
}
if e.server != nil {
if err := e.server.DidChange(ctx, params); err != nil {
if e.Server != nil {
if err := e.Server.DidChange(ctx, params); err != nil {
return fmt.Errorf("DidChange: %w", err)
}
}
@ -514,7 +514,7 @@ func (e *Editor) GoToDefinition(ctx context.Context, path string, pos Pos) (stri
params.TextDocument.URI = e.sandbox.Workdir.URI(path)
params.Position = pos.toProtocolPosition()
resp, err := e.server.Definition(ctx, params)
resp, err := e.Server.Definition(ctx, params)
if err != nil {
return "", Pos{}, fmt.Errorf("definition: %w", err)
}
@ -534,7 +534,7 @@ func (e *Editor) Symbol(ctx context.Context, query string) ([]SymbolInformation,
params := &protocol.WorkspaceSymbolParams{}
params.Query = query
resp, err := e.server.Symbol(ctx, params)
resp, err := e.Server.Symbol(ctx, params)
if err != nil {
return nil, fmt.Errorf("symbol: %w", err)
}
@ -572,7 +572,7 @@ func (e *Editor) ApplyQuickFixes(ctx context.Context, path string, diagnostics [
}
func (e *Editor) codeAction(ctx context.Context, path string, diagnostics []protocol.Diagnostic, only ...protocol.CodeActionKind) error {
if e.server == nil {
if e.Server == nil {
return nil
}
params := &protocol.CodeActionParams{}
@ -581,7 +581,7 @@ func (e *Editor) codeAction(ctx context.Context, path string, diagnostics []prot
if diagnostics != nil {
params.Context.Diagnostics = diagnostics
}
actions, err := e.server.CodeAction(ctx, params)
actions, err := e.Server.CodeAction(ctx, params)
if err != nil {
return fmt.Errorf("textDocument/codeAction: %w", err)
}
@ -623,7 +623,7 @@ func convertEdits(protocolEdits []protocol.TextEdit) []Edit {
// FormatBuffer gofmts a Go file.
func (e *Editor) FormatBuffer(ctx context.Context, path string) error {
if e.server == nil {
if e.Server == nil {
return nil
}
e.mu.Lock()
@ -631,7 +631,7 @@ func (e *Editor) FormatBuffer(ctx context.Context, path string) error {
e.mu.Unlock()
params := &protocol.DocumentFormattingParams{}
params.TextDocument.URI = e.sandbox.Workdir.URI(path)
resp, err := e.server.Formatting(ctx, params)
resp, err := e.Server.Formatting(ctx, params)
if err != nil {
return fmt.Errorf("textDocument/formatting: %w", err)
}
@ -662,7 +662,7 @@ func (e *Editor) checkBufferPosition(path string, pos Pos) error {
// change, so must be followed by a call to Workdir.CheckForFileChanges once
// the generate command has completed.
func (e *Editor) RunGenerate(ctx context.Context, dir string) error {
if e.server == nil {
if e.Server == nil {
return nil
}
absDir := e.sandbox.Workdir.filePath(dir)
@ -670,7 +670,7 @@ func (e *Editor) RunGenerate(ctx context.Context, dir string) error {
Command: "generate",
Arguments: []interface{}{absDir, false},
}
if _, err := e.server.ExecuteCommand(ctx, params); err != nil {
if _, err := e.Server.ExecuteCommand(ctx, params); err != nil {
return fmt.Errorf("running generate: %v", err)
}
// Unfortunately we can't simply poll the workdir for file changes here,
@ -682,7 +682,7 @@ func (e *Editor) RunGenerate(ctx context.Context, dir string) error {
// CodeLens execute a codelens request on the server.
func (e *Editor) CodeLens(ctx context.Context, path string) ([]protocol.CodeLens, error) {
if e.server == nil {
if e.Server == nil {
return nil, nil
}
e.mu.Lock()
@ -694,7 +694,7 @@ func (e *Editor) CodeLens(ctx context.Context, path string) ([]protocol.CodeLens
params := &protocol.CodeLensParams{
TextDocument: e.textDocumentIdentifier(path),
}
lens, err := e.server.CodeLens(ctx, params)
lens, err := e.Server.CodeLens(ctx, params)
if err != nil {
return nil, err
}

View File

@ -0,0 +1,60 @@
//+build go1.15,cgo
package regtest
import (
"runtime"
"testing"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
)
func TestRegenerateCgo(t *testing.T) {
// The android builders have a complex setup which causes this test to fail. See discussion on
// golang.org/cl/214943 for more details.
if runtime.GOOS == "android" {
t.Skip("android not supported")
}
const workspace = `
-- go.mod --
module example.com
-- cgo.go --
package x
/*
int fortythree() { return 42; }
*/
import "C"
func Foo() {
print(C.fortytwo())
}
`
runner.Run(t, workspace, func(t *testing.T, env *Env) {
// Open the file. We should have a nonexistant symbol.
env.OpenFile("cgo.go")
env.Await(env.DiagnosticAtRegexp("cgo.go", `C\.(fortytwo)`)) // could not determine kind of name for C.fortytwo
// Fix the C function name. We haven't regenerated cgo, so nothing should be fixed.
env.RegexpReplace("cgo.go", `int fortythree`, "int fortytwo")
env.SaveBuffer("cgo.go")
env.Await(env.DiagnosticAtRegexp("cgo.go", `C\.(fortytwo)`))
// Regenerate cgo, fixing the diagnostic.
lenses := env.CodeLens("cgo.go")
var lens protocol.CodeLens
for _, l := range lenses {
if l.Command.Command == source.CommandRegenerateCgo {
lens = l
}
}
if _, err := env.Editor.Server.ExecuteCommand(env.Ctx, &protocol.ExecuteCommandParams{
Command: lens.Command.Command,
Arguments: lens.Command.Arguments,
}); err != nil {
t.Fatal(err)
}
env.Await(EmptyDiagnostics("cgo.go"))
})
}

View File

@ -199,7 +199,7 @@ func (r *Runner) Run(t *testing.T, filedata string, test func(t *testing.T, e *E
ls.printBuffers(t.Name(), os.Stderr)
}
if err := env.Editor.Shutdown(ctx); err != nil {
panic(err)
t.Logf("Shutdown: %v", err)
}
}()
test(t, env)

View File

@ -16,6 +16,14 @@ import (
"golang.org/x/tools/internal/lsp/protocol"
)
type lensFunc func(context.Context, Snapshot, FileHandle, *ast.File, *protocol.ColumnMapper) ([]protocol.CodeLens, error)
var lensFuncs = map[string]lensFunc{
CommandGenerate: goGenerateCodeLens,
CommandTest: runTestCodeLens,
CommandRegenerateCgo: regenerateCgoLens,
}
// CodeLens computes code lens for Go source code.
func CodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.CodeLens, error) {
f, _, m, _, err := snapshot.View().Session().Cache().ParseGoHandle(fh, ParseFull).Parse(ctx)
@ -23,25 +31,19 @@ func CodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol
return nil, err
}
var codeLens []protocol.CodeLens
var result []protocol.CodeLens
for lens, lf := range lensFuncs {
if !snapshot.View().Options().EnabledCodeLens[lens] {
continue
}
added, err := lf(ctx, snapshot, fh, f, m)
if snapshot.View().Options().EnabledCodeLens[CommandGenerate] {
ggcl, err := goGenerateCodeLens(ctx, snapshot, fh, f, m)
if err != nil {
return nil, err
}
codeLens = append(codeLens, ggcl...)
result = append(result, added...)
}
if snapshot.View().Options().EnabledCodeLens[CommandTest] {
rtcl, err := runTestCodeLens(ctx, snapshot, fh, f, m)
if err != nil {
return nil, err
}
codeLens = append(codeLens, rtcl...)
}
return codeLens, nil
return result, nil
}
var testMatcher = regexp.MustCompile("^Test[^a-z]")
@ -159,3 +161,30 @@ func goGenerateCodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle, f
}
return nil, nil
}
func regenerateCgoLens(ctx context.Context, snapshot Snapshot, fh FileHandle, f *ast.File, m *protocol.ColumnMapper) ([]protocol.CodeLens, error) {
var c *ast.ImportSpec
for _, imp := range f.Imports {
if imp.Path.Value == `"C"` {
c = imp
}
}
if c == nil {
return nil, nil
}
fset := snapshot.View().Session().Cache().FileSet()
rng, err := newMappedRange(fset, m, c.Pos(), c.EndPos).Range()
if err != nil {
return nil, err
}
return []protocol.CodeLens{
{
Range: rng,
Command: protocol.Command{
Title: "regenerate cgo definitions",
Command: CommandRegenerateCgo,
Arguments: []interface{}{fh.Identity().URI},
},
},
}, nil
}

View File

@ -61,6 +61,7 @@ const (
CommandTidy = "tidy"
// CommandUpgradeDependency is a gopls command to upgrade a dependency.
CommandUpgradeDependency = "upgrade.dependency"
CommandRegenerateCgo = "regenerate_cgo"
)
// DefaultOptions is the options that are used for Gopls execution independent
@ -90,10 +91,11 @@ func DefaultOptions() Options {
Sum: {},
},
SupportedCommands: []string{
CommandTest, // for "go test" commands
CommandTidy, // for go.mod files
CommandUpgradeDependency, // for go.mod dependency upgrades
CommandGenerate, // for "go generate" commands
CommandTest,
CommandTidy,
CommandUpgradeDependency,
CommandGenerate,
CommandRegenerateCgo,
},
},
UserOptions: UserOptions{
@ -108,6 +110,7 @@ func DefaultOptions() Options {
EnabledCodeLens: map[string]bool{
CommandGenerate: true,
CommandUpgradeDependency: true,
CommandRegenerateCgo: true,
},
},
DebuggingOptions: DebuggingOptions{

View File

@ -220,13 +220,14 @@ type FileModification struct {
type FileAction int
const (
Open = FileAction(iota)
UnknownFileAction = FileAction(iota)
Open
Change
Close
Save
Create
Delete
UnknownFileAction
InvalidateMetadata
)
// Cache abstracts the core logic of dealing with the environment from the

View File

@ -30,6 +30,7 @@ const (
FromDidSave
// FromDidClose is a file modification caused by closing a file.
FromDidClose
FromRegenerateCgo
)
func (m ModificationSource) String() string {
@ -42,6 +43,8 @@ func (m ModificationSource) String() string {
return "files changed on disk"
case FromDidSave:
return "saved files"
case FromRegenerateCgo:
return "regenerate cgo"
default:
return "unknown file modification"
}