golang.org/x/tools/gopls@v0.15.3/internal/server/code_action.go (about) 1 // Copyright 2018 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package server 6 7 import ( 8 "context" 9 "fmt" 10 "sort" 11 "strings" 12 13 "golang.org/x/tools/gopls/internal/cache" 14 "golang.org/x/tools/gopls/internal/file" 15 "golang.org/x/tools/gopls/internal/golang" 16 "golang.org/x/tools/gopls/internal/mod" 17 "golang.org/x/tools/gopls/internal/protocol" 18 "golang.org/x/tools/gopls/internal/protocol/command" 19 "golang.org/x/tools/internal/event" 20 ) 21 22 func (s *server) CodeAction(ctx context.Context, params *protocol.CodeActionParams) ([]protocol.CodeAction, error) { 23 ctx, done := event.Start(ctx, "lsp.Server.codeAction") 24 defer done() 25 26 fh, snapshot, release, err := s.fileOf(ctx, params.TextDocument.URI) 27 if err != nil { 28 return nil, err 29 } 30 defer release() 31 uri := fh.URI() 32 33 // Determine the supported actions for this file kind. 34 kind := snapshot.FileKind(fh) 35 supportedCodeActions, ok := snapshot.Options().SupportedCodeActions[kind] 36 if !ok { 37 return nil, fmt.Errorf("no supported code actions for %v file kind", kind) 38 } 39 if len(supportedCodeActions) == 0 { 40 return nil, nil // not an error if there are none supported 41 } 42 43 // The Only field of the context specifies which code actions the client wants. 44 // If Only is empty, assume that the client wants all of the non-explicit code actions. 45 var want map[protocol.CodeActionKind]bool 46 { 47 // Explicit Code Actions are opt-in and shouldn't be returned to the client unless 48 // requested using Only. 49 // TODO: Add other CodeLenses such as GoGenerate, RegenerateCgo, etc.. 50 explicit := map[protocol.CodeActionKind]bool{ 51 protocol.GoTest: true, 52 } 53 54 if len(params.Context.Only) == 0 { 55 want = supportedCodeActions 56 } else { 57 want = make(map[protocol.CodeActionKind]bool) 58 for _, only := range params.Context.Only { 59 for k, v := range supportedCodeActions { 60 if only == k || strings.HasPrefix(string(k), string(only)+".") { 61 want[k] = want[k] || v 62 } 63 } 64 want[only] = want[only] || explicit[only] 65 } 66 } 67 } 68 if len(want) == 0 { 69 return nil, fmt.Errorf("no supported code action to execute for %s, wanted %v", uri, params.Context.Only) 70 } 71 72 switch kind { 73 case file.Mod: 74 var actions []protocol.CodeAction 75 76 fixes, err := s.codeActionsMatchingDiagnostics(ctx, fh.URI(), snapshot, params.Context.Diagnostics, want) 77 if err != nil { 78 return nil, err 79 } 80 81 // Group vulnerability fixes by their range, and select only the most 82 // appropriate upgrades. 83 // 84 // TODO(rfindley): can this instead be accomplished on the diagnosis side, 85 // so that code action handling remains uniform? 86 vulnFixes := make(map[protocol.Range][]protocol.CodeAction) 87 searchFixes: 88 for _, fix := range fixes { 89 for _, diag := range fix.Diagnostics { 90 if diag.Source == string(cache.Govulncheck) || diag.Source == string(cache.Vulncheck) { 91 vulnFixes[diag.Range] = append(vulnFixes[diag.Range], fix) 92 continue searchFixes 93 } 94 } 95 actions = append(actions, fix) 96 } 97 98 for _, fixes := range vulnFixes { 99 fixes = mod.SelectUpgradeCodeActions(fixes) 100 actions = append(actions, fixes...) 101 } 102 103 return actions, nil 104 105 case file.Go: 106 // Don't suggest fixes for generated files, since they are generally 107 // not useful and some editors may apply them automatically on save. 108 if golang.IsGenerated(ctx, snapshot, uri) { 109 return nil, nil 110 } 111 112 actions, err := s.codeActionsMatchingDiagnostics(ctx, uri, snapshot, params.Context.Diagnostics, want) 113 if err != nil { 114 return nil, err 115 } 116 117 moreActions, err := golang.CodeActions(ctx, snapshot, fh, params.Range, params.Context.Diagnostics, want) 118 if err != nil { 119 return nil, err 120 } 121 actions = append(actions, moreActions...) 122 123 return actions, nil 124 125 default: 126 // Unsupported file kind for a code action. 127 return nil, nil 128 } 129 } 130 131 // ResolveCodeAction resolves missing Edit information (that is, computes the 132 // details of the necessary patch) in the given code action using the provided 133 // Data field of the CodeAction, which should contain the raw json of a protocol.Command. 134 // 135 // This should be called by the client before applying code actions, when the 136 // client has code action resolve support. 137 // 138 // This feature allows capable clients to preview and selectively apply the diff 139 // instead of applying the whole thing unconditionally through workspace/applyEdit. 140 func (s *server) ResolveCodeAction(ctx context.Context, ca *protocol.CodeAction) (*protocol.CodeAction, error) { 141 ctx, done := event.Start(ctx, "lsp.Server.resolveCodeAction") 142 defer done() 143 144 // Only resolve the code action if there is Data provided. 145 var cmd protocol.Command 146 if ca.Data != nil { 147 if err := protocol.UnmarshalJSON(*ca.Data, &cmd); err != nil { 148 return nil, err 149 } 150 } 151 if cmd.Command != "" { 152 params := &protocol.ExecuteCommandParams{ 153 Command: cmd.Command, 154 Arguments: cmd.Arguments, 155 } 156 157 handler := &commandHandler{ 158 s: s, 159 params: params, 160 } 161 edit, err := command.Dispatch(ctx, params, handler) 162 if err != nil { 163 164 return nil, err 165 } 166 var ok bool 167 if ca.Edit, ok = edit.(*protocol.WorkspaceEdit); !ok { 168 return nil, fmt.Errorf("unable to resolve code action %q", ca.Title) 169 } 170 } 171 return ca, nil 172 } 173 174 // codeActionsMatchingDiagnostics fetches code actions for the provided 175 // diagnostics, by first attempting to unmarshal code actions directly from the 176 // bundled protocol.Diagnostic.Data field, and failing that by falling back on 177 // fetching a matching Diagnostic from the set of stored diagnostics for 178 // this file. 179 func (s *server) codeActionsMatchingDiagnostics(ctx context.Context, uri protocol.DocumentURI, snapshot *cache.Snapshot, pds []protocol.Diagnostic, want map[protocol.CodeActionKind]bool) ([]protocol.CodeAction, error) { 180 var actions []protocol.CodeAction 181 var unbundled []protocol.Diagnostic // diagnostics without bundled code actions in their Data field 182 for _, pd := range pds { 183 bundled := cache.BundledQuickFixes(pd) 184 if len(bundled) > 0 { 185 for _, fix := range bundled { 186 if want[fix.Kind] { 187 actions = append(actions, fix) 188 } 189 } 190 } else { 191 // No bundled actions: keep searching for a match. 192 unbundled = append(unbundled, pd) 193 } 194 } 195 196 for _, pd := range unbundled { 197 for _, sd := range s.findMatchingDiagnostics(uri, pd) { 198 diagActions, err := codeActionsForDiagnostic(ctx, snapshot, sd, &pd, want) 199 if err != nil { 200 return nil, err 201 } 202 actions = append(actions, diagActions...) 203 } 204 } 205 return actions, nil 206 } 207 208 func codeActionsForDiagnostic(ctx context.Context, snapshot *cache.Snapshot, sd *cache.Diagnostic, pd *protocol.Diagnostic, want map[protocol.CodeActionKind]bool) ([]protocol.CodeAction, error) { 209 var actions []protocol.CodeAction 210 for _, fix := range sd.SuggestedFixes { 211 if !want[fix.ActionKind] { 212 continue 213 } 214 changes := []protocol.DocumentChanges{} // must be a slice 215 for uri, edits := range fix.Edits { 216 fh, err := snapshot.ReadFile(ctx, uri) 217 if err != nil { 218 return nil, err 219 } 220 changes = append(changes, documentChanges(fh, edits)...) 221 } 222 actions = append(actions, protocol.CodeAction{ 223 Title: fix.Title, 224 Kind: fix.ActionKind, 225 Edit: &protocol.WorkspaceEdit{ 226 DocumentChanges: changes, 227 }, 228 Command: fix.Command, 229 Diagnostics: []protocol.Diagnostic{*pd}, 230 }) 231 } 232 return actions, nil 233 } 234 235 func (s *server) findMatchingDiagnostics(uri protocol.DocumentURI, pd protocol.Diagnostic) []*cache.Diagnostic { 236 s.diagnosticsMu.Lock() 237 defer s.diagnosticsMu.Unlock() 238 239 var sds []*cache.Diagnostic 240 for _, viewDiags := range s.diagnostics[uri].byView { 241 for _, sd := range viewDiags.diagnostics { 242 sameDiagnostic := (pd.Message == strings.TrimSpace(sd.Message) && // extra space may have been trimmed when converting to protocol.Diagnostic 243 protocol.CompareRange(pd.Range, sd.Range) == 0 && 244 pd.Source == string(sd.Source)) 245 246 if sameDiagnostic { 247 sds = append(sds, sd) 248 } 249 } 250 } 251 return sds 252 } 253 254 func (s *server) getSupportedCodeActions() []protocol.CodeActionKind { 255 allCodeActionKinds := make(map[protocol.CodeActionKind]struct{}) 256 for _, kinds := range s.Options().SupportedCodeActions { 257 for kind := range kinds { 258 allCodeActionKinds[kind] = struct{}{} 259 } 260 } 261 var result []protocol.CodeActionKind 262 for kind := range allCodeActionKinds { 263 result = append(result, kind) 264 } 265 sort.Slice(result, func(i, j int) bool { 266 return result[i] < result[j] 267 }) 268 return result 269 } 270 271 type unit = struct{} 272 273 func documentChanges(fh file.Handle, edits []protocol.TextEdit) []protocol.DocumentChanges { 274 return protocol.TextEditsToDocumentChanges(fh.URI(), fh.Version(), edits) 275 }