灵符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/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
}

← Back to index