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