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:
parent
da57b20156
commit
f92d7f89c6
75 changed files with 46802 additions and 40343 deletions
253
tools/ludic-tools/lsp/docs.ludic
Normal file
253
tools/ludic-tools/lsp/docs.ludic
Normal 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
|
||||
}
|
||||
}
|
||||
215
tools/ludic-tools/lsp/imports.ludic
Normal file
215
tools/ludic-tools/lsp/imports.ludic
Normal 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)
|
||||
}
|
||||
226
tools/ludic-tools/lsp/sighelp.ludic
Normal file
226
tools/ludic-tools/lsp/sighelp.ludic
Normal 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
|
||||
}
|
||||
277
tools/ludic-tools/lsp/types.ludic
Normal file
277
tools/ludic-tools/lsp/types.ludic
Normal 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue