this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

internal/lsp: implement organise imports code action

For now, this is limited to sorting blocks of imports, and removing
unused imports.

Signed-off-by: Matthew Sackman <matthew@cue.works>
Change-Id: I8810191c56bdb4df00b5e674ac364051bc689b5a
Reviewed-on: https://review.gerrithub.io/c/cue-lang/cue/+/1235340
TryBot-Result: CUEcueckoo <cueckoo@cuelang.org>
Reviewed-by: Daniel Martí <mvdan@mvdan.cc>
Unity-Result: CUE porcuepine <cue.porcuepine@gmail.com>

+354 -1
+247
cmd/cue/cmd/integration/workspace/organizeimports_test.go
··· 1 + package workspace 2 + 3 + import ( 4 + "slices" 5 + "testing" 6 + 7 + "cuelang.org/go/internal/golangorgx/gopls/protocol" 8 + . "cuelang.org/go/internal/golangorgx/gopls/test/integration" 9 + "github.com/go-quicktest/qt" 10 + ) 11 + 12 + func TestCodeActionOrganizeImports(t *testing.T) { 13 + type testCase struct { 14 + name string 15 + input string 16 + expected string 17 + } 18 + testCases := []testCase{ 19 + { 20 + name: "empty", 21 + input: "package p1\n", 22 + expected: "package p1\n", 23 + }, 24 + 25 + { 26 + name: "used_single", 27 + input: ` 28 + package p1 29 + 30 + import "mod.com/p2" 31 + 32 + x: p2 33 + `[1:], 34 + expected: ` 35 + package p1 36 + 37 + import "mod.com/p2" 38 + 39 + x: p2 40 + `[1:], 41 + }, 42 + 43 + { 44 + name: "used_multiple_separate", 45 + input: ` 46 + package p1 47 + 48 + import "mod.com/p3" 49 + import "mod.com/p2" 50 + 51 + x: p2 & p3 52 + `[1:], 53 + expected: ` 54 + package p1 55 + 56 + import "mod.com/p3" 57 + import "mod.com/p2" 58 + 59 + x: p2 & p3 60 + `[1:], 61 + }, 62 + 63 + { 64 + name: "used_multiple_joined", 65 + input: ` 66 + package p1 67 + 68 + import ( 69 + "mod.com/p3" 70 + "mod.com/p2" 71 + ) 72 + 73 + x: p2 & p3 74 + `[1:], 75 + expected: ` 76 + package p1 77 + 78 + import ( 79 + "mod.com/p2" 80 + "mod.com/p3" 81 + ) 82 + 83 + x: p2 & p3 84 + `[1:], 85 + }, 86 + 87 + { 88 + name: "mixed_separate", 89 + input: ` 90 + package p1 91 + 92 + import "mod.com/p3" 93 + import "mod.com/p2" 94 + 95 + x: p3 96 + `[1:], 97 + expected: ` 98 + package p1 99 + 100 + import "mod.com/p3" 101 + 102 + 103 + x: p3 104 + `[1:], 105 + }, 106 + 107 + { 108 + name: "mixed_joined_one_survives", 109 + input: ` 110 + package p1 111 + 112 + import ( 113 + "mod.com/p3" 114 + "mod.com/p2") 115 + 116 + x: p3 117 + `[1:], 118 + expected: ` 119 + package p1 120 + 121 + import "mod.com/p3" 122 + 123 + x: p3 124 + `[1:], 125 + }, 126 + 127 + { 128 + name: "mixed_joined_several_survive", 129 + input: ` 130 + package p1 131 + 132 + import ( 133 + "mod.com/p3" 134 + "mod.com/p4" 135 + "mod.com/p2") 136 + 137 + x: p4 & p2 138 + `[1:], 139 + expected: ` 140 + package p1 141 + 142 + import ( 143 + "mod.com/p2" 144 + "mod.com/p4" 145 + ) 146 + 147 + x: p4 & p2 148 + `[1:], 149 + }, 150 + 151 + { 152 + name: "mixed_mixed", 153 + input: ` 154 + package p1 155 + 156 + import ( 157 + "mod.com/p3" 158 + "mod.com/p2" 159 + ) 160 + 161 + import "mod.com/p1" 162 + 163 + import ( 164 + "mod.com/p4" 165 + "mod.com/p7" 166 + "mod.com/p6") 167 + 168 + import "mod.com/p5" 169 + 170 + x: p1 & p4 & p7 171 + `[1:], 172 + expected: ` 173 + package p1 174 + 175 + 176 + 177 + import "mod.com/p1" 178 + 179 + import ( 180 + "mod.com/p4" 181 + "mod.com/p7" 182 + ) 183 + 184 + 185 + 186 + x: p1 & p4 & p7 187 + `[1:], 188 + }, 189 + } 190 + 191 + for _, tc := range testCases { 192 + fun := func(t *testing.T, env *Env) { 193 + resolveSupport, err := env.Editor.EditResolveSupport() 194 + if err != nil { 195 + t.Fatal(err) 196 + } 197 + 198 + env.OpenFile("input.cue") 199 + env.Await(env.DoneWithOpen()) 200 + rootURI := env.Sandbox.Workdir.RootURI() 201 + 202 + cursor := protocol.Location{URI: rootURI + "/input.cue"} 203 + 204 + actions, err := env.Editor.CodeAction(env.Ctx, cursor, nil) 205 + if err != nil { 206 + qt.Assert(t, qt.IsNil(err)) 207 + } 208 + 209 + var action protocol.CodeAction 210 + found := slices.ContainsFunc(actions, func(a protocol.CodeAction) bool { 211 + if a.Title == "Organize Imports" { 212 + action = a 213 + return true 214 + } 215 + return false 216 + }) 217 + if !found { 218 + t.Fatal("Failed to find Organize Imports code action") 219 + } 220 + // If we advertised to the LSP that we support lazy 221 + // resolution for codeactions, we should have been sent back 222 + // a nil-Edit property. 223 + qt.Assert(t, qt.Equals(action.Edit == nil, resolveSupport)) 224 + // Calling ApplyCodeAction will make the additional call to 225 + // resolve the Edit property if necessary. 226 + env.ApplyCodeAction(action) 227 + after := env.BufferText("input.cue") 228 + qt.Check(t, qt.Equals(after, tc.expected)) 229 + } 230 + 231 + t.Run(tc.name+"/eager", func(t *testing.T) { 232 + WithOptions(RootURIAsDefaultFolder()).Run(t, "-- input.cue --\n"+tc.input, fun) 233 + }) 234 + 235 + t.Run(tc.name+"/lazy", func(t *testing.T) { 236 + WithOptions( 237 + RootURIAsDefaultFolder(), 238 + CapabilitiesJSON([]byte(`{ 239 + "textDocument": {"codeAction": { 240 + "dataSupport": true, 241 + "resolveSupport": {"properties": ["edit"]} 242 + }} 243 + }`)), 244 + ).Run(t, "-- input.cue --\n"+tc.input, fun) 245 + }) 246 + } 247 + }
+77
internal/lsp/cache/codeaction.go
··· 16 16 17 17 import ( 18 18 "bytes" 19 + "cmp" 19 20 "context" 20 21 "slices" 21 22 "strings" ··· 291 292 } 292 293 return "\n" 293 294 } 295 + 296 + // CodeActionOrganizeImports calculates the edits needed to organise 297 + // imports. 298 + // 299 + // Currently that only goes as far as: 300 + // 1. Removing unused imports; 301 + // 2. Sorting import specs lexicographically within each import decl. 302 + func (w *Workspace) CodeActionOrganizeImports(ctx context.Context, params *protocol.CodeActionParams, delayEdit bool) (*protocol.WorkspaceEdit, error) { 303 + fileUri := params.TextDocument.URI 304 + f, fe, mapper, err := w.FileEvaluatorForURI(fileUri, LoadAll) 305 + if f == nil || fe == nil || mapper == nil || err != nil { 306 + return nil, err 307 + } 308 + 309 + if delayEdit { 310 + return &protocol.WorkspaceEdit{}, nil 311 + } 312 + 313 + content := f.tokFile.Content() 314 + start := 0 315 + var organisedContent strings.Builder 316 + 317 + for decl := range f.syntax.ImportDecls() { 318 + organisedContent.Write(content[start:decl.Pos().Offset()]) 319 + start = decl.End().Offset() 320 + 321 + var survivors []*ast.ImportSpec 322 + for _, spec := range decl.Specs { 323 + pos := spec.Path.Pos() 324 + if spec.Name != nil { 325 + pos = spec.Name.Pos() 326 + } 327 + usages := fe.UsagesForOffset(pos.Offset(), false) 328 + if len(usages) > 0 { 329 + survivors = append(survivors, spec) 330 + } 331 + } 332 + 333 + if l := len(survivors); l > 0 { 334 + plural := l > 1 335 + if plural { 336 + slices.SortFunc(survivors, func(a, b *ast.ImportSpec) int { 337 + return cmp.Compare(a.Path.Value, b.Path.Value) 338 + }) 339 + } 340 + 341 + organisedContent.WriteString("import ") 342 + if plural { 343 + organisedContent.WriteString("(\n") 344 + } 345 + for _, spec := range survivors { 346 + if plural { 347 + organisedContent.WriteString("\t") 348 + } 349 + organisedContent.Write(content[spec.Pos().Offset():spec.End().Offset()]) 350 + if plural { 351 + organisedContent.WriteString("\n") 352 + } 353 + } 354 + if plural { 355 + organisedContent.WriteString(")") 356 + } 357 + } 358 + } 359 + 360 + organisedContent.Write(content[start:]) 361 + 362 + diffEdits := diff.Strings(string(content), organisedContent.String()) 363 + edits, err := protocol.EditsFromDiffEdits(f.mapper, diffEdits) 364 + if err != nil { 365 + return nil, nil 366 + } 367 + 368 + docChanges := protocol.TextEditsToDocumentChanges(params.TextDocument.URI, f.tokFile.Revision(), edits) 369 + return &protocol.WorkspaceEdit{DocumentChanges: docChanges}, nil 370 + }
+5 -1
internal/lsp/eval/eval.go
··· 2640 2640 return parentNav, "" 2641 2641 } 2642 2642 // Finally, inspect the globals 2643 - return fNav.parent.bindings[name], "" 2643 + if fNav.parent == nil { 2644 + return nil, "" 2645 + } else { 2646 + return fNav.parent.bindings[name], "" 2647 + } 2644 2648 } 2645 2649 } 2646 2650 return nil, ""
+25
internal/lsp/server/codeaction.go
··· 94 94 codeActions = append(codeActions, action) 95 95 } 96 96 97 + organizeImportsEdit, err := s.workspace.CodeActionOrganizeImports(ctx, params, delayEdit) 98 + if err != nil { 99 + return nil, err 100 + } 101 + if organizeImportsEdit != nil { 102 + action := protocol.CodeAction{ 103 + Title: "Organize Imports", 104 + Kind: protocol.SourceOrganizeImports, 105 + } 106 + if delayEdit { 107 + action.Data = &raw 108 + } else { 109 + action.Edit = organizeImportsEdit 110 + } 111 + codeActions = append(codeActions, action) 112 + } 113 + 97 114 return codeActions, nil 98 115 } 99 116 ··· 122 139 return nil, err 123 140 } 124 141 action.Edit = convertFromStructEdit 142 + return action, nil 143 + 144 + case protocol.SourceOrganizeImports: 145 + organizeImportsEdit, err := s.workspace.CodeActionOrganizeImports(ctx, &params, false) 146 + if err != nil { 147 + return nil, err 148 + } 149 + action.Edit = organizeImportsEdit 125 150 return action, nil 126 151 127 152 default: