Subject: Re: bug#77381: [PATCH] extend tex--prettify-symbols-alist

From: "Paul D. Nelson" <[email-masked]>
Date: Tue, 01 Apr 2025 13:59:17 +0200

>> 1. Wrap char-displayable-p in (with-selected-frame ...).
>> 
>> 2. Give char-displayable-p a frame argument, and wrap its body in
>>    (with-selected-frame ...).
>> 
>> 3. Drop the frame argument altogether (leaving (with-selected-frame
>>    ...) to callers of these functions).
>
> I prefer (1), since we cannot change char-displayable-p itself (it's
> used in too many places, including during startup).
>
>> Option (3) seemed simplest to me, and also at the least risk of
>> introducing confusion concerning the name of the new function: at a
>> glance, "char-displayable-on-frame-p" might suggest incorrectly that it
>> does the analogue for the given frame of what char-displayable-p does
>> for the selected frame.
>
> I acknowledge the potential confusion, but I think we can countermand
> that in the doc string (without even mentioning char-displayable-p).

Thanks, I've revised to (1).  Please see attached.

>From edba2f414a7085e203a4e0e52813fa5e5416b97b Mon Sep 17 00:00:00 2001
From: Paul Nelson <[email-masked]>
Date: Mon, 31 Mar 2025 15:37:14 +0200
Subject: [PATCH] Restrict symbol prettification to displayable ones

* lisp/international/mule.el (char-displayable-on-frame-p): New
function used to determine whether a character can be
meaningfully displayed on a given frame.
* doc/lispref/display.texi (Fontsets): Document it.
* lisp/progmodes/prog-mode.el
(prettify-symbols--composition-displayable-p): New function used
to restrict to displayable prettification symbols.  This
prevents issues with missing characters appearing as boxes.
(prettify-symbols--make-keywords): Use it.
---
 doc/lispref/display.texi    |  8 ++++++++
 etc/NEWS                    | 11 +++++++++++
 lisp/international/mule.el  | 26 ++++++++++++++++++++++++++
 lisp/progmodes/prog-mode.el | 31 +++++++++++++++++++++++++++++--
 4 files changed, 74 insertions(+), 2 deletions(-)

diff --git a/doc/lispref/display.texi b/doc/lispref/display.texi
index de0639187ab..0ba3b9d8d05 100644
--- a/doc/lispref/display.texi
+++ b/doc/lispref/display.texi
@@ -4122,6 +4122,14 @@ Fontsets
 terminal can encode the character (@pxref{Terminal I/O Encoding}).
 @end defun
 
+@defun char-displayable-on-frame-p char frame
+This function behaves like @code{char-displayable-p} does (relative to
+@var{frame}), but in the graphical case, it does not perform the final
+check of whether the underlying text terminal can encode the character.
+It thus provides a displayability check for @var{char} more specific to
+@var{frame}.
+@end defun
+
 @node Low-Level Font
 @subsection Low-Level Font Representation
 @cindex font property
diff --git a/etc/NEWS b/etc/NEWS
index 1bd2fd6d486..ed2e93bf467 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -64,6 +64,9 @@ init file.
 
 * Changes in Emacs 31.1
 
+** `prettify-symbols-mode' attempts to ignore undisplayable characters.
+Previously, such characters would be rendered as, e.g., white boxes.
+
 +++
 ** 'standard-display-table' now has more extra slots.
 'standard-display-table' has been extended to allow specifying glyphs
@@ -1713,6 +1716,14 @@ Previously, its argument was always evaluated using dynamic binding.
 
 * Lisp Changes in Emacs 31.1
 
++++
+
++++
+** 'char-displayable-on-frame-p' returns non-nil if Emacs ought to be able to
+display its char argument on a given frame.  This new function, unlike
+'char-displayable-p', does not check whether the character can be
+encoded by the underlying terminal.
+
 +++
 ** New macros 'static-when' and 'static-unless'.
 Like 'static-if', these macros evaluate their condition at
diff --git a/lisp/international/mule.el b/lisp/international/mule.el
index f72cc815287..cb2aa5b9480 100644
--- a/lisp/international/mule.el
+++ b/lisp/international/mule.el
@@ -528,6 +528,32 @@ char-displayable-p
                                    (throw 'tag3 charset)))
 			     charset-list)
 		       nil)))))))))))
+
+(defun char-displayable-on-frame-p (char &optional frame)
+  "Return non-nil if CHAR can be displayed in FRAME.
+FRAME nil means the selected frame.
+
+This function provides a stricter test than `char-displayable-p' does
+for determining if a character will display properly: in the graphical
+case, it does not check whether the underlying terminal can encode the
+character.
+
+Specifically, this function returns non-nil:
+
+- for a text terminal, if `char-displayable-p' returns non-nil.
+
+- for a graphical terminal, if `char-displayable-p' returns either t or
+  a font object.
+
+The two functions differ in behavior (i.e., `char-displayable-strict-p'
+returns nil but `char-displayable-p' does not) if the underlying
+terminal is graphical and can encode the character, but FRAME cannot."
+  (let ((display-capability (with-selected-frame (or frame (selected-frame))
+                              (char-displayable-p char))))
+    (if (display-graphic-p frame)
+        (or (eq display-capability t)
+            (fontp display-capability))
+      display-capability)))
 
 ;; Save the ASCII case table in case we need it later.  Some locales
 ;; (such as Turkish) modify the case behavior of ASCII characters,
diff --git a/lisp/progmodes/prog-mode.el b/lisp/progmodes/prog-mode.el
index 77724e04f11..33e0d354fee 100644
--- a/lisp/progmodes/prog-mode.el
+++ b/lisp/progmodes/prog-mode.el
@@ -230,10 +230,37 @@ prettify-symbols--compose-symbol
   ;; Return nil because we're not adding any face property.
   nil)
 
+(defun prettify-symbols--composition-displayable-p (composition)
+  "Return non-nil if COMPOSITION can be displayed with the current fonts.
+COMPOSITION can be a single character, a string, or a sequence (vector or
+list) of characters and composition rules as described in the documentation
+of `prettify-symbols-alist' and `compose-region'."
+  (cond
+   ((characterp composition)
+    (char-displayable-on-frame-p composition))
+   ((stringp composition)
+    (seq-every-p #'char-displayable-on-frame-p composition))
+   ((seqp composition)
+    ;; check that every even-indexed element is displayable
+    (seq-every-p
+     (lambda (idx-elt)
+       (if (evenp (car idx-elt))
+           (char-displayable-on-frame-p (cdr idx-elt))
+         t))
+     (seq-map-indexed #'cons composition)))
+   (t
+    ;; silently ignore invalid compositions
+    t)))
+
 (defun prettify-symbols--make-keywords ()
   (if prettify-symbols-alist
-      `((,(regexp-opt (mapcar 'car prettify-symbols-alist) t)
-         (0 (prettify-symbols--compose-symbol ',prettify-symbols-alist))))
+      (let ((filtered-alist
+             (seq-filter
+              (lambda (elt)
+                (prettify-symbols--composition-displayable-p (cdr elt)))
+              prettify-symbols-alist)))
+        `((,(regexp-opt (mapcar 'car filtered-alist) t)
+           (0 (prettify-symbols--compose-symbol ',filtered-alist)))))
     nil))
 
 (defvar-local prettify-symbols--keywords nil)
-- 
2.39.3 (Apple Git-145)