灵符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/modals.ling
# modals.ling - egui-style floating windows: the generic text prompt, confirm, the
# unsaved-changes guard, Settings, About and Shortcuts. The command palette lives in
# palette.ling but is dispatched from modal_overlay here.

fn modal_open(name) {
    st_set("modal", name)
    st_set("modal_t0", st_get("now", 0.0))
    st_set("focus", "")
}
fn modal_close() {
    st_set("modal", "")
    st_set("focus", "")
    if buf_cur() >= 0.0 { st_set("focus", "code")  ed_sync_text() }
}

# the text prompt: kind picks what OK does, arg carries a path / context
fn modal_text(title, kind, initial, arg) {
    st_set("text_title", title)
    st_set("text_kind", kind)
    st_set("text_arg", arg)
    modal_open("text")
    st_set("text_val", initial)
    text_set(initial)
    st_set("text_owner", "text_modal")
    st_set("focus", "text_modal")
}

# window chrome; leaves the content origin in st wx, wy
fn win_begin(title, w, h) {
    bind W = get_width()
    bind H = get_height()
    bind x = max(4.0, floor((W - w) * 0.5))
    bind y = max(4.0, floor((H - h) * 0.5) - 20.0)
    g_rect_a(0.0, 0.0, W, H, 0.0, 0.45)
    px_rrect(x + 3.0, y + 5.0, w, h, 8.0, 0.0, 0.5)
    g_rrect(x, y, w, h, 7.0, C_BG_PANEL)
    g_rframe(x, y, w, h, 7.0, C_POPUP_LINE)
    g_rrect(x, y, w, 26.0, 7.0, 2434344.0)
    g_rect(x, y + 14.0, w, 12.0, 2434344.0)
    g_text(x + 12.0, y + 6.0, 13.0, C_TEXT_STRONG, title)
    bind cbx = min(x + w, W - 4.0) - 26.0
    bind hot = u_hot(cbx, y + 3.0, 20.0, 20.0)
    if hot {
        g_rrect(cbx, y + 3.0, 20.0, 20.0, 4.0, C_RED)
        u_tip("Close (Esc)")
    }
    icon_draw("x", cbx + 10.0, y + 13.0, 10.0, C_TEXT_STRONG)
    if hot && u_pressed() { modal_close() }
    u_hsep(x, y + 26.0, w)
    st_set("wx", x)
    st_set("wy", y + 34.0)
    st_set("ww", w)
    st_set("wh", h - 34.0)
}

# ── text input (replaces core.ling's: shared-buffer ownership + Ctrl+V paste) ──
fn u_textfield(id, x, y, w, h, value) {
    bind focused = st_get("focus", "") == id
    bind hot = u_hot(x, y, w, h)
    if focused && u_pressed() && !hot && st_get("blk", 0.0) < 0.5 {
        st_set("focus", "")
        bind focused = false
    }
    if hot && u_pressed() && !focused {
        st_set("focus", id)
        text_set(value)
        st_set("text_owner", id)
        bind focused = true
    }
    # one text buffer serves every field: a field focused from code loads its own text first
    if focused && st_get("text_owner", "") != id {
        text_set(value)
        st_set("text_owner", id)
    }
    bind v = value
    if focused {
        bind v = str_replace(text_poll(), "\t", "")
        if u_ctrl() && key_pressed("v") {
            bind v = v + plat_clip_get()
            text_set(v)
        }
    }
    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 shown = v
    if str_ends_with(id, "_pw") && str_len(v) > 0.5 { bind shown = str_repeat("*", str_len(v)) }
    bind tw = g_text_w(12.0, shown)
    bind tx = x + 6.0
    if focused && tw > w - 12.0 { bind tx = x + w - 6.0 - tw }
    ds_clip_to(x + 2.0, y, w - 4.0, h)
    g_text(tx, y + (h - 12.0) * 0.5 - 1.0, 12.0, C_TEXT_STRONG, shown)
    ds_clip_end()
    if focused {
        bind cx = min(tx + tw, x + w - 6.0)
        if floor(st_get("now", 0.0) * 2.0) % 2.0 < 0.5 { g_rect(cx, y + 4.0, 1.0, h - 8.0, C_TEXT_STRONG) }
    }
    if hot { st_set("cursor_hand", 1.0) }
    return v
}

