Subject: Re: bug#78586: TeX-make-inline

From: Ikumi Keita <[email-masked]>
Date: Tue, 03 Jun 2025 04:24:27 +0900

Hi Paul,

>>>>> "Paul D. Nelson" <[email-masked]> writes:
> The next planned command was an "inverse" to LaTeX-make-inline, say
> LaTeX-make-display, that converts inline math to display math.  I had
> planned to propose that command in a separate bug, but the two are so
> intertwined that I think it makes sense to treat them together.

> The tricky part in designing LaTeX-make-display is that different users
> may prefer different sorts of display math: \[..\], $$..$$,
> equation/equation*/align/align*/(...).  Given that we don't want to add
> too many new user options, it seemed best to provide a general
> LaTeX-modify-math command, which the user can either invoke
> interactively or specialize like so:
> (defun my-LaTeX-make-brackets ()

[...]

> How does this plan sound?  The attached patch contains everything but
> documentation concerning LaTeX-modify-math, for which I await feedback
> on a location in the manual and the overall soundness of the approach.

It seems interesting to me. I remember Uwe Brauer once requested such
feature[1].

[1] https://lists.gnu.org/r/auctex-devel/2022-01/msg00010.html

Here are my comments about your proposal. (I haven't acutually run the
code, so I might be saying something very dumb, sorry):

+(defun LaTeX--modify-math-1 (open close inline new-open new-close new-inline pos)
+  "Helper function for `LaTeX-modify-math'.
+OPEN and CLOSE are the current delimiters, NEW-OPEN and NEW-CLOSE are
+the new delimiters.  INLINE and NEW-INLINE are booleans indicating
+whether the current and new delimiters are inline or display math.
+Assume point is at the start of the current OPEN delimiter.  POS is a
+marker that keeps track of cursor position."
+  (let ((converting-to-inline (and (not inline) new-inline)))
+    (when converting-to-inline
+      ;; Join with previous line if non-blank.
+      (when (and (looking-back "\n[[:blank:]]*" (point-min)) ... (1)
+                 (> (line-beginning-position) (point-min))   ... (2)
+                 (save-excursion
+                   (forward-line -1)
+                   (not (looking-at "^[[:blank:]]*$"))))

I'd write the conditional for the second `when' as
(save-excursion
  (skip-chars-backward "[:blank:]")
  (and 
    (bolp) (not (bobp))
    (progn
      (forward-char -1)
      (skip-chars-backward "[:blank:]")
      (not (bolp)))))
because `looking-back' isn't efficient. However, this piece of code
isn't as easy to read as yours, so you don't have to mind if you decide
to reject it.

Anyway, it seems to me that the condition (2) isn't needed because it is
always satisfied when the condition (1) is met, if I don't miss something.

+        (forward-char (length open))
+        (save-excursion (join-line))
+        (forward-char (- (length open)))))
+    (unless new-inline
+      ;; Ensure non-inline delimiters start on a blank line.
+      (unless (looking-back "\n[[:blank:]]*" (point-min))
+        (delete-horizontal-space)
+        (insert "\n")))
+    ;; Delete opening delimiter.
+    (delete-char (length open))
+    (let ((start (point)))
+      (search-forward close)
+      (when converting-to-inline
+        ;; Join with next line if non-blank.
+        (when (and (looking-at-p "[[:blank:]]*\n")
+                   (< (line-end-position) (point-max))  ... (3)
+                   (save-excursion
+                     (forward-line 1)
+                     (not (looking-at-p "^[[:blank:]]*$"))))

Again, the condition (3) seems dispensable.

+          (join-line 'next)))
+      (unless new-inline
+        (unless (looking-at-p "[[:blank:]]*\n")
+          (save-excursion
+            (insert "\n"))))
+      ;; Delete closing delimiter.
+      (delete-char (- (length close)))
+      (save-restriction
+        (narrow-to-region start (point))
+        ;; Clear labels.
+        (goto-char (point-min))
+        (let ((re (concat
+                   "\\(?:"
+                   (if (bound-and-true-p reftex-label-regexps)
+                       (mapconcat #'identity reftex-label-regexps "\\|")
+                     (format "%slabel%s%s%s"
+                             (regexp-quote TeX-esc)
+                             TeX-grop "[^}]*" TeX-grcl))
+                   "\\)")))
+          (while (re-search-forward re nil t)
+            (replace-match "")))
+        ;; Delete leading and trailing whitespace.

Is it really necessary to delete leading whitespaces? At the end of this
function, we do `indent-region', which I expect would do the job. (And
then, we can just call `delete-trailing-whitespace'.)

+        (dolist (re '("\\`[ \t\n\r]+" "[ \t\n\r]+\\'"))
+          (goto-char (point-min))
+          (when (re-search-forward re nil t)
+            (replace-match "")))

+        (unless new-inline
+          (goto-char (point-min))
+          (insert "\n")
+          (goto-char (point-max))
+          (insert "\n"))
+        ;; Insert new opening delimiter.
+        (goto-char (point-min))
+        (insert new-open)
+        ;; Insert new closing delimiter
+        (goto-char (point-max))
+        (when (eq (point) (marker-position pos))

We can simplify the conditional as
(= (point) pos)

+          (set-marker-insertion-type pos (not 'advance)))
+        (when converting-to-inline
+          ;; Leave punctuation outside.
+          (while (looking-back "[.,;:!?]"
+                               (max (point-min) (- (point) 5)))
+            (backward-char)))

We can use `skip-chars-backward' instead of the `while' loop.

+        (insert new-close)
+        ;; Indent, including one line past the modified region.
+        (widen)
+        (end-of-line 2)
+        (indent-region start (point))))))
+
+(defun LaTeX--closing (type)
+  "Return closing delimiter corresponding to given `texmathp' TYPE.
+TYPE must be one of the (La)TeX symbols $, $$, \\( or \\=\\[, or a valid
+environment name.  Macros such as \\ensuremath are not supported."
+  (pcase type
+    ((or "$" "$$") type)
+    ("\\[" "\\]")
+    ("\\(" "\\)")
+    (_ (unless (assoc type (LaTeX-environment-list-filtered))
+         (error "Invalid or unsupported opening delimiter: %s" type))
+       (concat TeX-esc "end" TeX-grop type TeX-grcl))))
+
+(defun LaTeX-modify-math (&optional new-type)

Why do you make the `new-type' argument optional? It is always non-nil
when called interactively, and must be non-nil when called in program
according to the following doc string.

+  "Modify the current math construct to NEW-TYPE.
+
+Interactively, prompt for NEW-TYPE from a list of inline math
+delimiters (\"$\", \"\\(\"), display math delimiters (\"$$\",
+\"\\=\\[\") and valid LaTeX environments (\"equation\", ...).
+
+Non-interactively, NEW-TYPE must be either
+- a string specifying the target delimiter or environment name, or
+- a cons cell ((OPEN . CLOSE) . INLINE), where OPEN and CLOSE are
+  delimiters and INLINE is non-nil if the math construct is to be
+  understood as inline.
+
+The function converts the math construct at point (inline, display, or
+environment) to the specified NEW-TYPE, preserving the content.  If
+point is not in a math construct, signal an error.  Clears any active
+previews at point before modification.
+
+Does not support modifying macro-based constructs such as \\ensuremath."
+  (interactive
+   (let* ((type (progn (texmathp) (car texmathp-why)))
+          (tbl (append '("$" "\\(" "$$" "\\[")
+                       (LaTeX-environment-list-filtered))))
+     (unless type (user-error "Not inside math"))
+     (LaTeX--closing type) ;; Check for errors.
+     (list (completing-read
+            (format "Convert %s → " type) tbl nil t nil nil
+            type))))
+  (let ((new-open (if (stringp new-type)
+                      new-type
+                    (caar new-type)))
+        (new-close (if (stringp new-type)
+                       (LaTeX--closing new-type)
+                     (cdar new-type)))
+        (new-inline (if (stringp new-type)
+                        (member new-type '("$" "\\("))
+                      (cdr new-type))))
+    (when (fboundp 'preview-clearout-at-point)
+      (preview-clearout-at-point))
+    (unless (called-interactively-p 'any)
+      (unless (texmathp) (error "Not inside math")))
+    (let ((type (car texmathp-why))
+          (math-start (cdr texmathp-why))
+          (pos (point-marker)))
+      (set-marker-insertion-type pos
+                                 (not
+                                  (and
+                                   (< (point) (point-max))
+                                   (save-excursion
+                                     (forward-char)
+                                     (not (texmathp))))))
+      (goto-char math-start)
+      (let* ((open (if (member type '("\\(" "$" "\\[" "$$"))
+                       type
+                     (concat TeX-esc "begin" TeX-grop type TeX-grcl)))
+             (close (LaTeX--closing type)))
+        (if (or (not (stringp new-type))
+                (member new-open '("$" "\\(" "\\[" "$$")))
+            ;; Conversion to inline or non-environment display.
+            (let* ((inline (member type '("$" "\\("))))
+              (LaTeX--modify-math-1 open close inline new-open new-close new-inline pos))
+          ;; Conversion to an environment.
+          (if (member type '("$" "\\(" "$$" "\\["))
+              (delete-char (length type))
+            (kill-line))
+          (push-mark (save-excursion
+                       (search-forward close)
+                       (delete-region (match-beginning 0) (match-end 0))
+                       (when (eq (point) (marker-position pos))

Again, we can do
(= (point) pos)
here.

+                         (setq pos nil))

I recommend to do
(set-marker pos nil)
before throwing away a temporal marker. See (elisp) Overview of Markers:
,----
| Insertion and deletion in a buffer must check all the markers and
| relocate them if necessary.  This slows processing in a buffer with a
| large number of markers.  For this reason, it is a good idea to make a
| marker point nowhere if you are sure you don't need it any more.
| Markers that can no longer be accessed are eventually removed (*note
| Garbage Collection::).
`----

+                       (when (member type '("$" "\\("))
+                         (while (looking-at-p "[.,;:!?]")
+                           (forward-char)))

Again, we can use `skip-chars-forward' here.

+                       (point)))
+          (activate-mark)
+          (LaTeX-insert-environment new-type)))
+      (when pos
+        (goto-char pos)))))

Again, I recommend to do
(set-marker pos nil)
when a temporal marker finishes its job.

+
+(defun LaTeX-make-inline ()
+  "Convert LaTeX display math construct at point to inline math.
+Remove the enclosing math construct (such as \\=\\[...\\=\\] or
+\\begin{equation}...\\end{equation}) and replace it with inline math
+surrounded by `TeX-electric-math' if non-nil, or \"$...$\", fitting the
+result onto one line.  Finally, leave any trailing punctuation outside
+the math delimiters."
+  (interactive)

How about
(interactive "*")
instead?
(Maybe we should consider to add
(barf-if-buffer-read-only)
in `LaTeX-modify-math' as well.)

+  (LaTeX-modify-math
+   (if TeX-electric-math
+       (cons TeX-electric-math 'inline)
+     "$")))
+

Regards,
Ikumi Keita
#StandWithUkraine #StopWarInUkraine
#Gaza #StopMassiveKilling #CeasefireNOW