github.com/v2fly/tools@v0.100.0/internal/lsp/regtest/wrappers.go (about) 1 // Copyright 2020 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 regtest 6 7 import ( 8 "encoding/json" 9 "io" 10 "path" 11 "testing" 12 13 "github.com/v2fly/tools/internal/lsp/command" 14 "github.com/v2fly/tools/internal/lsp/fake" 15 "github.com/v2fly/tools/internal/lsp/protocol" 16 errors "golang.org/x/xerrors" 17 ) 18 19 func (e *Env) ChangeFilesOnDisk(events []fake.FileEvent) { 20 e.T.Helper() 21 if err := e.Sandbox.Workdir.ChangeFilesOnDisk(e.Ctx, events); err != nil { 22 e.T.Fatal(err) 23 } 24 } 25 26 // RemoveWorkspaceFile deletes a file on disk but does nothing in the 27 // editor. It calls t.Fatal on any error. 28 func (e *Env) RemoveWorkspaceFile(name string) { 29 e.T.Helper() 30 if err := e.Sandbox.Workdir.RemoveFile(e.Ctx, name); err != nil { 31 e.T.Fatal(err) 32 } 33 } 34 35 // ReadWorkspaceFile reads a file from the workspace, calling t.Fatal on any 36 // error. 37 func (e *Env) ReadWorkspaceFile(name string) string { 38 e.T.Helper() 39 content, err := e.Sandbox.Workdir.ReadFile(name) 40 if err != nil { 41 e.T.Fatal(err) 42 } 43 return content 44 } 45 46 // WriteWorkspaceFile writes a file to disk but does nothing in the editor. 47 // It calls t.Fatal on any error. 48 func (e *Env) WriteWorkspaceFile(name, content string) { 49 e.T.Helper() 50 if err := e.Sandbox.Workdir.WriteFile(e.Ctx, name, content); err != nil { 51 e.T.Fatal(err) 52 } 53 } 54 55 // WriteWorkspaceFiles deletes a file on disk but does nothing in the 56 // editor. It calls t.Fatal on any error. 57 func (e *Env) WriteWorkspaceFiles(files map[string]string) { 58 e.T.Helper() 59 if err := e.Sandbox.Workdir.WriteFiles(e.Ctx, files); err != nil { 60 e.T.Fatal(err) 61 } 62 } 63 64 // OpenFile opens a file in the editor, calling t.Fatal on any error. 65 func (e *Env) OpenFile(name string) { 66 e.T.Helper() 67 if err := e.Editor.OpenFile(e.Ctx, name); err != nil { 68 e.T.Fatal(err) 69 } 70 } 71 72 // CreateBuffer creates a buffer in the editor, calling t.Fatal on any error. 73 func (e *Env) CreateBuffer(name string, content string) { 74 e.T.Helper() 75 if err := e.Editor.CreateBuffer(e.Ctx, name, content); err != nil { 76 e.T.Fatal(err) 77 } 78 } 79 80 // CloseBuffer closes an editor buffer without saving, calling t.Fatal on any 81 // error. 82 func (e *Env) CloseBuffer(name string) { 83 e.T.Helper() 84 if err := e.Editor.CloseBuffer(e.Ctx, name); err != nil { 85 e.T.Fatal(err) 86 } 87 } 88 89 // EditBuffer applies edits to an editor buffer, calling t.Fatal on any error. 90 func (e *Env) EditBuffer(name string, edits ...fake.Edit) { 91 e.T.Helper() 92 if err := e.Editor.EditBuffer(e.Ctx, name, edits); err != nil { 93 e.T.Fatal(err) 94 } 95 } 96 97 func (e *Env) SetBufferContent(name string, content string) { 98 e.T.Helper() 99 if err := e.Editor.SetBufferContent(e.Ctx, name, content); err != nil { 100 e.T.Fatal(err) 101 } 102 } 103 104 // RegexpRange returns the range of the first match for re in the buffer 105 // specified by name, calling t.Fatal on any error. It first searches for the 106 // position in open buffers, then in workspace files. 107 func (e *Env) RegexpRange(name, re string) (fake.Pos, fake.Pos) { 108 e.T.Helper() 109 start, end, err := e.Editor.RegexpRange(name, re) 110 if err == fake.ErrUnknownBuffer { 111 start, end, err = e.Sandbox.Workdir.RegexpRange(name, re) 112 } 113 if err != nil { 114 e.T.Fatalf("RegexpRange: %v, %v", name, err) 115 } 116 return start, end 117 } 118 119 // RegexpSearch returns the starting position of the first match for re in the 120 // buffer specified by name, calling t.Fatal on any error. It first searches 121 // for the position in open buffers, then in workspace files. 122 func (e *Env) RegexpSearch(name, re string) fake.Pos { 123 e.T.Helper() 124 pos, err := e.Editor.RegexpSearch(name, re) 125 if err == fake.ErrUnknownBuffer { 126 pos, err = e.Sandbox.Workdir.RegexpSearch(name, re) 127 } 128 if err != nil { 129 e.T.Fatalf("RegexpSearch: %v, %v", name, err) 130 } 131 return pos 132 } 133 134 // RegexpReplace replaces the first group in the first match of regexpStr with 135 // the replace text, calling t.Fatal on any error. 136 func (e *Env) RegexpReplace(name, regexpStr, replace string) { 137 e.T.Helper() 138 if err := e.Editor.RegexpReplace(e.Ctx, name, regexpStr, replace); err != nil { 139 e.T.Fatalf("RegexpReplace: %v", err) 140 } 141 } 142 143 // SaveBuffer saves an editor buffer, calling t.Fatal on any error. 144 func (e *Env) SaveBuffer(name string) { 145 e.T.Helper() 146 if err := e.Editor.SaveBuffer(e.Ctx, name); err != nil { 147 e.T.Fatal(err) 148 } 149 } 150 151 func (e *Env) SaveBufferWithoutActions(name string) { 152 e.T.Helper() 153 if err := e.Editor.SaveBufferWithoutActions(e.Ctx, name); err != nil { 154 e.T.Fatal(err) 155 } 156 } 157 158 // GoToDefinition goes to definition in the editor, calling t.Fatal on any 159 // error. It returns the path and position of the resulting jump. 160 func (e *Env) GoToDefinition(name string, pos fake.Pos) (string, fake.Pos) { 161 e.T.Helper() 162 n, p, err := e.Editor.GoToDefinition(e.Ctx, name, pos) 163 if err != nil { 164 e.T.Fatal(err) 165 } 166 return n, p 167 } 168 169 // Symbol returns symbols matching query 170 func (e *Env) Symbol(query string) []fake.SymbolInformation { 171 e.T.Helper() 172 r, err := e.Editor.Symbol(e.Ctx, query) 173 if err != nil { 174 e.T.Fatal(err) 175 } 176 return r 177 } 178 179 // FormatBuffer formats the editor buffer, calling t.Fatal on any error. 180 func (e *Env) FormatBuffer(name string) { 181 e.T.Helper() 182 if err := e.Editor.FormatBuffer(e.Ctx, name); err != nil { 183 e.T.Fatal(err) 184 } 185 } 186 187 // OrganizeImports processes the source.organizeImports codeAction, calling 188 // t.Fatal on any error. 189 func (e *Env) OrganizeImports(name string) { 190 e.T.Helper() 191 if err := e.Editor.OrganizeImports(e.Ctx, name); err != nil { 192 e.T.Fatal(err) 193 } 194 } 195 196 // ApplyQuickFixes processes the quickfix codeAction, calling t.Fatal on any error. 197 func (e *Env) ApplyQuickFixes(path string, diagnostics []protocol.Diagnostic) { 198 e.T.Helper() 199 if err := e.Editor.ApplyQuickFixes(e.Ctx, path, nil, diagnostics); err != nil { 200 e.T.Fatal(err) 201 } 202 } 203 204 // GetQuickFixes returns the available quick fix code actions. 205 func (e *Env) GetQuickFixes(path string, diagnostics []protocol.Diagnostic) []protocol.CodeAction { 206 e.T.Helper() 207 actions, err := e.Editor.GetQuickFixes(e.Ctx, path, nil, diagnostics) 208 if err != nil { 209 e.T.Fatal(err) 210 } 211 return actions 212 } 213 214 // Hover in the editor, calling t.Fatal on any error. 215 func (e *Env) Hover(name string, pos fake.Pos) (*protocol.MarkupContent, fake.Pos) { 216 e.T.Helper() 217 c, p, err := e.Editor.Hover(e.Ctx, name, pos) 218 if err != nil { 219 e.T.Fatal(err) 220 } 221 return c, p 222 } 223 224 func (e *Env) DocumentLink(name string) []protocol.DocumentLink { 225 e.T.Helper() 226 links, err := e.Editor.DocumentLink(e.Ctx, name) 227 if err != nil { 228 e.T.Fatal(err) 229 } 230 return links 231 } 232 233 func (e *Env) DocumentHighlight(name string, pos fake.Pos) []protocol.DocumentHighlight { 234 e.T.Helper() 235 highlights, err := e.Editor.DocumentHighlight(e.Ctx, name, pos) 236 if err != nil { 237 e.T.Fatal(err) 238 } 239 return highlights 240 } 241 242 func checkIsFatal(t *testing.T, err error) { 243 t.Helper() 244 if err != nil && !errors.Is(err, io.EOF) && !errors.Is(err, io.ErrClosedPipe) { 245 t.Fatal(err) 246 } 247 } 248 249 // CloseEditor shuts down the editor, calling t.Fatal on any error. 250 func (e *Env) CloseEditor() { 251 e.T.Helper() 252 checkIsFatal(e.T, e.Editor.Close(e.Ctx)) 253 } 254 255 // RunGenerate runs go:generate on the given dir, calling t.Fatal on any error. 256 // It waits for the generate command to complete and checks for file changes 257 // before returning. 258 func (e *Env) RunGenerate(dir string) { 259 e.T.Helper() 260 if err := e.Editor.RunGenerate(e.Ctx, dir); err != nil { 261 e.T.Fatal(err) 262 } 263 e.Await(NoOutstandingWork()) 264 // Ideally the fake.Workspace would handle all synthetic file watching, but 265 // we help it out here as we need to wait for the generate command to 266 // complete before checking the filesystem. 267 e.CheckForFileChanges() 268 } 269 270 // RunGoCommand runs the given command in the sandbox's default working 271 // directory. 272 func (e *Env) RunGoCommand(verb string, args ...string) { 273 e.T.Helper() 274 if err := e.Sandbox.RunGoCommand(e.Ctx, "", verb, args); err != nil { 275 e.T.Fatal(err) 276 } 277 } 278 279 // RunGoCommandInDir is like RunGoCommand, but executes in the given 280 // relative directory of the sandbox. 281 func (e *Env) RunGoCommandInDir(dir, verb string, args ...string) { 282 e.T.Helper() 283 if err := e.Sandbox.RunGoCommand(e.Ctx, dir, verb, args); err != nil { 284 e.T.Fatal(err) 285 } 286 } 287 288 // DumpGoSum prints the correct go.sum contents for dir in txtar format, 289 // for use in creating regtests. 290 func (e *Env) DumpGoSum(dir string) { 291 e.T.Helper() 292 293 if err := e.Sandbox.RunGoCommand(e.Ctx, dir, "list", []string{"-mod=mod", "..."}); err != nil { 294 e.T.Fatal(err) 295 } 296 sumFile := path.Join(dir, "/go.sum") 297 e.T.Log("\n\n-- " + sumFile + " --\n" + e.ReadWorkspaceFile(sumFile)) 298 e.T.Fatal("see contents above") 299 } 300 301 // CheckForFileChanges triggers a manual poll of the workspace for any file 302 // changes since creation, or since last polling. It is a workaround for the 303 // lack of true file watching support in the fake workspace. 304 func (e *Env) CheckForFileChanges() { 305 e.T.Helper() 306 if err := e.Sandbox.Workdir.CheckForFileChanges(e.Ctx); err != nil { 307 e.T.Fatal(err) 308 } 309 } 310 311 // CodeLens calls textDocument/codeLens for the given path, calling t.Fatal on 312 // any error. 313 func (e *Env) CodeLens(path string) []protocol.CodeLens { 314 e.T.Helper() 315 lens, err := e.Editor.CodeLens(e.Ctx, path) 316 if err != nil { 317 e.T.Fatal(err) 318 } 319 return lens 320 } 321 322 // ExecuteCodeLensCommand executes the command for the code lens matching the 323 // given command name. 324 func (e *Env) ExecuteCodeLensCommand(path string, cmd command.Command) { 325 e.T.Helper() 326 lenses := e.CodeLens(path) 327 var lens protocol.CodeLens 328 var found bool 329 for _, l := range lenses { 330 if l.Command.Command == cmd.ID() { 331 lens = l 332 found = true 333 } 334 } 335 if !found { 336 e.T.Fatalf("found no command with the ID %s", cmd.ID()) 337 } 338 e.ExecuteCommand(&protocol.ExecuteCommandParams{ 339 Command: lens.Command.Command, 340 Arguments: lens.Command.Arguments, 341 }, nil) 342 } 343 344 func (e *Env) ExecuteCommand(params *protocol.ExecuteCommandParams, result interface{}) { 345 e.T.Helper() 346 response, err := e.Editor.ExecuteCommand(e.Ctx, params) 347 if err != nil { 348 e.T.Fatal(err) 349 } 350 if result == nil { 351 return 352 } 353 // Hack: The result of an executeCommand request will be unmarshaled into 354 // maps. Re-marshal and unmarshal into the type we expect. 355 // 356 // This could be improved by generating a jsonrpc2 command client from the 357 // command.Interface, but that should only be done if we're consolidating 358 // this part of the tsprotocol generation. 359 data, err := json.Marshal(response) 360 if err != nil { 361 e.T.Fatal(err) 362 } 363 if err := json.Unmarshal(data, result); err != nil { 364 e.T.Fatal(err) 365 } 366 } 367 368 // References calls textDocument/references for the given path at the given 369 // position. 370 func (e *Env) References(path string, pos fake.Pos) []protocol.Location { 371 e.T.Helper() 372 locations, err := e.Editor.References(e.Ctx, path, pos) 373 if err != nil { 374 e.T.Fatal(err) 375 } 376 return locations 377 } 378 379 // Completion executes a completion request on the server. 380 func (e *Env) Completion(path string, pos fake.Pos) *protocol.CompletionList { 381 e.T.Helper() 382 completions, err := e.Editor.Completion(e.Ctx, path, pos) 383 if err != nil { 384 e.T.Fatal(err) 385 } 386 return completions 387 } 388 389 // AcceptCompletion accepts a completion for the given item at the given 390 // position. 391 func (e *Env) AcceptCompletion(path string, pos fake.Pos, item protocol.CompletionItem) { 392 e.T.Helper() 393 if err := e.Editor.AcceptCompletion(e.Ctx, path, pos, item); err != nil { 394 e.T.Fatal(err) 395 } 396 } 397 398 // CodeAction calls testDocument/codeAction for the given path, and calls 399 // t.Fatal if there are errors. 400 func (e *Env) CodeAction(path string, diagnostics []protocol.Diagnostic) []protocol.CodeAction { 401 e.T.Helper() 402 actions, err := e.Editor.CodeAction(e.Ctx, path, nil, diagnostics) 403 if err != nil { 404 e.T.Fatal(err) 405 } 406 return actions 407 } 408 409 func (e *Env) ChangeConfiguration(t *testing.T, config *fake.EditorConfig) { 410 e.Editor.Config = *config 411 if err := e.Editor.Server.DidChangeConfiguration(e.Ctx, &protocol.DidChangeConfigurationParams{ 412 // gopls currently ignores the Settings field 413 }); err != nil { 414 t.Fatal(err) 415 } 416 } 417 418 // ChangeEnv modifies the editor environment and reconfigures the LSP client. 419 // TODO: extend this to "ChangeConfiguration", once we refactor the way editor 420 // configuration is defined. 421 func (e *Env) ChangeEnv(overlay map[string]string) { 422 e.T.Helper() 423 // TODO: to be correct, this should probably be synchronized, but right now 424 // configuration is only ever modified synchronously in a regtest, so this 425 // correctness can wait for the previously mentioned refactoring. 426 if e.Editor.Config.Env == nil { 427 e.Editor.Config.Env = make(map[string]string) 428 } 429 for k, v := range overlay { 430 e.Editor.Config.Env[k] = v 431 } 432 var params protocol.DidChangeConfigurationParams 433 if err := e.Editor.Server.DidChangeConfiguration(e.Ctx, ¶ms); err != nil { 434 e.T.Fatal(err) 435 } 436 }