Subject: bug#78693: 14.0.9; Folding of math macros with a function spec is broken

From: "Paul D. Nelson" <[email-masked]>
Date: Sun, 15 Jun 2025 12:29:24 +0200

Hi Raghuzar and all,

Please see the attached patch which implements signature support for
macro folds, following the plan outlined in my earlier email.  This
should address both the issue in your report and that in my earlier bug.
We also need to update the manual, but I'll do that later once the
general shape of the proposal is agreed upon.  If you get the chance,
please let me know what you think.

Thanks, best,

Paul

>From 2e07366d1d5856f615c8bf7bc36fbbcdf5350055 Mon Sep 17 00:00:00 2001
From: Paul Nelson <[email-masked]>
Date: Sun, 15 Jun 2025 12:24:11 +0200
Subject: [PATCH] Add signature support for macro folding

Support signature restrictions to limit the number of arguments
considered when folding LaTeX macros (bug#78693).

* tex.el (TeX-find-macro-boundaries, TeX-find-macro-end)
(TeX-find-macro-end-helper): Add optional SIGNATURE argument to
restrict allowed arguments.

* tex-fold.el (TeX-fold-macro-spec-list): Update docstring and
type specification to document new signature format.
(TeX-fold-region-macro-or-env, TeX-fold-item): Extract signature
from spec and pass to 'TeX-fold-item-end'.
(TeX-fold-item-end): Add optional SIGNATURE parameter.  Remove
special handling for math type, reverting d0a57d8d and
delegating instead to 'TeX-find-macro-end'.

* latex.el (LaTeX-fold-math-spec-list): Add "signature 0" to
each entry, reflecting that it is a macro with no arguments.
---
 latex.el    |   2 +-
 tex-fold.el | 108 +++++++++++++++++++++++++++++++---------------------
 tex.el      | 108 +++++++++++++++++++++++++++++++---------------------
 3 files changed, 130 insertions(+), 88 deletions(-)

diff --git a/latex.el b/latex.el
index 5bff638b..94666fe8 100644
--- a/latex.el
+++ b/latex.el
@@ -6729,7 +6729,7 @@ char."
                                                             "Accents")))
                                   submenu)))
                       (when (and (stringp tex-token) (integerp uchar) noargp)
-                        `(,(char-to-string uchar) (,tex-token)))))
+                        `((,(char-to-string uchar) . 0) (,tex-token)))))
                   `((nil "to" "" 8594)
                     (nil "gets" "" 8592)
                     ,@LaTeX-math-default)))
diff --git a/tex-fold.el b/tex-fold.el
index 78e5b822..d980ac1b 100644
--- a/tex-fold.el
+++ b/tex-fold.el
@@ -95,38 +95,55 @@ macros, `math' for math macros and `comment' for comments."
         "textbf" "textsc" "textup")))
   "List of replacement specifiers and macros to fold.
 
-The first element of each item can be a string, an integer or a
-function symbol.  The second element is a list of macros to fold
-without the leading backslash.
-
-If the first element is a string, it will be used as a display
-replacement for the whole macro.  Numbers in braces, brackets,
-parens or angle brackets will be replaced by the respective macro
-argument.  For example \"{1}\" will be replaced by the first
-mandatory argument of the macro.  One can also define
-alternatives within the specifier which are used if an argument
-is not found.  Alternatives are separated by \"||\".  They are
-most useful with optional arguments.  As an example, the default
-specifier for \\item is \"[1]:||*\" which means that if there is
-an optional argument, its value is shown followed by a colon.  If
-there is no optional argument, only an asterisk is used as the
-display string.
-
-If the first element is an integer, the macro will be replaced by
-the respective macro argument.
-
-If the first element is a function symbol, the function will be
-called with all mandatory arguments of the macro and the result
-of the function call will be used as a replacement for the macro.
-Such functions typically return a string, but may also return the
-symbol `abort' to indicate that the macro should not be folded.
+The first element is of the form SPEC or (SPEC . SIG), where SPEC can be
+a string, an integer or a function symbol and SIG is described below.
+The second element is a list of macros to fold without the leading
+backslash.
+
+If SPEC is a string, it will be used as a display replacement for the
+whole macro.  Numbers in braces, brackets, parens or angle brackets will
+be replaced by the respective macro argument.  For example \"{1}\" will
+be replaced by the first mandatory argument of the macro.  One can also
+define alternatives within the specifier which are used if an argument
+is not found.  Alternatives are separated by \"||\".  They are most
+useful with optional arguments.  As an example, the default specifier
+for \\item is \"[1]:||*\" which means that if there is an optional
+argument, its value is shown followed by a colon.  If there is no
+optional argument, only an asterisk is used as the display string.
+
+If SPEC is an integer, the macro will be replaced by the respective
+macro argument.
+
+If SPEC is a function symbol, the function will be called with all
+mandatory arguments of the macro and the result of the function call
+will be used as a replacement for the macro.  Such functions typically
+return a string, but may also return the symbol `abort' to indicate that
+the macro should not be folded.
+
+SIG optionally restricts the number of arguments that follow the macro.
+It can be either nil, an integer, or a cons cell consisting of integers.
+If it is nil, then there is no restriction.  If it is an integer n, then
+at most n total arguments are allowed.  If it is a cons cell (p . q),
+then at most p optional and q required arguments are allowed.
 
 Setting this variable does not take effect immediately.  Use
 Customize or reset the mode."
-  :type '(repeat (group (choice (string :tag "Display String")
-                                (integer :tag "Number of argument" :value 1)
-                                (function :tag "Function to execute"))
-                        (repeat :tag "Macros" (string))))
+  :type '(repeat
+          (group
+           (choice
+            (string :tag "Display String")
+            (integer :tag "Number of argument" :value 1)
+            (function :tag "Function to execute")
+            (cons :tag "Spec with signature"
+                  (choice (string :tag "Display String")
+                          (integer :tag "Number of argument" :value 1)
+                          (function :tag "Function to execute"))
+                  (choice (const :tag "No restriction" nil)
+                          (integer :tag "Max total arguments")
+                          (cons :tag "Max optional and required"
+                                (integer :tag "Max optional arguments")
+                                (integer :tag "Max required arguments")))))
+           (repeat :tag "Macros" (string))))
   :package-version '(auctex . "14.0.8"))
 
 (defvar-local TeX-fold-macro-spec-list-internal nil
@@ -437,9 +454,13 @@ for macros and `math' for math macros."
                                                (string (char-after
                                                         (match-end 0)))))))
                 (let* ((item-start (match-beginning 0))
-                       (display-string-spec (cadr (assoc item-name
-                                                         fold-list)))
-                       (item-end (TeX-fold-item-end item-start type))
+                       (spec-maybe-sig (cadr (assoc item-name fold-list)))
+                       (display-string-spec (if (consp spec-maybe-sig)
+                                                (car spec-maybe-sig)
+                                              spec-maybe-sig))
+                       (sig (when (consp spec-maybe-sig)
+                              (cdr spec-maybe-sig)))
+                       (item-end (TeX-fold-item-end item-start type sig))
                        (ov (TeX-fold-make-overlay item-start item-end type
                                                   display-string-spec)))
                   (TeX-fold-hide-item ov))))))))))
@@ -536,7 +557,7 @@ Return non-nil if an item was found and folded, nil otherwise."
                                  TeX-fold-math-spec-list-internal)
                                 (t TeX-fold-macro-spec-list-internal)))
                fold-item
-               (display-string-spec
+               (spec-maybe-sig
                 (or (catch 'found
                       (while fold-list
                         (setq fold-item (car fold-list))
@@ -549,7 +570,12 @@ Return non-nil if an item was found and folded, nil otherwise."
                       (if (eq type 'env)
                           TeX-fold-unspec-env-display-string
                         TeX-fold-unspec-macro-display-string))))
-               (item-end (TeX-fold-item-end item-start type))
+               (display-string-spec (if (consp spec-maybe-sig)
+                                        (car spec-maybe-sig)
+                                      spec-maybe-sig))
+               (sig (when (consp spec-maybe-sig)
+                      (cdr spec-maybe-sig)))
+               (item-end (TeX-fold-item-end item-start type sig))
                (ov (TeX-fold-make-overlay item-start item-end type
                                           display-string-spec)))
           (TeX-fold-hide-item ov))))))
@@ -882,10 +908,12 @@ display property."
       (overlay-put ov 'display display-string))
     ov))
 
-(defun TeX-fold-item-end (start type)
+(defun TeX-fold-item-end (start type &optional signature)
   "Return the end of an item of type TYPE starting at START.
 TYPE can be either `env' for environments, `macro' for macros or
-`math' for math macros."
+`math' for math macros.
+Optional SIGNATURE, as in `TeX-find-macro-end', restricts the number of
+allowed arguments of LaTeX macros."
   (save-excursion
     (cond ((and (eq type 'env)
                 (eq major-mode 'ConTeXt-mode))
@@ -901,15 +929,9 @@ TYPE can be either `env' for environments, `macro' for macros or
            (goto-char (1+ start))
            (LaTeX-find-matching-end)
            (point))
