feat(tooling): JetBrains IDE support, LSP navigation, barrel imports, package scripts and hooks

- JetBrains plugin 1.4.0: semantic colours (builtin / vendor / own), template strings,
  brace handling, run configurations and a test console, package.ludic and
  package.lock.ludic editing (completion, docs, app preview, colour previews, asset
  navigation), External Libraries for the runtime and packages, doc pages for built-ins
- ludic-lsp: go to definition for imports, document links, hover with inferred types,
  type definition, signature help with parameters, docs from docs/language
- `import "dir"` resolves a barrel `dir/index.ludic`
- package.ludic `entry`, `script` and `hook before|after <command>`; `ludic <script>`,
  `ludic script`, `ludic scripts`
- `ludic test --verbose` and `--test NAME`; the test runner filters by name

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-16 16:00:28 +03:00
parent da57b20156
commit f92d7f89c6
75 changed files with 46802 additions and 40343 deletions

View file

@ -7,6 +7,11 @@
# whole compilation unit. Ground-truth compilation stays with ludicc; this layer
# supplies structure, not judgement.
program LudicLsp {
import "lsp/imports.ludic"
import "lsp/docs.ludic"
import "lsp/types.ludic"
import "lsp/sighelp.ludic"
# ---- token kinds ----
const LT_EOF: int = 0
const LT_NL: int = 1
@ -70,6 +75,13 @@ program LudicLsp {
const SC_OPERATOR: int = 22
const SC_ANNOTATION: int = 23
const SC_UNKNOWN: int = 24
const SC_NAMESPACE: int = 25
# semantic token modifier bits (the legend's order)
const SEM_DECLARATION: int = 1
const SEM_READONLY: int = 2
const SEM_DEFAULT_LIBRARY: int = 4
const SEM_VENDOR: int = 8
# ---- tiny helpers ----
function read_file(path: pointer) -> pointer {
@ -161,6 +173,8 @@ program LudicLsp {
bend: int = 0
parent: int = 0
exported: int = 0
init: int = 0 # first token of the initializer (let/var/const), or -1
mutable: int = 0 # a `var` local
}
property Doc {
path: pointer = null
@ -179,6 +193,7 @@ program LudicLsp {
linestart: []int
tmatch: []int
cls: []int
clsmods: []int # extra semantic modifiers per token (declaration, vendor, …)
syms: []Sym
imports: []pointer
had_diags: int = 0
@ -236,6 +251,8 @@ program LudicLsp {
D.linestart = new []int
push(D.linestart, 0)
var i = 0; var line = 0
var tmode = 0 # 0 code, 1 template text, 2 inside a template hole
var tdepth = 0 # braces opened inside the current hole
while s[i] != 0 {
let c = s[i]
if c == '\n' { push(D.tk_kind, LT_NL); push(D.tk_start, i); push(D.tk_end, i + 1); push(D.tk_line, line); i += 1; line += 1; push(D.linestart, i); continue }
@ -243,7 +260,33 @@ program LudicLsp {
var st = i
if c == '#' { while s[i] != 0 and s[i] != '\n' { i += 1 }; push(D.tk_kind, LT_COMMENT); push(D.tk_start, st); push(D.tk_end, i); push(D.tk_line, line); continue }
if c == '"' { i += 1; while s[i] != 0 and s[i] != '"' and s[i] != '\n' { if s[i] == '\\' and s[i + 1] != 0 { i += 2 } else { i += 1 } }; if s[i] == '"' { i += 1 }; push(D.tk_kind, LT_STR); push(D.tk_start, st); push(D.tk_end, i); push(D.tk_line, line); continue }
if c == '`' { i += 1; while s[i] != 0 and s[i] != '`' { if s[i] == '\\' and s[i + 1] != 0 { i += 2 } else { i += 1 } }; if s[i] == '`' { i += 1 }; push(D.tk_kind, LT_STR); push(D.tk_start, st); push(D.tk_end, i); push(D.tk_line, line); continue }
# `text {hole} text`: the text runs are string tokens (the hole's braces
# ride on them), the hole is lexed as code, so its names resolve
if c == '`' or (tmode == 2 and c == '}' and tdepth == 0) {
if c == '`' and tmode == 2 { i += 1; tmode = 0; push(D.tk_kind, LT_STR); push(D.tk_start, st); push(D.tk_end, i); push(D.tk_line, line); continue }
i += 1
tmode = 1
var seg_line = line
while s[i] != 0 and tmode == 1 {
if s[i] == '`' { i += 1; tmode = 0 }
else if s[i] == '{' and s[i + 1] == '{' { i += 2 }
else if s[i] == '}' and s[i + 1] == '}' { i += 2 }
else if s[i] == '{' { i += 1; tmode = 2; tdepth = 0 }
else if s[i] == '\\' and s[i + 1] != 0 { i += 2 }
else if s[i] == '\n' {
# a template may span lines: close this run, keep the line table right
if i > st { push(D.tk_kind, LT_STR); push(D.tk_start, st); push(D.tk_end, i); push(D.tk_line, seg_line) }
i += 1; line += 1; push(D.linestart, i)
st = i; seg_line = line
}
else { i += 1 }
}
if tmode == 1 { tmode = 0 }
if i > st { push(D.tk_kind, LT_STR); push(D.tk_start, st); push(D.tk_end, i); push(D.tk_line, seg_line) }
continue
}
if tmode == 2 and c == '{' { tdepth += 1 }
if tmode == 2 and c == '}' { tdepth -= 1 }
if c == '\'' { i += 1; if s[i] == '\\' and s[i + 1] != 0 { i += 2 } else { if s[i] != 0 and s[i] != '\n' { i += 1 } }; if s[i] == '\'' { i += 1 }; push(D.tk_kind, LT_CHAR); push(D.tk_start, st); push(D.tk_end, i); push(D.tk_line, line); continue }
if char_is_digit(c) {
if c == '0' and (s[i + 1] == 'x' or s[i + 1] == 'X') { i += 2; while char_is_hex(s[i]) { i += 1 }; push(D.tk_kind, LT_INT); push(D.tk_start, st); push(D.tk_end, i); push(D.tk_line, line); continue }
@ -271,6 +314,7 @@ program LudicLsp {
push(D.tk_kind, LT_ERR); push(D.tk_start, i); push(D.tk_end, i + ln); push(D.tk_line, line); i += ln
}
push(D.tk_kind, LT_EOF); push(D.tk_start, i); push(D.tk_end, i); push(D.tk_line, line)
demote_contextual_keywords(D)
}
function ntok(D: Doc) -> int { return len(D.tk_kind) }
@ -373,8 +417,13 @@ program LudicLsp {
if D.tk_kind[i] == LT_COMMENT {
var s = D.tk_start[i] + 1
while s < D.tk_end[i] and (D.text[s] == ' ' or D.text[s] == '\t' or D.text[s] == '#' or D.text[s] == '*') { s += 1 }
buf_addrange(out, D.text, s, D.tk_end[i])
buf_putc(out, '\n')
var rule = true
var r = s
while r < D.tk_end[i] { let rc = D.text[r]; if not (rc == '=' or rc == '-' or rc == '~' or rc == ' ') { rule = false }; r += 1 }
if not rule or s == D.tk_end[i] {
buf_addrange(out, D.text, s, D.tk_end[i])
buf_putc(out, '\n')
}
}
i += 1
}
@ -400,7 +449,7 @@ program LudicLsp {
function sym_add(kind: int, nametok: int, parent: int) -> int {
let s = new Sym
s.kind = kind; s.tok = nametok; s.parent = parent
s.name = ""; s.ty = ""; s.detail = ""; s.doc = ""
s.name = ""; s.ty = ""; s.detail = ""; s.doc = ""; s.init = -1; s.mutable = 0
if nametok >= 0 { s.start = pD.tk_start[nametok]; s.end = pD.tk_end[nametok]; s.name = ttext(pD, nametok) }
s.bstart = 0; s.bend = cstr_len(pD.text)
push(pD.syms, s)
@ -425,6 +474,7 @@ program LudicLsp {
if pis(":") { padv(); let tt = pname(); if tt >= 0 { ty = ttext(pD, tt) } }
let sidx = sym_add(kind, nt, owner)
pD.syms[sidx].ty = ty
pD.syms[sidx].detail = concat3("param ", pD.syms[sidx].name, concat3(": ", ty, ""))
if not first { buf_puts(sig, ", ") }
buf_puts(sig, pD.syms[sidx].name); buf_puts(sig, ": ")
if ty == "" { buf_puts(sig, "?") } else { buf_puts(sig, ty) }
@ -435,6 +485,17 @@ program LudicLsp {
buf_putc(sig, ')') # ')'
}
# the type written at pi (`int`, `[]Doc`, `?Actor`), consumed; "" when none
function lp_type_text() -> pointer {
var pre = ""
while pis("[") or pis("?") {
if pis("[") { let m = cur_match(); if m < 0 { return "" }; pi = m + 1; pre = pre + "[]" } else { pi += 1; pre = pre + "?" }
}
let tt = pname()
if tt < 0 { return "" }
return pre + ttext(pD, tt)
}
# (a, b) variable list -> token indices into out; returns count
function lp_varlist(out: []int) -> int {
var n = 0
@ -496,12 +557,15 @@ program LudicLsp {
pskipnl()
}
function lp_stmt(owner: int, scope_end: int) -> void {
if pis("let") {
if pis("let") or pis("var") {
let is_var = pis("var")
padv(); let nt = pname(); if nt < 0 { return }
var ty = ""
if pis(":") { padv(); let tt = pname(); if tt >= 0 { ty = ttext(pD, tt) } }
if pis(":") { padv(); ty = lp_type_text() }
let sidx = sym_add(LS_LOCAL, nt, owner)
pD.syms[sidx].ty = ty
if is_var { pD.syms[sidx].mutable = 1 }
if pis("=") { padv(); pD.syms[sidx].init = pi }
pD.syms[sidx].bstart = pD.tk_start[nt]; pD.syms[sidx].bend = scope_end
return
}
@ -612,19 +676,14 @@ program LudicLsp {
if pis(",") { padv(); continue }
while pk() == LT_ANNO { padv() } # @Sync / @Computed on a field
let ft = pname(); if ft < 0 { padv(); continue }
if is_comp {
let f = sym_add(LS_FIELD, ft, sidx)
var fty = ""
if pis(":") { padv(); let tt = pname(); if tt >= 0 { fty = ttext(pD, tt) } }
pD.syms[f].ty = fty
pD.syms[f].detail = concat3(pD.syms[sidx].name, ".", concat3(pD.syms[f].name, ": ", fty))
if pis("=") { padv(); while pi < ntok(pD) and not pis(",") and not pis("}") and pk() != LT_EOF { if pis("(") or pis("{") or pis("[") { skip_group() } else { padv() } } }
if not first { buf_putc(det, ',') }
buf_putc(det, ' '); buf_puts(det, pD.syms[f].name); buf_puts(det, ": "); buf_puts(det, fty)
} else {
if not first { buf_putc(det, ',') }
buf_putc(det, ' '); buf_puts(det, ttext(pD, ft))
}
let f = sym_add(LS_FIELD, ft, sidx)
var fty = ""
if pis(":") { padv(); fty = lp_type_text() }
pD.syms[f].ty = fty
pD.syms[f].detail = concat3(pD.syms[sidx].name, ".", concat3(pD.syms[f].name, ": ", fty))
if pis("=") { padv(); while pi < ntok(pD) and not pis(",") and not pis("}") and pk() != LT_EOF { if pis("(") or pis("{") or pis("[") { skip_group() } else { padv() } } }
if not first { buf_putc(det, ',') }
buf_putc(det, ' '); buf_puts(det, pD.syms[f].name); buf_puts(det, ": "); buf_puts(det, fty)
first = false
}
buf_puts(det, " }")
@ -639,7 +698,8 @@ program LudicLsp {
padv(); let nt = pname(); if nt < 0 { return }
let sidx = sym_add(kind, nt, parent)
pD.syms[sidx].doc = collect_doc(pD, decl_tok)
if pis(":") { padv(); let tt = pname(); if tt >= 0 { pD.syms[sidx].ty = ttext(pD, tt) } }
if pis(":") { padv(); pD.syms[sidx].ty = lp_type_text() }
if pis("=") { padv(); pD.syms[sidx].init = pi }
pD.syms[sidx].detail = concat3(w, " ", concat3(pD.syms[sidx].name, ": ", pD.syms[sidx].ty))
while pi < ntok(pD) and pk() != LT_NL and pk() != LT_EOF { if pis("(") or pis("{") or pis("[") { skip_group() } else { pi += 1 } }
pskipnl()
@ -1052,13 +1112,17 @@ program LudicLsp {
function imports_of(D: Doc, out: []Doc) -> void {
var i = 0
while i < len(D.imports) {
let full = join_path(dirname(D.path), D.imports[i])
let I = ensure_doc(full)
if (I != null) {
var seen = false
var k = 0
while k < len(out) { if (out[k].path == I.path) { seen = true }; k += 1 }
if not seen { push(out, I); imports_of(I, out) }
let files = resolve_import_all(D.path, D.imports[i])
var f = 0
while f < len(files) {
let I = ensure_doc(files[f])
if (I != null) {
var seen = false
var k = 0
while k < len(out) { if (out[k].path == I.path) { seen = true }; k += 1 }
if not seen { push(out, I); imports_of(I, out) }
}
f += 1
}
i += 1
}
@ -1585,6 +1649,9 @@ program LudicLsp {
function classify(D: Doc) -> void {
let N = ntok(D)
if len(D.cls) < N { D.cls = new []int; var z = 0; while z < N { push(D.cls, SC_NONE); z += 1 } }
D.clsmods = new []int
var zm = 0
while zm < N { push(D.clsmods, 0); zm += 1 }
var i = 0
while i < N {
let k = D.tk_kind[i]
@ -1603,9 +1670,26 @@ program LudicLsp {
var decl = -1
var s = 0
while s < len(D.syms) { if D.syms[s].tok == i { decl = s; break }; s += 1 }
if decl >= 0 { D.cls[i] = class_of_symkind(D.syms[decl].kind); i += 1; continue }
if decl >= 0 {
D.cls[i] = class_of_symkind(D.syms[decl].kind)
D.clsmods[i] = SEM_DECLARATION | vendor_mod(D)
i += 1; continue
}
let prev = prev_sig(D, i)
if prev >= 0 and tis(D, prev, ".") { D.cls[i] = SC_FIELD; i += 1; continue }
if prev >= 0 and tis(D, prev, ".") {
# Namespace.member: a standard-library call or constant
let recv = prev_sig(D, prev)
if recv >= 0 and D.tk_kind[recv] == LT_ID and is_namespace_name(ttext(D, recv)) {
let nx = next_sig(D, i)
if nx >= 0 and tis(D, nx, "(") { D.cls[i] = SC_BUILTIN } else { D.cls[i] = SC_CONST; D.clsmods[i] = SEM_DEFAULT_LIBRARY }
i += 1; continue
}
D.cls[i] = SC_FIELD; i += 1; continue
}
if is_namespace_name(name) and find_top(D, name) < 0 {
let nx = next_sig(D, i)
if nx >= 0 and tis(D, nx, ".") { D.cls[i] = SC_NAMESPACE; i += 1; continue }
}
let nxt = next_sig(D, i)
var followed_eq = false
if nxt >= 0 and tis(D, nxt, "=") { followed_eq = true }
@ -1619,8 +1703,8 @@ program LudicLsp {
let l = find_local(D, name, D.tk_start[i])
if l >= 0 { if D.syms[l].kind == LS_PARAM { D.cls[i] = SC_PARAM } else { D.cls[i] = SC_VARIABLE }; i += 1; continue }
let top = find_top(D, name)
if top >= 0 { D.cls[i] = class_of_symkind(g_owner.syms[top].kind); i += 1; continue }
if is_builtin_name(name) { D.cls[i] = SC_BUILTIN; i += 1; continue }
if top >= 0 { D.cls[i] = class_of_symkind(g_owner.syms[top].kind); D.clsmods[i] = vendor_mod(g_owner); i += 1; continue }
if is_builtin_name(name) or docs_index(name) >= 0 { D.cls[i] = SC_BUILTIN; i += 1; continue }
if followed_eq { D.cls[i] = SC_FIELD; i += 1; continue }
D.cls[i] = SC_UNKNOWN
i += 1
@ -1639,13 +1723,13 @@ program LudicLsp {
if cls == SC_LAYER { return 4 }
if cls == SC_UI { return 3 }
if cls == SC_WIDGET { g_semmods = 4; return 3 }
if cls == SC_SYSTEM { return 5 }
if cls == SC_SYSTEM { g_semmods = 2; return 5 } # a handler: readonly function
if cls == SC_FUNCTION { return 5 }
if cls == SC_BUILTIN { g_semmods = 4; return 5 }
if cls == SC_PARAM { return 6 }
if cls == SC_VARIABLE { return 7 }
if cls == SC_MODVAR { return 7 }
if cls == SC_CONST { g_semmods = 2; return 7 }
if cls == SC_MODVAR { g_semmods = 16; return 7 }
if cls == SC_CONST { g_semmods = 18; return 7 }
if cls == SC_PROP { return 8 }
if cls == SC_FIELD { return 8 }
if cls == SC_PHASE { g_semmods = 6; return 9 }
@ -1654,6 +1738,7 @@ program LudicLsp {
if cls == SC_COMMENT { return 12 }
if cls == SC_OPERATOR { return 13 }
if cls == SC_ANNOTATION { return 14 }
if cls == SC_NAMESPACE { g_semmods = 4; return 4 }
return -1
}
@ -2275,16 +2360,28 @@ program LudicLsp {
let off = off_of(msg, D, "params.position")
let tok = tok_at(D, off)
if tok < 0 { send_result(id, "null"); return }
if is_import_string(D, tok) {
let files = resolve_import_all(D.path, import_rel_of(D, tok))
let o = buf_new()
if len(files) == 0 { hover_md(o, "import " + ttext(D, tok), "_not found: not beside this file, in ludic_modules/, or in the toolchain's packages/_") }
else { hover_md(o, "import " + ttext(D, tok), "`" + files[0] + "`") }
send_result(id, buf_str(o)); return
}
resolve_at(D, off)
if (g_owner != null) and g_symidx >= 0 {
let ow = g_owner
let y = ow.syms[g_symidx]
var code = y.detail
if code == "" { code = y.name }
let si = g_symidx
let y = ow.syms[si]
let code = sym_hover_code(ow, si)
var docstr = y.doc
if not (ow.path == D.path) { docstr = concat3(docstr, "\n\n_declared in ", concat3(basename(ow.path), "_", "")) }
if not (ow.path == D.path) {
let where = concat3("_declared in ", basename(ow.path), "_")
if (docstr == "") { docstr = where } else { docstr = concat3(docstr, "\n\n", where) }
}
let o = buf_new(); hover_md(o, code, docstr); send_result(id, buf_str(o)); return
}
let dh = docs_hover(D, tok)
if (dh != null) { send_result(id, dh); return }
let name = ttext(D, tok)
let sig = builtin_sig(name)
if (sig != null) { let o = buf_new(); hover_md(o, sig, ""); send_result(id, buf_str(o)); return }
@ -2299,18 +2396,17 @@ program LudicLsp {
if (D == null) { send_result(id, "null"); return }
let off = off_of(msg, D, "params.position")
let tok = tok_at(D, off)
if tok >= 0 and D.tk_kind[tok] == LT_STR {
let p = prev_sig(D, tok)
if p >= 0 and tis(D, p, "import") {
var n = tlen(D, tok) - 2; if n < 0 { n = 0 }
let rel = D.src[D.tk_start[tok] + 1..D.tk_start[tok] + 1 + n]
let full = join_path(dirname(D.path), rel)
let T = ensure_doc(full)
if (T != null) { let o = buf_new(); put_location(o, T, 0, 0); send_result(id, buf_str(o)); return }
}
if is_import_string(D, tok) {
let r = import_definition(D, tok)
if (r != null) { send_result(id, r); return }
send_result(id, "null"); return
}
resolve_at(D, off)
if (g_owner == null) or g_symidx < 0 { send_result(id, "null"); return }
if (g_owner == null) or g_symidx < 0 {
# a built-in, a built-in type or a namespace: its reference page
if tok >= 0 { let dd = docs_definition(D, tok); if (dd != null) { send_result(id, dd); return } }
send_result(id, "null"); return
}
let ow = g_owner
let y = ow.syms[g_symidx]
let o = buf_new(); put_location(o, ow, y.start, y.end); send_result(id, buf_str(o))
@ -2356,6 +2452,35 @@ program LudicLsp {
}
buf_putc(o, ']'); send_result(id, buf_str(o))
}
# highlight usages: only the occurrences in THIS document — a highlight has no
# uri, so a range from another file would be painted onto this one
function on_document_highlight(msg: JVal, id: JVal) -> void {
let D = doc_of(msg)
if (D == null) { send_result(id, "[]"); return }
let off = off_of(msg, D, "params.position")
let tok = resolve_at(D, off)
if tok < 0 or (g_owner == null) or g_symidx < 0 { send_result(id, "[]"); return }
let tdoc = g_owner; let tsym = g_symidx
find_occurrences(D, tdoc, tsym)
let o = buf_new(); buf_putc(o, '[')
var first = true
var i = 0
while i < len(g_occ_doc) {
if (g_occ_doc[i].path == D.path) {
let k = g_occ_tok[i]
if not first { buf_putc(o, ',') }
first = false
buf_puts(o, "{\"range\":"); put_range(o, D, D.tk_start[k], D.tk_end[k])
var kind = 1
let nx = next_sig(D, k)
if D.tk_start[k] == tdoc.syms[tsym].start and (tdoc.path == D.path) { kind = 3 }
else if nx >= 0 and (tis(D, nx, "=") or tis(D, nx, "+=") or tis(D, nx, "-=") or tis(D, nx, "*=") or tis(D, nx, "/=")) { kind = 3 }
buf_puts(o, ",\"kind\":"); buf_puti(o, kind); buf_putc(o, '}')
}
i += 1
}
buf_putc(o, ']'); send_result(id, buf_str(o))
}
function on_rename(msg: JVal, id: JVal) -> void {
let D = doc_of(msg)
let newname = jstr(jpath(msg, "params.newName"), null)
@ -2397,25 +2522,34 @@ program LudicLsp {
# ---- completion ----
var g_first: int = 1
# the rank prefix for the next items: in-scope names before globals before
# builtins before keywords
var g_rank: pointer = "5"
function comp_item(o: Buf, label: pointer, kind: int, detail: pointer) -> void {
comp_item_full(o, label, kind, detail, g_rank + label, "")
}
function comp_item_full(o: Buf, label: pointer, kind: int, detail: pointer, sort: pointer, insert: pointer) -> void {
if g_first == 0 { buf_putc(o, ',') }
g_first = 0
buf_puts(o, "{\"label\":"); jstr_out(o, label); buf_puts(o, ",\"kind\":"); buf_puti(o, kind)
if (detail != null) and (detail[0] != 0) { buf_puts(o, ",\"detail\":"); jstr_out(o, detail) }
buf_puts(o, ",\"sortText\":"); jstr_out(o, sort)
if (insert != null) and (insert[0] != 0) { buf_puts(o, ",\"insertText\":"); jstr_out(o, insert) }
buf_putc(o, '}')
}
function comp_scope(o: Buf, D: Doc, off: int, types_only: int) -> void {
if types_only == 0 {
g_rank = "1"
var i = 0
while i < len(D.syms) {
let y = D.syms[i]
if is_local_kind(y.kind) and off >= y.bstart and off <= y.bend {
var det = y.detail; if det == "" { det = y.ty }
comp_item(o, y.name, 6, det)
if is_local_kind(y.kind) and off >= y.bstart and off <= y.bend and y.bstart <= off {
comp_item(o, y.name, 6, sym_type(D, i, 0))
}
i += 1
}
}
g_rank = "2"
let rel = related(D)
var r = 0
while r < len(rel) {
@ -2436,7 +2570,7 @@ program LudicLsp {
comp_item(o, "int", 14, "built-in type"); comp_item(o, "long", 14, "built-in type"); comp_item(o, "fixed", 14, "built-in type"); comp_item(o, "bool", 14, "built-in type")
comp_item(o, "entity", 14, "built-in type"); comp_item(o, "string", 14, "built-in type"); comp_item(o, "pointer", 14, "built-in type"); comp_item(o, "Vector", 14, "built-in type"); comp_item(o, "IVec2", 14, "built-in type"); comp_item(o, "Rect", 14, "built-in type"); comp_item(o, "void", 14, "built-in type")
}
function comp_builtins(o: Buf) -> void { var i = 0; while i < len(g_builtins) { comp_item(o, g_builtins[i], 3, g_bsigs[i]); i += 1 } }
function comp_builtins(o: Buf) -> void { g_rank = "3"; var i = 0; while i < len(g_builtins) { comp_item(o, g_builtins[i], 3, g_bsigs[i]); i += 1 } }
function on_completion(msg: JVal, id: JVal) -> void {
let D = doc_of(msg)
if (D == null) { send_result(id, "{\"isIncomplete\":false,\"items\":[]}"); return }
@ -2469,6 +2603,8 @@ program LudicLsp {
if in_qt == 1 { comp_scope(o, D, off, 1); done = 1 }
else { if anchor >= 0 {
if tis(D, anchor, ".") {
let recv = prev_sig(D, anchor)
if recv >= 0 and D.tk_kind[recv] == LT_ID { if comp_ns_members(o, ttext(D, recv)) { done = 1 } }
let comp = receiver_component(D, anchor)
if comp >= 0 and (g_owner != null) {
let co = g_owner
@ -2486,11 +2622,23 @@ program LudicLsp {
if done == 0 and in_ui_body(D, off) {
comp_item(o, "panel", 7, "widget"); comp_item(o, "col", 7, "widget"); comp_item(o, "row", 7, "widget"); comp_item(o, "label", 7, "widget"); comp_item(o, "button", 7, "widget"); comp_item(o, "image", 7, "widget"); comp_item(o, "spacer", 7, "widget")
}
var in_args = false
if done == 0 and find_call_at(D, off) {
in_args = true
if anchor >= 0 and (tis(D, anchor, "(") or tis(D, anchor, ",")) { if comp_call_args(o, D, off) { } }
}
if done == 0 and in_args {
comp_builtins(o)
comp_scope(o, D, off, 0)
done = 1
}
if done == 0 {
comp_item(o, "let", 14, "statement"); comp_item(o, "if", 14, "statement"); comp_item(o, "for", 14, "statement"); comp_item(o, "while", 14, "statement"); comp_item(o, "return", 14, "statement"); comp_item(o, "spawn", 14, "statement"); comp_item(o, "handler", 14, "declaration"); comp_item(o, "property", 14, "declaration"); comp_item(o, "model", 14, "declaration")
g_rank = "4"
comp_item(o, "let", 14, "statement"); comp_item(o, "var", 14, "statement"); comp_item(o, "if", 14, "statement"); comp_item(o, "for", 14, "statement"); comp_item(o, "while", 14, "statement"); comp_item(o, "return", 14, "statement"); comp_item(o, "spawn", 14, "statement"); comp_item(o, "handler", 14, "declaration"); comp_item(o, "property", 14, "declaration"); comp_item(o, "model", 14, "declaration")
comp_types(o)
comp_builtins(o)
comp_scope(o, D, off, 0)
g_rank = "5"
}
buf_puts(o, "]}")
send_result(id, buf_str(o))
@ -2514,51 +2662,6 @@ program LudicLsp {
}
buf_putc(o, ']'); send_result(id, buf_str(o))
}
function on_signature_help(msg: JVal, id: JVal) -> void {
let D = doc_of(msg)
if (D == null) { send_result(id, "null"); return }
let off = off_of(msg, D, "params.position")
var depth = 0; var callee = -1; var arg = 0
var i = ntok(D) - 1
while i >= 0 {
let t = i
if D.tk_start[t] >= off { i -= 1; continue }
if D.tk_kind[t] == LT_OP and tlen(D, t) == 1 {
let c = D.src[D.tk_start[t]]
if c == ')' { depth += 1 }
else { if c == '(' {
if depth == 0 {
callee = prev_sig(D, t)
var k = t + 1
while k < ntok(D) and D.tk_start[k] < off { if tis(D, k, ",") { arg += 1 }; k += 1 }
i = -1
} else { depth -= 1 }
} }
}
i -= 1
}
if callee < 0 or D.tk_kind[callee] != LT_ID { send_result(id, "null"); return }
let name = ttext(D, callee)
var sig: pointer = null
# a namespaced call `Namespace.method(` resolves through the ns surface first
let dot = prev_sig(D, callee)
if dot >= 0 and tis(D, dot, ".") {
let nsi = prev_sig(D, dot)
if nsi >= 0 and D.tk_kind[nsi] == LT_ID { sig = ns_method_sig(ttext(D, nsi), name) }
}
if (sig == null) { sig = builtin_sig(name) }
var docstr = ""
if (sig == null) {
let t = find_top(D, name)
if t >= 0 and (g_owner.syms[t].kind == LS_FN or g_owner.syms[t].kind == LS_EXTERN) { sig = g_owner.syms[t].detail; docstr = g_owner.syms[t].doc }
}
if (sig == null) { send_result(id, "null"); return }
let o = buf_new()
buf_puts(o, "{\"signatures\":[{\"label\":"); jstr_out(o, sig)
if (docstr != null) and (docstr[0] != 0) { buf_puts(o, ",\"documentation\":"); jstr_out(o, docstr) }
buf_puts(o, "}],\"activeSignature\":0,\"activeParameter\":"); buf_puti(o, arg); buf_putc(o, '}')
send_result(id, buf_str(o))
}
function on_semantic_tokens(msg: JVal, id: JVal) -> void {
let D = doc_of(msg)
if (D == null) { send_result(id, "null"); return }
@ -2580,7 +2683,9 @@ program LudicLsp {
if dl == 0 { dc = ch - prev_char }
if first == 0 { buf_putc(o, ',') }
first = 0
buf_puti(o, dl); buf_putc(o, ','); buf_puti(o, dc); buf_putc(o, ','); buf_puti(o, ln); buf_putc(o, ','); buf_puti(o, ty); buf_putc(o, ','); buf_puti(o, g_semmods)
var mods = g_semmods
if i < len(D.clsmods) { mods = mods | D.clsmods[i] }
buf_puti(o, dl); buf_putc(o, ','); buf_puti(o, dc); buf_putc(o, ','); buf_puti(o, ln); buf_putc(o, ','); buf_puti(o, ty); buf_putc(o, ','); buf_puti(o, mods)
prev_line = line; prev_char = ch
}
}
@ -2703,6 +2808,7 @@ program LudicLsp {
buf_puts(o, ",\"documentSymbolProvider\":true")
buf_puts(o, ",\"workspaceSymbolProvider\":true")
buf_puts(o, ",\"definitionProvider\":true")
buf_puts(o, ",\"typeDefinitionProvider\":true")
buf_puts(o, ",\"declarationProvider\":true")
buf_puts(o, ",\"referencesProvider\":true")
buf_puts(o, ",\"documentHighlightProvider\":true")
@ -2715,7 +2821,7 @@ program LudicLsp {
buf_puts(o, ",\"inlayHintProvider\":true")
buf_puts(o, ",\"completionProvider\":{\"triggerCharacters\":[\".\",\"@\",\"[\",\" \"],\"resolveProvider\":false}")
buf_puts(o, ",\"signatureHelpProvider\":{\"triggerCharacters\":[\"(\",\",\"]}")
buf_puts(o, ",\"semanticTokensProvider\":{\"legend\":{\"tokenTypes\":[\"keyword\",\"type\",\"struct\",\"class\",\"namespace\",\"function\",\"parameter\",\"variable\",\"property\",\"enumMember\",\"number\",\"string\",\"comment\",\"operator\",\"decorator\"],\"tokenModifiers\":[\"declaration\",\"readonly\",\"defaultLibrary\"]},\"full\":true,\"range\":true}")
buf_puts(o, ",\"semanticTokensProvider\":{\"legend\":{\"tokenTypes\":[\"keyword\",\"type\",\"struct\",\"class\",\"namespace\",\"function\",\"parameter\",\"variable\",\"property\",\"enumMember\",\"number\",\"string\",\"comment\",\"operator\",\"decorator\"],\"tokenModifiers\":[\"declaration\",\"readonly\",\"defaultLibrary\",\"vendor\",\"static\"]},\"full\":true,\"range\":true}")
buf_puts(o, "},\"serverInfo\":{\"name\":\"ludic-lsp\",\"version\":\"1.0.0\"}}")
return buf_str(o)
}
@ -2754,18 +2860,19 @@ program LudicLsp {
if (method == "textDocument/didSave") { on_did_save(msg); return }
if (method == "textDocument/documentSymbol") { let D = doc_of(msg); if (D == null) { send_result(id, "[]"); return }; let o = buf_new(); put_doc_symbols(o, D, -1); send_result(id, buf_str(o)); return }
if (method == "workspace/symbol") { on_workspace_symbol(msg, id); return }
if (method == "textDocument/definition") or (method == "textDocument/declaration") or (method == "textDocument/typeDefinition") { on_definition(msg, id); return }
if (method == "textDocument/definition") or (method == "textDocument/declaration") { on_definition(msg, id); return }
if (method == "textDocument/typeDefinition") { on_type_definition(msg, id); return }
if (method == "textDocument/hover") { on_hover(msg, id); return }
if (method == "textDocument/references") { on_references(msg, id); return }
if (method == "textDocument/documentHighlight") { on_references(msg, id); return }
if (method == "textDocument/documentHighlight") { on_document_highlight(msg, id); return }
if (method == "textDocument/rename") { on_rename(msg, id); return }
if (method == "textDocument/prepareRename") { let D = doc_of(msg); if (D == null) { send_result(id, "null"); return }; let off = off_of(msg, D, "params.position"); let tok = resolve_at(D, off); if tok < 0 or (g_owner == null) or g_symidx < 0 or D.tk_kind[tok] != LT_ID { send_result(id, "null"); return }; let o = buf_new(); buf_puts(o, "{\"range\":"); put_range(o, D, D.tk_start[tok], D.tk_end[tok]); buf_puts(o, ",\"placeholder\":"); jstr_out(o, ttext(D, tok)); buf_putc(o, '}'); send_result(id, buf_str(o)); return }
if (method == "textDocument/formatting") or (method == "textDocument/rangeFormatting") { on_formatting(msg, id); return }
if (method == "textDocument/foldingRange") { on_folding(msg, id); return }
if (method == "textDocument/inlayHint") { on_inlay_hint(msg, id); return }
if (method == "textDocument/signatureHelp") { on_signature_help(msg, id); return }
if (method == "textDocument/signatureHelp") { on_signature_help2(msg, id); return }
if (method == "textDocument/completion") { on_completion(msg, id); return }
if (method == "textDocument/documentLink") { send_result(id, "[]"); return }
if (method == "textDocument/documentLink") { on_document_link(msg, id); return }
if (method == "textDocument/semanticTokens/full") or (method == "textDocument/semanticTokens/range") { on_semantic_tokens(msg, id); return }
if (id != null) { send_result(id, "null") }
}

View file

@ -0,0 +1,253 @@
# lsp/docs.ludic — the language reference, indexed for hover and go-to.
#
# docs/language/**.md (shipped in the install root) holds one page per symbol,
# with front matter:
#
# id: screen-fill_circle -> <DOCS_URL><id>.html
# tokens: Screen.fill_circle the source spellings it documents
# sig: / tip: / kind: / ns:
#
# Built-ins, built-in types and namespace methods therefore get a signature, a
# one-line summary and a link in hover, and go-to-definition opens the page.
# Loaded once, on first use; a toolchain without docs simply has no entries.
var DOCS_URL: pointer = "https://workshopsoft.pages.workshopsoft.io/ludic/"
var g_docs_loaded: bool = false
var g_dx_token: []pointer
var g_dx_file: []pointer
var g_dx_id: []pointer
var g_dx_sig: []pointer
var g_dx_tip: []pointer
var g_dx_kind: []pointer
# namespace name -> its section page
var g_dxn_name: []pointer
var g_dxn_file: []pointer
var g_dxn_id: []pointer
# the value of `key: …` in a front-matter block, or ""
function fm_get(fm: pointer, key: pointer) -> pointer {
let n = cstr_len(fm)
let kl = cstr_len(key)
var i = 0
while i < n {
var e = i
while e < n and fm[e] != '\n' { e += 1 }
if e - i > kl and fm[i..i + kl] == key and fm[i + kl] == ':' {
var s = i + kl + 1
while s < e and fm[s] == ' ' { s += 1 }
var t = e
while t > s and (fm[t - 1] == ' ' or fm[t - 1] == '\r') { t -= 1 }
return fm[s..t]
}
i = e + 1
}
return ""
}
# the front matter of a page (between the leading `---` lines), or ""
function fm_of(text: pointer) -> pointer {
if not starts_with(text, "---\n") { return "" }
let n = cstr_len(text)
var i = 4
while i < n {
if text[i] == '-' and text[i - 1] == '\n' and i + 3 <= n and text[i..i + 3] == "---" { return text[4..i] }
i += 1
}
return ""
}
# the body after the front matter
function fm_body(text: pointer) -> pointer {
let fm = fm_of(text)
if (fm == "") { return text }
var i = 4 + cstr_len(fm) + 3
while text[i] == '\n' or text[i] == '\r' { i += 1 }
return text[i..cstr_len(text)]
}
function docs_add(tok: pointer, file: pointer, fm: pointer) -> void {
push(g_dx_token, tok)
push(g_dx_file, file)
push(g_dx_id, fm_get(fm, "id"))
push(g_dx_sig, fm_get(fm, "sig"))
push(g_dx_tip, fm_get(fm, "tip"))
push(g_dx_kind, fm_get(fm, "kind"))
}
function docs_load() -> void {
if g_docs_loaded { return }
g_docs_loaded = true
g_dx_token = new []pointer; g_dx_file = new []pointer; g_dx_id = new []pointer
g_dx_sig = new []pointer; g_dx_tip = new []pointer; g_dx_kind = new []pointer
g_dxn_name = new []pointer; g_dxn_file = new []pointer; g_dxn_id = new []pointer
let root = lsp_home() + "docs/language"
if not Fs.is_dir(root) { return }
let listfile = `{Os.temp_dir()}/ludic_lsp_docs_{Os.pid()}.txt`
run(`find '{root}' -name '*.md' > '{listfile}' 2>/dev/null`)
let list = read_file(listfile)
run(`rm -f '{listfile}'`)
if (list == null) { return }
var i = 0
var start = 0
while list[i] != 0 {
if list[i] == '\n' {
if i > start { docs_index_file(list[start..i]) }
start = i + 1
}
i += 1
}
if i > start { docs_index_file(list[start..i]) }
}
function docs_index_file(path: pointer) -> void {
if ends_with(path, "/_section.md") { return }
let text = read_file(path)
if (text == null) { return }
let fm = fm_of(text)
if (fm == "") { return }
let toks = fm_get(fm, "tokens")
var i = 0
let n = cstr_len(toks)
while i < n {
while i < n and toks[i] == ' ' { i += 1 }
let s = i
while i < n and toks[i] != ' ' { i += 1 }
if i > s { docs_add(toks[s..i], path, fm) }
}
# a namespace method also tells us where its namespace's section page is
let ns = fm_get(fm, "ns")
if not (ns == "") and docs_ns_index(ns) < 0 {
let sec = dirname(path) + "/_section.md"
if Fs.exists(sec) {
push(g_dxn_name, ns)
push(g_dxn_file, sec)
push(g_dxn_id, "ns-" + lower_str(ns))
}
}
}
function lower_str(s: pointer) -> pointer {
let n = cstr_len(s)
let b = bytes(n + 1)
var i = 0
while i < n { b[i] = lower(s[i]); i += 1 }
b[n] = 0
return b
}
function docs_index(tok: pointer) -> int {
docs_load()
var i = 0
while i < len(g_dx_token) { if (g_dx_token[i] == tok) { return i }; i += 1 }
return -1
}
function docs_ns_index(ns: pointer) -> int {
var i = 0
while i < len(g_dxn_name) { if (g_dxn_name[i] == ns) { return i }; i += 1 }
return -1
}
function docs_url(i: int) -> pointer { return DOCS_URL + g_dx_id[i] + ".html" }
# the spelling a token is documented under: `Screen.fill_circle` for the
# method name in `Screen.fill_circle(…)`, else the word itself
function doc_key_at(D: Doc, tok: int) -> pointer {
let name = ttext(D, tok)
let dot = prev_sig(D, tok)
if dot >= 0 and tis(D, dot, ".") {
let recv = prev_sig(D, dot)
if recv >= 0 { return ttext(D, recv) + "." + name }
}
return name
}
# a hover for a documented name, or null
function docs_hover(D: Doc, tok: int) -> pointer {
let key = doc_key_at(D, tok)
let i = docs_index(key)
if i >= 0 {
var sig = g_dx_sig[i]
if (sig == "") { sig = key }
let m = buf_new()
buf_puts(m, g_dx_tip[i])
let body = docs_first_paragraph(g_dx_file[i])
if not (body == "") and not (body == g_dx_tip[i]) { buf_puts(m, "\n\n"); buf_puts(m, body) }
buf_puts(m, "\n\n[Documentation](")
buf_puts(m, docs_url(i))
buf_puts(m, ")")
let o = buf_new()
hover_md(o, sig, buf_str(m))
return buf_str(o)
}
let n = docs_ns_index(ttext(D, tok))
if n >= 0 {
let o = buf_new()
let body = docs_first_paragraph(g_dxn_file[n])
hover_md(o, "namespace " + g_dxn_name[n], body + "\n\n[Documentation](" + DOCS_URL + g_dxn_id[n] + ".html)")
return buf_str(o)
}
return null
}
# go-to-definition for a documented name: its page
function docs_definition(D: Doc, tok: int) -> pointer {
var file: pointer = null
let i = docs_index(doc_key_at(D, tok))
if i >= 0 { file = g_dx_file[i] }
else {
let n = docs_ns_index(ttext(D, tok))
if n >= 0 { file = g_dxn_file[n] }
}
if (file == null) { return null }
let o = buf_new()
buf_puts(o, "{\"uri\":")
jstr_out(o, path_to_uri(file))
buf_puts(o, ",\"range\":{\"start\":{\"line\":0,\"character\":0},\"end\":{\"line\":0,\"character\":0}}}")
return buf_str(o)
}
# the first prose paragraph of a page, markup kept (the editor renders markdown)
function docs_first_paragraph(path: pointer) -> pointer {
let text = read_file(path)
if (text == null) { return "" }
let body = fm_body(text)
let n = cstr_len(body)
var i = 0
while i < n and (body[i] == '\n' or body[i] == ' ') { i += 1 }
if i + 2 < n and body[i..i + 3] == "```" { return "" }
let s = i
while i < n {
if body[i] == '\n' and i + 1 < n and body[i + 1] == '\n' { break }
i += 1
}
var p = body[s..i]
if cstr_len(p) > 600 { p = p[0..600] + "…" }
return p
}
# the documented signature of a namespace method or builtin, or null
function docs_sig(key: pointer) -> pointer {
let i = docs_index(key)
if i < 0 { return null }
if (g_dx_sig[i] == "") { return null }
return g_dx_sig[i]
}
function docs_tip(key: pointer) -> pointer {
let i = docs_index(key)
if i < 0 { return "" }
return g_dx_tip[i]
}
# every documented member of a namespace: `Screen` -> fill_circle, show, …
function docs_ns_members(ns: pointer, out: []int) -> void {
docs_load()
let pre = ns + "."
var i = 0
while i < len(g_dx_token) {
if starts_with(g_dx_token[i], pre) { push(out, i) }
i += 1
}
}

View file

@ -0,0 +1,215 @@
# lsp/imports.ludic — where an `import "…"` string points, resolved the way the
# compiler's do_import (selfhost/frontend/parse.ludic) resolves it:
#
# 1. relative to the importing file import "camp/save.ludic"
# 2. the package's ludic_modules/ (or $LUDIC_MODULES) third-party packages
# 3. the install root: runtime/… and packages/… import "ludic.render3d/r3d.ludic"
#
# `dir/*.ludic` imports every .ludic in the directory, sorted by name.
var g_home: pointer = null
# the install root with a trailing '/': $LUDIC_HOME, else the directory above
# this binary's bin/ (a checkout's bin/ludic-lsp gives the checkout itself)
function lsp_home() -> pointer {
if (g_home != null) { return g_home }
var h = Os.env_or("LUDIC_HOME", "")
if not (h == "") {
if not ends_with(h, "/") { h = h + "/" }
g_home = h
return h
}
let me = arg(0)
var i = cstr_len(me)
while i > 0 and me[i - 1] != '/' { i -= 1 }
var d = ""
if i > 0 { d = me[0..i] }
else { d = path_dir_holding(me) } # started by name, from $PATH
if not (d == "") and d[0] != '/' { # started by a relative path
let cwd = Os.env_or("PWD", "")
if not (cwd == "") { d = cwd + "/" + d }
}
if not (d == "") { d = normpath(d) + "/" }
if ends_with(d, "/bin/") { d = d[0..len(d) - 4] }
g_home = d
return d
}
# the first $PATH directory holding `name`, with a trailing '/', or ""
function path_dir_holding(name: pointer) -> pointer {
let p = Os.env_or("PATH", "")
let n = cstr_len(p)
var a = 0
while a < n {
var b = a
while b < n and p[b] != ':' { b += 1 }
if b > a {
let dir = p[a..b]
if Fs.exists(dir + "/" + name) { return dir + "/" }
}
a = b + 1
}
return ""
}
# the nearest directory at or above dir holding a package.ludic, or null
function package_root_of(dir: pointer) -> pointer {
var d = dir
var guard = 0
while guard < 64 {
if Fs.exists(d + "/package.ludic") { return d }
if (d == "/") or (d == ".") or (d == "") { return null }
d = dirname(d)
guard += 1
}
return null
}
function modules_dir_for(from: pointer) -> pointer {
let env = Os.env_or("LUDIC_MODULES", "")
if not (env == "") {
if env[0] == '/' { return env }
let r = package_root_of(dirname(from))
if (r != null) { return join_path(r, env) }
return null
}
let root = package_root_of(dirname(from))
if (root == null) { return null }
return root + "/ludic_modules"
}
# one file an import names, or null when it cannot be found anywhere
function resolve_import(from: pointer, rel: pointer) -> pointer {
if (rel == "") { return null }
# `import "camp"`: the directory's barrel
if not ends_with(rel, ".ludic") {
if ends_with(rel, "/") { return resolve_import(from, rel + "index.ludic") }
return resolve_import(from, rel + "/index.ludic")
}
let direct = join_path(dirname(from), rel)
if Fs.exists(direct) and not Fs.is_dir(direct) { return direct }
if rel[0] == '/' { return null }
let mods = modules_dir_for(from)
if (mods != null) {
let m = join_path(mods, rel)
if Fs.exists(m) and not Fs.is_dir(m) { return m }
}
let home = lsp_home()
if not (home == "") {
if starts_with(rel, "runtime/") {
let rt = join_path(home, rel)
if Fs.exists(rt) { return rt }
}
let pk = join_path(home + "packages", rel)
if Fs.exists(pk) and not Fs.is_dir(pk) { return pk }
}
return null
}
function is_glob_import(rel: pointer) -> bool { return ends_with(rel, "/*.ludic") }
# every file an import names: one for a plain import, the sorted directory
# listing for `dir/*.ludic`
function resolve_import_all(from: pointer, rel: pointer) -> []pointer {
let out = new []pointer
if not is_glob_import(rel) {
let one = resolve_import(from, rel)
if (one != null) { push(out, one) }
return out
}
let dirrel = rel[0..len(rel) - 8]
var dir = join_path(dirname(from), dirrel)
if not Fs.is_dir(dir) {
let home = lsp_home()
dir = join_path(home + "packages", dirrel)
if not Fs.is_dir(dir) { return out }
}
let names = Fs.list(dir)
if (names == null) { return out }
var i = 0
while i < len(names) {
var best = i
var j = i + 1
while j < len(names) { if str_before(names[j], names[best]) { best = j }; j += 1 }
let t = names[i]; names[i] = names[best]; names[best] = t
i += 1
}
i = 0
while i < len(names) {
if ends_with(names[i], ".ludic") { push(out, join_path(dir, names[i])) }
i += 1
}
return out
}
function str_before(a: pointer, b: pointer) -> bool {
var i = 0
while a[i] != 0 and b[i] != 0 {
if a[i] != b[i] { return a[i] < b[i] }
i += 1
}
return a[i] == 0 and b[i] != 0
}
# the text inside an import's string token (no quotes)
function import_rel_of(D: Doc, tok: int) -> pointer {
var n = tlen(D, tok) - 2
if n < 0 { n = 0 }
return D.src[D.tk_start[tok] + 1..D.tk_start[tok] + 1 + n]
}
# is tok the string of an `import "…"`?
function is_import_string(D: Doc, tok: int) -> bool {
if tok < 0 or D.tk_kind[tok] != LT_STR { return false }
let p = prev_sig(D, tok)
return p >= 0 and tis(D, p, "import")
}
# textDocument/documentLink: every import string links to its file, so the
# editor underlines it and Cmd/Ctrl-click opens it
function on_document_link(msg: JVal, id: JVal) -> void {
let D = doc_of(msg)
if (D == null) { send_result(id, "[]"); return }
let o = buf_new()
buf_putc(o, '[')
var first = true
var t = 0
while t < ntok(D) {
if is_import_string(D, t) {
let files = resolve_import_all(D.path, import_rel_of(D, t))
if len(files) > 0 and tlen(D, t) > 2 {
if not first { buf_putc(o, ',') }
first = false
buf_puts(o, "{\"range\":")
put_range(o, D, D.tk_start[t] + 1, D.tk_end[t] - 1)
buf_puts(o, ",\"target\":")
jstr_out(o, path_to_uri(files[0]))
buf_puts(o, ",\"tooltip\":")
jstr_out(o, files[0])
buf_putc(o, '}')
}
}
t += 1
}
buf_putc(o, ']')
send_result(id, buf_str(o))
}
# go-to-definition on an import string: the file (every file, for a glob)
function import_definition(D: Doc, tok: int) -> pointer {
let files = resolve_import_all(D.path, import_rel_of(D, tok))
if len(files) == 0 { return null }
let o = buf_new()
buf_putc(o, '[')
var i = 0
while i < len(files) {
let T = ensure_doc(files[i])
if (T != null) {
if o.len > 1 { buf_putc(o, ',') }
put_location(o, T, 0, 0)
}
i += 1
}
buf_putc(o, ']')
return buf_str(o)
}

View file

@ -0,0 +1,226 @@
# lsp/sighelp.ludic — the call the cursor is inside: signature help with
# per-parameter ranges (so the editor bolds the active one), and named-argument
# completion (`Screen.fill_circle(x: …, radius: …)`).
# the call around off: sets g_call_* and returns true when there is one
var g_call_callee: int = -1 # token of the function name
var g_call_open: int = -1 # token of its `(`
var g_call_arg: int = 0 # which argument (0-based) the cursor is in
var g_call_named: pointer = "" # `name:` written at the start of that argument, if any
var g_call_used: []pointer # names already given in earlier arguments
function find_call_at(D: Doc, off: int) -> bool {
g_call_callee = -1; g_call_open = -1; g_call_arg = 0; g_call_named = ""
g_call_used = new []pointer
var depth = 0
var i = ntok(D) - 1
while i >= 0 and D.tk_start[i] >= off { i -= 1 }
while i >= 0 {
if D.tk_kind[i] == LT_OP and tlen(D, i) == 1 {
let c = D.src[D.tk_start[i]]
if c == ')' or c == ']' { depth += 1 }
else if c == '[' { if depth == 0 { return false }; depth -= 1 }
else if c == '{' or c == '}' { if depth == 0 { return false } }
else if c == '(' {
if depth == 0 {
let callee = prev_sig(D, i)
if callee < 0 or D.tk_kind[callee] != LT_ID { return false }
g_call_callee = callee
g_call_open = i
call_args_before(D, i, off)
return true
}
depth -= 1
}
}
i -= 1
}
return false
}
# count the arguments before off and collect `name:` labels
function call_args_before(D: Doc, open: int, off: int) -> void {
var depth = 0
var arg_start = open + 1
var k = open + 1
while k < ntok(D) and D.tk_start[k] < off {
if D.tk_kind[k] == LT_OP and tlen(D, k) == 1 {
let c = D.src[D.tk_start[k]]
if c == '(' or c == '[' or c == '{' { depth += 1 }
else if c == ')' or c == ']' or c == '}' { depth -= 1 }
else if c == ',' and depth == 0 {
let lbl = arg_label(D, arg_start)
if not (lbl == "") { push(g_call_used, lbl) }
g_call_arg += 1
arg_start = k + 1
}
}
k += 1
}
let here = arg_label(D, arg_start)
if not (here == "") and D.tk_end[arg_start] <= off { g_call_named = here }
}
# `name:` at the start of an argument -> name, else ""
function arg_label(D: Doc, t: int) -> pointer {
var s = t
while s < ntok(D) and (D.tk_kind[s] == LT_NL or D.tk_kind[s] == LT_COMMENT) { s += 1 }
if s >= ntok(D) or D.tk_kind[s] != LT_ID { return "" }
let c = next_sig(D, s)
if c >= 0 and tis(D, c, ":") { return ttext(D, s) }
return ""
}
# the signature label and documentation for the call's callee
var g_call_doc: pointer = ""
function call_signature(D: Doc) -> pointer {
g_call_doc = ""
let name = ttext(D, g_call_callee)
let dot = prev_sig(D, g_call_callee)
if dot >= 0 and tis(D, dot, ".") {
let nsi = prev_sig(D, dot)
if nsi >= 0 and D.tk_kind[nsi] == LT_ID {
let key = ttext(D, nsi) + "." + name
let ds = docs_sig(key)
if (ds != null) { g_call_doc = docs_tip(key); return ds }
let ns = ns_method_sig(ttext(D, nsi), name)
if (ns != null) { return ns }
}
return null
}
let t = find_top(D, name)
if t >= 0 and (g_owner.syms[t].kind == LS_FN or g_owner.syms[t].kind == LS_EXTERN) {
g_call_doc = g_owner.syms[t].doc
return g_owner.syms[t].detail
}
let ds = docs_sig(name)
if (ds != null) { g_call_doc = docs_tip(name); return ds }
return builtin_sig(name)
}
# [start, end) of each parameter inside the label's outermost ( )
var g_ps: []int
var g_pe: []int
function split_params(label: pointer) -> void {
g_ps = new []int; g_pe = new []int
let n = cstr_len(label)
var open = 0
while open < n and label[open] != '(' { open += 1 }
if open >= n { return }
var depth = 0
var s = open + 1
var i = open + 1
while i < n {
let c = label[i]
if c == '(' or c == '[' { depth += 1 }
else if (c == ')' and depth == 0) or (c == ',' and depth == 0) {
var a = s
while a < i and label[a] == ' ' { a += 1 }
var b = i
while b > a and label[b - 1] == ' ' { b -= 1 }
if b > a { push(g_ps, a); push(g_pe, b) }
if c == ')' { return }
s = i + 1
}
else if c == ')' or c == ']' { depth -= 1 }
i += 1
}
}
# the parameter name in label[a..b): `radius`, `sk: Skin` -> sk
function param_name(label: pointer, a: int, b: int) -> pointer {
var e = a
while e < b and label[e] != ':' and label[e] != ' ' and label[e] != '=' { e += 1 }
return label[a..e]
}
function param_type(label: pointer, a: int, b: int) -> pointer {
var c = a
while c < b and label[c] != ':' { c += 1 }
if c >= b { return "" }
c += 1
while c < b and label[c] == ' ' { c += 1 }
return label[c..b]
}
function on_signature_help2(msg: JVal, id: JVal) -> void {
let D = doc_of(msg)
if (D == null) { send_result(id, "null"); return }
let off = off_of(msg, D, "params.position")
if not find_call_at(D, off) { send_result(id, "null"); return }
let sig = call_signature(D)
if (sig == null) { send_result(id, "null"); return }
split_params(sig)
var active = g_call_arg
if not (g_call_named == "") {
var p = 0
while p < len(g_ps) { if (param_name(sig, g_ps[p], g_pe[p]) == g_call_named) { active = p }; p += 1 }
}
let o = buf_new()
buf_puts(o, "{\"signatures\":[{\"label\":")
jstr_out(o, sig)
if (g_call_doc != null) and not (g_call_doc == "") {
buf_puts(o, ",\"documentation\":{\"kind\":\"markdown\",\"value\":")
jstr_out(o, g_call_doc)
buf_putc(o, '}')
}
buf_puts(o, ",\"parameters\":[")
var i = 0
while i < len(g_ps) {
if i > 0 { buf_putc(o, ',') }
buf_puts(o, "{\"label\":[")
buf_puti(o, utf16_len(sig, 0, g_ps[i]))
buf_putc(o, ',')
buf_puti(o, utf16_len(sig, 0, g_pe[i]))
buf_puts(o, "]")
let ty = param_type(sig, g_ps[i], g_pe[i])
if not (ty == "") { buf_puts(o, ",\"documentation\":"); jstr_out(o, ty) }
buf_putc(o, '}')
i += 1
}
buf_puts(o, "]}],\"activeSignature\":0,\"activeParameter\":")
buf_puti(o, active)
buf_putc(o, '}')
send_result(id, buf_str(o))
}
# completion inside a call's argument list: `name: ` for every parameter not
# given yet, ranked first. Returns true when it added anything.
function comp_call_args(o: Buf, D: Doc, off: int) -> bool {
if not find_call_at(D, off) { return false }
if not (g_call_named == "") { return false }
let sig = call_signature(D)
if (sig == null) { return false }
split_params(sig)
var added = false
var i = 0
while i < len(g_ps) {
let pn = param_name(sig, g_ps[i], g_pe[i])
var used = false
var u = 0
while u < len(g_call_used) { if (g_call_used[u] == pn) { used = true }; u += 1 }
if not used and cstr_len(pn) > 0 {
comp_item_full(o, pn + ":", 5, param_type(sig, g_ps[i], g_pe[i]), "0" + itoa(i + 10), pn + ": ")
added = true
}
i += 1
}
return added
}
# completion for `Namespace.`: its documented members
function comp_ns_members(o: Buf, ns: pointer) -> bool {
let idx = new []int
docs_ns_members(ns, idx)
var i = 0
while i < len(idx) {
let k = idx[i]
let full = g_dx_token[k]
let member = full[cstr_len(ns) + 1..cstr_len(full)]
var sig = g_dx_sig[k]
if (sig == "") { sig = g_dx_tip[k] }
comp_item_full(o, member, 2, sig, "0" + member, "")
i += 1
}
return len(idx) > 0
}

View file

@ -0,0 +1,277 @@
# lsp/types.ludic — what type a name has, for hover and go-to-type.
#
# Declared types are taken as written (`var hp: int`, `sk: Skin`). An
# undeclared one (`let i = skin_joint(sk, "hip")`) is inferred from its
# initializer, shallowly: literals, `new T`, calls to functions and builtins
# with a known return type, other names, fields, and comparisons. Anything
# cleverer is the compiler's job; an unknown type is simply left out.
# the return type written after the last `->` of a signature label, or ""
function sig_return(sig: pointer) -> pointer {
if (sig == null) { return "" }
let n = cstr_len(sig)
var at = -1
var i = 0
while i + 1 < n {
if sig[i] == '-' and sig[i + 1] == '>' { at = i }
i += 1
}
if at < 0 { return "" }
var s = at + 2
while s < n and sig[s] == ' ' { s += 1 }
var e = n
while e > s and sig[e - 1] == ' ' { e -= 1 }
return sig[s..e]
}
function is_compare_op(D: Doc, t: int) -> bool {
return tis(D, t, "==") or tis(D, t, "!=") or tis(D, t, "<") or tis(D, t, ">") or tis(D, t, "<=") or tis(D, t, ">=") or tis(D, t, "and") or tis(D, t, "or")
}
# does the expression starting at t contain a top-level comparison before it ends?
function expr_is_boolean(D: Doc, t: int) -> bool {
var depth = 0
var k = t
let line = D.tk_line[t]
while k >= 0 and k < ntok(D) {
let kind = D.tk_kind[k]
if kind == LT_NL or kind == LT_EOF or kind == LT_COMMENT { return false }
if tis(D, k, "(") or tis(D, k, "[") { depth += 1 }
else if tis(D, k, ")") or tis(D, k, "]") { if depth == 0 { return false }; depth -= 1 }
else if tis(D, k, "{") or tis(D, k, "}") or tis(D, k, ";") or tis(D, k, ",") { if depth == 0 { return false } }
else if depth == 0 and is_compare_op(D, k) { return true }
if D.tk_line[k] != line { return false }
k += 1
}
return false
}
# the declared or inferred type of symbol y (which lives in doc O), or ""
function sym_type(O: Doc, si: int, depth: int) -> pointer {
let y = O.syms[si]
if (y.ty != null) and not (y.ty == "") { return y.ty }
if y.init >= 0 and depth < 8 { return infer_type(O, y.init, depth + 1) }
return ""
}
# the type of the expression whose first token is t, or ""
function infer_type(D: Doc, t: int, depth: int) -> pointer {
if t < 0 or t >= ntok(D) or depth > 8 { return "" }
if expr_is_boolean(D, t) { return "bool" }
let k = D.tk_kind[t]
if k == LT_INT { return "int" }
if k == LT_FLOAT { return "fixed" }
if k == LT_STR { return "string" }
if k == LT_CHAR { return "int" }
if k == LT_BOOL { return "bool" }
if tis(D, t, "not") { return "bool" }
if tis(D, t, "(") { return infer_type(D, t + 1, depth + 1) }
if tis(D, t, "-") { return infer_type(D, next_sig(D, t), depth + 1) }
if tis(D, t, "new") {
let n = next_sig(D, t)
if n < 0 { return "" }
if tis(D, n, "[") {
let close = D.tmatch[n]
if close >= 0 { let el = next_sig(D, close); if el >= 0 { return "[]" + ttext(D, el) } }
return ""
}
return ttext(D, n)
}
if tis(D, t, "[") {
let first = next_sig(D, t)
if first >= 0 and not tis(D, first, "]") { let et = infer_type(D, first, depth + 1); if not (et == "") { return "[]" + et } }
return ""
}
if k != LT_ID and k != LT_TYPE { return "" }
let name = ttext(D, t)
let nx = next_sig(D, t)
# a call: its return type
if nx >= 0 and tis(D, nx, "(") {
if k == LT_TYPE { return name } # int(x), string(n)
let top = find_top(D, name)
if top >= 0 {
let ow = g_owner
if ow.syms[top].kind == LS_FN or ow.syms[top].kind == LS_EXTERN {
let r = ow.syms[top].ty
if (r == "") { return "void" }
return r
}
}
let r2 = sig_return(docs_sig(name))
if not (r2 == "") { return r2 }
return sig_return(builtin_sig(name))
}
# Name.member: a namespace call, or a field of a typed receiver
if nx >= 0 and tis(D, nx, ".") {
let m = next_sig(D, nx)
if m < 0 { return "" }
let mn = next_sig(D, m)
if mn >= 0 and tis(D, mn, "(") {
let r = sig_return(docs_sig(name + "." + ttext(D, m)))
if not (r == "") { return r }
return sig_return(ns_method_sig(name, ttext(D, m)))
}
let comp = receiver_component(D, nx)
if comp >= 0 and (g_owner != null) {
let co = g_owner
var f = 0
while f < len(co.syms) {
if co.syms[f].kind == LS_FIELD and co.syms[f].parent == comp and (co.syms[f].name == ttext(D, m)) { return co.syms[f].ty }
f += 1
}
}
return ""
}
# a record literal: `Pos { x: 1 }`
if nx >= 0 and tis(D, nx, "{") and name[0] >= 'A' and name[0] <= 'Z' { return name }
if k == LT_TYPE { return name }
# another name
let l = find_local(D, name, D.tk_start[t])
if l >= 0 {
if D.syms[l].init == t { return "" }
return sym_type(D, l, depth + 1)
}
let top = find_top(D, name)
if top >= 0 {
let ow = g_owner
let yk = ow.syms[top].kind
if yk == LS_VAR or yk == LS_CONST { return sym_type(ow, top, depth + 1) }
if yk == LS_FN or yk == LS_EXTERN { return ow.syms[top].detail }
}
return ""
}
# the hover code line for a symbol: `var hp: int`, `param sk: Skin`, …
function sym_hover_code(O: Doc, si: int) -> pointer {
let y = O.syms[si]
let k = y.kind
if k == LS_QUERYVAR and not (y.detail == "") { return y.detail }
if k == LS_LOCAL or k == LS_PARAM or k == LS_VAR or k == LS_CONST or k == LS_QUERYVAR {
let ty = sym_type(O, si, 0)
var head = "let "
if k == LS_LOCAL and y.mutable == 1 { head = "var " }
if k == LS_PARAM { head = "param " }
if k == LS_VAR { head = "var " }
if k == LS_CONST { head = "const " }
if k == LS_QUERYVAR { head = "query " }
if (ty == "") { return head + y.name }
return head + y.name + ": " + ty
}
if k == LS_COMPONENT or k == LS_ARCHETYPE { return record_hover_code(O, si) }
if (y.detail == "") { return y.name }
return y.detail
}
# `property Actor {` with one field per line, the way it is written
function record_hover_code(O: Doc, si: int) -> pointer {
let y = O.syms[si]
let b = buf_new()
if y.kind == LS_COMPONENT { buf_puts(b, "property ") } else { buf_puts(b, "model ") }
buf_puts(b, y.name)
buf_puts(b, " {")
var n = 0
var f = 0
while f < len(O.syms) {
let fy = O.syms[f]
if fy.kind == LS_FIELD and fy.parent == si {
buf_puts(b, "\n ")
buf_puts(b, fy.name)
if not (fy.ty == "") { buf_puts(b, ": "); buf_puts(b, fy.ty) }
n += 1
}
f += 1
}
if n == 0 { buf_puts(b, " }") } else { buf_puts(b, "\n}") }
return buf_str(b)
}
# the declaration of a symbol's type: a model/property in the workspace, else
# the reference page of a built-in type. Location JSON or null.
function type_definition_of(O: Doc, si: int, from: Doc) -> pointer {
var ty = sym_type(O, si, 0)
if (ty == "") { return null }
while starts_with(ty, "[]") { ty = ty[2..cstr_len(ty)] }
let top = find_top(from, ty)
if top >= 0 {
let ow = g_owner
let o = buf_new()
put_location(o, ow, ow.syms[top].start, ow.syms[top].end)
return buf_str(o)
}
let i = docs_index(ty)
if i < 0 { return null }
let o = buf_new()
buf_puts(o, "{\"uri\":")
jstr_out(o, path_to_uri(g_dx_file[i]))
buf_puts(o, ",\"range\":{\"start\":{\"line\":0,\"character\":0},\"end\":{\"line\":0,\"character\":0}}}")
return buf_str(o)
}
function on_type_definition(msg: JVal, id: JVal) -> void {
let D = doc_of(msg)
if (D == null) { send_result(id, "null"); return }
let off = off_of(msg, D, "params.position")
resolve_at(D, off)
if (g_owner == null) or g_symidx < 0 { on_definition(msg, id); return }
let r = type_definition_of(g_owner, g_symidx, D)
if (r == null) { on_definition(msg, id); return }
send_result(id, r)
}
# ---- contextual keywords ------------------------------------------------------
# The compiler reserves its declaration words only as function names
# (is_reserved_word in selfhost/frontend/parse.ludic): `var model = …`,
# `model: Model` and `a.model` are all ordinary names. A word is a keyword only
# where it could start or continue a declaration.
function is_contextual_word(w: pointer) -> bool {
if (w == "program") or (w == "import") or (w == "property") or (w == "model") or (w == "enum") or (w == "ui") { return true }
if (w == "function") or (w == "extern") or (w == "handler") or (w == "entry") or (w == "event") or (w == "scene") { return true }
if (w == "phase") or (w == "query") or (w == "on") or (w == "cancellable") or (w == "public") or (w == "layer") or (w == "start") { return true }
if (w == "state") or (w == "machine") or (w == "prefab") or (w == "namespace") or (w == "test") { return true }
return false
}
function demote_contextual_keywords(D: Doc) -> void {
let N = ntok(D)
var i = 0
while i < N {
if D.tk_kind[i] == LT_KW and is_contextual_word(ttext(D, i)) and used_as_name(D, i) { D.tk_kind[i] = LT_ID }
i += 1
}
}
function used_as_name(D: Doc, i: int) -> bool {
let p = prev_sig(D, i)
if p >= 0 {
if tis(D, p, ".") or tis(D, p, "var") or tis(D, p, "let") or tis(D, p, "const") or tis(D, p, "return") { return true }
if D.tk_kind[p] == LT_OP and not (tis(D, p, "{") or tis(D, p, "}") or tis(D, p, ";") or tis(D, p, ")") or tis(D, p, "]")) {
# `import` after `(` never happens; `query` after `in` is not an op
return true
}
}
let n = next_sig(D, i)
if n >= 0 and D.tk_kind[n] == LT_OP {
if tis(D, n, ":") or tis(D, n, ".") or tis(D, n, "=") or tis(D, n, ")") or tis(D, n, ",") or tis(D, n, "]") { return true }
if tis(D, n, "+=") or tis(D, n, "-=") or tis(D, n, "==") or tis(D, n, "!=") { return true }
}
return false
}
# is this a documented standard-library namespace (Screen, Color, Math, …)?
function is_namespace_name(w: pointer) -> bool {
if cstr_len(w) == 0 or w[0] < 'A' or w[0] > 'Z' { return false }
docs_load()
if docs_ns_index(w) >= 0 { return true }
return ns_method_sig(w, "") != null
}
# SEM_VENDOR when a symbol lives in a package (the toolchain's or ludic_modules/)
function vendor_mod(O: Doc) -> int {
if (O == null) { return 0 }
if Text.contains(O.path, "/ludic_modules/") { return SEM_VENDOR }
let home = lsp_home()
if not (home == "") and starts_with(O.path, home + "packages/") { return SEM_VENDOR }
if not (home == "") and starts_with(O.path, home + "runtime/") { return SEM_DEFAULT_LIBRARY }
return 0
}