灵符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/actions.ling
# actions.ling - every command behind the menus, the palette and the keyboard, plus the
# unsaved-changes flows. Action ids look like "file.save"; extension commands are
# "ext:<stem>:<id>" and go to the extension host.

fn buf_save_as(bi, path) {
    if bi < 0.0 || path == "" { return false }
    bind q = p_norm(path)
    if plat_write(q, buf_text(bi) + "\n") {
        st_set(buf_key(bi, "path"), q)
        st_set(buf_key(bi, "lang"), syn_lang(q))
        st_set(buf_key(bi, "dirty"), 0.0)
        recent_file_add(q)
        proj_files_invalidate()
        status("Saved as " + p_base(q))
        ext_event("save", q)
        return true
    }
    status("Could not write " + q)
    return false
}

# close tab k, with the unsaved guard
fn tab_close_ask(k) {
    bind bi = buf_at(k)
    if buf_dirty(bi) {
        unsaved_ask(k, "tabx.close_go")
        return 0.0
    }
    st_set("unsaved_k", k)
    do_action("tabx.close_go")
    return 0.0
}

# quitting: guard each dirty buffer in turn, then really quit
fn quit_request() {
    bind i = 0.0
    while i < buf_count() {
        if buf_dirty(buf_at(i)) {
            st_set("quit_pending", 1.0)
            unsaved_ask(i, "file.quit")
            return 0.0
        }
        bind i = i + 1.0
    }
    st_set("quit", 1.0)
    return 0.0
}

