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  }