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