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") }
}