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