Hi Ikumi,
> + (forward-char (length open))
> + (save-excursion (join-line))
> + (forward-char (- (length open)))))
>
> Why do you go forth and back here? What's wrong with a bare `join-line'
> without two `forward-char's?
Suppose the buffer contains:
--8<---------------cut here---------------start------------->8---
We have
\begin{equation*}
x + y = z.
\end{equation*}
--8<---------------cut here---------------end--------------->8---
With point on line 2, join-line yields a buffer with first line:
--8<---------------cut here---------------start------------->8---
We have \begin{equation*}
--8<---------------cut here---------------end--------------->8---
The motivation for the slightly convoluted code is that, when point is
at beginning of line, (save-excursion (join-line)) places point just
before " \begin" (rather than just before "\begin", as one might have
expected). On the other hand, it behaves in the expected way if point
occurs later in the line (e.g., after "}"). Rather than trying to
understand exactly why join-line behaves this way (which seems like a
potentially non-robust feature that could change in the future), it
seemed simpler just to do the little dance that you see in the code.
I've added a comment to the code summarizing the above.
> We have to keep
> (setq pos nil)
> after `set-marker', which doesn't change the value of `pos' itself.
Thanks, good catch.
> As far as I can see, the proposed feature doesn't support docTeX mode. I
> don't think that is a practical problem at all, but addition of FIXME
> comment and breif mention about it in the documentation would be nice.
I'll trust you on this one -- I'm regrettably ignorant of TeX outside a
limited practical subset of LaTeX. I've added some FIXME comments,
hopefully as intended.
There remains the question of where in the manual to document
LaTeX-modify-math and whether to illustrate it via examples like in my
earlier email.
Thanks, best,
Paul
>From af62d287b5f45fedbc963ad4231ef736045b38b4 Mon Sep 17 00:00:00 2001
From: Paul Nelson <[email protected]>
Date: Thu, 29 May 2025 11:14:11 +0200
Subject: [PATCH] Add LaTeX-modify-math and LaTeX-make-inline
* latex.el (LaTeX--strip-labels, LaTeX--modify-math-1)
(LaTeX--closing, LaTeX--math-environment-list): New helper
functions.
(LaTeX-modify-math, LaTeX-make-inline): New commands.
* tests/latex/latex-modify-math-test.el: New test file.
(latex-make-inline-test--with-temp-buffer): New test macro.
(LaTeX-modify-math-inline-bracket-period)
(LaTeX-modify-math-inline-double-dollar)
(LaTeX-modify-math-inline-electric-math)
(LaTeX-modify-math-inline-equation-env)
(LaTeX-modify-math-inline-noop)
(LaTeX-modify-math-inline-paren-to-dollar)
(LaTeX-modify-math-inline-multiline-equation)
(LaTeX-modify-math-inline-punctuation-semicolon)
(LaTeX-modify-math-inline-multiple-punctuation)
(LaTeX-modify-math-inline-whitespace-preservation)
(LaTeX-modify-math-inline-empty-lines)
(LaTeX-modify-math-dollar-to-bracket)
(LaTeX-modify-math-paren-to-double-dollar)
(LaTeX-modify-math-bracket-to-equation)
(LaTeX-modify-math-point-inline-to-display-after-content)
(LaTeX-modify-math-point-inline-to-display-before-content)
(LaTeX-modify-math-point-display-to-inline-after-content)
(LaTeX-modify-math-point-display-to-inline-before-content)
(LaTeX-modify-math-point-multiline-roundtrip): New test cases.
* doc/auctex.texi (Quotes): Document LaTeX-make-inline.
---
doc/auctex.texi | 11 ++
latex.el | 207 ++++++++++++++++++++++++++
tests/latex/latex-modify-math-test.el | 203 +++++++++++++++++++++++++
3 files changed, 421 insertions(+)
create mode 100644 tests/latex/latex-modify-math-test.el
diff --git a/doc/auctex.texi b/doc/auctex.texi
index 0b486c8c..d62f83b7 100644
--- a/doc/auctex.texi
+++ b/doc/auctex.texi
@@ -484,6 +484,17 @@ to prevent unmatched dollar.
Note that Texinfo mode does nothing special for @kbd{$}. It inserts
dollar sign(s) just in the same way as the other normal keys do.
+@AUCTeX{} provides the command @code{LaTeX-make-inline} which converts the
+display math environment at point to inline math.
+
+@deffn Command LaTeX-make-inline
+Convert @LaTeX{} display math environment at point to inline math. This
+command replaces the enclosing math environment such as @samp{\[...\]} or
+@samp{\begin@{equation@}...\end@{equation@}} with the value of
+@code{TeX-electric-math} or @samp{$...$} by default. (It may not work
+correctly in docTeX.)
+@end deffn
+
@subheading Braces
To avoid unbalanced braces, it is useful to insert them pairwise. You
diff --git a/latex.el b/latex.el
index 797513f2..29ddb9a8 100644
--- a/latex.el
+++ b/latex.el
@@ -9551,6 +9551,213 @@ no caption key is found, an error is issued. See also the docstring of
"LARGE" "huge" "Huge")
"List of LaTeX font size declarations.")
+(defun LaTeX--strip-labels ()
+ "Remove label commands between point and end of buffer."
+ (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))
+ "\\)")))
+ (save-excursion
+ (while (re-search-forward re nil t)
+ (replace-match "")))))
+
+(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 (save-excursion
+ (skip-chars-backward "[:blank:]")
+ (and
+ (bolp) (not (bobp))
+ (progn
+ (forward-char -1)
+ (skip-chars-backward "[:blank:]")
+ (not (bolp)))))
+ ;; The following dance gets around the slightly counterintuitive
+ ;; behavior of (save-excursion (join-line)) with point at bol.
+ (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 (save-excursion
+ (skip-chars-backward "[:blank:]")
+ (and
+ (bolp) (not (bobp))))
+ (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")
+ (save-excursion
+ (forward-line 1)
+ (not (looking-at-p "^[[:blank:]]*$"))))
+ (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))
+ (LaTeX--strip-labels)
+ ;; Delete leading and 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 (= (point) pos)
+ (set-marker-insertion-type pos (not 'advance)))
+ (when converting-to-inline
+ (skip-chars-backward ".,;:!?"))
+ (insert new-close)
+ ;; Indent, including one line past the modified region.
+ (widen)
+ (end-of-line 2)
+ (indent-region start (point))))))
+
+(defun LaTeX--math-environment-list ()
+ "Return list of defined math environments.
+This combines the env-on entries from `texmathp' and any user additions."
+ (texmathp-compile)
+ (mapcar #'car
+ (cl-remove-if-not
+ (lambda (entry)
+ (eq (nth 1 entry) 'env-on))
+ texmathp-tex-commands1)))
+
+(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 (member type (LaTeX--math-environment-list))
+ (error "Invalid or unsupported opening delimiter: %s" type))
+ (concat TeX-esc "end" TeX-grop type TeX-grcl))))
+
+(defun LaTeX-modify-math (new-type)
+ "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."
+ ;; FIXME: this function may not work correctly in docTeX
+ (interactive
+ (let* ((type (progn (texmathp) (car texmathp-why)))
+ (tbl (append '("$" "\\(" "$$" "\\[")
+ (LaTeX--math-environment-list))))
+ (barf-if-buffer-read-only)
+ (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.
+ (delete-char (length open))
+ (push-mark (save-excursion
+ (search-forward close)
+ (delete-region (match-beginning 0) (match-end 0))
+ (when (= (point) pos)
+ (set-marker pos nil)
+ (setq pos nil))
+ (when (member type '("$" "\\("))
+ (skip-chars-forward ".,;:!?"))
+ (point)))
+ (activate-mark)
+ (LaTeX-insert-environment new-type)))
+ (when pos
+ (goto-char pos)
+ (set-marker pos nil)))))
+
+(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 \"$...$\". Leave any
+trailing punctuation outside the math delimiters."
+ ;; FIXME: this function may not work correctly in docTeX
+ (interactive "*")
+ (LaTeX-modify-math
+ (if TeX-electric-math
+ (cons TeX-electric-math 'inline)
+ "$")))
+
(provide 'latex)
;;; latex.el ends here
diff --git a/tests/latex/latex-modify-math-test.el b/tests/latex/latex-modify-math-test.el
new file mode 100644
index 00000000..f50d9ae3
--- /dev/null
+++ b/tests/latex/latex-modify-math-test.el
@@ -0,0 +1,203 @@
+;;; latex-modify-math-test.el --- tests for LaTeX-make-inline -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2025 Free Software Foundation, Inc.
+
+;; This file is part of AUCTeX.
+
+;; AUCTeX is free software; you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by
+;; the Free Software Foundation; either version 3, or (at your option)
+;; any later version.
+
+;; AUCTeX is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+;; General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with AUCTeX; see the file COPYING. If not, write to the Free
+;; Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+;; 02110-1301, USA.
+
+;;; Code:
+
+(require 'ert)
+(require 'cl-lib)
+(require 'latex)
+
+(defmacro latex-modify-test--with-temp-buffer (contents &rest body)
+ "Create a temporary LaTeX buffer with CONTENTS and execute BODY.
+This macro is used to set up a test environment for `LaTeX-modify-math'."
+ (declare (indent 1) (debug t))
+ `(with-temp-buffer
+ (LaTeX-mode)
+ (insert ,contents)
+ (goto-char (point-min))
+ (cl-letf (((symbol-function 'preview-clearout-at-point) #'ignore))
+ ,@body)))
+
+(ert-deftest LaTeX-modify-math-inline-bracket-period ()
+ "Convert \\=\\[...\\=\\] to $..$ and keep trailing period."
+ (latex-modify-test--with-temp-buffer
+ "We have\n\\[ a+b = c. \\]"
+ (search-forward "b")
+ (LaTeX-make-inline)
+ (should (equal (buffer-string) "We have $a+b = c$."))))
+
+(ert-deftest LaTeX-modify-math-inline-double-dollar ()
+ "Convert $$..$$ to $..$."
+ (latex-modify-test--with-temp-buffer
+ "$$x!$$"
+ (search-forward "x")
+ (LaTeX-make-inline)
+ (should (equal (buffer-string) "$x$!"))))
+
+(ert-deftest LaTeX-modify-math-inline-electric-math ()
+ "Respect `TeX-electric-math'."
+ (let ((TeX-electric-math '("\\(" . "\\)")))
+ (latex-modify-test--with-temp-buffer
+ "\\[ x \\]"
+ (search-forward "x")
+ (LaTeX-make-inline)
+ (should (equal (buffer-string) "\\(x\\)")))))
+
+(ert-deftest LaTeX-modify-math-inline-equation-env ()
+ "Convert equation environment, drop \\label, keep comma."
+ (latex-modify-test--with-temp-buffer
+ "Hi.\n\nWe have\n\\begin{equation}\n\\label{l}x+y,\n\\end{equation}\n"
+ (search-forward "x")
+ (let ((TeX-electric-math '("\\(" . "\\)")))
+ (LaTeX-make-inline)
+ (should (equal (buffer-string) "Hi.\n\nWe have \\(x+y\\),\n")))))
+
+(ert-deftest LaTeX-modify-math-inline-noop ()
+ "Call inside inline math leaves buffer unchanged."
+ (latex-modify-test--with-temp-buffer
+ "Already $z$ inline."
+ (search-forward "z")
+ (LaTeX-make-inline)
+ (should (equal (buffer-string) "Already $z$ inline."))))
+
+(ert-deftest LaTeX-modify-math-inline-paren-to-dollar ()
+ "Convert \\(...\\) to $...$."
+ (latex-modify-test--with-temp-buffer
+ "Text \\(a + b\\) more text."
+ (search-forward "a")
+ (let ((TeX-electric-math nil))
+ (LaTeX-make-inline)
+ (should (equal (buffer-string) "Text $a + b$ more text.")))))
+
+(ert-deftest LaTeX-modify-math-inline-multiline-equation ()
+ "Convert multiline equation environment to inline, removing labels."
+ (latex-modify-test--with-temp-buffer
+ "Before\n\\begin{equation}\n x + y = z\n \\label{eq:test}\n\\end{equation}\nAfter"
+ (search-forward "x")
+ (LaTeX-make-inline)
+ (should (equal (buffer-string) "Before $x + y = z$ After"))))
+
+(ert-deftest LaTeX-modify-math-inline-punctuation-semicolon ()
+ "Move semicolon outside inline math."
+ (latex-modify-test--with-temp-buffer
+ "\\[ x + y; \\]"
+ (search-forward "x")
+ (LaTeX-make-inline)
+ (should (equal (buffer-string) "$x + y$;"))))
+
+(ert-deftest LaTeX-modify-math-inline-multiple-punctuation ()
+ "Handle multiple punctuation marks."
+ (latex-modify-test--with-temp-buffer
+ "\\[ result?! \\]"
+ (search-forward "result")
+ (LaTeX-make-inline)
+ (should (equal (buffer-string) "$result$?!"))))
+
+(ert-deftest LaTeX-modify-math-inline-whitespace-preservation ()
+ "Preserve surrounding whitespace appropriately."
+ (latex-modify-test--with-temp-buffer
+ "Text \\[ a + b \\] more."
+ (search-forward "a")
+ (LaTeX-make-inline)
+ (should (equal (buffer-string) "Text $a + b$ more."))))
+
+(ert-deftest LaTeX-modify-math-inline-empty-lines ()
+ "Remove empty lines from display math when converting."
+ (latex-modify-test--with-temp-buffer
+ "\\[\n\n x = y \n\n\\]"
+ (search-forward "x")
+ (LaTeX-make-inline)
+ (should (equal (buffer-string) "$x = y$"))))
+
+(ert-deftest LaTeX-modify-math-dollar-to-bracket ()
+ "Convert $...$ to \\=\\[...\\=\\]."
+ (latex-modify-test--with-temp-buffer
+ "Text $x + y$ more."
+ (search-forward "+")
+ (LaTeX-modify-math "\\[")
+ (should (equal (buffer-string) "Text\n\\[\n x + y\n\\]\nmore."))))
+
+(ert-deftest LaTeX-modify-math-paren-to-double-dollar ()
+ "Convert \\(...\\) to $$...$$."
+ (latex-modify-test--with-temp-buffer
+ "Text \\(a = b\\) end."
+ (search-forward "a")
+ (LaTeX-modify-math "$$")
+ (should (equal (buffer-string) "Text\n$$\na = b\n$$\nend."))))
+
+(ert-deftest LaTeX-modify-math-bracket-to-equation ()
+ "Convert \\=\\[...\\=\\] to equation environment."
+ (latex-modify-test--with-temp-buffer
+ "\\[ f(x) = x^2 \\]"
+ (search-forward "f")
+ (LaTeX-modify-math "equation")
+ (should (equal (buffer-string) "\\begin{equation}\n f(x) = x^2\n\\end{equation}"))))
+
+(ert-deftest LaTeX-modify-math-point-inline-to-display-after-content ()
+ "Point after inline content preserved after display conversion."
+ (latex-modify-test--with-temp-buffer
+ "A $x+y$ B"
+ (search-forward "y")
+ (LaTeX-modify-math "\\[")
+ (should (looking-back "y" (1- (point))))
+ (should (looking-at "\n[[:space:]]*\\\\\\]"))))
+
+(ert-deftest LaTeX-modify-math-point-inline-to-display-before-content ()
+ "Point before inline content preserved after display conversion."
+ (latex-modify-test--with-temp-buffer
+ "A $x+y$ B"
+ (search-forward "$")
+ (LaTeX-modify-math "\\[")
+ (looking-at "x")
+ (should (looking-at "x"))))
+
+(ert-deftest LaTeX-modify-math-point-display-to-inline-after-content ()
+ "Point after display content preserved after inline conversion."
+ (latex-modify-test--with-temp-buffer
+ "\\[\n x + y\n\\]"
+ (goto-char (point-min))
+ (re-search-forward "y")
+ (LaTeX-make-inline)
+ (should (looking-back "y" (1- (point))))
+ (should (looking-at "\\$"))))
+
+(ert-deftest LaTeX-modify-math-point-display-to-inline-before-content ()
+ "Point before display content preserved after inline conversion."
+ (latex-modify-test--with-temp-buffer
+ "\\[\n x + y\n\\]"
+ (goto-char (point-min))
+ (re-search-forward "x")
+ (forward-char -1)
+ (LaTeX-make-inline)
+ (should (looking-at "x"))))
+
+(ert-deftest LaTeX-modify-math-point-multiline-roundtrip ()
+ "Point before/after content preserved for round-trip conversion."
+ (latex-modify-test--with-temp-buffer
+ "foo $x+y$ bar"
+ (search-forward "y")
+ (backward-char)
+ (LaTeX-modify-math "\\[")
+ (should (looking-at "y"))
+ (LaTeX-make-inline)
+ (should (looking-at "y"))))
+
+;;; latex-modify-math-test.el ends here
--
2.39.3 (Apple Git-145)
_______________________________________________
bug-auctex mailing list
[email protected]
https://lists.gnu.org/mailman/listinfo/bug-auctex