fn do_action(a) {
    if a == "" || a == "none" { return 0.0 }
    bind bi = buf_cur()
    # ── file ──
    if a == "file.new" {
        if proj_root() != "" { modal_text("New file in " + proj_name(), "newfile", "", proj_root()) }
        else {
            bind p = plat_dlg_save("New file", "ling,toml,md,txt", "untitled.ling", plat_home())
            if p != "" {
                plat_write(p, "")
                buf_open(p)
            }
        }
        return 0.0
    }
    if a == "file.open" {
        bind p = plat_dlg_open("Open file", "", sel(proj_root() == "", plat_home(), proj_root()))
        if p != "" { buf_open(p) }
        return 0.0
    }
    if a == "file.open_folder" {
        bind p = plat_dlg_folder("Open folder", sel(proj_root() == "", plat_home(), p_dir(proj_root())))
        if p != "" { project_open_path(p) }
        return 0.0
    }
    if str_starts_with(a, "recentp:") {
        project_open_path(st_lget("recent_proj", to_number(str_slice(a, 8.0, str_len(a) - 8.0))))
        return 0.0
    }
    if str_starts_with(a, "recentf:") {
        buf_open(st_lget("recent_files", to_number(str_slice(a, 8.0, str_len(a) - 8.0))))
        return 0.0
    }
    if a == "file.save" { buf_save(bi)  return 0.0 }
    if a == "file.save_as" {
        if bi < 0.0 { return 0.0 }
        bind p = plat_dlg_save("Save as", "", p_base(buf_path(bi)), p_dir(buf_path(bi)))
        if p != "" { buf_save_as(bi, p) }
        return 0.0
    }
    if a == "file.save_all" { buf_save_all()  return 0.0 }
    if a == "file.close_tab" {
        if st_get("ptab_active", 0.0) > 0.5 { ide_close_panel_tab() }
        else if buf_count() > 0.5 { tab_close_ask(st_get("buf_cur", 0.0)) }
        return 0.0
    }
    if a == "file.close_folder" { project_close()  return 0.0 }
    if a == "file.settings" { modal_open("settings")  return 0.0 }
    if a == "file.quit" { quit_request()  return 0.0 }
    # ── edit ──
    if a == "edit.undo" { if bi >= 0.0 { buf_undo(bi) }  return 0.0 }
    if a == "edit.redo" { if bi >= 0.0 { buf_redo(bi) }  return 0.0 }
    if a == "edit.cut" { if bi >= 0.0 { ed_cut(bi) }  return 0.0 }
    if a == "edit.copy" { if bi >= 0.0 { ed_copy(bi) }  return 0.0 }
    if a == "edit.paste" { if bi >= 0.0 { ed_paste(bi) }  return 0.0 }
    if a == "edit.select_all" { if bi >= 0.0 { ed_select_all(bi) }  return 0.0 }
    if a == "edit.find" { find_open(false)  return 0.0 }
    if a == "edit.replace" { find_open(true)  return 0.0 }
    if a == "edit.findfiles" { side_show("search")  return 0.0 }
    if a == "edit.comment" { if bi >= 0.0 { ed_toggle_comment(bi) }  return 0.0 }
    if a == "edit.dup_line" { if bi >= 0.0 { ed_dup_line(bi) }  return 0.0 }
    if a == "edit.line_up" { if bi >= 0.0 { ed_move_line(bi, 0.0 - 1.0) }  return 0.0 }
    if a == "edit.line_down" { if bi >= 0.0 { ed_move_line(bi, 1.0) }  return 0.0 }
    # ── view ──
    if a == "view.files" { side_show("files")  return 0.0 }
    if a == "view.search" { side_show("search")  return 0.0 }
    if a == "view.ext" { side_show("ext")  return 0.0 }
    if a == "view.side" { st_set("show_side", 1.0 - st_get("show_side", 1.0))  return 0.0 }
    if a == "view.dock" { st_set("show_dock", 1.0 - st_get("show_dock", 1.0))  return 0.0 }
    if a == "view.problems" {
        st_set("show_dock", 1.0)
        st_set("dock_tab", 0.0)
        return 0.0
    }
    if a == "view.output" {
        st_set("show_dock", 1.0)
        st_set("dock_tab", 1.0)
        return 0.0
    }
    if a == "view.welcome" {
        bind p = p_join(app_root(), "README.md")
        if file_exists(p) { buf_open(p) } else { status("Welcome to LingCoder") }
        return 0.0
    }
    if a == "text.bigger" { text_size_set(text_k() + 0.1)  return 0.0 }
    if a == "text.smaller" { text_size_set(text_k() - 0.1)  return 0.0 }
    if a == "text.reset" { text_size_set(1.0)  return 0.0 }
    # ── go ──
    if a == "go.file" { pal_open("file")  return 0.0 }
    if a == "go.palette" { pal_open("cmd")  return 0.0 }
    if a == "go.line" {
        if bi >= 0.0 { modal_text("Go to line (1-" + num_str(buf_nlines(bi)) + ")", "goto", "", "") }
        return 0.0
    }
    if a == "go.next_tab" || a == "go.prev_tab" {
        bind n = buf_count()
        if n > 0.5 {
            bind d = sel(a == "go.next_tab", 1.0, n - 1.0)
            st_set("buf_cur", (st_get("buf_cur", 0.0) + d) % n)
            st_set("focus", "code")
            ed_sync_text()
        }
        return 0.0
    }
    if a == "go.next_problem" { problem_jump(1.0)  return 0.0 }
    if a == "go.prev_problem" { problem_jump(0.0 - 1.0)  return 0.0 }
    # ── run ──
    if a == "run.file" { run_file()  return 0.0 }
    if a == "run.project" { run_project()  return 0.0 }
    if a == "run.build" { run_build()  return 0.0 }
    if a == "run.stop" { run_stop()  return 0.0 }
    if a == "run.clear" { run_clear()  return 0.0 }
    if a == "run.pick_exe" { modal_text("Path to ling.exe (empty = auto)", "ling_exe", st_get("ling_exe", ""), "")  return 0.0 }
    # ── tools / help ──
    if a == "tools.ext_reload" { ext_reload_all()  return 0.0 }
    if a == "tools.ext_new" { modal_text("New extension name (letters, no spaces)", "ext_new", "myext", "")  return 0.0 }
    if a == "tools.ext_folder" { fs_reveal(ext_dir())  return 0.0 }
    if a == "help.about" { modal_open("about")  return 0.0 }
    if a == "help.shortcuts" { modal_open("shortcuts")  return 0.0 }
    if a == "help.extapi" {
        bind p = p_join(app_root(), "docs/EXTENSIONS.md")
        if file_exists(p) { buf_open(p) } else { status("docs/EXTENSIONS.md is missing") }
        return 0.0
    }
    # ── tab context menu ──
    if a == "tabx.close" { tab_close_ask(st_get("ctx_tab", 0.0))  return 0.0 }
    if a == "tabx.close_go" {
        bind k = st_get("unsaved_k", 0.0)
        if k < buf_count() { buf_close_at(k) }
        if st_get("quit_pending", 0.0) > 0.5 { quit_request() }
        return 0.0
    }
    if a == "tabx.others" {
        bind keep = buf_at(st_get("ctx_tab", 0.0))
        bind guard = 0.0
        while buf_count() > 1.0 && guard < 200.0 {
            bind k = sel(buf_at(0.0) == keep, 1.0, 0.0)
            if buf_dirty(buf_at(k)) { unsaved_ask(k, "tabx.others")  return 0.0 }
            buf_close_at(k)
            bind guard = guard + 1.0
        }
        st_set("buf_cur", 0.0)
        ed_sync_text()
        return 0.0
    }
    if a == "tabx.all" {
        bind guard = 0.0
        while buf_count() > 0.5 && guard < 200.0 {
            if buf_dirty(buf_at(0.0)) { unsaved_ask(0.0, "tabx.all")  return 0.0 }
            buf_close_at(0.0)
            bind guard = guard + 1.0
        }
        return 0.0
    }
    if a == "tabx.copy_path" {
        plat_clip_set(buf_path(buf_at(st_get("ctx_tab", 0.0))))
        status("Path copied")
        return 0.0
    }
    if a == "tabx.reveal" { plat_reveal(buf_path(buf_at(st_get("ctx_tab", 0.0))))  return 0.0 }
    # ── app ──
    if a == "app.recover_go" { recover_go()  return 0.0 }
    if a == "app.quit" { quit_request()  return 0.0 }
    # ── extension commands ──
    if str_starts_with(a, "ext:") { ext_run_action(a)  return 0.0 }
    status("Unknown command: " + a)
    return 0.0
}

