github.com/jd-ly/tools@v0.5.7/internal/lsp/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 lsp
     6  
     7  import (
     8  	"context"
     9  	"fmt"
    10  	"regexp"
    11  	"sort"
    12  	"strings"
    13  
    14  	"github.com/jd-ly/tools/go/analysis"
    15  	"github.com/jd-ly/tools/internal/event"
    16  	"github.com/jd-ly/tools/internal/imports"
    17  	"github.com/jd-ly/tools/internal/lsp/debug/tag"
    18  	"github.com/jd-ly/tools/internal/lsp/mod"
    19  	"github.com/jd-ly/tools/internal/lsp/protocol"
    20  	"github.com/jd-ly/tools/internal/lsp/source"
    21  	"github.com/jd-ly/tools/internal/span"
    22  )
    23  
    24  func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionParams) ([]protocol.CodeAction, error) {
    25  	snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.UnknownKind)
    26  	defer release()
    27  	if !ok {
    28  		return nil, err
    29  	}
    30  	uri := fh.URI()
    31  
    32  	// Determine the supported actions for this file kind.
    33  	supportedCodeActions, ok := snapshot.View().Options().SupportedCodeActions[fh.Kind()]
    34  	if !ok {
    35  		return nil, fmt.Errorf("no supported code actions for %v file kind", fh.Kind())
    36  	}
    37  
    38  	// The Only field of the context specifies which code actions the client wants.
    39  	// If Only is empty, assume that the client wants all of the non-explicit code actions.
    40  	var wanted map[protocol.CodeActionKind]bool
    41  
    42  	// Explicit Code Actions are opt-in and shouldn't be returned to the client unless
    43  	// requested using Only.
    44  	// TODO: Add other CodeLenses such as GoGenerate, RegenerateCgo, etc..
    45  	explicit := map[protocol.CodeActionKind]bool{
    46  		protocol.GoTest: true,
    47  	}
    48  
    49  	if len(params.Context.Only) == 0 {
    50  		wanted = supportedCodeActions
    51  	} else {
    52  		wanted = make(map[protocol.CodeActionKind]bool)
    53  		for _, only := range params.Context.Only {
    54  			wanted[only] = supportedCodeActions[only] || explicit[only]
    55  		}
    56  	}
    57  	if len(wanted) == 0 {
    58  		return nil, fmt.Errorf("no supported code action to execute for %s, wanted %v", uri, params.Context.Only)
    59  	}
    60  
    61  	var codeActions []protocol.CodeAction
    62  	switch fh.Kind() {
    63  	case source.Mod:
    64  		if diagnostics := params.Context.Diagnostics; len(diagnostics) > 0 {
    65  			modQuickFixes, err := moduleQuickFixes(ctx, snapshot, fh, diagnostics)
    66  			if source.IsNonFatalGoModError(err) {
    67  				return nil, nil
    68  			}
    69  			if err != nil {
    70  				return nil, err
    71  			}
    72  			codeActions = append(codeActions, modQuickFixes...)
    73  		}
    74  	case source.Go:
    75  		// Don't suggest fixes for generated files, since they are generally
    76  		// not useful and some editors may apply them automatically on save.
    77  		if source.IsGenerated(ctx, snapshot, uri) {
    78  			return nil, nil
    79  		}
    80  		diagnostics := params.Context.Diagnostics
    81  
    82  		// First, process any missing imports and pair them with the
    83  		// diagnostics they fix.
    84  		if wantQuickFixes := wanted[protocol.QuickFix] && len(diagnostics) > 0; wantQuickFixes || wanted[protocol.SourceOrganizeImports] {
    85  			importEdits, importEditsPerFix, err := source.AllImportsFixes(ctx, snapshot, fh)
    86  			if err != nil {
    87  				event.Error(ctx, "imports fixes", err, tag.File.Of(fh.URI().Filename()))
    88  			}
    89  			// Separate this into a set of codeActions per diagnostic, where
    90  			// each action is the addition, removal, or renaming of one import.
    91  			if wantQuickFixes {
    92  				for _, importFix := range importEditsPerFix {
    93  					fixes := importDiagnostics(importFix.Fix, diagnostics)
    94  					if len(fixes) == 0 {
    95  						continue
    96  					}
    97  					codeActions = append(codeActions, protocol.CodeAction{
    98  						Title: importFixTitle(importFix.Fix),
    99  						Kind:  protocol.QuickFix,
   100  						Edit: protocol.WorkspaceEdit{
   101  							DocumentChanges: documentChanges(fh, importFix.Edits),
   102  						},
   103  						Diagnostics: fixes,
   104  					})
   105  				}
   106  			}
   107  
   108  			// Fix unresolved imports with "go get". This is separate from the
   109  			// goimports fixes because goimports will not remove an import
   110  			// that appears to be used, even if currently unresolved.
   111  			actions, err := goGetFixes(ctx, snapshot, fh.URI(), diagnostics)
   112  			if err != nil {
   113  				return nil, err
   114  			}
   115  			codeActions = append(codeActions, actions...)
   116  
   117  			// Send all of the import edits as one code action if the file is
   118  			// being organized.
   119  			if wanted[protocol.SourceOrganizeImports] && len(importEdits) > 0 {
   120  				codeActions = append(codeActions, protocol.CodeAction{
   121  					Title: "Organize Imports",
   122  					Kind:  protocol.SourceOrganizeImports,
   123  					Edit: protocol.WorkspaceEdit{
   124  						DocumentChanges: documentChanges(fh, importEdits),
   125  					},
   126  				})
   127  			}
   128  		}
   129  		if ctx.Err() != nil {
   130  			return nil, ctx.Err()
   131  		}
   132  		pkg, err := snapshot.PackageForFile(ctx, fh.URI(), source.TypecheckFull, source.WidestPackage)
   133  		if err != nil {
   134  			return nil, err
   135  		}
   136  		if (wanted[protocol.QuickFix] || wanted[protocol.SourceFixAll]) && len(diagnostics) > 0 {
   137  			analysisQuickFixes, highConfidenceEdits, err := analysisFixes(ctx, snapshot, pkg, diagnostics)
   138  			if err != nil {
   139  				return nil, err
   140  			}
   141  			if wanted[protocol.QuickFix] {
   142  				// Add the quick fixes reported by go/analysis.
   143  				codeActions = append(codeActions, analysisQuickFixes...)
   144  
   145  				// If there are any diagnostics relating to the go.mod file,
   146  				// add their corresponding quick fixes.
   147  				modQuickFixes, err := moduleQuickFixes(ctx, snapshot, fh, diagnostics)
   148  				if source.IsNonFatalGoModError(err) {
   149  					// Not a fatal error.
   150  					event.Error(ctx, "module suggested fixes failed", err, tag.Directory.Of(snapshot.View().Folder()))
   151  				}
   152  				codeActions = append(codeActions, modQuickFixes...)
   153  			}
   154  			if wanted[protocol.SourceFixAll] && len(highConfidenceEdits) > 0 {
   155  				codeActions = append(codeActions, protocol.CodeAction{
   156  					Title: "Simplifications",
   157  					Kind:  protocol.SourceFixAll,
   158  					Edit: protocol.WorkspaceEdit{
   159  						DocumentChanges: highConfidenceEdits,
   160  					},
   161  				})
   162  			}
   163  		}
   164  		if ctx.Err() != nil {
   165  			return nil, ctx.Err()
   166  		}
   167  		// Add any suggestions that do not necessarily fix any diagnostics.
   168  		if wanted[protocol.RefactorRewrite] {
   169  			fixes, err := convenienceFixes(ctx, snapshot, pkg, uri, params.Range)
   170  			if err != nil {
   171  				return nil, err
   172  			}
   173  			codeActions = append(codeActions, fixes...)
   174  		}
   175  		if wanted[protocol.RefactorExtract] {
   176  			fixes, err := extractionFixes(ctx, snapshot, pkg, uri, params.Range)
   177  			if err != nil {
   178  				return nil, err
   179  			}
   180  			codeActions = append(codeActions, fixes...)
   181  		}
   182  
   183  		if wanted[protocol.GoTest] {
   184  			fixes, err := goTest(ctx, snapshot, uri, params.Range)
   185  			if err != nil {
   186  				return nil, err
   187  			}
   188  			codeActions = append(codeActions, fixes...)
   189  		}
   190  
   191  	default:
   192  		// Unsupported file kind for a code action.
   193  		return nil, nil
   194  	}
   195  	return codeActions, nil
   196  }
   197  
   198  func (s *Server) getSupportedCodeActions() []protocol.CodeActionKind {
   199  	allCodeActionKinds := make(map[protocol.CodeActionKind]struct{})
   200  	for _, kinds := range s.session.Options().SupportedCodeActions {
   201  		for kind := range kinds {
   202  			allCodeActionKinds[kind] = struct{}{}
   203  		}
   204  	}
   205  	var result []protocol.CodeActionKind
   206  	for kind := range allCodeActionKinds {
   207  		result = append(result, kind)
   208  	}
   209  	sort.Slice(result, func(i, j int) bool {
   210  		return result[i] < result[j]
   211  	})
   212  	return result
   213  }
   214  
   215  func importFixTitle(fix *imports.ImportFix) string {
   216  	var str string
   217  	switch fix.FixType {
   218  	case imports.AddImport:
   219  		str = fmt.Sprintf("Add import: %s %q", fix.StmtInfo.Name, fix.StmtInfo.ImportPath)
   220  	case imports.DeleteImport:
   221  		str = fmt.Sprintf("Delete import: %s %q", fix.StmtInfo.Name, fix.StmtInfo.ImportPath)
   222  	case imports.SetImportName:
   223  		str = fmt.Sprintf("Rename import: %s %q", fix.StmtInfo.Name, fix.StmtInfo.ImportPath)
   224  	}
   225  	return str
   226  }
   227  
   228  func importDiagnostics(fix *imports.ImportFix, diagnostics []protocol.Diagnostic) (results []protocol.Diagnostic) {
   229  	for _, diagnostic := range diagnostics {
   230  		switch {
   231  		// "undeclared name: X" may be an unresolved import.
   232  		case strings.HasPrefix(diagnostic.Message, "undeclared name: "):
   233  			ident := strings.TrimPrefix(diagnostic.Message, "undeclared name: ")
   234  			if ident == fix.IdentName {
   235  				results = append(results, diagnostic)
   236  			}
   237  		// "could not import: X" may be an invalid import.
   238  		case strings.HasPrefix(diagnostic.Message, "could not import: "):
   239  			ident := strings.TrimPrefix(diagnostic.Message, "could not import: ")
   240  			if ident == fix.IdentName {
   241  				results = append(results, diagnostic)
   242  			}
   243  		// "X imported but not used" is an unused import.
   244  		// "X imported but not used as Y" is an unused import.
   245  		case strings.Contains(diagnostic.Message, " imported but not used"):
   246  			idx := strings.Index(diagnostic.Message, " imported but not used")
   247  			importPath := diagnostic.Message[:idx]
   248  			if importPath == fmt.Sprintf("%q", fix.StmtInfo.ImportPath) {
   249  				results = append(results, diagnostic)
   250  			}
   251  		}
   252  	}
   253  	return results
   254  }
   255  
   256  func analysisFixes(ctx context.Context, snapshot source.Snapshot, pkg source.Package, diagnostics []protocol.Diagnostic) ([]protocol.CodeAction, []protocol.TextDocumentEdit, error) {
   257  	if len(diagnostics) == 0 {
   258  		return nil, nil, nil
   259  	}
   260  	var (
   261  		codeActions       []protocol.CodeAction
   262  		sourceFixAllEdits []protocol.TextDocumentEdit
   263  	)
   264  	for _, diag := range diagnostics {
   265  		srcErr, analyzer, ok := findSourceError(ctx, snapshot, pkg.ID(), diag)
   266  		if !ok {
   267  			continue
   268  		}
   269  		// If the suggested fix for the diagnostic is expected to be separate,
   270  		// see if there are any supported commands available.
   271  		if analyzer.Command != nil {
   272  			action, err := diagnosticToCommandCodeAction(ctx, snapshot, srcErr, &diag, protocol.QuickFix)
   273  			if err != nil {
   274  				return nil, nil, err
   275  			}
   276  			codeActions = append(codeActions, *action)
   277  			continue
   278  		}
   279  		for _, fix := range srcErr.SuggestedFixes {
   280  			action := protocol.CodeAction{
   281  				Title:       fix.Title,
   282  				Kind:        protocol.QuickFix,
   283  				Diagnostics: []protocol.Diagnostic{diag},
   284  				Edit:        protocol.WorkspaceEdit{},
   285  			}
   286  			for uri, edits := range fix.Edits {
   287  				fh, err := snapshot.GetVersionedFile(ctx, uri)
   288  				if err != nil {
   289  					return nil, nil, err
   290  				}
   291  				docChanges := documentChanges(fh, edits)
   292  				if analyzer.HighConfidence {
   293  					sourceFixAllEdits = append(sourceFixAllEdits, docChanges...)
   294  				}
   295  				action.Edit.DocumentChanges = append(action.Edit.DocumentChanges, docChanges...)
   296  			}
   297  			codeActions = append(codeActions, action)
   298  		}
   299  	}
   300  	return codeActions, sourceFixAllEdits, nil
   301  }
   302  
   303  func findSourceError(ctx context.Context, snapshot source.Snapshot, pkgID string, diag protocol.Diagnostic) (*source.Error, source.Analyzer, bool) {
   304  	analyzer := diagnosticToAnalyzer(snapshot, diag.Source, diag.Message)
   305  	if analyzer == nil {
   306  		return nil, source.Analyzer{}, false
   307  	}
   308  	analysisErrors, err := snapshot.Analyze(ctx, pkgID, analyzer.Analyzer)
   309  	if err != nil {
   310  		return nil, source.Analyzer{}, false
   311  	}
   312  	for _, err := range analysisErrors {
   313  		if err.Message != diag.Message {
   314  			continue
   315  		}
   316  		if protocol.CompareRange(err.Range, diag.Range) != 0 {
   317  			continue
   318  		}
   319  		if err.Category != analyzer.Analyzer.Name {
   320  			continue
   321  		}
   322  		// The error matches.
   323  		return err, *analyzer, true
   324  	}
   325  	return nil, source.Analyzer{}, false
   326  }
   327  
   328  // diagnosticToAnalyzer return the analyzer associated with a given diagnostic.
   329  // It assumes that the diagnostic's source will be the name of the analyzer.
   330  // If this changes, this approach will need to be reworked.
   331  func diagnosticToAnalyzer(snapshot source.Snapshot, src, msg string) (analyzer *source.Analyzer) {
   332  	// Make sure that the analyzer we found is enabled.
   333  	defer func() {
   334  		if analyzer != nil && !analyzer.IsEnabled(snapshot.View()) {
   335  			analyzer = nil
   336  		}
   337  	}()
   338  	if a, ok := snapshot.View().Options().DefaultAnalyzers[src]; ok {
   339  		return &a
   340  	}
   341  	if a, ok := snapshot.View().Options().StaticcheckAnalyzers[src]; ok {
   342  		return &a
   343  	}
   344  	if a, ok := snapshot.View().Options().ConvenienceAnalyzers[src]; ok {
   345  		return &a
   346  	}
   347  	// Hack: We publish diagnostics with the source "compiler" for type errors,
   348  	// but these analyzers have different names. Try both possibilities.
   349  	if a, ok := snapshot.View().Options().TypeErrorAnalyzers[src]; ok {
   350  		return &a
   351  	}
   352  	if src != "compiler" {
   353  		return nil
   354  	}
   355  	for _, a := range snapshot.View().Options().TypeErrorAnalyzers {
   356  		if a.FixesError(msg) {
   357  			return &a
   358  		}
   359  	}
   360  	return nil
   361  }
   362  
   363  var importErrorRe = regexp.MustCompile(`could not import ([^\s]+)`)
   364  
   365  func goGetFixes(ctx context.Context, snapshot source.Snapshot, uri span.URI, diagnostics []protocol.Diagnostic) ([]protocol.CodeAction, error) {
   366  	if snapshot.GoModForFile(ctx, uri) == "" {
   367  		// Go get only supports module mode for now.
   368  		return nil, nil
   369  	}
   370  
   371  	var actions []protocol.CodeAction
   372  	for _, diag := range diagnostics {
   373  		matches := importErrorRe.FindStringSubmatch(diag.Message)
   374  		if len(matches) == 0 {
   375  			return nil, nil
   376  		}
   377  		args, err := source.MarshalArgs(uri, matches[1])
   378  		if err != nil {
   379  			return nil, err
   380  		}
   381  		actions = append(actions, protocol.CodeAction{
   382  			Title:       fmt.Sprintf("go get package %v", matches[1]),
   383  			Diagnostics: []protocol.Diagnostic{diag},
   384  			Kind:        protocol.QuickFix,
   385  			Command: &protocol.Command{
   386  				Title:     source.CommandGoGetPackage.Title,
   387  				Command:   source.CommandGoGetPackage.ID(),
   388  				Arguments: args,
   389  			},
   390  		})
   391  	}
   392  	return actions, nil
   393  }
   394  
   395  func convenienceFixes(ctx context.Context, snapshot source.Snapshot, pkg source.Package, uri span.URI, rng protocol.Range) ([]protocol.CodeAction, error) {
   396  	var analyzers []*analysis.Analyzer
   397  	for _, a := range snapshot.View().Options().ConvenienceAnalyzers {
   398  		if !a.IsEnabled(snapshot.View()) {
   399  			continue
   400  		}
   401  		if a.Command == nil {
   402  			event.Error(ctx, "convenienceFixes", fmt.Errorf("no suggested fixes for convenience analyzer %s", a.Analyzer.Name))
   403  			continue
   404  		}
   405  		analyzers = append(analyzers, a.Analyzer)
   406  	}
   407  	diagnostics, err := snapshot.Analyze(ctx, pkg.ID(), analyzers...)
   408  	if err != nil {
   409  		return nil, err
   410  	}
   411  	var codeActions []protocol.CodeAction
   412  	for _, d := range diagnostics {
   413  		// For now, only show diagnostics for matching lines. Maybe we should
   414  		// alter this behavior in the future, depending on the user experience.
   415  		if d.URI != uri {
   416  			continue
   417  		}
   418  
   419  		if !protocol.Intersect(d.Range, rng) {
   420  			continue
   421  		}
   422  		action, err := diagnosticToCommandCodeAction(ctx, snapshot, d, nil, protocol.RefactorRewrite)
   423  		if err != nil {
   424  			return nil, err
   425  		}
   426  		codeActions = append(codeActions, *action)
   427  	}
   428  	return codeActions, nil
   429  }
   430  
   431  func diagnosticToCommandCodeAction(ctx context.Context, snapshot source.Snapshot, e *source.Error, d *protocol.Diagnostic, kind protocol.CodeActionKind) (*protocol.CodeAction, error) {
   432  	// The fix depends on the category of the analyzer. The diagnostic may be
   433  	// nil, so use the error's category.
   434  	analyzer := diagnosticToAnalyzer(snapshot, e.Category, e.Message)
   435  	if analyzer == nil {
   436  		return nil, fmt.Errorf("no convenience analyzer for category %s", e.Category)
   437  	}
   438  	if analyzer.Command == nil {
   439  		return nil, fmt.Errorf("no command for convenience analyzer %s", analyzer.Analyzer.Name)
   440  	}
   441  	jsonArgs, err := source.MarshalArgs(e.URI, e.Range)
   442  	if err != nil {
   443  		return nil, err
   444  	}
   445  	var diagnostics []protocol.Diagnostic
   446  	if d != nil {
   447  		diagnostics = append(diagnostics, *d)
   448  	}
   449  	return &protocol.CodeAction{
   450  		Title:       e.Message,
   451  		Kind:        kind,
   452  		Diagnostics: diagnostics,
   453  		Command: &protocol.Command{
   454  			Command:   analyzer.Command.ID(),
   455  			Title:     e.Message,
   456  			Arguments: jsonArgs,
   457  		},
   458  	}, nil
   459  }
   460  
   461  func extractionFixes(ctx context.Context, snapshot source.Snapshot, pkg source.Package, uri span.URI, rng protocol.Range) ([]protocol.CodeAction, error) {
   462  	if rng.Start == rng.End {
   463  		return nil, nil
   464  	}
   465  	fh, err := snapshot.GetFile(ctx, uri)
   466  	if err != nil {
   467  		return nil, err
   468  	}
   469  	jsonArgs, err := source.MarshalArgs(uri, rng)
   470  	if err != nil {
   471  		return nil, err
   472  	}
   473  	var actions []protocol.CodeAction
   474  	for _, command := range []*source.Command{
   475  		source.CommandExtractFunction,
   476  		source.CommandExtractVariable,
   477  	} {
   478  		if !command.Applies(ctx, snapshot, fh, rng) {
   479  			continue
   480  		}
   481  		actions = append(actions, protocol.CodeAction{
   482  			Title: command.Title,
   483  			Kind:  protocol.RefactorExtract,
   484  			Command: &protocol.Command{
   485  				Command:   command.ID(),
   486  				Arguments: jsonArgs,
   487  			},
   488  		})
   489  	}
   490  	return actions, nil
   491  }
   492  
   493  func documentChanges(fh source.VersionedFileHandle, edits []protocol.TextEdit) []protocol.TextDocumentEdit {
   494  	return []protocol.TextDocumentEdit{
   495  		{
   496  			TextDocument: protocol.VersionedTextDocumentIdentifier{
   497  				Version: fh.Version(),
   498  				TextDocumentIdentifier: protocol.TextDocumentIdentifier{
   499  					URI: protocol.URIFromSpanURI(fh.URI()),
   500  				},
   501  			},
   502  			Edits: edits,
   503  		},
   504  	}
   505  }
   506  
   507  func moduleQuickFixes(ctx context.Context, snapshot source.Snapshot, fh source.VersionedFileHandle, diagnostics []protocol.Diagnostic) ([]protocol.CodeAction, error) {
   508  	var modFH source.VersionedFileHandle
   509  	switch fh.Kind() {
   510  	case source.Mod:
   511  		modFH = fh
   512  	case source.Go:
   513  		modURI := snapshot.GoModForFile(ctx, fh.URI())
   514  		if modURI == "" {
   515  			return nil, nil
   516  		}
   517  		var err error
   518  		modFH, err = snapshot.GetVersionedFile(ctx, modURI)
   519  		if err != nil {
   520  			return nil, err
   521  		}
   522  	}
   523  	errors, err := mod.ErrorsForMod(ctx, snapshot, modFH)
   524  	if err != nil {
   525  		return nil, err
   526  	}
   527  	var quickFixes []protocol.CodeAction
   528  	for _, e := range errors {
   529  		var diag *protocol.Diagnostic
   530  		for _, d := range diagnostics {
   531  			if sameDiagnostic(d, e) {
   532  				diag = &d
   533  				break
   534  			}
   535  		}
   536  		if diag == nil {
   537  			continue
   538  		}
   539  		for _, fix := range e.SuggestedFixes {
   540  			action := protocol.CodeAction{
   541  				Title:       fix.Title,
   542  				Kind:        protocol.QuickFix,
   543  				Diagnostics: []protocol.Diagnostic{*diag},
   544  				Edit:        protocol.WorkspaceEdit{},
   545  				Command:     fix.Command,
   546  			}
   547  			for uri, edits := range fix.Edits {
   548  				if uri != modFH.URI() {
   549  					continue
   550  				}
   551  				action.Edit.DocumentChanges = append(action.Edit.DocumentChanges, protocol.TextDocumentEdit{
   552  					TextDocument: protocol.VersionedTextDocumentIdentifier{
   553  						Version: modFH.Version(),
   554  						TextDocumentIdentifier: protocol.TextDocumentIdentifier{
   555  							URI: protocol.URIFromSpanURI(modFH.URI()),
   556  						},
   557  					},
   558  					Edits: edits,
   559  				})
   560  			}
   561  			quickFixes = append(quickFixes, action)
   562  		}
   563  	}
   564  	return quickFixes, nil
   565  }
   566  
   567  func sameDiagnostic(d protocol.Diagnostic, e *source.Error) bool {
   568  	return d.Message == e.Message && protocol.CompareRange(d.Range, e.Range) == 0 && d.Source == e.Category
   569  }
   570  
   571  func goTest(ctx context.Context, snapshot source.Snapshot, uri span.URI, rng protocol.Range) ([]protocol.CodeAction, error) {
   572  	fh, err := snapshot.GetFile(ctx, uri)
   573  	if err != nil {
   574  		return nil, err
   575  	}
   576  	fns, err := source.TestsAndBenchmarks(ctx, snapshot, fh)
   577  	if err != nil {
   578  		return nil, err
   579  	}
   580  
   581  	var tests, benchmarks []string
   582  	for _, fn := range fns.Tests {
   583  		if !protocol.Intersect(fn.Rng, rng) {
   584  			continue
   585  		}
   586  		tests = append(tests, fn.Name)
   587  	}
   588  	for _, fn := range fns.Benchmarks {
   589  		if !protocol.Intersect(fn.Rng, rng) {
   590  			continue
   591  		}
   592  		benchmarks = append(benchmarks, fn.Name)
   593  	}
   594  
   595  	if len(tests) == 0 && len(benchmarks) == 0 {
   596  		return nil, nil
   597  	}
   598  
   599  	jsonArgs, err := source.MarshalArgs(uri, tests, benchmarks)
   600  	if err != nil {
   601  		return nil, err
   602  	}
   603  	return []protocol.CodeAction{{
   604  		Title: source.CommandTest.Name,
   605  		Kind:  protocol.GoTest,
   606  		Command: &protocol.Command{
   607  			Title:     source.CommandTest.Title,
   608  			Command:   source.CommandTest.ID(),
   609  			Arguments: jsonArgs,
   610  		},
   611  	}}, nil
   612  }