灵符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/project.ling
# project.ling - the opened folder, settings.txt, the status line, recent lists and the
# project file walk (Ctrl+P, Search). The "project" is just a folder; proj_root() is "" when
# only loose files are open.

fn status(msg) { st_set("status", msg) }

# where LingCoder lives (settings.txt, assets/, extensions/): the folder above ling/main.ling
fn app_find_root() {
    bind cwd = fs_cwd()
    if file_exists(p_join(cwd, "assets/font.ttf")) && is_true(fs_is_dir(p_join(cwd, "ling"))) { return p_norm(cwd) }
    if file_exists(p_join(cwd, "../assets/font.ttf")) { return p_norm(fs_parent(cwd)) }
    return p_norm(cwd)
}
fn app_root() { return st_get("app_root", ".") }

fn proj_root() { return st_get("proj_root", "") }
fn proj_name() {
    bind r = proj_root()
    if r == "" { return "no folder" }
    return p_base(r)
}
# absolute path of a project-relative one
fn proj_abs(p) {
    if p_is_abs(p) { return p_norm(p) }
    if proj_root() == "" { return p_norm(p) }
    return p_join(proj_root(), p)
}

fn project_open_path(path) {
    bind q = p_norm(path)
    if !is_true(fs_is_dir(q)) {
        if file_exists(q) { return buf_open(q) >= 0.0 }
        status("Not found: " + q)
        return false
    }
    st_set("proj_root", q)
    st_set("x_open_" + q, 1.0)
    proj_files_invalidate()
    recent_proj_add(q)
    ext_event("project", q)
    status("Folder: " + q)
    settings_save()
    return true
}
fn project_close() {
    st_set("proj_root", "")
    proj_files_invalidate()
    status("Folder closed")
}

# ── ignore rules for the walk (search, Ctrl+P, the tree shows everything) ─────
fn proj_dir_skip(name) {
    bind n = str_lower(name)
    if str_starts_with(n, ".") { return true }
    return n == "target" || n == "dist" || n == "node_modules" || n == "cache" || n == "__pycache__" || str_starts_with(n, "target-")
}
fn proj_file_skip(name, size) {
    if size > 2500000.0 { return true }
    bind e = p_ext(name)
    return e == "exe" || e == "dll" || e == "so" || e == "png" || e == "jpg" || e == "jpeg" || e == "bmp" || e == "ico" || e == "ttf" || e == "otf" || e == "wav" || e == "ogg" || e == "mp3" || e == "flac" || e == "zip" || e == "glb" || e == "pdf" || e == "lock" || e == "pyc" || e == "sf2"
}

# ── the project file list (st list "proj_files", relative paths) ──────────────
fn proj_files_invalidate() { st_set("proj_files_ok", 0.0) }
fn proj_files_walk(dir, rel, depth, cap) {
    if depth > 8.0 { return 0.0 }
    bind items = fs_list(dir)
    bind n = len(items) / 3.0
    bind i = 0.0
    while i < n {
        if st_llen("proj_files") >= cap { return 0.0 }
        bind nm = list_get(items, i * 3.0)
        bind isdir = is_true(list_get(items, i * 3.0 + 1.0))
        bind size = list_get(items, i * 3.0 + 2.0)
        bind r = sel(rel == "", nm, rel + "/" + nm)
        if isdir {
            if !proj_dir_skip(nm) { proj_files_walk(p_join(dir, nm), r, depth + 1.0, cap) }
        } else if !proj_file_skip(nm, size) {
            st_lpush("proj_files", r)
        }
        bind i = i + 1.0
    }
    return 0.0
}
fn proj_files() {
    if st_get("proj_files_ok", 0.0) > 0.5 { return st_llen("proj_files") }
    st_lclear("proj_files")
    if proj_root() != "" { proj_files_walk(proj_root(), "", 0.0, 4000.0) }
    st_set("proj_files_ok", 1.0)
    return st_llen("proj_files")
}

