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  }