Runs the compiler in-process against the Bjolang checkout beside it, and answers diagnostics, an outline, go to definition, hover and completion, plus a request of its own for macro expansion. Diagnostics come from the compiler's own check of the editor's text, saved or not: on open, on save, and 400 ms after typing pauses. The outline, definitions, hovers and completions read only as far as the reader, with no type checking, so they answer on every request: the top-level names of the file, its imports and the prelude, local names from the binding forms the parser knows, the declarations and docs as written, and the builtins with their types. emacs/bjolang-lsp.el shows a macro call's expansion, one step or all of it, in a buffer. test/smoke.py drives the server as an editor would. 🤖 Generated with [ECA](https://eca.dev) Co-Authored-By: eca-agent <git@eca.dev>
110 lines
4.3 KiB
FSharp
110 lines
4.3 KiB
FSharp
/// What can be written at a position: the top-level names of every module
|
|
/// the file sees, nearest first, the compiler's builtins, and the special
|
|
/// forms. The client filters them as the word is typed; the doc of one is
|
|
/// sent when the client asks for it, through `completionItem/resolve`.
|
|
module BjolangLsp.Completion
|
|
|
|
open System
|
|
open Bjolang
|
|
open BjolangLsp.Symbols
|
|
|
|
/// The heads the parsers recognise themselves, which no module defines. A
|
|
/// copy of the REPL's list in `Repl.fs`, which is private to it.
|
|
let private specialForms =
|
|
[ "->"; "and"; "begin"; "bjo"; "bjoroutine"; "by-colour"; "case"; "cast"; "def"; "def*"; "def/hash-extend"
|
|
"def/macro"; "def/mutable"; "def/pattern"; "def/trait"; "defbjo"; "defbjouble"; "defun"; "dyn"
|
|
"export"; "fun"; "if"; "impl"; "impl/extern"; "import"; "import/class"; "import/extern"; "include"
|
|
"let"; "let/mono"; "letrec"; "loop"; "match"; "module"; "not"; "or"; "parameterize"; "re-export"
|
|
"record"; "record-ref"; "record-set"; "record-set!"; "seq"; "seql"; "set!"; "spawn"; "spawn-evt"
|
|
"spawn/daemon"; "spawn/detached"; "struct"; "struct-ref"; "struct-set"; "struct-set!"; "task->event"
|
|
"try"; "type"; "type-rec"; "unless"; "when"; "with-open"; "with-return"; "yield"; "yield-from" ]
|
|
|
|
/// LSP's `CompletionItemKind`, the members of it used here.
|
|
type ItemKind =
|
|
| Method = 2
|
|
| Function = 3
|
|
| Field = 5
|
|
| Variable = 6
|
|
| Class = 7
|
|
| Interface = 8
|
|
| Keyword = 14
|
|
| EnumMember = 20
|
|
| Struct = 22
|
|
|
|
type Item =
|
|
{ Label: string
|
|
Kind: ItemKind
|
|
/// Where it comes from: a module's file name, `builtin`, `special form`.
|
|
From: string }
|
|
|
|
let private itemKind (k: Kind) =
|
|
match k with
|
|
| Kind.Function -> ItemKind.Function
|
|
| Kind.Variable -> ItemKind.Variable
|
|
| Kind.Class -> ItemKind.Class
|
|
| Kind.Interface -> ItemKind.Interface
|
|
| Kind.Method -> ItemKind.Method
|
|
| Kind.Field -> ItemKind.Field
|
|
| Kind.EnumMember -> ItemKind.EnumMember
|
|
| Kind.Struct -> ItemKind.Struct
|
|
| _ -> ItemKind.Variable
|
|
|
|
/// The word the cursor ends, as the start column of it on its line. A word
|
|
/// is what the lexer reads as one symbol, so `list-` and `string->` are
|
|
/// words and the item replaces all of it.
|
|
let wordStart (line: string) (column: int) : int =
|
|
let mutable i = min column line.Length
|
|
|
|
while i > 0 && Lexer.isSymbolChar line[i - 1] do
|
|
i <- i - 1
|
|
|
|
i
|
|
|
|
/// Whether `column` of `line` is in a comment or a string, where nothing is
|
|
/// offered. Read off the line alone, so a string that began on an earlier
|
|
/// line is not seen.
|
|
let inText (line: string) (column: int) : bool =
|
|
let mutable inString = false
|
|
let mutable inComment = false
|
|
let mutable i = 0
|
|
|
|
while i < min column line.Length && not inComment do
|
|
match line[i] with
|
|
| '\\' when inString -> i <- i + 1
|
|
| '#' when not inString && i + 1 < line.Length && line[i + 1] = '\\' -> i <- i + 2
|
|
| '"' -> inString <- not inString
|
|
| ';' when not inString -> inComment <- true
|
|
| _ -> ()
|
|
|
|
i <- i + 1
|
|
|
|
inString || inComment
|
|
|
|
/// Everything that can be written in the file of `text`, each name once, the
|
|
/// nearest definition of it winning.
|
|
let items (path: string) (text: string) : Item list =
|
|
let seen = Collections.Generic.HashSet<string>()
|
|
|
|
let fromModules =
|
|
[ for m in visible path text do
|
|
let from = IO.Path.GetFileName m.File
|
|
|
|
for s in m.Symbols do
|
|
// An `impl` is not a name, and a field is read with
|
|
// `record-ref` rather than written on its own.
|
|
if s.Kind <> Kind.Module && s.Kind <> Kind.Field && seen.Add s.Name then
|
|
yield { Label = s.Name; Kind = itemKind s.Kind; From = from } ]
|
|
|
|
let builtins =
|
|
[ for name in Prelude.builtinNames do
|
|
if seen.Add name then
|
|
// `Cons`, `None`, `Err`: the builtin constructors.
|
|
let kind = if Char.IsUpper name[0] then ItemKind.EnumMember else ItemKind.Function
|
|
yield { Label = name; Kind = kind; From = "builtin" } ]
|
|
|
|
let special =
|
|
[ for name in specialForms do
|
|
if seen.Add name then
|
|
yield { Label = name; Kind = ItemKind.Keyword; From = "special form" } ]
|
|
|
|
fromModules @ builtins @ special
|