`ludic syntax [--json] [-o FILE]` prints what `ludicc --emit-syntax` does (a line per entry, or the JSON). `ludic-dev syntax` writes, between "ludic-dev syntax: begin" / "end" lines, the keyword, type, phase and attribute tables of ludic_syntax.h, LudicVocabulary's sets (JetBrains), ludic-mode.el's lists and the language server's word tests (is_keyword_word and the rest; is_contextual_word is every word the parser does not reserve, and every declaring or modifying one), and every TextMate pattern marked "comment": "ludic-dev syntax: <group>" (shared and the VS Code copy). The grammars gain module uses port bind action reducer dispatch registry def open component prop view alias friend unsafe numbers of as from mut system; import and extern colour as declarations; the phase clause knows Overlay; the bitwise pattern matches | and ^ on their own again. `ludic-dev syntax --check` - and check-vocabulary, whose old parser comparison it replaces, and the regression suite (syntax_cases, one line per file) - fails when a written list is behind, when a grammar lacks a keyword, type or phase, when docs/language has no page for a keyword, type, phase or attribute, or when the parser (a scan of selfhost/frontend: is_id / text == words, a == / ann == attributes) tests a word or reads an attribute vocab.ludic lacks, or the reverse. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2928 lines
127 KiB
Text
2928 lines
127 KiB
Text
# lsp.ludic — the Ludic language server, written in Ludic (replaces the C
|
|
# ludic_lsp.c + ludic_index.h + ludic_workspace.h + ludic_json.h). Speaks LSP
|
|
# 3.17 over stdio: completion, hover, go-to-definition, references, rename,
|
|
# document/workspace symbols, semantic tokens, folding, signature help, inlay
|
|
# hints, formatting, and structural diagnostics — an error-tolerant model of the
|
|
# workspace that keeps parsing after a syntax error and resolves names across the
|
|
# 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
|
|
const LT_COMMENT: int = 2
|
|
const LT_ID: int = 3
|
|
const LT_KW: int = 4
|
|
const LT_TYPE: int = 5
|
|
const LT_PHASE: int = 6
|
|
const LT_BOOL: int = 7
|
|
const LT_INT: int = 8
|
|
const LT_FLOAT: int = 9
|
|
const LT_STR: int = 10
|
|
const LT_CHAR: int = 11
|
|
const LT_ANNO: int = 12
|
|
const LT_OP: int = 13
|
|
const LT_ERR: int = 14
|
|
|
|
# ---- symbol kinds ----
|
|
const LS_UNIT: int = 0
|
|
const LS_COMPONENT: int = 1
|
|
const LS_FIELD: int = 2
|
|
const LS_ARCHETYPE: int = 3
|
|
const LS_CONST: int = 4
|
|
const LS_VAR: int = 5
|
|
const LS_FN: int = 6
|
|
const LS_PARAM: int = 7
|
|
const LS_EXTERN: int = 8
|
|
const LS_SYSTEM: int = 9
|
|
const LS_UI: int = 10
|
|
const LS_WIDGET: int = 11
|
|
const LS_SCENE: int = 12
|
|
const LS_LAYER: int = 13
|
|
const LS_LOCAL: int = 14
|
|
const LS_QUERYVAR: int = 15
|
|
const LS_STATE: int = 16
|
|
const LS_IMPORT: int = 17
|
|
|
|
# ---- semantic classes ----
|
|
const SC_NONE: int = 0
|
|
const SC_KEYWORD: int = 1
|
|
const SC_TYPE: int = 2
|
|
const SC_COMPONENT: int = 3
|
|
const SC_ARCHETYPE: int = 4
|
|
const SC_SCENE: int = 5
|
|
const SC_LAYER: int = 6
|
|
const SC_UI: int = 7
|
|
const SC_WIDGET: int = 8
|
|
const SC_PROP: int = 9
|
|
const SC_FIELD: int = 10
|
|
const SC_SYSTEM: int = 11
|
|
const SC_FUNCTION: int = 12
|
|
const SC_BUILTIN: int = 13
|
|
const SC_PARAM: int = 14
|
|
const SC_VARIABLE: int = 15
|
|
const SC_CONST: int = 16
|
|
const SC_MODVAR: int = 17
|
|
const SC_PHASE: int = 18
|
|
const SC_NUMBER: int = 19
|
|
const SC_STRING: int = 20
|
|
const SC_COMMENT: int = 21
|
|
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 {
|
|
let f = file_open(path, "rb")
|
|
if (f == null) { return null }
|
|
file_seek(f, 0, 2)
|
|
let n = file_tell(f)
|
|
file_seek(f, 0, 0)
|
|
let buf = bytes(n + 1)
|
|
file_read(f, buf, n)
|
|
buf[n] = 0
|
|
file_close(f)
|
|
return buf
|
|
}
|
|
function cstr_len(s: pointer) -> int { var n = 0; while s[n] != 0 { n += 1 }; return n }
|
|
function char_is_digit(c: int) -> bool { return c >= '0' and c <= '9' }
|
|
function char_is_alpha(c: int) -> bool {
|
|
if c >= 'A' and c <= 'Z' { return true }
|
|
if c >= 'a' and c <= 'z' { return true }
|
|
return c == '_'
|
|
}
|
|
function char_is_alnum(c: int) -> bool { return char_is_alpha(c) or char_is_digit(c) }
|
|
function char_is_hex(c: int) -> bool { return char_is_digit(c) or (c >= 'a' and c <= 'f') or (c >= 'A' and c <= 'F') }
|
|
function itoa(v: int) -> pointer {
|
|
if v == 0 { let z = bytes(2); z[0] = '0'; z[1] = 0; return z }
|
|
var neg = false; var x = v
|
|
if x < 0 { neg = true; x = -x }
|
|
let tmp = bytes(16); var n = 0
|
|
while x > 0 { tmp[n] = 48 + x % 10; x /= 10; n += 1 }
|
|
var total = n
|
|
if neg { total += 1 }
|
|
let out = bytes(total + 1); var k = 0
|
|
if neg { out[0] = '-'; k = 1 }
|
|
var i = 0
|
|
while i < n { out[k + i] = tmp[n - 1 - i]; i += 1 }
|
|
out[total] = 0
|
|
return out
|
|
}
|
|
function atoi(s: pointer) -> int {
|
|
var n = 0; var i = 0; var neg = false
|
|
while s[i] == ' ' or s[i] == '\t' { i += 1 }
|
|
if s[i] == '-' { neg = true; i += 1 }
|
|
while char_is_digit(s[i]) { n = n * 10 + (s[i] - 48); i += 1 }
|
|
if neg { return -n }
|
|
return n
|
|
}
|
|
|
|
# ---- growable byte buffer ----
|
|
property Buf { data: pointer = null, len: int = 0, cap: int = 0 }
|
|
function buf_new() -> Buf { let b = new Buf; b.cap = 256; b.data = bytes(b.cap); b.len = 0; return b }
|
|
function buf_ensure(b: Buf, extra: int) -> void {
|
|
if b.len + extra + 1 <= b.cap { return }
|
|
while b.len + extra + 1 > b.cap { b.cap *= 2 }
|
|
b.data = resize(b.data, b.cap)
|
|
}
|
|
function buf_putc(b: Buf, c: int) -> void { buf_ensure(b, 1); b.data[b.len] = c; b.len += 1 }
|
|
function buf_puts(b: Buf, s: pointer) -> void { var i = 0; while s[i] != 0 { buf_putc(b, s[i]); i += 1 } }
|
|
function buf_puti(b: Buf, n: int) -> void { buf_puts(b, itoa(n)) }
|
|
function buf_addrange(b: Buf, s: pointer, a: int, e: int) -> void { var k = a; while k < e { buf_putc(b, s[k]); k += 1 } }
|
|
function buf_str(b: Buf) -> pointer { b.data[b.len] = 0; return b.data }
|
|
|
|
# ---- string helpers ----
|
|
function streq(a: pointer, b: pointer) -> bool { return (a == b) }
|
|
function concat3(a: pointer, b: pointer, c: pointer) -> pointer { return ((a + b) + c) }
|
|
function ends_with(s: pointer, suf: pointer) -> bool {
|
|
let n = cstr_len(s); let m = cstr_len(suf)
|
|
if m > n { return false }
|
|
return (s[n - m..n] == suf)
|
|
}
|
|
function starts_with(s: pointer, pre: pointer) -> bool {
|
|
let m = cstr_len(pre)
|
|
if m > cstr_len(s) { return false }
|
|
return (s[0..m] == pre)
|
|
}
|
|
|
|
# ============================================================================
|
|
# documents
|
|
# ============================================================================
|
|
property Sym {
|
|
kind: int = 0
|
|
name: pointer = null
|
|
ty: pointer = null
|
|
detail: pointer = null
|
|
doc: pointer = null
|
|
tok: int = 0
|
|
start: int = 0
|
|
end: int = 0
|
|
bstart: int = 0
|
|
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
|
|
uri: pointer = null
|
|
raw: pointer = null
|
|
text: pointer = null
|
|
is_md: int = 0
|
|
is_unit: int = 0
|
|
is_module: int = 0
|
|
unit: pointer = null
|
|
src: pointer = null
|
|
tk_kind: []int
|
|
tk_start: []int
|
|
tk_end: []int
|
|
tk_line: []int
|
|
linestart: []int
|
|
tmatch: []int
|
|
cls: []int
|
|
clsmods: []int # extra semantic modifiers per token (declaration, vendor, …)
|
|
syms: []Sym
|
|
imports: []pointer
|
|
had_diags: int = 0
|
|
isopen: int = 0
|
|
version: int = 0
|
|
cc_line: int = 0 # the compiler's last error on this file (1-based line), or 0
|
|
cc_msg: pointer = null
|
|
}
|
|
|
|
var g_docs: []Doc
|
|
var g_root: pointer = null
|
|
var g_shutdown: int = 0
|
|
# initializationOptions.compilerPath — the ludicc to run on every save when
|
|
# compilerDiagnostics is on (default: the ludicc beside this binary). null = off.
|
|
var g_compiler: pointer = null
|
|
|
|
# ---- the lexer, over a Doc ----
|
|
function is_op2(c0: int, c1: int) -> bool {
|
|
if c0 == '-' and c1 == '>' { return true }
|
|
if c1 == '=' and (c0 == '+' or c0 == '-' or c0 == '*' or c0 == '/' or c0 == '=' or c0 == '!' or c0 == '<' or c0 == '>') { return true }
|
|
if c0 == '<' and c1 == '<' { return true } # <<
|
|
if c0 == '>' and c1 == '>' { return true } # >>
|
|
if c0 == '.' and c1 == '.' { return true }
|
|
if c0 == '=' and c1 == '>' { return true }
|
|
return false
|
|
}
|
|
function is_op1(c: int) -> bool {
|
|
if c == '+' or c == '-' or c == '*' or c == '/' or c == '%' or c == '<' or c == '>' or c == '=' { return true } # + - * / % < > =
|
|
if c == '(' or c == ')' or c == '{' or c == '}' or c == '[' or c == ']' { return true } # ( ) { } [ ]
|
|
if c == ',' or c == ':' or c == '.' or c == '!' or c == '@' or c == ';' { return true } # , : . ! @ ;
|
|
return c == '&' or c == '|' or c == '^' or c == '~' # & | ^ ~
|
|
}
|
|
# the vocabulary, written by `ludic-dev syntax` from `ludicc --emit-syntax`
|
|
# ludic-dev syntax: begin - generated from `ludicc --emit-syntax` (selfhost/frontend/vocab.ludic); run `ludic-dev syntax`, do not edit
|
|
function is_type_word(w: pointer) -> bool {
|
|
if (w == "int") or (w == "long") or (w == "float") or (w == "double") or (w == "fixed") or (w == "bool") { return true }
|
|
if (w == "byte") or (w == "string") or (w == "pointer") or (w == "void") or (w == "entity") or (w == "countdown") { return true }
|
|
if (w == "words") or (w == "floats") or (w == "doubles") or (w == "fixeds") or (w == "pointers") or (w == "Vector") { return true }
|
|
if (w == "IVec2") or (w == "Rect") { return true }
|
|
return false
|
|
}
|
|
function is_phase_word(w: pointer) -> bool {
|
|
if (w == "Start") or (w == "Input") or (w == "FixedUpdate") or (w == "Update") or (w == "LateUpdate") or (w == "Render") { return true }
|
|
if (w == "Overlay") { return true }
|
|
return false
|
|
}
|
|
function is_keyword_word(w: pointer) -> bool {
|
|
if (w == "program") or (w == "module") or (w == "import") or (w == "property") or (w == "state") or (w == "event") { return true }
|
|
if (w == "action") or (w == "reducer") or (w == "enum") or (w == "model") or (w == "prefab") or (w == "scene") { return true }
|
|
if (w == "handler") or (w == "ui") or (w == "namespace") or (w == "alias") or (w == "port") or (w == "bind") { return true }
|
|
if (w == "registry") or (w == "def") or (w == "component") or (w == "view") or (w == "prop") or (w == "const") { return true }
|
|
if (w == "var") or (w == "function") or (w == "fn") or (w == "extern") or (w == "entry") or (w == "test") { return true }
|
|
if (w == "export") or (w == "internal") or (w == "friend") or (w == "uses") or (w == "open") or (w == "of") { return true }
|
|
if (w == "as") or (w == "from") or (w == "numbers") or (w == "unsafe") or (w == "mut") or (w == "phase") { return true }
|
|
if (w == "query") or (w == "on") or (w == "cancellable") or (w == "public") or (w == "layer") or (w == "start") { return true }
|
|
if (w == "shows") or (w == "lasts") or (w == "then") or (w == "loads") or (w == "system") or (w == "let") { return true }
|
|
if (w == "if") or (w == "else") or (w == "while") or (w == "for") or (w == "in") or (w == "where") { return true }
|
|
if (w == "match") or (w == "machine") or (w == "become") or (w == "return") or (w == "break") or (w == "continue") { return true }
|
|
if (w == "new") or (w == "spawn") or (w == "despawn") or (w == "enable") or (w == "disable") or (w == "attach") { return true }
|
|
if (w == "detach") or (w == "emit") or (w == "cancel") or (w == "dispatch") or (w == "try") or (w == "and") { return true }
|
|
if (w == "or") or (w == "not") { return true }
|
|
return false
|
|
}
|
|
# ludic-dev syntax: end
|
|
function is_widget_word(w: pointer) -> bool {
|
|
return (w == "panel") or (w == "col") or (w == "row") or (w == "label") or (w == "button") or (w == "image") or (w == "spacer")
|
|
}
|
|
|
|
function lex_doc(D: Doc) -> void {
|
|
let s = D.src
|
|
D.tk_kind = new []int; D.tk_start = new []int; D.tk_end = new []int; D.tk_line = new []int
|
|
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 }
|
|
if c == ' ' or c == '\t' or c == '\r' { i += 1; continue }
|
|
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 }
|
|
# `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 }
|
|
while char_is_digit(s[i]) { i += 1 }
|
|
if s[i] == '.' and char_is_digit(s[i + 1]) { i += 1; while char_is_digit(s[i]) { i += 1 }; push(D.tk_kind, LT_FLOAT); push(D.tk_start, st); push(D.tk_end, i); push(D.tk_line, line); continue }
|
|
push(D.tk_kind, LT_INT); push(D.tk_start, st); push(D.tk_end, i); push(D.tk_line, line); continue
|
|
}
|
|
if c == '@' and (char_is_alpha(s[i + 1]) or s[i + 1] == '_') { i += 1; while char_is_alnum(s[i]) { i += 1 }; push(D.tk_kind, LT_ANNO); push(D.tk_start, st); push(D.tk_end, i); push(D.tk_line, line); continue }
|
|
if char_is_alpha(c) {
|
|
while char_is_alnum(s[i]) { i += 1 }
|
|
let w = s[st..i]
|
|
var k = LT_ID
|
|
if (w == "true") or (w == "false") or (w == "null") { k = LT_BOOL }
|
|
else { if is_type_word(w) { k = LT_TYPE }
|
|
else { if is_phase_word(w) { k = LT_PHASE }
|
|
else { if is_keyword_word(w) { k = LT_KW } } } }
|
|
push(D.tk_kind, k); push(D.tk_start, st); push(D.tk_end, i); push(D.tk_line, line); continue
|
|
}
|
|
if is_op2(c, s[i + 1]) { push(D.tk_kind, LT_OP); push(D.tk_start, i); push(D.tk_end, i + 2); push(D.tk_line, line); i += 2; continue }
|
|
if is_op1(c) { push(D.tk_kind, LT_OP); push(D.tk_start, i); push(D.tk_end, i + 1); push(D.tk_line, line); i += 1; continue }
|
|
var ln = 1
|
|
if c >= 240 { ln = 4 } else { if c >= 224 { ln = 3 } else { if c >= 128 { ln = 2 } } }
|
|
var kk = 1
|
|
while kk < ln { if s[i + kk] == 0 or (s[i + kk] & 192) != 128 { ln = kk }; kk += 1 }
|
|
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) }
|
|
function tlen(D: Doc, i: int) -> int { return D.tk_end[i] - D.tk_start[i] }
|
|
function ttext(D: Doc, i: int) -> pointer { return D.src[D.tk_start[i]..D.tk_end[i]] }
|
|
function tis(D: Doc, i: int, s: pointer) -> bool { if i < 0 or i >= ntok(D) { return false }; return (ttext(D, i) == s) }
|
|
function next_sig(D: Doc, i: int) -> int { var j = i + 1; while j < ntok(D) { let k = D.tk_kind[j]; if k != LT_NL and k != LT_COMMENT { return j }; j += 1 }; return -1 }
|
|
function prev_sig(D: Doc, i: int) -> int { var j = i - 1; while j >= 0 { let k = D.tk_kind[j]; if k != LT_NL and k != LT_COMMENT { return j }; j -= 1 }; return -1 }
|
|
|
|
# ---- positions (UTF-16 for LSP) ----
|
|
function utf16_len(s: pointer, a: int, nbytes: int) -> int {
|
|
var u = 0; var i = 0
|
|
while i < nbytes {
|
|
let c = s[a + i]
|
|
if c < 128 { i += 1; u += 1 }
|
|
else { if c < 224 { i += 2; u += 1 }
|
|
else { if c < 240 { i += 3; u += 1 }
|
|
else { i += 4; u += 2 } } }
|
|
}
|
|
return u
|
|
}
|
|
function line_of(D: Doc, off: int) -> int {
|
|
var lo = 0; var hi = len(D.linestart) - 1
|
|
while lo < hi { let mid = (lo + hi + 1) / 2; if D.linestart[mid] <= off { lo = mid } else { hi = mid - 1 } }
|
|
return lo
|
|
}
|
|
function col_of(D: Doc, off: int) -> int { let l = line_of(D, off); return utf16_len(D.text, D.linestart[l], off - D.linestart[l]) }
|
|
function offset_of(D: Doc, line: int, ch: int) -> int {
|
|
if line < 0 { return 0 }
|
|
if line >= len(D.linestart) { return cstr_len(D.text) }
|
|
var off = D.linestart[line]
|
|
var endo = cstr_len(D.text)
|
|
if line + 1 < len(D.linestart) { endo = D.linestart[line + 1] }
|
|
var u = 0
|
|
while off < endo and u < ch {
|
|
let c = D.text[off]
|
|
if c < 128 { off += 1; u += 1 }
|
|
else { if c < 224 { off += 2; u += 1 }
|
|
else { if c < 240 { off += 3; u += 1 }
|
|
else { off += 4; u += 2 } } }
|
|
}
|
|
return off
|
|
}
|
|
function tok_at(D: Doc, off: int) -> int {
|
|
var touching = -1
|
|
var i = 0
|
|
let N = ntok(D)
|
|
while i < N {
|
|
let k = D.tk_kind[i]
|
|
if k != LT_NL and k != LT_EOF {
|
|
if off >= D.tk_start[i] and off < D.tk_end[i] { return i }
|
|
if off == D.tk_end[i] and touching < 0 { touching = i }
|
|
if D.tk_start[i] > off { return touching }
|
|
}
|
|
i += 1
|
|
}
|
|
return touching
|
|
}
|
|
|
|
# ---- brace matching ----
|
|
function match_braces(D: Doc) -> void {
|
|
let N = ntok(D)
|
|
D.tmatch = new []int
|
|
var i = 0
|
|
while i < N { push(D.tmatch, -1); i += 1 }
|
|
let stack = words(1200)
|
|
var top = 0
|
|
i = 0
|
|
while i < N {
|
|
if D.tk_kind[i] == LT_OP and tlen(D, i) == 1 {
|
|
let c = D.src[D.tk_start[i]]
|
|
if c == '{' or c == '(' or c == '[' { if top < 1200 { stack[top] = i; top += 1 } }
|
|
else { if c == '}' or c == ')' or c == ']' { if top > 0 { top -= 1; let o = stack[top]; D.tmatch[o] = i; D.tmatch[i] = o } } }
|
|
}
|
|
i += 1
|
|
}
|
|
}
|
|
|
|
# ---- doc comment: the comment block directly above a declaration ----
|
|
function collect_doc(D: Doc, decl_tok: int) -> pointer {
|
|
var line = D.tk_line[decl_tok]
|
|
var first = -1
|
|
var i = decl_tok - 1
|
|
while i >= 0 {
|
|
let k = D.tk_kind[i]
|
|
if k == LT_NL { i -= 1; continue }
|
|
if k != LT_COMMENT { break }
|
|
if D.tk_line[i] >= line { break }
|
|
let p = prev_sig(D, i)
|
|
if p >= 0 and D.tk_line[p] == D.tk_line[i] { break }
|
|
if D.tk_line[i] < line - 1 and first >= 0 { break }
|
|
if first >= 0 and D.tk_line[first] != D.tk_line[i] + 1 { break }
|
|
first = i; line = D.tk_line[i]
|
|
i -= 1
|
|
}
|
|
if first < 0 { return "" }
|
|
let out = buf_new()
|
|
i = first
|
|
while i < decl_tok {
|
|
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 }
|
|
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
|
|
}
|
|
return buf_str(out)
|
|
}
|
|
|
|
# ============================================================================
|
|
# the shallow parser — records declarations and bindings with their spans
|
|
# ============================================================================
|
|
var pD: Doc = null
|
|
var pi: int = 0
|
|
|
|
function pk() -> int { if pi < ntok(pD) { return pD.tk_kind[pi] }; return LT_EOF }
|
|
function pis(s: pointer) -> bool { return tis(pD, pi, s) }
|
|
function pskipnl() -> void { while pi < ntok(pD) and (pk() == LT_NL or pk() == LT_COMMENT) { pi += 1 } }
|
|
function padv() -> void { if pi < ntok(pD) { pi += 1 }; pskipnl() }
|
|
function pword() -> pointer { if pi < ntok(pD) { return ttext(pD, pi) }; return "" }
|
|
function pname() -> int {
|
|
let k = pk()
|
|
if k == LT_ID or k == LT_TYPE or k == LT_PHASE or k == LT_KW or k == LT_BOOL { let t = pi; padv(); return t }
|
|
return -1
|
|
}
|
|
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.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)
|
|
return len(pD.syms) - 1
|
|
}
|
|
function cur_match() -> int { if pi < ntok(pD) { return pD.tmatch[pi] }; return -1 }
|
|
function skip_group() -> void { let m = cur_match(); if m < 0 { padv(); return }; pi = m + 1; pskipnl() }
|
|
|
|
# `name: Type` pairs inside ( ) — record params, build the signature into sig
|
|
function lp_params(owner: int, kind: int, sig: Buf) -> void {
|
|
if not pis("(") { return }
|
|
let close = cur_match()
|
|
buf_putc(sig, '(') # '('
|
|
padv()
|
|
var first = true
|
|
while pi < ntok(pD) and (close < 0 or pi < close) {
|
|
if pis(")") { break }
|
|
if pis(",") { padv(); continue }
|
|
let nt = pname()
|
|
if nt < 0 { padv(); continue }
|
|
var ty = ""
|
|
var mu = ""
|
|
if pis(":") {
|
|
padv()
|
|
if pis("mut") { padv(); mu = "mut " } # a state it may change
|
|
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(": ", mu, 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) }
|
|
first = false
|
|
}
|
|
if close >= 0 { pi = close + 1 }
|
|
pskipnl()
|
|
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
|
|
if not pis("(") { return 0 }
|
|
let close = cur_match()
|
|
padv()
|
|
while pi < ntok(pD) and (close < 0 or pi < close) {
|
|
if pis(")") { break }
|
|
if pis(",") { padv(); continue }
|
|
let nt = pname()
|
|
if nt < 0 { padv(); continue }
|
|
push(out, nt); n += 1
|
|
}
|
|
if close >= 0 { pi = close + 1 }
|
|
pskipnl()
|
|
return n
|
|
}
|
|
|
|
# [Pos, Vel, {Enemy}] — bind listed components to vars in order (skip {Tag})
|
|
function lp_query_terms(owner: int, vars: []int, nvars: int, scope_start: int, scope_end: int) -> void {
|
|
if not pis("[") { return }
|
|
let close = cur_match()
|
|
padv()
|
|
var bind = 0
|
|
while pi < ntok(pD) and (close < 0 or pi < close) {
|
|
if pis("]") { break }
|
|
if pis(",") { padv(); continue }
|
|
if pis("{") { let cb = cur_match(); padv(); let nt = pname(); if cb >= 0 { pi = cb + 1 } else { padv() }; pskipnl(); continue }
|
|
let nt = pname()
|
|
if nt < 0 { padv(); continue }
|
|
if bind < nvars and vars[bind] >= 0 {
|
|
let sidx = sym_add(LS_QUERYVAR, vars[bind], owner)
|
|
pD.syms[sidx].ty = ttext(pD, nt)
|
|
pD.syms[sidx].bstart = scope_start; pD.syms[sidx].bend = scope_end
|
|
pD.syms[sidx].detail = concat3(pD.syms[sidx].name, ": ", concat3(pD.syms[sidx].ty, " (query binding)", ""))
|
|
}
|
|
bind += 1
|
|
}
|
|
if close >= 0 { pi = close + 1 }
|
|
pskipnl()
|
|
if pis("where") { padv() }
|
|
}
|
|
|
|
function lp_block(owner: int, scope_end: int) -> void {
|
|
if not pis("{") { return }
|
|
let close = cur_match()
|
|
var endo = scope_end
|
|
if close >= 0 { endo = pD.tk_start[close] }
|
|
padv()
|
|
while pi < ntok(pD) and pk() != LT_EOF {
|
|
if close >= 0 and pi >= close { break }
|
|
if pis("}") { break }
|
|
if pis("{") { lp_block(owner, endo); continue }
|
|
let before = pi
|
|
lp_stmt(owner, endo)
|
|
if pi == before { padv() }
|
|
}
|
|
if close >= 0 { pi = close + 1 } else { padv() }
|
|
pskipnl()
|
|
}
|
|
function lp_stmt(owner: int, scope_end: int) -> void {
|
|
if pis("let") or pis("var") {
|
|
let is_var = pis("var")
|
|
padv(); let nt = pname(); if nt < 0 { return }
|
|
var ty = ""
|
|
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
|
|
}
|
|
if pis("for") {
|
|
padv()
|
|
if pis("(") {
|
|
let vars = new []int
|
|
let nv = lp_varlist(vars)
|
|
if pis("in") { padv() }
|
|
if pis("query") { padv() }
|
|
var here = pD.tk_start[ntok(pD) - 1]
|
|
if pi < ntok(pD) { here = pD.tk_start[pi] }
|
|
lp_query_terms(owner, vars, nv, here, scope_end)
|
|
return
|
|
}
|
|
let nt = pname()
|
|
if nt >= 0 {
|
|
let sidx = sym_add(LS_LOCAL, nt, owner)
|
|
pD.syms[sidx].ty = "int"
|
|
pD.syms[sidx].bstart = pD.tk_start[nt]; pD.syms[sidx].bend = scope_end
|
|
}
|
|
return
|
|
}
|
|
if pis("state") { padv(); let nt = pname(); if nt >= 0 { let x = sym_add(LS_STATE, nt, owner) }; return }
|
|
padv()
|
|
}
|
|
|
|
# panel id=Root w=288 { … } — each id= mints a UI_<Name> handle
|
|
function lp_widget(owner: int, uisym: int) -> void {
|
|
let type_tok = pname()
|
|
if type_tok < 0 { return }
|
|
while pi < ntok(pD) and pk() == LT_ID {
|
|
let ns = next_sig(pD, pi)
|
|
var checkidx = ns
|
|
if checkidx < 0 { checkidx = pi }
|
|
if not tis(pD, checkidx, "=") { break }
|
|
let kb = ttext(pD, pi)
|
|
padv()
|
|
if pis("=") { padv() }
|
|
if kb == "id" {
|
|
let nt = pname()
|
|
if nt >= 0 {
|
|
let sidx = sym_add(LS_WIDGET, nt, uisym)
|
|
pD.syms[sidx].name = concat3("UI_", ttext(pD, nt), "")
|
|
pD.syms[sidx].detail = concat3(ttext(pD, type_tok), " widget handle", "")
|
|
pD.syms[sidx].ty = "int"
|
|
}
|
|
} else {
|
|
while pi < ntok(pD) and pk() != LT_EOF and not pis("{") and not pis("}") {
|
|
if pis("(") or pis("[") { skip_group(); continue }
|
|
let nx = next_sig(pD, pi)
|
|
if pk() == LT_ID and nx >= 0 and tis(pD, nx, "=") { break }
|
|
padv()
|
|
}
|
|
}
|
|
}
|
|
if pis("{") {
|
|
let close = cur_match()
|
|
padv()
|
|
while pi < ntok(pD) and pk() != LT_EOF and not pis("}") {
|
|
if close >= 0 and pi >= close { break }
|
|
let before = pi
|
|
lp_widget(owner, uisym)
|
|
if pi == before { padv() }
|
|
}
|
|
if close >= 0 { pi = close + 1 } else { padv() }
|
|
pskipnl()
|
|
}
|
|
}
|
|
|
|
# one top-level declaration
|
|
function lp_decl(parent: int) -> void {
|
|
let w = pword()
|
|
let decl_tok = pi
|
|
|
|
if w == "import" {
|
|
padv()
|
|
if pk() == LT_STR {
|
|
let t = pi
|
|
var n = tlen(pD, t) - 2
|
|
if n < 0 { n = 0 }
|
|
let rel = pD.src[pD.tk_start[t] + 1..pD.tk_start[t] + 1 + n]
|
|
push(pD.imports, rel)
|
|
let sidx = sym_add(LS_IMPORT, t, parent)
|
|
pD.syms[sidx].name = rel
|
|
padv()
|
|
}
|
|
return
|
|
}
|
|
var wd = w
|
|
if w == "export" and tis(pD, pi + 1, "state") { padv(); wd = "state" }
|
|
if (wd == "property") or (wd == "model") or (wd == "state") { # a state: a record with one instance
|
|
let is_comp = (wd == "property") or (wd == "state")
|
|
padv(); let nt = pname(); if nt < 0 { return }
|
|
var kind = LS_ARCHETYPE
|
|
if is_comp { kind = LS_COMPONENT }
|
|
let sidx = sym_add(kind, nt, parent)
|
|
pD.syms[sidx].doc = collect_doc(pD, decl_tok)
|
|
while pk() == LT_ANNO { padv() }
|
|
let det = buf_new()
|
|
buf_puts(det, w); buf_putc(det, ' '); buf_puts(det, pD.syms[sidx].name); buf_puts(det, " {")
|
|
if not pis("{") { pD.syms[sidx].detail = buf_str(det); return }
|
|
let close = cur_match()
|
|
pD.syms[sidx].bstart = pD.tk_start[pi]
|
|
if close >= 0 { pD.syms[sidx].bend = pD.tk_end[close] } else { pD.syms[sidx].bend = cstr_len(pD.text) }
|
|
padv()
|
|
var first = true
|
|
while pi < ntok(pD) and pk() != LT_EOF and not pis("}") {
|
|
if close >= 0 and pi >= close { break }
|
|
if pis(",") { padv(); continue }
|
|
while pk() == LT_ANNO { padv() } # @Sync / @Computed on a field
|
|
let ft = pname(); if ft < 0 { padv(); continue }
|
|
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, " }")
|
|
pD.syms[sidx].detail = buf_str(det)
|
|
if close >= 0 { pi = close + 1 } else { padv() }
|
|
pskipnl()
|
|
return
|
|
}
|
|
if (w == "const") or (w == "var") {
|
|
var kind = LS_VAR
|
|
if w == "const" { kind = LS_CONST }
|
|
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(); 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()
|
|
return
|
|
}
|
|
if w == "extern" {
|
|
padv(); if pis("function") { padv() }
|
|
let nt = pname(); if nt < 0 { return }
|
|
let sidx = sym_add(LS_EXTERN, nt, parent)
|
|
pD.syms[sidx].doc = collect_doc(pD, decl_tok)
|
|
let sig = buf_new(); buf_puts(sig, "extern function "); buf_puts(sig, pD.syms[sidx].name)
|
|
lp_params(sidx, LS_PARAM, sig)
|
|
if pis("->") { padv(); let tt = pname(); if tt >= 0 { pD.syms[sidx].ty = ttext(pD, tt); buf_puts(sig, " -> "); buf_puts(sig, pD.syms[sidx].ty) } }
|
|
pD.syms[sidx].detail = buf_str(sig)
|
|
while pi < ntok(pD) and pk() != LT_NL and pk() != LT_EOF { pi += 1 }
|
|
pskipnl()
|
|
return
|
|
}
|
|
if (w == "function") or (w == "pure") or (w == "export") {
|
|
let exported = (w == "export")
|
|
if (w == "pure") or exported { padv(); if pis("pure") { padv() } }
|
|
if pis("function") { padv() }
|
|
let nt = pname(); if nt < 0 { return }
|
|
let sidx = sym_add(LS_FN, nt, parent)
|
|
if exported { pD.syms[sidx].exported = 1 }
|
|
pD.syms[sidx].doc = collect_doc(pD, decl_tok)
|
|
let sig = buf_new(); if exported { buf_puts(sig, "export ") }; buf_puts(sig, "fn "); buf_puts(sig, pD.syms[sidx].name)
|
|
lp_params(sidx, LS_PARAM, sig)
|
|
if pis("->") { padv(); let tt = pname(); if tt >= 0 { pD.syms[sidx].ty = ttext(pD, tt) } }
|
|
buf_puts(sig, " -> "); if pD.syms[sidx].ty == "" { buf_puts(sig, "void") } else { buf_puts(sig, pD.syms[sidx].ty) }
|
|
pD.syms[sidx].detail = buf_str(sig)
|
|
while pis("requires") or pis("ensures") or pis("invariant") or pis("effects") {
|
|
padv()
|
|
while pi < ntok(pD) and pk() != LT_NL and pk() != LT_EOF and not pis("{") { if pis("(") or pis("[") { skip_group() } else { pi += 1 } }
|
|
pskipnl()
|
|
}
|
|
if pis("{") {
|
|
let close = cur_match()
|
|
pD.syms[sidx].bstart = pD.tk_start[pi]
|
|
if close >= 0 { pD.syms[sidx].bend = pD.tk_end[close] } else { pD.syms[sidx].bend = cstr_len(pD.text) }
|
|
var q = 0
|
|
while q < len(pD.syms) { if pD.syms[q].parent == sidx and pD.syms[q].kind == LS_PARAM { pD.syms[q].bstart = pD.syms[sidx].bstart; pD.syms[q].bend = pD.syms[sidx].bend }; q += 1 }
|
|
lp_block(sidx, pD.syms[sidx].bend)
|
|
}
|
|
return
|
|
}
|
|
if w == "entry" {
|
|
padv()
|
|
let first_param = len(pD.syms)
|
|
if pis("(") { lp_params(parent, LS_PARAM, buf_new()) } # entry (h: mut Hiker): its states
|
|
if pis("{") {
|
|
let close = cur_match()
|
|
var endo = cstr_len(pD.text)
|
|
if close >= 0 { endo = pD.tk_end[close] }
|
|
var q = first_param
|
|
while q < len(pD.syms) { if pD.syms[q].kind == LS_PARAM { pD.syms[q].bstart = pD.tk_start[pi]; pD.syms[q].bend = endo }; q += 1 }
|
|
lp_block(parent, endo)
|
|
}
|
|
return
|
|
}
|
|
if (w == "handler") or (w == "edge") {
|
|
if w == "edge" { padv(); if not pis("handler") { return } }
|
|
padv()
|
|
let nt = pname(); if nt < 0 { return }
|
|
let sidx = sym_add(LS_SYSTEM, nt, parent)
|
|
pD.syms[sidx].doc = collect_doc(pD, decl_tok)
|
|
pD.syms[sidx].ty = "Update"
|
|
if pis("(") { lp_params(sidx, LS_PARAM, buf_new()) } # handler Draw(h: Hiker): its states
|
|
while pk() == LT_ANNO { padv() }
|
|
let vars = new []int
|
|
var nv = 0
|
|
var have_query = false
|
|
while true {
|
|
if pis("phase") { padv(); let tt = pname(); if tt >= 0 { pD.syms[sidx].ty = ttext(pD, tt) } }
|
|
else { if pis("query") { padv(); have_query = true; nv = lp_varlist(vars); break }
|
|
else { if pis("reads") or pis("writes") or pis("uses") or pis("effects") { padv(); if pis("[") { skip_group() } }
|
|
else { if pis("needs") { padv(); let x = pname(); if pis("[") { skip_group() } }
|
|
else { break } } } }
|
|
pskipnl()
|
|
}
|
|
pD.syms[sidx].detail = concat3("system ", pD.syms[sidx].name, concat3(" phase ", pD.syms[sidx].ty, ""))
|
|
if have_query {
|
|
let save = pi
|
|
var scan = pi
|
|
while scan < ntok(pD) and not tis(pD, scan, "{") and pD.tk_kind[scan] != LT_EOF { scan += 1 }
|
|
var cl = -1
|
|
if scan < ntok(pD) { cl = pD.tmatch[scan] }
|
|
var bs = 0
|
|
if scan < ntok(pD) { bs = pD.tk_start[scan] }
|
|
var be = cstr_len(pD.text)
|
|
if cl >= 0 { be = pD.tk_end[cl] }
|
|
pi = save
|
|
lp_query_terms(sidx, vars, nv, bs, be)
|
|
}
|
|
if pis("{") {
|
|
let close = cur_match()
|
|
pD.syms[sidx].bstart = pD.tk_start[pi]
|
|
if close >= 0 { pD.syms[sidx].bend = pD.tk_end[close] } else { pD.syms[sidx].bend = cstr_len(pD.text) }
|
|
var q = 0
|
|
while q < len(pD.syms) { if pD.syms[q].parent == sidx and pD.syms[q].kind == LS_PARAM { pD.syms[q].bstart = pD.syms[sidx].bstart; pD.syms[q].bend = pD.syms[sidx].bend }; q += 1 }
|
|
lp_block(sidx, pD.syms[sidx].bend)
|
|
}
|
|
return
|
|
}
|
|
if w == "ui" {
|
|
padv(); let nt = pname(); if nt < 0 { return }
|
|
let sidx = sym_add(LS_UI, nt, parent)
|
|
pD.syms[sidx].doc = collect_doc(pD, decl_tok)
|
|
pD.syms[sidx].detail = concat3("ui ", pD.syms[sidx].name, "")
|
|
let h = sym_add(LS_WIDGET, nt, sidx)
|
|
pD.syms[h].name = concat3("UI_", pD.syms[sidx].name, "")
|
|
pD.syms[h].ty = "int"
|
|
pD.syms[h].detail = concat3("root handle of ui ", pD.syms[sidx].name, "")
|
|
if pis("{") {
|
|
let close = cur_match()
|
|
pD.syms[sidx].bstart = pD.tk_start[pi]
|
|
if close >= 0 { pD.syms[sidx].bend = pD.tk_end[close] } else { pD.syms[sidx].bend = cstr_len(pD.text) }
|
|
padv()
|
|
while pi < ntok(pD) and pk() != LT_EOF and not pis("}") {
|
|
if close >= 0 and pi >= close { break }
|
|
let before = pi
|
|
lp_widget(sidx, sidx)
|
|
if pi == before { padv() }
|
|
}
|
|
if close >= 0 { pi = close + 1 } else { padv() }
|
|
pskipnl()
|
|
}
|
|
return
|
|
}
|
|
if w == "scene" {
|
|
padv(); let nt = pname(); if nt < 0 { return }
|
|
let sidx = sym_add(LS_SCENE, nt, parent)
|
|
pD.syms[sidx].doc = collect_doc(pD, decl_tok)
|
|
var startmod = ""
|
|
if pis("start") { startmod = " start"; padv() }
|
|
pD.syms[sidx].detail = concat3("scene ", pD.syms[sidx].name, startmod)
|
|
if pis("{") {
|
|
let close = cur_match()
|
|
pD.syms[sidx].bstart = pD.tk_start[pi]
|
|
if close >= 0 { pD.syms[sidx].bend = pD.tk_end[close] } else { pD.syms[sidx].bend = cstr_len(pD.text) }
|
|
padv()
|
|
while pi < ntok(pD) and pk() != LT_EOF and not pis("}") {
|
|
if close >= 0 and pi >= close { break }
|
|
let before = pi
|
|
if pis("on") { padv(); let x = pname(); lp_block(sidx, pD.syms[sidx].bend) }
|
|
else { if pis("layer") {
|
|
padv(); let lt = pname()
|
|
var lsidx = -1
|
|
if lt >= 0 { lsidx = sym_add(LS_LAYER, lt, sidx); pD.syms[lsidx].detail = concat3("layer ", pD.syms[lsidx].name, concat3(" of scene ", pD.syms[sidx].name, "")) }
|
|
if pis("{") {
|
|
let lc = cur_match()
|
|
if lsidx >= 0 { pD.syms[lsidx].bstart = pD.tk_start[pi]; if lc >= 0 { pD.syms[lsidx].bend = pD.tk_end[lc] } else { pD.syms[lsidx].bend = pD.syms[sidx].bend } }
|
|
padv()
|
|
while pi < ntok(pD) and pk() != LT_EOF and not pis("}") {
|
|
if lc >= 0 and pi >= lc { break }
|
|
let b2 = pi
|
|
var lo = sidx
|
|
if lsidx >= 0 { lo = lsidx }
|
|
lp_decl(lo)
|
|
if pi == b2 { padv() }
|
|
}
|
|
if lc >= 0 { pi = lc + 1 } else { padv() }
|
|
pskipnl()
|
|
}
|
|
} else { padv() } }
|
|
if pi == before { padv() }
|
|
}
|
|
if close >= 0 { pi = close + 1 } else { padv() }
|
|
pskipnl()
|
|
}
|
|
return
|
|
}
|
|
padv()
|
|
}
|
|
|
|
function parse_doc(D: Doc) -> void {
|
|
D.syms = new []Sym
|
|
D.imports = new []pointer
|
|
D.is_unit = 0; D.is_module = 0; D.unit = ""
|
|
pD = D; pi = 0
|
|
pskipnl()
|
|
while pis("import") { lp_decl(-1); pskipnl() }
|
|
if pis("program") {
|
|
D.is_module = 1; D.is_unit = 1
|
|
padv()
|
|
let nt = pname()
|
|
var up = -1
|
|
if nt >= 0 {
|
|
up = sym_add(LS_UNIT, nt, -1)
|
|
D.unit = pD.syms[up].name
|
|
pD.syms[up].detail = concat3("program ", D.unit, "")
|
|
}
|
|
if pis("{") {
|
|
let close = cur_match()
|
|
if up >= 0 { pD.syms[up].bstart = pD.tk_start[pi]; if close >= 0 { pD.syms[up].bend = pD.tk_end[close] } else { pD.syms[up].bend = cstr_len(pD.text) } }
|
|
padv()
|
|
while pi < ntok(pD) and pk() != LT_EOF and not pis("}") {
|
|
if close >= 0 and pi >= close { break }
|
|
let before = pi; lp_decl(up)
|
|
if pi == before { padv() }
|
|
}
|
|
}
|
|
return
|
|
}
|
|
while pi < ntok(pD) and pk() != LT_EOF {
|
|
let before = pi; lp_decl(-1)
|
|
if pi == before { padv() }
|
|
}
|
|
}
|
|
|
|
# ============================================================================
|
|
# workspace: many documents, one project
|
|
# ============================================================================
|
|
function is_local_kind(k: int) -> bool { return k == LS_LOCAL or k == LS_PARAM or k == LS_QUERYVAR or k == LS_STATE }
|
|
function is_toplevel_kind(k: int) -> bool {
|
|
return k == LS_COMPONENT or k == LS_ARCHETYPE or k == LS_CONST or k == LS_VAR or k == LS_FN or k == LS_EXTERN or k == LS_SYSTEM or k == LS_UI or k == LS_WIDGET or k == LS_SCENE or k == LS_LAYER or k == LS_UNIT
|
|
}
|
|
function is_outline_kind(k: int) -> bool {
|
|
return k == LS_UNIT or k == LS_COMPONENT or k == LS_FIELD or k == LS_ARCHETYPE or k == LS_CONST or k == LS_VAR or k == LS_FN or k == LS_EXTERN or k == LS_SYSTEM or k == LS_UI or k == LS_WIDGET or k == LS_SCENE or k == LS_LAYER
|
|
}
|
|
|
|
# ---- paths & URIs ----
|
|
function hexval(c: int) -> int {
|
|
if c >= '0' and c <= '9' { return c - 48 }
|
|
let l = c | 32
|
|
if l >= 97 and l <= 102 { return l - 97 + 10 }
|
|
return -1
|
|
}
|
|
function uri_to_path(uri: pointer) -> pointer {
|
|
var s = uri
|
|
if starts_with(uri, "file://") { s = uri[7..cstr_len(uri)] }
|
|
let n = cstr_len(s)
|
|
let out = bytes(n + 1)
|
|
var j = 0; var i = 0
|
|
while i < n {
|
|
if s[i] == '%' and hexval(s[i + 1]) >= 0 and hexval(s[i + 2]) >= 0 { out[j] = hexval(s[i + 1]) * 16 + hexval(s[i + 2]); j += 1; i += 3 }
|
|
else { out[j] = s[i]; j += 1; i += 1 }
|
|
}
|
|
out[j] = 0
|
|
return out
|
|
}
|
|
function uri_safe(c: int) -> bool {
|
|
if char_is_alnum(c) { return true }
|
|
return c == '-' or c == '.' or c == '~' or c == '/' # - . ~ /
|
|
}
|
|
function hexch(n: int) -> int { if n < 10 { return 48 + n }; return 55 + n }
|
|
function path_to_uri(path: pointer) -> pointer {
|
|
let out = buf_new()
|
|
buf_puts(out, "file://")
|
|
var i = 0
|
|
while path[i] != 0 {
|
|
let c = path[i]
|
|
if uri_safe(c) { buf_putc(out, c) }
|
|
else { buf_putc(out, '%'); buf_putc(out, hexch((c & 255) / 16)); buf_putc(out, hexch((c & 255) % 16)) }
|
|
i += 1
|
|
}
|
|
return buf_str(out)
|
|
}
|
|
function dirname(p: pointer) -> pointer {
|
|
var last = -1
|
|
var i = 0
|
|
while p[i] != 0 { if p[i] == '/' { last = i }; i += 1 }
|
|
if last < 0 { return "." }
|
|
if last == 0 { return "/" }
|
|
return p[0..last]
|
|
}
|
|
function normpath(p: pointer) -> pointer {
|
|
let n = cstr_len(p)
|
|
let abs = (n > 0 and p[0] == '/')
|
|
let list = new []pointer
|
|
var cnt = 0
|
|
var i = 0
|
|
while i < n {
|
|
while i < n and p[i] == '/' { i += 1 }
|
|
let st = i
|
|
while i < n and p[i] != '/' { i += 1 }
|
|
if i > st {
|
|
let c = p[st..i]
|
|
if (c == ".") { }
|
|
else { if (c == "..") {
|
|
if cnt > 0 and not (list[cnt - 1] == "..") { cnt -= 1 }
|
|
else { if not abs { if cnt < len(list) { list[cnt] = c } else { push(list, c) }; cnt += 1 } }
|
|
} else {
|
|
if cnt < len(list) { list[cnt] = c } else { push(list, c) }; cnt += 1
|
|
} }
|
|
}
|
|
}
|
|
if cnt == 0 { if abs { return "/" }; return "." }
|
|
let out = buf_new()
|
|
if abs { buf_putc(out, '/') }
|
|
var k = 0
|
|
while k < cnt { if k > 0 { buf_putc(out, '/') }; buf_puts(out, list[k]); k += 1 }
|
|
return buf_str(out)
|
|
}
|
|
function join_path(dir: pointer, rel: pointer) -> pointer {
|
|
if rel[0] == '/' { return normpath(rel) }
|
|
return normpath(concat3(dir, "/", rel))
|
|
}
|
|
|
|
# ---- markdown scrub: blank everything outside a ```ludic fence ----
|
|
function word_ludic_at(s: pointer, at: int) -> bool {
|
|
return ((s[at] == 'l' or s[at] == 'L')) and ((s[at + 1] == 'u' or s[at + 1] == 'U')) and ((s[at + 2] == 'd' or s[at + 2] == 'D')) and ((s[at + 3] == 'i' or s[at + 3] == 'I')) and ((s[at + 4] == 'c' or s[at + 4] == 'C'))
|
|
}
|
|
function scrub_markdown(src: pointer) -> pointer {
|
|
let n = cstr_len(src)
|
|
let out = bytes(n + 1)
|
|
var c0 = 0
|
|
while c0 < n { out[c0] = src[c0]; c0 += 1 }
|
|
out[n] = 0
|
|
var i = 0
|
|
while i < n {
|
|
let ls = i
|
|
var le = i
|
|
while le < n and src[le] != '\n' { le += 1 }
|
|
var ind = 0
|
|
while ls + ind < le and src[ls + ind] == ' ' { ind += 1 }
|
|
let j = ls + ind
|
|
var m = 0
|
|
if j < le { m = src[j] }
|
|
var run = 0
|
|
while j + run < le and src[j + run] == m { run += 1 }
|
|
let fenced = (m == '`' or m == '~') and run >= 3
|
|
let info = j + run
|
|
var is_ludic = false
|
|
if fenced {
|
|
var kk = info
|
|
while kk < le and (src[kk] == ' ' or src[kk] == '\t') { kk += 1 }
|
|
if le - kk >= 5 { if word_ludic_at(src, kk) and (le - kk == 5 or src[kk + 5] == ' ' or src[kk + 5] == '\t' or src[kk + 5] == '\r') { is_ludic = true } }
|
|
}
|
|
var b = ls
|
|
while b < le { out[b] = ' '; b += 1 }
|
|
i = le + 1; if le >= n { i = n }
|
|
if not fenced { continue }
|
|
while i < n {
|
|
let bs = i
|
|
var be = i
|
|
while be < n and src[be] != '\n' { be += 1 }
|
|
var bi = 0
|
|
while bs + bi < be and src[bs + bi] == ' ' { bi += 1 }
|
|
let cj = bs + bi
|
|
var crun = 0
|
|
while cj + crun < be and src[cj + crun] == m { crun += 1 }
|
|
var only = true
|
|
var k2 = cj + crun
|
|
while k2 < be { if src[k2] != ' ' and src[k2] != '\r' { only = false; break }; k2 += 1 }
|
|
if crun >= run and only { var bb = bs; while bb < be { out[bb] = ' '; bb += 1 }; i = be + 1; if be >= n { i = n }; break }
|
|
if not is_ludic { var bb = bs; while bb < be { out[bb] = ' '; bb += 1 } }
|
|
i = be + 1; if be >= n { i = n }
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
# ---- documents ----
|
|
function set_text(D: Doc, raw: pointer) -> void {
|
|
D.raw = raw
|
|
if D.is_md == 1 { D.text = scrub_markdown(raw) } else { D.text = raw }
|
|
D.src = D.text
|
|
}
|
|
function reindex(D: Doc) -> void {
|
|
lex_doc(D)
|
|
match_braces(D)
|
|
parse_doc(D)
|
|
D.cls = new []int
|
|
var i = 0
|
|
let N = ntok(D)
|
|
while i < N { push(D.cls, SC_NONE); i += 1 }
|
|
}
|
|
function new_doc(path: pointer, text: pointer) -> Doc {
|
|
let D = new Doc
|
|
D.path = normpath(path)
|
|
D.uri = path_to_uri(D.path)
|
|
D.is_md = 0
|
|
if ends_with(D.path, ".md") or ends_with(D.path, ".markdown") { D.is_md = 1 }
|
|
D.syms = new []Sym; D.imports = new []pointer; D.unit = ""
|
|
set_text(D, text)
|
|
reindex(D)
|
|
return D
|
|
}
|
|
function by_path(path: pointer) -> Doc {
|
|
let p = normpath(path)
|
|
var i = 0
|
|
while i < len(g_docs) { if (g_docs[i].path == p) { return g_docs[i] }; i += 1 }
|
|
return null
|
|
}
|
|
function by_uri(uri: pointer) -> Doc {
|
|
let p = uri_to_path(uri)
|
|
let D = by_path(p)
|
|
if (D != null) { return D }
|
|
var i = 0
|
|
while i < len(g_docs) { if (g_docs[i].uri == uri) { return g_docs[i] }; i += 1 }
|
|
return null
|
|
}
|
|
function ensure_doc(path: pointer) -> Doc {
|
|
let D = by_path(path)
|
|
if (D != null) { return D }
|
|
let text = read_file(path)
|
|
if (text == null) { return null }
|
|
let nd = new_doc(path, text)
|
|
push(g_docs, nd)
|
|
return nd
|
|
}
|
|
|
|
function scan_root(root: pointer) -> void {
|
|
if (root == null) { return }
|
|
let listfile = `{Os.temp_dir()}/ludic_lsp_scan_{Os.pid()}.txt` # per process: editors run several servers
|
|
run(concat3(concat3("find '", root, "' -name '*.ludic' -not -path '*/build/*' -not -path '*/node_modules/*' -not -path '*/.*' > "), listfile, " 2>/dev/null"))
|
|
let list = read_file(listfile)
|
|
if (list == null) { return }
|
|
var i = 0; var start = 0
|
|
while list[i] != 0 {
|
|
if list[i] == '\n' { if i > start { let d = ensure_doc(list[start..i]) }; start = i + 1 }
|
|
i += 1
|
|
}
|
|
if i > start { let d = ensure_doc(list[start..i]) }
|
|
}
|
|
|
|
# ---- cross-file resolution ----
|
|
var g_owner: Doc = null
|
|
var g_symidx: int = 0
|
|
|
|
function imports_of(D: Doc, out: []Doc) -> void {
|
|
var i = 0
|
|
while i < len(D.imports) {
|
|
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
|
|
}
|
|
}
|
|
function related(D: Doc) -> []Doc {
|
|
let out = new []Doc
|
|
if (D == null) { return out }
|
|
push(out, D)
|
|
imports_of(D, out)
|
|
var i = 0
|
|
while i < len(g_docs) {
|
|
let R = g_docs[i]
|
|
if R.is_unit == 1 and not (R.path == D.path) {
|
|
let reach = new []Doc
|
|
imports_of(R, reach)
|
|
var hits = false
|
|
var k = 0
|
|
while k < len(reach) { if (reach[k].path == D.path) { hits = true }; k += 1 }
|
|
if hits {
|
|
var seen = false
|
|
k = 0
|
|
while k < len(out) { if (out[k].path == R.path) { seen = true }; k += 1 }
|
|
if not seen { push(out, R) }
|
|
k = 0
|
|
while k < len(reach) {
|
|
var s2 = false
|
|
var q = 0
|
|
while q < len(out) { if (out[q].path == reach[k].path) { s2 = true }; q += 1 }
|
|
if not s2 { push(out, reach[k]) }
|
|
k += 1
|
|
}
|
|
}
|
|
}
|
|
i += 1
|
|
}
|
|
return out
|
|
}
|
|
function unit_root(D: Doc) -> Doc {
|
|
if (D == null) or D.is_md == 1 { return null }
|
|
if D.is_unit == 1 { return D }
|
|
let rel = related(D)
|
|
var i = 0
|
|
while i < len(rel) { if rel[i].is_unit == 1 and rel[i].is_md == 0 { return rel[i] }; i += 1 }
|
|
return null
|
|
}
|
|
function find_local(D: Doc, name: pointer, off: int) -> int {
|
|
var best = -1
|
|
var i = 0
|
|
while i < len(D.syms) {
|
|
let s = D.syms[i]
|
|
if is_local_kind(s.kind) and (s.name == name) {
|
|
if off >= s.bstart and off <= s.bend { if best < 0 or s.bstart >= D.syms[best].bstart { best = i } }
|
|
}
|
|
i += 1
|
|
}
|
|
return best
|
|
}
|
|
# returns sym index in g_owner (set), or -1
|
|
function find_top(D: Doc, name: pointer) -> int {
|
|
let rel = related(D)
|
|
var i = 0
|
|
while i < len(rel) {
|
|
var k = 0
|
|
while k < len(rel[i].syms) {
|
|
if is_toplevel_kind(rel[i].syms[k].kind) and (rel[i].syms[k].name == name) { g_owner = rel[i]; return k }
|
|
k += 1
|
|
}
|
|
i += 1
|
|
}
|
|
i = 0
|
|
while i < len(g_docs) {
|
|
var k = 0
|
|
while k < len(g_docs[i].syms) {
|
|
if is_toplevel_kind(g_docs[i].syms[k].kind) and (g_docs[i].syms[k].name == name) { g_owner = g_docs[i]; return k }
|
|
k += 1
|
|
}
|
|
i += 1
|
|
}
|
|
return -1
|
|
}
|
|
# component sym index (in g_owner) for a dotted receiver, or -1
|
|
function receiver_component(D: Doc, dot_tok: int) -> int {
|
|
let recv = prev_sig(D, dot_tok)
|
|
if recv < 0 or D.tk_kind[recv] != LT_ID { return -1 }
|
|
let name = ttext(D, recv)
|
|
var ty = null
|
|
let l = find_local(D, name, D.tk_start[recv])
|
|
if l >= 0 { ty = D.syms[l].ty }
|
|
else { let t = find_top(D, name); if t >= 0 { ty = g_owner.syms[t].ty } }
|
|
if (ty == null) { return -1 }
|
|
if (ty == "") { return -1 }
|
|
return find_top(D, ty)
|
|
}
|
|
# resolve the token: sets g_owner/g_symidx; returns the token index (or -1)
|
|
function resolve_tok(D: Doc, tok: int) -> int {
|
|
g_owner = null; g_symidx = -1
|
|
if tok < 0 { return -1 }
|
|
if D.tk_kind[tok] != LT_ID and D.tk_kind[tok] != LT_STR { return -1 }
|
|
let name = ttext(D, tok)
|
|
var i = 0
|
|
while i < len(D.syms) { if D.syms[i].tok == tok { g_owner = D; g_symidx = i; return tok }; i += 1 }
|
|
let prev = prev_sig(D, tok)
|
|
if prev >= 0 and tis(D, prev, ".") {
|
|
let comp = receiver_component(D, prev)
|
|
if comp >= 0 and (g_owner != null) {
|
|
let co = g_owner
|
|
var k = 0
|
|
while k < len(co.syms) {
|
|
if co.syms[k].kind == LS_FIELD and co.syms[k].parent == comp and (co.syms[k].name == name) { g_owner = co; g_symidx = k; return tok }
|
|
k += 1
|
|
}
|
|
}
|
|
g_owner = null; g_symidx = -1
|
|
return tok
|
|
}
|
|
let l = find_local(D, name, D.tk_start[tok])
|
|
if l >= 0 { g_owner = D; g_symidx = l; return tok }
|
|
let t = find_top(D, name)
|
|
if t >= 0 { g_symidx = t; return tok }
|
|
g_owner = null; g_symidx = -1
|
|
return tok
|
|
}
|
|
function resolve_at(D: Doc, off: int) -> int { return resolve_tok(D, tok_at(D, off)) }
|
|
|
|
function first_on_line(D: Doc, tok: int) -> bool { let p = prev_sig(D, tok); return p < 0 or D.tk_line[p] != D.tk_line[tok] }
|
|
function in_ui_body(D: Doc, off: int) -> bool {
|
|
var i = 0
|
|
while i < len(D.syms) { if D.syms[i].kind == LS_UI and off > D.syms[i].bstart and off < D.syms[i].bend { return true }; i += 1 }
|
|
return false
|
|
}
|
|
|
|
# ---- builtins / intrinsics (name -> signature) ----
|
|
var g_builtins: []pointer
|
|
var g_bsigs: []pointer
|
|
function badd(name: pointer, sig: pointer) -> void { push(g_builtins, name); push(g_bsigs, sig) }
|
|
function is_builtin_name(name: pointer) -> bool { var i = 0; while i < len(g_builtins) { if (g_builtins[i] == name) { return true }; i += 1 }; return false }
|
|
function builtin_sig(name: pointer) -> pointer { var i = 0; while i < len(g_builtins) { if (g_builtins[i] == name) { return g_bsigs[i] }; i += 1 }; return null }
|
|
|
|
# The namespaced standard-library surface: `Namespace.method`, mirroring the
|
|
# compiler's emit_ns_call (selfhost/backend/emit_call.ludic). Screen/Random/Input/Map
|
|
# methods resolve to the underlying bare builtin's signature; Math/Text/List
|
|
# are pure fixed-point / string / slice ops with their own signatures. Returns
|
|
# the signature label for signature help, or null if it is not a known method.
|
|
function ns_method_sig(ns: pointer, meth: pointer) -> pointer {
|
|
if (ns == "Screen") {
|
|
if (meth == "clear") { return builtin_sig("clear") }
|
|
if (meth == "fill_rectangle") { return builtin_sig("fill_rect") }
|
|
if (meth == "draw_rectangle") { return builtin_sig("frame_rect") }
|
|
if (meth == "put_pixel") { return builtin_sig("put_px") }
|
|
if (meth == "draw_text") { return builtin_sig("text") }
|
|
if (meth == "draw_number") { return builtin_sig("text_int") }
|
|
if (meth == "show") { return builtin_sig("present") }
|
|
if (meth == "width") { return "Screen.width() -> int" }
|
|
if (meth == "height") { return "Screen.height() -> int" }
|
|
if (meth == "status") { return builtin_sig("status") }
|
|
if (meth == "line") { return "Screen.line(x1, y1, x2, y2, color)" }
|
|
if (meth == "circle") { return "Screen.circle(x, y, radius, color)" }
|
|
if (meth == "fill_circle") { return "Screen.fill_circle(x, y, radius, color)" }
|
|
if (meth == "triangle") { return "Screen.triangle(x1, y1, x2, y2, x3, y3, color)" }
|
|
if (meth == "fill_triangle") { return "Screen.fill_triangle(x1, y1, x2, y2, x3, y3, color)" }
|
|
if (meth == "sprite") { return "Screen.sprite(id, x, y)" }
|
|
if (meth == "sprite_scaled") { return "Screen.sprite_scaled(id, x, y, scale)" }
|
|
}
|
|
if (ns == "Map") {
|
|
if (meth == "size") { return builtin_sig("map_size") }
|
|
if (meth == "row") { return builtin_sig("map_row") }
|
|
if (meth == "tile") { return builtin_sig("tile") }
|
|
}
|
|
if (ns == "Random") {
|
|
if (meth == "range") { return builtin_sig("rng_range") }
|
|
if (meth == "chance") { return builtin_sig("rng_chance") }
|
|
if (meth == "seed") { return builtin_sig("seed") }
|
|
if (meth == "value") { return "Random.value() -> fixed" }
|
|
if (meth == "int") { return "Random.int(max) -> int" }
|
|
if (meth == "sign") { return "Random.sign() -> int" }
|
|
}
|
|
if (ns == "Color") {
|
|
if (meth == "rgb") { return "Color.rgb(r, g, b) -> int" }
|
|
if (meth == "rgba") { return "Color.rgba(r, g, b, a) -> int" }
|
|
if (meth == "lerp") { return "Color.lerp(c0, c1, t) -> int" }
|
|
if (meth == "darken") { return "Color.darken(c, amount) -> int" }
|
|
if (meth == "lighten") { return "Color.lighten(c, amount) -> int" }
|
|
if (meth == "with_alpha") { return "Color.with_alpha(c, a) -> int" }
|
|
}
|
|
if (ns == "Input") {
|
|
if (meth == "key") { return builtin_sig("key") }
|
|
}
|
|
if (ns == "Math") {
|
|
if (meth == "min") { return "Math.min(a, b) -> int" }
|
|
if (meth == "max") { return "Math.max(a, b) -> int" }
|
|
if (meth == "abs") { return "Math.abs(x) -> int" }
|
|
if (meth == "clamp") { return "Math.clamp(v, lo, hi) -> int" }
|
|
if (meth == "sign") { return "Math.sign(x) -> int" }
|
|
if (meth == "floor") { return "Math.floor(x) -> int" }
|
|
if (meth == "ceil") { return "Math.ceil(x) -> int" }
|
|
if (meth == "round") { return "Math.round(x) -> int" }
|
|
if (meth == "lerp") { return "Math.lerp(a, b, t) -> fixed" }
|
|
if (meth == "inverse_lerp") { return "Math.inverse_lerp(a, b, v) -> fixed" }
|
|
if (meth == "remap") { return "Math.remap(v, in0, in1, out0, out1) -> fixed" }
|
|
if (meth == "sqrt") { return "Math.sqrt(x) -> fixed" }
|
|
if (meth == "sin") { return "Math.sin(radians) -> fixed" }
|
|
if (meth == "cos") { return "Math.cos(radians) -> fixed" }
|
|
if (meth == "tan") { return "Math.tan(radians) -> fixed" }
|
|
if (meth == "atan2") { return "Math.atan2(y, x) -> fixed" }
|
|
if (meth == "asin") { return "Math.asin(x) -> fixed" }
|
|
if (meth == "acos") { return "Math.acos(x) -> fixed" }
|
|
if (meth == "hypot") { return "Math.hypot(x, y) -> fixed" }
|
|
if (meth == "dist") { return "Math.dist(x0, y0, x1, y1) -> fixed" }
|
|
if (meth == "dist2") { return "Math.dist2(x0, y0, x1, y1) -> fixed" }
|
|
if (meth == "deg_to_rad") { return "Math.deg_to_rad(degrees) -> fixed" }
|
|
if (meth == "rad_to_deg") { return "Math.rad_to_deg(radians) -> fixed" }
|
|
if (meth == "posmod") { return "Math.posmod(a, m) -> int" }
|
|
if (meth == "wrap") { return "Math.wrap(v, lo, hi) -> int" }
|
|
if (meth == "ping_pong") { return "Math.ping_pong(t, len) -> int" }
|
|
if (meth == "snapped") { return "Math.snapped(v, step) -> fixed" }
|
|
if (meth == "move_toward") { return "Math.move_toward(from, to, delta) -> fixed" }
|
|
if (meth == "smoothstep") { return "Math.smoothstep(e0, e1, x) -> fixed" }
|
|
}
|
|
if (ns == "Text") {
|
|
if (meth == "length") { return "Text.length(s) -> int" }
|
|
if (meth == "char_at") { return "Text.char_at(s, i) -> int" }
|
|
if (meth == "slice") { return "Text.slice(s, a, b) -> string" }
|
|
if (meth == "starts_with") { return "Text.starts_with(s, prefix) -> bool" }
|
|
if (meth == "ends_with") { return "Text.ends_with(s, suffix) -> bool" }
|
|
if (meth == "contains") { return "Text.contains(s, sub) -> bool" }
|
|
if (meth == "index_of") { return "Text.index_of(s, sub) -> int" }
|
|
if (meth == "equals") { return "Text.equals(a, b) -> bool" }
|
|
if (meth == "concat") { return "Text.concat(a, b) -> string" }
|
|
if (meth == "to_int") { return "Text.to_int(s) -> int" }
|
|
if (meth == "to_float") { return "Text.to_float(s) -> float" }
|
|
if (meth == "from_int") { return "Text.from_int(n) -> string" }
|
|
if (meth == "upper") { return "Text.upper(s) -> string" }
|
|
if (meth == "lower") { return "Text.lower(s) -> string" }
|
|
if (meth == "trim") { return "Text.trim(s) -> string" }
|
|
if (meth == "repeat") { return "Text.repeat(s, n) -> string" }
|
|
if (meth == "pad_left") { return "Text.pad_left(s, width) -> string" }
|
|
if (meth == "pad_right") { return "Text.pad_right(s, width) -> string" }
|
|
if (meth == "split") { return "Text.split(s, sep) -> []string" }
|
|
if (meth == "join") { return "Text.join(parts, sep) -> string" }
|
|
if (meth == "replace") { return "Text.replace(s, from, to) -> string" }
|
|
}
|
|
if (ns == "Hash") {
|
|
if (meth == "of") { return "Hash.of(s) -> int" }
|
|
if (meth == "fnv1a") { return "Hash.fnv1a(s) -> int" }
|
|
if (meth == "crc32") { return "Hash.crc32(s) -> int" }
|
|
if (meth == "mix") { return "Hash.mix(x) -> int" }
|
|
if (meth == "combine") { return "Hash.combine(...) -> int" }
|
|
if (meth == "of64") { return "Hash.of64(s) -> long" }
|
|
if (meth == "fnv1a_64") { return "Hash.fnv1a_64(s) -> long" }
|
|
if (meth == "mix64") { return "Hash.mix64(x) -> long" }
|
|
}
|
|
if (ns == "Vector") {
|
|
if (meth == "make") { return "Vector.make(x, y) -> Vector" }
|
|
if (meth == "zero") { return "Vector.zero() -> Vector" }
|
|
if (meth == "x") { return "Vector.x(v) -> fixed" }
|
|
if (meth == "y") { return "Vector.y(v) -> fixed" }
|
|
if (meth == "add") { return "Vector.add(a, b) -> Vector" }
|
|
if (meth == "sub") { return "Vector.sub(a, b) -> Vector" }
|
|
if (meth == "scale") { return "Vector.scale(v, s) -> Vector" }
|
|
if (meth == "dot") { return "Vector.dot(a, b) -> fixed" }
|
|
if (meth == "length") { return "Vector.length(v) -> fixed" }
|
|
if (meth == "distance") { return "Vector.distance(a, b) -> fixed" }
|
|
if (meth == "normalize") { return "Vector.normalize(v) -> Vector" }
|
|
if (meth == "lerp") { return "Vector.lerp(a, b, t) -> Vector" }
|
|
if (meth == "rotate") { return "Vector.rotate(v, angle) -> Vector" }
|
|
if (meth == "angle") { return "Vector.angle(v) -> fixed" }
|
|
if (meth == "from_angle") { return "Vector.from_angle(a) -> Vector" }
|
|
}
|
|
if (ns == "IVec2") {
|
|
if (meth == "make") { return "IVec2.make(x, y) -> IVec2" }
|
|
if (meth == "zero") { return "IVec2.zero() -> IVec2" }
|
|
if (meth == "x") { return "IVec2.x(v) -> int" }
|
|
if (meth == "y") { return "IVec2.y(v) -> int" }
|
|
if (meth == "add") { return "IVec2.add(a, b) -> IVec2" }
|
|
if (meth == "sub") { return "IVec2.sub(a, b) -> IVec2" }
|
|
if (meth == "scale") { return "IVec2.scale(v, s) -> IVec2" }
|
|
if (meth == "dot") { return "IVec2.dot(a, b) -> int" }
|
|
if (meth == "equal") { return "IVec2.equal(a, b) -> bool" }
|
|
if (meth == "manhattan") { return "IVec2.manhattan(a, b) -> int" }
|
|
if (meth == "to_vector") { return "IVec2.to_vector(v) -> Vector" }
|
|
}
|
|
if (ns == "Rect") {
|
|
if (meth == "make") { return "Rect.make(x, y, w, h) -> Rect" }
|
|
if (meth == "x") { return "Rect.x(r) -> fixed" }
|
|
if (meth == "y") { return "Rect.y(r) -> fixed" }
|
|
if (meth == "w") { return "Rect.w(r) -> fixed" }
|
|
if (meth == "h") { return "Rect.h(r) -> fixed" }
|
|
if (meth == "right") { return "Rect.right(r) -> fixed" }
|
|
if (meth == "bottom") { return "Rect.bottom(r) -> fixed" }
|
|
if (meth == "center") { return "Rect.center(r) -> Vector" }
|
|
if (meth == "contains") { return "Rect.contains(r, px, py) -> bool" }
|
|
if (meth == "intersects") { return "Rect.intersects(a, b) -> bool" }
|
|
}
|
|
if (ns == "List") {
|
|
if (meth == "len") { return "List.len(s) -> int" }
|
|
if (meth == "push") { return "List.push(s, v)" }
|
|
if (meth == "clear") { return "List.clear(s)" }
|
|
if (meth == "first") { return "List.first(s) -> T" }
|
|
if (meth == "last") { return "List.last(s) -> T" }
|
|
if (meth == "pop") { return "List.pop(s) -> T" }
|
|
if (meth == "swap") { return "List.swap(s, i, j)" }
|
|
if (meth == "contains") { return "List.contains(s, v) -> bool" }
|
|
if (meth == "index_of") { return "List.index_of(s, v) -> int" }
|
|
if (meth == "reverse") { return "List.reverse(s)" }
|
|
if (meth == "insert") { return "List.insert(s, i, v)" }
|
|
if (meth == "remove_at") { return "List.remove_at(s, i)" }
|
|
if (meth == "remove") { return "List.remove(s, v)" }
|
|
if (meth == "sort") { return "List.sort(s)" }
|
|
}
|
|
if (ns == "Ease") {
|
|
if (meth == "in") { return "Ease.in(t) -> fixed" }
|
|
if (meth == "out") { return "Ease.out(t) -> fixed" }
|
|
if (meth == "in_out") { return "Ease.in_out(t) -> fixed" }
|
|
if (meth == "back") { return "Ease.back(t) -> fixed" }
|
|
if (meth == "bounce") { return "Ease.bounce(t) -> fixed" }
|
|
}
|
|
if (ns == "Collision") {
|
|
if (meth == "rects") { return "Collision.rects(ax, ay, aw, ah, bx, by, bw, bh) -> bool" }
|
|
if (meth == "point_rect") { return "Collision.point_rect(px, py, rx, ry, rw, rh) -> bool" }
|
|
if (meth == "circles") { return "Collision.circles(ax, ay, ar, bx, by, br) -> bool" }
|
|
if (meth == "rect_circle") { return "Collision.rect_circle(rx, ry, rw, rh, cx, cy, cr) -> bool" }
|
|
}
|
|
if (ns == "World") {
|
|
if (meth == "get") { return "World.get(prop_id, field_id, entity) -> int" }
|
|
if (meth == "set") { return "World.set(prop_id, field_id, entity, value)" }
|
|
if (meth == "has") { return "World.has(entity, prop_id) -> int" }
|
|
if (meth == "count") { return "World.count() -> int" }
|
|
if (meth == "size") { return "World.size() -> int" }
|
|
if (meth == "spawn") { return "World.spawn(model_id) -> entity" }
|
|
if (meth == "save") { return "World.save(buf) -> int" }
|
|
if (meth == "load") { return "World.load(buf, len)" }
|
|
if (meth == "prop_id") { return "World.prop_id(name) -> int" }
|
|
if (meth == "field_id") { return "World.field_id(prop_id, name) -> int" }
|
|
if (meth == "model_id") { return "World.model_id(name) -> int" }
|
|
if (meth == "kind") { return "World.kind(entity) -> int" }
|
|
if (meth == "register_prop") { return "World.register_prop(name, size) -> int" }
|
|
if (meth == "attach") { return "World.attach(entity, prop_id)" }
|
|
if (meth == "detach") { return "World.detach(entity, prop_id)" }
|
|
if (meth == "query_next") { return "World.query_next(query_id, cursor) -> int" }
|
|
}
|
|
if (ns == "Network") {
|
|
if (meth == "send") { return "Network.send(peer, buf, len)" }
|
|
if (meth == "poll") { return "Network.poll(buf, cap) -> int" }
|
|
if (meth == "serialize") { return "Network.serialize(entity, buf) -> int" }
|
|
if (meth == "apply") { return "Network.apply(entity, buf) -> int" }
|
|
if (meth == "owner") { return "Network.owner(entity) -> int" }
|
|
if (meth == "set_owner") { return "Network.set_owner(entity, peer)" }
|
|
if (meth == "is_server") { return "Network.is_server() -> bool" }
|
|
if (meth == "is_owner") { return "Network.is_owner(entity) -> bool" }
|
|
if (meth == "local_id") { return "Network.local_id() -> int" }
|
|
}
|
|
if (ns == "System") {
|
|
if (meth == "run") { return "System.run(command) -> int" }
|
|
if (meth == "read_char") { return "System.read_char() -> int" }
|
|
if (meth == "file_open") { return "System.file_open(path, mode) -> pointer" }
|
|
if (meth == "file_read") { return "System.file_read(file, buf, n) -> int" }
|
|
if (meth == "file_write") { return "System.file_write(file, buf, n) -> int" }
|
|
if (meth == "file_seek") { return "System.file_seek(file, offset, whence) -> int" }
|
|
if (meth == "file_tell") { return "System.file_tell(file) -> int" }
|
|
if (meth == "file_close") { return "System.file_close(file)" }
|
|
}
|
|
if (ns == "Save") {
|
|
if (meth == "write") { return "Save.write()" }
|
|
if (meth == "read") { return "Save.read() -> bool" }
|
|
}
|
|
if (ns == "Time") {
|
|
if (meth == "frame") { return "Time.frame() -> int" }
|
|
if (meth == "delta") { return "Time.delta() -> fixed" }
|
|
if (meth == "elapsed") { return "Time.elapsed() -> fixed" }
|
|
if (meth == "now") { return "Time.now() -> int" }
|
|
if (meth == "since") { return "Time.since(past) -> int" }
|
|
}
|
|
if (ns == "Duration") {
|
|
if (meth == "seconds") { return "Duration.seconds(n) -> int" }
|
|
if (meth == "minutes") { return "Duration.minutes(n) -> int" }
|
|
if (meth == "hours") { return "Duration.hours(n) -> int" }
|
|
if (meth == "days") { return "Duration.days(n) -> int" }
|
|
if (meth == "as_seconds") { return "Duration.as_seconds(span) -> int" }
|
|
if (meth == "as_minutes") { return "Duration.as_minutes(span) -> int" }
|
|
if (meth == "as_hours") { return "Duration.as_hours(span) -> int" }
|
|
if (meth == "as_days") { return "Duration.as_days(span) -> int" }
|
|
}
|
|
if (ns == "Date") {
|
|
if (meth == "new") { return "Date.new(year, month, day) -> int" }
|
|
if (meth == "year") { return "Date.year(date) -> int" }
|
|
if (meth == "month") { return "Date.month(date) -> int" }
|
|
if (meth == "day") { return "Date.day(date) -> int" }
|
|
if (meth == "weekday") { return "Date.weekday(date) -> int" }
|
|
if (meth == "is_leap") { return "Date.is_leap(year) -> bool" }
|
|
if (meth == "days_in_month") { return "Date.days_in_month(year, month) -> int" }
|
|
if (meth == "to_epoch") { return "Date.to_epoch(date) -> int" }
|
|
if (meth == "add_days") { return "Date.add_days(date, n) -> int" }
|
|
if (meth == "diff_days") { return "Date.diff_days(a, b) -> int" }
|
|
}
|
|
if (ns == "DateTime") {
|
|
if (meth == "from") { return "DateTime.from(year, month, day, hour, minute, second) -> int" }
|
|
if (meth == "date") { return "DateTime.date(dt) -> int" }
|
|
if (meth == "add") { return "DateTime.add(dt, span) -> int" }
|
|
if (meth == "year") { return "DateTime.year(dt) -> int" }
|
|
if (meth == "month") { return "DateTime.month(dt) -> int" }
|
|
if (meth == "day") { return "DateTime.day(dt) -> int" }
|
|
if (meth == "weekday") { return "DateTime.weekday(dt) -> int" }
|
|
if (meth == "hour") { return "DateTime.hour(dt) -> int" }
|
|
if (meth == "minute") { return "DateTime.minute(dt) -> int" }
|
|
if (meth == "second") { return "DateTime.second(dt) -> int" }
|
|
if (meth == "format") { return "DateTime.format(dt, pattern) -> string" }
|
|
if (meth == "parse") { return "DateTime.parse(text, pattern) -> int" }
|
|
}
|
|
if (ns == "Clock") {
|
|
if (meth == "now") { return "Clock.now() -> int" }
|
|
if (meth == "set") { return "Clock.set(t)" }
|
|
if (meth == "advance") { return "Clock.advance(span)" }
|
|
if (meth == "reset") { return "Clock.reset()" }
|
|
}
|
|
if (ns == "Memory") {
|
|
if (meth == "bytes") { return "Memory.bytes(n) -> pointer" }
|
|
if (meth == "words") { return "Memory.words(n) -> words" }
|
|
if (meth == "copy") { return "Memory.copy(dst, src, n)" }
|
|
if (meth == "fill") { return "Memory.fill(buf, value, n)" }
|
|
if (meth == "peek") { return "Memory.peek(buf, i) -> int" }
|
|
if (meth == "poke") { return "Memory.poke(buf, i, value)" }
|
|
}
|
|
return null
|
|
}
|
|
function init_builtins() -> void {
|
|
g_builtins = new []pointer; g_bsigs = new []pointer
|
|
badd("min", "min(a: int, b: int) -> int")
|
|
badd("max", "max(a: int, b: int) -> int")
|
|
badd("abs", "abs(a: int) -> int")
|
|
badd("clamp", "clamp(v: int, lo: int, hi: int) -> int")
|
|
badd("seed", "seed(i: int)")
|
|
badd("rng_range", "rng_range(lo: int, hi: int) -> int")
|
|
badd("rng_chance", "rng_chance(pct: int) -> bool")
|
|
badd("fx", "fixed(i: int) -> fixed")
|
|
badd("flr", "floor(f: fixed) -> int")
|
|
badd("map_size", "map_size(w: int, h: int)")
|
|
badd("map_row", "map_row(y: int, row: string)")
|
|
badd("tile", "tile(x: int, y: int) -> int")
|
|
badd("clear", "clear(color: int)")
|
|
badd("present", "present()")
|
|
badd("fill_rect", "fill_rect(x: int, y: int, w: int, h: int, color: int)")
|
|
badd("frame_rect", "frame_rect(x: int, y: int, w: int, h: int, color: int)")
|
|
badd("put_px", "put_px(x: int, y: int, color: int)")
|
|
badd("text", "text(x: int, y: int, s: string, color: int, scale: int)")
|
|
badd("text_int", "text_int(x: int, y: int, n: int, color: int, scale: int)")
|
|
badd("font_load", "font_load(path: string) -> int")
|
|
badd("text_ttf", "text_ttf(font: int, x: int, y: int, utf8: string, color: int, px: int)")
|
|
badd("text_w", "text_w(font: int, utf8: string, px: int) -> int")
|
|
badd("text_h", "text_h(font: int, px: int) -> int")
|
|
badd("image_load", "image_load(path: string) -> int")
|
|
badd("draw_image", "draw_image(id: int, x: int, y: int)")
|
|
badd("draw_image_scaled", "draw_image_scaled(id: int, x: int, y: int, w: int, h: int)")
|
|
badd("draw_9slice", "draw_9slice(id: int, x: int, y: int, w: int, h: int, inset: int)")
|
|
badd("png_load", "png_load(path: string) -> int")
|
|
badd("sprites_load", "sprites_load(path: string)")
|
|
badd("draw_sprite", "draw_sprite(id: int, x: int, y: int)")
|
|
badd("draw_sprite_scaled", "draw_sprite_scaled(id: int, x: int, y: int, scale: int)")
|
|
badd("ui_build", "ui_build()")
|
|
badd("ui_open", "ui_open(id: int)")
|
|
badd("ui_tick", "ui_tick(key: int)")
|
|
badd("ui_render", "ui_render()")
|
|
badd("ui_clicked", "ui_clicked(id: int) -> bool")
|
|
badd("ui_set_text", "ui_set_text(id: int, s: string)")
|
|
badd("ui_set_int", "ui_set_int(id: int, n: int)")
|
|
badd("ui_focus", "ui_focus(id: int)")
|
|
badd("ui_focused", "ui_focused() -> int")
|
|
badd("ui_visible", "ui_visible(id: int, on: bool)")
|
|
badd("key", "key() -> int")
|
|
badd("reg", "reg(i: int) -> int")
|
|
badd("set_reg", "set_reg(i: int, v: int)")
|
|
badd("self", "self() -> entity")
|
|
badd("save", "save()")
|
|
badd("load", "load() -> bool")
|
|
badd("status", "status(s: string)")
|
|
badd("print", "print(x)")
|
|
badd("string", "string(x) -> string")
|
|
badd("quit", "quit()")
|
|
badd("bytes", "bytes(n: int) -> pointer")
|
|
badd("words", "words(n: int) -> words")
|
|
badd("resize", "resize(p: pointer, n: int) -> pointer")
|
|
badd("arg_count", "arg_count() -> int")
|
|
badd("arg", "arg(i: int) -> string")
|
|
badd("file_stderr", "file_stderr() -> pointer")
|
|
badd("file_stdout", "file_stdout() -> pointer")
|
|
badd("free", "free(p: pointer)")
|
|
badd("fill", "fill(p: pointer, byte: int, n: int)")
|
|
badd("offset", "offset(p: pointer, off: int) -> pointer")
|
|
badd("as_fixed", "as_fixed(i: int) -> fixed")
|
|
badd("as_int", "as_int(f: fixed) -> int")
|
|
badd("file_open", "file_open(path: string, mode: string) -> pointer")
|
|
badd("file_read", "file_read(f: pointer, buf: pointer, n: int) -> int")
|
|
badd("file_write", "file_write(f: pointer, buf: pointer, n: int) -> int")
|
|
badd("file_seek", "file_seek(f: pointer, off: int, whence: int) -> int")
|
|
badd("file_tell", "file_tell(f: pointer) -> int")
|
|
badd("file_close", "file_close(f: pointer)")
|
|
badd("read_char", "read_char() -> int")
|
|
badd("exit", "exit(code: int)")
|
|
badd("is_windowed", "is_windowed() -> bool")
|
|
badd("game_title", "game_title() -> string")
|
|
badd("win_open", "win_open(title: string, w: int, h: int, scale: int)")
|
|
badd("win_poll", "win_poll() -> int")
|
|
badd("win_present", "win_present(px: pointer, w: int, h: int)")
|
|
badd("win_running", "win_running() -> bool")
|
|
badd("win_close", "win_close()")
|
|
}
|
|
|
|
# ---- classification ----
|
|
function class_of_symkind(k: int) -> int {
|
|
if k == LS_COMPONENT { return SC_COMPONENT }
|
|
if k == LS_ARCHETYPE { return SC_ARCHETYPE }
|
|
if k == LS_FIELD { return SC_FIELD }
|
|
if k == LS_CONST { return SC_CONST }
|
|
if k == LS_VAR { return SC_MODVAR }
|
|
if k == LS_FN or k == LS_EXTERN { return SC_FUNCTION }
|
|
if k == LS_SYSTEM { return SC_SYSTEM }
|
|
if k == LS_UI { return SC_UI }
|
|
if k == LS_WIDGET { return SC_CONST }
|
|
if k == LS_SCENE { return SC_SCENE }
|
|
if k == LS_LAYER { return SC_LAYER }
|
|
if k == LS_PARAM { return SC_PARAM }
|
|
if k == LS_QUERYVAR or k == LS_LOCAL or k == LS_STATE { return SC_VARIABLE }
|
|
if k == LS_UNIT { return SC_UI }
|
|
return SC_UNKNOWN
|
|
}
|
|
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]
|
|
var c = SC_NONE
|
|
if k == LT_COMMENT { c = SC_COMMENT }
|
|
else { if k == LT_STR or k == LT_CHAR { c = SC_STRING }
|
|
else { if k == LT_INT or k == LT_FLOAT { c = SC_NUMBER }
|
|
else { if k == LT_BOOL or k == LT_KW { c = SC_KEYWORD }
|
|
else { if k == LT_TYPE { c = SC_TYPE }
|
|
else { if k == LT_PHASE { c = SC_PHASE }
|
|
else { if k == LT_ANNO { c = SC_ANNOTATION }
|
|
else { if k == LT_OP { c = SC_OPERATOR }
|
|
else { if k == LT_ID { c = SC_UNKNOWN } } } } } } } } }
|
|
if k != LT_ID { D.cls[i] = c; i += 1; continue }
|
|
let name = ttext(D, i)
|
|
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)
|
|
D.clsmods[i] = SEM_DECLARATION | vendor_mod(D)
|
|
i += 1; continue
|
|
}
|
|
let prev = prev_sig(D, i)
|
|
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 }
|
|
if in_ui_body(D, D.tk_start[i]) {
|
|
if followed_eq { D.cls[i] = SC_PROP; i += 1; continue }
|
|
if first_on_line(D, i) or (prev >= 0 and (tis(D, prev, "{") or tis(D, prev, "}"))) {
|
|
if is_widget_word(name) { D.cls[i] = SC_WIDGET } else { D.cls[i] = SC_UI }
|
|
i += 1; continue
|
|
}
|
|
}
|
|
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); 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
|
|
}
|
|
}
|
|
|
|
# semantic token type index + modifiers (via g_semmods)
|
|
var g_semmods: int = 0
|
|
function sem_type_of(cls: int) -> int {
|
|
g_semmods = 0
|
|
if cls == SC_KEYWORD { return 0 }
|
|
if cls == SC_TYPE { g_semmods = 4; return 1 }
|
|
if cls == SC_COMPONENT { return 2 }
|
|
if cls == SC_ARCHETYPE { return 3 }
|
|
if cls == SC_SCENE { return 4 }
|
|
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 { 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 { 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 }
|
|
if cls == SC_NUMBER { return 10 }
|
|
if cls == SC_STRING { return 11 }
|
|
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
|
|
}
|
|
|
|
# ============================================================================
|
|
# JSON (just enough for LSP)
|
|
# ============================================================================
|
|
const JNULL: int = 0
|
|
const JBOOL: int = 1
|
|
const JNUM: int = 2
|
|
const JSTR: int = 3
|
|
const JARR: int = 4
|
|
const JOBJ: int = 5
|
|
property JVal { t: int = 0, num: int = 0, b: int = 0, s: pointer = null, kids: []JVal, keys: []pointer }
|
|
function jval(t: int) -> JVal { let v = new JVal; v.t = t; v.kids = new []JVal; v.keys = new []pointer; return v }
|
|
|
|
var jsrc: pointer = null
|
|
var jp: int = 0
|
|
function jskip() -> void { while jsrc[jp] == ' ' or jsrc[jp] == '\t' or jsrc[jp] == '\n' or jsrc[jp] == '\r' { jp += 1 } }
|
|
function jhex4(s: pointer, at: int) -> int {
|
|
var v = 0; var i = 0
|
|
while i < 4 { let h = hexval(s[at + i]); if h < 0 { return v }; v = v * 16 + h; i += 1 }
|
|
return v
|
|
}
|
|
function utf8_emit(o: Buf, cp: int) -> void {
|
|
if cp < 128 { buf_putc(o, cp) }
|
|
else { if cp < 2048 { buf_putc(o, 192 | (cp >> 6)); buf_putc(o, 128 | (cp & 63)) }
|
|
else { if cp < 65536 { buf_putc(o, 224 | (cp >> 12)); buf_putc(o, 128 | ((cp >> 6) & 63)); buf_putc(o, 128 | (cp & 63)) }
|
|
else { buf_putc(o, 240 | (cp >> 18)); buf_putc(o, 128 | ((cp >> 12) & 63)); buf_putc(o, 128 | ((cp >> 6) & 63)); buf_putc(o, 128 | (cp & 63)) } } }
|
|
}
|
|
function jparse_string() -> pointer {
|
|
jp += 1 # skip opening quote
|
|
let o = buf_new()
|
|
while jsrc[jp] != 0 and jsrc[jp] != '"' {
|
|
if jsrc[jp] == '\\' {
|
|
jp += 1
|
|
let c = jsrc[jp]; jp += 1
|
|
if c == 'n' { buf_putc(o, '\n') }
|
|
else { if c == 't' { buf_putc(o, '\t') }
|
|
else { if c == 'r' { buf_putc(o, '\r') }
|
|
else { if c == 'b' { buf_putc(o, 8) }
|
|
else { if c == 'f' { buf_putc(o, 12) }
|
|
else { if c == 'u' {
|
|
var cp = jhex4(jsrc, jp); jp += 4
|
|
if cp >= 55296 and cp < 56320 and jsrc[jp] == '\\' and jsrc[jp + 1] == 'u' {
|
|
let lo = jhex4(jsrc, jp + 2); jp += 6
|
|
cp = 65536 + ((cp - 55296) << 10) + (lo - 56320)
|
|
}
|
|
utf8_emit(o, cp)
|
|
} else { buf_putc(o, c) } } } } } }
|
|
} else { buf_putc(o, jsrc[jp]); jp += 1 }
|
|
}
|
|
if jsrc[jp] == '"' { jp += 1 }
|
|
return buf_str(o)
|
|
}
|
|
function jparse() -> JVal {
|
|
jskip()
|
|
let c = jsrc[jp]
|
|
if c == '{' { # '{'
|
|
jp += 1
|
|
let v = jval(JOBJ)
|
|
while true {
|
|
jskip()
|
|
if jsrc[jp] == '}' { jp += 1; break }
|
|
var key = ""
|
|
if jsrc[jp] == '"' { key = jparse_string() }
|
|
jskip(); if jsrc[jp] == ':' { jp += 1 }
|
|
let kid = jparse()
|
|
push(v.keys, key); push(v.kids, kid)
|
|
jskip()
|
|
if jsrc[jp] == ',' { jp += 1 }
|
|
else { if jsrc[jp] == '}' { jp += 1; break } else { if jsrc[jp] == 0 { break } } }
|
|
}
|
|
return v
|
|
}
|
|
if c == '[' { # '['
|
|
jp += 1
|
|
let v = jval(JARR)
|
|
while true {
|
|
jskip()
|
|
if jsrc[jp] == ']' { jp += 1; break }
|
|
let kid = jparse()
|
|
push(v.keys, ""); push(v.kids, kid)
|
|
jskip()
|
|
if jsrc[jp] == ',' { jp += 1 }
|
|
else { if jsrc[jp] == ']' { jp += 1; break } else { if jsrc[jp] == 0 { break } } }
|
|
}
|
|
return v
|
|
}
|
|
if c == '"' { let v = jval(JSTR); v.s = jparse_string(); return v }
|
|
if jsrc[jp] == 't' and jsrc[jp + 1] == 'r' { jp += 4; let v = jval(JBOOL); v.b = 1; return v } # true
|
|
if jsrc[jp] == 'f' and jsrc[jp + 1] == 'a' { jp += 5; let v = jval(JBOOL); v.b = 0; return v } # false
|
|
if jsrc[jp] == 'n' and jsrc[jp + 1] == 'u' { jp += 4; return jval(JNULL) } # null
|
|
# number
|
|
var neg = false
|
|
if jsrc[jp] == '-' { neg = true; jp += 1 }
|
|
if char_is_digit(jsrc[jp]) {
|
|
var n = 0
|
|
while char_is_digit(jsrc[jp]) { n = n * 10 + (jsrc[jp] - 48); jp += 1 }
|
|
if jsrc[jp] == '.' { jp += 1; while char_is_digit(jsrc[jp]) { jp += 1 } } # skip fraction
|
|
let v = jval(JNUM); if neg { v.num = -n } else { v.num = n }; return v
|
|
}
|
|
jp += 1
|
|
return jval(JNULL)
|
|
}
|
|
function jget(v: JVal, key: pointer) -> JVal {
|
|
if (v == null) { return null }
|
|
if v.t != JOBJ { return null }
|
|
var i = 0
|
|
while i < len(v.keys) { if (v.keys[i] == key) { return v.kids[i] }; i += 1 }
|
|
return null
|
|
}
|
|
function jat(v: JVal, i: int) -> JVal { if (v == null) { return null }; if i < 0 or i >= len(v.kids) { return null }; return v.kids[i] }
|
|
function jlen(v: JVal) -> int { if (v == null) { return 0 }; return len(v.kids) }
|
|
function jstr(v: JVal, def: pointer) -> pointer { if (v != null) and v.t == JSTR and (v.s != null) { return v.s }; return def }
|
|
function jint(v: JVal, def: int) -> int { if (v != null) and v.t == JNUM { return v.num }; return def }
|
|
function jpath(v: JVal, path: pointer) -> JVal {
|
|
var cur = v
|
|
let n = cstr_len(path)
|
|
var start = 0; var i = 0
|
|
while i <= n {
|
|
if i == n or path[i] == '.' {
|
|
let key = path[start..i]
|
|
cur = jget(cur, key)
|
|
if (cur == null) { return null }
|
|
start = i + 1
|
|
}
|
|
i += 1
|
|
}
|
|
return cur
|
|
}
|
|
|
|
# ============================================================================
|
|
# the formatter (Doc-based; reused for LSP formatting) — mirrors fmt.ludic
|
|
# ============================================================================
|
|
function f_name_like(k: int) -> bool { return k == LT_ID or k == LT_KW or k == LT_TYPE or k == LT_PHASE or k == LT_BOOL }
|
|
function f_dot_tight(F: Doc, t: int) -> bool {
|
|
if f_name_like(F.tk_kind[t]) { return true }
|
|
if F.tk_kind[t] != LT_OP { return false }
|
|
let n = tlen(F, t); let c0 = F.src[F.tk_start[t]]
|
|
if n == 1 and (c0 == ')' or c0 == ']' or c0 == '.') { return true }
|
|
if n == 2 and c0 == '.' and F.src[F.tk_start[t] + 1] == 46 { return true }
|
|
return false
|
|
}
|
|
function f_is_unary(F: Doc, i: int) -> bool {
|
|
if F.tk_kind[i] != LT_OP { return false }
|
|
let n = tlen(F, i)
|
|
if not (n == 1 and (F.src[F.tk_start[i]] == 45 or F.src[F.tk_start[i]] == 33)) { return false }
|
|
let p = prev_sig(F, i)
|
|
if p < 0 { return true }
|
|
let pk2 = F.tk_kind[p]
|
|
if pk2 == LT_ID or pk2 == LT_INT or pk2 == LT_FLOAT or pk2 == LT_STR or pk2 == LT_CHAR or pk2 == LT_BOOL or pk2 == LT_TYPE or pk2 == LT_PHASE { return false }
|
|
if pk2 == LT_OP { let c = F.src[F.tk_start[p]]; return not (tlen(F, p) == 1 and (c == ')' or c == ']' or c == '}')) }
|
|
return true
|
|
}
|
|
function f_space_before(F: Doc, prev: int, cur: int) -> int {
|
|
if prev < 0 { return 0 }
|
|
let pk2 = F.tk_kind[prev]; let ck = F.tk_kind[cur]
|
|
let plen = tlen(F, prev); let clen = tlen(F, cur)
|
|
let p0 = F.src[F.tk_start[prev]]; let c0 = F.src[F.tk_start[cur]]
|
|
let p1 = (plen == 1); let c1 = (clen == 1)
|
|
if pk2 == LT_ERR or ck == LT_ERR { return F.tk_start[cur] - F.tk_end[prev] }
|
|
if c1 and (c0 == ')' or c0 == ']' or c0 == ',' or c0 == ':' or c0 == ';') { return 0 }
|
|
if c1 and c0 == '.' and ck == LT_OP and f_dot_tight(F, prev) { return 0 }
|
|
if p1 and p0 == '.' and pk2 == LT_OP and f_dot_tight(F, cur) { return 0 }
|
|
if p1 and (p0 == '(' or p0 == '[') and pk2 == LT_OP { return 0 }
|
|
if pk2 == LT_OP and f_is_unary(F, prev) { return 0 }
|
|
if pk2 == LT_ANNO and c1 and c0 == '(' { return 0 }
|
|
if c1 and c0 == '(' and ck == LT_OP {
|
|
if pk2 == LT_ID or pk2 == LT_TYPE or pk2 == LT_PHASE { return 0 }
|
|
if pk2 == LT_OP { if p1 and (p0 == ')' or p0 == ']') { return 1 }; return 0 }
|
|
return 1
|
|
}
|
|
return 1
|
|
}
|
|
function f_is_clause(w: pointer) -> bool {
|
|
return (w == "phase") or (w == "query") or (w == "reads") or (w == "writes") or (w == "needs") or (w == "uses") or (w == "requires") or (w == "ensures") or (w == "invariant") or (w == "effects")
|
|
}
|
|
function make_text_doc(text: pointer) -> Doc {
|
|
let F = new Doc
|
|
F.raw = text; F.text = text; F.src = text; F.is_md = 0
|
|
lex_doc(F)
|
|
return F
|
|
}
|
|
function format_doc(F: Doc, iw: int) -> pointer {
|
|
let o = buf_new()
|
|
let stack = words(600)
|
|
let hang = words(600)
|
|
var sp = 0; var cur = 0; var open = 0; var brack = 0; var ui_depth = -1
|
|
var pending_blank = 0; var wrote_any = 0; var prev_line_had_comment = 0
|
|
let N = ntok(F)
|
|
var i = 0
|
|
while i < N and F.tk_kind[i] != LT_EOF {
|
|
let a = i
|
|
while i < N and F.tk_kind[i] != LT_NL and F.tk_kind[i] != LT_EOF { i += 1 }
|
|
let b = i
|
|
if i < N and F.tk_kind[i] == LT_NL { i += 1 }
|
|
if a == b { if wrote_any == 1 { pending_blank = 1 }; continue }
|
|
var line_level = cur
|
|
var tsp = sp; var t = a
|
|
while t < b and F.tk_kind[t] == LT_OP and tlen(F, t) == 1 and F.src[F.tk_start[t]] == 125 { if tsp > 0 { tsp -= 1; line_level = stack[tsp] }; t += 1 }
|
|
var orig_ind = 0
|
|
var kk = F.linestart[F.tk_line[a]]
|
|
while kk < F.tk_start[a] { if F.src[kk] == '\t' { orig_ind += 4 } else { orig_ind += 1 }; kk += 1 }
|
|
var comment_run = 0
|
|
if F.tk_kind[a] == LT_COMMENT and b == a + 1 and prev_line_had_comment == 1 { comment_run = 1 }
|
|
var ind = 0
|
|
var hangprev = 0
|
|
if sp > 0 { hangprev = hang[sp - 1] }
|
|
if open > 0 or (sp > 0 and hangprev == 1) {
|
|
var k2 = F.linestart[F.tk_line[a]]
|
|
ind = 0
|
|
while k2 < F.tk_start[a] { if F.src[k2] == '\t' { ind += 4 } else { ind += 1 }; k2 += 1 }
|
|
} else {
|
|
var extra = 0
|
|
if f_is_clause(ttext(F, a)) and F.tk_kind[a] == LT_KW { extra = iw }
|
|
ind = line_level * iw + extra
|
|
if comment_run == 1 and orig_ind > ind { ind = orig_ind }
|
|
}
|
|
if pending_blank == 1 and wrote_any == 1 { buf_putc(o, '\n') }
|
|
pending_blank = 0
|
|
var wi = 0
|
|
while wi < ind { buf_putc(o, ' '); wi += 1 }
|
|
var prev = -1
|
|
var line_brack = brack
|
|
var line_ui_open = 0
|
|
if ui_depth >= 0 and sp > ui_depth { line_ui_open = 1 }
|
|
t = a
|
|
while t < b {
|
|
var want = 0
|
|
if F.tk_kind[t] == LT_COMMENT { if prev >= 0 { want = 2 } else { want = 0 } }
|
|
else {
|
|
want = f_space_before(F, prev, t)
|
|
if line_brack > 0 {
|
|
if F.tk_kind[t] == LT_OP and tlen(F, t) == 1 and F.src[F.tk_start[t]] == 125 { want = 0 }
|
|
if prev >= 0 and F.tk_kind[prev] == LT_OP and tlen(F, prev) == 1 and F.src[F.tk_start[prev]] == 123 { want = 0 }
|
|
}
|
|
if line_ui_open == 1 {
|
|
var eqh = 0
|
|
if F.tk_kind[t] == LT_OP and tlen(F, t) == 1 and F.src[F.tk_start[t]] == 61 { eqh = 1 }
|
|
var eqp = 0
|
|
if prev >= 0 and F.tk_kind[prev] == LT_OP and tlen(F, prev) == 1 and F.src[F.tk_start[prev]] == 61 { eqp = 1 }
|
|
if eqh == 1 or eqp == 1 { want = 0 }
|
|
}
|
|
}
|
|
var gap = 0
|
|
if prev >= 0 { gap = F.tk_start[t] - F.tk_end[prev] }
|
|
if gap >= 2 and want >= 1 { if gap > 60 { gap = 60 }; want = gap }
|
|
if want < 0 { want = 0 }
|
|
var w2 = 0
|
|
while w2 < want { buf_putc(o, ' '); w2 += 1 }
|
|
buf_addrange(o, F.src, F.tk_start[t], F.tk_end[t])
|
|
prev = t
|
|
if F.tk_kind[t] == LT_OP and tlen(F, t) == 1 {
|
|
let c = F.src[F.tk_start[t]]
|
|
if c == '[' { line_brack += 1 }
|
|
else { if c == ']' { line_brack -= 1; if line_brack < 0 { line_brack = 0 } } }
|
|
}
|
|
t += 1
|
|
}
|
|
buf_putc(o, '\n')
|
|
wrote_any = 1
|
|
prev_line_had_comment = 0
|
|
if prev >= 0 and F.tk_kind[prev] == LT_COMMENT { prev_line_had_comment = 1 }
|
|
if F.tk_kind[a] == LT_KW and (ttext(F, a) == "ui") and ui_depth < 0 { ui_depth = sp }
|
|
var level = cur
|
|
t = a
|
|
while t < b {
|
|
if F.tk_kind[t] == LT_OP and tlen(F, t) == 1 {
|
|
let c = F.src[F.tk_start[t]]
|
|
if c == '{' {
|
|
if sp < 512 { let nxt = next_sig(F, t); stack[sp] = line_level; var h = 0; if nxt >= 0 and nxt < b { h = 1 }; hang[sp] = h; sp += 1 }
|
|
level = line_level + 1
|
|
}
|
|
else { if c == '}' { if sp > 0 { sp -= 1; level = stack[sp] } }
|
|
else { if c == '(' or c == '[' { open += 1; if c == '[' { brack += 1 } }
|
|
else { if c == ')' or c == ']' { open -= 1; if open < 0 { open = 0 }; if c == ']' { brack -= 1; if brack < 0 { brack = 0 } } } } } }
|
|
}
|
|
t += 1
|
|
}
|
|
cur = level
|
|
if ui_depth >= 0 and sp <= ui_depth { ui_depth = -1 }
|
|
}
|
|
return buf_str(o)
|
|
}
|
|
# markdown fence formatting (mirrors fmt.ludic)
|
|
var mf_flen: int = 0
|
|
var mf_info: int = 0
|
|
var mf_marker: int = 0
|
|
function mf_fence_at(s: pointer, i: int) -> bool {
|
|
var j = i; while s[j] == ' ' { j += 1 }
|
|
let m = s[j]
|
|
if m != '`' and m != '~' { return false }
|
|
var n = 0; while s[j] == m { j += 1; n += 1 }
|
|
if n < 3 { return false }
|
|
mf_flen = n; mf_info = j; mf_marker = m
|
|
return true
|
|
}
|
|
function f_line_end(s: pointer, i: int) -> int { var e = i; while s[e] != 0 and s[e] != '\n' { e += 1 }; return e }
|
|
function format_md(s: pointer, iw: int) -> pointer {
|
|
let o = buf_new()
|
|
var i = 0
|
|
while s[i] != 0 {
|
|
let ls = i
|
|
let le = f_line_end(s, ls)
|
|
var indent = 0; while s[ls + indent] == ' ' { indent += 1 }
|
|
var is_ludic_fence = false
|
|
if mf_fence_at(s, ls) {
|
|
var kk = mf_info; while s[kk] == ' ' or s[kk] == '\t' { kk += 1 }
|
|
if word_ludic_at(s, kk) { is_ludic_fence = true }
|
|
}
|
|
if is_ludic_fence {
|
|
var e0 = le; if s[le] != 0 { e0 = le + 1 }
|
|
buf_addrange(o, s, ls, e0)
|
|
i = e0
|
|
let bs = i
|
|
var be = bs
|
|
let openm = mf_marker; let openlen = mf_flen
|
|
while true {
|
|
if s[be] == 0 { break }
|
|
let ps = be; let pe = f_line_end(s, ps)
|
|
if mf_fence_at(s, ps) {
|
|
if mf_marker == openm and mf_flen >= openlen {
|
|
var only = 1; var k = mf_info
|
|
while k < pe { if s[k] != ' ' and s[k] != '\r' { only = 0; break }; k += 1 }
|
|
if only == 1 { be = ps; break }
|
|
}
|
|
}
|
|
if s[pe] != 0 { be = pe + 1 } else { be = pe }
|
|
}
|
|
let body = buf_new()
|
|
var p = bs
|
|
while p < be {
|
|
let q = f_line_end(s, p)
|
|
var skip = 0
|
|
while skip < indent and (p + skip) < q and s[p + skip] == ' ' { skip += 1 }
|
|
buf_addrange(body, s, p + skip, q)
|
|
buf_putc(body, '\n')
|
|
if s[q] != 0 { p = q + 1 } else { p = q }
|
|
}
|
|
let F = make_text_doc(buf_str(body))
|
|
let ftext = format_doc(F, iw)
|
|
var fp = 0
|
|
while ftext[fp] != 0 {
|
|
var fq = fp; while ftext[fq] != 0 and ftext[fq] != '\n' { fq += 1 }
|
|
if fq > fp { var wi = 0; while wi < indent { buf_putc(o, ' '); wi += 1 } }
|
|
buf_addrange(o, ftext, fp, fq)
|
|
buf_putc(o, '\n')
|
|
if ftext[fq] != 0 { fp = fq + 1 } else { fp = fq }
|
|
}
|
|
i = be
|
|
continue
|
|
}
|
|
var e1 = le; if s[le] != 0 { e1 = le + 1 }
|
|
buf_addrange(o, s, ls, e1)
|
|
i = e1
|
|
}
|
|
return buf_str(o)
|
|
}
|
|
function skip_ws2(s: pointer, at: int) -> int { var a = at; while s[a] == ' ' or s[a] == '\t' { a += 1 }; return a }
|
|
|
|
# ============================================================================
|
|
# framing + output
|
|
# ============================================================================
|
|
extern function fflush(f: pointer) -> int = "fflush"
|
|
|
|
function jstr_out(o: Buf, z: pointer) -> void {
|
|
buf_putc(o, '"')
|
|
if (z == null) { buf_putc(o, '"'); return }
|
|
var i = 0
|
|
while z[i] != 0 {
|
|
let c = z[i]
|
|
if c == '"' { buf_putc(o, '\\'); buf_putc(o, '"') }
|
|
else { if c == '\\' { buf_putc(o, '\\'); buf_putc(o, '\\') }
|
|
else { if c == '\n' { buf_putc(o, '\\'); buf_putc(o, 'n') }
|
|
else { if c == '\r' { buf_putc(o, '\\'); buf_putc(o, 'r') }
|
|
else { if c == '\t' { buf_putc(o, '\\'); buf_putc(o, 't') }
|
|
else { if c >= 0 and c < ' ' { buf_putc(o, '\\'); buf_putc(o, 'u'); buf_putc(o, '0'); buf_putc(o, '0'); buf_putc(o, hexch(c / 16)); buf_putc(o, hexch(c % 16)) }
|
|
else { buf_putc(o, c) } } } } } }
|
|
i += 1
|
|
}
|
|
buf_putc(o, '"')
|
|
}
|
|
|
|
function send_raw(body: pointer) -> void {
|
|
let hdr = buf_new()
|
|
buf_puts(hdr, "Content-Length: "); buf_puti(hdr, cstr_len(body)); buf_putc(hdr, '\r'); buf_putc(hdr, '\n'); buf_putc(hdr, '\r'); buf_putc(hdr, '\n')
|
|
let hs = buf_str(hdr)
|
|
let out = file_stdout()
|
|
file_write(out, hs, cstr_len(hs))
|
|
file_write(out, body, cstr_len(body))
|
|
let z = fflush(out)
|
|
}
|
|
function send_result(idv: JVal, result: pointer) -> void {
|
|
let o = buf_new()
|
|
buf_puts(o, "{\"jsonrpc\":\"2.0\",\"id\":")
|
|
if (idv == null) { buf_puts(o, "null") }
|
|
else { if idv.t == JSTR { jstr_out(o, idv.s) } else { buf_puti(o, idv.num) } }
|
|
buf_puts(o, ",\"result\":")
|
|
if (result == null) or (result[0] == 0) { buf_puts(o, "null") } else { buf_puts(o, result) }
|
|
buf_putc(o, '}')
|
|
send_raw(buf_str(o))
|
|
}
|
|
function send_notify(method: pointer, params: pointer) -> void {
|
|
let o = buf_new()
|
|
buf_puts(o, "{\"jsonrpc\":\"2.0\",\"method\":"); jstr_out(o, method); buf_puts(o, ",\"params\":"); buf_puts(o, params); buf_putc(o, '}')
|
|
send_raw(buf_str(o))
|
|
}
|
|
|
|
function lower(c: int) -> int { if c >= 'A' and c <= 'Z' { return c + 32 }; return c }
|
|
function starts_with_ci(s: pointer, pre: pointer) -> bool {
|
|
var i = 0
|
|
while pre[i] != 0 { if s[i] == 0 or lower(s[i]) != lower(pre[i]) { return false }; i += 1 }
|
|
return true
|
|
}
|
|
function read_message() -> pointer {
|
|
var length = -1
|
|
let line = buf_new()
|
|
while true {
|
|
line.len = 0
|
|
var c = read_char()
|
|
if c < 0 { return null }
|
|
while c >= 0 and c != '\n' { buf_putc(line, c); c = read_char() }
|
|
let s = buf_str(line)
|
|
var L = cstr_len(s)
|
|
if L > 0 and s[L - 1] == '\r' { s[L - 1] = 0; L -= 1 }
|
|
if L == 0 { break }
|
|
if starts_with_ci(s, "content-length:") { length = atoi(s[15..cstr_len(s)]) }
|
|
}
|
|
if length <= 0 { return null }
|
|
let buf = bytes(length + 1)
|
|
var got = 0
|
|
while got < length { let c = read_char(); if c < 0 { break }; buf[got] = c; got += 1 }
|
|
buf[got] = 0
|
|
return buf
|
|
}
|
|
|
|
# ---- position output ----
|
|
function put_pos(o: Buf, D: Doc, off: int) -> void { buf_puts(o, "{\"line\":"); buf_puti(o, line_of(D, off)); buf_puts(o, ",\"character\":"); buf_puti(o, col_of(D, off)); buf_putc(o, '}') }
|
|
function put_range(o: Buf, D: Doc, a: int, b: int) -> void { buf_puts(o, "{\"start\":"); put_pos(o, D, a); buf_puts(o, ",\"end\":"); put_pos(o, D, b); buf_putc(o, '}') }
|
|
function put_location(o: Buf, D: Doc, a: int, b: int) -> void { buf_puts(o, "{\"uri\":"); jstr_out(o, D.uri); buf_puts(o, ",\"range\":"); put_range(o, D, a, b); buf_putc(o, '}') }
|
|
|
|
function doc_of(msg: JVal) -> Doc {
|
|
let uri = jstr(jpath(msg, "params.textDocument.uri"), null)
|
|
if (uri == null) { return null }
|
|
let D = by_uri(uri)
|
|
if (D != null) { return D }
|
|
return ensure_doc(uri_to_path(uri))
|
|
}
|
|
function off_of(msg: JVal, D: Doc, base: pointer) -> int {
|
|
let line = jint(jpath(msg, concat3(base, ".line", "")), 0)
|
|
let ch = jint(jpath(msg, concat3(base, ".character", "")), 0)
|
|
return offset_of(D, line, ch)
|
|
}
|
|
function basename(p: pointer) -> pointer { var last = -1; var i = 0; while p[i] != 0 { if p[i] == '/' { last = i }; i += 1 }; return p[last + 1..i] }
|
|
|
|
# ---- LSP symbol kinds & completion kinds ----
|
|
function lsp_symbol_kind(k: int) -> int {
|
|
if k == LS_UNIT { return 2 }
|
|
if k == LS_COMPONENT { return 23 }
|
|
if k == LS_FIELD { return 8 }
|
|
if k == LS_ARCHETYPE { return 5 }
|
|
if k == LS_CONST { return 14 }
|
|
if k == LS_VAR { return 13 }
|
|
if k == LS_FN or k == LS_EXTERN { return 12 }
|
|
if k == LS_SYSTEM { return 6 }
|
|
if k == LS_UI { return 5 }
|
|
if k == LS_WIDGET { return 14 }
|
|
if k == LS_SCENE { return 3 }
|
|
if k == LS_LAYER { return 3 }
|
|
return 13
|
|
}
|
|
function comp_kind_of(k: int) -> int {
|
|
if k == LS_COMPONENT { return 22 }
|
|
if k == LS_ARCHETYPE { return 7 }
|
|
if k == LS_CONST { return 21 }
|
|
if k == LS_VAR { return 6 }
|
|
if k == LS_FN or k == LS_EXTERN { return 3 }
|
|
if k == LS_SYSTEM { return 23 }
|
|
if k == LS_UI { return 7 }
|
|
if k == LS_WIDGET { return 21 }
|
|
if k == LS_SCENE { return 11 }
|
|
if k == LS_LAYER { return 11 }
|
|
if k == LS_FIELD { return 5 }
|
|
return 6
|
|
}
|
|
|
|
# ---- diagnostics (structural) ----
|
|
function diag_structural(D: Doc, o: Buf) -> void {
|
|
let ds_s = new []int; let ds_e = new []int; let ds_m = new []pointer
|
|
let stack = words(1200); var top = 0
|
|
var i = 0; let N = ntok(D)
|
|
while i < N {
|
|
if D.tk_kind[i] != LT_OP or tlen(D, i) != 1 { i += 1; continue }
|
|
let c = D.src[D.tk_start[i]]
|
|
if c == '{' or c == '(' or c == '[' { if top < 1200 { stack[top] = i; top += 1 } }
|
|
else { if c == '}' or c == ')' or c == ']' {
|
|
if top == 0 { push(ds_s, D.tk_start[i]); push(ds_e, D.tk_end[i]); push(ds_m, concat3("unmatched '", D.src[D.tk_start[i]..D.tk_end[i]], "'")) }
|
|
else { top -= 1; let oi = stack[top]; let oc = D.src[D.tk_start[oi]]
|
|
var wantc = ']'
|
|
if oc == '{' { wantc = '}' } else if oc == '(' { wantc = ')' }
|
|
if wantc != c { push(ds_s, D.tk_start[i]); push(ds_e, D.tk_end[i]); push(ds_m, "mismatched bracket") }
|
|
}
|
|
} }
|
|
i += 1
|
|
}
|
|
var k = 0
|
|
while k < top { let oi = stack[k]; push(ds_s, D.tk_start[oi]); push(ds_e, D.tk_end[oi]); push(ds_m, concat3("unclosed '", D.src[D.tk_start[oi]..D.tk_end[oi]], "'")); k += 1 }
|
|
var j = 0
|
|
while j < len(ds_s) {
|
|
if j > 0 { buf_putc(o, ',') }
|
|
buf_puts(o, "{\"range\":"); put_range(o, D, ds_s[j], ds_e[j]); buf_puts(o, ",\"severity\":1,\"source\":\"ludic\",\"message\":"); jstr_out(o, ds_m[j]); buf_putc(o, '}')
|
|
j += 1
|
|
}
|
|
}
|
|
# the compiler's error, as one whole-line diagnostic from source "ludicc"
|
|
function diag_compiler(D: Doc, o: Buf, first: bool) -> void {
|
|
if (D.cc_msg == null) or D.cc_line < 1 or D.cc_line > len(D.linestart) { return }
|
|
let a = D.linestart[D.cc_line - 1]
|
|
var b = cstr_len(D.src)
|
|
if D.cc_line < len(D.linestart) { b = D.linestart[D.cc_line] - 1 }
|
|
if not first { buf_putc(o, ',') }
|
|
buf_puts(o, "{\"range\":"); put_range(o, D, a, b); buf_puts(o, ",\"severity\":1,\"source\":\"ludicc\",\"message\":"); jstr_out(o, D.cc_msg); buf_putc(o, '}')
|
|
}
|
|
function publish_diag(D: Doc) -> void {
|
|
let o = buf_new()
|
|
buf_puts(o, "{\"uri\":"); jstr_out(o, D.uri); buf_puts(o, ",\"diagnostics\":[")
|
|
if D.is_md == 0 {
|
|
let before = o.len
|
|
diag_structural(D, o)
|
|
diag_compiler(D, o, o.len == before)
|
|
}
|
|
buf_puts(o, "]}")
|
|
send_notify("textDocument/publishDiagnostics", buf_str(o))
|
|
}
|
|
|
|
# ---- compiler diagnostics, on save ----
|
|
# ludicc stops at its first error and reports it as `path:line: error: msg`;
|
|
# run it over the saved document's compilation unit, clear the previous
|
|
# compiler error from every document of that unit, and pin the new one (if
|
|
# any) on the document it names — publishing both so a fixed error clears.
|
|
function compiler_diags(D: Doc) -> void {
|
|
if (g_compiler == null) or D.is_md == 1 { return }
|
|
let root = unit_root(D)
|
|
if (root == null) { return }
|
|
var i = 0
|
|
while i < len(g_docs) { if (g_docs[i].unit == root.unit) { g_docs[i].cc_msg = null; g_docs[i].cc_line = 0 }; i += 1 }
|
|
let errf = `{Os.temp_dir()}/ludic_lsp_cc_{Os.pid()}.txt`
|
|
run(`'{g_compiler}' --emit-llvm '{root.path}' -o /dev/null > /dev/null 2> '{errf}'`)
|
|
let out = read_file(errf)
|
|
run(`rm -f '{errf}'`)
|
|
if (out == null) { return }
|
|
# the first `path:line: error: msg` line
|
|
var a = 0
|
|
while out[a] != 0 {
|
|
var b = a
|
|
while out[b] != 0 and out[b] != '\n' { b += 1 }
|
|
let line = out[a..b]
|
|
let e = Text.index_of(line, ": error: ")
|
|
if e > 0 {
|
|
var c = e - 1
|
|
while c > 0 and line[c - 1] >= '0' and line[c - 1] <= '9' { c -= 1 }
|
|
if c > 1 and line[c - 1] == ':' {
|
|
let target = by_path(line[0..c - 1])
|
|
if (target != null) {
|
|
target.cc_line = Text.to_int(line[c..e])
|
|
target.cc_msg = line[e + 9..cstr_len(line)]
|
|
if (target != D) and target.isopen == 1 { publish_diag(target) }
|
|
}
|
|
}
|
|
return
|
|
}
|
|
a = b
|
|
if out[a] == '\n' { a += 1 }
|
|
}
|
|
}
|
|
function on_did_save(msg: JVal) -> void {
|
|
let D = doc_of(msg)
|
|
if (D == null) { return }
|
|
compiler_diags(D)
|
|
publish_diag(D)
|
|
}
|
|
|
|
# ---- document symbols ----
|
|
function put_doc_symbols(o: Buf, D: Doc, parent: int) -> void {
|
|
buf_putc(o, '[')
|
|
var first = 1
|
|
var i = 0
|
|
while i < len(D.syms) {
|
|
let y = D.syms[i]
|
|
if y.parent == parent and is_outline_kind(y.kind) and (y.name != null) and (cstr_len(y.name) > 0) {
|
|
if first == 0 { buf_putc(o, ',') }
|
|
first = 0
|
|
buf_puts(o, "{\"name\":"); jstr_out(o, y.name)
|
|
buf_puts(o, ",\"detail\":"); jstr_out(o, y.detail)
|
|
buf_puts(o, ",\"kind\":"); buf_puti(o, lsp_symbol_kind(y.kind))
|
|
buf_puts(o, ",\"range\":")
|
|
var a = y.start; var b = y.end
|
|
if y.bend > y.end and y.bstart >= y.start { b = y.bend }
|
|
put_range(o, D, a, b)
|
|
buf_puts(o, ",\"selectionRange\":"); put_range(o, D, y.start, y.end)
|
|
buf_puts(o, ",\"children\":"); put_doc_symbols(o, D, i)
|
|
buf_putc(o, '}')
|
|
}
|
|
i += 1
|
|
}
|
|
buf_putc(o, ']')
|
|
}
|
|
|
|
# ---- hover ----
|
|
function hover_md(o: Buf, code: pointer, docstr: pointer) -> void {
|
|
let m = buf_new()
|
|
buf_puts(m, "```ludic\n"); buf_puts(m, code); buf_puts(m, "\n```")
|
|
if (docstr != null) and (docstr[0] != 0) { buf_puts(m, "\n\n"); buf_puts(m, docstr) }
|
|
buf_puts(o, "{\"contents\":{\"kind\":\"markdown\",\"value\":"); jstr_out(o, buf_str(m)); buf_puts(o, "}}")
|
|
}
|
|
function on_hover(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")
|
|
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 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) {
|
|
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 }
|
|
if D.tk_kind[tok] == LT_PHASE { let o = buf_new(); hover_md(o, name, "A system phase."); send_result(id, buf_str(o)); return }
|
|
if D.tk_kind[tok] == LT_TYPE { let o = buf_new(); hover_md(o, name, "A built-in type."); send_result(id, buf_str(o)); return }
|
|
send_result(id, "null")
|
|
}
|
|
|
|
# ---- definition ----
|
|
function on_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")
|
|
let tok = tok_at(D, off)
|
|
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 {
|
|
# 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))
|
|
}
|
|
|
|
# ---- occurrences (references / rename) ----
|
|
var g_occ_doc: []Doc
|
|
var g_occ_tok: []int
|
|
function find_occurrences(from: Doc, tdoc: Doc, tsym: int) -> void {
|
|
g_occ_doc = new []Doc; g_occ_tok = new []int
|
|
let want = tdoc.syms[tsym].name
|
|
var docs = new []Doc
|
|
if is_local_kind(tdoc.syms[tsym].kind) { push(docs, from) } else { docs = related(from) }
|
|
var i = 0
|
|
while i < len(docs) {
|
|
let T = docs[i]
|
|
var k = 0
|
|
while k < ntok(T) {
|
|
if T.tk_kind[k] == LT_ID and (ttext(T, k) == want) {
|
|
resolve_tok(T, k)
|
|
if (g_owner != null) and (g_owner.path == tdoc.path) and g_symidx == tsym { push(g_occ_doc, T); push(g_occ_tok, k) }
|
|
}
|
|
k += 1
|
|
}
|
|
i += 1
|
|
}
|
|
}
|
|
function on_references(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 i = 0
|
|
while i < len(g_occ_doc) {
|
|
if i > 0 { buf_putc(o, ',') }
|
|
let T = g_occ_doc[i]; let k = g_occ_tok[i]
|
|
put_location(o, T, T.tk_start[k], T.tk_end[k])
|
|
i += 1
|
|
}
|
|
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)
|
|
if (D == null) or (newname == null) or (newname[0] == 0) { 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 { send_result(id, "null"); return }
|
|
let tdoc = g_owner; let tsym = g_symidx
|
|
find_occurrences(D, tdoc, tsym)
|
|
# group by doc
|
|
let seen = new []Doc
|
|
let o = buf_new(); buf_puts(o, "{\"changes\":{")
|
|
var gi = 0
|
|
while gi < len(g_occ_doc) {
|
|
var already = false
|
|
var s = 0
|
|
while s < len(seen) { if (seen[s].path == g_occ_doc[gi].path) { already = true }; s += 1 }
|
|
if not already {
|
|
push(seen, g_occ_doc[gi])
|
|
if len(seen) > 1 { buf_putc(o, ',') }
|
|
jstr_out(o, g_occ_doc[gi].uri); buf_putc(o, ':'); buf_putc(o, '[')
|
|
var first = 1
|
|
var k = 0
|
|
while k < len(g_occ_doc) {
|
|
if (g_occ_doc[k].path == g_occ_doc[gi].path) {
|
|
if first == 0 { buf_putc(o, ',') }
|
|
first = 0
|
|
let T = g_occ_doc[k]; let tk = g_occ_tok[k]
|
|
buf_puts(o, "{\"range\":"); put_range(o, T, T.tk_start[tk], T.tk_end[tk]); buf_puts(o, ",\"newText\":"); jstr_out(o, newname); buf_putc(o, '}')
|
|
}
|
|
k += 1
|
|
}
|
|
buf_putc(o, ']')
|
|
}
|
|
gi += 1
|
|
}
|
|
buf_puts(o, "}}"); send_result(id, buf_str(o))
|
|
}
|
|
|
|
# ---- 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 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) {
|
|
var k = 0
|
|
while k < len(rel[r].syms) {
|
|
let y = rel[r].syms[k]
|
|
if is_toplevel_kind(y.kind) and y.kind != LS_UNIT {
|
|
var okk = true
|
|
if types_only == 1 and not (y.kind == LS_COMPONENT or y.kind == LS_ARCHETYPE) { okk = false }
|
|
if okk { comp_item(o, y.name, comp_kind_of(y.kind), y.detail) }
|
|
}
|
|
k += 1
|
|
}
|
|
r += 1
|
|
}
|
|
}
|
|
function comp_types(o: Buf) -> void {
|
|
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 { 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 }
|
|
let off = off_of(msg, D, "params.position")
|
|
let o = buf_new()
|
|
buf_puts(o, "{\"isIncomplete\":false,\"items\":[")
|
|
g_first = 1
|
|
var tok = -1
|
|
var i = ntok(D) - 1
|
|
while i >= 0 {
|
|
let k = D.tk_kind[i]
|
|
if k != LT_NL and k != LT_EOF and k != LT_COMMENT { if D.tk_end[i] <= off { tok = i; break } }
|
|
i -= 1
|
|
}
|
|
var anchor = tok
|
|
if anchor >= 0 and D.tk_kind[anchor] == LT_ID and D.tk_end[anchor] >= off { anchor = prev_sig(D, anchor) }
|
|
var in_qt = 0
|
|
var depth = 0
|
|
var j = anchor
|
|
while j >= 0 {
|
|
if D.tk_kind[j] == LT_OP and tlen(D, j) == 1 {
|
|
let ch = D.src[D.tk_start[j]]
|
|
if ch == ']' { depth += 1 }
|
|
else { if ch == '[' { if depth == 0 { in_qt = 1; j = -1 } else { depth -= 1 } }
|
|
else { if ch == '{' or ch == '}' { j = -1 } } }
|
|
}
|
|
j -= 1
|
|
}
|
|
var done = 0
|
|
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
|
|
var k = 0
|
|
while k < len(co.syms) { if co.syms[k].kind == LS_FIELD and co.syms[k].parent == comp { comp_item(o, co.syms[k].name, 5, co.syms[k].detail) }; k += 1 }
|
|
}
|
|
done = 1
|
|
}
|
|
else { if tis(D, anchor, "phase") { comp_item(o, "Start", 13, ""); comp_item(o, "Input", 13, ""); comp_item(o, "FixedUpdate", 13, ""); comp_item(o, "Update", 13, ""); comp_item(o, "LateUpdate", 13, ""); comp_item(o, "Render", 13, ""); done = 1 }
|
|
else { if tis(D, anchor, ":") or tis(D, anchor, "->") { comp_types(o); comp_scope(o, D, off, 1); done = 1 }
|
|
else { if tis(D, anchor, "spawn") { comp_scope(o, D, off, 1); done = 1 }
|
|
else { if D.tk_kind[anchor] == LT_OP and tlen(D, anchor) == 1 and D.src[D.tk_start[anchor]] == 64 { comp_item(o, "deterministic", 14, "@deterministic"); done = 1 }
|
|
} } } }
|
|
} }
|
|
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 {
|
|
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))
|
|
}
|
|
|
|
# ---- inlay hints, signature help, semantic tokens, folding ----
|
|
function on_inlay_hint(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 = 1
|
|
var i = 0
|
|
while i < len(D.syms) {
|
|
let y = D.syms[i]
|
|
if y.kind == LS_QUERYVAR and (y.ty != null) and (y.ty[0] != 0) {
|
|
if first == 0 { buf_putc(o, ',') }
|
|
first = 0
|
|
buf_puts(o, "{\"position\":"); put_pos(o, D, y.end); buf_puts(o, ",\"label\":"); jstr_out(o, concat3(": ", y.ty, "")); buf_puts(o, ",\"kind\":1,\"paddingLeft\":false}")
|
|
}
|
|
i += 1
|
|
}
|
|
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 }
|
|
classify(D)
|
|
let o = buf_new(); buf_puts(o, "{\"data\":[")
|
|
var prev_line = 0; var prev_char = 0; var first = 1
|
|
var i = 0; let N = ntok(D)
|
|
while i < N {
|
|
let k = D.tk_kind[i]
|
|
if k != LT_NL and k != LT_EOF {
|
|
let ty = sem_type_of(D.cls[i])
|
|
if ty >= 0 {
|
|
let line = D.tk_line[i]
|
|
let ch = col_of(D, D.tk_start[i])
|
|
let ln = utf16_len(D.text, D.tk_start[i], tlen(D, i))
|
|
if ln > 0 {
|
|
let dl = line - prev_line
|
|
var dc = ch
|
|
if dl == 0 { dc = ch - prev_char }
|
|
if first == 0 { buf_putc(o, ',') }
|
|
first = 0
|
|
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
|
|
}
|
|
}
|
|
}
|
|
i += 1
|
|
}
|
|
buf_puts(o, "]}"); send_result(id, buf_str(o))
|
|
}
|
|
function on_folding(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 = 1
|
|
var i = 0; let N = ntok(D)
|
|
while i < N {
|
|
if D.tk_kind[i] == LT_OP and tlen(D, i) == 1 and D.src[D.tk_start[i]] == 123 {
|
|
let m = D.tmatch[i]
|
|
if m >= 0 {
|
|
let a = D.tk_line[i]; let b = D.tk_line[m]
|
|
if b > a {
|
|
if first == 0 { buf_putc(o, ',') }
|
|
first = 0
|
|
buf_puts(o, "{\"startLine\":"); buf_puti(o, a); buf_puts(o, ",\"endLine\":"); buf_puti(o, b - 1); buf_puts(o, ",\"kind\":\"region\"}")
|
|
}
|
|
}
|
|
}
|
|
i += 1
|
|
}
|
|
buf_putc(o, ']'); send_result(id, buf_str(o))
|
|
}
|
|
function on_workspace_symbol(msg: JVal, id: JVal) -> void {
|
|
let q = jstr(jpath(msg, "params.query"), "")
|
|
let o = buf_new(); buf_putc(o, '['); var first = 1
|
|
var d = 0
|
|
while d < len(g_docs) {
|
|
let D = g_docs[d]
|
|
var k = 0
|
|
while k < len(D.syms) {
|
|
let y = D.syms[k]
|
|
if is_outline_kind(y.kind) and (y.name != null) and (cstr_len(y.name) > 0) {
|
|
var keep = true
|
|
if q[0] != 0 { keep = contains_ci(y.name, q) }
|
|
if keep {
|
|
if first == 0 { buf_putc(o, ',') }
|
|
first = 0
|
|
buf_puts(o, "{\"name\":"); jstr_out(o, y.name); buf_puts(o, ",\"kind\":"); buf_puti(o, lsp_symbol_kind(y.kind)); buf_puts(o, ",\"location\":"); put_location(o, D, y.start, y.end); buf_putc(o, '}')
|
|
}
|
|
}
|
|
k += 1
|
|
}
|
|
d += 1
|
|
}
|
|
buf_putc(o, ']'); send_result(id, buf_str(o))
|
|
}
|
|
function contains_ci(hay: pointer, needle: pointer) -> bool {
|
|
let hn = cstr_len(hay); let nn = cstr_len(needle)
|
|
if nn == 0 { return true }
|
|
var i = 0
|
|
while i + nn <= hn {
|
|
var m = true
|
|
var j = 0
|
|
while j < nn { if lower(hay[i + j]) != lower(needle[j]) { m = false; break }; j += 1 }
|
|
if m { return true }
|
|
i += 1
|
|
}
|
|
return false
|
|
}
|
|
function on_formatting(msg: JVal, id: JVal) -> void {
|
|
let D = doc_of(msg)
|
|
if (D == null) { send_result(id, "null"); return }
|
|
var tab = jint(jpath(msg, "params.options.tabSize"), 2)
|
|
if tab < 1 or tab > 8 { tab = 2 }
|
|
var out = null
|
|
if D.is_md == 1 { out = format_md(D.raw, tab) }
|
|
else { let F = make_text_doc(D.raw); out = format_doc(F, tab) }
|
|
let endline = len(D.linestart) - 1
|
|
let ls0 = D.linestart[endline]
|
|
let e0 = cstr_len(D.text)
|
|
let endchar = utf16_len(D.text, ls0, e0 - ls0)
|
|
let o = buf_new()
|
|
buf_puts(o, "[{\"range\":{\"start\":{\"line\":0,\"character\":0},\"end\":{\"line\":"); buf_puti(o, endline); buf_puts(o, ",\"character\":"); buf_puti(o, endchar); buf_puts(o, "}},\"newText\":"); jstr_out(o, out); buf_puts(o, "}]")
|
|
send_result(id, buf_str(o))
|
|
}
|
|
|
|
# ---- document lifecycle ----
|
|
function on_did_open(msg: JVal) -> void {
|
|
let uri = jstr(jpath(msg, "params.textDocument.uri"), null)
|
|
let text = jstr(jpath(msg, "params.textDocument.text"), "")
|
|
if (uri == null) { return }
|
|
let path = uri_to_path(uri)
|
|
var D = by_path(path)
|
|
if (D == null) { D = new_doc(path, text); push(g_docs, D) }
|
|
else { set_text(D, text); reindex(D) }
|
|
D.isopen = 1
|
|
publish_diag(D)
|
|
}
|
|
function on_did_change(msg: JVal) -> void {
|
|
let D = doc_of(msg)
|
|
if (D == null) { return }
|
|
let changes = jpath(msg, "params.contentChanges")
|
|
let last = jat(changes, jlen(changes) - 1)
|
|
let text = jstr(jget(last, "text"), null)
|
|
if (text == null) { return }
|
|
set_text(D, text)
|
|
reindex(D)
|
|
publish_diag(D)
|
|
}
|
|
function on_did_close(msg: JVal) -> void {
|
|
let D = doc_of(msg)
|
|
if (D == null) { return }
|
|
D.isopen = 0
|
|
let disk = read_file(D.path)
|
|
if (disk != null) { set_text(D, disk); reindex(D) }
|
|
}
|
|
|
|
function caps() -> pointer {
|
|
let o = buf_new()
|
|
buf_puts(o, "{\"capabilities\":{")
|
|
buf_puts(o, "\"positionEncoding\":\"utf-16\"")
|
|
buf_puts(o, ",\"textDocumentSync\":{\"openClose\":true,\"change\":1,\"save\":{\"includeText\":false}}")
|
|
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")
|
|
buf_puts(o, ",\"hoverProvider\":true")
|
|
buf_puts(o, ",\"renameProvider\":{\"prepareProvider\":true}")
|
|
buf_puts(o, ",\"documentFormattingProvider\":true")
|
|
buf_puts(o, ",\"documentRangeFormattingProvider\":true")
|
|
buf_puts(o, ",\"foldingRangeProvider\":true")
|
|
buf_puts(o, ",\"documentLinkProvider\":{\"resolveProvider\":false}")
|
|
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\",\"vendor\",\"static\"]},\"full\":true,\"range\":true}")
|
|
buf_puts(o, "},\"serverInfo\":{\"name\":\"ludic-lsp\",\"version\":\"1.0.0\"}}")
|
|
return buf_str(o)
|
|
}
|
|
# the ludicc that ships beside this server binary, when there is one
|
|
function compiler_beside_me() -> pointer {
|
|
let me = arg(0)
|
|
var i = cstr_len(me)
|
|
while i > 0 and me[i - 1] != '/' { i -= 1 }
|
|
let cand = me[0..i] + "ludicc"
|
|
if run(`test -x '{cand}'`) == 0 { return cand }
|
|
return null
|
|
}
|
|
function on_initialize(msg: JVal, id: JVal) -> void {
|
|
let root = jpath(msg, "params.rootUri")
|
|
if (root != null) and root.t == JSTR { g_root = uri_to_path(root.s) }
|
|
if (g_root != null) { scan_root(g_root) }
|
|
let want = jpath(msg, "params.initializationOptions.compilerDiagnostics")
|
|
if (want != null) and want.t == JBOOL and want.b == 1 {
|
|
g_compiler = jstr(jpath(msg, "params.initializationOptions.compilerPath"), null)
|
|
if (g_compiler == null) or cstr_len(g_compiler) == 0 { g_compiler = compiler_beside_me() }
|
|
}
|
|
send_result(id, caps())
|
|
}
|
|
|
|
function handle(msg: JVal) -> void {
|
|
let method = jstr(jget(msg, "method"), null)
|
|
let id = jget(msg, "id")
|
|
if (method == null) { return }
|
|
if (method == "initialize") { on_initialize(msg, id); return }
|
|
if (method == "initialized") { return }
|
|
if (method == "shutdown") { g_shutdown = 1; send_result(id, "null"); return }
|
|
if (method == "exit") { if g_shutdown == 1 { exit(0) }; exit(1) }
|
|
if (method == "textDocument/didOpen") { on_did_open(msg); return }
|
|
if (method == "textDocument/didChange") { on_did_change(msg); return }
|
|
if (method == "textDocument/didClose") { on_did_close(msg); return }
|
|
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") { 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_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_help2(msg, id); return }
|
|
if (method == "textDocument/completion") { on_completion(msg, 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") }
|
|
}
|
|
|
|
entry {
|
|
init_builtins()
|
|
g_docs = new []Doc
|
|
while true {
|
|
let raw = read_message()
|
|
if (raw == null) { break }
|
|
jsrc = raw; jp = 0
|
|
let msg = jparse()
|
|
handle(msg)
|
|
}
|
|
}
|
|
}
|