Hi both,

preview-automatic frequent updating of the preview (while fast-typing)
surfaced some stability issues in the preview code that were difficult
to come across in normal non-automatic use. So to address these first, I
am attaching a patch to preview.el which should be applied independently
of preview-automatic. These are the issues and fixes:

- LaTeX can report a tiny positive bounding box for a visually empty
  snippet (e.g. \( \)), which Emacs's image loader would reject and
  display "Invalid image size" messages. Fixed by testing for
  degeneracy (preview--degenerate-box-p) and showing the error icon
  instead of the image at both display paths.
- When an overlay borrowed a still-rendering placeholder from a prior
  overlay, ownership was transferred via a single assoc-list
  splice. Under fast re-triggering, two overlays could end up sharing
  (and double-deleting) a file. Fixed by using the reference counting
  machinery (preview--filenames-refcounted-p/-entry-string).
- preview-parse-messages locates snippet boundaries by searching for
  text captured at compile time; if the buffer was edited mid-compile,
  the search could land in the wrong place with no way to detect it,
  silently installing a preview for the wrong text. Fixed by tracking
  failure explicitly (reloc-failed) and falling back to an existing
  overlay's own bounds (preview--fallback-bounds), or skipping placement
  if there's nothing to anchor to.
- When buframe showed a preview, it compared overlays by identity
  instead of content, rebuilding on every re-preview even when nothing
  visible changed. Fixed by comparing the image by value.

Following Paul's reports, I also modified preview-automatic to be more
stable, especially with regards to silent updates, and prioritizing the
user's processes. This is in the attached file.

> I suspect we can converge on the mechanism quickly.  Al's
> preview-silent-errors is close.  The additional primitive I'd like is a
> function that behaves like preview-automatic-update, but accepts a
> region rather than a point and specifies no policy.

I am happy to factor this out, but wouldn't setting policy function be
able to supply the region?

> For the built-in policy, I'd be happy for preview-at-point to select the
> region by default, as Al suggests.  My immediate issues are related to
> regions getting previewed without first checking that they are complete.
> texmathp identifies whether point is in math, but not whether the math
> is complete.  preview-auto searches for matching closing delimiters (and
> I'd be happy to try to adapt some of that code here, if we can't think
> of a simpler way).

I knew of this issue, but I wasn't sure it's worth fixing
out-of-the-box. We can definitely use texmathp to test if the region is
closed or not, which would work for environments and latex's \[ \] or \(
\). For example, see that attached function.

This does not check if $ or $$ are properly closed which is much more
annoying, and might make auto-previewing significantly slower, so it
depends on how much we want to protect against this.

Best regards,
-- Al
>From 006ddaf7352745050862c807af5e5bdcfe2addff Mon Sep 17 00:00:00 2001
From: Al Haji-Ali <[email protected]>
Date: Fri, 24 Jul 2026 00:09:27 +0100
Subject: [PATCH] New feature: preview-automatic

Code derived from preview-auto.el (Copyright 2024 FSF, by Paul
D. Nelson), adapted and integrated here.

* doc/preview-latex.texi: Add docs for preview-automatic.
* preview.el (preview-silent-errors): New local variable.
(preview-log-error, preview-reraise-error): Silence errors
conditionally.
(preview-gs-filter): Call preview-reraise-error from process buffer.
(preview-gs-queue-empty): Do not delete queued overlays  on a
deliberate preview-automatic abort.
(preview-handle-modification): Update previews automatically.
(preview--silent): New macro to suppress compilation messages.
(preview-generate-preview): Mark process as an automatic job when
chained after one.
(preview-automatic-function, preview-automatic-delay): New user
options.
(preview-automatic-mode): New minor mode.
(preview-automatic--in-progress): New variable marking a process
buffer as an ongoing, or just aborted, automatic job.
(preview-automatic--ours-p, preview-automatic--kill-ours): New
functions.
(preview-automatic--tag-for-resume): New function.
(preview-automatic--interrupt-for-edit): New function; kill and
reschedule on every edit rather than after a debounce period.
(preview-automatic--update-1, preview-automatic-update,
preview-automatic--update-timer): New functions and variable.
(preview-automatic--after-change, preview-automatic--after-process):
New functions.
(preview-automatic--reschedule): New function; advises TeX-command to
preempt and resume preview-automatic jobs.
---
 doc/preview-latex.texi |   3 +
 preview.el             | 289 ++++++++++++++++++++++++++++++++++++++---
 2 files changed, 274 insertions(+), 18 deletions(-)

diff --git a/doc/preview-latex.texi b/doc/preview-latex.texi
index 0f3c63b1..a38d030d 100644
--- a/doc/preview-latex.texi
+++ b/doc/preview-latex.texi
@@ -481,6 +481,9 @@ math (@code{$@dots{}$}), or if your usage of @code{$} conflicts with
 @samp{Preview Latex} group, remove @code{textmath} from
 @code{preview-default-option-list} by customizing this variable.
 
+See also @code{preview-automatic-mode} to enable automatic updating or
+generation of previews.
+
 @item Customize where previews are shown
 
 Set @code{preview-visibility-style} to control when previews appear:
