L
lingcoder
↓ 0 downloadsLingCoder: a Geany / VS Code-style IDE for Ling, written in Ling. Explorer, tabs, syntax-coloured editor with lint, find in files, command palette, run/build panel, and hot-reloaded extensions - including a Claude assistant.
Keywords ide, editor, ling, claude, extensions
lingfu install lingcoderCode / Files (34)
ling/editor.ling
# editor.ling - the code editor view: syntax-coloured text, cursor + selection, typing,
# clipboard, line operations, the find/replace bar and mouse handling.
#
# Typing rides the runtime's text buffer: the text BEFORE the cursor on the current line is
# kept in text_set/text_poll (st "ed_expect"), so typed characters and backspaces (with key
# repeat) arrive as a diff against it each frame. Cursor keys, Enter, Delete and friends are
# handled with key_rep (key_pressed + a repeat timer).
bind ED_FS = 13.0
fn ed_lh() { return floor(19.0 * text_k()) }
# ── key repeat ────────────────────────────────────────────────────────────────
fn key_rep(name) {
if key_pressed(name) {
st_set("kr_t0_" + name, st_get("now", 0.0))
st_set("kr_n_" + name, 0.0)
return true
}
if !key_down(name) { return false }
bind held = st_get("now", 0.0) - st_get("kr_t0_" + name, 0.0)
if held < 0.3 { return false }
bind fired = st_get("kr_n_" + name, 0.0)
bind due = floor((held - 0.3) / 0.024)
if due > fired {
st_set("kr_n_" + name, due)
return true
}
return false
}
# ── selection ─────────────────────────────────────────────────────────────────
fn ed_has_sel(bi) {
bind al = st_get(buf_key(bi, "al"), 0.0 - 1.0)
if al < 0.0 { return false }
return al != st_get(buf_key(bi, "cl"), 0.0) || st_get(buf_key(bi, "ac"), 0.0) != st_get(buf_key(bi, "cc"), 0.0)
}
# [l0, c0, l1, c1] in document order (l0 = -1 when no selection)
fn ed_sel_range(bi) {
if !ed_has_sel(bi) { return [0.0 - 1.0, 0.0, 0.0, 0.0] }
bind al = st_get(buf_key(bi, "al"), 0.0)
bind ac = st_get(buf_key(bi, "ac"), 0.0)
bind cl = st_get(buf_key(bi, "cl"), 0.0)
bind cc = st_get(buf_key(bi, "cc"), 0.0)
if al < cl || (al == cl && ac <= cc) { return [al, ac, cl, cc] }
return [cl, cc, al, ac]
}
fn ed_sel_count(bi) {
if bi < 0.0 { return 0.0 }
bind r = ed_sel_range(bi)
if list_get(r, 0) < 0.0 { return 0.0 }
bind l0 = list_get(r, 0)
bind c0 = list_get(r, 1)
bind l1 = list_get(r, 2)
bind c1 = list_get(r, 3)
if l0 == l1 { return c1 - c0 }
bind n = str_len(buf_line(bi, l0)) - c0 + 1.0
bind i = l0 + 1.0
while i < l1 {
bind n = n + str_len(buf_line(bi, i)) + 1.0
bind i = i + 1.0
}
return n + c1
}
fn ed_sel_text(bi) {
bind r = ed_sel_range(bi)
if list_get(r, 0) < 0.0 { return "" }
bind l0 = list_get(r, 0)
bind c0 = list_get(r, 1)
bind l1 = list_get(r, 2)
bind c1 = list_get(r, 3)
if l0 == l1 { return str_slice(buf_line(bi, l0), c0, c1 - c0) }
bind s = str_slice(buf_line(bi, l0), c0, str_len(buf_line(bi, l0)) - c0)
bind i = l0 + 1.0
while i < l1 {
bind s = s + "\n" + buf_line(bi, i)
bind i = i + 1.0
}
return s + "\n" + str_slice(buf_line(bi, l1), 0.0, c1)
}
fn ed_clear_sel(bi) { st_set(buf_key(bi, "al"), 0.0 - 1.0) }
fn ed_anchor_if(bi, extend) {
if extend {
if st_get(buf_key(bi, "al"), 0.0 - 1.0) < 0.0 {
st_set(buf_key(bi, "al"), st_get(buf_key(bi, "cl"), 0.0))
st_set(buf_key(bi, "ac"), st_get(buf_key(bi, "cc"), 0.0))
}
} else { ed_clear_sel(bi) }
}
# ── cursor movement ───────────────────────────────────────────────────────────
fn ed_set_cursor(bi, l, c) {
bind nl = clampf(l, 0.0, buf_nlines(bi) - 1.0)
st_set(buf_key(bi, "cl"), nl)
st_set(buf_key(bi, "cc"), clampf(c, 0.0, str_len(buf_line(bi, nl))))
st_set("ed_blink", st_get("now", 0.0))
ed_sync_text()
ed_reveal(bi)
}
fn ed_goto_line(bi, l) {
if bi < 0.0 { return 0.0 }
ed_clear_sel(bi)
ed_set_cursor(bi, l, 0.0)
st_set("focus", "code")
return 0.0
}
# keep the runtime text buffer = the text before the cursor on the current line
fn ed_sync_text() {
bind bi = buf_cur()
if bi < 0.0 || st_get("focus", "") != "code" { return 0.0 }
bind pre = str_slice(buf_line(bi, st_get(buf_key(bi, "cl"), 0.0)), 0.0, st_get(buf_key(bi, "cc"), 0.0))
text_set(pre)
st_set("ed_expect", pre)
st_set("text_owner", "code")
return 0.0
}
fn ed_reveal(bi) {
bind lh = ed_lh()
bind h = st_get("ed_h", 400.0)
bind cy = st_get(buf_key(bi, "cl"), 0.0) * lh
bind off = st_get(buf_key(bi, "scroll"), 0.0)
if cy < off { bind off = cy }
if cy > off + h - lh * 2.0 { bind off = cy - h + lh * 2.0 }
st_set(buf_key(bi, "scroll"), max(0.0, off))
bind cx = ed_col_x(bi, st_get(buf_key(bi, "cl"), 0.0), st_get(buf_key(bi, "cc"), 0.0))
bind offx = st_get(buf_key(bi, "scrollx"), 0.0)
bind w = st_get("ed_w", 600.0)
if cx < offx + 8.0 { bind offx = max(0.0, cx - 8.0) }
if cx > offx + w - 40.0 { bind offx = cx - w + 40.0 }
st_set(buf_key(bi, "scrollx"), offx)
}
# x pixel offset of column c on line l (0 at the text origin)
fn ed_col_x(bi, l, c) {
return g_text_w(ED_FS, str_slice(buf_line(bi, l), 0.0, c))
}
# column at x pixel offset (nearest boundary)
fn ed_x_col(bi, l, x) {
bind ln = buf_line(bi, l)
bind n = str_len(ln)
if x <= 0.0 { return 0.0 }
bind i = 0.0
bind prev = 0.0
while i < n {
bind wnext = g_text_w(ED_FS, str_slice(ln, 0.0, i + 1.0))
if wnext >= x { return sel(x - prev < wnext - x, i, i + 1.0) }
bind prev = wnext
bind i = i + 1.0
}
return n
}
fn ed_word_l(bi, l, c) {
bind ln = buf_line(bi, l)
bind i = c
while i > 0.0 && !syn_is_word(str_slice(ln, i - 1.0, 1.0)) { bind i = i - 1.0 }
while i > 0.0 && syn_is_word(str_slice(ln, i - 1.0, 1.0)) { bind i = i - 1.0 }
return i
}
fn ed_word_r(bi, l, c) {
bind ln = buf_line(bi, l)
bind n = str_len(ln)
bind i = c
while i < n && !syn_is_word(str_slice(ln, i, 1.0)) { bind i = i + 1.0 }
while i < n && syn_is_word(str_slice(ln, i, 1.0)) { bind i = i + 1.0 }
return i
}
# ── edits ─────────────────────────────────────────────────────────────────────
fn ed_delete_range(bi, l0, c0, l1, c1) {
bind head = str_slice(buf_line(bi, l0), 0.0, c0)
bind tail = str_slice(buf_line(bi, l1), c1, str_len(buf_line(bi, l1)) - c1)
buf_set_line(bi, l0, head + tail)
bind i = l1
while i > l0 {
buf_del_line(bi, i)
bind i = i - 1.0
}
ed_clear_sel(bi)
st_set(buf_key(bi, "cl"), l0)
st_set(buf_key(bi, "cc"), c0)
buf_mark(bi)
}
fn ed_del_sel(bi) {
bind r = ed_sel_range(bi)
if list_get(r, 0) < 0.0 { return false }
buf_undo_push(bi)
ed_delete_range(bi, list_get(r, 0), list_get(r, 1), list_get(r, 2), list_get(r, 3))
return true
}
# insert (possibly multi-line) text at the cursor
fn ed_insert(bi, text) {
buf_undo_push(bi)
if ed_has_sel(bi) { ed_del_sel(bi) }
bind l = st_get(buf_key(bi, "cl"), 0.0)
bind c = st_get(buf_key(bi, "cc"), 0.0)
bind ln = buf_line(bi, l)
bind head = str_slice(ln, 0.0, c)
bind tail = str_slice(ln, c, str_len(ln) - c)
bind parts = str_split(str_replace(str_replace(text, "\r", ""), "\t", " "), "\n")
bind np = len(parts)
if np <= 1.0 {
buf_set_line(bi, l, head + text + tail)
st_set(buf_key(bi, "cc"), c + str_len(text))
} else {
buf_set_line(bi, l, head + list_get(parts, 0))
bind i = 1.0
while i < np - 1.0 {
buf_insert_line(bi, l + i, list_get(parts, i))
bind i = i + 1.0
}
buf_insert_line(bi, l + np - 1.0, list_get(parts, np - 1.0) + tail)
st_set(buf_key(bi, "cl"), l + np - 1.0)
st_set(buf_key(bi, "cc"), str_len(list_get(parts, np - 1.0)))
}
buf_mark(bi)
ed_sync_text()
ed_reveal(bi)
}
fn ed_leading_spaces(s) {
bind i = 0.0
while i < str_len(s) && str_slice(s, i, 1.0) == " " { bind i = i + 1.0 }
return i
}
fn ed_enter(bi) {
buf_undo_push(bi)
if ed_has_sel(bi) { ed_del_sel(bi) }
bind l = st_get(buf_key(bi, "cl"), 0.0)
bind c = st_get(buf_key(bi, "cc"), 0.0)
bind ln = buf_line(bi, l)
bind head = str_slice(ln, 0.0, c)
bind tail = str_slice(ln, c, str_len(ln) - c)
bind ind = ed_leading_spaces(ln)
if str_ends_with(str_trim(head), "{") { bind ind = ind + 4.0 }
buf_set_line(bi, l, head)
buf_insert_line(bi, l + 1.0, str_repeat(" ", ind) + str_trim(tail))
st_set(buf_key(bi, "cl"), l + 1.0)
st_set(buf_key(bi, "cc"), ind)
buf_mark(bi)
ed_sync_text()
ed_reveal(bi)
}
fn ed_backspace_edge(bi) {
# called when the cursor is at column 0: join with the previous line
bind l = st_get(buf_key(bi, "cl"), 0.0)
if l < 0.5 { return 0.0 }
buf_undo_push(bi)
bind prev = buf_line(bi, l - 1.0)
bind c = str_len(prev)
buf_set_line(bi, l - 1.0, prev + buf_line(bi, l))
buf_del_line(bi, l)
st_set(buf_key(bi, "cl"), l - 1.0)
st_set(buf_key(bi, "cc"), c)
buf_mark(bi)
ed_sync_text()
ed_reveal(bi)
}
fn ed_delete_key(bi) {
if ed_del_sel(bi) { ed_sync_text() return 0.0 }
bind l = st_get(buf_key(bi, "cl"), 0.0)
bind c = st_get(buf_key(bi, "cc"), 0.0)
bind ln = buf_line(bi, l)
if c < str_len(ln) {
buf_undo_push(bi)
buf_set_line(bi, l, str_slice(ln, 0.0, c) + str_slice(ln, c + 1.0, str_len(ln) - c - 1.0))
buf_mark(bi)
} else if l < buf_nlines(bi) - 1.0 {
buf_undo_push(bi)
buf_set_line(bi, l, ln + buf_line(bi, l + 1.0))
buf_del_line(bi, l + 1.0)
buf_mark(bi)
}
return 0.0
}
# the selected whole-line span [l0, l1] (the cursor line when nothing is selected)
fn ed_line_span(bi) {
bind r = ed_sel_range(bi)
if list_get(r, 0) < 0.0 {
bind l = st_get(buf_key(bi, "cl"), 0.0)
return [l, l]
}
bind l1 = list_get(r, 2)
if list_get(r, 3) == 0.0 && l1 > list_get(r, 0) { bind l1 = l1 - 1.0 }
return [list_get(r, 0), l1]
}
fn ed_indent(bi, out) {
buf_undo_push(bi)
bind span = ed_line_span(bi)
bind i = list_get(span, 0)
while i <= list_get(span, 1) {
bind ln = buf_line(bi, i)
if out {
bind cut = min(4.0, ed_leading_spaces(ln))
if cut > 0.0 { buf_set_line(bi, i, str_slice(ln, cut, str_len(ln) - cut)) }
} else {
buf_set_line(bi, i, " " + ln)
}
bind i = i + 1.0
}
bind d = sel(out, 0.0 - 4.0, 4.0)
st_set(buf_key(bi, "cc"), max(0.0, st_get(buf_key(bi, "cc"), 0.0) + d))
if ed_has_sel(bi) { st_set(buf_key(bi, "ac"), max(0.0, st_get(buf_key(bi, "ac"), 0.0) + d)) }
buf_mark(bi)
ed_sync_text()
}
fn ed_toggle_comment(bi) {
buf_undo_push(bi)
bind span = ed_line_span(bi)
# commented only if every non-empty line in the span starts with #
bind all = true
bind i = list_get(span, 0)
while i <= list_get(span, 1) {
bind t = str_trim(buf_line(bi, i))
if t != "" && !str_starts_with(t, "#") { bind all = false }
bind i = i + 1.0
}
bind i = list_get(span, 0)
while i <= list_get(span, 1) {
bind ln = buf_line(bi, i)
bind ind = ed_leading_spaces(ln)
bind t = str_slice(ln, ind, str_len(ln) - ind)
if all {
if str_starts_with(t, "# ") { buf_set_line(bi, i, str_slice(ln, 0.0, ind) + str_slice(t, 2.0, str_len(t) - 2.0)) }
else if str_starts_with(t, "#") { buf_set_line(bi, i, str_slice(ln, 0.0, ind) + str_slice(t, 1.0, str_len(t) - 1.0)) }
} else if t != "" {
buf_set_line(bi, i, str_slice(ln, 0.0, ind) + "# " + t)
}
bind i = i + 1.0
}
buf_mark(bi)
ed_sync_text()
}
fn ed_dup_line(bi) {
buf_undo_push(bi)
if ed_has_sel(bi) {
bind t = ed_sel_text(bi)
bind r = ed_sel_range(bi)
ed_clear_sel(bi)
st_set(buf_key(bi, "cl"), list_get(r, 2))
st_set(buf_key(bi, "cc"), list_get(r, 3))
ed_insert(bi, t)
return 0.0
}
bind l = st_get(buf_key(bi, "cl"), 0.0)
buf_insert_line(bi, l + 1.0, buf_line(bi, l))
st_set(buf_key(bi, "cl"), l + 1.0)
buf_mark(bi)
ed_sync_text()
ed_reveal(bi)
return 0.0
}
fn ed_move_line(bi, dir) {
bind span = ed_line_span(bi)
bind l0 = list_get(span, 0)
bind l1 = list_get(span, 1)
if dir < 0.0 && l0 < 0.5 { return 0.0 }
if dir > 0.0 && l1 >= buf_nlines(bi) - 1.0 { return 0.0 }
buf_undo_push(bi)
if dir < 0.0 {
bind moved = buf_line(bi, l0 - 1.0)
buf_del_line(bi, l0 - 1.0)
buf_insert_line(bi, l1, moved)
} else {
bind moved = buf_line(bi, l1 + 1.0)
buf_del_line(bi, l1 + 1.0)
buf_insert_line(bi, l0, moved)
}
st_set(buf_key(bi, "cl"), clampf(st_get(buf_key(bi, "cl"), 0.0) + dir, 0.0, buf_nlines(bi) - 1.0))
if ed_has_sel(bi) { st_set(buf_key(bi, "al"), clampf(st_get(buf_key(bi, "al"), 0.0) + dir, 0.0, buf_nlines(bi) - 1.0)) }
buf_mark(bi)
ed_sync_text()
ed_reveal(bi)
return 0.0
}
fn ed_copy(bi) {
if ed_has_sel(bi) { plat_clip_set(ed_sel_text(bi)) status("Copied") }
else {
plat_clip_set(buf_line(bi, st_get(buf_key(bi, "cl"), 0.0)) + "\n")
status("Copied line")
}
return 0.0
}
fn ed_cut(bi) {
if ed_has_sel(bi) {
plat_clip_set(ed_sel_text(bi))
ed_del_sel(bi)
ed_sync_text()
} else {
bind l = st_get(buf_key(bi, "cl"), 0.0)
plat_clip_set(buf_line(bi, l) + "\n")
buf_undo_push(bi)
if buf_nlines(bi) > 1.0 { buf_del_line(bi, l) } else { buf_set_line(bi, l, "") }
st_set(buf_key(bi, "cl"), clampf(l, 0.0, buf_nlines(bi) - 1.0))
st_set(buf_key(bi, "cc"), 0.0)
buf_mark(bi)
ed_sync_text()
}
return 0.0
}
fn ed_paste(bi) {
bind t = plat_clip_get()
if t == "" { return 0.0 }
ed_insert(bi, t)
return 0.0
}
fn ed_select_all(bi) {
st_set(buf_key(bi, "al"), 0.0)
st_set(buf_key(bi, "ac"), 0.0)
bind last = buf_nlines(bi) - 1.0
st_set(buf_key(bi, "cl"), last)
st_set(buf_key(bi, "cc"), str_len(buf_line(bi, last)))
ed_sync_text()
return 0.0
}
# ── typed-character reconciliation (see the header note) ──────────────────────
fn ed_poll(bi) {
if st_get("text_owner", "") != "code" { ed_sync_text() return 0.0 }
bind polled = str_replace(text_poll(), "\t", "")
bind expect = st_get("ed_expect", "")
if polled == expect { return 0.0 }
# common prefix
bind np = str_len(polled)
bind ne = str_len(expect)
bind p = 0.0
while p < np && p < ne && str_slice(polled, p, 1.0) == str_slice(expect, p, 1.0) { bind p = p + 1.0 }
bind removed = ne - p
bind added = str_slice(polled, p, np - p)
st_set("ed_blink", st_get("now", 0.0))
if ed_has_sel(bi) {
# a backspace with a selection deletes just the selection; typing replaces it
ed_del_sel(bi)
if added != "" { ed_insert(bi, added) }
ed_sync_text()
return 0.0
}
buf_undo_push(bi)
bind l = st_get(buf_key(bi, "cl"), 0.0)
bind c = st_get(buf_key(bi, "cc"), 0.0)
bind ln = buf_line(bi, l)
bind head = str_slice(ln, 0.0, c - removed)
bind tail = str_slice(ln, c, str_len(ln) - c)
buf_set_line(bi, l, head + added + tail)
st_set(buf_key(bi, "cc"), c - removed + str_len(added))
st_set("ed_expect", polled)
buf_mark(bi)
ed_reveal(bi)
return 0.0
}
# ── keyboard (focus == "code", no overlay) ────────────────────────────────────
fn ed_keys(bi) {
bind ctrl = u_ctrl()
bind shift = u_shift()
bind alt = u_alt()
bind l = st_get(buf_key(bi, "cl"), 0.0)
bind c = st_get(buf_key(bi, "cc"), 0.0)
# navigation
if key_rep("left") && !alt {
ed_anchor_if(bi, shift)
if ctrl { ed_set_cursor(bi, l, ed_word_l(bi, l, c)) }
else if !shift && ed_has_sel(bi) { bind r = ed_sel_range(bi) ed_clear_sel(bi) ed_set_cursor(bi, list_get(r, 0), list_get(r, 1)) }
else if c > 0.0 { ed_set_cursor(bi, l, c - 1.0) }
else if l > 0.0 { ed_set_cursor(bi, l - 1.0, str_len(buf_line(bi, l - 1.0))) }
st_set(buf_key(bi, "pref"), 0.0 - 1.0)
}
if key_rep("right") && !alt {
ed_anchor_if(bi, shift)
if ctrl { ed_set_cursor(bi, l, ed_word_r(bi, l, c)) }
else if !shift && ed_has_sel(bi) { bind r = ed_sel_range(bi) ed_clear_sel(bi) ed_set_cursor(bi, list_get(r, 2), list_get(r, 3)) }
else if c < str_len(buf_line(bi, l)) { ed_set_cursor(bi, l, c + 1.0) }
else if l < buf_nlines(bi) - 1.0 { ed_set_cursor(bi, l + 1.0, 0.0) }
st_set(buf_key(bi, "pref"), 0.0 - 1.0)
}
if key_rep("up") && !alt {
ed_anchor_if(bi, shift)
bind px = st_get(buf_key(bi, "pref"), 0.0 - 1.0)
if px < 0.0 { bind px = ed_col_x(bi, l, c) st_set(buf_key(bi, "pref"), px) }
ed_set_cursor(bi, l - 1.0, ed_x_col(bi, max(0.0, l - 1.0), px))
}
if key_rep("down") && !alt {
ed_anchor_if(bi, shift)
bind px = st_get(buf_key(bi, "pref"), 0.0 - 1.0)
if px < 0.0 { bind px = ed_col_x(bi, l, c) st_set(buf_key(bi, "pref"), px) }
ed_set_cursor(bi, l + 1.0, ed_x_col(bi, min(buf_nlines(bi) - 1.0, l + 1.0), px))
}
if alt && key_rep("up") { ed_move_line(bi, 0.0 - 1.0) }
if alt && key_rep("down") { ed_move_line(bi, 1.0) }
if key_rep("home") {
ed_anchor_if(bi, shift)
if ctrl { ed_set_cursor(bi, 0.0, 0.0) }
else {
bind ind = ed_leading_spaces(buf_line(bi, l))
ed_set_cursor(bi, l, sel(c == ind, 0.0, ind))
}
st_set(buf_key(bi, "pref"), 0.0 - 1.0)
}
if key_rep("end") {
ed_anchor_if(bi, shift)
if ctrl { ed_set_cursor(bi, buf_nlines(bi) - 1.0, str_len(buf_line(bi, buf_nlines(bi) - 1.0))) }
else { ed_set_cursor(bi, l, str_len(buf_line(bi, l))) }
st_set(buf_key(bi, "pref"), 0.0 - 1.0)
}
bind page = floor(st_get("ed_h", 400.0) / ed_lh()) - 2.0
if key_rep("pageup") { ed_anchor_if(bi, shift) ed_set_cursor(bi, l - page, c) }
if key_rep("pagedown") { ed_anchor_if(bi, shift) ed_set_cursor(bi, l + page, c) }
# edits
if key_rep("enter") && !ctrl { ed_enter(bi) }
if key_rep("backspace") {
# mid-line backspace is handled by ed_poll through the text buffer; this is the edge
if ed_has_sel(bi) { ed_del_sel(bi) ed_sync_text() }
else if ctrl && c > 0.0 {
bind w0 = ed_word_l(bi, l, c)
buf_undo_push(bi)
ed_delete_range(bi, l, w0, l, c)
ed_sync_text()
}
else if c == 0.0 { ed_backspace_edge(bi) }
}
if key_rep("delete") {
if ctrl && !ed_has_sel(bi) {
bind w1 = ed_word_r(bi, l, c)
if w1 > c { buf_undo_push(bi) ed_delete_range(bi, l, c, l, w1) ed_sync_text() }
} else { ed_delete_key(bi) }
}
if key_pressed("tab") && st_get("modal", "") == "" {
if shift { ed_indent(bi, true) }
else if ed_has_sel(bi) { ed_indent(bi, false) }
else { ed_insert(bi, " ") }
}
if key_pressed("escape") {
if st_get("find_on", 0.0) > 0.5 { find_close() }
else { ed_clear_sel(bi) }
}
if ctrl {
if key_pressed("c") { ed_copy(bi) }
if key_pressed("x") && !shift { ed_cut(bi) }
if key_pressed("v") { ed_paste(bi) }
if key_pressed("a") { ed_select_all(bi) }
if key_pressed("d") { ed_dup_line(bi) }
if key_pressed("z") {
if shift { buf_redo(bi) } else { buf_undo(bi) }
}
if key_pressed("y") { buf_redo(bi) }
if key_pressed("slash") { ed_toggle_comment(bi) }
}
ed_poll(bi)
return 0.0
}
# ── syntax token cache ───────────────────────────────────────────────────────
fn ed_tokens(bi, i) {
bind ln = buf_line(bi, i)
bind sk = "toks_" + num_str(bi) + "_" + num_str(i)
if st_has(sk) > 0.5 && st_get(sk, "") == ln { return st_get("tokv_" + num_str(bi) + "_" + num_str(i), []) }
bind toks = syn_line(buf_lang(bi), ln)
st_set(sk, ln)
st_set("tokv_" + num_str(bi) + "_" + num_str(i), toks)
return toks
}
# ── the view ─────────────────────────────────────────────────────────────────
fn ed_view(x, y, w, h) {
# an extension's full-size tab (the Claude chat tab) takes over the editor area
if st_get("ptab_on", 0.0) > 0.5 && st_get("ptab_active", 0.0) > 0.5 {
g_rect(x, y, w, h, C_BG_APP)
ext_side_panel(st_get("ptab_stem", ""), x, y, w, h)
return 0.0
}
bind bi = buf_cur()
if bi < 0.0 { welcome_view(x, y, w, h) return 0.0 }
bind fy = y
if st_get("find_on", 0.0) > 0.5 {
find_bar(x, y, w)
bind fy = y + 34.0
bind h = h - 34.0
}
st_set("ed_w", w)
st_set("ed_h", h - 12.0)
bind lh = ed_lh()
bind n = buf_nlines(bi)
g_rect(x, fy, w, h, E_BG)
bind gw = g_text_w(11.0, num_str(max(10.0, n))) + 20.0
bind tx = x + gw + 6.0
bind off = clampf(st_get(buf_key(bi, "scroll"), 0.0), 0.0, max(0.0, n * lh - h + lh))
bind offx = max(0.0, st_get(buf_key(bi, "scrollx"), 0.0))
# wheel
if u_hot(x, fy, w, h) && u_scroll() != 0.0 && st_get("wheel_used", 0.0) < 0.5 && !u_ctrl() {
if u_shift() { bind offx = max(0.0, offx - u_scroll() * 48.0) }
else { bind off = clampf(off - u_scroll() * lh * 3.0, 0.0, max(0.0, n * lh - h + lh)) }
st_set("wheel_used", 1.0)
}
if u_hot(x, fy, w, h) && u_ctrl() && u_scroll() != 0.0 && st_get("wheel_used", 0.0) < 0.5 {
text_size_set(text_k() + u_scroll() * 0.05)
st_set("wheel_used", 1.0)
}
bind cl = st_get(buf_key(bi, "cl"), 0.0)
bind cc = st_get(buf_key(bi, "cc"), 0.0)
bind r = ed_sel_range(bi)
bind l0 = floor(off / lh)
bind rows = ceil(h / lh) + 1.0
bind fq = str_lower(st_get("find_q", ""))
bind fqon = st_get("find_on", 0.0) > 0.5 && fq != ""
u_clip(x, fy, w, h)
# current line stripe (under everything)
if list_get(r, 0) < 0.0 && cl >= l0 && cl < l0 + rows {
g_rect(x + gw, fy + cl * lh - off, w - gw, lh, E_CURLINE)
}
bind i = l0
while i < min(n, l0 + rows) {
bind ry = fy + i * lh - off
bind ln = buf_line(bi, i)
# selection band
if list_get(r, 0) >= 0.0 && i >= list_get(r, 0) && i <= list_get(r, 2) {
bind sx0 = sel(i == list_get(r, 0), ed_col_x(bi, i, list_get(r, 1)), 0.0)
bind sx1 = sel(i == list_get(r, 2), ed_col_x(bi, i, list_get(r, 3)), g_text_w(ED_FS, ln) + 8.0)
g_rect_a(tx + sx0 - offx, ry, max(2.0, sx1 - sx0), lh, E_SELECT, 0.22)
}
# find matches
if fqon {
bind low = str_lower(ln)
bind from = 0.0
bind guard = 0.0
while guard < 40.0 {
bind at = str_find(str_slice(low, from, str_len(low) - from), fq)
if at < 0.0 { bind guard = 40.0 }
else {
bind mx0 = ed_col_x(bi, i, from + at)
bind mx1 = ed_col_x(bi, i, from + at + str_len(fq))
g_rect_a(tx + mx0 - offx, ry + 1.0, mx1 - mx0, lh - 2.0, E_FIND, 0.3)
bind from = from + at + str_len(fq)
bind guard = guard + 1.0
}
}
}
# indent guides
bind ind = ed_leading_spaces(ln)
bind g = 4.0
while g <= ind && str_trim(ln) != "" {
g_rect(tx + g_text_w(ED_FS, str_repeat(" ", g)) - offx, ry, 1.0, lh, E_WS)
bind g = g + 4.0
}
# tokens
bind toks = ed_tokens(bi, i)
bind t = 0.0
while t < len(toks) {
bind s0 = list_get(toks, t)
bind tl = list_get(toks, t + 1.0)
bind col = list_get(toks, t + 2.0)
bind px = ed_col_x(bi, i, s0) - offx
if px < w - gw && px + 600.0 > 0.0 {
g_text(tx + px, ry + (lh - ED_FS) * 0.5 - 1.0, ED_FS, col, str_slice(ln, s0, tl))
}
bind t = t + 3.0
}
# problems marker
if ed_line_has_problem(bi, i + 1.0) {
bind ww = max(24.0, g_text_w(ED_FS, ln))
bind zx = tx - offx
while zx < tx - offx + ww {
px_line(zx, ry + lh - 3.0, zx + 2.0, ry + lh - 1.0, E_ERROR, 0.8)
px_line(zx + 2.0, ry + lh - 1.0, zx + 4.0, ry + lh - 3.0, E_ERROR, 0.8)
bind zx = zx + 4.0
}
}
bind i = i + 1.0
}
# cursor (solid right after any input, then blinking)
if cl >= l0 && cl < l0 + rows && st_get("focus", "") == "code" {
if st_get("now", 0.0) - st_get("ed_blink", 0.0) < 0.45 || floor(st_get("now", 0.0) * 2.5) % 2.0 < 1.0 {
g_rect(tx + ed_col_x(bi, cl, cc) - offx, fy + cl * lh - off + 1.0, 2.0, lh - 2.0, E_CURSOR)
}
}
u_unclip()
# gutter over the text (clipped separately so long lines never cross it)
g_rect(x, fy, gw, h, E_BG_GUTTER)
g_vsep(x + gw, fy, h)
u_clip(x, fy, gw, h)
bind i = l0
while i < min(n, l0 + rows) {
bind ry = fy + i * lh - off
bind num = num_str(i + 1.0)
bind nc = sel(i == cl, E_LINENUM_CUR, E_LINENUM)
if ed_line_has_problem(bi, i + 1.0) { bind nc = E_ERROR }
g_text_r(x + gw - 8.0, ry + (lh - 11.0) * 0.5, 11.0, nc, num)
bind i = i + 1.0
}
u_unclip()
# scrollbars
bind newoff = u_scroll_y("ed_sb", x, fy, w, h, n * lh + lh, off)
if newoff != off { bind off = newoff }
bind maxw = 0.0
bind i = l0
while i < min(n, l0 + rows) {
bind maxw = max(maxw, g_text_w(ED_FS, buf_line(bi, i)))
bind i = i + 1.0
}
if maxw + 16.0 > w - gw {
bind offx = u_bar_h("ed_sbx", x + gw, fy + h - 10.0, w - gw - 12.0, w - gw, maxw + 60.0, offx)
}
st_set(buf_key(bi, "scroll"), off)
st_set(buf_key(bi, "scrollx"), offx)
# mouse
ed_mouse(bi, x, fy, w, h, tx, off, offx, lh)
return 0.0
}
fn ed_line_has_problem(bi, lineno) {
if st_get("problems_for", "") != buf_path(bi) { return false }
bind n = st_llen("problems")
bind i = 0.0
while i < n {
if to_number(list_get(str_split(st_lget("problems", i), "|"), 0)) == lineno { return true }
bind i = i + 1.0
}
return false
}
fn ed_mouse(bi, x, y, w, h, tx, off, offx, lh) {
bind hot = u_hot(x, y, w - 12.0, h - 12.0)
if hot { st_set("cursor_hand", 0.0) }
bind ml = clampf(floor((u_my() - y + off) / lh), 0.0, buf_nlines(bi) - 1.0)
bind mc = ed_x_col(bi, ml, u_mx() - tx + offx)
if hot && u_pressed() && st_get("bar_busy", 0.0) < 0.5 {
st_set("focus", "code")
if u_double() {
st_set(buf_key(bi, "al"), ml)
st_set(buf_key(bi, "ac"), ed_word_l(bi, ml, min(mc + 1.0, str_len(buf_line(bi, ml)))))
ed_set_cursor(bi, ml, ed_word_r(bi, ml, mc))
} else {
ed_anchor_if(bi, u_shift())
if !u_shift() { ed_clear_sel(bi) st_set(buf_key(bi, "al"), ml) st_set(buf_key(bi, "ac"), mc) }
ed_set_cursor(bi, ml, mc)
u_set_active("ed_drag")
}
st_set(buf_key(bi, "pref"), 0.0 - 1.0)
}
if u_active() == "ed_drag" && u_down() {
ed_set_cursor(bi, ml, mc)
# autoscroll while dragging past the edges
if u_my() > y + h - 18.0 { st_set(buf_key(bi, "scroll"), off + lh * 0.5) }
if u_my() < y + 14.0 { st_set(buf_key(bi, "scroll"), max(0.0, off - lh * 0.5)) }
}
if hot && u_rpressed() {
st_set("focus", "code")
ctx_open("edit", "", u_mx(), u_my() + 6.0)
}
return 0.0
}
# ── the find / replace bar ────────────────────────────────────────────────────
fn find_open(withreplace) {
st_set("find_on", 1.0)
st_set("find_repl_on", bool_num(withreplace))
bind bi = buf_cur()
if bi >= 0.0 && ed_has_sel(bi) {
bind t = ed_sel_text(bi)
if str_find(t, "\n") < 0.0 && t != "" { st_set("find_q", t) }
}
st_set("focus", "find_q")
st_set("text_owner", "")
}
fn find_close() {
st_set("find_on", 0.0)
st_set("focus", "code")
ed_sync_text()
}
fn find_count(bi, q) {
bind n = 0.0
bind i = 0.0
bind lq = str_lower(q)
while i < buf_nlines(bi) {
bind low = str_lower(buf_line(bi, i))
bind from = 0.0
bind go = true
while go {
bind at = str_find(str_slice(low, from, str_len(low) - from), lq)
if at < 0.0 { bind go = false }
else {
bind n = n + 1.0
bind from = from + at + max(1.0, str_len(lq))
}
}
bind i = i + 1.0
}
return n
}
# jump to the next (dir=1) / previous (dir=-1) match from the cursor
fn ed_find_next(bi, dir) {
bind q = str_lower(st_get("find_q", ""))
if q == "" || bi < 0.0 { return 0.0 }
bind n = buf_nlines(bi)
bind cl = st_get(buf_key(bi, "cl"), 0.0)
bind cc = st_get(buf_key(bi, "cc"), 0.0)
bind k = 0.0
bind l = cl
while k <= n {
bind low = str_lower(buf_line(bi, l))
if dir > 0.0 {
bind from = sel(k == 0.0, cc + sel(ed_has_sel(bi), 0.0, 1.0), 0.0)
if k == 0.0 && ed_has_sel(bi) { bind from = cc + 1.0 }
bind at = str_find(str_slice(low, from, str_len(low) - from), q)
if at >= 0.0 {
st_set(buf_key(bi, "al"), l)
st_set(buf_key(bi, "ac"), from + at)
ed_set_cursor(bi, l, from + at + str_len(q))
return 1.0
}
bind l = (l + 1.0) % n
} else {
# previous: scan the line for the last match before the cursor
bind limit = sel(k == 0.0, max(0.0, cc - str_len(q) - sel(ed_has_sel(bi), str_len(q), 0.0)), str_len(low))
if k == 0.0 && ed_has_sel(bi) {
bind rr = ed_sel_range(bi)
bind limit = max(0.0, list_get(rr, 1) - 1.0)
}
bind best = 0.0 - 1.0
bind from = 0.0
bind go = true
while go {
bind at = str_find(str_slice(low, from, str_len(low) - from), q)
if at < 0.0 || from + at > limit { bind go = false }
else {
bind best = from + at
bind from = from + at + 1.0
}
}
if best >= 0.0 {
st_set(buf_key(bi, "al"), l)
st_set(buf_key(bi, "ac"), best)
ed_set_cursor(bi, l, best + str_len(q))
return 1.0
}
bind l = l - 1.0
if l < 0.0 { bind l = n - 1.0 }
}
bind k = k + 1.0
}
status("No match for \"" + st_get("find_q", "") + "\"")
return 0.0
}
fn ed_replace_one(bi) {
bind q = st_get("find_q", "")
if q == "" { return 0.0 }
if ed_has_sel(bi) && str_lower(ed_sel_text(bi)) == str_lower(q) {
ed_insert(bi, st_get("find_repl", ""))
}
ed_find_next(bi, 1.0)
return 0.0
}
fn ed_replace_all(bi) {
bind q = st_get("find_q", "")
if q == "" { return 0.0 }
buf_undo_push(bi)
bind rep = st_get("find_repl", "")
bind cnt = 0.0
bind i = 0.0
while i < buf_nlines(bi) {
bind ln = buf_line(bi, i)
# case-sensitive replace (str_replace is exact); count by length difference
bind nl2 = str_replace(ln, q, rep)
if nl2 != ln {
bind cnt = cnt + 1.0
buf_set_line(bi, i, nl2)
}
bind i = i + 1.0
}
buf_mark(bi)
ed_sync_text()
status("Replaced in " + num_str(cnt) + " line(s)")
return 0.0
}
fn find_bar(x, y, w) {
g_rect(x, y, w, 34.0, C_BG_PANEL)
u_hsep(x, y + 33.0, w)
icon_draw("search", x + 16.0, y + 17.0, 12.0, C_TEXT_DIM)
bind q = u_textfield("find_q", x + 30.0, y + 5.0, 200.0, 24.0, st_get("find_q", ""))
st_set("find_q", q)
bind bi = buf_cur()
bind bx = x + 238.0
if st_get("find_repl_on", 0.0) > 0.5 {
bind rp = u_textfield("find_repl", bx, y + 5.0, 170.0, 24.0, st_get("find_repl", ""))
st_set("find_repl", rp)
bind bx = bx + 176.0
if u_button("find_r1", bx, y + 5.0, 70.0, 24.0, "Replace", 0.0) { ed_replace_one(bi) }
bind bx = bx + 74.0
if u_button("find_rall", bx, y + 5.0, 54.0, 24.0, "All", C_SEL) { ed_replace_all(bi) }
bind bx = bx + 62.0
}
if u_button("find_prev", bx, y + 5.0, 28.0, 24.0, "<", 0.0) { ed_find_next(bi, 0.0 - 1.0) }
if u_button("find_next", bx + 31.0, y + 5.0, 28.0, 24.0, ">", 0.0) { ed_find_next(bi, 1.0) }
bind cnt = 0.0
if q != "" && bi >= 0.0 { bind cnt = find_count(bi, q) }
g_text(bx + 68.0, y + 10.0, 11.0, sel(cnt > 0.5 || q == "", C_TEXT_DIM, C_RED), sel(q == "", "find (Enter next, Shift+Enter previous)", num_str(cnt) + " match" + sel(cnt == 1.0, "", "es")))
bind cx = x + w - 26.0
if u_icon_button("find_x", cx, y + 5.0, 22.0, 24.0, "x", 10.0, C_TEXT_DIM, false) { find_close() }
if st_get("modal", "") == "" && st_get("focus", "") == "find_q" && key_pressed("enter") { ed_find_next(bi, sel(u_shift(), 0.0 - 1.0, 1.0)) }
return 0.0
}
# ── the welcome view (no buffer open) ────────────────────────────────────────
fn welcome_view(x, y, w, h) {
g_rect(x, y, w, h, E_BG)
bind cx = x + w * 0.5
bind ty = y + h * 0.18
ds_word(cx - ds_word_w(26.0, "LINGCODER") * 0.5, ty, 26.0, "LINGCODER", ds_col("bind"), false)
g_text_c(cx, ty + 44.0, 13.0, C_TEXT_DIM, "An IDE for the Ling language, written in Ling")
bind by = ty + 90.0
bind bw = 300.0
if u_button("wl_new", cx - bw * 0.5, by, bw, 30.0, "New File Ctrl+N", 0.0) { do_action("file.new") }
if u_button("wl_open", cx - bw * 0.5, by + 38.0, bw, 30.0, "Open Folder... Ctrl+Shift+O", 0.0) { do_action("file.open_folder") }
if u_button("wl_pal", cx - bw * 0.5, by + 76.0, bw, 30.0, "Command Palette Ctrl+Shift+P", 0.0) { do_action("go.palette") }
if u_button("wl_keys", cx - bw * 0.5, by + 114.0, bw, 30.0, "Keyboard Shortcuts F1", 0.0) { do_action("help.shortcuts") }
bind n = st_llen("recent_proj")
if n > 0.5 {
g_text_c(cx, by + 162.0, 11.0, C_TEXT_DIM, "RECENT FOLDERS")
bind i = 0.0
while i < min(n, 5.0) {
bind p = st_lget("recent_proj", i)
bind ry = by + 182.0 + i * 24.0
bind hot = u_hot(cx - bw * 0.5, ry, bw, 22.0)
if hot { g_rrect_a(cx - bw * 0.5, ry, bw, 22.0, 3.0, C_BLUE, 0.2) }
icon_draw("folder", cx - bw * 0.5 + 12.0, ry + 11.0, 10.0, C_FOLDER)
g_text_fit(cx - bw * 0.5 + 26.0, ry + 5.0, bw - 32.0, 12.0, sel(hot, C_TEXT_STRONG, C_TEXT), p_base(p) + " " + p)
if hot && u_pressed() { project_open_path(p) }
bind i = i + 1.0
}
}
return 0.0
}