github.com/jmigpin/editor@v1.6.0/core/lsproto/client.go (about) 1 package lsproto 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "io" 8 "net" 9 "net/rpc" 10 "sync" 11 "time" 12 13 "github.com/jmigpin/editor/util/ctxutil" 14 "github.com/jmigpin/editor/util/iout" 15 "github.com/jmigpin/editor/util/iout/iorw" 16 ) 17 18 type Client struct { 19 rcli *rpc.Client 20 li *LangInstance 21 readLoopWait sync.WaitGroup 22 23 lock struct { 24 sync.Mutex 25 fversions map[string]int 26 //folders []*WorkspaceFolder 27 } 28 29 serverCapabilities struct { 30 workspace struct { 31 folders bool 32 symbol bool 33 } 34 rename bool 35 } 36 } 37 38 //---------- 39 40 func NewClientTCP(ctx context.Context, addr string, li *LangInstance) (*Client, error) { 41 dialer := net.Dialer{Timeout: 5 * time.Second} 42 conn, err := dialer.DialContext(ctx, "tcp", addr) 43 if err != nil { 44 return nil, err 45 } 46 cli := NewClientIO(ctx, conn, li) 47 return cli, nil 48 } 49 50 //---------- 51 52 func NewClientIO(ctx context.Context, rwc io.ReadWriteCloser, li *LangInstance) *Client { 53 cli := &Client{li: li} 54 cli.lock.fversions = map[string]int{} 55 56 cc := NewJsonCodec(rwc) 57 cc.OnNotificationMessage = cli.onNotificationMessage 58 cc.OnUnexpectedServerReply = cli.onUnexpectedServerReply 59 60 cli.rcli = rpc.NewClientWithCodec(cc) 61 62 // wait for the codec readloop 63 cli.readLoopWait.Add(1) 64 go func() { 65 defer cli.readLoopWait.Done() 66 if err := cc.ReadLoop(); err != nil { 67 cli.li.lang.PrintWrapError(err) 68 cli.li.cancelCtx() 69 } 70 }() 71 72 // close when ctx is done 73 go func() { 74 select { 75 case <-ctx.Done(): 76 if err := cli.sendClose(); err != nil { 77 // Commented: best effort, ignore errors 78 //cli.li.lang.PrintWrapError(err) 79 } 80 if err := rwc.Close(); err != nil { 81 cli.li.lang.PrintWrapError(err) 82 } 83 } 84 }() 85 86 return cli 87 } 88 89 //---------- 90 91 func (cli *Client) Wait() error { 92 cli.readLoopWait.Wait() 93 return nil 94 } 95 96 func (cli *Client) sendClose() error { 97 me := iout.MultiError{} 98 if err := cli.ShutdownRequest(); err != nil { 99 me.Add(err) 100 } else { 101 me.Add(cli.ExitNotification()) 102 } 103 return me.Result() 104 } 105 106 //---------- 107 108 func (cli *Client) Call(ctx context.Context, method string, args, reply interface{}) error { 109 lspResp := &Response{} 110 fn := func() error { 111 return cli.rcli.Call(method, args, lspResp) 112 } 113 lateFn := func(err error) { 114 if err != nil { 115 err = fmt.Errorf("call late: %w", err) 116 cli.li.lang.PrintWrapError(err) 117 } 118 } 119 err := ctxutil.Call(ctx, method, fn, lateFn) 120 if err != nil { 121 err = fmt.Errorf("call: %w", err) 122 return cli.li.lang.WrapError(err) 123 } 124 125 // not expecting a reply 126 if _, ok := noreplyMethod(method); ok { 127 return nil 128 } 129 130 // soft error (rpc data with error content) 131 if lspResp.Error != nil { 132 return cli.li.lang.WrapError(lspResp.Error) 133 } 134 135 // decode result 136 return decodeJsonRaw(lspResp.Result, reply) 137 } 138 139 //---------- 140 141 func (cli *Client) onNotificationMessage(msg *NotificationMessage) { 142 // Msgs like: 143 // - a notification was sent to the srv, not expecting a reply, but it receives one because it was an error (has id) 144 // {"error":{"code":-32601,"message":"method not found"},"id":2,"jsonrpc":"2.0"} 145 146 //logJson("notification <--: ", msg) 147 } 148 149 func (cli *Client) onUnexpectedServerReply(resp *Response) { 150 if resp.Error != nil { 151 // json-rpc error codes: https://www.jsonrpc.org/specification 152 report := false 153 switch resp.Error.Code { 154 case -32601: // method not found 155 report = true 156 case -32602: // invalid params 157 report = true 158 159 //case -32603: // internal error 160 //report = true 161 } 162 if report { 163 err := fmt.Errorf("id=%v, code=%v, msg=%q, data=%v", resp.Id, resp.Error.Code, resp.Error.Message, resp.Error.Data) 164 cli.li.lang.PrintWrapError(err) 165 } 166 } 167 } 168 169 //---------- 170 171 func (cli *Client) Initialize(ctx context.Context) error { 172 //opt, err := cli.initializeParams() 173 //if err != nil { 174 // return err 175 //} 176 opt := json.RawMessage("{}") 177 logJson("opt -->: ", opt) 178 179 serverCapabilities := (interface{})(nil) 180 if err := cli.Call(ctx, "initialize", opt, &serverCapabilities); err != nil { 181 return err 182 } 183 logJson("initialize <--: ", serverCapabilities) 184 185 cli.readServerCapabilities(serverCapabilities) 186 187 // send "initialized" (gopls: "no views" error without this) 188 opt2 := json.RawMessage("{}") 189 return cli.Call(ctx, "noreply:initialized", opt2, nil) 190 } 191 192 //func (cli *Client) initializeParams() (json.RawMessage, error) { 193 // rootUri, err := cli.rootUri() 194 // if err != nil { 195 // return nil, err 196 // } 197 // _ = rootUri 198 199 // // workspace folders 200 // cli.folders = []*WorkspaceFolder{{Uri: rootUri}} 201 // foldersBytes, err := encodeJson(cli.folders) 202 // if err != nil { 203 // return nil, err 204 // } 205 206 // // other capabilities 207 // //"capabilities":{ 208 // // "workspace":{ 209 // // "configuration":true, 210 // // "workspaceFolders":true 211 // // }, 212 // // "textDocument":{ 213 // // "publishDiagnostics":{ 214 // // "relatedInformation":true 215 // // } 216 // // } 217 // //} 218 219 // raw := json.RawMessage("{" + 220 // // TODO: gopls is not allowing rooturi=null at the moment... 221 // fmt.Sprintf("%q:%q", "rootUri", rootUri) + "," + 222 // // set workspace folders to use the later as "remove" value 223 // fmt.Sprintf("%q:%s", "workspaceFolders", foldersBytes) + 224 // "}") 225 // return raw, nil 226 //} 227 228 //func (cli *Client) rootUri() (DocumentUri, error) { 229 // // using a non-existent dir to prevent an lsp server to start scanning the user disk doesn't work well (ex: gopls gives "no views in the session" after the cache is gone) 230 // // use initial request file 231 // dir := filepath.Dir(cli.li.lang.InstanceReqFilename) 232 // rootUrl, err := absFilenameToUrl(dir) 233 // if err != nil { 234 // return "", err 235 // } 236 // return DocumentUri(rootUrl), nil 237 //} 238 239 func (cli *Client) readServerCapabilities(caps interface{}) { 240 path := "capabilities.workspace.workspaceFolders.supported" 241 v, err := JsonGetPath(caps, path) 242 if err == nil { 243 if b, ok := v.(bool); ok && b == true { 244 cli.serverCapabilities.workspace.folders = true 245 } 246 } 247 248 path = "capabilities.workspaceSymbolProvider" 249 v, err = JsonGetPath(caps, path) 250 if err == nil { 251 if b, ok := v.(bool); ok && b == true { 252 cli.serverCapabilities.workspace.symbol = true 253 } 254 } 255 256 path = "capabilities.renameProvider" 257 v, err = JsonGetPath(caps, path) 258 if err == nil { 259 if b, ok := v.(bool); ok && b == true { 260 cli.serverCapabilities.rename = true 261 } 262 } 263 } 264 265 //---------- 266 267 func (cli *Client) ShutdownRequest() error { 268 // https://microsoft.github.io/language-server-protocol/specification#shutdown 269 270 // TODO: shutdown request should expect a reply 271 // * clangd is sending a reply (ok) 272 // * gopls is not sending a reply (NOT OK) 273 274 // best effort, impose timeout 275 ctx := context.Background() 276 ctx2, cancel := context.WithTimeout(ctx, 1000*time.Millisecond) 277 defer cancel() 278 ctx = ctx2 279 280 err := cli.Call(ctx, "shutdown", nil, nil) 281 return err 282 } 283 284 func (cli *Client) ExitNotification() error { 285 // https://microsoft.github.io/language-server-protocol/specification#exit 286 287 // no ctx timeout needed, it's not expecting a reply 288 ctx := context.Background() 289 err := cli.Call(ctx, "noreply:exit", nil, nil) 290 return err 291 } 292 293 //---------- 294 295 func (cli *Client) TextDocumentDidOpen(ctx context.Context, filename, text string, version int) error { 296 // https://microsoft.github.io/language-server-protocol/specification#textDocument_didOpen 297 298 opt := &DidOpenTextDocumentParams{} 299 opt.TextDocument.LanguageId = cli.li.lang.Reg.Language 300 opt.TextDocument.Version = version 301 opt.TextDocument.Text = text 302 url, err := AbsFilenameToUrl(filename) 303 if err != nil { 304 return err 305 } 306 opt.TextDocument.Uri = DocumentUri(url) 307 return cli.Call(ctx, "noreply:textDocument/didOpen", opt, nil) 308 } 309 310 func (cli *Client) TextDocumentDidClose(ctx context.Context, filename string) error { 311 // https://microsoft.github.io/language-server-protocol/specification#textDocument_didClose 312 313 opt := &DidCloseTextDocumentParams{} 314 url, err := AbsFilenameToUrl(filename) 315 if err != nil { 316 return err 317 } 318 opt.TextDocument.Uri = DocumentUri(url) 319 return cli.Call(ctx, "noreply:textDocument/didClose", opt, nil) 320 } 321 322 func (cli *Client) TextDocumentDidChange(ctx context.Context, filename, text string, version int) error { 323 // https://microsoft.github.io/language-server-protocol/specification#textDocument_didChange 324 325 opt := &DidChangeTextDocumentParams{} 326 opt.TextDocument.Version = &version 327 url, err := AbsFilenameToUrl(filename) 328 if err != nil { 329 return err 330 } 331 opt.TextDocument.Uri = DocumentUri(url) 332 333 // text end line/column 334 rd := iorw.NewStringReaderAt(text) 335 pos, err := OffsetToPosition(rd, len(text)) 336 if err != nil { 337 return err 338 } 339 340 // changes 341 opt.ContentChanges = []*TextDocumentContentChangeEvent{ 342 { 343 Range: Range{ 344 Start: Position{0, 0}, 345 End: pos, 346 }, 347 //RangeLength: len(text), // TODO: not working? 348 Text: text, 349 }, 350 } 351 return cli.Call(ctx, "noreply:textDocument/didChange", opt, nil) 352 } 353 354 func (cli *Client) TextDocumentDidSave(ctx context.Context, filename string, text []byte) error { 355 // https://microsoft.github.io/language-server-protocol/specification#textDocument_didSave 356 357 opt := &DidSaveTextDocumentParams{} 358 opt.Text = string(text) // has omitempty 359 url, err := AbsFilenameToUrl(filename) 360 if err != nil { 361 return err 362 } 363 opt.TextDocument.Uri = DocumentUri(url) 364 365 return cli.Call(ctx, "noreply:textDocument/didSave", opt, nil) 366 } 367 368 //---------- 369 370 func (cli *Client) TextDocumentDefinition(ctx context.Context, filename string, pos Position) (*Location, error) { 371 // https://microsoft.github.io/language-server-protocol/specification#textDocument_definition 372 373 opt := &TextDocumentPositionParams{} 374 opt.Position = pos 375 url, err := AbsFilenameToUrl(filename) 376 if err != nil { 377 return nil, err 378 } 379 opt.TextDocument.Uri = DocumentUri(url) 380 381 result := []*Location{} 382 if err := cli.Call(ctx, "textDocument/definition", opt, &result); err != nil { 383 return nil, err 384 } 385 if len(result) == 0 { 386 return nil, fmt.Errorf("no results") 387 } 388 return result[0], nil // first result only 389 } 390 391 //---------- 392 393 func (cli *Client) TextDocumentImplementation(ctx context.Context, filename string, pos Position) (*Location, error) { 394 // https://microsoft.github.io/language-server-protocol/specification#textDocument_implementation 395 396 opt := &TextDocumentPositionParams{} 397 opt.Position = pos 398 url, err := AbsFilenameToUrl(filename) 399 if err != nil { 400 return nil, err 401 } 402 opt.TextDocument.Uri = DocumentUri(url) 403 404 result := []*Location{} 405 if err := cli.Call(ctx, "textDocument/implementation", opt, &result); err != nil { 406 return nil, err 407 } 408 if len(result) == 0 { 409 return nil, fmt.Errorf("no results") 410 } 411 return result[0], nil // first result only 412 } 413 414 //---------- 415 416 func (cli *Client) TextDocumentCompletion(ctx context.Context, filename string, pos Position) (*CompletionList, error) { 417 // https://microsoft.github.io/language-server-protocol/specification#textDocument_completion 418 419 opt := &CompletionParams{} 420 opt.Context.TriggerKind = 1 // invoked 421 opt.Position = pos 422 url, err := AbsFilenameToUrl(filename) 423 if err != nil { 424 return nil, err 425 } 426 opt.TextDocument.Uri = DocumentUri(url) 427 428 result := CompletionList{} 429 if err := cli.Call(ctx, "textDocument/completion", opt, &result); err != nil { 430 return nil, err 431 } 432 //logJson(result) 433 return &result, nil 434 } 435 436 //---------- 437 438 func (cli *Client) TextDocumentDidOpenVersion(ctx context.Context, filename string, b []byte) error { 439 440 cli.lock.Lock() 441 v, ok := cli.lock.fversions[filename] 442 if !ok { 443 v = 1 444 } else { 445 v++ 446 } 447 cli.lock.fversions[filename] = v 448 cli.lock.Unlock() 449 450 return cli.TextDocumentDidOpen(ctx, filename, string(b), v) 451 } 452 453 //---------- 454 455 //func (cli *Client) WorkspaceDidChangeWorkspaceFolders(ctx context.Context, added, removed []*WorkspaceFolder) error { 456 // opt := &DidChangeWorkspaceFoldersParams{} 457 // opt.Event = &WorkspaceFoldersChangeEvent{} 458 // opt.Event.Added = added 459 // opt.Event.Removed = removed 460 // err := cli.Call(ctx, "noreply:workspace/didChangeWorkspaceFolders", opt, nil) 461 // return err 462 //} 463 464 //---------- 465 466 //func (cli *Client) UpdateWorkspaceFolder(ctx context.Context, dir string) error { 467 // if !cli.serverCapabilities.workspace.folders { 468 // return nil 469 // } 470 471 // removed := cli.folders 472 // url, err := absFilenameToUrl(dir) 473 // if err != nil { 474 // return err 475 // } 476 // cli.folders = []*WorkspaceFolder{{Uri: DocumentUri(url)}} 477 // return cli.WorkspaceDidChangeWorkspaceFolders(ctx, cli.folders, removed) 478 479 //} 480 481 // TODO 482 //return cli.WorkspaceDidChangeConfiguration(ctx, dir) 483 //func (cli *Client) WorkspaceDidChangeConfiguration(ctx context.Context, dir string) error { 484 // url, err := absFilenameToUrl(dir) 485 // if err != nil { 486 // return err 487 // } 488 // //"settings":{"rootUri":"` + url + `"} 489 // opt := json.RawMessage(`{ 490 // "settings":{"workspaceFolders":[{"uri":"` + url + `"}]} 491 // }`) 492 // return cli.Call(ctx, "noreply:workspace/didChangeConfiguration", opt, nil) 493 //} 494 495 //---------- 496 497 func (cli *Client) TextDocumentRename(ctx context.Context, filename string, pos Position, newName string) (*WorkspaceEdit, error) { 498 //// Commented: try it anyway 499 //if !cli.serverCapabilities.rename { 500 // return nil, fmt.Errorf("server did not advertize rename capability") 501 //} 502 503 opt := &RenameParams{} 504 opt.NewName = newName 505 opt.Position = pos 506 url, err := AbsFilenameToUrl(filename) 507 if err != nil { 508 return nil, err 509 } 510 opt.TextDocument.Uri = DocumentUri(url) 511 result := WorkspaceEdit{} 512 err = cli.Call(ctx, "textDocument/rename", opt, &result) 513 return &result, err 514 } 515 516 //---------- 517 518 func (cli *Client) TextDocumentPrepareCallHierarchy(ctx context.Context, filename string, pos Position) ([]*CallHierarchyItem, error) { 519 opt := &CallHierarchyPrepareParams{} 520 opt.Position = pos 521 url, err := AbsFilenameToUrl(filename) 522 if err != nil { 523 return nil, err 524 } 525 opt.TextDocument.Uri = DocumentUri(url) 526 result := []*CallHierarchyItem{} 527 err = cli.Call(ctx, "textDocument/prepareCallHierarchy", opt, &result) 528 return result, err 529 } 530 func (cli *Client) CallHierarchyCalls(ctx context.Context, typ CallHierarchyCallType, item *CallHierarchyItem) ([]*CallHierarchyCall, error) { 531 method := "" 532 switch typ { 533 case IncomingChct: 534 method = "callHierarchy/incomingCalls" 535 case OutgoingChct: 536 method = "callHierarchy/outgoingCalls" 537 default: 538 panic("bad type") 539 } 540 opt := &CallHierarchyCallsParams{} 541 opt.Item = item 542 result := []*CallHierarchyCall{} 543 err := cli.Call(ctx, method, opt, &result) 544 return result, err 545 } 546 547 //---------- 548 549 func (cli *Client) TextDocumentReferences(ctx context.Context, filename string, pos Position) ([]*Location, error) { 550 opt := &ReferenceParams{} 551 opt.Context.IncludeDeclaration = true 552 url, err := AbsFilenameToUrl(filename) 553 if err != nil { 554 return nil, err 555 } 556 opt.TextDocument.Uri = DocumentUri(url) 557 opt.Position = pos 558 result := []*Location{} 559 err = cli.Call(ctx, "textDocument/references", opt, &result) 560 return result, err 561 }