sync
ober
165dd32fa155c3fc31ddc5b14c53751f722e1548
--- a/.build.yml +++ b/.build.yml @@ -11,7 +11,6 @@ packages: - unzip - zip sources: - - https://git.sr.ht/~lisp/jerboa - https://git.sr.ht/~lisp/jerboa-code tasks: - setup-rust: | @@ -22,11 +21,6 @@ tasks: # install one via rustup and persist it onto PATH for every later task. curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain stable echo 'source "$HOME/.cargo/env"' >> ~/.buildenv - - install-jerbuild: | - cd jerboa - make jerboa - make install PREFIX=$HOME/.local - echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.buildenv - vendor-code: | cd jerboa-code mkdir -p vendor --- a/Makefile +++ b/Makefile @@ -1,17 +1,20 @@ # jerbuild is the self-contained build tool: it bundles Chez Scheme + the # jerboa stdlib under ~/.cache/jerbuild/<sha>/, so the host dev loop and the # native `binary` need NO jerboa source checkout and NO separately-built Chez. -JERBUILD ?= jerbuild +JERBOA_VERSION ?= v0.2.0 +JERBOA_TOOL_DIR ?= $(CURDIR)/.jerboa/bin +JERBUILD ?= $(shell if [ -x ./jerbuild ] && [ -x ./jerboa ]; then echo ./jerbuild; \ + elif [ -x "$(JERBOA_TOOL_DIR)/jerbuild" ] && [ -x "$(JERBOA_TOOL_DIR)/jerboa" ]; then echo "$(JERBOA_TOOL_DIR)/jerbuild"; \ + elif [ -x ../jerboa/dist/jerbuild ] && [ -x ../jerboa/dist/jerboa ]; then echo ../jerboa/dist/jerbuild; \ + elif command -v jerbuild >/dev/null 2>&1 && command -v jerboa >/dev/null 2>&1; then command -v jerbuild; \ + else echo "$(JERBOA_TOOL_DIR)/jerbuild"; fi) # Bundled stdlib path (Chez + jerboa libs live under $(JH)/lib). Resolved by # asking the jerbuild binary where it unpacked its bundle. -JH := $(shell $(JERBUILD) --jerboa-home 2>/dev/null) -ifeq ($(JH),) -$(error jerbuild not found on PATH (or '$(JERBUILD) --jerboa-home' failed). Install jerbuild, or set JERBUILD=/path/to/jerbuild) -endif +JH = $(shell "$(JERBUILD)" --jerboa-home 2>/dev/null) -LIBDIRS := --libdirs ./lib:vendor/jerboa-sqlite/lib:vendor/jerboa-websearch/src:$(JH)/lib -JEXEC := $(JERBUILD) exec $(LIBDIRS) +LIBDIRS = --libdirs ./lib:vendor/jerboa-sqlite/lib:vendor/jerboa-websearch/src:$(JH)/lib +JEXEC = $(JERBUILD) exec $(LIBDIRS) # Library paths for FFI shared objects (macOS: dylib, Linux: so). The native # Rust lib now lives in ./lib (dropped by the native-rs target), matching the @@ -46,12 +49,13 @@ else JCODE_OS_LIBS := -lm -ldl -lpthread -luuid -lncurses -lstdc++ endif -.PHONY: all help build gen run test test-providers clean repl binary install tui-shim run-tui native-rs sqlite-bundled linux linux-check linux-amd64 linux-arm64 jcode-linux-amd64 jcode-linux-arm64 test-linux test-linux-amd64 freebsd freebsd-amd64 jcode-freebsd-amd64 purge-stale sqlite-shim sqlite-lib android android-clean vendor-deps vendor-clean +.PHONY: all help ensure-jerboa-tools build gen run test test-providers clean repl binary install tui-shim run-tui native-rs sqlite-bundled linux linux-check linux-amd64 linux-arm64 jcode-linux-amd64 jcode-linux-arm64 test-linux test-linux-amd64 freebsd freebsd-amd64 jcode-freebsd-amd64 purge-stale sqlite-shim sqlite-lib android android-clean vendor-deps vendor-clean all: help help: @echo "Available targets:" + @echo " ensure-jerboa-tools Ensure project-local jerboa/jerbuild are available" @echo " build Compile src/ → lib/ and pre-compile imports (jerbuild)" @echo " test Run test suite (test/run.ss)" @echo " test-providers Live 'say hello' smoke test per configured provider" @@ -77,6 +81,24 @@ help: @echo " android Build Android APK in Termux (android/build/*.apk)" @echo " android-clean Remove android/build/" +ensure-jerboa-tools: + @if [ -x ./jerbuild ] && [ -x ./jerboa ]; then \ + echo "=== Using project-local ./jerbuild ==="; \ + elif [ -x "$(JERBOA_TOOL_DIR)/jerbuild" ] && [ -x "$(JERBOA_TOOL_DIR)/jerboa" ]; then \ + echo "=== Using downloaded Jerboa toolchain: $(JERBOA_TOOL_DIR) ==="; \ + elif [ -x ../jerboa/dist/jerbuild ] && [ -x ../jerboa/dist/jerboa ]; then \ + echo "=== Using sibling Jerboa build: ../jerboa/dist/jerbuild ==="; \ + elif command -v jerbuild >/dev/null 2>&1 && command -v jerboa >/dev/null 2>&1; then \ + echo "=== Using Jerboa toolchain from PATH: $$(command -v jerbuild) ==="; \ + else \ + echo "=== Fetching Jerboa $(JERBOA_VERSION) release tools into $(JERBOA_TOOL_DIR) ==="; \ + sh support/ensure-jerboa.sh "$(JERBOA_VERSION)" "$(JERBOA_TOOL_DIR)"; \ + fi + @"$(JERBUILD)" --jerboa-home >/dev/null || { \ + echo "ERROR: Jerboa toolchain is unavailable; set JERBUILD=/path/to/jerbuild or JERBOA_VERSION=<tag>"; \ + exit 1; \ + } + # ── Stale-artifact prevention ─────────────────────────────────────────────── # Delete any compiled .so/.wpo whose source .ss/.sls is newer (or whose # source no longer exists). This makes "make build" idempotent and immune @@ -130,7 +152,7 @@ sqlite-shim: vendor/jerboa-sqlite sqlite-bundled vendor/jerboa-sqlite/jerboa_sqlite_shim.c $(SQLITE_BUNDLED_A); \ fi -sqlite-lib: vendor/jerboa-sqlite +sqlite-lib: ensure-jerboa-tools vendor/jerboa-sqlite @rm -rf $(SQLITE_STAGE) @mkdir -p $(SQLITE_STAGE) @sed '/^(import (jerboa prelude))$$/d' \ @@ -216,14 +238,14 @@ $(REPL_TOKEN_FILE): @head -c 16 /dev/urandom | od -An -tx1 | tr -d ' \n' > $@ @echo "Generated debug-REPL auth token in $(REPL_TOKEN_FILE)" -gen: vendor-deps purge-stale sqlite-shim sqlite-lib $(REPL_TOKEN_FILE) +gen: ensure-jerboa-tools vendor-deps purge-stale sqlite-shim sqlite-lib $(REPL_TOKEN_FILE) JCODE_REPL_TOKEN=$$(cat $(REPL_TOKEN_FILE)) $(JERBUILD) transpile src lib -build: gen native-rs +build: ensure-jerboa-tools gen native-rs DYLD_LIBRARY_PATH=$(LDPATH) LD_LIBRARY_PATH=$(LDPATH) \ $(JERBUILD) compile $(LIBDIRS) support/build-check.ss < /dev/null -run: native-rs +run: ensure-jerboa-tools native-rs DYLD_LIBRARY_PATH=$(LDPATH) LD_LIBRARY_PATH=$(LDPATH) \ $(JEXEC) main.ss @@ -235,11 +257,11 @@ tui-shim: vendor/termbox2 -o $(TUI_SHIM_DIR)/jcode_tui_shim.dylib \ src/jcode/ui/jcode_tui_shim.c -run-tui: build tui-shim native-rs +run-tui: ensure-jerboa-tools build tui-shim native-rs DYLD_LIBRARY_PATH=$(LDPATH) LD_LIBRARY_PATH=$(LDPATH) \ $(JEXEC) main.ss --tui -repl: +repl: ensure-jerboa-tools DYLD_LIBRARY_PATH=$(LDPATH) LD_LIBRARY_PATH=$(LDPATH) \ $(JEXEC) support/repl.ss @@ -259,7 +281,7 @@ test-providers: build # Self-contained: `jerbuild build` reads .jerbuild (entry, libdirs, ffi-symbols, # main-c, C shims, rust-crate), bundles Chez + stdlib, runs cargo for the native # .a, and links it all. Per-OS link flags come from --os-libs. -binary: gen native-rs +binary: ensure-jerboa-tools gen native-rs DYLD_LIBRARY_PATH=$(LDPATH) LD_LIBRARY_PATH=$(LDPATH) \ JCODE_REPL_TOKEN=$$(cat $(REPL_TOKEN_FILE)) \ $(JERBUILD) build --config .jerbuild --os-libs "$(JCODE_OS_LIBS)" @@ -293,7 +315,7 @@ linux: linux-amd64 # Runs compile-program on main-binary.ss with the same flags the cross build # uses, but skips the C compile and link. Catches drift (new imports, removed # stdlib exports) in ~10s before burning minutes in the full cross build. -linux-check: gen +linux-check: ensure-jerboa-tools gen @echo "=== Running linux-check (fast cross-build drift check) ===" DYLD_LIBRARY_PATH=$(LDPATH) LD_LIBRARY_PATH=$(LDPATH) \ $(SCHEME) -q --libdirs "$(XC_LIBDIRS)" --script linux-check.ss @@ -303,7 +325,7 @@ linux-check: gen # - <arch>-linux-musl-gcc on PATH (brew install FiloSottile/musl-cross/musl-cross) # - $(JERBOA_HOME)/.chez-cross-<m> (cd $(JERBOA_HOME) && make chez-cross[-tarm64le]) # - rustup target add <arch>-unknown-linux-musl -linux-amd64: gen +linux-amd64: ensure-jerboa-tools gen @command -v x86_64-linux-musl-gcc >/dev/null 2>&1 || { \ echo "ERROR: x86_64-linux-musl-gcc not found on PATH."; \ echo "Install with: brew install FiloSottile/musl-cross/musl-cross"; \ @@ -319,7 +341,7 @@ linux-amd64: gen @ls -lh jcode-linux-amd64 @file jcode-linux-amd64 -linux-arm64: gen +linux-arm64: ensure-jerboa-tools gen @command -v aarch64-linux-musl-gcc >/dev/null 2>&1 || { \ echo "ERROR: aarch64-linux-musl-gcc not found on PATH."; \ echo "Install with: brew install FiloSottile/musl-cross/musl-cross"; \ @@ -356,7 +378,7 @@ test-linux: test-linux-amd64 # - $(JERBOA_HOME)/.chez-cross-ta6fb (cd $(JERBOA_HOME) && make binary) # - FreeBSD sysroot at $(JERBOA_HOME)/.freebsd-sysroot/amd64 # - rustup target add x86_64-unknown-freebsd -freebsd-amd64: gen +freebsd-amd64: ensure-jerboa-tools gen @command -v $(firstword $(FREEBSD_AMD64_CC)) >/dev/null 2>&1 || { \ echo "ERROR: $(FREEBSD_AMD64_CC) not found or not executable" >&2; \ echo " See top of Makefile freebsd-amd64 target for setup notes." >&2; \ --- a/README.md +++ b/README.md @@ -44,9 +44,10 @@ tools dependably. ## Quickstart -You need the installed Jerboa toolchain (`jerbuild` on `PATH`), a C compiler, -and Rust/Cargo. See **[docs/getting-started.md](docs/getting-started.md)** for -the full setup. +You need a C compiler and Rust/Cargo. `make build` and `make binary` use a +local Jerboa toolchain when present, otherwise they fetch the configured +release bundle into `.jerboa/bin`. See +**[docs/getting-started.md](docs/getting-started.md)** for the full setup. ```bash git clone https://git.sr.ht/~lisp/jerboa-code && cd jerboa-code --- a/docs/cli.md +++ b/docs/cli.md @@ -147,7 +147,10 @@ so it doubles as a smoke test. Full semantics in [FORGE.md](FORGE.md). | Variable | Purpose | |---|---| -| `JERBOA_HOME` | Legacy fallback build path only; normal builds use installed `jerbuild`. | +| `JERBUILD` | Optional explicit path to `jerbuild`. | +| `JERBOA_VERSION` | Jerboa release tag to download when no local toolchain is available; defaults to `v0.2.0`. | +| `JERBOA_TOOL_DIR` | Project-local download directory for `jerboa`, `jerbuild`, `jpkg`, `jmcp`, and `jlsp`; defaults to `.jerboa/bin`. | +| `JERBOA_HOME` | Legacy cross-build fallback path only; normal builds use `jerbuild`. | | `JCODE_TRACE` | Path to write a full trace file (same as `--trace`). | | `JCODE_PASSPHRASE` | Passphrase for the encrypted key store (non-interactive). | | `JERBOA_STATIC` | `1` → use the statically-linked termbox shim instead of the `.dylib`/`.so`. | --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -5,21 +5,21 @@ prerequisites, building on each platform, configuration, and your first session. ## Prerequisites -`jcode` builds with the installed Jerboa toolchain. The build needs three -things: +`jcode` builds with the Jerboa toolchain. The build needs three things: -1. **`jerbuild` on `PATH`.** Install the Jerboa multicall toolchain so - `jerboa`, `jerbuild`, `jmcp`, and `jlsp` are available: +1. **A Jerboa toolchain.** The Makefile checks, in order: + project-local `./jerbuild` + `./jerboa`, `.jerboa/bin`, a sibling + `../jerboa/dist` build, `PATH`, then downloads the configured SourceHut + release artifact into `.jerboa/bin`. ```bash - cd ~/mine/jerboa - make jerboa - make install + make ensure-jerboa-tools ``` - After install, `jerbuild --jerboa-home` should print the extracted bundled - stdlib path. A jcode build does not need `JERBOA_HOME` or a separate Chez - install for the normal `build`, `binary`, `linux-*`, or `freebsd` targets. + Override `JERBOA_VERSION` to pin a different Jerboa release tag, or set + `JERBUILD=/path/to/jerbuild` to force a specific local tool. A jcode build + does not need `JERBOA_HOME` or a separate Chez install for the normal + `build`, `binary`, `linux-*`, or `freebsd` targets. 2. **A Rust toolchain.** `jerbuild` builds the vendored `jerboa-native-rs` crate with `cargo` for the standalone binary, and the dev --- a/src/jcode/ui/tui-ffi.ss +++ b/src/jcode/ui/tui-ffi.ss @@ -99,7 +99,14 @@ (define-tb c-tb-width "jcode_tb_width" () int) (define-tb c-tb-height "jcode_tb_height" () int) (define-tb c-tb-clear "jcode_tb_clear" () void) -(define-tb c-tb-present "jcode_tb_present" () void) +;; __collect_safe: tb_present writes the frame diff to the TTY with a +;; blocking write(). Under tmux/vterm backpressure that write can stall; +;; without collect-safe it pins the GC rendezvous and freezes EVERY +;; thread (the streaming worker included) until the terminal drains. +(def c-tb-present + (if (and _shim-loaded (foreign-entry? "jcode_tb_present")) + (foreign-procedure __collect_safe "jcode_tb_present" () void) + (lambda args (error 'tui-ffi "termbox shim not loaded" "jcode_tb_present")))) (define-tb c-tb-set-cursor "jcode_tb_set_cursor" (int int) void) (define-tb c-tb-hide-cursor "jcode_tb_hide_cursor" () void) (define-tb c-tb-change-cell "jcode_tb_change_cell" (int int unsigned-32 unsigned-32 unsigned-32) void) --- a/src/jcode/ui/tui-input.ss +++ b/src/jcode/ui/tui-input.ss @@ -39,25 +39,51 @@ ;; ---- Slash commands ---- +;; Every command handle-slash-command! (tui.ss) accepts MUST be listed +;; here -- test/run.ss scans both sources and fails the suite when a +;; dispatcher command is missing from this popup list. (def *slash-commands* '(("/help" . "Show help") ("/model" . "Show or set model") ("/provider" . "Show or set provider") ("/expert" . "Force a prompt to the configured expert model") ("/tools" . "List available tools") + ("/mcp" . "MCP server status") + ("/skills" . "List skills") + ("/agents" . "List agent definitions") + ("/mode" . "Show or set PLAN/BUILD mode") + ("/plan" . "Switch to PLAN (read-only) mode") + ("/build" . "Switch to BUILD (read+write) mode") ("/clear" . "Start new session") ("/sessions" . "List saved sessions") + ("/resume" . "Resume a stored session in this tab") ("/search" . "Search session history") ("/compact" . "Compact conversation") ("/copy" . "Copy last reply to a file") - ("/quit" . "Exit jcode") - ("/themes" . "List color themes") - ("/theme" . "Switch color theme") - ("/sidebar" . "Toggle sidebar") + ("/save" . "Save conversation to a file") + ("/activity" . "Live harness activity screen (Esc returns)") + ("/side" . "Open a parallel jcode conversation tab") + ("/tab" . "Switch to tab N (Ctrl-O cycles)") + ("/tabs" . "List open tabs") + ("/jcode" . "Switch back to the main jcode tab") + ("/main" . "Switch back to the main jcode tab") + ("/close-tab" . "Close the current tab") + ("/close" . "Close the current tab") + ("/claude" . "Open/focus a Claude Code tab") + ("/codex" . "Open/focus a Codex tab") + ("/gemini" . "Open/focus a Gemini CLI tab") + ("/opencode" . "Open/focus an opencode tab") + ("/grok" . "Open/focus a Grok CLI tab") ("/ask-claude" . "Second opinion from claude CLI (sandboxed)") ("/ask-gemini" . "Second opinion from gemini CLI (sandboxed)") ("/ask-codex" . "Second opinion from codex CLI (sandboxed)") - ("/ask-opencode" . "Second opinion from opencode CLI (sandboxed)"))) + ("/ask-opencode" . "Second opinion from opencode CLI (sandboxed)") + ("/ask-grok" . "Second opinion from grok CLI (sandboxed)") + ("/refresh-models" . "Refresh provider model list") + ("/themes" . "List color themes") + ("/theme" . "Switch color theme") + ("/sidebar" . "Toggle sidebar") + ("/quit" . "Exit jcode"))) ;; ---- Key handling ---- ;; Returns: 'submit | 'cancel | 'continue | 'quit new file mode 100755 --- /dev/null +++ b/support/ensure-jerboa.sh @@ -0,0 +1,121 @@ +#!/bin/sh +# Bootstrap a project-local Jerboa toolchain from SourceHut release artifacts. +# +# Usage: +# support/ensure-jerboa.sh v0.2.0 .jerboa/bin +# +# Override the artifact location with: +# JERBOA_RELEASE_BASE=https://example.org/releases/v0.2.0 +# or the SourceHut repo with: +# JERBOA_RELEASE_REPO=~lisp/jerboa +# For testing or unusual hosts, override target detection with: +# JERBOA_RELEASE_TARGET=macos-arm64 + +set -eu + +usage() { + echo "Usage: $0 VERSION [BINDIR]" >&2 +} + +[ "${1:-}" ] || { usage; exit 2; } + +version=$1 +bindir=${2:-.jerboa/bin} +repo=${JERBOA_RELEASE_REPO:-~lisp/jerboa} +origin=${JERBOA_RELEASE_ORIGIN:-https://git.sr.ht} + +if [ "${JERBOA_RELEASE_TARGET:-}" ]; then + target=$JERBOA_RELEASE_TARGET +else + os=$(uname -s) + arch=$(uname -m) + case "$os-$arch" in + Darwin-arm64) target=macos-arm64 ;; + Linux-x86_64|Linux-amd64) target=linux-amd64 ;; + Linux-aarch64|Linux-arm64) target=linux-arm64 ;; + FreeBSD-amd64|FreeBSD-x86_64) target=freebsd-amd64 ;; + *) + echo "ERROR: unsupported platform for Jerboa release artifacts: $os $arch" >&2 + echo " set JERBUILD=/path/to/jerbuild or build Jerboa locally for this host" >&2 + exit 1 + ;; + esac +fi + +case "$target" in + macos-arm64|linux-amd64|linux-arm64|freebsd-amd64) ;; + *) + echo "ERROR: unsupported release target: $target" >&2 + exit 1 + ;; +esac + +file="jerboa-${version}-${target}.tar.gz" +base=${JERBOA_RELEASE_BASE:-${origin}/${repo}/refs/download/${version}} +url="${base%/}/${file}" +sum_url="${url}.sha256" + +download() { + src=$1 + dst=$2 + if command -v curl >/dev/null 2>&1; then + curl -fsSL "$src" -o "$dst" + elif command -v fetch >/dev/null 2>&1; then + fetch -q -o "$dst" "$src" + elif command -v wget >/dev/null 2>&1; then + wget -q -O "$dst" "$src" + else + echo "ERROR: need curl, fetch, or wget to download Jerboa" >&2 + exit 1 + fi +} + +sha256_file() { + path=$1 + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$path" | awk '{print $1}' + elif command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$path" | awk '{print $1}' + elif command -v sha256 >/dev/null 2>&1; then + sha256 -q "$path" + else + echo "ERROR: need sha256sum, shasum, or sha256" >&2 + exit 1 + fi +} + +tmp=$(mktemp -d "${TMPDIR:-/tmp}/jerboa-bootstrap.XXXXXX") +trap 'rm -rf "$tmp"' EXIT HUP INT TERM + +archive="$tmp/$file" +sum_file="$tmp/$file.sha256" + +echo "fetching $url" +download "$url" "$archive" +download "$sum_url" "$sum_file" + +expected=$(awk '{print $1; exit}' "$sum_file") +actual=$(sha256_file "$archive") +if [ "$expected" != "$actual" ]; then + echo "ERROR: checksum mismatch for $file" >&2 + echo "expected: $expected" >&2 + echo "actual: $actual" >&2 + exit 1 +fi + +mkdir -p "$tmp/extract" +tar -xzf "$archive" -C "$tmp/extract" +root="$tmp/extract/jerboa-${version}-${target}" +[ -x "$root/bin/jerboa" ] || { + echo "ERROR: archive did not contain bin/jerboa" >&2 + exit 1 +} + +mkdir -p "$bindir" +cp "$root/bin/jerboa" "$bindir/jerboa" +chmod 0755 "$bindir/jerboa" +for link in jmcp jlsp jerbuild jpkg; do + ln -sf jerboa "$bindir/$link" +done + +echo "installed $bindir/{jerboa,jmcp,jlsp,jerbuild,jpkg}" --- a/test/run.ss +++ b/test/run.ss @@ -2074,6 +2074,76 @@ (builtin-skill "create-plan") (lambda (s) (str-contains? s "plans/"))) +;; ── Slash-command completion coverage ───────────────────────────── +;; Every command the TUI dispatcher (handle-slash-command! in tui.ss) +;; accepts must appear in the completion popup (*slash-commands* in +;; tui-input.ss). Scans the SOURCE text so adding a new +;; (equal? cmd "...") / (string-prefix? "... " cmd) clause without a +;; completion entry fails the suite. + +(define (slurp-file path) + (call-with-input-file path (lambda (p) (get-string-all p)))) + +(define (str-find src needle start) + (let ([nl (string-length needle)] [sl (string-length src)]) + (let loop ([i start]) + (cond + [(> (+ i nl) sl) #f] + [(string=? (substring src i (+ i nl)) needle) i] + [else (loop (+ i 1))])))) + +(define (collect-quoted src marker require-cmd-after?) + ;; For each MARKER occurrence, the text up to the next double-quote. + ;; With require-cmd-after?, keep only matches whose quote is followed + ;; by " cmd)" -- filters dispatcher clauses from unrelated + ;; string-prefix? calls. + (let ([mlen (string-length marker)]) + (let loop ([i 0] [acc '()]) + (let ([hit (str-find src marker i)]) + (if (not hit) + (reverse acc) + (let* ([s (+ hit mlen)] + [q (str-find src "\"" s)]) + (if (not q) + (reverse acc) + (let ([keep? (or (not require-cmd-after?) + (and (<= (+ q 6) (string-length src)) + (string=? (substring src (+ q 1) (+ q 6)) + " cmd)")))]) + (loop (+ q 1) + (if keep? (cons (substring src s q) acc) acc)))))))))) + +(define (strip-trailing-space s) + (let ([n (string-length s)]) + (if (and (> n 0) (char=? (string-ref s (- n 1)) #\space)) + (substring s 0 (- n 1)) + s))) + +(define (dedupe lst) + (let loop ([l lst] [acc '()]) + (cond + [(null? l) (reverse acc)] + [(member (car l) acc) (loop (cdr l) acc)] + [else (loop (cdr l) (cons (car l) acc))]))) + +(let* ([tui-src (slurp-file "src/jcode/ui/tui.ss")] + [input-src (slurp-file "src/jcode/ui/tui-input.ss")] + [dispatcher (dedupe + (map strip-trailing-space + (append + (collect-quoted tui-src "(equal? cmd \"" #f) + (collect-quoted tui-src "(string-prefix? \"" #t))))] + [completions (collect-quoted input-src "(\"/" #f)]) + (check-pred! "dispatcher commands found by source scan" + dispatcher + (lambda (d) (> (length d) 30))) + (for-each + (lambda (cmd) + (check-pred! (format "completion popup has /~a" cmd) + completions + (lambda (cs) (and (member cmd cs) #t)))) + dispatcher)) + ;; ── Results ─────────────────────────────────────────────────────── (printf "~n~a passed, ~a failed~n" pass-count fail-count)