lsp-bjolang/Completion.fs
Linus Björnstam c6f357c6f8 A language server for Bjolang
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>
2026-10-15 22:38:22 +02:00

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