github.com/v2fly/tools@v0.100.0/internal/lsp/cmd/suggested_fix.go (about)

     1  // Copyright 2019 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 cmd
     6  
     7  import (
     8  	"context"
     9  	"flag"
    10  	"fmt"
    11  	"io/ioutil"
    12  
    13  	"github.com/v2fly/tools/internal/lsp/diff"
    14  	"github.com/v2fly/tools/internal/lsp/protocol"
    15  	"github.com/v2fly/tools/internal/lsp/source"
    16  	"github.com/v2fly/tools/internal/span"
    17  	"github.com/v2fly/tools/internal/tool"
    18  	errors "golang.org/x/xerrors"
    19  )
    20  
    21  // suggestedFix implements the fix verb for gopls.
    22  type suggestedFix struct {
    23  	Diff  bool `flag:"d" help:"display diffs instead of rewriting files"`
    24  	Write bool `flag:"w" help:"write result to (source) file instead of stdout"`
    25  	All   bool `flag:"a" help:"apply all fixes, not just preferred fixes"`
    26  
    27  	app *Application
    28  }
    29  
    30  func (s *suggestedFix) Name() string      { return "fix" }
    31  func (s *suggestedFix) Usage() string     { return "<filename>" }
    32  func (s *suggestedFix) ShortHelp() string { return "apply suggested fixes" }
    33  func (s *suggestedFix) DetailedHelp(f *flag.FlagSet) {
    34  	fmt.Fprintf(f.Output(), `
    35  Example: apply suggested fixes for this file:
    36  
    37    $ gopls fix -w internal/lsp/cmd/check.go
    38  
    39  gopls fix flags are:
    40  `)
    41  	f.PrintDefaults()
    42  }
    43  
    44  // Run performs diagnostic checks on the file specified and either;
    45  // - if -w is specified, updates the file in place;
    46  // - if -d is specified, prints out unified diffs of the changes; or
    47  // - otherwise, prints the new versions to stdout.
    48  func (s *suggestedFix) Run(ctx context.Context, args ...string) error {
    49  	if len(args) < 1 {
    50  		return tool.CommandLineErrorf("fix expects at least 1 argument")
    51  	}
    52  	conn, err := s.app.connect(ctx)
    53  	if err != nil {
    54  		return err
    55  	}
    56  	defer conn.terminate(ctx)
    57  
    58  	from := span.Parse(args[0])
    59  	uri := from.URI()
    60  	file := conn.AddFile(ctx, uri)
    61  	if file.err != nil {
    62  		return file.err
    63  	}
    64  
    65  	if err := conn.diagnoseFiles(ctx, []span.URI{uri}); err != nil {
    66  		return err
    67  	}
    68  	conn.Client.filesMu.Lock()
    69  	defer conn.Client.filesMu.Unlock()
    70  
    71  	codeActionKinds := []protocol.CodeActionKind{protocol.QuickFix}
    72  	if len(args) > 1 {
    73  		codeActionKinds = []protocol.CodeActionKind{}
    74  		for _, k := range args[1:] {
    75  			codeActionKinds = append(codeActionKinds, protocol.CodeActionKind(k))
    76  		}
    77  	}
    78  
    79  	rng, err := file.mapper.Range(from)
    80  	if err != nil {
    81  		return err
    82  	}
    83  	p := protocol.CodeActionParams{
    84  		TextDocument: protocol.TextDocumentIdentifier{
    85  			URI: protocol.URIFromSpanURI(uri),
    86  		},
    87  		Context: protocol.CodeActionContext{
    88  			Only:        codeActionKinds,
    89  			Diagnostics: file.diagnostics,
    90  		},
    91  		Range: rng,
    92  	}
    93  	actions, err := conn.CodeAction(ctx, &p)
    94  	if err != nil {
    95  		return errors.Errorf("%v: %v", from, err)
    96  	}
    97  	var edits []protocol.TextEdit
    98  	for _, a := range actions {
    99  		if a.Command != nil {
   100  			return fmt.Errorf("ExecuteCommand is not yet supported on the command line")
   101  		}
   102  		if !a.IsPreferred && !s.All {
   103  			continue
   104  		}
   105  		if !from.HasPosition() {
   106  			for _, c := range a.Edit.DocumentChanges {
   107  				if fileURI(c.TextDocument.URI) == uri {
   108  					edits = append(edits, c.Edits...)
   109  				}
   110  			}
   111  			continue
   112  		}
   113  		// If the span passed in has a position, then we need to find
   114  		// the codeaction that has the same range as the passed in span.
   115  		for _, diag := range a.Diagnostics {
   116  			spn, err := file.mapper.RangeSpan(diag.Range)
   117  			if err != nil {
   118  				continue
   119  			}
   120  			if span.ComparePoint(from.Start(), spn.Start()) == 0 {
   121  				for _, c := range a.Edit.DocumentChanges {
   122  					if fileURI(c.TextDocument.URI) == uri {
   123  						edits = append(edits, c.Edits...)
   124  					}
   125  				}
   126  				break
   127  			}
   128  		}
   129  
   130  		// If suggested fix is not a diagnostic, still must collect edits.
   131  		if len(a.Diagnostics) == 0 {
   132  			for _, c := range a.Edit.DocumentChanges {
   133  				if fileURI(c.TextDocument.URI) == uri {
   134  					edits = append(edits, c.Edits...)
   135  				}
   136  			}
   137  		}
   138  	}
   139  
   140  	sedits, err := source.FromProtocolEdits(file.mapper, edits)
   141  	if err != nil {
   142  		return errors.Errorf("%v: %v", edits, err)
   143  	}
   144  	newContent := diff.ApplyEdits(string(file.mapper.Content), sedits)
   145  
   146  	filename := file.uri.Filename()
   147  	switch {
   148  	case s.Write:
   149  		if len(edits) > 0 {
   150  			ioutil.WriteFile(filename, []byte(newContent), 0644)
   151  		}
   152  	case s.Diff:
   153  		diffs := diff.ToUnified(filename+".orig", filename, string(file.mapper.Content), sedits)
   154  		fmt.Print(diffs)
   155  	default:
   156  		fmt.Print(string(newContent))
   157  	}
   158  	return nil
   159  }