灵符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/ext.ling
# ext.ling - the extension host. An extension is plain Ling in extensions/<name>/<name>.ling
# (or extensions/<name>.ling): every function starts with "<name>_", it is loaded with the
# runtime's script_load (which refuses other prefixes, so an extension cannot override the
# IDE), called with script_call (errors are caught and shown in the Extensions panel), and
# reloaded by itself when the file changes. See docs/EXTENSIONS.md; the hooks:
#   <name>_info()                -> [title, description, version]      (required)
#   <name>_commands()            -> ["id|Label|Shortcut|icon", ...]    (optional; Tools menu + palette)
#   <name>_run(id)               -> message                            (with _commands)
#   <name>_panel_info()          -> [title, icon]                      (optional; an activity-bar panel)
#   <name>_panel(x, y, w, h)                                           (with _panel_info; drawn each frame)
#   <name>_tick()                                                      (optional; every frame)
#   <name>_on_event(ev, arg)                                           (optional; ev: open save project buffer)
#   <name>_ctx_edit()            -> menu items for the editor's right-click menu (optional)

fn ext_dir() {
    bind d = p_join(app_root(), "extensions")
    plat_mkdir(d)
    return d
}

fn ext_enabled(stem) { return str_find(st_get("ext_off", ""), "|" + stem + "|") < 0.0 }
fn ext_toggle(stem) {
    bind off = st_get("ext_off", "")
    if ext_enabled(stem) {
        st_set("ext_off", off + "|" + stem + "|")
        status("Extension " + stem + " disabled")
    } else {
        st_set("ext_off", str_replace(off, "|" + stem + "|", ""))
        st_set("ext_mt_" + stem, 0.0)
        status("Extension " + stem + " enabled")
    }
    settings_save()
    ext_rebuild()
    return 0.0
}

# where <stem>'s file lives: extensions/<stem>/<stem>.ling or extensions/<stem>.ling
fn ext_path(stem) {
    bind nested = p_join(ext_dir(), stem + "/" + stem + ".ling")
    if file_exists(nested) { return nested }
    return p_join(ext_dir(), stem + ".ling")
}

# find every extension file; reload the changed ones; rebuild menus when something changed
fn ext_scan() {
    st_set("ext_scan_t", st_get("now", 0.0))
    bind items = fs_list(ext_dir())
    bind n = len(items) / 3.0
    bind names = []
    bind changed = false
    bind i = 0.0
    while i < n {
        bind nm = list_get(items, i * 3.0)
        bind stem = ""
        if is_true(list_get(items, i * 3.0 + 1.0)) {
            if file_exists(p_join(ext_dir(), nm + "/" + nm + ".ling")) { bind stem = nm }
        } else if p_ext(nm) == "ling" {
            bind stem = p_stem(nm)
        }
        if stem != "" && !l_has(names, stem) {
            bind names = list_push(names, stem)
            if ext_enabled(stem) {
                bind mt = plat_file_modified(ext_path(stem))
                if mt != st_get("ext_mt_" + stem, 0.0) {
                    st_set("ext_mt_" + stem, mt)
                    ext_load(stem)
                    bind changed = true
                }
            }
        }
        bind i = i + 1.0
    }
    sl_from_list("ext_names", names)
    if changed { ext_rebuild() }
    return 0.0
}

fn ext_load(stem) {
    bind r = script_load(ext_path(stem))
    if !is_true(list_get(r, 0)) {
        st_set("ext_err_" + stem, "" + list_get(r, 1))
        st_set("ext_title_" + stem, stem)
        status("Extension " + stem + " failed to load")
        return 0.0
    }
    st_set("ext_err_" + stem, "")
    bind inf = script_call(stem + "_info", [])
    if list_get(inf, 0) == true {
        bind v = list_get(inf, 1)
        st_set("ext_title_" + stem, list_get(v, 0))
        st_set("ext_desc_" + stem, list_get(v, 1))
    } else {
        st_set("ext_title_" + stem, stem)
        st_set("ext_desc_" + stem, "")
        st_set("ext_err_" + stem, "no " + stem + "_info(): " + list_get(inf, 1))
    }
    # capabilities, probed once per load
    bind t = script_call(stem + "_tick", [])
    st_set("ext_tick_" + stem, bool_num(list_get(t, 0) == true))
    bind p = script_call(stem + "_panel_info", [])
    if list_get(p, 0) == true {
        bind v = list_get(p, 1)
        st_set("ext_panel_" + stem, list_get(v, 0) + "|" + list_get(v, 1))
    } else { st_set("ext_panel_" + stem, "") }
    bind ini = script_call(stem + "_init", [])
    status("Extension loaded: " + st_get("ext_title_" + stem, stem))
    return 0.0
}

# rebuild the Tools-menu / activity-bar / context-menu contributions
fn ext_rebuild() {
    bind menu = []
    st_lclear("ext_panels")
    bind ctxe = []
    bind n = st_llen("ext_names")
    bind i = 0.0
    while i < n {
        bind stem = st_lget("ext_names", i)
        if ext_enabled(stem) && st_get("ext_err_" + stem, "") == "" {
            bind c = script_call(stem + "_commands", [])
            if list_get(c, 0) == true {
                bind items = list_get(c, 1)
                bind k = 0.0
                while k < len(items) {
                    bind it = list_get(items, k)
                    bind parts = str_split(it, "|")
                    if len(parts) >= 2.0 {
                        bind menu = list_push(menu, list_get(parts, 1) + "|ext:" + stem + ":" + list_get(parts, 0) + "|" + sel(len(parts) > 2.0, list_get(parts, 2), "") + "|" + sel(len(parts) > 3.0, list_get(parts, 3), "chip"))
                    }
                    bind k = k + 1.0
                }
            }
            bind pinfo = st_get("ext_panel_" + stem, "")
            if pinfo != "" {
                bind pp = str_split(pinfo, "|")
                st_lpush("ext_panels", stem + "|" + list_get(pp, 0) + "|" + sel(len(pp) > 1.0, list_get(pp, 1), "chip"))
            }
            bind cx = script_call(stem + "_ctx_edit", [])
            if list_get(cx, 0) == true {
                bind items = list_get(cx, 1)
                bind k = 0.0
                while k < len(items) {
                    bind ctxe = list_push(ctxe, list_get(items, k))
                    bind k = k + 1.0
                }
            }
        }
        bind i = i + 1.0
    }
    st_set("ext_menu", menu)
    st_set("ext_ctx_edit", ctxe)
    return 0.0
}

fn ext_reload_all() {
    bind i = 0.0
    while i < st_llen("ext_names") {
        st_set("ext_mt_" + st_lget("ext_names", i), 0.0)
        bind i = i + 1.0
    }
    ext_scan()
    status("Extensions reloaded")
    return 0.0
}

# every frame: the per-extension ticks; a rescan every 2 s picks up edited files
fn ext_tick_all() {
    if st_get("now", 0.0) - st_get("ext_scan_t", 0.0 - 10.0) > 2.0 { ext_scan() }
    bind n = st_llen("ext_names")
    bind i = 0.0
    while i < n {
        bind stem = st_lget("ext_names", i)
        if ext_enabled(stem) && st_get("ext_tick_" + stem, 0.0) > 0.5 {
            bind r = script_call(stem + "_tick", [])
            if list_get(r, 0) != true { st_set("ext_err_" + stem, "" + list_get(r, 1)) }
        }
        bind i = i + 1.0
    }
    return 0.0
}

# events from the IDE: "open" path, "save" path, "project" root, "buffer" id
fn ext_event(ev, arg) {
    bind n = st_llen("ext_names")
    bind i = 0.0
    while i < n {
        bind stem = st_lget("ext_names", i)
        if ext_enabled(stem) && st_get("ext_err_" + stem, "") == "" {
            bind r = script_call(stem + "_on_event", [ev, arg])
        }
        bind i = i + 1.0
    }
    return 0.0
}

# "ext:<stem>:<id>" from a menu / the palette / a context menu
fn ext_run_action(a) {
    bind rest = str_slice(a, 4.0, str_len(a) - 4.0)
    bind cp = str_find(rest, ":")
    if cp < 0.0 { return 0.0 }
    bind stem = str_slice(rest, 0.0, cp)
    bind id = str_slice(rest, cp + 1.0, str_len(rest) - cp - 1.0)
    bind r = script_call(stem + "_run", [id])
    if list_get(r, 0) == true {
        bind msg = list_get(r, 1)
        if msg != "" && msg != 0.0 { status("" + msg) }
    } else {
        st_set("ext_err_" + stem, "" + list_get(r, 1))
        status("Extension " + stem + ": " + list_get(r, 1))
    }
    return 0.0
}

# an extension view as a full editor tab (the Claude chat tab)
fn ide_open_panel_tab(stem, title, icon) {
    st_set("ptab_stem", stem)
    st_set("ptab_title", title)
    st_set("ptab_icon", icon)
    st_set("ptab_on", 1.0)
    st_set("ptab_active", 1.0)
    st_set("focus", "")
    return 0.0
}
fn ide_close_panel_tab() {
    st_set("ptab_on", 0.0)
    st_set("ptab_active", 0.0)
    if buf_cur() >= 0.0 { st_set("focus", "code")  ed_sync_text() }
    return 0.0
}

# an extension's activity-bar panel
fn ext_side_panel(stem, x, y, w, h) {
    bind r = script_call(stem + "_panel", [x, y, w, h])
    if list_get(r, 0) != true {
        g_text_wrap(x + 12.0, y + 12.0, w - 24.0, 11.0, ds_col("asm"), "Panel error in " + stem + ": " + list_get(r, 1))
    }
    return 0.0
}