-          ((eq type 'math)
-           (goto-char (1+ start))
-           (if (zerop (skip-chars-forward "A-Za-z@"))
-               (forward-char)
-             (skip-chars-forward "*"))
-           (point))
           (t
            (goto-char start)
-           (TeX-find-macro-end)))))
+           (TeX-find-macro-end signature)))))
 
 (defun TeX-fold-overfull-p (ov-start ov-end display-string)
   "Return t if an overfull line will result after adding an overlay.
diff --git a/tex.el b/tex.el
index 6f8267ac..74572d4e 100644
--- a/tex.el
+++ b/tex.el
@@ -5790,11 +5790,16 @@ If LIMIT is non-nil, do not search further up than this position
 in the buffer."
   (TeX-find-balanced-brace -1 depth limit))
 
-(defun TeX-find-macro-boundaries (&optional lower-bound)
+(defun TeX-find-macro-boundaries (&optional lower-bound signature)
   "Return a cons containing the start and end of a macro.
 If LOWER-BOUND is given, do not search backward further than this
 point in buffer.  Arguments enclosed in brackets or braces are
-considered part of the macro."
+considered part of the macro.
+
+If SIGNATURE is given, restrict the total number of arguments.  If
+SIGNATURE is an integer N, allow at most N total arguments.  If
+SIGNATURE is a cons cell (P . Q), allow at most P optional and Q
+required arguments."
   ;; FIXME: Pay attention to `texmathp-allow-detached-args' and
   ;; `reftex-allow-detached-macro-args'.
   ;; Should we handle cases like \"{o} and \\[3mm] (that is, a macro
@@ -5839,16 +5844,17 @@ considered part of the macro."
       ;; Search forward for the end of the macro.
       (when start-point
         (save-excursion
-          (goto-char (TeX-find-macro-end-helper start-point))
+          (goto-char (TeX-find-macro-end-helper start-point signature))
           (if (< orig-point (point))
               (cons start-point (point))
             nil))))))
 