# a light multi-line input (Claude chat, commit-style text): typing appends at the end,
# Ctrl+V pastes, Shift+Enter makes a new line. Rendering wraps; returns the value.
fn u_textarea_lite(id, x, y, w, h, value) {
    bind focused = st_get("focus", "") == id
    bind hot = u_hot(x, y, w, h)
    if hot && u_pressed() && !focused {
        st_set("focus", id)
        text_set(value)
        st_set("text_owner", id)
        bind focused = true
    }
    if focused && st_get("text_owner", "") != id {
        text_set(value)
        st_set("text_owner", id)
    }
    bind v = value
    if focused {
        bind v = str_replace(text_poll(), "\t", "    ")
        if u_ctrl() && key_pressed("v") {
            bind v = v + plat_clip_get()
            text_set(v)
        }
        if u_shift() && key_pressed("enter") {
            bind v = v + "\n"
            text_set(v)
        }
    }
    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))
    u_clip(x + 2.0, y + 2.0, w - 4.0, h - 4.0)
    bind lines = str_split(v, "\n")
    bind ly = y + 5.0
    bind i = 0.0
    bind used = 0.0
    while i < len(lines) {
        bind used = used + g_text_wrap(x + 7.0, ly + used, w - 14.0, 12.0, C_TEXT_STRONG, list_get(lines, i))
        if list_get(lines, i) == "" { bind used = used + 16.0 }
        bind i = i + 1.0
    }
    if focused && floor(st_get("now", 0.0) * 2.0) % 2.0 < 0.5 {
        bind last = list_get(lines, len(lines) - 1.0)
        g_rect(x + 7.0 + g_text_w(12.0, last) + 1.0, ly + max(0.0, used - 16.0), 1.0, 14.0, C_TEXT_STRONG)
    }
    u_unclip()
    if v == "" && !focused { g_text(x + 7.0, y + 5.0, 12.0, C_TEXT_DIM, st_get(id + "_hint", "")) }
    return v
}

fn modal_overlay() {
    bind m = st_get("modal", "")
    if m == "" { return 0.0 }
    st_set("blk", sel(st_get("cb_open", "") != "" || st_get("ctx_open", 0.0) > 0.5, 1.0, 0.0))
    if m == "text" { modal_text_win() }
    else if m == "confirm" { modal_confirm() }
    else if m == "unsaved" { modal_unsaved() }
    else if m == "settings" { modal_settings() }
    else if m == "about" { modal_about() }
    else if m == "shortcuts" { modal_shortcuts() }
    else if m == "palette" { modal_palette() }
    if key_pressed("escape") && st_get("cb_open", "") == "" { modal_close() }
    return 0.0
}

# ── confirm ───────────────────────────────────────────────────────────────────
fn confirm_open(title, message, act) {
    st_set("confirm_title", title)
    st_set("confirm_msg", message)
    st_set("confirm_act", act)
    modal_open("confirm")
}
fn modal_confirm() {
    win_begin(st_get("confirm_title", "Confirm"), 420.0, 130.0)
    bind x = st_get("wx", 0.0)
    bind y = st_get("wy", 0.0)
    g_text_wrap(x + 16.0, y + 10.0, 388.0, 12.0, C_TEXT, st_get("confirm_msg", ""))
    if u_button("cf_yes", x + 16.0, y + 58.0, 110.0, 26.0, "Yes", C_RED) {
        bind act = st_get("confirm_act", "")
        modal_close()
        do_action(act)
    }
    if u_button("cf_no", x + 134.0, y + 58.0, 110.0, 26.0, "Cancel", 0.0) { modal_close() }
    if key_pressed("enter") {
        bind act = st_get("confirm_act", "")
        modal_close()
        do_action(act)
    }
}

