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