github.com/jhump/golang-x-tools@v0.0.0-20220218190644-4958d6d39439/internal/lsp/source/format.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 source provides core features for use by Go editors and tools. 6 package source 7 8 import ( 9 "bytes" 10 "context" 11 "fmt" 12 "go/ast" 13 "go/format" 14 "go/parser" 15 "go/token" 16 "strings" 17 "text/scanner" 18 19 "github.com/jhump/golang-x-tools/internal/event" 20 "github.com/jhump/golang-x-tools/internal/imports" 21 "github.com/jhump/golang-x-tools/internal/lsp/diff" 22 "github.com/jhump/golang-x-tools/internal/lsp/lsppos" 23 "github.com/jhump/golang-x-tools/internal/lsp/protocol" 24 "github.com/jhump/golang-x-tools/internal/span" 25 ) 26 27 // Format formats a file with a given range. 28 func Format(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.TextEdit, error) { 29 ctx, done := event.Start(ctx, "source.Format") 30 defer done() 31 32 // Generated files shouldn't be edited. So, don't format them 33 if IsGenerated(ctx, snapshot, fh.URI()) { 34 return nil, fmt.Errorf("can't format %q: file is generated", fh.URI().Filename()) 35 } 36 37 pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) 38 if err != nil { 39 return nil, err 40 } 41 // Even if this file has parse errors, it might still be possible to format it. 42 // Using format.Node on an AST with errors may result in code being modified. 43 // Attempt to format the source of this file instead. 44 if pgf.ParseErr != nil { 45 formatted, err := formatSource(ctx, fh) 46 if err != nil { 47 return nil, err 48 } 49 return computeTextEdits(ctx, snapshot, pgf, string(formatted)) 50 } 51 52 fset := snapshot.FileSet() 53 54 // format.Node changes slightly from one release to another, so the version 55 // of Go used to build the LSP server will determine how it formats code. 56 // This should be acceptable for all users, who likely be prompted to rebuild 57 // the LSP server on each Go release. 58 buf := &bytes.Buffer{} 59 if err := format.Node(buf, fset, pgf.File); err != nil { 60 return nil, err 61 } 62 formatted := buf.String() 63 64 // Apply additional formatting, if any is supported. Currently, the only 65 // supported additional formatter is gofumpt. 66 if format := snapshot.View().Options().Hooks.GofumptFormat; snapshot.View().Options().Gofumpt && format != nil { 67 b, err := format(ctx, buf.Bytes()) 68 if err != nil { 69 return nil, err 70 } 71 formatted = string(b) 72 } 73 return computeTextEdits(ctx, snapshot, pgf, formatted) 74 } 75 76 func formatSource(ctx context.Context, fh FileHandle) ([]byte, error) { 77 _, done := event.Start(ctx, "source.formatSource") 78 defer done() 79 80 data, err := fh.Read() 81 if err != nil { 82 return nil, err 83 } 84 return format.Source(data) 85 } 86 87 type ImportFix struct { 88 Fix *imports.ImportFix 89 Edits []protocol.TextEdit 90 } 91 92 // AllImportsFixes formats f for each possible fix to the imports. 93 // In addition to returning the result of applying all edits, 94 // it returns a list of fixes that could be applied to the file, with the 95 // corresponding TextEdits that would be needed to apply that fix. 96 func AllImportsFixes(ctx context.Context, snapshot Snapshot, fh FileHandle) (allFixEdits []protocol.TextEdit, editsPerFix []*ImportFix, err error) { 97 ctx, done := event.Start(ctx, "source.AllImportsFixes") 98 defer done() 99 100 pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) 101 if err != nil { 102 return nil, nil, err 103 } 104 if err := snapshot.RunProcessEnvFunc(ctx, func(opts *imports.Options) error { 105 allFixEdits, editsPerFix, err = computeImportEdits(snapshot, pgf, opts) 106 return err 107 }); err != nil { 108 return nil, nil, fmt.Errorf("AllImportsFixes: %v", err) 109 } 110 return allFixEdits, editsPerFix, nil 111 } 112 113 // computeImportEdits computes a set of edits that perform one or all of the 114 // necessary import fixes. 115 func computeImportEdits(snapshot Snapshot, pgf *ParsedGoFile, options *imports.Options) (allFixEdits []protocol.TextEdit, editsPerFix []*ImportFix, err error) { 116 filename := pgf.URI.Filename() 117 118 // Build up basic information about the original file. 119 allFixes, err := imports.FixImports(filename, pgf.Src, options) 120 if err != nil { 121 return nil, nil, err 122 } 123 124 allFixEdits, err = computeFixEdits(snapshot, pgf, options, allFixes) 125 if err != nil { 126 return nil, nil, err 127 } 128 129 // Apply all of the import fixes to the file. 130 // Add the edits for each fix to the result. 131 for _, fix := range allFixes { 132 edits, err := computeFixEdits(snapshot, pgf, options, []*imports.ImportFix{fix}) 133 if err != nil { 134 return nil, nil, err 135 } 136 editsPerFix = append(editsPerFix, &ImportFix{ 137 Fix: fix, 138 Edits: edits, 139 }) 140 } 141 return allFixEdits, editsPerFix, nil 142 } 143 144 // ComputeOneImportFixEdits returns text edits for a single import fix. 145 func ComputeOneImportFixEdits(snapshot Snapshot, pgf *ParsedGoFile, fix *imports.ImportFix) ([]protocol.TextEdit, error) { 146 options := &imports.Options{ 147 LocalPrefix: snapshot.View().Options().Local, 148 // Defaults. 149 AllErrors: true, 150 Comments: true, 151 Fragment: true, 152 FormatOnly: false, 153 TabIndent: true, 154 TabWidth: 8, 155 } 156 return computeFixEdits(snapshot, pgf, options, []*imports.ImportFix{fix}) 157 } 158 159 func computeFixEdits(snapshot Snapshot, pgf *ParsedGoFile, options *imports.Options, fixes []*imports.ImportFix) ([]protocol.TextEdit, error) { 160 // trim the original data to match fixedData 161 left, err := importPrefix(pgf.Src) 162 if err != nil { 163 return nil, err 164 } 165 extra := !strings.Contains(left, "\n") // one line may have more than imports 166 if extra { 167 left = string(pgf.Src) 168 } 169 if len(left) > 0 && left[len(left)-1] != '\n' { 170 left += "\n" 171 } 172 // Apply the fixes and re-parse the file so that we can locate the 173 // new imports. 174 flags := parser.ImportsOnly 175 if extra { 176 // used all of origData above, use all of it here too 177 flags = 0 178 } 179 fixedData, err := imports.ApplyFixes(fixes, "", pgf.Src, options, flags) 180 if err != nil { 181 return nil, err 182 } 183 if fixedData == nil || fixedData[len(fixedData)-1] != '\n' { 184 fixedData = append(fixedData, '\n') // ApplyFixes may miss the newline, go figure. 185 } 186 edits, err := snapshot.View().Options().ComputeEdits(pgf.URI, left, string(fixedData)) 187 if err != nil { 188 return nil, err 189 } 190 return ProtocolEditsFromSource([]byte(left), edits, pgf.Mapper.Converter) 191 } 192 193 // importPrefix returns the prefix of the given file content through the final 194 // import statement. If there are no imports, the prefix is the package 195 // statement and any comment groups below it. 196 func importPrefix(src []byte) (string, error) { 197 fset := token.NewFileSet() 198 // do as little parsing as possible 199 f, err := parser.ParseFile(fset, "", src, parser.ImportsOnly|parser.ParseComments) 200 if err != nil { // This can happen if 'package' is misspelled 201 return "", fmt.Errorf("importPrefix: failed to parse: %s", err) 202 } 203 tok := fset.File(f.Pos()) 204 var importEnd int 205 for _, d := range f.Decls { 206 if x, ok := d.(*ast.GenDecl); ok && x.Tok == token.IMPORT { 207 if e, err := Offset(tok, d.End()); err != nil { 208 return "", fmt.Errorf("importPrefix: %s", err) 209 } else if e > importEnd { 210 importEnd = e 211 } 212 } 213 } 214 215 maybeAdjustToLineEnd := func(pos token.Pos, isCommentNode bool) int { 216 offset, err := Offset(tok, pos) 217 if err != nil { 218 return -1 219 } 220 221 // Don't go past the end of the file. 222 if offset > len(src) { 223 offset = len(src) 224 } 225 // The go/ast package does not account for different line endings, and 226 // specifically, in the text of a comment, it will strip out \r\n line 227 // endings in favor of \n. To account for these differences, we try to 228 // return a position on the next line whenever possible. 229 switch line := tok.Line(tok.Pos(offset)); { 230 case line < tok.LineCount(): 231 nextLineOffset, err := Offset(tok, tok.LineStart(line+1)) 232 if err != nil { 233 return -1 234 } 235 // If we found a position that is at the end of a line, move the 236 // offset to the start of the next line. 237 if offset+1 == nextLineOffset { 238 offset = nextLineOffset 239 } 240 case isCommentNode, offset+1 == tok.Size(): 241 // If the last line of the file is a comment, or we are at the end 242 // of the file, the prefix is the entire file. 243 offset = len(src) 244 } 245 return offset 246 } 247 if importEnd == 0 { 248 pkgEnd := f.Name.End() 249 importEnd = maybeAdjustToLineEnd(pkgEnd, false) 250 } 251 for _, cgroup := range f.Comments { 252 for _, c := range cgroup.List { 253 if end, err := Offset(tok, c.End()); err != nil { 254 return "", err 255 } else if end > importEnd { 256 startLine := tok.Position(c.Pos()).Line 257 endLine := tok.Position(c.End()).Line 258 259 // Work around golang/go#41197 by checking if the comment might 260 // contain "\r", and if so, find the actual end position of the 261 // comment by scanning the content of the file. 262 startOffset, err := Offset(tok, c.Pos()) 263 if err != nil { 264 return "", err 265 } 266 if startLine != endLine && bytes.Contains(src[startOffset:], []byte("\r")) { 267 if commentEnd := scanForCommentEnd(src[startOffset:]); commentEnd > 0 { 268 end = startOffset + commentEnd 269 } 270 } 271 importEnd = maybeAdjustToLineEnd(tok.Pos(end), true) 272 } 273 } 274 } 275 if importEnd > len(src) { 276 importEnd = len(src) 277 } 278 return string(src[:importEnd]), nil 279 } 280 281 // scanForCommentEnd returns the offset of the end of the multi-line comment 282 // at the start of the given byte slice. 283 func scanForCommentEnd(src []byte) int { 284 var s scanner.Scanner 285 s.Init(bytes.NewReader(src)) 286 s.Mode ^= scanner.SkipComments 287 288 t := s.Scan() 289 if t == scanner.Comment { 290 return s.Pos().Offset 291 } 292 return 0 293 } 294 295 func computeTextEdits(ctx context.Context, snapshot Snapshot, pgf *ParsedGoFile, formatted string) ([]protocol.TextEdit, error) { 296 _, done := event.Start(ctx, "source.computeTextEdits") 297 defer done() 298 299 edits, err := snapshot.View().Options().ComputeEdits(pgf.URI, string(pgf.Src), formatted) 300 if err != nil { 301 return nil, err 302 } 303 return ToProtocolEdits(pgf.Mapper, edits) 304 } 305 306 // ProtocolEditsFromSource converts text edits to LSP edits using the original 307 // source. 308 func ProtocolEditsFromSource(src []byte, edits []diff.TextEdit, converter span.Converter) ([]protocol.TextEdit, error) { 309 m := lsppos.NewMapper(src) 310 var result []protocol.TextEdit 311 for _, edit := range edits { 312 spn, err := edit.Span.WithOffset(converter) 313 if err != nil { 314 return nil, fmt.Errorf("computing offsets: %v", err) 315 } 316 startLine, startChar := m.Position(spn.Start().Offset()) 317 endLine, endChar := m.Position(spn.End().Offset()) 318 if startLine < 0 || endLine < 0 { 319 return nil, fmt.Errorf("out of bound span: %v", spn) 320 } 321 322 pstart := protocol.Position{Line: uint32(startLine), Character: uint32(startChar)} 323 pend := protocol.Position{Line: uint32(endLine), Character: uint32(endChar)} 324 if pstart == pend && edit.NewText == "" { 325 // Degenerate case, which may result from a diff tool wanting to delete 326 // '\r' in line endings. Filter it out. 327 continue 328 } 329 result = append(result, protocol.TextEdit{ 330 Range: protocol.Range{Start: pstart, End: pend}, 331 NewText: edit.NewText, 332 }) 333 } 334 return result, nil 335 } 336 337 func ToProtocolEdits(m *protocol.ColumnMapper, edits []diff.TextEdit) ([]protocol.TextEdit, error) { 338 if edits == nil { 339 return nil, nil 340 } 341 result := make([]protocol.TextEdit, len(edits)) 342 for i, edit := range edits { 343 rng, err := m.Range(edit.Span) 344 if err != nil { 345 return nil, err 346 } 347 result[i] = protocol.TextEdit{ 348 Range: rng, 349 NewText: edit.NewText, 350 } 351 } 352 return result, nil 353 } 354 355 func FromProtocolEdits(m *protocol.ColumnMapper, edits []protocol.TextEdit) ([]diff.TextEdit, error) { 356 if edits == nil { 357 return nil, nil 358 } 359 result := make([]diff.TextEdit, len(edits)) 360 for i, edit := range edits { 361 spn, err := m.RangeSpan(edit.Range) 362 if err != nil { 363 return nil, err 364 } 365 result[i] = diff.TextEdit{ 366 Span: spn, 367 NewText: edit.NewText, 368 } 369 } 370 return result, nil 371 }