灵符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/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
}

← Back to index