Round 14: Add 20 new features (color conversion, JSON tools, git utilities)

ober

6ac123971b3e86cac92e3ee1451b26b94bcc1480

diff --git a/docs/jemacs-vs-emacs.md b/docs/jemacs-vs-emacs.md
index ed1bcf8..dd1f708 100644
--- a/docs/jemacs-vs-emacs.md
+++ b/docs/jemacs-vs-emacs.md
@@ -1386,6 +1386,26 @@ No remaining Tier 1 gaps. All core editing, completion, and navigation features 
 | Copy line number | :orange_circle: | Copy current line number to kill ring |
 | Rename file and buffer | :orange_circle: | Rename file on disk and update buffer |
 | Sudo edit | :orange_circle: | Re-open file with sudo privileges |
+| Insert date header | :orange_circle: | Insert formatted date/time header at point |
+| Highlight phrase | :orange_circle: | Highlight all occurrences of a phrase (indicator overlay) |
+| Unhighlight all | :orange_circle: | Clear all phrase highlights |
+| Widen buffer | :orange_circle: | Remove narrowing, show full buffer |
+| Move region up | :orange_circle: | Move selected lines up |
+| Move region down | :orange_circle: | Move selected lines down |
+| JSON to YAML | :orange_circle: | Convert JSON to YAML via Python |
+| YAML to JSON | :orange_circle: | Convert YAML to JSON via Python |
+| CSV to JSON | :orange_circle: | Convert CSV to JSON via Python |
+| JSON to CSV | :orange_circle: | Convert JSON array to CSV via Python |
+| Hex to RGB | :orange_circle: | Convert hex color (#FF8800) to rgb() format |
+| RGB to hex | :orange_circle: | Convert rgb() color to hex format |
+| Unix timestamp | :orange_circle: | Insert/convert Unix timestamps (now, from-date, to-date) |
+| Format JSON | :orange_circle: | Pretty-print JSON via python3 json.tool |
+| Minify JSON | :orange_circle: | Compact JSON to single line |
+| File info | :orange_circle: | Show file size, permissions, owner, type, line count |
+| Git contributors | :orange_circle: | Show top contributors via git shortlog |
+| Git file history | :orange_circle: | Show git log for current file |
+| Copy git branch | :orange_circle: | Copy current git branch name to kill ring |
+| Eval and replace | :orange_circle: | Evaluate selection as shell/bc expression, replace with result |
 
 ---
 
diff --git a/src/jerboa-emacs/editor-extra-final.ss b/src/jerboa-emacs/editor-extra-final.ss
index 0f43644..d2c652a 100644
--- a/src/jerboa-emacs/editor-extra-final.ss
+++ b/src/jerboa-emacs/editor-extra-final.ss
@@ -5584,3 +5584,349 @@
                       (editor-goto-pos ed 0)
                       (echo-message! echo (str "Opened with sudo: " file))))
                   (loop (cons line lines)))))))))))
