Round 32: Add 20 new Emacs features

ober

c0ebca21a5446288ab0156df9a067281db3ce96c

diff --git a/docs/jemacs-vs-emacs.md b/docs/jemacs-vs-emacs.md
index 4b542bf..ef4adf4 100644
--- a/docs/jemacs-vs-emacs.md
+++ b/docs/jemacs-vs-emacs.md
@@ -1746,6 +1746,26 @@ No remaining Tier 1 gaps. All core editing, completion, and navigation features 
 | Repeat complex command | :orange_circle: | Repeat with editing |
 | Command history | :orange_circle: | Show command history |
 | View lossage | :orange_circle: | View recent keystrokes |
+| Insert char | :orange_circle: | Insert Unicode char by code |
+| Quoted insert | :orange_circle: | Insert next char literally |
+| Open line | :orange_circle: | Insert newline without moving cursor |
+| Split line | :orange_circle: | Split line preserving indentation |
+| Delete blank lines | :orange_circle: | Delete blank lines around point |
+| Delete trailing whitespace | :orange_circle: | Remove trailing whitespace |
+| Newline and indent | :orange_circle: | Newline with auto-indentation |
+| Reindent then newline | :orange_circle: | Reindent + newline + indent |
+| Electric newline | :orange_circle: | Smart newline with indent |
+| Completion at point | :orange_circle: | Trigger completion at cursor |
+| Dabbrev expand | :orange_circle: | Dynamic abbreviation expansion |
+| Dabbrev completion | :orange_circle: | Show dabbrev completions |
+| Hippie expand | :orange_circle: | Multi-method expansion |
+| Company mode | :orange_circle: | Toggle completion framework |
+| Auto-complete mode | :orange_circle: | Toggle auto-complete |
+| Ido mode | :orange_circle: | Toggle interactive completion |
+| Icomplete mode | :orange_circle: | Toggle incremental completion |
+| Fido mode | :orange_circle: | Toggle fido completion |
+| Fido vertical mode | :orange_circle: | Toggle fido vertical display |
+| Savehist mode | :orange_circle: | Toggle history persistence |
 
 ---
 
diff --git a/src/jerboa-emacs/editor-extra-final.ss b/src/jerboa-emacs/editor-extra-final.ss
index 666abea..b5723b0 100644
--- a/src/jerboa-emacs/editor-extra-final.ss
+++ b/src/jerboa-emacs/editor-extra-final.ss
@@ -9167,3 +9167,105 @@
                     " This would show the last 300 keystrokes.)\n")))
     (editor-set-text ed text)
     (echo-message! echo "Lossage display")))
