灵符lingfu
L

lingcoder

↓ 0 downloads

LingCoder: 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 lingcoder
LicenseApache-2.0 OR MIT
Authorsling-lin
READMECode / FilesVersionsDependency GraphDependentsSecurityBuildsSettings

Code / 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
}

← Back to index