# ── the text prompt ───────────────────────────────────────────────────────────
fn modal_text_win() {
    win_begin(st_get("text_title", "Input"), 420.0, 122.0)
    bind x = st_get("wx", 0.0)
    bind y = st_get("wy", 0.0)
    if st_get("focus", "") == "" { st_set("focus", "text_modal") }
    bind v = u_textfield("text_modal", x + 14.0, y + 10.0, 392.0, 26.0, st_get("text_val", ""))
    st_set("text_val", v)
    bind okb = u_button("txt_ok", x + 14.0, y + 50.0, 86.0, 26.0, "OK", C_SEL)
    bind cancel = u_button("txt_cancel", x + 108.0, y + 50.0, 86.0, 26.0, "Cancel", 0.0)
    if key_pressed("enter") { bind okb = true }
    if okb {
        bind kind = st_get("text_kind", "")
        bind arg = st_get("text_arg", "")
        bind t = str_trim(v)
        modal_close()
        if t != "" {
            if kind == "newfile" { file_new_at(arg, t) }
            else if kind == "newfolder" { folder_new_at(arg, t) }
            else if kind == "rename" { file_rename_to(arg, t) }
            else if kind == "saveas" { buf_save_as(buf_cur(), proj_abs(t)) }
            else if kind == "goto" { ed_goto_line(buf_cur(), to_number(t) - 1.0) }
            else if kind == "ext_new" { ext_scaffold(t) }
            else if kind == "ling_exe" { st_set("ling_exe", t)  settings_save() }
        }
    }
    if cancel { modal_close() }
}

# ── the unsaved guard ─────────────────────────────────────────────────────────
# ask about buffer at tab position k; after = the action to run once it is settled
fn unsaved_ask(k, after) {
    st_set("unsaved_k", k)
    st_set("after_unsaved", after)
    modal_open("unsaved")
}
fn modal_unsaved() {
    bind k = st_get("unsaved_k", 0.0)
    bind bi = buf_at(k)
    win_begin("Unsaved Changes", 420.0, 132.0)
    bind x = st_get("wx", 0.0)
    bind y = st_get("wy", 0.0)
    g_text_fit(x + 16.0, y + 10.0, 388.0, 13.0, C_TEXT, p_base(buf_path(bi)) + " has unsaved changes.")
    g_text(x + 16.0, y + 30.0, 12.0, C_TEXT_DIM, "Save before closing?")
    if u_button("us_save", x + 16.0, y + 60.0, 110.0, 26.0, "Save", C_SEL) {
        buf_save(bi)
        bind then = st_get("after_unsaved", "")
        modal_close()
        do_action(then)
    }
    if u_button("us_discard", x + 134.0, y + 60.0, 110.0, 26.0, "Discard", C_RED) {
        st_set(buf_key(bi, "dirty"), 0.0)
        bind then = st_get("after_unsaved", "")
        modal_close()
        do_action(then)
    }
    if u_button("us_cancel", x + 252.0, y + 60.0, 110.0, 26.0, "Cancel", 0.0) {
        st_set("quit_pending", 0.0)
        modal_close()
    }
}

