- 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>
226 lines
7.3 KiB
Text
226 lines
7.3 KiB
Text
# 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
|
|
}
|