An action names one row of a ludic.base Table<T> by its handle, in a field marked @Target, and `reducer Deer in Herd.deer on Spook(r: mut Row<Deer>, n: Noise, a: Spook)` runs once, for that row alone (the table may sit down a path, S.w.tab). The drain resolves the handle (tb_row) and hands the reducer a Row<T> - new in ludic.base: tb, row, h, rec - that the queue keeps, one per row reducer, filled in place, so a targeted action allocates nothing; a stale handle runs nothing, and LUDIC_ACTIONS_LOG=1 prints a line for it (@alloc_ok). Row reducers order among an action's by their state's name, then the table's path. Checked at compile time (actions_rows.ludic): the row reaches r.rec and r.h only - r.tb / r.row refused, the view never assigned, stored, copied or handed on except to a @RowVerb (a function of the record's own module taking Row<T> first; any other function taking a row is refused); a field marked @Column (a table column mirrors it) is not written through r.rec; only the module owning the state declares a row reducer; one @Target, an int, per action; the states between the row and the action are read. `mut` is allowed on a Row<T> parameter. ludic schema's code section gains row_reducers (record, table, state, action, target, predicted, net, module, at) and row_verbs (name, record, module, at), and every action its target; row reducers are left out of `reducers`. ludic deps and ludic-lsp name a row reducer `reducer Deer in Herd.deer on Spook`. vocab: @Target, @Column, @RowVerb; docs/language pages; LANGUAGE.md "A reducer on a row"; examples actions/rows and ten rejects; test.ludic feat, reject and schema cases (not run); changes/row-reducers.md. Reseeded; bootstrap-cfree fixpoint holds (307497 lines); Maroon Lake's `ludic build --check` is clean against this tree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3186 lines
139 KiB
Text
3186 lines
139 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
|
|
const LS_REGISTRY: int = 18 # registry R of T: a table of named entries
|
|
const LS_ENUM: int = 19
|
|
const LS_ENUMMEMBER: int = 20
|
|
const LS_VIEW: int = 21 # a UI component or a view
|
|
const LS_REDUCER: int = 22 # reducer S on A: named "S on A" in the outline
|
|
|
|
# ---- 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
|
|
decl: pointer = null # the word that declared it (property, state, event, action, port, ...)
|
|
}
|
|
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 == "Key") or (w == "pointer") or (w == "void") or (w == "entity") { return true }
|
|
if (w == "countdown") or (w == "words") or (w == "floats") or (w == "doubles") or (w == "fixeds") or (w == "pointers") { return true }
|
|
if (w == "Vector") or (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; s.decl = ""
|
|
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() }
|
|
# past a bracketed group, staying on its line (skip_group also takes the newlines after it)
|
|
function lp_jump_group() -> void { let m = cur_match(); if m < 0 { pi += 1 } else { pi = m + 1 } }
|
|
# the tokens up to the end of this line as written, joined by single spaces (after a leading one)
|
|
function lp_rest_of_line() -> pointer {
|
|
let b = buf_new()
|
|
while pi < ntok(pD) and pk() != LT_EOF and pk() != LT_NL {
|
|
if pk() != LT_COMMENT { buf_putc(b, ' '); buf_puts(b, ttext(pD, pi)) }
|
|
pi += 1
|
|
}
|
|
return buf_str(b)
|
|
}
|
|
# a component's or a view's body: its props and state (and a view's fields) as fields, its
|
|
# functions and events as functions
|
|
function lp_view_body(sidx: int) -> void {
|
|
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("function") { lp_decl(sidx) }
|
|
else if pis("on") {
|
|
padv()
|
|
let nt = pname()
|
|
if nt >= 0 {
|
|
let f = sym_add(LS_FN, nt, sidx)
|
|
pD.syms[f].detail = "on " + pD.syms[f].name
|
|
if pis("(") { lp_params(f, LS_PARAM, buf_new()) }
|
|
}
|
|
while pi < ntok(pD) and pk() != LT_EOF and not pis("{") and pk() != LT_NL { pi += 1 }
|
|
if pis("{") { skip_group() }
|
|
}
|
|
else {
|
|
var word = ""
|
|
if pis("prop") or pis("state") { word = pword(); padv() }
|
|
let ft = pname()
|
|
if ft >= 0 and (pis(":") or pis("=")) {
|
|
let f = sym_add(LS_FIELD, ft, sidx)
|
|
if pis(":") { padv(); pD.syms[f].ty = lp_type_text() }
|
|
var head = word
|
|
if (head == "") { head = pD.syms[sidx].name + "." }
|
|
else { head = head + " " }
|
|
pD.syms[f].detail = head + pD.syms[f].name + ": " + pD.syms[f].ty
|
|
}
|
|
while pi < ntok(pD) and pk() != LT_EOF and pk() != LT_NL and not pis(",") and not pis("}") {
|
|
if close >= 0 and pi >= close { break }
|
|
if pis("(") or pis("{") or pis("[") { lp_jump_group() } else { pi += 1 }
|
|
}
|
|
}
|
|
if pi == before { padv() } else { pskipnl() }
|
|
if pis(",") { padv() }
|
|
}
|
|
if close >= 0 { pi = close + 1 } else { padv() }
|
|
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 "" }
|
|
var ty = pre + ttext(pD, tt)
|
|
# a function type, `fn(int, float) -> bool`, and a generic's arguments, `Pool<Item>`, as written
|
|
if (ttext(pD, tt) == "fn") and pis("(") {
|
|
let m = cur_match()
|
|
if m >= 0 {
|
|
ty = ty + pD.src[pD.tk_start[pi]..pD.tk_end[m]]
|
|
pi = m + 1
|
|
if pis("->") { pi += 1; ty = ty + " -> " + lp_type_text() }
|
|
}
|
|
}
|
|
else if pis("<") {
|
|
let a = pi
|
|
var depth = 0
|
|
while pi < ntok(pD) and pk() != LT_EOF and pk() != LT_NL {
|
|
if pis("<") { depth += 1 }
|
|
if pis(">") { depth -= 1 }
|
|
pi += 1
|
|
if depth == 0 { break }
|
|
}
|
|
ty = ty + pD.src[pD.tk_start[a]..pD.tk_end[pi - 1]]
|
|
}
|
|
return ty
|
|
}
|
|
|
|
# (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()
|
|
}
|
|
}
|
|
|
|
var lp_anchor: int = -1 # where a declaration begins when `export`, `unsafe` or @attributes lead it
|
|
|
|
# one top-level declaration
|
|
function lp_decl(parent: int) -> void {
|
|
let w = pword()
|
|
var decl_tok = pi
|
|
if lp_anchor >= 0 { decl_tok = lp_anchor; lp_anchor = -1 }
|
|
# a lead-in: @Attr(args) (a registry's @AppendOnly, a handler's @On(E)), `export` or `unsafe`
|
|
# before a declaration - read past, the declaration keeps where it began and says it is exported
|
|
if pk() == LT_ANNO {
|
|
padv()
|
|
if pis("(") { skip_group() }
|
|
lp_anchor = decl_tok
|
|
lp_decl(parent)
|
|
return
|
|
}
|
|
if (w == "export" or w == "unsafe") and pi + 1 < ntok(pD) and not tis(pD, pi + 1, "function") and not tis(pD, pi + 1, "pure") {
|
|
let first = len(pD.syms)
|
|
padv()
|
|
lp_anchor = decl_tok
|
|
lp_decl(parent)
|
|
if w == "export" {
|
|
var q = first
|
|
while q < len(pD.syms) { if pD.syms[q].parent == parent { pD.syms[q].exported = 1 }; q += 1 }
|
|
}
|
|
return
|
|
}
|
|
|
|
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
|
|
}
|
|
let wd = w
|
|
# records: a property, a state (one instance), an event, an action, a port (its members are
|
|
# fn-typed), a model (its members are components)
|
|
if (wd == "property") or (wd == "model") or (wd == "state") or (wd == "event") or (wd == "action") or (wd == "port") {
|
|
let is_comp = not (wd == "model")
|
|
padv()
|
|
if wd == "event" and pis("cancellable") { 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].decl = wd
|
|
pD.syms[sidx].doc = collect_doc(pD, decl_tok)
|
|
if pis("<") { while pi < ntok(pD) and not pis(">") and pk() != LT_EOF { padv() }; padv() } # property Pool<T>
|
|
while pk() == LT_ANNO { padv(); if pis("(") { skip_group() } }
|
|
let det = buf_new()
|
|
buf_puts(det, wd); 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(); if pis("(") { skip_group() } } # @Ref(Items), @Range(0, 1), @Sync ...
|
|
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))
|
|
# the rest of the member - a fn type's parameters and result, a generic's arguments, the
|
|
# default - ends at a comma, the closing brace or the end of its line
|
|
while pi < ntok(pD) and pk() != LT_EOF and pk() != LT_NL and not pis(",") and not pis("}") {
|
|
if close >= 0 and pi >= close { break }
|
|
if pis("(") or pis("{") or pis("[") { lp_jump_group() } else { pi += 1 }
|
|
}
|
|
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 == "open" and tis(pD, pi + 1, "registry") { padv(); lp_anchor = decl_tok; lp_decl(parent); return }
|
|
if w == "registry" { # registry Name of Record [as P] [from "file.lres"]
|
|
padv(); let nt = pname(); if nt < 0 { return }
|
|
let sidx = sym_add(LS_REGISTRY, nt, parent)
|
|
pD.syms[sidx].decl = "registry"
|
|
pD.syms[sidx].doc = collect_doc(pD, decl_tok)
|
|
if pis("of") { pD.syms[sidx].ty = ttext(pD, pi + 1) } # its record, for go-to-type
|
|
pD.syms[sidx].detail = "registry " + pD.syms[sidx].name + lp_rest_of_line()
|
|
pskipnl()
|
|
return
|
|
}
|
|
if w == "enum" { # enum Name { A, B = 2, C }
|
|
padv(); let nt = pname(); if nt < 0 { return }
|
|
let sidx = sym_add(LS_ENUM, nt, parent)
|
|
pD.syms[sidx].decl = "enum"
|
|
pD.syms[sidx].doc = collect_doc(pD, decl_tok)
|
|
pD.syms[sidx].detail = "enum " + 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] }
|
|
padv()
|
|
while pi < ntok(pD) and pk() != LT_EOF and not pis("}") {
|
|
if close >= 0 and pi >= close { break }
|
|
let mt = pname()
|
|
if mt < 0 { padv(); continue }
|
|
let m = sym_add(LS_ENUMMEMBER, mt, sidx)
|
|
pD.syms[m].detail = pD.syms[sidx].name + "." + pD.syms[m].name
|
|
while pi < ntok(pD) and pk() != LT_EOF and pk() != LT_NL and not pis(",") and not pis("}") { pi += 1 }
|
|
if pis(",") { padv() } else { pskipnl() }
|
|
}
|
|
if close >= 0 { pi = close + 1 } else { padv() }
|
|
pskipnl()
|
|
}
|
|
return
|
|
}
|
|
if (w == "component") or (w == "view") { # component Name[(states)] { prop / state / function / on }
|
|
padv(); let nt = pname(); if nt < 0 { return }
|
|
let sidx = sym_add(LS_VIEW, nt, parent)
|
|
pD.syms[sidx].decl = w
|
|
pD.syms[sidx].doc = collect_doc(pD, decl_tok)
|
|
pD.syms[sidx].detail = w + " " + pD.syms[sidx].name
|
|
if pis("(") { lp_params(sidx, LS_PARAM, buf_new()) }
|
|
if pis("{") { lp_view_body(sidx) }
|
|
return
|
|
}
|
|
if w == "reducer" { # reducer State on Action(states, a: Action) { ... }
|
|
padv(); let nt = pname(); if nt < 0 { return }
|
|
let sidx = sym_add(LS_REDUCER, nt, parent)
|
|
pD.syms[sidx].decl = "reducer"
|
|
pD.syms[sidx].doc = collect_doc(pD, decl_tok)
|
|
if pis("in") { # 27.3: reducer Record in State.table on Action - "Record in State.table on Action"
|
|
padv()
|
|
let st = pname()
|
|
if st >= 0 { pD.syms[sidx].name = pD.syms[sidx].name + " in " + ttext(pD, st) }
|
|
while st >= 0 and pis(".") {
|
|
padv()
|
|
let ft = pname()
|
|
if ft < 0 { break }
|
|
pD.syms[sidx].name = pD.syms[sidx].name + "." + ttext(pD, ft)
|
|
}
|
|
}
|
|
if pis("on") { padv(); let at = pname(); if at >= 0 { pD.syms[sidx].name = pD.syms[sidx].name + " on " + ttext(pD, at) } }
|
|
pD.syms[sidx].detail = "reducer " + pD.syms[sidx].name
|
|
if pis("(") { lp_params(sidx, LS_PARAM, buf_new()) }
|
|
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
|
|
}
|
|
# a def's entry and a bind's answers are data: their bodies hold no declaration
|
|
if (w == "def") or (w == "bind") {
|
|
padv()
|
|
while pi < ntok(pD) and pk() != LT_EOF and pk() != LT_NL and not pis("{") { pi += 1 }
|
|
if pis("{") { skip_group() } else { pskipnl() }
|
|
return
|
|
}
|
|
# a line that says what a file is: module / friend module / numbers float
|
|
if (w == "module") or (w == "friend") or (w == "numbers") {
|
|
while pi < ntok(pD) and pk() != LT_EOF and pk() != LT_NL { pi += 1 }
|
|
pskipnl()
|
|
return
|
|
}
|
|
if (w == "const") or (w == "var") or (w == "let") {
|
|
var kind = LS_VAR
|
|
if (w == "const") or (w == "let") { 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 or k == LS_REGISTRY or k == LS_ENUM or k == LS_VIEW
|
|
}
|
|
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 or k == LS_REGISTRY or k == LS_ENUM or k == LS_ENUMMEMBER or k == LS_VIEW or k == LS_REDUCER
|
|
}
|
|
|
|
# ---- 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
|
|
}
|
|
# a name the whole unit sees: a top-level kind, and not a member of a component or a view
|
|
function is_top_sym(D: Doc, k: int) -> bool {
|
|
let y = D.syms[k]
|
|
if not is_toplevel_kind(y.kind) { return false }
|
|
return y.parent < 0 or D.syms[y.parent].kind != LS_VIEW
|
|
}
|
|
# the field an attribute's argument names - `@Node(model)`, `@Clip(model)` - in the record the
|
|
# attribute is written in, or -1 (a registry, `@Ref(Items)`, and a constant resolve as any name)
|
|
function attr_arg_field(D: Doc, tok: int) -> int {
|
|
var p = prev_sig(D, tok)
|
|
while p >= 0 and (tis(D, p, ",") or D.tk_kind[p] == LT_ID or D.tk_kind[p] == LT_STR or D.tk_kind[p] == LT_INT or D.tk_kind[p] == LT_FLOAT) { p = prev_sig(D, p) }
|
|
if p < 0 or not tis(D, p, "(") { return -1 }
|
|
let a = prev_sig(D, p)
|
|
if a < 0 or D.tk_kind[a] != LT_ANNO { return -1 }
|
|
let off = D.tk_start[tok]
|
|
let name = ttext(D, tok)
|
|
var rec = -1
|
|
var i = 0
|
|
while i < len(D.syms) {
|
|
let y = D.syms[i]
|
|
if (y.kind == LS_COMPONENT or y.kind == LS_ARCHETYPE) and off >= y.bstart and off <= y.bend and y.bend > y.bstart { rec = i }
|
|
i += 1
|
|
}
|
|
if rec < 0 { return -1 }
|
|
i = 0
|
|
while i < len(D.syms) {
|
|
if D.syms[i].kind == LS_FIELD and D.syms[i].parent == rec and (D.syms[i].name == name) { return i }
|
|
i += 1
|
|
}
|
|
return -1
|
|
}
|
|
# 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_top_sym(rel[i], k) 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_top_sym(g_docs[i], k) 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 af = attr_arg_field(D, tok)
|
|
if af >= 0 { g_owner = D; g_symidx = af; 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 }
|
|
if k == LS_REGISTRY or k == LS_ENUMMEMBER { return SC_CONST }
|
|
if k == LS_ENUM { return SC_TYPE }
|
|
if k == LS_VIEW { return SC_UI }
|
|
if k == LS_REDUCER { return SC_FUNCTION }
|
|
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 }
|
|
if k == LS_REGISTRY { return 18 }
|
|
if k == LS_ENUM { return 10 }
|
|
if k == LS_ENUMMEMBER { return 22 }
|
|
if k == LS_VIEW { return 5 }
|
|
if k == LS_REDUCER { return 12 }
|
|
return 13
|
|
}
|
|
# a symbol's LSP SymbolKind: a record says which it is by the word that declared it
|
|
function sym_lsp_kind(y: Sym) -> int {
|
|
if y.kind == LS_COMPONENT {
|
|
if (y.decl == "event") or (y.decl == "action") { return 24 }
|
|
if (y.decl == "port") { return 11 }
|
|
}
|
|
return lsp_symbol_kind(y.kind)
|
|
}
|
|
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 }
|
|
if k == LS_REGISTRY { return 21 }
|
|
if k == LS_ENUM { return 13 }
|
|
if k == LS_ENUMMEMBER { return 20 }
|
|
if k == LS_VIEW { return 7 }
|
|
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, sym_lsp_kind(y))
|
|
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, sym_lsp_kind(y)); 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)
|
|
}
|
|
}
|
|
}
|