src/lsp/nav.bend

bolt@1.27.0.0 · 453 lines · 40 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.27.0.0/src/syntax/outline.bend as Outline
10import bolt@1.27.0.0/src/syntax/word.bend as Word
11import bolt@1.27.0.0/src/syntax/bind.bend as Bind
12import bolt@1.27.0.0/src/lsp/files/service.bend as F
13import bolt@1.27.0.0/src/lsp/docs.bend as Docs
14import bolt@1.27.0.0/src/lsp/path.bend as Path
15import bolt@1.27.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))