Skip to content

Commit 8ceaad4

Browse files
committed
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 <[email protected]>
1 parent 0c330b0 commit 8ceaad4

14 files changed

+229
-117
lines changed

internal/lsp/source/format.go

Lines changed: 143 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ package source
88
import (
99
"bytes"
1010
"context"
11+
"go/ast"
1112
"go/format"
13+
"go/parser"
14+
"go/token"
1215

1316
"golang.org/x/tools/internal/imports"
1417
"golang.org/x/tools/internal/lsp/diff"
@@ -84,35 +87,44 @@ func formatSource(ctx context.Context, s Snapshot, f File) ([]byte, error) {
8487
return format.Source(data)
8588
}
8689

87-
// Imports formats a file using the goimports tool.
88-
func Imports(ctx context.Context, view View, f File) ([]protocol.TextEdit, error) {
89-
ctx, done := trace.StartSpan(ctx, "source.Imports")
90+
type ImportFix struct {
91+
Fix *imports.ImportFix
92+
Edits []protocol.TextEdit
93+
}
94+
95+
// AllImportsFixes formats f for each possible fix to the imports.
96+
// In addition to returning the result of applying all edits,
97+
// it returns a list of fixes that could be applied to the file, with the
98+
// corresponding TextEdits that would be needed to apply that fix.
99+
func AllImportsFixes(ctx context.Context, view View, f File) (allFixEdits []protocol.TextEdit, editsPerFix []*ImportFix, err error) {
100+
ctx, done := trace.StartSpan(ctx, "source.AllImportsFixes")
90101
defer done()
91102

92103
_, cphs, err := view.CheckPackageHandles(ctx, f)
93104
if err != nil {
94-
return nil, err
105+
return nil, nil, err
95106
}
96107
cph, err := NarrowestCheckPackageHandle(cphs)
97108
if err != nil {
98-
return nil, err
109+
return nil, nil, err
99110
}
100111
pkg, err := cph.Check(ctx)
101112
if err != nil {
102-
return nil, err
113+
return nil, nil, err
103114
}
104115
if hasListErrors(pkg) {
105-
return nil, errors.Errorf("%s has list errors, not running goimports", f.URI())
116+
return nil, nil, errors.Errorf("%s has list errors, not running goimports", f.URI())
106117
}
107-
ph, err := pkg.File(f.URI())
108-
if err != nil {
109-
return nil, err
118+
var ph ParseGoHandle
119+
for _, h := range pkg.Files() {
120+
if h.File().Identity().URI == f.URI() {
121+
ph = h
122+
}
110123
}
111-
// Be extra careful that the file's ParseMode is correct,
112-
// otherwise we might replace the user's code with a trimmed AST.
113-
if ph.Mode() != ParseFull {
114-
return nil, errors.Errorf("%s was parsed in the incorrect mode", ph.File().Identity().URI)
124+
if ph == nil {
125+
return nil, nil, errors.Errorf("no ParseGoHandle for %s", f.URI())
115126
}
127+
116128
options := &imports.Options{
117129
// Defaults.
118130
AllErrors: true,
@@ -122,122 +134,150 @@ func Imports(ctx context.Context, view View, f File) ([]protocol.TextEdit, error
122134
TabIndent: true,
123135
TabWidth: 8,
124136
}
125-
var formatted []byte
126-
importFn := func(opts *imports.Options) error {
127-
data, _, err := ph.File().Read(ctx)
128-
if err != nil {
129-
return err
130-
}
131-
formatted, err = imports.Process(ph.File().Identity().URI.Filename(), data, opts)
137+
err = view.RunProcessEnvFunc(ctx, func(opts *imports.Options) error {
138+
allFixEdits, editsPerFix, err = computeImportEdits(ctx, view, ph, opts)
132139
return err
133-
}
134-
err = view.RunProcessEnvFunc(ctx, importFn, options)
135-
if err != nil {
136-
return nil, err
137-
}
138-
_, m, _, err := ph.Parse(ctx)
140+
}, options)
139141
if err != nil {
140-
return nil, err
142+
return nil, nil, err
141143
}
142-
return computeTextEdits(ctx, view, ph.File(), m, string(formatted))
143-
}
144144

145-
type ImportFix struct {
146-
Fix *imports.ImportFix
147-
Edits []protocol.TextEdit
145+
return allFixEdits, editsPerFix, nil
148146
}
149147

150-
// AllImportsFixes formats f for each possible fix to the imports.
151-
// In addition to returning the result of applying all edits,
152-
// it returns a list of fixes that could be applied to the file, with the
153-
// corresponding TextEdits that would be needed to apply that fix.
154-
func AllImportsFixes(ctx context.Context, view View, f File) (edits []protocol.TextEdit, editsPerFix []*ImportFix, err error) {
155-
ctx, done := trace.StartSpan(ctx, "source.AllImportsFixes")
156-
defer done()
148+
// computeImportEdits computes a set of edits that perform one or all of the
149+
// necessary import fixes.
150+
func computeImportEdits(ctx context.Context, view View, ph ParseGoHandle, options *imports.Options) (allFixEdits []protocol.TextEdit, editsPerFix []*ImportFix, err error) {
151+
filename := ph.File().Identity().URI.Filename()
157152

158-
_, cphs, err := view.CheckPackageHandles(ctx, f)
153+
// Build up basic information about the original file.
154+
origData, _, err := ph.File().Read(ctx)
159155
if err != nil {
160156
return nil, nil, err
161157
}
162-
cph, err := NarrowestCheckPackageHandle(cphs)
163-
if err != nil {
164-
return nil, nil, err
165-
}
166-
pkg, err := cph.Check(ctx)
158+
origAST, origMapper, _, err := ph.Parse(ctx)
167159
if err != nil {
168160
return nil, nil, err
169161
}
170-
if hasListErrors(pkg) {
171-
return nil, nil, errors.Errorf("%s has list errors, not running goimports", f.URI())
172-
}
173-
options := &imports.Options{
174-
// Defaults.
175-
AllErrors: true,
176-
Comments: true,
177-
Fragment: true,
178-
FormatOnly: false,
179-
TabIndent: true,
180-
TabWidth: 8,
181-
}
182-
importFn := func(opts *imports.Options) error {
183-
var ph ParseGoHandle
184-
for _, h := range pkg.Files() {
185-
if h.File().Identity().URI == f.URI() {
186-
ph = h
187-
}
188-
}
189-
if ph == nil {
190-
return errors.Errorf("no ParseGoHandle for %s", f.URI())
191-
}
192-
data, _, err := ph.File().Read(ctx)
193-
if err != nil {
194-
return err
195-
}
196-
fixes, err := imports.FixImports(f.URI().Filename(), data, opts)
197-
if err != nil {
198-
return err
199-
}
200-
// Do not change the file if there are no import fixes.
201-
if len(fixes) == 0 {
202-
return nil
203-
}
204-
// Apply all of the import fixes to the file.
205-
formatted, err := imports.ApplyFixes(fixes, f.URI().Filename(), data, options)
206-
if err != nil {
207-
return err
208-
}
209-
_, m, _, err := ph.Parse(ctx)
162+
origImports, origImportOffset := trimToImports(view.Session().Cache().FileSet(), origAST, origData)
163+
164+
computeFixEdits := func(fixes []*imports.ImportFix) ([]protocol.TextEdit, error) {
165+
// Apply the fixes and re-parse the file so that we can locate the
166+
// new imports.
167+
fixedData, err := imports.ApplyFixes(fixes, filename, origData, options)
210168
if err != nil {
211-
return err
169+
return nil, err
212170
}
213-
edits, err = computeTextEdits(ctx, view, ph.File(), m, string(formatted))
171+
fixedFset := token.NewFileSet()
172+
fixedAST, err := parser.ParseFile(fixedFset, filename, fixedData, parser.ImportsOnly)
214173
if err != nil {
215-
return err
174+
return nil, err
216175
}
217-
// Add the edits for each fix to the result.
218-
editsPerFix = make([]*ImportFix, len(fixes))
219-
for i, fix := range fixes {
220-
formatted, err := imports.ApplyFixes([]*imports.ImportFix{fix}, f.URI().Filename(), data, options)
176+
fixedImports, fixedImportsOffset := trimToImports(fixedFset, fixedAST, fixedData)
177+
178+
// Prepare the diff. If both sides had import statements, we can diff
179+
// just those sections against each other, then shift the resulting
180+
// edits to the right lines in the original file.
181+
left, right := origImports, fixedImports
182+
converter := span.NewContentConverter(filename, origImports)
183+
offset := origImportOffset
184+
185+
// If one side or the other has no imports, we won't know where to
186+
// anchor the diffs. Instead, use the beginning of the file, up to its
187+
// first non-imports decl. We know the imports code will insert
188+
// somewhere before that.
189+
if origImportOffset == 0 || fixedImportsOffset == 0 {
190+
left = trimToFirstNonImport(view.Session().Cache().FileSet(), origAST, origData)
191+
// We need the whole AST here, not just the ImportsOnly AST we parsed above.
192+
fixedAST, err = parser.ParseFile(fixedFset, filename, fixedData, 0)
221193
if err != nil {
222-
return err
194+
return nil, err
223195
}
224-
edits, err := computeTextEdits(ctx, view, ph.File(), m, string(formatted))
196+
right = trimToFirstNonImport(fixedFset, fixedAST, fixedData)
197+
// We're now working with a prefix of the original file, so we can
198+
// use the original converter, and there is no offset on the edits.
199+
converter = origMapper.Converter
200+
offset = 0
201+
}
202+
203+
// Perform the diff and adjust the results for the trimming, if any.
204+
edits := view.Options().ComputeEdits(ph.File().Identity().URI, string(left), string(right))
205+
for i := range edits {
206+
s, err := edits[i].Span.WithPosition(converter)
225207
if err != nil {
226-
return err
227-
}
228-
editsPerFix[i] = &ImportFix{
229-
Fix: fix,
230-
Edits: edits,
208+
return nil, err
231209
}
210+
start := span.NewPoint(s.Start().Line()+offset, s.Start().Column(), -1)
211+
end := span.NewPoint(s.End().Line()+offset, s.End().Column(), -1)
212+
edits[i].Span = span.New(s.URI(), start, end)
232213
}
233-
return nil
214+
return ToProtocolEdits(origMapper, edits)
234215
}
235-
err = view.RunProcessEnvFunc(ctx, importFn, options)
216+
217+
allFixes, err := imports.FixImports(filename, origData, options)
236218
if err != nil {
237219
return nil, nil, err
238220
}
239221

240-
return edits, editsPerFix, nil
222+
allFixEdits, err = computeFixEdits(allFixes)
223+
if err != nil {
224+
return nil, nil, err
225+
}
226+
227+
// Apply all of the import fixes to the file.
228+
// Add the edits for each fix to the result.
229+
for _, fix := range allFixes {
230+
edits, err := computeFixEdits([]*imports.ImportFix{fix})
231+
if err != nil {
232+
return nil, nil, err
233+
}
234+
editsPerFix = append(editsPerFix, &ImportFix{
235+
Fix: fix,
236+
Edits: edits,
237+
})
238+
}
239+
return allFixEdits, editsPerFix, nil
240+
}
241+
242+
// trimToImports returns a section of the source file that covers all of the
243+
// import declarations, and the line offset into the file that section starts at.
244+
func trimToImports(fset *token.FileSet, f *ast.File, src []byte) ([]byte, int) {
245+
var firstImport, lastImport ast.Decl
246+
for _, decl := range f.Decls {
247+
if gen, ok := decl.(*ast.GenDecl); ok && gen.Tok == token.IMPORT {
248+
if firstImport == nil {
249+
firstImport = decl
250+
}
251+
lastImport = decl
252+
}
253+
}
254+
255+
if firstImport == nil {
256+
return nil, 0
257+
}
258+
start := firstImport.Pos()
259+
end := fset.File(f.Pos()).LineStart(fset.Position(lastImport.End()).Line + 1)
260+
startLineOffset := fset.Position(start).Line - 1 // lines are 1-indexed.
261+
return src[fset.Position(firstImport.Pos()).Offset:fset.Position(end).Offset], startLineOffset
262+
}
263+
264+
// trimToFirstNonImport returns src from the beginning to the first non-import
265+
// declaration, or the end of the file if there is no such decl.
266+
func trimToFirstNonImport(fset *token.FileSet, f *ast.File, src []byte) []byte {
267+
var firstDecl ast.Decl
268+
for _, decl := range f.Decls {
269+
if gen, ok := decl.(*ast.GenDecl); ok && gen.Tok == token.IMPORT {
270+
continue
271+
}
272+
firstDecl = decl
273+
break
274+
}
275+
276+
end := f.End()
277+
if firstDecl != nil {
278+
end = fset.File(f.Pos()).LineStart(fset.Position(firstDecl.Pos()).Line - 1)
279+
}
280+
return src[fset.Position(f.Pos()).Offset:fset.Position(end).Offset]
241281
}
242282

243283
// CandidateImports returns every import that could be added to filename.

internal/lsp/source/source_test.go

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -450,22 +450,14 @@ func (r *runner) Import(t *testing.T, spn span.Span) {
450450
ctx := r.ctx
451451
uri := spn.URI()
452452
filename := uri.Filename()
453-
goimported := string(r.data.Golden("goimports", filename, func() ([]byte, error) {
454-
cmd := exec.Command("goimports", filename)
455-
out, _ := cmd.Output() // ignore error, sometimes we have intentionally ungofmt-able files
456-
return out, nil
457-
}))
458453
f, err := r.view.GetFile(ctx, uri)
459454
if err != nil {
460455
t.Fatalf("failed for %v: %v", spn, err)
461456
}
462457
fh := r.view.Snapshot().Handle(r.ctx, f)
463-
edits, err := source.Imports(ctx, r.view, f)
458+
edits, _, err := source.AllImportsFixes(ctx, r.view, f)
464459
if err != nil {
465-
if goimported != "" {
466-
t.Error(err)
467-
}
468-
return
460+
t.Error(err)
469461
}
470462
data, _, err := fh.Read(ctx)
471463
if err != nil {
@@ -480,8 +472,11 @@ func (r *runner) Import(t *testing.T, spn span.Span) {
480472
t.Error(err)
481473
}
482474
got := diff.ApplyEdits(string(data), diffEdits)
483-
if goimported != got {
484-
t.Errorf("import failed for %s, expected:\n%v\ngot:\n%v", filename, goimported, got)
475+
want := string(r.data.Golden("goimports", filename, func() ([]byte, error) {
476+
return []byte(got), nil
477+
}))
478+
if want != got {
479+
t.Errorf("import failed for %s, expected:\n%v\ngot:\n%v", filename, want, got)
485480
}
486481
}
487482

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
-- goimports --
2+
package imports //@import("package")
3+
4+
import (
5+
"bytes"
6+
"fmt"
7+
)
8+
9+
func _() {
10+
fmt.Println("")
11+
bytes.NewBuffer(nil)
12+
}
13+
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package imports //@import("package")
2+
3+
import (
4+
"fmt"
5+
)
6+
7+
func _() {
8+
fmt.Println("")
9+
bytes.NewBuffer(nil)
10+
}

internal/lsp/testdata/imports/good_imports.go.golden

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ package imports //@import("package")
44
import "fmt"
55

66
func _() {
7-
fmt.Println("")
7+
fmt.Println("")
88
}
99

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package imports //@import("package")
2+
3+
import "fmt"
4+
5+
func _() {
6+
fmt.Println("")
7+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
-- goimports --
2+
package imports //@import("package")
3+
4+
import "fmt"
5+
6+
func _() {
7+
fmt.Println("")
8+
}
9+

0 commit comments

Comments
 (0)