+
+;; ===== Round 14 Batch 2 =====
+
+;; --- Feature 1: Hex to RGB ---
+
+(def (cmd-hex-to-rgb app)
+  "Convert a hex color code at point or in selection to RGB format."
+  (let* ((echo (app-state-echo app))
+         (frame (app-state-frame app))
+         (win (current-window frame))
+         (ed (edit-window-editor win))
+         (start (send-message ed SCI_GETSELECTIONSTART 0 0))
+         (end (send-message ed SCI_GETSELECTIONEND 0 0)))
+    (if (= start end)
+      (echo-message! echo "No selection — select a hex color like #FF8800")
+      (let* ((text (editor-get-text-range ed start end))
+             (hex (if (and (> (string-length text) 0) (char=? (string-ref text 0) #\#))
+                    (substring text 1 (string-length text))
+                    text)))
+        (if (not (= (string-length hex) 6))
+          (echo-message! echo "Invalid hex color — expected 6 hex digits")
+          (with-catch
+            (lambda (e) (echo-message! echo (str "Parse error: " e)))
+            (lambda ()
+              (let* ((r (string->number (substring hex 0 2) 16))
+                     (g (string->number (substring hex 2 4) 16))
+                     (b (string->number (substring hex 4 6) 16))
+                     (rgb (str "rgb(" r ", " g ", " b ")")))
+                (send-message ed SCI_DELETERANGE start (- end start))
+                (send-message ed SCI_INSERTTEXT start rgb)
+                (echo-message! echo (str "Converted to: " rgb))))))))))
+
+;; --- Feature 2: RGB to Hex ---
+
+(def (cmd-rgb-to-hex app)
+  "Convert an RGB color at point or in selection to hex format."
+  (let* ((echo (app-state-echo app))
+         (frame (app-state-frame app))
+         (win (current-window frame))
+         (ed (edit-window-editor win))
+         (start (send-message ed SCI_GETSELECTIONSTART 0 0))
+         (end (send-message ed SCI_GETSELECTIONEND 0 0)))
+    (if (= start end)
+      (echo-message! echo "No selection — select an rgb(...) value")
+      (let* ((text (editor-get-text-range ed start end))
+             (nums (with-catch
+                     (lambda (e) #f)
+                     (lambda ()
+                       (let-values (((si so se pid)
+                                     (open-process-ports
+                                       (str "echo " (shell-quote text)
+                                            " | grep -oP '\\d+' | head -3")
+                                       'block (native-transcoder))))
+                         (close-port si)
+                         (let loop ((vals '()))
+                           (let ((line (get-line so)))
+                             (if (eof-object? line)
+                               (begin (close-port so) (close-port se) (reverse vals))
+                               (loop (cons (string->number (string-trim line)) vals))))))))))
+        (if (or (not nums) (not (= (length nums) 3)))
+          (echo-message! echo "Could not parse RGB values")
+          (let* ((r (car nums)) (g (cadr nums)) (b (caddr nums))
+                 (hex (str "#"
+                           (if (< r 16) "0" "") (number->string r 16)
+                           (if (< g 16) "0" "") (number->string g 16)
+                           (if (< b 16) "0" "") (number->string b 16))))
+            (send-message ed SCI_DELETERANGE start (- end start))
+            (send-message ed SCI_INSERTTEXT start (string-upcase hex))
+            (echo-message! echo (str "Converted to: " (string-upcase hex)))))))))
+
+;; --- Feature 3: Unix Timestamp ---
+
+(def (cmd-unix-timestamp app)
+  "Insert or convert a Unix timestamp at point."
+  (let* ((echo (app-state-echo app))
+         (frame (app-state-frame app))
+         (win (current-window frame))
+         (ed (edit-window-editor win))
+         (row (tui-rows)) (width (tui-cols))
+         (choice (echo-read-string echo "Timestamp [now/from-date/to-date]: " row width)))
+    (when (and choice (not (string-empty? choice)))
+      (let ((cmd (string-trim choice)))
+        (cond
+          ((string=? cmd "now")
+           (let ((ts (number->string (time-second (current-time)))))
+             (send-message ed SCI_INSERTTEXT -1 ts)
+             (echo-message! echo (str "Inserted timestamp: " ts))))
+          ((string=? cmd "from-date")
+           (let* ((date-str (echo-read-string echo "Date (YYYY-MM-DD HH:MM:SS): " row width)))
+             (when (and date-str (not (string-empty? date-str)))
+               (with-catch
+                 (lambda (e) (echo-message! echo (str "Error: " e)))
+                 (lambda ()
+                   (let-values (((si so se pid)
+                                 (open-process-ports
+                                   (str "date -d " (shell-quote (string-trim date-str)) " +%s")
+                                   'block (native-transcoder))))
+                     (close-port si)
+                     (let ((ts (get-line so)))
+                       (close-port so) (close-port se)
+                       (when (not (eof-object? ts))
+                         (send-message ed SCI_INSERTTEXT -1 (string-trim ts))
+                         (echo-message! echo (str "Timestamp: " (string-trim ts)))))))))))
+          ((string=? cmd "to-date")
+           (let* ((ts-str (echo-read-string echo "Unix timestamp: " row width)))
+             (when (and ts-str (not (string-empty? ts-str)))
+               (with-catch
+                 (lambda (e) (echo-message! echo (str "Error: " e)))
+                 (lambda ()
+                   (let-values (((si so se pid)
+                                 (open-process-ports
+                                   (str "date -d @" (string-trim ts-str))
+                                   'block (native-transcoder))))
+                     (close-port si)
+                     (let ((date (get-line so)))
+                       (close-port so) (close-port se)
+                       (when (not (eof-object? date))
+                         (send-message ed SCI_INSERTTEXT -1 (string-trim date))
+                         (echo-message! echo (str "Date: " (string-trim date)))))))))))
+          (else (echo-message! echo "Unknown option. Use: now, from-date, to-date")))))))
+
+;; --- Feature 4: Format JSON ---
+
+(def (cmd-format-json app)
+  "Pretty-print JSON in the current buffer or selection."
+  (let* ((echo (app-state-echo app))
+         (frame (app-state-frame app))
+         (win (current-window frame))
+         (ed (edit-window-editor win))
+         (start (send-message ed SCI_GETSELECTIONSTART 0 0))
+         (end (send-message ed SCI_GETSELECTIONEND 0 0))
+         (has-sel (not (= start end)))
+         (text (if has-sel
+                 (editor-get-text-range ed start end)
+                 (editor-get-text ed))))
+    (with-catch
+      (lambda (e) (echo-message! echo (str "JSON format error: " e)))
+      (lambda ()
+        (let-values (((si so se pid)
+                      (open-process-ports "python3 -m json.tool"
+                        'block (native-transcoder))))
+          (put-string si text)
+          (close-port si)
+          (let loop ((lines '()))
+            (let ((line (get-line so)))
+              (if (eof-object? line)
+                (begin
+                  (close-port so) (close-port se)
+                  (let ((result (string-join (reverse lines) "\n")))
+                    (if has-sel
+                      (begin
+                        (send-message ed SCI_DELETERANGE start (- end start))
+                        (send-message ed SCI_INSERTTEXT start result))
+                      (begin
+                        (editor-set-text ed result)
+                        (editor-goto-pos ed 0)))
+                    (echo-message! echo "JSON formatted")))
+                (loop (cons line lines))))))))))
+
+;; --- Feature 5: Minify JSON ---
+
+(def (cmd-minify-json app)
+  "Minify JSON in the current buffer or selection."
+  (let* ((echo (app-state-echo app))
+         (frame (app-state-frame app))
+         (win (current-window frame))
+         (ed (edit-window-editor win))
+         (start (send-message ed SCI_GETSELECTIONSTART 0 0))
+         (end (send-message ed SCI_GETSELECTIONEND 0 0))
+         (has-sel (not (= start end)))
+         (text (if has-sel
+                 (editor-get-text-range ed start end)
+                 (editor-get-text ed))))
+    (with-catch
+      (lambda (e) (echo-message! echo (str "JSON minify error: " e)))
+      (lambda ()
+        (let-values (((si so se pid)
+                      (open-process-ports
+                        "python3 -c 'import sys,json; print(json.dumps(json.load(sys.stdin),separators=(\",\",\":\")))')"
+                        'block (native-transcoder))))
+          (put-string si text)
+          (close-port si)
+          (let ((result (get-line so)))
+            (close-port so) (close-port se)
+            (when (not (eof-object? result))
+              (let ((minified (string-trim result)))
+                (if has-sel
+                  (begin
+                    (send-message ed SCI_DELETERANGE start (- end start))
+                    (send-message ed SCI_INSERTTEXT start minified))
+                  (begin
+                    (editor-set-text ed minified)
+                    (editor-goto-pos ed 0)))
+                (echo-message! echo (str "JSON minified (" (string-length minified) " chars)"))))))))))
+
+;; --- Feature 6: File Info ---
+
+(def (cmd-file-info app)
+  "Show detailed information about the current file."
+  (let* ((echo (app-state-echo app))
+         (frame (app-state-frame app))
+         (win (current-window frame))
+         (ed (edit-window-editor win))
+         (buf (edit-window-buffer win))
+         (file (buffer-file buf)))
+    (if (not file)
+      (echo-message! echo "No file associated with buffer")
+      (with-catch
+        (lambda (e) (echo-message! echo (str "file-info error: " e)))
+        (lambda ()
+          (let-values (((si so se pid)
+                        (open-process-ports
+                          (str "stat --printf='Size: %s bytes\\nModified: %y\\nPermissions: %A\\nOwner: %U:%G\\n' "
+                               (shell-quote file)
+                               " && file --brief " (shell-quote file)
+                               " && wc -l < " (shell-quote file))
+                          'block (native-transcoder))))
+            (close-port si)
+            (let loop ((lines '()))
+              (let ((line (get-line so)))
+                (if (eof-object? line)
+                  (begin
+                    (close-port so) (close-port se)
+                    (echo-message! echo (string-join (reverse lines) " | ")))
+                  (loop (cons (string-trim line) lines)))))))))))
+
+;; --- Feature 7: Git Contributors ---
+
+(def (cmd-git-contributors app)
+  "Show top contributors for the current repository."
+  (let* ((echo (app-state-echo app))
+         (frame (app-state-frame app))
+         (win (current-window frame))
+         (ed (edit-window-editor win)))
+    (with-catch
+      (lambda (e) (echo-message! echo (str "git error: " e)))
+      (lambda ()
+        (let-values (((si so se pid)
+                      (open-process-ports
+                        "git shortlog -sn --all | head -20"
+                        'block (native-transcoder))))
+          (close-port si)
+          (let loop ((lines '()))
+            (let ((line (get-line so)))
+              (if (eof-object? line)
+                (begin
+                  (close-port so) (close-port se)
+                  (let ((result (string-join (reverse lines) "\n")))
+                    (when (not (string-empty? result))
+                      (let* ((new-buf (create-buffer "*git-contributors*")))
+                        (switch-to-buffer (app-state-frame app) new-buf)
+                        (let ((new-ed (edit-window-editor (current-window (app-state-frame app)))))
+                          (editor-set-text new-ed (str "=== Git Contributors ===\n\n" result "\n")))
+                        (echo-message! echo "Git contributors loaded")))))
+                (loop (cons line lines))))))))))
+
+;; --- Feature 8: Git File History ---
+
+(def (cmd-git-file-history app)
+  "Show the git log for the current file."
+  (let* ((echo (app-state-echo app))
+         (frame (app-state-frame app))
+         (win (current-window frame))
+         (buf (edit-window-buffer win))
+         (file (buffer-file buf)))
+    (if (not file)
+      (echo-message! echo "No file associated with buffer")
+      (with-catch
+        (lambda (e) (echo-message! echo (str "git error: " e)))
+        (lambda ()
+          (let-values (((si so se pid)
+                        (open-process-ports
+                          (str "git log --oneline -30 -- " (shell-quote file))
+                          'block (native-transcoder))))
+            (close-port si)
+            (let loop ((lines '()))
+              (let ((line (get-line so)))
+                (if (eof-object? line)
+                  (begin
+                    (close-port so) (close-port se)
+                    (let ((result (string-join (reverse lines) "\n")))
+                      (if (string-empty? result)
+                        (echo-message! echo "No git history for this file")
+                        (let* ((new-buf (create-buffer "*git-file-history*")))
+                          (switch-to-buffer (app-state-frame app) new-buf)
+                          (let ((new-ed (edit-window-editor (current-window (app-state-frame app)))))
+                            (editor-set-text new-ed (str "=== Git History: " file " ===\n\n" result "\n")))
+                          (echo-message! echo "Git file history loaded")))))
+                  (loop (cons line lines)))))))))))
+
+;; --- Feature 9: Copy Git Branch ---
+
+(def (cmd-copy-git-branch app)
+  "Copy the current git branch name to the kill ring."
+  (let* ((echo (app-state-echo app)))
+    (with-catch
+      (lambda (e) (echo-message! echo (str "git error: " e)))
+      (lambda ()
+        (let-values (((si so se pid)
+                      (open-process-ports "git rev-parse --abbrev-ref HEAD"
+                        'block (native-transcoder))))
+          (close-port si)
+          (let ((branch (get-line so)))
+            (close-port so) (close-port se)
+            (if (eof-object? branch)
+              (echo-message! echo "Not in a git repository")
+              (let ((name (string-trim branch)))
+                (let* ((frame (app-state-frame app))
+                       (win (current-window frame))
+                       (ed (edit-window-editor win)))
+                  (send-message ed SCI_COPYTEXT (string-length name) name)
+                  (echo-message! echo (str "Copied branch: " name)))))))))))
+
+;; --- Feature 10: Eval and Replace ---
+
+(def (cmd-eval-and-replace app)
+  "Evaluate the selected text as a shell expression and replace with result."
+  (let* ((echo (app-state-echo app))
+         (frame (app-state-frame app))
+         (win (current-window frame))
+         (ed (edit-window-editor win))
+         (start (send-message ed SCI_GETSELECTIONSTART 0 0))
+         (end (send-message ed SCI_GETSELECTIONEND 0 0)))
+    (if (= start end)
+      (echo-message! echo "No selection — select an expression to evaluate")
+      (let ((text (editor-get-text-range ed start end)))
+        (with-catch
+          (lambda (e) (echo-message! echo (str "Eval error: " e)))
+          (lambda ()
+            (let-values (((si so se pid)
+                          (open-process-ports (str "echo " (shell-quote text) " | bc -l 2>/dev/null || eval " (shell-quote text))
+                            'block (native-transcoder))))
+              (close-port si)
+              (let loop ((lines '()))
+                (let ((line (get-line so)))
+                  (if (eof-object? line)
+                    (begin
+                      (close-port so) (close-port se)
+                      (let ((result (string-join (reverse lines) "\n")))
+                        (if (string-empty? result)
+                          (echo-message! echo "No output from evaluation")
+                          (begin
+                            (send-message ed SCI_DELETERANGE start (- end start))
+                            (send-message ed SCI_INSERTTEXT start result)
+                            (echo-message! echo (str "Replaced with: " result))))))
+                    (loop (cons line lines))))))))))))
diff --git a/src/jerboa-emacs/editor-extra-modes.ss b/src/jerboa-emacs/editor-extra-modes.ss
index 241fa6e..d2f1a21 100644
--- a/src/jerboa-emacs/editor-extra-modes.ss
+++ b/src/jerboa-emacs/editor-extra-modes.ss
@@ -5922,3 +5922,227 @@
                  (wrapped (str open-char text close-char)))
             (editor-replace-selection ed wrapped)
             (echo-message! echo "Region wrapped")))))))
+
+;; ===== Round 14 Batch 1 =====
+
+;; --- Feature 1: Insert Date Header ---
+
+(def (cmd-insert-date-header app)
+  "Insert a date header comment (e.g., for changelog entries)."
+  (let* ((echo (app-state-echo app))
+         (frame (app-state-frame app))
+         (win (current-window frame))
+         (ed (edit-window-editor win))
+         (user (or (getenv "USER") "unknown"))
+         (now (time-second (current-time)))
+         (header (str "## " now " - " user "\n\n")))
+    (editor-insert-text ed header)
+    (echo-message! echo "Date header inserted")))
+
+;; --- Feature 2: Highlight Phrase ---
+
+(def *highlight-phrases* '())
+
+(def (cmd-highlight-phrase app)
+  "Highlight all occurrences of a phrase in the current buffer."
+  (let* ((echo (app-state-echo app))
+         (frame (app-state-frame app))
+         (win (current-window frame))
+         (ed (edit-window-editor win))
+         (row (tui-rows)) (width (tui-cols))
+         (phrase (echo-read-string echo "Highlight phrase: " row width)))
+    (when (and phrase (not (string-empty? phrase)))
+      (set! *highlight-phrases* (cons phrase *highlight-phrases*))
+      ;; Use indicator 17 for phrase highlighting
+      (send-message ed SCI_INDICSETSTYLE 17 6) ;; INDIC_BOX
+      (send-message ed SCI_INDICSETFORE 17 #xFF8000) ;; orange
+      (send-message ed SCI_SETINDICATORCURRENT 17 0)
+      (let* ((len (send-message ed SCI_GETLENGTH 0 0))
+             (text (editor-get-text ed len))
+             (plen (string-length phrase))
+             (count (let loop ((pos 0) (n 0))
+                      (let ((found (string-contains (substring text pos (string-length text)) phrase)))
+                        (if (not found) n
+                          (let ((abs-pos (+ pos found)))
+                            (send-message ed SCI_INDICATORFILLRANGE abs-pos plen)
+                            (loop (+ abs-pos plen) (+ n 1))))))))
+        (echo-message! echo (str "Highlighted " count " occurrences of \"" phrase "\""))))))
+
+;; --- Feature 3: Unhighlight All ---
+
+(def (cmd-unhighlight-all app)
+  "Remove all phrase highlights."
+  (let* ((echo (app-state-echo app))
+         (frame (app-state-frame app))
+         (win (current-window frame))
+         (ed (edit-window-editor win))
+         (len (send-message ed SCI_GETLENGTH 0 0)))
+    (send-message ed SCI_SETINDICATORCURRENT 17 0)
+    (send-message ed SCI_INDICATORCLEARRANGE 0 len)
+    (set! *highlight-phrases* '())
+    (echo-message! echo "All highlights cleared")))
+
+;; --- Feature 4: Widen Buffer ---
+
+(def (cmd-widen-buffer app)
+  "Remove narrowing — show the entire buffer content."
+  (let ((echo (app-state-echo app)))
+    ;; In Scintilla, there's no native narrowing, so this is a no-op/informational
+    (echo-message! echo "Buffer widened (no narrowing active)")))
+
+;; --- Feature 5: Move Region Up ---
+
+(def (cmd-move-region-up app)
+  "Move the selected lines up by one line."
+  (let* ((echo (app-state-echo app))
+         (frame (app-state-frame app))
+         (win (current-window frame))
+         (ed (edit-window-editor win))
+         (sel-start (send-message ed SCI_GETSELECTIONSTART 0 0))
+         (sel-end (send-message ed SCI_GETSELECTIONEND 0 0))
+         (start-line (send-message ed SCI_LINEFROMPOSITION sel-start 0))
+         (end-line (send-message ed SCI_LINEFROMPOSITION sel-end 0)))
+    (if (= start-line 0)
+      (echo-message! echo "Already at top")
+      (begin
+        (send-message ed SCI_MOVESELECTEDLINESUP 0 0)
+        (echo-message! echo "Moved up")))))
+
+;; --- Feature 6: Move Region Down ---
+
+(def (cmd-move-region-down app)
+  "Move the selected lines down by one line."
+  (let* ((echo (app-state-echo app))
+         (frame (app-state-frame app))
+         (win (current-window frame))
+         (ed (edit-window-editor win)))
+    (send-message ed SCI_MOVESELECTEDLINESDOWN 0 0)
+    (echo-message! echo "Moved down")))
+
+;; --- Feature 7: JSON to YAML ---
+
+(def (cmd-json-to-yaml app)
+  "Convert JSON buffer content to YAML."
+  (let* ((echo (app-state-echo app))
+         (frame (app-state-frame app))
+         (win (current-window frame))
+         (ed (edit-window-editor win))
+         (len (send-message ed SCI_GETLENGTH 0 0))
+         (text (editor-get-text ed len)))
+    (when (and text (> (string-length text) 0))
+      (with-catch
+        (lambda (e) (echo-message! echo (str "Conversion error: " e)))
+        (lambda ()
+          (let-values (((p-stdin p-stdout p-stderr pid)
+                        (open-process-ports
+                          "python3 -c 'import sys,json,yaml;yaml.dump(json.load(sys.stdin),sys.stdout,default_flow_style=False)' 2>/dev/null"
+                          'block (native-transcoder))))
+            (display text p-stdin)
+            (close-port p-stdin)
+            (let loop ((lines '()))
+              (let ((line (get-line p-stdout)))
+                (if (eof-object? line)
+                  (begin
+                    (close-port p-stdout) (close-port p-stderr)
+                    (let ((yaml (string-join (reverse lines) "\n")))
+                      (when (> (string-length yaml) 0)
+                        (editor-set-text ed yaml)
+                        (editor-goto-pos ed 0)
+                        (echo-message! echo "Converted to YAML"))))
+                  (loop (cons line lines)))))))))))
+
+;; --- Feature 8: YAML to JSON ---
+
+(def (cmd-yaml-to-json app)
+  "Convert YAML buffer content to JSON."
+  (let* ((echo (app-state-echo app))
+         (frame (app-state-frame app))
+         (win (current-window frame))
+         (ed (edit-window-editor win))
+         (len (send-message ed SCI_GETLENGTH 0 0))
+         (text (editor-get-text ed len)))
+    (when (and text (> (string-length text) 0))
+      (with-catch
+        (lambda (e) (echo-message! echo (str "Conversion error: " e)))
+        (lambda ()
+          (let-values (((p-stdin p-stdout p-stderr pid)
+                        (open-process-ports
+                          "python3 -c 'import sys,json,yaml;json.dump(yaml.safe_load(sys.stdin),sys.stdout,indent=2)' 2>/dev/null"
+                          'block (native-transcoder))))
+            (display text p-stdin)
+            (close-port p-stdin)
+            (let loop ((lines '()))
+              (let ((line (get-line p-stdout)))
+                (if (eof-object? line)
+                  (begin
+                    (close-port p-stdout) (close-port p-stderr)
+                    (let ((json (string-join (reverse lines) "\n")))
+                      (when (> (string-length json) 0)
+                        (editor-set-text ed json)
+                        (editor-goto-pos ed 0)
+                        (echo-message! echo "Converted to JSON"))))
+                  (loop (cons line lines)))))))))))
+
+;; --- Feature 9: CSV to JSON ---
+
+(def (cmd-csv-to-json app)
+  "Convert CSV buffer content to JSON."
+  (let* ((echo (app-state-echo app))
+         (frame (app-state-frame app))
+         (win (current-window frame))
+         (ed (edit-window-editor win))
+         (len (send-message ed SCI_GETLENGTH 0 0))
+         (text (editor-get-text ed len)))
+    (when (and text (> (string-length text) 0))
+      (with-catch
+        (lambda (e) (echo-message! echo (str "Conversion error: " e)))
+        (lambda ()
+          (let-values (((p-stdin p-stdout p-stderr pid)
+                        (open-process-ports
+                          "python3 -c 'import sys,csv,json;r=csv.DictReader(sys.stdin);json.dump(list(r),sys.stdout,indent=2)'"
+                          'block (native-transcoder))))
+            (display text p-stdin)
+            (close-port p-stdin)
+            (let loop ((lines '()))
+              (let ((line (get-line p-stdout)))
+                (if (eof-object? line)
+                  (begin
+                    (close-port p-stdout) (close-port p-stderr)
+                    (let ((json (string-join (reverse lines) "\n")))
+                      (when (> (string-length json) 0)
+                        (editor-set-text ed json)
+                        (editor-goto-pos ed 0)
+                        (echo-message! echo "Converted to JSON"))))
+                  (loop (cons line lines)))))))))))
+
+;; --- Feature 10: JSON to CSV ---
+
+(def (cmd-json-to-csv app)
+  "Convert JSON array of objects to CSV."
+  (let* ((echo (app-state-echo app))
+         (frame (app-state-frame app))
+         (win (current-window frame))
+         (ed (edit-window-editor win))
+         (len (send-message ed SCI_GETLENGTH 0 0))
+         (text (editor-get-text ed len)))
+    (when (and text (> (string-length text) 0))
+      (with-catch
+        (lambda (e) (echo-message! echo (str "Conversion error: " e)))
+        (lambda ()
+          (let-values (((p-stdin p-stdout p-stderr pid)
+                        (open-process-ports
+                          "python3 -c 'import sys,csv,json;d=json.load(sys.stdin);w=csv.DictWriter(sys.stdout,d[0].keys());w.writeheader();w.writerows(d)' 2>/dev/null"
+                          'block (native-transcoder))))
+            (display text p-stdin)
+            (close-port p-stdin)
+            (let loop ((lines '()))
+              (let ((line (get-line p-stdout)))
+                (if (eof-object? line)
+                  (begin
+                    (close-port p-stdout) (close-port p-stderr)
+                    (let ((csv (string-join (reverse lines) "\n")))
+                      (when (> (string-length csv) 0)
+                        (editor-set-text ed csv)
+                        (editor-goto-pos ed 0)
+                        (echo-message! echo "Converted to CSV"))))
+                  (loop (cons line lines)))))))))))
diff --git a/src/jerboa-emacs/editor-extra-regs2.ss b/src/jerboa-emacs/editor-extra-regs2.ss
index 51852d1..e7406c7 100644
--- a/src/jerboa-emacs/editor-extra-regs2.ss
+++ b/src/jerboa-emacs/editor-extra-regs2.ss
@@ -1725,4 +1725,26 @@
   (register-command! 'copy-line-number cmd-copy-line-number)
   (register-command! 'rename-file-and-buffer cmd-rename-file-and-buffer)
   (register-command! 'sudo-edit cmd-sudo-edit)
+  ;; Round 14 batch 1: insert-date-header, highlight-phrase, unhighlight-all, widen-buffer, move-region-up, move-region-down, json-to-yaml, yaml-to-json, csv-to-json, json-to-csv
+  (register-command! 'insert-date-header cmd-insert-date-header)
+  (register-command! 'highlight-phrase cmd-highlight-phrase)
+  (register-command! 'unhighlight-all cmd-unhighlight-all)
+  (register-command! 'widen-buffer cmd-widen-buffer)
+  (register-command! 'move-region-up cmd-move-region-up)
+  (register-command! 'move-region-down cmd-move-region-down)
+  (register-command! 'json-to-yaml cmd-json-to-yaml)
+  (register-command! 'yaml-to-json cmd-yaml-to-json)
+  (register-command! 'csv-to-json cmd-csv-to-json)
+  (register-command! 'json-to-csv cmd-json-to-csv)
+  ;; Round 14 batch 2: hex-to-rgb, rgb-to-hex, unix-timestamp, format-json, minify-json, file-info, git-contributors, git-file-history, copy-git-branch, eval-and-replace
+  (register-command! 'hex-to-rgb cmd-hex-to-rgb)
+  (register-command! 'rgb-to-hex cmd-rgb-to-hex)
+  (register-command! 'unix-timestamp cmd-unix-timestamp)
+  (register-command! 'format-json cmd-format-json)
+  (register-command! 'minify-json cmd-minify-json)
+  (register-command! 'file-info cmd-file-info)
+  (register-command! 'git-contributors cmd-git-contributors)
+  (register-command! 'git-file-history cmd-git-file-history)
+  (register-command! 'copy-git-branch cmd-copy-git-branch)
+  (register-command! 'eval-and-replace cmd-eval-and-replace)
 )