branch: externals/auctex commit e5c822d1dfcbb30be47f97c8373b15fa0f3546b2 Author: Paul Nelson <ultr...@gmail.com> Commit: Paul Nelson <ultr...@gmail.com>
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 (bug#78586). * 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. (Mathematics): Document LaTeX-modify-math and variants. --- doc/auctex.texi | 114 +++++++++++++++++++ latex.el | 207 ++++++++++++++++++++++++++++++++++ tests/latex/latex-modify-math-test.el | 203 +++++++++++++++++++++++++++++++++ 3 files changed, 524 insertions(+) diff --git a/doc/auctex.texi b/doc/auctex.texi index 0b486c8ca5..5629a693ab 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 @@ -1290,6 +1301,109 @@ is typed to begin math mode or a math environment is inserted by @kbd{C-c C-e} (@code{LaTeX-environment}). @end defopt +@subheading Modifying Math Delimiters and Environments +@cindex LaTeX-modify-math +@cindex Inline math, converting +@cindex Display math, converting +@cindex Math environment, converting + +@AUCTeX{} offers the command @code{LaTeX-modify-math} to convert the +mathematical construct at point---whether it is inline math such as +@samp{$...$} or @samp{\(...\)}, a display construct such as @samp{$$...$$} +or @samp{\[...\]}, or an environment such as @samp{\begin@{equation@} +... \end@{equation@}}---into a different kind of delimiter or environment. + +@deffn Command LaTeX-modify-math +Interactively, prompt for the target delimiter or environment. The +completion table contains the inline delimiters @samp{$} and @samp{\(}, +the display delimiters @samp{$$} and @samp{\[}, and every math environment +known to @code{texmathp}, such as @samp{equation}, @samp{align*}, or +anything in the user option @code{texmathp-tex-commands}. The current +construct is then rewritten using the chosen form, taking care to +@itemize @bullet +@item keep any trailing punctuation outside inline math, +@item put display constructs on their own lines, and +@item strip any @code{\label@{@}} commands when converting to inline math. +@end itemize + +When called from Lisp, @var{new-type} may be a string naming a delimiter +or environment, or a cons @code{((@var{open} . @var{close}) +. @var{inline})} specifying custom delimiters, where @var{inline} is +non-@code{nil} for inline math. + +This command does @emph{not} understand macro-based math wrappers such as +@code{\ensuremath}. It may also fail in docTeX buffers. +@end deffn + +A related command, invoked with a prefix argument, is @kbd{C-u C-c C-e} +(@code{LaTeX-environment}) (@pxref{Environments}). This modifies the +current @LaTeX{} environment, while @code{LaTeX-modify-math} also handles +inline/display constructs. + +You can define commands that convert to a particular form, e.g.@: by +adding the following to your init file:: + +@lisp +(defun my-LaTeX-make-brackets () + "Convert math construct at point to \"\\=\\[..\\]\"." + (interactive) + (LaTeX-modify-math "\\[")) +@end lisp + +@lisp +(defun my-LaTeX-make-equation* () + "Convert math construct at point to \"equation*\"." + (interactive) + (LaTeX-modify-math "equation*")) +@end lisp + +You can use @code{LaTeX-modify-math} to build higher‑level toggles. The +following modifies any math construct to an @samp{equation*} environment, +then toggles the numbered status: + +@lisp +(defun my-LaTeX-toggle-numbered () + "Convert math construct at point to \"equation*\". +If the math construct is already \"equation*\", then toggle with the +numbered variant \"equation\"." + (interactive) + (unless (texmathp) (user-error "Not inside math")) + (let ((current (car texmathp-why))) + (LaTeX-modify-math + (pcase current + ("equation*" "equation") + ("equation" "equation*") + (_ "equation*"))))) +@end lisp + +A further example toggles between @samp{equation}, @samp{align} and their +starred forms: + +@lisp +(defun my-LaTeX-toggle-align () + "Toggle math environment at point between \"equation\" and \"align\"." + (interactive) + (unless (texmathp) (user-error "Not inside math")) + (let ((current (car texmathp-why))) + (LaTeX-modify-math + (pcase current + ("align*" "equation*") + ("equation*" "align*") + ("align" "equation") + ("equation" "align") + (_ "align*"))))) +@end lisp + +Such helper commands can be bound in @code{LaTeX-mode-map} as you see fit, +e.g.@: by adding the following to your init file: + +@lisp +(keymap-set LaTeX-mode-map "C-c e" #'my-LaTeX-make-equation*) +@end lisp + +@xref{Quotes,,LaTeX-make-inline}, for a built-in convenience wrapper that +converts display constructs to inline math. + @node Completion @section Completion @cindex Completion diff --git a/latex.el b/latex.el index 0f75bc5736..f4c206fa2a 100644 --- a/latex.el +++ b/latex.el @@ -9554,6 +9554,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 0000000000..f50d9ae359 --- /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