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