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
5package regtest
6
7import (
8	"encoding/json"
9	"path"
10	"testing"
11
12	"golang.org/x/tools/internal/lsp/command"
13	"golang.org/x/tools/internal/lsp/fake"
14	"golang.org/x/tools/internal/lsp/protocol"
15)
16
17func (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.
26func (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.
35func (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.
46func (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.
55func (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.
63func (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.
71func (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.
80func (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.
88func (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
95func (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.
105func (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.
120func (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.
134func (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.
142func (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
149func (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.
158func (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
168func (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.
178func (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.
187func (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.
195func (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.
203func (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.
211func (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.
221func (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
230func (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
239func (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.
251func (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.
265func (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.
274func (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.
283func (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.
297func (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.
306func (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.
317func (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
337func (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// References calls textDocument/references for the given path at the given
362// position.
363func (e *Env) References(path string, pos fake.Pos) []protocol.Location {
364	e.T.Helper()
365	locations, err := e.Editor.References(e.Ctx, path, pos)
366	if err != nil {
367		e.T.Fatal(err)
368	}
369	return locations
370}
371
372// Completion executes a completion request on the server.
373func (e *Env) Completion(path string, pos fake.Pos) *protocol.CompletionList {
374	e.T.Helper()
375	completions, err := e.Editor.Completion(e.Ctx, path, pos)
376	if err != nil {
377		e.T.Fatal(err)
378	}
379	return completions
380}
381
382// AcceptCompletion accepts a completion for the given item at the given
383// position.
384func (e *Env) AcceptCompletion(path string, pos fake.Pos, item protocol.CompletionItem) {
385	e.T.Helper()
386	if err := e.Editor.AcceptCompletion(e.Ctx, path, pos, item); err != nil {
387		e.T.Fatal(err)
388	}
389}
390
391// CodeAction calls testDocument/codeAction for the given path, and calls
392// t.Fatal if there are errors.
393func (e *Env) CodeAction(path string, diagnostics []protocol.Diagnostic) []protocol.CodeAction {
394	e.T.Helper()
395	actions, err := e.Editor.CodeAction(e.Ctx, path, nil, diagnostics)
396	if err != nil {
397		e.T.Fatal(err)
398	}
399	return actions
400}
401
402func (e *Env) ChangeConfiguration(t *testing.T, config *fake.EditorConfig) {
403	e.Editor.Config = *config
404	if err := e.Editor.Server.DidChangeConfiguration(e.Ctx, &protocol.DidChangeConfigurationParams{
405		// gopls currently ignores the Settings field
406	}); err != nil {
407		t.Fatal(err)
408	}
409}
410
411// ChangeEnv modifies the editor environment and reconfigures the LSP client.
412// TODO: extend this to "ChangeConfiguration", once we refactor the way editor
413// configuration is defined.
414func (e *Env) ChangeEnv(overlay map[string]string) {
415	e.T.Helper()
416	// TODO: to be correct, this should probably be synchronized, but right now
417	// configuration is only ever modified synchronously in a regtest, so this
418	// correctness can wait for the previously mentioned refactoring.
419	if e.Editor.Config.Env == nil {
420		e.Editor.Config.Env = make(map[string]string)
421	}
422	for k, v := range overlay {
423		e.Editor.Config.Env[k] = v
424	}
425	var params protocol.DidChangeConfigurationParams
426	if err := e.Editor.Server.DidChangeConfiguration(e.Ctx, &params); err != nil {
427		e.T.Fatal(err)
428	}
429}
430