-(defun TeX-find-macro-end-helper (start)
+(defun TeX-find-macro-end-helper (start &optional signature)
   "Find the end of a macro given its START.
 START is the position just before the starting token of the macro.
 If the macro is followed by square brackets or curly braces,
-those will be considered part of it."
+those will be considered part of it.  SIGNATURE, as in
+`TeX-find-macro-boundaries', restricts how many arguments are allowed."
   (save-excursion
     (save-match-data
       (catch 'found
@@ -5856,43 +5862,56 @@ those will be considered part of it."
         (if (zerop (skip-chars-forward "A-Za-z@"))
             (forward-char)
           (skip-chars-forward "*"))
-        (while (not (eobp))
-          (cond
-           ;; Skip over pairs of square brackets
-           ((or (looking-at "[ \t]*\n?[ \t]*\\(\\[\\)") ; Be conservative: Consider
+        (let* ((max-tot (and (integerp signature) signature))
+               (max-opt (and (consp signature) (car signature)))
+               (max-req (and (consp signature) (cdr signature)))
+               (num-opt 0)
+               (num-req 0))
+          (while (not (eobp))
+            (cond
+             ;; Skip over pairs of square brackets
+             ((or (looking-at "[ \t]*\n?[ \t]*\\(\\[\\)") ; Be conservative: Consider
                                         ; only consecutive lines.
-                (and (looking-at (concat "[ \t]*" TeX-comment-start-regexp))
-                     (save-excursion
-                       (forward-line 1)
-                       (looking-at "[ \t]*\\(\\[\\)"))))
-            (goto-char (match-beginning 1))
-            ;; Imitate `font-latex-find-matching-close', motivated by
-            ;; examples like \begin{enumerate}[a{]}].
-            (let ((syntax (TeX-search-syntax-table ?\[ ?\]))
-                  (parse-sexp-ignore-comments
-                   (not (derived-mode-p 'docTeX-mode))))
-              (modify-syntax-entry ?\{ "|" syntax)
-              (modify-syntax-entry ?\} "|" syntax)
-              (modify-syntax-entry ?\\ "/" syntax)
-              (condition-case nil
-                  (with-syntax-table syntax
-                    (forward-sexp))
-                (scan-error (throw 'found (point))))))
-           ;; Skip over pairs of curly braces
-           ((or (looking-at "[ \t]*\n?[ \t]*{") ; Be conservative: Consider
+                  (and (looking-at (concat "[ \t]*" TeX-comment-start-regexp))
+                       (save-excursion
+                         (forward-line 1)
+                         (looking-at "[ \t]*\\(\\[\\)"))))
+              (if (or (and max-tot (>= (+ num-opt num-req) max-tot))
+                      (and max-opt (>= num-opt max-opt)))
+                  (throw 'found (point)))
+              (cl-incf num-opt)
+              (goto-char (match-beginning 1))
+              ;; Imitate `font-latex-find-matching-close', motivated by
+              ;; examples like \begin{enumerate}[a{]}].
+              (let ((syntax (TeX-search-syntax-table ?\[ ?\]))
+                    (parse-sexp-ignore-comments
+                     (not (derived-mode-p 'docTeX-mode))))
+                (modify-syntax-entry ?\{ "|" syntax)
+                (modify-syntax-entry ?\} "|" syntax)
+                (modify-syntax-entry ?\\ "/" syntax)
+                (condition-case nil
+                    (with-syntax-table syntax
+                      (forward-sexp))
+                  (scan-error (throw 'found (point))))))
+             ;; Skip over pairs of curly braces
+             ((or (looking-at "[ \t]*\n?[ \t]*{") ; Be conservative: Consider
                                         ; only consecutive lines.
-                (and (looking-at (concat "[ \t]*" TeX-comment-start-regexp))
-                     (save-excursion
-                       (forward-line 1)
-                       (looking-at "[ \t]*{"))))
-            (goto-char (match-end 0))
-            (goto-char (or (TeX-find-closing-brace)
-                           ;; If we cannot find a regular end, use the
-                           ;; next whitespace.
-                           (save-excursion (skip-chars-forward "^ \t\n")
-                                           (point)))))
-           (t
-            (throw 'found (point)))))
+                  (and (looking-at (concat "[ \t]*" TeX-comment-start-regexp))
+                       (save-excursion
+                         (forward-line 1)
+                         (looking-at "[ \t]*{"))))
+              (if (or (and max-tot (>= (+ num-opt num-req) max-tot))
+                      (and max-req (>= num-req max-req)))
+                  (throw 'found (match-beginning 0)))
+              (cl-incf num-req)
+              (goto-char (match-end 0))
+              (goto-char (or (TeX-find-closing-brace)
+                             ;; If we cannot find a regular end, use the
+                             ;; next whitespace.
+                             (save-excursion (skip-chars-forward "^ \t\n")
+                                             (point)))))
+             (t
+              (throw 'found (point))))))
         ;; Make sure that this function does not return nil, even
         ;; when the above `while' loop is totally skipped. (bug#35638)
         (throw 'found (point))))))
@@ -5904,11 +5923,12 @@ in buffer.  Arguments enclosed in brackets or braces are
 considered part of the macro."
   (car (TeX-find-macro-boundaries limit)))
 
-(defun TeX-find-macro-end ()
+(defun TeX-find-macro-end (&optional signature)
   "Return the end of a macro.
-Arguments enclosed in brackets or braces are considered part of
-the macro."
-  (cdr (TeX-find-macro-boundaries)))
+Arguments enclosed in brackets or braces are considered part of the
+macro.  SIGNATURE, as in `TeX-find-macro-boundaries', restricts how many
+arguments are allowed."
+  (cdr (TeX-find-macro-boundaries nil signature)))
 
 (defun TeX-search-forward-unescaped (string &optional bound noerror)
   "Search forward from point for unescaped STRING.
-- 
2.39.3 (Apple Git-145)