Hello Paul,

On 28/11/2025, Paul D. Nelson wrote:
> ...the previews are shown by default, together with the tex code, which
> is not the intended effect.

Indeed, this was an issue with the previous implementation (one too many
simplifications). I've now addressed it in the attached patch. I've
tried to test all combinations of: preview-point-where,
preview-always-show, preview-protect-point,
preview-leave-open-previews-visible (I realize now that
preview-protect-point is meaningless when preview-always-show is nil).

With this more through testing, I've noticed several issues which the
attached patch fixes as well. One is an issue with the old preview code
(even without my patches) when previewing a statement like

$\ifPreview\special{ps: junk}\fi f \geq 2$

with the cursor being inside the equation (moving away from the preview closes 
it which is not intended).

I also refactored the code for `preview-disabled-string` and 
`preview-inactive-string` and added appropriate new-lines to the strings 
depending on the value of `preview-point-where` (I didn't notice this issue 
before).

Best regards,
-- Al

>From 2ae17738172f8fbd740fdb6d34ea65e43d503f77 Mon Sep 17 00:00:00 2001
From: Al Haji-Ali <[email protected]>
Date: Wed, 26 Nov 2025 12:26:40 +0100
Subject: [PATCH] New feature: preview at point

* doc/preview-latex.texi: Add docs for previewing at point.

* preview.el (preview-always-show, preview-point-where): New
customizations.
(preview--update-buframe, preview-overlay-updated): New functions.
(preview--string): New macro.
(preview-replace-active-icon): Call preview-overlay-updated.
(preview-place-preview): Clear-out before toggling.
(preview-mark-point, preview--open-for-replace): Add condition on
`preview-always-show`.
(preview-toggle): Update to handle values of preview-always-show and
preview-point-where and call preview-overlay-updated. Handle ARG being
equal to `maybe-open'.
(preview-move-point): Handle `preview-always-show` and call
`preview--update-buframe`.
(preview-active-string, preview-disabled-string): Refactor into
preview--string.
(preview-disable): Always call preview-toggle and condition deleting
files on `preview-leave-open-previews-visible`.
(preview-parse-messages): Restore point before placing preview.
(preview-region): Update documentation to return started process.
(preview-dvi*-command): Fix case in docstring.
(preview-gs-flag-error): Fix spacing, fix bug and conditionally open
previews.
---
 doc/preview-latex.texi |  26 +++
 preview.el             | 383 ++++++++++++++++++++++++++++-------------
 2 files changed, 294 insertions(+), 115 deletions(-)

diff --git a/doc/preview-latex.texi b/doc/preview-latex.texi
index 19210a3506..c41211156c 100644
--- a/doc/preview-latex.texi
+++ b/doc/preview-latex.texi
@@ -480,6 +480,32 @@ math (@code{$@dots{}$}), or if your usage of @code{$} conflicts with
 @previewlatex{}'s, you can turn off inline math previews.  In the
 @samp{Preview Latex} group, remove @code{textmath} from
 @code{preview-default-option-list} by customizing this variable.
+
+@item Show previews always or at point.
+
+By default, previews are always shown when available, but this can be
+disabled by setting @code{preview-always-show} to @code{nil}. In this
+case, the preview is only shown when the cursor enters the corresponding
+TeX source.
+
+@item Control placement of previews.
+
+You can set @code{preview-point-where} to determine where previews are
+placed relative to the TeX source when both are being shown. This variable
+can take the following values:
+
+@table @code
+
+@item before-string
+Show the preview before the TeX source (default).
+
+@item after-string
+Show the preview after the TeX source.
+
+@item buframe
+Show the preview in a separate frame next to the cursor. Requires the
+@code{buframe} package to be installed (available on ELPA).
+@end table
 @end itemize

 @node Known problems, For advanced users, Simple customization, top
diff --git a/preview.el b/preview.el
index 9481288675..4ecf83333f 100644
--- a/preview.el
+++ b/preview.el
@@ -456,6 +456,37 @@ set to `postscript'."
   :group 'preview-latex
   :type 'boolean)

