@@ -8,7 +8,10 @@ package source
8
8
import (
9
9
"bytes"
10
10
"context"
11
+ "go/ast"
11
12
"go/format"
13
+ "go/parser"
14
+ "go/token"
12
15
13
16
"golang.org/x/tools/internal/imports"
14
17
"golang.org/x/tools/internal/lsp/diff"
@@ -84,35 +87,44 @@ func formatSource(ctx context.Context, s Snapshot, f File) ([]byte, error) {
84
87
return format .Source (data )
85
88
}
86
89
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" )
90
101
defer done ()
91
102
92
103
_ , cphs , err := view .CheckPackageHandles (ctx , f )
93
104
if err != nil {
94
- return nil , err
105
+ return nil , nil , err
95
106
}
96
107
cph , err := NarrowestCheckPackageHandle (cphs )
97
108
if err != nil {
98
- return nil , err
109
+ return nil , nil , err
99
110
}
100
111
pkg , err := cph .Check (ctx )
101
112
if err != nil {
102
- return nil , err
113
+ return nil , nil , err
103
114
}
104
115
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 ())
106
117
}
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
+ }
110
123
}
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 ())
115
126
}
127
+
116
128
options := & imports.Options {
117
129
// Defaults.
118
130
AllErrors : true ,
@@ -122,122 +134,150 @@ func Imports(ctx context.Context, view View, f File) ([]protocol.TextEdit, error
122
134
TabIndent : true ,
123
135
TabWidth : 8 ,
124
136
}
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 )
132
139
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 )
139
141
if err != nil {
140
- return nil , err
142
+ return nil , nil , err
141
143
}
142
- return computeTextEdits (ctx , view , ph .File (), m , string (formatted ))
143
- }
144
144
145
- type ImportFix struct {
146
- Fix * imports.ImportFix
147
- Edits []protocol.TextEdit
145
+ return allFixEdits , editsPerFix , nil
148
146
}
149
147
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 ()
157
152
158
- _ , cphs , err := view .CheckPackageHandles (ctx , f )
153
+ // Build up basic information about the original file.
154
+ origData , _ , err := ph .File ().Read (ctx )
159
155
if err != nil {
160
156
return nil , nil , err
161
157
}
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 )
167
159
if err != nil {
168
160
return nil , nil , err
169
161
}
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 )
210
168
if err != nil {
211
- return err
169
+ return nil , err
212
170
}
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 )
214
173
if err != nil {
215
- return err
174
+ return nil , err
216
175
}
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 )
221
193
if err != nil {
222
- return err
194
+ return nil , err
223
195
}
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 )
225
207
if err != nil {
226
- return err
227
- }
228
- editsPerFix [i ] = & ImportFix {
229
- Fix : fix ,
230
- Edits : edits ,
208
+ return nil , err
231
209
}
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 )
232
213
}
233
- return nil
214
+ return ToProtocolEdits ( origMapper , edits )
234
215
}
235
- err = view .RunProcessEnvFunc (ctx , importFn , options )
216
+
217
+ allFixes , err := imports .FixImports (filename , origData , options )
236
218
if err != nil {
237
219
return nil , nil , err
238
220
}
239
221
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 ]
241
281
}
242
282
243
283
// CandidateImports returns every import that could be added to filename.
0 commit comments