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  }