L
lingcoder
↓ 0 downloadsLingCoder: 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 lingcoderCode / 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
}