Round 33: Add 20 new Emacs features

ober

508b934328119a3caed7b43aaa66220da01f3ce1

diff --git a/docs/jemacs-vs-emacs.md b/docs/jemacs-vs-emacs.md
index ef4adf4..e85cd09 100644
--- a/docs/jemacs-vs-emacs.md
+++ b/docs/jemacs-vs-emacs.md
@@ -1766,6 +1766,26 @@ No remaining Tier 1 gaps. All core editing, completion, and navigation features 
 | Fido mode | :orange_circle: | Toggle fido completion |
 | Fido vertical mode | :orange_circle: | Toggle fido vertical display |
 | Savehist mode | :orange_circle: | Toggle history persistence |
+| Recentf mode | :orange_circle: | Toggle recent files tracking |
+| Recentf open files | :orange_circle: | Show recent files list |
+| Saveplace mode | :orange_circle: | Remember cursor position per file |
+| Global auto-revert mode | :orange_circle: | Toggle global auto-revert |
+| Global hl-line mode | :orange_circle: | Toggle global line highlighting |
+| Global display line numbers | :orange_circle: | Toggle global line numbers |
+| Global visual-line mode | :orange_circle: | Toggle global word wrap |
+| Delete-selection mode | :orange_circle: | Typing replaces selection |
+| CUA mode | :orange_circle: | CUA keybindings (C-c/v/x) |
+| Transient-mark mode | :orange_circle: | Toggle transient mark |
+| Shift-select mode | :orange_circle: | Toggle shift-select |
+| Set mark command | :orange_circle: | Set mark at point |
+| Exchange point and mark | :orange_circle: | Swap cursor and mark |
+| Pop mark | :orange_circle: | Pop mark from ring |
+| Pop global mark | :orange_circle: | Pop from global mark ring |
+| Push mark | :orange_circle: | Push mark to ring |
+| Mark ring max | :orange_circle: | Show mark ring limit |
+| Set mark command repeat | :orange_circle: | Set mark (repeatable) |
+| Mark defun | :orange_circle: | Select top-level form |
+| Narrow to defun | :orange_circle: | Narrow to top-level form |
 
 ---
 
diff --git a/src/jerboa-emacs/editor-extra-final.ss b/src/jerboa-emacs/editor-extra-final.ss
index b5723b0..2a99801 100644
--- a/src/jerboa-emacs/editor-extra-final.ss
+++ b/src/jerboa-emacs/editor-extra-final.ss
@@ -9269,3 +9269,117 @@
     (if (mode-enabled? app 'savehist-mode)
       (echo-message! echo "Savehist mode enabled (history persisted)")
       (echo-message! echo "Savehist mode disabled"))))
