Subject: [PATCH] Improve help-fns-edit-variable for Lisp editing
From: Spencer Baugh <[email-masked]>
To: [email-masked]
Date: Tue, 15 Apr 2025 17:18:37 -0400
List-Id: <debbugs-submit.debbugs.gnu.org>

Tags: patch

Before d50c82f3e98e ("Simplify
'help-enable-variable-value-editing' using 'string-edit'"),
help-fns-edit-variable would open a buffer in emacs-lisp-mode
and would not allow exiting that buffer with an invalid Lisp
expression.  Restore that functionality by enhancing string-edit
to allow choosing a major mode and allow passing a function to
validate the buffer contents before returning.

In GNU Emacs 30.1.50 (build 8, x86_64-pc-linux-gnu, X toolkit, cairo
 version 1.15.12, Xaw scroll bars) of 2025-04-10 built on
 igm-qws-u22796a
Repository revision: 128bc06bfcc56a35d6b5555cc546faaf0d964df0
Repository branch: emacs-30
Windowing system distributor 'The X.Org Foundation', version 11.0.12011000
System Description: Rocky Linux 8.10 (Green Obsidian)

Configured using:
 'configure --config-cache --with-x-toolkit=lucid --without-gpm
 --without-gconf --without-selinux --without-imagemagick --with-modules
 --with-gif=no --with-cairo --with-rsvg --without-compress-install
 --with-tree-sitter --with-native-compilation=aot'

From 26ea5e0cbbe85950d4362489ffe054cd570395c8 Mon Sep 17 00:00:00 2001
From: Spencer Baugh <[email-masked]>
Date: Tue, 15 Apr 2025 17:17:27 -0400
Subject: [PATCH] Improve help-fns-edit-variable for Lisp editing

Before d50c82f3e98e ("Simplify
'help-enable-variable-value-editing' using 'string-edit'"),
help-fns-edit-variable would open a buffer in emacs-lisp-mode
and would not allow exiting that buffer with an invalid Lisp
expression.  Restore that functionality by enhancing string-edit
to allow choosing a major mode and allow passing a function to
validate the buffer contents before returning.

* lisp/help-fns.el (help-fns-edit-variable): Pass
emacs-lisp-mode and read to read-string-from-buffer.
* lisp/textmodes/string-edit.el (string-edit--read): Add.
(string-edit, read-string-from-buffer): Add :major-mode and
:read arguments.
(string-edit-mode-map, string-edit-minor-mode-map)
(string-edit-mode, string-edit-minor-mode): Move string-edit
keybindings to a minor mode.
(string-edit-done): Call string-edit--read before exiting.
---
 lisp/help-fns.el              | 10 +++--
 lisp/textmodes/string-edit.el | 70 +++++++++++++++++++++++++----------
 2 files changed, 56 insertions(+), 24 deletions(-)

diff --git a/lisp/help-fns.el b/lisp/help-fns.el
index 0eb0a7a40be..57aab4d82e1 100644
--- a/lisp/help-fns.el
+++ b/lisp/help-fns.el
@@ -1561,10 +1561,12 @@ help-fns-edit-variable
   (let ((var (get-text-property (point) 'help-fns--edit-variable)))
     (unless var
       (error "No variable under point"))
-    (let ((str (read-string-from-buffer
-                (format ";; Edit the `%s' variable." (nth 0 var))
-                (prin1-to-string (nth 1 var)))))
-      (set (nth 0 var) (read str))
+    (let ((expr (read-string-from-buffer
+                 (format ";; Edit the `%s' variable." (nth 0 var))
+                 (prin1-to-string (nth 1 var))
+                 :major-mode #'emacs-lisp-mode
+                 :read #'read)))
+      (set (nth 0 var) expr)
       (revert-buffer))))
 
 (autoload 'shortdoc-help-fns-examples-function "shortdoc")
diff --git a/lisp/textmodes/string-edit.el b/lisp/textmodes/string-edit.el
index 3c76db202c7..47b5d4c337d 100644
--- a/lisp/textmodes/string-edit.el
+++ b/lisp/textmodes/string-edit.el
@@ -33,21 +33,28 @@ string-edit-prompt
 
 (defvar string-edit--success-callback)
 (defvar string-edit--abort-callback)
+(defvar string-edit--read)
 
 ;;;###autoload
 (cl-defun string-edit (prompt string success-callback
-                              &key abort-callback)
+                              &key abort-callback major-mode read)
   "Switch to a new buffer to edit STRING.
-When the user finishes editing (with \\<string-edit-mode-map>\\[string-edit-done]), SUCCESS-CALLBACK
-is called with the resulting string.
+When the user finishes editing (with `string-edit-done'),
+SUCCESS-CALLBACK is called with the resulting string.  If READ is
+non-nil, it is called on the string and its result is passed to
+SUCCESS-CALLBACK instead.  If READ signals an error, the buffer remains
+open, giving the user a chance to correct a typo.
 
-If the user aborts (with \\<string-edit-mode-map>\\[string-edit-abort]), ABORT-CALLBACK (if any) is
+If the user aborts (with `string-edit-abort'), ABORT-CALLBACK (if any) is
 called with no parameters.
 
 PROMPT will be inserted at the start of the buffer, but won't be
 included in the resulting string.  If PROMPT is nil, no help text
 will be inserted.
 
+MAJOR-MODE is called to set up the new buffer; if not passed,
+`string-edit-mode' is used.
+
 Also see `read-string-from-buffer'."
   (with-current-buffer (generate-new-buffer "*edit string*")
     (when prompt
@@ -74,26 +81,39 @@ string-edit
 
     (set-buffer-modified-p nil)
     (setq buffer-undo-list nil)
-    (string-edit-mode)
+    (funcall (or major-mode #'string-edit-mode))
+    (string-edit-minor-mode)
     (setq-local string-edit--success-callback success-callback)
     (setq-local string-edit--abort-callback abort-callback)
+    (setq-local string-edit--read read)
     (setq-local header-line-format
                 (substitute-command-keys
-                 "Type \\<string-edit-mode-map>\\[string-edit-done] when you've finished editing or \\[string-edit-abort] to abort"))
+                 "Type \\<string-edit-minor-mode-map>\\[string-edit-done] when you've finished editing or \\[string-edit-abort] to abort"))
     (message "%s" (substitute-command-keys
-                   "Type \\<string-edit-mode-map>\\[string-edit-done] when you've finished editing"))))
+                   "Type \\<string-edit-minor-mode-map>\\[string-edit-done] when you've finished editing"))))
 
 ;;;###autoload
-(defun read-string-from-buffer (prompt string)
+(cl-defun read-string-from-buffer (prompt string
+                                          &key major-mode
+                                          read)
   "Switch to a new buffer to edit STRING in a recursive edit.
-The user finishes editing with \\<string-edit-mode-map>\\[string-edit-done], or aborts with \\<string-edit-mode-map>\\[string-edit-abort]).
+
+When the user finishes editing with `string-edit-done', this function
+returns that string.  If READ is non-nil, it is called on the string and
+its return value is returned instead.  If READ signals an error, the
+buffer remains open, giving the user a chance to correct a typo.
+
+If the user aborts with `string-edit-abort', this function signals an
+error.
 
 PROMPT will be inserted at the start of the buffer, but won't be
 included in the resulting string.  If nil, no prompt will be
 inserted in the buffer.
 
-When the user exits recursive edit, this function returns the
-edited STRING.
+MAJOR-MODE is called to set up the new buffer; if not passed,
+`string-edit-mode' is used.
+
+When the user finished editing
 
 Also see `string-edit'."
   (string-edit
@@ -104,15 +124,22 @@ read-string-from-buffer
      (exit-recursive-edit))
    :abort-callback (lambda ()
                      (exit-recursive-edit)
-                     (error "Aborted edit")))
+                     (error "Aborted edit"))
+   :major-mode major-mode
+   :read read)
   (recursive-edit)
   string)
 
-(defvar-keymap string-edit-mode-map
+(defvar-keymap string-edit-minor-mode-map
   "C-c C-c" #'string-edit-done
   "C-c C-k" #'string-edit-abort)
 
-(define-derived-mode string-edit-mode text-mode "String"
+(define-minor-mode string-edit-minor-mode
+  "Minor mode for editing strings"
+  :lighter "String"
+  :interactive nil)
+
+(define-derived-mode string-edit-mode text-mode "Text"
   "Mode for editing strings."
   :interactive nil)
 
@@ -120,13 +147,16 @@ string-edit-done
   "Finish editing the string and call the callback function.
 This will kill the current buffer."
   (interactive)
-  (goto-char (point-min))
-  ;; Skip past the help text.
-  (text-property-search-forward 'string-edit--prompt)
-  (let ((string (buffer-substring (point) (point-max)))
-        (callback string-edit--success-callback))
+  (let* ((string
+          (save-excursion
+            (goto-char (point-min))
+            ;; Skip past the help text.
+            (text-property-search-forward 'string-edit--prompt)
+            (buffer-substring (point) (point-max))))
+         (valid (funcall string-edit--read string))
+         (callback string-edit--success-callback))
     (quit-window 'kill)
-    (funcall callback string)))
+    (funcall callback valid)))
 
 (defun string-edit-abort ()
   "Abort editing the current string."
-- 
2.39.3