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