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/explorer.ling
# explorer.ling - the sidebar: the Explorer file tree, Search (find in files) and the
# Extensions panel. Which one shows is st "side_tab" (the activity bar switches it).
fn side_panel(x, y, w, h) {
g_rect(x, y, w, h, C_BG_SIDE)
g_vsep(x + w - 1.0, y, h)
bind t = st_get("side_tab", "files")
if t == "files" { explorer_panel(x, y, w, h) }
else if t == "search" { search_panel(x, y, w, h) }
else if t == "ext" { ext_panel(x, y, w, h) }
else if str_starts_with(t, "ext:") { ext_side_panel(str_slice(t, 4.0, str_len(t) - 4.0), x, y, w, h) }
return 0.0
}
# ── directory cache (3 s) ─────────────────────────────────────────────────────
fn dir_list(path) {
bind ck = "dct_" + path
if st_get("now", 0.0) - st_get(ck, 0.0 - 10.0) > 3.0 {
st_set(ck, st_get("now", 0.0))
st_set("dls_" + path, fs_list(path))
}
return st_get("dls_" + path, [])
}
fn dir_invalidate(path) { st_set("dct_" + path, 0.0 - 10.0) }
# ── the explorer tree ─────────────────────────────────────────────────────────
fn explorer_panel(x, y, w, h) {
ds_word(x + 10.0, y + 10.0, 10.0, "EXPLORER", ds_col("call"), false)
bind bx = x + w - 66.0
if u_icon_button("xp_new", bx, y + 6.0, 20.0, 18.0, "plus", 10.0, C_TEXT_DIM, false) {
if proj_root() != "" { modal_text("New file in " + proj_name(), "newfile", "", proj_root()) }
}
if u_icon_button("xp_refresh", bx + 22.0, y + 6.0, 20.0, 18.0, "refresh", 10.0, C_TEXT_DIM, false) {
st_set("xp_flush", st_get("now", 0.0))
proj_files_invalidate()
}
if u_icon_button("xp_collapse", bx + 44.0, y + 6.0, 20.0, 18.0, "minus", 10.0, C_TEXT_DIM, false) { st_set("xp_collapse", st_get("now", 0.0)) }
bind root = proj_root()
bind ty = y + 30.0
bind th = h - 32.0
if root == "" {
g_text_wrap(x + 12.0, ty + 10.0, w - 24.0, 12.0, C_TEXT_DIM, "No folder open.")
if u_button("xp_open", x + 12.0, ty + 40.0, w - 24.0, 28.0, "Open Folder...", C_SEL) { do_action("file.open_folder") }
bind n = st_llen("recent_proj")
bind i = 0.0
while i < min(n, 6.0) {
bind p = st_lget("recent_proj", i)
bind ry = ty + 84.0 + i * 24.0
bind hot = u_hot(x + 8.0, ry, w - 16.0, 22.0)
if hot { g_rect(x, ry, w, 22.0, C_HOVER_ROW) }
icon_draw("folder", x + 18.0, ry + 11.0, 10.0, C_FOLDER)
g_text_fit(x + 30.0, ry + 5.0, w - 40.0, 12.0, sel(hot, C_TEXT_STRONG, C_TEXT), p_base(p))
if hot { u_tip(p) }
if hot && u_pressed() { project_open_path(p) }
bind i = i + 1.0
}
return 0.0
}
# root header row
bind rhot = u_hot(x, ty, w, 20.0)
if rhot { g_rect(x, ty, w, 20.0, C_HOVER_ROW) }
icon_draw("folder_open", x + 14.0, ty + 10.0, 11.0, C_FOLDER)
g_text_fit(x + 26.0, ty + 4.0, w - 34.0, 12.0, C_TEXT_STRONG, str_upper(p_base(root)))
if rhot && u_rpressed() { st_set("ctx_path", root) ctx_open("root", root, u_mx(), u_my() + 6.0) }
bind ty = ty + 22.0
bind th = th - 22.0
# count rows for the scrollbar, then draw
bind total = tree_count(root, 0.0) * 21.0
bind off = u_scroll_y("xp_sb", x, ty, w, th, total, st_get("xp_off", 0.0))
st_set("xp_off", off)
u_clip(x, ty, w, th)
tree_draw(root, x, ty - off, w - 12.0, 0.0, ty, ty + th)
u_unclip()
return 0.0
}
fn tree_open(path) { return st_get("x_open_" + path, 0.0) > 0.5 }
fn tree_count(dir, depth) {
if depth > 10.0 { return 0.0 }
bind items = dir_list(dir)
bind n = len(items) / 3.0
bind rows = 0.0
bind i = 0.0
while i < n {
bind rows = rows + 1.0
bind nm = list_get(items, i * 3.0)
if is_true(list_get(items, i * 3.0 + 1.0)) {
bind sub = p_join(dir, nm)
if tree_open(sub) { bind rows = rows + tree_count(sub, depth + 1.0) }
}
bind i = i + 1.0
}
return rows
}
# draws rows, returns the y after the last one
fn tree_draw(dir, x, y, w, depth, clip0, clip1) {
if depth > 10.0 { return y }
bind items = dir_list(dir)
bind n = len(items) / 3.0
bind cur = buf_cur()
bind curpath = sel(cur >= 0.0, buf_path(cur), "")
bind i = 0.0
while i < n {
bind nm = list_get(items, i * 3.0)
bind isdir = is_true(list_get(items, i * 3.0 + 1.0))
bind full = p_join(dir, nm)
if y + 21.0 > clip0 && y < clip1 {
bind hot = u_hot(x, y, w, 21.0)
bind active = !isdir && full == curpath
if active { g_rect(x, y, w + 12.0, 21.0, C_SEL_ROW) }
else if hot { g_rect(x, y, w + 12.0, 21.0, C_HOVER_ROW) }
bind ix = x + 12.0 + depth * 14.0
if isdir {
icon_draw(sel(tree_open(full), "chev_d", "chev_r"), ix - 4.0, y + 10.0, 8.0, C_TEXT_DIM)
icon_draw(sel(tree_open(full), "folder_open", "folder"), ix + 10.0, y + 10.0, 10.0, C_FOLDER)
} else {
icon_draw(ext_icon(p_ext(nm)), ix + 10.0, y + 10.0, 10.0, ext_col(p_ext(nm)))
}
bind tc = C_TEXT
if active { bind tc = C_TEXT_STRONG }
bind dirty = false
if !isdir {
bind obj = buf_find(full)
if obj >= 0.0 && buf_dirty(obj) { bind dirty = true }
}
g_text_fit(ix + 22.0, y + 4.0, w - (ix + 26.0 - x), 12.0, tc, nm + sel(dirty, " *", ""))
if hot && u_pressed() {
if isdir { st_set("x_open_" + full, 1.0 - st_get("x_open_" + full, 0.0)) }
else { buf_open(full) }
}
if hot && u_rpressed() {
st_set("ctx_path", full)
ctx_open(sel(isdir, "folder", "file"), full, u_mx(), u_my() + 6.0)
}
}
bind y = y + 21.0
if isdir && tree_open(full) {
if st_get("xp_collapse", 0.0) > st_get("xpc_seen_" + full, 0.0) {
st_set("x_open_" + full, 0.0)
st_set("xpc_seen_" + full, st_get("xp_collapse", 0.0))
} else {
if st_get("xp_flush", 0.0) > st_get("dfl_seen_" + full, 0.0) {
dir_invalidate(full)
st_set("dfl_seen_" + full, st_get("xp_flush", 0.0))
}
bind y = tree_draw(full, x, y, w, depth + 1.0, clip0, clip1)
}
}
bind i = i + 1.0
}
return y
}
# ── find in files ─────────────────────────────────────────────────────────────
fn search_panel(x, y, w, h) {
ds_word(x + 10.0, y + 10.0, 10.0, "SEARCH", ds_col("ctl"), false)
bind q = u_textfield("search_q", x + 10.0, y + 28.0, w - 20.0, 24.0, st_get("search_q", ""))
st_set("search_q", q)
bind go = false
if st_get("focus", "") == "search_q" && key_pressed("enter") { bind go = true }
if u_button("search_go", x + 10.0, y + 58.0, 74.0, 22.0, "Search", C_SEL) { bind go = true }
bind cs = st_get("search_case", 0.0) > 0.5
if u_check("search_case", x + 94.0, y + 61.0, "Match case", cs) { st_set("search_case", 1.0 - st_get("search_case", 0.0)) }
if go && str_trim(q) != "" { search_run(str_trim(q)) }
bind n = st_llen("sr_line")
bind ry0 = y + 88.0
bind rh = h - 92.0
if st_get("sr_done", 0.0) > 0.5 {
g_text(x + 10.0, ry0 - 16.0, 11.0, C_TEXT_DIM, num_str(n) + " hit" + sel(n == 1.0, "", "s") + " in " + num_str(st_get("sr_files", 0.0)) + " file(s)" + sel(st_get("sr_capped", 0.0) > 0.5, " (first 300)", ""))
}
bind off = u_scroll_y("sr_sb", x, ry0, w, rh, n * 32.0, st_get("sr_off", 0.0))
st_set("sr_off", off)
u_clip(x, ry0, w, rh)
bind i = floor(off / 32.0)
bind last = min(n, i + ceil(rh / 32.0) + 1.0)
while i < last {
bind ry = ry0 + i * 32.0 - off
bind f = st_lget("sr_file", i)
bind lnum = st_lget("sr_line", i)
bind hot = u_hot(x, ry, w - 12.0, 31.0)
if hot { g_rect(x, ry, w, 31.0, C_HOVER_ROW) }
icon_draw(ext_icon(p_ext(f)), x + 14.0, ry + 9.0, 9.0, ext_col(p_ext(f)))
g_text_fit(x + 26.0, ry + 3.0, w - 60.0, 11.0, sel(hot, C_TEXT_STRONG, C_TEXT), f + " : " + num_str(lnum))
g_text_fit(x + 26.0, ry + 17.0, w - 40.0, 10.0, C_TEXT_DIM, str_trim(st_lget("sr_text", i)))
if hot && u_pressed() {
bind bi = buf_open(proj_abs(f))
if bi >= 0.0 {
ed_goto_line(bi, lnum - 1.0)
st_set("find_q", st_get("search_q", ""))
}
}
bind i = i + 1.0
}
u_unclip()
if n < 0.5 && st_get("sr_done", 0.0) > 0.5 { g_text(x + 12.0, ry0 + 8.0, 12.0, C_TEXT_DIM, "Nothing found.") }
if proj_root() == "" { g_text_wrap(x + 10.0, ry0 + 8.0, w - 20.0, 11.0, C_TEXT_DIM, "Open a folder to search in it (Ctrl+Shift+O).") }
return 0.0
}
fn search_run(q) {
st_lclear("sr_file")
st_lclear("sr_line")
st_lclear("sr_text")
st_set("sr_capped", 0.0)
st_set("sr_files", 0.0)
if proj_root() == "" { st_set("sr_done", 1.0) return 0.0 }
bind cs = st_get("search_case", 0.0) > 0.5
bind needle = sel(cs, q, str_lower(q))
bind n = proj_files()
bind fi = 0.0
while fi < n && st_llen("sr_line") < 300.0 {
bind rel = st_lget("proj_files", fi)
bind body = plat_read(proj_abs(rel))
bind hay = sel(cs, body, str_lower(body))
if str_find(hay, needle) >= 0.0 {
st_set("sr_files", st_get("sr_files", 0.0) + 1.0)
bind lines = str_split(hay, "\n")
bind raw = str_split(body, "\n")
bind li = 0.0
while li < len(lines) && st_llen("sr_line") < 300.0 {
if str_find(list_get(lines, li), needle) >= 0.0 {
st_lpush("sr_file", rel)
st_lpush("sr_line", li + 1.0)
st_lpush("sr_text", g_fit(10.0, 400.0, list_get(raw, li)))
}
bind li = li + 1.0
}
}
bind fi = fi + 1.0
}
if st_llen("sr_line") >= 300.0 { st_set("sr_capped", 1.0) }
st_set("sr_done", 1.0)
status("Search: " + num_str(st_llen("sr_line")) + " hit(s) for \"" + q + "\"")
return 0.0
}
# ── extensions panel ─────────────────────────────────────────────────────────
fn ext_panel(x, y, w, h) {
ds_word(x + 10.0, y + 10.0, 10.0, "EXTENSIONS", ds_col("func"), false)
bind n = st_llen("ext_names")
bind ry0 = y + 32.0
bind rh = h - 76.0
bind off = u_scroll_y("ex_sb", x, ry0, w, rh, n * 64.0, st_get("ex_off", 0.0))
st_set("ex_off", off)
u_clip(x, ry0, w, rh)
bind i = 0.0
while i < n {
bind ry = ry0 + 4.0 + i * 64.0 - off
if ry + 64.0 > ry0 && ry < ry0 + rh {
bind stem = st_lget("ext_names", i)
bind err = st_get("ext_err_" + stem, "")
bind on = ext_enabled(stem)
bind col = sel(err != "", ds_col("asm"), sel(on, ds_col("func"), ds_col("ink")))
px_rect(x + 8.0, ry, w - 22.0, 58.0, c_mix(C_BG_PANEL, col, 0.05))
px_outline(x + 8.0, ry, w - 22.0, 58.0, c_mix(C_BG_PANEL, col, 0.5), 0.7)
ds_brackets(x + 8.0, ry, w - 22.0, 58.0, 5.0, col)
g_text_fit(x + 18.0, ry + 7.0, w - 90.0, 13.0, sel(on, C_TEXT_STRONG, C_TEXT_DIM), st_get("ext_title_" + stem, stem))
if err != "" { g_text_fit(x + 18.0, ry + 26.0, w - 40.0, 10.0, ds_col("asm"), err) }
else { g_text_fit(x + 18.0, ry + 26.0, w - 40.0, 10.0, C_TEXT_DIM, st_get("ext_desc_" + stem, "")) }
g_text(x + 18.0, ry + 42.0, 10.0, C_TEXT_DIM, stem + ".ling")
bind tog = u_chip("ex_on" + stem, x + w - 70.0, ry + 5.0, 52.0, 18.0, sel(on, "on", "off"), on, sel(on, ds_col("ctl"), ds_col("ink")))
if tog { ext_toggle(stem) }
}
bind i = i + 1.0
}
if n < 0.5 { g_text_wrap(x + 12.0, ry0 + 10.0, w - 24.0, 12.0, C_TEXT_DIM, "No extensions found. New Extension makes a working example in extensions/.") }
u_unclip()
bind by = y + h - 36.0
if u_button("ex_new", x + 8.0, by, 66.0, 24.0, "New", 0.0) { do_action("tools.ext_new") }
if u_button("ex_reload", x + 78.0, by, 66.0, 24.0, "Reload", 0.0) { do_action("tools.ext_reload") }
if u_button("ex_dir", x + 148.0, by, 66.0, 24.0, "Folder", 0.0) { do_action("tools.ext_folder") }
return 0.0
}