Subject: bug#15925: 24.3.50; error when customizing whitespace-display-mappings

From: Mauro Aranda <[email-masked]>
Date: Fri, 25 Sep 2020 11:32:41 -0300

How does the attached patch look? I didn't include changes for the rest
of the escape sequences, because of Eli's objections.

The reason I used the notify function is because it is much simpler than
rebinding insertion commands and having to copy some of the
functionality.  And I don't think the change in the calls to the notify
function for editable-field widgets (and descendants) will be much of a
problem.  I looked for functions that assume EVENT is nil and found
none, nor in-tree or in third-party packages.
From e5ccdb868b71667797dbf7399e9247503d71b809 Mon Sep 17 00:00:00 2001
From: Mauro Aranda <[email-masked]>
Date: Fri, 25 Sep 2020 11:16:14 -0300
Subject: [PATCH] Display some character widget values in a more user-friendly
 way (Bug#15925)

* lisp/wid-edit.el (widget-character--escape-sequences-alist): New
variable.
(widget-character--change-character-display): New function.  Use the new
variable.
(widget-character-notify): New function, to keep track of the changes
in the character widget, and display characters like tab,
newline and spaces better.
(character widget): Use widget-character-notify as the notify
function.  Use widget-character--change-character-display for the
internal representation of value.
---
 lisp/wid-edit.el | 66 +++++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 62 insertions(+), 4 deletions(-)

diff --git a/lisp/wid-edit.el b/lisp/wid-edit.el
index 8ad99f49aa..509c487bf2 100644
--- a/lisp/wid-edit.el
+++ b/lisp/wid-edit.el
@@ -1370,7 +1370,8 @@ widget-before-change
 	     (signal 'text-read-only
 		     '("Attempt to change text outside editable field")))
 	    (widget-field-use-before-change
-	     (widget-apply from-field :notify from-field))))))
+	     (widget-apply from-field :notify
+                           from-field (list 'before-change from to)))))))
 
 (defun widget-add-change ()
   (remove-hook 'post-command-hook 'widget-add-change t)
@@ -1407,7 +1408,7 @@ widget-after-change
 				 (> (point) begin))
 		       (delete-char -1)))))))
 	(widget-specify-secret field))
-      (widget-apply field :notify field))))
+      (widget-apply field :notify field (list 'after-change from to)))))
 
 ;;; Widget Functions
 ;;
@@ -3533,13 +3534,70 @@ 'character
   :value-to-internal (lambda (_widget value)
 		       (if (stringp value)
 			   value
-			 (char-to-string value)))
+                         (let ((disp
+                                (widget-character--change-character-display
+                                 value)))
+                           (if disp
+                               (propertize (char-to-string value) 'display disp)
+                             (char-to-string value)))))
   :value-to-external (lambda (_widget value)
 		       (if (stringp value)
 			   (aref value 0)
 			 value))
   :match (lambda (_widget value)
-	   (characterp value)))
+	   (characterp value))
+  :notify #'widget-character-notify)
+
+;; Only some escape sequences, not all of them.  (Bug#15925)
+(defvar widget-character--escape-sequences-alist
+  '((?\t . ?t)
+    (?\n . ?n)
+    (?\s . ?s))
+  "Alist that associates escape sequences to a character.
+Each element has the form (ESCAPE-SEQUENCE . CHARACTER).
+
+The character widget uses this alist to display the
+non-printable character represented by ESCAPE-SEQUENCE as \\CHARACTER,
+since that makes it easier to see what's in the widget.")
+
+(defun widget-character--change-character-display (c)
+  "Return a string to represent the character C, or nil.
+
+The character widget represents some characters (e.g., the newline character
+or the tab character) specially, to make it easier for the user to see what's
+in it.  For those characters, return a string to display that character in a
+more user-friendly way.
+
+For the caller, nil should mean that it is good enough to use the return value
+of `char-to-string' for the representation of C."
+  (let ((char (alist-get c widget-character--escape-sequences-alist)))
+    (and char (propertize (format "\\%c" char) 'face 'escape-glyph))))
+
+(defun widget-character-notify (widget child &optional event)
+  "Notify function for the character widget.
+
+This function allows the widget character to better display some characters,
+like the newline character or the tab character."
+  (when (eq (car-safe event) 'after-change)
+    (let* ((start (nth 1 event))
+           (end (nth 2 event))
+           str)
+      (if (eql start end)
+          (when (char-equal (widget-value widget) ?\s)
+            ;; The character widget is not really empty:
+            ;; its value is a single space character.
+            ;; We need to propertize it again, if it became empty for a while.
+            (let ((ov (widget-get widget :field-overlay)))
+              (put-text-property
+               (overlay-start ov) (overlay-end ov)
+               'display (widget-character--change-character-display ?\s))))
+        (setq str (buffer-substring-no-properties start end))
+        ;; This assumes the user enters one character at a time,
+        ;; and does nothing crazy, like yanking a long string.
+        (let ((disp (widget-character--change-character-display (aref str 0))))
+          (when disp
+            (put-text-property start end 'display disp))))))
+  (widget-default-notify widget child event))
 
 (define-widget 'list 'group
   "A Lisp list."
-- 
2.28.0