1 # lsp/server: the loop. The transport the messages ride on, the files 2 # behind imports and Base, and the checker behind the diagnostics are passed 3 # in (transport/, files/, checker/answer.bend's Checker; checker/bend.bend's 4 # service is the real checker). The open documents are the 5 # loop's state. The checker runs on open and save, against the file on disk; 6 # the linter on every open, change and save, on the text the editor shows, 7 # each file told the role the imports of the tree give it, read as `bolt 8 # ` reads them (src/lint/plan.bend's graph) on open and save and kept 9 # for the changes between. 10 import Base 11 import 0xa81e161008ecc2f297c0a3065cb9b188/main.bend as Ezjson 12 import 0xa81e161008ecc2f297c0a3065cb9b188/src/value.bend as J 13 import ./proto.bend as Proto 14 import ./docs.bend as Docs 15 import ./nav.bend as Nav 16 import ./semantic.bend as Semantic 17 import ../rules.bend as Lint 18 import ../finding.bend as Finding 19 import ../config.bend as Config 20 import ../lint/plan.bend as Plan 21 import ../lint/world.bend as W 22 import ../role.bend as Role 23 import ../src.bend as Src 24 import ../noqa.bend as Noqa 25 import ./transport/service.bend as T 26 import ./checker/answer.bend as C 27 import ./files/service.bend as F 28 import ./report.bend as Rep 29 import ./enc.bend as Enc 30 import ../syntax/outline.bend as Outline 31 import ../syntax/lex.bend as Lex 32 import ../syntax/word.bend as Word 33 34 # the methods the server answers; MOther is anything else 35 type Method is Data: 36 MInitialize{} 37 MShutdown{} 38 MExit{} 39 MOpen{} 40 MChange{} 41 MSave{} 42 MClose{} 43 MHover{} 44 MDefinition{} 45 MSymbols{} 46 MCompletion{} 47 MReferences{} 48 MRename{} 49 MSemantic{} 50 MOther{} 51 52 # a message the server sends: a notification, already printed, or the 53 # response to a request, its result or its error 54 type Reply is Data: 55 RNote{body: String} 56 RResult{id: J.Json, result: J.Json} 57 RError{id: J.Json, code: String, message: String} 58 59 # what a request is answered with: a result, or an error's code and message 60 type Answer is Data: 61 Answer{result: J.Json} 62 Refusal{code: String, message: String} 63 64 # what the work behind a method gave: the notifications it sends, the answer 65 # (read for a request only), the documents after it. It never sees the id 66 type Work is Data: 67 Work{notes: List<&2, String>, answer: Answer, docs: List<&2, Docs.Doc>} 68 69 # what one message leads to: the replies to send, whether to stop, the documents 70 type Out is Data: 71 Out{replies: List<&2, Reply>, quit: Bool, docs: List<&2, Docs.Doc>} 72 73 # how the server treats a method: a request owed one response, a 74 # notification, exit, or a method it does not know 75 type Role is Data: 76 RRequest{} 77 RNotice{} 78 RExit{} 79 RUnknown{} 80 81 # a method by its name 82 def method(+name: String) -> Method: 83 Bool.pick(Method, String.eq(name, "initialize"), MInitialize{}, 84 Bool.pick(Method, String.eq(name, "shutdown"), MShutdown{}, 85 Bool.pick(Method, String.eq(name, "exit"), MExit{}, 86 Bool.pick(Method, String.eq(name, "textDocument/didOpen"), MOpen{}, 87 Bool.pick(Method, String.eq(name, "textDocument/didChange"), MChange{}, 88 Bool.pick(Method, String.eq(name, "textDocument/didSave"), MSave{}, 89 Bool.pick(Method, String.eq(name, "textDocument/didClose"), MClose{}, 90 Bool.pick(Method, String.eq(name, "textDocument/hover"), MHover{}, 91 Bool.pick(Method, String.eq(name, "textDocument/definition"), MDefinition{}, 92 Bool.pick(Method, String.eq(name, "textDocument/documentSymbol"), MSymbols{}, 93 Bool.pick(Method, String.eq(name, "textDocument/completion"), MCompletion{}, 94 Bool.pick(Method, String.eq(name, "textDocument/references"), MReferences{}, 95 Bool.pick(Method, String.eq(name, "textDocument/rename"), MRename{}, 96 Bool.pick(Method, String.eq(name, "textDocument/semanticTokens/full"), MSemantic{}, 97 MOther{})))))))))))))) 98 99 # a method's role 100 def role(mm: Method) -> Role: 101 match mm: 102 case MInitialize{}: 103 RRequest{} 104 case MShutdown{}: 105 RRequest{} 106 case MExit{}: 107 RExit{} 108 case MOpen{}: 109 RNotice{} 110 case MChange{}: 111 RNotice{} 112 case MSave{}: 113 RNotice{} 114 case MClose{}: 115 RNotice{} 116 case MHover{}: 117 RRequest{} 118 case MDefinition{}: 119 RRequest{} 120 case MSymbols{}: 121 RRequest{} 122 case MCompletion{}: 123 RRequest{} 124 case MReferences{}: 125 RRequest{} 126 case MRename{}: 127 RRequest{} 128 case MSemantic{}: 129 RRequest{} 130 case MOther{}: 131 RUnknown{} 132 133 # a message's method name, empty when it has none 134 def name_of(msg: J.Json) -> String: 135 Maybe.default(&2, String, Ezjson.as_str(Ezjson.get(msg, "method")), "") 136 137 # a message's id: null for a notification 138 def id_of(msg: J.Json) -> J.Json: 139 Ezjson.get(msg, "id") 140 141 # the document a request is about 142 def uri_of(params: J.Json) -> String: 143 Maybe.default(&2, String, Ezjson.as_str(Ezjson.get(Ezjson.get(params, "textDocument"), "uri")), "") 144 145 # the line of a request's position 146 def line_of(params: J.Json) -> U32: 147 Maybe.default(&2, U32, Ezjson.as_u32(Ezjson.get(Ezjson.get(params, "position"), "line")), 0) 148 149 # the character of a request's position 150 def col_of(params: J.Json) -> U32: 151 Maybe.default(&2, U32, Ezjson.as_u32(Ezjson.get(Ezjson.get(params, "position"), "character")), 0) 152 153 # a reply as the text sent 154 def text(rr: Reply) -> String: 155 match rr: 156 case RNote{body}: 157 body 158 case RResult{id, result}: 159 Ezjson.print(Proto.response(id, result)) 160 case RError{id, code, message}: 161 Ezjson.print(Proto.failure(id, code, message)) 162 163 # notifications as replies 164 def notices(ns: List<&2, String>) -> List<&2, Reply>: 165 match ns: 166 case Nil{}: 167 Nil{} 168 case Con{n, t}: 169 RNote{n} <> notices(t) 170 171 # the response to the request with this id 172 def respond(id: J.Json, aa: Answer) -> Reply: 173 match aa: 174 case Answer{result}: 175 RResult{id, result} 176 case Refusal{code, message}: 177 RError{id, code, message} 178 179 # a request the server does not know is refused; a notification is ignored 180 def unknown(id: J.Json) -> List<&2, Reply>: 181 match id: 182 case J.JNull{}: 183 [] 184 case other: 185 [RError{other, "-32601", "method not found"}] 186 187 # the work of a request: its result, no notifications 188 def answered(result: J.Json, docs: List<&2, Docs.Doc>) -> Work: 189 Work{[], Answer{result}, docs} 190 191 # the work of a notification: what it sends, and no answer 192 def noted(notes: List<&2, String>, docs: List<&2, Docs.Doc>) -> Work: 193 Work{notes, Answer{J.JNull{}}, docs} 194 195 def step.go(rr: Role, id: J.Json, ww: Work) -> Out: 196 match rr: 197 case RRequest{}: 198 Work{notes, answer, docs} = ww 199 Out{[respond(id, answer)], False{}, docs} 200 case RNotice{}: 201 Work{notes, answer, docs} = ww 202 Out{notices(notes), False{}, docs} 203 case RExit{}: 204 Work{notes, answer, docs} = ww 205 Out{[], True{}, docs} 206 case RUnknown{}: 207 Work{notes, answer, docs} = ww 208 Out{unknown(id), False{}, docs} 209 210 # the dispatcher's decision for one message, whatever its method's work gave: 211 # a request gets one response with its id, a notification its notices, exit 212 # stops, and a method it does not know is refused or ignored 213 def step(mm: Method, id: J.Json, ww: Work) -> Out: 214 step.go(role(mm), id, ww) 215 216 # publishing 217 # ---------- 218 # What open, change, save and close publish is decided by the pure pub.* 219 # defs below, over what the IO gave them: the checker's errors (ds) and the 220 # texts of the document's bolt.bend candidates (rs). work only gathers those. 221 222 # a document's diagnostics, as the notification sent: the checker's errors, 223 # then the linter's findings 224 def pub.note(cc: Enc.Cols, uri: String, ds: List<&2, Rep.Diag>, gs: List<&2, Finding.Graded>) -> String: 225 Ezjson.print(Proto.publish(cc, uri, ds, gs)) 226 227 # the bolt.bend candidates of a document's path, nearest first: the ones the 228 # CLI reads for a file at that path, run from the same directory 229 def pub.cands(cwd: String, path: String) -> List<&2, String>: 230 Config.chain(Config.home(cwd, Config.dir_of(path))) 231 232 # the linter's findings on the text the editor shows, each rule told the 233 # file's role among the marks, at the levels the nearest bolt.bend read gives 234 # them, less what the text's noqa comments 235 # silence, then the `noqa` rule's, which judges no project rule's code (the 236 # editor runs none); none for a bolt.bend, which is bolt's configuration, 237 # not source 238 def pub.lint( 239 +path: String, 240 +text: String, 241 rs: List<&2, Maybe<&2, String>>, 242 ms: List<&2, Role.Mark> 243 ) -> List<&2, Finding.Graded>: 244 +cfg = Config.nearest(rs) 245 Bool.pick(List<&2, Finding.Graded>, Plan.is_config(path), [], 246 Plan.after([Noqa.file(path, Lex.tokens(text))], False{}, 247 Lint.graded(cfg, Lint.on.role(Role.of(ms, path), Src.of(path, text))), fs => Lint.graded(cfg, fs))) 248 249 # the role marks `bolt ` reads from a World: the imports of every file 250 # of the tree (Plan.graph), each file read for its imports alone 251 def pub.marks(+world: W.World) -> List<&2, Role.Mark>: 252 Plan.marks.named(world) 253 254 # open or save: the checker's errors with the lint of the text, the errors 255 # kept for the changes after 256 def pub.checked( 257 ee: Enc.Enc, 258 +uri: String, 259 +text: String, 260 +ds: List<&2, Rep.Diag>, 261 rs: List<&2, Maybe<&2, String>>, 262 +ms: List<&2, Role.Mark>, 263 +docs: List<&2, Docs.Doc> 264 ) -> Work: 265 noted([pub.note(Enc.cols(ee, text), uri, ds, pub.lint(Proto.path(uri), text, rs, ms))], 266 Docs.set_checked(docs, uri, ds, ms)) 267 268 # open: the document's text kept, then published with the checker's errors 269 def pub.open( 270 ee: Enc.Enc, 271 +uri: String, 272 +text: String, 273 ds: List<&2, Rep.Diag>, 274 rs: List<&2, Maybe<&2, String>>, 275 +ms: List<&2, Role.Mark>, 276 +docs: List<&2, Docs.Doc> 277 ) -> Work: 278 pub.checked(ee, uri, text, ds, rs, ms, Docs.set(docs, uri, text)) 279 280 # save: the text the editor shows published with the checker's new errors 281 def pub.save( 282 ee: Enc.Enc, 283 +uri: String, 284 ds: List<&2, Rep.Diag>, 285 rs: List<&2, Maybe<&2, String>>, 286 +ms: List<&2, Role.Mark>, 287 +docs: List<&2, Docs.Doc> 288 ) -> Work: 289 pub.checked(ee, uri, Maybe.default(&2, String, Docs.get(docs, uri), ""), ds, rs, ms, docs) 290 291 # change: the new text kept, and its lint published with the checker's last 292 # errors and the marks read with them 293 def pub.change( 294 ee: Enc.Enc, 295 +uri: String, 296 +text: String, 297 rs: List<&2, Maybe<&2, String>>, 298 +docs: List<&2, Docs.Doc> 299 ) -> Work: 300 noted([pub.note(Enc.cols(ee, text), uri, Docs.errors(docs, uri), 301 pub.lint(Proto.path(uri), text, rs, Docs.marks(docs, uri)))], Docs.set(docs, uri, text)) 302 303 # close: the document forgotten, and an empty list published 304 def pub.close(ee: Enc.Enc, +uri: String, docs: List<&2, Docs.Doc>) -> Work: 305 noted([pub.note(Enc.cols(ee, ""), uri, [], [])], Docs.del(docs, uri)) 306 307 # the texts of a document's bolt.bend candidates, as the files hold them 308 def pub.configs(~fs: F.Files, +uri: String) -> IO(List<&2, Maybe<&2, String>>): 309 do IO>>: 310 cwd : String <- F.cwd(fs) 311 F.read_all(~fs, pub.cands(cwd, Proto.path(uri))) 312 313 # a file's text as an answer 314 def pub.read.answer(mm: Maybe<&2, String>) -> W.Answer: 315 match mm: 316 case None{}: 317 W.Unread{} 318 case Some{text}: 319 W.Read{text} 320 321 # one question of the planner's, answered from the files: a directory's 322 # entries, or a file's text (a directory reads as no text) 323 def pub.answer(~fs: F.Files, ask: W.Ask) -> IO(W.Answer): 324 match ask: 325 case W.Entries{dir}: 326 do IO: 327 es : List<&2, String> <- F.entries(fs, dir) 328 return W.Listed{es} 329 case W.Contents{path}: 330 do IO: 331 mm : Maybe<&2, String> <- F.read(fs, path) 332 return pub.read.answer(mm) 333 334 # every question answered, in order 335 def pub.answers(~fs: F.Files, asks: List<&2, W.Ask>) -> IO(List<&2, W.Reply>): 336 match asks: 337 case Nil{}: 338 IO.pure(List<&2, W.Reply>, []) 339 case Con{+a, rest}: 340 do IO>: 341 r : W.Answer <- pub.answer(~fs, a) 342 more : List<&2, W.Reply> <- pub.answers(~fs, rest) 343 return W.Reply{a, r} <> more 344 345 # the World with more answers in it 346 def pub.world.more(world: W.World, rs: List<&2, W.Reply>) -> W.World: 347 W.World{paths, cwd, replies} = world 348 W.World{paths, cwd, List.append(&2, W.Reply, replies, rs)} 349 350 # the planner's questions answered and the World gone round again, or the 351 # World as it is once the planner asks none 352 def pub.world.step( 353 ~fs: F.Files, 354 asks: List<&2, W.Ask>, 355 +world: W.World, 356 go: W.World -> IO(W.World) 357 ) -> IO(W.World): 358 match asks: 359 case Nil{}: 360 IO.pure(W.World, world) 361 case Con{a, rest}: 362 do IO: 363 rs : List<&2, W.Reply> <- pub.answers(~fs, a <> rest) 364 go(pub.world.more(world, rs)) 365 366 # the World `bolt ` reads, gathered as src/lint.bend gathers it: the 367 # planner's questions answered until it asks none, under the same fuel 368 def pub.world.go(~fs: F.Files, fuel: Nat, +world: W.World) -> IO(W.World): 369 match fuel: 370 case 0n: 371 IO.pure(W.World, world) 372 case 1n+f: 373 pub.world.step(~fs, Plan.wants(world), world, ww => pub.world.go(~fs, f, ww)) 374 375 # the role marks the tree's imports give a document, as `bolt ` reads 376 # them 377 def pub.seen(~fs: F.Files, +uri: String) -> IO(List<&2, Role.Mark>): 378 do IO>: 379 cwd : String <- F.cwd(fs) 380 ww : W.World <- pub.world.go(~fs, Nat.add(Plan.limit(), 2n), W.World{[Proto.path(uri)], cwd, []}) 381 return pub.marks(ww) 382 383 # open, on the checker's run on the file on disk 384 def pub.opened( 385 ~fs: F.Files, 386 ~ck: C.Checker, 387 ee: Enc.Enc, 388 +uri: String, 389 text: String, 390 docs: List<&2, Docs.Doc> 391 ) -> IO(Work): 392 do IO: 393 aa : C.Answer <- C.check(ck, Proto.path(uri)) 394 rs : List<&2, Maybe<&2, String>> <- pub.configs(~fs, uri) 395 ms : List<&2, Role.Mark> <- pub.seen(~fs, uri) 396 return pub.open(ee, uri, text, C.errors(aa), rs, ms, docs) 397 398 # save, on the checker's run on the file on disk 399 def pub.saved(~fs: F.Files, ~ck: C.Checker, ee: Enc.Enc, +uri: String, docs: List<&2, Docs.Doc>) -> IO(Work): 400 do IO: 401 aa : C.Answer <- C.check(ck, Proto.path(uri)) 402 rs : List<&2, Maybe<&2, String>> <- pub.configs(~fs, uri) 403 ms : List<&2, Role.Mark> <- pub.seen(~fs, uri) 404 return pub.save(ee, uri, C.errors(aa), rs, ms, docs) 405 406 # change: the linter only 407 def pub.changed(~fs: F.Files, ee: Enc.Enc, +uri: String, text: String, docs: List<&2, Docs.Doc>) -> IO(Work): 408 do IO: 409 rs : List<&2, Maybe<&2, String>> <- pub.configs(~fs, uri) 410 return pub.change(ee, uri, text, rs, docs) 411 412 def hover.of(mm: Maybe<&2, Nav.Found>) -> J.Json: 413 match mm: 414 case None{}: 415 J.JNull{} 416 case Some{Nav.Found{uri, item}}: 417 Proto.hover(item) 418 case Some{Nav.Here{uri, b}}: 419 Proto.hover.local(b) 420 421 def definition.of(cc: Enc.Cols, mm: Maybe<&2, Nav.Found>) -> J.Json: 422 match mm: 423 case None{}: 424 J.JNull{} 425 case Some{Nav.Found{uri, item}}: 426 Proto.location(cc, uri, item) 427 case Some{Nav.Here{uri, b}}: 428 Proto.location.local(cc, uri, b) 429 430 # the column a request's position names, read in the negotiated encoding of 431 # the document's text 432 def col_in(cc: Enc.Cols, +params: J.Json) -> U32: 433 Enc.into(cc, line_of(params), col_of(params)) 434 435 # hover at a request's position 436 def hovered( # noqa: L001 LSP IO glue 437 ~fs: F.Files, 438 ee: Enc.Enc, 439 +docs: List<&2, Docs.Doc>, 440 +uri: String, 441 +params: J.Json 442 ) -> IO(Work): 443 do IO: 444 text : String <- Nav.text_of(~fs, docs, uri) 445 f : Maybe<&2, Nav.Found> <- Nav.hover(~fs, docs, uri, line_of(params), col_in(Enc.cols(ee, text), params)) 446 return answered(hover.of(f), docs) 447 448 # definition at a request's position 449 def defined( # noqa: L001 LSP IO glue 450 ~fs: F.Files, 451 ee: Enc.Enc, 452 +docs: List<&2, Docs.Doc>, 453 +uri: String, 454 +params: J.Json 455 ) -> IO(Work): 456 do IO: 457 +text : String <- Nav.text_of(~fs, docs, uri) 458 +cc : Enc.Cols = Enc.cols(ee, text) 459 f : Maybe<&2, Nav.Found> <- Nav.at(~fs, docs, uri, line_of(params), col_in(cc, params)) 460 return answered(definition.of(cc, f), docs) 461 462 def complete.with( 463 ~fs: F.Files, 464 cc: Enc.Cols, 465 +prefix: String, 466 +docs: List<&2, Docs.Doc>, 467 uri: String, 468 +line: U32, 469 +col: U32 470 ) -> IO(Work): 471 do IO: 472 items : List<&2, Outline.Item> <- Nav.candidates(~fs, docs, uri, prefix, line) 473 return answered(Proto.completions(cc, items, line, (col - U32.from_nat(String.length(prefix)) : U32), col), docs) 474 475 # the candidates for the name being typed at a position 476 def complete( # noqa: L001 LSP IO glue 477 ~fs: F.Files, 478 ee: Enc.Enc, 479 +docs: List<&2, Docs.Doc>, 480 +uri: String, 481 +params: J.Json 482 ) -> IO(Work): 483 do IO: 484 +text : String <- Nav.text_of(~fs, docs, uri) 485 +cc : Enc.Cols = Enc.cols(ee, text) 486 +line : U32 = line_of(params) 487 +col : U32 = col_in(cc, params) 488 complete.with(~fs, cc, Word.before(text, line, col), docs, uri, line, col) 489 490 def references.of(cc: Enc.Cols, ss: Nav.Sites, uri: String, name: String) -> J.Json: 491 Nav.Sites{mine, at} = ss 492 Proto.locations(cc, uri, name, at) 493 494 # only a name bound in the document renames; the editor shows the refusal 495 def rename.of(cc: Enc.Cols, ss: Nav.Sites, uri: String, name: String, new: String) -> Answer: 496 Nav.Sites{mine, at} = ss 497 match mine: 498 case True{}: 499 Answer{Proto.rename(cc, uri, name, new, at)} 500 case False{}: 501 Refusal{"-32803", "only a name bound in this file can be renamed"} 502 503 # every position of the name at a position 504 def references( # noqa: L001 LSP IO glue 505 ~fs: F.Files, 506 ee: Enc.Enc, 507 +docs: List<&2, Docs.Doc>, 508 +uri: String, 509 +params: J.Json 510 ) -> IO(Work): 511 do IO: 512 +text : String <- Nav.text_of(~fs, docs, uri) 513 +cc : Enc.Cols = Enc.cols(ee, text) 514 +line : U32 = line_of(params) 515 +col : U32 = col_in(cc, params) 516 s : Nav.Sites <- Nav.sites(~fs, docs, uri, line, col) 517 return answered(references.of(cc, s, uri, Word.at(text, line, col)), docs) 518 519 # the edit renaming the name at a position, or a refusal 520 def renamed( # noqa: L001 LSP IO glue 521 ~fs: F.Files, 522 ee: Enc.Enc, 523 +docs: List<&2, Docs.Doc>, 524 +uri: String, 525 +params: J.Json 526 ) -> IO(Work): 527 do IO: 528 +text : String <- Nav.text_of(~fs, docs, uri) 529 +cc : Enc.Cols = Enc.cols(ee, text) 530 +line : U32 = line_of(params) 531 +col : U32 = col_in(cc, params) 532 s : Nav.Sites <- Nav.sites(~fs, docs, uri, line, col) 533 return Work{[], 534 rename.of(cc, s, uri, Word.at(text, line, col), 535 Maybe.default(&2, String, Ezjson.as_str(Ezjson.get(params, "newName")), "")), 536 docs} 537 538 # the document's semantic tokens 539 def semantic(~fs: F.Files, ee: Enc.Enc, +docs: List<&2, Docs.Doc>, uri: String) -> IO(Work): # noqa: L001 LSP IO glue 540 do IO: 541 text : String <- Nav.text_of(~fs, docs, uri) 542 return answered(Proto.semantic(Semantic.data(ee, text)), docs) 543 544 # the document's symbols 545 def symbols(~fs: F.Files, ee: Enc.Enc, +docs: List<&2, Docs.Doc>, +uri: String) -> IO(Work): # noqa: L001 LSP IO glue 546 do IO: 547 text : String <- Nav.text_of(~fs, docs, uri) 548 items : List<&2, Outline.Item> <- Nav.outline(~fs, docs, uri) 549 return answered(Proto.symbols(Enc.cols(ee, text), items), docs) 550 551 # the work behind a method: what the checker, the files and the documents 552 # give it. It does not see the message's id: step alone puts that on a reply 553 def work( # noqa: L001 LSP IO glue 554 ~fs: F.Files, 555 ~ck: C.Checker, 556 ee: Enc.Enc, 557 mm: Method, 558 +params: J.Json, 559 +docs: List<&2, Docs.Doc> 560 ) -> IO(Work): 561 match mm: 562 case MInitialize{}: 563 IO.pure(Work, answered(Proto.capabilities(ee), docs)) 564 case MShutdown{}: 565 IO.pure(Work, answered(J.JNull{}, docs)) 566 case MExit{}: 567 IO.pure(Work, noted([], docs)) 568 case MOpen{}: 569 pub.opened(~fs, ~ck, ee, uri_of(params), 570 Maybe.default(&2, String, Ezjson.as_str(Ezjson.get(Ezjson.get(params, "textDocument"), "text")), ""), docs) 571 case MChange{}: 572 pub.changed(~fs, ee, uri_of(params), 573 Maybe.default(&2, String, Ezjson.as_str(Ezjson.get(Ezjson.at(Ezjson.get(params, "contentChanges"), 0), "text")), ""), docs) 574 case MSave{}: 575 pub.saved(~fs, ~ck, ee, uri_of(params), docs) 576 case MClose{}: 577 IO.pure(Work, pub.close(ee, uri_of(params), docs)) 578 case MHover{}: 579 hovered(~fs, ee, docs, uri_of(params), params) 580 case MDefinition{}: 581 defined(~fs, ee, docs, uri_of(params), params) 582 case MSymbols{}: 583 symbols(~fs, ee, docs, uri_of(params)) 584 case MCompletion{}: 585 complete(~fs, ee, docs, uri_of(params), params) 586 case MReferences{}: 587 references(~fs, ee, docs, uri_of(params), params) 588 case MRename{}: 589 renamed(~fs, ee, docs, uri_of(params), params) 590 case MSemantic{}: 591 semantic(~fs, ee, docs, uri_of(params)) 592 case MOther{}: 593 IO.pure(Work, noted([], docs)) 594 595 # the encoding after a message: initialize settles it (enc.bend's 596 # negotiate); every other message keeps it 597 def encoding(mm: Method, params: J.Json, ee: Enc.Enc) -> Enc.Enc: 598 match mm: 599 case MInitialize{}: 600 Enc.negotiate(params) 601 case _other: 602 ee 603 604 # one message: its method's work, then step's decision on it 605 def handle( # noqa: L001 LSP IO glue 606 ~fs: F.Files, 607 ~ck: C.Checker, 608 ee: Enc.Enc, 609 +msg: J.Json, 610 docs: List<&2, Docs.Doc> 611 ) -> IO(Out): 612 +mm = method(name_of(msg)) 613 do IO: 614 w : Work <- work(~fs, ~ck, ee, mm, Ezjson.get(msg, "params"), docs) 615 return step(mm, id_of(msg), w) 616 617 # a parsed message, or null when it did not parse 618 def decoded(mm: Maybe<&2, J.Json>) -> J.Json: 619 Maybe.default(&2, J.Json, mm, J.JNull{}) 620 621 # every reply sent, in order 622 def send_all(~H: Type, ~tr: T.Transport, replies: List<&2, Reply>, h3: H) -> IO(H): # noqa: L001 serve loop IO 623 match replies: 624 case Nil{}: 625 IO.pure(H, h3) 626 case Con{body, t}: 627 do IO: 628 h2 : H <- T.send(H, tr, h3, text(body)) 629 send_all(~H, ~tr, t, h2) 630 631 # the loop goes on with the documents, unless the message said to quit 632 def next( # noqa: L001 serve loop IO 633 -H: Type, 634 quit: Bool, 635 h3: H, 636 ee: Enc.Enc, 637 docs: List<&2, Docs.Doc>, 638 rest: H -> Enc.Enc -> List<&2, Docs.Doc> -> IO(Unit) 639 ) -> IO(Unit): 640 match quit: 641 case True{}: 642 IO.pure(Unit, Unit{}) 643 case False{}: 644 rest(h3, ee, docs) 645 646 # the replies sent, then the loop goes on 647 def answer( # noqa: L001 serve loop IO 648 ~H: Type, 649 ~tr: T.Transport, 650 out: Out, 651 h3: H, 652 ee: Enc.Enc, 653 rest: H -> Enc.Enc -> List<&2, Docs.Doc> -> IO(Unit) 654 ) -> IO(Unit): 655 Out{replies, quit, docs} = out 656 do IO: 657 h2 : H <- send_all(~H, ~tr, replies, h3) 658 next(H, quit, h2, ee, docs, rest) 659 660 # a message handled under the encoding it leaves, and answered 661 def turn.msg( 662 ~H: Type, 663 ~tr: T.Transport, 664 ~fs: F.Files, 665 ~ck: C.Checker, 666 +msg: J.Json, 667 h3: H, 668 ee: Enc.Enc, 669 docs: List<&2, Docs.Doc>, 670 rest: H -> Enc.Enc -> List<&2, Docs.Doc> -> IO(Unit) 671 ) -> IO(Unit): 672 +e2 = encoding(method(name_of(msg)), Ezjson.get(msg, "params"), ee) 673 do IO: 674 out : Out <- handle(~fs, ~ck, e2, msg, docs) 675 answer(~H, ~tr, out, h3, e2, rest) 676 677 # one message body handled and answered; none means the peer is gone 678 def turn( # noqa: L001 serve loop IO 679 ~H: Type, 680 ~tr: T.Transport, 681 ~fs: F.Files, 682 ~ck: C.Checker, 683 mm: Maybe<&2, String>, 684 h3: H, 685 ee: Enc.Enc, 686 docs: List<&2, Docs.Doc>, 687 rest: H -> Enc.Enc -> List<&2, Docs.Doc> -> IO(Unit) 688 ) -> IO(Unit): 689 match mm: 690 case None{}: 691 IO.pure(Unit, Unit{}) 692 case Some{body}: 693 turn.msg(~H, ~tr, ~fs, ~ck, decoded(Ezjson.parse(body)), h3, ee, docs, rest) 694 695 # what recv gave: the handle and maybe a body 696 def got( # noqa: L001 serve loop IO 697 ~H: Type, 698 ~tr: T.Transport, 699 ~fs: F.Files, 700 ~ck: C.Checker, 701 rr: H & Maybe<&2, String>, 702 ee: Enc.Enc, 703 docs: List<&2, Docs.Doc>, 704 rest: H -> Enc.Enc -> List<&2, Docs.Doc> -> IO(Unit) 705 ) -> IO(Unit): 706 (h, m) = rr 707 turn(~H, ~tr, ~fs, ~ck, m, h, ee, docs, rest) 708 709 def serve.go( 710 ~H: Type, 711 ~tr: T.Transport, 712 ~fs: F.Files, 713 ~ck: C.Checker, 714 fuel: Nat, 715 h3: H, 716 ee: Enc.Enc, 717 docs: List<&2, Docs.Doc> 718 ) -> IO(Unit): 719 match fuel: 720 case 0n: 721 IO.pure(Unit, Unit{}) 722 case 1n+f: 723 do IO: 724 r : H & Maybe<&2, String> <- T.recv(H, tr, h3) 725 got(~H, ~tr, ~fs, ~ck, r, ee, docs, hh => e2 => dd => serve.go(~H, ~tr, ~fs, ~ck, f, hh, e2, dd)) 726 727 # serves until exit, the peer's end, or the fuel's (a message each), in 728 # UTF-16 until initialize negotiates 729 def serve( # noqa: L001 serve loop IO 730 ~H: Type, 731 ~tr: T.Transport, 732 ~fs: F.Files, 733 ~ck: C.Checker, 734 fuel: Nat, 735 h3: H, 736 docs: List<&2, Docs.Doc> 737 ) -> IO(Unit): 738 serve.go(~H, ~tr, ~fs, ~ck, fuel, h3, Enc.Utf16{}, docs)