+;;; preview-point customizations and variables.
+(defcustom preview-always-show t
+  "If non-nil, always show previews.
+
+When nil, previews are only shown when a cursor enters their source.
+See `preview-point-where' to control where they are shown."
+  :type 'boolean)
+
+(defcustom preview-point-where 'before-string
+  "Specifies where to show the preview relative to TeX source.
+
+Can be `before-string', `after-string' to show the preview at before or
+after the TeX code or `buframe' to show it in a separate frame (the
+`buframe' package must be installed).  Can also be \\='(buframe FN-POS
+FRAME-PARAMETERS BUF-PARAMETERS) where FN-POS is a position
+function (default is `buframe-position-right-of-overlay') and
+FRAME-PARAMETERS is an alist of additional frame parameters, default is
+nil and BUF-PARAMETERS is an alist of buffer local variables and their
+values."
+  :type '(choice
+          (const :tag "Before string (default)" before-string)
+          (const :tag "After string" after-string)
+          (const :tag "On frame" buframe)
+          (list :tag "On frame with explicit parameters"
+                (const buframe)
+                (choice
+                 (const :tag "Default" nil)
+                 (function :tag "Position function"))
+                (alist :tag "Frame parameters")
+                (alist :tag "Buffer parameters"))))
+
 (defun preview-string-expand (arg &optional separator)
   "Expand ARG as a string.
 It can already be a string.  Or it can be a list, then it is
@@ -567,7 +598,7 @@ CMD can be used to override the command line which is used as a basis.

 You may set the variable `preview-dvi*-command' to
 `preview-dvisvgm-command' and set `preview-dvi*-image-type' to
-`svg' to produce svg images instead of png ones."
+\\='svg to produce SVG images instead of PNG ones."
   (let* ((scale (* (/ (preview-hook-enquiry preview-scale)
                       (preview-get-magnification))
                    (with-current-buffer TeX-command-buffer
@@ -1292,7 +1323,8 @@ ready.  This behavior suppresses flicker in the appearance."
     (when (and preview-leave-open-previews-visible
                (consp img))
       ;; No "TeX icon" has been shown, so we flush manually.
-      (image-flush (car img) t))))
+      (image-flush (car img) t))
+    (preview-overlay-updated ov)))

 (defun preview-gs-place (ov snippet box run-buffer tempdir ps-file _imagetype)
   "Generate an image placeholder rendered over by Ghostscript.
@@ -1430,16 +1462,16 @@ Try \\[ps-run-start] \\[ps-run-buffer] and \
          (ps-open
           (let ((string
                  (concat
-                (mapconcat #'shell-quote-argument
-                            (append (list
-                                     preview-gs-command
-                                     outfile)
-                                    preview-gs-command-line)
-                            " ")
-                 "\nGS>"
-                 preview-gs-init-string
-                 (aref (overlay-get ov 'queued) 1)
-                 err)))
+                  (mapconcat #'shell-quote-argument
+                             (append (list
+                                      preview-gs-command
+                                      outfile)
+                                     preview-gs-command-line)
+                             " ")
+                  "\nGS>"
+                  preview-gs-init-string
+                  (aref (overlay-get ov 'queued) 1)
+                  err)))
             (lambda () (interactive "@") (preview-mouse-open-error string))))
          (str
           (preview-make-clickable
@@ -1463,7 +1495,9 @@ Try \\[ps-run-start] \\[ps-run-buffer] and \
                                     (apply #'preview-mouse-open-eps
                                            args))])))))))
     (overlay-put ov 'strings (cons str str))
-    (preview-toggle ov)))
+    (preview-toggle ov (and (not preview-always-show) 'maybe-open))
+    (setq preview-temporary-opened
+          (cl-delete ov preview-temporary-opened))))

 (defun preview-gs-transact (process answer)
   "Work off Ghostscript transaction.
@@ -2034,6 +2068,7 @@ Disable it if that is the case.  Ignores text properties."
                 (overlay-put ov 'insert-in-front-hooks nil)
                 (overlay-put ov 'insert-behind-hooks nil)
                 (when (and preview-leave-open-previews-visible
+                           preview-always-show
                            (eq (overlay-get ov 'preview-state) 'active))
                   ;; This is so that remote commands, such as `undo',
                   ;; open active previews before disabling them.
@@ -2076,40 +2111,63 @@ definition of OV, AFTER-CHANGE, BEG, END and LENGTH."

 (defun preview-toggle (ov &optional arg event)
   "Toggle visibility of preview overlay OV.
-ARG can be one of the following: t displays the overlay,
-nil displays the underlying text, and `toggle' toggles.
-If EVENT is given, it indicates the window where the event
-occured, either by being a mouse event or by directly being
-the window in question.  This may be used for cursor restoration
+ARG can be one of the following: t displays the overlay, nil displays
+the underlying text, and `toggle' toggles.  ARG can also be `maybe-open'
+to display the underlying text only when the cursor the overlay's buffer
+is on the overlay (in this case OV is added to
+`preview-temporary-opened').  If EVENT is given, it indicates the window
+where the event occured, either by being a mouse event or by directly
+being the window in question.  This may be used for cursor restoration
 purposes."
-  (let ((old-urgent (preview-remove-urgentization ov))
-        (preview-state
-         (if (if (eq arg 'toggle)
-                 (null (eq (overlay-get ov 'preview-state) 'active))
-               arg)
-             'active
-           'inactive))
-        (strings (overlay-get ov 'strings)))
-    (unless (eq (overlay-get ov 'preview-state) 'disabled)
-      (overlay-put ov 'preview-state preview-state)
-      (if (eq preview-state 'active)
-          (progn
+  (let ((old-urgent (preview-remove-urgentization ov)))
+    (unwind-protect
+        (let ((preview-state
+               (if (cond
+                    ((eq arg 'toggle)
+                     (null (eq (overlay-get ov 'preview-state) 'active)))
+                    ((eq arg 'maybe-open)
+                     (with-current-buffer (overlay-buffer ov)
+                       ;; if (point) is on the overlay
+                       (or (not (memq ov (overlays-at (point))))
+                           (progn (push ov preview-temporary-opened)
+                                  nil))))
+                    (t arg))
+                   'active
+                 'inactive))
+              (prop (if (consp preview-point-where)
+                        (car preview-point-where)
+                      preview-point-where))
+              (strings (overlay-get ov 'strings)))
+          (unless (eq (overlay-get ov 'preview-state) 'disabled)
+            (overlay-put ov 'preview-state preview-state)
             (overlay-put ov 'category 'preview-overlay)
-            (if (eq (overlay-start ov) (overlay-end ov))
-                (overlay-put ov 'before-string (car strings))
+            (if (eq preview-state 'active)
+                (progn
+                  (if (eq (overlay-start ov) (overlay-end ov))
+                      (overlay-put ov prop (car strings))
+                    (when preview-always-show
+                      (dolist (prop '(display keymap mouse-face help-echo))
+                        (overlay-put ov prop
+                                     (get-text-property 0 prop
+                                                        (car strings)))))
+                    (overlay-put ov prop nil))
+                  (overlay-put ov 'face nil))
               (dolist (prop '(display keymap mouse-face help-echo))
-                (overlay-put ov prop
-                             (get-text-property 0 prop (car strings))))
-              (overlay-put ov 'before-string nil))
-            (overlay-put ov 'face nil))
-        (dolist (prop '(display keymap mouse-face help-echo))
-          (overlay-put ov prop nil))
-        (overlay-put ov 'face 'preview-face)
-        (unless (cdr strings)
-          (setcdr strings (preview-inactive-string ov)))
-        (overlay-put ov 'before-string (cdr strings)))
-      (if old-urgent
-          (apply #'preview-add-urgentization old-urgent))))
+                (overlay-put ov prop nil))
+              (when preview-always-show
+                (overlay-put ov 'face 'preview-face))
+              (unless (cdr strings)
+                ;; If `preview-always-show' is nil, then we should
+                ;; always show the preview (i.e. leave it open) when the
+                ;; preview is "inactive".
+                (let ((preview-leave-open-previews-visible
+                       (or preview-leave-open-previews-visible
+                           (not preview-always-show))))
+                  (setcdr strings (preview-inactive-string ov))))
+              (overlay-put ov prop (cdr strings)))
+            (preview-overlay-updated ov)))
+      (when old-urgent
+        (apply #'preview-add-urgentization old-urgent))))
   (if event
       (preview-restore-position
        ov
@@ -2117,6 +2175,13 @@ purposes."
            event
          (posn-window (event-start event))))))