# ── recent projects (settings) ────────────────────────────────────────────────
fn recent_proj_add(path) {
    bind keep = [path]
    bind i = 0.0
    while i < st_llen("recent_proj") && len(keep) < 8.0 {
        bind r = st_lget("recent_proj", i)
        if r != path { bind keep = list_push(keep, r) }
        bind i = i + 1.0
    }
    sl_from_list("recent_proj", keep)
    recent_menu_rebuild()
}
fn recent_menu_rebuild() {
    bind m = []
    bind i = 0.0
    while i < st_llen("recent_proj") {
        bind p = st_lget("recent_proj", i)
        bind m = list_push(m, p_base(p) + "|recentp:" + num_str(i) + "||folder")
        bind i = i + 1.0
    }
    bind j = 0.0
    while j < st_llen("recent_files") && j < 8.0 {
        bind p = st_lget("recent_files", j)
        bind m = list_push(m, p_base(p) + "|recentf:" + num_str(j) + "||file")
        bind j = j + 1.0
    }
    if len(m) < 0.5 { bind m = ["(empty)|none||"] }
    st_set("recent_menu", m)
}

# ── settings.txt (key=value lines, at the app root) ───────────────────────────
fn settings_path() { return p_join(app_root(), "settings.txt") }
fn settings_load() {
    st_lclear("recent_proj")
    st_lclear("recent_files")
    bind t = plat_read(settings_path())
    if t == "" { recent_menu_rebuild()  return 0.0 }
    bind lines = str_split(t, "\n")
    bind i = 0.0
    while i < len(lines) {
        bind ln = str_trim(list_get(lines, i))
        bind eq = str_find(ln, "=")
        if eq > 0.0 {
            bind k = str_slice(ln, 0.0, eq)
            bind v = str_slice(ln, eq + 1.0, str_len(ln) - eq - 1.0)
            if k == "recent_proj" { st_lpush("recent_proj", v) }
            else if k == "recent_file" { st_lpush("recent_files", v) }
            else if k == "font_scale" { st_set("font_scale", clampf(to_number(v), 0.7, 1.6)) }
            else if k == "side_w" { st_set("side_w", to_number(v)) }
            else if k == "dock_h" { st_set("dock_h", to_number(v)) }
            else if k == "show_side" { st_set("show_side", to_number(v)) }
            else if k == "show_dock" { st_set("show_dock", to_number(v)) }
            else if k == "theme_black" { st_set("theme_black", to_number(v)) }
            else if k == "load_last" { st_set("load_last", to_number(v)) }
            else if k == "autosave_min" { st_set("autosave_min", to_number(v)) }
            else if k == "ling_exe" { st_set("ling_exe", v) }
            else if k == "miao_home" { st_set("miao_home", v) }
            else if k == "wrap" { st_set("wrap", to_number(v)) }
            else if k == "ext_off" { st_set("ext_off", v) }
            else if k == "claude_key" { st_set("claude_key", v) }
            else if k == "claude_model" { st_set("claude_model", v) }
            else if k == "claude_tokens" { st_set("claude_tokens", to_number(v)) }
            else if k == "claude_panel_w" { st_set("claude_panel_w", to_number(v)) }
        }
        bind i = i + 1.0
    }
    recent_menu_rebuild()
    return 0.0
}
fn settings_save() {
    if !plat_can_write() { return 0.0 }
    bind s = "# LingCoder settings (written on exit and when they change)\n"
    bind i = 0.0
    while i < st_llen("recent_proj") {
        bind s = s + "recent_proj=" + st_lget("recent_proj", i) + "\n"
        bind i = i + 1.0
    }
    bind i = 0.0
    while i < st_llen("recent_files") {
        bind s = s + "recent_file=" + st_lget("recent_files", i) + "\n"
        bind i = i + 1.0
    }
    bind s = s + "font_scale=" + num_str(st_get("font_scale", 1.0)) + "\n"
    bind s = s + "side_w=" + num_str(st_get("side_w", 230.0)) + "\n"
    bind s = s + "dock_h=" + num_str(st_get("dock_h", 180.0)) + "\n"
    bind s = s + "show_side=" + num_str(st_get("show_side", 1.0)) + "\n"
    bind s = s + "show_dock=" + num_str(st_get("show_dock", 1.0)) + "\n"
    bind s = s + "theme_black=" + num_str(st_get("theme_black", 0.0)) + "\n"
    bind s = s + "load_last=" + num_str(st_get("load_last", 1.0)) + "\n"
    bind s = s + "autosave_min=" + num_str(st_get("autosave_min", 1.0)) + "\n"
    bind s = s + "ling_exe=" + st_get("ling_exe", "") + "\n"
    bind s = s + "miao_home=" + st_get("miao_home", "") + "\n"
    bind s = s + "wrap=" + num_str(st_get("wrap", 0.0)) + "\n"
    bind s = s + "ext_off=" + st_get("ext_off", "") + "\n"
    bind s = s + "claude_key=" + st_get("claude_key", "") + "\n"
    bind s = s + "claude_model=" + st_get("claude_model", "") + "\n"
    bind s = s + "claude_tokens=" + num_str(st_get("claude_tokens", 4096.0)) + "\n"
    bind s = s + "claude_panel_w=" + num_str(st_get("claude_panel_w", 340.0)) + "\n"
    plat_write(settings_path(), s)
    return 0.0
}

