From d60d5bef78841d548cfbcdca3a461cb9c52e4314 Mon Sep 17 00:00:00 2001 From: Karthik Chikmagalur Date: Fri, 23 Aug 2024 15:46:53 -0700 Subject: [PATCH] org-link: Move inline image display to org-link Make inline image previews a part of a more universal org-link preview feature. Each link type can now be previewed differently based on a new link parameter. * lisp/ol.el (org-link-parameters, org-link-preview-overlays, org-link-preview--get-overlays, org-link-preview--remove-overlay, org-link-previeworg-link-preview-region, org-link-preview-clear, org-link-preview-file): Add new commands `org-link-preview', `org-link-preview-region' and `org-link-preview-clear' for creating link previews for any kind of link. Add new org-link parameter `:preview' for specifying how a link type should be previewed. File links and attachments are previewed using inline image previews as before. * testing/lisp/test-org-fold.el: Use `org-link-preview'. * lisp/org.el (org-toggle-inline-images, org-toggle-inline-images-command, org-display-inline-images, org--inline-image-overlays, org-inline-image-overlays, org-redisplay-inline-images, org-image-align, org-display-inline-remove-overlay, org-remove-inline-images): Obsolete and move `org-toggle-inline-images', `org-display-inline-images' and `org-redisplay-inline-images' to org-compat. These are obsoleted by `org-link-preview' and `org-link-preview-region'. Remove `org-toggle-inline-images-command'. Move the other internal functions to org-link. * lisp/org-plot.el (org-plot/redisplay-img-in-buffer): Modify to use `org-link-preview'. * lisp/org-keys.el: Bind `C-c C-x C-v' to new command `org-link-preview', which has the same prefix arg behaviors as `org-latex-preview'. In addition to these, it supports numeric prefix args 1 and 11 to preview links with descriptions at point/region (with 1) and across the buffer (with 11). * lisp/org-cycle.el (org-cycle-display-inline-images): Use `org-link-preview'. * lisp/org-compat.el (org-display-inline-remove-overlay, org--inline-image-overlays, org-remove-inline-images, org-inline-image-overlays, org-display-inline-images, org-toggle-inline-images): * lisp/org-attach.el: Add new `:preview' link parameter for links of type "attachment". --- lisp/ol.el | 280 ++++++++++++++++++++++++++++++++- lisp/org-attach.el | 10 +- lisp/org-compat.el | 189 +++++++++++++++++++++++ lisp/org-cycle.el | 10 +- lisp/org-keys.el | 4 +- lisp/org-plot.el | 2 +- lisp/org.el | 283 +--------------------------------- testing/lisp/test-org-fold.el | 4 +- 8 files changed, 489 insertions(+), 293 deletions(-) diff --git a/lisp/ol.el b/lisp/ol.el index 52ea62d69..baed73daa 100644 --- a/lisp/ol.el +++ b/lisp/ol.el @@ -82,6 +82,13 @@ (declare-function org-src-source-buffer "org-src" ()) (declare-function org-src-source-type "org-src" ()) (declare-function org-time-stamp-format "org" (&optional long inactive)) (declare-function outline-next-heading "outline" ()) +(declare-function image-flush "image" (spec &optional frame)) +(declare-function org-entry-end-position "org" ()) +(declare-function org-element-contents-begin "org-element" (node)) +(declare-function org-attach-expand "org-attach" (file)) +(declare-function org-display-inline-image--width "org" (link)) +(declare-function org-image--align "org" (link)) +(declare-function org--create-inline-image "org" (file width)) ;;; Customization @@ -171,6 +178,16 @@ (defcustom org-link-parameters nil The default face is `org-link'. +`:preview' + + Function to run to generate an in-buffer preview for the link. It + must accept three arguments: + - an overlay placed from the start to the end of the link. + - the link path, as a string. + - the link element + + This function must return a non-nil value to indicate success. + `:help-echo' String or function used as a value for the `help-echo' text @@ -649,6 +666,13 @@ (defvar org-link--insert-history nil (defvar org-link--search-failed nil "Non-nil when last link search failed.") +(defvar-local org-link-preview-overlays nil) +;; Preserve when switching modes or when restarting Org. +;; If we clear the overlay list and later enable Or mode, the existing +;; image overlays will never be cleared by `org-link-preview' +;; and `org-link-preview-clear'. +(put 'org-link-preview-overlays 'permanent-local t) + ;;; Internal Functions @@ -881,6 +905,30 @@ (defun org-link--file-link-to-here () (setq desc search-desc)))) (cons link desc))) +(defun org-link-preview--get-overlays (&optional beg end) + "Return link preview overlays between BEG and END." + (let* ((beg (or beg (point-min))) + (end (or end (point-max))) + (overlays (overlays-in beg end)) + result) + (dolist (ov overlays result) + (when (memq ov org-link-preview-overlays) + (push ov result))))) + +(defun org-link-preview--remove-overlay (ov after _beg _end &optional _len) + "Remove link-preview overlay OV if a corresponding region is modified. + +AFTER is true when this function is called post-change." + (when (and ov after) + (setq org-link-preview-overlays (delete ov org-link-preview-overlays)) + ;; Clear image from cache to avoid image not updating upon + ;; changing on disk. See Emacs bug#59902. + (when-let* (((overlay-get ov 'org-image-overlay)) + (disp (overlay-get ov 'display)) + ((imagep disp))) + (image-flush disp)) + (delete-overlay ov))) + ;;; Public API @@ -1573,6 +1621,194 @@ (defun org-link-add-angle-brackets (s) (unless (equal (substring s -1) ">") (setq s (concat s ">"))) s) +;;;###autoload +(defun org-link-preview (&optional arg beg end) + "Toggle display of link previews in the buffer. + +When region BEG..END is active, preview links in the +region. + +When point is at a link, display a preview for that link only. +Otherwise, display previews for links in current entry. + +With numeric prefix ARG 1, preview links with description as +well. + +With prefix ARG `\\[universal-argument]', clear link previews at +point or in the current entry. + +With prefix ARG `\\[universal-argument] \\[universal-argument]', + display link previews in the accessible portion of the + buffer. With numeric prefix ARG 11, do the same, but include + links with descriptions. + +With prefix ARG `\\[universal-argument] \\[universal-argument] \\[universal-argument]', +hide all link previews in the accessible portion of the buffer. + +This command is designed for interactive use. From Elisp, you can +also use `org-link-preview-region'." + (interactive (cons current-prefix-arg + (when (use-region-p) + (list (region-beginning) (region-end))))) + (let* ((include-linked + (cond + ((member arg '(nil (4) (16)) ) nil) + ((member arg '(1 11)) 'include-linked) + (t 'include-linked))) + (interactive? (called-interactively-p 'any)) + (toggle-previews + (lambda (&optional beg end scope remove) + (let* ((beg (or beg (point-min))) + (end (or end (point-max))) + (old (org-link-preview--get-overlays beg end)) + (scope (or scope (format "%d:%d" beg end)))) + (if remove + (progn + (org-link-preview-clear beg end) + (when interactive? + (message + "[%s] Inline link previews turned off (removed %d images)" + scope (length old)))) + (org-link-preview-region include-linked t beg end) + (when interactive? + (let ((new (org-link-preview--get-overlays beg end))) + (message + (if new + (format "[%s] %d images displayed inline %s" + scope (length new) + (if include-linked "(including images with description)" + "")) + (format "[%s] No images to display inline" scope)))))))))) + (cond + ;; Region selected :: display previews in region. + ((and beg end) + (funcall toggle-previews beg end "region" + (and (equal arg '(4)) 'remove))) + ;; C-u argument: clear image at point or in entry + ((equal arg '(4)) + (if (get-char-property (point) 'org-image-overlay) + ;; clear link preview at point + (when-let ((context (org-element-context)) + ((org-element-type-p context 'link))) + (funcall toggle-previews + (org-element-begin context) + (org-element-end context) + "preview at point" 'remove)) + ;; Clear link previews in entry + (funcall toggle-previews + (if (org-before-first-heading-p) (point-min) + (save-excursion + (org-with-limited-levels (org-back-to-heading t) (point)))) + (org-with-limited-levels (org-entry-end-position)) + "current section" 'remove))) + ;; C-u C-u or C-11 argument :: display images in the whole buffer. + ((member arg '(11 (16))) (funcall toggle-previews nil nil "buffer")) + ;; C-u C-u C-u argument :: unconditionally hide images in the buffer. + ((equal arg '(64)) (funcall toggle-previews nil nil "buffer" 'remove)) + ;; Argument nil or 1, no region selected :: display images in + ;; current section or image link at point. + ((and (member arg '(nil 1)) (null beg) (null end)) + (let ((context (org-element-context))) + ;; toggle display of inline image link at point. + (if (org-element-type-p context 'link) + (let* ((ov (cdr-safe (get-char-property-and-overlay + (point) 'org-image-overlay))) + (remove? (and ov (memq ov org-link-preview-overlays) + 'remove))) + (funcall toggle-previews + (org-element-begin context) + (org-element-end context) + "image at point" remove?)) + (let ((beg (if (org-before-first-heading-p) (point-min) + (save-excursion + (org-with-limited-levels (org-back-to-heading t) (point))))) + (end (org-with-limited-levels (org-entry-end-position)))) + (funcall toggle-previews beg end "current section"))))) + ;; Any other non-nil argument. + ((not (null arg)) (funcall toggle-previews beg end "region"))))) + +(defun org-link-preview-region (&optional include-linked refresh beg end) + "Display link previews. + +A previewable link type is one that has a `:preview' link +parameter, see `org-link-parameters'. + +By default, a file link or attachment is previewable if it +follows either of these conventions: + + 1. Its path is a file with an extension matching return value + from `image-file-name-regexp' and it has no contents. + + 2. Its description consists in a single link of the previous + type. In this case, that link must be a well-formed plain + or angle link, i.e., it must have an explicit \"file\" or + \"attachment\" type. + +File links are equipped with the keymap `image-map'. + +When optional argument INCLUDE-LINKED is non-nil, links with a +text description part will also be inlined. This can be nice for +a quick look at those images, but it does not reflect what +exported files will look like. + +When optional argument REFRESH is non-nil, refresh existing +images between BEG and END. This will create new image displays +only if necessary. + +BEG and END define the considered part. They default to the +buffer boundaries with possible narrowing." + (interactive "P") + (when (display-graphic-p) + (when refresh (org-link-preview-clear beg end)) + (when (fboundp 'clear-image-cache) (clear-image-cache))) + (org-with-point-at (or beg (point-min)) + (let ((case-fold-search t)) + (while (re-search-forward org-link-any-re end t) + (when-let* ((link (org-element-lineage + (save-match-data (org-element-context)) + 'link t)) + (linktype (org-element-property :type link)) + (preview-func (org-link-get-parameter linktype :preview)) + (path (and (or include-linked + (not (org-element-contents-begin link))) + (org-element-property :path link)))) + ;; Create an overlay to hold the preview + (let ((ov (make-overlay + (org-element-begin link) + (progn + (goto-char + (org-element-end link)) + (unless (eolp) (skip-chars-backward " \t")) + (point))))) + ;; TODO: Change this overlay property to `org-link-preview' everywhere. + (overlay-put ov 'org-image-overlay t) + (overlay-put ov 'modification-hooks + (list 'org-link-preview--remove-overlay)) + ;; call preview function for link type + (if (funcall preview-func ov path link) + (when (overlay-buffer ov) + (push ov org-link-preview-overlays)) + ;; Preview was unsuccessful, delete overlay + (delete-overlay ov)))))))) + +(defun org-link-preview-clear (&optional beg end) + "Clear link previews in region BEG to END." + (interactive (and (use-region-p) (list (region-beginning) (region-end)))) + (let* ((beg (or beg (point-min))) + (end (or end (point-max))) + (overlays (overlays-in beg end))) + (dolist (ov overlays) + (when (memq ov org-link-preview-overlays) + (when-let ((image (overlay-get ov 'display)) + ((imagep image))) + (image-flush image)) + (setq org-link-preview-overlays (delq ov org-link-preview-overlays)) + (delete-overlay ov))) + ;; Clear removed overlays. + (dolist (ov org-link-preview-overlays) + (unless (overlay-buffer ov) + (setq org-link-preview-overlays (delq ov org-link-preview-overlays)))))) + ;;; Built-in link types @@ -1595,7 +1831,49 @@ (defun org-link--open-elisp (path _) (org-link-set-parameters "elisp" :follow #'org-link--open-elisp) ;;;; "file" link type -(org-link-set-parameters "file" :complete #'org-link-complete-file) +(org-link-set-parameters "file" + :complete #'org-link-complete-file + :preview #'org-link-preview-file) + +(defun org-link-preview-file (ov path link) + "Display image file PATH in overlay OV for LINK. + +LINK is the Org element being previewed. + +Equip each image with the keymap `image-map'. + +This is intended to be used as the `:preview' link property of +file links, see `org-link-parameters'." + (if (not (display-graphic-p)) + (prog1 nil + (message "Your Emacs does not support displaying images!")) + (when-let* ((file-full (expand-file-name path)) + (file (substitute-in-file-name file-full)) + ((string-match-p (image-file-name-regexp) file)) + ((file-exists-p file))) + (let* ((width (org-display-inline-image--width link)) + (align (org-image--align link)) + (image (org--create-inline-image file width))) + (when image ; Add image to overlay + ;; See bug#59902. We cannot rely + ;; on Emacs to update image if the file + ;; has changed. + (image-flush image) + (overlay-put ov 'display image) + (overlay-put ov 'face 'default) + (overlay-put ov 'org-image-overlay t) + (when (boundp 'image-map) + (overlay-put ov 'keymap image-map)) + (when align + (overlay-put + ov 'before-string + (propertize + " " 'face 'default + 'display + (pcase align + ("center" `(space :align-to (- center (0.5 . ,image)))) + ("right" `(space :align-to (- right ,image))))))) + t))))) ;;;; "help" link type (defun org-link--open-help (path _) diff --git a/lisp/org-attach.el b/lisp/org-attach.el index 7a03d170e..4b0887fcd 100644 --- a/lisp/org-attach.el +++ b/lisp/org-attach.el @@ -797,9 +797,17 @@ (defun org-attach-follow (file arg) See `org-open-file' for details about ARG." (org-link-open-as-file (org-attach-expand file) arg)) +(defun org-attach-preview-file (ov path link) + "Preview attachment with PATH in overlay OV. + +LINK is the Org link element being previewed." + (org-link-preview-file + ov (org-attach-expand path) link)) + (org-link-set-parameters "attachment" :follow #'org-attach-follow - :complete #'org-attach-complete-link) + :complete #'org-attach-complete-link + :preview #'org-attach-preview-file) (defun org-attach-complete-link () "Advise the user with the available files in the attachment directory." diff --git a/lisp/org-compat.el b/lisp/org-compat.el index d843216f3..242b46a86 100644 --- a/lisp/org-compat.el +++ b/lisp/org-compat.el @@ -783,6 +783,195 @@ (defun org-add-link-type (type &optional follow export) (make-obsolete 'org-add-link-type "use `org-link-set-parameters' instead." "9.0") +(declare-function org-link-preview--remove-overlay "ol") +(declare-function org-link-preview--get-overlays "ol") +(declare-function org-link-preview-clear "ol") +(declare-function org-link-preview--remove-overlay "ol") + +(define-obsolete-function-alias 'org-display-inline-remove-overlay + 'org-link-preview--remove-overlay "9.8") +(define-obsolete-function-alias 'org--inline-image-overlays + 'org-link-preview--get-overlays "9.8") +(define-obsolete-function-alias 'org-remove-inline-images + 'org-link-preview-clear "9.8") +(define-obsolete-variable-alias 'org-inline-image-overlays + 'org-link-preview-overlays "9.8") +(defvar org-link-preview-overlays) +(defvar org-link-abbrev-alist-local) +(defvar org-link-abbrev-alist) +(defvar org-link-angle-re) +(defvar org-link-plain-re) +(declare-function org-attach-expand "org-attach") +(declare-function org-display-inline-image--width "org") +(declare-function org-image--align "org") +(declare-function org--create-inline-image "org") + +(make-obsolete 'org-display-inline-images + 'org-link-preview-region "9.8") +;; FIXME: Unused; obsoleted; to be removed +(defun org-display-inline-images (&optional include-linked refresh beg end) + "Display inline images. + +An inline image is a link which follows either of these +conventions: + + 1. Its path is a file with an extension matching return value + from `image-file-name-regexp' and it has no contents. + + 2. Its description consists in a single link of the previous + type. In this case, that link must be a well-formed plain + or angle link, i.e., it must have an explicit \"file\" or + \"attachment\" type. + +Equip each image with the key-map `image-map'. + +When optional argument INCLUDE-LINKED is non-nil, also links with +a text description part will be inlined. This can be nice for +a quick look at those images, but it does not reflect what +exported files will look like. + +When optional argument REFRESH is non-nil, refresh existing +images between BEG and END. This will create new image displays +only if necessary. + +BEG and END define the considered part. They default to the +buffer boundaries with possible narrowing." + (interactive "P") + (when (display-graphic-p) + (when refresh + (org-link-preview-clear beg end) + (when (fboundp 'clear-image-cache) (clear-image-cache))) + (let ((end (or end (point-max)))) + (org-with-point-at (or beg (point-min)) + (let* ((case-fold-search t) + (file-extension-re (image-file-name-regexp)) + (link-abbrevs (mapcar #'car + (append org-link-abbrev-alist-local + org-link-abbrev-alist))) + ;; Check absolute, relative file names and explicit + ;; "file:" links. Also check link abbreviations since + ;; some might expand to "file" links. + (file-types-re + (format "\\[\\[\\(?:file%s:\\|attachment:\\|[./~]\\)\\|\\]\\[\\(