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	"context"
9	"fmt"
10	"strings"
11	"sync"
12
13	"golang.org/x/tools/internal/jsonrpc2"
14	"golang.org/x/tools/internal/lsp/protocol"
15)
16
17// Editor is a fake editor client.  It keeps track of client state and can be
18// used for writing LSP tests.
19type Editor struct {
20	// server, client, and workspace are concurrency safe and written only at
21	// construction, so do not require synchronization.
22	server protocol.Server
23	client *Client
24	ws     *Workspace
25
26	// Since this editor is intended just for testing, we use very coarse
27	// locking.
28	mu sync.Mutex
29	// Editor state.
30	buffers     map[string]buffer
31	lastMessage *protocol.ShowMessageParams
32	logs        []*protocol.LogMessageParams
33	diagnostics *protocol.PublishDiagnosticsParams
34	events      []interface{}
35	// Capabilities / Options
36	serverCapabilities protocol.ServerCapabilities
37}
38
39type buffer struct {
40	version int
41	path    string
42	content []string
43}
44
45func (b buffer) text() string {
46	return strings.Join(b.content, "\n")
47}
48
49// NewConnectedEditor creates a new editor that dispatches the LSP across the
50// provided jsonrpc2 connection.
51//
52// The returned editor is initialized and ready to use.
53func NewConnectedEditor(ctx context.Context, ws *Workspace, conn *jsonrpc2.Conn) (*Editor, error) {
54	e := NewEditor(ws)
55	e.server = protocol.ServerDispatcher(conn)
56	e.client = &Client{Editor: e}
57	conn.AddHandler(protocol.ClientHandler(e.client))
58	if err := e.initialize(ctx); err != nil {
59		return nil, err
60	}
61	e.ws.AddWatcher(e.onFileChanges)
62	return e, nil
63}
64
65// NewEditor Creates a new Editor.
66func NewEditor(ws *Workspace) *Editor {
67	return &Editor{
68		buffers: make(map[string]buffer),
69		ws:      ws,
70	}
71}
72
73// ShutdownAndExit shuts down the client and issues the editor exit.
74func (e *Editor) ShutdownAndExit(ctx context.Context) error {
75	if e.server != nil {
76		if err := e.server.Shutdown(ctx); err != nil {
77			return fmt.Errorf("Shutdown: %v", err)
78		}
79		// Not all LSP clients issue the exit RPC, but we do so here to ensure that
80		// we gracefully handle it on multi-session servers.
81		if err := e.server.Exit(ctx); err != nil {
82			return fmt.Errorf("Exit: %v", err)
83		}
84	}
85	return nil
86}
87
88// Client returns the LSP client for this editor.
89func (e *Editor) Client() *Client {
90	return e.client
91}
92
93func (e *Editor) configuration() map[string]interface{} {
94	return map[string]interface{}{
95		"env": map[string]interface{}{
96			"GOPATH":      e.ws.GOPATH(),
97			"GO111MODULE": "on",
98		},
99	}
100}
101
102func (e *Editor) initialize(ctx context.Context) error {
103	params := &protocol.ParamInitialize{}
104	params.ClientInfo.Name = "fakeclient"
105	params.ClientInfo.Version = "v1.0.0"
106	params.RootURI = e.ws.RootURI()
107
108	// TODO: set client capabilities.
109	params.Trace = "messages"
110	// TODO: support workspace folders.
111
112	if e.server != nil {
113		resp, err := e.server.Initialize(ctx, params)
114		if err != nil {
115			return fmt.Errorf("Initialize: %v", err)
116		}
117		e.mu.Lock()
118		e.serverCapabilities = resp.Capabilities
119		e.mu.Unlock()
120
121		if err := e.server.Initialized(ctx, &protocol.InitializedParams{}); err != nil {
122			return fmt.Errorf("Initialized: %v", err)
123		}
124	}
125	return nil
126}
127
128func (e *Editor) onFileChanges(ctx context.Context, evts []FileEvent) {
129	if e.server == nil {
130		return
131	}
132	var lspevts []protocol.FileEvent
133	for _, evt := range evts {
134		lspevts = append(lspevts, evt.ProtocolEvent)
135	}
136	e.server.DidChangeWatchedFiles(ctx, &protocol.DidChangeWatchedFilesParams{
137		Changes: lspevts,
138	})
139}
140
141// OpenFile creates a buffer for the given workspace-relative file.
142func (e *Editor) OpenFile(ctx context.Context, path string) error {
143	content, err := e.ws.ReadFile(path)
144	if err != nil {
145		return err
146	}
147	buf := newBuffer(path, content)
148	e.mu.Lock()
149	e.buffers[path] = buf
150	item := textDocumentItem(e.ws, buf)
151	e.mu.Unlock()
152
153	if e.server != nil {
154		if err := e.server.DidOpen(ctx, &protocol.DidOpenTextDocumentParams{
155			TextDocument: item,
156		}); err != nil {
157			return fmt.Errorf("DidOpen: %v", err)
158		}
159	}
160	return nil
161}
162
163func newBuffer(path, content string) buffer {
164	return buffer{
165		version: 1,
166		path:    path,
167		content: strings.Split(content, "\n"),
168	}
169}
170
171func textDocumentItem(ws *Workspace, buf buffer) protocol.TextDocumentItem {
172	uri := ws.URI(buf.path)
173	languageID := ""
174	if strings.HasSuffix(buf.path, ".go") {
175		// TODO: what about go.mod files? What is their language ID?
176		languageID = "go"
177	}
178	return protocol.TextDocumentItem{
179		URI:        uri,
180		LanguageID: languageID,
181		Version:    float64(buf.version),
182		Text:       buf.text(),
183	}
184}
185
186// CreateBuffer creates a new unsaved buffer corresponding to the workspace
187// path, containing the given textual content.
188func (e *Editor) CreateBuffer(ctx context.Context, path, content string) error {
189	buf := newBuffer(path, content)
190	e.mu.Lock()
191	e.buffers[path] = buf
192	item := textDocumentItem(e.ws, buf)
193	e.mu.Unlock()
194
195	if e.server != nil {
196		if err := e.server.DidOpen(ctx, &protocol.DidOpenTextDocumentParams{
197			TextDocument: item,
198		}); err != nil {
199			return fmt.Errorf("DidOpen: %v", err)
200		}
201	}
202	return nil
203}
204
205// CloseBuffer removes the current buffer (regardless of whether it is saved).
206func (e *Editor) CloseBuffer(ctx context.Context, path string) error {
207	e.mu.Lock()
208	_, ok := e.buffers[path]
209	if !ok {
210		e.mu.Unlock()
211		return fmt.Errorf("unknown path %q", path)
212	}
213	delete(e.buffers, path)
214	e.mu.Unlock()
215
216	if e.server != nil {
217		if err := e.server.DidClose(ctx, &protocol.DidCloseTextDocumentParams{
218			TextDocument: protocol.TextDocumentIdentifier{
219				URI: e.ws.URI(path),
220			},
221		}); err != nil {
222			return fmt.Errorf("DidClose: %v", err)
223		}
224	}
225	return nil
226}
227
228// WriteBuffer writes the content of the buffer specified by the given path to
229// the filesystem.
230func (e *Editor) WriteBuffer(ctx context.Context, path string) error {
231	e.mu.Lock()
232	buf, ok := e.buffers[path]
233	if !ok {
234		e.mu.Unlock()
235		return fmt.Errorf(fmt.Sprintf("unknown buffer: %q", path))
236	}
237	content := buf.text()
238	includeText := false
239	syncOptions, ok := e.serverCapabilities.TextDocumentSync.(protocol.TextDocumentSyncOptions)
240	if ok {
241		includeText = syncOptions.Save.IncludeText
242	}
243	e.mu.Unlock()
244
245	docID := protocol.TextDocumentIdentifier{
246		URI: e.ws.URI(buf.path),
247	}
248	if e.server != nil {
249		if err := e.server.WillSave(ctx, &protocol.WillSaveTextDocumentParams{
250			TextDocument: docID,
251			Reason:       protocol.Manual,
252		}); err != nil {
253			return fmt.Errorf("WillSave: %v", err)
254		}
255	}
256	if err := e.ws.WriteFile(ctx, path, content); err != nil {
257		return fmt.Errorf("writing %q: %v", path, err)
258	}
259	if e.server != nil {
260		params := &protocol.DidSaveTextDocumentParams{
261			TextDocument: protocol.VersionedTextDocumentIdentifier{
262				Version:                float64(buf.version),
263				TextDocumentIdentifier: docID,
264			},
265		}
266		if includeText {
267			params.Text = &content
268		}
269		if err := e.server.DidSave(ctx, params); err != nil {
270			return fmt.Errorf("DidSave: %v", err)
271		}
272	}
273	return nil
274}
275
276// EditBuffer applies the given test edits to the buffer identified by path.
277func (e *Editor) EditBuffer(ctx context.Context, path string, edits []Edit) error {
278	params, err := e.doEdits(ctx, path, edits)
279	if err != nil {
280		return err
281	}
282	if e.server != nil {
283		if err := e.server.DidChange(ctx, params); err != nil {
284			return fmt.Errorf("DidChange: %v", err)
285		}
286	}
287	return nil
288}
289
290func (e *Editor) doEdits(ctx context.Context, path string, edits []Edit) (*protocol.DidChangeTextDocumentParams, error) {
291	e.mu.Lock()
292	defer e.mu.Unlock()
293	buf, ok := e.buffers[path]
294	if !ok {
295		return nil, fmt.Errorf("unknown buffer %q", path)
296	}
297	var (
298		content = make([]string, len(buf.content))
299		err     error
300		evts    []protocol.TextDocumentContentChangeEvent
301	)
302	copy(content, buf.content)
303	for _, edit := range edits {
304		content, err = editContent(content, edit)
305		if err != nil {
306			return nil, err
307		}
308		evts = append(evts, edit.toProtocolChangeEvent())
309	}
310	buf.content = content
311	buf.version++
312	e.buffers[path] = buf
313	params := &protocol.DidChangeTextDocumentParams{
314		TextDocument: protocol.VersionedTextDocumentIdentifier{
315			Version: float64(buf.version),
316			TextDocumentIdentifier: protocol.TextDocumentIdentifier{
317				URI: e.ws.URI(buf.path),
318			},
319		},
320		ContentChanges: evts,
321	}
322	return params, nil
323}
324
325// GoToDefinition jumps to the definition of the symbol at the given position
326// in an open buffer.
327func (e *Editor) GoToDefinition(ctx context.Context, path string, pos Pos) (string, Pos, error) {
328	if err := e.checkBufferPosition(path, pos); err != nil {
329		return "", Pos{}, err
330	}
331	params := &protocol.DefinitionParams{}
332	params.TextDocument.URI = e.ws.URI(path)
333	params.Position = pos.toProtocolPosition()
334
335	resp, err := e.server.Definition(ctx, params)
336	if err != nil {
337		return "", Pos{}, fmt.Errorf("Definition: %v", err)
338	}
339	if len(resp) == 0 {
340		return "", Pos{}, nil
341	}
342	newPath := e.ws.URIToPath(resp[0].URI)
343	newPos := fromProtocolPosition(resp[0].Range.Start)
344	e.OpenFile(ctx, newPath)
345	return newPath, newPos, nil
346}
347
348func (e *Editor) checkBufferPosition(path string, pos Pos) error {
349	e.mu.Lock()
350	defer e.mu.Unlock()
351	buf, ok := e.buffers[path]
352	if !ok {
353		return fmt.Errorf("buffer %q is not open", path)
354	}
355	if !inText(pos, buf.content) {
356		return fmt.Errorf("position %v is invalid in buffer %q", pos, path)
357	}
358	return nil
359}
360
361// TODO: expose more client functionality, for example Hover, CodeAction,
362// Rename, Completion, etc.  setting the content of an entire buffer, etc.
363