Switch embedded REPL from bare Chez to full Jerboa REPL with 40+ comma commands
ober
82372d9043f021151b8462031d5f48f993735b4d
--- a/lib/jerboa-emacs/qt/app.sls +++ b/lib/jerboa-emacs/qt/app.sls @@ -1531,9 +1531,6 @@ (qt-plain-text-edit-append! ed trimmed) - (qt-plain-text-edit-append! - ed - repl-prompt) (repl-state-prompt-pos-set! rs (string-length --- a/lib/jerboa-emacs/qt/commands-edit.sls +++ b/lib/jerboa-emacs/qt/commands-edit.sls @@ -1251,11 +1251,8 @@ (qt-edit-window-buffer-set! (qt-current-window fr) buf) (let ([rs (repl-start!)]) (hash-put! *repl-state* buf rs) - (qt-plain-text-edit-set-text! ed repl-prompt) - (qt-plain-text-edit-move-cursor! ed QT_CURSOR_END) - (repl-state-prompt-pos-set! - rs - (string-length repl-prompt))) + (qt-plain-text-edit-set-text! ed "") + (repl-state-prompt-pos-set! rs 999999999)) (echo-message! (app-state-echo app) "REPL started"))))) (def (cmd-repl-send app) "Send the current input line to the Chez Scheme subprocess." --- a/src/jerboa-emacs/app.ss +++ b/src/jerboa-emacs/app.ss @@ -341,9 +341,8 @@ (let ((win (find-window-for-buffer (app-state-frame app) buf))) (when win (let ((ed (edit-window-editor win))) - ;; Insert output + new prompt at end + ;; Insert output (subprocess sends its own prompt) (editor-append-text ed output) - (editor-append-text ed repl-prompt) ;; Update prompt-pos to after the new prompt (set! (repl-state-prompt-pos rs) (editor-get-text-length ed)) --- a/src/jerboa-emacs/editor-core.ss +++ b/src/jerboa-emacs/editor-core.ss @@ -2199,14 +2199,13 @@ ;; Attach buffer to editor (buffer-attach! ed buf) (set! (edit-window-buffer (current-window fr)) buf) - ;; Spawn Chez Scheme subprocess + ;; Spawn Jerboa REPL subprocess (let ((rs (repl-start!))) (hash-put! *repl-state* buf rs) - ;; Insert initial prompt - (editor-set-text ed repl-prompt) - (let ((len (editor-get-text-length ed))) - (set! (repl-state-prompt-pos rs) len) - (editor-goto-pos ed len))) + ;; Don't insert prompt — subprocess sends its own banner + prompt. + ;; Set prompt-pos high to block typing until first poll delivers output. + (editor-set-text ed "") + (set! (repl-state-prompt-pos rs) 999999999)) (echo-message! (app-state-echo app) "REPL started"))))) (def (cmd-repl-send app) --- a/src/jerboa-emacs/qt/app.ss +++ b/src/jerboa-emacs/qt/app.ss @@ -1234,9 +1234,8 @@ (let* ((ed (qt-edit-window-editor (car wins))) ;; Strip trailing newline — append! adds its own (trimmed (string-trim-eol output))) - ;; Insert output + new prompt + ;; Insert output (subprocess sends its own prompt) (qt-plain-text-edit-append! ed trimmed) - (qt-plain-text-edit-append! ed repl-prompt) (set! (repl-state-prompt-pos rs) (string-length (qt-plain-text-edit-text ed))) (qt-plain-text-edit-move-cursor! ed QT_CURSOR_END) --- a/src/jerboa-emacs/qt/commands-edit.ss +++ b/src/jerboa-emacs/qt/commands-edit.ss @@ -1126,16 +1126,13 @@ ;; Attach buffer to editor (qt-buffer-attach! ed buf) (set! (qt-edit-window-buffer (qt-current-window fr)) buf) - ;; Spawn Chez Scheme subprocess + ;; Spawn Jerboa REPL subprocess (let ((rs (repl-start!))) (hash-put! *repl-state* buf rs) - ;; Show prompt immediately — scheme -q in non-interactive mode - ;; does NOT send a startup banner, so the timer would never fire and - ;; prompt-pos would stay at 999999999, blocking all typing. - (qt-plain-text-edit-set-text! ed repl-prompt) - (qt-plain-text-edit-move-cursor! ed QT_CURSOR_END) - (set! (repl-state-prompt-pos rs) - (string-length repl-prompt))) + ;; Don't insert prompt — the subprocess sends its own banner + prompt. + ;; Set prompt-pos high to block typing until first poll delivers output. + (qt-plain-text-edit-set-text! ed "") + (set! (repl-state-prompt-pos rs) 999999999)) (echo-message! (app-state-echo app) "REPL started"))))) (def (cmd-repl-send app) --- a/src/jerboa-emacs/repl.ss +++ b/src/jerboa-emacs/repl.ss @@ -1,7 +1,8 @@ ;;; -*- Gerbil -*- ;;; REPL subprocess management for jemacs ;;; -;;; Manages a Chez Scheme subprocess: spawn, send input, read output, stop. +;;; Manages a Jerboa REPL subprocess: spawn, send input, read output, stop. +;;; Uses (std repl) with 40+ comma commands, value history, etc. ;;; No backend imports — shared between TUI and Qt. (export @@ -25,23 +26,107 @@ history) ; list of previous inputs transparent: #t) -(def repl-prompt "chez> ") +(def repl-prompt "jerboa> ") ;;;============================================================================ ;;; Lifecycle ;;;============================================================================ +(def (jerboa-lib-dir) + "Find jerboa lib directory from current process library-directories." + (let loop ((dirs (library-directories))) + (if (null? dirs) + ;; Fallback: try common locations + (let ((home (or (getenv "HOME") ""))) + (let try ((candidates (list (string-append home "/mine/jerboa/lib") + (string-append home "/.local/lib/jerboa") + "/usr/local/lib/jerboa"))) + (if (null? candidates) + #f + (if (file-exists? (string-append (car candidates) "/std")) + (car candidates) + (try (cdr candidates)))))) + (let ((d (car (car dirs)))) + (if (and (string? d) + (> (string-length d) 4) + (file-exists? (string-append d "/std/repl.sls"))) + d + (loop (cdr dirs))))))) + (def (repl-start!) - "Spawn a Chez Scheme subprocess and return a repl-state." - ;; Chez open-process-ports returns: (write-stdin read-stdout read-stderr pid) - (let-values (((p-stdin p-stdout p-stderr pid) - (open-process-ports "scheme -q" (buffer-mode none) (native-transcoder)))) - (close-port p-stderr) - ;; Store as (read-port . write-port) per struct contract - (make-repl-state (cons p-stdout p-stdin) 0 '()))) + "Spawn a Jerboa REPL subprocess and return a repl-state." + ;; Build command with --libdirs for the jerboa stdlib + (let* ((libdir (jerboa-lib-dir)) + (libdirs-arg + (if libdir + (string-append " --libdirs " libdir) + "")) + (cmd (string-append "scheme -q" libdirs-arg))) + ;; Chez open-process-ports returns: (write-stdin read-stdout read-stderr pid) + (let-values (((p-stdin p-stdout p-stderr pid) + (open-process-ports cmd (buffer-mode none) (native-transcoder)))) + (close-port p-stderr) + (let ((rs (make-repl-state (cons p-stdout p-stdin) 0 '()))) + ;; Boot the jerboa REPL: import and start it + (repl-send! rs "(import (std repl))") + (repl-send! rs "(jerboa-repl)") + ;; Disable ANSI colors — QScintilla/TUI can't render escape codes + (repl-send! rs ",set color off") + rs)))) + +;;;============================================================================ +;;; ANSI escape code stripping +;;;============================================================================ + +(def esc-char (integer->char 27)) ;; ESC = 0x1B +(def bel-char (integer->char 7)) ;; BEL = 0x07 + +(def (strip-ansi str) + "Remove ANSI escape sequences from a string. + Handles CSI sequences (ESC [ ... final-byte) and OSC sequences." + (let ((len (string-length str)) + (out (open-output-string))) + (let loop ((i 0)) + (when (< i len) + (let ((ch (string-ref str i))) + (if (char=? ch esc-char) + ;; Skip ESC sequence + (if (< (+ i 1) len) + (let ((next (string-ref str (+ i 1)))) + (cond + ;; CSI: ESC [ ... (ends at letter 0x40-0x7E) + ((char=? next #\[) + (let skip ((j (+ i 2))) + (if (>= j len) + (loop j) + (let ((c (string-ref str j))) + (if (and (char>=? c #\@) (char<=? c #\~)) + (loop (+ j 1)) + (skip (+ j 1))))))) + ;; OSC: ESC ] ... (ends at BEL or ST) + ((char=? next #\]) + (let skip ((j (+ i 2))) + (if (>= j len) + (loop j) + (let ((c (string-ref str j))) + (if (or (char=? c bel-char) + (char=? c esc-char)) + (loop (+ j 1)) + (skip (+ j 1))))))) + ;; Other ESC sequences (2 char) + (else (loop (+ i 2))))) + (loop (+ i 1))) + (begin + (write-char ch out) + (loop (+ i 1))))))) + (get-output-string out))) + +;;;============================================================================ +;;; I/O +;;;============================================================================ (def (repl-send! rs input) - "Send a line of input to the Chez Scheme subprocess." + "Send a line of input to the Jerboa REPL subprocess." (let ((out-port (cdr (repl-state-process rs)))) (put-string out-port input) (put-char out-port #\newline) @@ -51,8 +136,8 @@ (cons input (repl-state-history rs)))) (def (repl-read-available rs) - "Read all available output from the Chez subprocess (non-blocking). - Returns a string, or #f if nothing available." + "Read all available output from the Jerboa REPL subprocess (non-blocking). + Strips ANSI escape codes. Returns a string, or #f if nothing available." (let ((in-port (car (repl-state-process rs)))) (if (char-ready? in-port) (let ((out (open-output-string))) @@ -62,13 +147,18 @@ (unless (eof-object? ch) (write-char ch out) (loop))))) - (let ((s (get-output-string out))) + (let ((s (strip-ansi (get-output-string out)))) (if (string-empty? s) #f s))) #f))) (def (repl-stop! rs) - "Shut down the Chez Scheme subprocess." + "Shut down the Jerboa REPL subprocess." (let ((in-port (car (repl-state-process rs))) (out-port (cdr (repl-state-process rs)))) + ;; Try graceful shutdown first + (with-catch (lambda (_e) (void)) + (lambda () + (put-string out-port ",quit\n") + (flush-output-port out-port))) (with-catch (lambda (_e) (void)) (lambda () (close-port out-port))) (with-catch (lambda (_e) (void)) (lambda () (close-port in-port)))))