+(defun preview-overlay-updated (ov)
+  "Mark preview OV as updated.
+Has effect only if `preview-always-show' is nil."
+  (with-current-buffer (window-buffer)
+    ;; Only force an update if the cursor is at point
+    (preview--update-buframe (memq ov (overlays-at (point))))))
+
 (defvar preview-marker (make-marker)
   "Marker for fake intangibility.")

@@ -2127,13 +2192,14 @@ purposes."

 (defun preview-mark-point ()
   "Mark position for fake intangibility."
-  (when (eq (get-char-property (point) 'preview-state) 'active)
-    (unless preview-last-location
-      (setq preview-last-location (make-marker)))
-    (set-marker preview-last-location (point))
-    (set-marker preview-marker (point))
-    (preview-move-point))
-  (set-marker preview-marker (point)))
+  (when preview-always-show
+    (when (eq (get-char-property (point) 'preview-state) 'active)
+      (unless preview-last-location
+        (setq preview-last-location (make-marker)))
+      (set-marker preview-last-location (point))
+      (set-marker preview-marker (point))
+      (preview-move-point))
+    (set-marker preview-marker (point))))

 (defun preview-restore-position (ov window)
   "Tweak position after opening/closing preview.
@@ -2177,17 +2243,19 @@ overlays not in the active window."
                               (current-buffer))
                           (- pt (marker-position preview-marker))))))
           (preview-open-overlays lst)
-        (while lst
-          (setq lst
-                (if (and
-                     (eq (overlay-get (car lst) 'preview-state) 'active)
-                     (> pt (overlay-start (car lst))))
-                    (overlays-at
-                     (setq pt (if (and distance (< distance 0))
-                                  (overlay-start (car lst))
-                                (overlay-end (car lst)))))
-                  (cdr lst))))
-        (goto-char pt)))))
+        (when preview-always-show
+          (while lst
+            (setq lst
+                  (if (and
+                       (eq (overlay-get (car lst) 'preview-state) 'active)
+                       (> pt (overlay-start (car lst))))
+                      (overlays-at
+                       (setq pt (if (and distance (< distance 0))
+                                    (overlay-start (car lst))
+                                  (overlay-end (car lst)))))
+                    (cdr lst))))
+          (goto-char pt)))))
+  (preview--update-buframe))

 (defun preview-open-overlays (list &optional pos)
   "Open all previews in LIST, optionally restricted to enclosing POS."
@@ -2202,7 +2270,8 @@ overlays not in the active window."

 (defun preview--open-for-replace (beg end &rest _)
   "Make `query-replace' open preview text about to be replaced."
-  (preview-open-overlays (overlays-in beg end)))
+  (when preview-always-show
+    (preview-open-overlays (overlays-in beg end))))

 (defcustom preview-query-replace-reveal t
   "Make `query-replace' autoreveal previews."
@@ -2312,22 +2381,41 @@ active (`transient-mark-mode'), it is run through `preview-region'."
       (preview-region (preview-next-border t)
                       (preview-next-border nil)))))