# ── settings ──────────────────────────────────────────────────────────────────
fn modal_settings() {
    win_begin("Settings", 560.0, 470.0)
    bind x = st_get("wx", 0.0)
    bind y = st_get("wy", 0.0)
    u_section(x + 16.0, y + 8.0, 528.0, "EDITOR", ds_col("bind"))
    bind ny = y + 28.0
    if u_check("set_loadlast", x + 16.0, ny, "Reopen the last folder at start", st_get("load_last", 1.0) > 0.5) { st_set("load_last", 1.0 - st_get("load_last", 1.0)) }
    bind ny = ny + 24.0
    if u_check("set_black", x + 16.0, ny, "Black theme (pure black panels)", st_get("theme_black", 0.0) > 0.5) { st_set("theme_black", 1.0 - st_get("theme_black", 0.0)) }
    bind ny = ny + 24.0
    g_text(x + 16.0, ny + 5.0, 12.0, C_TEXT, "Autosave copies of unsaved work every")
    bind nv = u_dragval("set_autosave", x + 262.0, ny, 56.0, 24.0, st_get("autosave_min", 1.0), 0.0, 30.0, 0.05, num_str(round(st_get("autosave_min", 1.0) * 10.0) / 10.0))
    if u_has(nv) { st_set("autosave_min", round(nv * 10.0) / 10.0) }
    g_text(x + 326.0, ny + 5.0, 12.0, C_TEXT, "minutes (0 = off)")
    bind ny = ny + 30.0
    g_text(x + 16.0, ny + 5.0, 12.0, C_TEXT, "Text size")
    if u_button("set_am", x + 80.0, ny, 30.0, 24.0, "A-", 0.0) { text_size_set(text_k() - 0.1) }
    if u_button("set_ar", x + 113.0, ny, 56.0, 24.0, num_str(round(text_k() * 100.0)) + "%", 0.0) { text_size_set(1.0) }
    if u_button("set_ap", x + 172.0, ny, 30.0, 24.0, "A+", 0.0) { text_size_set(text_k() + 0.1) }
    bind ny = ny + 36.0
    u_section(x + 16.0, ny, 528.0, "RUN", ds_col("ctl"))
    bind ny = ny + 20.0
    g_text(x + 16.0, ny + 5.0, 12.0, C_TEXT, "ling exe")
    bind lv = u_textfield("set_lingexe", x + 80.0, ny, 360.0, 24.0, st_get("ling_exe", ""))
    if lv != st_get("ling_exe", "") { st_set("ling_exe", lv) }
    if u_button("set_lingpick", x + 446.0, ny, 70.0, 24.0, "Browse", 0.0) {
        bind p = plat_dlg_open("Choose the ling executable", "exe", app_root())
        if p != "" { st_set("ling_exe", p) }
    }
    bind ny = ny + 28.0
    g_text(x + 80.0, ny, 11.0, C_TEXT_DIM, "empty = found next door (../ling/target/release/ling.exe) or `ling` on PATH: " + ling_exe_path())
    bind ny = ny + 26.0
    u_section(x + 16.0, ny, 528.0, "CLAUDE", ds_col("mod"))
    bind ny = ny + 20.0
    g_text(x + 16.0, ny + 5.0, 12.0, C_TEXT, "API key")
    bind kv = u_textfield("set_claude_pw", x + 80.0, ny, 360.0, 24.0, st_get("claude_key", ""))
    if kv != st_get("claude_key", "") { st_set("claude_key", kv) }
    bind ny = ny + 28.0
    g_text(x + 80.0, ny, 11.0, C_TEXT_DIM, sel(st_get("claude_key", "") == "", sel(plat_env("ANTHROPIC_API_KEY", "") == "", "no key: set one here or the ANTHROPIC_API_KEY environment variable", "using the ANTHROPIC_API_KEY environment variable"), "key saved in settings.txt (local file)"))
    bind ny = ny + 24.0
    g_text(x + 16.0, ny + 5.0, 12.0, C_TEXT, "Model")
    bind models = claude_models()
    bind curm = l_index(models, claude_model())
    if curm < 0.0 { bind curm = 0.0 }
    bind nm = u_combo("set_claude_model", x + 80.0, ny, 220.0, 24.0, models, curm)
    if nm >= 0.0 { st_set("claude_model", list_get(models, nm)) }
    g_text(x + 316.0, ny + 5.0, 12.0, C_TEXT, "Max tokens")
    bind tv = u_dragval("set_claude_tok", x + 392.0, ny, 70.0, 24.0, st_get("claude_tokens", 4096.0), 256.0, 32000.0, 16.0, num_str(st_get("claude_tokens", 4096.0)))
    if u_has(tv) { st_set("claude_tokens", round(tv)) }
    bind ny = ny + 40.0
    if u_button("set_open", x + 16.0, ny, 150.0, 26.0, "Open settings.txt", 0.0) {
        settings_save()
        buf_open(settings_path())
        modal_close()
    }
    if u_button("set_close", x + 430.0, ny, 110.0, 26.0, "Done", C_SEL) {
        settings_save()
        modal_close()
    }
}

# ── about / shortcuts ─────────────────────────────────────────────────────────
fn modal_about() {
    win_begin("About LingCoder", 440.0, 250.0)
    bind x = st_get("wx", 0.0)
    bind y = st_get("wy", 0.0)
    ds_word(x + 20.0, y + 12.0, 20.0, "LINGCODER", ds_col("bind"), false)
    g_text(x + 20.0, y + 44.0, 12.0, C_TEXT, "An IDE for the Ling language, written in Ling.")
    g_text(x + 20.0, y + 64.0, 12.0, C_TEXT_DIM, "Chrome: the LingStationX egui-dark look with the ROYGBIV keys.")
    g_text(x + 20.0, y + 80.0, 12.0, C_TEXT_DIM, "Editor colours: Ling Dark (editors/vscode-ling).")
    g_text(x + 20.0, y + 104.0, 12.0, C_TEXT, "Extensions live in extensions/ - plain Ling, hot reloaded.")
    g_text(x + 20.0, y + 120.0, 12.0, C_TEXT_DIM, "Help > Extension API shows how to write one.")
    g_text(x + 20.0, y + 148.0, 11.0, C_TEXT_DIM, "ling " + st_get("ling_version", "") + "   -   " + plat_os_name() + " / " + plat_arch())
    if u_button("ab_ok", x + 320.0, y + 170.0, 96.0, 26.0, "Close", C_SEL) { modal_close() }
}