diff --git a/preview.el b/preview.el
index d11f584e..a10e4fee 100644
--- a/preview.el
+++ b/preview.el
@@ -372,6 +372,10 @@ See also `preview-gs-command'."
   "List of overlays to convert using gs.
 Buffer-local to the appropriate TeX process buffer.")
 
+(defvar-local preview-silent-errors nil
+  "When non-nil, do not signal preview errors nor display output buffer.
+This variable should be set in the process buffer.")
+
 (defvar-local preview-gs-outstanding nil
   "Overlays currently processed.")
 
@@ -670,11 +674,17 @@ You may set the variable `preview-dvi*-command' to
   :type 'string)
 
 (defun preview-gs-queue-empty ()
-  "Kill off everything remaining in `preview-gs-queue'."
-  (mapc #'preview-delete preview-gs-outstanding)
-  (dolist (ov preview-gs-queue)
-    (if (overlay-get ov 'queued)
-        (preview-delete ov)))
+  "Kill off everything remaining in `preview-gs-queue'.
+Do not delete queued overlays when `preview-automatic--in-progress' is
+\\='aborted (see `preview-automatic--kill-ours') and
+`preview-keep-stale-images' is set. This was an intentional, automatic
+aborting of the preview."
+  (unless (and preview-keep-stale-images
+               (eq preview-automatic--in-progress 'aborted))
+    (mapc #'preview-delete preview-gs-outstanding)
+    (dolist (ov preview-gs-queue)
+      (if (overlay-get ov 'queued)
+          (preview-delete ov))))
   (setq preview-gs-outstanding nil)
   (setq preview-gs-queue nil))
 
@@ -699,7 +709,8 @@ is to be used."
         (insert-before-markers
          (format "%s: %s\n"
                  context (error-message-string err)))
-        (display-buffer (current-buffer)))))
+        (unless preview-silent-errors
+          (display-buffer (current-buffer))))))
   (setq preview-error-condition err))
 
 (defun preview-reraise-error (&optional process)
@@ -708,7 +719,12 @@ Makes sure that PROCESS is removed from the \"Compilation\"
 tag in the mode line."
   (when preview-error-condition
     (unwind-protect
-        (signal (car preview-error-condition) (cdr preview-error-condition))
+        (unless (buffer-local-value 'preview-silent-errors
+                                    (or (and process
+                                             (process-buffer process))
+                                        (current-buffer)))
+          (signal (car preview-error-condition)
+                  (cdr preview-error-condition)))
       (setq preview-error-condition nil
             compilation-in-progress (delq process compilation-in-progress)))))
 
@@ -825,8 +841,10 @@ Gets the usual PROCESS and STRING parameters, see
         (setq preview-gs-answer (substring preview-gs-answer pos))
         (condition-case err
             (preview-gs-transact process answer)
-          (error (preview-log-error err "Ghostscript filter" process))))))
-  (preview-reraise-error))
+          (error (preview-log-error err "Ghostscript filter" process)))))
+    ;; Call `preview-reraise-error' from the process buffer since we do
+    ;; not pass the `process'.
+    (preview-reraise-error)))
 
 (defun preview-gs-restart ()
   "Start a new Ghostscript conversion process."
@@ -2195,11 +2213,17 @@ for definition of OV, AFTER-CHANGE, BEG, END and LENGTH."
     (preview-register-change ov)))
 
 (defun preview-handle-modification
-  (ov after-change _beg _end &optional _length)
+    (ov after-change _beg _end &optional _length)
   "Hook function for `modification-hooks' property.
 See info node `(elisp) Overlay Properties' for
 definition of OV, AFTER-CHANGE, BEG, END and LENGTH."
-  (unless after-change
+  (if after-change
+      (when (buffer-local-value 'preview-automatic-mode
+                                (overlay-buffer ov))
+        (preview-automatic--interrupt-for-edit (overlay-buffer ov)
+                                               (overlay-start ov))
+        (preview-automatic-update (overlay-buffer ov)
+                                  (overlay-start ov)))
     (preview-register-change ov)))
 
 (defun preview--string (ov use-icon helpstring &optional click1)
@@ -4558,6 +4582,24 @@ environments is selected."
     (preview-region (region-beginning) (region-end))))
 
 
+(defmacro preview--silent (silent &rest body)
+  "Evaluate BODY with compilation messages suppressed if SILENT.
+Binds `TeX-suppress-compilation-message', `save-silently',
+`noninteractive' and the variable `inhibit-message' to t, and
+`message-log-max' to nil, for the dynamic extent of BODY -- but only
+when SILENT is non-nil; otherwise the ambient values of those
+variables are left untouched."
+  (declare (indent 1) (debug (form body)))
+  (let ((silentp (make-symbol "silent")))
+    `(let* ((,silentp ,silent)
+            (TeX-suppress-compilation-message
+             (if ,silentp t TeX-suppress-compilation-message))
+            (save-silently (if ,silentp t save-silently))
+            (noninteractive (if ,silentp t noninteractive))
+            (inhibit-message (if ,silentp t inhibit-message))
+            (message-log-max (if ,silentp nil message-log-max)))
+       ,@body)))
+
 (defun preview-generate-preview (file command)
   "Generate a preview.
 FILE the file (without default extension), COMMAND is the command
@@ -4589,13 +4631,25 @@ Return the started process."
                   (let ((prev-fun TeX-sentinel-function))
                     (lambda (process string)
                       (funcall prev-fun process string)
-                      (TeX-inline-preview-internal
-                       command file
-                       pr-file commandbuff
-                       dumped-cons
-                       master
-                       geometry
-                       (buffer-string))))))))
+                      ;; Suppress output message only if the dump was
+                      ;; itself part of an automatic job.
+                      (let ((ours (preview-automatic--ours-p process)))
+                        (preview--silent ours
+                          (let ((new-process
+                                 (TeX-inline-preview-internal
+                                  command file
+                                  pr-file commandbuff
+                                  dumped-cons
+                                  master
+                                  geometry
+                                  (buffer-string))))
+                            (when (and ours new-process
+                                       (process-buffer new-process))
+                              (with-current-buffer (process-buffer new-process)
+                                (setq-local preview-automatic--in-progress t)
+                                (setq-local preview-silent-errors t
+                                            preview-locating-previews-message
+                                            nil))))))))))))
     (or process
         (TeX-inline-preview-internal command file
                                      pr-file commandbuff
@@ -4789,6 +4843,205 @@ See `preview-at-point-placement'."))
         (set-frame-parameter old-frame 'auctex-preview nil)
         (buframe-disable old-frame)))))
 
+;;; preview-automatic -- Auto-preview
+
+(defcustom preview-automatic-function nil
+  "Function to determine if a preview should be created at point.
+
+When `preview-automatic-mode' is enabled and this variable is non-nil,
+it should be a function that is called with no arguments at (point)
+when there is not a preview already.  If the function returns a non-nil
+value, `preview-at-point' will be called.
+
+If the function returns a cons, it should be the (BEGIN . END), which
+will be used as arguments for `preview-region'."
+  :type 'symbol
+  :group 'preview-latex
+  :package-version '(auctex . "14.2.0"))
+
+;;;###autoload
+(define-minor-mode preview-automatic-mode
+  "Enable automatic refreshing and generation of previews.
+When enabled, existing previews are automatically updated when their
+text is changed.  Moreover, previews are automatically created whenever
+`preview-automatic-function' returns a non-nil value at point."
+  :group 'preview-latex
+  :init-value nil
+  (if preview-automatic-mode
+      (progn
+        (add-hook 'after-change-functions
+                  #'preview-automatic--after-change nil t)
+        (advice-add 'TeX-command
+                    :around #'preview-automatic--reschedule)
+        (preview-automatic-update (current-buffer) (point)))
+    ;; Don't remove advice, in case the minor mode is enabled in another
+    ;; file.
+    (remove-hook 'after-change-functions
+                 #'preview-automatic--after-change t)))
+
+(defcustom preview-automatic-delay 0.1
+  "Delay in seconds for automatic preview timer."
+  :type 'number
+  :group 'preview-latex
+  :package-version '(auctex . "14.2.0"))
+
+(defvar-local preview-automatic--in-progress nil
+  "Non-nil if the buffer holds an ongoing `preview-automatic' job.
+Set on the process buffer.  The value \\='aborted specifically means
+`preview-automatic--kill-ours' just killed that job.")
+
+(defun preview-automatic--ours-p (process)
+  "Return non-nil if PROCESS belongs to an ongoing `preview-automatic' job.
+This is the case when `preview-automatic--in-progress' is set in its
+process buffer."
+  (when-let* ((buf (process-buffer process))
+              ((buffer-live-p buf)))
+    (buffer-local-value 'preview-automatic--in-progress buf)))
+
+(defun preview-automatic--kill-ours (process)
+  "Kill PROCESS if it belongs to an ongoing `preview-automatic' job.
+Sets `preview-abort-flag' in `TeX-command-buffer' so the killed run's
+output is discarded.  Return non-nil if PROCESS was ours."
+  (when (preview-automatic--ours-p process)
+    (when-let* ((proc-buffer (process-buffer process))
+                ((buffer-live-p proc-buffer)))
+      (with-current-buffer proc-buffer
+        (setq-local preview-automatic--in-progress 'aborted)
+        (when (local-variable-p 'TeX-command-buffer)
+          (with-current-buffer TeX-command-buffer
+            (setq preview-abort-flag t)))))
+    (delete-process process)
+    t))
+
+(defun preview-automatic--tag-for-resume (process buffer pt)
+  "Tag PROCESS to resume `preview-automatic-update' at PT in BUFFER.
+Safe to call repeatedly."
+  (let ((already-tagged (consp (process-get process 'preview-automatic))))
+    (process-put process 'preview-automatic (list buffer pt))
+    (unless already-tagged
+      (add-function :after (process-sentinel process)
+                    #'preview-automatic--after-process
+                    '((name . preview-automatic)
+                      (depth . -99))))))
+
+(defun preview-automatic--interrupt-for-edit (buffer pt)
+  "Interrupt any ongoing `preview-automatic' process for BUFFER.
+Tags it to resume at PT once it is gone."
+  (when (buffer-live-p buffer)
+    (with-current-buffer buffer
+      (when-let* ((cur-process
+                   (or (get-buffer-process (TeX-process-buffer-name
+                                            (TeX-region-file)))
+                       (get-buffer-process (TeX-process-buffer-name
+                                           (TeX-master-file))))))
+        (preview-automatic--tag-for-resume cur-process buffer pt)
+        (preview-automatic--kill-ours cur-process)))))
+
+(defun preview-automatic--update-1 (buffer pt)
+  "Update preview at PT in BUFFER."
+  (when (buffer-live-p buffer)
+    (with-current-buffer buffer
+      (if (or (get-buffer-process (TeX-process-buffer-name (TeX-region-file)))
+              (get-buffer-process (TeX-process-buffer-name (TeX-master-file))))
+          ;; Safety net; `preview-automatic--interrupt-for-edit' already
+          ;; handles this on every edit.
+          (preview-automatic--interrupt-for-edit buffer pt)
+        (when-let* ((region
+                     (save-excursion
+                       (goto-char pt)
+                       (let* ((cur (cl-find-if
+                                    (lambda (ov)
+                                      (eq (overlay-get ov 'category)
+                                          'preview-overlay))
+                                    (overlays-at (point))))
+                              (region (or
+                                       (and cur
+                                            (eq
+                                             (overlay-get cur 'preview-state)
+                                             'disabled))
+                                       (and (not cur)
+                                            preview-automatic-function
+                                            (funcall
+                                             preview-automatic-function)))))
+                         (when region
+                           (if (consp region)
+                               region
+                             (cons (preview-next-border t)
+                                   (preview-next-border nil))))))))
+          (preview--silent t
+            (when-let* ((process (preview-region (car region)
+                                                 (cdr region))))
+              (with-current-buffer (process-buffer process)
+                (setq-local preview-automatic--in-progress t)
+                (setq-local preview-silent-errors t
+                            preview-locating-previews-message nil)))))))))
+
+(if (require 'timeout nil t)
+    (defalias 'preview-automatic-update
+      (timeout-debounced-func 'preview-automatic--update-1
+                              'preview-automatic-delay))
+  ;; `timeout' is not available, define debounced
+  ;; `preview-automatic-update' manually.
+  (defvar preview-automatic--update-timer nil
+    "Timer used for debouncing preview-automatic-update.")
+  (defun preview-automatic-update (buffer pt)
+    (:documentation (documentation 'preview-automatic--update-1))
+    (when preview-automatic--update-timer
+      (cancel-timer preview-automatic--update-timer)
+      (setq preview-automatic--update-timer nil))
+
+    (setq preview-automatic--update-timer
+          (run-with-idle-timer
+           preview-automatic-delay nil
+           #'preview-automatic--update-1
+           buffer pt))))
+
+(defun preview-automatic--after-change (&rest _)
+  "Ensure a preview is updated after buffer change, if needed."
+  ;; Kill un-debounced, on every edit, so a stale process never
+  ;; survives long enough to install a result for text that has
+  ;; already changed; starting a replacement is still debounced.
+  (preview-automatic--interrupt-for-edit (current-buffer) (point))
+  (preview-automatic-update (current-buffer) (point)))
+
+(defun preview-automatic--after-process (process _)
+  "Ensure a preview is updated after PROCESS terminates, if needed."
+  (when-let* ((buf (process-buffer process))
+              ((buffer-live-p buf)))
+    (with-current-buffer buf
+      (setq-local preview-automatic--in-progress nil)))
+  (when-let* ((args (process-get process 'preview-automatic)))
+    (when (consp args)
+      (apply #'preview-automatic-update args))))
+
+(defun preview-automatic--reschedule (orig-fn name file-fn &rest args)
+  "Preempt and reschedule any pending `preview-automatic' process.
+Wraps `TeX-command' as :around advice; NAME, FILE-FN and ARGS are
+`TeX-command's own arguments, forwarded to ORIG-FN unchanged."
+  (let ((buffer (current-buffer))
+        (pt (point))
+        (preempted nil))
+    (dolist (file (delq nil (delete-dups
+                             (list (ignore-errors (TeX-region-file))
+                                   (ignore-errors (TeX-master-file))))))
+      (let* ((process (get-buffer-process (TeX-process-buffer-name file)))
+             (proc-buf (and process (process-buffer process))))
+        (when (and process
+                   (process-live-p process)
+                   (preview-automatic--kill-ours process))
+          (setq preempted t)
+          ;; Clear the flag: ownership is passing to a genuine user command.
+          (when (and proc-buf (buffer-live-p proc-buf))
+            (with-current-buffer proc-buf
+              (setq-local preview-automatic--in-progress nil))))))
+    (prog1 (apply orig-fn name file-fn args)
+      (when (and preempted (buffer-live-p buffer))
+        (with-current-buffer buffer
+          (when-let* ((file (ignore-errors (funcall file-fn)))
+                      (process (get-buffer-process
+                                (TeX-process-buffer-name file))))
+            (preview-automatic--tag-for-resume process buffer pt)))))))
+
 ;;;###autoload
 (defun preview-report-bug () "Report a bug in the preview-latex package."
        (interactive)
-- 
2.50.1 (Apple Git-155)

>From ca875155efd6d4faa2bf1a80b4e5d6665edc32de Mon Sep 17 00:00:00 2001
From: Al Haji-Ali <[email protected]>
Date: Fri, 24 Jul 2026 00:09:27 +0100
Subject: [PATCH] preview: Stability improvements

* preview.el (preview--degenerate-box-p): New function.
(preview-gs-place): Save result of degenerate-box test; stash in
'queued.  Track stale-image filenames by reference count.
(preview--filenames-refcounted-p, preview--filenames-entry-string): New
functions.
(preview--delete-overlay-files): Allow excepting a file.
(preview-delete): Invalidate buframe popup before deleting files.
(preview--invalidate-buframe-for): New function; does nothing if a
replacement overlay already covers the same text.
(preview--update-buframe): Compare images by value; avoid needless
rebuilds.
(preview-dvi*-place-all, preview-gs-transact): Show a static icon for
degenerate boxes.  Fix preview-gs-transact issue with multiple
filenames.
(preview--fallback-bounds, preview--pm-place): New functions.
(preview-parse-messages): Fall back to an overlay's own bounds, or
skip, when context-string relocation fails.
---
 preview.el | 428 +++++++++++++++++++++++++++++++++--------------------
 1 file changed, 271 insertions(+), 157 deletions(-)

diff --git a/preview.el b/preview.el
index 37da8f1d..d11f584e 100644
--- a/preview.el
+++ b/preview.el
@@ -1413,8 +1413,10 @@ for the file extension."
                    (or ps-file
                        (format "preview.%03d" snippet))
                    tempdir))))
+  ;; cache the result of `preview--degenerate-box-p' here since it
+  ;; depends on buffer-local variables.
   (overlay-put ov 'queued
-               (vector box nil snippet))
+               (vector box nil snippet (preview--degenerate-box-p box)))
 
   (if-let* ((old-ov
              (and preview-keep-stale-images
@@ -1430,20 +1432,29 @@ for the file extension."
                      (overlays-at (overlay-start ov))))))))
       (let* ((img (overlay-get old-ov 'preview-image))
              (filename (cadr img))
-             (files-oov (overlay-get old-ov 'filenames))
-             (files-ov  (overlay-get ov  'filenames)))
+             (files-ov  (overlay-get ov  'filenames))
+             (entry (and filename
+                         (catch 'found
+                           (dolist (entry (overlay-get old-ov 'filenames))
+                             (let ((part (car entry)))
+                               (when (equal
+                                      (if (preview--filenames-refcounted-p
+                                           part)
+                                          (car part)
+                                        part)
+                                      filename)
+                                 (throw 'found entry))))
+                           nil))))
         (when img
-          (overlay-put ov 'preview-image img)
-          ;; Transfer filename ownership to new overlay.  The old one
-          ;; will be cleared out and its files deleted.
-          (when-let* ((entry (assoc filename files-oov)))
-            (overlay-put old-ov 'filenames
-                         (assq-delete-all filename files-oov))
-            ;; Add the filename to the current overlay instead
-            ;; if it's not already there
-            (unless (assoc filename files-ov)
-              (overlay-put ov 'filenames
-                           (cons entry files-ov))))))
+          ;; Use copy-tree to avoid `preview-replace-active-icon'
+          ;; being called on the old overlay mutating the image.
+          (overlay-put ov 'preview-image (copy-tree img))
+          (when (and entry (not (memq entry files-ov)))
+            ;; Bump the ref count, starting at 2 (old owner + this one).
+            (if (preview--filenames-refcounted-p (car entry))
+                (setcdr (car entry) (1+ (cdar entry)))
+              (setcar entry (cons (car entry) 2)))
+            (overlay-put ov 'filenames (cons entry files-ov)))))
     (overlay-put ov 'preview-image
                  (list (preview-icon-copy preview-nonready-icon))))
 
@@ -1583,21 +1594,19 @@ given as ANSWER."
         (when queued
           (let* ((bbox (aref queued 0))
                  (filenames (overlay-get ov 'filenames))
-                 (oldfile (nth 0 filenames))
-                 (newfile (nth 1 filenames)))
+                 (newfile (car (last filenames))))
             (if have-error
                 (preview-gs-flag-error ov answer)
-              (condition-case nil
-                  (preview-delete-file oldfile)
-                (file-error nil))
-              (overlay-put ov 'filenames (cdr filenames))
+              (preview--delete-overlay-files ov newfile)
               (preview-replace-active-icon
                ov
-               (preview-create-icon (car newfile)
-                                    preview-gs-image-type
-                                    (preview-ascent-from-bb
-                                     bbox)
-                                    (aref preview-colors 2))))
+               (if (aref queued 3) ;; Degenerate box
+                   (list (preview-icon-copy preview-error-icon))
+                 (preview-create-icon (car newfile)
+                                      preview-gs-image-type
+                                      (preview-ascent-from-bb
+                                       bbox)
+                                      (aref preview-colors 2)))))
             (overlay-put ov 'queued nil)))))
     (while (and (< (length preview-gs-outstanding)
                    preview-gs-outstanding-limit)
@@ -1824,6 +1833,19 @@ numbers (can be float if available)."
         (round (* 100.0 (/ (- top 720.0) (- top bottom))))
       100)))
 
+(defun preview--degenerate-box-p (box)
+  "Return non-nil if BOX would render at 1 device pixel or less.
+Happens for visually empty snippets (e.g. \\( \\)); installing such an
+image risks Emacs's image loader rejecting it (\"Invalid image size\")."
+  (and (vectorp box)
+       (= (length box) 4)
+       (let* ((width (- (aref box 2) (aref box 0)))
+              (height (- (aref box 3) (aref box 1)))
+              (dpi (or (car-safe preview-resolution) 72))
+              (scale (or preview-scale 1.0))
+              (px-per-pt (/ (* dpi scale) (* 72.0 (preview-get-magnification)))))
+         (or (<= (* width px-per-pt) 1) (<= (* height px-per-pt) 1)))))
+
 (defface preview-face '((((background dark))
                          (:background "dark slate gray"))
                         (t
@@ -2521,11 +2543,13 @@ active (`transient-mark-mode'), it is run through `preview-region'."
       (preview--delete-overlay-files ovr))
     (overlay-put ovr 'preview-state 'disabled)))
 
-(defun preview--delete-overlay-files (ovr)
+(defun preview--delete-overlay-files (ovr &optional except)
   "Delete files owned by OVR."
   (let ((filenames (overlay-get ovr 'filenames)))
-    (overlay-put ovr 'filenames nil)
-    (dolist (filename filenames)
+    (overlay-put ovr 'filenames (when (and except
+                                           (memq except filenames))
+                                  (list except)))
+    (dolist (filename (delq except filenames))
       (condition-case nil
           (preview-delete-file filename)
         (file-error nil)))))
@@ -2534,6 +2558,7 @@ active (`transient-mark-mode'), it is run through `preview-region'."
   "Delete preview overlay OVR, taking any associated file along.
 IGNORED arguments are ignored, making this function usable as a hook in
 some cases."
+  (preview--invalidate-buframe-for ovr)
   (preview--delete-overlay-files ovr)
   (delete-overlay ovr))
 
@@ -2767,11 +2792,13 @@ Deletes the dvi file when finished."
               (overlay-put ov 'filenames (list filename))
               (preview-replace-active-icon
                ov
-               (preview-create-icon (car filename)
-                                    preview-dvi*-image-type
-                                    (preview-ascent-from-bb
-                                     (aref queued 0))
-                                    (aref preview-colors 2)))
+               (if (aref queued 3) ;; Degenerate box
+                   (list (preview-icon-copy preview-error-icon))
+                 (preview-create-icon (car filename)
+                                      preview-dvi*-image-type
+                                      (preview-ascent-from-bb
+                                       (aref queued 0))
+                                      (aref preview-colors 2))))
               (overlay-put ov 'queued nil))
           (push filename oldfiles)
           ;; Do note modify `filenames' if we are not replacing
@@ -2844,6 +2871,19 @@ gets converted into a CONS-cell with a name and a reference count."
     (cons (expand-file-name file (nth 0 tempdir))
           tempdir)))
 
+(defun preview--filenames-refcounted-p (part)
+  "Return non-nil if PART is in refcounted form.
+Refcounted form is \\=(FILENAME . N)."
+  (and (consp part) (integerp (cdr part))))
+
+(defun preview--filenames-entry-string (entry)
+  "Return the file name, or list of file names, for `filenames' ENTRY.
+Unwrapped from the refcounted \(... . N) form if present."
+  (let ((part (car entry)))
+    (if (preview--filenames-refcounted-p part)
+        (car part)
+      part)))
+
 (defun preview-attach-filename (attached file)
   "Attaches the absolute file name ATTACHED to FILE."
   (if (listp (caar file))
@@ -3624,6 +3664,39 @@ displayed."
 
 (defvar preview-locating-previews-message "locating previews...")
 
+(defun preview--fallback-bounds (pos)
+  "Return (BEG . END) of an existing preview overlay at or near POS, or nil.
+Used by `preview-parse-messages' as a placement anchor when
+context-string relocation fails."
+  (let ((ov (or (cl-find-if (lambda (o) (overlay-get o 'preview-state))
+                             (overlays-at pos))
+                (cl-find-if (lambda (o) (overlay-get o 'preview-state))
+                            (overlays-in (line-beginning-position)
+                                         (line-end-position))))))
+    (and ov (cons (overlay-start ov) (overlay-end ov)))))
+
+(defun preview--pm-place (snippet region-beg region-end box lcounters counters
+                                   tempdir open-data point-current)
+  "Place a preview overlay for SNIPPET between REGION-BEG and REGION-END.
+Helper for `preview-parse-messages'.  BOX, LCOUNTERS, COUNTERS,
+TEMPDIR, OPEN-DATA and POINT-CURRENT are as in that function.
+Returns the OVL list to append to `close-data'."
+  (let (ovl)
+    (save-excursion
+      (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)))
+      (when (and (eq preview-visibility-style 'always)
+                 (<= 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))
+
 (defun preview-parse-messages (open-closure)
   "Turn all preview snippets into overlays.
 This parses the pseudo error messages from the preview
@@ -3634,7 +3707,7 @@ call, and in its CDR the final stuff for the placement hook."
   (with-temp-message preview-locating-previews-message
     (let (TeX-error-file TeX-error-offset snippet box counters
           file line
-          (lsnippet 0) lstart (lfile "") lline lbuffer lpoint
+          (lsnippet 0) lstart lstart-failed reloc-failed (lfile "") lline lbuffer lpoint
           lpos
           lcounters
           string after-string
@@ -3866,7 +3939,8 @@ name(\\([^)]+\\))\\)\\|\
                   (setq lfile file))
                 (save-excursion
                   (save-restriction
-                    (setq point-current (point))
+                    (setq point-current (point)
+                          reloc-failed nil)
                     (widen)
                     ;; a fast hook might have positioned us already:
                     (if (number-or-marker-p string)
@@ -3912,89 +3986,99 @@ name(\\([^)]+\\))\\)\\|\
                            (= lline line)
                            (goto-char (max (point) (- (1+ lpos) (length string)))))
 
-                      (cond
-                       ((search-forward (concat string after-string)
-                                        (line-end-position) t)
-                        (backward-char (length after-string)))
-                       ;;ok, transform ^^ sequences
-                       ((search-forward-regexp
-                         (concat "\\("
-                                 (setq string
-                                       (preview-error-quote
-                                        string))
-                                 "\\)"
-                                 (setq after-string
-                                       (preview-error-quote
-                                        after-string)))
-                         (line-end-position) t)
-                        (goto-char (match-end 1)))
-                       ((search-forward-regexp
-                         (concat "\\("
-                                 (if (string-match
-                                      "^[^\0-\177]\\{1,6\\}" string)
-                                     (setq string
-                                           (substring string (match-end 0)))
-                                   string)
-                                 "\\)"
-                                 (if (string-match
-                                      "[^\0-\177]\\{1,6\\}$" after-string)
-                                     (setq after-string
-                                           (substring after-string
-                                                      0 (match-beginning 0)))))
-                         (line-end-position) t)
-                        (goto-char (match-end 1)))
-                       (t (search-forward-regexp
-                           string
-                           (line-end-position) t))))
+                      (let ((pos-before-search (point)))
+                        (cond
+                         ((search-forward (concat string after-string)
+                                          (line-end-position) t)
+                          (backward-char (length after-string)))
+                         ;;ok, transform ^^ sequences
+                         ((search-forward-regexp
+                           (concat "\\("
+                                   (setq string
+                                         (preview-error-quote
+                                          string))
+                                   "\\)"
+                                   (setq after-string
+                                         (preview-error-quote
+                                          after-string)))
+                           (line-end-position) t)
+                          (goto-char (match-end 1)))
+                         ((search-forward-regexp
+                           (concat "\\("
+                                   (if (string-match
+                                        "^[^\0-\177]\\{1,6\\}" string)
+                                       (setq string
+                                             (substring string (match-end 0)))
+                                     string)
+                                   "\\)"
+                                   (if (string-match
+                                        "[^\0-\177]\\{1,6\\}$" after-string)
+                                       (setq after-string
+                                             (substring after-string
+                                                        0 (match-beginning 0)))))
+                           (line-end-position) t)
+                          (goto-char (match-end 1)))
+                         (t (search-forward-regexp
+                             string
+                             (line-end-position) t)))
+                        ;; If point didn't move, the compile-time context
+                        ;; no longer occurs on this line (buffer edited
+                        ;; mid-compile); a region computed from here
+                        ;; would be bogus.
+                        (setq reloc-failed (= pos-before-search (point)))))
                     (setq lline line
                           lpos (point)
                           lbuffer (current-buffer))
                     (if box
                         (progn
-                          (if (and lstart (= snippet lsnippet))
-                              (let* ((region-beg
-                                      (save-excursion
-                                        (preview-back-command
-                                         (= (prog1 (point)
-                                              (goto-char lstart))
-                                            lstart))
-                                        (point)))
-                                     (region-end
-                                      (if preview-find-end-function
-                                          (funcall preview-find-end-function
-                                                   region-beg)
-                                        (point)))
-                                     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
-                                         (eq preview-visibility-style 'always)
-                                         (<= 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))))
+                          (cond
+                           ((and lstart (= snippet lsnippet)
+                                 (not lstart-failed) (not reloc-failed))
+                            (let ((region-beg
+                                   (save-excursion
+                                     (preview-back-command
+                                      (= (prog1 (point)
+                                           (goto-char lstart))
+                                         lstart))
+                                     (point))))
+                              (setq close-data
+                                    (nconc (preview--pm-place
+                                            snippet region-beg
+                                            (if preview-find-end-function
+                                                (funcall
+                                                 preview-find-end-function
+                                                 region-beg)
+                                              (point))
+                                            box lcounters counters
+                                            tempdir open-data point-current)
+                                           close-data))))
+                           ((and lstart (= snippet lsnippet)
+                                 (preview--fallback-bounds (or lstart (point))))
+                            ;; Relocation failed; anchor to the existing
+                            ;; overlay's own bounds instead.
+                            (let ((bounds (preview--fallback-bounds
+                                           (or lstart (point)))))
+                              (setq close-data
+                                    (nconc (preview--pm-place
+                                            snippet (car bounds) (cdr bounds)
+                                            box lcounters counters
+                                            tempdir open-data point-current)
+                                           close-data))))
+                           ((and lstart (= snippet lsnippet))
+                            ;; No overlay to anchor to either; skip and
+                            ;; let a later, uninterrupted compile place it.
+                            nil)
+                           (t
                             (with-current-buffer run-buffer
                               (preview-log-error
                                (list 'error
                                      (format
                                       "End of Preview snippet %d unexpected"
-                                      snippet)) "Parser")))
-                          (setq lstart nil))
+                                      snippet)) "Parser"))))
+                          (setq lstart nil lstart-failed nil))
                       ;; else-part of if box
-                      (setq lstart (point) lcounters counters)
+                      (setq lstart (point) lstart-failed reloc-failed
+                            lcounters counters)
                       ;; >= because snippets in between might have
                       ;; been ignored because of TeX-default-extension
                       (unless (>= snippet (1+ lsnippet))
@@ -4605,15 +4689,35 @@ If not a regular release, the date of the last change.")
                                                  &optional enable))
 (declare-function buframe-find "ext:buframe"
                   (&optional frame-or-name buffer parent noerror))
+(declare-function buframe-update "ext:buframe" (frame-or-name))
 
 (defvar preview--buframe-error t
   "When non-nil, issue a one-time error if loading buframe fails.")
 
+(defun preview--invalidate-buframe-for (ovr)
+  "Hide the buframe popup if it is currently showing OVR's content.
+Call before OVR's backing image file(s) are removed, so a cached but
+untouched popup doesn't repaint from a now-missing file later.  Does
+nothing if a replacement overlay already covers the same text (e.g.
+OVR is being superseded during a recompile): `preview--update-buframe'
+will pick that one up on its own, without a visible hide/rebuild."
+  (when (featurep 'buframe)
+    (when-let* ((frame (buframe-find "auctex-preview" nil nil t))
+                (info (frame-parameter frame 'auctex-preview))
+                ((eq (car info) ovr))
+                ((not (cl-find-if
+                       (lambda (o) (and (not (eq o ovr))
+                                        (overlay-get o 'preview-image)))
+                       (overlays-in (overlay-start ovr) (overlay-end ovr))))))
+      (set-frame-parameter frame 'auctex-preview nil)
+      (buframe-disable frame))))
+
 (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."
+Only rebuilds when the `buframe' property or `preview-image' changed,
+comparing the image by value since `preview-replace-active-icon'
+mutates it in place.  FORCE repositions and shows the frame regardless,
+but does not by itself force the more expensive rebuild."
   (let* ((frame-name "auctex-preview")
          (buf-name " *auctex-preview-buffer*")
          (old-frame (and (featurep 'buframe)
@@ -4624,53 +4728,63 @@ unless FORCE is non-nil."
                    (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)))))
-          (if (require 'buframe nil t)
-              (let* ((buf (buframe-make-buffer
-                           buf-name
-                           (car-safe
-                            (cddr (cdr-safe
-                                   preview-at-point-placement)))))
-                     (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-at-point-placement))
-                              #'buframe-position-right-of-overlay)
-                          frame
-                          ov))
-                       buf
-                       (overlay-buffer ov)
-                       (window-frame)
-                       (car-safe (cdr (cdr-safe
-                                       preview-at-point-placement)))))
-                (set-frame-parameter old-frame 'auctex-preview
-                                     (cons ov str)))
-            (when preview--buframe-error
-              (setq preview--buframe-error nil)
-              (error "buframe is unavailable for use with preview. \
-See `preview-at-point-placement'."))))
+        (let* ((image (overlay-get ov 'preview-image))
+               (same (and old-frame
+                         (pcase (frame-parameter old-frame 'auctex-preview)
+                           (`(,o ,s ,img)
+                            (and (eq o ov) (eq s str) (equal img image)))))))
+          (when (or (not same) force)
+            ;; Require buframe (without autoloading it if it was never
+            ;; loaded to begin with) before calling any of its
+            ;; functions.
+            (if (not (require 'buframe nil t))
+                (when preview--buframe-error
+                  (setq preview--buframe-error nil)
+                  (error "buframe is unavailable for use with preview. \
+See `preview-at-point-placement'."))
+              (if same
+                  (buframe-update old-frame)
+                (let* ((buf (buframe-make-buffer
+                             buf-name
+                             (car-safe
+                              (cddr (cdr-safe
+                                     preview-at-point-placement)))))
+                       (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-at-point-placement))
+                                #'buframe-position-right-of-overlay)
+                            frame
+                            ov))
+                         buf
+                         (overlay-buffer ov)
+                         (window-frame)
+                         (car-safe (cdr (cdr-safe
+                                         preview-at-point-placement)))))
+                  ;; `copy-tree' the image spec: `preview-replace-active-icon'
+                  ;; mutates it in place.
+                  (set-frame-parameter
+                   old-frame 'auctex-preview
+                   (list ov str (copy-tree image))))))))
       (when old-frame
         (set-frame-parameter old-frame 'auctex-preview nil)
         (buframe-disable old-frame)))))
-- 
2.50.1 (Apple Git-155)

Attachment: texmathp-ex.el
Description: application/emacs-lisp

_______________________________________________
bug-auctex mailing list
[email protected]
https://lists.gnu.org/mailman/listinfo/bug-auctex

Reply via email to