-(defun preview-disabled-string (ov)
-  "Generate a before-string for disabled preview overlay OV."
-  (concat (preview-make-clickable
-           (overlay-get ov 'preview-map)
-           preview-icon
-           "\
-%s regenerates preview
-%s more options"
-           (lambda () (interactive) (preview-regenerate ov)))
-;; icon on separate line only for stuff starting on its own line
-          (with-current-buffer (overlay-buffer ov)
+(defmacro preview--string (ov helpstring &optional click1)
+  "Generate a string for preview overlay OV.
+Use HELPSTRING and CLICK1 for the string using `preview-make-clickable'."
+  ;; Add new lines at beginning or end of string to separate the string
+  ;; only for when the overlay starts or ends on its own line.
+  `(concat
+    (or (when (eq preview-point-where 'after-string)
+          (with-current-buffer (overlay-buffer ,ov)
+            (save-excursion
+              (save-restriction
+                (widen)
+                (goto-char (overlay-end ,ov))
+                (when (eolp) "\n")))))
+        "")
+    (preview-make-clickable
+     (overlay-get ,ov 'preview-map)
+     (if preview-leave-open-previews-visible
+         (overlay-get ,ov 'preview-image)
+       preview-icon)
+     ,helpstring
+     ,click1)
+    (or (when (eq preview-point-where 'before-string)
+          (with-current-buffer (overlay-buffer ,ov)
             (save-excursion
               (save-restriction
                 (widen)
-                (goto-char (overlay-start ov))
-                (if (bolp) "\n" ""))))))
+                (goto-char (overlay-start ,ov))
+                (when (bolp) "\n")))))
+        "")))
+
+(defun preview-disabled-string (ov)
+  "Generate a string for disabled preview overlay OV."
+  (preview--string ov
+                   "%s regenerates preview\n%s more options"
+                   (lambda () (interactive) (preview-regenerate ov))))

 (defun preview-disable (ovr)
   "Change overlay behaviour of OVR after source edits."
@@ -2341,10 +2429,10 @@ active (`transient-mark-mode'), it is run through `preview-region'."
     (overlay-put ovr 'preview-image nil))
   (overlay-put ovr 'timestamp nil)
   (setcdr (overlay-get ovr 'strings) (preview-disabled-string ovr))
+  (preview-toggle ovr)
   (unless preview-leave-open-previews-visible
-    (preview-toggle ovr))
g-  (overlay-put ovr 'preview-state 'disabled)
-  (preview--delete-overlay-files ovr))
+    (preview--delete-overlay-files ovr))
+  (overlay-put ovr 'preview-state 'disabled))

 (defun preview--delete-overlay-files (ovr)
   "Delete files owned by OVR."
@@ -2574,25 +2662,12 @@ This is called as a hook when exiting Emacs."
   (mapc #'preview-format-kill preview-dumped-alist))

 (defun preview-inactive-string (ov)
-  "Generate before-string for an inactive preview overlay OV.
+  "Generate string for an inactive preview overlay OV.
 This is for overlays where the source text has been clicked
 visible.  For efficiency reasons it is expected that the buffer
 is already selected and unnarrowed."
-  (concat
-   (preview-make-clickable (overlay-get ov 'preview-map)
-                           (if preview-leave-open-previews-visible
-                               (overlay-get ov 'preview-image)
-                             preview-icon)
-                           "\
-%s redisplays preview
-%s more options")
-;; icon on separate line only for stuff starting on its own line
-   (with-current-buffer (overlay-buffer ov)
-     (save-excursion
-       (save-restriction
-         (widen)
-         (goto-char (overlay-start ov))
-         (if (bolp) "\n" ""))))))
+  (preview--string ov
+                   "%s redisplays preview\n%s more options"))

 (defun preview-dvi*-place-all ()
   "Place all images that the dvi convertion process has created, if any.
@@ -2666,7 +2741,7 @@ Deletes the dvi file when finished."
   'preview-dvipng-place-all #'preview-dvi*-place-all "14.2.0")

 (defun preview-active-string (ov)
-  "Generate before-string for active image overlay OV."
+  "Generate string for active image overlay OV."
   (preview-make-clickable
    (overlay-get ov 'preview-map)
    (car (overlay-get ov 'preview-image))
@@ -2758,8 +2833,8 @@ to the close hook."
                   place-opts)
       (overlay-put ov 'strings
                    (list (preview-active-string ov)))
-      (preview-toggle ov t)
-      (preview-clearout start end tempdir ov))))
+      (preview-clearout start end tempdir ov)
+      (preview-toggle ov (or preview-always-show 'maybe-open)))))

 (defun preview-counter-find (begin)
   "Fetch the next preceding or next preview-counters property.
@@ -3803,22 +3878,28 @@ name(\\([^)]+\\))\\)\\|\
                                           (funcall preview-find-end-function
                                                    region-beg)
                                         (point)))
-                                     (ovl (preview-place-preview
-                                           snippet
-                                           region-beg
-                                           region-end
-                                           (preview-TeX-bb box)
-                                           (cons lcounters counters)
-                                           tempdir
-                                           (cdr open-data))))
-                                (setq close-data (nconc ovl close-data))
-                                (when (and preview-protect-point
-                                           (<= region-beg point-current)
-                                           (< point-current region-end))
-                                  ;; Temporarily open the preview if it
-                                  ;; would bump the point.
-                                  (preview-toggle (car ovl))
-                                  (push (car ovl) preview-temporary-opened)))
+                                     ovl)
+                                (save-excursion
+                                  ;; Restore point to current one before
+                                  ;; placing preview
+                                  (goto-char point-current)
+                                  (setq ovl (preview-place-preview
+                                             snippet
+                                             region-beg
+                                             region-end
+                                             (preview-TeX-bb box)
+                                             (cons lcounters counters)
+                                             tempdir
+                                             (cdr open-data)))
+                                  (setq close-data (nconc ovl close-data))
+                                  (when (and preview-protect-point
+                                             preview-always-show
+                                             (<= region-beg point-current)
+                                             (< point-current region-end))
+                                    ;; Temporarily open the preview if it
+                                    ;; would bump the point.
+                                    (preview-toggle (car ovl))
+                                    (push (car ovl) preview-temporary-opened))))
                             (with-current-buffer run-buffer
                               (preview-log-error
                                (list 'error
@@ -4211,7 +4292,9 @@ The functions in this variable will each be called inside
 `preview-region' with one argument which is a string.")

 (defun preview-region (begin end)
-  "Run preview on region between BEGIN and END."
+  "Run preview on region between BEGIN and END.
+
+It returns the started process."
   (interactive "r")
   (let ((TeX-region-extra
          ;; Write out counter information to region.
@@ -4426,6 +4509,76 @@ If not a regular release, the date of the last change.")
           (insert "\n")))
     (error nil)))

+;;; buframe specific
+
+;; Buframe functions, it will be assumed that buframe is installed so
+;; that it can be required.
+(declare-function buframe-position-right-of-overlay "ext:buframe"
+                  (frame ov &optional location))
+(declare-function buframe-make-buffer "ext:buframe" (name &optional locals))
+(declare-function buframe-make "ext:buframe"
+                  (frame-or-name fn-pos buffer &optional
+                                 parent-buffer parent-frame parameters))
+(declare-function buframe-disable "ext:buframe" (frame-or-name
+                                                 &optional enable))
+(declare-function buframe--find "ext:buframe"
+                  (&optional frame-or-name buffer parent noerror))
+
+(defun preview--update-buframe (&optional force)
+  "Show or hide a buframe popup depending on overlays at point.
+
+The frame is not updated if the `buframe' property has not changed,
+unless FORCE is non-nil."
+  (let* ((frame-name "auctex-preview")
+         (buf-name " *auctex-preview-buffer*")
+         (old-frame (buframe--find frame-name nil nil t)))
+    (if-let* ((ov (cl-find-if
+                   (lambda (ov) (when (overlay-get ov 'buframe) ov))
+                   (overlays-at (point))))
+              (str (overlay-get ov 'buframe)))
+        (unless (and (not force)
+                     old-frame
+                     (pcase (frame-parameter old-frame 'auctex-preview)
+                       (`(,o . ,s) (and (eq o ov) (eq s str)))))
+          (let* ((buf (buframe-make-buffer buf-name
+                                           (car-safe
+                                            (cddr (cdr-safe
+                                                   preview-point-where)))))
+                 (max-image-size
+                  (if (integerp max-image-size)
+                      max-image-size
+                    ;; Set the size max-image-size using the current frame
+                    ;; since the popup frame will be small to begin with
+                    (* max-image-size (frame-width)))))
+            (with-current-buffer buf
+              (let (buffer-read-only)
+                (with-silent-modifications
+                  (erase-buffer)
+                  (insert (propertize str
+                                      ;; Remove unnecessary properties
+                                      'help-echo nil
+                                      'keymap nil
+                                      'mouse-face nil))
+                  (goto-char (point-min)))))
+            (setq old-frame
+                  (buframe-make
+                   frame-name
+                   (lambda (frame)
+                     (funcall
+                      (or (car-safe (cdr-safe preview-point-where))
+                          #'buframe-position-right-of-overlay)
+                      frame
+                      ov))
+                   buf
+                   (overlay-buffer ov)
+                   (window-frame)
+                   (car-safe (cdr (cdr-safe preview-point-where)))))
+            (set-frame-parameter old-frame 'auctex-preview
+                                 (cons ov str))))
+      (when old-frame
+        (set-frame-parameter old-frame 'auctex-preview nil)
+        (buframe-disable old-frame)))))
+
 ;;;###autoload
 (defun preview-report-bug () "Report a bug in the preview-latex package."
        (interactive)
--
2.39.5 (Apple Git-154)
_______________________________________________
bug-auctex mailing list
[email protected]
https://lists.gnu.org/mailman/listinfo/bug-auctex

Reply via email to