fn sc_row(x, y, keys, what) {
    g_text(x, y, 11.0, C_TEXT_STRONG, keys)
    g_text(x + 120.0, y, 11.0, C_TEXT, what)
}
fn modal_shortcuts() {
    win_begin("Keyboard Shortcuts", 640.0, 430.0)
    bind x = st_get("wx", 0.0)
    bind y = st_get("wy", 0.0)
    bind c1 = x + 20.0
    bind c2 = x + 330.0
    u_section(c1, y + 6.0, 280.0, "FILES", ds_col("asm"))
    sc_row(c1, y + 24.0, "Ctrl+N / Ctrl+O", "New file / open file")
    sc_row(c1, y + 40.0, "Ctrl+Shift+O", "Open folder")
    sc_row(c1, y + 56.0, "Ctrl+S / Ctrl+Alt+S", "Save / save all")
    sc_row(c1, y + 72.0, "Ctrl+W", "Close tab")
    sc_row(c1, y + 88.0, "Ctrl+P", "Go to file (type :12 for a line)")
    sc_row(c1, y + 104.0, "Ctrl+G", "Go to line")
    sc_row(c1, y + 120.0, "Ctrl+PgUp / PgDn", "Previous / next tab")
    u_section(c1, y + 146.0, 280.0, "EDIT", ds_col("bind"))
    sc_row(c1, y + 164.0, "Ctrl+Z / Ctrl+Y", "Undo / redo")
    sc_row(c1, y + 180.0, "Ctrl+X / C / V", "Cut / copy / paste")
    sc_row(c1, y + 196.0, "Ctrl+D", "Duplicate line / selection")
    sc_row(c1, y + 212.0, "Alt+Up / Alt+Down", "Move line up / down")
    sc_row(c1, y + 228.0, "Ctrl+/", "Toggle comment")
    sc_row(c1, y + 244.0, "Tab / Shift+Tab", "Indent / outdent selection")
    sc_row(c1, y + 260.0, "Ctrl+Left / Right", "Jump by word")
    sc_row(c1, y + 276.0, "Ctrl+Home / End", "Top / bottom of file")
    u_section(c2, y + 6.0, 280.0, "FIND + RUN", ds_col("ctl"))
    sc_row(c2, y + 24.0, "Ctrl+F / Ctrl+H", "Find / replace in file")
    sc_row(c2, y + 40.0, "Ctrl+Shift+F", "Find in files")
    sc_row(c2, y + 56.0, "F3 / Shift+F3", "Next / previous match")
    sc_row(c2, y + 72.0, "F5 / Ctrl+F5", "Run file / run project")
    sc_row(c2, y + 88.0, "F7 / Shift+F5", "Build / stop")
    sc_row(c2, y + 104.0, "F8 / Shift+F8", "Next / previous problem")
    u_section(c2, y + 146.0, 280.0, "VIEW", ds_col("call"))
    sc_row(c2, y + 164.0, "Ctrl+Shift+P", "Command palette")
    sc_row(c2, y + 180.0, "Ctrl+Shift+E / F / X", "Explorer / search / extensions")
    sc_row(c2, y + 196.0, "Ctrl+J / F12", "Panel / sidebar")
    sc_row(c2, y + 212.0, "Ctrl+Shift+M / U", "Problems / output")
    sc_row(c2, y + 228.0, "Ctrl+Shift+= / -", "Text size, Ctrl+0 resets")
    sc_row(c2, y + 244.0, "Ctrl+K", "Ask Claude about the selection")
    g_text(c1, y + 310.0, 11.0, C_TEXT_DIM, "The full list is in the menus; every command is also in the palette (Ctrl+Shift+P).")
    if u_button("sc_ok", x + 520.0, y + 350.0, 96.0, 26.0, "Close", C_SEL) { modal_close() }
}

← Back to index