src/lsp/server.bend

bolt@1.27.0.0 · 738 lines · 66 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,
7# each file told the role the imports of the tree give it, read as `bolt
8# <path>` reads them (src/lint/plan.bend's graph) on open and save and kept
9# for the changes between.
10import Base
11import emerging-ezjson@1.2.0.0/main.bend as Ezjson
12import emerging-ezjson@1.2.0.0/src/value.bend as J
13import bolt@1.27.0.0/src/lsp/proto.bend as Proto
14import bolt@1.27.0.0/src/lsp/docs.bend as Docs
15import bolt@1.27.0.0/src/lsp/nav.bend as Nav
16import bolt@1.27.0.0/src/lsp/semantic.bend as Semantic
17import bolt@1.27.0.0/src/rules.bend as Lint
18import bolt@1.27.0.0/src/finding.bend as Finding
19import bolt@1.27.0.0/src/config.bend as Config
20import bolt@1.27.0.0/src/lint/plan.bend as Plan
21import bolt@1.27.0.0/src/lint/world.bend as W
22import bolt@1.27.0.0/src/role.bend as Role
23import bolt@1.27.0.0/src/src.bend as Src
24import bolt@1.27.0.0/src/noqa.bend as Noqa
25import bolt@1.27.0.0/src/lsp/transport/service.bend as T
26import bolt@1.27.0.0/src/lsp/checker/answer.bend as C
27import bolt@1.27.0.0/src/lsp/files/service.bend as F
28import bolt@1.27.0.0/src/lsp/report.bend as Rep
29import bolt@1.27.0.0/src/lsp/enc.bend as Enc
30import bolt@1.27.0.0/src/syntax/outline.bend as Outline
31import bolt@1.27.0.0/src/syntax/lex.bend as Lex
32import bolt@1.27.0.0/src/syntax/word.bend as Word
33
34# the methods the server answers; MOther is anything else
35type 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
54type 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
60type 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
66type 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
70type 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
75type Role is Data:
76  RRequest{}
77  RNotice{}
78  RExit{}
79  RUnknown{}
80
81# a method by its name
82def 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
100def 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
134def 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
138def id_of(msg: J.Json) -> J.Json:
139  Ezjson.get(msg, "id")
140
141# the document a request is about
142def 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
146def 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
150def 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
154def 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
164def 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
172def 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
180def 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
188def 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
192def noted(notes: List<&2, String>, docs: List<&2, Docs.Doc>) -> Work:
193  Work{notes, Answer{J.JNull{}}, docs}
194
195def 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
213def 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
224def 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
229def 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
238def 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 <path>` reads from a World: the imports of every file
250# of the tree (Plan.graph), each file read for its imports alone
251def 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
256def 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
269def 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
281def 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
293def 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
304def 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
308def pub.configs(~fs: F.Files, +uri: String) -> IO(List<&2, Maybe<&2, String>>):
309  do IO<List<&2, Maybe<&2, String>>>:
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
314def 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)
323def pub.answer(~fs: F.Files, ask: W.Ask) -> IO(W.Answer):
324  match ask:
325    case W.Entries{dir}:
326      do IO<W.Answer>:
327        es : List<&2, String> <- F.entries(fs, dir)
328        return W.Listed{es}
329    case W.Contents{path}:
330      do IO<W.Answer>:
331        mm : Maybe<&2, String> <- F.read(fs, path)
332        return pub.read.answer(mm)
333
334# every question answered, in order
335def 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<List<&2, W.Reply>>:
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
346def 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
352def 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<W.World>:
363        rs : List<&2, W.Reply> <- pub.answers(~fs, a <> rest)
364        go(pub.world.more(world, rs))
365
366# the World `bolt <path>` reads, gathered as src/lint.bend gathers it: the
367# planner's questions answered until it asks none, under the same fuel
368def 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 <path>` reads
376# them
377def pub.seen(~fs: F.Files, +uri: String) -> IO(List<&2, Role.Mark>):
378  do IO<List<&2, Role.Mark>>:
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
384def 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<Work>:
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
399def pub.saved(~fs: F.Files, ~ck: C.Checker, ee: Enc.Enc, +uri: String, docs: List<&2, Docs.Doc>) -> IO(Work):
400  do IO<Work>:
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
407def pub.changed(~fs: F.Files, ee: Enc.Enc, +uri: String, text: String, docs: List<&2, Docs.Doc>) -> IO(Work):
408  do IO<Work>:
409    rs : List<&2, Maybe<&2, String>> <- pub.configs(~fs, uri)
410    return pub.change(ee, uri, text, rs, docs)
411
412def 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
421def 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
432def 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
436def 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<Work>:
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
449def 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<Work>:
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
462def 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<Work>:
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
476def 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<Work>:
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
490def 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
495def 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
504def 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<Work>:
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
520def 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<Work>:
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
539def semantic(~fs: F.Files, ee: Enc.Enc, +docs: List<&2, Docs.Doc>, uri: String) -> IO(Work):  # noqa: L001 LSP IO glue
540  do IO<Work>:
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
545def symbols(~fs: F.Files, ee: Enc.Enc, +docs: List<&2, Docs.Doc>, +uri: String) -> IO(Work):  # noqa: L001 LSP IO glue
546  do IO<Work>:
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
553def 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
597def 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
605def 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<Out>:
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
618def decoded(mm: Maybe<&2, J.Json>) -> J.Json:
619  Maybe.default(&2, J.Json, mm, J.JNull{})
620
621# every reply sent, in order
622def send_all(~H: Type, ~tr: T.Transport<H>, 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<H>:
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
632def 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
647def answer(  # noqa: L001 serve loop IO
648  ~H: Type,
649  ~tr: T.Transport<H>,
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<Unit>:
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
661def turn.msg(
662  ~H: Type,
663  ~tr: T.Transport<H>,
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<Unit>:
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
678def turn(  # noqa: L001 serve loop IO
679  ~H: Type,
680  ~tr: T.Transport<H>,
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
696def got(  # noqa: L001 serve loop IO
697  ~H: Type,
698  ~tr: T.Transport<H>,
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
709def serve.go(
710  ~H: Type,
711  ~tr: T.Transport<H>,
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<Unit>:
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
729def serve(  # noqa: L001 serve loop IO
730  ~H: Type,
731  ~tr: T.Transport<H>,
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)