+
+;; Round 33 batch 2: shift-select-mode, set-mark-command, exchange-point-and-mark,
+;; pop-mark, pop-global-mark, push-mark, mark-ring-max, set-mark-command-repeat,
+;; mark-defun, narrow-to-defun
+
+(def (cmd-shift-select-mode app)
+  (let* ((echo (app-state-echo app)))
+    (toggle-mode! app 'shift-select-mode)
+    (if (mode-enabled? app 'shift-select-mode)
+      (echo-message! echo "Shift-select mode enabled")
+      (echo-message! echo "Shift-select mode disabled"))))
+
+(def (cmd-set-mark-command app)
+  (let* ((buf (app-state-current-buffer app))
+         (ed (buffer-editor buf))
+         (echo (app-state-echo app))
+         (pos (editor-cursor-position ed)))
+    (hash-put! (app-state-modes app) 'mark-position pos)
+    (echo-message! echo (str "Mark set at position " pos))))
+
+(def (cmd-exchange-point-and-mark app)
+  (let* ((buf (app-state-current-buffer app))
+         (ed (buffer-editor buf))
+         (echo (app-state-echo app))
+         (mark (hash-get (app-state-modes app) 'mark-position))
+         (pos (editor-cursor-position ed)))
+    (if (not mark)
+      (echo-message! echo "No mark set")
+      (begin
+        (hash-put! (app-state-modes app) 'mark-position pos)
+        (editor-set-cursor ed mark)
+        (echo-message! echo (str "Exchanged point and mark"))))))
+
+(def (cmd-pop-mark app)
+  (let* ((echo (app-state-echo app))
+         (mark (hash-get (app-state-modes app) 'mark-position)))
+    (if (not mark)
+      (echo-message! echo "Mark ring empty")
+      (begin
+        (hash-remove! (app-state-modes app) 'mark-position)
+        (echo-message! echo "Mark popped")))))
+
+(def (cmd-pop-global-mark app)
+  (let* ((echo (app-state-echo app)))
+    (echo-message! echo "Global mark ring not yet implemented")))
+
+(def (cmd-push-mark app)
+  (let* ((buf (app-state-current-buffer app))
+         (ed (buffer-editor buf))
+         (echo (app-state-echo app))
+         (pos (editor-cursor-position ed)))
+    (hash-put! (app-state-modes app) 'mark-position pos)
+    (echo-message! echo (str "Mark pushed at " pos))))
+
+(def (cmd-mark-ring-max app)
+  (let* ((echo (app-state-echo app)))
+    (echo-message! echo "Mark ring max: 16 (default)")))
+
+(def (cmd-set-mark-command-repeat app)
+  (cmd-set-mark-command app))
+
+(def (cmd-mark-defun app)
+  (let* ((buf (app-state-current-buffer app))
+         (ed (buffer-editor buf))
+         (echo (app-state-echo app))
+         (pos (editor-cursor-position ed))
+         (text (editor-get-text ed))
+         (len (string-length text)))
+    ;; Find top-level form start
+    (let loop ((i pos))
+      (if (< i 0)
+        (echo-message! echo "No defun at point")
+        (if (and (char=? (string-ref text i) #\()
+                 (or (= i 0) (char=? (string-ref text (- i 1)) #\newline)))
+          (let find-end ((j (+ i 1)) (depth 1))
+            (if (>= j len)
+              (echo-message! echo "Unmatched paren")
+              (let ((c (string-ref text j)))
+                (cond
+                  ((char=? c #\() (find-end (+ j 1) (+ depth 1)))
+                  ((char=? c #\)) (if (= depth 1)
+                                    (begin
+                                      (editor-set-selection ed i (+ j 1))
+                                      (echo-message! echo "Defun marked"))
+                                    (find-end (+ j 1) (- depth 1))))
+                  (else (find-end (+ j 1) depth))))))
+          (loop (- i 1)))))))
+
+(def (cmd-narrow-to-defun app)
+  (let* ((buf (app-state-current-buffer app))
+         (ed (buffer-editor buf))
+         (echo (app-state-echo app))
+         (pos (editor-cursor-position ed))
+         (text (editor-get-text ed))
+         (len (string-length text)))
+    (let loop ((i pos))
+      (if (< i 0)
+        (echo-message! echo "No defun at point")
+        (if (and (char=? (string-ref text i) #\()
+                 (or (= i 0) (char=? (string-ref text (- i 1)) #\newline)))
+          (let find-end ((j (+ i 1)) (depth 1))
+            (if (>= j len)
+              (echo-message! echo "Unmatched paren")
+              (let ((c (string-ref text j)))
+                (cond
+                  ((char=? c #\() (find-end (+ j 1) (+ depth 1)))
+                  ((char=? c #\)) (if (= depth 1)
+                                    (let ((defun-text (substring text i (+ j 1))))
+                                      (hash-put! (app-state-modes app) 'narrow-original text)
+                                      (editor-set-text ed defun-text)
+                                      (echo-message! echo "Narrowed to defun"))
+                                    (find-end (+ j 1) (- depth 1))))
+                  (else (find-end (+ j 1) depth))))))
+          (loop (- i 1)))))))
diff --git a/src/jerboa-emacs/editor-extra-modes.ss b/src/jerboa-emacs/editor-extra-modes.ss
index 773efcf..21b5aa3 100644
--- a/src/jerboa-emacs/editor-extra-modes.ss
+++ b/src/jerboa-emacs/editor-extra-modes.ss
@@ -9839,4 +9839,81 @@
                   (find-end (+ j 1))))
               (loop (+ i 1)))))))))
 
+;; Round 33 batch 1: recentf-mode, recentf-open-files, saveplace-mode, global-auto-revert-mode,
+;; global-hl-line-mode, global-display-line-numbers-mode, global-visual-line-mode,
+;; delete-selection-mode, cua-mode, transient-mark-mode
+
+(def (cmd-recentf-mode app)
+  (let* ((echo (app-state-echo app)))
+    (toggle-mode! app 'recentf-mode)
+    (if (mode-enabled? app 'recentf-mode)
+      (echo-message! echo "Recentf mode enabled (recent files tracked)")
+      (echo-message! echo "Recentf mode disabled"))))
+
+(def (cmd-recentf-open-files app)
+  (let* ((buf (app-state-current-buffer app))
+         (ed (buffer-editor buf))
+         (echo (app-state-echo app))
+         (text (str "=== Recent Files ===\n\n"
+                    "(Recent file tracking not yet persisted.\n"
+                    " Enable recentf-mode and files will be tracked.)\n")))
+    (editor-set-text ed text)
+    (echo-message! echo "Recent files list")))
+
+(def (cmd-saveplace-mode app)
+  (let* ((echo (app-state-echo app)))
+    (toggle-mode! app 'saveplace-mode)
+    (if (mode-enabled? app 'saveplace-mode)
+      (echo-message! echo "Save-place mode enabled (cursor position remembered)")
+      (echo-message! echo "Save-place mode disabled"))))
+
+(def (cmd-global-auto-revert-mode app)
+  (let* ((echo (app-state-echo app)))
+    (toggle-mode! app 'global-auto-revert-mode)
+    (if (mode-enabled? app 'global-auto-revert-mode)
+      (echo-message! echo "Global auto-revert mode enabled")
+      (echo-message! echo "Global auto-revert mode disabled"))))
+
+(def (cmd-global-hl-line-mode app)
+  (let* ((echo (app-state-echo app)))
+    (toggle-mode! app 'global-hl-line-mode)
+    (if (mode-enabled? app 'global-hl-line-mode)
+      (echo-message! echo "Global hl-line mode enabled")
+      (echo-message! echo "Global hl-line mode disabled"))))
+
+(def (cmd-global-display-line-numbers-mode app)
+  (let* ((echo (app-state-echo app)))
+    (toggle-mode! app 'global-display-line-numbers-mode)
+    (if (mode-enabled? app 'global-display-line-numbers-mode)
+      (echo-message! echo "Global display-line-numbers mode enabled")
+      (echo-message! echo "Global display-line-numbers mode disabled"))))
+
+(def (cmd-global-visual-line-mode app)
+  (let* ((echo (app-state-echo app)))
+    (toggle-mode! app 'global-visual-line-mode)
+    (if (mode-enabled? app 'global-visual-line-mode)
+      (echo-message! echo "Global visual-line mode enabled")
+      (echo-message! echo "Global visual-line mode disabled"))))
+
+(def (cmd-delete-selection-mode app)
+  (let* ((echo (app-state-echo app)))
+    (toggle-mode! app 'delete-selection-mode)
+    (if (mode-enabled? app 'delete-selection-mode)
+      (echo-message! echo "Delete-selection mode enabled (typing replaces selection)")
+      (echo-message! echo "Delete-selection mode disabled"))))
+
+(def (cmd-cua-mode app)
+  (let* ((echo (app-state-echo app)))
+    (toggle-mode! app 'cua-mode)
+    (if (mode-enabled? app 'cua-mode)
+      (echo-message! echo "CUA mode enabled (C-c=copy, C-v=paste, C-x=cut)")
+      (echo-message! echo "CUA mode disabled"))))
+
+(def (cmd-transient-mark-mode app)
+  (let* ((echo (app-state-echo app)))
+    (toggle-mode! app 'transient-mark-mode)
+    (if (mode-enabled? app 'transient-mark-mode)
+      (echo-message! echo "Transient-mark mode enabled")
+      (echo-message! echo "Transient-mark mode disabled"))))
+
 
diff --git a/src/jerboa-emacs/editor-extra-regs2.ss b/src/jerboa-emacs/editor-extra-regs2.ss
index 8e6e302..804a864 100644
--- a/src/jerboa-emacs/editor-extra-regs2.ss
+++ b/src/jerboa-emacs/editor-extra-regs2.ss
@@ -2136,4 +2136,25 @@
   (register-command! 'fido-mode cmd-fido-mode)
   (register-command! 'fido-vertical-mode cmd-fido-vertical-mode)
   (register-command! 'savehist-mode cmd-savehist-mode)
+  ;; Round 33
+  (register-command! 'recentf-mode cmd-recentf-mode)
+  (register-command! 'recentf-open-files cmd-recentf-open-files)
+  (register-command! 'saveplace-mode cmd-saveplace-mode)
+  (register-command! 'global-auto-revert-mode cmd-global-auto-revert-mode)
+  (register-command! 'global-hl-line-mode cmd-global-hl-line-mode)
+  (register-command! 'global-display-line-numbers-mode cmd-global-display-line-numbers-mode)
+  (register-command! 'global-visual-line-mode cmd-global-visual-line-mode)
+  (register-command! 'delete-selection-mode cmd-delete-selection-mode)
+  (register-command! 'cua-mode cmd-cua-mode)
+  (register-command! 'transient-mark-mode cmd-transient-mark-mode)
+  (register-command! 'shift-select-mode cmd-shift-select-mode)
+  (register-command! 'set-mark-command cmd-set-mark-command)
+  (register-command! 'exchange-point-and-mark cmd-exchange-point-and-mark)
+  (register-command! 'pop-mark cmd-pop-mark)
+  (register-command! 'pop-global-mark cmd-pop-global-mark)
+  (register-command! 'push-mark cmd-push-mark)
+  (register-command! 'mark-ring-max cmd-mark-ring-max)
+  (register-command! 'set-mark-command-repeat cmd-set-mark-command-repeat)
+  (register-command! 'mark-defun cmd-mark-defun)
+  (register-command! 'narrow-to-defun cmd-narrow-to-defun)
 )