From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from mp1.migadu.com ([2001:41d0:303:e16b::]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)) by ms13.migadu.com with LMTPS id 8PNoDdww6mbQSAAA62LTzQ:P1 (envelope-from ) for ; Wed, 18 Sep 2024 01:46:04 +0000 Received: from aspmx1.migadu.com ([2001:41d0:303:e16b::]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)) by mp1.migadu.com with LMTPS id 8PNoDdww6mbQSAAA62LTzQ (envelope-from ) for ; Wed, 18 Sep 2024 03:46:04 +0200 X-Envelope-To: larch@yhetil.org Authentication-Results: aspmx1.migadu.com; dkim=fail ("body hash did not verify") header.d=gmail.com header.s=20230601 header.b=dm5lEetB; dmarc=fail reason="SPF not aligned (relaxed)" header.from=gmail.com (policy=none); spf=pass (aspmx1.migadu.com: domain of "emacs-orgmode-bounces+larch=yhetil.org@gnu.org" designates 209.51.188.17 as permitted sender) smtp.mailfrom="emacs-orgmode-bounces+larch=yhetil.org@gnu.org" ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=yhetil.org; s=key1; t=1726623964; h=from:from:sender:sender:reply-to:subject:subject:date:date: message-id:message-id:to:to:cc:cc:mime-version:mime-version: content-type:content-type:in-reply-to:in-reply-to: references:references:list-id:list-help:list-unsubscribe: list-subscribe:list-post:dkim-signature; bh=QBnQ6oouzA6ql3A0wdp4+/0Jd8CefJ6Uggs/9CxyOAI=; b=kgWAyP4QFdy3ylOSE0qGWAyZNjYc77qyW9v7ZPOlyD7xxhxU/pSOWhQdE0wWarWz4gXp3d 7DLb2zbR7snhqut+WzdbnCMoTlGVN4bbauHFvZF9WyPJx6mvchWqVLaudnvEbnCnsbC8ct PdaHM1X9nZra4U9d5v5GiImOCSzY2Q+BUdrMBDKVbC+bfMVZYK1LigsAAE8ulWbgFA+RYn FRPJfejhef+zoGkbec52GEjicWblST7migcQd4SxmntnTFDT5ycM7kcRVOpMvGRClMN1sA S1S22IrL91qECuZaGskFjBkKxAhc0uCiq9+1YRitga5kkuDpAUyjrUdSq6tqbw== ARC-Seal: i=1; s=key1; d=yhetil.org; t=1726623964; a=rsa-sha256; cv=none; b=Am5vn4E2Bib7Sk62foQoLtdDDYJJOcSqpTcgUn2c9NN16K2RabhcNXhI0k6HGACM1fIhgT PPbD8v9PZeE014GamufhD3Icn2DU4xWHxwmdtJLjVUKAZ/IH4jIJxvh1l6y4nP7oK18kGS uzw0uXf7XR0TSB59IjroSCZaxQ+aTPySGn+woLaRSLk6pD1XTpNI9SfhmKNPU596CKjH2o j8YsubrE38uxZhaqiunRNrCqnoasQ1b0fVwl9RRAVjQGKR1HFdy2wg8dZoJ0oym3gnw3GV bd3AxKMkzWsl8SKAMDaE7Zl0P1lQKytbSU2bAOwlC1cIVM7MEi4Yf/BCY+hc1A== ARC-Authentication-Results: i=1; aspmx1.migadu.com; dkim=fail ("body hash did not verify") header.d=gmail.com header.s=20230601 header.b=dm5lEetB; dmarc=fail reason="SPF not aligned (relaxed)" header.from=gmail.com (policy=none); spf=pass (aspmx1.migadu.com: domain of "emacs-orgmode-bounces+larch=yhetil.org@gnu.org" designates 209.51.188.17 as permitted sender) smtp.mailfrom="emacs-orgmode-bounces+larch=yhetil.org@gnu.org" Received: from lists.gnu.org (lists.gnu.org [209.51.188.17]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by aspmx1.migadu.com (Postfix) with ESMTPS id A94729296 for ; Wed, 18 Sep 2024 03:46:03 +0200 (CEST) Received: from localhost ([::1] helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1sqjkm-0000TN-9D; Tue, 17 Sep 2024 21:45:12 -0400 Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1sqjkk-0000Ll-87 for emacs-orgmode@gnu.org; Tue, 17 Sep 2024 21:45:10 -0400 Received: from mail-pf1-x436.google.com ([2607:f8b0:4864:20::436]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_128_GCM_SHA256:128) (Exim 4.90_1) (envelope-from ) id 1sqjke-0007si-T1 for emacs-orgmode@gnu.org; Tue, 17 Sep 2024 21:45:09 -0400 Received: by mail-pf1-x436.google.com with SMTP id d2e1a72fcca58-718d91eef2eso219359b3a.1 for ; Tue, 17 Sep 2024 18:45:03 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20230601; t=1726623902; x=1727228702; darn=gnu.org; h=mime-version:message-id:date:references:in-reply-to:subject:cc:to :from:from:to:cc:subject:date:message-id:reply-to; bh=tMYgHrr12sfDFoUDnouiMBcghl+xuJdZhaZTA9+Qctc=; b=dm5lEetB9dlfFYGU3mUZnuFJUchJm67InH5aQrbv0bcZUEhoYXeommJIXtSaDQOdCG hyYuiJy+a8pbHpd4vmzRHwwpGOhAtq6RNB+RAK/zB8EVeG5mGze6AOBQbEDGGT04McUU +Ilsmj5r7rrsGAFBE35FQsDQ5BhPauMwmh/JHly5lxEXuvy210uuPvj+xNwC/8lUvCgT 32Uifct0kWpHEhqeQJsYriJ5MCQuiQlP/X7V9M1oCxDOL1G95gP262oyAYlcNKTWFhEJ 6IRHmMrYsKgxWZZcPMi82TPnEkFhE5xK/x0LlqcQ52TwcVDtXlYVyhb80J2v7y0f4uv2 Pwsg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1726623902; x=1727228702; h=mime-version:message-id:date:references:in-reply-to:subject:cc:to :from:x-gm-message-state:from:to:cc:subject:date:message-id:reply-to; bh=tMYgHrr12sfDFoUDnouiMBcghl+xuJdZhaZTA9+Qctc=; b=N0Fnj8Jz+wG9RapOjEWijCjdkk0WRm6t+Vk3A6HqyVqqAb5D97mD9ZTCaDVb4LaVp8 MK8ogizCfc63ficF0iXFxVTLHAA3+4tkfk/GsER159nJSJ+tnZ28QNdPcRaLcLhYGoip V8UTH/fB/MhZr5/bXWdFZ50b2h88feKA/s98k+1obWKg028JVMhCjZdCuEiZEDGuszqR gxsg1RoxucdutM2Xwo7HMIx1KbtNWEYIXzBBeBW2LQU31GqqjQlIo1kXwd1E2XOQilCA 6z2GvCi9AhEn9bhXgqLRnoCRpgjDsWbIidqgBxmKPIJfHvG2sziW1+4GZNRaJGSEcSIp n4/g== X-Forwarded-Encrypted: i=1; AJvYcCVHeGzzR6LRnVk/o+qHHPN1ObmIiX2zSCZJVhe6UiKDqwW0QSb+0VHnPNlOn0e82UTPZdr1IrWC1AursZKJ@gnu.org X-Gm-Message-State: AOJu0Yw6SEk59+oZU+qS3vxkgPGcIp/Vs6LWGXHviRCADB8ebagIMQ4U FuN9Ity03PQc5dtn3v6LjVCSTd1DYdZaVcXnLGp0tuK6goYIUEz3AL3Qtg== X-Google-Smtp-Source: AGHT+IGKCsZQMkQN+lJ2L/BHx51jC2SgMEd/I8fuaD+gMGJbyYjzCkjHbW3DUzIkvQN/uHIfN5osxg== X-Received: by 2002:a05:6a00:6f4d:b0:718:eeab:97ca with SMTP id d2e1a72fcca58-71907d98483mr48245096b3a.2.1726623901843; Tue, 17 Sep 2024 18:45:01 -0700 (PDT) Received: from localhost ([2600:8802:5726:2500:bda0:6c3a:916c:a73f]) by smtp.gmail.com with ESMTPSA id d2e1a72fcca58-71944b97608sm5752636b3a.153.2024.09.17.18.45.00 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Tue, 17 Sep 2024 18:45:00 -0700 (PDT) From: Karthik Chikmagalur To: Ihor Radchenko Cc: stardiviner , Org mode Subject: Re: [PATCH v5] Inline image display as part of a new org-link-preview system In-Reply-To: <87o74mjgcy.fsf@localhost> References: <6461a84b.a70a0220.b6d36.5d00@mx.google.com> <87o75yhwnu.fsf@localhost> <87v7zyyvm3.fsf@localhost> <87frr07xz8.fsf@gmail.com> <87cym38aj8.fsf@gmail.com> <87r0ajawgj.fsf@localhost> <87a5h77zb1.fsf@gmail.com> <87msl4wv8d.fsf@localhost> <875xrqg6cb.fsf@gmail.com> <874j70n559.fsf@localhost> <87msksabld.fsf@gmail.com> <87jzfwljkq.fsf@localhost> <87h6b09v4o.fsf@gmail.com> <878qwb8qw1.fsf@localhost> <878qw9ak6a.fsf@gmail.com> <87o74ypp3b.fsf@localhost> <87r09rxpjg.fsf@gmail.com> <87h6ah72ui.fsf@localhost> <8734m060ma.fsf@gmail.com> <87o74mjgcy.fsf@localhost> Date: Tue, 17 Sep 2024 18:44:59 -0700 Message-ID: <8734lx4twk.fsf@gmail.com> MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="=-=-=" Received-SPF: pass client-ip=2607:f8b0:4864:20::436; envelope-from=karthikchikmagalur@gmail.com; helo=mail-pf1-x436.google.com X-Spam_score_int: -20 X-Spam_score: -2.1 X-Spam_bar: -- X-Spam_report: (-2.1 / 5.0 requ) BAYES_00=-1.9, DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, FREEMAIL_FROM=0.001, RCVD_IN_DNSWL_NONE=-0.0001, SPF_HELO_NONE=0.001, SPF_PASS=-0.001 autolearn=ham autolearn_force=no X-Spam_action: no action X-BeenThere: emacs-orgmode@gnu.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: "General discussions about Org-mode." List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: emacs-orgmode-bounces+larch=yhetil.org@gnu.org Sender: emacs-orgmode-bounces+larch=yhetil.org@gnu.org X-Migadu-Country: US X-Migadu-Flow: FLOW_IN X-Migadu-Spam-Score: -3.31 X-Spam-Score: -3.31 X-Migadu-Scanner: mx13.migadu.com X-Migadu-Queue-Id: A94729296 X-TUID: ygDNi5GWi7S2 --=-=-= Content-Type: text/plain Next version of patch attached. > 1. I run preview on the whole buffer > 2. M-x list-timers shows the timer, and it is ugly: > * 10.0s - #f(compiled-function () # > > [...] > > 10+ sec] > - So, no closures please. Use normal named functions Done. Only one entry per buffer, `org-link-preview--timer', shows up in `list-timers' now. > - It may also be a good idea to manage preview queue globally, so > that subsequent preview requests can be merged automatically (see > below) > 3. It will take a long time to preview all those images, so I get > impatient and cancel the previews. The timer still remains... > 4. I request previews in the whole buffer again. Now, there are two > timers. This may be repeated, and the user may end up with many > useless idle timers - each of them will take a long time to end, > because "preview batches" do not care whether we actually try to > preview images or skip them because the underlying overlays were > "canceled" by another command - I think that the timers should not > group images in batches beforehand, but simply consume images from a > single queue until a given number of images are actually submitted > for preview generation (overlays are still present). New design: 1. Only one preview queue per buffer, stored in the buffer-local variable `org-link-preview--queue'. This queue is FIFO. 2. Only one (possibly periodically refreshed) preview timer per buffer, stored in the buffer-local variable `org-link-preview--timer'. The function `org-link-preview--process-queue' runs through this queue `org-link-preview-batch-size' items at a time, and sets `org-link-preview--timer'. No explicit batches are created. If you think it's a good idea, I can add pending previews to the queue in a LIFO fashion instead, so that if you call `org-link-preview' in two different sections before the first one is done previewing, the later one is processed first. If `org-link-preview-clear' is run in a region where there are previews pending, these region-specific previews are removed from the queue. Note that this new code in `org-link-preview-clear' is not actually necessary: (when (memq ov org-link-preview-overlays) ;; Remove pending preview tasks between BEG and END (when-let ((spec (cl-find ov org-link-preview--queue :key #'cadr))) (setq org-link-preview--queue (delq spec org-link-preview--queue))) This is because the overlay placed over the links for which previews are pending are removed anyway. So when the `--process-queue' function gets to that preview-pending link, it skips it since the overlay is gone. Also, the complexity of the above code is quadratic, so I can remove it if you think it's not required. I included it to be thorough. Also, I think the variable `org-link-preview-overlays' is actually unnecessary. We can just use an overlay property for link-preview overlays and use `overlays-in' and `org-find-overlays', and reduce the amount of global state a little bit this way. Let me know if I should remove it. > And please fix the compiler warnings. As of this patch, I don't see any flymake errors with the `elisp-flymake-byte-compile' backend. Is this not sufficient? Byte-compiling in my active Emacs session doesn't work as the state is "polluted". I don't know of a convenient way to byte-compile code in a sandbox. Please note: I need some help with code style, for example `org-link--preview-queue' vs `org-link-preview--queue', etc. Let's postpone this particular discussion until after the design is final? Karthik --=-=-= Content-Type: text/x-patch Content-Disposition: attachment; filename=0001-org-link-Customizable-preview-API-for-arbitrary-link.patch >From e35fdf594f34ca5b433013d0e7cfedc71b5321e7 Mon Sep 17 00:00:00 2001 From: Karthik Chikmagalur Date: Fri, 23 Aug 2024 15:46:53 -0700 Subject: [PATCH] org-link: Customizable preview API for arbitrary link types Add a customizable preview API for arbitrary link types. Make inline image previews a part of the 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-batch-size, org-link-preview-delay, org-link-preview--timer, org-link-preview--queue, org-link-preview-overlays, org-link-preview--get-overlays, org-link-preview--remove-overlay, org-link-preview, org-link-preview-region, org-link-preview--process-queue, org-link-preview-clear, org-link-preview-file, org-display-remote-inline-images, org-image-align, org--create-inline-image, org-display-inline-image--width, org-image--align): 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. This link parameter is a function called asynchronously to place previes. File links and attachments are previewed using inline image previews as before. Move image handling utilities from lisp/org.el to lisp/ol.el. * 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 (org-attach-preview-file): Add new `:preview' link parameter for links of type "attachment", set to the new function `org-attach-preview-file'. --- lisp/ol.el | 594 +++++++++++++++++++++++++++++++++- lisp/org-attach.el | 11 +- lisp/org-compat.el | 189 +++++++++++ lisp/org-cycle.el | 10 +- lisp/org-keys.el | 4 +- lisp/org-plot.el | 2 +- lisp/org.el | 534 +----------------------------- testing/lisp/test-org-fold.el | 4 +- 8 files changed, 803 insertions(+), 545 deletions(-) diff --git a/lisp/ol.el b/lisp/ol.el index 52ea62d69..b0d7294fe 100644 --- a/lisp/ol.el +++ b/lisp/ol.el @@ -82,6 +82,11 @@ (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-element-contents-end "org-element" (node)) +(declare-function org-property-or-variable-value "org" (var &optional inherit)) ;;; Customization @@ -171,6 +176,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 @@ -521,6 +536,80 @@ (defcustom org-link-keep-stored-after-insertion nil :type 'boolean :safe #'booleanp) +(defcustom org-link-preview-delay 0.05 + "Idle delay in seconds between link previews when using +`org-link-preview'. Links are previewed in batches (see +`org-link-preview-batch-size') spaced out by this delay. Set +this to a small number for more immediate previews, but at the +expense of higher lag." + :group 'org-link + :type 'number) + +(defcustom org-link-preview-batch-size 6 + "Number of links that are previewed at once with +`org-link-preview'. Links are previewed in batches spaced out in +time (see `org-link-preview-delay'). Set this to a large integer +for more immediate previews, but at the expense of higher lag." + :group 'org-link + :type 'natnum) + +(defcustom org-display-remote-inline-images 'skip + "How to display remote inline images. +Possible values of this option are: + +skip Don't display remote images. +download Always download and display remote images. +t +cache Display remote images, and open them in separate buffers + for caching. Silently update the image buffer when a file + change is detected." + :group 'org-appearance + :package-version '(Org . "9.7") + :type '(choice + (const :tag "Ignore remote images" skip) + (const :tag "Always display remote images" download) + (const :tag "Display and silently update remote images" cache)) + :safe #'symbolp) + +(defcustom org-image-max-width 'fill-column + "When non-nil, limit the displayed image width. +This setting only takes effect when `org-image-actual-width' is set to +t or when #+ATTR* is set to t. + +Possible values: +- `fill-column' :: limit width to `fill-column' +- `window' :: limit width to window width +- integer :: limit width to number in pixels +- float :: limit width to that fraction of window width +- nil :: do not limit image width" + :group 'org-appearance + :package-version '(Org . "9.7") + :type '(choice + (const :tag "Do not limit image width" nil) + (const :tag "Limit to `fill-column'" fill-column) + (const :tag "Limit to window width" window) + (integer :tag "Limit to a number of pixels") + (float :tag "Limit to a fraction of window width"))) + +(defcustom org-image-align 'left + "How to align images previewed using `org-link-preview-region'. + +Only stand-alone image links are affected by this setting. These +are links without surrounding text. + +Possible values of this option are: + +left Insert image at specified position. +center Center image previews. +right Right-align image previews." + :group 'org-appearance + :package-version '(Org . "9.7") + :type '(choice + (const :tag "Left align (or don\\='t align) image previews" left) + (const :tag "Center image previews" center) + (const :tag "Right align image previews" right)) + :safe #'symbolp) + ;;; Public variables (defconst org-target-regexp (let ((border "[^<>\n\r \t]")) @@ -649,6 +738,29 @@ (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) + +(defvar-local org-link-preview--timer nil + "Timer for previewing Org links in buffer. + +This timer creates previews for specs in +`org-link-preview--queue'.") + +(defvar-local org-link-preview--queue nil + "Queue of pending previews for Org links in buffer. + +Each element of this queue is a list of the form + +(PREVIEW-FUNC OVERLAY PATH LINK) + +where PREVIEW-FUNC places a preview of PATH using OVERLAY. LINK +is the Org element being previewed.") + ;;; Internal Functions @@ -881,7 +993,228 @@ (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 (delq 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))) + + +;;;; Utilities for image preview display + +;; For without-x builds. +(declare-function image-flush "image" (spec &optional frame)) + +(defun org--create-inline-image (file width) + "Create image located at FILE, or return nil. +WIDTH is the width of the image. The image may not be created +according to the value of `org-display-remote-inline-images'." + (let* ((remote? (file-remote-p file)) + (file-or-data + (pcase org-display-remote-inline-images + ((guard (not remote?)) file) + (`download (with-temp-buffer + (set-buffer-multibyte nil) + (insert-file-contents-literally file) + (buffer-string))) + ((or `cache `t) + (let ((revert-without-query '("."))) + (with-current-buffer (find-file-noselect file) + (buffer-string)))) + (`skip nil) + (other + (message "Invalid value of `org-display-remote-inline-images': %S" + other) + nil)))) + (when file-or-data + (create-image file-or-data + (and (image-type-available-p 'imagemagick) + width + 'imagemagick) + remote? + :width width + :max-width + (pcase org-image-max-width + (`fill-column (* fill-column (frame-char-width (selected-frame)))) + (`window (window-width nil t)) + ((pred integerp) org-image-max-width) + ((pred floatp) (floor (* org-image-max-width (window-width nil t)))) + (`nil nil) + (_ (error "Unsupported value of `org-image-max-width': %S" + org-image-max-width))) + :scale 1)))) + +(declare-function org-export-read-attribute "ox" + (attribute element &optional property)) +(defvar visual-fill-column-width) ; Silence compiler warning +(defun org-display-inline-image--width (link) + "Determine the display width of the image LINK, in pixels. +- When `org-image-actual-width' is t, the image's pixel width is used. +- When `org-image-actual-width' is a number, that value will is used. +- When `org-image-actual-width' is nil or a list, :width attribute of + #+attr_org or the first #+attr_... (if it exists) is used to set the + image width. A width of X% is divided by 100. If the value is a + float between 0 and 2, it interpreted as that proportion of the text + width in the buffer. + + If no :width attribute is given and `org-image-actual-width' is a + list with a number as the car, then that number is used as the + default value." + ;; Apply `org-image-actual-width' specifications. + ;; Support subtree-level property "ORG-IMAGE-ACTUAL-WIDTH" specified + ;; width. + (let ((org-image-actual-width (org-property-or-variable-value 'org-image-actual-width))) + (cond + ((eq org-image-actual-width t) nil) + ((listp org-image-actual-width) + (require 'ox) + (let* ((par (org-element-lineage link 'paragraph)) + ;; Try to find an attribute providing a :width. + ;; #+ATTR_ORG: :width ... + (attr-width (org-export-read-attribute :attr_org par :width)) + (width-unreadable? + (lambda (value) + (or (not (stringp value)) + (unless (string= value "t") + (or (not (string-match + (rx bos (opt "+") + (or + ;; Number of pixels + ;; must be a lone number, not + ;; things like 4in + (seq (1+ (in "0-9")) eos) + ;; Numbers ending with % + (seq (1+ (in "0-9.")) (group-n 1 "%")) + ;; Fractions + (seq (0+ (in "0-9")) "." (1+ (in "0-9"))))) + value)) + (let ((number (string-to-number value))) + (and (floatp number) + (not (match-string 1 value)) ; X% + (not (<= 0.0 number 2.0))))))))) + ;; #+ATTR_BACKEND: :width ... + (attr-other + (catch :found + (org-element-properties-map + (lambda (prop _) + (when (and + (not (eq prop :attr_org)) + (string-match-p "^:attr_" (symbol-name prop)) + (not (funcall width-unreadable? (org-export-read-attribute prop par :width)))) + (throw :found prop))) + par))) + (attr-width + (if (not (funcall width-unreadable? attr-width)) + attr-width + ;; When #+attr_org: does not have readable :width + (and attr-other + (org-export-read-attribute attr-other par :width)))) + (width + (cond + ;; Treat :width t as if `org-image-actual-width' were t. + ((string= attr-width "t") nil) + ;; Fallback to `org-image-actual-width' if no interprable width is given. + ((funcall width-unreadable? attr-width) + (car org-image-actual-width)) + ;; Convert numeric widths to numbers, converting percentages. + ((string-match-p "\\`[[+]?[0-9.]+%" attr-width) + (/ (string-to-number attr-width) 100.0)) + (t (string-to-number attr-width))))) + (if (and (floatp width) (<= 0.0 width 2.0)) + ;; A float in [0,2] should be interpereted as this portion of + ;; the text width in the window. This works well with cases like + ;; #+attr_latex: :width 0.X\{line,page,column,etc.}width, + ;; as the "0.X" is pulled out as a float. We use 2 as the upper + ;; bound as cases such as 1.2\linewidth are feasible. + (round (* width + (window-pixel-width) + (/ (or (and (bound-and-true-p visual-fill-column-mode) + (or visual-fill-column-width auto-fill-function)) + (when auto-fill-function fill-column) + (- (window-text-width) (line-number-display-width))) + (float (window-total-width))))) + width))) + ((numberp org-image-actual-width) + org-image-actual-width) + (t nil)))) + +(defun org-image--align (link) + "Determine the alignment of the image LINK. +LINK is a link object. + +In decreasing order of priority, this is controlled: +- Per image by the value of `:center' or `:align' in the +affiliated keyword `#+attr_org'. +- By the `#+attr_html' or `#+attr_latex` keywords with valid + `:center' or `:align' values. +- Globally by the user option `org-image-align'. + +The result is either nil or one of the strings \"left\", +\"center\" or \"right\". + +\"center\" will cause the image preview to be centered, \"right\" +will cause it to be right-aligned. A value of \"left\" or nil +implies no special alignment." + (let ((par (org-element-lineage link 'paragraph))) + ;; Only align when image is not surrounded by paragraph text: + (when (and par ; when image is not in paragraph, but in table/headline/etc, do not align + (= (org-element-begin link) + (save-excursion + (goto-char (org-element-contents-begin par)) + (skip-chars-forward "\t ") + (point))) ;account for leading space + ;before link + (<= (- (org-element-contents-end par) + (org-element-end link)) + 1)) ;account for trailing newline + ;at end of paragraph + (save-match-data + ;; Look for a valid ":center t" or ":align left|center|right" + ;; attribute. + ;; + ;; An attr_org keyword has the highest priority, with + ;; any attr.* next. Choosing between these is + ;; unspecified. + (let ((center-re ":\\(center\\)[[:space:]]+t\\b") + (align-re ":align[[:space:]]+\\(left\\|center\\|right\\)\\b") + attr-align) + (catch 'exit + (org-element-properties-mapc + (lambda (propname propval) + (when (and propval + (string-match-p ":attr.*" (symbol-name propname))) + (setq propval (car-safe propval)) + (when (or (string-match center-re propval) + (string-match align-re propval)) + (setq attr-align (match-string 1 propval)) + (when (eq propname :attr_org) + (throw 'exit t))))) + par)) + (if attr-align + (when (member attr-align '("center" "right")) attr-align) + ;; No image-specific keyword, check global alignment property + (when (memq org-image-align '(center right)) + (symbol-name org-image-align)))))))) + ;;; Public API (defun org-link-types () @@ -1573,6 +1906,224 @@ (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] Displaying %d images 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-let ((ov (cdr (get-char-property-and-overlay + (point) 'org-image-overlay)))) + ;; clear link preview at point + (funcall toggle-previews + (overlay-start ov) (overlay-end ov) + "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 refresh (org-link-preview-clear beg end)) + (org-with-point-at (or beg (point-min)) + (let ((case-fold-search t) + (preview-queue)) + ;; Collect links to preview + (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))))) + (overlay-put ov 'org-image-overlay t) + (overlay-put ov 'modification-hooks + (list 'org-link-preview--remove-overlay)) + (push ov org-link-preview-overlays) + (push (list preview-func ov path link) preview-queue)))) + ;; Collect previews in buffer-local preview queue + (setq org-link-preview--queue + (nconc org-link-preview--queue (nreverse preview-queue))) + ;; Run preview possibly asynchronously + (when org-link-preview--queue + (org-link-preview--process-queue (current-buffer)))))) + +(defun org-link-preview--process-queue (org-buffer) + "Preview pending Org link previews in ORG-BUFFER. + +Previews are generated from the specs in +`org-link-preview--queue', which see." + (with-current-buffer org-buffer + (cl-loop + for spec in org-link-preview--queue + for ov = (cadr spec) ;SPEC is (preview-func ov path link) + for count from org-link-preview-batch-size above 0 + do (pop org-link-preview--queue) + if (overlay-buffer ov) do + (unless (apply spec) + ;; Preview was unsuccessful, delete overlay + (delete-overlay ov) + (setq org-link-preview-overlays + (delq ov org-link-preview-overlays))) + else do (cl-incf count) end + finally do + (setq org-link-preview--timer + (and org-link-preview--queue + (run-with-idle-timer + (time-add (or (current-idle-time) 0) + org-link-preview-delay) + nil #'org-link-preview--process-queue org-buffer)))))) + +(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) + ;; Remove pending preview tasks between BEG and END + (when-let ((spec (cl-find ov org-link-preview--queue + :key #'cadr))) + (setq org-link-preview--queue (delq spec org-link-preview--queue))) + ;; Remove placed overlays between BEG and END + (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 +2146,48 @@ (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!")) + (require 'image) + (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 '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..3c4e1bfb6 100644 --- a/lisp/org-attach.el +++ b/lisp/org-attach.el @@ -797,9 +797,18 @@ (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-with-point-at (org-element-begin link) + (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:\\|[./~]\\)\\|\\]\\[\\(