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