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/editbox.ling
# editbox.ling - u_editbox: a multi-line text input with word wrap, a movable cursor
# (click / arrows), selection (drag, Shift+arrows, double-click, Ctrl+A), clipboard,
# held-key repeats (backspace included) and wheel scrolling. The Claude chat input and
# any extension can use it. Typing rides text_set/text_poll exactly like the code
# editor: the text before the cursor on the current line is the poll buffer.
#
# u_editbox(id, x, y, w, h, send_on_enter, hint) -> true when Enter submitted it
# eb_text(id) / eb_set(id, text) / eb_clear(id)
#
# State: st list "<id>_eln" (lines) and st keys <id>_ecl/_ecc (cursor), _eal/_eac
# (anchor, -1 = none), _esc (scroll px), _epx (preferred x for up/down).
bind EB_FS = 12.0
fn eb_lh() { return floor(17.0 * text_k()) }
fn eb_n(id) { return max(1.0, st_llen(id + "_eln")) }
fn eb_line(id, i) {
if i < 0.0 || i >= st_llen(id + "_eln") { return "" }
return st_lget(id + "_eln", i)
}
fn eb_setline(id, i, s) {
while st_llen(id + "_eln") <= i { st_lpush(id + "_eln", "") }
st_lset(id + "_eln", i, s)
}
fn eb_text(id) {
bind s = ""
bind n = st_llen(id + "_eln")
bind i = 0.0
while i < n {
bind s = s + st_lget(id + "_eln", i)
if i < n - 1.0 { bind s = s + "\n" }
bind i = i + 1.0
}
return s
}
fn eb_set(id, text) {
sl_from_list(id + "_eln", str_split(str_replace(str_replace(text, "\r", ""), "\t", " "), "\n"))
if st_llen(id + "_eln") < 0.5 { st_lpush(id + "_eln", "") }
bind last = st_llen(id + "_eln") - 1.0
st_set(id + "_ecl", last)
st_set(id + "_ecc", str_len(eb_line(id, last)))
st_set(id + "_eal", 0.0 - 1.0)
st_set(id + "_erev", 1.0)
if st_get("focus", "") == id { eb_sync(id) }
}
fn eb_clear(id) { eb_set(id, "") }
fn eb_sync(id) {
bind pre = str_slice(eb_line(id, st_get(id + "_ecl", 0.0)), 0.0, st_get(id + "_ecc", 0.0))
text_set(pre)
st_set("eb_expect", pre)
st_set("text_owner", id)
st_set("eb_blink", st_get("now", 0.0))
}
fn eb_cursor(id, l, c) {
bind nl = clampf(l, 0.0, eb_n(id) - 1.0)
st_set(id + "_ecl", nl)
st_set(id + "_ecc", clampf(c, 0.0, str_len(eb_line(id, nl))))
st_set(id + "_erev", 1.0)
eb_sync(id)
}
# ── selection ────────────────────────────────────────────────────────────────
fn eb_has_sel(id) {
bind al = st_get(id + "_eal", 0.0 - 1.0)
if al < 0.0 { return false }
return al != st_get(id + "_ecl", 0.0) || st_get(id + "_eac", 0.0) != st_get(id + "_ecc", 0.0)
}
fn eb_sel_range(id) {
if !eb_has_sel(id) { return [0.0 - 1.0, 0.0, 0.0, 0.0] }
bind al = st_get(id + "_eal", 0.0)
bind ac = st_get(id + "_eac", 0.0)
bind cl = st_get(id + "_ecl", 0.0)
bind cc = st_get(id + "_ecc", 0.0)
if al < cl || (al == cl && ac <= cc) { return [al, ac, cl, cc] }
return [cl, cc, al, ac]
}
fn eb_sel_text(id) {
bind r = eb_sel_range(id)
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(eb_line(id, l0), c0, c1 - c0) }
bind s = str_slice(eb_line(id, l0), c0, str_len(eb_line(id, l0)) - c0)
bind i = l0 + 1.0
while i < l1 {
bind s = s + "\n" + eb_line(id, i)
bind i = i + 1.0
}
return s + "\n" + str_slice(eb_line(id, l1), 0.0, c1)
}
fn eb_clear_sel(id) { st_set(id + "_eal", 0.0 - 1.0) }
fn eb_anchor_if(id, extend) {
if extend {
if st_get(id + "_eal", 0.0 - 1.0) < 0.0 {
st_set(id + "_eal", st_get(id + "_ecl", 0.0))
st_set(id + "_eac", st_get(id + "_ecc", 0.0))
}
} else { eb_clear_sel(id) }
}
fn eb_del_sel(id) {
bind r = eb_sel_range(id)
if list_get(r, 0) < 0.0 { return false }
bind l0 = list_get(r, 0)
bind c0 = list_get(r, 1)
bind l1 = list_get(r, 2)
bind c1 = list_get(r, 3)
bind head = str_slice(eb_line(id, l0), 0.0, c0)
bind tail = str_slice(eb_line(id, l1), c1, str_len(eb_line(id, l1)) - c1)
eb_setline(id, l0, head + tail)
bind i = l1
while i > l0 {
st_ldel(id + "_eln", i)
bind i = i - 1.0
}
eb_clear_sel(id)
st_set(id + "_ecl", l0)
st_set(id + "_ecc", c0)
return true
}
fn eb_insert(id, text) {
if eb_has_sel(id) { eb_del_sel(id) }
bind l = st_get(id + "_ecl", 0.0)
bind c = st_get(id + "_ecc", 0.0)
bind ln = eb_line(id, 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 {
eb_setline(id, l, head + text + tail)
st_set(id + "_ecc", c + str_len(text))
} else {
eb_setline(id, l, head + list_get(parts, 0))
bind i = 1.0
while i < np - 1.0 {
sl_insert(id + "_eln", l + i, list_get(parts, i))
bind i = i + 1.0
}
sl_insert(id + "_eln", l + np - 1.0, list_get(parts, np - 1.0) + tail)
st_set(id + "_ecl", l + np - 1.0)
st_set(id + "_ecc", str_len(list_get(parts, np - 1.0)))
}
st_set(id + "_erev", 1.0)
eb_sync(id)
}
# ── word wrap: start columns of each visual row of `s` at width maxw ─────────
fn eb_wrap_starts(s, maxw) {
bind out = [0.0]
bind n = str_len(s)
if n < 0.5 { return out }
bind words = str_split(s, " ")
bind spw = g_text_w(EB_FS, " ")
bind col = 0.0
bind accw = 0.0
bind wi = 0.0
while wi < len(words) {
bind wd = list_get(words, wi)
bind wl = str_len(wd)
bind ww = g_text_w(EB_FS, wd)
if ww > maxw {
# a word wider than the box: break it by characters
if accw > 0.5 { bind out = list_push(out, col) bind accw = 0.0 }
bind k = 0.0
bind seg0 = 0.0
while k < wl {
if g_text_w(EB_FS, str_slice(wd, seg0, k + 1.0 - seg0)) > maxw && k > seg0 {
bind out = list_push(out, col + k)
bind seg0 = k
}
bind k = k + 1.0
}
bind accw = g_text_w(EB_FS, str_slice(wd, seg0, wl - seg0)) + spw
} else if accw + ww > maxw && accw > 0.5 {
bind out = list_push(out, col)
bind accw = ww + spw
} else {
bind accw = accw + ww + spw
}
bind col = col + wl + 1.0
bind wi = wi + 1.0
}
return out
}
# the visual rows of the whole box: a flat list [line, startCol, endCol, ...]
fn eb_rows(id, maxw) {
bind rows = []
bind n = eb_n(id)
bind li = 0.0
while li < n {
bind s = eb_line(id, li)
bind starts = eb_wrap_starts(s, maxw)
bind k = 0.0
while k < len(starts) {
bind s0 = list_get(starts, k)
bind s1 = sel(k + 1.0 < len(starts), list_get(starts, k + 1.0), str_len(s))
bind rows = list_push(rows, li)
bind rows = list_push(rows, s0)
bind rows = list_push(rows, s1)
bind k = k + 1.0
}
bind li = li + 1.0
}
return rows
}
# the visual row index holding (l, c)
fn eb_row_of(rows, l, c) {
bind nr = len(rows) / 3.0
bind r = 0.0
while r < nr {
if list_get(rows, r * 3.0) == l {
bind s0 = list_get(rows, r * 3.0 + 1.0)
bind s1 = list_get(rows, r * 3.0 + 2.0)
bind lastrow = r + 1.0 >= nr || list_get(rows, (r + 1.0) * 3.0) != l
if c >= s0 && (c < s1 || (lastrow && c <= s1)) { return r }
}
bind r = r + 1.0
}
return max(0.0, nr - 1.0)
}
# column at pixel x within row r
fn eb_col_at(id, rows, r, x) {
bind l = list_get(rows, r * 3.0)
bind s0 = list_get(rows, r * 3.0 + 1.0)
bind s1 = list_get(rows, r * 3.0 + 2.0)
bind s = eb_line(id, l)
bind i = s0
bind prev = 0.0
while i < s1 {
bind wnext = g_text_w(EB_FS, str_slice(s, s0, i + 1.0 - s0))
if wnext >= x { return sel(x - prev < wnext - x, i, i + 1.0) }
bind prev = wnext
bind i = i + 1.0
}
return s1
}
# ── typed-character reconciliation (same trick as the code editor) ────────────
fn eb_poll(id) {
if st_get("text_owner", "") != id { eb_sync(id) return 0.0 }
bind polled = str_replace(text_poll(), "\t", "")
bind expect = st_get("eb_expect", "")
if polled == expect { return 0.0 }
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("eb_blink", st_get("now", 0.0))
if eb_has_sel(id) {
eb_del_sel(id)
if added != "" { eb_insert(id, added) }
eb_sync(id)
return 0.0
}
bind l = st_get(id + "_ecl", 0.0)
bind c = st_get(id + "_ecc", 0.0)
bind ln = eb_line(id, l)
bind head = str_slice(ln, 0.0, c - removed)
bind tail = str_slice(ln, c, str_len(ln) - c)
eb_setline(id, l, head + added + tail)
st_set(id + "_ecc", c - removed + str_len(added))
st_set("eb_expect", polled)
st_set(id + "_erev", 1.0)
return 0.0
}
fn eb_word_l(s, c) {
bind i = c
while i > 0.0 && !syn_is_word(str_slice(s, i - 1.0, 1.0)) { bind i = i - 1.0 }
while i > 0.0 && syn_is_word(str_slice(s, i - 1.0, 1.0)) { bind i = i - 1.0 }
return i
}
fn eb_word_r(s, c) {
bind n = str_len(s)
bind i = c
while i < n && !syn_is_word(str_slice(s, i, 1.0)) { bind i = i + 1.0 }
while i < n && syn_is_word(str_slice(s, i, 1.0)) { bind i = i + 1.0 }
return i
}
# ── keys (only while focused and nothing overlays the panel) ─────────────────
fn eb_keys(id, rows, maxw, send_on_enter) {
bind shift = u_shift()
bind ctrl = u_ctrl()
bind l = st_get(id + "_ecl", 0.0)
bind c = st_get(id + "_ecc", 0.0)
bind submitted = false
if key_rep("left") {
eb_anchor_if(id, shift)
if ctrl { eb_cursor(id, l, eb_word_l(eb_line(id, l), c)) }
else if !shift && eb_has_sel(id) { bind r = eb_sel_range(id) eb_clear_sel(id) eb_cursor(id, list_get(r, 0), list_get(r, 1)) }
else if c > 0.0 { eb_cursor(id, l, c - 1.0) }
else if l > 0.0 { eb_cursor(id, l - 1.0, str_len(eb_line(id, l - 1.0))) }
st_set(id + "_epx", 0.0 - 1.0)
}
if key_rep("right") {
eb_anchor_if(id, shift)
if ctrl { eb_cursor(id, l, eb_word_r(eb_line(id, l), c)) }
else if !shift && eb_has_sel(id) { bind r = eb_sel_range(id) eb_clear_sel(id) eb_cursor(id, list_get(r, 2), list_get(r, 3)) }
else if c < str_len(eb_line(id, l)) { eb_cursor(id, l, c + 1.0) }
else if l < eb_n(id) - 1.0 { eb_cursor(id, l + 1.0, 0.0) }
st_set(id + "_epx", 0.0 - 1.0)
}
if key_rep("up") || key_rep("down") {
bind down = key_down("down")
eb_anchor_if(id, shift)
bind r = eb_row_of(rows, l, c)
bind px = st_get(id + "_epx", 0.0 - 1.0)
if px < 0.0 {
bind px = g_text_w(EB_FS, str_slice(eb_line(id, l), list_get(rows, r * 3.0 + 1.0), c - list_get(rows, r * 3.0 + 1.0)))
st_set(id + "_epx", px)
}
bind nr = len(rows) / 3.0
bind tr = clampf(r + sel(down, 1.0, 0.0 - 1.0), 0.0, nr - 1.0)
eb_cursor(id, list_get(rows, tr * 3.0), eb_col_at(id, rows, tr, px))
}
if key_rep("home") {
eb_anchor_if(id, shift)
eb_cursor(id, sel(ctrl, 0.0, l), 0.0)
st_set(id + "_epx", 0.0 - 1.0)
}
if key_rep("end") {
eb_anchor_if(id, shift)
bind tl = sel(ctrl, eb_n(id) - 1.0, l)
eb_cursor(id, tl, str_len(eb_line(id, tl)))
st_set(id + "_epx", 0.0 - 1.0)
}
if key_rep("backspace") {
if eb_has_sel(id) { eb_del_sel(id) eb_sync(id) }
else if ctrl && c > 0.0 {
bind w0 = eb_word_l(eb_line(id, l), c)
bind ln = eb_line(id, l)
eb_setline(id, l, str_slice(ln, 0.0, w0) + str_slice(ln, c, str_len(ln) - c))
st_set(id + "_ecc", w0)
eb_sync(id)
} else if c == 0.0 && l > 0.0 {
bind prev = eb_line(id, l - 1.0)
eb_setline(id, l - 1.0, prev + eb_line(id, l))
st_ldel(id + "_eln", l)
st_set(id + "_ecl", l - 1.0)
st_set(id + "_ecc", str_len(prev))
st_set(id + "_erev", 1.0)
eb_sync(id)
}
}
if key_rep("delete") {
if eb_has_sel(id) { eb_del_sel(id) eb_sync(id) }
else {
bind ln = eb_line(id, l)
if c < str_len(ln) {
eb_setline(id, l, str_slice(ln, 0.0, c) + str_slice(ln, c + 1.0, str_len(ln) - c - 1.0))
eb_sync(id)
} else if l < eb_n(id) - 1.0 {
eb_setline(id, l, ln + eb_line(id, l + 1.0))
st_ldel(id + "_eln", l + 1.0)
eb_sync(id)
}
}
}
if key_rep("enter") {
if send_on_enter && !shift { bind submitted = true }
else { eb_insert(id, "\n") }
}
if key_pressed("tab") && st_get("modal", "") == "" { eb_insert(id, " ") }
if ctrl {
if key_pressed("a") {
st_set(id + "_eal", 0.0)
st_set(id + "_eac", 0.0)
bind last = eb_n(id) - 1.0
st_set(id + "_ecl", last)
st_set(id + "_ecc", str_len(eb_line(id, last)))
eb_sync(id)
}
if key_pressed("c") && eb_has_sel(id) { plat_clip_set(eb_sel_text(id)) }
if key_pressed("x") && eb_has_sel(id) {
plat_clip_set(eb_sel_text(id))
eb_del_sel(id)
eb_sync(id)
}
if key_pressed("v") {
bind t = plat_clip_get()
if t != "" { eb_insert(id, t) }
}
}
return submitted
}
# ── the widget ───────────────────────────────────────────────────────────────
fn u_editbox(id, x, y, w, h, send_on_enter, hint) {
bind focused = st_get("focus", "") == id
bind hot = u_hot(x, y, w, h)
bind lh = eb_lh()
bind maxw = w - 22.0
if st_llen(id + "_eln") < 0.5 { st_lpush(id + "_eln", "") }
bind submitted = false
if focused && st_get("blk", 0.0) < 0.5 && st_get("modal", "") == "" && st_get("menu_open", 0.0 - 1.0) < 0.0 {
bind rows0 = eb_rows(id, maxw)
bind submitted = eb_keys(id, rows0, maxw, send_on_enter)
eb_poll(id)
}
bind rows = eb_rows(id, maxw)
bind nr = len(rows) / 3.0
bind content = nr * lh + 10.0
bind off = clampf(st_get(id + "_esc", 0.0), 0.0, max(0.0, content - h))
# keep the cursor's visual row in view after edits / moves
if st_get(id + "_erev", 0.0) > 0.5 {
st_set(id + "_erev", 0.0)
bind cr = eb_row_of(rows, st_get(id + "_ecl", 0.0), st_get(id + "_ecc", 0.0))
bind cy = cr * lh
if cy < off { bind off = cy }
if cy + lh > off + h - 8.0 { bind off = cy + lh - h + 8.0 }
bind off = clampf(off, 0.0, max(0.0, content - h))
}
g_rrect(x, y, w, h, 3.0, C_BG_DEEP)
g_rframe(x, y, w, h, 3.0, sel(focused, C_BLUE, C_LINE))
bind r = eb_sel_range(id)
bind cl = st_get(id + "_ecl", 0.0)
bind cc = st_get(id + "_ecc", 0.0)
u_clip(x + 2.0, y + 2.0, w - 4.0, h - 4.0)
bind ri = floor(off / lh)
bind rlast = min(nr, ri + ceil(h / lh) + 1.0)
while ri < rlast {
bind li = list_get(rows, ri * 3.0)
bind s0 = list_get(rows, ri * 3.0 + 1.0)
bind s1 = list_get(rows, ri * 3.0 + 2.0)
bind s = eb_line(id, li)
bind ry = y + 5.0 + ri * lh - off
# selection band over this row
if list_get(r, 0) >= 0.0 && li >= list_get(r, 0) && li <= list_get(r, 2) {
bind a0 = sel(li == list_get(r, 0), max(s0, list_get(r, 1)), s0)
bind a1 = sel(li == list_get(r, 2), min(s1, list_get(r, 3)), s1)
if a1 > a0 || (li > list_get(r, 0) && li < list_get(r, 2) && s1 == s0) {
bind sx0 = g_text_w(EB_FS, str_slice(s, s0, max(0.0, a0 - s0)))
bind sx1 = g_text_w(EB_FS, str_slice(s, s0, max(0.0, a1 - s0)))
g_rect_a(x + 7.0 + sx0, ry - 1.0, max(3.0, sx1 - sx0), lh, C_SEL, 0.55)
}
}
g_text(x + 7.0, ry, EB_FS, C_TEXT_STRONG, str_slice(s, s0, s1 - s0))
# cursor on this row
if focused && li == cl {
bind lastrow = ri + 1.0 >= nr || list_get(rows, (ri + 1.0) * 3.0) != li
if cc >= s0 && (cc < s1 || (lastrow && cc <= s1)) {
bind blink = st_get("now", 0.0) - st_get("eb_blink", 0.0) < 0.45 || floor(st_get("now", 0.0) * 2.5) % 2.0 < 1.0
if blink { g_rect(x + 7.0 + g_text_w(EB_FS, str_slice(s, s0, cc - s0)), ry - 1.0, 1.5, lh, C_TEXT_STRONG) }
}
}
bind ri = ri + 1.0
}
u_unclip()
if eb_text(id) == "" && !focused && hint != "" { g_text_fit(x + 7.0, y + 5.0, w - 14.0, EB_FS, C_TEXT_DIM, hint) }
# wheel + bar
bind noff = u_scroll_y(id + "_esb", x, y, w, h, content, off)
if noff != off { bind off = noff }
st_set(id + "_esc", off)
# mouse
if hot && u_pressed() && st_get("bar_busy", 0.0) < 0.5 {
if !focused {
st_set("focus", id)
st_set("text_owner", "")
}
bind mr = clampf(floor((u_my() - y - 5.0 + off) / lh), 0.0, nr - 1.0)
bind ml = list_get(rows, mr * 3.0)
bind mc = eb_col_at(id, rows, mr, u_mx() - x - 7.0)
if u_double() {
bind s = eb_line(id, ml)
st_set(id + "_eal", ml)
st_set(id + "_eac", eb_word_l(s, min(mc + 1.0, str_len(s))))
eb_cursor(id, ml, eb_word_r(s, mc))
} else {
if u_shift() { eb_anchor_if(id, true) }
else {
st_set(id + "_eal", ml)
st_set(id + "_eac", mc)
}
eb_cursor(id, ml, mc)
u_set_active(id + "_edrag")
}
st_set(id + "_epx", 0.0 - 1.0)
}
if u_active() == id + "_edrag" && u_down() {
bind mr = clampf(floor((u_my() - y - 5.0 + off) / lh), 0.0, nr - 1.0)
eb_cursor(id, list_get(rows, mr * 3.0), eb_col_at(id, rows, mr, u_mx() - x - 7.0))
if u_my() > y + h - 10.0 { st_set(id + "_esc", off + lh * 0.4) }
if u_my() < y + 10.0 { st_set(id + "_esc", max(0.0, off - lh * 0.4)) }
}
if focused && u_pressed() && !hot && st_get("blk", 0.0) < 0.5 && st_get("bar_busy", 0.0) < 0.5 { st_set("focus", "") }
if hot { st_set("cursor_hand", 1.0) }
return submitted
}