# Ctrl+K: the Claude extension's quick ask, when it is installed
fn ext_claude_quick() {
    if l_has(sl_to_list("ext_names"), "claude") && ext_enabled("claude") {
        ext_run_action("ext:claude:ask_selection")
    } else {
        status("The Claude extension is not loaded (see the Extensions panel, Ctrl+Shift+X)")
    }
    return 0.0
}

# ── the IDE API extensions call (keep in step with docs/EXTENSIONS.md) ─────────
fn ide_status(msg) { status(msg)  return 0.0 }
fn ide_open(path) { return buf_open(path) }
fn ide_cur_path() {
    bind bi = buf_cur()
    if bi < 0.0 { return "" }
    return buf_path(bi)
}
fn ide_cur_text() {
    bind bi = buf_cur()
    if bi < 0.0 { return "" }
    return buf_text(bi)
}
fn ide_selection() {
    bind bi = buf_cur()
    if bi < 0.0 { return "" }
    return ed_sel_text(bi)
}
fn ide_cursor_line() {
    bind bi = buf_cur()
    if bi < 0.0 { return 0.0 }
    return st_get(buf_key(bi, "cl"), 0.0)
}
fn ide_insert(text) {
    bind bi = buf_cur()
    if bi < 0.0 { return false }
    ed_insert(bi, text)
    return true
}
fn ide_replace_selection(text) {
    bind bi = buf_cur()
    if bi < 0.0 { return false }
    ed_insert(bi, text)
    return true
}
fn ide_goto(line) {
    ed_goto_line(buf_cur(), line)
    return 0.0
}
fn ide_project_root() { return proj_root() }
fn ide_save_all() { buf_save_all()  return 0.0 }
fn ide_problems_text() {
    bind s = ""
    bind i = 0.0
    while i < st_llen("problems") {
        bind p = str_split(st_lget("problems", i), "|")
        bind s = s + "line " + list_get(p, 0) + ": " + list_get(p, 2) + "\n"
        bind i = i + 1.0
    }
    return s
}
fn ide_run_action(a) { do_action(a)  return 0.0 }

# ── Claude defaults shared by Settings and the claude extension ────────────────
fn claude_models() { return ["claude-sonnet-5-5", "claude-haiku-5-5", "claude-opus-5-5"] }
fn claude_model() {
    bind m = str_trim(st_get("claude_model", ""))
    if m == "" { return "claude-sonnet-5-5" }
    return m
}
fn claude_api_key() {
    bind k = str_trim(st_get("claude_key", ""))
    if k != "" { return k }
    return str_trim(plat_env("ANTHROPIC_API_KEY", ""))
}

# ── scaffold: Tools > New Extension ───────────────────────────────────────────
fn ext_scaffold(rawname) {
    bind name = str_lower(str_replace(str_replace(str_trim(rawname), " ", "_"), "-", "_"))
    if name == "" { return 0.0 }
    bind dir = p_join(ext_dir(), name)
    plat_mkdir(dir)
    bind path = p_join(dir, name + ".ling")
    if file_exists(path) {
        buf_open(path)
        status("That extension already exists - opened it")
        return 0.0
    }
    bind nl = "\n"
    bind s = "# " + name + ".ling - a LingCoder extension. Every function must start with \"" + name + "_\"." + nl
    bind s = s + "# Save this file and the IDE reloads it by itself. API: docs/EXTENSIONS.md (Help menu)." + nl + nl
    bind s = s + "fn " + name + "_info() { return [\"" + name + "\", \"What it does, in one line\", \"0.1\"] }" + nl + nl
    bind s = s + "# commands appear in Tools and the palette (Ctrl+Shift+P)" + nl
    bind s = s + "fn " + name + "_commands() { return [\"hello|" + name + ": Hello||chip\"] }" + nl + nl
    bind s = s + "fn " + name + "_run(id) {" + nl
    bind s = s + "    if id == \"hello\" {" + nl
    bind s = s + "        ide_insert(\"Hello from " + name + "! Editing \" + ide_cur_path())" + nl
    bind s = s + "        return \"" + name + " says hello\"" + nl
    bind s = s + "    }" + nl
    bind s = s + "    return \"\"" + nl
    bind s = s + "}" + nl + nl
    bind s = s + "# react to the IDE (ev: open / save / project, arg = the path)" + nl
    bind s = s + "fn " + name + "_on_event(ev, arg) {" + nl
    bind s = s + "    if ev == \"save\" { ide_status(\"" + name + " saw you save \" + arg) }" + nl
    bind s = s + "    return 0.0" + nl
    bind s = s + "}" + nl
    plat_write(path, s)
    ext_scan()
    buf_open(path)
    status("Extension " + name + " created - it is already live; edit and save to reload")
    return 0.0
}

← Back to index