src/lsp/nav.bend
bolt@1.22.0.0 · 453 lines · 0 definitions
1# lsp/nav: from a name to the item it names. A use is answered as the binder
2# resolved it: a binder of the document, an item of the document (before any
3# alias), `rest` in the file behind the import `Alias` of `Alias.rest`, or
4# Base. A word the binder has nothing for (a half-typed name) is looked up
5# behind its alias, else in the document, else in Base. Open documents are
6# read from the store (what the editor shows), everything else through the
7# files service.
8import Base
9import bolt@1.22.0.0/src/syntax/outline.bend as Outline
10import bolt@1.22.0.0/src/syntax/word.bend as Word
11import bolt@1.22.0.0/src/syntax/bind.bend as Bind
12import bolt@1.22.0.0/src/lsp/files/service.bend as F
13import bolt@1.22.0.0/src/lsp/docs.bend as Docs
14import bolt@1.22.0.0/src/lsp/path.bend as Path
15import bolt@1.22.0.0/src/lsp/proto.bend as Proto
16
17# what a name names: an item and the document it lives in, or a binder of
18# the document itself (a parameter, a local, a field, ..)
19type Found is Data:
20 Found{uri: String, item: Outline.Item}
21 Here{uri: String, bind: Bind.Bind}
22
23# an item, when there is one, in a document
24def found(mm: Maybe<&2, Outline.Item>, uri: String) -> Maybe<&2, Found>:
25 match mm:
26 case None{}:
27 None{}
28 case Some{item}:
29 Some{Found{uri, item}}
30
31def load.miss(~fs: F.Files, open: Maybe<&2, String>, path: String) -> IO(String):
32 match open:
33 case Some{text}:
34 IO.pure(String, text)
35 case None{}:
36 do IO<String>:
37 r : Maybe<&2, String> <- F.read(fs, path)
38 return Maybe.default(&2, String, r, "")
39
40# a document's text: the open one, or the file's; "" when there is neither
41def load(~fs: F.Files, docs: List<&2, Docs.Doc>, uri: String, path: String) -> IO(String):
42 load.miss(~fs, Docs.get(docs, uri), path)
43
44# the item of a name in a file
45def in_file(~fs: F.Files, docs: List<&2, Docs.Doc>, +path: String, name: String) -> IO(Maybe<&2, Found>):
46 do IO<Maybe<&2, Found>>:
47 text : String <- load(~fs, docs, "file://" ++ path, path)
48 return found(Outline.find(Outline.items(text), name), "file://" ++ path)
49
50# the item of a name in Base
51def in_base(~fs: F.Files, docs: List<&2, Docs.Doc>, name: String) -> IO(Maybe<&2, Found>):
52 do IO<Maybe<&2, Found>>:
53 path : String <- F.base_path(fs)
54 in_file(~fs, docs, path, name)
55
56# an item of the document, else of Base
57def own(
58 ~fs: F.Files,
59 mm: Maybe<&2, Outline.Item>,
60 docs: List<&2, Docs.Doc>,
61 uri: String,
62 name: String
63) -> IO(Maybe<&2, Found>):
64 match mm:
65 case Some{item}:
66 IO.pure(Maybe<&2, Found>, Some{Found{uri, item}})
67 case None{}:
68 in_base(~fs, docs, name)
69
70# through an import alias when the name has one, else the document's own
71def via(
72 ~fs: F.Files,
73 imp: Maybe<&2, String>,
74 docs: List<&2, Docs.Doc>,
75 +uri: String,
76 +name: String,
77 items: List<&2, Outline.Item>
78) -> IO(Maybe<&2, Found>):
79 match imp:
80 case Some{rel}:
81 in_file(~fs, docs, Path.join(Path.dir(Proto.path(uri)), rel), Word.rest(name))
82 case None{}:
83 own(~fs, Outline.find(items, name), docs, uri, name)
84
85# a name against a document's items
86def among(
87 ~fs: F.Files,
88 +items: List<&2, Outline.Item>,
89 docs: List<&2, Docs.Doc>,
90 uri: String,
91 +name: String
92) -> IO(Maybe<&2, Found>):
93 via(~fs, Outline.import_path(items, Word.head(name)), docs, uri, name, items)
94
95# the item a name in a document names
96def resolve(~fs: F.Files, +docs: List<&2, Docs.Doc>, +uri: String, name: String) -> IO(Maybe<&2, Found>):
97 do IO<Maybe<&2, Found>>:
98 text : String <- load(~fs, docs, uri, Proto.path(uri))
99 among(~fs, Outline.items(text), docs, uri, name)
100
101# a binder, when there is one, in a document
102def here(mm: Maybe<&2, Bind.Bind>, uri: String) -> Maybe<&2, Found>:
103 match mm:
104 case None{}:
105 None{}
106 case Some{b}:
107 Some{Here{uri, b}}
108
109# a name behind an import alias: `rest` in the file the import names,
110# relative to the document; the document's own item when the outline has no
111# import of that alias
112def at.qual(
113 ~fs: F.Files,
114 imp: Maybe<&2, String>,
115 rest: String,
116 items: List<&2, Outline.Item>,
117 docs: List<&2, Docs.Doc>,
118 +uri: String,
119 +name: String
120) -> IO(Maybe<&2, Found>):
121 match imp:
122 case Some{rel}:
123 in_file(~fs, docs, Path.join(Path.dir(Proto.path(uri)), rel), rest)
124 case None{}:
125 own(~fs, Outline.find(items, name), docs, uri, name)
126
127# a use, by what the binder resolved it to: a binder of the document is that
128# binder; an item of the document is that item, before any alias; a name
129# behind an alias is looked up in the file the import names; a free name is
130# the document's item of that name, else Base's
131def at.target(
132 ~fs: F.Files,
133 tg: Bind.Target,
134 +name: String,
135 binds: List<&2, Bind.Bind>,
136 +items: List<&2, Outline.Item>,
137 docs: List<&2, Docs.Doc>,
138 +uri: String
139) -> IO(Maybe<&2, Found>):
140 match tg:
141 case Bind.TLocal{tl, tc}:
142 IO.pure(Maybe<&2, Found>, here(Bind.binder(binds, tl, tc), uri))
143 case Bind.TItem{+item}:
144 own(~fs, Outline.find(items, item), docs, uri, item)
145 case Bind.TQual{alias, rest}:
146 at.qual(~fs, Outline.import_path(items, alias), rest, items, docs, uri, name)
147 case Bind.TFree{}:
148 own(~fs, Outline.find(items, name), docs, uri, name)
149
150# a binder of the document is answered from the document, and a use as the
151# binder resolved it; nothing at all falls back to the word under the cursor
152# (a half-typed name)
153def at.site(
154 ~fs: F.Files,
155 site: Maybe<&2, Bind.Site>,
156 binds: List<&2, Bind.Bind>,
157 items: List<&2, Outline.Item>,
158 docs: List<&2, Docs.Doc>,
159 +uri: String,
160 name: String
161) -> IO(Maybe<&2, Found>):
162 match site:
163 case Some{Bind.SBind{b}}:
164 IO.pure(Maybe<&2, Found>, Some{Here{uri, b}})
165 case Some{Bind.SUse{Bind.Use{n, _l, _c, tg}}}:
166 at.target(~fs, tg, n, binds, items, docs, uri)
167 case None{}:
168 resolve(~fs, docs, uri, name)
169
170def at.bound(
171 ~fs: F.Files,
172 bb: Bind.Bound,
173 items: List<&2, Outline.Item>,
174 docs: List<&2, Docs.Doc>,
175 uri: String,
176 name: String,
177 +line: U32,
178 +col: U32
179) -> IO(Maybe<&2, Found>):
180 Bind.Bound{+binds, uses, scopes} = bb
181 at.site(~fs, Bind.at(Bind.Bound{binds, uses, scopes}, line, col), binds, items, docs, uri, name)
182
183def at.in(
184 ~fs: F.Files,
185 +text: String,
186 docs: List<&2, Docs.Doc>,
187 uri: String,
188 +line: U32,
189 +col: U32
190) -> IO(Maybe<&2, Found>):
191 at.bound(~fs, Bind.bound(text), Outline.items(text), docs, uri, Word.at(text, line, col), line, col)
192
193# what the name under a position of a document names
194def at(~fs: F.Files, +docs: List<&2, Docs.Doc>, +uri: String, line: U32, col: U32) -> IO(Maybe<&2, Found>):
195 do IO<Maybe<&2, Found>>:
196 text : String <- load(~fs, docs, uri, Proto.path(uri))
197 at.in(~fs, text, docs, uri, line, col)
198
199# the binder, when the outline has nothing for that name
200def hover.binder(mm: Maybe<&2, Found>, uri: String, bb: Bind.Bind) -> Maybe<&2, Found>:
201 match mm:
202 case Some{f}:
203 Some{f}
204 case None{}:
205 Some{Here{uri, bb}}
206
207# an outline item when the name resolved, else one in this document, else the
208# binder. A comment above a law stays on that law: a file header separated by
209# an import or a blank line is not part of it
210def hover.pick(mm: Maybe<&2, Found>, local: Maybe<&2, Found>, uri: String, bb: Bind.Bind) -> Maybe<&2, Found>:
211 match mm:
212 case Some{f}:
213 Some{f}
214 case None{}:
215 hover.binder(local, uri, bb)
216
217# the outline item a top-level name hovers as. `Alias.law` is the law in the
218# file behind Alias (the comment lives there); a bare name is this document's
219def hover.item(
220 ~fs: F.Files,
221 +name: String,
222 items: List<&2, Outline.Item>,
223 docs: List<&2, Docs.Doc>,
224 +uri: String,
225 bb: Bind.Bind
226) -> IO(Maybe<&2, Found>):
227 do IO<Maybe<&2, Found>>:
228 m : Maybe<&2, Found> <- resolve(~fs, docs, uri, name)
229 return hover.pick(m, found(Outline.find(items, name), uri), uri, bb)
230
231# a def, law or type is its outline item; any other binder is itself
232def hover.kind(
233 ~fs: F.Files,
234 kind: Bind.BindKind,
235 name: String,
236 items: List<&2, Outline.Item>,
237 docs: List<&2, Docs.Doc>,
238 uri: String,
239 bb: Bind.Bind
240) -> IO(Maybe<&2, Found>):
241 match kind:
242 case Bind.KItem{}:
243 hover.item(~fs, name, items, docs, uri, bb)
244 case other:
245 IO.pure(Maybe<&2, Found>, Some{Here{uri, bb}})
246
247# the binder under the cursor, read for hover
248def hover.bind(
249 ~fs: F.Files,
250 bb: Bind.Bind,
251 items: List<&2, Outline.Item>,
252 docs: List<&2, Docs.Doc>,
253 uri: String
254) -> IO(Maybe<&2, Found>):
255 Bind.Bind{+name, line, col, +kind, note} = bb
256 hover.kind(~fs, kind, name, items, docs, uri, Bind.Bind{name, line, col, kind, note})
257
258# a binder of an item is the outline item (signature, then the comment above
259# it). A use is answered the same way `at` answers it, so a call keeps the
260# callee's doc
261def hover.site(
262 ~fs: F.Files,
263 site: Maybe<&2, Bind.Site>,
264 items: List<&2, Outline.Item>,
265 +binds: List<&2, Bind.Bind>,
266 docs: List<&2, Docs.Doc>,
267 uri: String,
268 +name: String
269) -> IO(Maybe<&2, Found>):
270 match site:
271 case Some{Bind.SBind{b}}:
272 hover.bind(~fs, b, items, docs, uri)
273 case other:
274 at.site(~fs, site, binds, items, docs, uri, name)
275
276# hover at a binding site of a parsed document
277def hover.bound(
278 ~fs: F.Files,
279 bb: Bind.Bound,
280 items: List<&2, Outline.Item>,
281 docs: List<&2, Docs.Doc>,
282 uri: String,
283 name: String,
284 +line: U32,
285 +col: U32
286) -> IO(Maybe<&2, Found>):
287 Bind.Bound{+binds, uses, scopes} = bb
288 hover.site(~fs, Bind.at(Bind.Bound{binds, uses, scopes}, line, col), items, binds, docs, uri, name)
289
290# hover at a position of a document's text
291def hover.in(
292 ~fs: F.Files,
293 +text: String,
294 docs: List<&2, Docs.Doc>,
295 uri: String,
296 +line: U32,
297 +col: U32
298) -> IO(Maybe<&2, Found>):
299 hover.bound(~fs, Bind.bound(text), Outline.items(text), docs, uri, Word.at(text, line, col), line, col)
300
301# what a hover at a position should show. Definition stays on `at`: an item's
302# own name is still that binder
303def hover(~fs: F.Files, +docs: List<&2, Docs.Doc>, +uri: String, line: U32, col: U32) -> IO(Maybe<&2, Found>):
304 do IO<Maybe<&2, Found>>:
305 text : String <- load(~fs, docs, uri, Proto.path(uri))
306 hover.in(~fs, text, docs, uri, line, col)
307
308# references and rename
309# ---------------------
310
311# the positions a name is at in its document, and whether the name is the
312# document's own (a binder, or an item of it) -- only those can be renamed
313type Sites is Data:
314 Sites{mine: Bool, at: List<&2, Bind.Pos>}
315
316def sites.of(site: Maybe<&2, Bind.Site>, +bb: Bind.Bound) -> Sites:
317 match site:
318 case None{}:
319 Sites{False{}, []}
320 case Some{Bind.SBind{Bind.Bind{name, l, c, Bind.KItem{}, note}}}:
321 Sites{True{}, Bind.item_sites(bb, name)}
322 case Some{Bind.SBind{Bind.Bind{name, l, c, Bind.KCtor{}, note}}}:
323 Sites{True{}, Bind.item_sites(bb, name)}
324 case Some{Bind.SBind{Bind.Bind{name, l, c, k, note}}}:
325 Sites{True{}, Bind.sites(bb, l, c)}
326 case Some{Bind.SUse{Bind.Use{n, l, c, Bind.TLocal{tl, tc}}}}:
327 Sites{True{}, Bind.sites(bb, tl, tc)}
328 case Some{Bind.SUse{Bind.Use{n, l, c, Bind.TItem{name}}}}:
329 Sites{True{}, Bind.item_sites(bb, name)}
330 case Some{Bind.SUse{Bind.Use{n, l, c, other}}}:
331 Sites{False{}, Bind.named_sites(bb, n)}
332
333def sites.in(+text: String, +line: U32, +col: U32) -> Sites:
334 +b = Bind.bound(text)
335 sites.of(Bind.at(b, line, col), b)
336
337# the positions of the name at a position of a document
338def sites(~fs: F.Files, docs: List<&2, Docs.Doc>, +uri: String, line: U32, col: U32) -> IO(Sites):
339 do IO<Sites>:
340 text : String <- load(~fs, docs, uri, Proto.path(uri))
341 return sites.in(text, line, col)
342
343# the items of a document
344def outline( # noqa: L001 IO: reads files
345 ~fs: F.Files,
346 docs: List<&2, Docs.Doc>,
347 +uri: String
348) -> IO(List<&2, Outline.Item>):
349 do IO<List<&2, Outline.Item>>:
350 text : String <- load(~fs, docs, uri, Proto.path(uri))
351 return Outline.items(text)
352
353# completion
354# ----------
355
356# the items of a file that start with a prefix, renamed for insertion
357def starting_in(
358 ~fs: F.Files,
359 docs: List<&2, Docs.Doc>,
360 +path: String,
361 prefix: String,
362 qual: String
363) -> IO(List<&2, Outline.Item>):
364 do IO<List<&2, Outline.Item>>:
365 text : String <- load(~fs, docs, "file://" ++ path, path)
366 return Outline.starting(Outline.items(text), prefix, qual)
367
368# Base is big: it is offered once something is typed
369def from_base(~fs: F.Files, typed: Bool, docs: List<&2, Docs.Doc>, prefix: String) -> IO(List<&2, Outline.Item>):
370 match typed:
371 case False{}:
372 IO.pure(List<&2, Outline.Item>, [])
373 case True{}:
374 do IO<List<&2, Outline.Item>>:
375 path : String <- F.base_path(fs)
376 starting_in(~fs, docs, path, prefix, "")
377
378# a binder as a candidate: its note stands for a signature
379def as_items(ls: List<&2, Bind.Bind>) -> List<&2, Outline.Item>:
380 match ls:
381 case Nil{}:
382 Nil{}
383 case Con{Bind.Bind{name, line, col, kind, note}, t}:
384 Outline.Item{Outline.ILocal{}, name, line, note, Proto.kind_name(kind), ""} <> as_items(t)
385
386# candidates for a plain prefix: visible names, the document's items, its
387# aliases, then Base
388def unqualified(
389 ~fs: F.Files,
390 +items: List<&2, Outline.Item>,
391 docs: List<&2, Docs.Doc>,
392 +prefix: String,
393 locals: List<&2, Outline.Item>
394) -> IO(List<&2, Outline.Item>):
395 do IO<List<&2, Outline.Item>>:
396 base : List<&2, Outline.Item> <- from_base(~fs, Bool.not(String.is_empty(prefix)), docs, prefix)
397 return List.append(&2, Outline.Item, Outline.starting(locals, prefix, ""),
398 List.append(&2, Outline.Item, Outline.starting(items, prefix, ""),
399 List.append(&2, Outline.Item, Outline.aliases(items, prefix), base)))
400
401# `Alias.re` completes from the file behind the alias; anything else from the
402# document, its aliases and Base
403def candidates.via(
404 ~fs: F.Files,
405 imp: Maybe<&2, String>,
406 items: List<&2, Outline.Item>,
407 docs: List<&2, Docs.Doc>,
408 uri: String,
409 +prefix: String,
410 locals: List<&2, Outline.Item>
411) -> IO(List<&2, Outline.Item>):
412 match imp:
413 case Some{rel}:
414 starting_in(~fs, docs, Path.join(Path.dir(Proto.path(uri)), rel), Word.rest(prefix), Word.head(prefix) ++ ".")
415 case None{}:
416 unqualified(~fs, items, docs, prefix, locals)
417
418def candidates.among(
419 ~fs: F.Files,
420 +items: List<&2, Outline.Item>,
421 docs: List<&2, Docs.Doc>,
422 uri: String,
423 +prefix: String,
424 locals: List<&2, Outline.Item>
425) -> IO(List<&2, Outline.Item>):
426 candidates.via(~fs, Outline.import_path(items, Word.head(prefix)), items, docs, uri, prefix, locals)
427
428def candidates.in(
429 ~fs: F.Files,
430 +text: String,
431 docs: List<&2, Docs.Doc>,
432 uri: String,
433 prefix: String,
434 line: U32
435) -> IO(List<&2, Outline.Item>):
436 candidates.among(~fs, Outline.items(text), docs, uri, prefix, as_items(Bind.visible(Bind.bound(text), line)))
437
438# what could complete the prefix typed at a line of a document: the locals
439# visible there, then items, each named by the text to insert
440def candidates(
441 ~fs: F.Files,
442 +docs: List<&2, Docs.Doc>,
443 +uri: String,
444 prefix: String,
445 line: U32
446) -> IO(List<&2, Outline.Item>):
447 do IO<List<&2, Outline.Item>>:
448 text : String <- load(~fs, docs, uri, Proto.path(uri))
449 candidates.in(~fs, text, docs, uri, prefix, line)
450
451# a document's text
452def text_of(~fs: F.Files, docs: List<&2, Docs.Doc>, +uri: String) -> IO(String):
453 load(~fs, docs, uri, Proto.path(uri))