From a248b2a7bfe345c94524032e7a1e27a5b6cb922e Mon Sep 17 00:00:00 2001 From: Marcin Gasperowicz Date: Sat, 19 Sep 2026 20:05:53 +0100 Subject: [PATCH] feat: teach vibe the editor APIs and keep a bad segment from crashing Vibe's catalog only listed stdlib, so a modeline clock came out as (string/trim (os/sh ...)) or (now) that C-c C-e couldn't resolve. The catalog now includes the live scripting surface; throwing segments become a badge; eval-buffer runs in the current ns like C-x C-e. Co-authored-by: Cursor --- docs/GUIDE.md | 21 +++++++++++++++++-- legmacs/modeline.lg | 45 +++++++++++++++++++++++++++++++++++++--- legmacs/modes/letgo.lg | 40 +++++++++++++++++++++-------------- legmacs/modes/repl.lg | 7 ++++--- legmacs/render.lg | 5 ++++- legmacs/vibe.lg | 46 ++++++++++++++++++++++++++++++++++++++++- test/letgo_mode_test.lg | 30 ++++++++++++++++++++++----- test/modeline_test.lg | 24 +++++++++++++++++++++ test/render_test.lg | 6 +++++- test/repl_mode_test.lg | 3 ++- test/vibe_test.lg | 37 ++++++++++++++++++++++++++++++++- 11 files changed, 231 insertions(+), 33 deletions(-) diff --git a/docs/GUIDE.md b/docs/GUIDE.md index 2d2fd27..9c6cb5b 100644 --- a/docs/GUIDE.md +++ b/docs/GUIDE.md @@ -263,13 +263,15 @@ whatever) are caught and shown in the echo area rather than crashing the editor. `C-c C-e` puts the current namespace back when it's done. It has to: -`load-string`ing a buffer that opens with `(ns foo)` -- which every real +evaluating a buffer that opens with `(ns foo)` -- which every real `.lg` file does -- relocates the running namespace permanently, and since that namespace *is* the editor's own, one `C-c C-e` on an ordinary file would otherwise leave `buf/`, `km/`, `dispatch/` and `vibe` unresolvable for the rest of the session. `C-x C-e` and `C-j` are deliberately left alone, as is the `*repl*` buffer: an `(in-ns 'foo)` you typed and evaluated -on purpose should still move you. +on purpose should still move you. A buffer with no ns form (*scratch*) +evals in the editor ns, so lg-isms like `(now)` resolve the same as +`C-x C-e`. `C-c C-z` opens (or switches to) `*repl*` -- a dedicated, persistent REPL buffer rather than a transcript you build by hand with `C-j`. It's plain @@ -346,6 +348,15 @@ file to splice requires into. JVM-Clojure prefixes in the reply (`clojure.string/join`) are rewritten to the canonical names before anything is inserted. +The catalog also includes the live editor's scripting surface +(`legmacs.keymap`, `legmacs.modeline`, `legmacs.theme`, `legmacs.modes`, +`legmacs.buffer`, `legmacs.minibuffer`, `legmacs.modes.prog`) — the same +APIs `legmacs-init.lg` uses. A `(vibe "clock on the modeline")` can +therefore emit `register-modeline-segment!` / `bind-key!` / `set-theme!` +code that fits the file. Those calls take effect when you evaluate them +(`C-x C-e` / `C-c C-e`), the same as any other init form; vibe only +splices the source. + Setup is `~/.config/legmacs/vibe.edn` (or `$LEGMACS_CONFIG_DIR/vibe.edn` -- the same variable `bin/legmacs` already honours for `legmacs_init.lg`): @@ -702,6 +713,12 @@ privileged built-in path here either. (ml/set-modeline-order! [:mode :buffer :position]) ``` +A segment fn that throws, or that returns a non-string `:text` (the usual +`(string/trim (os/sh ...))` mistake — `os/sh` returns a map), is shown as a +red `id!` badge instead of taking down the frame. `:text` must be a string; +for a live clock, `(.Format (now) "15:04")` is cheap enough to run every +paint, `os/sh` is not. + A minor mode's `:status`/`:status-right` lighters (see keycast above) still work unchanged: they're aggregated into the built-in `:modes-status`/ `:modes-status-right` segments, which you can recolor, move, or remove the diff --git a/legmacs/modeline.lg b/legmacs/modeline.lg index ddf2d90..06eca26 100644 --- a/legmacs/modeline.lg +++ b/legmacs/modeline.lg @@ -17,6 +17,10 @@ ;; with its own colors is a "block" ;; that pops out of the bar ;; +;; :text must be a string. A throw, or a non-string :text (e.g. the map +;; os/sh returns), becomes a red "id!" badge instead of escaping into +;; render and taking down the frame. +;; ;; Segments render left-to-right in registration order unless ;; set-modeline-order! has changed it, each side joined by `separator`. @@ -82,14 +86,40 @@ (cond (nil? s) nil (string? s) (when (seq s) {:text s}) - (map? s) (when (seq (:text s)) s) + (map? s) (when (and (string? (:text s)) (seq (:text s))) s) :else nil)) +(defn- seg-label [id] + (if (keyword? id) (name id) (str id))) + +(defn- broken-seg + "A visible stand-in when a segment fn throws or returns a non-string + :text, so a bad user/vibe segment cannot take down the frame." + [id] + {:text (str " " (seg-label id) "! ") + :fg [245 246 250] + :bg [204 90 90]}) + +(defn- call-seg + "Run a segment fn. Throws and illegal :text become a broken-seg badge + rather than escaping into render. nil and \"\" still omit the segment." + [id f state] + (let [outcome (try {:ok (f state)} (catch e {:err e}))] + (if (contains? outcome :err) + (broken-seg id) + (let [v (:ok outcome)] + (cond + (nil? v) nil + (and (string? v) (not (seq v))) nil + (and (map? v) (or (nil? (:text v)) (and (string? (:text v)) + (not (seq (:text v)))))) nil + :else (or (normalize-seg v) (broken-seg id))))))) + (defn- ordered [side state] (->> @segments (filter (fn [[_ seg]] (= side (:side seg)))) (sort-by (fn [[_ seg]] (:order seg))) - (keep (fn [[_ seg]] (normalize-seg ((:fn seg) state)))))) + (keep (fn [[id seg]] (call-seg id (:fn seg) state))))) (defn left-segments "Every :left segment visible this frame, as {:text :fg :bg} maps, in @@ -129,11 +159,20 @@ :fg [30 36 30] :bg [152 195 121]})) +(defn- call-lighter + "Run a mode :status / :status-right fn. A throw omits that lighter + this frame rather than killing the mode line." + [f state] + (let [outcome (try {:ok (f state)} (catch e {:err e}))] + (when-not (contains? outcome :err) + (let [s (:ok outcome)] + (when (and (string? s) (seq s)) s))))) + (defn- mode-lighters [state accessor] (->> (modes/active-modes state) (keep (fn [mode-kw] (when-let [f (accessor mode-kw)] - (let [s (f state)] (when (seq s) s))))) + (call-lighter f state)))) (string/join " "))) (defn- modes-status-segment [state] (mode-lighters state modes/mode-status)) diff --git a/legmacs/modes/letgo.lg b/legmacs/modes/letgo.lg index e6c0581..d691b1f 100644 --- a/legmacs/modes/letgo.lg +++ b/legmacs/modes/letgo.lg @@ -1,16 +1,17 @@ ;; legmacs.modes.letgo — a small REPL built into the editor: evaluate ;; let-go code straight out of the buffer, live, in the same process. ;; -;; This works because let-go's `eval`/`read-string`/`load-string` (see -;; pkg/compiler/eval.go) compile and run an already-read form *in whatever -;; namespace is current* -- and that's legmacs.main the whole time legmacs -;; is running, so code typed here sees exactly the aliases main.lg set up: -;; `buf/`, `keys/`, `km/`, `dispatch/`, `render/`, plus all of clojure.core -;; unqualified. It's not a sandboxed toy evaluator -- it's the running -;; editor's own namespace. +;; This works because let-go's `eval`/`read-string` compile and run an +;; already-read form *in whatever namespace is current* -- and that's +;; legmacs.main the whole time legmacs is running, so code typed here sees +;; exactly the aliases main.lg set up: `buf/`, `keys/`, `km/`, `dispatch/`, +;; `render/`, plus clojure.core and let-go.core (now, spy, …) unqualified. +;; It's not a sandboxed toy evaluator -- it's the running editor's own +;; namespace. C-c C-e / *repl* eval form-by-form the same way (let-go's +;; load-string compiles in clojure.core, which does not refer let-go.core). ;; ;; Which is also why `preserving-ns` below exists: that invariant isn't -;; free. `load-string`ing a buffer that opens with `(ns foo)` -- every real +;; free. Evaluating a buffer that opens with `(ns foo)` -- every real ;; .lg file does -- relocates the current namespace permanently, and one ;; C-c C-e would otherwise cost you every alias above for the rest of the ;; session. eval-buffer always restores the editor ns after the load. @@ -308,12 +309,20 @@ [form] (capturing-out (fn [] (eval form)))) +(defn- eval-each-form + "eval every form in `forms` in the current *ns*, returning the last + result. let-go's load-string compiles in clojure.core, which does not + refer let-go.core, so lg-isms like `now` fail and defs intern into + core -- that is the wrong home for *scratch* / C-c C-e / *repl*." + [forms] + (reduce (fn [_ form] (eval form)) nil forms)) + (defn load-in-editor - "load-string `s` with *out*/*err* captured (see capturing-out). The - *repl* buffer uses this so a typed (go (println ...)) cannot clobber - the frame either." + "Read `s` and eval each form in the current *ns*, with *out*/*err* + captured (see capturing-out). The *repl* buffer uses this so a typed + (go (println ...)) cannot clobber the frame either." [s] - (capturing-out (fn [] (load-string s)))) + (capturing-out (fn [] (eval-each-form (read-all-string s))))) (defn- last-sexp-before-point [state] (let [text (buf/text-before-cursor state)] @@ -390,7 +399,7 @@ :error (assoc state :message (str "Read error: " form)) (try (f state form (eval-form-in-state state form)) - (catch e (assoc state :message (str "Eval error: " (ex-message e)))))))) + (catch e (assoc state :message (str "Eval error: " (or (ex-message e) (str e))))))))) (defcommand eval-last-sexp "Evaluate the s-expression immediately before point; show the result in @@ -408,12 +417,13 @@ (buf/insert-string state (str "\n" (format-result result)))))) (defcommand eval-buffer - "Evaluate the entire buffer, top to bottom, as one `load-string`." + "Evaluate the entire buffer, top to bottom, form by form in the current + namespace (or the buffer's ns form, restored afterwards)." [state] (try (let [result (preserving-ns (fn [] (load-in-editor (buf/buffer-text state))))] (assoc state :message (str "=> " (format-result result)))) - (catch e (assoc state :message (str "Eval error: " (ex-message e)))))) + (catch e (assoc state :message (str "Eval error: " (or (ex-message e) (str e))))))) ;; --- auto-indent -------------------------------------------------------- ;; diff --git a/legmacs/modes/repl.lg b/legmacs/modes/repl.lg index 1b06c71..2053b01 100644 --- a/legmacs/modes/repl.lg +++ b/legmacs/modes/repl.lg @@ -8,9 +8,10 @@ ;; It's plain let-go-mode (major mode :let-go) for content -- highlighting, ;; auto-pairing, paren-matching, auto-indent are exactly what a normal .lg ;; buffer gets, for free -- plus one minor mode, :repl, that shadows RET: -;; - a complete top-level form -> evaluate it (load-string, so a form -;; spanning several typed expressions works the same as eval-buffer), -;; print the result, and open a fresh prompt right after it +;; - a complete top-level form -> evaluate it (same as eval-buffer: +;; each form in the current *ns*, so a multi-expression paste works +;; and lg-isms like `now` resolve), print the result, and open a +;; fresh prompt right after it ;; - an incomplete one (an open bracket -- ls/depth-at > 0) -> insert a ;; newline and auto-indent instead, exactly like ordinary let-go-mode, ;; so a multi-line form can keep being typed diff --git a/legmacs/render.lg b/legmacs/render.lg index fd21a97..c33628c 100644 --- a/legmacs/render.lg +++ b/legmacs/render.lg @@ -114,7 +114,9 @@ (defn sanitize "Replace control characters in `s` with caret notation (^[ for ESC, ^M - for CR, ^? for DEL). Everything headed for the echo area goes through + for CR, ^? for DEL). Tab is indent, not a control glyph -- it becomes + four spaces so a CompileError's \"\\tcaused by\" chain is readable + instead of `^I`. Everything headed for the echo area goes through this: a message can carry control bytes from sources the key decoder never sees (an eval result containing (char 27), a swiper label from a file with control characters) and writing them raw would let the @@ -123,6 +125,7 @@ (apply str (map (fn [ch] (let [c (int ch)] (cond + (= c 9) " " (< c 32) (str "^" (char (+ c 64))) (= c 127) "^?" :else (str ch)))) diff --git a/legmacs/vibe.lg b/legmacs/vibe.lg index 389c264..19d704b 100644 --- a/legmacs/vibe.lg +++ b/legmacs/vibe.lg @@ -68,6 +68,8 @@ "edit the ns form; missing requires are added for you.\n" "- json/read-json takes an optional {:keywords? true}. json/write-json " "accepts keyword or string map keys.\n" + "- os/sh returns {:exit n :out s :err s}, not a string. Use " + "(:out (os/sh \"date\" \"+%H:%M\")), then string/trim that.\n" "- (throw \"msg\") is a plain string. Catch with (or (ex-message e) (str e)).\n" "- Do not use finally for cleanup that must run on throw; reify the outcome " "then clean up.\n" @@ -75,6 +77,32 @@ "defined above it.\n" "- count, subs, and string/index-of on a string are O(n) (rune-indexed). " "Do not call them per character in a scan; convert to a char vector once.\n\n" + "This process is the live editor. When the request is about this editor " + "(keys, modeline, theme, modes, commands), use the Editor scripting catalog " + "and these shapes:\n" + "- Commands: (defcommand name \"doc\" [state] ...) then " + "(km/bind-key! \"C-c x\" :name). A command is (fn [state] -> state); " + "return new state, do not mutate a buffer atom.\n" + "- Modeline: (legmacs.modeline/register-modeline-segment! :id :right " + "(fn [state] {:text \"...\" :fg [r g b] :bg [r g b]})). Built-in ids: " + ":buffer, :mode, :position, :pending-keys, :async-jobs, :modes-status, " + ":modes-status-right. nil or \"\" omits the segment that frame. " + "The fn runs every frame: it must be cheap, must not throw, must not " + "do I/O (no os/sh, http), and :text must be a string. For a clock, " + "(.Format (now) \"15:04\"). now is let-go.core/now, auto-referred; " + "do not require it. Do not add an (ns ...) form to a file that does " + "not already have one.\n" + "- Theme: theme/set-theme! merges faces; theme/load-theme! replaces. " + "The :modeline face is the bar's default colors; a segment's :fg/:bg " + "override per block.\n" + "- Modes: register-mode! / register-minor-mode!. A :status / :status-right " + "lighter on a minor mode is the other way to put text on the bar.\n" + "- Prefer an existing :as alias; otherwise fully-qualify " + "(legmacs.modeline/register-modeline-segment!). Never (require 'legmacs.main). " + "Session-lasting config belongs in a legmacs-init ns at " + "~/.config/legmacs/legmacs_init.lg, not a random project file.\n" + "- Do not return a capturing fn literal from if/when/cond (AOT hoist); " + "name the closure.\n\n" "You get a source file with the marker " marker " at exactly one spot, " "and a request. Reply with the code that should replace that marker.\n\n" "Rules:\n" @@ -109,6 +137,13 @@ "pprint" "test" "math" "net" "hash" "graph" "transit" "bencode" "storage" "unix" "syscall" "pods" "xxh3"]) +(def editor-libs + "Editor namespaces we always offer in the catalog when loaded. + Curated: the scripting surface (keys, modeline, theme, modes, buffer + primitives, minibuffer, language-pack hook), not every legmacs.* ns." + ["legmacs.keymap" "legmacs.modeline" "legmacs.theme" "legmacs.modes" + "legmacs.buffer" "legmacs.minibuffer" "legmacs.modes.prog"]) + (def skip-catalog-ns #{"user" "core" "clojure.core" "gogen" "resolver" "rt" "ir" "ir.data" "disasm" "profile" "js" "let-go.core" "let-go.types" "let-go.semver"}) @@ -577,6 +612,9 @@ (defn- available-stdlib [] (filter (fn [nm] (find-ns (symbol nm))) stdlib-libs)) +(defn- available-editor [] + (filter (fn [nm] (find-ns (symbol nm))) editor-libs)) + (defn- parse-libspec [spec] (try (cond @@ -673,6 +711,7 @@ (map (fn [lib] (let [alias (some (fn [[a l]] (when (= l lib) a)) aliases) extra (when (and (not (contains? (set stdlib-libs) lib)) + (not (contains? (set editor-libs) lib)) (find-ns (symbol lib))) (str ": " (format-var-names (symbol lib))))] (str " " lib @@ -682,13 +721,18 @@ "\n")))) (defn namespace-catalog - "Live stdlib (and this file's requires) for the user message." + "Live stdlib, editor scripting surface, and this file's requires." ([] (namespace-catalog nil)) ([text] (str "Loaded stdlib (canonical names — never clojure.string, always string/):\n" (string/join "\n" (map (fn [nm] (str " " (format-ns-line nm))) (available-stdlib))) "\n" + "Editor scripting (this process is the live editor; C-x C-e applies " + "registry mutations):\n" + (string/join "\n" (map (fn [nm] (str " " (format-ns-line nm))) + (available-editor))) + "\n" (when (seq text) (format-file-requires text))))) diff --git a/test/letgo_mode_test.lg b/test/letgo_mode_test.lg index 621ac6c..a171d08 100644 --- a/test/letgo_mode_test.lg +++ b/test/letgo_mode_test.lg @@ -75,10 +75,9 @@ (deftest eval-last-sexp-sees-defs-after-an-ns-form ;; Regression: C-x C-e / C-j evaluate one already-read form via `eval`, - ;; while C-c C-e `load-string`s the whole buffer. load-string compiles - ;; and runs each form in order, so `(ns foo)` is in force for later - ;; forms; a C-x C-e of `(x)` after separately evaluating `(ns foo)` and - ;; `(defn x [] 1)` has to land in the same namespace or it reports + ;; while C-c C-e evals every form in order. `(ns foo)` is in force for + ;; later forms; a C-x C-e of `(x)` after separately evaluating `(ns foo)` + ;; and `(defn x [] 1)` has to land in the same namespace or it reports ;; "Can't resolve x in this context" even though eval-buffer of the ;; same three lines returns 1. (testing "sequential C-x C-e of ns, defn, then a call finds the defn" @@ -132,7 +131,7 @@ (is (= "=> 1" (:message s)))))) (deftest eval-last-sexp-after-eval-buffer-uses-the-buffer-ns - ;; C-c C-e load-string's the buffer (defs land in foo) then preserving-ns + ;; C-c C-e evals the buffer (defs land in foo) then preserving-ns ;; puts *ns* back to the editor. C-x C-e of @a used to compile in the ;; editor ns and report "Can't resolve a". Eval last-sexp in the ;; buffer's (ns ...) instead, like CIDER. @@ -155,6 +154,27 @@ s (eval-last-at-row s 2)] (is (= "=> 42" (:message s)))))) +(deftest eval-buffer-scratch-resolves-lg-isms + ;; let-go's load-string compiles in clojure.core, which does not refer + ;; let-go.core -- so `(now)` in *scratch* (no ns form) used to fail + ;; "Can't resolve now" on C-c C-e. Form-by-form eval in the current + ;; *ns* is what C-x C-e already did. + (testing "C-c C-e of a no-ns-form buffer can call now" + (let [s (lg-mode/eval-buffer + (buf/new-state nil ["(.Format (now) \"15:04\")"]))] + (is (string/starts-with? (:message s) "=> ")) + (is (not (string/includes? (:message s) "Can't resolve"))))) + + (testing "a defn using now interned in the current ns, not core" + (let [s (lg-mode/eval-buffer + (buf/new-state nil ["(defn letgo-mode-test-clock [state]" + " {:text (.Format (now) \"15:04\")})" + "(letgo-mode-test-clock nil)"]))] + (is (string/starts-with? (:message s) "=> ")) + (is (string/includes? (:message s) ":text")) + (is (fn? @(resolve 'letgo-mode-test-clock))) + (is (nil? (ns-resolve (find-ns 'core) 'letgo-mode-test-clock)))))) + (deftest let-go-mode-is-registered-for-dot-lg-files (testing ".lg is in the auto-mode-alist, pointing at :let-go" (is (= :let-go (modes/mode-for-filename "foo.lg"))))) diff --git a/test/modeline_test.lg b/test/modeline_test.lg index 6112d9c..3027e6c 100644 --- a/test/modeline_test.lg +++ b/test/modeline_test.lg @@ -80,3 +80,27 @@ (is (= before (count (ml/left-segments state)))) (ml/remove-modeline-segment! :nil-seg) (ml/remove-modeline-segment! :blank-seg)))) + +(deftest a-throwing-segment-does-not-kill-the-bar + (testing "the os/sh-into-trim mistake becomes a badge instead of crashing render" + (let [state (buf/new-state "a.txt" [""]) + left-before (ml/left-segments state)] + (ml/register-modeline-segment! :clock :right + (fn [_] (string/trim {:exit 0 :out "12:34\n" :err ""}))) + (try + (let [right (ml/right-segments state) + texts (map :text right)] + (is (some (fn [t] (string/includes? t "clock!")) texts)) + (is (= left-before (ml/left-segments state)))) + (finally + (ml/remove-modeline-segment! :clock))))) + + (testing "a non-string :text is the same class of bug, same badge" + (let [state (buf/new-state "a.txt" [""])] + (ml/register-modeline-segment! :clock :right + (fn [_] {:text {:exit 0 :out "12:34\n"}})) + (try + (is (some (fn [t] (string/includes? t "clock!")) + (map :text (ml/right-segments state)))) + (finally + (ml/remove-modeline-segment! :clock)))))) diff --git a/test/render_test.lg b/test/render_test.lg index 4ef6c02..83b85b5 100644 --- a/test/render_test.lg +++ b/test/render_test.lg @@ -168,7 +168,11 @@ (testing "explicit newlines still split into rows -- only the bytes inside each line are escaped" - (is (= ["a" "^[x"] (vec (r/message-lines (str "a\n" (char 27) "x") 40)))))) + (is (= ["a" "^[x"] (vec (r/message-lines (str "a\n" (char 27) "x") 40))))) + + (testing "a tab is indent, not caret-I -- CompileError chains use \\tcaused by" + (is (= [" caused by boom"] (vec (r/message-lines (str (char 9) "caused by boom") 40)))) + (is (not (string/includes? (r/sanitize (str (char 9) "caused by")) "^I"))))) (defn- fit [s cols rows] (r/scroll-to-fit s (r/dims s cols rows))) diff --git a/test/repl_mode_test.lg b/test/repl_mode_test.lg index 96b6f36..0dbe08a 100644 --- a/test/repl_mode_test.lg +++ b/test/repl_mode_test.lg @@ -54,7 +54,8 @@ (type-text "(* x x)") (d/dispatch "RET") (d/dispatch ")") (d/dispatch "RET") (type-text "(f 5)") (d/dispatch "RET"))] - (is (string/includes? (string/join "\n" (:lines s)) "#'core/f")) + (is (string/includes? (string/join "\n" (:lines s)) + (str "#'" (ns-name *ns*) "/f"))) (is (= "25" (nth (:lines s) (- (count (:lines s)) 3))))))) (deftest eval-errors-print-in-the-transcript-not-just-the-echo-area diff --git a/test/vibe_test.lg b/test/vibe_test.lg index d063f9d..da7ba4f 100644 --- a/test/vibe_test.lg +++ b/test/vibe_test.lg @@ -2,6 +2,14 @@ (:require [test :refer :all] [string] [legmacs.buffer :as buf] + ;; Loaded so the catalog's editor-libs block is populated the + ;; same way a real session sees them (vibe itself only requires + ;; buffer/keymap; modeline/theme/modes live elsewhere). + [legmacs.modeline] + [legmacs.theme] + [legmacs.modes] + [legmacs.minibuffer] + [legmacs.modes.prog] ;; :refer too, mirroring main.lg: eval-vibe-form `eval`s the ;; form in whatever namespace is current, so `vibe` has to ;; resolve here for the error-path test to reach the real fn. @@ -226,7 +234,15 @@ (is (string/includes? p "string/join")) (is (string/includes? p "clojure.string")) (is (string/includes? p "docstring")) - (is (string/includes? p "ns-publics")))) + (is (string/includes? p "ns-publics")) + (is (string/includes? p "this editor")) + (is (string/includes? p "defcommand")) + (is (string/includes? p "register-modeline-segment!")) + (is (string/includes? p "os/sh returns")) + (is (string/includes? p "every frame")) + (is (string/includes? p "(.Format (now)")) + (is (string/includes? p "let-go.core/now")) + (is (string/includes? p "Do not add an (ns")))) (deftest namespace-catalog-lists-live-stdlib (let [cat (vibe/namespace-catalog)] @@ -235,11 +251,30 @@ (is (string/includes? cat "json:")) (is (string/includes? cat "read-json")))) +(deftest namespace-catalog-lists-editor-scripting + (let [cat (vibe/namespace-catalog)] + (is (string/includes? cat "Editor scripting")) + (is (string/includes? cat "legmacs.modeline:")) + (is (string/includes? cat "register-modeline-segment!")) + (is (string/includes? cat "legmacs.keymap:")) + (is (string/includes? cat "string:")))) + (deftest user-message-includes-the-namespace-catalog (let [msg (vibe/user-message "make it go" nil)] (is (string/includes? msg "----- namespaces -----")) (is (string/includes? msg "string:")))) +(deftest user-message-scratch-carries-editor-catalog + (testing "no ns form still includes the editor block, so a top-level vibe in *scratch* can emit register-modeline-segment!" + (let [msg (vibe/user-message "clock on the modeline" + {:text vibe/marker + :filename "*scratch*" + :column 0})] + (is (string/includes? msg "*scratch*")) + (is (not (string/includes? msg "This file's ns form requires:"))) + (is (string/includes? msg "Editor scripting")) + (is (string/includes? msg "legmacs.modeline:"))))) + (deftest user-message-lists-the-file-requires (let [text (str "(ns foo\n (:require [string]\n [json]))\n" vibe/marker) msg (vibe/user-message "x" {:text text :filename "foo.lg" :column 0})]