# the explorer / tab context menus land here (ctx_kind + ctx_arg are set)
fn do_ctx_action(a) {
    bind kind = st_get("ctx_kind", "")
    bind arg = st_get("ctx_arg", "")
    if kind == "edit" { do_action(a)  return 0.0 }
    if kind == "tab" { do_action(a)  return 0.0 }
    if a == "fx.open" { buf_open(arg)  return 0.0 }
    if a == "fx.run" {
        buf_open(arg)
        run_file()
        return 0.0
    }
    if a == "fx.new_file" {
        bind dir = sel(kind == "file", p_dir(arg), arg)
        modal_text("New file in " + p_base(dir), "newfile", "", dir)
        return 0.0
    }
    if a == "fx.new_folder" {
        bind dir = sel(kind == "file", p_dir(arg), arg)
        modal_text("New folder in " + p_base(dir), "newfolder", "", dir)
        return 0.0
    }
    if a == "fx.rename" {
        modal_text("Rename " + p_base(arg), "rename", p_base(arg), arg)
        return 0.0
    }
    if a == "fx.dup" {
        bind e = p_ext(arg)
        bind stem = p_stem(arg)
        bind dst = p_join(p_dir(arg), stem + " copy" + sel(e == "", "", "." + e))
        plat_copy(arg, dst)
        dir_invalidate(p_dir(arg))
        proj_files_invalidate()
        return 0.0
    }
    if a == "fx.copy_path" {
        plat_clip_set(arg)
        status("Path copied")
        return 0.0
    }
    if a == "fx.reveal" { plat_reveal(arg)  return 0.0 }
    if a == "fx.refresh" {
        st_set("xp_flush", st_get("now", 0.0))
        dir_invalidate(arg)
        proj_files_invalidate()
        return 0.0
    }
    if a == "fx.delete" {
        st_set("confirm_path", arg)
        confirm_open("Delete", "Permanently delete " + p_base(arg) + sel(is_true(fs_is_dir(arg)), " and everything in it?", " from disk?"), "fx.delete_go")
        return 0.0
    }
    if a == "fx.delete_go" {
        file_delete_go(st_get("confirm_path", ""))
        dir_invalidate(p_dir(st_get("confirm_path", "")))
        return 0.0
    }
    do_action(a)
    return 0.0
}

# ── the keyboard (no overlay open) ────────────────────────────────────────────
fn keyboard() {
    bind ctrl = u_ctrl()
    bind shift = u_shift()
    bind alt = u_alt()
    bind focus = st_get("focus", "")
    bind intext = focus != "" && focus != "code"
    if ctrl {
        if key_pressed("n") { do_action("file.new") }
        if key_pressed("o") { do_action(sel(shift, "file.open_folder", "file.open")) }
        if key_pressed("s") {
            if alt { do_action("file.save_all") } else { do_action("file.save") }
        }
        if key_pressed("w") { do_action("file.close_tab") }
        if key_pressed("q") { do_action("file.quit") }
        if key_pressed("comma") { do_action("file.settings") }
        if key_pressed("p") { do_action(sel(shift, "go.palette", "go.file")) }
        if key_pressed("g") { do_action("go.line") }
        if !intext {
            if key_pressed("f") { do_action(sel(shift, "edit.findfiles", "edit.find")) }
            if key_pressed("h") { do_action("edit.replace") }
        }
        if key_pressed("j") { do_action("view.dock") }
        if shift && key_pressed("e") { do_action("view.files") }
        if shift && key_pressed("x") { do_action("view.ext") }
        if shift && key_pressed("m") { do_action("view.problems") }
        if shift && key_pressed("u") { do_action("view.output") }
        if key_pressed("k") { ext_claude_quick() }
        if key_pressed("f5") { do_action("run.project") }
        if shift && (key_pressed("=") || key_pressed("equals") || key_pressed("numpad+")) { text_size_set(text_k() + 0.1) }
        if shift && (key_pressed("-") || key_pressed("minus") || key_pressed("numpad-")) { text_size_set(text_k() - 0.1) }
        if key_pressed("0") || key_pressed("numpad0") { text_size_set(1.0) }
        if key_pressed("pagedown") { do_action("go.next_tab") }
        if key_pressed("pageup") { do_action("go.prev_tab") }
    } else {
        if key_pressed("f1") { do_action("help.shortcuts") }
        if key_pressed("f3") && buf_cur() >= 0.0 { ed_find_next(buf_cur(), sel(shift, 0.0 - 1.0, 1.0)) }
        if key_pressed("f5") { do_action(sel(shift, "run.stop", "run.file")) }
        if key_pressed("f7") { do_action("run.build") }
        if key_pressed("f8") { do_action(sel(shift, "go.prev_problem", "go.next_problem")) }
        if key_pressed("f12") { do_action("view.side") }
    }
    if focus == "code" && buf_cur() >= 0.0 { ed_keys(buf_cur()) }
    return 0.0
}

← Back to index