# ── crash-safe copies of dirty buffers (cache/autosave/) ─────────────────────
fn autosave_dir() { return p_join(app_root(), "cache/autosave") }
fn autosave_now() {
    bind any = false
    bind s = ""
    bind i = 0.0
    while i < buf_count() {
        bind bi = buf_at(i)
        if buf_dirty(bi) && buf_path(bi) != "" {
            bind cp = p_join(autosave_dir(), "a" + num_str(bi) + ".txt")
            plat_write(cp, buf_text(bi))
            bind s = s + cp + "|" + buf_path(bi) + "\n"
            bind any = true
        }
        bind i = i + 1.0
    }
    bind rp = p_join(autosave_dir(), "recover.txt")
    if any { plat_write(rp, s) }
    else if file_exists(rp) { plat_delete(rp) }
    return 0.0
}
fn autosave_tick() {
    bind mins = st_get("autosave_min", 1.0)
    if mins < 0.1 { return 0.0 }
    bind now = st_get("now", 0.0)
    if st_has("autosave_t") < 0.5 { st_set("autosave_t", now) }
    if now - st_get("autosave_t", now) > mins * 60.0 {
        st_set("autosave_t", now)
        autosave_now()
    }
    return 0.0
}
# at start: offer the recovery copies if the last session ended with unsaved work
fn recover_check() {
    bind rp = p_join(autosave_dir(), "recover.txt")
    if !file_exists(rp) { return 0.0 }
    bind lines = str_split(plat_read(rp), "\n")
    bind n = 0.0
    bind i = 0.0
    while i < len(lines) {
        if str_find(list_get(lines, i), "|") > 0.0 { bind n = n + 1.0 }
        bind i = i + 1.0
    }
    if n > 0.5 { confirm_open("Recover session", "The last session ended with " + num_str(n) + " unsaved file(s). Restore them?", "app.recover_go") }
    return 0.0
}
fn recover_go() {
    bind rp = p_join(autosave_dir(), "recover.txt")
    bind lines = str_split(plat_read(rp), "\n")
    bind i = 0.0
    while i < len(lines) {
        bind ln = str_trim(list_get(lines, i))
        bind sp = str_find(ln, "|")
        if sp > 0.0 {
            bind cp = str_slice(ln, 0.0, sp)
            bind orig = str_slice(ln, sp + 1.0, str_len(ln) - sp - 1.0)
            if file_exists(cp) && file_exists(orig) {
                bind bi = buf_open(orig)
                if bi >= 0.0 {
                    sl_from_list(buf_key(bi, "lines"), str_split(str_replace(plat_read(cp), "\r", ""), "\n"))
                    buf_mark(bi)
                }
            }
        }
        bind i = i + 1.0
    }
    plat_delete(rp)
    status("Recovered unsaved work - review and save (Ctrl+S)")
    return 0.0
}

# ── file operations used by the explorer and menus ───────────────────────────
fn file_new_at(dir, name) {
    bind p = p_join(dir, name)
    if file_exists(p) { status("Already exists: " + name)  return 0.0 }
    plat_write(p, "")
    proj_files_invalidate()
    buf_open(p)
    return 0.0
}
fn folder_new_at(dir, name) {
    plat_mkdir(p_join(dir, name))
    proj_files_invalidate()
    return 0.0
}
fn file_rename_to(path, newname) {
    bind np = p_join(p_dir(path), newname)
    if file_exists(np) { status("Already exists: " + newname)  return 0.0 }
    if !plat_rename(path, np) { status("Could not rename " + p_base(path))  return 0.0 }
    bind bi = buf_find(path)
    if bi >= 0.0 {
        st_set(buf_key(bi, "path"), p_norm(np))
        st_set(buf_key(bi, "lang"), syn_lang(np))
    }
    proj_files_invalidate()
    status("Renamed to " + newname)
    return 0.0
}
fn file_delete_go(path) {
    bind bi = buf_find(path)
    if bi >= 0.0 { buf_close_at(buf_pos(bi)) }
    plat_delete(path)
    proj_files_invalidate()
    status("Deleted " + p_base(path))
    return 0.0
}

← Back to index