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)
texmathp-ex.el
Description: application/emacs-lisp
_______________________________________________ bug-auctex mailing list [email protected] https://lists.gnu.org/mailman/listinfo/bug-auctex
