mirror of
https://github.com/golang/go
synced 2024-11-18 16:14:46 -07:00
internal/lsp/source: don't format the whole file when adding imports
We want people to add imports as they need them. That means we probably don't want adding an import to reformat your whole file while you're in the middle of editing it. Unfortunately, the AST package doesn't offer any help with this -- there's no good way to get a diff out of it. Instead, we apply the changes, then diff a subset of the file. Picking that subset is tricky, see the code for details. Also delete a dead function, Imports, which should have been unused but was still being called in tests. Fixes golang/go#30843. Change-Id: I09a5344e910f65510003c4006ea5b11657922315 Reviewed-on: https://go-review.googlesource.com/c/tools/+/205678 Reviewed-by: Rebecca Stambler <rstambler@golang.org>
This commit is contained in:
parent
0c330b00b1
commit
8ceaad4c15
@ -8,7 +8,10 @@ package source
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"go/ast"
|
||||
"go/format"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
|
||||
"golang.org/x/tools/internal/imports"
|
||||
"golang.org/x/tools/internal/lsp/diff"
|
||||
@ -84,64 +87,6 @@ func formatSource(ctx context.Context, s Snapshot, f File) ([]byte, error) {
|
||||
return format.Source(data)
|
||||
}
|
||||
|
||||
// Imports formats a file using the goimports tool.
|
||||
func Imports(ctx context.Context, view View, f File) ([]protocol.TextEdit, error) {
|
||||
ctx, done := trace.StartSpan(ctx, "source.Imports")
|
||||
defer done()
|
||||
|
||||
_, cphs, err := view.CheckPackageHandles(ctx, f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cph, err := NarrowestCheckPackageHandle(cphs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pkg, err := cph.Check(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if hasListErrors(pkg) {
|
||||
return nil, errors.Errorf("%s has list errors, not running goimports", f.URI())
|
||||
}
|
||||
ph, err := pkg.File(f.URI())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Be extra careful that the file's ParseMode is correct,
|
||||
// otherwise we might replace the user's code with a trimmed AST.
|
||||
if ph.Mode() != ParseFull {
|
||||
return nil, errors.Errorf("%s was parsed in the incorrect mode", ph.File().Identity().URI)
|
||||
}
|
||||
options := &imports.Options{
|
||||
// Defaults.
|
||||
AllErrors: true,
|
||||
Comments: true,
|
||||
Fragment: true,
|
||||
FormatOnly: false,
|
||||
TabIndent: true,
|
||||
TabWidth: 8,
|
||||
}
|
||||
var formatted []byte
|
||||
importFn := func(opts *imports.Options) error {
|
||||
data, _, err := ph.File().Read(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
formatted, err = imports.Process(ph.File().Identity().URI.Filename(), data, opts)
|
||||
return err
|
||||
}
|
||||
err = view.RunProcessEnvFunc(ctx, importFn, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, m, _, err := ph.Parse(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return computeTextEdits(ctx, view, ph.File(), m, string(formatted))
|
||||
}
|
||||
|
||||
type ImportFix struct {
|
||||
Fix *imports.ImportFix
|
||||
Edits []protocol.TextEdit
|
||||
@ -151,7 +96,7 @@ type ImportFix struct {
|
||||
// In addition to returning the result of applying all edits,
|
||||
// it returns a list of fixes that could be applied to the file, with the
|
||||
// corresponding TextEdits that would be needed to apply that fix.
|
||||
func AllImportsFixes(ctx context.Context, view View, f File) (edits []protocol.TextEdit, editsPerFix []*ImportFix, err error) {
|
||||
func AllImportsFixes(ctx context.Context, view View, f File) (allFixEdits []protocol.TextEdit, editsPerFix []*ImportFix, err error) {
|
||||
ctx, done := trace.StartSpan(ctx, "source.AllImportsFixes")
|
||||
defer done()
|
||||
|
||||
@ -170,6 +115,16 @@ func AllImportsFixes(ctx context.Context, view View, f File) (edits []protocol.T
|
||||
if hasListErrors(pkg) {
|
||||
return nil, nil, errors.Errorf("%s has list errors, not running goimports", f.URI())
|
||||
}
|
||||
var ph ParseGoHandle
|
||||
for _, h := range pkg.Files() {
|
||||
if h.File().Identity().URI == f.URI() {
|
||||
ph = h
|
||||
}
|
||||
}
|
||||
if ph == nil {
|
||||
return nil, nil, errors.Errorf("no ParseGoHandle for %s", f.URI())
|
||||
}
|
||||
|
||||
options := &imports.Options{
|
||||
// Defaults.
|
||||
AllErrors: true,
|
||||
@ -179,65 +134,150 @@ func AllImportsFixes(ctx context.Context, view View, f File) (edits []protocol.T
|
||||
TabIndent: true,
|
||||
TabWidth: 8,
|
||||
}
|
||||
importFn := func(opts *imports.Options) error {
|
||||
var ph ParseGoHandle
|
||||
for _, h := range pkg.Files() {
|
||||
if h.File().Identity().URI == f.URI() {
|
||||
ph = h
|
||||
}
|
||||
}
|
||||
if ph == nil {
|
||||
return errors.Errorf("no ParseGoHandle for %s", f.URI())
|
||||
}
|
||||
data, _, err := ph.File().Read(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fixes, err := imports.FixImports(f.URI().Filename(), data, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Do not change the file if there are no import fixes.
|
||||
if len(fixes) == 0 {
|
||||
return nil
|
||||
}
|
||||
// Apply all of the import fixes to the file.
|
||||
formatted, err := imports.ApplyFixes(fixes, f.URI().Filename(), data, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, m, _, err := ph.Parse(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
edits, err = computeTextEdits(ctx, view, ph.File(), m, string(formatted))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Add the edits for each fix to the result.
|
||||
editsPerFix = make([]*ImportFix, len(fixes))
|
||||
for i, fix := range fixes {
|
||||
formatted, err := imports.ApplyFixes([]*imports.ImportFix{fix}, f.URI().Filename(), data, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
edits, err := computeTextEdits(ctx, view, ph.File(), m, string(formatted))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
editsPerFix[i] = &ImportFix{
|
||||
Fix: fix,
|
||||
Edits: edits,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
err = view.RunProcessEnvFunc(ctx, importFn, options)
|
||||
err = view.RunProcessEnvFunc(ctx, func(opts *imports.Options) error {
|
||||
allFixEdits, editsPerFix, err = computeImportEdits(ctx, view, ph, opts)
|
||||
return err
|
||||
}, options)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return edits, editsPerFix, nil
|
||||
return allFixEdits, editsPerFix, nil
|
||||
}
|
||||
|
||||
// computeImportEdits computes a set of edits that perform one or all of the
|
||||
// necessary import fixes.
|
||||
func computeImportEdits(ctx context.Context, view View, ph ParseGoHandle, options *imports.Options) (allFixEdits []protocol.TextEdit, editsPerFix []*ImportFix, err error) {
|
||||
filename := ph.File().Identity().URI.Filename()
|
||||
|
||||
// Build up basic information about the original file.
|
||||
origData, _, err := ph.File().Read(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
origAST, origMapper, _, err := ph.Parse(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
origImports, origImportOffset := trimToImports(view.Session().Cache().FileSet(), origAST, origData)
|
||||
|
||||
computeFixEdits := func(fixes []*imports.ImportFix) ([]protocol.TextEdit, error) {
|
||||
// Apply the fixes and re-parse the file so that we can locate the
|
||||
// new imports.
|
||||
fixedData, err := imports.ApplyFixes(fixes, filename, origData, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fixedFset := token.NewFileSet()
|
||||
fixedAST, err := parser.ParseFile(fixedFset, filename, fixedData, parser.ImportsOnly)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fixedImports, fixedImportsOffset := trimToImports(fixedFset, fixedAST, fixedData)
|
||||
|
||||
// Prepare the diff. If both sides had import statements, we can diff
|
||||
// just those sections against each other, then shift the resulting
|
||||
// edits to the right lines in the original file.
|
||||
left, right := origImports, fixedImports
|
||||
converter := span.NewContentConverter(filename, origImports)
|
||||
offset := origImportOffset
|
||||
|
||||
// If one side or the other has no imports, we won't know where to
|
||||
// anchor the diffs. Instead, use the beginning of the file, up to its
|
||||
// first non-imports decl. We know the imports code will insert
|
||||
// somewhere before that.
|
||||
if origImportOffset == 0 || fixedImportsOffset == 0 {
|
||||
left = trimToFirstNonImport(view.Session().Cache().FileSet(), origAST, origData)
|
||||
// We need the whole AST here, not just the ImportsOnly AST we parsed above.
|
||||
fixedAST, err = parser.ParseFile(fixedFset, filename, fixedData, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
right = trimToFirstNonImport(fixedFset, fixedAST, fixedData)
|
||||
// We're now working with a prefix of the original file, so we can
|
||||
// use the original converter, and there is no offset on the edits.
|
||||
converter = origMapper.Converter
|
||||
offset = 0
|
||||
}
|
||||
|
||||
// Perform the diff and adjust the results for the trimming, if any.
|
||||
edits := view.Options().ComputeEdits(ph.File().Identity().URI, string(left), string(right))
|
||||
for i := range edits {
|
||||
s, err := edits[i].Span.WithPosition(converter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
start := span.NewPoint(s.Start().Line()+offset, s.Start().Column(), -1)
|
||||
end := span.NewPoint(s.End().Line()+offset, s.End().Column(), -1)
|
||||
edits[i].Span = span.New(s.URI(), start, end)
|
||||
}
|
||||
return ToProtocolEdits(origMapper, edits)
|
||||
}
|
||||
|
||||
allFixes, err := imports.FixImports(filename, origData, options)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
allFixEdits, err = computeFixEdits(allFixes)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Apply all of the import fixes to the file.
|
||||
// Add the edits for each fix to the result.
|
||||
for _, fix := range allFixes {
|
||||
edits, err := computeFixEdits([]*imports.ImportFix{fix})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
editsPerFix = append(editsPerFix, &ImportFix{
|
||||
Fix: fix,
|
||||
Edits: edits,
|
||||
})
|
||||
}
|
||||
return allFixEdits, editsPerFix, nil
|
||||
}
|
||||
|
||||
// trimToImports returns a section of the source file that covers all of the
|
||||
// import declarations, and the line offset into the file that section starts at.
|
||||
func trimToImports(fset *token.FileSet, f *ast.File, src []byte) ([]byte, int) {
|
||||
var firstImport, lastImport ast.Decl
|
||||
for _, decl := range f.Decls {
|
||||
if gen, ok := decl.(*ast.GenDecl); ok && gen.Tok == token.IMPORT {
|
||||
if firstImport == nil {
|
||||
firstImport = decl
|
||||
}
|
||||
lastImport = decl
|
||||
}
|
||||
}
|
||||
|
||||
if firstImport == nil {
|
||||
return nil, 0
|
||||
}
|
||||
start := firstImport.Pos()
|
||||
end := fset.File(f.Pos()).LineStart(fset.Position(lastImport.End()).Line + 1)
|
||||
startLineOffset := fset.Position(start).Line - 1 // lines are 1-indexed.
|
||||
return src[fset.Position(firstImport.Pos()).Offset:fset.Position(end).Offset], startLineOffset
|
||||
}
|
||||
|
||||
// trimToFirstNonImport returns src from the beginning to the first non-import
|
||||
// declaration, or the end of the file if there is no such decl.
|
||||
func trimToFirstNonImport(fset *token.FileSet, f *ast.File, src []byte) []byte {
|
||||
var firstDecl ast.Decl
|
||||
for _, decl := range f.Decls {
|
||||
if gen, ok := decl.(*ast.GenDecl); ok && gen.Tok == token.IMPORT {
|
||||
continue
|
||||
}
|
||||
firstDecl = decl
|
||||
break
|
||||
}
|
||||
|
||||
end := f.End()
|
||||
if firstDecl != nil {
|
||||
end = fset.File(f.Pos()).LineStart(fset.Position(firstDecl.Pos()).Line - 1)
|
||||
}
|
||||
return src[fset.Position(f.Pos()).Offset:fset.Position(end).Offset]
|
||||
}
|
||||
|
||||
// CandidateImports returns every import that could be added to filename.
|
||||
|
@ -450,22 +450,14 @@ func (r *runner) Import(t *testing.T, spn span.Span) {
|
||||
ctx := r.ctx
|
||||
uri := spn.URI()
|
||||
filename := uri.Filename()
|
||||
goimported := string(r.data.Golden("goimports", filename, func() ([]byte, error) {
|
||||
cmd := exec.Command("goimports", filename)
|
||||
out, _ := cmd.Output() // ignore error, sometimes we have intentionally ungofmt-able files
|
||||
return out, nil
|
||||
}))
|
||||
f, err := r.view.GetFile(ctx, uri)
|
||||
if err != nil {
|
||||
t.Fatalf("failed for %v: %v", spn, err)
|
||||
}
|
||||
fh := r.view.Snapshot().Handle(r.ctx, f)
|
||||
edits, err := source.Imports(ctx, r.view, f)
|
||||
edits, _, err := source.AllImportsFixes(ctx, r.view, f)
|
||||
if err != nil {
|
||||
if goimported != "" {
|
||||
t.Error(err)
|
||||
}
|
||||
return
|
||||
t.Error(err)
|
||||
}
|
||||
data, _, err := fh.Read(ctx)
|
||||
if err != nil {
|
||||
@ -480,8 +472,11 @@ func (r *runner) Import(t *testing.T, spn span.Span) {
|
||||
t.Error(err)
|
||||
}
|
||||
got := diff.ApplyEdits(string(data), diffEdits)
|
||||
if goimported != got {
|
||||
t.Errorf("import failed for %s, expected:\n%v\ngot:\n%v", filename, goimported, got)
|
||||
want := string(r.data.Golden("goimports", filename, func() ([]byte, error) {
|
||||
return []byte(got), nil
|
||||
}))
|
||||
if want != got {
|
||||
t.Errorf("import failed for %s, expected:\n%v\ngot:\n%v", filename, want, got)
|
||||
}
|
||||
}
|
||||
|
||||
|
13
internal/lsp/testdata/imports/add_import.go.golden
vendored
Normal file
13
internal/lsp/testdata/imports/add_import.go.golden
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
-- goimports --
|
||||
package imports //@import("package")
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func _() {
|
||||
fmt.Println("")
|
||||
bytes.NewBuffer(nil)
|
||||
}
|
||||
|
10
internal/lsp/testdata/imports/add_import.go.in
vendored
Normal file
10
internal/lsp/testdata/imports/add_import.go.in
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
package imports //@import("package")
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func _() {
|
||||
fmt.Println("")
|
||||
bytes.NewBuffer(nil)
|
||||
}
|
@ -4,6 +4,6 @@ package imports //@import("package")
|
||||
import "fmt"
|
||||
|
||||
func _() {
|
||||
fmt.Println("")
|
||||
fmt.Println("")
|
||||
}
|
||||
|
||||
|
7
internal/lsp/testdata/imports/good_imports.go.in
vendored
Normal file
7
internal/lsp/testdata/imports/good_imports.go.in
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
package imports //@import("package")
|
||||
|
||||
import "fmt"
|
||||
|
||||
func _() {
|
||||
fmt.Println("")
|
||||
}
|
9
internal/lsp/testdata/imports/multiple_blocks.go.golden
vendored
Normal file
9
internal/lsp/testdata/imports/multiple_blocks.go.golden
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
-- goimports --
|
||||
package imports //@import("package")
|
||||
|
||||
import "fmt"
|
||||
|
||||
func _() {
|
||||
fmt.Println("")
|
||||
}
|
||||
|
@ -2,6 +2,8 @@ package imports //@import("package")
|
||||
|
||||
import "fmt"
|
||||
|
||||
import "bytes"
|
||||
|
||||
func _() {
|
||||
fmt.Println("")
|
||||
}
|
11
internal/lsp/testdata/imports/remove_import.go.golden
vendored
Normal file
11
internal/lsp/testdata/imports/remove_import.go.golden
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
-- goimports --
|
||||
package imports //@import("package")
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func _() {
|
||||
fmt.Println("")
|
||||
}
|
||||
|
10
internal/lsp/testdata/imports/remove_import.go.in
vendored
Normal file
10
internal/lsp/testdata/imports/remove_import.go.in
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
package imports //@import("package")
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func _() {
|
||||
fmt.Println("")
|
||||
}
|
6
internal/lsp/testdata/imports/remove_imports.go.golden
vendored
Normal file
6
internal/lsp/testdata/imports/remove_imports.go.golden
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
-- goimports --
|
||||
package imports //@import("package")
|
||||
|
||||
func _() {
|
||||
}
|
||||
|
9
internal/lsp/testdata/imports/remove_imports.go.in
vendored
Normal file
9
internal/lsp/testdata/imports/remove_imports.go.in
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
package imports //@import("package")
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func _() {
|
||||
}
|
2
internal/lsp/testdata/summary.txt.golden
vendored
2
internal/lsp/testdata/summary.txt.golden
vendored
@ -9,7 +9,7 @@ CaseSensitiveCompletionsCount = 4
|
||||
DiagnosticsCount = 22
|
||||
FoldingRangesCount = 2
|
||||
FormatCount = 6
|
||||
ImportCount = 2
|
||||
ImportCount = 6
|
||||
SuggestedFixCount = 1
|
||||
DefinitionsCount = 38
|
||||
TypeDefinitionsCount = 2
|
||||
|
Loading…
Reference in New Issue
Block a user