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 fake
6
7import (
8	"bufio"
9	"context"
10	"fmt"
11	"os"
12	"path/filepath"
13	"regexp"
14	"strings"
15	"sync"
16
17	"golang.org/x/tools/internal/jsonrpc2"
18	"golang.org/x/tools/internal/lsp/protocol"
19	"golang.org/x/tools/internal/lsp/source"
20	"golang.org/x/tools/internal/span"
21	errors "golang.org/x/xerrors"
22)
23
24// Editor is a fake editor client.  It keeps track of client state and can be
25// used for writing LSP tests.
26type Editor struct {
27	Config EditorConfig
28
29	// Server, client, and sandbox are concurrency safe and written only
30	// at construction time, so do not require synchronization.
31	Server     protocol.Server
32	serverConn jsonrpc2.Conn
33	client     *Client
34	sandbox    *Sandbox
35	defaultEnv map[string]string
36
37	// Since this editor is intended just for testing, we use very coarse
38	// locking.
39	mu sync.Mutex
40	// Editor state.
41	buffers map[string]buffer
42	// Capabilities / Options
43	serverCapabilities protocol.ServerCapabilities
44	// Call metrics for the purpose of expectations. This is done in an ad-hoc
45	// manner for now. Perhaps in the future we should do something more
46	// systematic.
47	calls CallCounts
48}
49
50type CallCounts struct {
51	DidOpen, DidChange, DidChangeWatchedFiles int
52}
53
54type buffer struct {
55	version int
56	path    string
57	content []string
58	dirty   bool
59}
60
61func (b buffer) text() string {
62	return strings.Join(b.content, "\n")
63}
64
65// EditorConfig configures the editor's LSP session. This is similar to
66// source.UserOptions, but we use a separate type here so that we expose only
67// that configuration which we support.
68//
69// The zero value for EditorConfig should correspond to its defaults.
70type EditorConfig struct {
71	Env        map[string]string
72	BuildFlags []string
73
74	// CodeLenses is a map defining whether codelens are enabled, keyed by the
75	// codeLens command. CodeLenses which are not present in this map are left in
76	// their default state.
77	CodeLenses map[string]bool
78
79	// SymbolMatcher is the config associated with the "symbolMatcher" gopls
80	// config option.
81	SymbolMatcher, SymbolStyle *string
82
83	// LimitWorkspaceScope is true if the user does not want to expand their
84	// workspace scope to the entire module.
85	LimitWorkspaceScope bool
86
87	// WorkspaceFolders is the workspace folders to configure on the LSP server,
88	// relative to the sandbox workdir.
89	//
90	// As a special case, if WorkspaceFolders is nil the editor defaults to
91	// configuring a single workspace folder corresponding to the workdir root.
92	// To explicitly send no workspace folders, use an empty (non-nil) slice.
93	WorkspaceFolders []string
94
95	// EnableStaticcheck enables staticcheck analyzers.
96	EnableStaticcheck bool
97
98	// AllExperiments sets the "allExperiments" configuration, which enables
99	// all of gopls's opt-in settings.
100	AllExperiments bool
101
102	// Whether to send the current process ID, for testing data that is joined to
103	// the PID. This can only be set by one test.
104	SendPID bool
105
106	DirectoryFilters []string
107
108	VerboseOutput bool
109}
110
111// NewEditor Creates a new Editor.
112func NewEditor(sandbox *Sandbox, config EditorConfig) *Editor {
113	return &Editor{
114		buffers:    make(map[string]buffer),
115		sandbox:    sandbox,
116		defaultEnv: sandbox.GoEnv(),
117		Config:     config,
118	}
119}
120
121// Connect configures the editor to communicate with an LSP server on conn. It
122// is not concurrency safe, and should be called at most once, before using the
123// editor.
124//
125// It returns the editor, so that it may be called as follows:
126//   editor, err := NewEditor(s).Connect(ctx, conn)
127func (e *Editor) Connect(ctx context.Context, conn jsonrpc2.Conn, hooks ClientHooks) (*Editor, error) {
128	e.serverConn = conn
129	e.Server = protocol.ServerDispatcher(conn)
130	e.client = &Client{editor: e, hooks: hooks}
131	conn.Go(ctx,
132		protocol.Handlers(
133			protocol.ClientHandler(e.client,
134				jsonrpc2.MethodNotFound)))
135	if err := e.initialize(ctx, e.Config.WorkspaceFolders); err != nil {
136		return nil, err
137	}
138	e.sandbox.Workdir.AddWatcher(e.onFileChanges)
139	return e, nil
140}
141
142func (e *Editor) Stats() CallCounts {
143	e.mu.Lock()
144	defer e.mu.Unlock()
145	return e.calls
146}
147
148// Shutdown issues the 'shutdown' LSP notification.
149func (e *Editor) Shutdown(ctx context.Context) error {
150	if e.Server != nil {
151		if err := e.Server.Shutdown(ctx); err != nil {
152			return errors.Errorf("Shutdown: %w", err)
153		}
154	}
155	return nil
156}
157
158// Exit issues the 'exit' LSP notification.
159func (e *Editor) Exit(ctx context.Context) error {
160	if e.Server != nil {
161		// Not all LSP clients issue the exit RPC, but we do so here to ensure that
162		// we gracefully handle it on multi-session servers.
163		if err := e.Server.Exit(ctx); err != nil {
164			return errors.Errorf("Exit: %w", err)
165		}
166	}
167	return nil
168}
169
170// Close issues the shutdown and exit sequence an editor should.
171func (e *Editor) Close(ctx context.Context) error {
172	if err := e.Shutdown(ctx); err != nil {
173		return err
174	}
175	if err := e.Exit(ctx); err != nil {
176		return err
177	}
178	// called close on the editor should result in the connection closing
179	select {
180	case <-e.serverConn.Done():
181		// connection closed itself
182		return nil
183	case <-ctx.Done():
184		return errors.Errorf("connection not closed: %w", ctx.Err())
185	}
186}
187
188// Client returns the LSP client for this editor.
189func (e *Editor) Client() *Client {
190	return e.client
191}
192
193func (e *Editor) overlayEnv() map[string]string {
194	env := make(map[string]string)
195	for k, v := range e.defaultEnv {
196		env[k] = v
197	}
198	for k, v := range e.Config.Env {
199		env[k] = v
200	}
201	return env
202}
203
204func (e *Editor) configuration() map[string]interface{} {
205	config := map[string]interface{}{
206		"verboseWorkDoneProgress": true,
207		"env":                     e.overlayEnv(),
208		"expandWorkspaceToModule": !e.Config.LimitWorkspaceScope,
209		"completionBudget":        "10s",
210	}
211
212	if e.Config.BuildFlags != nil {
213		config["buildFlags"] = e.Config.BuildFlags
214	}
215	if e.Config.DirectoryFilters != nil {
216		config["directoryFilters"] = e.Config.DirectoryFilters
217	}
218	if e.Config.CodeLenses != nil {
219		config["codelenses"] = e.Config.CodeLenses
220	}
221	if e.Config.SymbolMatcher != nil {
222		config["symbolMatcher"] = *e.Config.SymbolMatcher
223	}
224	if e.Config.SymbolStyle != nil {
225		config["symbolStyle"] = *e.Config.SymbolStyle
226	}
227	if e.Config.EnableStaticcheck {
228		config["staticcheck"] = true
229	}
230	if e.Config.AllExperiments {
231		config["allExperiments"] = true
232	}
233
234	if e.Config.VerboseOutput {
235		config["verboseOutput"] = true
236	}
237
238	// TODO(rFindley): change to the new settings name once it is no longer
239	// designated experimental.
240	config["experimentalDiagnosticsDelay"] = "10ms"
241
242	// ExperimentalWorkspaceModule is only set as a mode, not a configuration.
243	return config
244}
245
246func (e *Editor) initialize(ctx context.Context, workspaceFolders []string) error {
247	params := &protocol.ParamInitialize{}
248	params.ClientInfo.Name = "fakeclient"
249	params.ClientInfo.Version = "v1.0.0"
250
251	if workspaceFolders == nil {
252		workspaceFolders = []string{string(e.sandbox.Workdir.RelativeTo)}
253	}
254	for _, folder := range workspaceFolders {
255		params.WorkspaceFolders = append(params.WorkspaceFolders, protocol.WorkspaceFolder{
256			URI:  string(e.sandbox.Workdir.URI(folder)),
257			Name: filepath.Base(folder),
258		})
259	}
260
261	params.Capabilities.Workspace.Configuration = true
262	params.Capabilities.Window.WorkDoneProgress = true
263	// TODO: set client capabilities
264	params.InitializationOptions = e.configuration()
265	if e.Config.SendPID {
266		params.ProcessID = float64(os.Getpid())
267	}
268
269	// This is a bit of a hack, since the fake editor doesn't actually support
270	// watching changed files that match a specific glob pattern. However, the
271	// editor does send didChangeWatchedFiles notifications, so set this to
272	// true.
273	params.Capabilities.Workspace.DidChangeWatchedFiles.DynamicRegistration = true
274
275	params.Trace = "messages"
276	// TODO: support workspace folders.
277	if e.Server != nil {
278		resp, err := e.Server.Initialize(ctx, params)
279		if err != nil {
280			return errors.Errorf("initialize: %w", err)
281		}
282		e.mu.Lock()
283		e.serverCapabilities = resp.Capabilities
284		e.mu.Unlock()
285
286		if err := e.Server.Initialized(ctx, &protocol.InitializedParams{}); err != nil {
287			return errors.Errorf("initialized: %w", err)
288		}
289	}
290	// TODO: await initial configuration here, or expect gopls to manage that?
291	return nil
292}
293
294func (e *Editor) onFileChanges(ctx context.Context, evts []FileEvent) {
295	if e.Server == nil {
296		return
297	}
298	e.mu.Lock()
299	defer e.mu.Unlock()
300	var lspevts []protocol.FileEvent
301	for _, evt := range evts {
302		// Always send an on-disk change, even for events that seem useless
303		// because they're shadowed by an open buffer.
304		lspevts = append(lspevts, evt.ProtocolEvent)
305
306		if buf, ok := e.buffers[evt.Path]; ok {
307			// Following VS Code, don't honor deletions or changes to dirty buffers.
308			if buf.dirty || evt.ProtocolEvent.Type == protocol.Deleted {
309				continue
310			}
311
312			content, err := e.sandbox.Workdir.ReadFile(evt.Path)
313			if err != nil {
314				continue // A race with some other operation.
315			}
316			// During shutdown, this call will fail. Ignore the error.
317			_ = e.setBufferContentLocked(ctx, evt.Path, false, strings.Split(content, "\n"), nil)
318		}
319	}
320	e.Server.DidChangeWatchedFiles(ctx, &protocol.DidChangeWatchedFilesParams{
321		Changes: lspevts,
322	})
323	e.calls.DidChangeWatchedFiles++
324}
325
326// OpenFile creates a buffer for the given workdir-relative file.
327func (e *Editor) OpenFile(ctx context.Context, path string) error {
328	content, err := e.sandbox.Workdir.ReadFile(path)
329	if err != nil {
330		return err
331	}
332	return e.createBuffer(ctx, path, false, content)
333}
334
335func textDocumentItem(wd *Workdir, buf buffer) protocol.TextDocumentItem {
336	uri := wd.URI(buf.path)
337	languageID := ""
338	if strings.HasSuffix(buf.path, ".go") {
339		// TODO: what about go.mod files? What is their language ID?
340		languageID = "go"
341	}
342	return protocol.TextDocumentItem{
343		URI:        uri,
344		LanguageID: languageID,
345		Version:    float64(buf.version),
346		Text:       buf.text(),
347	}
348}
349
350// CreateBuffer creates a new unsaved buffer corresponding to the workdir path,
351// containing the given textual content.
352func (e *Editor) CreateBuffer(ctx context.Context, path, content string) error {
353	return e.createBuffer(ctx, path, true, content)
354}
355
356func (e *Editor) createBuffer(ctx context.Context, path string, dirty bool, content string) error {
357	buf := buffer{
358		version: 1,
359		path:    path,
360		content: strings.Split(content, "\n"),
361		dirty:   dirty,
362	}
363	e.mu.Lock()
364	defer e.mu.Unlock()
365	e.buffers[path] = buf
366	item := textDocumentItem(e.sandbox.Workdir, buf)
367
368	if e.Server != nil {
369		if err := e.Server.DidOpen(ctx, &protocol.DidOpenTextDocumentParams{
370			TextDocument: item,
371		}); err != nil {
372			return errors.Errorf("DidOpen: %w", err)
373		}
374		e.calls.DidOpen++
375	}
376	return nil
377}
378
379// CloseBuffer removes the current buffer (regardless of whether it is saved).
380func (e *Editor) CloseBuffer(ctx context.Context, path string) error {
381	e.mu.Lock()
382	_, ok := e.buffers[path]
383	if !ok {
384		e.mu.Unlock()
385		return ErrUnknownBuffer
386	}
387	delete(e.buffers, path)
388	e.mu.Unlock()
389
390	if e.Server != nil {
391		if err := e.Server.DidClose(ctx, &protocol.DidCloseTextDocumentParams{
392			TextDocument: e.textDocumentIdentifier(path),
393		}); err != nil {
394			return errors.Errorf("DidClose: %w", err)
395		}
396	}
397	return nil
398}
399
400func (e *Editor) textDocumentIdentifier(path string) protocol.TextDocumentIdentifier {
401	return protocol.TextDocumentIdentifier{
402		URI: e.sandbox.Workdir.URI(path),
403	}
404}
405
406// SaveBuffer writes the content of the buffer specified by the given path to
407// the filesystem.
408func (e *Editor) SaveBuffer(ctx context.Context, path string) error {
409	if err := e.OrganizeImports(ctx, path); err != nil {
410		return errors.Errorf("organizing imports before save: %w", err)
411	}
412	if err := e.FormatBuffer(ctx, path); err != nil {
413		return errors.Errorf("formatting before save: %w", err)
414	}
415	return e.SaveBufferWithoutActions(ctx, path)
416}
417
418func (e *Editor) SaveBufferWithoutActions(ctx context.Context, path string) error {
419	e.mu.Lock()
420	defer e.mu.Unlock()
421	buf, ok := e.buffers[path]
422	if !ok {
423		return fmt.Errorf(fmt.Sprintf("unknown buffer: %q", path))
424	}
425	content := buf.text()
426	includeText := false
427	syncOptions, ok := e.serverCapabilities.TextDocumentSync.(protocol.TextDocumentSyncOptions)
428	if ok {
429		includeText = syncOptions.Save.IncludeText
430	}
431
432	docID := e.textDocumentIdentifier(buf.path)
433	if e.Server != nil {
434		if err := e.Server.WillSave(ctx, &protocol.WillSaveTextDocumentParams{
435			TextDocument: docID,
436			Reason:       protocol.Manual,
437		}); err != nil {
438			return errors.Errorf("WillSave: %w", err)
439		}
440	}
441	if err := e.sandbox.Workdir.WriteFile(ctx, path, content); err != nil {
442		return errors.Errorf("writing %q: %w", path, err)
443	}
444
445	buf.dirty = false
446	e.buffers[path] = buf
447
448	if e.Server != nil {
449		params := &protocol.DidSaveTextDocumentParams{
450			TextDocument: protocol.VersionedTextDocumentIdentifier{
451				Version:                float64(buf.version),
452				TextDocumentIdentifier: docID,
453			},
454		}
455		if includeText {
456			params.Text = &content
457		}
458		if err := e.Server.DidSave(ctx, params); err != nil {
459			return errors.Errorf("DidSave: %w", err)
460		}
461	}
462	return nil
463}
464
465// contentPosition returns the (Line, Column) position corresponding to offset
466// in the buffer referenced by path.
467func contentPosition(content string, offset int) (Pos, error) {
468	scanner := bufio.NewScanner(strings.NewReader(content))
469	start := 0
470	line := 0
471	for scanner.Scan() {
472		end := start + len([]rune(scanner.Text())) + 1
473		if offset < end {
474			return Pos{Line: line, Column: offset - start}, nil
475		}
476		start = end
477		line++
478	}
479	if err := scanner.Err(); err != nil {
480		return Pos{}, errors.Errorf("scanning content: %w", err)
481	}
482	// Scan() will drop the last line if it is empty. Correct for this.
483	if (strings.HasSuffix(content, "\n") || content == "") && offset == start {
484		return Pos{Line: line, Column: 0}, nil
485	}
486	return Pos{}, fmt.Errorf("position %d out of bounds in %q (line = %d, start = %d)", offset, content, line, start)
487}
488
489// ErrNoMatch is returned if a regexp search fails.
490var (
491	ErrNoMatch       = errors.New("no match")
492	ErrUnknownBuffer = errors.New("unknown buffer")
493)
494
495// regexpRange returns the start and end of the first occurrence of either re
496// or its singular subgroup. It returns ErrNoMatch if the regexp doesn't match.
497func regexpRange(content, re string) (Pos, Pos, error) {
498	var start, end int
499	rec, err := regexp.Compile(re)
500	if err != nil {
501		return Pos{}, Pos{}, err
502	}
503	indexes := rec.FindStringSubmatchIndex(content)
504	if indexes == nil {
505		return Pos{}, Pos{}, ErrNoMatch
506	}
507	switch len(indexes) {
508	case 2:
509		// no subgroups: return the range of the regexp expression
510		start, end = indexes[0], indexes[1]
511	case 4:
512		// one subgroup: return its range
513		start, end = indexes[2], indexes[3]
514	default:
515		return Pos{}, Pos{}, fmt.Errorf("invalid search regexp %q: expect either 0 or 1 subgroups, got %d", re, len(indexes)/2-1)
516	}
517	startPos, err := contentPosition(content, start)
518	if err != nil {
519		return Pos{}, Pos{}, err
520	}
521	endPos, err := contentPosition(content, end)
522	if err != nil {
523		return Pos{}, Pos{}, err
524	}
525	return startPos, endPos, nil
526}
527
528// RegexpRange returns the first range in the buffer bufName matching re. See
529// RegexpSearch for more information on matching.
530func (e *Editor) RegexpRange(bufName, re string) (Pos, Pos, error) {
531	e.mu.Lock()
532	defer e.mu.Unlock()
533	buf, ok := e.buffers[bufName]
534	if !ok {
535		return Pos{}, Pos{}, ErrUnknownBuffer
536	}
537	return regexpRange(buf.text(), re)
538}
539
540// RegexpSearch returns the position of the first match for re in the buffer
541// bufName. For convenience, RegexpSearch supports the following two modes:
542//  1. If re has no subgroups, return the position of the match for re itself.
543//  2. If re has one subgroup, return the position of the first subgroup.
544// It returns an error re is invalid, has more than one subgroup, or doesn't
545// match the buffer.
546func (e *Editor) RegexpSearch(bufName, re string) (Pos, error) {
547	start, _, err := e.RegexpRange(bufName, re)
548	return start, err
549}
550
551// RegexpReplace edits the buffer corresponding to path by replacing the first
552// instance of re, or its first subgroup, with the replace text. See
553// RegexpSearch for more explanation of these two modes.
554// It returns an error if re is invalid, has more than one subgroup, or doesn't
555// match the buffer.
556func (e *Editor) RegexpReplace(ctx context.Context, path, re, replace string) error {
557	e.mu.Lock()
558	defer e.mu.Unlock()
559	buf, ok := e.buffers[path]
560	if !ok {
561		return ErrUnknownBuffer
562	}
563	content := buf.text()
564	start, end, err := regexpRange(content, re)
565	if err != nil {
566		return err
567	}
568	return e.editBufferLocked(ctx, path, []Edit{{
569		Start: start,
570		End:   end,
571		Text:  replace,
572	}})
573}
574
575// EditBuffer applies the given test edits to the buffer identified by path.
576func (e *Editor) EditBuffer(ctx context.Context, path string, edits []Edit) error {
577	e.mu.Lock()
578	defer e.mu.Unlock()
579	return e.editBufferLocked(ctx, path, edits)
580}
581
582func (e *Editor) SetBufferContent(ctx context.Context, path, content string) error {
583	e.mu.Lock()
584	defer e.mu.Unlock()
585	lines := strings.Split(content, "\n")
586	return e.setBufferContentLocked(ctx, path, true, lines, nil)
587}
588
589// HasBuffer reports whether the file name is open in the editor.
590func (e *Editor) HasBuffer(name string) bool {
591	e.mu.Lock()
592	defer e.mu.Unlock()
593	_, ok := e.buffers[name]
594	return ok
595}
596
597// BufferText returns the content of the buffer with the given name.
598func (e *Editor) BufferText(name string) string {
599	e.mu.Lock()
600	defer e.mu.Unlock()
601	return e.buffers[name].text()
602}
603
604// BufferVersion returns the current version of the buffer corresponding to
605// name (or 0 if it is not being edited).
606func (e *Editor) BufferVersion(name string) int {
607	e.mu.Lock()
608	defer e.mu.Unlock()
609	return e.buffers[name].version
610}
611
612func (e *Editor) editBufferLocked(ctx context.Context, path string, edits []Edit) error {
613	buf, ok := e.buffers[path]
614	if !ok {
615		return fmt.Errorf("unknown buffer %q", path)
616	}
617	content := make([]string, len(buf.content))
618	copy(content, buf.content)
619	content, err := editContent(content, edits)
620	if err != nil {
621		return err
622	}
623	return e.setBufferContentLocked(ctx, path, true, content, edits)
624}
625
626func (e *Editor) setBufferContentLocked(ctx context.Context, path string, dirty bool, content []string, fromEdits []Edit) error {
627	buf, ok := e.buffers[path]
628	if !ok {
629		return fmt.Errorf("unknown buffer %q", path)
630	}
631	buf.content = content
632	buf.version++
633	buf.dirty = dirty
634	e.buffers[path] = buf
635	// A simple heuristic: if there is only one edit, send it incrementally.
636	// Otherwise, send the entire content.
637	var evts []protocol.TextDocumentContentChangeEvent
638	if len(fromEdits) == 1 {
639		evts = append(evts, fromEdits[0].toProtocolChangeEvent())
640	} else {
641		evts = append(evts, protocol.TextDocumentContentChangeEvent{
642			Text: buf.text(),
643		})
644	}
645	params := &protocol.DidChangeTextDocumentParams{
646		TextDocument: protocol.VersionedTextDocumentIdentifier{
647			Version:                float64(buf.version),
648			TextDocumentIdentifier: e.textDocumentIdentifier(buf.path),
649		},
650		ContentChanges: evts,
651	}
652	if e.Server != nil {
653		if err := e.Server.DidChange(ctx, params); err != nil {
654			return errors.Errorf("DidChange: %w", err)
655		}
656		e.calls.DidChange++
657	}
658	return nil
659}
660
661// GoToDefinition jumps to the definition of the symbol at the given position
662// in an open buffer.
663func (e *Editor) GoToDefinition(ctx context.Context, path string, pos Pos) (string, Pos, error) {
664	if err := e.checkBufferPosition(path, pos); err != nil {
665		return "", Pos{}, err
666	}
667	params := &protocol.DefinitionParams{}
668	params.TextDocument.URI = e.sandbox.Workdir.URI(path)
669	params.Position = pos.ToProtocolPosition()
670
671	resp, err := e.Server.Definition(ctx, params)
672	if err != nil {
673		return "", Pos{}, errors.Errorf("definition: %w", err)
674	}
675	if len(resp) == 0 {
676		return "", Pos{}, nil
677	}
678	newPath := e.sandbox.Workdir.URIToPath(resp[0].URI)
679	newPos := fromProtocolPosition(resp[0].Range.Start)
680	if !e.HasBuffer(newPath) {
681		if err := e.OpenFile(ctx, newPath); err != nil {
682			return "", Pos{}, errors.Errorf("OpenFile: %w", err)
683		}
684	}
685	return newPath, newPos, nil
686}
687
688// Symbol performs a workspace symbol search using query
689func (e *Editor) Symbol(ctx context.Context, query string) ([]SymbolInformation, error) {
690	params := &protocol.WorkspaceSymbolParams{}
691	params.Query = query
692
693	resp, err := e.Server.Symbol(ctx, params)
694	if err != nil {
695		return nil, errors.Errorf("symbol: %w", err)
696	}
697	var res []SymbolInformation
698	for _, si := range resp {
699		ploc := si.Location
700		path := e.sandbox.Workdir.URIToPath(ploc.URI)
701		start := fromProtocolPosition(ploc.Range.Start)
702		end := fromProtocolPosition(ploc.Range.End)
703		rnge := Range{
704			Start: start,
705			End:   end,
706		}
707		loc := Location{
708			Path:  path,
709			Range: rnge,
710		}
711		res = append(res, SymbolInformation{
712			Name:     si.Name,
713			Kind:     si.Kind,
714			Location: loc,
715		})
716	}
717	return res, nil
718}
719
720// OrganizeImports requests and performs the source.organizeImports codeAction.
721func (e *Editor) OrganizeImports(ctx context.Context, path string) error {
722	return e.codeAction(ctx, path, nil, nil, protocol.SourceOrganizeImports)
723}
724
725// RefactorRewrite requests and performs the source.refactorRewrite codeAction.
726func (e *Editor) RefactorRewrite(ctx context.Context, path string, rng *protocol.Range) error {
727	return e.codeAction(ctx, path, rng, nil, protocol.RefactorRewrite)
728}
729
730// ApplyQuickFixes requests and performs the quickfix codeAction.
731func (e *Editor) ApplyQuickFixes(ctx context.Context, path string, rng *protocol.Range, diagnostics []protocol.Diagnostic) error {
732	return e.codeAction(ctx, path, rng, diagnostics, protocol.QuickFix, protocol.SourceFixAll)
733}
734
735func (e *Editor) codeAction(ctx context.Context, path string, rng *protocol.Range, diagnostics []protocol.Diagnostic, only ...protocol.CodeActionKind) error {
736	if e.Server == nil {
737		return nil
738	}
739	params := &protocol.CodeActionParams{}
740	params.TextDocument.URI = e.sandbox.Workdir.URI(path)
741	params.Context.Only = only
742	if diagnostics != nil {
743		params.Context.Diagnostics = diagnostics
744	}
745	if rng != nil {
746		params.Range = *rng
747	}
748	actions, err := e.Server.CodeAction(ctx, params)
749	if err != nil {
750		return errors.Errorf("textDocument/codeAction: %w", err)
751	}
752	for _, action := range actions {
753		if action.Title == "" {
754			return errors.Errorf("empty title for code action")
755		}
756		var match bool
757		for _, o := range only {
758			if action.Kind == o {
759				match = true
760				break
761			}
762		}
763		if !match {
764			continue
765		}
766		for _, change := range action.Edit.DocumentChanges {
767			path := e.sandbox.Workdir.URIToPath(change.TextDocument.URI)
768			if float64(e.buffers[path].version) != change.TextDocument.Version {
769				// Skip edits for old versions.
770				continue
771			}
772			edits := convertEdits(change.Edits)
773			if err := e.EditBuffer(ctx, path, edits); err != nil {
774				return errors.Errorf("editing buffer %q: %w", path, err)
775			}
776		}
777		// Execute any commands. The specification says that commands are
778		// executed after edits are applied.
779		if action.Command != nil {
780			if _, err := e.ExecuteCommand(ctx, &protocol.ExecuteCommandParams{
781				Command:   action.Command.Command,
782				Arguments: action.Command.Arguments,
783			}); err != nil {
784				return err
785			}
786		}
787		// Some commands may edit files on disk.
788		if err := e.sandbox.Workdir.CheckForFileChanges(ctx); err != nil {
789			return err
790		}
791	}
792	return nil
793}
794
795func (e *Editor) ExecuteCommand(ctx context.Context, params *protocol.ExecuteCommandParams) (interface{}, error) {
796	if e.Server == nil {
797		return nil, nil
798	}
799	var match bool
800	// Ensure that this command was actually listed as a supported command.
801	for _, command := range e.serverCapabilities.ExecuteCommandProvider.Commands {
802		if command == params.Command {
803			match = true
804			break
805		}
806	}
807	if !match {
808		return nil, fmt.Errorf("unsupported command %q", params.Command)
809	}
810	result, err := e.Server.ExecuteCommand(ctx, params)
811	if err != nil {
812		return nil, err
813	}
814	// Some commands use the go command, which writes directly to disk.
815	// For convenience, check for those changes.
816	if err := e.sandbox.Workdir.CheckForFileChanges(ctx); err != nil {
817		return nil, err
818	}
819	return result, nil
820}
821
822func convertEdits(protocolEdits []protocol.TextEdit) []Edit {
823	var edits []Edit
824	for _, lspEdit := range protocolEdits {
825		edits = append(edits, fromProtocolTextEdit(lspEdit))
826	}
827	return edits
828}
829
830// FormatBuffer gofmts a Go file.
831func (e *Editor) FormatBuffer(ctx context.Context, path string) error {
832	if e.Server == nil {
833		return nil
834	}
835	e.mu.Lock()
836	version := e.buffers[path].version
837	e.mu.Unlock()
838	params := &protocol.DocumentFormattingParams{}
839	params.TextDocument.URI = e.sandbox.Workdir.URI(path)
840	resp, err := e.Server.Formatting(ctx, params)
841	if err != nil {
842		return errors.Errorf("textDocument/formatting: %w", err)
843	}
844	e.mu.Lock()
845	defer e.mu.Unlock()
846	if versionAfter := e.buffers[path].version; versionAfter != version {
847		return fmt.Errorf("before receipt of formatting edits, buffer version changed from %d to %d", version, versionAfter)
848	}
849	edits := convertEdits(resp)
850	if len(edits) == 0 {
851		return nil
852	}
853	return e.editBufferLocked(ctx, path, edits)
854}
855
856func (e *Editor) checkBufferPosition(path string, pos Pos) error {
857	e.mu.Lock()
858	defer e.mu.Unlock()
859	buf, ok := e.buffers[path]
860	if !ok {
861		return fmt.Errorf("buffer %q is not open", path)
862	}
863	if !inText(pos, buf.content) {
864		return fmt.Errorf("position %v is invalid in buffer %q", pos, path)
865	}
866	return nil
867}
868
869// RunGenerate runs `go generate` non-recursively in the workdir-relative dir
870// path. It does not report any resulting file changes as a watched file
871// change, so must be followed by a call to Workdir.CheckForFileChanges once
872// the generate command has completed.
873func (e *Editor) RunGenerate(ctx context.Context, dir string) error {
874	if e.Server == nil {
875		return nil
876	}
877	absDir := e.sandbox.Workdir.AbsPath(dir)
878	jsonArgs, err := source.MarshalArgs(span.URIFromPath(absDir), false)
879	if err != nil {
880		return err
881	}
882	params := &protocol.ExecuteCommandParams{
883		Command:   source.CommandGenerate.ID(),
884		Arguments: jsonArgs,
885	}
886	if _, err := e.ExecuteCommand(ctx, params); err != nil {
887		return fmt.Errorf("running generate: %v", err)
888	}
889	// Unfortunately we can't simply poll the workdir for file changes here,
890	// because server-side command may not have completed. In regtests, we can
891	// Await this state change, but here we must delegate that responsibility to
892	// the caller.
893	return nil
894}
895
896// CodeLens executes a codelens request on the server.
897func (e *Editor) CodeLens(ctx context.Context, path string) ([]protocol.CodeLens, error) {
898	if e.Server == nil {
899		return nil, nil
900	}
901	e.mu.Lock()
902	_, ok := e.buffers[path]
903	e.mu.Unlock()
904	if !ok {
905		return nil, fmt.Errorf("buffer %q is not open", path)
906	}
907	params := &protocol.CodeLensParams{
908		TextDocument: e.textDocumentIdentifier(path),
909	}
910	lens, err := e.Server.CodeLens(ctx, params)
911	if err != nil {
912		return nil, err
913	}
914	return lens, nil
915}
916
917// Completion executes a completion request on the server.
918func (e *Editor) Completion(ctx context.Context, path string, pos Pos) (*protocol.CompletionList, error) {
919	if e.Server == nil {
920		return nil, nil
921	}
922	e.mu.Lock()
923	_, ok := e.buffers[path]
924	e.mu.Unlock()
925	if !ok {
926		return nil, fmt.Errorf("buffer %q is not open", path)
927	}
928	params := &protocol.CompletionParams{
929		TextDocumentPositionParams: protocol.TextDocumentPositionParams{
930			TextDocument: e.textDocumentIdentifier(path),
931			Position:     pos.ToProtocolPosition(),
932		},
933	}
934	completions, err := e.Server.Completion(ctx, params)
935	if err != nil {
936		return nil, err
937	}
938	return completions, nil
939}
940
941// AcceptCompletion accepts a completion for the given item at the given
942// position.
943func (e *Editor) AcceptCompletion(ctx context.Context, path string, pos Pos, item protocol.CompletionItem) error {
944	if e.Server == nil {
945		return nil
946	}
947	e.mu.Lock()
948	defer e.mu.Unlock()
949	_, ok := e.buffers[path]
950	if !ok {
951		return fmt.Errorf("buffer %q is not open", path)
952	}
953	return e.editBufferLocked(ctx, path, convertEdits(append([]protocol.TextEdit{
954		*item.TextEdit,
955	}, item.AdditionalTextEdits...)))
956}
957
958// References executes a reference request on the server.
959func (e *Editor) References(ctx context.Context, path string, pos Pos) ([]protocol.Location, error) {
960	if e.Server == nil {
961		return nil, nil
962	}
963	e.mu.Lock()
964	_, ok := e.buffers[path]
965	e.mu.Unlock()
966	if !ok {
967		return nil, fmt.Errorf("buffer %q is not open", path)
968	}
969	params := &protocol.ReferenceParams{
970		TextDocumentPositionParams: protocol.TextDocumentPositionParams{
971			TextDocument: e.textDocumentIdentifier(path),
972			Position:     pos.ToProtocolPosition(),
973		},
974		Context: protocol.ReferenceContext{
975			IncludeDeclaration: true,
976		},
977	}
978	locations, err := e.Server.References(ctx, params)
979	if err != nil {
980		return nil, err
981	}
982	return locations, nil
983}
984
985// CodeAction executes a codeAction request on the server.
986func (e *Editor) CodeAction(ctx context.Context, path string, rng *protocol.Range) ([]protocol.CodeAction, error) {
987	if e.Server == nil {
988		return nil, nil
989	}
990	e.mu.Lock()
991	_, ok := e.buffers[path]
992	e.mu.Unlock()
993	if !ok {
994		return nil, fmt.Errorf("buffer %q is not open", path)
995	}
996	params := &protocol.CodeActionParams{
997		TextDocument: e.textDocumentIdentifier(path),
998	}
999	if rng != nil {
1000		params.Range = *rng
1001	}
1002	lens, err := e.Server.CodeAction(ctx, params)
1003	if err != nil {
1004		return nil, err
1005	}
1006	return lens, nil
1007}
1008
1009// Hover triggers a hover at the given position in an open buffer.
1010func (e *Editor) Hover(ctx context.Context, path string, pos Pos) (*protocol.MarkupContent, Pos, error) {
1011	if err := e.checkBufferPosition(path, pos); err != nil {
1012		return nil, Pos{}, err
1013	}
1014	params := &protocol.HoverParams{}
1015	params.TextDocument.URI = e.sandbox.Workdir.URI(path)
1016	params.Position = pos.ToProtocolPosition()
1017
1018	resp, err := e.Server.Hover(ctx, params)
1019	if err != nil {
1020		return nil, Pos{}, errors.Errorf("hover: %w", err)
1021	}
1022	if resp == nil {
1023		return nil, Pos{}, nil
1024	}
1025	return &resp.Contents, fromProtocolPosition(resp.Range.Start), nil
1026}
1027
1028func (e *Editor) DocumentLink(ctx context.Context, path string) ([]protocol.DocumentLink, error) {
1029	if e.Server == nil {
1030		return nil, nil
1031	}
1032	params := &protocol.DocumentLinkParams{}
1033	params.TextDocument.URI = e.sandbox.Workdir.URI(path)
1034	return e.Server.DocumentLink(ctx, params)
1035}
1036