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// Shutdown issues the 'shutdown' LSP notification.
74func (e *Editor) Shutdown(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	}
80	return nil
81}
82
83// Exit issues the 'exit' LSP notification.
84func (e *Editor) Exit(ctx context.Context) error {
85	if e.server != nil {
86		// Not all LSP clients issue the exit RPC, but we do so here to ensure that
87		// we gracefully handle it on multi-session servers.
88		if err := e.server.Exit(ctx); err != nil {
89			return fmt.Errorf("Exit: %v", err)
90		}
91	}
92	return nil
93}
94
95// Client returns the LSP client for this editor.
96func (e *Editor) Client() *Client {
97	return e.client
98}
99
100func (e *Editor) configuration() map[string]interface{} {
101	return map[string]interface{}{
102		"env": map[string]interface{}{
103			"GOPATH":      e.ws.GOPATH(),
104			"GO111MODULE": "on",
105		},
106	}
107}
108
109func (e *Editor) initialize(ctx context.Context) error {
110	params := &protocol.ParamInitialize{}
111	params.ClientInfo.Name = "fakeclient"
112	params.ClientInfo.Version = "v1.0.0"
113	params.RootURI = e.ws.RootURI()
114
115	// TODO: set client capabilities.
116	params.Trace = "messages"
117	// TODO: support workspace folders.
118
119	if e.server != nil {
120		resp, err := e.server.Initialize(ctx, params)
121		if err != nil {
122			return fmt.Errorf("initialize: %v", err)
123		}
124		e.mu.Lock()
125		e.serverCapabilities = resp.Capabilities
126		e.mu.Unlock()
127
128		if err := e.server.Initialized(ctx, &protocol.InitializedParams{}); err != nil {
129			return fmt.Errorf("initialized: %v", err)
130		}
131	}
132	return nil
133}
134
135func (e *Editor) onFileChanges(ctx context.Context, evts []FileEvent) {
136	if e.server == nil {
137		return
138	}
139	var lspevts []protocol.FileEvent
140	for _, evt := range evts {
141		lspevts = append(lspevts, evt.ProtocolEvent)
142	}
143	e.server.DidChangeWatchedFiles(ctx, &protocol.DidChangeWatchedFilesParams{
144		Changes: lspevts,
145	})
146}
147
148// OpenFile creates a buffer for the given workspace-relative file.
149func (e *Editor) OpenFile(ctx context.Context, path string) error {
150	content, err := e.ws.ReadFile(path)
151	if err != nil {
152		return err
153	}
154	buf := newBuffer(path, content)
155	e.mu.Lock()
156	e.buffers[path] = buf
157	item := textDocumentItem(e.ws, buf)
158	e.mu.Unlock()
159
160	if e.server != nil {
161		if err := e.server.DidOpen(ctx, &protocol.DidOpenTextDocumentParams{
162			TextDocument: item,
163		}); err != nil {
164			return fmt.Errorf("DidOpen: %v", err)
165		}
166	}
167	return nil
168}
169
170func newBuffer(path, content string) buffer {
171	return buffer{
172		version: 1,
173		path:    path,
174		content: strings.Split(content, "\n"),
175	}
176}
177
178func textDocumentItem(ws *Workspace, buf buffer) protocol.TextDocumentItem {
179	uri := ws.URI(buf.path)
180	languageID := ""
181	if strings.HasSuffix(buf.path, ".go") {
182		// TODO: what about go.mod files? What is their language ID?
183		languageID = "go"
184	}
185	return protocol.TextDocumentItem{
186		URI:        uri,
187		LanguageID: languageID,
188		Version:    float64(buf.version),
189		Text:       buf.text(),
190	}
191}
192
193// CreateBuffer creates a new unsaved buffer corresponding to the workspace
194// path, containing the given textual content.
195func (e *Editor) CreateBuffer(ctx context.Context, path, content string) error {
196	buf := newBuffer(path, content)
197	e.mu.Lock()
198	e.buffers[path] = buf
199	item := textDocumentItem(e.ws, buf)
200	e.mu.Unlock()
201
202	if e.server != nil {
203		if err := e.server.DidOpen(ctx, &protocol.DidOpenTextDocumentParams{
204			TextDocument: item,
205		}); err != nil {
206			return fmt.Errorf("DidOpen: %v", err)
207		}
208	}
209	return nil
210}
211
212// CloseBuffer removes the current buffer (regardless of whether it is saved).
213func (e *Editor) CloseBuffer(ctx context.Context, path string) error {
214	e.mu.Lock()
215	_, ok := e.buffers[path]
216	if !ok {
217		e.mu.Unlock()
218		return fmt.Errorf("unknown path %q", path)
219	}
220	delete(e.buffers, path)
221	e.mu.Unlock()
222
223	if e.server != nil {
224		if err := e.server.DidClose(ctx, &protocol.DidCloseTextDocumentParams{
225			TextDocument: protocol.TextDocumentIdentifier{
226				URI: e.ws.URI(path),
227			},
228		}); err != nil {
229			return fmt.Errorf("DidClose: %v", err)
230		}
231	}
232	return nil
233}
234
235// WriteBuffer writes the content of the buffer specified by the given path to
236// the filesystem.
237func (e *Editor) WriteBuffer(ctx context.Context, path string) error {
238	e.mu.Lock()
239	buf, ok := e.buffers[path]
240	if !ok {
241		e.mu.Unlock()
242		return fmt.Errorf(fmt.Sprintf("unknown buffer: %q", path))
243	}
244	content := buf.text()
245	includeText := false
246	syncOptions, ok := e.serverCapabilities.TextDocumentSync.(protocol.TextDocumentSyncOptions)
247	if ok {
248		includeText = syncOptions.Save.IncludeText
249	}
250	e.mu.Unlock()
251
252	docID := protocol.TextDocumentIdentifier{
253		URI: e.ws.URI(buf.path),
254	}
255	if e.server != nil {
256		if err := e.server.WillSave(ctx, &protocol.WillSaveTextDocumentParams{
257			TextDocument: docID,
258			Reason:       protocol.Manual,
259		}); err != nil {
260			return fmt.Errorf("WillSave: %v", err)
261		}
262	}
263	if err := e.ws.WriteFile(ctx, path, content); err != nil {
264		return fmt.Errorf("writing %q: %v", path, err)
265	}
266	if e.server != nil {
267		params := &protocol.DidSaveTextDocumentParams{
268			TextDocument: protocol.VersionedTextDocumentIdentifier{
269				Version:                float64(buf.version),
270				TextDocumentIdentifier: docID,
271			},
272		}
273		if includeText {
274			params.Text = &content
275		}
276		if err := e.server.DidSave(ctx, params); err != nil {
277			return fmt.Errorf("DidSave: %v", err)
278		}
279	}
280	return nil
281}
282
283// EditBuffer applies the given test edits to the buffer identified by path.
284func (e *Editor) EditBuffer(ctx context.Context, path string, edits []Edit) error {
285	params, err := e.doEdits(ctx, path, edits)
286	if err != nil {
287		return err
288	}
289	if e.server != nil {
290		if err := e.server.DidChange(ctx, params); err != nil {
291			return fmt.Errorf("DidChange: %v", err)
292		}
293	}
294	return nil
295}
296
297func (e *Editor) doEdits(ctx context.Context, path string, edits []Edit) (*protocol.DidChangeTextDocumentParams, error) {
298	e.mu.Lock()
299	defer e.mu.Unlock()
300	buf, ok := e.buffers[path]
301	if !ok {
302		return nil, fmt.Errorf("unknown buffer %q", path)
303	}
304	var (
305		content = make([]string, len(buf.content))
306		err     error
307		evts    []protocol.TextDocumentContentChangeEvent
308	)
309	copy(content, buf.content)
310	for _, edit := range edits {
311		content, err = editContent(content, edit)
312		if err != nil {
313			return nil, err
314		}
315		evts = append(evts, edit.toProtocolChangeEvent())
316	}
317	buf.content = content
318	buf.version++
319	e.buffers[path] = buf
320	params := &protocol.DidChangeTextDocumentParams{
321		TextDocument: protocol.VersionedTextDocumentIdentifier{
322			Version: float64(buf.version),
323			TextDocumentIdentifier: protocol.TextDocumentIdentifier{
324				URI: e.ws.URI(buf.path),
325			},
326		},
327		ContentChanges: evts,
328	}
329	return params, nil
330}
331
332// GoToDefinition jumps to the definition of the symbol at the given position
333// in an open buffer.
334func (e *Editor) GoToDefinition(ctx context.Context, path string, pos Pos) (string, Pos, error) {
335	if err := e.checkBufferPosition(path, pos); err != nil {
336		return "", Pos{}, err
337	}
338	params := &protocol.DefinitionParams{}
339	params.TextDocument.URI = e.ws.URI(path)
340	params.Position = pos.toProtocolPosition()
341
342	resp, err := e.server.Definition(ctx, params)
343	if err != nil {
344		return "", Pos{}, fmt.Errorf("definition: %v", err)
345	}
346	if len(resp) == 0 {
347		return "", Pos{}, nil
348	}
349	newPath := e.ws.URIToPath(resp[0].URI)
350	newPos := fromProtocolPosition(resp[0].Range.Start)
351	if err := e.OpenFile(ctx, newPath); err != nil {
352		return "", Pos{}, fmt.Errorf("OpenFile: %v", err)
353	}
354	return newPath, newPos, nil
355}
356
357func (e *Editor) checkBufferPosition(path string, pos Pos) error {
358	e.mu.Lock()
359	defer e.mu.Unlock()
360	buf, ok := e.buffers[path]
361	if !ok {
362		return fmt.Errorf("buffer %q is not open", path)
363	}
364	if !inText(pos, buf.content) {
365		return fmt.Errorf("position %v is invalid in buffer %q", pos, path)
366	}
367	return nil
368}
369
370// TODO: expose more client functionality, for example Hover, CodeAction,
371// Rename, Completion, etc.  setting the content of an entire buffer, etc.
372