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. 8 import Base 9 import ../syntax/outline.bend as Outline 10 import ../syntax/word.bend as Word 11 import ../syntax/bind.bend as Bind 12 import ./files/service.bend as F 13 import ./docs.bend as Docs 14 import ./path.bend as Path 15 import ./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, ..) 19 type 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 24 def 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 31 def 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: 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 41 def 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 45 def in_file(~fs: F.Files, docs: List<&2, Docs.Doc>, +path: String, name: String) -> IO(Maybe<&2, Found>): 46 do IO>: 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 51 def in_base(~fs: F.Files, docs: List<&2, Docs.Doc>, name: String) -> IO(Maybe<&2, Found>): 52 do IO>: 53 path : String <- F.base_path(fs) 54 in_file(~fs, docs, path, name) 55 56 # an item of the document, else of Base 57 def 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 71 def 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 86 def 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 96 def resolve(~fs: F.Files, +docs: List<&2, Docs.Doc>, +uri: String, name: String) -> IO(Maybe<&2, Found>): 97 do IO>: 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 102 def 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 112 def 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 131 def 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) 153 def 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 170 def 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 183 def 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 194 def at(~fs: F.Files, +docs: List<&2, Docs.Doc>, +uri: String, line: U32, col: U32) -> IO(Maybe<&2, Found>): 195 do IO>: 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 200 def 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 210 def 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 219 def 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>: 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 232 def 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 248 def 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 261 def 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 277 def 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 291 def 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 303 def hover(~fs: F.Files, +docs: List<&2, Docs.Doc>, +uri: String, line: U32, col: U32) -> IO(Maybe<&2, Found>): 304 do IO>: 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 313 type Sites is Data: 314 Sites{mine: Bool, at: List<&2, Bind.Pos>} 315 316 def 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 333 def 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 338 def sites(~fs: F.Files, docs: List<&2, Docs.Doc>, +uri: String, line: U32, col: U32) -> IO(Sites): 339 do IO: 340 text : String <- load(~fs, docs, uri, Proto.path(uri)) 341 return sites.in(text, line, col) 342 343 # the items of a document 344 def 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>: 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 357 def 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>: 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 369 def 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>: 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 379 def 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 388 def 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>: 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 403 def 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 418 def 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 428 def 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 440 def 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>: 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 452 def text_of(~fs: F.Files, docs: List<&2, Docs.Doc>, +uri: String) -> IO(String): 453 load(~fs, docs, uri, Proto.path(uri))