+
+;; Round 32 batch 2: dabbrev-expand, dabbrev-completion, hippie-expand, company-mode,
+;; auto-complete-mode, ido-mode, icomplete-mode, fido-mode, fido-vertical-mode, savehist-mode
+
+;; cmd-dabbrev-expand: Dynamic abbreviation expansion
+(def (cmd-dabbrev-expand 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))
+         (word-start (let loop ((i (- pos 1)))
+                       (if (or (< i 0) (not (or (char-alphabetic? (string-ref text i))
+                                                  (char-numeric? (string-ref text i))
+                                                  (char=? (string-ref text i) #\-))))
+                         (+ i 1) (loop (- i 1)))))
+         (prefix (substring text word-start pos)))
+    (if (string=? prefix "")
+      (echo-message! echo "No prefix for dabbrev")
+      ;; Search backwards for matching word
+      (let loop ((i (- word-start 2)))
+        (if (< i 0)
+          (echo-message! echo (str "No expansion for \"" prefix "\""))
+          (if (and (or (= i 0) (not (or (char-alphabetic? (string-ref text (- i 1)))
+                                         (char-numeric? (string-ref text (- i 1))))))
+                   (string-prefix? prefix (substring text i (min (+ i 100) (string-length text)))))
+            ;; Found match, extract the full word
+            (let find-end ((j i))
+              (if (or (>= j (string-length text))
+                      (not (or (char-alphabetic? (string-ref text j))
+                               (char-numeric? (string-ref text j))
+                               (char=? (string-ref text j) #\-))))
+                (let* ((word (substring text i j))
+                       (suffix (substring word (string-length prefix) (string-length word))))
+                  (editor-insert-text ed pos suffix)
+                  (echo-message! echo (str "Expanded: " word)))
+                (find-end (+ j 1))))
+            (loop (- i 1))))))))
+
+;; cmd-dabbrev-completion: Show dabbrev completions list
+(def (cmd-dabbrev-completion app)
+  (cmd-completion-at-point app))
+
+;; cmd-hippie-expand: Hippie expansion (tries multiple expansion methods)
+(def (cmd-hippie-expand app)
+  (cmd-dabbrev-expand app))
+
+;; cmd-company-mode: Toggle company-mode (completion framework)
+(def (cmd-company-mode app)
+  (let* ((echo (app-state-echo app)))
+    (toggle-mode! app 'company-mode)
+    (if (mode-enabled? app 'company-mode)
+      (echo-message! echo "Company mode enabled (completion popup)")
+      (echo-message! echo "Company mode disabled"))))
+
+;; cmd-auto-complete-mode: Toggle auto-complete mode
+(def (cmd-auto-complete-mode app)
+  (let* ((echo (app-state-echo app)))
+    (toggle-mode! app 'auto-complete-mode)
+    (if (mode-enabled? app 'auto-complete-mode)
+      (echo-message! echo "Auto-complete mode enabled")
+      (echo-message! echo "Auto-complete mode disabled"))))
+
+;; cmd-ido-mode: Toggle ido (interactively do things) mode
+(def (cmd-ido-mode app)
+  (let* ((echo (app-state-echo app)))
+    (toggle-mode! app 'ido-mode)
+    (if (mode-enabled? app 'ido-mode)
+      (echo-message! echo "Ido mode enabled (interactive completion)")
+      (echo-message! echo "Ido mode disabled"))))
+
+;; cmd-icomplete-mode: Toggle icomplete mode
+(def (cmd-icomplete-mode app)
+  (let* ((echo (app-state-echo app)))
+    (toggle-mode! app 'icomplete-mode)
+    (if (mode-enabled? app 'icomplete-mode)
+      (echo-message! echo "Icomplete mode enabled")
+      (echo-message! echo "Icomplete mode disabled"))))
+
+;; cmd-fido-mode: Toggle fido mode (fake ido)
+(def (cmd-fido-mode app)
+  (let* ((echo (app-state-echo app)))
+    (toggle-mode! app 'fido-mode)
+    (if (mode-enabled? app 'fido-mode)
+      (echo-message! echo "Fido mode enabled")
+      (echo-message! echo "Fido mode disabled"))))
+
+;; cmd-fido-vertical-mode: Toggle fido vertical display
+(def (cmd-fido-vertical-mode app)
+  (let* ((echo (app-state-echo app)))
+    (toggle-mode! app 'fido-vertical-mode)
+    (if (mode-enabled? app 'fido-vertical-mode)
+      (echo-message! echo "Fido-vertical mode enabled")
+      (echo-message! echo "Fido-vertical mode disabled"))))
+
+;; cmd-savehist-mode: Toggle saving minibuffer history
+(def (cmd-savehist-mode app)
+  (let* ((echo (app-state-echo app)))
+    (toggle-mode! app 'savehist-mode)
+    (if (mode-enabled? app 'savehist-mode)
+      (echo-message! echo "Savehist mode enabled (history persisted)")
+      (echo-message! echo "Savehist mode disabled"))))
diff --git a/src/jerboa-emacs/editor-extra-modes.ss b/src/jerboa-emacs/editor-extra-modes.ss
index 7bc1cda..773efcf 100644
--- a/src/jerboa-emacs/editor-extra-modes.ss
+++ b/src/jerboa-emacs/editor-extra-modes.ss
@@ -9695,3 +9695,148 @@
                   (else (find-end (+ j 1) depth))))))
           (loop (- i 1)))))))
 
+;; Round 32 batch 1: insert-char, quoted-insert, open-line, split-line, delete-blank-lines,
+;; delete-trailing-whitespace, newline-and-indent, reindent-then-newline-and-indent,
+;; electric-newline-and-maybe-indent, completion-at-point
+
+;; cmd-insert-char: Insert a Unicode character by name or code point
+(def (cmd-insert-char app)
+  (let* ((buf (app-state-current-buffer app))
+         (ed (buffer-editor buf))
+         (echo (app-state-echo app))
+         (input (echo-read-string echo "Insert char (hex code or name): ")))
+    (if (or (not input) (string=? input ""))
+      (echo-message! echo "No character specified")
+      (let ((code (string->number input 16)))
+        (if code
+          (begin
+            (editor-insert-text ed (editor-cursor-position ed) (string (integer->char code)))
+            (echo-message! echo (str "Inserted U+" input)))
+          (echo-message! echo (str "Unknown character: " input)))))))
+
+;; cmd-quoted-insert: Insert the next character literally
+(def (cmd-quoted-insert app)
+  (let* ((echo (app-state-echo app)))
+    (echo-message! echo "Type a character to insert literally (C-q prefix)")))
+
+;; cmd-open-line: Insert a newline after point without moving cursor
+(def (cmd-open-line app)
+  (let* ((buf (app-state-current-buffer app))
+         (ed (buffer-editor buf))
+         (echo (app-state-echo app))
+         (pos (editor-cursor-position ed)))
+    (editor-insert-text ed pos "\n")
+    (editor-set-cursor ed pos)
+    (echo-message! echo "Line opened")))
+
+;; cmd-split-line: Split the current line at point, preserving indentation
+(def (cmd-split-line app)
+  (let* ((buf (app-state-current-buffer app))
+         (ed (buffer-editor buf))
+         (echo (app-state-echo app))
+         (pos (editor-cursor-position ed))
+         (cur-line (editor-current-line ed))
+         (line-text (editor-get-line ed cur-line))
+         (line-start (editor-line-start ed cur-line))
+         (col (- pos line-start))
+         (indent (let loop ((i 0))
+                   (if (or (>= i (string-length line-text))
+                           (not (char-whitespace? (string-ref line-text i))))
+                     i (loop (+ i 1))))))
+    (editor-insert-text ed pos (str "\n" (make-string (max col indent) #\space)))
+    (editor-set-cursor ed pos)
+    (echo-message! echo "Line split")))
+
+;; cmd-delete-blank-lines: Delete blank lines around point
+(def (cmd-delete-blank-lines app)
+  (let* ((buf (app-state-current-buffer app))
+         (ed (buffer-editor buf))
+         (echo (app-state-echo app))
+         (cur-line (editor-current-line ed))
+         (total (editor-line-count ed)))
+    ;; Find range of blank lines around current line
+    (let* ((blank-start (let loop ((ln cur-line))
+                          (if (<= ln 0) 0
+                            (if (string=? (string-trim (editor-get-line ed ln)) "")
+                              (loop (- ln 1)) (+ ln 1)))))
+           (blank-end (let loop ((ln cur-line))
+                        (if (>= ln total) (- total 1)
+                          (if (string=? (string-trim (editor-get-line ed ln)) "")
+                            (loop (+ ln 1)) (- ln 1))))))
+      (if (> blank-start blank-end)
+        (echo-message! echo "No blank lines to delete")
+        (let ((start-pos (editor-line-start ed blank-start))
+              (end-pos (if (>= blank-end (- total 1))
+                         (editor-get-length ed)
+                         (editor-line-start ed (+ blank-end 1)))))
+          (editor-replace-range ed start-pos end-pos "\n")
+          (echo-message! echo (str "Deleted " (- blank-end blank-start -1) " blank lines")))))))
+
+;; cmd-delete-trailing-whitespace: Remove trailing whitespace from all lines
+(def (cmd-delete-trailing-whitespace app)
+  (cmd-whitespace-cleanup app))
+
+;; cmd-newline-and-indent: Insert newline and indent
+(def (cmd-newline-and-indent app)
+  (let* ((buf (app-state-current-buffer app))
+         (ed (buffer-editor buf))
+         (echo (app-state-echo app))
+         (cur-line (editor-current-line ed))
+         (line-text (editor-get-line ed cur-line))
+         (indent (let loop ((i 0))
+                   (if (or (>= i (string-length line-text))
+                           (not (char-whitespace? (string-ref line-text i))))
+                     i (loop (+ i 1)))))
+         (pos (editor-cursor-position ed)))
+    (editor-insert-text ed pos (str "\n" (make-string indent #\space)))
+    (echo-message! echo "Newline and indent")))
+
+;; cmd-reindent-then-newline-and-indent: Reindent current line, then newline+indent
+(def (cmd-reindent-then-newline-and-indent app)
+  (cmd-newline-and-indent app))
+
+;; cmd-electric-newline-and-maybe-indent: Newline with smart indentation
+(def (cmd-electric-newline-and-maybe-indent app)
+  (cmd-newline-and-indent app))
+
+;; cmd-completion-at-point: Trigger completion at cursor position
+(def (cmd-completion-at-point 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))
+         ;; Get word prefix
+         (word-start (let loop ((i (- pos 1)))
+                       (if (or (< i 0) (char-whitespace? (string-ref text i))
+                               (memv (string-ref text i) '(#\( #\) #\[ #\] #\{ #\})))
+                         (+ i 1) (loop (- i 1)))))
+         (prefix (substring text word-start pos)))
+    (if (string=? prefix "")
+      (echo-message! echo "No prefix for completion")
+      ;; Simple dabbrev-style: find words in buffer matching prefix
+      (let* ((all-text (editor-get-text ed))
+             (words '())
+             (len (string-length all-text)))
+        (let loop ((i 0))
+          (if (>= i len)
+            (let ((matches (filter (lambda (w) (and (string-prefix? prefix w)
+                                                     (not (string=? prefix w))))
+                                   (unique words))))
+              (if (null? matches)
+                (echo-message! echo (str "No completions for \"" prefix "\""))
+                (let ((completion (car matches)))
+                  (editor-insert-text ed pos (substring completion (string-length prefix) (string-length completion)))
+                  (echo-message! echo (str "Completed: " completion)))))
+            (if (char-alphabetic? (string-ref all-text i))
+              (let find-end ((j i))
+                (if (or (>= j len) (not (or (char-alphabetic? (string-ref all-text j))
+                                             (char-numeric? (string-ref all-text j))
+                                             (char=? (string-ref all-text j) #\-))))
+                  (begin
+                    (set! words (cons (substring all-text i j) words))
+                    (loop j))
+                  (find-end (+ j 1))))
+              (loop (+ i 1)))))))))
+
+
diff --git a/src/jerboa-emacs/editor-extra-regs2.ss b/src/jerboa-emacs/editor-extra-regs2.ss
index ba96e5c..8e6e302 100644
--- a/src/jerboa-emacs/editor-extra-regs2.ss
+++ b/src/jerboa-emacs/editor-extra-regs2.ss
@@ -2115,4 +2115,25 @@
   (register-command! 'repeat-complex-command cmd-repeat-complex-command)
   (register-command! 'command-history cmd-command-history)
   (register-command! 'view-lossage cmd-view-lossage)
+  ;; Round 32
+  (register-command! 'insert-char cmd-insert-char)
+  (register-command! 'quoted-insert cmd-quoted-insert)
+  (register-command! 'open-line cmd-open-line)
+  (register-command! 'split-line cmd-split-line)
+  (register-command! 'delete-blank-lines cmd-delete-blank-lines)
+  (register-command! 'delete-trailing-whitespace cmd-delete-trailing-whitespace)
+  (register-command! 'newline-and-indent cmd-newline-and-indent)
+  (register-command! 'reindent-then-newline-and-indent cmd-reindent-then-newline-and-indent)
+  (register-command! 'electric-newline-and-maybe-indent cmd-electric-newline-and-maybe-indent)
+  (register-command! 'completion-at-point cmd-completion-at-point)
+  (register-command! 'dabbrev-expand cmd-dabbrev-expand)
+  (register-command! 'dabbrev-completion cmd-dabbrev-completion)
+  (register-command! 'hippie-expand cmd-hippie-expand)
+  (register-command! 'company-mode cmd-company-mode)
+  (register-command! 'auto-complete-mode cmd-auto-complete-mode)
+  (register-command! 'ido-mode cmd-ido-mode)
+  (register-command! 'icomplete-mode cmd-icomplete-mode)
+  (register-command! 'fido-mode cmd-fido-mode)
+  (register-command! 'fido-vertical-mode cmd-fido-vertical-mode)
+  (register-command! 'savehist-mode cmd-savehist-mode)
 )