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 }