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	"io"
9	"testing"
10
11	"golang.org/x/tools/internal/lsp/fake"
12	"golang.org/x/tools/internal/lsp/protocol"
13	"golang.org/x/tools/internal/lsp/source"
14	errors "golang.org/x/xerrors"
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
149// GoToDefinition goes to definition in the editor, calling t.Fatal on any
150// error.
151func (e *Env) GoToDefinition(name string, pos fake.Pos) (string, fake.Pos) {
152	e.T.Helper()
153	n, p, err := e.Editor.GoToDefinition(e.Ctx, name, pos)
154	if err != nil {
155		e.T.Fatal(err)
156	}
157	return n, p
158}
159
160// Symbol returns symbols matching query
161func (e *Env) Symbol(query string) []fake.SymbolInformation {
162	e.T.Helper()
163	r, err := e.Editor.Symbol(e.Ctx, query)
164	if err != nil {
165		e.T.Fatal(err)
166	}
167	return r
168}
169
170// FormatBuffer formats the editor buffer, calling t.Fatal on any error.
171func (e *Env) FormatBuffer(name string) {
172	e.T.Helper()
173	if err := e.Editor.FormatBuffer(e.Ctx, name); err != nil {
174		e.T.Fatal(err)
175	}
176}
177
178// OrganizeImports processes the source.organizeImports codeAction, calling
179// t.Fatal on any error.
180func (e *Env) OrganizeImports(name string) {
181	e.T.Helper()
182	if err := e.Editor.OrganizeImports(e.Ctx, name); err != nil {
183		e.T.Fatal(err)
184	}
185}
186
187// ApplyQuickFixes processes the quickfix codeAction, calling t.Fatal on any error.
188func (e *Env) ApplyQuickFixes(path string, diagnostics []protocol.Diagnostic) {
189	e.T.Helper()
190	if err := e.Editor.ApplyQuickFixes(e.Ctx, path, nil, diagnostics); err != nil {
191		e.T.Fatal(err)
192	}
193}
194
195// Hover in the editor, calling t.Fatal on any error.
196func (e *Env) Hover(name string, pos fake.Pos) (*protocol.MarkupContent, fake.Pos) {
197	e.T.Helper()
198	c, p, err := e.Editor.Hover(e.Ctx, name, pos)
199	if err != nil {
200		e.T.Fatal(err)
201	}
202	return c, p
203}
204
205func (e *Env) DocumentLink(name string) []protocol.DocumentLink {
206	e.T.Helper()
207	links, err := e.Editor.DocumentLink(e.Ctx, name)
208	if err != nil {
209		e.T.Fatal(err)
210	}
211	return links
212}
213
214func checkIsFatal(t *testing.T, err error) {
215	t.Helper()
216	if err != nil && !errors.Is(err, io.EOF) && !errors.Is(err, io.ErrClosedPipe) {
217		t.Fatal(err)
218	}
219}
220
221// CloseEditor shuts down the editor, calling t.Fatal on any error.
222func (e *Env) CloseEditor() {
223	e.T.Helper()
224	checkIsFatal(e.T, e.Editor.Close(e.Ctx))
225}
226
227// RunGenerate runs go:generate on the given dir, calling t.Fatal on any error.
228// It waits for the generate command to complete and checks for file changes
229// before returning.
230func (e *Env) RunGenerate(dir string) {
231	e.T.Helper()
232	if err := e.Editor.RunGenerate(e.Ctx, dir); err != nil {
233		e.T.Fatal(err)
234	}
235	e.Await(NoOutstandingWork())
236	// Ideally the fake.Workspace would handle all synthetic file watching, but
237	// we help it out here as we need to wait for the generate command to
238	// complete before checking the filesystem.
239	e.CheckForFileChanges()
240}
241
242// RunGoCommand runs the given command in the sandbox's default working
243// directory.
244func (e *Env) RunGoCommand(verb string, args ...string) {
245	if err := e.Sandbox.RunGoCommand(e.Ctx, "", verb, args); err != nil {
246		e.T.Fatal(err)
247	}
248}
249
250func (e *Env) DumpGoSum() {
251	e.T.Helper()
252	e.RunGoCommand("list", "-mod=mod", "...")
253	e.T.Log("\n\n-- go.sum --\n" + e.ReadWorkspaceFile("go.sum"))
254	e.T.Fatal("see contents above")
255}
256
257// CheckForFileChanges triggers a manual poll of the workspace for any file
258// changes since creation, or since last polling. It is a workaround for the
259// lack of true file watching support in the fake workspace.
260func (e *Env) CheckForFileChanges() {
261	e.T.Helper()
262	if err := e.Sandbox.Workdir.CheckForFileChanges(e.Ctx); err != nil {
263		e.T.Fatal(err)
264	}
265}
266
267// CodeLens calls textDocument/codeLens for the given path, calling t.Fatal on
268// any error.
269func (e *Env) CodeLens(path string) []protocol.CodeLens {
270	e.T.Helper()
271	lens, err := e.Editor.CodeLens(e.Ctx, path)
272	if err != nil {
273		e.T.Fatal(err)
274	}
275	return lens
276}
277
278// ExecuteCodeLensCommand executes the command for the code lens matching the
279// given command name.
280func (e *Env) ExecuteCodeLensCommand(path string, cmd *source.Command) {
281	lenses := e.CodeLens(path)
282	var lens protocol.CodeLens
283	var found bool
284	for _, l := range lenses {
285		if l.Command.Command == cmd.ID() {
286			lens = l
287			found = true
288		}
289	}
290	if !found {
291		e.T.Fatalf("found no command with the ID %s", cmd.ID())
292	}
293	if _, err := e.Editor.ExecuteCommand(e.Ctx, &protocol.ExecuteCommandParams{
294		Command:   lens.Command.Command,
295		Arguments: lens.Command.Arguments,
296	}); err != nil {
297		e.T.Fatal(err)
298	}
299}
300
301// References calls textDocument/references for the given path at the given
302// position.
303func (e *Env) References(path string, pos fake.Pos) []protocol.Location {
304	e.T.Helper()
305	locations, err := e.Editor.References(e.Ctx, path, pos)
306	if err != nil {
307		e.T.Fatal(err)
308	}
309	return locations
310}
311
312// Completion executes a completion request on the server.
313func (e *Env) Completion(path string, pos fake.Pos) *protocol.CompletionList {
314	e.T.Helper()
315	completions, err := e.Editor.Completion(e.Ctx, path, pos)
316	if err != nil {
317		e.T.Fatal(err)
318	}
319	return completions
320}
321
322// CodeAction calls testDocument/codeAction for the given path, and calls
323// t.Fatal if there are errors.
324func (e *Env) CodeAction(path string) []protocol.CodeAction {
325	e.T.Helper()
326	actions, err := e.Editor.CodeAction(e.Ctx, path, nil)
327	if err != nil {
328		e.T.Fatal(err)
329	}
330	return actions
331}
332
333func (e *Env) changeConfiguration(t *testing.T, config *fake.EditorConfig) {
334	e.Editor.Config = *config
335	if err := e.Editor.Server.DidChangeConfiguration(e.Ctx, &protocol.DidChangeConfigurationParams{
336		// gopls currently ignores the Settings field
337	}); err != nil {
338		t.Fatal(err)
339	}
340}
341
342// ChangeEnv modifies the editor environment and reconfigures the LSP client.
343// TODO: extend this to "ChangeConfiguration", once we refactor the way editor
344// configuration is defined.
345func (e *Env) ChangeEnv(overlay map[string]string) {
346	e.T.Helper()
347	// TODO: to be correct, this should probably be synchronized, but right now
348	// configuration is only ever modified synchronously in a regtest, so this
349	// correctness can wait for the previously mentioned refactoring.
350	if e.Editor.Config.Env == nil {
351		e.Editor.Config.Env = make(map[string]string)
352	}
353	for k, v := range overlay {
354		e.Editor.Config.Env[k] = v
355	}
356	var params protocol.DidChangeConfigurationParams
357	if err := e.Editor.Server.DidChangeConfiguration(e.Ctx, &params); err != nil {
358		e.T.Fatal(err)
359	}
360}
361