Skip to content

Commit 44dda2b

Browse files
committed
gopls/internal/server: don't pop up errors when resolving code actions
When codeAction/resolve fails to resolve edits, don't fail the resolve operation: instead, simply return the codeaction (with its Command set), and let the action fail when the client invokes the command. This may result in redundant work, but avoids pop-ups on clients that eagerly resolve the code actions (golang/go#75442). Fixes golang/go#75442 Change-Id: I33df382dbbe02cd33aefdebe6bcb9102dd3deb3c Reviewed-on: https://go-review.googlesource.com/c/tools/+/704736 LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Alan Donovan <adonovan@google.com>
1 parent 1ca4aa2 commit 44dda2b

File tree

5 files changed

+189
-47
lines changed

5 files changed

+189
-47
lines changed

gopls/internal/server/code_action.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,19 +243,32 @@ func (s *server) ResolveCodeAction(ctx context.Context, ca *protocol.CodeAction)
243243
return nil, err
244244
}
245245
}
246+
ca.Data = nil
246247
if cmd.Command != "" {
248+
// Try to resolve codeaction edits by running the command.
249+
//
250+
// Unfortunately, we can't simply report failure to execute the command, as
251+
// some clients (namely, VS Code with the Live Share extension) eagerly
252+
// resolve *all* code actions, and returning an error leads to spammy
253+
// popups. Instead, we simply set the command on the code action, and let
254+
// it fail if/when invoked (golang/go#75442).
255+
//
256+
// This is arguably a client-side bug, but we work around it as it causes
257+
// significant problems for our users.
258+
// https://github.com/microsoft/live-share/issues/5293
247259
params := &protocol.ExecuteCommandParams{
248260
Command: cmd.Command,
249261
Arguments: cmd.Arguments,
250262
}
251-
252263
handler := &commandHandler{
253264
s: s,
254265
params: params,
255266
}
256267
edit, err := command.Dispatch(ctx, params, handler)
257268
if err != nil {
258-
return nil, err
269+
// Resolving edits failed, so just resolve the command.
270+
ca.Command = &cmd
271+
return ca, nil
259272
}
260273
var ok bool
261274
if ca.Edit, ok = edit.(*protocol.WorkspaceEdit); !ok {

gopls/internal/test/integration/fake/editor.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1001,7 +1001,7 @@ func (e *Editor) ApplyCodeAction(ctx context.Context, action protocol.CodeAction
10011001
if err != nil {
10021002
return err
10031003
}
1004-
action.Edit = ca.Edit
1004+
action = *ca
10051005
}
10061006
}
10071007

gopls/internal/test/marker/doc.go

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -144,16 +144,23 @@ Here is the list of supported action markers:
144144
completion candidate produced at the given location with provided label
145145
results in the given golden state.
146146
147-
- codeaction(start location, kind string, end=location, edit=golden, result=golden, err=stringMatcher)
147+
- codeaction(start location, kind string, diag=regexp, end=location, action=golden, edit=golden, result=golden, err=stringMatcher)
148148
149149
Specifies a code action to request at the location, with given kind.
150-
151-
If end is set, the location is defined to be between start.Start and end.End.
152-
153-
Exactly one of edit, result, or err must be set. If edit is set, it is a
154-
golden reference to the edits resulting from the code action. If result is
155-
set, it is a golden reference to the full set of changed files resulting
156-
from the code action. If err is set, it is the code action error.
150+
If diag is set, the code action must be associated with the given
151+
diagnostic.
152+
If end is set, the location is defined to be between start.Start and
153+
end.End.
154+
155+
Exactly one of action, edit, result, or err must be set:
156+
If action is set, it is a golden reference to a JSON blob representing the
157+
resolved code action, which is not applied.
158+
If edit is set, it is a golden reference to the edits resulting from the
159+
code action.
160+
If result is set, it is a golden reference to the full set of changed files
161+
resulting from the code action.
162+
If err is set, it is expected to match the error resulting from applying
163+
the code action.
157164
158165
- codelens(location, title): specifies that a codelens is expected at the
159166
given location, with given title. Must be used in conjunction with

gopls/internal/test/marker/marker_test.go

Lines changed: 95 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -588,7 +588,7 @@ var valueMarkerFuncs = map[string]func(marker){
588588
// See doc.go for marker documentation.
589589
var actionMarkerFuncs = map[string]func(marker){
590590
"acceptcompletion": actionMarkerFunc(acceptCompletionMarker),
591-
"codeaction": actionMarkerFunc(codeActionMarker, "end", "result", "edit", "err"),
591+
"codeaction": actionMarkerFunc(codeActionMarker, "end", "diag", "action", "result", "edit", "err"),
592592
"codelenses": actionMarkerFunc(codeLensesMarker),
593593
"complete": actionMarkerFunc(completeMarker),
594594
"def": actionMarkerFunc(defMarker),
@@ -2168,7 +2168,7 @@ func changedFiles(env *integration.Env, changes []protocol.DocumentChange) (map[
21682168
}
21692169

21702170
func codeActionMarker(mark marker, loc protocol.Location, kind string) {
2171-
if !exactlyOneNamedArg(mark, "edit", "result", "err") {
2171+
if !exactlyOneNamedArg(mark, "action", "edit", "result", "err") {
21722172
return
21732173
}
21742174

@@ -2181,14 +2181,61 @@ func codeActionMarker(mark marker, loc protocol.Location, kind string) {
21812181
}
21822182

21832183
var (
2184-
edit = namedArg(mark, "edit", expect.Identifier(""))
2185-
result = namedArg(mark, "result", expect.Identifier(""))
2186-
wantErr = namedArgFunc(mark, "err", convertStringMatcher, stringMatcher{})
2184+
edit = namedArg(mark, "edit", expect.Identifier(""))
2185+
result = namedArg(mark, "result", expect.Identifier(""))
2186+
wantAction = namedArg(mark, "action", expect.Identifier(""))
2187+
wantErr = namedArgFunc(mark, "err", convertStringMatcher, stringMatcher{})
21872188
)
21882189

2189-
changed, err := codeAction(mark.run.env, loc.URI, loc.Range, protocol.CodeActionKind(kind), nil)
2190-
if err != nil && wantErr.empty() {
2191-
mark.errorf("codeAction failed: %v", err)
2190+
var diag *protocol.Diagnostic
2191+
if re := namedArg(mark, "diag", (*regexp.Regexp)(nil)); re != nil {
2192+
d, ok := removeDiagnostic(mark, loc, false, re)
2193+
if !ok {
2194+
mark.errorf("no diagnostic at %v matches %q", loc, re)
2195+
return
2196+
}
2197+
diag = &d
2198+
}
2199+
2200+
action, err := resolveCodeAction(mark.run.env, loc.URI, loc.Range, protocol.CodeActionKind(kind), diag)
2201+
if err != nil {
2202+
if !wantErr.empty() {
2203+
wantErr.checkErr(mark, err)
2204+
} else {
2205+
mark.errorf("resolveCodeAction failed: %v", err)
2206+
}
2207+
return
2208+
}
2209+
2210+
// If when 'action' is set, we simply compare the action, and don't apply it.
2211+
if wantAction != "" {
2212+
g := mark.getGolden(wantAction)
2213+
if action == nil {
2214+
mark.errorf("no matching codeAction")
2215+
return
2216+
}
2217+
data, err := json.MarshalIndent(action, "", "\t")
2218+
if err != nil {
2219+
mark.errorf("failed to marshal codeaction: %v", err)
2220+
return
2221+
}
2222+
data = bytes.ReplaceAll(data, []byte(mark.run.env.Sandbox.Workdir.RootURI()), []byte("$WORKDIR"))
2223+
compareGolden(mark, data, g)
2224+
return
2225+
}
2226+
2227+
changes, err := applyCodeAction(mark.run.env, action)
2228+
if err != nil {
2229+
if !wantErr.empty() {
2230+
wantErr.checkErr(mark, err)
2231+
} else {
2232+
mark.errorf("codeAction failed: %v", err)
2233+
}
2234+
return
2235+
}
2236+
changed, err := changedFiles(mark.run.env, changes)
2237+
if err != nil {
2238+
mark.errorf("changedFiles failed: %v", err)
21922239
return
21932240
}
21942241

@@ -2319,29 +2366,20 @@ func quickfixErrMarker(mark marker, loc protocol.Location, re *regexp.Regexp, wa
23192366
// applied. Currently, this function does not support code actions that return
23202367
// edits directly; it only supports code action commands.
23212368
func codeAction(env *integration.Env, uri protocol.DocumentURI, rng protocol.Range, kind protocol.CodeActionKind, diag *protocol.Diagnostic) (map[string][]byte, error) {
2322-
changes, err := codeActionChanges(env, uri, rng, kind, diag)
2369+
action, err := resolveCodeAction(env, uri, rng, kind, diag)
2370+
if err != nil {
2371+
return nil, err
2372+
}
2373+
changes, err := applyCodeAction(env, action)
23232374
if err != nil {
23242375
return nil, err
23252376
}
23262377
return changedFiles(env, changes)
23272378
}
23282379

2329-
// codeActionChanges executes a textDocument/codeAction request for the
2330-
// specified location and kind, and captures the resulting document changes.
2331-
// If diag is non-nil, it is used as the code action context.
2332-
func codeActionChanges(env *integration.Env, uri protocol.DocumentURI, rng protocol.Range, kind protocol.CodeActionKind, diag *protocol.Diagnostic) ([]protocol.DocumentChange, error) {
2333-
// Collect any server-initiated changes created by workspace/applyEdit.
2334-
//
2335-
// We set up this handler immediately, not right before executing the code
2336-
// action command, so we can assert that neither the codeAction request nor
2337-
// codeAction resolve request cause edits as a side effect (golang/go#71405).
2338-
var changes []protocol.DocumentChange
2339-
restore := env.Editor.Client().SetApplyEditHandler(func(ctx context.Context, wsedit *protocol.WorkspaceEdit) error {
2340-
changes = append(changes, wsedit.DocumentChanges...)
2341-
return nil
2342-
})
2343-
defer restore()
2344-
2380+
// resolveCodeAction resolves the code action specified by the given location,
2381+
// kind, and diagnostic.
2382+
func resolveCodeAction(env *integration.Env, uri protocol.DocumentURI, rng protocol.Range, kind protocol.CodeActionKind, diag *protocol.Diagnostic) (*protocol.CodeAction, error) {
23452383
// Request all code actions that apply to the diagnostic.
23462384
// A production client would set Only=[kind],
23472385
// but we can give a better error if we don't filter.
@@ -2379,16 +2417,6 @@ func codeActionChanges(env *integration.Env, uri protocol.DocumentURI, rng proto
23792417
}
23802418
action := candidates[0]
23812419

2382-
// Apply the codeAction.
2383-
//
2384-
// Spec:
2385-
// "If a code action provides an edit and a command, first the edit is
2386-
// executed and then the command."
2387-
// An action may specify an edit and/or a command, to be
2388-
// applied in that order. But since applyDocumentChanges(env,
2389-
// action.Edit.DocumentChanges) doesn't compose, for now we
2390-
// assert that actions return one or the other.
2391-
23922420
// Resolve code action edits first if the client has resolve support
23932421
// and the code action has no edits.
23942422
if action.Edit == nil {
@@ -2401,10 +2429,41 @@ func codeActionChanges(env *integration.Env, uri protocol.DocumentURI, rng proto
24012429
if err != nil {
24022430
return nil, err
24032431
}
2404-
action.Edit = resolved.Edit
2432+
action = *resolved
24052433
}
24062434
}
24072435

2436+
return &action, nil
2437+
}
2438+
2439+
// applyCodeAction applies the given code action, and captures the resulting
2440+
// document changes.
2441+
func applyCodeAction(env *integration.Env, action *protocol.CodeAction) ([]protocol.DocumentChange, error) {
2442+
// Collect any server-initiated changes created by workspace/applyEdit.
2443+
//
2444+
// We set up this handler immediately, not right before executing the code
2445+
// action command, so we can assert that neither the codeAction request nor
2446+
// codeAction resolve request cause edits as a side effect (golang/go#71405).
2447+
var changes []protocol.DocumentChange
2448+
restore := env.Editor.Client().SetApplyEditHandler(func(ctx context.Context, wsedit *protocol.WorkspaceEdit) error {
2449+
changes = append(changes, wsedit.DocumentChanges...)
2450+
return nil
2451+
})
2452+
defer restore()
2453+
2454+
if action.Edit == nil && action.Command == nil {
2455+
panic("bad action")
2456+
}
2457+
2458+
// Apply the codeAction.
2459+
//
2460+
// Spec:
2461+
// "If a code action provides an edit and a command, first the edit is
2462+
// executed and then the command."
2463+
// An action may specify an edit and/or a command, to be
2464+
// applied in that order. But since applyDocumentChanges(env,
2465+
// action.Edit.DocumentChanges) doesn't compose, for now we
2466+
// assert that actions return one or the other.
24082467
if action.Edit != nil {
24092468
if len(action.Edit.Changes) > 0 {
24102469
env.TB.Errorf("internal error: discarding unexpected CodeAction{Kind=%s, Title=%q}.Edit.Changes", action.Kind, action.Title)
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
This test checks that failing to produce edits doesn't fail the
2+
codeAction/resolve operation. Instead, we simply return the action
3+
with its command, and let the action fail when its command is applied
4+
(golang/go#75442).
5+
6+
-- flags --
7+
-ignore_extra_diags
8+
9+
-- go.mod --
10+
module golang.org/lsptests/failedresolve
11+
12+
go 1.18
13+
14+
-- resolve.go --
15+
package extract
16+
17+
func _() {
18+
if x := 1; true {
19+
20+
} else if y := x + 1; true { //@codeaction("x + 1", "refactor.extract.variable", action=act)
21+
22+
}
23+
}
24+
25+
-- apply.go --
26+
package extract
27+
28+
func _() {
29+
if x := 1; true {
30+
31+
} else if y := x + 1; true { //@codeaction("x + 1", "refactor.extract.variable", err=re"cannot find location to insert")
32+
33+
}
34+
}
35+
36+
-- @act --
37+
{
38+
"title": "Extract variable",
39+
"kind": "refactor.extract.variable",
40+
"command": {
41+
"title": "Extract variable",
42+
"command": "gopls.apply_fix",
43+
"arguments": [
44+
{
45+
"Fix": "extract_variable",
46+
"Location": {
47+
"uri": "$WORKDIR/resolve.go",
48+
"range": {
49+
"start": {
50+
"line": 5,
51+
"character": 16
52+
},
53+
"end": {
54+
"line": 5,
55+
"character": 21
56+
}
57+
}
58+
},
59+
"ResolveEdits": true
60+
}
61+
]
62+
}
63+
}

0 commit comments

Comments
 (0)