* [PATCH] org-id: allow using parent's existing id in links to headlines @ 2023-07-24 11:40 Rick Lupton 2023-07-25 7:43 ` Ihor Radchenko 2023-11-04 23:01 ` [PATCH] " Rick Lupton 0 siblings, 2 replies; 48+ messages in thread From: Rick Lupton @ 2023-07-24 11:40 UTC (permalink / raw) To: emacs-orgmode [-- Attachment #1: Type: text/plain, Size: 1335 bytes --] Hi, Here is a small new feature for org-id that I have been using and finding useful. The patch adds the option to look for ancestors of the current headline that have an ID defined and use that together with a link search string to link to specific headlines, without needing every single headline to have its own ID. For example if you have: #+begin_example * H1 :PROPERTIES: :ID: abc :END: ** H2 <point>Link to here #+end_example with `org-id-link-to-org-use-id' set to `t`, the result of org-store-link will be that "H2" has a new id generated, and the link is to that new ID: `[[id:new-id][H2]]`. Now, with `org-id-link-to-org-use-id' set to `inherit`, "H2" is not modified, and the resulting link is `[[id:abc::*H2][H2]]`, which will still take you to the same place as long as the sub-heading is unique within the parent heading with an ID. As an example, I find this useful in situations like this: #+begin_example * Project 1 :PROPERTIES: :ID: project-1 :END: ** <2023-07-01> Meeting A ** <2023-07-08> Meeting B ** <2023-07-15> Meeting C #+end_example ... so that I can link to specific meetings without needing every one to have its own org ID. Feedback on the patch welcome. If you would like to merge this I will (I assume) need to sort out FSF copyright assignment and update ORG-NEWS and the manual. Best Rick [-- Attachment #2: 0001-lisp-org-id.el-Allow-using-a-parent-s-existing-id.patch --] [-- Type: application/octet-stream, Size: 8328 bytes --] From 99b439865b214ecfbbb2b6685ed7782293c157c1 Mon Sep 17 00:00:00 2001 From: Rick Lupton <mail@ricklupton.name> Date: Mon, 24 Jul 2023 12:29:30 +0100 Subject: [PATCH] lisp/org-id.el: Allow using a parent's existing id * lisp/ol.el (org-store-link): When `org-id-link-to-org-use-id` is `inherit`, look for existing IDs on ancestors of the current headline, and use a link search string to find the current headline within that ancestor. * lisp/org-id.el (org-id-link-to-org-use-id): Introduce new `inherit` value. (org-id-get-create, org-id-get, org-id-store-link): Add optional `inherit` argument which considers parents' IDs if the current entry does not have one. * testing/lisp/test-ol.el: Add test for `org-id-link-to-org-use-id` set to `inherit`. This feature allows for more precise links when using org-id to link to org headings, without requiring every single headline to have an id. --- lisp/ol.el | 38 +++++++++++++++++++++++++++++++++++++- lisp/org-id.el | 27 +++++++++++++++++++-------- testing/lisp/test-ol.el | 20 ++++++++++++++++++++ 3 files changed, 76 insertions(+), 9 deletions(-) diff --git a/lisp/ol.el b/lisp/ol.el index 3a8ca5f39..2e863e47b 100644 --- a/lisp/ol.el +++ b/lisp/ol.el @@ -63,7 +63,7 @@ (declare-function org-find-property "org" (property &optional value)) (declare-function org-get-heading "org" (&optional no-tags no-todo no-priority no-comment)) (declare-function org-id-find-id-file "org-id" (id)) -(declare-function org-id-store-link "org-id" ()) +(declare-function org-id-store-link "org-id" (&optional inherit)) (declare-function org-insert-heading "org" (&optional arg invisible-ok top)) (declare-function org-load-modules-maybe "org" (&optional force)) (declare-function org-mark-ring-push "org" (&optional pos buffer)) @@ -1700,6 +1700,42 @@ non-nil." (concat "file:" (abbreviate-file-name (buffer-file-name (buffer-base-buffer)))))))) + ((and (featurep 'org-id) + (eq org-id-link-to-org-use-id 'inherit)) + ;; Store a link using the inherited ID and search string + (setq cpltxt (condition-case nil + (prog1 (org-id-store-link 'inherit) + (setq desc (plist-get org-store-link-plist :description))) + (error + ;; Probably before first headline, link only to file + (concat "file:" + (abbreviate-file-name + (buffer-file-name (buffer-base-buffer))))))) + ;; Add a context search string, limited by current region + (when (org-xor org-link-context-for-files (equal arg '(4))) + (let* ((element (org-element-at-point)) + (name (org-element-property :name element)) + (context + (cond + ((let ((region (org-link--context-from-region))) + (and region (org-link--normalize-string region t)))) + (name) + ((org-before-first-heading-p) + (org-link--normalize-string (org-current-line-string) t)) + (t (org-link-heading-search-string))))) + (when (org-string-nw-p context) + (setq cpltxt (format "%s::%s" cpltxt context)) + (setq desc + (or name + ;; Although description is not a search + ;; string, use `org-link--normalize-string' + ;; to prettify it (contiguous white spaces) + ;; and remove volatile contents (statistics + ;; cookies). + (and (not (org-before-first-heading-p)) + (org-link--normalize-string + (org-get-heading t t t t))) + "NONE")))))) (t ;; Just link to current headline. (setq cpltxt (concat "file:" diff --git a/lisp/org-id.el b/lisp/org-id.el index dae3a0ca8..7b57c8289 100644 --- a/lisp/org-id.el +++ b/lisp/org-id.el @@ -114,6 +114,10 @@ create-if-interactive-and-no-custom-id use-existing Use existing ID, do not create one. +inherit + Use existing ID from a parent headline, and use a text + search to find this headline within it. + nil Never use an ID to make a link, instead link using a text search for the headline text." :group 'org-link-store @@ -255,14 +259,17 @@ This variable is only relevant when `org-id-track-globally' is set." ;;; The API functions ;;;###autoload -(defun org-id-get-create (&optional force) +(defun org-id-get-create (&optional force inherit) "Create an ID for the current entry and return it. If the entry already has an ID, just return it. -With optional argument FORCE, force the creation of a new ID." +With optional argument FORCE, force the creation of a new ID. +With optional argument INHERIT, consider parents' IDs if the +current entry does not have one." (interactive "P") (when force - (org-entry-put (point) "ID" nil)) - (org-id-get (point) 'create)) + (org-entry-put (point) "ID" nil) + (setq inherit nil)) + (org-id-get (point) 'create nil inherit)) ;;;###autoload (defun org-id-copy () @@ -277,15 +284,16 @@ This is useful when working with contents in a temporary buffer that will be copied back to the original.") ;;;###autoload -(defun org-id-get (&optional epom create prefix) +(defun org-id-get (&optional epom create prefix inherit) "Get the ID property of the entry at EPOM. EPOM is an element, marker, or buffer position. If EPOM is nil, refer to the entry at point. If the entry does not have an ID, the function returns nil. +If INHERIT is non-nil, parents' IDs are also considered. However, when CREATE is non-nil, create an ID if none is present already. PREFIX will be passed through to `org-id-new'. In any case, the ID of the entry is returned." - (let ((id (org-entry-get epom "ID"))) + (let ((id (org-entry-get epom "ID" inherit))) (cond ((and id (stringp id) (string-match "\\S-" id)) id) @@ -680,14 +688,17 @@ optional argument MARKERP, return the position as a new marker." ;; so we do have to add it to `org-store-link-functions'. ;;;###autoload -(defun org-id-store-link () +(defun org-id-store-link (&optional inherit) "Store a link to the current entry, using its ID. +If INHERIT is non-nil, consider also parents' IDs if the current +entry does not have an ID. + If before first heading store first title-keyword as description or filename if no title." (interactive) (when (and (buffer-file-name (buffer-base-buffer)) (derived-mode-p 'org-mode)) - (let* ((link (concat "id:" (org-id-get-create))) + (let* ((link (concat "id:" (org-id-get-create nil inherit))) (case-fold-search nil) (desc (save-excursion (org-back-to-heading-or-point-min t) diff --git a/testing/lisp/test-ol.el b/testing/lisp/test-ol.el index a38d9f979..7a4d9999a 100644 --- a/testing/lisp/test-ol.el +++ b/testing/lisp/test-ol.el @@ -381,6 +381,26 @@ See https://github.com/yantar92/org/issues/4." (equal (format "[[file:%s::*foo bar][foo bar]]" file file) (org-store-link nil))))))) +(ert-deftest test-org-link/store-link-with-id () + "Test `org-store-link' specifications with org-id." + ;; On a headline, link to that headline's ID. Use heading as the + ;; description of the link. + (should + (let ((org-stored-links nil) + (org-id-link-to-org-use-id t)) + (org-test-with-temp-text-in-file "* H1\n:PROPERTIES:\n:ID: abc\n:END:\n" + (equal "[[id:abc][H1]]" + (org-store-link nil))))) + ;; On a headline without an ID, link to that headline's parent's ID, + ;; with the current headline as context. Use heading as the + ;; description of the link. + (should + (let ((org-stored-links nil) + (org-id-link-to-org-use-id 'inherit)) + (org-test-with-temp-text-in-file "* H1\n:PROPERTIES:\n:ID: abc\n:END:\n** H2\n** H3<point>\n** H4\n" + (let ((link (org-store-link nil))) + (equal link "[[id:abc::*H3][H3]]")))))) + \f ;;; Radio Targets -- 2.37.1 (Apple Git-137.1) ^ permalink raw reply related [flat|nested] 48+ messages in thread
* Re: [PATCH] org-id: allow using parent's existing id in links to headlines 2023-07-24 11:40 [PATCH] org-id: allow using parent's existing id in links to headlines Rick Lupton @ 2023-07-25 7:43 ` Ihor Radchenko 2023-07-25 15:16 ` Max Nikulin 2023-11-09 20:56 ` Rick Lupton 2023-11-04 23:01 ` [PATCH] " Rick Lupton 1 sibling, 2 replies; 48+ messages in thread From: Ihor Radchenko @ 2023-07-25 7:43 UTC (permalink / raw) To: Rick Lupton; +Cc: emacs-orgmode "Rick Lupton" <mail@ricklupton.name> writes: > Here is a small new feature for org-id that I have been using and finding useful. The patch adds the option to look for ancestors of the current headline that have an ID defined and use that together with a link search string to link to specific headlines, without needing every single headline to have its own ID. I think that it will be a reasonable addition. > Now, with `org-id-link-to-org-use-id' set to `inherit`, "H2" is not modified, and the resulting link is `[[id:abc::*H2][H2]]`, which will still take you to the same place as long as the sub-heading is unique within the parent heading with an ID. What about inherited CUSTOM_ID? > Feedback on the patch welcome. If you would like to merge this I will (I assume) need to sort out FSF copyright assignment and update ORG-NEWS and the manual. Yes, on both questions. See https://orgmode.org/worg/org-contribute.html#copyright > + ((and (featurep 'org-id) > + (eq org-id-link-to-org-use-id 'inherit)) What if none of the parents have an ID? > + ;; Store a link using the inherited ID and search string > + (setq cpltxt (condition-case nil > + (prog1 (org-id-store-link 'inherit) > ... > + "NONE")))))) This is code duplication from another branch of the same function. Please, rewrite without copy-pasting large chunks of code. For example, you can extract the common parts of the code into a private helper function. > ;;;###autoload > -(defun org-id-store-link () > +(defun org-id-store-link (&optional inherit) > "Store a link to the current entry, using its ID. > > +If INHERIT is non-nil, consider also parents' IDs if the current > +entry does not have an ID. > + This will no longer store a link to current entry with INHERIT argument. The search string will be missing from the link path. Ideally, we should have all the necessary logic to store the link within `org-id-store-link' and then use `org-link-set-parameters' to configure id links. -- Ihor Radchenko // yantar92, Org mode contributor, Learn more about Org mode at <https://orgmode.org/>. Support Org development at <https://liberapay.com/org-mode>, or support my work at <https://liberapay.com/yantar92> ^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH] org-id: allow using parent's existing id in links to headlines 2023-07-25 7:43 ` Ihor Radchenko @ 2023-07-25 15:16 ` Max Nikulin 2023-07-26 8:10 ` Ihor Radchenko 2023-11-09 20:56 ` Rick Lupton 1 sibling, 1 reply; 48+ messages in thread From: Max Nikulin @ 2023-07-25 15:16 UTC (permalink / raw) To: emacs-orgmode; +Cc: Rick Lupton On 25/07/2023 14:43, Ihor Radchenko wrote: > "Rick Lupton" writes: > >> Now, with `org-id-link-to-org-use-id' set to `inherit`, "H2" is not >> modified, and the resulting link is `[[id:abc::*H2][H2]]`, which will >> still take you to the same place as long as the sub-heading is unique >> within the parent heading with an ID. > What about inherited CUSTOM_ID? I am not excited by the idea of extending id links for heading hierarchy. From my point of view it is more natural to add the ID property to the heading that should be link target. Sometimes I do not mind to disambiguate heading search link by specifying title of its ancestor. I usually add the CUSTOM_ID property or rename heading to be unique. I am afraid that allowing arbitrary link types to specify path to an element is overkill. It is not XPath and not CSS selectors. ^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH] org-id: allow using parent's existing id in links to headlines 2023-07-25 15:16 ` Max Nikulin @ 2023-07-26 8:10 ` Ihor Radchenko 2023-07-27 0:16 ` Samuel Wales 2023-07-28 19:56 ` [PATCH] org-id: allow using parent's existing id in links to headlines Rick Lupton 0 siblings, 2 replies; 48+ messages in thread From: Ihor Radchenko @ 2023-07-26 8:10 UTC (permalink / raw) To: Max Nikulin; +Cc: emacs-orgmode, Rick Lupton Max Nikulin <manikulin@gmail.com> writes: > I am not excited by the idea of extending id links for heading > hierarchy. From my point of view it is more natural to add the ID > property to the heading that should be link target. > > Sometimes I do not mind to disambiguate heading search link by > specifying title of its ancestor. I usually add the CUSTOM_ID property > or rename heading to be unique. > > I am afraid that allowing arbitrary link types to specify path to an > element is overkill. It is not XPath and not CSS selectors. I am looking at it from an opposite direction: we already have file: links with ::search term, but file is not a very reliable link anchor. File ID will persist even when the file is moved. So, instead of having something like <file:/path/to/foo.org::* Heading>, we should better also provide <id:ID::*heading> with ID defined in the top-level property drawer. ID being some sub-heading is then a natural extension of the same idea. -- Ihor Radchenko // yantar92, Org mode contributor, Learn more about Org mode at <https://orgmode.org/>. Support Org development at <https://liberapay.com/org-mode>, or support my work at <https://liberapay.com/yantar92> ^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH] org-id: allow using parent's existing id in links to headlines 2023-07-26 8:10 ` Ihor Radchenko @ 2023-07-27 0:16 ` Samuel Wales 2023-07-27 7:42 ` IDs below headline level (for paragraphs, lists, etc) (was: [PATCH] org-id: allow using parent's existing id in links to headlines) Ihor Radchenko 2023-07-28 19:56 ` [PATCH] org-id: allow using parent's existing id in links to headlines Rick Lupton 1 sibling, 1 reply; 48+ messages in thread From: Samuel Wales @ 2023-07-27 0:16 UTC (permalink / raw) To: Ihor Radchenko; +Cc: Max Nikulin, emacs-orgmode, Rick Lupton i can see the appeal given the granularity of id [headings, files]. yu want to point to smaller things. but what if those smaller things could have ids without drawers? id markers. then changes in surrounding text would not break anything. On 7/26/23, Ihor Radchenko <yantar92@posteo.net> wrote: > Max Nikulin <manikulin@gmail.com> writes: > >> I am not excited by the idea of extending id links for heading >> hierarchy. From my point of view it is more natural to add the ID >> property to the heading that should be link target. >> >> Sometimes I do not mind to disambiguate heading search link by >> specifying title of its ancestor. I usually add the CUSTOM_ID property >> or rename heading to be unique. >> >> I am afraid that allowing arbitrary link types to specify path to an >> element is overkill. It is not XPath and not CSS selectors. > > I am looking at it from an opposite direction: we already have file: > links with ::search term, but file is not a very reliable link anchor. > File ID will persist even when the file is moved. So, instead of having > something like <file:/path/to/foo.org::* Heading>, we should better also > provide <id:ID::*heading> with ID defined in the top-level property > drawer. ID being some sub-heading is then a natural extension of the > same idea. > > -- > Ihor Radchenko // yantar92, > Org mode contributor, > Learn more about Org mode at <https://orgmode.org/>. > Support Org development at <https://liberapay.com/org-mode>, > or support my work at <https://liberapay.com/yantar92> > > -- The Kafka Pandemic A blog about science, health, human rights, and misopathy: https://thekafkapandemic.blogspot.com ^ permalink raw reply [flat|nested] 48+ messages in thread
* IDs below headline level (for paragraphs, lists, etc) (was: [PATCH] org-id: allow using parent's existing id in links to headlines) 2023-07-27 0:16 ` Samuel Wales @ 2023-07-27 7:42 ` Ihor Radchenko 2023-07-28 20:00 ` Rick Lupton 0 siblings, 1 reply; 48+ messages in thread From: Ihor Radchenko @ 2023-07-27 7:42 UTC (permalink / raw) To: Samuel Wales; +Cc: Max Nikulin, emacs-orgmode, Rick Lupton Samuel Wales <samologist@gmail.com> writes: > ... but what if those smaller things > could have ids without drawers? id markers. then changes in > surrounding text would not break anything. I recall similar idea raised in https://list.orgmode.org/orgmode/CAJniy+OVD0NCWZZTPit5T7wvsbLbgLLXZmPub5tgq3gsHsGhYw@mail.gmail.com/ But there was not much interest. It was pointed that we already have link targets, although they are not global. Making link targets global is doable. -- Ihor Radchenko // yantar92, Org mode contributor, Learn more about Org mode at <https://orgmode.org/>. Support Org development at <https://liberapay.com/org-mode>, or support my work at <https://liberapay.com/yantar92> ^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: IDs below headline level (for paragraphs, lists, etc) (was: [PATCH] org-id: allow using parent's existing id in links to headlines) 2023-07-27 7:42 ` IDs below headline level (for paragraphs, lists, etc) (was: [PATCH] org-id: allow using parent's existing id in links to headlines) Ihor Radchenko @ 2023-07-28 20:00 ` Rick Lupton 0 siblings, 0 replies; 48+ messages in thread From: Rick Lupton @ 2023-07-28 20:00 UTC (permalink / raw) To: Ihor Radchenko, Samuel Wales; +Cc: Max Nikulin, Y. E. I can see this being useful in general, but not avoiding the need for my patch. Org links using search strings already strike a good compromise between working with arbitrary plain text, and allowing links to specific locations. When a search string is enough to find the thing you want to link to, there’s no need to add more IDs manually. If this is already intended to be an unrelated discussion then feel free to ignore this comment! On Thu, 27 Jul 2023, at 8:42 AM, Ihor Radchenko wrote: > Samuel Wales <samologist@gmail.com> writes: > >> ... but what if those smaller things >> could have ids without drawers? id markers. then changes in >> surrounding text would not break anything. > > I recall similar idea raised in > https://list.orgmode.org/orgmode/CAJniy+OVD0NCWZZTPit5T7wvsbLbgLLXZmPub5tgq3gsHsGhYw@mail.gmail.com/ > > But there was not much interest. > > It was pointed that we already have link targets, although they are not > global. Making link targets global is doable. > > -- > Ihor Radchenko // yantar92, > Org mode contributor, > Learn more about Org mode at <https://orgmode.org/>. > Support Org development at <https://liberapay.com/org-mode>, > or support my work at <https://liberapay.com/yantar92> ^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH] org-id: allow using parent's existing id in links to headlines 2023-07-26 8:10 ` Ihor Radchenko 2023-07-27 0:16 ` Samuel Wales @ 2023-07-28 19:56 ` Rick Lupton 2023-07-29 8:33 ` Ihor Radchenko 1 sibling, 1 reply; 48+ messages in thread From: Rick Lupton @ 2023-07-28 19:56 UTC (permalink / raw) To: emacs-orgmode Hi Ihor, Thanks for the comments, I will take a look. A question below. On Wed, 26 Jul 2023, at 9:10 AM, Ihor Radchenko wrote: > > I am looking at it from an opposite direction: we already have file: > links with ::search term, but file is not a very reliable link anchor. > File ID will persist even when the file is moved. So, instead of having > something like <file:/path/to/foo.org::* Heading>, we should better also > provide <id:ID::*heading> with ID defined in the top-level property > drawer. ID being some sub-heading is then a natural extension of the > same idea. This is a good description of the motivation from my point of view. > What about inherited CUSTOM_ID? I’m not sure what you mean. Are you thinking of CUSTOM_ID links, and whether they would behave consistently with a search string to this proposal? Like: [[custom-id:my-id::*H2][H2]] Or using custom id as a search string? Like: [[id:abc::#my-id][Description]] Thanks Rick ^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH] org-id: allow using parent's existing id in links to headlines 2023-07-28 19:56 ` [PATCH] org-id: allow using parent's existing id in links to headlines Rick Lupton @ 2023-07-29 8:33 ` Ihor Radchenko 0 siblings, 0 replies; 48+ messages in thread From: Ihor Radchenko @ 2023-07-29 8:33 UTC (permalink / raw) To: Rick Lupton; +Cc: emacs-orgmode "Rick Lupton" <mail@ricklupton.name> writes: >> What about inherited CUSTOM_ID? > > I’m not sure what you mean. > > Are you thinking of CUSTOM_ID links, and whether they would behave consistently with a search string to this proposal? Like: [[custom-id:my-id::*H2][H2]] > > Or using custom id as a search string? Like: [[id:abc::#my-id][Description]] No. I was thinking about something like [[#my-id::search string]]. However, this will only make sense in internal links. file: links would need multiple search terms, which we currently do not support. So, never mind my comment. -- Ihor Radchenko // yantar92, Org mode contributor, Learn more about Org mode at <https://orgmode.org/>. Support Org development at <https://liberapay.com/org-mode>, or support my work at <https://liberapay.com/yantar92> ^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH] org-id: allow using parent's existing id in links to headlines 2023-07-25 7:43 ` Ihor Radchenko 2023-07-25 15:16 ` Max Nikulin @ 2023-11-09 20:56 ` Rick Lupton 2023-11-10 10:03 ` Ihor Radchenko 1 sibling, 1 reply; 48+ messages in thread From: Rick Lupton @ 2023-11-09 20:56 UTC (permalink / raw) To: Y. E. On Tue, 25 Jul 2023, at 8:43 AM, Ihor Radchenko wrote: > Ideally, we should have all the necessary logic to store the link within > `org-id-store-link' and then use `org-link-set-parameters' to configure > id links. I agree this would be neater, but looking at how this would work, I have a question: Behaviour in `org-store-link` currently depends on the `interactive?` argument, e.g. in this logic (and interactive? (or (eq org-id-link-to-org-use-id 'create-if-interactive) (and (eq org-id-link-to-org-use-id 'create-if-interactive-and-no-custom-id) (not custom-id)))) To move this logic to `org-id-store-link`, is there a way that `org-id-store-link` can tell whether `org-store-link` was called (a) interactively, or (b) with the `interactive?` argument true? Thanks Rick ^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH] org-id: allow using parent's existing id in links to headlines 2023-11-09 20:56 ` Rick Lupton @ 2023-11-10 10:03 ` Ihor Radchenko 2023-11-19 15:21 ` Rick Lupton 0 siblings, 1 reply; 48+ messages in thread From: Ihor Radchenko @ 2023-11-10 10:03 UTC (permalink / raw) To: Rick Lupton; +Cc: Y. E. "Rick Lupton" <mail@ricklupton.name> writes: > On Tue, 25 Jul 2023, at 8:43 AM, Ihor Radchenko wrote: >> Ideally, we should have all the necessary logic to store the link within >> `org-id-store-link' and then use `org-link-set-parameters' to configure >> id links. > > I agree this would be neater, but looking at how this would work, I have a question: > > Behaviour in `org-store-link` currently depends on the `interactive?` argument, e.g. in this logic > > (and interactive? > (or (eq org-id-link-to-org-use-id 'create-if-interactive) > (and (eq org-id-link-to-org-use-id > 'create-if-interactive-and-no-custom-id) > (not custom-id)))) > > To move this logic to `org-id-store-link`, is there a way that `org-id-store-link` can tell whether `org-store-link` was called (a) interactively, or (b) with the `interactive?` argument true? I think that we need to make a change in the rules for :store functions. `interactive?' may be passed as the argument to these functions. In order to not cause breakage, we need something like (condition-case nil (funcall protocol path desc backend info) ;; XXX: The function used (< Org 9.4) to accept only ;; three mandatory arguments. Type-specific `:export' ;; functions in the wild may not handle current ;; signature. Provide backward compatibility support ;; for them. (wrong-number-of-arguments (funcall protocol path desc backend))) to keep the old :store functions that accept 0 arguments working. -- Ihor Radchenko // yantar92, Org mode contributor, Learn more about Org mode at <https://orgmode.org/>. Support Org development at <https://liberapay.com/org-mode>, or support my work at <https://liberapay.com/yantar92> ^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH] org-id: allow using parent's existing id in links to headlines 2023-11-10 10:03 ` Ihor Radchenko @ 2023-11-19 15:21 ` Rick Lupton 2023-12-04 13:23 ` Rick Lupton 2023-12-10 13:35 ` Ihor Radchenko 0 siblings, 2 replies; 48+ messages in thread From: Rick Lupton @ 2023-11-19 15:21 UTC (permalink / raw) To: Ihor Radchenko; +Cc: emacs-orgmode list [-- Attachment #1: Type: text/plain, Size: 1527 bytes --] Here's an updated patch, which adds (optional) search strings to ID links, and the option to inherit ID targets from parent headline / the top level file properties. I've also updated ORG-NEWS and the manual, and added tests. I think I've fixed all the issues with my first patch about which headline gets used for the description when inheriting IDs, what happens if there is no ID, etc. > Ideally, we should have all the necessary logic to store the link within `org-id-store-link' and then use `org-link-set-parameters' to configure id links. > ... > I think that we need to make a change in the rules for :store functions. `interactive?' may be passed as the argument to these functions. I've also moved the org-id specific logic from `org-store-link` to `org-id-store-link`, and added the `interactive?` argument to link store functions as discussed. >> So my question is: should search strings be added to all org-id links? > Sounds as a reasonable default, but users should have an option to revert to previous behaviour with heading id being stored. The default value for the new option `org-id-link-use-context` is `t`, but it can be set to `nil` (or disabled with a prefix argument to `org-store-link` temporarily). This is a change in default behaviour when storing ID links with point at a subheading, named block, or target, or with an active region. The option `org-id-link-consider-parent-id` I've left with a default value of `nil`, since I'm not sure if everyone will want this behaviour. Thanks Rick [-- Attachment #2: 0001-org-id.el-Extend-links-with-search-strings-inherit-p.patch --] [-- Type: application/octet-stream, Size: 38990 bytes --] From b94cd1beba9b54ae23947d59e6a5cdc77fb2640e Mon Sep 17 00:00:00 2001 From: Rick Lupton <mail@ricklupton.name> Date: Sun, 19 Nov 2023 14:52:05 +0000 Subject: [PATCH] org-id.el: Extend links with search strings, inherit parent IDs * lisp/ol.el (org-store-link): Refactor org-id links to use standard `org-store-link-functions'. (org-link-precise-link-target): New function extracting logic to identify a precise link target, e.g. a heading, named object, or text search. (org-link-try-link-store-functions): Extract logic to call external link store functions. Pass them a new `interactive?' argument. * lisp/org-id.el (org-id-link-consider-parent-id): New option to allow a parent heading with an id to be considered as a link target. (org-id-link-use-context): New option equivalent to `org-link-context-for-files`, but for org-id links. (org-id-get-create, org-id-get): Add optional `inherit' argument which considers parents' IDs if the current entry does not have one. (org-id-store-link): Move logic from `org-store-link' here to determine when an org-id link should be stored. Consider IDs of parent headings as link targets when current heading has no ID and `org-id-link-consider-parent-id' is set. Add a search string to the link when enabled. (org-id-open): Recognise search strings after "::" in org-id links. * testing/lisp/test-ol.el: Add tests for `org-link-precise-link-target' and `org-id-store-link' functions, testing new options. * doc/org-manual.org: Update documentation about links. * etc/ORG-NEWS: Document changes and new options. These feature allows for more precise links when using org-id to link to org headings, without requiring every single headline to have an id. Link: https://list.orgmode.org/118435e8-0b20-46fd-af6a-88de8e19fac6@app.fastmail.com/ --- doc/org-manual.org | 118 +++++++++++++----------- etc/ORG-NEWS | 33 +++++++ lisp/ol.el | 198 ++++++++++++++++++++++++---------------- lisp/org-id.el | 96 ++++++++++++++++--- testing/lisp/test-ol.el | 134 +++++++++++++++++++++++++++ 5 files changed, 433 insertions(+), 146 deletions(-) diff --git a/doc/org-manual.org b/doc/org-manual.org index 85568e7ab..eedaf365a 100644 --- a/doc/org-manual.org +++ b/doc/org-manual.org @@ -3296,10 +3296,6 @@ Here is the full set of built-in link types: File links. File name may be remote, absolute, or relative. - Additionally, you can specify a line number, or a text search. - In Org files, you may link to a headline name, a custom ID, or a - code reference instead. - As a special case, "file" prefix may be omitted if the file name is complete, e.g., it starts with =./=, or =/=. @@ -3363,44 +3359,50 @@ Here is the full set of built-in link types: Execute a shell command upon activation. + +For =file:= and =id:= links, you can additionally specify a line +number, or a text search string, separated by =::=. In Org files, you +may link to a headline name, a custom ID, or a code reference instead. + The following table illustrates the link types above, along with their options: -| Link Type | Example | -|------------+----------------------------------------------------------| -| http | =http://staff.science.uva.nl/c.dominik/= | -| https | =https://orgmode.org/= | -| doi | =doi:10.1000/182= | -| file | =file:/home/dominik/images/jupiter.jpg= | -| | =/home/dominik/images/jupiter.jpg= (same as above) | -| | =file:papers/last.pdf= | -| | =./papers/last.pdf= (same as above) | -| | =file:/ssh:me@some.where:papers/last.pdf= (remote) | -| | =/ssh:me@some.where:papers/last.pdf= (same as above) | -| | =file:sometextfile::NNN= (jump to line number) | -| | =file:projects.org= | -| | =file:projects.org::some words= (text search)[fn:12] | -| | =file:projects.org::*task title= (headline search) | -| | =file:projects.org::#custom-id= (headline search) | -| attachment | =attachment:projects.org= | -| | =attachment:projects.org::some words= (text search) | -| docview | =docview:papers/last.pdf::NNN= | -| id | =id:B7423F4D-2E8A-471B-8810-C40F074717E9= | -| news | =news:comp.emacs= | -| mailto | =mailto:adent@galaxy.net= | -| mhe | =mhe:folder= (folder link) | -| | =mhe:folder#id= (message link) | -| rmail | =rmail:folder= (folder link) | -| | =rmail:folder#id= (message link) | -| gnus | =gnus:group= (group link) | -| | =gnus:group#id= (article link) | -| bbdb | =bbdb:R.*Stallman= (record with regexp) | -| irc | =irc:/irc.com/#emacs/bob= | -| help | =help:org-store-link= | -| info | =info:org#External links= | -| shell | =shell:ls *.org= | -| elisp | =elisp:(find-file "Elisp.org")= (Elisp form to evaluate) | -| | =elisp:org-agenda= (interactive Elisp command) | +| Link Type | Example | +|------------+--------------------------------------------------------------------| +| http | =http://staff.science.uva.nl/c.dominik/= | +| https | =https://orgmode.org/= | +| doi | =doi:10.1000/182= | +| file | =file:/home/dominik/images/jupiter.jpg= | +| | =/home/dominik/images/jupiter.jpg= (same as above) | +| | =file:papers/last.pdf= | +| | =./papers/last.pdf= (same as above) | +| | =file:/ssh:me@some.where:papers/last.pdf= (remote) | +| | =/ssh:me@some.where:papers/last.pdf= (same as above) | +| | =file:sometextfile::NNN= (jump to line number) | +| | =file:projects.org= | +| | =file:projects.org::some words= (text search)[fn:12] | +| | =file:projects.org::*task title= (headline search) | +| | =file:projects.org::#custom-id= (headline search) | +| attachment | =attachment:projects.org= | +| | =attachment:projects.org::some words= (text search) | +| docview | =docview:papers/last.pdf::NNN= | +| id | =id:B7423F4D-2E8A-471B-8810-C40F074717E9= | +| | =id:B7423F4D-2E8A-471B-8810-C40F074717E9::*task= (headline search) | +| news | =news:comp.emacs= | +| mailto | =mailto:adent@galaxy.net= | +| mhe | =mhe:folder= (folder link) | +| | =mhe:folder#id= (message link) | +| rmail | =rmail:folder= (folder link) | +| | =rmail:folder#id= (message link) | +| gnus | =gnus:group= (group link) | +| | =gnus:group#id= (article link) | +| bbdb | =bbdb:R.*Stallman= (record with regexp) | +| irc | =irc:/irc.com/#emacs/bob= | +| help | =help:org-store-link= | +| info | =info:org#External links= | +| shell | =shell:ls *.org= | +| elisp | =elisp:(find-file "Elisp.org")= (Elisp form to evaluate) | +| | =elisp:org-agenda= (interactive Elisp command) | #+cindex: VM links #+cindex: Wanderlust links @@ -3461,8 +3463,9 @@ current buffer: - /Org mode buffers/ :: For Org files, if there is a =<<target>>= at point, the link points - to the target. Otherwise it points to the current headline, which - is also the description. + to the target. If there is a named block (using =#+name:=) at + point, the link points to that name. Otherwise it points to the + current headline, which is also the description. #+vindex: org-id-link-to-org-use-id #+cindex: @samp{CUSTOM_ID}, property @@ -3480,6 +3483,13 @@ current buffer: timestamp, depending on ~org-id-method~. Later, when inserting the link, you need to decide which one to use. + #+vindex: org-id-link-consider-parent-id + When ~org-id-link-consider-parent-id~ is ~t~, parent =ID= properties + are considered. This allows linking to specific targets, named + blocks, or headlines (which may not have a globally unique =ID= + themselves) within the context of a parent headline or file which + does. + - /Email/News clients: VM, Rmail, Wanderlust, MH-E, Gnus/ :: #+vindex: org-link-email-description-format @@ -3753,13 +3763,15 @@ the link completion function like this: (org-link-set-parameter "type" :complete #'some-completion-function) #+end_src -** Search Options in File Links +** Search Options in File and ID Links :PROPERTIES: :DESCRIPTION: Linking to a specific location. :ALT_TITLE: Search Options :END: #+cindex: search option in file links +#+cindex: search option in id links #+cindex: file links, searching +#+cindex: id links, searching #+cindex: attachment links, searching File links can contain additional information to make Emacs jump to a @@ -3771,8 +3783,8 @@ example, when the command ~org-store-link~ creates a link (see line as a search string that can be used to find this line back later when following the link with {{{kbd(C-c C-o)}}}. -Note that all search options apply for Attachment links in the same -way that they apply for File links. +Note that all search options apply for Attachment and ID links in the +same way that they apply for File links. Here is the syntax of the different ways to attach a search to a file link, together with explanations for each: @@ -21177,7 +21189,7 @@ The following =ol-man.el= file implements it PATH should be a topic that can be thrown at the man command." (funcall org-man-command path)) -(defun org-man-store-link () +(defun org-man-store-link (&optional _interactive?) "Store a link to a man page." (when (memq major-mode '(Man-mode woman-mode)) ;; This is a man page, we do make this link. @@ -21237,13 +21249,15 @@ A review of =ol-man.el=: For example, ~org-man-store-link~ is responsible for storing a link when ~org-store-link~ (see [[*Handling Links]]) is called from a buffer - displaying a man page. It first checks if the major mode is - appropriate. If check fails, the function returns ~nil~, which - means it isn't responsible for creating a link to the current - buffer. Otherwise the function makes a link string by combining - the =man:= prefix with the man topic. It also provides a default - description. The function ~org-insert-link~ can insert it back - into an Org buffer later on. + displaying a man page. It is passed an argument ~interactive?~ + which this function does not use, but other store functions use to + behave differently when a link is stored interactively by the user. + It first checks if the major mode is appropriate. If check fails, + the function returns ~nil~, which means it isn't responsible for + creating a link to the current buffer. Otherwise the function + makes a link string by combining the =man:= prefix with the man + topic. It also provides a default description. The function + ~org-insert-link~ can insert it back into an Org buffer later on. ** Adding Export Backends :PROPERTIES: diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS index 19117821a..6c6171dc7 100644 --- a/etc/ORG-NEWS +++ b/etc/ORG-NEWS @@ -229,6 +229,12 @@ timestamp object. Possible values: ~timerange~, ~daterange~, ~nil~. ~org-element-timestamp-interpreter~ takes into account this property and returns an appropriate timestamp string. +**** =org-link= store functions are passed an ~interactive?~ argument + +The ~:store:~ functions set for link types using +~org-link-set-parameters~ are now passed an ~interactive?~ argument, +indicating whether ~org-store-link~ was called interactively. + *** ~org-priority=show~ command no longer adjusts for scheduled/deadline In agenda views, ~org-priority=show~ command previously displayed the @@ -307,6 +313,17 @@ The change is breaking when ~org-use-property-inheritance~ is set to ~t~. *** ~org-babel-lilypond-compile-lilyfile~ ignores optional second argument The =TEST= parameter is better served by Emacs debugging tools. + +*** ~org-id-store-link~ now adds search strings for precise link targets + +Previously, search strings are supported for =file:= links but not for +=id:= links. This change adds support for search strings to =id:= +links. + +This new behaviour can be disabled generally by setting +~org-id-link-use-context~ to ~nil~, or when storing a specific link by +passing a prefix argument to ~org-store-link~. + ** New and changed options *** New variable ~org-clock-out-removed-last-clock~ @@ -485,6 +502,22 @@ Currently implemented options are: tasks this technically violates the iCalendar spec, but some iCalendar programs support this usage. +*** New option ~org-id-link-consider-parent-id~ to allow =id:= links to parent headlines + +For =id:= links, when this option is enabled, ~org-store-link~ will +look for ids from parent/ancestor headlines, if the current headline +does not have an id. + +Combined with the new ability for =id:= links to use search strings +for precise link targets (when =org-id-link-use-context= is =t=, which +is the default), this allows linking to specific headlines without +requiring every headline to have an id property, as long as the +headline is unique within a subtree that does have an id property. + +By giving files top-level id properties, links to headlines in the +file can be made more robust by using the file id instead of the file +path. + ** New features *** =ob-plantuml.el=: Support tikz file format output diff --git a/lisp/ol.el b/lisp/ol.el index e684b9504..e5938ee13 100644 --- a/lisp/ol.el +++ b/lisp/ol.el @@ -63,7 +63,6 @@ (declare-function org-find-property "org" (property &optional value)) (declare-function org-get-heading "org" (&optional no-tags no-todo no-priority no-comment)) (declare-function org-id-find-id-file "org-id" (id)) -(declare-function org-id-store-link "org-id" ()) (declare-function org-insert-heading "org" (&optional arg invisible-ok top)) (declare-function org-load-modules-maybe "org" (&optional force)) (declare-function org-mark-ring-push "org" (&optional pos buffer)) @@ -819,6 +818,46 @@ spec." (not (org-in-regexp org-link-any-re)))) \f +(defun org-link--try-link-store-functions (interactive?) + "Try storing external links, prompting if more than one is possible. + +Each function returned by `org-store-link-functions` is called in +turn. If multiple functions return non-nil, prompt for which link +should be stored. + +Return t if a function has successfully stored a link, which will +be stored in `org-link-store-props`. +" + (let ((results-alist nil)) + (dolist (f (org-store-link-functions)) + (when (condition-case nil + (funcall f interactive?) + ;; XXX: The store function used (< Org 9.7) to accept no + ;; arguments; provide backward compatibility support for + ;; them. + (wrong-number-of-arguments + (funcall f))) + ;; XXX: return value is not link's plist, so we + ;; store the new value before it is modified. It + ;; would be cleaner to ask store link functions to + ;; return the plist instead. + (push (cons f (copy-sequence org-store-link-plist)) + results-alist))) + (pcase results-alist + (`nil nil) + (`((,_ . ,_)) t) ;single choice: nothing to do + (`((,name . ,_) . ,_) + ;; Reinstate link plist associated to the chosen + ;; function. + (apply #'org-link-store-props + (cdr (assoc-string + (completing-read + (format "Store link with (default %s): " name) + (mapcar #'car results-alist) + nil t nil nil (symbol-name name)) + results-alist))) + t)))) + ;;; Public API (defun org-link-types () @@ -1334,6 +1373,57 @@ priority cookie or tag." (org-link--normalize-string (or string (org-get-heading t t t t))))) +(defun org-link-precise-link-target (&optional relative-to) + "Determine search string and description for storing a link. + +If a search string is found, return cons cell `(search-string +. desc)`. Otherwise, return nil. + +If there is an active region, the contents is used (see +`org-link--context-from-region`). + +In org-mode buffers, if point is at a named element (e.g. a +source block), the name is used. If within a heading, the current +heading is used. + +If none of those finds a suitable search string, the current line +is used as the search string. + +Optional argument RELATIVE-TO specifies the buffer position where +the search will start from. If the search target that would be +returned is already at this location, return nil to avoid +unnecessary search strings (for example, when using search +strings to find targets within org-id links)." + (let ((result + (cond + ((derived-mode-p 'org-mode) + (let* ((element (org-element-at-point)) + (name (org-element-property :name element)) + (heading (org-element-lineage element 'headline t))) + (cond + ((let ((region (org-link--context-from-region))) + (and region (cons (org-link--normalize-string region t) nil)))) + (name + (cons name name)) + ((org-before-first-heading-p) + (cons (org-link--normalize-string (org-current-line-string) t) nil)) + ((and heading + (> (org-element-begin heading) (or relative-to 0))) + (cons (org-link-heading-search-string) + (org-link--normalize-string + (org-get-heading t t t t))))))) + + ;; Not in an org-mode buffer + (t + (cons (org-link--normalize-string + (or (org-link--context-from-region) (org-current-line-string)) + t) + nil))))) + + ;; Only use search option if there is some text. + (when (org-string-nw-p (car result)) + result))) + (defun org-link-open-as-file (path in-emacs) "Pretend PATH is a file name and open it. @@ -1560,29 +1650,7 @@ non-nil." ;; available. If more than one can generate a link from current ;; location, ask which one to use. ((and (not (equal arg '(16))) - (let ((results-alist nil)) - (dolist (f (org-store-link-functions)) - (when (funcall f) - ;; XXX: return value is not link's plist, so we - ;; store the new value before it is modified. It - ;; would be cleaner to ask store link functions to - ;; return the plist instead. - (push (cons f (copy-sequence org-store-link-plist)) - results-alist))) - (pcase results-alist - (`nil nil) - (`((,_ . ,_)) t) ;single choice: nothing to do - (`((,name . ,_) . ,_) - ;; Reinstate link plist associated to the chosen - ;; function. - (apply #'org-link-store-props - (cdr (assoc-string - (completing-read - (format "Store link with (default %s): " name) - (mapcar #'car results-alist) - nil t nil nil (symbol-name name)) - results-alist))) - t)))) + (org-link--try-link-store-functions interactive?)) (setq link (plist-get org-store-link-plist :link)) ;; If store function actually set `:description' property, use ;; it, even if it is nil. Otherwise, fallback to nil (ask user). @@ -1634,6 +1702,7 @@ non-nil." (org-with-point-at m (setq agenda-link (org-store-link nil interactive?)))))) + ;; Calendar mode ((eq major-mode 'calendar-mode) (let ((cd (calendar-cursor-to-date))) (setq link @@ -1642,6 +1711,7 @@ non-nil." (org-encode-time 0 0 0 (nth 1 cd) (nth 0 cd) (nth 2 cd)))) (org-link-store-props :type "calendar" :date cd))) + ;; Image mode ((eq major-mode 'image-mode) (setq cpltxt (concat "file:" (abbreviate-file-name buffer-file-name)) @@ -1659,12 +1729,20 @@ non-nil." (setq cpltxt (concat "file:" file) link cpltxt))) + ;; Try `org-create-file-search-functions`. If any are + ;; successful, create a file link to the current buffer with + ;; the provided search string. (sets `link` and `cpltxt` to + ;; the same thing; it looks like the intention originally was + ;; that cpltxt was a description, which might have been set by + ;; the search-function (removed in switch to lexical binding)). ((setq search (run-hook-with-args-until-success 'org-create-file-search-functions)) (setq link (concat "file:" (abbreviate-file-name buffer-file-name) "::" search)) (setq cpltxt (or link))) ;; description + ;; Main logic for storing built-in link types in org-mode + ;; buffers ((and (buffer-file-name (buffer-base-buffer)) (derived-mode-p 'org-mode)) (org-with-limited-levels (setq custom-id (org-entry-get nil "CUSTOM_ID")) @@ -1684,71 +1762,33 @@ non-nil." desc nil ;; Do not append #CUSTOM_ID link below. custom-id nil)) - ((and (featurep 'org-id) - (or (eq org-id-link-to-org-use-id t) - (and interactive? - (or (eq org-id-link-to-org-use-id 'create-if-interactive) - (and (eq org-id-link-to-org-use-id - 'create-if-interactive-and-no-custom-id) - (not custom-id)))) - (and org-id-link-to-org-use-id (org-entry-get nil "ID")))) - ;; Store a link using the ID at point - (setq link (condition-case nil - (prog1 (org-id-store-link) - (setq desc (plist-get org-store-link-plist :description))) - (error - ;; Probably before first headline, link only to file - (concat "file:" - (abbreviate-file-name - (buffer-file-name (buffer-base-buffer)))))))) - (t + (t ;; Just link to current headline. (setq cpltxt (concat "file:" (abbreviate-file-name (buffer-file-name (buffer-base-buffer))))) - ;; Add a context search string. - (when (org-xor org-link-context-for-files (equal arg '(4))) - (let* ((element (org-element-at-point)) - (name (org-element-property :name element)) - (context - (cond - ((let ((region (org-link--context-from-region))) - (and region (org-link--normalize-string region t)))) - (name) - ((org-before-first-heading-p) - (org-link--normalize-string (org-current-line-string) t)) - (t (org-link-heading-search-string))))) - (when (org-string-nw-p context) - (setq cpltxt (format "%s::%s" cpltxt context)) - (setq desc - (or name - ;; Although description is not a search - ;; string, use `org-link--normalize-string' - ;; to prettify it (contiguous white spaces) - ;; and remove volatile contents (statistics - ;; cookies). - (and (not (org-before-first-heading-p)) - (org-link--normalize-string - (org-get-heading t t t t))) - "NONE"))))) - (setq link cpltxt))))) - + (when (org-xor org-link-context-for-files (equal arg '(4))) + (pcase (org-link-precise-link-target) + (`nil nil) + (`(,search-string . ,search-desc) + (setq cpltxt (format "%s::%s" cpltxt search-string)) + (setq desc search-desc)))) + (setq link cpltxt))))) + + ;; Buffer linked to file, but not an org-mode buffer. ((buffer-file-name (buffer-base-buffer)) ;; Just link to this file here. (setq cpltxt (concat "file:" (abbreviate-file-name (buffer-file-name (buffer-base-buffer))))) ;; Add a context search string. - (when (org-xor org-link-context-for-files (equal arg '(4))) - (let ((context (org-link--normalize-string - (or (org-link--context-from-region) - (org-current-line-string)) - t))) - ;; Only use search option if there is some text. - (when (org-string-nw-p context) - (setq cpltxt (format "%s::%s" cpltxt context)) - (setq desc "NONE")))) - (setq link cpltxt)) + (when (org-xor org-link-context-for-files (equal arg '(4))) + (pcase (org-link-precise-link-target) + (`nil nil) + (`(,search-string . ,search-desc) + (setq cpltxt (format "%s::%s" cpltxt search-string)) + (setq desc search-desc)))) + (setq link cpltxt)) (interactive? (user-error "No method for storing a link from this buffer")) diff --git a/lisp/org-id.el b/lisp/org-id.el index fbe6a0ed0..784d7cb00 100644 --- a/lisp/org-id.el +++ b/lisp/org-id.el @@ -129,6 +129,37 @@ nil Never use an ID to make a link, instead link using a text search for (const :tag "Only use existing" use-existing) (const :tag "Do not use ID to create link" nil))) +(defcustom org-id-link-consider-parent-id nil + "Non-nil means storing a link to an Org file will consider parent entry IDs. + +Use this with `org-id-link-use-context` set to `t` to allow +linking to uniquely-named sub-entries within a parent entry with +an ID, without requiring every sub-entry to have its own ID." + :group 'org-link-store + :group 'org-id + :package-version '(Org . "9.7") + :type 'boolean) + +(defcustom org-id-link-use-context t + "Non-nil means org-id links from `org-id-store-link' contain context. +\\<org-mode-map> +A search string is added to the id with \"::\" as separator and +used to find the context when the link is activated by the +command `org-open-at-point'. When this option is t, the entire +active region is be placed in the search string of the file link. +If set to a positive integer N, only the first N lines of context +are stored. + +Using a prefix argument to the commands `org-store-link' \ +\(`\\[universal-argument] \\[org-store-link]') or +`org-id-store-link` negates this setting for the duration of the +command." + :group 'org-link-store + :group 'org-id + :package-version '(Org . "9.7") + :type '(choice boolean integer) + :safe (lambda (val) (or (booleanp val) (integerp val)))) + (defcustom org-id-uuid-program "uuidgen" "The uuidgen program." :group 'org-id @@ -258,14 +289,17 @@ This variable is only relevant when `org-id-track-globally' is set." ;;; The API functions ;;;###autoload -(defun org-id-get-create (&optional force) +(defun org-id-get-create (&optional force inherit) "Create an ID for the current entry and return it. If the entry already has an ID, just return it. -With optional argument FORCE, force the creation of a new ID." +With optional argument FORCE, force the creation of a new ID. +With optional argument INHERIT, consider parents' IDs if the +current entry does not have one." (interactive "P") (when force - (org-entry-put (point) "ID" nil)) - (org-id-get (point) 'create)) + (org-entry-put (point) "ID" nil) + (setq inherit nil)) + (org-id-get (point) 'create nil inherit)) ;;;###autoload (defun org-id-copy () @@ -280,15 +314,16 @@ This is useful when working with contents in a temporary buffer that will be copied back to the original.") ;;;###autoload -(defun org-id-get (&optional epom create prefix) +(defun org-id-get (&optional epom create prefix inherit) "Get the ID property of the entry at EPOM. EPOM is an element, marker, or buffer position. If EPOM is nil, refer to the entry at point. If the entry does not have an ID, the function returns nil. +If INHERIT is non-nil, parents' IDs are also considered. However, when CREATE is non-nil, create an ID if none is present already. PREFIX will be passed through to `org-id-new'. In any case, the ID of the entry is returned." - (let ((id (org-entry-get epom "ID"))) + (let ((id (org-entry-get epom "ID" inherit))) (cond ((and id (stringp id) (string-match "\\S-" id)) id) @@ -704,17 +739,33 @@ optional argument MARKERP, return the position as a new marker." ;; so we do have to add it to `org-store-link-functions'. ;;;###autoload -(defun org-id-store-link () +(defun org-id-store-link (interactive?) "Store a link to the current entry, using its ID. +See also `org-id-link-to-org-use-id`, +`org-id-link-use-context`, +`org-id-link-consider-parent-id`. + If before first heading store first title-keyword as description or filename if no title." - (interactive) - (when (and (buffer-file-name (buffer-base-buffer)) (derived-mode-p 'org-mode)) - (let* ((link (concat "id:" (org-id-get-create))) + (interactive "p") + (when (and (buffer-file-name (buffer-base-buffer)) + (derived-mode-p 'org-mode) + (or (eq org-id-link-to-org-use-id t) + (and interactive? + (or (eq org-id-link-to-org-use-id 'create-if-interactive) + (and (eq org-id-link-to-org-use-id + 'create-if-interactive-and-no-custom-id) + (not (org-entry-get nil "CUSTOM_ID"))))) + (and org-id-link-to-org-use-id + (org-entry-get nil "ID" org-id-link-consider-parent-id)))) + (let* ((link (concat "id:" (org-id-get-create nil org-id-link-consider-parent-id))) + (id-location (or (and org-entry-property-inherited-from + (marker-position org-entry-property-inherited-from)) + (save-excursion (org-back-to-heading-or-point-min) (point)))) (case-fold-search nil) (desc (save-excursion - (org-back-to-heading-or-point-min t) + (goto-char id-location) (cond ((org-before-first-heading-p) (let ((keywords (org-collect-keywords '("TITLE")))) (if keywords @@ -726,14 +777,25 @@ or filename if no title." (match-string 4) (match-string 0))) (t link))))) + ;; Prefix to `org-store-link` negates preference from `org-id-link-use-context`. + (when (org-xor current-prefix-arg org-id-link-use-context) + (pcase (org-link-precise-link-target id-location) + (`nil nil) + (`(,search-string . ,search-desc) + (setq link (concat link "::" search-string)) + (setq desc search-desc)))) (org-link-store-props :link link :description desc :type "id") link))) (defun org-id-open (id _) "Go to the entry with id ID." - (org-mark-ring-push) - (let ((m (org-id-find id 'marker)) - cmd) + (let* ((option (and (string-match "::\\(.*\\)\\'" id) + (match-string 1 id))) + (id (if (not option) id + (substring id 0 (match-beginning 0)))) + m cmd) + (org-mark-ring-push) + (setq m (org-id-find id 'marker)) (unless m (error "Cannot find entry with ID \"%s\"" id)) ;; Use a buffer-switching command in analogy to finding files @@ -750,9 +812,13 @@ or filename if no title." (funcall cmd (marker-buffer m))) (goto-char m) (move-marker m nil) + (when option + (org-link-search option)) (org-fold-show-context))) -(org-link-set-parameters "id" :follow #'org-id-open) +(org-link-set-parameters "id" + :follow #'org-id-open + :store #'org-id-store-link) (provide 'org-id) diff --git a/testing/lisp/test-ol.el b/testing/lisp/test-ol.el index e0cec0854..24d926084 100644 --- a/testing/lisp/test-ol.el +++ b/testing/lisp/test-ol.el @@ -381,6 +381,140 @@ See https://github.com/yantar92/org/issues/4." (equal (format "[[file:%s::*foo bar][foo bar]]" file file) (org-store-link nil))))))) +(ert-deftest test-org-link/precise-link-target () + "Test `org-link-precise-link-target` specifications." + (should + (equal '("*H1" . "H1") + (org-test-with-temp-text "* H1<point>\n* H2\n" + (org-link-precise-link-target)))) + (should + (equal '("foo" . "foo") + (org-test-with-temp-text "* H1\n#+name: foo<point>\n#+begin_example\nhi\n#+end_example\n" + (org-link-precise-link-target)))) + (should + (equal '("Text" . nil) + (org-test-with-temp-text "\nText<point>\n* H1\n" + (org-link-precise-link-target)))) + (should + (equal nil + (org-test-with-temp-text "\n<point>\n* H1\n" + (org-link-precise-link-target)))) + ;; relative to a heading + (should + (equal nil + (org-test-with-temp-text "* H1<point>\n* H2\n" + (org-link-precise-link-target 1)))) + (should + (equal '("*H2" . "H2") + (org-test-with-temp-text "* H1\n* H2<point>\n" + (org-link-precise-link-target 1)))) + (should + (equal nil + (org-test-with-temp-text "* H1\n* H2<point>\n" + (org-link-precise-link-target 6)))) + ) + +(defmacro test-ol-stored-link-with-text (text &rest body) + "Return :link and :description from link stored in body." + (declare (indent 1)) + `(let (org-store-link-plist) + (org-test-with-temp-text-in-file ,text + ,@body + (list (plist-get org-store-link-plist :link) + (plist-get org-store-link-plist :description))))) + +(ert-deftest test-org-link/id-store-link () + "Test `org-id-store-link' specifications." + (let ((org-id-link-to-org-use-id nil)) + (should + (equal '(nil nil) + (test-ol-stored-link-with-text "* H1\n:PROPERTIES:\n:ID: abc\n:END:\n" + (org-id-store-link t))))) + ;; On a headline, link to that headline's ID. Use heading as the + ;; description of the link. + (let ((org-id-link-to-org-use-id t)) + (should + (equal '("id:abc" "H1") + (test-ol-stored-link-with-text "* H1\n:PROPERTIES:\n:ID: abc\n:END:\n" + (org-id-store-link t))))) + ;; Remove TODO keywords etc from description of the link. + (let ((org-id-link-to-org-use-id t)) + (should + (equal '("id:abc" "H1") + (test-ol-stored-link-with-text "* TODO [#A] H1 :tag:\n:PROPERTIES:\n:ID: abc\n:END:\n" + (org-id-store-link t))))) + ;; create-if-interactive + (let ((org-id-link-to-org-use-id 'create-if-interactive)) + (should + (equal '("id:abc" "H1") + (cl-letf (((symbol-function 'org-id-new) + (lambda (&rest _rest) "abc"))) + (test-ol-stored-link-with-text "* H1\n" + (org-id-store-link t))))) + (should + (equal '(nil nil) + (test-ol-stored-link-with-text "* H1\n" + (org-id-store-link nil))))) + ;; create-if-interactive-and-no-custom-id + (let ((org-id-link-to-org-use-id 'create-if-interactive-and-no-custom-id)) + (should + (equal '("id:abc" "H1") + (cl-letf (((symbol-function 'org-id-new) + (lambda (&rest _rest) "abc"))) + (test-ol-stored-link-with-text "* H1\n" + (org-id-store-link t))))) + (should + (equal '(nil nil) + (test-ol-stored-link-with-text "* H1\n:PROPERTIES:\n:CUSTOM_ID: xyz\n:END:\n" + (org-id-store-link t)))) + (should + (equal '(nil nil) + (test-ol-stored-link-with-text "* H1\n" + (org-id-store-link nil)))))) + +(ert-deftest test-org-link/id-store-link-using-parent () + "Test `org-id-store-link' specifications with `org-id-link-consider-parent-id` set." + (let ((org-id-link-to-org-use-id 'use-existing) + (org-id-link-consider-parent-id nil)) + (should + (equal '(nil nil) + (test-ol-stored-link-with-text "* H1\n:PROPERTIES:\n:ID: abc\n:END:\n** H2\n<point>" + (org-id-store-link t))))) + ;; when using context to still find specific heading + (let ((org-id-link-to-org-use-id 'use-existing) + (org-id-link-consider-parent-id t) + (org-id-link-use-context t)) + (should + (equal '("id:abc::*H2" "H2") + (test-ol-stored-link-with-text "* H1\n:PROPERTIES:\n:ID: abc\n:END:\n** H2\n<point>" + (org-id-store-link t)))) + (should + (equal '("id:abc::name" "name") + (test-ol-stored-link-with-text "* H1\n:PROPERTIES:\n:ID: abc\n:END:\n\n#+name: name\n<point>#+begin_example\nhi\n#+end_example\n" + (org-id-store-link t)))) + (should + (equal '("id:abc" "H1") + (test-ol-stored-link-with-text "* H1<point>\n:PROPERTIES:\n:ID: abc\n:END:\n** H2\n" + (org-id-store-link t))))) + ;; when not using context, description should be the parent/file + (let ((org-id-link-to-org-use-id 'use-existing) + (org-id-link-consider-parent-id t) + (org-id-link-use-context nil)) + (should + (equal '("id:abc" "H1") + (test-ol-stored-link-with-text "* H1\n:PROPERTIES:\n:ID: abc\n:END:\n** H2\n<point>" + (org-id-store-link t)))) + (should + (let ((result (test-ol-stored-link-with-text ":PROPERTIES:\n:ID: top\n:END:\n:* H1\n<point>" + (org-id-store-link t)))) + (equal "id:top" (car result)) + ;; strip random buffer file name + (equal "org-test" (substring (cadr result) 0 8)))) + (should + (equal '("id:top" "title") + (test-ol-stored-link-with-text ":PROPERTIES:\n:ID: top\n:END:\n#+TITLE: title\n\n:* H1\n<point>" + (org-id-store-link t)))))) + \f ;;; Radio Targets -- 2.39.2 (Apple Git-143) ^ permalink raw reply related [flat|nested] 48+ messages in thread
* Re: [PATCH] org-id: allow using parent's existing id in links to headlines 2023-11-19 15:21 ` Rick Lupton @ 2023-12-04 13:23 ` Rick Lupton 2023-12-10 13:35 ` Ihor Radchenko 1 sibling, 0 replies; 48+ messages in thread From: Rick Lupton @ 2023-12-04 13:23 UTC (permalink / raw) To: Ihor Radchenko; +Cc: Y. E. Hi, I can’t see this patch listed at https://tracker.orgmode.org/ so just wanted to check it hasn’t got lost? Thanks Rick On Sun, 19 Nov 2023, at 3:21 PM, Rick Lupton wrote: > Here's an updated patch, which adds (optional) search strings to ID > links, and the option to inherit ID targets from parent headline / the > top level file properties. I've also updated ORG-NEWS and the manual, > and added tests. > > I think I've fixed all the issues with my first patch about which > headline gets used for the description when inheriting IDs, what > happens if there is no ID, etc. > >> Ideally, we should have all the necessary logic to store the link within `org-id-store-link' and then use `org-link-set-parameters' to configure id links. >> ... >> I think that we need to make a change in the rules for :store functions. `interactive?' may be passed as the argument to these functions. > > I've also moved the org-id specific logic from `org-store-link` to > `org-id-store-link`, and added the `interactive?` argument to link > store functions as discussed. > >>> So my question is: should search strings be added to all org-id links? >> Sounds as a reasonable default, but users should have an option to revert to previous behaviour with heading id being stored. > > The default value for the new option `org-id-link-use-context` is `t`, > but it can be set to `nil` (or disabled with a prefix argument to > `org-store-link` temporarily). This is a change in default behaviour > when storing ID links with point at a subheading, named block, or > target, or with an active region. > > The option `org-id-link-consider-parent-id` I've left with a default > value of `nil`, since I'm not sure if everyone will want this behaviour. > > Thanks > Rick > > Attachments: > * 0001-org-id.el-Extend-links-with-search-strings-inherit-p.patch ^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH] org-id: allow using parent's existing id in links to headlines 2023-11-19 15:21 ` Rick Lupton 2023-12-04 13:23 ` Rick Lupton @ 2023-12-10 13:35 ` Ihor Radchenko 2023-12-14 20:42 ` Rick Lupton 1 sibling, 1 reply; 48+ messages in thread From: Ihor Radchenko @ 2023-12-10 13:35 UTC (permalink / raw) To: Rick Lupton; +Cc: emacs-orgmode list "Rick Lupton" <mail@ricklupton.name> writes: > Here's an updated patch, which adds (optional) search strings to ID links, and the option to inherit ID targets from parent headline / the top level file properties. I've also updated ORG-NEWS and the manual, and added tests. > > I think I've fixed all the issues with my first patch about which headline gets used for the description when inheriting IDs, what happens if there is no ID, etc. Thanks! > +*** ~org-id-store-link~ now adds search strings for precise link targets > + > +Previously, search strings are supported for =file:= links but not for > +=id:= links. This change adds support for search strings to =id:= > +links. "This change..." part sounds like a commit message, not a NEWS item. You may probably remove this paragraph. > \f > +(defun org-link--try-link-store-functions (interactive?) > + "Try storing external links, prompting if more than one is possible. > + > +Each function returned by `org-store-link-functions` is called in > +turn. If multiple functions return non-nil, prompt for which link > +should be stored. We use `...' Elisp quoting, not markdown. Also, please keep two spaces between sentences - you do it inconsistently across the patch. > +Return t if a function has successfully stored a link, which will > +be stored in `org-link-store-props`. I'd better say "Return t when the link is stored in `org-store-link-plist'." > +(defcustom org-id-link-consider-parent-id nil > + "Non-nil means storing a link to an Org file will consider parent entry IDs. > + > +Use this with `org-id-link-use-context` set to `t` to allow > +linking to uniquely-named sub-entries within a parent entry with > +an ID, without requiring every sub-entry to have its own ID." 1. `...' quoting 2. The docstring is slightly confusing. Having an example would be helpful. > +(defcustom org-id-link-use-context t > + "Non-nil means org-id links from `org-id-store-link' contain context. > +\\<org-mode-map> > +A search string is added to the id with \"::\" as separator and > +used to find the context when the link is activated by the > +command `org-open-at-point'. When this option is t, the entire > +active region is be placed in the search string of the file link. > +If set to a positive integer N, only the first N lines of context > +are stored. It does not look like integer value is respected in the patch. > +Using a prefix argument to the commands `org-store-link' \ > +\(`\\[universal-argument] \\[org-store-link]') or > +`org-id-store-link` negates this setting for the duration of the > +command." You should also update the docstring of `org-store-link' accordingly. > ;;;###autoload > -(defun org-id-get-create (&optional force) > +(defun org-id-get-create (&optional force inherit) >... > ;;;###autoload > -(defun org-id-get (&optional epom create prefix) > +(defun org-id-get (&optional epom create prefix inherit) Please document this new optional arguments in the NEWS. > - (let ((id (org-entry-get epom "ID"))) > + (let ((id (org-entry-get epom "ID" inherit))) This makes your description of INHERIT argument slightly inaccurate - for `org-entry-get', INHERIT can also be a special symbol 'selective. > -(defun org-id-store-link () > +(defun org-id-store-link (interactive?) Please make this new argument optional and document the argument in the docstring and NEWS. Non-optional new argument is a breaking change that may break third-party code. > "Store a link to the current entry, using its ID. > > +See also `org-id-link-to-org-use-id`, > +`org-id-link-use-context`, > +`org-id-link-consider-parent-id`. > + > If before first heading store first title-keyword as description > or filename if no title." > - (interactive) > - (when (and (buffer-file-name (buffer-base-buffer)) (derived-mode-p 'org-mode)) > - (let* ((link (concat "id:" (org-id-get-create))) > + (interactive "p") > + (when (and (buffer-file-name (buffer-base-buffer)) > + (derived-mode-p 'org-mode) > + (or (eq org-id-link-to-org-use-id t) I do not like this change - `org-id-store-link' is not only used by `org-store-link'. Suddenly honoring `org-id-link-to-org-use-id' will be a breaking change. Instead, I suggest you to write a wrapper function, like `org-id-store-link-maybe' and use it as :store id link property. That function will call `org-id-store-link' as necessary according to user customization. > + ;; Prefix to `org-store-link` negates preference from `org-id-link-use-context`. > + (when (org-xor current-prefix-arg org-id-link-use-context) This is not reliable. `org-id-store-link' may be called from a completely different command, not `org-store-link'. Then, the effect of prefix argument will be unexpected. You should instead process prefix argument right in `org-store-link' by let-binding `org-id-link-use-context' around the call to `org-id-store-link'. > + (pcase (org-link-precise-link-target id-location) Why not passing the RELATIVE-TO argument? > (defun org-id-open (id _) > "Go to the entry with id ID." > - (org-mark-ring-push) > - (let ((m (org-id-find id 'marker)) > - cmd) > + (let* ((option (and (string-match "::\\(.*\\)\\'" id) > + (match-string 1 id))) > + (id (if (not option) id > + (substring id 0 (match-beginning 0)))) > + m cmd) > + (org-mark-ring-push) > + (setq m (org-id-find id 'marker)) This means that the existing IDs that happen to contain :: will be broken. For such IDs, we should (1) document the problem in the news; (2) try harder to match them calling `org-id-find' with all the possible ID values until one matches. > + (when option > + (org-link-search option)) > (org-fold-show-context))) `org-link-search' does not always search from point. So, you may end up matching, for example, a duplicate CUSTOM_ID above. Moreover, regular expression match option will be broken - `org-link-search' creates sparse tree in the whole buffer and will disregard the ID part of the link. I suspect that you will need to make dedicated modifications to `org-link-search' as well in order to implement opening ID links with search option cleanly. -- Ihor Radchenko // yantar92, Org mode contributor, Learn more about Org mode at <https://orgmode.org/>. Support Org development at <https://liberapay.com/org-mode>, or support my work at <https://liberapay.com/yantar92> ^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH] org-id: allow using parent's existing id in links to headlines 2023-12-10 13:35 ` Ihor Radchenko @ 2023-12-14 20:42 ` Rick Lupton 2023-12-15 12:55 ` Ihor Radchenko 0 siblings, 1 reply; 48+ messages in thread From: Rick Lupton @ 2023-12-14 20:42 UTC (permalink / raw) To: Ihor Radchenko; +Cc: Y. E. Dear Ihor, Thanks for taking a look at the patch and for your feedback. Re various docstrings, documenting new optional argument -- I will try to address these. Other comments/questions below. On Sun, 10 Dec 2023, at 1:35 PM, Ihor Radchenko wrote: > "This change..." part sounds like a commit message, not a NEWS item. I think there are lots of other examples that are written like this in ORG-NEWS (but I agree my sentence was unnecessary and have removed it). >> +(defcustom org-id-link-use-context t >> + "Non-nil means org-id links from `org-id-store-link' contain context. >> +\\<org-mode-map> >> +A search string is added to the id with \"::\" as separator and >> +used to find the context when the link is activated by the >> +command `org-open-at-point'. When this option is t, the entire >> +active region is be placed in the search string of the file link. >> +If set to a positive integer N, only the first N lines of context >> +are stored. > > It does not look like integer value is respected in the patch. You're right. Do you have a preference between (a) sticking to this docstring, which creates the possibility of using different numbers of lines for id: and file: links' context, and makes the code slightly more complicated, but keeps the meaning of `org-link-context-for-files' specifically `for files'; or (b) Always use `org-link-context-for-files' to set the number of lines of context used for all links; `org-id-link-use-context' is just a boolean. The code is simpler. ? >> - (let ((id (org-entry-get epom "ID"))) >> + (let ((id (org-entry-get epom "ID" inherit))) > > This makes your description of INHERIT argument slightly inaccurate - for > `org-entry-get', INHERIT can also be a special symbol 'selective. Good point; I think the answer is to force INHERIT to t or nil, rather than documenting and continuing to accept 'selective (when INHERIT is used, it should definitely take effect). >> -(defun org-id-store-link () >> +(defun org-id-store-link (interactive?) > > Please make this new argument optional and document the argument in the > docstring and NEWS. Non-optional new argument is a breaking change that > may break third-party code. Oops, yes -- but in fact this argument is only needed on `org-id-store-link-maybe' (as below), so I can remove it again here. >> "Store a link to the current entry, using its ID. >> >> +See also `org-id-link-to-org-use-id`, >> +`org-id-link-use-context`, >> +`org-id-link-consider-parent-id`. >> + >> If before first heading store first title-keyword as description >> or filename if no title." >> - (interactive) >> - (when (and (buffer-file-name (buffer-base-buffer)) (derived-mode-p 'org-mode)) >> - (let* ((link (concat "id:" (org-id-get-create))) >> + (interactive "p") >> + (when (and (buffer-file-name (buffer-base-buffer)) >> + (derived-mode-p 'org-mode) >> + (or (eq org-id-link-to-org-use-id t) > > I do not like this change - `org-id-store-link' is not only used by > `org-store-link'. Suddenly honoring `org-id-link-to-org-use-id' will be > a breaking change. Instead, I suggest you to write a wrapper function, > like `org-id-store-link-maybe' and use it as :store id link property. > That function will call `org-id-store-link' as necessary according to > user customization. Ok, yes that makes sense. >> + ;; Prefix to `org-store-link` negates preference from `org-id-link-use-context`. >> + (when (org-xor current-prefix-arg org-id-link-use-context) > > This is not reliable. `org-id-store-link' may be called from a completely > different command, not `org-store-link'. Then, the effect of prefix > argument will be unexpected. You should instead process prefix argument > right in `org-store-link' by let-binding `org-id-link-use-context' > around the call to `org-id-store-link'. Now that `org-id-store-link' is called via :store link property, `org-store-link` does not have special logic for org-id, which I thought was an improvement, so it would be a step backwards to add in special logic for `org-id-link-use-context'? Instead, I think this logic could be in `org-id-store-link-maybe' as above. That is, it is safe to take account of `current-prefix-arg' within a link :store function, and assume it represents prefix args as used with `org-store-link'? > >> + (pcase (org-link-precise-link-target id-location) > > Why not passing the RELATIVE-TO argument? The `id-location' is the RELATIVE-TO argument. Or do I misunderstand you? >> (defun org-id-open (id _) >> "Go to the entry with id ID." >> - (org-mark-ring-push) >> - (let ((m (org-id-find id 'marker)) >> - cmd) >> + (let* ((option (and (string-match "::\\(.*\\)\\'" id) >> + (match-string 1 id))) >> + (id (if (not option) id >> + (substring id 0 (match-beginning 0)))) >> + m cmd) >> + (org-mark-ring-push) >> + (setq m (org-id-find id 'marker)) > > This means that the existing IDs that happen to contain :: will be > broken. For such IDs, we should (1) document the problem in the news; > (2) try harder to match them calling `org-id-find' with all the possible > ID values until one matches. Good point, ok, I'll try this. >> + (when option >> + (org-link-search option)) >> (org-fold-show-context))) > > `org-link-search' does not always search from point. So, you may end up > matching, for example, a duplicate CUSTOM_ID above. > Moreover, regular expression match option will be broken - > `org-link-search' creates sparse tree in the whole buffer and will > disregard the ID part of the link. I suspect that you will need to make > dedicated modifications to `org-link-search' as well in order to > implement opening ID links with search option cleanly. Thanks, yes. It looks to me (from the code and some testing) that narrowing to the target heading first before calling `org-link-search' does the right thing. Was there a particular reason you thought `org-link-search' would need to be changed? Thanks, Rick > > -- > Ihor Radchenko // yantar92, > Org mode contributor, > Learn more about Org mode at <https://orgmode.org/>. > Support Org development at <https://liberapay.com/org-mode>, > or support my work at <https://liberapay.com/yantar92> ^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH] org-id: allow using parent's existing id in links to headlines 2023-12-14 20:42 ` Rick Lupton @ 2023-12-15 12:55 ` Ihor Radchenko 2023-12-15 16:16 ` Rick Lupton 0 siblings, 1 reply; 48+ messages in thread From: Ihor Radchenko @ 2023-12-15 12:55 UTC (permalink / raw) To: Rick Lupton; +Cc: Y. E. "Rick Lupton" <mail@ricklupton.name> writes: >>> +(defcustom org-id-link-use-context t >> ... >> It does not look like integer value is respected in the patch. > > You're right. Do you have a preference between > > (a) sticking to this docstring, which creates the possibility of using different numbers of lines for id: and file: links' context, and makes the code slightly more complicated, but keeps the meaning of `org-link-context-for-files' specifically `for files'; or > > (b) Always use `org-link-context-for-files' to set the number of lines of context used for all links; `org-id-link-use-context' is just a boolean. The code is simpler. > > ? I think that (b) makes sense. There is no reason to make the customization yet more granular and complex when there is no clear need. We can always do it later, if necessary, anyway. >>> - (let ((id (org-entry-get epom "ID"))) >>> + (let ((id (org-entry-get epom "ID" inherit))) >> >> This makes your description of INHERIT argument slightly inaccurate - for >> `org-entry-get', INHERIT can also be a special symbol 'selective. > > Good point; I think the answer is to force INHERIT to t or nil, rather than documenting and continuing to accept 'selective (when INHERIT is used, it should definitely take effect). Agree. >>> + ;; Prefix to `org-store-link` negates preference from `org-id-link-use-context`. >>> + (when (org-xor current-prefix-arg org-id-link-use-context) >> >> This is not reliable. `org-id-store-link' may be called from a completely >> different command, not `org-store-link'. Then, the effect of prefix >> argument will be unexpected. You should instead process prefix argument >> right in `org-store-link' by let-binding `org-id-link-use-context' >> around the call to `org-id-store-link'. > > Now that `org-id-store-link' is called via :store link property, `org-store-link` does not have special logic for org-id, which I thought was an improvement, so it would be a step backwards to add in special logic for `org-id-link-use-context'? > > Instead, I think this logic could be in `org-id-store-link-maybe' as above. That is, it is safe to take account of `current-prefix-arg' within a link :store function, and assume it represents prefix args as used with `org-store-link'? No, it is generally not safe. For a different reason. Let me illustrate with an example: (defun yant/test2 () (message "current-prefix-arg: %S" current-prefix-arg)) (defun yant/test (arg) (interactive "P") (yant/test2)) When you call M-x yant/test, you will see "current-prefix-arg: nil". However, when you call C-u M-x yant/test, you will see "current-prefix-arg: (4)". Similar logic applies to the non-interactive calls to `org-store-link'. If some Elisp code implements a command like (defun yant/my-command (arg) (interactive "P") <do staff> (org-store-link nil)) then, `org-store-link' may call `org-id-store-link-maybe' and `org-id-store-link-maybe' will still "see" the top-level prefix argument passed to `yant/my-command' - the prefix argument that has nothing at all to do with prefix arguments of `org-store-link'. Conclusion: It is unsafe to use `current-prefix-arg' value. We need to pass this information some other way. The way I proposed is actually not any special for ID links. What I meant it to let-bind `org-link-context-for-files' around the whole call to `org-store-link-functions', so that the custom :store functions will get access to the adjusted value of `org-link-context-for-files'. Does this explanation make more sense? >>> + (pcase (org-link-precise-link-target id-location) >> >> Why not passing the RELATIVE-TO argument? > > The `id-location' is the RELATIVE-TO argument. Or do I misunderstand you? I just did not notice ID-LOCATION :facepalm: >>> + (when option >>> + (org-link-search option)) >>> (org-fold-show-context))) >> >> `org-link-search' does not always search from point. So, you may end up >> matching, for example, a duplicate CUSTOM_ID above. >> Moreover, regular expression match option will be broken - >> `org-link-search' creates sparse tree in the whole buffer and will >> disregard the ID part of the link. I suspect that you will need to make >> dedicated modifications to `org-link-search' as well in order to >> implement opening ID links with search option cleanly. > > Thanks, yes. It looks to me (from the code and some testing) that narrowing to the target heading first before calling `org-link-search' does the right thing. Was there a particular reason you thought `org-link-search' would need to be changed? Because a lot of the code (except some part `org-link-open') assumes that `org-link-search' searches the whole buffer, not just the narrowed part. But that's not your problem - I will update the docstring of `org-link-search' to explicitly specify that it is searching within the accessible portion of the buffer and update the callers to account for this. https://git.savannah.gnu.org/cgit/emacs/org-mode.git/commit/?id=89164e605 https://git.savannah.gnu.org/cgit/emacs/org-mode.git/commit/?id=5c543cd9d https://git.savannah.gnu.org/cgit/emacs/org-mode.git/commit/?id=cb71bde7c https://git.savannah.gnu.org/cgit/emacs/org-mode.git/commit/?id=63ef7b924 But does your code do narrowing? I did not notice it. -- Ihor Radchenko // yantar92, Org mode contributor, Learn more about Org mode at <https://orgmode.org/>. Support Org development at <https://liberapay.com/org-mode>, or support my work at <https://liberapay.com/yantar92> ^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH] org-id: allow using parent's existing id in links to headlines 2023-12-15 12:55 ` Ihor Radchenko @ 2023-12-15 16:16 ` Rick Lupton 2023-12-16 14:20 ` Ihor Radchenko 0 siblings, 1 reply; 48+ messages in thread From: Rick Lupton @ 2023-12-15 16:16 UTC (permalink / raw) To: Ihor Radchenko; +Cc: Y. E. On Fri, 15 Dec 2023, at 12:55 PM, Ihor Radchenko wrote: > No, it is generally not safe. For a different reason. > > Let me illustrate with an example: > > ... > > Conclusion: It is unsafe to use `current-prefix-arg' value. We need to > pass this information some other way. > > The way I proposed is actually not any special for ID links. What I > meant it to let-bind `org-link-context-for-files' around the whole call > to `org-store-link-functions', so that the custom :store functions will > get access to the adjusted value of `org-link-context-for-files'. > Does this explanation make more sense? Thanks for the example and explanation. Yes that does make sense, mostly. I assume this would look like this in org-store-link: (let ((org-link-context-for-files (org-xor org-link-context-for-files (equal arg '(4)))) (...call store link functions...)) The meaning of `org-link-context-for-files' is then shifting from being "should file: links include search strings (and how much should be included when the region is active)" from "should any link that supports search strings include them (and how much should be included when the region is active)". Is it necessary to rename it to reflect this? (e.g. to `org-link-use-context' or similar). It's also then less clear what the role of `org-id-link-use-context' is and how it interacts with `org-link-context-for-files'. I had included `org-id-link-use-context' to give a way to opt out of the new behaviour (i.e. using the update discussed above, a search string is added if (and org-link-context-for-files org-id-link-use-context) ). But perhaps this is also unnecessarily complicated, and `org-id-link-use-context' could be removed again completely? > I will update the docstring of > `org-link-search' to explicitly specify that it is searching within the > accessible portion of the buffer and update the callers to account for > this. > https://git.savannah.gnu.org/cgit/emacs/org-mode.git/commit/?id=89164e605 > https://git.savannah.gnu.org/cgit/emacs/org-mode.git/commit/?id=5c543cd9d > https://git.savannah.gnu.org/cgit/emacs/org-mode.git/commit/?id=cb71bde7c > https://git.savannah.gnu.org/cgit/emacs/org-mode.git/commit/?id=63ef7b924 > > But does your code do narrowing? I did not notice it. Not in the patch I sent, I added it later after you pointed this out. I'll send an updated patch next. Thanks, Rick ^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH] org-id: allow using parent's existing id in links to headlines 2023-12-15 16:16 ` Rick Lupton @ 2023-12-16 14:20 ` Ihor Radchenko 2023-12-17 19:07 ` [PATCH v2] " Rick Lupton 0 siblings, 1 reply; 48+ messages in thread From: Ihor Radchenko @ 2023-12-16 14:20 UTC (permalink / raw) To: Rick Lupton; +Cc: Y. E. "Rick Lupton" <mail@ricklupton.name> writes: > Thanks for the example and explanation. Yes that does make sense, mostly. I assume this would look like this in org-store-link: > > (let ((org-link-context-for-files (org-xor org-link-context-for-files (equal arg '(4)))) > (...call store link functions...)) Yes. > The meaning of `org-link-context-for-files' is then shifting from being "should file: links include search strings (and how much should be included when the region is active)" from "should any link that supports search strings include them (and how much should be included when the region is active)". Is it necessary to rename it to reflect this? (e.g. to `org-link-use-context' or similar). I do not think so - with your addition, we are still linking to files. May simply update the docstring for `org-link-context-for-files' and `org-store-link'. > It's also then less clear what the role of `org-id-link-use-context' is and how it interacts with `org-link-context-for-files'. I had included `org-id-link-use-context' to give a way to opt out of the new behaviour (i.e. using the update discussed above, a search string is added if (and org-link-context-for-files org-id-link-use-context) ). But perhaps this is also unnecessarily complicated, and `org-id-link-use-context' could be removed again completely? I do not think so - it is important to keep an option for users to return to previous behaviour. We might, in theory, modify `org-link-context-for-files' to allow per-link type customization (then, people could set things back to "context just for file: links"), but it would be more complicated IMHO. -- Ihor Radchenko // yantar92, Org mode contributor, Learn more about Org mode at <https://orgmode.org/>. Support Org development at <https://liberapay.com/org-mode>, or support my work at <https://liberapay.com/yantar92> ^ permalink raw reply [flat|nested] 48+ messages in thread
* [PATCH v2] org-id: allow using parent's existing id in links to headlines 2023-12-16 14:20 ` Ihor Radchenko @ 2023-12-17 19:07 ` Rick Lupton 2023-12-18 12:27 ` Ihor Radchenko 0 siblings, 1 reply; 48+ messages in thread From: Rick Lupton @ 2023-12-17 19:07 UTC (permalink / raw) To: Ihor Radchenko; +Cc: Y. E. [-- Attachment #1: Type: text/plain, Size: 151 bytes --] Please find attached updated patch which I think addresses all the points discussed. Let me know if you see any further changes needed. Thanks, Rick [-- Attachment #2: 0001-org-id.el-Extend-links-with-search-strings-inherit-v2.patch --] [-- Type: application/octet-stream, Size: 39400 bytes --] diff --git a/doc/org-manual.org b/doc/org-manual.org index ee2413248..a82265e04 100644 --- a/doc/org-manual.org +++ b/doc/org-manual.org @@ -3296,10 +3296,6 @@ Here is the full set of built-in link types: File links. File name may be remote, absolute, or relative. - Additionally, you can specify a line number, or a text search. - In Org files, you may link to a headline name, a custom ID, or a - code reference instead. - As a special case, "file" prefix may be omitted if the file name is complete, e.g., it starts with =./=, or =/=. @@ -3363,44 +3359,50 @@ Here is the full set of built-in link types: Execute a shell command upon activation. + +For =file:= and =id:= links, you can additionally specify a line +number, or a text search string, separated by =::=. In Org files, you +may link to a headline name, a custom ID, or a code reference instead. + The following table illustrates the link types above, along with their options: -| Link Type | Example | -|------------+----------------------------------------------------------| -| http | =http://staff.science.uva.nl/c.dominik/= | -| https | =https://orgmode.org/= | -| doi | =doi:10.1000/182= | -| file | =file:/home/dominik/images/jupiter.jpg= | -| | =/home/dominik/images/jupiter.jpg= (same as above) | -| | =file:papers/last.pdf= | -| | =./papers/last.pdf= (same as above) | -| | =file:/ssh:me@some.where:papers/last.pdf= (remote) | -| | =/ssh:me@some.where:papers/last.pdf= (same as above) | -| | =file:sometextfile::NNN= (jump to line number) | -| | =file:projects.org= | -| | =file:projects.org::some words= (text search)[fn:12] | -| | =file:projects.org::*task title= (headline search) | -| | =file:projects.org::#custom-id= (headline search) | -| attachment | =attachment:projects.org= | -| | =attachment:projects.org::some words= (text search) | -| docview | =docview:papers/last.pdf::NNN= | -| id | =id:B7423F4D-2E8A-471B-8810-C40F074717E9= | -| news | =news:comp.emacs= | -| mailto | =mailto:adent@galaxy.net= | -| mhe | =mhe:folder= (folder link) | -| | =mhe:folder#id= (message link) | -| rmail | =rmail:folder= (folder link) | -| | =rmail:folder#id= (message link) | -| gnus | =gnus:group= (group link) | -| | =gnus:group#id= (article link) | -| bbdb | =bbdb:R.*Stallman= (record with regexp) | -| irc | =irc:/irc.com/#emacs/bob= | -| help | =help:org-store-link= | -| info | =info:org#External links= | -| shell | =shell:ls *.org= | -| elisp | =elisp:(find-file "Elisp.org")= (Elisp form to evaluate) | -| | =elisp:org-agenda= (interactive Elisp command) | +| Link Type | Example | +|------------+--------------------------------------------------------------------| +| http | =http://staff.science.uva.nl/c.dominik/= | +| https | =https://orgmode.org/= | +| doi | =doi:10.1000/182= | +| file | =file:/home/dominik/images/jupiter.jpg= | +| | =/home/dominik/images/jupiter.jpg= (same as above) | +| | =file:papers/last.pdf= | +| | =./papers/last.pdf= (same as above) | +| | =file:/ssh:me@some.where:papers/last.pdf= (remote) | +| | =/ssh:me@some.where:papers/last.pdf= (same as above) | +| | =file:sometextfile::NNN= (jump to line number) | +| | =file:projects.org= | +| | =file:projects.org::some words= (text search)[fn:12] | +| | =file:projects.org::*task title= (headline search) | +| | =file:projects.org::#custom-id= (headline search) | +| attachment | =attachment:projects.org= | +| | =attachment:projects.org::some words= (text search) | +| docview | =docview:papers/last.pdf::NNN= | +| id | =id:B7423F4D-2E8A-471B-8810-C40F074717E9= | +| | =id:B7423F4D-2E8A-471B-8810-C40F074717E9::*task= (headline search) | +| news | =news:comp.emacs= | +| mailto | =mailto:adent@galaxy.net= | +| mhe | =mhe:folder= (folder link) | +| | =mhe:folder#id= (message link) | +| rmail | =rmail:folder= (folder link) | +| | =rmail:folder#id= (message link) | +| gnus | =gnus:group= (group link) | +| | =gnus:group#id= (article link) | +| bbdb | =bbdb:R.*Stallman= (record with regexp) | +| irc | =irc:/irc.com/#emacs/bob= | +| help | =help:org-store-link= | +| info | =info:org#External links= | +| shell | =shell:ls *.org= | +| elisp | =elisp:(find-file "Elisp.org")= (Elisp form to evaluate) | +| | =elisp:org-agenda= (interactive Elisp command) | #+cindex: VM links #+cindex: Wanderlust links @@ -3461,8 +3463,9 @@ current buffer: - /Org mode buffers/ :: For Org files, if there is a =<<target>>= at point, the link points - to the target. Otherwise it points to the current headline, which - is also the description. + to the target. If there is a named block (using =#+name:=) at + point, the link points to that name. Otherwise it points to the + current headline, which is also the description. #+vindex: org-id-link-to-org-use-id #+cindex: @samp{CUSTOM_ID}, property @@ -3480,6 +3483,13 @@ current buffer: timestamp, depending on ~org-id-method~. Later, when inserting the link, you need to decide which one to use. + #+vindex: org-id-link-consider-parent-id + When ~org-id-link-consider-parent-id~ is ~t~, parent =ID= properties + are considered. This allows linking to specific targets, named + blocks, or headlines (which may not have a globally unique =ID= + themselves) within the context of a parent headline or file which + does. + - /Email/News clients: VM, Rmail, Wanderlust, MH-E, Gnus/ :: #+vindex: org-link-email-description-format @@ -3753,13 +3763,15 @@ the link completion function like this: (org-link-set-parameter "type" :complete #'some-completion-function) #+end_src -** Search Options in File Links +** Search Options in File and ID Links :PROPERTIES: :DESCRIPTION: Linking to a specific location. :ALT_TITLE: Search Options :END: #+cindex: search option in file links +#+cindex: search option in id links #+cindex: file links, searching +#+cindex: id links, searching #+cindex: attachment links, searching File links can contain additional information to make Emacs jump to a @@ -3771,8 +3783,8 @@ example, when the command ~org-store-link~ creates a link (see line as a search string that can be used to find this line back later when following the link with {{{kbd(C-c C-o)}}}. -Note that all search options apply for Attachment links in the same -way that they apply for File links. +Note that all search options apply for Attachment and ID links in the +same way that they apply for File links. Here is the syntax of the different ways to attach a search to a file link, together with explanations for each: @@ -21252,7 +21264,7 @@ The following =ol-man.el= file implements it PATH should be a topic that can be thrown at the man command." (funcall org-man-command path)) -(defun org-man-store-link () +(defun org-man-store-link (&optional _interactive?) "Store a link to a man page." (when (memq major-mode '(Man-mode woman-mode)) ;; This is a man page, we do make this link. @@ -21312,13 +21324,15 @@ A review of =ol-man.el=: For example, ~org-man-store-link~ is responsible for storing a link when ~org-store-link~ (see [[*Handling Links]]) is called from a buffer - displaying a man page. It first checks if the major mode is - appropriate. If check fails, the function returns ~nil~, which - means it isn't responsible for creating a link to the current - buffer. Otherwise the function makes a link string by combining - the =man:= prefix with the man topic. It also provides a default - description. The function ~org-insert-link~ can insert it back - into an Org buffer later on. + displaying a man page. It is passed an argument ~interactive?~ + which this function does not use, but other store functions use to + behave differently when a link is stored interactively by the user. + It first checks if the major mode is appropriate. If check fails, + the function returns ~nil~, which means it isn't responsible for + creating a link to the current buffer. Otherwise the function + makes a link string by combining the =man:= prefix with the man + topic. It also provides a default description. The function + ~org-insert-link~ can insert it back into an Org buffer later on. ** Adding Export Backends :PROPERTIES: diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS index 6c81221c1..426e8e820 100644 --- a/etc/ORG-NEWS +++ b/etc/ORG-NEWS @@ -283,6 +283,12 @@ timestamp object. Possible values: ~timerange~, ~daterange~, ~nil~. ~org-element-timestamp-interpreter~ takes into account this property and returns an appropriate timestamp string. +**** =org-link= store functions are passed an ~interactive?~ argument + +The ~:store:~ functions set for link types using +~org-link-set-parameters~ are now passed an ~interactive?~ argument, +indicating whether ~org-store-link~ was called interactively. + *** ~org-priority=show~ command no longer adjusts for scheduled/deadline In agenda views, ~org-priority=show~ command previously displayed the @@ -361,6 +367,18 @@ The change is breaking when ~org-use-property-inheritance~ is set to ~t~. *** ~org-babel-lilypond-compile-lilyfile~ ignores optional second argument The =TEST= parameter is better served by Emacs debugging tools. + +*** ~org-id-store-link~ now adds search strings for precise link targets + +This new behaviour can be disabled generally by setting +~org-id-link-use-context~ to ~nil~, or when storing a specific link by +passing a prefix argument to ~org-store-link~. + +When using this feature, IDs should not include =::=, which is used in +links to indicate the start of the search string. For backwards +compability, existing IDs including =::= will still be matched (but +cannot be used together with precise link targets). + ** New and changed options *** New variable ~org-clock-out-removed-last-clock~ @@ -544,6 +562,22 @@ Currently implemented options are: The capture template expansion element =%K= creates links using ~org-store-link~, which respects the values of ~org-id-link-to-use-id~. +*** New option ~org-id-link-consider-parent-id~ to allow =id:= links to parent headlines + +For =id:= links, when this option is enabled, ~org-store-link~ will +look for ids from parent/ancestor headlines, if the current headline +does not have an id. + +Combined with the new ability for =id:= links to use search strings +for precise link targets (when =org-id-link-use-context= is =t=, which +is the default), this allows linking to specific headlines without +requiring every headline to have an id property, as long as the +headline is unique within a subtree that does have an id property. + +By giving files top-level id properties, links to headlines in the +file can be made more robust by using the file id instead of the file +path. + ** New features *** =ob-plantuml.el=: Support tikz file format output @@ -808,6 +842,11 @@ as the function can also act on objects. *** ~org-export-get-parent~ is renamed to ~org-element-parent~ and moved to =lisp/org-element.el= *** ~org-export-get-parent-element~ is renamed to ~org-element-parent-element~ and moved to =lisp/org-element.el= +*** New optional argument for ~org-id-get~ and ~org-id-get-create~ + +New optional argument =INHERIT= means inherited ID properties from +parent entries are considered when getting an entry's ID (see +~org-id-link-consider-parent-id~ option). ** Miscellaneous *** =org-crypt.el= now applies initial visibility settings to decrypted entries diff --git a/lisp/ol.el b/lisp/ol.el index 6480b780d..b6f5c7ce7 100644 --- a/lisp/ol.el +++ b/lisp/ol.el @@ -63,7 +63,6 @@ (declare-function org-find-property "org" (property &optional value)) (declare-function org-get-heading "org" (&optional no-tags no-todo no-priority no-comment)) (declare-function org-id-find-id-file "org-id" (id)) -(declare-function org-id-store-link "org-id" ()) (declare-function org-insert-heading "org" (&optional arg invisible-ok top)) (declare-function org-load-modules-maybe "org" (&optional force)) (declare-function org-mark-ring-push "org" (&optional pos buffer)) @@ -818,6 +817,44 @@ spec." (org-with-point-at (car region) (not (org-in-regexp org-link-any-re)))) +(defun org-link--try-link-store-functions (interactive?) + "Try storing external links, prompting if more than one is possible. + +Each function returned by `org-store-link-functions' is called in +turn. If multiple functions return non-nil, prompt for which +link should be stored. + +Return t when a link has been stored in `org-link-store-props'." + (let ((results-alist nil)) + (dolist (f (org-store-link-functions)) + (when (condition-case nil + (funcall f interactive?) + ;; XXX: The store function used (< Org 9.7) to accept no + ;; arguments; provide backward compatibility support for + ;; them. + (wrong-number-of-arguments + (funcall f))) + ;; XXX: return value is not link's plist, so we + ;; store the new value before it is modified. It + ;; would be cleaner to ask store link functions to + ;; return the plist instead. + (push (cons f (copy-sequence org-store-link-plist)) + results-alist))) + (pcase results-alist + (`nil nil) + (`((,_ . ,_)) t) ;single choice: nothing to do + (`((,name . ,_) . ,_) + ;; Reinstate link plist associated to the chosen + ;; function. + (apply #'org-link-store-props + (cdr (assoc-string + (completing-read + (format "Store link with (default %s): " name) + (mapcar #'car results-alist) + nil t nil nil (symbol-name name)) + results-alist))) + t)))) + \f ;;; Public API @@ -1335,6 +1372,57 @@ priority cookie or tag." (org-link--normalize-string (or string (org-get-heading t t t t))))) +(defun org-link-precise-link-target (&optional relative-to) + "Determine search string and description for storing a link. + +If a search string is found, return cons cell (SEARCH-STRING +. DESC). Otherwise, return nil. + +If there is an active region, the contents is used (see +`org-link--context-from-region'). + +In org-mode buffers, if point is at a named element (e.g. a +source block), the name is used. If within a heading, the current +heading is used. + +If none of those finds a suitable search string, the current line +is used as the search string. + +Optional argument RELATIVE-TO specifies the buffer position where +the search will start from. If the search target that would be +returned is already at this location, return nil to avoid +unnecessary search strings (for example, when using search +strings to find targets within org-id links)." + (let ((result + (cond + ((derived-mode-p 'org-mode) + (let* ((element (org-element-at-point)) + (name (org-element-property :name element)) + (heading (org-element-lineage element 'headline t))) + (cond + ((let ((region (org-link--context-from-region))) + (and region (cons (org-link--normalize-string region t) nil)))) + (name + (cons name name)) + ((org-before-first-heading-p) + (cons (org-link--normalize-string (org-current-line-string) t) nil)) + ((and heading + (> (org-element-begin heading) (or relative-to 0))) + (cons (org-link-heading-search-string) + (org-link--normalize-string + (org-get-heading t t t t))))))) + + ;; Not in an org-mode buffer + (t + (cons (org-link--normalize-string + (or (org-link--context-from-region) (org-current-line-string)) + t) + nil))))) + + ;; Only use search option if there is some text. + (when (org-string-nw-p (car result)) + result))) + (defun org-link-open-as-file (path in-emacs) "Pretend PATH is a file name and open it. @@ -1557,36 +1645,17 @@ non-nil." (move-beginning-of-line 2) (set-mark (point))))) (setq org-store-link-plist nil) - (let (link cpltxt desc search custom-id agenda-link) ;; description + (let ((org-link-context-for-files (org-xor org-link-context-for-files + (equal arg '(4)))) + link cpltxt desc search custom-id agenda-link) ;; description (cond ;; Store a link using an external link type, if any function is - ;; available. If more than one can generate a link from current - ;; location, ask which one to use. + ;; available. If more than one can generate a link from + ;; current location, ask which one to use. Negate + ;; `org-context-in-file-links' when given a single prefix arg. ((and (not (equal arg '(16))) - (let ((results-alist nil)) - (dolist (f (org-store-link-functions)) - (when (funcall f) - ;; XXX: return value is not link's plist, so we - ;; store the new value before it is modified. It - ;; would be cleaner to ask store link functions to - ;; return the plist instead. - (push (cons f (copy-sequence org-store-link-plist)) - results-alist))) - (pcase results-alist - (`nil nil) - (`((,_ . ,_)) t) ;single choice: nothing to do - (`((,name . ,_) . ,_) - ;; Reinstate link plist associated to the chosen - ;; function. - (apply #'org-link-store-props - (cdr (assoc-string - (completing-read - (format "Store link with (default %s): " name) - (mapcar #'car results-alist) - nil t nil nil (symbol-name name)) - results-alist))) - t)))) - (setq link (plist-get org-store-link-plist :link)) + (org-link--try-link-store-functions interactive?)) + (setq link (plist-get org-store-link-plist :link)) ;; If store function actually set `:description' property, use ;; it, even if it is nil. Otherwise, fallback to nil (ask user). (setq desc (plist-get org-store-link-plist :description))) @@ -1637,6 +1706,7 @@ non-nil." (org-with-point-at m (setq agenda-link (org-store-link nil interactive?)))))) + ;; Calendar mode ((eq major-mode 'calendar-mode) (let ((cd (calendar-cursor-to-date))) (setq link @@ -1645,6 +1715,7 @@ non-nil." (org-encode-time 0 0 0 (nth 1 cd) (nth 0 cd) (nth 2 cd)))) (org-link-store-props :type "calendar" :date cd))) + ;; Image mode ((eq major-mode 'image-mode) (setq cpltxt (concat "file:" (abbreviate-file-name buffer-file-name)) @@ -1662,12 +1733,20 @@ non-nil." (setq cpltxt (concat "file:" file) link cpltxt))) + ;; Try `org-create-file-search-functions`. If any are + ;; successful, create a file link to the current buffer with + ;; the provided search string. (sets `link` and `cpltxt` to + ;; the same thing; it looks like the intention originally was + ;; that cpltxt was a description, which might have been set by + ;; the search-function (removed in switch to lexical binding)). ((setq search (run-hook-with-args-until-success 'org-create-file-search-functions)) (setq link (concat "file:" (abbreviate-file-name buffer-file-name) "::" search)) (setq cpltxt (or link))) ;; description + ;; Main logic for storing built-in link types in org-mode + ;; buffers ((and (buffer-file-name (buffer-base-buffer)) (derived-mode-p 'org-mode)) (org-with-limited-levels (setq custom-id (org-entry-get nil "CUSTOM_ID")) @@ -1687,71 +1766,33 @@ non-nil." desc nil ;; Do not append #CUSTOM_ID link below. custom-id nil)) - ((and (featurep 'org-id) - (or (eq org-id-link-to-org-use-id t) - (and interactive? - (or (eq org-id-link-to-org-use-id 'create-if-interactive) - (and (eq org-id-link-to-org-use-id - 'create-if-interactive-and-no-custom-id) - (not custom-id)))) - (and org-id-link-to-org-use-id (org-entry-get nil "ID")))) - ;; Store a link using the ID at point - (setq link (condition-case nil - (prog1 (org-id-store-link) - (setq desc (plist-get org-store-link-plist :description))) - (error - ;; Probably before first headline, link only to file - (concat "file:" - (abbreviate-file-name - (buffer-file-name (buffer-base-buffer)))))))) - (t + (t ;; Just link to current headline. (setq cpltxt (concat "file:" (abbreviate-file-name (buffer-file-name (buffer-base-buffer))))) - ;; Add a context search string. - (when (org-xor org-link-context-for-files (equal arg '(4))) - (let* ((element (org-element-at-point)) - (name (org-element-property :name element)) - (context - (cond - ((let ((region (org-link--context-from-region))) - (and region (org-link--normalize-string region t)))) - (name) - ((org-before-first-heading-p) - (org-link--normalize-string (org-current-line-string) t)) - (t (org-link-heading-search-string))))) - (when (org-string-nw-p context) - (setq cpltxt (format "%s::%s" cpltxt context)) - (setq desc - (or name - ;; Although description is not a search - ;; string, use `org-link--normalize-string' - ;; to prettify it (contiguous white spaces) - ;; and remove volatile contents (statistics - ;; cookies). - (and (not (org-before-first-heading-p)) - (org-link--normalize-string - (org-get-heading t t t t))) - "NONE"))))) - (setq link cpltxt))))) - + (when org-link-context-for-files + (pcase (org-link-precise-link-target) + (`nil nil) + (`(,search-string . ,search-desc) + (setq cpltxt (format "%s::%s" cpltxt search-string)) + (setq desc search-desc)))) + (setq link cpltxt))))) + + ;; Buffer linked to file, but not an org-mode buffer. ((buffer-file-name (buffer-base-buffer)) ;; Just link to this file here. (setq cpltxt (concat "file:" (abbreviate-file-name (buffer-file-name (buffer-base-buffer))))) ;; Add a context search string. - (when (org-xor org-link-context-for-files (equal arg '(4))) - (let ((context (org-link--normalize-string - (or (org-link--context-from-region) - (org-current-line-string)) - t))) - ;; Only use search option if there is some text. - (when (org-string-nw-p context) - (setq cpltxt (format "%s::%s" cpltxt context)) - (setq desc "NONE")))) - (setq link cpltxt)) + (when org-link-context-for-files + (pcase (org-link-precise-link-target) + (`nil nil) + (`(,search-string . ,search-desc) + (setq cpltxt (format "%s::%s" cpltxt search-string)) + (setq desc search-desc)))) + (setq link cpltxt)) (interactive? (user-error "No method for storing a link from this buffer")) diff --git a/lisp/org-id.el b/lisp/org-id.el index fbe6a0ed0..f3175b23e 100644 --- a/lisp/org-id.el +++ b/lisp/org-id.el @@ -129,6 +129,49 @@ nil Never use an ID to make a link, instead link using a text search for (const :tag "Only use existing" use-existing) (const :tag "Do not use ID to create link" nil))) +(defcustom org-id-link-consider-parent-id nil + "Non-nil means storing a link to an Org entry considers inherited IDs. + +When this option is non-nil, ID properties inherited from parent +entries will be considered when storing an ID link. If no ID is +found in this way, a new one may be created as normal (see +`org-id-link-to-org-use-id'). + +For example, given this org file: + +* Parent +:PROPERTIES: +:ID: abc +:END: +** Child 1 +** Child 2 + +With `org-id-link-consider-parent-id' set to t, storing a link +with point at \"Child 1\" will produce a link \"id:abc\" to +\"Parent\". + +This is particularly useful with `org-id-link-use-context' +enabled, as it allows linking to uniquely-named sub-entries +within a parent entry with an ID, without requiring every +sub-entry to have its own ID. In that case, the example link +above would be \"id:abc::*Child 1\", which links directly to +\"Child 1\" despite it not having its own ID property." + :group 'org-link-store + :group 'org-id + :package-version '(Org . "9.7") + :type 'boolean) + +(defcustom org-id-link-use-context t + "Non-nil means enables search string context in org-id links. + +Search strings are added by `org-id-store-link' when both the +general option `org-link-context-for-files' and the org-id option +`org-id-link-use-context' are non-nil." + :group 'org-link-store + :group 'org-id + :package-version '(Org . "9.7") + :type 'boolean) + (defcustom org-id-uuid-program "uuidgen" "The uuidgen program." :group 'org-id @@ -258,14 +301,17 @@ This variable is only relevant when `org-id-track-globally' is set." ;;; The API functions ;;;###autoload -(defun org-id-get-create (&optional force) +(defun org-id-get-create (&optional force inherit) "Create an ID for the current entry and return it. If the entry already has an ID, just return it. -With optional argument FORCE, force the creation of a new ID." +With optional argument FORCE, force the creation of a new ID. +With optional argument INHERIT, consider parents' IDs if the +current entry does not have one." (interactive "P") (when force - (org-entry-put (point) "ID" nil)) - (org-id-get (point) 'create)) + (org-entry-put (point) "ID" nil) + (setq inherit nil)) + (org-id-get (point) 'create nil inherit)) ;;;###autoload (defun org-id-copy () @@ -280,15 +326,16 @@ This is useful when working with contents in a temporary buffer that will be copied back to the original.") ;;;###autoload -(defun org-id-get (&optional epom create prefix) +(defun org-id-get (&optional epom create prefix inherit) "Get the ID property of the entry at EPOM. EPOM is an element, marker, or buffer position. If EPOM is nil, refer to the entry at point. If the entry does not have an ID, the function returns nil. +If INHERIT is non-nil, parents' IDs are also considered. However, when CREATE is non-nil, create an ID if none is present already. PREFIX will be passed through to `org-id-new'. In any case, the ID of the entry is returned." - (let ((id (org-entry-get epom "ID"))) + (let ((id (org-entry-get epom "ID" (and inherit t)))) (cond ((and id (stringp id) (string-match "\\S-" id)) id) @@ -707,14 +754,26 @@ optional argument MARKERP, return the position as a new marker." (defun org-id-store-link () "Store a link to the current entry, using its ID. -If before first heading store first title-keyword as description -or filename if no title." +The link description is based on the heading, or if before the +first heading, the title keyword if available, or else the +filename. + +When `org-link-context-for-files' and `org-id-link-use-context' +are non-nil, add a search string to the link. The link +description is then based on the search string target. + +When `org-id-link-consider-parent-id' is non-nil, ID properties +are inherited from parent entries." (interactive) - (when (and (buffer-file-name (buffer-base-buffer)) (derived-mode-p 'org-mode)) - (let* ((link (concat "id:" (org-id-get-create))) + (when (and (buffer-file-name (buffer-base-buffer)) + (derived-mode-p 'org-mode)) + (let* ((link (concat "id:" (org-id-get-create nil org-id-link-consider-parent-id))) + (id-location (or (and org-entry-property-inherited-from + (marker-position org-entry-property-inherited-from)) + (save-excursion (org-back-to-heading-or-point-min) (point)))) (case-fold-search nil) (desc (save-excursion - (org-back-to-heading-or-point-min t) + (goto-char id-location) (cond ((org-before-first-heading-p) (let ((keywords (org-collect-keywords '("TITLE")))) (if keywords @@ -726,14 +785,59 @@ or filename if no title." (match-string 4) (match-string 0))) (t link))))) + (when (and org-link-context-for-files org-id-link-use-context) + (pcase (org-link-precise-link-target id-location) + (`nil nil) + (`(,search-string . ,search-desc) + (setq link (concat link "::" search-string)) + (setq desc search-desc)))) (org-link-store-props :link link :description desc :type "id") link))) -(defun org-id-open (id _) - "Go to the entry with id ID." - (org-mark-ring-push) - (let ((m (org-id-find id 'marker)) - cmd) +;;;###autoload +(defun org-id-store-link-maybe (&optional interactive?) + "Store a link to the current entry using its ID if enabled. + +The value of `org-id-link-to-org-use-id' determines whether an ID +link should be stored, using `org-id-store-link'. + +Assume the function is called interactively if INTERACTIVE? is +non-nil." + (interactive "p") + (when (and (buffer-file-name (buffer-base-buffer)) + (derived-mode-p 'org-mode) + (or (eq org-id-link-to-org-use-id t) + (and interactive? + (or (eq org-id-link-to-org-use-id 'create-if-interactive) + (and (eq org-id-link-to-org-use-id + 'create-if-interactive-and-no-custom-id) + (not (org-entry-get nil "CUSTOM_ID"))))) + (and org-id-link-to-org-use-id + (org-entry-get nil "ID" org-id-link-consider-parent-id)))) + (org-id-store-link))) + +(defun org-id-open (link _) + "Go to the entry indicated by id link LINK. + +The link can include a search string after \"::\", which is +passed to `org-link-search'. + +For backwards compatibility with IDs that contain \"::\", if no +match is found for the ID, the full link string including \"::\" +will be tried as an ID." + (let* ((option (and (string-match "::\\(.*\\)\\'" link) + (match-string 1 link))) + (id (if (not option) link + (substring link 0 (match-beginning 0)))) + m cmd) + (org-mark-ring-push) + (setq m (org-id-find id 'marker)) + (when (and (not m) option) + ;; Backwards compatibility: if id is not found, try treating + ;; whole link as an id. + (setq m (org-id-find link 'marker)) + (when m + (setq option nil))) (unless m (error "Cannot find entry with ID \"%s\"" id)) ;; Use a buffer-switching command in analogy to finding files @@ -750,9 +854,16 @@ or filename if no title." (funcall cmd (marker-buffer m))) (goto-char m) (move-marker m nil) + (when option + (save-restriction + (unless (org-before-first-heading-p) + (org-narrow-to-subtree)) + (org-link-search option))) (org-fold-show-context))) -(org-link-set-parameters "id" :follow #'org-id-open) +(org-link-set-parameters "id" + :follow #'org-id-open + :store #'org-id-store-link-maybe) (provide 'org-id) diff --git a/testing/lisp/test-ol.el b/testing/lisp/test-ol.el index e0cec0854..fa8d15c2b 100644 --- a/testing/lisp/test-ol.el +++ b/testing/lisp/test-ol.el @@ -381,6 +381,132 @@ See https://github.com/yantar92/org/issues/4." (equal (format "[[file:%s::*foo bar][foo bar]]" file file) (org-store-link nil))))))) +(ert-deftest test-org-link/precise-link-target () + "Test `org-link-precise-link-target` specifications." + (should + (equal '("*H1" . "H1") + (org-test-with-temp-text "* H1<point>\n* H2\n" + (org-link-precise-link-target)))) + (should + (equal '("foo" . "foo") + (org-test-with-temp-text "* H1\n#+name: foo<point>\n#+begin_example\nhi\n#+end_example\n" + (org-link-precise-link-target)))) + (should + (equal '("Text" . nil) + (org-test-with-temp-text "\nText<point>\n* H1\n" + (org-link-precise-link-target)))) + (should + (equal nil + (org-test-with-temp-text "\n<point>\n* H1\n" + (org-link-precise-link-target)))) + ;; relative to a heading + (should + (equal nil + (org-test-with-temp-text "* H1<point>\n* H2\n" + (org-link-precise-link-target 1)))) + (should + (equal '("*H2" . "H2") + (org-test-with-temp-text "* H1\n* H2<point>\n" + (org-link-precise-link-target 1)))) + (should + (equal nil + (org-test-with-temp-text "* H1\n* H2<point>\n" + (org-link-precise-link-target 6)))) + ) + +(defmacro test-ol-stored-link-with-text (text &rest body) + "Return :link and :description from link stored in body." + (declare (indent 1)) + `(let (org-store-link-plist) + (org-test-with-temp-text-in-file ,text + ,@body + (list (plist-get org-store-link-plist :link) + (plist-get org-store-link-plist :description))))) + +(ert-deftest test-org-link/id-store-link () + "Test `org-id-store-link' specifications." + (let ((org-id-link-to-org-use-id nil)) + (should + (equal '(nil nil) + (test-ol-stored-link-with-text "* H1\n:PROPERTIES:\n:ID: abc\n:END:\n" + (org-id-store-link-maybe t))))) + ;; On a headline, link to that headline's ID. Use heading as the + ;; description of the link. + (let ((org-id-link-to-org-use-id t)) + (should + (equal '("id:abc" "H1") + (test-ol-stored-link-with-text "* H1\n:PROPERTIES:\n:ID: abc\n:END:\n" + (org-id-store-link-maybe t))))) + ;; Remove TODO keywords etc from description of the link. + (let ((org-id-link-to-org-use-id t)) + (should + (equal '("id:abc" "H1") + (test-ol-stored-link-with-text "* TODO [#A] H1 :tag:\n:PROPERTIES:\n:ID: abc\n:END:\n" + (org-id-store-link-maybe t))))) + ;; create-if-interactive + (let ((org-id-link-to-org-use-id 'create-if-interactive)) + (should + (equal '("id:abc" "H1") + (cl-letf (((symbol-function 'org-id-new) + (lambda (&rest _rest) "abc"))) + (test-ol-stored-link-with-text "* H1\n" + (org-id-store-link-maybe t))))) + (should + (equal '(nil nil) + (test-ol-stored-link-with-text "* H1\n" + (org-id-store-link-maybe nil))))) + ;; create-if-interactive-and-no-custom-id + (let ((org-id-link-to-org-use-id 'create-if-interactive-and-no-custom-id)) + (should + (equal '("id:abc" "H1") + (cl-letf (((symbol-function 'org-id-new) + (lambda (&rest _rest) "abc"))) + (test-ol-stored-link-with-text "* H1\n" + (org-id-store-link-maybe t))))) + (should + (equal '(nil nil) + (test-ol-stored-link-with-text "* H1\n:PROPERTIES:\n:CUSTOM_ID: xyz\n:END:\n" + (org-id-store-link-maybe t)))) + (should + (equal '(nil nil) + (test-ol-stored-link-with-text "* H1\n" + (org-id-store-link-maybe nil)))))) + +(ert-deftest test-org-link/id-store-link-using-parent () + "Test `org-id-store-link' specifications with `org-id-link-consider-parent-id` set." + ;; when using context to still find specific heading + (let ((org-id-link-consider-parent-id t) + (org-id-link-use-context t)) + (should + (equal '("id:abc::*H2" "H2") + (test-ol-stored-link-with-text "* H1\n:PROPERTIES:\n:ID: abc\n:END:\n** H2\n<point>" + (org-id-store-link)))) + (should + (equal '("id:abc::name" "name") + (test-ol-stored-link-with-text "* H1\n:PROPERTIES:\n:ID: abc\n:END:\n\n#+name: name\n<point>#+begin_example\nhi\n#+end_example\n" + (org-id-store-link)))) + (should + (equal '("id:abc" "H1") + (test-ol-stored-link-with-text "* H1<point>\n:PROPERTIES:\n:ID: abc\n:END:\n** H2\n" + (org-id-store-link))))) + ;; when not using context, description should be the parent/file + (let ((org-id-link-consider-parent-id t) + (org-id-link-use-context nil)) + (should + (equal '("id:abc" "H1") + (test-ol-stored-link-with-text "* H1\n:PROPERTIES:\n:ID: abc\n:END:\n** H2\n<point>" + (org-id-store-link)))) + (should + (let ((result (test-ol-stored-link-with-text ":PROPERTIES:\n:ID: top\n:END:\n:* H1\n<point>" + (org-id-store-link)))) + (equal "id:top" (car result)) + ;; strip random buffer file name + (equal "org-test" (substring (cadr result) 0 8)))) + (should + (equal '("id:top" "title") + (test-ol-stored-link-with-text ":PROPERTIES:\n:ID: top\n:END:\n#+TITLE: title\n\n:* H1\n<point>" + (org-id-store-link)))))) + \f ;;; Radio Targets ^ permalink raw reply related [flat|nested] 48+ messages in thread
* Re: [PATCH v2] org-id: allow using parent's existing id in links to headlines 2023-12-17 19:07 ` [PATCH v2] " Rick Lupton @ 2023-12-18 12:27 ` Ihor Radchenko 2024-01-02 16:13 ` Rick Lupton 2024-01-28 22:47 ` Rick Lupton 0 siblings, 2 replies; 48+ messages in thread From: Ihor Radchenko @ 2023-12-18 12:27 UTC (permalink / raw) To: Rick Lupton; +Cc: Y. E. "Rick Lupton" <mail@ricklupton.name> writes: > Please find attached updated patch which I think addresses all the points discussed. Let me know if you see any further changes needed. Thanks! I played around with the patch a bit and found a couple of rough edges: 1. When I try to open a link to non-existing search target, like <id:some-id::non-existing-target>, I get a query to create a new heading. If I reply "yes", a new heading is created. However, the heading is created at the end of the file and is always level 1, regardless of the "some-id" parent context. It would make more sense to create a new heading at the end of the id:some-id subtree. 2. Consider the following setting: (setq org-id-link-consider-parent-id t) (setq org-id-link-to-org-use-id 'create-if-interactive-and-no-custom-id) Then, create the following Org file * Sub * Parent here ** This is test :PROPERTIES: :ID: fe40252e-0527-44c1-a990-12498991f167 :END: *** Sub <point here> :PROPERTIES: :CUSTOM_ID: subid :END: When you M-x org-store-link, the stored link has ::*Sub instead of the expected ::#subid 3. Consider (setq org-id-link-consider-parent-id t) (setq org-id-link-to-org-use-id t) Then, create a new empty Org file M-x org-store-link with create a top-level properties drawer with ID and store the link. However, that link will not be a simple ID link, but also have ::PROPERTIES search string, which is not expected. More inline comments below. > + #+vindex: org-id-link-consider-parent-id > + When ~org-id-link-consider-parent-id~ is ~t~, parent =ID= properties > + are considered. This allows linking to specific targets, named > + blocks, or headlines (which may not have a globally unique =ID= > + themselves) within the context of a parent headline or file which > + does. It would be nice to add an example, similar to what you did in the docstring. > -(defun org-man-store-link () > +(defun org-man-store-link (&optional _interactive?) > "Store a link to a man page." > (when (memq major-mode '(Man-mode woman-mode)) > ;; This is a man page, we do make this link. > @@ -21312,13 +21324,15 @@ A review of =ol-man.el=: Please, update the actual built-in :store functions in lisp/ol-*.el to handle the new optional argument as well. > +**** =org-link= store functions are passed an ~interactive?~ argument > + > +The ~:store:~ functions set for link types using > +~org-link-set-parameters~ are now passed an ~interactive?~ argument, > +indicating whether ~org-store-link~ was called interactively. Please also explain that the existing functions are not broken. > +*** ~org-id-store-link~ now adds search strings for precise link targets > + > +This new behaviour can be disabled generally by setting > +~org-id-link-use-context~ to ~nil~, or when storing a specific link by > +passing a prefix argument to ~org-store-link~. universal argument. There are several possible prefix arguments in `org-store-link', but only C-u (universal argument) will give the described effect. Also, won't the behavior be _toggled_ by the universal argument? > +When using this feature, IDs should not include =::=, which is used in > +links to indicate the start of the search string. For backwards > +compability, existing IDs including =::= will still be matched (but > +cannot be used together with precise link targets). Please add an org-lint checker that warns about such IDs and mention this checker in the above. Also, this paragraph belongs to "Breaking changes", not "new and changed options". > +*** New option ~org-id-link-consider-parent-id~ to allow =id:= links to parent headlines > + > +For =id:= links, when this option is enabled, ~org-store-link~ will > +look for ids from parent/ancestor headlines, if the current headline > +does not have an id. > + > +Combined with the new ability for =id:= links to use search strings > +for precise link targets (when =org-id-link-use-context= is =t=, which > +is the default), this allows linking to specific headlines without > +requiring every headline to have an id property, as long as the > +headline is unique within a subtree that does have an id property. > + > +By giving files top-level id properties, links to headlines in the > +file can be made more robust by using the file id instead of the file > +path. Please, provide an example here as well. > +(defun org-link--try-link-store-functions (interactive?) > + "Try storing external links, prompting if more than one is possible. > + > +Each function returned by `org-store-link-functions' is called in > +turn. If multiple functions return non-nil, prompt for which > +link should be stored. > + > +Return t when a link has been stored in `org-link-store-props'." Please document INTERACTIVE? argument in the docstring. > + (let ((results-alist nil)) > + (dolist (f (org-store-link-functions)) > + (when (condition-case nil > + (funcall f interactive?) > + ;; XXX: The store function used (< Org 9.7) to accept no > + ;; arguments; provide backward compatibility support for > + ;; them. Use FIXME, not XXX. (I have no idea why it is XXX in the existing code). > +(defun org-link-precise-link-target (&optional relative-to) > + "Determine search string and description for storing a link. > + > +If a search string is found, return cons cell (SEARCH-STRING > +. DESC). Otherwise, return nil. > + > +If there is an active region, the contents is used (see > +`org-link--context-from-region'). It is not clear from this sentence whether the contents is used for SEARCH-STRING of DESC. > +In org-mode buffers, if point is at a named element (e.g. a > +source block), the name is used. If within a heading, the current > +heading is used. Please use double space between sentences. > +Optional argument RELATIVE-TO specifies the buffer position where > +the search will start from. If the search target that would be > +returned is already at this location, return nil to avoid > +unnecessary search strings (for example, when using search > +strings to find targets within org-id links)." It is not clear what will happen if RELATIVE-TO is before/after point. > - (let (link cpltxt desc search custom-id agenda-link) ;; description > + (let ((org-link-context-for-files (org-xor org-link-context-for-files > + (equal arg '(4)))) > + link cpltxt desc search custom-id agenda-link) ;; description > (cond > ;; Store a link using an external link type, if any function is > - ;; available. If more than one can generate a link from current > - ;; location, ask which one to use. > + ;; available. If more than one can generate a link from > + ;; current location, ask which one to use. Negate > + ;; `org-context-in-file-links' when given a single prefix arg. The part of the comment about negation, should probably be moved near the let binding of `org-link-context-for-files'. > +For example, given this org file: > + > +* Parent > +:PROPERTIES: > +:ID: abc > +:END: > +** Child 1 > +** Child 2 > + > +With `org-id-link-consider-parent-id' set to t, storing a link > +with point at \"Child 1\" will produce a link \"id:abc\" to > +\"Parent\". This is actually confusing. May we only consider parent when `org-id-link-use-context' is enabled? > -(defun org-id-get (&optional epom create prefix) > +(defun org-id-get (&optional epom create prefix inherit) > "Get the ID property of the entry at EPOM. > EPOM is an element, marker, or buffer position. > If EPOM is nil, refer to the entry at point. > If the entry does not have an ID, the function returns nil. > +If INHERIT is non-nil, parents' IDs are also considered. > However, when CREATE is non-nil, create an ID if none is present already. > PREFIX will be passed through to `org-id-new'. > In any case, the ID of the entry is returned." What about both CREATE and INHERIT being non-nil? > +;;;###autoload > +(defun org-id-store-link-maybe (&optional interactive?) > + "Store a link to the current entry using its ID if enabled. > + > +The value of `org-id-link-to-org-use-id' determines whether an ID > +link should be stored, using `org-id-store-link'. > + > +Assume the function is called interactively if INTERACTIVE? is > +non-nil." > + (interactive "p") Do we really need to make it interactive? -- Ihor Radchenko // yantar92, Org mode contributor, Learn more about Org mode at <https://orgmode.org/>. Support Org development at <https://liberapay.com/org-mode>, or support my work at <https://liberapay.com/yantar92> ^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH v2] org-id: allow using parent's existing id in links to headlines 2023-12-18 12:27 ` Ihor Radchenko @ 2024-01-02 16:13 ` Rick Lupton 2024-01-03 14:17 ` Ihor Radchenko 2024-01-28 22:47 ` Rick Lupton 1 sibling, 1 reply; 48+ messages in thread From: Rick Lupton @ 2024-01-02 16:13 UTC (permalink / raw) To: Ihor Radchenko; +Cc: Y. E. On Mon, 18 Dec 2023, at 12:27 PM, Ihor Radchenko wrote: > I played around with the patch a bit and found a couple of rough edges: > > 1. When I try to open a link to non-existing search target, like > <id:some-id::non-existing-target>, I get a query to create a new > heading. If I reply "yes", a new heading is created. However, the > heading is created at the end of the file and is always level 1, > regardless of the "some-id" parent context. > It would make more sense to create a new heading at the end of the > id:some-id subtree. Thanks for the comments. On this point, I'd like to modify `org-insert-heading' to allow for choosing the level of the newly inserted heading, but first wanted to check if you have a preference for how to change it. I think it would be simplest to change the current: (defun org-insert-heading (&optional arg invisible-ok top) "...When optional argument TOP is non-nil, insert a level 1 heading, unconditionally." to: (defun org-insert-heading (&optional arg invisible-ok level) "...When optional argument LEVEL is a number, insert a heading at that level. For backwards compatibility, when LEVEL is non-nil but not a number, insert a level-1 heading." but that is not totally backwards compatible -- is that ok? If it should be completely backwards compatible, alternatively could add an additional optional argument: (defun org-insert-heading (&optional arg invisible-ok top top-level) "...When optional argument TOP is non-nil, insert a top-level heading, unconditionally. When TOP-LEVEL is non-nil, use that level, otherwise level 1." Alternatively I could preserve the intention of TOP but add a special value to change what "top-level" means, so the docstring would become something like this: "When optional argument TOP is non-nil, insert a top-level heading, unconditionally. Specifically, when TOP is `relative', \"top-level\" means one level deeper than the outline level at minimum point position (respecting any narrowing of the buffer). Otherwise, \"top-level\" means level 1." (the motivation for this is that when the buffer is narrowed to the subtree with the matching ID, the new heading will be created at the appropriate level). Best Rick ^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH v2] org-id: allow using parent's existing id in links to headlines 2024-01-02 16:13 ` Rick Lupton @ 2024-01-03 14:17 ` Ihor Radchenko 0 siblings, 0 replies; 48+ messages in thread From: Ihor Radchenko @ 2024-01-03 14:17 UTC (permalink / raw) To: Rick Lupton; +Cc: Y. E. "Rick Lupton" <mail@ricklupton.name> writes: > Thanks for the comments. On this point, I'd like to modify `org-insert-heading' to allow for choosing the level of the newly inserted heading, but first wanted to check if you have a preference for how to change it. > > > I think it would be simplest to change the current: > > (defun org-insert-heading (&optional arg invisible-ok top) > "...When optional argument TOP is non-nil, insert a level 1 heading, unconditionally." > > to: > > (defun org-insert-heading (&optional arg invisible-ok level) > "...When optional argument LEVEL is a number, insert a heading at that level. For backwards compatibility, when LEVEL is non-nil but not a number, insert a level-1 heading." > > but that is not totally backwards compatible -- is that ok? I think that it is OK. I very much doubt that anyone at all uses numerical value for TOP in the existing `org-insert-heading' calls from Elisp. And adding yet another extra optional argument is not justified in this particular case. -- Ihor Radchenko // yantar92, Org mode contributor, Learn more about Org mode at <https://orgmode.org/>. Support Org development at <https://liberapay.com/org-mode>, or support my work at <https://liberapay.com/yantar92> ^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH v2] org-id: allow using parent's existing id in links to headlines 2023-12-18 12:27 ` Ihor Radchenko 2024-01-02 16:13 ` Rick Lupton @ 2024-01-28 22:47 ` Rick Lupton 2024-01-29 0:20 ` Samuel Wales 2024-01-29 13:00 ` Ihor Radchenko 1 sibling, 2 replies; 48+ messages in thread From: Rick Lupton @ 2024-01-28 22:47 UTC (permalink / raw) To: Ihor Radchenko; +Cc: Y. E. [-- Attachment #1: Type: text/plain, Size: 12049 bytes --] Hi, Thanks for trying it out. Updated patches attached, comments below. On Mon, 18 Dec 2023, at 12:27 PM, Ihor Radchenko wrote: > I played around with the patch a bit and found a couple of rough edges: > > 1. When I try to open a link to non-existing search target, like > <id:some-id::non-existing-target>, I get a query to create a new > heading. If I reply "yes", a new heading is created. However, the > heading is created at the end of the file and is always level 1, > regardless of the "some-id" parent context. > It would make more sense to create a new heading at the end of the > id:some-id subtree. Fixed in updated patches -- first patch adds generic new flexibility to `org-insert-heading', second patch uses it so new headings now added at correct level at the end of the id:sub-id subtree. > 2. Consider the following setting: > (setq org-id-link-consider-parent-id t) > (setq org-id-link-to-org-use-id 'create-if-interactive-and-no-custom-id) > > Then, create the following Org file > > * Sub > * Parent here > ** This is test > :PROPERTIES: > :ID: fe40252e-0527-44c1-a990-12498991f167 > :END: > > *** Sub <point here> > :PROPERTIES: > :CUSTOM_ID: subid > :END: > > When you M-x org-store-link, the stored link has ::*Sub instead of > the expected ::#subid Updated so that search strings prefer custom-ids (::#subid) to headline matches (::*Sub). This makes this example behave as you expect. The correct behaviour of org-store-link doesn't seem totally obvious to me about id vs custom-id links. Currently org-store-link has special logic to store TWO links (one <file:xx::#subid>, one <file:xx::*Sub>) when a CUSTOM_ID is present. In the manual, it says: If the headline has a ‘CUSTOM_ID’ property, store a link to this custom ID. In addition or alternatively, depending on the value of ‘org-id-link-to-org-use-id’, create and/or use a globally unique ‘ID’ property for the link(1). So using this command in Org buffers potentially creates two links: a human-readable link from the custom ID, and one that is globally unique and works even if the entry is moved from file to file. The ‘ID’ property can be either a UUID (default) or a timestamp, depending on ‘org-id-method’. Later, when inserting the link, you need to decide which one to use. That refers to ID links specifically, but now, using the generic link store functions, there is only the possibility to store one link type, so it's not possible to neatly keep exactly the same behaviour (i.e. for ID links but not for other external link types). I think the intention of what's described in the manual is to distinguish "human-readable" vs "persistent id" links. There could be other types of "persistent id" links apart from org-id links, such as mu4e: links to email message-ids. Therefore I've updated org-store-link to simply store a <file:xx.org::#custom-id> link as an additional option, whether or not the first matched link was an org-id link (this is the current behaviour) or another external link type (this is changed behaviour). Added a note to ORG-NEWS about this. > 3. Consider > (setq org-id-link-consider-parent-id t) > (setq org-id-link-to-org-use-id t) > > Then, create a new empty Org file > M-x org-store-link with create a top-level properties drawer with ID > and store the link. However, that link will not be a simple ID link, > but also have ::PROPERTIES search string, which is not expected. This is because it is trying to link to the current line of the file, which contains the text "PROPERTIES". On main, with (setq org-id-link-to-org-use-id nil), you see the equivalent behaviour (a link to [[file:test.org:::PROPERTIES:]]) when point is before the first heading. So, this seems consistent with non-org-id links? (these links don't actually work with the default value of `org-link-search-must-match-exact-headline', but I think that's a separate issue). >> + #+vindex: org-id-link-consider-parent-id >> + When ~org-id-link-consider-parent-id~ is ~t~, parent =ID= properties >> + are considered. This allows linking to specific targets, named >> + blocks, or headlines (which may not have a globally unique =ID= >> + themselves) within the context of a parent headline or file which >> + does. > > It would be nice to add an example, similar to what you did in the docstring. Added. > >> -(defun org-man-store-link () >> +(defun org-man-store-link (&optional _interactive?) >> "Store a link to a man page." >> (when (memq major-mode '(Man-mode woman-mode)) >> ;; This is a man page, we do make this link. >> @@ -21312,13 +21324,15 @@ A review of =ol-man.el=: > > Please, update the actual built-in :store functions in lisp/ol-*.el to > handle the new optional argument as well. Updated. >> +**** =org-link= store functions are passed an ~interactive?~ argument >> + >> +The ~:store:~ functions set for link types using >> +~org-link-set-parameters~ are now passed an ~interactive?~ argument, >> +indicating whether ~org-store-link~ was called interactively. > > Please also explain that the existing functions are not broken. Done. >> +*** ~org-id-store-link~ now adds search strings for precise link targets >> + >> +This new behaviour can be disabled generally by setting >> +~org-id-link-use-context~ to ~nil~, or when storing a specific link by >> +passing a prefix argument to ~org-store-link~. > > universal argument. > There are several possible prefix arguments in `org-store-link', but > only C-u (universal argument) will give the described effect. > Also, won't the behavior be _toggled_ by the universal argument? Updated. >> +When using this feature, IDs should not include =::=, which is used in >> +links to indicate the start of the search string. For backwards >> +compability, existing IDs including =::= will still be matched (but >> +cannot be used together with precise link targets). > > Please add an org-lint checker that warns about such IDs and mention > this checker in the above. Added. > Also, this paragraph belongs to "Breaking changes", not "new and changed > options". That's where it is, I think. >> +*** New option ~org-id-link-consider-parent-id~ to allow =id:= links to parent headlines >> + >> +For =id:= links, when this option is enabled, ~org-store-link~ will >> +look for ids from parent/ancestor headlines, if the current headline >> +does not have an id. >> + >> +Combined with the new ability for =id:= links to use search strings >> +for precise link targets (when =org-id-link-use-context= is =t=, which >> +is the default), this allows linking to specific headlines without >> +requiring every headline to have an id property, as long as the >> +headline is unique within a subtree that does have an id property. >> + >> +By giving files top-level id properties, links to headlines in the >> +file can be made more robust by using the file id instead of the file >> +path. > > Please, provide an example here as well. Done. >> +(defun org-link--try-link-store-functions (interactive?) >> + "Try storing external links, prompting if more than one is possible. >> + >> +Each function returned by `org-store-link-functions' is called in >> +turn. If multiple functions return non-nil, prompt for which >> +link should be stored. >> + >> +Return t when a link has been stored in `org-link-store-props'." > > Please document INTERACTIVE? argument in the docstring. Done. >> + (let ((results-alist nil)) >> + (dolist (f (org-store-link-functions)) >> + (when (condition-case nil >> + (funcall f interactive?) >> + ;; XXX: The store function used (< Org 9.7) to accept no >> + ;; arguments; provide backward compatibility support for >> + ;; them. > > Use FIXME, not XXX. (I have no idea why it is XXX in the existing code). Changed. >> +(defun org-link-precise-link-target (&optional relative-to) >> + "Determine search string and description for storing a link. >> + >> +If a search string is found, return cons cell (SEARCH-STRING >> +. DESC). Otherwise, return nil. >> + >> +If there is an active region, the contents is used (see >> +`org-link--context-from-region'). > > It is not clear from this sentence whether the contents is used for > SEARCH-STRING of DESC. > >> +In org-mode buffers, if point is at a named element (e.g. a >> +source block), the name is used. If within a heading, the current >> +heading is used. > > Please use double space between sentences. > >> +Optional argument RELATIVE-TO specifies the buffer position where >> +the search will start from. If the search target that would be >> +returned is already at this location, return nil to avoid >> +unnecessary search strings (for example, when using search >> +strings to find targets within org-id links)." > > It is not clear what will happen if RELATIVE-TO is before/after point. Updated the docstring. >> - (let (link cpltxt desc search custom-id agenda-link) ;; description >> + (let ((org-link-context-for-files (org-xor org-link-context-for-files >> + (equal arg '(4)))) >> + link cpltxt desc search custom-id agenda-link) ;; description >> (cond >> ;; Store a link using an external link type, if any function is >> - ;; available. If more than one can generate a link from current >> - ;; location, ask which one to use. >> + ;; available. If more than one can generate a link from >> + ;; current location, ask which one to use. Negate >> + ;; `org-context-in-file-links' when given a single prefix arg. > > The part of the comment about negation, should probably be moved near > the let binding of `org-link-context-for-files'. Done. >> +For example, given this org file: >> + >> +* Parent >> +:PROPERTIES: >> +:ID: abc >> +:END: >> +** Child 1 >> +** Child 2 >> + >> +With `org-id-link-consider-parent-id' set to t, storing a link >> +with point at \"Child 1\" will produce a link \"id:abc\" to >> +\"Parent\". > > This is actually confusing. May we only consider parent when > `org-id-link-use-context' is enabled? Yes, I was trying to keep them independent but I agree it's probably more useful to only consider parent when `org-id-link-use-context' is enabled (which in turn depends on `org-context-in-file-links' being enabled). >> -(defun org-id-get (&optional epom create prefix) >> +(defun org-id-get (&optional epom create prefix inherit) >> "Get the ID property of the entry at EPOM. >> EPOM is an element, marker, or buffer position. >> If EPOM is nil, refer to the entry at point. >> If the entry does not have an ID, the function returns nil. >> +If INHERIT is non-nil, parents' IDs are also considered. >> However, when CREATE is non-nil, create an ID if none is present already. >> PREFIX will be passed through to `org-id-new'. >> In any case, the ID of the entry is returned." > > What about both CREATE and INHERIT being non-nil? Rewrote the docstring. Also removed INHERIT argument for `org-id-get-create' again, as other functions can be re-written to use `org-id-get' directly, and INHERIT isn't particularly useful when using `org-id-get-create' interactively. >> +;;;###autoload >> +(defun org-id-store-link-maybe (&optional interactive?) >> + "Store a link to the current entry using its ID if enabled. >> + >> +The value of `org-id-link-to-org-use-id' determines whether an ID >> +link should be stored, using `org-id-store-link'. >> + >> +Assume the function is called interactively if INTERACTIVE? is >> +non-nil." >> + (interactive "p") > > Do we really need to make it interactive? No, removed. Thanks, Rick [-- Attachment #2: 0001-lisp-org.el-org-insert-heading-allow-specifying-head.patch --] [-- Type: application/octet-stream, Size: 5080 bytes --] From 04d677f48004467875a656c4bd79d78e559f1016 Mon Sep 17 00:00:00 2001 From: Rick Lupton <mail@ricklupton.name> Date: Wed, 3 Jan 2024 22:37:38 +0000 Subject: [PATCH 1/2] lisp/org.el (org-insert-heading): allow specifying heading level * lisp/org.el (org-insert-heading): Change optional argument TOP to LEVEL, accepting a number to force a specific heading level. * testing/lisp/test-org.el (test-org/insert-heading): Add tests * etc/ORG-NEWS: Document changes --- etc/ORG-NEWS | 6 ++++++ lisp/org.el | 21 ++++++++++++++------- testing/lisp/test-org.el | 26 ++++++++++++++++++++++++-- 3 files changed, 44 insertions(+), 9 deletions(-) diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS index 1bf7eb5b4..ec01004f8 100644 --- a/etc/ORG-NEWS +++ b/etc/ORG-NEWS @@ -941,6 +941,12 @@ as the function can also act on objects. *** ~org-export-get-parent-element~ is renamed to ~org-element-parent-element~ and moved to =lisp/org-element.el= +*** ~org-insert-heading~ optional argument =TOP= is now =LEVEL= + +A numeric value forces a heading at that level to be inserted. For +backwards compatibility, non-numeric non-nil values insert level 1 +headings as before. + ** Miscellaneous *** =org-crypt.el= now applies initial visibility settings to decrypted entries diff --git a/lisp/org.el b/lisp/org.el index 796545392..87b94a54d 100644 --- a/lisp/org.el +++ b/lisp/org.el @@ -6352,7 +6352,7 @@ headline instead of current one." (`(heading . ,value) value) (_ nil))) -(defun org-insert-heading (&optional arg invisible-ok top) +(defun org-insert-heading (&optional arg invisible-ok level) "Insert a new heading or an item with the same depth at point. If point is at the beginning of a heading, insert a new heading @@ -6381,12 +6381,19 @@ When INVISIBLE-OK is set, stop at invisible headlines when going back. This is important for non-interactive uses of the command. -When optional argument TOP is non-nil, insert a level 1 heading, -unconditionally." +When optional argument LEVEL is a number, insert a heading at +that level. For backwards compatibility, when LEVEL is non-nil +but not a number, insert a level-1 heading." (interactive "P") (let* ((blank? (org--blank-before-heading-p (equal arg '(16)))) - (level (org-current-level)) - (stars (make-string (if (and level (not top)) level 1) ?*))) + (current-level (org-current-level)) + (num-stars (or + ;; Backwards compat: if LEVEL non-nil, level is 1 + (and level (if (wholenump level) level 1)) + current-level + ;; This `1' is for when before first headline + 1)) + (stars (make-string num-stars ?*))) (cond ((or org-insert-heading-respect-content (member arg '((4) (16))) @@ -6395,7 +6402,7 @@ unconditionally." ;; Position point at the location of insertion. Make sure we ;; end up on a visible headline if INVISIBLE-OK is nil. (org-with-limited-levels - (if (not level) (outline-next-heading) ;before first headline + (if (not current-level) (outline-next-heading) ;before first headline (org-back-to-heading invisible-ok) (when (equal arg '(16)) (org-up-heading-safe)) (org-end-of-subtree invisible-ok 'to-heading))) @@ -6408,7 +6415,7 @@ unconditionally." (org-before-first-heading-p))) (insert "\n") (backward-char)) - (when (and (not level) (not (eobp)) (not (bobp))) + (when (and (not current-level) (not (eobp)) (not (bobp))) (when (org-at-heading-p) (insert "\n")) (backward-char)) (unless (and blank? (org-previous-line-empty-p)) diff --git a/testing/lisp/test-org.el b/testing/lisp/test-org.el index 822cbc67a..fc50dc787 100644 --- a/testing/lisp/test-org.el +++ b/testing/lisp/test-org.el @@ -1980,8 +1980,30 @@ CLOCK: [2022-09-17 sam. 11:00]--[2022-09-17 sam. 11:46] => 0:46" (let ((org-insert-heading-respect-content nil)) (org-insert-heading '(16))) (buffer-string)))) - ;; When optional TOP-LEVEL argument is non-nil, always insert - ;; a level 1 heading. + ;; When optional LEVEL argument is a number, insert a heading at + ;; that level. + (should + (equal "* H1\n** H2\n* " + (org-test-with-temp-text "* H1\n** H2<point>" + (org-insert-heading nil nil 1) + (buffer-string)))) + (should + (equal "* H1\n** H2\n** " + (org-test-with-temp-text "* H1\n** H2<point>" + (org-insert-heading nil nil 2) + (buffer-string)))) + (should + (equal "* H1\n** H2\n*** " + (org-test-with-temp-text "* H1\n** H2<point>" + (org-insert-heading nil nil 3) + (buffer-string)))) + (should + (equal "* H1\n- item\n* " + (org-test-with-temp-text "* H1\n- item<point>" + (org-insert-heading nil nil 1) + (buffer-string)))) + ;; When optional LEVEL argument is non-nil, always insert a level 1 + ;; heading. (should (equal "* H1\n** H2\n* " (org-test-with-temp-text "* H1\n** H2<point>" -- 2.39.2 (Apple Git-143) [-- Attachment #3: 0002-org-id.el-Extend-links-with-search-strings-inherit-p.patch --] [-- Type: application/octet-stream, Size: 55325 bytes --] From 245e2decedb5741a509779a1a896f3982cf8f54c Mon Sep 17 00:00:00 2001 From: Rick Lupton <mail@ricklupton.name> Date: Sun, 19 Nov 2023 14:52:05 +0000 Subject: [PATCH 2/2] org-id.el: Extend links with search strings, inherit parent IDs * lisp/ol.el (org-store-link): Refactor org-id links to use standard `org-store-link-functions'. (org-link-search): Create new headings at appropriate level. (org-link-precise-link-target): New function extracting logic to identify a precise link target, e.g. a heading, named object, or text search. (org-link-try-link-store-functions): Extract logic to call external link store functions. Pass them a new `interactive?' argument. * lisp/ol-bbdb.el (org-bbdb-store-link): * lisp/ol-bibtex.el (org-bibtex-store-link): * lisp/ol-docview.el (org-docview-store-link): * lisp/ol-eshell.el (org-eshell-store-link): * lisp/ol-eww.el (org-eww-store-link): * lisp/ol-gnus.el (org-gnus-store-link): * lisp/ol-info.el (org-info-store-link): * lisp/ol-irc.el (org-irc-store-link): * lisp/ol-man.el (org-man-store-link): * lisp/ol-mhe.el (org-mhe-store-link): * lisp/ol-rmail.el (org-rmail-store-link): Accept optional arg. * lisp/org-id.el (org-id-link-consider-parent-id): New option to allow a parent heading with an id to be considered as a link target. (org-id-link-use-context): New option to add context to org-id links. (org-id-get): Add optional `inherit' argument which considers parents' IDs if the current entry does not have one. (org-id-store-link): Consider IDs of parent headings as link targets when current heading has no ID and `org-id-link-consider-parent-id' is set. Add a search string to the link when enabled. (org-id-store-link-maybe): Function set as :store option for custom id link property. Move logic from `org-store-link' here to determine when an org-id link should be stored using `org-id-store-link'. (org-id-open): Recognise search strings after "::" in org-id links. * lisp/org-lint.el: add checker for "::" in ID properties. * testing/lisp/test-ol.el: Add tests for `org-link-precise-link-target' and `org-id-store-link' functions, testing new options. * doc/org-manual.org: Update documentation about links. * etc/ORG-NEWS: Document changes and new options. These feature allows for more precise links when using org-id to link to org headings, without requiring every single headline to have an id. Link: https://list.orgmode.org/118435e8-0b20-46fd-af6a-88de8e19fac6@app.fastmail.com/ --- doc/org-manual.org | 134 +++++++++++-------- etc/ORG-NEWS | 64 +++++++++ lisp/ol-bbdb.el | 2 +- lisp/ol-bibtex.el | 2 +- lisp/ol-docview.el | 2 +- lisp/ol-eshell.el | 2 +- lisp/ol-eww.el | 2 +- lisp/ol-gnus.el | 2 +- lisp/ol-info.el | 2 +- lisp/ol-irc.el | 2 +- lisp/ol-man.el | 2 +- lisp/ol-mhe.el | 2 +- lisp/ol-rmail.el | 2 +- lisp/ol.el | 284 +++++++++++++++++++++++++--------------- lisp/org-id.el | 165 ++++++++++++++++++++--- lisp/org-lint.el | 16 +++ testing/lisp/test-ol.el | 126 ++++++++++++++++++ 17 files changed, 620 insertions(+), 191 deletions(-) diff --git a/doc/org-manual.org b/doc/org-manual.org index 7e5ac0673..1f1013e69 100644 --- a/doc/org-manual.org +++ b/doc/org-manual.org @@ -3297,10 +3297,6 @@ Here is the full set of built-in link types: File links. File name may be remote, absolute, or relative. - Additionally, you can specify a line number, or a text search. - In Org files, you may link to a headline name, a custom ID, or a - code reference instead. - As a special case, "file" prefix may be omitted if the file name is complete, e.g., it starts with =./=, or =/=. @@ -3364,44 +3360,50 @@ Here is the full set of built-in link types: Execute a shell command upon activation. + +For =file:= and =id:= links, you can additionally specify a line +number, or a text search string, separated by =::=. In Org files, you +may link to a headline name, a custom ID, or a code reference instead. + The following table illustrates the link types above, along with their options: -| Link Type | Example | -|------------+----------------------------------------------------------| -| http | =http://staff.science.uva.nl/c.dominik/= | -| https | =https://orgmode.org/= | -| doi | =doi:10.1000/182= | -| file | =file:/home/dominik/images/jupiter.jpg= | -| | =/home/dominik/images/jupiter.jpg= (same as above) | -| | =file:papers/last.pdf= | -| | =./papers/last.pdf= (same as above) | -| | =file:/ssh:me@some.where:papers/last.pdf= (remote) | -| | =/ssh:me@some.where:papers/last.pdf= (same as above) | -| | =file:sometextfile::NNN= (jump to line number) | -| | =file:projects.org= | -| | =file:projects.org::some words= (text search)[fn:12] | -| | =file:projects.org::*task title= (headline search) | -| | =file:projects.org::#custom-id= (headline search) | -| attachment | =attachment:projects.org= | -| | =attachment:projects.org::some words= (text search) | -| docview | =docview:papers/last.pdf::NNN= | -| id | =id:B7423F4D-2E8A-471B-8810-C40F074717E9= | -| news | =news:comp.emacs= | -| mailto | =mailto:adent@galaxy.net= | -| mhe | =mhe:folder= (folder link) | -| | =mhe:folder#id= (message link) | -| rmail | =rmail:folder= (folder link) | -| | =rmail:folder#id= (message link) | -| gnus | =gnus:group= (group link) | -| | =gnus:group#id= (article link) | -| bbdb | =bbdb:R.*Stallman= (record with regexp) | -| irc | =irc:/irc.com/#emacs/bob= | -| help | =help:org-store-link= | -| info | =info:org#External links= | -| shell | =shell:ls *.org= | -| elisp | =elisp:(find-file "Elisp.org")= (Elisp form to evaluate) | -| | =elisp:org-agenda= (interactive Elisp command) | +| Link Type | Example | +|------------+--------------------------------------------------------------------| +| http | =http://staff.science.uva.nl/c.dominik/= | +| https | =https://orgmode.org/= | +| doi | =doi:10.1000/182= | +| file | =file:/home/dominik/images/jupiter.jpg= | +| | =/home/dominik/images/jupiter.jpg= (same as above) | +| | =file:papers/last.pdf= | +| | =./papers/last.pdf= (same as above) | +| | =file:/ssh:me@some.where:papers/last.pdf= (remote) | +| | =/ssh:me@some.where:papers/last.pdf= (same as above) | +| | =file:sometextfile::NNN= (jump to line number) | +| | =file:projects.org= | +| | =file:projects.org::some words= (text search)[fn:12] | +| | =file:projects.org::*task title= (headline search) | +| | =file:projects.org::#custom-id= (headline search) | +| attachment | =attachment:projects.org= | +| | =attachment:projects.org::some words= (text search) | +| docview | =docview:papers/last.pdf::NNN= | +| id | =id:B7423F4D-2E8A-471B-8810-C40F074717E9= | +| | =id:B7423F4D-2E8A-471B-8810-C40F074717E9::*task= (headline search) | +| news | =news:comp.emacs= | +| mailto | =mailto:adent@galaxy.net= | +| mhe | =mhe:folder= (folder link) | +| | =mhe:folder#id= (message link) | +| rmail | =rmail:folder= (folder link) | +| | =rmail:folder#id= (message link) | +| gnus | =gnus:group= (group link) | +| | =gnus:group#id= (article link) | +| bbdb | =bbdb:R.*Stallman= (record with regexp) | +| irc | =irc:/irc.com/#emacs/bob= | +| help | =help:org-store-link= | +| info | =info:org#External links= | +| shell | =shell:ls *.org= | +| elisp | =elisp:(find-file "Elisp.org")= (Elisp form to evaluate) | +| | =elisp:org-agenda= (interactive Elisp command) | #+cindex: VM links #+cindex: Wanderlust links @@ -3462,8 +3464,9 @@ current buffer: - /Org mode buffers/ :: For Org files, if there is a =<<target>>= at point, the link points - to the target. Otherwise it points to the current headline, which - is also the description. + to the target. If there is a named block (using =#+name:=) at + point, the link points to that name. Otherwise it points to the + current headline, which is also the description. #+vindex: org-id-link-to-org-use-id #+cindex: @samp{CUSTOM_ID}, property @@ -3481,6 +3484,29 @@ current buffer: timestamp, depending on ~org-id-method~. Later, when inserting the link, you need to decide which one to use. + #+vindex: org-id-link-consider-parent-id + When ~org-id-link-consider-parent-id~ is ~t~ (and + ~org-context-in-file-links~ and ~org-id-link-use-context~ are both + enabled), parent =ID= properties are considered. This allows + linking to specific targets, named blocks, or headlines (which may + not have a globally unique =ID= themselves) within the context of a + parent headline or file which does. + + For example, given this org file with those variables set: + + #+begin_src org + ,* Parent + :PROPERTIES: + :ID: abc + :END: + ,** Child 1 + ,** Child 2 + #+end_src + + Storing a link with point at "Child 1" will produce a link + =<id:abc::*Child 1>=, which precisely links to the "Child 1" + headline even though it does not have its own ID. + - /Email/News clients: VM, Rmail, Wanderlust, MH-E, Gnus/ :: #+vindex: org-link-email-description-format @@ -3754,13 +3780,15 @@ the link completion function like this: (org-link-set-parameter "type" :complete #'some-completion-function) #+end_src -** Search Options in File Links +** Search Options in File and ID Links :PROPERTIES: :DESCRIPTION: Linking to a specific location. :ALT_TITLE: Search Options :END: #+cindex: search option in file links +#+cindex: search option in id links #+cindex: file links, searching +#+cindex: id links, searching #+cindex: attachment links, searching File links can contain additional information to make Emacs jump to a @@ -3772,8 +3800,8 @@ example, when the command ~org-store-link~ creates a link (see line as a search string that can be used to find this line back later when following the link with {{{kbd(C-c C-o)}}}. -Note that all search options apply for Attachment links in the same -way that they apply for File links. +Note that all search options apply for Attachment and ID links in the +same way that they apply for File links. Here is the syntax of the different ways to attach a search to a file link, together with explanations for each: @@ -21355,7 +21383,7 @@ The following =ol-man.el= file implements it PATH should be a topic that can be thrown at the man command." (funcall org-man-command path)) -(defun org-man-store-link () +(defun org-man-store-link (&optional _interactive?) "Store a link to a man page." (when (memq major-mode '(Man-mode woman-mode)) ;; This is a man page, we do make this link. @@ -21415,13 +21443,15 @@ A review of =ol-man.el=: For example, ~org-man-store-link~ is responsible for storing a link when ~org-store-link~ (see [[*Handling Links]]) is called from a buffer - displaying a man page. It first checks if the major mode is - appropriate. If check fails, the function returns ~nil~, which - means it isn't responsible for creating a link to the current - buffer. Otherwise the function makes a link string by combining - the =man:= prefix with the man topic. It also provides a default - description. The function ~org-insert-link~ can insert it back - into an Org buffer later on. + displaying a man page. It is passed an argument ~interactive?~ + which this function does not use, but other store functions use to + behave differently when a link is stored interactively by the user. + It first checks if the major mode is appropriate. If check fails, + the function returns ~nil~, which means it isn't responsible for + creating a link to the current buffer. Otherwise the function + makes a link string by combining the =man:= prefix with the man + topic. It also provides a default description. The function + ~org-insert-link~ can insert it back into an Org buffer later on. ** Adding Export Backends :PROPERTIES: diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS index ec01004f8..1115e3bb4 100644 --- a/etc/ORG-NEWS +++ b/etc/ORG-NEWS @@ -345,6 +345,14 @@ timestamp object. Possible values: ~timerange~, ~daterange~, ~nil~. ~org-element-timestamp-interpreter~ takes into account this property and returns an appropriate timestamp string. +**** =org-link= store functions are passed an ~interactive?~ argument + +The ~:store:~ functions set for link types using +~org-link-set-parameters~ are now passed an ~interactive?~ argument, +indicating whether ~org-store-link~ was called interactively. + +Existing store functions will continue to work. + *** ~org-priority=show~ command no longer adjusts for scheduled/deadline In agenda views, ~org-priority=show~ command previously displayed the @@ -423,6 +431,27 @@ The change is breaking when ~org-use-property-inheritance~ is set to ~t~. *** ~org-babel-lilypond-compile-lilyfile~ ignores optional second argument The =TEST= parameter is better served by Emacs debugging tools. + +*** ~org-id-store-link~ now adds search strings for precise link targets + +This new behaviour can be disabled generally by setting +~org-id-link-use-context~ to ~nil~, or the setting can be toggled for +a single call to ~org-store-link~ with a universal argument. + +When using this feature, IDs should not include =::=, which is used in +links to indicate the start of the search string. For backwards +compability, existing IDs including =::= will still be matched (but +cannot be used together with precise link targets). An org-lint +checker has been added to warn about this. + +*** ~org-store-link~ behaviour storing additional =CUSTOM_ID= links has changed + +As well as an =id:= link, ~org-store-link~ stores an additional "human +readable" link using a node's =CUSTOM_ID= property, if available. +This behaviour has been expanded to store an additional =CUSTOM_ID= +link when storing any type of external link type in an Org file, not +just =id:= links. + ** New and changed options *** The default value of ~org-attach-store-link-p~ is now ~attached~ @@ -659,6 +688,35 @@ manner with ~run-python~. This allows to run functions after ~org-indent~ intializes a buffer to enrich its properties. +*** New option ~org-id-link-consider-parent-id~ to allow =id:= links to parent headlines + +For =id:= links, when this option is enabled, ~org-store-link~ will +look for ids from parent/ancestor headlines, if the current headline +does not have an id. + +Combined with the new ability for =id:= links to use search strings +for precise link targets (when =org-id-link-use-context= is =t=, which +is the default), this allows linking to specific headlines without +requiring every headline to have an id property, as long as the +headline is unique within a subtree that does have an id property. + +For example, given this org file: + +#+begin_src org +,* Parent +:PROPERTIES: +:ID: abc +:END: +,** Child 1 +,** Child 2 +#+end_src + +Storing a link with point at "Child 1" will produce a link +=<id:abc::*Child 1>=, which precisely links to the "Child 1" headline +even though it does not have its own ID. By giving files top-level id +properties, links to headlines in the file can also be made more +robust by using the file id instead of the file path. + ** New features *** =ob-plantuml.el=: Support tikz file format output @@ -947,6 +1005,12 @@ A numeric value forces a heading at that level to be inserted. For backwards compatibility, non-numeric non-nil values insert level 1 headings as before. +*** New optional argument for ~org-id-get~ + +New optional argument =INHERIT= means inherited ID properties from +parent entries are considered when getting an entry's ID (see +~org-id-link-consider-parent-id~ option). + ** Miscellaneous *** =org-crypt.el= now applies initial visibility settings to decrypted entries diff --git a/lisp/ol-bbdb.el b/lisp/ol-bbdb.el index be3924fc9..6ea060f70 100644 --- a/lisp/ol-bbdb.el +++ b/lisp/ol-bbdb.el @@ -226,7 +226,7 @@ date year)." ;;; Implementation -(defun org-bbdb-store-link () +(defun org-bbdb-store-link (&optional _interactive?) "Store a link to a BBDB database entry." (when (eq major-mode 'bbdb-mode) ;; This is BBDB, we make this link! diff --git a/lisp/ol-bibtex.el b/lisp/ol-bibtex.el index c5a950e2d..38468f32f 100644 --- a/lisp/ol-bibtex.el +++ b/lisp/ol-bibtex.el @@ -507,7 +507,7 @@ ARG, when non-nil, is a universal prefix argument. See `org-open-file' for details." (org-link-open-as-file path arg)) -(defun org-bibtex-store-link () +(defun org-bibtex-store-link (&optional _interactive?) "Store a link to a BibTeX entry." (when (eq major-mode 'bibtex-mode) (let* ((search (org-create-file-search-in-bibtex)) diff --git a/lisp/ol-docview.el b/lisp/ol-docview.el index b31f1ce5e..0907ddee1 100644 --- a/lisp/ol-docview.el +++ b/lisp/ol-docview.el @@ -83,7 +83,7 @@ (error "No such file: %s" path)) (when page (doc-view-goto-page page)))) -(defun org-docview-store-link () +(defun org-docview-store-link (&optional _interactive?) "Store a link to a docview buffer." (when (eq major-mode 'doc-view-mode) ;; This buffer is in doc-view-mode diff --git a/lisp/ol-eshell.el b/lisp/ol-eshell.el index 2c7ec6bef..595dd0ee0 100644 --- a/lisp/ol-eshell.el +++ b/lisp/ol-eshell.el @@ -60,7 +60,7 @@ followed by a colon." (insert command) (eshell-send-input))) -(defun org-eshell-store-link () +(defun org-eshell-store-link (&optional _interactive?) "Store eshell link. When opened, the link switches back to the current eshell buffer and the current working directory." diff --git a/lisp/ol-eww.el b/lisp/ol-eww.el index 40b820d2b..c13dbf339 100644 --- a/lisp/ol-eww.el +++ b/lisp/ol-eww.el @@ -62,7 +62,7 @@ "Open URL with Eww in the current buffer." (eww url)) -(defun org-eww-store-link () +(defun org-eww-store-link (&optional _interactive?) "Store a link to the url of an EWW buffer." (when (eq major-mode 'eww-mode) (org-link-store-props diff --git a/lisp/ol-gnus.el b/lisp/ol-gnus.el index e105fdb2c..b9ee8683f 100644 --- a/lisp/ol-gnus.el +++ b/lisp/ol-gnus.el @@ -123,7 +123,7 @@ If `org-store-link' was called with a prefix arg the meaning of (url-encode-url message-id)) (concat "gnus:" group "#" message-id))) -(defun org-gnus-store-link () +(defun org-gnus-store-link (&optional _interactive?) "Store a link to a Gnus folder or message." (pcase major-mode (`gnus-group-mode diff --git a/lisp/ol-info.el b/lisp/ol-info.el index 0edf9a13f..6062cab34 100644 --- a/lisp/ol-info.el +++ b/lisp/ol-info.el @@ -50,7 +50,7 @@ :insert-description #'org-info-description-as-command) ;; Implementation -(defun org-info-store-link () +(defun org-info-store-link (&optional _interactive?) "Store a link to an Info file and node." (when (eq major-mode 'Info-mode) (let ((link (concat "info:" diff --git a/lisp/ol-irc.el b/lisp/ol-irc.el index 78c4884b0..b263e52db 100644 --- a/lisp/ol-irc.el +++ b/lisp/ol-irc.el @@ -103,7 +103,7 @@ attributes that are found." parts)) ;;;###autoload -(defun org-irc-store-link () +(defun org-irc-store-link (&optional _interactive?) "Dispatch to the appropriate function to store a link to an IRC session." (cond ((eq major-mode 'erc-mode) diff --git a/lisp/ol-man.el b/lisp/ol-man.el index e3f13815e..42aacea81 100644 --- a/lisp/ol-man.el +++ b/lisp/ol-man.el @@ -82,7 +82,7 @@ matched strings in man buffer." (set-window-point window point) (set-window-start window point))))))) -(defun org-man-store-link () +(defun org-man-store-link (&optional _interactive?) "Store a link to a README file." (when (memq major-mode '(Man-mode woman-mode)) ;; This is a man page, we do make this link diff --git a/lisp/ol-mhe.el b/lisp/ol-mhe.el index 106cfedc9..a32481324 100644 --- a/lisp/ol-mhe.el +++ b/lisp/ol-mhe.el @@ -80,7 +80,7 @@ supported by MH-E." (org-link-set-parameters "mhe" :follow #'org-mhe-open :store #'org-mhe-store-link) ;; Implementation -(defun org-mhe-store-link () +(defun org-mhe-store-link (&optional _interactive?) "Store a link to an MH-E folder or message." (when (or (eq major-mode 'mh-folder-mode) (eq major-mode 'mh-show-mode)) diff --git a/lisp/ol-rmail.el b/lisp/ol-rmail.el index f6031ab52..f1f753b6f 100644 --- a/lisp/ol-rmail.el +++ b/lisp/ol-rmail.el @@ -51,7 +51,7 @@ :store #'org-rmail-store-link) ;; Implementation -(defun org-rmail-store-link () +(defun org-rmail-store-link (&optional _interactive?) "Store a link to an Rmail folder or message." (when (or (eq major-mode 'rmail-mode) (eq major-mode 'rmail-summary-mode)) diff --git a/lisp/ol.el b/lisp/ol.el index cf59c8556..59b87eba7 100644 --- a/lisp/ol.el +++ b/lisp/ol.el @@ -63,7 +63,6 @@ (declare-function org-find-property "org" (property &optional value)) (declare-function org-get-heading "org" (&optional no-tags no-todo no-priority no-comment)) (declare-function org-id-find-id-file "org-id" (id)) -(declare-function org-id-store-link "org-id" ()) (declare-function org-insert-heading "org" (&optional arg invisible-ok top)) (declare-function org-load-modules-maybe "org" (&optional force)) (declare-function org-mark-ring-push "org" (&optional pos buffer)) @@ -815,6 +814,60 @@ spec." (org-with-point-at (car region) (not (org-in-regexp org-link-any-re)))) +(defun org-link--try-link-store-functions (interactive?) + "Try storing external links, prompting if more than one is possible. + +Each function returned by `org-store-link-functions' is called in +turn. If multiple functions return non-nil, prompt for which +link should be stored. + +Argument INTERACTIVE? indicates whether `org-store-link' was +called interactively and is passed to the link store functions. + +Return t when a link has been stored in `org-link-store-props'." + (let ((results-alist nil)) + (dolist (f (org-store-link-functions)) + (when (condition-case nil + (funcall f interactive?) + ;; FIXME: The store function used (< Org 9.7) to accept + ;; no arguments; provide backward compatibility support + ;; for them. + (wrong-number-of-arguments + (funcall f))) + ;; FIXME: return value is not link's plist, so we store the + ;; new value before it is modified. It would be cleaner to + ;; ask store link functions to return the plist instead. + (push (cons f (copy-sequence org-store-link-plist)) + results-alist))) + (pcase results-alist + (`nil nil) + (`((,_ . ,_)) t) ;single choice: nothing to do + (`((,name . ,_) . ,_) + ;; Reinstate link plist associated to the chosen + ;; function. + (apply #'org-link-store-props + (cdr (assoc-string + (completing-read + (format "Store link with (default %s): " name) + (mapcar #'car results-alist) + nil t nil nil (symbol-name name)) + results-alist))) + t)))) + +(defun org-link--add-to-stored-links (link desc) + "Add LINK to `org-stored-links' with description DESC." + (cond + ((not (member (list link desc) org-stored-links)) + (push (list link desc) org-stored-links) + (message "Stored: %s" (or desc link))) + ((equal (list link desc) (car org-stored-links)) + (message "This link has already been stored")) + (t + (setq org-stored-links + (delete (list link desc) org-stored-links)) + (push (list link desc) org-stored-links) + (message "Link moved to front: %s" (or desc link))))) + \f ;;; Public API @@ -1280,7 +1333,11 @@ respects buffer narrowing." (yes-or-no-p "No match - create this as a new heading? ")) (goto-char (point-max)) (unless (bolp) (newline)) - (org-insert-heading nil t t) + ;; Find appropriate level for new heading + (let ((level (save-excursion + (goto-char (point-min)) + (+ 1 (or (org-current-level) 0))))) + (org-insert-heading nil t level)) (insert s "\n") (forward-line -1)) ;; Only headlines are looked after. No need to process @@ -1332,6 +1389,66 @@ priority cookie or tag." (org-link--normalize-string (or string (org-get-heading t t t t))))) +(defun org-link-precise-link-target (&optional relative-to) + "Determine search string and description for storing a link. + +If a search string (see 'org-link-search') is found, return cons +cell (SEARCH-STRING . DESC). Otherwise, return nil. + +If there is an active region, the contents (or a part of it, see +`org-link-context-for-files') is used as the search string. + +In Org buffers, if point is at a named element (such as a source +block), the name is used for the search string. If at a heading, +its CUSTOM_ID is used to form a search string of the form +\"#id\", if present, otherwise the current heading text is used +in the form \"*Heading\". + +If none of those finds a suitable search string, the current line +is used as the search string. + +The description DESC is nil (meaning the user will be prompted +for a description when inserting the link) for search strings +based on a region or the current line. For other cases, DESC is +a cleaned-up version of the name or heading at point. + +Optional argument RELATIVE-TO specifies the buffer position where +the search will start from. If the search target that would be +returned is a heading before or at this location, return nil to +avoid unnecessary search strings (for example, when using search +strings to find targets within org-id links)." + (let ((result + (cond + ((derived-mode-p 'org-mode) + (let* ((element (org-element-at-point)) + (name (org-element-property :name element)) + (heading (org-element-lineage element 'headline t)) + (custom-id (org-entry-get nil "CUSTOM_ID"))) + (cond + ((let ((region (org-link--context-from-region))) + (and region (cons (org-link--normalize-string region t) nil)))) + (name + (cons name name)) + ((org-before-first-heading-p) + (cons (org-link--normalize-string (org-current-line-string) t) nil)) + ((and heading + (> (org-element-begin heading) (or relative-to 0))) + (cons (if custom-id (concat "#" custom-id) + (org-link-heading-search-string)) + (org-link--normalize-string + (org-get-heading t t t t))))))) + + ;; Not in an org-mode buffer + (t + (cons (org-link--normalize-string + (or (org-link--context-from-region) (org-current-line-string)) + t) + nil))))) + + ;; Only use search option if there is some text. + (when (org-string-nw-p (car result)) + result))) + (defun org-link-open-as-file (path in-emacs) "Pretend PATH is a file name and open it. @@ -1404,7 +1521,7 @@ PATH is a symbol name, as a string." ((and (pred boundp) variable) (describe-variable variable)) (name (user-error "Unknown function or variable: %s" name)))) -(defun org-link--store-help () +(defun org-link--store-help (&optional _interactive?) "Store \"help\" type link." (when (eq major-mode 'help-mode) (let ((symbol @@ -1539,7 +1656,11 @@ prefix ARG forces storing a link for each line in the active region. Assume the function is called interactively if INTERACTIVE? is -non-nil." +non-nil. + +In Org buffers, an additional \"human-readable\" simple file link +is stored as an alternative to persistent org-id or other links, +if the current heading has a CUSTOM_ID property." (interactive "P\np") (org-load-modules-maybe) (if (and (equal arg '(64)) (org-region-active-p)) @@ -1554,36 +1675,19 @@ non-nil." (move-beginning-of-line 2) (set-mark (point))))) (setq org-store-link-plist nil) - (let (link cpltxt desc search custom-id agenda-link) ;; description + ;; Negate `org-context-in-file-links' when given a single universal arg. + (let ((org-link-context-for-files (org-xor org-link-context-for-files + (equal arg '(4)))) + link cpltxt desc search agenda-link) ;; description (cond ;; Store a link using an external link type, if any function is - ;; available. If more than one can generate a link from current - ;; location, ask which one to use. + ;; available, unless external link types are skipped for this + ;; call using two universal args. If more than one function + ;; can generate a link from current location, ask the user + ;; which one to use. ((and (not (equal arg '(16))) - (let ((results-alist nil)) - (dolist (f (org-store-link-functions)) - (when (funcall f) - ;; XXX: return value is not link's plist, so we - ;; store the new value before it is modified. It - ;; would be cleaner to ask store link functions to - ;; return the plist instead. - (push (cons f (copy-sequence org-store-link-plist)) - results-alist))) - (pcase results-alist - (`nil nil) - (`((,_ . ,_)) t) ;single choice: nothing to do - (`((,name . ,_) . ,_) - ;; Reinstate link plist associated to the chosen - ;; function. - (apply #'org-link-store-props - (cdr (assoc-string - (completing-read - (format "Store link with (default %s): " name) - (mapcar #'car results-alist) - nil t nil nil (symbol-name name)) - results-alist))) - t)))) - (setq link (plist-get org-store-link-plist :link)) + (org-link--try-link-store-functions interactive?)) + (setq link (plist-get org-store-link-plist :link)) ;; If store function actually set `:description' property, use ;; it, even if it is nil. Otherwise, fallback to nil (ask user). (setq desc (plist-get org-store-link-plist :description))) @@ -1634,6 +1738,7 @@ non-nil." (org-with-point-at m (setq agenda-link (org-store-link nil interactive?)))))) + ;; Calendar mode ((eq major-mode 'calendar-mode) (let ((cd (calendar-cursor-to-date))) (setq link @@ -1642,6 +1747,7 @@ non-nil." (org-encode-time 0 0 0 (nth 1 cd) (nth 0 cd) (nth 2 cd)))) (org-link-store-props :type "calendar" :date cd))) + ;; Image mode ((eq major-mode 'image-mode) (setq cpltxt (concat "file:" (abbreviate-file-name buffer-file-name)) @@ -1659,15 +1765,22 @@ non-nil." (setq cpltxt (concat "file:" file) link cpltxt))) + ;; Try `org-create-file-search-functions`. If any are + ;; successful, create a file link to the current buffer with + ;; the provided search string. (sets `link` and `cpltxt` to + ;; the same thing; it looks like the intention originally was + ;; that cpltxt was a description, which might have been set by + ;; the search-function (removed in switch to lexical binding)). ((setq search (run-hook-with-args-until-success 'org-create-file-search-functions)) (setq link (concat "file:" (abbreviate-file-name buffer-file-name) "::" search)) (setq cpltxt (or link))) ;; description + ;; Main logic for storing built-in link types in org-mode + ;; buffers ((and (buffer-file-name (buffer-base-buffer)) (derived-mode-p 'org-mode)) (org-with-limited-levels - (setq custom-id (org-entry-get nil "CUSTOM_ID")) (cond ;; Store a link using the target at point ((org-in-regexp "[^<]<<\\([^<>]+\\)>>[^>]" 1) @@ -1681,74 +1794,34 @@ non-nil." ;; links. Maybe the case of identical target and ;; description should be handled by `org-insert-link'. cpltxt nil - desc nil - ;; Do not append #CUSTOM_ID link below. - custom-id nil)) - ((and (featurep 'org-id) - (or (eq org-id-link-to-org-use-id t) - (and interactive? - (or (eq org-id-link-to-org-use-id 'create-if-interactive) - (and (eq org-id-link-to-org-use-id - 'create-if-interactive-and-no-custom-id) - (not custom-id)))) - (and org-id-link-to-org-use-id (org-entry-get nil "ID")))) - ;; Store a link using the ID at point - (setq link (condition-case nil - (prog1 (org-id-store-link) - (setq desc (plist-get org-store-link-plist :description))) - (error - ;; Probably before first headline, link only to file - (concat "file:" - (abbreviate-file-name - (buffer-file-name (buffer-base-buffer)))))))) - (t + desc nil)) + (t ;; Just link to current headline. (setq cpltxt (concat "file:" (abbreviate-file-name (buffer-file-name (buffer-base-buffer))))) - ;; Add a context search string. - (when (org-xor org-link-context-for-files (equal arg '(4))) - (let* ((element (org-element-at-point)) - (name (org-element-property :name element)) - (context - (cond - ((let ((region (org-link--context-from-region))) - (and region (org-link--normalize-string region t)))) - (name) - ((org-before-first-heading-p) - (org-link--normalize-string (org-current-line-string) t)) - (t (org-link-heading-search-string))))) - (when (org-string-nw-p context) - (setq cpltxt (format "%s::%s" cpltxt context)) - (setq desc - (or name - ;; Although description is not a search - ;; string, use `org-link--normalize-string' - ;; to prettify it (contiguous white spaces) - ;; and remove volatile contents (statistics - ;; cookies). - (and (not (org-before-first-heading-p)) - (org-link--normalize-string - (org-get-heading t t t t))) - "NONE"))))) - (setq link cpltxt))))) - + (when org-link-context-for-files + (pcase (org-link-precise-link-target) + (`nil nil) + (`(,search-string . ,search-desc) + (setq cpltxt (format "%s::%s" cpltxt search-string)) + (setq desc search-desc)))) + (setq link cpltxt))))) + + ;; Buffer linked to file, but not an org-mode buffer. ((buffer-file-name (buffer-base-buffer)) ;; Just link to this file here. (setq cpltxt (concat "file:" (abbreviate-file-name (buffer-file-name (buffer-base-buffer))))) ;; Add a context search string. - (when (org-xor org-link-context-for-files (equal arg '(4))) - (let ((context (org-link--normalize-string - (or (org-link--context-from-region) - (org-current-line-string)) - t))) - ;; Only use search option if there is some text. - (when (org-string-nw-p context) - (setq cpltxt (format "%s::%s" cpltxt context)) - (setq desc "NONE")))) - (setq link cpltxt)) + (when org-link-context-for-files + (pcase (org-link-precise-link-target) + (`nil nil) + (`(,search-string . ,search-desc) + (setq cpltxt (format "%s::%s" cpltxt search-string)) + (setq desc search-desc)))) + (setq link cpltxt)) (interactive? (user-error "No method for storing a link from this buffer")) @@ -1764,24 +1837,19 @@ non-nil." ;; Store and return the link (if (not (and interactive? link)) (or agenda-link (and link (org-link-make-string link desc))) - (dotimes (_ (if custom-id 2 1)) ; Store 2 links when CUSTOM-ID is non-nil. - (cond - ((not (member (list link desc) org-stored-links)) - (push (list link desc) org-stored-links) - (message "Stored: %s" (or desc link))) - ((equal (list link desc) (car org-stored-links)) - (message "This link has already been stored")) - (t - (setq org-stored-links - (delete (list link desc) org-stored-links)) - (push (list link desc) org-stored-links) - (message "Link moved to front: %s" (or desc link)))) - (when custom-id - (setq link (concat "file:" - (abbreviate-file-name - (buffer-file-name (buffer-base-buffer))) - "::#" custom-id)))) - (car org-stored-links))))) + (org-link--add-to-stored-links link desc) + ;; In org buffers, store an additional "human-readable" link + ;; using custom id, if available. + (when (and (buffer-file-name (buffer-base-buffer)) + (derived-mode-p 'org-mode) + (org-entry-get nil "CUSTOM_ID")) + (setq link (concat "file:" + (abbreviate-file-name + (buffer-file-name (buffer-base-buffer))) + "::#" (org-entry-get nil "CUSTOM_ID"))) + (unless (equal (list link desc) (car org-stored-links)) + (org-link--add-to-stored-links link desc))) + (car org-stored-links))))) ;;;###autoload (defun org-insert-link (&optional complete-file link-location description) diff --git a/lisp/org-id.el b/lisp/org-id.el index 8647a57cc..d70bec3c1 100644 --- a/lisp/org-id.el +++ b/lisp/org-id.el @@ -129,6 +129,46 @@ nil Never use an ID to make a link, instead link using a text search for (const :tag "Only use existing" use-existing) (const :tag "Do not use ID to create link" nil))) +(defcustom org-id-link-consider-parent-id nil + "Non-nil means storing a link to an Org entry considers inherited IDs. + +When this option is non-nil and `org-id-link-use-context' is +enabled, ID properties inherited from parent entries will be +considered when storing an ID link. If no ID is found in this +way, a new one may be created as normal (see +`org-id-link-to-org-use-id'). + +For example, given this org file: + +* Parent +:PROPERTIES: +:ID: abc +:END: +** Child 1 +** Child 2 + +With `org-id-link-consider-parent-id' and +`org-id-link-use-context' both enabled, storing a link with point +at \"Child 1\" will produce a link \"<id:abc::*Child 1>\". This +allows linking to uniquely-named sub-entries within a parent +entry with an ID, without requiring every sub-entry to have its +own ID." + :group 'org-link-store + :group 'org-id + :package-version '(Org . "9.7") + :type 'boolean) + +(defcustom org-id-link-use-context t + "Non-nil means enables search string context in org-id links. + +Search strings are added by `org-id-store-link' when both the +general option `org-link-context-for-files' and the org-id option +`org-id-link-use-context' are non-nil." + :group 'org-link-store + :group 'org-id + :package-version '(Org . "9.7") + :type 'boolean) + (defcustom org-id-uuid-program "uuidgen" "The uuidgen program." :group 'org-id @@ -280,15 +320,21 @@ This is useful when working with contents in a temporary buffer that will be copied back to the original.") ;;;###autoload -(defun org-id-get (&optional epom create prefix) - "Get the ID property of the entry at EPOM. -EPOM is an element, marker, or buffer position. -If EPOM is nil, refer to the entry at point. -If the entry does not have an ID, the function returns nil. -However, when CREATE is non-nil, create an ID if none is present already. -PREFIX will be passed through to `org-id-new'. -In any case, the ID of the entry is returned." - (let ((id (org-entry-get epom "ID"))) +(defun org-id-get (&optional epom create prefix inherit) + "Get the ID of the entry at EPOM. + +EPOM is an element, marker, or buffer position. If EPOM is nil, +refer to the entry at point. + +If INHERIT is non-nil, ID properties inherited from parent +entries are considered. Otherwise, only ID properties on the +entry itself are considered. + +When CREATE is nil, return the ID of the entry if found, +otherwise nil. When CREATE is non-nil, create an ID if none has +been found, and return the new ID. PREFIX will be passed through +to `org-id-new'." + (let ((id (org-entry-get epom "ID" (and inherit t)))) (cond ((and id (stringp id) (string-match "\\S-" id)) id) @@ -703,18 +749,45 @@ optional argument MARKERP, return the position as a new marker." ;; Calling the following function is hard-coded into `org-store-link', ;; so we do have to add it to `org-store-link-functions'. +(defun org-id--get-id-to-store-link (&optional create) + "Get or create the relevant ID for storing a link. + +Optional argument CREATE is passed to `org-id-get'. + +Inherited IDs are only considered when +`org-id-link-consider-parent-id', `org-id-link-use-context' and +`org-link-context-for-files' are all enabled, since inherited IDs +are confusing without the additional search string context." + (let* ((inherit-id (and org-id-link-consider-parent-id + org-id-link-use-context + org-link-context-for-files))) + (org-id-get nil create nil inherit-id))) + ;;;###autoload (defun org-id-store-link () "Store a link to the current entry, using its ID. -If before first heading store first title-keyword as description -or filename if no title." +The link description is based on the heading, or if before the +first heading, the title keyword if available, or else the +filename. + +When `org-link-context-for-files' and `org-id-link-use-context' +are non-nil, add a search string to the link. The link +description is then based on the search string target. + +When in addition `org-id-link-consider-parent-id' is non-nil, the +ID can be inherited from a parent entry, with the search string +used to still link to the current location." (interactive) - (when (and (buffer-file-name (buffer-base-buffer)) (derived-mode-p 'org-mode)) - (let* ((link (concat "id:" (org-id-get-create))) + (when (and (buffer-file-name (buffer-base-buffer)) + (derived-mode-p 'org-mode)) + (let* ((link (concat "id:" (org-id--get-id-to-store-link 'create))) + (id-location (or (and org-entry-property-inherited-from + (marker-position org-entry-property-inherited-from)) + (save-excursion (org-back-to-heading-or-point-min) (point)))) (case-fold-search nil) (desc (save-excursion - (org-back-to-heading-or-point-min t) + (goto-char id-location) (cond ((org-before-first-heading-p) (let ((keywords (org-collect-keywords '("TITLE")))) (if keywords @@ -726,14 +799,59 @@ or filename if no title." (match-string 4) (match-string 0))) (t link))))) + (when (and org-link-context-for-files org-id-link-use-context) + (pcase (org-link-precise-link-target id-location) + (`nil nil) + (`(,search-string . ,search-desc) + (setq link (concat link "::" search-string)) + (setq desc search-desc)))) (org-link-store-props :link link :description desc :type "id") link))) -(defun org-id-open (id _) - "Go to the entry with id ID." - (org-mark-ring-push) - (let ((m (org-id-find id 'marker)) - cmd) +;;;###autoload +(defun org-id-store-link-maybe (&optional interactive?) + "Store a link to the current entry using its ID if enabled. + +The value of `org-id-link-to-org-use-id' determines whether an ID +link should be stored, using `org-id-store-link'. + +Assume the function is called interactively if INTERACTIVE? is +non-nil." + (when (and (buffer-file-name (buffer-base-buffer)) + (derived-mode-p 'org-mode) + (or (eq org-id-link-to-org-use-id t) + (and interactive? + (or (eq org-id-link-to-org-use-id 'create-if-interactive) + (and (eq org-id-link-to-org-use-id + 'create-if-interactive-and-no-custom-id) + (not (org-entry-get nil "CUSTOM_ID"))))) + ;; 'use-existing + (and org-id-link-to-org-use-id + (org-id--get-id-to-store-link)))) + (org-id-store-link))) + +(defun org-id-open (link _) + "Go to the entry indicated by id link LINK. + +The link can include a search string after \"::\", which is +passed to `org-link-search'. + +For backwards compatibility with IDs that contain \"::\", if no +match is found for the ID, the full link string including \"::\" +will be tried as an ID." + (let* ((option (and (string-match "::\\(.*\\)\\'" link) + (match-string 1 link))) + (id (if (not option) link + (substring link 0 (match-beginning 0)))) + m cmd) + (org-mark-ring-push) + (setq m (org-id-find id 'marker)) + (when (and (not m) option) + ;; Backwards compatibility: if id is not found, try treating + ;; whole link as an id. + (setq m (org-id-find link 'marker)) + (when m + (setq option nil))) (unless m (error "Cannot find entry with ID \"%s\"" id)) ;; Use a buffer-switching command in analogy to finding files @@ -750,9 +868,16 @@ or filename if no title." (funcall cmd (marker-buffer m))) (goto-char m) (move-marker m nil) + (when option + (save-restriction + (unless (org-before-first-heading-p) + (org-narrow-to-subtree)) + (org-link-search option))) (org-fold-show-context))) -(org-link-set-parameters "id" :follow #'org-id-open) +(org-link-set-parameters "id" + :follow #'org-id-open + :store #'org-id-store-link-maybe) (provide 'org-id) diff --git a/lisp/org-lint.el b/lisp/org-lint.el index 4d2a55d15..b23afcca3 100644 --- a/lisp/org-lint.el +++ b/lisp/org-lint.el @@ -65,6 +65,7 @@ ;; - special properties in properties drawers, ;; - obsolete syntax for properties drawers, ;; - invalid duration in EFFORT property, +;; - invalid ID property with a double colon, ;; - missing definition for footnote references, ;; - missing reference for footnote definitions, ;; - non-footnote definitions in footnote section, @@ -686,6 +687,16 @@ Use :header-args: instead" (list (org-element-begin p) (format "Invalid effort duration format: %S" value)))))))) +(defun org-lint-invalid-id-property (ast) + (org-element-map ast 'node-property + (lambda (p) + (when (equal "ID" (org-element-property :key p)) + (let ((value (org-element-property :value p))) + (and (org-string-nw-p value) + (string-match-p "::" value) + (list (org-element-begin p) + (format "IDs should not include \"::\": %S" value)))))))) + (defun org-lint-link-to-local-file (ast) (org-element-map ast 'link (lambda (l) @@ -1684,6 +1695,11 @@ AST is the buffer parse tree." #'org-lint-invalid-effort-property :categories '(properties)) +(org-lint-add-checker 'invalid-id-property + "Report search string delimiter \"::\" in ID property" + #'org-lint-invalid-id-property + :categories '(properties)) + (org-lint-add-checker 'undefined-footnote-reference "Report missing definition for footnote references" #'org-lint-undefined-footnote-reference diff --git a/testing/lisp/test-ol.el b/testing/lisp/test-ol.el index e0cec0854..fa8d15c2b 100644 --- a/testing/lisp/test-ol.el +++ b/testing/lisp/test-ol.el @@ -381,6 +381,132 @@ See https://github.com/yantar92/org/issues/4." (equal (format "[[file:%s::*foo bar][foo bar]]" file file) (org-store-link nil))))))) +(ert-deftest test-org-link/precise-link-target () + "Test `org-link-precise-link-target` specifications." + (should + (equal '("*H1" . "H1") + (org-test-with-temp-text "* H1<point>\n* H2\n" + (org-link-precise-link-target)))) + (should + (equal '("foo" . "foo") + (org-test-with-temp-text "* H1\n#+name: foo<point>\n#+begin_example\nhi\n#+end_example\n" + (org-link-precise-link-target)))) + (should + (equal '("Text" . nil) + (org-test-with-temp-text "\nText<point>\n* H1\n" + (org-link-precise-link-target)))) + (should + (equal nil + (org-test-with-temp-text "\n<point>\n* H1\n" + (org-link-precise-link-target)))) + ;; relative to a heading + (should + (equal nil + (org-test-with-temp-text "* H1<point>\n* H2\n" + (org-link-precise-link-target 1)))) + (should + (equal '("*H2" . "H2") + (org-test-with-temp-text "* H1\n* H2<point>\n" + (org-link-precise-link-target 1)))) + (should + (equal nil + (org-test-with-temp-text "* H1\n* H2<point>\n" + (org-link-precise-link-target 6)))) + ) + +(defmacro test-ol-stored-link-with-text (text &rest body) + "Return :link and :description from link stored in body." + (declare (indent 1)) + `(let (org-store-link-plist) + (org-test-with-temp-text-in-file ,text + ,@body + (list (plist-get org-store-link-plist :link) + (plist-get org-store-link-plist :description))))) + +(ert-deftest test-org-link/id-store-link () + "Test `org-id-store-link' specifications." + (let ((org-id-link-to-org-use-id nil)) + (should + (equal '(nil nil) + (test-ol-stored-link-with-text "* H1\n:PROPERTIES:\n:ID: abc\n:END:\n" + (org-id-store-link-maybe t))))) + ;; On a headline, link to that headline's ID. Use heading as the + ;; description of the link. + (let ((org-id-link-to-org-use-id t)) + (should + (equal '("id:abc" "H1") + (test-ol-stored-link-with-text "* H1\n:PROPERTIES:\n:ID: abc\n:END:\n" + (org-id-store-link-maybe t))))) + ;; Remove TODO keywords etc from description of the link. + (let ((org-id-link-to-org-use-id t)) + (should + (equal '("id:abc" "H1") + (test-ol-stored-link-with-text "* TODO [#A] H1 :tag:\n:PROPERTIES:\n:ID: abc\n:END:\n" + (org-id-store-link-maybe t))))) + ;; create-if-interactive + (let ((org-id-link-to-org-use-id 'create-if-interactive)) + (should + (equal '("id:abc" "H1") + (cl-letf (((symbol-function 'org-id-new) + (lambda (&rest _rest) "abc"))) + (test-ol-stored-link-with-text "* H1\n" + (org-id-store-link-maybe t))))) + (should + (equal '(nil nil) + (test-ol-stored-link-with-text "* H1\n" + (org-id-store-link-maybe nil))))) + ;; create-if-interactive-and-no-custom-id + (let ((org-id-link-to-org-use-id 'create-if-interactive-and-no-custom-id)) + (should + (equal '("id:abc" "H1") + (cl-letf (((symbol-function 'org-id-new) + (lambda (&rest _rest) "abc"))) + (test-ol-stored-link-with-text "* H1\n" + (org-id-store-link-maybe t))))) + (should + (equal '(nil nil) + (test-ol-stored-link-with-text "* H1\n:PROPERTIES:\n:CUSTOM_ID: xyz\n:END:\n" + (org-id-store-link-maybe t)))) + (should + (equal '(nil nil) + (test-ol-stored-link-with-text "* H1\n" + (org-id-store-link-maybe nil)))))) + +(ert-deftest test-org-link/id-store-link-using-parent () + "Test `org-id-store-link' specifications with `org-id-link-consider-parent-id` set." + ;; when using context to still find specific heading + (let ((org-id-link-consider-parent-id t) + (org-id-link-use-context t)) + (should + (equal '("id:abc::*H2" "H2") + (test-ol-stored-link-with-text "* H1\n:PROPERTIES:\n:ID: abc\n:END:\n** H2\n<point>" + (org-id-store-link)))) + (should + (equal '("id:abc::name" "name") + (test-ol-stored-link-with-text "* H1\n:PROPERTIES:\n:ID: abc\n:END:\n\n#+name: name\n<point>#+begin_example\nhi\n#+end_example\n" + (org-id-store-link)))) + (should + (equal '("id:abc" "H1") + (test-ol-stored-link-with-text "* H1<point>\n:PROPERTIES:\n:ID: abc\n:END:\n** H2\n" + (org-id-store-link))))) + ;; when not using context, description should be the parent/file + (let ((org-id-link-consider-parent-id t) + (org-id-link-use-context nil)) + (should + (equal '("id:abc" "H1") + (test-ol-stored-link-with-text "* H1\n:PROPERTIES:\n:ID: abc\n:END:\n** H2\n<point>" + (org-id-store-link)))) + (should + (let ((result (test-ol-stored-link-with-text ":PROPERTIES:\n:ID: top\n:END:\n:* H1\n<point>" + (org-id-store-link)))) + (equal "id:top" (car result)) + ;; strip random buffer file name + (equal "org-test" (substring (cadr result) 0 8)))) + (should + (equal '("id:top" "title") + (test-ol-stored-link-with-text ":PROPERTIES:\n:ID: top\n:END:\n#+TITLE: title\n\n:* H1\n<point>" + (org-id-store-link)))))) + \f ;;; Radio Targets -- 2.39.2 (Apple Git-143) ^ permalink raw reply related [flat|nested] 48+ messages in thread
* Re: [PATCH v2] org-id: allow using parent's existing id in links to headlines 2024-01-28 22:47 ` Rick Lupton @ 2024-01-29 0:20 ` Samuel Wales 2024-01-29 13:06 ` Ihor Radchenko 2024-01-29 13:00 ` Ihor Radchenko 1 sibling, 1 reply; 48+ messages in thread From: Samuel Wales @ 2024-01-29 0:20 UTC (permalink / raw) To: Rick Lupton; +Cc: Ihor Radchenko, Y. E. sounds like a lot of contribution. i do not want to impede anything anybody else wants, but want to point out my user experience over years in case useful to anybody. my experiene is that context, search, and file links typically break for me, as i change headers, refile, fix typos, change paths, etc. so i stick with just org-id and, for export, custom id where possible. however, i would DEFINITELY also use org id link targets that are puttable in various locations [e.g. id markers]. what i am pointing out is probably obvious, but might be worth pointing out in a sentence in manual, or for setting defaults? On 1/28/24, Rick Lupton <mail@ricklupton.name> wrote: > Hi, > > Thanks for trying it out. Updated patches attached, comments below. > > On Mon, 18 Dec 2023, at 12:27 PM, Ihor Radchenko wrote: >> I played around with the patch a bit and found a couple of rough edges: >> >> 1. When I try to open a link to non-existing search target, like >> <id:some-id::non-existing-target>, I get a query to create a new >> heading. If I reply "yes", a new heading is created. However, the >> heading is created at the end of the file and is always level 1, >> regardless of the "some-id" parent context. >> It would make more sense to create a new heading at the end of the >> id:some-id subtree. > > Fixed in updated patches -- first patch adds generic new flexibility to > `org-insert-heading', second patch uses it so new headings now added at > correct level at the end of the id:sub-id subtree. > >> 2. Consider the following setting: >> (setq org-id-link-consider-parent-id t) >> (setq org-id-link-to-org-use-id >> 'create-if-interactive-and-no-custom-id) >> >> Then, create the following Org file >> >> * Sub >> * Parent here >> ** This is test >> :PROPERTIES: >> :ID: fe40252e-0527-44c1-a990-12498991f167 >> :END: >> >> *** Sub <point here> >> :PROPERTIES: >> :CUSTOM_ID: subid >> :END: >> >> When you M-x org-store-link, the stored link has ::*Sub instead of >> the expected ::#subid > > Updated so that search strings prefer custom-ids (::#subid) to headline > matches (::*Sub). This makes this example behave as you expect. > > The correct behaviour of org-store-link doesn't seem totally obvious to me > about id vs custom-id links. Currently org-store-link has special logic to > store TWO links (one <file:xx::#subid>, one <file:xx::*Sub>) when a > CUSTOM_ID is present. In the manual, it says: > > If the headline has a ‘CUSTOM_ID’ property, store a link to this > custom ID. In addition or alternatively, depending on the value of > ‘org-id-link-to-org-use-id’, create and/or use a globally unique > ‘ID’ property for the link(1). So using this command in Org > buffers potentially creates two links: a human-readable link from > the custom ID, and one that is globally unique and works even if > the entry is moved from file to file. The ‘ID’ property can be > either a UUID (default) or a timestamp, depending on > ‘org-id-method’. Later, when inserting the link, you need to > decide which one to use. > > That refers to ID links specifically, but now, using the generic link store > functions, there is only the possibility to store one link type, so it's not > possible to neatly keep exactly the same behaviour (i.e. for ID links but > not for other external link types). > > I think the intention of what's described in the manual is to distinguish > "human-readable" vs "persistent id" links. There could be other types of > "persistent id" links apart from org-id links, such as mu4e: links to email > message-ids. Therefore I've updated org-store-link to simply store a > <file:xx.org::#custom-id> link as an additional option, whether or not the > first matched link was an org-id link (this is the current behaviour) or > another external link type (this is changed behaviour). > > Added a note to ORG-NEWS about this. > >> 3. Consider >> (setq org-id-link-consider-parent-id t) >> (setq org-id-link-to-org-use-id t) >> >> Then, create a new empty Org file >> M-x org-store-link with create a top-level properties drawer with ID >> and store the link. However, that link will not be a simple ID link, >> but also have ::PROPERTIES search string, which is not expected. > > This is because it is trying to link to the current line of the file, which > contains the text "PROPERTIES". On main, with (setq > org-id-link-to-org-use-id nil), you see the equivalent behaviour (a link to > [[file:test.org:::PROPERTIES:]]) when point is before the first heading. > So, this seems consistent with non-org-id links? > > (these links don't actually work with the default value of > `org-link-search-must-match-exact-headline', but I think that's a separate > issue). > >>> + #+vindex: org-id-link-consider-parent-id >>> + When ~org-id-link-consider-parent-id~ is ~t~, parent =ID= properties >>> + are considered. This allows linking to specific targets, named >>> + blocks, or headlines (which may not have a globally unique =ID= >>> + themselves) within the context of a parent headline or file which >>> + does. >> >> It would be nice to add an example, similar to what you did in the >> docstring. > > Added. > >> >>> -(defun org-man-store-link () >>> +(defun org-man-store-link (&optional _interactive?) >>> "Store a link to a man page." >>> (when (memq major-mode '(Man-mode woman-mode)) >>> ;; This is a man page, we do make this link. >>> @@ -21312,13 +21324,15 @@ A review of =ol-man.el=: >> >> Please, update the actual built-in :store functions in lisp/ol-*.el to >> handle the new optional argument as well. > > Updated. > >>> +**** =org-link= store functions are passed an ~interactive?~ argument >>> + >>> +The ~:store:~ functions set for link types using >>> +~org-link-set-parameters~ are now passed an ~interactive?~ argument, >>> +indicating whether ~org-store-link~ was called interactively. >> >> Please also explain that the existing functions are not broken. > > Done. > >>> +*** ~org-id-store-link~ now adds search strings for precise link >>> targets >>> + >>> +This new behaviour can be disabled generally by setting >>> +~org-id-link-use-context~ to ~nil~, or when storing a specific link by >>> +passing a prefix argument to ~org-store-link~. >> >> universal argument. >> There are several possible prefix arguments in `org-store-link', but >> only C-u (universal argument) will give the described effect. >> Also, won't the behavior be _toggled_ by the universal argument? > > Updated. > >>> +When using this feature, IDs should not include =::=, which is used in >>> +links to indicate the start of the search string. For backwards >>> +compability, existing IDs including =::= will still be matched (but >>> +cannot be used together with precise link targets). >> >> Please add an org-lint checker that warns about such IDs and mention >> this checker in the above. > > Added. > >> Also, this paragraph belongs to "Breaking changes", not "new and changed >> options". > > That's where it is, I think. > >>> +*** New option ~org-id-link-consider-parent-id~ to allow =id:= links to >>> parent headlines >>> + >>> +For =id:= links, when this option is enabled, ~org-store-link~ will >>> +look for ids from parent/ancestor headlines, if the current headline >>> +does not have an id. >>> + >>> +Combined with the new ability for =id:= links to use search strings >>> +for precise link targets (when =org-id-link-use-context= is =t=, which >>> +is the default), this allows linking to specific headlines without >>> +requiring every headline to have an id property, as long as the >>> +headline is unique within a subtree that does have an id property. >>> + >>> +By giving files top-level id properties, links to headlines in the >>> +file can be made more robust by using the file id instead of the file >>> +path. >> >> Please, provide an example here as well. > > Done. > >>> +(defun org-link--try-link-store-functions (interactive?) >>> + "Try storing external links, prompting if more than one is possible. >>> + >>> +Each function returned by `org-store-link-functions' is called in >>> +turn. If multiple functions return non-nil, prompt for which >>> +link should be stored. >>> + >>> +Return t when a link has been stored in `org-link-store-props'." >> >> Please document INTERACTIVE? argument in the docstring. > > Done. > >>> + (let ((results-alist nil)) >>> + (dolist (f (org-store-link-functions)) >>> + (when (condition-case nil >>> + (funcall f interactive?) >>> + ;; XXX: The store function used (< Org 9.7) to accept no >>> + ;; arguments; provide backward compatibility support for >>> + ;; them. >> >> Use FIXME, not XXX. (I have no idea why it is XXX in the existing code). > > Changed. > >>> +(defun org-link-precise-link-target (&optional relative-to) >>> + "Determine search string and description for storing a link. >>> + >>> +If a search string is found, return cons cell (SEARCH-STRING >>> +. DESC). Otherwise, return nil. >>> + >>> +If there is an active region, the contents is used (see >>> +`org-link--context-from-region'). >> >> It is not clear from this sentence whether the contents is used for >> SEARCH-STRING of DESC. >> >>> +In org-mode buffers, if point is at a named element (e.g. a >>> +source block), the name is used. If within a heading, the current >>> +heading is used. >> >> Please use double space between sentences. >> >>> +Optional argument RELATIVE-TO specifies the buffer position where >>> +the search will start from. If the search target that would be >>> +returned is already at this location, return nil to avoid >>> +unnecessary search strings (for example, when using search >>> +strings to find targets within org-id links)." >> >> It is not clear what will happen if RELATIVE-TO is before/after point. > > Updated the docstring. > >>> - (let (link cpltxt desc search custom-id agenda-link) ;; description >>> + (let ((org-link-context-for-files (org-xor >>> org-link-context-for-files >>> + (equal arg '(4)))) >>> + link cpltxt desc search custom-id agenda-link) ;; description >>> (cond >>> ;; Store a link using an external link type, if any function is >>> - ;; available. If more than one can generate a link from current >>> - ;; location, ask which one to use. >>> + ;; available. If more than one can generate a link from >>> + ;; current location, ask which one to use. Negate >>> + ;; `org-context-in-file-links' when given a single prefix arg. >> >> The part of the comment about negation, should probably be moved near >> the let binding of `org-link-context-for-files'. > > Done. > >>> +For example, given this org file: >>> + >>> +* Parent >>> +:PROPERTIES: >>> +:ID: abc >>> +:END: >>> +** Child 1 >>> +** Child 2 >>> + >>> +With `org-id-link-consider-parent-id' set to t, storing a link >>> +with point at \"Child 1\" will produce a link \"id:abc\" to >>> +\"Parent\". >> >> This is actually confusing. May we only consider parent when >> `org-id-link-use-context' is enabled? > > Yes, I was trying to keep them independent but I agree it's probably more > useful to only consider parent when `org-id-link-use-context' is enabled > (which in turn depends on `org-context-in-file-links' being enabled). > >>> -(defun org-id-get (&optional epom create prefix) >>> +(defun org-id-get (&optional epom create prefix inherit) >>> "Get the ID property of the entry at EPOM. >>> EPOM is an element, marker, or buffer position. >>> If EPOM is nil, refer to the entry at point. >>> If the entry does not have an ID, the function returns nil. >>> +If INHERIT is non-nil, parents' IDs are also considered. >>> However, when CREATE is non-nil, create an ID if none is present >>> already. >>> PREFIX will be passed through to `org-id-new'. >>> In any case, the ID of the entry is returned." >> >> What about both CREATE and INHERIT being non-nil? > > Rewrote the docstring. > > Also removed INHERIT argument for `org-id-get-create' again, as other > functions can be re-written to use `org-id-get' directly, and INHERIT isn't > particularly useful when using `org-id-get-create' interactively. > >>> +;;;###autoload >>> +(defun org-id-store-link-maybe (&optional interactive?) >>> + "Store a link to the current entry using its ID if enabled. >>> + >>> +The value of `org-id-link-to-org-use-id' determines whether an ID >>> +link should be stored, using `org-id-store-link'. >>> + >>> +Assume the function is called interactively if INTERACTIVE? is >>> +non-nil." >>> + (interactive "p") >> >> Do we really need to make it interactive? > > No, removed. > > Thanks, > Rick -- The Kafka Pandemic A blog about science, health, human rights, and misopathy: https://thekafkapandemic.blogspot.com ^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH v2] org-id: allow using parent's existing id in links to headlines 2024-01-29 0:20 ` Samuel Wales @ 2024-01-29 13:06 ` Ihor Radchenko 2024-01-30 0:03 ` Samuel Wales 0 siblings, 1 reply; 48+ messages in thread From: Ihor Radchenko @ 2024-01-29 13:06 UTC (permalink / raw) To: Samuel Wales; +Cc: Rick Lupton, Y. E. Samuel Wales <samologist@gmail.com> writes: > my experiene is that context, search, and file links typically break > for me, as i change headers, refile, fix typos, change paths, etc. so > i stick with just org-id and, for export, custom id where possible. That's why we have `org-id-link-to-org-use-id'. > however, i would DEFINITELY also use org id link targets that are > puttable in various locations [e.g. id markers]. You mentioned this feature request in the past. It is not forgotten. > what i am pointing out is probably obvious, but might be worth > pointing out in a sentence in manual, or for setting defaults? May you please elaborate what you want to add to the manual and where? -- Ihor Radchenko // yantar92, Org mode contributor, Learn more about Org mode at <https://orgmode.org/>. Support Org development at <https://liberapay.com/org-mode>, or support my work at <https://liberapay.com/yantar92> ^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH v2] org-id: allow using parent's existing id in links to headlines 2024-01-29 13:06 ` Ihor Radchenko @ 2024-01-30 0:03 ` Samuel Wales 2024-02-03 15:08 ` Ihor Radchenko 0 siblings, 1 reply; 48+ messages in thread From: Samuel Wales @ 2024-01-30 0:03 UTC (permalink / raw) To: Ihor Radchenko; +Cc: Rick Lupton, Y. E. On 1/29/24, Ihor Radchenko <yantar92@posteo.net> wrote: > You mentioned this feature request in the past. It is not forgotten. thank you. > May you please elaborate what you want to add to the manual and where? had been merely thinking mentioning non-brittleness for newcomers. in handling links. but MAYBE also org-id could be slightly more integrated in org, such as not having to load the library. 9.6 info (info "(org) Handling Links") has context of org buffers. in future, i'd like to link to org-id in non-org files. (info "(org) Include Files") does not mention whether org-id works. could eliminate the need to specify file. ^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH v2] org-id: allow using parent's existing id in links to headlines 2024-01-30 0:03 ` Samuel Wales @ 2024-02-03 15:08 ` Ihor Radchenko 2024-11-13 3:23 ` Samuel Wales 0 siblings, 1 reply; 48+ messages in thread From: Ihor Radchenko @ 2024-02-03 15:08 UTC (permalink / raw) To: Samuel Wales; +Cc: Rick Lupton, Y. E. Samuel Wales <samologist@gmail.com> writes: >> May you please elaborate what you want to add to the manual and where? > > had been merely thinking mentioning non-brittleness for newcomers. > in handling links. Isn't it already mentioned? ... In addition or alternatively, depending on the value of ‘org-id-link-to-org-use-id’, create and/or use a globally unique ‘ID’ property for the link(1). So using this command in Org buffers potentially creates two links: a human-readable link from the custom ID, and one that is globally unique and works even if the entry is moved from file to file. The ‘ID’ property can be either a UUID (default) or a timestamp, depending on ‘org-id-method’. Later, when inserting the link, you need to decide which one to use. > but MAYBE also org-id could be slightly more integrated in org, such > as not having to load the library. The default value of `org-modules' is indeed a subject of discussion. AFAIK, the current problem is that adding extra link types into `org-modules' significantly slows down loading Org. > ... (info "(org) Include Files") does not > mention whether org-id works. could eliminate the need to specify > file. May you elaborate? -- Ihor Radchenko // yantar92, Org mode contributor, Learn more about Org mode at <https://orgmode.org/>. Support Org development at <https://liberapay.com/org-mode>, or support my work at <https://liberapay.com/yantar92> ^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH v2] org-id: allow using parent's existing id in links to headlines 2024-02-03 15:08 ` Ihor Radchenko @ 2024-11-13 3:23 ` Samuel Wales 0 siblings, 0 replies; 48+ messages in thread From: Samuel Wales @ 2024-11-13 3:23 UTC (permalink / raw) To: Ihor Radchenko; +Cc: Rick Lupton, Y. E. the idea is that if this were possible to do, it would eliminate the need to use duplicate text given that paths can change. #+INCLUDE: [[id:5f433209-07a1-4cac-bd63-17ce49fd5c33][x]] On 2/3/24, Ihor Radchenko <yantar92@posteo.net> wrote: > Samuel Wales <samologist@gmail.com> writes: > >>> May you please elaborate what you want to add to the manual and where? >> >> had been merely thinking mentioning non-brittleness for newcomers. >> in handling links. > > Isn't it already mentioned? > > ... In addition or alternatively, depending on the value of > ‘org-id-link-to-org-use-id’, create and/or use a globally unique > ‘ID’ property for the link(1). So using this command in Org > buffers potentially creates two links: a human-readable link from > the custom ID, and one that is globally unique and works even if > the entry is moved from file to file. The ‘ID’ property can be > either a UUID (default) or a timestamp, depending on > ‘org-id-method’. Later, when inserting the link, you need to > decide which one to use. > >> but MAYBE also org-id could be slightly more integrated in org, such >> as not having to load the library. > > The default value of `org-modules' is indeed a subject of discussion. > AFAIK, the current problem is that adding extra link types into > `org-modules' significantly slows down loading Org. > >> ... (info "(org) Include Files") does not >> mention whether org-id works. could eliminate the need to specify >> file. > > May you elaborate? > > -- > Ihor Radchenko // yantar92, > Org mode contributor, > Learn more about Org mode at <https://orgmode.org/>. > Support Org development at <https://liberapay.com/org-mode>, > or support my work at <https://liberapay.com/yantar92> > -- The Kafka Pandemic A blog about science, health, human rights, and misopathy: https://thekafkapandemic.blogspot.com ^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH v2] org-id: allow using parent's existing id in links to headlines 2024-01-28 22:47 ` Rick Lupton 2024-01-29 0:20 ` Samuel Wales @ 2024-01-29 13:00 ` Ihor Radchenko 2024-01-31 18:11 ` Rick Lupton 1 sibling, 1 reply; 48+ messages in thread From: Ihor Radchenko @ 2024-01-29 13:00 UTC (permalink / raw) To: Rick Lupton; +Cc: Y. E. "Rick Lupton" <mail@ricklupton.name> writes: > Thanks for trying it out. Updated patches attached, comments below. Thanks! >> 3. Consider >> (setq org-id-link-consider-parent-id t) >> (setq org-id-link-to-org-use-id t) >> >> Then, create a new empty Org file >> M-x org-store-link with create a top-level properties drawer with ID >> and store the link. However, that link will not be a simple ID link, >> but also have ::PROPERTIES search string, which is not expected. > > This is because it is trying to link to the current line of the file, which contains the text "PROPERTIES". On main, with (setq org-id-link-to-org-use-id nil), you see the equivalent behaviour (a link to [[file:test.org:::PROPERTIES:]]) when point is before the first heading. So, this seems consistent with non-org-id links? No. Do note that my instructions start from _empty_ file. With org-id-link-to-org-use-id, PROPERTIES drawer is not created. This is different from what happens with your patch - it is unexpected in your patch that the search string is added for text that did not exist in the buffer previously. > (these links don't actually work with the default value of > `org-link-search-must-match-exact-headline', but I think that's a > separate issue). That's a good catch. The fact that links stored via `org-store-link' cannot be open with default settings is not good. Also, your patch disregards this setting - it should not match non-headline search strings with the default value of `org-link-search-must-match-exact-headline'. Probably, changing the default value of `org-link-search-must-match-exact-headline' to nil is due. > Subject: [PATCH 2/2] org-id.el: Extend links with search strings, inherit > parent IDs I ran make test, and it looks like one test is failing with your patch: 1 unexpected results: FAILED test-org-link/id-store-link-using-parent ((should (equal '("id:abc" "H1") (test-ol-stored-link-with-text "* H1\n:PROPERTIES:\n:ID: abc\n:END:\n** H2\n<point>" (org-id-store-link)))) :form (equal ("id:abc" "H1") ("id:88e0c8d7-90a6-4628-b35a-cea989e3561b" "H2")) :value nil :explanation (list-elt 0 (arrays-of-different-length 6 39 "id:abc" "id:88e0c8d7-90a6-4628-b35a-cea989e3561b" first-mismatch-at 3))) > + #+vindex: org-id-link-consider-parent-id > + When ~org-id-link-consider-parent-id~ is ~t~ (and > + ~org-context-in-file-links~ and ~org-id-link-use-context~ are both `org-context-in-file-links' is an obsolete name. Use `org-link-context-for-files'. Also, please add `org-id-link-use-context' to #+vindex. > +**** =org-link= store functions are passed an ~interactive?~ argument > + > +The ~:store:~ functions set for link types using > +~org-link-set-parameters~ are now passed an ~interactive?~ argument, > +indicating whether ~org-store-link~ was called interactively. > + > +Existing store functions will continue to work. Please update the docstring of `org-store-link-functions' to specify that an argument is passed to :store functions. (org-link-parameters docstring says :store Function responsible for storing the link. See the function org-store-link-functions for a description of the expected arguments. ) > - (org-insert-heading nil t t) > + ;; Find appropriate level for new heading > + (let ((level (save-excursion > + (goto-char (point-min)) > + (+ 1 (or (org-current-level) 0))))) This is fragile. You assume that `point-min' always contains a heading. That may or may not be the case - `org-link-search' may be called by third-party code that does not care about setting narrowing in certain ways. It is more reliable to do something like (while (org-up-heading-safe) ...) to find the lowest-level ancestor. > +(defun org-link-precise-link-target (&optional relative-to) > + "Determine search string and description for storing a link. > + > +If a search string (see 'org-link-search') is found, return cons Quoting: `org-link-search'. > + (let* ((element (org-element-at-point)) > + (name (org-element-property :name element)) > + (heading (org-element-lineage element 'headline t)) What about inlinetasks? > + (custom-id (org-entry-get nil "CUSTOM_ID"))) May as well pass HEADING as the first argument of `org-entry-get'. It will be slightly more efficient. > + (org-link--add-to-stored-links link desc) > + ;; In org buffers, store an additional "human-readable" link > + ;; using custom id, if available. > + (when (and (buffer-file-name (buffer-base-buffer)) > + (derived-mode-p 'org-mode) > + (org-entry-get nil "CUSTOM_ID")) > + (setq link (concat "file:" > + (abbreviate-file-name > + (buffer-file-name (buffer-base-buffer))) > + "::#" (org-entry-get nil "CUSTOM_ID"))) This is fragile - you are relying upon the exact code used to store file:...#CUSTOM-ID link. Instead, please refactor the function to re-use that code. > + (id-location (or (and org-entry-property-inherited-from > + (marker-position org-entry-property-inherited-from)) > + (save-excursion (org-back-to-heading-or-point-min) (point)))) > (case-fold-search nil) > (desc (save-excursion > - (org-back-to-heading-or-point-min t) > + (goto-char id-location) You are calling `org-back-to-heading-or-point-min' without optional argument INVISIBLE-OK. This looks like an oversight. -- Ihor Radchenko // yantar92, Org mode contributor, Learn more about Org mode at <https://orgmode.org/>. Support Org development at <https://liberapay.com/org-mode>, or support my work at <https://liberapay.com/yantar92> ^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH v2] org-id: allow using parent's existing id in links to headlines 2024-01-29 13:00 ` Ihor Radchenko @ 2024-01-31 18:11 ` Rick Lupton 2024-02-01 12:13 ` Ihor Radchenko 2024-02-03 13:10 ` Ihor Radchenko 0 siblings, 2 replies; 48+ messages in thread From: Rick Lupton @ 2024-01-31 18:11 UTC (permalink / raw) To: Ihor Radchenko; +Cc: Y. E. [-- Attachment #1: Type: text/plain, Size: 6192 bytes --] On Mon, 29 Jan 2024, at 1:00 PM, Ihor Radchenko wrote: >>> 3. Consider >>> (setq org-id-link-consider-parent-id t) >>> (setq org-id-link-to-org-use-id t) >>> >>> Then, create a new empty Org file >>> M-x org-store-link with create a top-level properties drawer with ID >>> and store the link. However, that link will not be a simple ID link, >>> but also have ::PROPERTIES search string, which is not expected. >> >> This is because it is trying to link to the current line of the file, which contains the text "PROPERTIES". On main, with (setq org-id-link-to-org-use-id nil), you see the equivalent behaviour (a link to [[file:test.org:::PROPERTIES:]]) when point is before the first heading. So, this seems consistent with non-org-id links? > > No. Do note that my instructions start from _empty_ file. With > org-id-link-to-org-use-id, PROPERTIES drawer is not created. This is > different from what happens with your patch - it is unexpected in your > patch that the search string is added for text that did not exist in the > buffer previously. I see. Updated to get the search string first, before the possible properties draw appears. To make this work I changed `org-link-precise-link-target': instead of accepting the RELATIVE-TO argument and rejecting unsuitable targets internally, it now sets a marker `org-link-precise-target-marker' showing where the target that was found is, so the caller can decide if the found target is suitable. I copied the approach from `org-entry-property-inherited-from', hope that doesn't cause any other issues. > That's a good catch. > The fact that links stored via `org-store-link' cannot be open with > default settings is not good. > Also, your patch disregards this setting - it should not match > non-headline search strings with the default value of > `org-link-search-must-match-exact-headline'. `org-link-search-must-match-exact-headline' affects `org-link-search', which is called by `org-id-open' -- so I think the behaviour for these org-id links should be the same as for other file links? Am I missing something? Or, maybe you mean links that rely on `org-link-search-must-match-exact-headline' should not be stored. That would seem reasonable, but also doesn't need to be part of these changes here? > Probably, changing the default value of > `org-link-search-must-match-exact-headline' to nil is due. It seems like the behaviour below would be desirable, but doesn't currently exist with any setting of `org-link-search-must-match-exact-headline'? (org-link-search "plain text") --> fuzzy search for all text (org-link-search "*heading") --> search only headings, optionally creating if missing >> Subject: [PATCH 2/2] org-id.el: Extend links with search strings, inherit >> parent IDs > > I ran make test, and it looks like one test is failing with your patch: Oops, fixed now I think. > `org-context-in-file-links' is an obsolete name. Use > `org-link-context-for-files'. > > Also, please add `org-id-link-use-context' to #+vindex. Updated > Please update the docstring of `org-store-link-functions' to specify > that an argument is passed to :store functions. Updated >> - (org-insert-heading nil t t) >> + ;; Find appropriate level for new heading >> + (let ((level (save-excursion >> + (goto-char (point-min)) >> + (+ 1 (or (org-current-level) 0))))) > > This is fragile. You assume that `point-min' always contains a heading. > That may or may not be the case - `org-link-search' may be called by > third-party code that does not care about setting narrowing in certain > ways. I don't think it's a problem. (org-current-level) returns something suitable whether or not point-min contains a heading. Both the situations below seem reasonable choices for the level of the newly created heading at the end: ---start of narrowing--- Text * H1 ** H2 * A new level 1 heading is created at the end ---end of narrowing--- ---start of narrowing--- * H1 ** H2 ** A new level 2 heading is created at the end ---end of narrowing--- (this is how it currently works, unless I'm missing something) >> +(defun org-link-precise-link-target (&optional relative-to) >> + "Determine search string and description for storing a link. >> + >> +If a search string (see 'org-link-search') is found, return cons > > Quoting: `org-link-search'. Fixed >> + (let* ((element (org-element-at-point)) >> + (name (org-element-property :name element)) >> + (heading (org-element-lineage element 'headline t)) > > What about inlinetasks? I added inlinetasks to the element types, so they are picked up the same as headlines now. >> + (custom-id (org-entry-get nil "CUSTOM_ID"))) > > May as well pass HEADING as the first argument of `org-entry-get'. It > will be slightly more efficient. Ok >> + (org-link--add-to-stored-links link desc) >> + ;; In org buffers, store an additional "human-readable" link >> + ;; using custom id, if available. >> + (when (and (buffer-file-name (buffer-base-buffer)) >> + (derived-mode-p 'org-mode) >> + (org-entry-get nil "CUSTOM_ID")) >> + (setq link (concat "file:" >> + (abbreviate-file-name >> + (buffer-file-name (buffer-base-buffer))) >> + "::#" (org-entry-get nil "CUSTOM_ID"))) > > This is fragile - you are relying upon the exact code used to store > file:...#CUSTOM-ID link. Instead, please refactor the function to re-use > that code. Ok >> + (id-location (or (and org-entry-property-inherited-from >> + (marker-position org-entry-property-inherited-from)) >> + (save-excursion (org-back-to-heading-or-point-min) (point)))) >> (case-fold-search nil) >> (desc (save-excursion >> - (org-back-to-heading-or-point-min t) >> + (goto-char id-location) > > You are calling `org-back-to-heading-or-point-min' without optional > argument INVISIBLE-OK. This looks like an oversight. Fixed [-- Attachment #2: 0001-lisp-org.el-org-insert-heading-allow-specifying-head.patch --] [-- Type: application/octet-stream, Size: 5082 bytes --] From 347d4062113cbbfc9dcf8d2b9377589318d2f060 Mon Sep 17 00:00:00 2001 From: Rick Lupton <mail@ricklupton.name> Date: Wed, 3 Jan 2024 22:37:38 +0000 Subject: [PATCH 1/2] lisp/org.el (org-insert-heading): allow specifying heading level * lisp/org.el (org-insert-heading): Change optional argument TOP to LEVEL, accepting a number to force a specific heading level. * testing/lisp/test-org.el (test-org/insert-heading): Add tests * etc/ORG-NEWS: Document changes --- etc/ORG-NEWS | 6 ++++++ lisp/org.el | 21 ++++++++++++++------- testing/lisp/test-org.el | 26 ++++++++++++++++++++++++-- 3 files changed, 44 insertions(+), 9 deletions(-) diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS index 1bf7eb5b4..ec01004f8 100644 --- a/etc/ORG-NEWS +++ b/etc/ORG-NEWS @@ -941,6 +941,12 @@ as the function can also act on objects. *** ~org-export-get-parent-element~ is renamed to ~org-element-parent-element~ and moved to =lisp/org-element.el= +*** ~org-insert-heading~ optional argument =TOP= is now =LEVEL= + +A numeric value forces a heading at that level to be inserted. For +backwards compatibility, non-numeric non-nil values insert level 1 +headings as before. + ** Miscellaneous *** =org-crypt.el= now applies initial visibility settings to decrypted entries diff --git a/lisp/org.el b/lisp/org.el index 796545392..87b94a54d 100644 --- a/lisp/org.el +++ b/lisp/org.el @@ -6352,7 +6352,7 @@ headline instead of current one." (`(heading . ,value) value) (_ nil))) -(defun org-insert-heading (&optional arg invisible-ok top) +(defun org-insert-heading (&optional arg invisible-ok level) "Insert a new heading or an item with the same depth at point. If point is at the beginning of a heading, insert a new heading @@ -6381,12 +6381,19 @@ When INVISIBLE-OK is set, stop at invisible headlines when going back. This is important for non-interactive uses of the command. -When optional argument TOP is non-nil, insert a level 1 heading, -unconditionally." +When optional argument LEVEL is a number, insert a heading at +that level. For backwards compatibility, when LEVEL is non-nil +but not a number, insert a level-1 heading." (interactive "P") (let* ((blank? (org--blank-before-heading-p (equal arg '(16)))) - (level (org-current-level)) - (stars (make-string (if (and level (not top)) level 1) ?*))) + (current-level (org-current-level)) + (num-stars (or + ;; Backwards compat: if LEVEL non-nil, level is 1 + (and level (if (wholenump level) level 1)) + current-level + ;; This `1' is for when before first headline + 1)) + (stars (make-string num-stars ?*))) (cond ((or org-insert-heading-respect-content (member arg '((4) (16))) @@ -6395,7 +6402,7 @@ unconditionally." ;; Position point at the location of insertion. Make sure we ;; end up on a visible headline if INVISIBLE-OK is nil. (org-with-limited-levels - (if (not level) (outline-next-heading) ;before first headline + (if (not current-level) (outline-next-heading) ;before first headline (org-back-to-heading invisible-ok) (when (equal arg '(16)) (org-up-heading-safe)) (org-end-of-subtree invisible-ok 'to-heading))) @@ -6408,7 +6415,7 @@ unconditionally." (org-before-first-heading-p))) (insert "\n") (backward-char)) - (when (and (not level) (not (eobp)) (not (bobp))) + (when (and (not current-level) (not (eobp)) (not (bobp))) (when (org-at-heading-p) (insert "\n")) (backward-char)) (unless (and blank? (org-previous-line-empty-p)) diff --git a/testing/lisp/test-org.el b/testing/lisp/test-org.el index 822cbc67a..fc50dc787 100644 --- a/testing/lisp/test-org.el +++ b/testing/lisp/test-org.el @@ -1980,8 +1980,30 @@ CLOCK: [2022-09-17 sam. 11:00]--[2022-09-17 sam. 11:46] => 0:46" (let ((org-insert-heading-respect-content nil)) (org-insert-heading '(16))) (buffer-string)))) - ;; When optional TOP-LEVEL argument is non-nil, always insert - ;; a level 1 heading. + ;; When optional LEVEL argument is a number, insert a heading at + ;; that level. + (should + (equal "* H1\n** H2\n* " + (org-test-with-temp-text "* H1\n** H2<point>" + (org-insert-heading nil nil 1) + (buffer-string)))) + (should + (equal "* H1\n** H2\n** " + (org-test-with-temp-text "* H1\n** H2<point>" + (org-insert-heading nil nil 2) + (buffer-string)))) + (should + (equal "* H1\n** H2\n*** " + (org-test-with-temp-text "* H1\n** H2<point>" + (org-insert-heading nil nil 3) + (buffer-string)))) + (should + (equal "* H1\n- item\n* " + (org-test-with-temp-text "* H1\n- item<point>" + (org-insert-heading nil nil 1) + (buffer-string)))) + ;; When optional LEVEL argument is non-nil, always insert a level 1 + ;; heading. (should (equal "* H1\n** H2\n* " (org-test-with-temp-text "* H1\n** H2<point>" -- 2.37.1 (Apple Git-137.1) [-- Attachment #3: 0002-org-id.el-Extend-links-with-search-strings-inherit-p.patch --] [-- Type: application/octet-stream, Size: 57626 bytes --] From e62c94b0e23f647195b9196fdcdac225cd96bcbb Mon Sep 17 00:00:00 2001 From: Rick Lupton <mail@ricklupton.name> Date: Sun, 19 Nov 2023 14:52:05 +0000 Subject: [PATCH 2/2] org-id.el: Extend links with search strings, inherit parent IDs * lisp/ol.el (org-store-link): Refactor org-id links to use standard `org-store-link-functions'. (org-link-search): Create new headings at appropriate level. (org-link-precise-link-target): New function extracting logic to identify a precise link target, e.g. a heading, named object, or text search. (org-link-try-link-store-functions): Extract logic to call external link store functions. Pass them a new `interactive?' argument. * lisp/ol-bbdb.el (org-bbdb-store-link): * lisp/ol-bibtex.el (org-bibtex-store-link): * lisp/ol-docview.el (org-docview-store-link): * lisp/ol-eshell.el (org-eshell-store-link): * lisp/ol-eww.el (org-eww-store-link): * lisp/ol-gnus.el (org-gnus-store-link): * lisp/ol-info.el (org-info-store-link): * lisp/ol-irc.el (org-irc-store-link): * lisp/ol-man.el (org-man-store-link): * lisp/ol-mhe.el (org-mhe-store-link): * lisp/ol-rmail.el (org-rmail-store-link): Accept optional arg. * lisp/org-id.el (org-id-link-consider-parent-id): New option to allow a parent heading with an id to be considered as a link target. (org-id-link-use-context): New option to add context to org-id links. (org-id-get): Add optional `inherit' argument which considers parents' IDs if the current entry does not have one. (org-id-store-link): Consider IDs of parent headings as link targets when current heading has no ID and `org-id-link-consider-parent-id' is set. Add a search string to the link when enabled. (org-id-store-link-maybe): Function set as :store option for custom id link property. Move logic from `org-store-link' here to determine when an org-id link should be stored using `org-id-store-link'. (org-id-open): Recognise search strings after "::" in org-id links. * lisp/org-lint.el: add checker for "::" in ID properties. * testing/lisp/test-ol.el: Add tests for `org-link-precise-link-target' and `org-id-store-link' functions, testing new options. * doc/org-manual.org: Update documentation about links. * etc/ORG-NEWS: Document changes and new options. These feature allows for more precise links when using org-id to link to org headings, without requiring every single headline to have an id. Link: https://list.orgmode.org/118435e8-0b20-46fd-af6a-88de8e19fac6@app.fastmail.com/ --- doc/org-manual.org | 133 ++++++++++------- etc/ORG-NEWS | 64 +++++++++ lisp/ol-bbdb.el | 2 +- lisp/ol-bibtex.el | 2 +- lisp/ol-docview.el | 2 +- lisp/ol-eshell.el | 2 +- lisp/ol-eww.el | 2 +- lisp/ol-gnus.el | 2 +- lisp/ol-info.el | 2 +- lisp/ol-irc.el | 2 +- lisp/ol-man.el | 2 +- lisp/ol-mhe.el | 2 +- lisp/ol-rmail.el | 2 +- lisp/ol.el | 312 +++++++++++++++++++++++++--------------- lisp/org-id.el | 178 ++++++++++++++++++++--- lisp/org-lint.el | 16 +++ testing/lisp/test-ol.el | 130 +++++++++++++++++ 17 files changed, 658 insertions(+), 197 deletions(-) diff --git a/doc/org-manual.org b/doc/org-manual.org index 7e5ac0673..f0287e095 100644 --- a/doc/org-manual.org +++ b/doc/org-manual.org @@ -3297,10 +3297,6 @@ Here is the full set of built-in link types: File links. File name may be remote, absolute, or relative. - Additionally, you can specify a line number, or a text search. - In Org files, you may link to a headline name, a custom ID, or a - code reference instead. - As a special case, "file" prefix may be omitted if the file name is complete, e.g., it starts with =./=, or =/=. @@ -3364,44 +3360,50 @@ Here is the full set of built-in link types: Execute a shell command upon activation. + +For =file:= and =id:= links, you can additionally specify a line +number, or a text search string, separated by =::=. In Org files, you +may link to a headline name, a custom ID, or a code reference instead. + The following table illustrates the link types above, along with their options: -| Link Type | Example | -|------------+----------------------------------------------------------| -| http | =http://staff.science.uva.nl/c.dominik/= | -| https | =https://orgmode.org/= | -| doi | =doi:10.1000/182= | -| file | =file:/home/dominik/images/jupiter.jpg= | -| | =/home/dominik/images/jupiter.jpg= (same as above) | -| | =file:papers/last.pdf= | -| | =./papers/last.pdf= (same as above) | -| | =file:/ssh:me@some.where:papers/last.pdf= (remote) | -| | =/ssh:me@some.where:papers/last.pdf= (same as above) | -| | =file:sometextfile::NNN= (jump to line number) | -| | =file:projects.org= | -| | =file:projects.org::some words= (text search)[fn:12] | -| | =file:projects.org::*task title= (headline search) | -| | =file:projects.org::#custom-id= (headline search) | -| attachment | =attachment:projects.org= | -| | =attachment:projects.org::some words= (text search) | -| docview | =docview:papers/last.pdf::NNN= | -| id | =id:B7423F4D-2E8A-471B-8810-C40F074717E9= | -| news | =news:comp.emacs= | -| mailto | =mailto:adent@galaxy.net= | -| mhe | =mhe:folder= (folder link) | -| | =mhe:folder#id= (message link) | -| rmail | =rmail:folder= (folder link) | -| | =rmail:folder#id= (message link) | -| gnus | =gnus:group= (group link) | -| | =gnus:group#id= (article link) | -| bbdb | =bbdb:R.*Stallman= (record with regexp) | -| irc | =irc:/irc.com/#emacs/bob= | -| help | =help:org-store-link= | -| info | =info:org#External links= | -| shell | =shell:ls *.org= | -| elisp | =elisp:(find-file "Elisp.org")= (Elisp form to evaluate) | -| | =elisp:org-agenda= (interactive Elisp command) | +| Link Type | Example | +|------------+--------------------------------------------------------------------| +| http | =http://staff.science.uva.nl/c.dominik/= | +| https | =https://orgmode.org/= | +| doi | =doi:10.1000/182= | +| file | =file:/home/dominik/images/jupiter.jpg= | +| | =/home/dominik/images/jupiter.jpg= (same as above) | +| | =file:papers/last.pdf= | +| | =./papers/last.pdf= (same as above) | +| | =file:/ssh:me@some.where:papers/last.pdf= (remote) | +| | =/ssh:me@some.where:papers/last.pdf= (same as above) | +| | =file:sometextfile::NNN= (jump to line number) | +| | =file:projects.org= | +| | =file:projects.org::some words= (text search)[fn:12] | +| | =file:projects.org::*task title= (headline search) | +| | =file:projects.org::#custom-id= (headline search) | +| attachment | =attachment:projects.org= | +| | =attachment:projects.org::some words= (text search) | +| docview | =docview:papers/last.pdf::NNN= | +| id | =id:B7423F4D-2E8A-471B-8810-C40F074717E9= | +| | =id:B7423F4D-2E8A-471B-8810-C40F074717E9::*task= (headline search) | +| news | =news:comp.emacs= | +| mailto | =mailto:adent@galaxy.net= | +| mhe | =mhe:folder= (folder link) | +| | =mhe:folder#id= (message link) | +| rmail | =rmail:folder= (folder link) | +| | =rmail:folder#id= (message link) | +| gnus | =gnus:group= (group link) | +| | =gnus:group#id= (article link) | +| bbdb | =bbdb:R.*Stallman= (record with regexp) | +| irc | =irc:/irc.com/#emacs/bob= | +| help | =help:org-store-link= | +| info | =info:org#External links= | +| shell | =shell:ls *.org= | +| elisp | =elisp:(find-file "Elisp.org")= (Elisp form to evaluate) | +| | =elisp:org-agenda= (interactive Elisp command) | #+cindex: VM links #+cindex: Wanderlust links @@ -3462,8 +3464,9 @@ current buffer: - /Org mode buffers/ :: For Org files, if there is a =<<target>>= at point, the link points - to the target. Otherwise it points to the current headline, which - is also the description. + to the target. If there is a named block (using =#+name:=) at + point, the link points to that name. Otherwise it points to the + current headline, which is also the description. #+vindex: org-id-link-to-org-use-id #+cindex: @samp{CUSTOM_ID}, property @@ -3481,6 +3484,30 @@ current buffer: timestamp, depending on ~org-id-method~. Later, when inserting the link, you need to decide which one to use. + #+vindex: org-id-link-consider-parent-id + #+vindex: org-id-link-use-context + When ~org-id-link-consider-parent-id~ is ~t~ (and + ~org-link-context-for-files~ and ~org-id-link-use-context~ are both + enabled), parent =ID= properties are considered. This allows + linking to specific targets, named blocks, or headlines (which may + not have a globally unique =ID= themselves) within the context of a + parent headline or file which does. + + For example, given this org file with those variables set: + + #+begin_src org + ,* Parent + :PROPERTIES: + :ID: abc + :END: + ,** Child 1 + ,** Child 2 + #+end_src + + Storing a link with point at "Child 1" will produce a link + =<id:abc::*Child 1>=, which precisely links to the "Child 1" + headline even though it does not have its own ID. + - /Email/News clients: VM, Rmail, Wanderlust, MH-E, Gnus/ :: #+vindex: org-link-email-description-format @@ -3760,7 +3787,9 @@ the link completion function like this: :ALT_TITLE: Search Options :END: #+cindex: search option in file links +#+cindex: search option in id links #+cindex: file links, searching +#+cindex: id links, searching #+cindex: attachment links, searching File links can contain additional information to make Emacs jump to a @@ -3772,8 +3801,8 @@ example, when the command ~org-store-link~ creates a link (see line as a search string that can be used to find this line back later when following the link with {{{kbd(C-c C-o)}}}. -Note that all search options apply for Attachment links in the same -way that they apply for File links. +Note that all search options apply for Attachment and ID links in the +same way that they apply for File links. Here is the syntax of the different ways to attach a search to a file link, together with explanations for each: @@ -21355,7 +21384,7 @@ The following =ol-man.el= file implements it PATH should be a topic that can be thrown at the man command." (funcall org-man-command path)) -(defun org-man-store-link () +(defun org-man-store-link (&optional _interactive?) "Store a link to a man page." (when (memq major-mode '(Man-mode woman-mode)) ;; This is a man page, we do make this link. @@ -21415,13 +21444,15 @@ A review of =ol-man.el=: For example, ~org-man-store-link~ is responsible for storing a link when ~org-store-link~ (see [[*Handling Links]]) is called from a buffer - displaying a man page. It first checks if the major mode is - appropriate. If check fails, the function returns ~nil~, which - means it isn't responsible for creating a link to the current - buffer. Otherwise the function makes a link string by combining - the =man:= prefix with the man topic. It also provides a default - description. The function ~org-insert-link~ can insert it back - into an Org buffer later on. + displaying a man page. It is passed an argument ~interactive?~ + which this function does not use, but other store functions use to + behave differently when a link is stored interactively by the user. + It first checks if the major mode is appropriate. If check fails, + the function returns ~nil~, which means it isn't responsible for + creating a link to the current buffer. Otherwise the function + makes a link string by combining the =man:= prefix with the man + topic. It also provides a default description. The function + ~org-insert-link~ can insert it back into an Org buffer later on. ** Adding Export Backends :PROPERTIES: diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS index ec01004f8..1115e3bb4 100644 --- a/etc/ORG-NEWS +++ b/etc/ORG-NEWS @@ -345,6 +345,14 @@ timestamp object. Possible values: ~timerange~, ~daterange~, ~nil~. ~org-element-timestamp-interpreter~ takes into account this property and returns an appropriate timestamp string. +**** =org-link= store functions are passed an ~interactive?~ argument + +The ~:store:~ functions set for link types using +~org-link-set-parameters~ are now passed an ~interactive?~ argument, +indicating whether ~org-store-link~ was called interactively. + +Existing store functions will continue to work. + *** ~org-priority=show~ command no longer adjusts for scheduled/deadline In agenda views, ~org-priority=show~ command previously displayed the @@ -423,6 +431,27 @@ The change is breaking when ~org-use-property-inheritance~ is set to ~t~. *** ~org-babel-lilypond-compile-lilyfile~ ignores optional second argument The =TEST= parameter is better served by Emacs debugging tools. + +*** ~org-id-store-link~ now adds search strings for precise link targets + +This new behaviour can be disabled generally by setting +~org-id-link-use-context~ to ~nil~, or the setting can be toggled for +a single call to ~org-store-link~ with a universal argument. + +When using this feature, IDs should not include =::=, which is used in +links to indicate the start of the search string. For backwards +compability, existing IDs including =::= will still be matched (but +cannot be used together with precise link targets). An org-lint +checker has been added to warn about this. + +*** ~org-store-link~ behaviour storing additional =CUSTOM_ID= links has changed + +As well as an =id:= link, ~org-store-link~ stores an additional "human +readable" link using a node's =CUSTOM_ID= property, if available. +This behaviour has been expanded to store an additional =CUSTOM_ID= +link when storing any type of external link type in an Org file, not +just =id:= links. + ** New and changed options *** The default value of ~org-attach-store-link-p~ is now ~attached~ @@ -659,6 +688,35 @@ manner with ~run-python~. This allows to run functions after ~org-indent~ intializes a buffer to enrich its properties. +*** New option ~org-id-link-consider-parent-id~ to allow =id:= links to parent headlines + +For =id:= links, when this option is enabled, ~org-store-link~ will +look for ids from parent/ancestor headlines, if the current headline +does not have an id. + +Combined with the new ability for =id:= links to use search strings +for precise link targets (when =org-id-link-use-context= is =t=, which +is the default), this allows linking to specific headlines without +requiring every headline to have an id property, as long as the +headline is unique within a subtree that does have an id property. + +For example, given this org file: + +#+begin_src org +,* Parent +:PROPERTIES: +:ID: abc +:END: +,** Child 1 +,** Child 2 +#+end_src + +Storing a link with point at "Child 1" will produce a link +=<id:abc::*Child 1>=, which precisely links to the "Child 1" headline +even though it does not have its own ID. By giving files top-level id +properties, links to headlines in the file can also be made more +robust by using the file id instead of the file path. + ** New features *** =ob-plantuml.el=: Support tikz file format output @@ -947,6 +1005,12 @@ A numeric value forces a heading at that level to be inserted. For backwards compatibility, non-numeric non-nil values insert level 1 headings as before. +*** New optional argument for ~org-id-get~ + +New optional argument =INHERIT= means inherited ID properties from +parent entries are considered when getting an entry's ID (see +~org-id-link-consider-parent-id~ option). + ** Miscellaneous *** =org-crypt.el= now applies initial visibility settings to decrypted entries diff --git a/lisp/ol-bbdb.el b/lisp/ol-bbdb.el index be3924fc9..6ea060f70 100644 --- a/lisp/ol-bbdb.el +++ b/lisp/ol-bbdb.el @@ -226,7 +226,7 @@ date year)." ;;; Implementation -(defun org-bbdb-store-link () +(defun org-bbdb-store-link (&optional _interactive?) "Store a link to a BBDB database entry." (when (eq major-mode 'bbdb-mode) ;; This is BBDB, we make this link! diff --git a/lisp/ol-bibtex.el b/lisp/ol-bibtex.el index c5a950e2d..38468f32f 100644 --- a/lisp/ol-bibtex.el +++ b/lisp/ol-bibtex.el @@ -507,7 +507,7 @@ ARG, when non-nil, is a universal prefix argument. See `org-open-file' for details." (org-link-open-as-file path arg)) -(defun org-bibtex-store-link () +(defun org-bibtex-store-link (&optional _interactive?) "Store a link to a BibTeX entry." (when (eq major-mode 'bibtex-mode) (let* ((search (org-create-file-search-in-bibtex)) diff --git a/lisp/ol-docview.el b/lisp/ol-docview.el index b31f1ce5e..0907ddee1 100644 --- a/lisp/ol-docview.el +++ b/lisp/ol-docview.el @@ -83,7 +83,7 @@ (error "No such file: %s" path)) (when page (doc-view-goto-page page)))) -(defun org-docview-store-link () +(defun org-docview-store-link (&optional _interactive?) "Store a link to a docview buffer." (when (eq major-mode 'doc-view-mode) ;; This buffer is in doc-view-mode diff --git a/lisp/ol-eshell.el b/lisp/ol-eshell.el index 2c7ec6bef..595dd0ee0 100644 --- a/lisp/ol-eshell.el +++ b/lisp/ol-eshell.el @@ -60,7 +60,7 @@ followed by a colon." (insert command) (eshell-send-input))) -(defun org-eshell-store-link () +(defun org-eshell-store-link (&optional _interactive?) "Store eshell link. When opened, the link switches back to the current eshell buffer and the current working directory." diff --git a/lisp/ol-eww.el b/lisp/ol-eww.el index 40b820d2b..c13dbf339 100644 --- a/lisp/ol-eww.el +++ b/lisp/ol-eww.el @@ -62,7 +62,7 @@ "Open URL with Eww in the current buffer." (eww url)) -(defun org-eww-store-link () +(defun org-eww-store-link (&optional _interactive?) "Store a link to the url of an EWW buffer." (when (eq major-mode 'eww-mode) (org-link-store-props diff --git a/lisp/ol-gnus.el b/lisp/ol-gnus.el index e105fdb2c..b9ee8683f 100644 --- a/lisp/ol-gnus.el +++ b/lisp/ol-gnus.el @@ -123,7 +123,7 @@ If `org-store-link' was called with a prefix arg the meaning of (url-encode-url message-id)) (concat "gnus:" group "#" message-id))) -(defun org-gnus-store-link () +(defun org-gnus-store-link (&optional _interactive?) "Store a link to a Gnus folder or message." (pcase major-mode (`gnus-group-mode diff --git a/lisp/ol-info.el b/lisp/ol-info.el index 0edf9a13f..6062cab34 100644 --- a/lisp/ol-info.el +++ b/lisp/ol-info.el @@ -50,7 +50,7 @@ :insert-description #'org-info-description-as-command) ;; Implementation -(defun org-info-store-link () +(defun org-info-store-link (&optional _interactive?) "Store a link to an Info file and node." (when (eq major-mode 'Info-mode) (let ((link (concat "info:" diff --git a/lisp/ol-irc.el b/lisp/ol-irc.el index 78c4884b0..b263e52db 100644 --- a/lisp/ol-irc.el +++ b/lisp/ol-irc.el @@ -103,7 +103,7 @@ attributes that are found." parts)) ;;;###autoload -(defun org-irc-store-link () +(defun org-irc-store-link (&optional _interactive?) "Dispatch to the appropriate function to store a link to an IRC session." (cond ((eq major-mode 'erc-mode) diff --git a/lisp/ol-man.el b/lisp/ol-man.el index e3f13815e..42aacea81 100644 --- a/lisp/ol-man.el +++ b/lisp/ol-man.el @@ -82,7 +82,7 @@ matched strings in man buffer." (set-window-point window point) (set-window-start window point))))))) -(defun org-man-store-link () +(defun org-man-store-link (&optional _interactive?) "Store a link to a README file." (when (memq major-mode '(Man-mode woman-mode)) ;; This is a man page, we do make this link diff --git a/lisp/ol-mhe.el b/lisp/ol-mhe.el index 106cfedc9..a32481324 100644 --- a/lisp/ol-mhe.el +++ b/lisp/ol-mhe.el @@ -80,7 +80,7 @@ supported by MH-E." (org-link-set-parameters "mhe" :follow #'org-mhe-open :store #'org-mhe-store-link) ;; Implementation -(defun org-mhe-store-link () +(defun org-mhe-store-link (&optional _interactive?) "Store a link to an MH-E folder or message." (when (or (eq major-mode 'mh-folder-mode) (eq major-mode 'mh-show-mode)) diff --git a/lisp/ol-rmail.el b/lisp/ol-rmail.el index f6031ab52..f1f753b6f 100644 --- a/lisp/ol-rmail.el +++ b/lisp/ol-rmail.el @@ -51,7 +51,7 @@ :store #'org-rmail-store-link) ;; Implementation -(defun org-rmail-store-link () +(defun org-rmail-store-link (&optional _interactive?) "Store a link to an Rmail folder or message." (when (or (eq major-mode 'rmail-mode) (eq major-mode 'rmail-summary-mode)) diff --git a/lisp/ol.el b/lisp/ol.el index cf59c8556..7e7df468a 100644 --- a/lisp/ol.el +++ b/lisp/ol.el @@ -63,7 +63,6 @@ (declare-function org-find-property "org" (property &optional value)) (declare-function org-get-heading "org" (&optional no-tags no-todo no-priority no-comment)) (declare-function org-id-find-id-file "org-id" (id)) -(declare-function org-id-store-link "org-id" ()) (declare-function org-insert-heading "org" (&optional arg invisible-ok top)) (declare-function org-load-modules-maybe "org" (&optional force)) (declare-function org-mark-ring-push "org" (&optional pos buffer)) @@ -620,6 +619,12 @@ If it decides that it is not responsible for this link, it must return nil to indicate that Org can continue with other options like exact and fuzzy text search.") +(defvar org-link-precise-target-marker (make-marker) + "Marker pointing to the target identified for a link search string. +Each call to `org-link-precise-link-target' will set this marker +to the location where the returned target was found. If there +was no target, the marker will point nowhere.") + \f ;;; Internal Variables @@ -815,6 +820,74 @@ spec." (org-with-point-at (car region) (not (org-in-regexp org-link-any-re)))) +(defun org-link--try-link-store-functions (interactive?) + "Try storing external links, prompting if more than one is possible. + +Each function returned by `org-store-link-functions' is called in +turn. If multiple functions return non-nil, prompt for which +link should be stored. + +Argument INTERACTIVE? indicates whether `org-store-link' was +called interactively and is passed to the link store functions. + +Return t when a link has been stored in `org-link-store-props'." + (let ((results-alist nil)) + (dolist (f (org-store-link-functions)) + (when (condition-case nil + (funcall f interactive?) + ;; FIXME: The store function used (< Org 9.7) to accept + ;; no arguments; provide backward compatibility support + ;; for them. + (wrong-number-of-arguments + (funcall f))) + ;; FIXME: return value is not link's plist, so we store the + ;; new value before it is modified. It would be cleaner to + ;; ask store link functions to return the plist instead. + (push (cons f (copy-sequence org-store-link-plist)) + results-alist))) + (pcase results-alist + (`nil nil) + (`((,_ . ,_)) t) ;single choice: nothing to do + (`((,name . ,_) . ,_) + ;; Reinstate link plist associated to the chosen + ;; function. + (apply #'org-link-store-props + (cdr (assoc-string + (completing-read + (format "Store link with (default %s): " name) + (mapcar #'car results-alist) + nil t nil nil (symbol-name name)) + results-alist))) + t)))) + +(defun org-link--add-to-stored-links (link desc) + "Add LINK to `org-stored-links' with description DESC." + (cond + ((not (member (list link desc) org-stored-links)) + (push (list link desc) org-stored-links) + (message "Stored: %s" (or desc link))) + ((equal (list link desc) (car org-stored-links)) + (message "This link has already been stored")) + (t + (setq org-stored-links + (delete (list link desc) org-stored-links)) + (push (list link desc) org-stored-links) + (message "Link moved to front: %s" (or desc link))))) + +(defun org-link--file-link-to-here () + "Return as (LINK . DESC) a file link with search string to here." + (let ((link (concat "file:" + (abbreviate-file-name + (buffer-file-name (buffer-base-buffer))))) + desc) + (when org-link-context-for-files + (pcase (org-link-precise-link-target) + (`nil nil) + (`(,search-string . ,search-desc) + (setq link (format "%s::%s" link search-string)) + (setq desc search-desc)))) + (cons link desc))) + \f ;;; Public API @@ -1041,7 +1114,9 @@ LINK is escaped with backslashes for inclusion in buffer." "List of functions that are called to create and store a link. The functions are defined in the `:store' property of -`org-link-parameters'. +`org-link-parameters'. Each function should accept an argument +INTERACTIVE? which indicates whether the user has initiated +`org-store-link' interactively. Each function will be called in turn until one returns a non-nil value. Each function should check if it is responsible for @@ -1280,7 +1355,11 @@ respects buffer narrowing." (yes-or-no-p "No match - create this as a new heading? ")) (goto-char (point-max)) (unless (bolp) (newline)) - (org-insert-heading nil t t) + ;; Find appropriate level for new heading + (let ((level (save-excursion + (goto-char (point-min)) + (+ 1 (or (org-current-level) 0))))) + (org-insert-heading nil t level)) (insert s "\n") (forward-line -1)) ;; Only headlines are looked after. No need to process @@ -1332,6 +1411,71 @@ priority cookie or tag." (org-link--normalize-string (or string (org-get-heading t t t t))))) +(defun org-link-precise-link-target () + "Determine search string and description for storing a link. + +If a search string (see `org-link-search') is found, return cons +cell (SEARCH-STRING . DESC). Otherwise, return nil. + +If there is an active region, the contents (or a part of it, see +`org-link-context-for-files') is used as the search string. + +In Org buffers, if point is at a named element (such as a source +block), the name is used for the search string. If at a heading, +its CUSTOM_ID is used to form a search string of the form +\"#id\", if present, otherwise the current heading text is used +in the form \"*Heading\". + +If none of those finds a suitable search string, the current line +is used as the search string. + +The description DESC is nil (meaning the user will be prompted +for a description when inserting the link) for search strings +based on a region or the current line. For other cases, DESC is +a cleaned-up version of the name or heading at point. + +`org-link-precise-target-marker' is set to the location to which the +search string refers, or to nowhere if a target is not identified." + (move-marker org-link-precise-target-marker nil) + (let* ((region (org-link--context-from-region)) + (result + (cond + (region + (move-marker org-link-precise-target-marker (region-beginning)) + (cons (org-link--normalize-string region t) nil)) + + ((derived-mode-p 'org-mode) + (let* ((element (org-element-at-point)) + (name (org-element-property :name element)) + (heading (org-element-lineage element '(headline inlinetask) t)) + (custom-id (org-entry-get heading "CUSTOM_ID"))) + (cond + (name + (move-marker org-link-precise-target-marker + (org-element-begin element)) + (cons name name)) + ((org-before-first-heading-p) + (move-marker org-link-precise-target-marker + (line-beginning-position)) + (cons (org-link--normalize-string (org-current-line-string) t) nil)) + (heading + (move-marker org-link-precise-target-marker + (org-element-begin heading)) + (cons (if custom-id (concat "#" custom-id) + (org-link-heading-search-string)) + (org-link--normalize-string + (org-get-heading t t t t))))))) + + ;; Not in an org-mode buffer, no region + (t + (move-marker org-link-precise-target-marker + (line-beginning-position)) + (cons (org-link--normalize-string (org-current-line-string) t) nil))))) + + ;; Only use search option if there is some text. + (when (org-string-nw-p (car result)) + result))) + (defun org-link-open-as-file (path in-emacs) "Pretend PATH is a file name and open it. @@ -1404,7 +1548,7 @@ PATH is a symbol name, as a string." ((and (pred boundp) variable) (describe-variable variable)) (name (user-error "Unknown function or variable: %s" name)))) -(defun org-link--store-help () +(defun org-link--store-help (&optional _interactive?) "Store \"help\" type link." (when (eq major-mode 'help-mode) (let ((symbol @@ -1539,7 +1683,12 @@ prefix ARG forces storing a link for each line in the active region. Assume the function is called interactively if INTERACTIVE? is -non-nil." +non-nil. + +In Org buffers, an additional \"human-readable\" simple file link +is stored as an alternative to persistent org-id or other links, +if at a heading with a CUSTOM_ID property or an element with a +NAME." (interactive "P\np") (org-load-modules-maybe) (if (and (equal arg '(64)) (org-region-active-p)) @@ -1554,36 +1703,19 @@ non-nil." (move-beginning-of-line 2) (set-mark (point))))) (setq org-store-link-plist nil) - (let (link cpltxt desc search custom-id agenda-link) ;; description + ;; Negate `org-context-in-file-links' when given a single universal arg. + (let ((org-link-context-for-files (org-xor org-link-context-for-files + (equal arg '(4)))) + link cpltxt desc search agenda-link) ;; description (cond ;; Store a link using an external link type, if any function is - ;; available. If more than one can generate a link from current - ;; location, ask which one to use. + ;; available, unless external link types are skipped for this + ;; call using two universal args. If more than one function + ;; can generate a link from current location, ask the user + ;; which one to use. ((and (not (equal arg '(16))) - (let ((results-alist nil)) - (dolist (f (org-store-link-functions)) - (when (funcall f) - ;; XXX: return value is not link's plist, so we - ;; store the new value before it is modified. It - ;; would be cleaner to ask store link functions to - ;; return the plist instead. - (push (cons f (copy-sequence org-store-link-plist)) - results-alist))) - (pcase results-alist - (`nil nil) - (`((,_ . ,_)) t) ;single choice: nothing to do - (`((,name . ,_) . ,_) - ;; Reinstate link plist associated to the chosen - ;; function. - (apply #'org-link-store-props - (cdr (assoc-string - (completing-read - (format "Store link with (default %s): " name) - (mapcar #'car results-alist) - nil t nil nil (symbol-name name)) - results-alist))) - t)))) - (setq link (plist-get org-store-link-plist :link)) + (org-link--try-link-store-functions interactive?)) + (setq link (plist-get org-store-link-plist :link)) ;; If store function actually set `:description' property, use ;; it, even if it is nil. Otherwise, fallback to nil (ask user). (setq desc (plist-get org-store-link-plist :description))) @@ -1634,6 +1766,7 @@ non-nil." (org-with-point-at m (setq agenda-link (org-store-link nil interactive?)))))) + ;; Calendar mode ((eq major-mode 'calendar-mode) (let ((cd (calendar-cursor-to-date))) (setq link @@ -1642,6 +1775,7 @@ non-nil." (org-encode-time 0 0 0 (nth 1 cd) (nth 0 cd) (nth 2 cd)))) (org-link-store-props :type "calendar" :date cd))) + ;; Image mode ((eq major-mode 'image-mode) (setq cpltxt (concat "file:" (abbreviate-file-name buffer-file-name)) @@ -1659,15 +1793,22 @@ non-nil." (setq cpltxt (concat "file:" file) link cpltxt))) + ;; Try `org-create-file-search-functions`. If any are + ;; successful, create a file link to the current buffer with + ;; the provided search string. (sets `link` and `cpltxt` to + ;; the same thing; it looks like the intention originally was + ;; that cpltxt was a description, which might have been set by + ;; the search-function (removed in switch to lexical binding)). ((setq search (run-hook-with-args-until-success 'org-create-file-search-functions)) (setq link (concat "file:" (abbreviate-file-name buffer-file-name) "::" search)) (setq cpltxt (or link))) ;; description + ;; Main logic for storing built-in link types in org-mode + ;; buffers ((and (buffer-file-name (buffer-base-buffer)) (derived-mode-p 'org-mode)) (org-with-limited-levels - (setq custom-id (org-entry-get nil "CUSTOM_ID")) (cond ;; Store a link using the target at point ((org-in-regexp "[^<]<<\\([^<>]+\\)>>[^>]" 1) @@ -1681,74 +1822,21 @@ non-nil." ;; links. Maybe the case of identical target and ;; description should be handled by `org-insert-link'. cpltxt nil - desc nil - ;; Do not append #CUSTOM_ID link below. - custom-id nil)) - ((and (featurep 'org-id) - (or (eq org-id-link-to-org-use-id t) - (and interactive? - (or (eq org-id-link-to-org-use-id 'create-if-interactive) - (and (eq org-id-link-to-org-use-id - 'create-if-interactive-and-no-custom-id) - (not custom-id)))) - (and org-id-link-to-org-use-id (org-entry-get nil "ID")))) - ;; Store a link using the ID at point - (setq link (condition-case nil - (prog1 (org-id-store-link) - (setq desc (plist-get org-store-link-plist :description))) - (error - ;; Probably before first headline, link only to file - (concat "file:" - (abbreviate-file-name - (buffer-file-name (buffer-base-buffer)))))))) - (t + desc nil)) + (t ;; Just link to current headline. - (setq cpltxt (concat "file:" - (abbreviate-file-name - (buffer-file-name (buffer-base-buffer))))) - ;; Add a context search string. - (when (org-xor org-link-context-for-files (equal arg '(4))) - (let* ((element (org-element-at-point)) - (name (org-element-property :name element)) - (context - (cond - ((let ((region (org-link--context-from-region))) - (and region (org-link--normalize-string region t)))) - (name) - ((org-before-first-heading-p) - (org-link--normalize-string (org-current-line-string) t)) - (t (org-link-heading-search-string))))) - (when (org-string-nw-p context) - (setq cpltxt (format "%s::%s" cpltxt context)) - (setq desc - (or name - ;; Although description is not a search - ;; string, use `org-link--normalize-string' - ;; to prettify it (contiguous white spaces) - ;; and remove volatile contents (statistics - ;; cookies). - (and (not (org-before-first-heading-p)) - (org-link--normalize-string - (org-get-heading t t t t))) - "NONE"))))) - (setq link cpltxt))))) + (let ((here (org-link--file-link-to-here))) + (setq cpltxt (car here)) + (setq desc (cdr here))) + (setq link cpltxt))))) + ;; Buffer linked to file, but not an org-mode buffer. ((buffer-file-name (buffer-base-buffer)) ;; Just link to this file here. - (setq cpltxt (concat "file:" - (abbreviate-file-name - (buffer-file-name (buffer-base-buffer))))) - ;; Add a context search string. - (when (org-xor org-link-context-for-files (equal arg '(4))) - (let ((context (org-link--normalize-string - (or (org-link--context-from-region) - (org-current-line-string)) - t))) - ;; Only use search option if there is some text. - (when (org-string-nw-p context) - (setq cpltxt (format "%s::%s" cpltxt context)) - (setq desc "NONE")))) - (setq link cpltxt)) + (let ((here (org-link--file-link-to-here))) + (setq cpltxt (car here)) + (setq desc (cdr here))) + (setq link cpltxt)) (interactive? (user-error "No method for storing a link from this buffer")) @@ -1764,24 +1852,18 @@ non-nil." ;; Store and return the link (if (not (and interactive? link)) (or agenda-link (and link (org-link-make-string link desc))) - (dotimes (_ (if custom-id 2 1)) ; Store 2 links when CUSTOM-ID is non-nil. - (cond - ((not (member (list link desc) org-stored-links)) - (push (list link desc) org-stored-links) - (message "Stored: %s" (or desc link))) - ((equal (list link desc) (car org-stored-links)) - (message "This link has already been stored")) - (t - (setq org-stored-links - (delete (list link desc) org-stored-links)) - (push (list link desc) org-stored-links) - (message "Link moved to front: %s" (or desc link)))) - (when custom-id - (setq link (concat "file:" - (abbreviate-file-name - (buffer-file-name (buffer-base-buffer))) - "::#" custom-id)))) - (car org-stored-links))))) + (org-link--add-to-stored-links link desc) + ;; In org buffers, store an additional "human-readable" link + ;; using custom id, if available. + (when (and (buffer-file-name (buffer-base-buffer)) + (derived-mode-p 'org-mode) + (org-entry-get nil "CUSTOM_ID")) + (let ((here (org-link--file-link-to-here))) + (setq link (car here)) + (setq desc (cdr here))) + (unless (equal (list link desc) (car org-stored-links)) + (org-link--add-to-stored-links link desc))) + (car org-stored-links))))) ;;;###autoload (defun org-insert-link (&optional complete-file link-location description) diff --git a/lisp/org-id.el b/lisp/org-id.el index 8647a57cc..7200be34d 100644 --- a/lisp/org-id.el +++ b/lisp/org-id.el @@ -129,6 +129,46 @@ nil Never use an ID to make a link, instead link using a text search for (const :tag "Only use existing" use-existing) (const :tag "Do not use ID to create link" nil))) +(defcustom org-id-link-consider-parent-id nil + "Non-nil means storing a link to an Org entry considers inherited IDs. + +When this option is non-nil and `org-id-link-use-context' is +enabled, ID properties inherited from parent entries will be +considered when storing an ID link. If no ID is found in this +way, a new one may be created as normal (see +`org-id-link-to-org-use-id'). + +For example, given this org file: + +* Parent +:PROPERTIES: +:ID: abc +:END: +** Child 1 +** Child 2 + +With `org-id-link-consider-parent-id' and +`org-id-link-use-context' both enabled, storing a link with point +at \"Child 1\" will produce a link \"<id:abc::*Child 1>\". This +allows linking to uniquely-named sub-entries within a parent +entry with an ID, without requiring every sub-entry to have its +own ID." + :group 'org-link-store + :group 'org-id + :package-version '(Org . "9.7") + :type 'boolean) + +(defcustom org-id-link-use-context t + "Non-nil means enables search string context in org-id links. + +Search strings are added by `org-id-store-link' when both the +general option `org-link-context-for-files' and the org-id option +`org-id-link-use-context' are non-nil." + :group 'org-link-store + :group 'org-id + :package-version '(Org . "9.7") + :type 'boolean) + (defcustom org-id-uuid-program "uuidgen" "The uuidgen program." :group 'org-id @@ -280,15 +320,21 @@ This is useful when working with contents in a temporary buffer that will be copied back to the original.") ;;;###autoload -(defun org-id-get (&optional epom create prefix) - "Get the ID property of the entry at EPOM. -EPOM is an element, marker, or buffer position. -If EPOM is nil, refer to the entry at point. -If the entry does not have an ID, the function returns nil. -However, when CREATE is non-nil, create an ID if none is present already. -PREFIX will be passed through to `org-id-new'. -In any case, the ID of the entry is returned." - (let ((id (org-entry-get epom "ID"))) +(defun org-id-get (&optional epom create prefix inherit) + "Get the ID of the entry at EPOM. + +EPOM is an element, marker, or buffer position. If EPOM is nil, +refer to the entry at point. + +If INHERIT is non-nil, ID properties inherited from parent +entries are considered. Otherwise, only ID properties on the +entry itself are considered. + +When CREATE is nil, return the ID of the entry if found, +otherwise nil. When CREATE is non-nil, create an ID if none has +been found, and return the new ID. PREFIX will be passed through +to `org-id-new'." + (let ((id (org-entry-get epom "ID" (and inherit t)))) (cond ((and id (stringp id) (string-match "\\S-" id)) id) @@ -703,18 +749,56 @@ optional argument MARKERP, return the position as a new marker." ;; Calling the following function is hard-coded into `org-store-link', ;; so we do have to add it to `org-store-link-functions'. +(defun org-id--get-id-to-store-link (&optional create) + "Get or create the relevant ID for storing a link. + +Optional argument CREATE is passed to `org-id-get'. + +Inherited IDs are only considered when +`org-id-link-consider-parent-id', `org-id-link-use-context' and +`org-link-context-for-files' are all enabled, since inherited IDs +are confusing without the additional search string context. + +Note that this function resets the +`org-entry-property-inherited-from' marker: it will either point +to nil (if the id was not inherited) or to the point it was +inherited from." + (let* ((inherit-id (and org-id-link-consider-parent-id + org-id-link-use-context + org-link-context-for-files))) + (move-marker org-entry-property-inherited-from nil) + (org-id-get nil create nil inherit-id))) + ;;;###autoload (defun org-id-store-link () "Store a link to the current entry, using its ID. -If before first heading store first title-keyword as description -or filename if no title." +The link description is based on the heading, or if before the +first heading, the title keyword if available, or else the +filename. + +When `org-link-context-for-files' and `org-id-link-use-context' +are non-nil, add a search string to the link. The link +description is then based on the search string target. + +When in addition `org-id-link-consider-parent-id' is non-nil, the +ID can be inherited from a parent entry, with the search string +used to still link to the current location." (interactive) - (when (and (buffer-file-name (buffer-base-buffer)) (derived-mode-p 'org-mode)) - (let* ((link (concat "id:" (org-id-get-create))) + (when (and (buffer-file-name (buffer-base-buffer)) + (derived-mode-p 'org-mode)) + ;; Get the precise target first, in case looking for an id causes + ;; a properties drawer to be added at the current location. + (let* ((precise-target (and org-link-context-for-files + org-id-link-use-context + (org-link-precise-link-target))) + (link (concat "id:" (org-id--get-id-to-store-link 'create))) + (id-location (or (and org-entry-property-inherited-from + (marker-position org-entry-property-inherited-from)) + (save-excursion (org-back-to-heading-or-point-min t) (point)))) (case-fold-search nil) (desc (save-excursion - (org-back-to-heading-or-point-min t) + (goto-char id-location) (cond ((org-before-first-heading-p) (let ((keywords (org-collect-keywords '("TITLE")))) (if keywords @@ -726,14 +810,61 @@ or filename if no title." (match-string 4) (match-string 0))) (t link))))) + ;; Precise targets should be after id-location to avoid + ;; duplicating the current headline as a search string + (when (and precise-target + org-link-precise-target-marker + (> (marker-position org-link-precise-target-marker) + id-location)) + (setq link (concat link "::" (car precise-target))) + (setq desc (cdr precise-target))) (org-link-store-props :link link :description desc :type "id") link))) -(defun org-id-open (id _) - "Go to the entry with id ID." - (org-mark-ring-push) - (let ((m (org-id-find id 'marker)) - cmd) +;;;###autoload +(defun org-id-store-link-maybe (&optional interactive?) + "Store a link to the current entry using its ID if enabled. + +The value of `org-id-link-to-org-use-id' determines whether an ID +link should be stored, using `org-id-store-link'. + +Assume the function is called interactively if INTERACTIVE? is +non-nil." + (when (and (buffer-file-name (buffer-base-buffer)) + (derived-mode-p 'org-mode) + (or (eq org-id-link-to-org-use-id t) + (and interactive? + (or (eq org-id-link-to-org-use-id 'create-if-interactive) + (and (eq org-id-link-to-org-use-id + 'create-if-interactive-and-no-custom-id) + (not (org-entry-get nil "CUSTOM_ID"))))) + ;; 'use-existing + (and org-id-link-to-org-use-id + (org-id--get-id-to-store-link)))) + (org-id-store-link))) + +(defun org-id-open (link _) + "Go to the entry indicated by id link LINK. + +The link can include a search string after \"::\", which is +passed to `org-link-search'. + +For backwards compatibility with IDs that contain \"::\", if no +match is found for the ID, the full link string including \"::\" +will be tried as an ID." + (let* ((option (and (string-match "::\\(.*\\)\\'" link) + (match-string 1 link))) + (id (if (not option) link + (substring link 0 (match-beginning 0)))) + m cmd) + (org-mark-ring-push) + (setq m (org-id-find id 'marker)) + (when (and (not m) option) + ;; Backwards compatibility: if id is not found, try treating + ;; whole link as an id. + (setq m (org-id-find link 'marker)) + (when m + (setq option nil))) (unless m (error "Cannot find entry with ID \"%s\"" id)) ;; Use a buffer-switching command in analogy to finding files @@ -750,9 +881,16 @@ or filename if no title." (funcall cmd (marker-buffer m))) (goto-char m) (move-marker m nil) + (when option + (save-restriction + (unless (org-before-first-heading-p) + (org-narrow-to-subtree)) + (org-link-search option))) (org-fold-show-context))) -(org-link-set-parameters "id" :follow #'org-id-open) +(org-link-set-parameters "id" + :follow #'org-id-open + :store #'org-id-store-link-maybe) (provide 'org-id) diff --git a/lisp/org-lint.el b/lisp/org-lint.el index 4d2a55d15..b23afcca3 100644 --- a/lisp/org-lint.el +++ b/lisp/org-lint.el @@ -65,6 +65,7 @@ ;; - special properties in properties drawers, ;; - obsolete syntax for properties drawers, ;; - invalid duration in EFFORT property, +;; - invalid ID property with a double colon, ;; - missing definition for footnote references, ;; - missing reference for footnote definitions, ;; - non-footnote definitions in footnote section, @@ -686,6 +687,16 @@ Use :header-args: instead" (list (org-element-begin p) (format "Invalid effort duration format: %S" value)))))))) +(defun org-lint-invalid-id-property (ast) + (org-element-map ast 'node-property + (lambda (p) + (when (equal "ID" (org-element-property :key p)) + (let ((value (org-element-property :value p))) + (and (org-string-nw-p value) + (string-match-p "::" value) + (list (org-element-begin p) + (format "IDs should not include \"::\": %S" value)))))))) + (defun org-lint-link-to-local-file (ast) (org-element-map ast 'link (lambda (l) @@ -1684,6 +1695,11 @@ AST is the buffer parse tree." #'org-lint-invalid-effort-property :categories '(properties)) +(org-lint-add-checker 'invalid-id-property + "Report search string delimiter \"::\" in ID property" + #'org-lint-invalid-id-property + :categories '(properties)) + (org-lint-add-checker 'undefined-footnote-reference "Report missing definition for footnote references" #'org-lint-undefined-footnote-reference diff --git a/testing/lisp/test-ol.el b/testing/lisp/test-ol.el index e0cec0854..4be6b3055 100644 --- a/testing/lisp/test-ol.el +++ b/testing/lisp/test-ol.el @@ -381,6 +381,136 @@ See https://github.com/yantar92/org/issues/4." (equal (format "[[file:%s::*foo bar][foo bar]]" file file) (org-store-link nil))))))) +(ert-deftest test-org-link/precise-link-target () + "Test `org-link-precise-link-target` specifications." + (org-test-with-temp-text "* H1<point>\n* H2\n" + (should + (equal '("*H1" . "H1") + (org-link-precise-link-target))) + (should + (equal 1 (marker-position org-link-precise-target-marker)))) + (org-test-with-temp-text "* H1\n#+name: foo<point>\n#+begin_example\nhi\n#+end_example\n" + (should + (equal '("foo" . "foo") + (org-link-precise-link-target))) + (should + (equal 6 (marker-position org-link-precise-target-marker)))) + (org-test-with-temp-text "\nText<point>\n* H1\n" + (should + (equal '("Text" . nil) + (org-link-precise-link-target))) + (should + (equal 2 (marker-position org-link-precise-target-marker)))) + (org-test-with-temp-text "\n<point>\n* H1\n" + (should + (equal nil (org-link-precise-link-target))) + (should + (equal 2 (marker-position org-link-precise-target-marker))))) + +(defmacro test-ol-stored-link-with-text (text &rest body) + "Return :link and :description from link stored in body." + (declare (indent 1)) + `(let (org-store-link-plist) + (org-test-with-temp-text-in-file ,text + ,@body + (list (plist-get org-store-link-plist :link) + (plist-get org-store-link-plist :description))))) + +(ert-deftest test-org-link/id-store-link () + "Test `org-id-store-link' specifications." + (let ((org-id-link-to-org-use-id nil)) + (should + (equal '(nil nil) + (test-ol-stored-link-with-text "* H1\n:PROPERTIES:\n:ID: abc\n:END:\n" + (org-id-store-link-maybe t))))) + ;; On a headline, link to that headline's ID. Use heading as the + ;; description of the link. + (let ((org-id-link-to-org-use-id t)) + (should + (equal '("id:abc" "H1") + (test-ol-stored-link-with-text "* H1\n:PROPERTIES:\n:ID: abc\n:END:\n" + (org-id-store-link-maybe t))))) + ;; Remove TODO keywords etc from description of the link. + (let ((org-id-link-to-org-use-id t)) + (should + (equal '("id:abc" "H1") + (test-ol-stored-link-with-text "* TODO [#A] H1 :tag:\n:PROPERTIES:\n:ID: abc\n:END:\n" + (org-id-store-link-maybe t))))) + ;; create-if-interactive + (let ((org-id-link-to-org-use-id 'create-if-interactive)) + (should + (equal '("id:abc" "H1") + (cl-letf (((symbol-function 'org-id-new) + (lambda (&rest _rest) "abc"))) + (test-ol-stored-link-with-text "* H1\n" + (org-id-store-link-maybe t))))) + (should + (equal '(nil nil) + (test-ol-stored-link-with-text "* H1\n" + (org-id-store-link-maybe nil))))) + ;; create-if-interactive-and-no-custom-id + (let ((org-id-link-to-org-use-id 'create-if-interactive-and-no-custom-id)) + (should + (equal '("id:abc" "H1") + (cl-letf (((symbol-function 'org-id-new) + (lambda (&rest _rest) "abc"))) + (test-ol-stored-link-with-text "* H1\n" + (org-id-store-link-maybe t))))) + (should + (equal '(nil nil) + (test-ol-stored-link-with-text "* H1\n:PROPERTIES:\n:CUSTOM_ID: xyz\n:END:\n" + (org-id-store-link-maybe t)))) + (should + (equal '(nil nil) + (test-ol-stored-link-with-text "* H1\n" + (org-id-store-link-maybe nil))))) + ;; use-context should have no effect when on the headline with an id + (let ((org-id-link-to-org-use-id t) + (org-id-link-use-context t)) + (should + (equal '("id:abc" "H2") + (test-ol-stored-link-with-text "* H1\n** H2<point>\n:PROPERTIES:\n:ID: abc\n:END:\n" + ;; simulate previously getting an inherited value + (move-marker org-entry-property-inherited-from 1) + (org-id-store-link-maybe t)))))) + +(ert-deftest test-org-link/id-store-link-using-parent () + "Test `org-id-store-link' specifications with `org-id-link-consider-parent-id` set." + ;; when using context to still find specific heading + (let ((org-id-link-to-org-use-id t) + (org-id-link-consider-parent-id t) + (org-id-link-use-context t)) + (should + (equal '("id:abc::*H2" "H2") + (test-ol-stored-link-with-text "* H1\n:PROPERTIES:\n:ID: abc\n:END:\n** H2\n<point>" + (org-id-store-link)))) + (should + (equal '("id:abc::name" "name") + (test-ol-stored-link-with-text "* H1\n:PROPERTIES:\n:ID: abc\n:END:\n\n#+name: name\n<point>#+begin_example\nhi\n#+end_example\n" + (org-id-store-link)))) + (should + (equal '("id:abc" "H1") + (test-ol-stored-link-with-text "* H1<point>\n:PROPERTIES:\n:ID: abc\n:END:\n** H2\n" + (org-id-store-link)))) + ;; should not use newly added ids as search string, e.g. in an empty file + (should + (let (name result) + (setq result + (cl-letf (((symbol-function 'org-id-new) + (lambda (&rest _rest) "abc"))) + (test-ol-stored-link-with-text "<point>" + (setq name (buffer-name)) + (org-id-store-link)))) + (equal `("id:abc" ,name) result)))) + ;; should not find targets in the next section + (let ((org-id-link-to-org-use-id 'use-existing) + (org-id-link-consider-parent-id t) + (org-id-link-use-context t)) + (should + (equal '(nil nil) + (test-ol-stored-link-with-text "* H1\n:PROPERTIES:\n:ID: abc\n:END:\n* H2\n** <point>Target\n" + (org-id-store-link-maybe t)))))) + \f ;;; Radio Targets -- 2.37.1 (Apple Git-137.1) ^ permalink raw reply related [flat|nested] 48+ messages in thread
* Re: [PATCH v2] org-id: allow using parent's existing id in links to headlines 2024-01-31 18:11 ` Rick Lupton @ 2024-02-01 12:13 ` Ihor Radchenko 2024-02-01 16:37 ` Rick Lupton 2024-02-03 13:10 ` Ihor Radchenko 1 sibling, 1 reply; 48+ messages in thread From: Ihor Radchenko @ 2024-02-01 12:13 UTC (permalink / raw) To: Rick Lupton; +Cc: Y. E. The patch does not apply onto the latest main. May you please update it? ^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH v2] org-id: allow using parent's existing id in links to headlines 2024-02-01 12:13 ` Ihor Radchenko @ 2024-02-01 16:37 ` Rick Lupton 0 siblings, 0 replies; 48+ messages in thread From: Rick Lupton @ 2024-02-01 16:37 UTC (permalink / raw) To: Ihor Radchenko; +Cc: Y. E. [-- Attachment #1: Type: text/plain, Size: 366 bytes --] On Thu, 1 Feb 2024, at 12:13 PM, Ihor Radchenko wrote: > The patch does not apply onto the latest main. May you please update it? I have rebased onto the latest main. It changes quickly! (there were no conflicts during the rebase, which I'd have thought would mean the patches shouldn't be a problem to apply? It the problem was something else, please let me know) [-- Attachment #2: 0001-lisp-org.el-org-insert-heading-allow-specifying-head.patch --] [-- Type: application/octet-stream, Size: 5082 bytes --] From 036659c63341f1d895825e02c90883eb8f2f856e Mon Sep 17 00:00:00 2001 From: Rick Lupton <mail@ricklupton.name> Date: Wed, 3 Jan 2024 22:37:38 +0000 Subject: [PATCH 1/2] lisp/org.el (org-insert-heading): allow specifying heading level * lisp/org.el (org-insert-heading): Change optional argument TOP to LEVEL, accepting a number to force a specific heading level. * testing/lisp/test-org.el (test-org/insert-heading): Add tests * etc/ORG-NEWS: Document changes --- etc/ORG-NEWS | 6 ++++++ lisp/org.el | 21 ++++++++++++++------- testing/lisp/test-org.el | 26 ++++++++++++++++++++++++-- 3 files changed, 44 insertions(+), 9 deletions(-) diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS index ad5a0684f..adda55532 100644 --- a/etc/ORG-NEWS +++ b/etc/ORG-NEWS @@ -991,6 +991,12 @@ as the function can also act on objects. *** ~org-export-get-parent-element~ is renamed to ~org-element-parent-element~ and moved to =lisp/org-element.el= +*** ~org-insert-heading~ optional argument =TOP= is now =LEVEL= + +A numeric value forces a heading at that level to be inserted. For +backwards compatibility, non-numeric non-nil values insert level 1 +headings as before. + ** Miscellaneous *** =org-crypt.el= now applies initial visibility settings to decrypted entries diff --git a/lisp/org.el b/lisp/org.el index 2e53d98d3..96d8fad3a 100644 --- a/lisp/org.el +++ b/lisp/org.el @@ -6360,7 +6360,7 @@ headline instead of current one." (`(heading . ,value) value) (_ nil))) -(defun org-insert-heading (&optional arg invisible-ok top) +(defun org-insert-heading (&optional arg invisible-ok level) "Insert a new heading or an item with the same depth at point. If point is at the beginning of a heading, insert a new heading @@ -6389,12 +6389,19 @@ When INVISIBLE-OK is set, stop at invisible headlines when going back. This is important for non-interactive uses of the command. -When optional argument TOP is non-nil, insert a level 1 heading, -unconditionally." +When optional argument LEVEL is a number, insert a heading at +that level. For backwards compatibility, when LEVEL is non-nil +but not a number, insert a level-1 heading." (interactive "P") (let* ((blank? (org--blank-before-heading-p (equal arg '(16)))) - (level (org-current-level)) - (stars (make-string (if (and level (not top)) level 1) ?*))) + (current-level (org-current-level)) + (num-stars (or + ;; Backwards compat: if LEVEL non-nil, level is 1 + (and level (if (wholenump level) level 1)) + current-level + ;; This `1' is for when before first headline + 1)) + (stars (make-string num-stars ?*))) (cond ((or org-insert-heading-respect-content (member arg '((4) (16))) @@ -6403,7 +6410,7 @@ unconditionally." ;; Position point at the location of insertion. Make sure we ;; end up on a visible headline if INVISIBLE-OK is nil. (org-with-limited-levels - (if (not level) (outline-next-heading) ;before first headline + (if (not current-level) (outline-next-heading) ;before first headline (org-back-to-heading invisible-ok) (when (equal arg '(16)) (org-up-heading-safe)) (org-end-of-subtree invisible-ok 'to-heading))) @@ -6416,7 +6423,7 @@ unconditionally." (org-before-first-heading-p))) (insert "\n") (backward-char)) - (when (and (not level) (not (eobp)) (not (bobp))) + (when (and (not current-level) (not (eobp)) (not (bobp))) (when (org-at-heading-p) (insert "\n")) (backward-char)) (unless (and blank? (org-previous-line-empty-p)) diff --git a/testing/lisp/test-org.el b/testing/lisp/test-org.el index 822cbc67a..fc50dc787 100644 --- a/testing/lisp/test-org.el +++ b/testing/lisp/test-org.el @@ -1980,8 +1980,30 @@ CLOCK: [2022-09-17 sam. 11:00]--[2022-09-17 sam. 11:46] => 0:46" (let ((org-insert-heading-respect-content nil)) (org-insert-heading '(16))) (buffer-string)))) - ;; When optional TOP-LEVEL argument is non-nil, always insert - ;; a level 1 heading. + ;; When optional LEVEL argument is a number, insert a heading at + ;; that level. + (should + (equal "* H1\n** H2\n* " + (org-test-with-temp-text "* H1\n** H2<point>" + (org-insert-heading nil nil 1) + (buffer-string)))) + (should + (equal "* H1\n** H2\n** " + (org-test-with-temp-text "* H1\n** H2<point>" + (org-insert-heading nil nil 2) + (buffer-string)))) + (should + (equal "* H1\n** H2\n*** " + (org-test-with-temp-text "* H1\n** H2<point>" + (org-insert-heading nil nil 3) + (buffer-string)))) + (should + (equal "* H1\n- item\n* " + (org-test-with-temp-text "* H1\n- item<point>" + (org-insert-heading nil nil 1) + (buffer-string)))) + ;; When optional LEVEL argument is non-nil, always insert a level 1 + ;; heading. (should (equal "* H1\n** H2\n* " (org-test-with-temp-text "* H1\n** H2<point>" -- 2.37.1 (Apple Git-137.1) [-- Attachment #3: 0002-org-id.el-Extend-links-with-search-strings-inherit-p.patch --] [-- Type: application/octet-stream, Size: 57720 bytes --] From e630dc8071808893b44bb8dbe5da8f95517118a3 Mon Sep 17 00:00:00 2001 From: Rick Lupton <mail@ricklupton.name> Date: Sun, 19 Nov 2023 14:52:05 +0000 Subject: [PATCH 2/2] org-id.el: Extend links with search strings, inherit parent IDs * lisp/ol.el (org-store-link): Refactor org-id links to use standard `org-store-link-functions'. (org-link-search): Create new headings at appropriate level. (org-link-precise-link-target): New function extracting logic to identify a precise link target, e.g. a heading, named object, or text search. (org-link-try-link-store-functions): Extract logic to call external link store functions. Pass them a new `interactive?' argument. * lisp/ol-bbdb.el (org-bbdb-store-link): * lisp/ol-bibtex.el (org-bibtex-store-link): * lisp/ol-docview.el (org-docview-store-link): * lisp/ol-eshell.el (org-eshell-store-link): * lisp/ol-eww.el (org-eww-store-link): * lisp/ol-gnus.el (org-gnus-store-link): * lisp/ol-info.el (org-info-store-link): * lisp/ol-irc.el (org-irc-store-link): * lisp/ol-man.el (org-man-store-link): * lisp/ol-mhe.el (org-mhe-store-link): * lisp/ol-rmail.el (org-rmail-store-link): Accept optional arg. * lisp/org-id.el (org-id-link-consider-parent-id): New option to allow a parent heading with an id to be considered as a link target. (org-id-link-use-context): New option to add context to org-id links. (org-id-get): Add optional `inherit' argument which considers parents' IDs if the current entry does not have one. (org-id-store-link): Consider IDs of parent headings as link targets when current heading has no ID and `org-id-link-consider-parent-id' is set. Add a search string to the link when enabled. (org-id-store-link-maybe): Function set as :store option for custom id link property. Move logic from `org-store-link' here to determine when an org-id link should be stored using `org-id-store-link'. (org-id-open): Recognise search strings after "::" in org-id links. * lisp/org-lint.el: add checker for "::" in ID properties. * testing/lisp/test-ol.el: Add tests for `org-link-precise-link-target' and `org-id-store-link' functions, testing new options. * doc/org-manual.org: Update documentation about links. * etc/ORG-NEWS: Document changes and new options. These feature allows for more precise links when using org-id to link to org headings, without requiring every single headline to have an id. Link: https://list.orgmode.org/118435e8-0b20-46fd-af6a-88de8e19fac6@app.fastmail.com/ --- doc/org-manual.org | 133 ++++++++++------- etc/ORG-NEWS | 64 +++++++++ lisp/ol-bbdb.el | 2 +- lisp/ol-bibtex.el | 2 +- lisp/ol-docview.el | 2 +- lisp/ol-eshell.el | 2 +- lisp/ol-eww.el | 2 +- lisp/ol-gnus.el | 2 +- lisp/ol-info.el | 2 +- lisp/ol-irc.el | 2 +- lisp/ol-man.el | 2 +- lisp/ol-mhe.el | 2 +- lisp/ol-rmail.el | 2 +- lisp/ol.el | 312 +++++++++++++++++++++++++--------------- lisp/org-id.el | 178 ++++++++++++++++++++--- lisp/org-lint.el | 16 +++ testing/lisp/test-ol.el | 130 +++++++++++++++++ 17 files changed, 658 insertions(+), 197 deletions(-) diff --git a/doc/org-manual.org b/doc/org-manual.org index 64b4fd0f5..4596b401b 100644 --- a/doc/org-manual.org +++ b/doc/org-manual.org @@ -3297,10 +3297,6 @@ Here is the full set of built-in link types: File links. File name may be remote, absolute, or relative. - Additionally, you can specify a line number, or a text search. - In Org files, you may link to a headline name, a custom ID, or a - code reference instead. - As a special case, "file" prefix may be omitted if the file name is complete, e.g., it starts with =./=, or =/=. @@ -3364,44 +3360,50 @@ Here is the full set of built-in link types: Execute a shell command upon activation. + +For =file:= and =id:= links, you can additionally specify a line +number, or a text search string, separated by =::=. In Org files, you +may link to a headline name, a custom ID, or a code reference instead. + The following table illustrates the link types above, along with their options: -| Link Type | Example | -|------------+----------------------------------------------------------| -| http | =http://staff.science.uva.nl/c.dominik/= | -| https | =https://orgmode.org/= | -| doi | =doi:10.1000/182= | -| file | =file:/home/dominik/images/jupiter.jpg= | -| | =/home/dominik/images/jupiter.jpg= (same as above) | -| | =file:papers/last.pdf= | -| | =./papers/last.pdf= (same as above) | -| | =file:/ssh:me@some.where:papers/last.pdf= (remote) | -| | =/ssh:me@some.where:papers/last.pdf= (same as above) | -| | =file:sometextfile::NNN= (jump to line number) | -| | =file:projects.org= | -| | =file:projects.org::some words= (text search)[fn:12] | -| | =file:projects.org::*task title= (headline search) | -| | =file:projects.org::#custom-id= (headline search) | -| attachment | =attachment:projects.org= | -| | =attachment:projects.org::some words= (text search) | -| docview | =docview:papers/last.pdf::NNN= | -| id | =id:B7423F4D-2E8A-471B-8810-C40F074717E9= | -| news | =news:comp.emacs= | -| mailto | =mailto:adent@galaxy.net= | -| mhe | =mhe:folder= (folder link) | -| | =mhe:folder#id= (message link) | -| rmail | =rmail:folder= (folder link) | -| | =rmail:folder#id= (message link) | -| gnus | =gnus:group= (group link) | -| | =gnus:group#id= (article link) | -| bbdb | =bbdb:R.*Stallman= (record with regexp) | -| irc | =irc:/irc.com/#emacs/bob= | -| help | =help:org-store-link= | -| info | =info:org#External links= | -| shell | =shell:ls *.org= | -| elisp | =elisp:(find-file "Elisp.org")= (Elisp form to evaluate) | -| | =elisp:org-agenda= (interactive Elisp command) | +| Link Type | Example | +|------------+--------------------------------------------------------------------| +| http | =http://staff.science.uva.nl/c.dominik/= | +| https | =https://orgmode.org/= | +| doi | =doi:10.1000/182= | +| file | =file:/home/dominik/images/jupiter.jpg= | +| | =/home/dominik/images/jupiter.jpg= (same as above) | +| | =file:papers/last.pdf= | +| | =./papers/last.pdf= (same as above) | +| | =file:/ssh:me@some.where:papers/last.pdf= (remote) | +| | =/ssh:me@some.where:papers/last.pdf= (same as above) | +| | =file:sometextfile::NNN= (jump to line number) | +| | =file:projects.org= | +| | =file:projects.org::some words= (text search)[fn:12] | +| | =file:projects.org::*task title= (headline search) | +| | =file:projects.org::#custom-id= (headline search) | +| attachment | =attachment:projects.org= | +| | =attachment:projects.org::some words= (text search) | +| docview | =docview:papers/last.pdf::NNN= | +| id | =id:B7423F4D-2E8A-471B-8810-C40F074717E9= | +| | =id:B7423F4D-2E8A-471B-8810-C40F074717E9::*task= (headline search) | +| news | =news:comp.emacs= | +| mailto | =mailto:adent@galaxy.net= | +| mhe | =mhe:folder= (folder link) | +| | =mhe:folder#id= (message link) | +| rmail | =rmail:folder= (folder link) | +| | =rmail:folder#id= (message link) | +| gnus | =gnus:group= (group link) | +| | =gnus:group#id= (article link) | +| bbdb | =bbdb:R.*Stallman= (record with regexp) | +| irc | =irc:/irc.com/#emacs/bob= | +| help | =help:org-store-link= | +| info | =info:org#External links= | +| shell | =shell:ls *.org= | +| elisp | =elisp:(find-file "Elisp.org")= (Elisp form to evaluate) | +| | =elisp:org-agenda= (interactive Elisp command) | #+cindex: VM links #+cindex: Wanderlust links @@ -3462,8 +3464,9 @@ current buffer: - /Org mode buffers/ :: For Org files, if there is a =<<target>>= at point, the link points - to the target. Otherwise it points to the current headline, which - is also the description. + to the target. If there is a named block (using =#+name:=) at + point, the link points to that name. Otherwise it points to the + current headline, which is also the description. #+vindex: org-id-link-to-org-use-id #+cindex: @samp{CUSTOM_ID}, property @@ -3481,6 +3484,30 @@ current buffer: timestamp, depending on ~org-id-method~. Later, when inserting the link, you need to decide which one to use. + #+vindex: org-id-link-consider-parent-id + #+vindex: org-id-link-use-context + When ~org-id-link-consider-parent-id~ is ~t~ (and + ~org-link-context-for-files~ and ~org-id-link-use-context~ are both + enabled), parent =ID= properties are considered. This allows + linking to specific targets, named blocks, or headlines (which may + not have a globally unique =ID= themselves) within the context of a + parent headline or file which does. + + For example, given this org file with those variables set: + + #+begin_src org + ,* Parent + :PROPERTIES: + :ID: abc + :END: + ,** Child 1 + ,** Child 2 + #+end_src + + Storing a link with point at "Child 1" will produce a link + =<id:abc::*Child 1>=, which precisely links to the "Child 1" + headline even though it does not have its own ID. + - /Email/News clients: VM, Rmail, Wanderlust, MH-E, Gnus/ :: #+vindex: org-link-email-description-format @@ -3760,7 +3787,9 @@ the link completion function like this: :ALT_TITLE: Search Options :END: #+cindex: search option in file links +#+cindex: search option in id links #+cindex: file links, searching +#+cindex: id links, searching #+cindex: attachment links, searching File links can contain additional information to make Emacs jump to a @@ -3772,8 +3801,8 @@ example, when the command ~org-store-link~ creates a link (see line as a search string that can be used to find this line back later when following the link with {{{kbd(C-c C-o)}}}. -Note that all search options apply for Attachment links in the same -way that they apply for File links. +Note that all search options apply for Attachment and ID links in the +same way that they apply for File links. Here is the syntax of the different ways to attach a search to a file link, together with explanations for each: @@ -21360,7 +21389,7 @@ The following =ol-man.el= file implements it PATH should be a topic that can be thrown at the man command." (funcall org-man-command path)) -(defun org-man-store-link () +(defun org-man-store-link (&optional _interactive?) "Store a link to a man page." (when (memq major-mode '(Man-mode woman-mode)) ;; This is a man page, we do make this link. @@ -21420,13 +21449,15 @@ A review of =ol-man.el=: For example, ~org-man-store-link~ is responsible for storing a link when ~org-store-link~ (see [[*Handling Links]]) is called from a buffer - displaying a man page. It first checks if the major mode is - appropriate. If check fails, the function returns ~nil~, which - means it isn't responsible for creating a link to the current - buffer. Otherwise the function makes a link string by combining - the =man:= prefix with the man topic. It also provides a default - description. The function ~org-insert-link~ can insert it back - into an Org buffer later on. + displaying a man page. It is passed an argument ~interactive?~ + which this function does not use, but other store functions use to + behave differently when a link is stored interactively by the user. + It first checks if the major mode is appropriate. If check fails, + the function returns ~nil~, which means it isn't responsible for + creating a link to the current buffer. Otherwise the function + makes a link string by combining the =man:= prefix with the man + topic. It also provides a default description. The function + ~org-insert-link~ can insert it back into an Org buffer later on. ** Adding Export Backends :PROPERTIES: diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS index adda55532..6a3d49f6c 100644 --- a/etc/ORG-NEWS +++ b/etc/ORG-NEWS @@ -373,6 +373,14 @@ timestamp object. Possible values: ~timerange~, ~daterange~, ~nil~. ~org-element-timestamp-interpreter~ takes into account this property and returns an appropriate timestamp string. +**** =org-link= store functions are passed an ~interactive?~ argument + +The ~:store:~ functions set for link types using +~org-link-set-parameters~ are now passed an ~interactive?~ argument, +indicating whether ~org-store-link~ was called interactively. + +Existing store functions will continue to work. + *** ~org-priority=show~ command no longer adjusts for scheduled/deadline In agenda views, ~org-priority=show~ command previously displayed the @@ -451,6 +459,27 @@ The change is breaking when ~org-use-property-inheritance~ is set to ~t~. *** ~org-babel-lilypond-compile-lilyfile~ ignores optional second argument The =TEST= parameter is better served by Emacs debugging tools. + +*** ~org-id-store-link~ now adds search strings for precise link targets + +This new behaviour can be disabled generally by setting +~org-id-link-use-context~ to ~nil~, or the setting can be toggled for +a single call to ~org-store-link~ with a universal argument. + +When using this feature, IDs should not include =::=, which is used in +links to indicate the start of the search string. For backwards +compability, existing IDs including =::= will still be matched (but +cannot be used together with precise link targets). An org-lint +checker has been added to warn about this. + +*** ~org-store-link~ behaviour storing additional =CUSTOM_ID= links has changed + +As well as an =id:= link, ~org-store-link~ stores an additional "human +readable" link using a node's =CUSTOM_ID= property, if available. +This behaviour has been expanded to store an additional =CUSTOM_ID= +link when storing any type of external link type in an Org file, not +just =id:= links. + ** New and changed options *** New custom setting ~org-icalendar-ttl~ for the ~ox-icalendar~ backend @@ -709,6 +738,35 @@ This option starts the agenda to automatically include archives, propagating the value for this variable to ~org-agenda-archives-mode~. For acceptable values and their meaning, see the value of that variable. +*** New option ~org-id-link-consider-parent-id~ to allow =id:= links to parent headlines + +For =id:= links, when this option is enabled, ~org-store-link~ will +look for ids from parent/ancestor headlines, if the current headline +does not have an id. + +Combined with the new ability for =id:= links to use search strings +for precise link targets (when =org-id-link-use-context= is =t=, which +is the default), this allows linking to specific headlines without +requiring every headline to have an id property, as long as the +headline is unique within a subtree that does have an id property. + +For example, given this org file: + +#+begin_src org +,* Parent +:PROPERTIES: +:ID: abc +:END: +,** Child 1 +,** Child 2 +#+end_src + +Storing a link with point at "Child 1" will produce a link +=<id:abc::*Child 1>=, which precisely links to the "Child 1" headline +even though it does not have its own ID. By giving files top-level id +properties, links to headlines in the file can also be made more +robust by using the file id instead of the file path. + ** New features *** =ob-plantuml.el=: Support tikz file format output @@ -997,6 +1055,12 @@ A numeric value forces a heading at that level to be inserted. For backwards compatibility, non-numeric non-nil values insert level 1 headings as before. +*** New optional argument for ~org-id-get~ + +New optional argument =INHERIT= means inherited ID properties from +parent entries are considered when getting an entry's ID (see +~org-id-link-consider-parent-id~ option). + ** Miscellaneous *** =org-crypt.el= now applies initial visibility settings to decrypted entries diff --git a/lisp/ol-bbdb.el b/lisp/ol-bbdb.el index be3924fc9..6ea060f70 100644 --- a/lisp/ol-bbdb.el +++ b/lisp/ol-bbdb.el @@ -226,7 +226,7 @@ date year)." ;;; Implementation -(defun org-bbdb-store-link () +(defun org-bbdb-store-link (&optional _interactive?) "Store a link to a BBDB database entry." (when (eq major-mode 'bbdb-mode) ;; This is BBDB, we make this link! diff --git a/lisp/ol-bibtex.el b/lisp/ol-bibtex.el index c5a950e2d..38468f32f 100644 --- a/lisp/ol-bibtex.el +++ b/lisp/ol-bibtex.el @@ -507,7 +507,7 @@ ARG, when non-nil, is a universal prefix argument. See `org-open-file' for details." (org-link-open-as-file path arg)) -(defun org-bibtex-store-link () +(defun org-bibtex-store-link (&optional _interactive?) "Store a link to a BibTeX entry." (when (eq major-mode 'bibtex-mode) (let* ((search (org-create-file-search-in-bibtex)) diff --git a/lisp/ol-docview.el b/lisp/ol-docview.el index b31f1ce5e..0907ddee1 100644 --- a/lisp/ol-docview.el +++ b/lisp/ol-docview.el @@ -83,7 +83,7 @@ (error "No such file: %s" path)) (when page (doc-view-goto-page page)))) -(defun org-docview-store-link () +(defun org-docview-store-link (&optional _interactive?) "Store a link to a docview buffer." (when (eq major-mode 'doc-view-mode) ;; This buffer is in doc-view-mode diff --git a/lisp/ol-eshell.el b/lisp/ol-eshell.el index 2c7ec6bef..595dd0ee0 100644 --- a/lisp/ol-eshell.el +++ b/lisp/ol-eshell.el @@ -60,7 +60,7 @@ followed by a colon." (insert command) (eshell-send-input))) -(defun org-eshell-store-link () +(defun org-eshell-store-link (&optional _interactive?) "Store eshell link. When opened, the link switches back to the current eshell buffer and the current working directory." diff --git a/lisp/ol-eww.el b/lisp/ol-eww.el index 40b820d2b..c13dbf339 100644 --- a/lisp/ol-eww.el +++ b/lisp/ol-eww.el @@ -62,7 +62,7 @@ "Open URL with Eww in the current buffer." (eww url)) -(defun org-eww-store-link () +(defun org-eww-store-link (&optional _interactive?) "Store a link to the url of an EWW buffer." (when (eq major-mode 'eww-mode) (org-link-store-props diff --git a/lisp/ol-gnus.el b/lisp/ol-gnus.el index e105fdb2c..b9ee8683f 100644 --- a/lisp/ol-gnus.el +++ b/lisp/ol-gnus.el @@ -123,7 +123,7 @@ If `org-store-link' was called with a prefix arg the meaning of (url-encode-url message-id)) (concat "gnus:" group "#" message-id))) -(defun org-gnus-store-link () +(defun org-gnus-store-link (&optional _interactive?) "Store a link to a Gnus folder or message." (pcase major-mode (`gnus-group-mode diff --git a/lisp/ol-info.el b/lisp/ol-info.el index 0edf9a13f..6062cab34 100644 --- a/lisp/ol-info.el +++ b/lisp/ol-info.el @@ -50,7 +50,7 @@ :insert-description #'org-info-description-as-command) ;; Implementation -(defun org-info-store-link () +(defun org-info-store-link (&optional _interactive?) "Store a link to an Info file and node." (when (eq major-mode 'Info-mode) (let ((link (concat "info:" diff --git a/lisp/ol-irc.el b/lisp/ol-irc.el index 78c4884b0..b263e52db 100644 --- a/lisp/ol-irc.el +++ b/lisp/ol-irc.el @@ -103,7 +103,7 @@ attributes that are found." parts)) ;;;###autoload -(defun org-irc-store-link () +(defun org-irc-store-link (&optional _interactive?) "Dispatch to the appropriate function to store a link to an IRC session." (cond ((eq major-mode 'erc-mode) diff --git a/lisp/ol-man.el b/lisp/ol-man.el index e3f13815e..42aacea81 100644 --- a/lisp/ol-man.el +++ b/lisp/ol-man.el @@ -82,7 +82,7 @@ matched strings in man buffer." (set-window-point window point) (set-window-start window point))))))) -(defun org-man-store-link () +(defun org-man-store-link (&optional _interactive?) "Store a link to a README file." (when (memq major-mode '(Man-mode woman-mode)) ;; This is a man page, we do make this link diff --git a/lisp/ol-mhe.el b/lisp/ol-mhe.el index 106cfedc9..a32481324 100644 --- a/lisp/ol-mhe.el +++ b/lisp/ol-mhe.el @@ -80,7 +80,7 @@ supported by MH-E." (org-link-set-parameters "mhe" :follow #'org-mhe-open :store #'org-mhe-store-link) ;; Implementation -(defun org-mhe-store-link () +(defun org-mhe-store-link (&optional _interactive?) "Store a link to an MH-E folder or message." (when (or (eq major-mode 'mh-folder-mode) (eq major-mode 'mh-show-mode)) diff --git a/lisp/ol-rmail.el b/lisp/ol-rmail.el index f6031ab52..f1f753b6f 100644 --- a/lisp/ol-rmail.el +++ b/lisp/ol-rmail.el @@ -51,7 +51,7 @@ :store #'org-rmail-store-link) ;; Implementation -(defun org-rmail-store-link () +(defun org-rmail-store-link (&optional _interactive?) "Store a link to an Rmail folder or message." (when (or (eq major-mode 'rmail-mode) (eq major-mode 'rmail-summary-mode)) diff --git a/lisp/ol.el b/lisp/ol.el index cf59c8556..7e7df468a 100644 --- a/lisp/ol.el +++ b/lisp/ol.el @@ -63,7 +63,6 @@ (declare-function org-find-property "org" (property &optional value)) (declare-function org-get-heading "org" (&optional no-tags no-todo no-priority no-comment)) (declare-function org-id-find-id-file "org-id" (id)) -(declare-function org-id-store-link "org-id" ()) (declare-function org-insert-heading "org" (&optional arg invisible-ok top)) (declare-function org-load-modules-maybe "org" (&optional force)) (declare-function org-mark-ring-push "org" (&optional pos buffer)) @@ -620,6 +619,12 @@ If it decides that it is not responsible for this link, it must return nil to indicate that Org can continue with other options like exact and fuzzy text search.") +(defvar org-link-precise-target-marker (make-marker) + "Marker pointing to the target identified for a link search string. +Each call to `org-link-precise-link-target' will set this marker +to the location where the returned target was found. If there +was no target, the marker will point nowhere.") + \f ;;; Internal Variables @@ -815,6 +820,74 @@ spec." (org-with-point-at (car region) (not (org-in-regexp org-link-any-re)))) +(defun org-link--try-link-store-functions (interactive?) + "Try storing external links, prompting if more than one is possible. + +Each function returned by `org-store-link-functions' is called in +turn. If multiple functions return non-nil, prompt for which +link should be stored. + +Argument INTERACTIVE? indicates whether `org-store-link' was +called interactively and is passed to the link store functions. + +Return t when a link has been stored in `org-link-store-props'." + (let ((results-alist nil)) + (dolist (f (org-store-link-functions)) + (when (condition-case nil + (funcall f interactive?) + ;; FIXME: The store function used (< Org 9.7) to accept + ;; no arguments; provide backward compatibility support + ;; for them. + (wrong-number-of-arguments + (funcall f))) + ;; FIXME: return value is not link's plist, so we store the + ;; new value before it is modified. It would be cleaner to + ;; ask store link functions to return the plist instead. + (push (cons f (copy-sequence org-store-link-plist)) + results-alist))) + (pcase results-alist + (`nil nil) + (`((,_ . ,_)) t) ;single choice: nothing to do + (`((,name . ,_) . ,_) + ;; Reinstate link plist associated to the chosen + ;; function. + (apply #'org-link-store-props + (cdr (assoc-string + (completing-read + (format "Store link with (default %s): " name) + (mapcar #'car results-alist) + nil t nil nil (symbol-name name)) + results-alist))) + t)))) + +(defun org-link--add-to-stored-links (link desc) + "Add LINK to `org-stored-links' with description DESC." + (cond + ((not (member (list link desc) org-stored-links)) + (push (list link desc) org-stored-links) + (message "Stored: %s" (or desc link))) + ((equal (list link desc) (car org-stored-links)) + (message "This link has already been stored")) + (t + (setq org-stored-links + (delete (list link desc) org-stored-links)) + (push (list link desc) org-stored-links) + (message "Link moved to front: %s" (or desc link))))) + +(defun org-link--file-link-to-here () + "Return as (LINK . DESC) a file link with search string to here." + (let ((link (concat "file:" + (abbreviate-file-name + (buffer-file-name (buffer-base-buffer))))) + desc) + (when org-link-context-for-files + (pcase (org-link-precise-link-target) + (`nil nil) + (`(,search-string . ,search-desc) + (setq link (format "%s::%s" link search-string)) + (setq desc search-desc)))) + (cons link desc))) + \f ;;; Public API @@ -1041,7 +1114,9 @@ LINK is escaped with backslashes for inclusion in buffer." "List of functions that are called to create and store a link. The functions are defined in the `:store' property of -`org-link-parameters'. +`org-link-parameters'. Each function should accept an argument +INTERACTIVE? which indicates whether the user has initiated +`org-store-link' interactively. Each function will be called in turn until one returns a non-nil value. Each function should check if it is responsible for @@ -1280,7 +1355,11 @@ respects buffer narrowing." (yes-or-no-p "No match - create this as a new heading? ")) (goto-char (point-max)) (unless (bolp) (newline)) - (org-insert-heading nil t t) + ;; Find appropriate level for new heading + (let ((level (save-excursion + (goto-char (point-min)) + (+ 1 (or (org-current-level) 0))))) + (org-insert-heading nil t level)) (insert s "\n") (forward-line -1)) ;; Only headlines are looked after. No need to process @@ -1332,6 +1411,71 @@ priority cookie or tag." (org-link--normalize-string (or string (org-get-heading t t t t))))) +(defun org-link-precise-link-target () + "Determine search string and description for storing a link. + +If a search string (see `org-link-search') is found, return cons +cell (SEARCH-STRING . DESC). Otherwise, return nil. + +If there is an active region, the contents (or a part of it, see +`org-link-context-for-files') is used as the search string. + +In Org buffers, if point is at a named element (such as a source +block), the name is used for the search string. If at a heading, +its CUSTOM_ID is used to form a search string of the form +\"#id\", if present, otherwise the current heading text is used +in the form \"*Heading\". + +If none of those finds a suitable search string, the current line +is used as the search string. + +The description DESC is nil (meaning the user will be prompted +for a description when inserting the link) for search strings +based on a region or the current line. For other cases, DESC is +a cleaned-up version of the name or heading at point. + +`org-link-precise-target-marker' is set to the location to which the +search string refers, or to nowhere if a target is not identified." + (move-marker org-link-precise-target-marker nil) + (let* ((region (org-link--context-from-region)) + (result + (cond + (region + (move-marker org-link-precise-target-marker (region-beginning)) + (cons (org-link--normalize-string region t) nil)) + + ((derived-mode-p 'org-mode) + (let* ((element (org-element-at-point)) + (name (org-element-property :name element)) + (heading (org-element-lineage element '(headline inlinetask) t)) + (custom-id (org-entry-get heading "CUSTOM_ID"))) + (cond + (name + (move-marker org-link-precise-target-marker + (org-element-begin element)) + (cons name name)) + ((org-before-first-heading-p) + (move-marker org-link-precise-target-marker + (line-beginning-position)) + (cons (org-link--normalize-string (org-current-line-string) t) nil)) + (heading + (move-marker org-link-precise-target-marker + (org-element-begin heading)) + (cons (if custom-id (concat "#" custom-id) + (org-link-heading-search-string)) + (org-link--normalize-string + (org-get-heading t t t t))))))) + + ;; Not in an org-mode buffer, no region + (t + (move-marker org-link-precise-target-marker + (line-beginning-position)) + (cons (org-link--normalize-string (org-current-line-string) t) nil))))) + + ;; Only use search option if there is some text. + (when (org-string-nw-p (car result)) + result))) + (defun org-link-open-as-file (path in-emacs) "Pretend PATH is a file name and open it. @@ -1404,7 +1548,7 @@ PATH is a symbol name, as a string." ((and (pred boundp) variable) (describe-variable variable)) (name (user-error "Unknown function or variable: %s" name)))) -(defun org-link--store-help () +(defun org-link--store-help (&optional _interactive?) "Store \"help\" type link." (when (eq major-mode 'help-mode) (let ((symbol @@ -1539,7 +1683,12 @@ prefix ARG forces storing a link for each line in the active region. Assume the function is called interactively if INTERACTIVE? is -non-nil." +non-nil. + +In Org buffers, an additional \"human-readable\" simple file link +is stored as an alternative to persistent org-id or other links, +if at a heading with a CUSTOM_ID property or an element with a +NAME." (interactive "P\np") (org-load-modules-maybe) (if (and (equal arg '(64)) (org-region-active-p)) @@ -1554,36 +1703,19 @@ non-nil." (move-beginning-of-line 2) (set-mark (point))))) (setq org-store-link-plist nil) - (let (link cpltxt desc search custom-id agenda-link) ;; description + ;; Negate `org-context-in-file-links' when given a single universal arg. + (let ((org-link-context-for-files (org-xor org-link-context-for-files + (equal arg '(4)))) + link cpltxt desc search agenda-link) ;; description (cond ;; Store a link using an external link type, if any function is - ;; available. If more than one can generate a link from current - ;; location, ask which one to use. + ;; available, unless external link types are skipped for this + ;; call using two universal args. If more than one function + ;; can generate a link from current location, ask the user + ;; which one to use. ((and (not (equal arg '(16))) - (let ((results-alist nil)) - (dolist (f (org-store-link-functions)) - (when (funcall f) - ;; XXX: return value is not link's plist, so we - ;; store the new value before it is modified. It - ;; would be cleaner to ask store link functions to - ;; return the plist instead. - (push (cons f (copy-sequence org-store-link-plist)) - results-alist))) - (pcase results-alist - (`nil nil) - (`((,_ . ,_)) t) ;single choice: nothing to do - (`((,name . ,_) . ,_) - ;; Reinstate link plist associated to the chosen - ;; function. - (apply #'org-link-store-props - (cdr (assoc-string - (completing-read - (format "Store link with (default %s): " name) - (mapcar #'car results-alist) - nil t nil nil (symbol-name name)) - results-alist))) - t)))) - (setq link (plist-get org-store-link-plist :link)) + (org-link--try-link-store-functions interactive?)) + (setq link (plist-get org-store-link-plist :link)) ;; If store function actually set `:description' property, use ;; it, even if it is nil. Otherwise, fallback to nil (ask user). (setq desc (plist-get org-store-link-plist :description))) @@ -1634,6 +1766,7 @@ non-nil." (org-with-point-at m (setq agenda-link (org-store-link nil interactive?)))))) + ;; Calendar mode ((eq major-mode 'calendar-mode) (let ((cd (calendar-cursor-to-date))) (setq link @@ -1642,6 +1775,7 @@ non-nil." (org-encode-time 0 0 0 (nth 1 cd) (nth 0 cd) (nth 2 cd)))) (org-link-store-props :type "calendar" :date cd))) + ;; Image mode ((eq major-mode 'image-mode) (setq cpltxt (concat "file:" (abbreviate-file-name buffer-file-name)) @@ -1659,15 +1793,22 @@ non-nil." (setq cpltxt (concat "file:" file) link cpltxt))) + ;; Try `org-create-file-search-functions`. If any are + ;; successful, create a file link to the current buffer with + ;; the provided search string. (sets `link` and `cpltxt` to + ;; the same thing; it looks like the intention originally was + ;; that cpltxt was a description, which might have been set by + ;; the search-function (removed in switch to lexical binding)). ((setq search (run-hook-with-args-until-success 'org-create-file-search-functions)) (setq link (concat "file:" (abbreviate-file-name buffer-file-name) "::" search)) (setq cpltxt (or link))) ;; description + ;; Main logic for storing built-in link types in org-mode + ;; buffers ((and (buffer-file-name (buffer-base-buffer)) (derived-mode-p 'org-mode)) (org-with-limited-levels - (setq custom-id (org-entry-get nil "CUSTOM_ID")) (cond ;; Store a link using the target at point ((org-in-regexp "[^<]<<\\([^<>]+\\)>>[^>]" 1) @@ -1681,74 +1822,21 @@ non-nil." ;; links. Maybe the case of identical target and ;; description should be handled by `org-insert-link'. cpltxt nil - desc nil - ;; Do not append #CUSTOM_ID link below. - custom-id nil)) - ((and (featurep 'org-id) - (or (eq org-id-link-to-org-use-id t) - (and interactive? - (or (eq org-id-link-to-org-use-id 'create-if-interactive) - (and (eq org-id-link-to-org-use-id - 'create-if-interactive-and-no-custom-id) - (not custom-id)))) - (and org-id-link-to-org-use-id (org-entry-get nil "ID")))) - ;; Store a link using the ID at point - (setq link (condition-case nil - (prog1 (org-id-store-link) - (setq desc (plist-get org-store-link-plist :description))) - (error - ;; Probably before first headline, link only to file - (concat "file:" - (abbreviate-file-name - (buffer-file-name (buffer-base-buffer)))))))) - (t + desc nil)) + (t ;; Just link to current headline. - (setq cpltxt (concat "file:" - (abbreviate-file-name - (buffer-file-name (buffer-base-buffer))))) - ;; Add a context search string. - (when (org-xor org-link-context-for-files (equal arg '(4))) - (let* ((element (org-element-at-point)) - (name (org-element-property :name element)) - (context - (cond - ((let ((region (org-link--context-from-region))) - (and region (org-link--normalize-string region t)))) - (name) - ((org-before-first-heading-p) - (org-link--normalize-string (org-current-line-string) t)) - (t (org-link-heading-search-string))))) - (when (org-string-nw-p context) - (setq cpltxt (format "%s::%s" cpltxt context)) - (setq desc - (or name - ;; Although description is not a search - ;; string, use `org-link--normalize-string' - ;; to prettify it (contiguous white spaces) - ;; and remove volatile contents (statistics - ;; cookies). - (and (not (org-before-first-heading-p)) - (org-link--normalize-string - (org-get-heading t t t t))) - "NONE"))))) - (setq link cpltxt))))) + (let ((here (org-link--file-link-to-here))) + (setq cpltxt (car here)) + (setq desc (cdr here))) + (setq link cpltxt))))) + ;; Buffer linked to file, but not an org-mode buffer. ((buffer-file-name (buffer-base-buffer)) ;; Just link to this file here. - (setq cpltxt (concat "file:" - (abbreviate-file-name - (buffer-file-name (buffer-base-buffer))))) - ;; Add a context search string. - (when (org-xor org-link-context-for-files (equal arg '(4))) - (let ((context (org-link--normalize-string - (or (org-link--context-from-region) - (org-current-line-string)) - t))) - ;; Only use search option if there is some text. - (when (org-string-nw-p context) - (setq cpltxt (format "%s::%s" cpltxt context)) - (setq desc "NONE")))) - (setq link cpltxt)) + (let ((here (org-link--file-link-to-here))) + (setq cpltxt (car here)) + (setq desc (cdr here))) + (setq link cpltxt)) (interactive? (user-error "No method for storing a link from this buffer")) @@ -1764,24 +1852,18 @@ non-nil." ;; Store and return the link (if (not (and interactive? link)) (or agenda-link (and link (org-link-make-string link desc))) - (dotimes (_ (if custom-id 2 1)) ; Store 2 links when CUSTOM-ID is non-nil. - (cond - ((not (member (list link desc) org-stored-links)) - (push (list link desc) org-stored-links) - (message "Stored: %s" (or desc link))) - ((equal (list link desc) (car org-stored-links)) - (message "This link has already been stored")) - (t - (setq org-stored-links - (delete (list link desc) org-stored-links)) - (push (list link desc) org-stored-links) - (message "Link moved to front: %s" (or desc link)))) - (when custom-id - (setq link (concat "file:" - (abbreviate-file-name - (buffer-file-name (buffer-base-buffer))) - "::#" custom-id)))) - (car org-stored-links))))) + (org-link--add-to-stored-links link desc) + ;; In org buffers, store an additional "human-readable" link + ;; using custom id, if available. + (when (and (buffer-file-name (buffer-base-buffer)) + (derived-mode-p 'org-mode) + (org-entry-get nil "CUSTOM_ID")) + (let ((here (org-link--file-link-to-here))) + (setq link (car here)) + (setq desc (cdr here))) + (unless (equal (list link desc) (car org-stored-links)) + (org-link--add-to-stored-links link desc))) + (car org-stored-links))))) ;;;###autoload (defun org-insert-link (&optional complete-file link-location description) diff --git a/lisp/org-id.el b/lisp/org-id.el index 8647a57cc..7200be34d 100644 --- a/lisp/org-id.el +++ b/lisp/org-id.el @@ -129,6 +129,46 @@ nil Never use an ID to make a link, instead link using a text search for (const :tag "Only use existing" use-existing) (const :tag "Do not use ID to create link" nil))) +(defcustom org-id-link-consider-parent-id nil + "Non-nil means storing a link to an Org entry considers inherited IDs. + +When this option is non-nil and `org-id-link-use-context' is +enabled, ID properties inherited from parent entries will be +considered when storing an ID link. If no ID is found in this +way, a new one may be created as normal (see +`org-id-link-to-org-use-id'). + +For example, given this org file: + +* Parent +:PROPERTIES: +:ID: abc +:END: +** Child 1 +** Child 2 + +With `org-id-link-consider-parent-id' and +`org-id-link-use-context' both enabled, storing a link with point +at \"Child 1\" will produce a link \"<id:abc::*Child 1>\". This +allows linking to uniquely-named sub-entries within a parent +entry with an ID, without requiring every sub-entry to have its +own ID." + :group 'org-link-store + :group 'org-id + :package-version '(Org . "9.7") + :type 'boolean) + +(defcustom org-id-link-use-context t + "Non-nil means enables search string context in org-id links. + +Search strings are added by `org-id-store-link' when both the +general option `org-link-context-for-files' and the org-id option +`org-id-link-use-context' are non-nil." + :group 'org-link-store + :group 'org-id + :package-version '(Org . "9.7") + :type 'boolean) + (defcustom org-id-uuid-program "uuidgen" "The uuidgen program." :group 'org-id @@ -280,15 +320,21 @@ This is useful when working with contents in a temporary buffer that will be copied back to the original.") ;;;###autoload -(defun org-id-get (&optional epom create prefix) - "Get the ID property of the entry at EPOM. -EPOM is an element, marker, or buffer position. -If EPOM is nil, refer to the entry at point. -If the entry does not have an ID, the function returns nil. -However, when CREATE is non-nil, create an ID if none is present already. -PREFIX will be passed through to `org-id-new'. -In any case, the ID of the entry is returned." - (let ((id (org-entry-get epom "ID"))) +(defun org-id-get (&optional epom create prefix inherit) + "Get the ID of the entry at EPOM. + +EPOM is an element, marker, or buffer position. If EPOM is nil, +refer to the entry at point. + +If INHERIT is non-nil, ID properties inherited from parent +entries are considered. Otherwise, only ID properties on the +entry itself are considered. + +When CREATE is nil, return the ID of the entry if found, +otherwise nil. When CREATE is non-nil, create an ID if none has +been found, and return the new ID. PREFIX will be passed through +to `org-id-new'." + (let ((id (org-entry-get epom "ID" (and inherit t)))) (cond ((and id (stringp id) (string-match "\\S-" id)) id) @@ -703,18 +749,56 @@ optional argument MARKERP, return the position as a new marker." ;; Calling the following function is hard-coded into `org-store-link', ;; so we do have to add it to `org-store-link-functions'. +(defun org-id--get-id-to-store-link (&optional create) + "Get or create the relevant ID for storing a link. + +Optional argument CREATE is passed to `org-id-get'. + +Inherited IDs are only considered when +`org-id-link-consider-parent-id', `org-id-link-use-context' and +`org-link-context-for-files' are all enabled, since inherited IDs +are confusing without the additional search string context. + +Note that this function resets the +`org-entry-property-inherited-from' marker: it will either point +to nil (if the id was not inherited) or to the point it was +inherited from." + (let* ((inherit-id (and org-id-link-consider-parent-id + org-id-link-use-context + org-link-context-for-files))) + (move-marker org-entry-property-inherited-from nil) + (org-id-get nil create nil inherit-id))) + ;;;###autoload (defun org-id-store-link () "Store a link to the current entry, using its ID. -If before first heading store first title-keyword as description -or filename if no title." +The link description is based on the heading, or if before the +first heading, the title keyword if available, or else the +filename. + +When `org-link-context-for-files' and `org-id-link-use-context' +are non-nil, add a search string to the link. The link +description is then based on the search string target. + +When in addition `org-id-link-consider-parent-id' is non-nil, the +ID can be inherited from a parent entry, with the search string +used to still link to the current location." (interactive) - (when (and (buffer-file-name (buffer-base-buffer)) (derived-mode-p 'org-mode)) - (let* ((link (concat "id:" (org-id-get-create))) + (when (and (buffer-file-name (buffer-base-buffer)) + (derived-mode-p 'org-mode)) + ;; Get the precise target first, in case looking for an id causes + ;; a properties drawer to be added at the current location. + (let* ((precise-target (and org-link-context-for-files + org-id-link-use-context + (org-link-precise-link-target))) + (link (concat "id:" (org-id--get-id-to-store-link 'create))) + (id-location (or (and org-entry-property-inherited-from + (marker-position org-entry-property-inherited-from)) + (save-excursion (org-back-to-heading-or-point-min t) (point)))) (case-fold-search nil) (desc (save-excursion - (org-back-to-heading-or-point-min t) + (goto-char id-location) (cond ((org-before-first-heading-p) (let ((keywords (org-collect-keywords '("TITLE")))) (if keywords @@ -726,14 +810,61 @@ or filename if no title." (match-string 4) (match-string 0))) (t link))))) + ;; Precise targets should be after id-location to avoid + ;; duplicating the current headline as a search string + (when (and precise-target + org-link-precise-target-marker + (> (marker-position org-link-precise-target-marker) + id-location)) + (setq link (concat link "::" (car precise-target))) + (setq desc (cdr precise-target))) (org-link-store-props :link link :description desc :type "id") link))) -(defun org-id-open (id _) - "Go to the entry with id ID." - (org-mark-ring-push) - (let ((m (org-id-find id 'marker)) - cmd) +;;;###autoload +(defun org-id-store-link-maybe (&optional interactive?) + "Store a link to the current entry using its ID if enabled. + +The value of `org-id-link-to-org-use-id' determines whether an ID +link should be stored, using `org-id-store-link'. + +Assume the function is called interactively if INTERACTIVE? is +non-nil." + (when (and (buffer-file-name (buffer-base-buffer)) + (derived-mode-p 'org-mode) + (or (eq org-id-link-to-org-use-id t) + (and interactive? + (or (eq org-id-link-to-org-use-id 'create-if-interactive) + (and (eq org-id-link-to-org-use-id + 'create-if-interactive-and-no-custom-id) + (not (org-entry-get nil "CUSTOM_ID"))))) + ;; 'use-existing + (and org-id-link-to-org-use-id + (org-id--get-id-to-store-link)))) + (org-id-store-link))) + +(defun org-id-open (link _) + "Go to the entry indicated by id link LINK. + +The link can include a search string after \"::\", which is +passed to `org-link-search'. + +For backwards compatibility with IDs that contain \"::\", if no +match is found for the ID, the full link string including \"::\" +will be tried as an ID." + (let* ((option (and (string-match "::\\(.*\\)\\'" link) + (match-string 1 link))) + (id (if (not option) link + (substring link 0 (match-beginning 0)))) + m cmd) + (org-mark-ring-push) + (setq m (org-id-find id 'marker)) + (when (and (not m) option) + ;; Backwards compatibility: if id is not found, try treating + ;; whole link as an id. + (setq m (org-id-find link 'marker)) + (when m + (setq option nil))) (unless m (error "Cannot find entry with ID \"%s\"" id)) ;; Use a buffer-switching command in analogy to finding files @@ -750,9 +881,16 @@ or filename if no title." (funcall cmd (marker-buffer m))) (goto-char m) (move-marker m nil) + (when option + (save-restriction + (unless (org-before-first-heading-p) + (org-narrow-to-subtree)) + (org-link-search option))) (org-fold-show-context))) -(org-link-set-parameters "id" :follow #'org-id-open) +(org-link-set-parameters "id" + :follow #'org-id-open + :store #'org-id-store-link-maybe) (provide 'org-id) diff --git a/lisp/org-lint.el b/lisp/org-lint.el index 4d2a55d15..b23afcca3 100644 --- a/lisp/org-lint.el +++ b/lisp/org-lint.el @@ -65,6 +65,7 @@ ;; - special properties in properties drawers, ;; - obsolete syntax for properties drawers, ;; - invalid duration in EFFORT property, +;; - invalid ID property with a double colon, ;; - missing definition for footnote references, ;; - missing reference for footnote definitions, ;; - non-footnote definitions in footnote section, @@ -686,6 +687,16 @@ Use :header-args: instead" (list (org-element-begin p) (format "Invalid effort duration format: %S" value)))))))) +(defun org-lint-invalid-id-property (ast) + (org-element-map ast 'node-property + (lambda (p) + (when (equal "ID" (org-element-property :key p)) + (let ((value (org-element-property :value p))) + (and (org-string-nw-p value) + (string-match-p "::" value) + (list (org-element-begin p) + (format "IDs should not include \"::\": %S" value)))))))) + (defun org-lint-link-to-local-file (ast) (org-element-map ast 'link (lambda (l) @@ -1684,6 +1695,11 @@ AST is the buffer parse tree." #'org-lint-invalid-effort-property :categories '(properties)) +(org-lint-add-checker 'invalid-id-property + "Report search string delimiter \"::\" in ID property" + #'org-lint-invalid-id-property + :categories '(properties)) + (org-lint-add-checker 'undefined-footnote-reference "Report missing definition for footnote references" #'org-lint-undefined-footnote-reference diff --git a/testing/lisp/test-ol.el b/testing/lisp/test-ol.el index e0cec0854..4be6b3055 100644 --- a/testing/lisp/test-ol.el +++ b/testing/lisp/test-ol.el @@ -381,6 +381,136 @@ See https://github.com/yantar92/org/issues/4." (equal (format "[[file:%s::*foo bar][foo bar]]" file file) (org-store-link nil))))))) +(ert-deftest test-org-link/precise-link-target () + "Test `org-link-precise-link-target` specifications." + (org-test-with-temp-text "* H1<point>\n* H2\n" + (should + (equal '("*H1" . "H1") + (org-link-precise-link-target))) + (should + (equal 1 (marker-position org-link-precise-target-marker)))) + (org-test-with-temp-text "* H1\n#+name: foo<point>\n#+begin_example\nhi\n#+end_example\n" + (should + (equal '("foo" . "foo") + (org-link-precise-link-target))) + (should + (equal 6 (marker-position org-link-precise-target-marker)))) + (org-test-with-temp-text "\nText<point>\n* H1\n" + (should + (equal '("Text" . nil) + (org-link-precise-link-target))) + (should + (equal 2 (marker-position org-link-precise-target-marker)))) + (org-test-with-temp-text "\n<point>\n* H1\n" + (should + (equal nil (org-link-precise-link-target))) + (should + (equal 2 (marker-position org-link-precise-target-marker))))) + +(defmacro test-ol-stored-link-with-text (text &rest body) + "Return :link and :description from link stored in body." + (declare (indent 1)) + `(let (org-store-link-plist) + (org-test-with-temp-text-in-file ,text + ,@body + (list (plist-get org-store-link-plist :link) + (plist-get org-store-link-plist :description))))) + +(ert-deftest test-org-link/id-store-link () + "Test `org-id-store-link' specifications." + (let ((org-id-link-to-org-use-id nil)) + (should + (equal '(nil nil) + (test-ol-stored-link-with-text "* H1\n:PROPERTIES:\n:ID: abc\n:END:\n" + (org-id-store-link-maybe t))))) + ;; On a headline, link to that headline's ID. Use heading as the + ;; description of the link. + (let ((org-id-link-to-org-use-id t)) + (should + (equal '("id:abc" "H1") + (test-ol-stored-link-with-text "* H1\n:PROPERTIES:\n:ID: abc\n:END:\n" + (org-id-store-link-maybe t))))) + ;; Remove TODO keywords etc from description of the link. + (let ((org-id-link-to-org-use-id t)) + (should + (equal '("id:abc" "H1") + (test-ol-stored-link-with-text "* TODO [#A] H1 :tag:\n:PROPERTIES:\n:ID: abc\n:END:\n" + (org-id-store-link-maybe t))))) + ;; create-if-interactive + (let ((org-id-link-to-org-use-id 'create-if-interactive)) + (should + (equal '("id:abc" "H1") + (cl-letf (((symbol-function 'org-id-new) + (lambda (&rest _rest) "abc"))) + (test-ol-stored-link-with-text "* H1\n" + (org-id-store-link-maybe t))))) + (should + (equal '(nil nil) + (test-ol-stored-link-with-text "* H1\n" + (org-id-store-link-maybe nil))))) + ;; create-if-interactive-and-no-custom-id + (let ((org-id-link-to-org-use-id 'create-if-interactive-and-no-custom-id)) + (should + (equal '("id:abc" "H1") + (cl-letf (((symbol-function 'org-id-new) + (lambda (&rest _rest) "abc"))) + (test-ol-stored-link-with-text "* H1\n" + (org-id-store-link-maybe t))))) + (should + (equal '(nil nil) + (test-ol-stored-link-with-text "* H1\n:PROPERTIES:\n:CUSTOM_ID: xyz\n:END:\n" + (org-id-store-link-maybe t)))) + (should + (equal '(nil nil) + (test-ol-stored-link-with-text "* H1\n" + (org-id-store-link-maybe nil))))) + ;; use-context should have no effect when on the headline with an id + (let ((org-id-link-to-org-use-id t) + (org-id-link-use-context t)) + (should + (equal '("id:abc" "H2") + (test-ol-stored-link-with-text "* H1\n** H2<point>\n:PROPERTIES:\n:ID: abc\n:END:\n" + ;; simulate previously getting an inherited value + (move-marker org-entry-property-inherited-from 1) + (org-id-store-link-maybe t)))))) + +(ert-deftest test-org-link/id-store-link-using-parent () + "Test `org-id-store-link' specifications with `org-id-link-consider-parent-id` set." + ;; when using context to still find specific heading + (let ((org-id-link-to-org-use-id t) + (org-id-link-consider-parent-id t) + (org-id-link-use-context t)) + (should + (equal '("id:abc::*H2" "H2") + (test-ol-stored-link-with-text "* H1\n:PROPERTIES:\n:ID: abc\n:END:\n** H2\n<point>" + (org-id-store-link)))) + (should + (equal '("id:abc::name" "name") + (test-ol-stored-link-with-text "* H1\n:PROPERTIES:\n:ID: abc\n:END:\n\n#+name: name\n<point>#+begin_example\nhi\n#+end_example\n" + (org-id-store-link)))) + (should + (equal '("id:abc" "H1") + (test-ol-stored-link-with-text "* H1<point>\n:PROPERTIES:\n:ID: abc\n:END:\n** H2\n" + (org-id-store-link)))) + ;; should not use newly added ids as search string, e.g. in an empty file + (should + (let (name result) + (setq result + (cl-letf (((symbol-function 'org-id-new) + (lambda (&rest _rest) "abc"))) + (test-ol-stored-link-with-text "<point>" + (setq name (buffer-name)) + (org-id-store-link)))) + (equal `("id:abc" ,name) result)))) + ;; should not find targets in the next section + (let ((org-id-link-to-org-use-id 'use-existing) + (org-id-link-consider-parent-id t) + (org-id-link-use-context t)) + (should + (equal '(nil nil) + (test-ol-stored-link-with-text "* H1\n:PROPERTIES:\n:ID: abc\n:END:\n* H2\n** <point>Target\n" + (org-id-store-link-maybe t)))))) + \f ;;; Radio Targets -- 2.37.1 (Apple Git-137.1) ^ permalink raw reply related [flat|nested] 48+ messages in thread
* Re: [PATCH v2] org-id: allow using parent's existing id in links to headlines 2024-01-31 18:11 ` Rick Lupton 2024-02-01 12:13 ` Ihor Radchenko @ 2024-02-03 13:10 ` Ihor Radchenko 2024-02-08 8:24 ` [PATCH] lisp/ol.el: Improve docstring Rick Lupton 2024-02-08 8:46 ` [PATCH v2] org-id: allow using parent's existing id in links to headlines Rick Lupton 1 sibling, 2 replies; 48+ messages in thread From: Ihor Radchenko @ 2024-02-03 13:10 UTC (permalink / raw) To: Rick Lupton; +Cc: Y. E. "Rick Lupton" <mail@ricklupton.name> writes: > I see. Updated to get the search string first, before the possible properties draw appears. > > To make this work I changed `org-link-precise-link-target': instead of > accepting the RELATIVE-TO argument and rejecting unsuitable targets > internally, it now sets a marker `org-link-precise-target-marker' > showing where the target that was found is, so the caller can decide > if the found target is suitable. I copied the approach from > `org-entry-property-inherited-from', hope that doesn't cause any other > issues. I'd prefer to avoid using global variables here. `org-entry-property-inherited-from' dates to pre-lexical binding times and is a potential source of subtle bugs if several `org-entry-get' calls happen unexpectedly to the code, changing `org-entry-property-inherited-from' multiple times. Instead, I suggest changing the return value of `org-link-precise-link-target' to a list that includes marker in addition to search string and description. >> The fact that links stored via `org-store-link' cannot be open with >> default settings is not good. >> Also, your patch disregards this setting - it should not match >> non-headline search strings with the default value of >> `org-link-search-must-match-exact-headline'. > > `org-link-search-must-match-exact-headline' affects `org-link-search', which is called by `org-id-open' -- so I think the behaviour for these org-id links should be the same as for other file links? Am I missing something? No, you don't. In my testing, I used #+name: as link target. However, what I missed is that #+name targets are matched even when `org-link-search-must-match-exact-headline' is set to 'query-to-create. The docstring is not accurate there and must be updated. > Or, maybe you mean links that rely on `org-link-search-must-match-exact-headline' should not be stored. That would seem reasonable, but also doesn't need to be part of these changes here? Yes, I also meant this. Indeed, it is out of scope of your patch. It was a comment for future reference. >> Probably, changing the default value of >> `org-link-search-must-match-exact-headline' to nil is due. > > It seems like the behaviour below would be desirable, but doesn't currently exist with any setting of `org-link-search-must-match-exact-headline'? > > (org-link-search "plain text") --> fuzzy search for all text > (org-link-search "*heading") --> search only headings, optionally creating if missing That would also make sense. I like this idea. >>> - (org-insert-heading nil t t) >>> + ;; Find appropriate level for new heading >>> + (let ((level (save-excursion >>> + (goto-char (point-min)) >>> + (+ 1 (or (org-current-level) 0))))) >> >> This is fragile. You assume that `point-min' always contains a heading. >> That may or may not be the case - `org-link-search' may be called by >> third-party code that does not care about setting narrowing in certain >> ways. > > I don't think it's a problem. (org-current-level) returns something suitable whether or not point-min contains a heading. Both the situations below seem reasonable choices for the level of the newly created heading at the end: That's right. > ---start of narrowing--- > Text > * H1 > ** H2 > * A new level 1 heading is created at the end > ---end of narrowing--- > > ---start of narrowing--- > * H1 > ** H2 > ** A new level 2 heading is created at the end > ---end of narrowing--- However, the second scenario is unexpected - consider that your narrowing is not a narrowing but the whole contents of an Org file. Before your patch, in both cases, a new level 1 heading is created. With your patch, the second case will create a new level 2 heading even for [[*Foo]] links. It looks like we cannot simply rely on narrowing to determine the created heading level. -- Ihor Radchenko // yantar92, Org mode contributor, Learn more about Org mode at <https://orgmode.org/>. Support Org development at <https://liberapay.com/org-mode>, or support my work at <https://liberapay.com/yantar92> ^ permalink raw reply [flat|nested] 48+ messages in thread
* [PATCH] lisp/ol.el: Improve docstring 2024-02-03 13:10 ` Ihor Radchenko @ 2024-02-08 8:24 ` Rick Lupton 2024-02-08 14:52 ` Ihor Radchenko 2024-02-08 8:46 ` [PATCH v2] org-id: allow using parent's existing id in links to headlines Rick Lupton 1 sibling, 1 reply; 48+ messages in thread From: Rick Lupton @ 2024-02-08 8:24 UTC (permalink / raw) To: Ihor Radchenko; +Cc: Y. E. [-- Attachment #1: Type: text/plain, Size: 321 bytes --] On Sat, 3 Feb 2024, at 1:10 PM, Ihor Radchenko wrote: > In my testing, I used #+name: as link target. > However, what I missed is that #+name targets are matched even when > `org-link-search-must-match-exact-headline' is set to 'query-to-create. > The docstring is not accurate there and must be updated. How about this? [-- Attachment #2: 0001-lisp-ol.el-Improve-docstring.patch --] [-- Type: application/octet-stream, Size: 1649 bytes --] From 112badfb3f96d1927e3edde35d19a83cd0cf8761 Mon Sep 17 00:00:00 2001 From: Rick Lupton <mail@ricklupton.name> Date: Thu, 8 Feb 2024 08:19:40 +0000 Subject: [PATCH] lisp/ol.el: Improve docstring * lisp/ol.el (org-link-search-must-match-exact-headline): Make the docstring more accurately describe behaviour. Link: https://list.orgmode.org/87cytdithi.fsf@localhost/ --- lisp/ol.el | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/lisp/ol.el b/lisp/ol.el index bc1ad99ea..f8d911127 100644 --- a/lisp/ol.el +++ b/lisp/ol.el @@ -368,14 +368,17 @@ another window." (const wl-other-frame))))) (defcustom org-link-search-must-match-exact-headline 'query-to-create - "Non-nil means internal fuzzy links can only match headlines. + "Control fuzzy link behaviour when specific matches not found. -When nil, the fuzzy link may point to a target or a named -construct in the document. When set to the special value -`query-to-create', offer to create a new headline when none -matched. +When nil, if a fuzzy link does not match a more specific +target (such as a heading, named block, target, or code ref), +attempt a regular text search. When set to the special value +`query-to-create', offer to create a new heading matching the +link instead. Otherwise, signal an error rather than attempting +a regular text search. -Spaces and statistics cookies are ignored during heading searches." +This option only affects behaviour in Org buffers. Spaces and +statistics cookies are ignored during heading searches." :group 'org-link-follow :version "24.1" :type '(choice -- 2.37.1 (Apple Git-137.1) ^ permalink raw reply related [flat|nested] 48+ messages in thread
* Re: [PATCH] lisp/ol.el: Improve docstring 2024-02-08 8:24 ` [PATCH] lisp/ol.el: Improve docstring Rick Lupton @ 2024-02-08 14:52 ` Ihor Radchenko 0 siblings, 0 replies; 48+ messages in thread From: Ihor Radchenko @ 2024-02-08 14:52 UTC (permalink / raw) To: Rick Lupton; +Cc: Y. E. "Rick Lupton" <mail@ricklupton.name> writes: > On Sat, 3 Feb 2024, at 1:10 PM, Ihor Radchenko wrote: >> In my testing, I used #+name: as link target. >> However, what I missed is that #+name targets are matched even when >> `org-link-search-must-match-exact-headline' is set to 'query-to-create. >> The docstring is not accurate there and must be updated. > > How about this? Thanks! Applied, onto main, with minor amendment to the commit message. https://git.savannah.gnu.org/cgit/emacs/org-mode.git/commit/?id=f016545aa -- Ihor Radchenko // yantar92, Org mode contributor, Learn more about Org mode at <https://orgmode.org/>. Support Org development at <https://liberapay.com/org-mode>, or support my work at <https://liberapay.com/yantar92> ^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH v2] org-id: allow using parent's existing id in links to headlines 2024-02-03 13:10 ` Ihor Radchenko 2024-02-08 8:24 ` [PATCH] lisp/ol.el: Improve docstring Rick Lupton @ 2024-02-08 8:46 ` Rick Lupton 2024-02-08 13:02 ` Ihor Radchenko 1 sibling, 1 reply; 48+ messages in thread From: Rick Lupton @ 2024-02-08 8:46 UTC (permalink / raw) To: Ihor Radchenko; +Cc: Y. E. [-- Attachment #1: Type: text/plain, Size: 1481 bytes --] On Sat, 3 Feb 2024, at 1:10 PM, Ihor Radchenko wrote: > I'd prefer to avoid using global variables here. > `org-entry-property-inherited-from' dates to pre-lexical binding times > and is a potential source of subtle bugs if several `org-entry-get' > calls happen unexpectedly to the code, changing > `org-entry-property-inherited-from' multiple times. > > Instead, I suggest changing the return value of > `org-link-precise-link-target' to a list that includes marker in > addition to search string and description. Makes sense -- I changed it to work that way and it is neater. I returned simply the buffer position rather than a marker, since it is always in the current buffer, and avoids needing to worry about cleaning up the marker when finished or if not of interest. > It looks like we cannot simply rely on narrowing to determine the > created heading level. I think you're right. I have extended `org-link-search' to accept an optional argument describing the org element where newly created headings should go as subheadings. My thought was that this was not significantly more complicated than just passing the numeric level for new headings, but actually more flexible (e.g. you could if you wanted (with additional future elisp) create missing headings as part of a "To be filed" subtree within the file, rather than always at the end). Does that look ok? [is it useful to keep attaching the unchanged first patch so they are available as a set?] Thanks Rick [-- Attachment #2: 0001-lisp-org.el-org-insert-heading-allow-specifying-head.patch --] [-- Type: application/octet-stream, Size: 5084 bytes --] From 1f9b776548baca13032a078150b79f4d6b827c71 Mon Sep 17 00:00:00 2001 From: Rick Lupton <mail@ricklupton.name> Date: Wed, 3 Jan 2024 22:37:38 +0000 Subject: [PATCH 1/2] lisp/org.el (org-insert-heading): allow specifying heading level * lisp/org.el (org-insert-heading): Change optional argument TOP to LEVEL, accepting a number to force a specific heading level. * testing/lisp/test-org.el (test-org/insert-heading): Add tests * etc/ORG-NEWS: Document changes --- etc/ORG-NEWS | 6 ++++++ lisp/org.el | 21 ++++++++++++++------- testing/lisp/test-org.el | 26 ++++++++++++++++++++++++-- 3 files changed, 44 insertions(+), 9 deletions(-) diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS index 92d363b80..9e68bcdcb 100644 --- a/etc/ORG-NEWS +++ b/etc/ORG-NEWS @@ -1033,6 +1033,12 @@ as the function can also act on objects. *** ~org-export-get-parent-element~ is renamed to ~org-element-parent-element~ and moved to =lisp/org-element.el= +*** ~org-insert-heading~ optional argument =TOP= is now =LEVEL= + +A numeric value forces a heading at that level to be inserted. For +backwards compatibility, non-numeric non-nil values insert level 1 +headings as before. + ** Miscellaneous *** =org-crypt.el= now applies initial visibility settings to decrypted entries diff --git a/lisp/org.el b/lisp/org.el index da315fccb..54748f495 100644 --- a/lisp/org.el +++ b/lisp/org.el @@ -6364,7 +6364,7 @@ headline instead of current one." (`(heading . ,value) value) (_ nil))) -(defun org-insert-heading (&optional arg invisible-ok top) +(defun org-insert-heading (&optional arg invisible-ok level) "Insert a new heading or an item with the same depth at point. If point is at the beginning of a heading, insert a new heading @@ -6393,12 +6393,19 @@ When INVISIBLE-OK is set, stop at invisible headlines when going back. This is important for non-interactive uses of the command. -When optional argument TOP is non-nil, insert a level 1 heading, -unconditionally." +When optional argument LEVEL is a number, insert a heading at +that level. For backwards compatibility, when LEVEL is non-nil +but not a number, insert a level-1 heading." (interactive "P") (let* ((blank? (org--blank-before-heading-p (equal arg '(16)))) - (level (org-current-level)) - (stars (make-string (if (and level (not top)) level 1) ?*))) + (current-level (org-current-level)) + (num-stars (or + ;; Backwards compat: if LEVEL non-nil, level is 1 + (and level (if (wholenump level) level 1)) + current-level + ;; This `1' is for when before first headline + 1)) + (stars (make-string num-stars ?*))) (cond ((or org-insert-heading-respect-content (member arg '((4) (16))) @@ -6407,7 +6414,7 @@ unconditionally." ;; Position point at the location of insertion. Make sure we ;; end up on a visible headline if INVISIBLE-OK is nil. (org-with-limited-levels - (if (not level) (outline-next-heading) ;before first headline + (if (not current-level) (outline-next-heading) ;before first headline (org-back-to-heading invisible-ok) (when (equal arg '(16)) (org-up-heading-safe)) (org-end-of-subtree invisible-ok 'to-heading))) @@ -6420,7 +6427,7 @@ unconditionally." (org-before-first-heading-p))) (insert "\n") (backward-char)) - (when (and (not level) (not (eobp)) (not (bobp))) + (when (and (not current-level) (not (eobp)) (not (bobp))) (when (org-at-heading-p) (insert "\n")) (backward-char)) (unless (and blank? (org-previous-line-empty-p)) diff --git a/testing/lisp/test-org.el b/testing/lisp/test-org.el index 822cbc67a..fc50dc787 100644 --- a/testing/lisp/test-org.el +++ b/testing/lisp/test-org.el @@ -1980,8 +1980,30 @@ CLOCK: [2022-09-17 sam. 11:00]--[2022-09-17 sam. 11:46] => 0:46" (let ((org-insert-heading-respect-content nil)) (org-insert-heading '(16))) (buffer-string)))) - ;; When optional TOP-LEVEL argument is non-nil, always insert - ;; a level 1 heading. + ;; When optional LEVEL argument is a number, insert a heading at + ;; that level. + (should + (equal "* H1\n** H2\n* " + (org-test-with-temp-text "* H1\n** H2<point>" + (org-insert-heading nil nil 1) + (buffer-string)))) + (should + (equal "* H1\n** H2\n** " + (org-test-with-temp-text "* H1\n** H2<point>" + (org-insert-heading nil nil 2) + (buffer-string)))) + (should + (equal "* H1\n** H2\n*** " + (org-test-with-temp-text "* H1\n** H2<point>" + (org-insert-heading nil nil 3) + (buffer-string)))) + (should + (equal "* H1\n- item\n* " + (org-test-with-temp-text "* H1\n- item<point>" + (org-insert-heading nil nil 1) + (buffer-string)))) + ;; When optional LEVEL argument is non-nil, always insert a level 1 + ;; heading. (should (equal "* H1\n** H2\n* " (org-test-with-temp-text "* H1\n** H2<point>" -- 2.37.1 (Apple Git-137.1) [-- Attachment #3: 0002-org-id.el-Add-search-strings-inherit-parent-IDs.patch --] [-- Type: application/octet-stream, Size: 58214 bytes --] From d5759dd95bec88be38ddbde07fa4437c0528469a Mon Sep 17 00:00:00 2001 From: Rick Lupton <mail@ricklupton.name> Date: Sun, 19 Nov 2023 14:52:05 +0000 Subject: [PATCH 2/2] org-id.el: Add search strings, inherit parent IDs * lisp/ol.el (org-store-link): Refactor org-id links to use standard `org-store-link-functions'. (org-link-search): Create new headings at appropriate level. (org-link-precise-link-target): New function extracting logic to identify a precise link target, e.g. a heading, named object, or text search. (org-link-try-link-store-functions): Extract logic to call external link store functions. Pass them a new `interactive?' argument. * lisp/ol-bbdb.el (org-bbdb-store-link): * lisp/ol-bibtex.el (org-bibtex-store-link): * lisp/ol-docview.el (org-docview-store-link): * lisp/ol-eshell.el (org-eshell-store-link): * lisp/ol-eww.el (org-eww-store-link): * lisp/ol-gnus.el (org-gnus-store-link): * lisp/ol-info.el (org-info-store-link): * lisp/ol-irc.el (org-irc-store-link): * lisp/ol-man.el (org-man-store-link): * lisp/ol-mhe.el (org-mhe-store-link): * lisp/ol-rmail.el (org-rmail-store-link): Accept optional arg. * lisp/org-id.el (org-id-link-consider-parent-id): New option to allow a parent heading with an id to be considered as a link target. (org-id-link-use-context): New option to add context to org-id links. (org-id-get): Add optional `inherit' argument which considers parents' IDs if the current entry does not have one. (org-id-store-link): Consider IDs of parent headings as link targets when current heading has no ID and `org-id-link-consider-parent-id' is set. Add a search string to the link when enabled. (org-id-store-link-maybe): Function set as :store option for custom id link property. Move logic from `org-store-link' here to determine when an org-id link should be stored using `org-id-store-link'. (org-id-open): Recognise search strings after "::" in org-id links. * lisp/org-lint.el: add checker for "::" in ID properties. * testing/lisp/test-ol.el: Add tests for `org-link-precise-link-target' and `org-id-store-link' functions, testing new options. * doc/org-manual.org: Update documentation about links. * etc/ORG-NEWS: Document changes and new options. These feature allows for more precise links when using org-id to link to org headings, without requiring every single headline to have an id. Link: https://list.orgmode.org/118435e8-0b20-46fd-af6a-88de8e19fac6@app.fastmail.com/ --- doc/org-manual.org | 133 ++++++++++------- etc/ORG-NEWS | 64 ++++++++ lisp/ol-bbdb.el | 2 +- lisp/ol-bibtex.el | 2 +- lisp/ol-docview.el | 2 +- lisp/ol-eshell.el | 2 +- lisp/ol-eww.el | 2 +- lisp/ol-gnus.el | 2 +- lisp/ol-info.el | 2 +- lisp/ol-irc.el | 2 +- lisp/ol-man.el | 2 +- lisp/ol-mhe.el | 2 +- lisp/ol-rmail.el | 2 +- lisp/ol.el | 324 +++++++++++++++++++++++++--------------- lisp/org-id.el | 178 +++++++++++++++++++--- lisp/org-lint.el | 16 ++ testing/lisp/test-ol.el | 122 +++++++++++++++ 17 files changed, 655 insertions(+), 204 deletions(-) diff --git a/doc/org-manual.org b/doc/org-manual.org index 1a025a139..49fce9113 100644 --- a/doc/org-manual.org +++ b/doc/org-manual.org @@ -3300,10 +3300,6 @@ Here is the full set of built-in link types: File links. File name may be remote, absolute, or relative. - Additionally, you can specify a line number, or a text search. - In Org files, you may link to a headline name, a custom ID, or a - code reference instead. - As a special case, "file" prefix may be omitted if the file name is complete, e.g., it starts with =./=, or =/=. @@ -3367,44 +3363,50 @@ Here is the full set of built-in link types: Execute a shell command upon activation. + +For =file:= and =id:= links, you can additionally specify a line +number, or a text search string, separated by =::=. In Org files, you +may link to a headline name, a custom ID, or a code reference instead. + The following table illustrates the link types above, along with their options: -| Link Type | Example | -|------------+----------------------------------------------------------| -| http | =http://staff.science.uva.nl/c.dominik/= | -| https | =https://orgmode.org/= | -| doi | =doi:10.1000/182= | -| file | =file:/home/dominik/images/jupiter.jpg= | -| | =/home/dominik/images/jupiter.jpg= (same as above) | -| | =file:papers/last.pdf= | -| | =./papers/last.pdf= (same as above) | -| | =file:/ssh:me@some.where:papers/last.pdf= (remote) | -| | =/ssh:me@some.where:papers/last.pdf= (same as above) | -| | =file:sometextfile::NNN= (jump to line number) | -| | =file:projects.org= | -| | =file:projects.org::some words= (text search)[fn:12] | -| | =file:projects.org::*task title= (headline search) | -| | =file:projects.org::#custom-id= (headline search) | -| attachment | =attachment:projects.org= | -| | =attachment:projects.org::some words= (text search) | -| docview | =docview:papers/last.pdf::NNN= | -| id | =id:B7423F4D-2E8A-471B-8810-C40F074717E9= | -| news | =news:comp.emacs= | -| mailto | =mailto:adent@galaxy.net= | -| mhe | =mhe:folder= (folder link) | -| | =mhe:folder#id= (message link) | -| rmail | =rmail:folder= (folder link) | -| | =rmail:folder#id= (message link) | -| gnus | =gnus:group= (group link) | -| | =gnus:group#id= (article link) | -| bbdb | =bbdb:R.*Stallman= (record with regexp) | -| irc | =irc:/irc.com/#emacs/bob= | -| help | =help:org-store-link= | -| info | =info:org#External links= | -| shell | =shell:ls *.org= | -| elisp | =elisp:(find-file "Elisp.org")= (Elisp form to evaluate) | -| | =elisp:org-agenda= (interactive Elisp command) | +| Link Type | Example | +|------------+--------------------------------------------------------------------| +| http | =http://staff.science.uva.nl/c.dominik/= | +| https | =https://orgmode.org/= | +| doi | =doi:10.1000/182= | +| file | =file:/home/dominik/images/jupiter.jpg= | +| | =/home/dominik/images/jupiter.jpg= (same as above) | +| | =file:papers/last.pdf= | +| | =./papers/last.pdf= (same as above) | +| | =file:/ssh:me@some.where:papers/last.pdf= (remote) | +| | =/ssh:me@some.where:papers/last.pdf= (same as above) | +| | =file:sometextfile::NNN= (jump to line number) | +| | =file:projects.org= | +| | =file:projects.org::some words= (text search)[fn:12] | +| | =file:projects.org::*task title= (headline search) | +| | =file:projects.org::#custom-id= (headline search) | +| attachment | =attachment:projects.org= | +| | =attachment:projects.org::some words= (text search) | +| docview | =docview:papers/last.pdf::NNN= | +| id | =id:B7423F4D-2E8A-471B-8810-C40F074717E9= | +| | =id:B7423F4D-2E8A-471B-8810-C40F074717E9::*task= (headline search) | +| news | =news:comp.emacs= | +| mailto | =mailto:adent@galaxy.net= | +| mhe | =mhe:folder= (folder link) | +| | =mhe:folder#id= (message link) | +| rmail | =rmail:folder= (folder link) | +| | =rmail:folder#id= (message link) | +| gnus | =gnus:group= (group link) | +| | =gnus:group#id= (article link) | +| bbdb | =bbdb:R.*Stallman= (record with regexp) | +| irc | =irc:/irc.com/#emacs/bob= | +| help | =help:org-store-link= | +| info | =info:org#External links= | +| shell | =shell:ls *.org= | +| elisp | =elisp:(find-file "Elisp.org")= (Elisp form to evaluate) | +| | =elisp:org-agenda= (interactive Elisp command) | #+cindex: VM links #+cindex: Wanderlust links @@ -3465,8 +3467,9 @@ current buffer: - /Org mode buffers/ :: For Org files, if there is a =<<target>>= at point, the link points - to the target. Otherwise it points to the current headline, which - is also the description. + to the target. If there is a named block (using =#+name:=) at + point, the link points to that name. Otherwise it points to the + current headline, which is also the description. #+vindex: org-id-link-to-org-use-id #+cindex: @samp{CUSTOM_ID}, property @@ -3484,6 +3487,30 @@ current buffer: timestamp, depending on ~org-id-method~. Later, when inserting the link, you need to decide which one to use. + #+vindex: org-id-link-consider-parent-id + #+vindex: org-id-link-use-context + When ~org-id-link-consider-parent-id~ is ~t~ (and + ~org-link-context-for-files~ and ~org-id-link-use-context~ are both + enabled), parent =ID= properties are considered. This allows + linking to specific targets, named blocks, or headlines (which may + not have a globally unique =ID= themselves) within the context of a + parent headline or file which does. + + For example, given this org file with those variables set: + + #+begin_src org + ,* Parent + :PROPERTIES: + :ID: abc + :END: + ,** Child 1 + ,** Child 2 + #+end_src + + Storing a link with point at "Child 1" will produce a link + =<id:abc::*Child 1>=, which precisely links to the "Child 1" + headline even though it does not have its own ID. + - /Email/News clients: VM, Rmail, Wanderlust, MH-E, Gnus/ :: #+vindex: org-link-email-description-format @@ -3763,7 +3790,9 @@ the link completion function like this: :ALT_TITLE: Search Options :END: #+cindex: search option in file links +#+cindex: search option in id links #+cindex: file links, searching +#+cindex: id links, searching #+cindex: attachment links, searching File links can contain additional information to make Emacs jump to a @@ -3775,8 +3804,8 @@ example, when the command ~org-store-link~ creates a link (see line as a search string that can be used to find this line back later when following the link with {{{kbd(C-c C-o)}}}. -Note that all search options apply for Attachment links in the same -way that they apply for File links. +Note that all search options apply for Attachment and ID links in the +same way that they apply for File links. Here is the syntax of the different ways to attach a search to a file link, together with explanations for each: @@ -21367,7 +21396,7 @@ The following =ol-man.el= file implements it PATH should be a topic that can be thrown at the man command." (funcall org-man-command path)) -(defun org-man-store-link () +(defun org-man-store-link (&optional _interactive?) "Store a link to a man page." (when (memq major-mode '(Man-mode woman-mode)) ;; This is a man page, we do make this link. @@ -21427,13 +21456,15 @@ A review of =ol-man.el=: For example, ~org-man-store-link~ is responsible for storing a link when ~org-store-link~ (see [[*Handling Links]]) is called from a buffer - displaying a man page. It first checks if the major mode is - appropriate. If check fails, the function returns ~nil~, which - means it isn't responsible for creating a link to the current - buffer. Otherwise the function makes a link string by combining - the =man:= prefix with the man topic. It also provides a default - description. The function ~org-insert-link~ can insert it back - into an Org buffer later on. + displaying a man page. It is passed an argument ~interactive?~ + which this function does not use, but other store functions use to + behave differently when a link is stored interactively by the user. + It first checks if the major mode is appropriate. If check fails, + the function returns ~nil~, which means it isn't responsible for + creating a link to the current buffer. Otherwise the function + makes a link string by combining the =man:= prefix with the man + topic. It also provides a default description. The function + ~org-insert-link~ can insert it back into an Org buffer later on. ** Adding Export Backends :PROPERTIES: diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS index 9e68bcdcb..84bbc5243 100644 --- a/etc/ORG-NEWS +++ b/etc/ORG-NEWS @@ -390,6 +390,14 @@ timestamp object. Possible values: ~timerange~, ~daterange~, ~nil~. ~org-element-timestamp-interpreter~ takes into account this property and returns an appropriate timestamp string. +**** =org-link= store functions are passed an ~interactive?~ argument + +The ~:store:~ functions set for link types using +~org-link-set-parameters~ are now passed an ~interactive?~ argument, +indicating whether ~org-store-link~ was called interactively. + +Existing store functions will continue to work. + *** ~org-priority=show~ command no longer adjusts for scheduled/deadline In agenda views, ~org-priority=show~ command previously displayed the @@ -468,6 +476,27 @@ The change is breaking when ~org-use-property-inheritance~ is set to ~t~. *** ~org-babel-lilypond-compile-lilyfile~ ignores optional second argument The =TEST= parameter is better served by Emacs debugging tools. + +*** ~org-id-store-link~ now adds search strings for precise link targets + +This new behaviour can be disabled generally by setting +~org-id-link-use-context~ to ~nil~, or the setting can be toggled for +a single call to ~org-store-link~ with a universal argument. + +When using this feature, IDs should not include =::=, which is used in +links to indicate the start of the search string. For backwards +compability, existing IDs including =::= will still be matched (but +cannot be used together with precise link targets). An org-lint +checker has been added to warn about this. + +*** ~org-store-link~ behaviour storing additional =CUSTOM_ID= links has changed + +As well as an =id:= link, ~org-store-link~ stores an additional "human +readable" link using a node's =CUSTOM_ID= property, if available. +This behaviour has been expanded to store an additional =CUSTOM_ID= +link when storing any type of external link type in an Org file, not +just =id:= links. + ** New and changed options *** ~repeated-after-deadline~ value of ~org-agenda-skip-scheduled-repeats-after-deadline~ is moved to a new customization @@ -743,6 +772,35 @@ This option starts the agenda to automatically include archives, propagating the value for this variable to ~org-agenda-archives-mode~. For acceptable values and their meaning, see the value of that variable. +*** New option ~org-id-link-consider-parent-id~ to allow =id:= links to parent headlines + +For =id:= links, when this option is enabled, ~org-store-link~ will +look for ids from parent/ancestor headlines, if the current headline +does not have an id. + +Combined with the new ability for =id:= links to use search strings +for precise link targets (when =org-id-link-use-context= is =t=, which +is the default), this allows linking to specific headlines without +requiring every headline to have an id property, as long as the +headline is unique within a subtree that does have an id property. + +For example, given this org file: + +#+begin_src org +,* Parent +:PROPERTIES: +:ID: abc +:END: +,** Child 1 +,** Child 2 +#+end_src + +Storing a link with point at "Child 1" will produce a link +=<id:abc::*Child 1>=, which precisely links to the "Child 1" headline +even though it does not have its own ID. By giving files top-level id +properties, links to headlines in the file can also be made more +robust by using the file id instead of the file path. + ** New features *** =ob-plantuml.el=: Support tikz file format output @@ -1039,6 +1097,12 @@ A numeric value forces a heading at that level to be inserted. For backwards compatibility, non-numeric non-nil values insert level 1 headings as before. +*** New optional argument for ~org-id-get~ + +New optional argument =INHERIT= means inherited ID properties from +parent entries are considered when getting an entry's ID (see +~org-id-link-consider-parent-id~ option). + ** Miscellaneous *** =org-crypt.el= now applies initial visibility settings to decrypted entries diff --git a/lisp/ol-bbdb.el b/lisp/ol-bbdb.el index be3924fc9..6ea060f70 100644 --- a/lisp/ol-bbdb.el +++ b/lisp/ol-bbdb.el @@ -226,7 +226,7 @@ date year)." ;;; Implementation -(defun org-bbdb-store-link () +(defun org-bbdb-store-link (&optional _interactive?) "Store a link to a BBDB database entry." (when (eq major-mode 'bbdb-mode) ;; This is BBDB, we make this link! diff --git a/lisp/ol-bibtex.el b/lisp/ol-bibtex.el index c5a950e2d..38468f32f 100644 --- a/lisp/ol-bibtex.el +++ b/lisp/ol-bibtex.el @@ -507,7 +507,7 @@ ARG, when non-nil, is a universal prefix argument. See `org-open-file' for details." (org-link-open-as-file path arg)) -(defun org-bibtex-store-link () +(defun org-bibtex-store-link (&optional _interactive?) "Store a link to a BibTeX entry." (when (eq major-mode 'bibtex-mode) (let* ((search (org-create-file-search-in-bibtex)) diff --git a/lisp/ol-docview.el b/lisp/ol-docview.el index b31f1ce5e..0907ddee1 100644 --- a/lisp/ol-docview.el +++ b/lisp/ol-docview.el @@ -83,7 +83,7 @@ (error "No such file: %s" path)) (when page (doc-view-goto-page page)))) -(defun org-docview-store-link () +(defun org-docview-store-link (&optional _interactive?) "Store a link to a docview buffer." (when (eq major-mode 'doc-view-mode) ;; This buffer is in doc-view-mode diff --git a/lisp/ol-eshell.el b/lisp/ol-eshell.el index 2c7ec6bef..595dd0ee0 100644 --- a/lisp/ol-eshell.el +++ b/lisp/ol-eshell.el @@ -60,7 +60,7 @@ followed by a colon." (insert command) (eshell-send-input))) -(defun org-eshell-store-link () +(defun org-eshell-store-link (&optional _interactive?) "Store eshell link. When opened, the link switches back to the current eshell buffer and the current working directory." diff --git a/lisp/ol-eww.el b/lisp/ol-eww.el index 40b820d2b..c13dbf339 100644 --- a/lisp/ol-eww.el +++ b/lisp/ol-eww.el @@ -62,7 +62,7 @@ "Open URL with Eww in the current buffer." (eww url)) -(defun org-eww-store-link () +(defun org-eww-store-link (&optional _interactive?) "Store a link to the url of an EWW buffer." (when (eq major-mode 'eww-mode) (org-link-store-props diff --git a/lisp/ol-gnus.el b/lisp/ol-gnus.el index e105fdb2c..b9ee8683f 100644 --- a/lisp/ol-gnus.el +++ b/lisp/ol-gnus.el @@ -123,7 +123,7 @@ If `org-store-link' was called with a prefix arg the meaning of (url-encode-url message-id)) (concat "gnus:" group "#" message-id))) -(defun org-gnus-store-link () +(defun org-gnus-store-link (&optional _interactive?) "Store a link to a Gnus folder or message." (pcase major-mode (`gnus-group-mode diff --git a/lisp/ol-info.el b/lisp/ol-info.el index 0edf9a13f..6062cab34 100644 --- a/lisp/ol-info.el +++ b/lisp/ol-info.el @@ -50,7 +50,7 @@ :insert-description #'org-info-description-as-command) ;; Implementation -(defun org-info-store-link () +(defun org-info-store-link (&optional _interactive?) "Store a link to an Info file and node." (when (eq major-mode 'Info-mode) (let ((link (concat "info:" diff --git a/lisp/ol-irc.el b/lisp/ol-irc.el index 78c4884b0..b263e52db 100644 --- a/lisp/ol-irc.el +++ b/lisp/ol-irc.el @@ -103,7 +103,7 @@ attributes that are found." parts)) ;;;###autoload -(defun org-irc-store-link () +(defun org-irc-store-link (&optional _interactive?) "Dispatch to the appropriate function to store a link to an IRC session." (cond ((eq major-mode 'erc-mode) diff --git a/lisp/ol-man.el b/lisp/ol-man.el index e3f13815e..42aacea81 100644 --- a/lisp/ol-man.el +++ b/lisp/ol-man.el @@ -82,7 +82,7 @@ matched strings in man buffer." (set-window-point window point) (set-window-start window point))))))) -(defun org-man-store-link () +(defun org-man-store-link (&optional _interactive?) "Store a link to a README file." (when (memq major-mode '(Man-mode woman-mode)) ;; This is a man page, we do make this link diff --git a/lisp/ol-mhe.el b/lisp/ol-mhe.el index 106cfedc9..a32481324 100644 --- a/lisp/ol-mhe.el +++ b/lisp/ol-mhe.el @@ -80,7 +80,7 @@ supported by MH-E." (org-link-set-parameters "mhe" :follow #'org-mhe-open :store #'org-mhe-store-link) ;; Implementation -(defun org-mhe-store-link () +(defun org-mhe-store-link (&optional _interactive?) "Store a link to an MH-E folder or message." (when (or (eq major-mode 'mh-folder-mode) (eq major-mode 'mh-show-mode)) diff --git a/lisp/ol-rmail.el b/lisp/ol-rmail.el index f6031ab52..f1f753b6f 100644 --- a/lisp/ol-rmail.el +++ b/lisp/ol-rmail.el @@ -51,7 +51,7 @@ :store #'org-rmail-store-link) ;; Implementation -(defun org-rmail-store-link () +(defun org-rmail-store-link (&optional _interactive?) "Store a link to an Rmail folder or message." (when (or (eq major-mode 'rmail-mode) (eq major-mode 'rmail-summary-mode)) diff --git a/lisp/ol.el b/lisp/ol.el index f8d911127..762d7a0d8 100644 --- a/lisp/ol.el +++ b/lisp/ol.el @@ -63,7 +63,6 @@ (declare-function org-find-property "org" (property &optional value)) (declare-function org-get-heading "org" (&optional no-tags no-todo no-priority no-comment)) (declare-function org-id-find-id-file "org-id" (id)) -(declare-function org-id-store-link "org-id" ()) (declare-function org-insert-heading "org" (&optional arg invisible-ok top)) (declare-function org-load-modules-maybe "org" (&optional force)) (declare-function org-mark-ring-push "org" (&optional pos buffer)) @@ -818,6 +817,74 @@ spec." (org-with-point-at (car region) (not (org-in-regexp org-link-any-re)))) +(defun org-link--try-link-store-functions (interactive?) + "Try storing external links, prompting if more than one is possible. + +Each function returned by `org-store-link-functions' is called in +turn. If multiple functions return non-nil, prompt for which +link should be stored. + +Argument INTERACTIVE? indicates whether `org-store-link' was +called interactively and is passed to the link store functions. + +Return t when a link has been stored in `org-link-store-props'." + (let ((results-alist nil)) + (dolist (f (org-store-link-functions)) + (when (condition-case nil + (funcall f interactive?) + ;; FIXME: The store function used (< Org 9.7) to accept + ;; no arguments; provide backward compatibility support + ;; for them. + (wrong-number-of-arguments + (funcall f))) + ;; FIXME: return value is not link's plist, so we store the + ;; new value before it is modified. It would be cleaner to + ;; ask store link functions to return the plist instead. + (push (cons f (copy-sequence org-store-link-plist)) + results-alist))) + (pcase results-alist + (`nil nil) + (`((,_ . ,_)) t) ;single choice: nothing to do + (`((,name . ,_) . ,_) + ;; Reinstate link plist associated to the chosen + ;; function. + (apply #'org-link-store-props + (cdr (assoc-string + (completing-read + (format "Store link with (default %s): " name) + (mapcar #'car results-alist) + nil t nil nil (symbol-name name)) + results-alist))) + t)))) + +(defun org-link--add-to-stored-links (link desc) + "Add LINK to `org-stored-links' with description DESC." + (cond + ((not (member (list link desc) org-stored-links)) + (push (list link desc) org-stored-links) + (message "Stored: %s" (or desc link))) + ((equal (list link desc) (car org-stored-links)) + (message "This link has already been stored")) + (t + (setq org-stored-links + (delete (list link desc) org-stored-links)) + (push (list link desc) org-stored-links) + (message "Link moved to front: %s" (or desc link))))) + +(defun org-link--file-link-to-here () + "Return as (LINK . DESC) a file link with search string to here." + (let ((link (concat "file:" + (abbreviate-file-name + (buffer-file-name (buffer-base-buffer))))) + desc) + (when org-link-context-for-files + (pcase (org-link-precise-link-target) + (`nil nil) + (`(,search-string ,search-desc ,_position) + (setq link (format "%s::%s" link search-string)) + (setq desc search-desc)))) + (cons link desc))) + \f ;;; Public API @@ -1044,7 +1111,9 @@ LINK is escaped with backslashes for inclusion in buffer." "List of functions that are called to create and store a link. The functions are defined in the `:store' property of -`org-link-parameters'. +`org-link-parameters'. Each function should accept an argument +INTERACTIVE? which indicates whether the user has initiated +`org-store-link' interactively. Each function will be called in turn until one returns a non-nil value. Each function should check if it is responsible for @@ -1163,7 +1232,7 @@ Optional argument ARG is passed to `org-open-file' when S is a (`nil (user-error "No valid link in %S" s)) (link (org-link-open link arg)))) -(defun org-link-search (s &optional avoid-pos stealth) +(defun org-link-search (s &optional avoid-pos stealth new-heading-container) "Search for a search string S in the accessible part of the buffer. If S starts with \"#\", it triggers a custom ID search. @@ -1183,6 +1252,13 @@ When optional argument STEALTH is non-nil, do not modify visibility around point, thus ignoring `org-show-context-detail' variable. +When optional argument NEW-HEADING-CONTAINER is an element, any +new heading that is created (see +`org-link-search-must-match-exact-headline') will be added as a +subheading of NEW-HEADING-CONTAINER. Otherwise, new headings are +created at level 1 at the end of the accessible part of the +buffer. + Search is case-insensitive and ignores white spaces. Return type of matched result, which is either `dedicated' or `fuzzy'. Search respects buffer narrowing." @@ -1281,11 +1357,17 @@ respects buffer narrowing." ((and (derived-mode-p 'org-mode) (eq org-link-search-must-match-exact-headline 'query-to-create) (yes-or-no-p "No match - create this as a new heading? ")) - (goto-char (point-max)) - (unless (bolp) (newline)) - (org-insert-heading nil t t) - (insert s "\n") - (forward-line -1)) + (let* ((new-heading-position (if new-heading-container + (- (org-element-end new-heading-container) 1) + (point-max))) + (new-heading-level (if new-heading-container + (+ 1 (org-element-property :level new-heading-container)) + 1))) + (goto-char new-heading-position) + (unless (bolp) (newline)) + (org-insert-heading nil t new-heading-level) + (insert (if starred (substring s 1) s) "\n") + (forward-line -1))) ;; Only headlines are looked after. No need to process ;; further: throw an error. ((and (derived-mode-p 'org-mode) @@ -1335,6 +1417,70 @@ priority cookie or tag." (org-link--normalize-string (or string (org-get-heading t t t t))))) +(defun org-link-precise-link-target () + "Determine search string and description for storing a link. + +If a search string (see `org-link-search') is found, return +list (SEARCH-STRING DESC POSITION). Otherwise, return nil. + +If there is an active region, the contents (or a part of it, see +`org-link-context-for-files') is used as the search string. + +In Org buffers, if point is at a named element (such as a source +block), the name is used for the search string. If at a heading, +its CUSTOM_ID is used to form a search string of the form +\"#id\", if present, otherwise the current heading text is used +in the form \"*Heading\". + +If none of those finds a suitable search string, the current line +is used as the search string. + +The description DESC is nil (meaning the user will be prompted +for a description when inserting the link) for search strings +based on a region or the current line. For other cases, DESC is +a cleaned-up version of the name or heading at point. + +POSITION is the buffer position at which the search string +matches." + (let* ((region (org-link--context-from-region)) + (result + (cond + (region + (list (org-link--normalize-string region t) + nil + (region-beginning))) + + ((derived-mode-p 'org-mode) + (let* ((element (org-element-at-point)) + (name (org-element-property :name element)) + (heading (org-element-lineage element '(headline inlinetask) t)) + (custom-id (org-entry-get heading "CUSTOM_ID"))) + (cond + (name + (list name + name + (org-element-begin element))) + ((org-before-first-heading-p) + (list (org-link--normalize-string (org-current-line-string) t) + nil + (line-beginning-position))) + (heading + (list (if custom-id (concat "#" custom-id) + (org-link-heading-search-string)) + (org-link--normalize-string + (org-get-heading t t t t)) + (org-element-begin heading)))))) + + ;; Not in an org-mode buffer, no region + (t + (list (org-link--normalize-string (org-current-line-string) t) + nil + (line-beginning-position)))))) + + ;; Only use search option if there is some text. + (when (org-string-nw-p (car result)) + result))) + (defun org-link-open-as-file (path in-emacs) "Pretend PATH is a file name and open it. @@ -1407,7 +1553,7 @@ PATH is a symbol name, as a string." ((and (pred boundp) variable) (describe-variable variable)) (name (user-error "Unknown function or variable: %s" name)))) -(defun org-link--store-help () +(defun org-link--store-help (&optional _interactive?) "Store \"help\" type link." (when (eq major-mode 'help-mode) (let ((symbol @@ -1542,7 +1688,12 @@ prefix ARG forces storing a link for each line in the active region. Assume the function is called interactively if INTERACTIVE? is -non-nil." +non-nil. + +In Org buffers, an additional \"human-readable\" simple file link +is stored as an alternative to persistent org-id or other links, +if at a heading with a CUSTOM_ID property or an element with a +NAME." (interactive "P\np") (org-load-modules-maybe) (if (and (equal arg '(64)) (org-region-active-p)) @@ -1557,36 +1708,19 @@ non-nil." (move-beginning-of-line 2) (set-mark (point))))) (setq org-store-link-plist nil) - (let (link cpltxt desc search custom-id agenda-link) ;; description + ;; Negate `org-context-in-file-links' when given a single universal arg. + (let ((org-link-context-for-files (org-xor org-link-context-for-files + (equal arg '(4)))) + link cpltxt desc search agenda-link) ;; description (cond ;; Store a link using an external link type, if any function is - ;; available. If more than one can generate a link from current - ;; location, ask which one to use. + ;; available, unless external link types are skipped for this + ;; call using two universal args. If more than one function + ;; can generate a link from current location, ask the user + ;; which one to use. ((and (not (equal arg '(16))) - (let ((results-alist nil)) - (dolist (f (org-store-link-functions)) - (when (funcall f) - ;; XXX: return value is not link's plist, so we - ;; store the new value before it is modified. It - ;; would be cleaner to ask store link functions to - ;; return the plist instead. - (push (cons f (copy-sequence org-store-link-plist)) - results-alist))) - (pcase results-alist - (`nil nil) - (`((,_ . ,_)) t) ;single choice: nothing to do - (`((,name . ,_) . ,_) - ;; Reinstate link plist associated to the chosen - ;; function. - (apply #'org-link-store-props - (cdr (assoc-string - (completing-read - (format "Store link with (default %s): " name) - (mapcar #'car results-alist) - nil t nil nil (symbol-name name)) - results-alist))) - t)))) - (setq link (plist-get org-store-link-plist :link)) + (org-link--try-link-store-functions interactive?)) + (setq link (plist-get org-store-link-plist :link)) ;; If store function actually set `:description' property, use ;; it, even if it is nil. Otherwise, fallback to nil (ask user). (setq desc (plist-get org-store-link-plist :description))) @@ -1637,6 +1771,7 @@ non-nil." (org-with-point-at m (setq agenda-link (org-store-link nil interactive?)))))) + ;; Calendar mode ((eq major-mode 'calendar-mode) (let ((cd (calendar-cursor-to-date))) (setq link @@ -1645,6 +1780,7 @@ non-nil." (org-encode-time 0 0 0 (nth 1 cd) (nth 0 cd) (nth 2 cd)))) (org-link-store-props :type "calendar" :date cd))) + ;; Image mode ((eq major-mode 'image-mode) (setq cpltxt (concat "file:" (abbreviate-file-name buffer-file-name)) @@ -1662,15 +1798,22 @@ non-nil." (setq cpltxt (concat "file:" file) link cpltxt))) + ;; Try `org-create-file-search-functions`. If any are + ;; successful, create a file link to the current buffer with + ;; the provided search string. (sets `link` and `cpltxt` to + ;; the same thing; it looks like the intention originally was + ;; that cpltxt was a description, which might have been set by + ;; the search-function (removed in switch to lexical binding)). ((setq search (run-hook-with-args-until-success 'org-create-file-search-functions)) (setq link (concat "file:" (abbreviate-file-name buffer-file-name) "::" search)) (setq cpltxt (or link))) ;; description + ;; Main logic for storing built-in link types in org-mode + ;; buffers ((and (buffer-file-name (buffer-base-buffer)) (derived-mode-p 'org-mode)) (org-with-limited-levels - (setq custom-id (org-entry-get nil "CUSTOM_ID")) (cond ;; Store a link using the target at point ((org-in-regexp "[^<]<<\\([^<>]+\\)>>[^>]" 1) @@ -1684,74 +1827,21 @@ non-nil." ;; links. Maybe the case of identical target and ;; description should be handled by `org-insert-link'. cpltxt nil - desc nil - ;; Do not append #CUSTOM_ID link below. - custom-id nil)) - ((and (featurep 'org-id) - (or (eq org-id-link-to-org-use-id t) - (and interactive? - (or (eq org-id-link-to-org-use-id 'create-if-interactive) - (and (eq org-id-link-to-org-use-id - 'create-if-interactive-and-no-custom-id) - (not custom-id)))) - (and org-id-link-to-org-use-id (org-entry-get nil "ID")))) - ;; Store a link using the ID at point - (setq link (condition-case nil - (prog1 (org-id-store-link) - (setq desc (plist-get org-store-link-plist :description))) - (error - ;; Probably before first headline, link only to file - (concat "file:" - (abbreviate-file-name - (buffer-file-name (buffer-base-buffer)))))))) - (t + desc nil)) + (t ;; Just link to current headline. - (setq cpltxt (concat "file:" - (abbreviate-file-name - (buffer-file-name (buffer-base-buffer))))) - ;; Add a context search string. - (when (org-xor org-link-context-for-files (equal arg '(4))) - (let* ((element (org-element-at-point)) - (name (org-element-property :name element)) - (context - (cond - ((let ((region (org-link--context-from-region))) - (and region (org-link--normalize-string region t)))) - (name) - ((org-before-first-heading-p) - (org-link--normalize-string (org-current-line-string) t)) - (t (org-link-heading-search-string))))) - (when (org-string-nw-p context) - (setq cpltxt (format "%s::%s" cpltxt context)) - (setq desc - (or name - ;; Although description is not a search - ;; string, use `org-link--normalize-string' - ;; to prettify it (contiguous white spaces) - ;; and remove volatile contents (statistics - ;; cookies). - (and (not (org-before-first-heading-p)) - (org-link--normalize-string - (org-get-heading t t t t))) - "NONE"))))) - (setq link cpltxt))))) + (let ((here (org-link--file-link-to-here))) + (setq cpltxt (car here)) + (setq desc (cdr here))) + (setq link cpltxt))))) + ;; Buffer linked to file, but not an org-mode buffer. ((buffer-file-name (buffer-base-buffer)) ;; Just link to this file here. - (setq cpltxt (concat "file:" - (abbreviate-file-name - (buffer-file-name (buffer-base-buffer))))) - ;; Add a context search string. - (when (org-xor org-link-context-for-files (equal arg '(4))) - (let ((context (org-link--normalize-string - (or (org-link--context-from-region) - (org-current-line-string)) - t))) - ;; Only use search option if there is some text. - (when (org-string-nw-p context) - (setq cpltxt (format "%s::%s" cpltxt context)) - (setq desc "NONE")))) - (setq link cpltxt)) + (let ((here (org-link--file-link-to-here))) + (setq cpltxt (car here)) + (setq desc (cdr here))) + (setq link cpltxt)) (interactive? (user-error "No method for storing a link from this buffer")) @@ -1767,24 +1857,18 @@ non-nil." ;; Store and return the link (if (not (and interactive? link)) (or agenda-link (and link (org-link-make-string link desc))) - (dotimes (_ (if custom-id 2 1)) ; Store 2 links when CUSTOM-ID is non-nil. - (cond - ((not (member (list link desc) org-stored-links)) - (push (list link desc) org-stored-links) - (message "Stored: %s" (or desc link))) - ((equal (list link desc) (car org-stored-links)) - (message "This link has already been stored")) - (t - (setq org-stored-links - (delete (list link desc) org-stored-links)) - (push (list link desc) org-stored-links) - (message "Link moved to front: %s" (or desc link)))) - (when custom-id - (setq link (concat "file:" - (abbreviate-file-name - (buffer-file-name (buffer-base-buffer))) - "::#" custom-id)))) - (car org-stored-links))))) + (org-link--add-to-stored-links link desc) + ;; In org buffers, store an additional "human-readable" link + ;; using custom id, if available. + (when (and (buffer-file-name (buffer-base-buffer)) + (derived-mode-p 'org-mode) + (org-entry-get nil "CUSTOM_ID")) + (let ((here (org-link--file-link-to-here))) + (setq link (car here)) + (setq desc (cdr here))) + (unless (equal (list link desc) (car org-stored-links)) + (org-link--add-to-stored-links link desc))) + (car org-stored-links))))) ;;;###autoload (defun org-insert-link (&optional complete-file link-location description) diff --git a/lisp/org-id.el b/lisp/org-id.el index 8647a57cc..58d51deca 100644 --- a/lisp/org-id.el +++ b/lisp/org-id.el @@ -129,6 +129,46 @@ nil Never use an ID to make a link, instead link using a text search for (const :tag "Only use existing" use-existing) (const :tag "Do not use ID to create link" nil))) +(defcustom org-id-link-consider-parent-id nil + "Non-nil means storing a link to an Org entry considers inherited IDs. + +When this option is non-nil and `org-id-link-use-context' is +enabled, ID properties inherited from parent entries will be +considered when storing an ID link. If no ID is found in this +way, a new one may be created as normal (see +`org-id-link-to-org-use-id'). + +For example, given this org file: + +* Parent +:PROPERTIES: +:ID: abc +:END: +** Child 1 +** Child 2 + +With `org-id-link-consider-parent-id' and +`org-id-link-use-context' both enabled, storing a link with point +at \"Child 1\" will produce a link \"<id:abc::*Child 1>\". This +allows linking to uniquely-named sub-entries within a parent +entry with an ID, without requiring every sub-entry to have its +own ID." + :group 'org-link-store + :group 'org-id + :package-version '(Org . "9.7") + :type 'boolean) + +(defcustom org-id-link-use-context t + "Non-nil means enables search string context in org-id links. + +Search strings are added by `org-id-store-link' when both the +general option `org-link-context-for-files' and the org-id option +`org-id-link-use-context' are non-nil." + :group 'org-link-store + :group 'org-id + :package-version '(Org . "9.7") + :type 'boolean) + (defcustom org-id-uuid-program "uuidgen" "The uuidgen program." :group 'org-id @@ -280,15 +320,21 @@ This is useful when working with contents in a temporary buffer that will be copied back to the original.") ;;;###autoload -(defun org-id-get (&optional epom create prefix) - "Get the ID property of the entry at EPOM. -EPOM is an element, marker, or buffer position. -If EPOM is nil, refer to the entry at point. -If the entry does not have an ID, the function returns nil. -However, when CREATE is non-nil, create an ID if none is present already. -PREFIX will be passed through to `org-id-new'. -In any case, the ID of the entry is returned." - (let ((id (org-entry-get epom "ID"))) +(defun org-id-get (&optional epom create prefix inherit) + "Get the ID of the entry at EPOM. + +EPOM is an element, marker, or buffer position. If EPOM is nil, +refer to the entry at point. + +If INHERIT is non-nil, ID properties inherited from parent +entries are considered. Otherwise, only ID properties on the +entry itself are considered. + +When CREATE is nil, return the ID of the entry if found, +otherwise nil. When CREATE is non-nil, create an ID if none has +been found, and return the new ID. PREFIX will be passed through +to `org-id-new'." + (let ((id (org-entry-get epom "ID" (and inherit t)))) (cond ((and id (stringp id) (string-match "\\S-" id)) id) @@ -700,21 +746,56 @@ optional argument MARKERP, return the position as a new marker." ;; id link type -;; Calling the following function is hard-coded into `org-store-link', -;; so we do have to add it to `org-store-link-functions'. +(defun org-id--get-id-to-store-link (&optional create) + "Get or create the relevant ID for storing a link. + +Optional argument CREATE is passed to `org-id-get'. + +Inherited IDs are only considered when +`org-id-link-consider-parent-id', `org-id-link-use-context' and +`org-link-context-for-files' are all enabled, since inherited IDs +are confusing without the additional search string context. + +Note that this function resets the +`org-entry-property-inherited-from' marker: it will either point +to nil (if the id was not inherited) or to the point it was +inherited from." + (let* ((inherit-id (and org-id-link-consider-parent-id + org-id-link-use-context + org-link-context-for-files))) + (move-marker org-entry-property-inherited-from nil) + (org-id-get nil create nil inherit-id))) ;;;###autoload (defun org-id-store-link () "Store a link to the current entry, using its ID. -If before first heading store first title-keyword as description -or filename if no title." +The link description is based on the heading, or if before the +first heading, the title keyword if available, or else the +filename. + +When `org-link-context-for-files' and `org-id-link-use-context' +are non-nil, add a search string to the link. The link +description is then based on the search string target. + +When in addition `org-id-link-consider-parent-id' is non-nil, the +ID can be inherited from a parent entry, with the search string +used to still link to the current location." (interactive) - (when (and (buffer-file-name (buffer-base-buffer)) (derived-mode-p 'org-mode)) - (let* ((link (concat "id:" (org-id-get-create))) + (when (and (buffer-file-name (buffer-base-buffer)) + (derived-mode-p 'org-mode)) + ;; Get the precise target first, in case looking for an id causes + ;; a properties drawer to be added at the current location. + (let* ((precise-target (and org-link-context-for-files + org-id-link-use-context + (org-link-precise-link-target))) + (link (concat "id:" (org-id--get-id-to-store-link 'create))) + (id-location (or (and org-entry-property-inherited-from + (marker-position org-entry-property-inherited-from)) + (save-excursion (org-back-to-heading-or-point-min t) (point)))) (case-fold-search nil) (desc (save-excursion - (org-back-to-heading-or-point-min t) + (goto-char id-location) (cond ((org-before-first-heading-p) (let ((keywords (org-collect-keywords '("TITLE")))) (if keywords @@ -726,14 +807,59 @@ or filename if no title." (match-string 4) (match-string 0))) (t link))))) + ;; Precise targets should be after id-location to avoid + ;; duplicating the current headline as a search string + (when (and precise-target + (> (nth 2 precise-target) id-location)) + (setq link (concat link "::" (nth 0 precise-target))) + (setq desc (nth 1 precise-target))) (org-link-store-props :link link :description desc :type "id") link))) -(defun org-id-open (id _) - "Go to the entry with id ID." - (org-mark-ring-push) - (let ((m (org-id-find id 'marker)) - cmd) +;;;###autoload +(defun org-id-store-link-maybe (&optional interactive?) + "Store a link to the current entry using its ID if enabled. + +The value of `org-id-link-to-org-use-id' determines whether an ID +link should be stored, using `org-id-store-link'. + +Assume the function is called interactively if INTERACTIVE? is +non-nil." + (when (and (buffer-file-name (buffer-base-buffer)) + (derived-mode-p 'org-mode) + (or (eq org-id-link-to-org-use-id t) + (and interactive? + (or (eq org-id-link-to-org-use-id 'create-if-interactive) + (and (eq org-id-link-to-org-use-id + 'create-if-interactive-and-no-custom-id) + (not (org-entry-get nil "CUSTOM_ID"))))) + ;; 'use-existing + (and org-id-link-to-org-use-id + (org-id--get-id-to-store-link)))) + (org-id-store-link))) + +(defun org-id-open (link _) + "Go to the entry indicated by id link LINK. + +The link can include a search string after \"::\", which is +passed to `org-link-search'. + +For backwards compatibility with IDs that contain \"::\", if no +match is found for the ID, the full link string including \"::\" +will be tried as an ID." + (let* ((option (and (string-match "::\\(.*\\)\\'" link) + (match-string 1 link))) + (id (if (not option) link + (substring link 0 (match-beginning 0)))) + m cmd) + (org-mark-ring-push) + (setq m (org-id-find id 'marker)) + (when (and (not m) option) + ;; Backwards compatibility: if id is not found, try treating + ;; whole link as an id. + (setq m (org-id-find link 'marker)) + (when m + (setq option nil))) (unless m (error "Cannot find entry with ID \"%s\"" id)) ;; Use a buffer-switching command in analogy to finding files @@ -750,9 +876,17 @@ or filename if no title." (funcall cmd (marker-buffer m))) (goto-char m) (move-marker m nil) + (when option + (save-restriction + (unless (org-before-first-heading-p) + (org-narrow-to-subtree)) + (org-link-search option nil nil + (org-element-lineage (org-element-at-point) 'headline t)))) (org-fold-show-context))) -(org-link-set-parameters "id" :follow #'org-id-open) +(org-link-set-parameters "id" + :follow #'org-id-open + :store #'org-id-store-link-maybe) (provide 'org-id) diff --git a/lisp/org-lint.el b/lisp/org-lint.el index 4d2a55d15..b23afcca3 100644 --- a/lisp/org-lint.el +++ b/lisp/org-lint.el @@ -65,6 +65,7 @@ ;; - special properties in properties drawers, ;; - obsolete syntax for properties drawers, ;; - invalid duration in EFFORT property, +;; - invalid ID property with a double colon, ;; - missing definition for footnote references, ;; - missing reference for footnote definitions, ;; - non-footnote definitions in footnote section, @@ -686,6 +687,16 @@ Use :header-args: instead" (list (org-element-begin p) (format "Invalid effort duration format: %S" value)))))))) +(defun org-lint-invalid-id-property (ast) + (org-element-map ast 'node-property + (lambda (p) + (when (equal "ID" (org-element-property :key p)) + (let ((value (org-element-property :value p))) + (and (org-string-nw-p value) + (string-match-p "::" value) + (list (org-element-begin p) + (format "IDs should not include \"::\": %S" value)))))))) + (defun org-lint-link-to-local-file (ast) (org-element-map ast 'link (lambda (l) @@ -1684,6 +1695,11 @@ AST is the buffer parse tree." #'org-lint-invalid-effort-property :categories '(properties)) +(org-lint-add-checker 'invalid-id-property + "Report search string delimiter \"::\" in ID property" + #'org-lint-invalid-id-property + :categories '(properties)) + (org-lint-add-checker 'undefined-footnote-reference "Report missing definition for footnote references" #'org-lint-undefined-footnote-reference diff --git a/testing/lisp/test-ol.el b/testing/lisp/test-ol.el index e0cec0854..3150b4e2f 100644 --- a/testing/lisp/test-ol.el +++ b/testing/lisp/test-ol.el @@ -381,6 +381,128 @@ See https://github.com/yantar92/org/issues/4." (equal (format "[[file:%s::*foo bar][foo bar]]" file file) (org-store-link nil))))))) +(ert-deftest test-org-link/precise-link-target () + "Test `org-link-precise-link-target` specifications." + (org-test-with-temp-text "* H1<point>\n* H2\n" + (should + (equal '("*H1" "H1" 1) + (org-link-precise-link-target)))) + (org-test-with-temp-text "* H1\n#+name: foo<point>\n#+begin_example\nhi\n#+end_example\n" + (should + (equal '("foo" "foo" 6) + (org-link-precise-link-target)))) + (org-test-with-temp-text "\nText<point>\n* H1\n" + (should + (equal '("Text" nil 2) + (org-link-precise-link-target)))) + (org-test-with-temp-text "\n<point>\n* H1\n" + (should + (equal nil (org-link-precise-link-target))))) + +(defmacro test-ol-stored-link-with-text (text &rest body) + "Return :link and :description from link stored in body." + (declare (indent 1)) + `(let (org-store-link-plist) + (org-test-with-temp-text-in-file ,text + ,@body + (list (plist-get org-store-link-plist :link) + (plist-get org-store-link-plist :description))))) + +(ert-deftest test-org-link/id-store-link () + "Test `org-id-store-link' specifications." + (let ((org-id-link-to-org-use-id nil)) + (should + (equal '(nil nil) + (test-ol-stored-link-with-text "* H1\n:PROPERTIES:\n:ID: abc\n:END:\n" + (org-id-store-link-maybe t))))) + ;; On a headline, link to that headline's ID. Use heading as the + ;; description of the link. + (let ((org-id-link-to-org-use-id t)) + (should + (equal '("id:abc" "H1") + (test-ol-stored-link-with-text "* H1\n:PROPERTIES:\n:ID: abc\n:END:\n" + (org-id-store-link-maybe t))))) + ;; Remove TODO keywords etc from description of the link. + (let ((org-id-link-to-org-use-id t)) + (should + (equal '("id:abc" "H1") + (test-ol-stored-link-with-text "* TODO [#A] H1 :tag:\n:PROPERTIES:\n:ID: abc\n:END:\n" + (org-id-store-link-maybe t))))) + ;; create-if-interactive + (let ((org-id-link-to-org-use-id 'create-if-interactive)) + (should + (equal '("id:abc" "H1") + (cl-letf (((symbol-function 'org-id-new) + (lambda (&rest _rest) "abc"))) + (test-ol-stored-link-with-text "* H1\n" + (org-id-store-link-maybe t))))) + (should + (equal '(nil nil) + (test-ol-stored-link-with-text "* H1\n" + (org-id-store-link-maybe nil))))) + ;; create-if-interactive-and-no-custom-id + (let ((org-id-link-to-org-use-id 'create-if-interactive-and-no-custom-id)) + (should + (equal '("id:abc" "H1") + (cl-letf (((symbol-function 'org-id-new) + (lambda (&rest _rest) "abc"))) + (test-ol-stored-link-with-text "* H1\n" + (org-id-store-link-maybe t))))) + (should + (equal '(nil nil) + (test-ol-stored-link-with-text "* H1\n:PROPERTIES:\n:CUSTOM_ID: xyz\n:END:\n" + (org-id-store-link-maybe t)))) + (should + (equal '(nil nil) + (test-ol-stored-link-with-text "* H1\n" + (org-id-store-link-maybe nil))))) + ;; use-context should have no effect when on the headline with an id + (let ((org-id-link-to-org-use-id t) + (org-id-link-use-context t)) + (should + (equal '("id:abc" "H2") + (test-ol-stored-link-with-text "* H1\n** H2<point>\n:PROPERTIES:\n:ID: abc\n:END:\n" + ;; simulate previously getting an inherited value + (move-marker org-entry-property-inherited-from 1) + (org-id-store-link-maybe t)))))) + +(ert-deftest test-org-link/id-store-link-using-parent () + "Test `org-id-store-link' specifications with `org-id-link-consider-parent-id` set." + ;; when using context to still find specific heading + (let ((org-id-link-to-org-use-id t) + (org-id-link-consider-parent-id t) + (org-id-link-use-context t)) + (should + (equal '("id:abc::*H2" "H2") + (test-ol-stored-link-with-text "* H1\n:PROPERTIES:\n:ID: abc\n:END:\n** H2\n<point>" + (org-id-store-link)))) + (should + (equal '("id:abc::name" "name") + (test-ol-stored-link-with-text "* H1\n:PROPERTIES:\n:ID: abc\n:END:\n\n#+name: name\n<point>#+begin_example\nhi\n#+end_example\n" + (org-id-store-link)))) + (should + (equal '("id:abc" "H1") + (test-ol-stored-link-with-text "* H1<point>\n:PROPERTIES:\n:ID: abc\n:END:\n** H2\n" + (org-id-store-link)))) + ;; should not use newly added ids as search string, e.g. in an empty file + (should + (let (name result) + (setq result + (cl-letf (((symbol-function 'org-id-new) + (lambda (&rest _rest) "abc"))) + (test-ol-stored-link-with-text "<point>" + (setq name (buffer-name)) + (org-id-store-link)))) + (equal `("id:abc" ,name) result)))) + ;; should not find targets in the next section + (let ((org-id-link-to-org-use-id 'use-existing) + (org-id-link-consider-parent-id t) + (org-id-link-use-context t)) + (should + (equal '(nil nil) + (test-ol-stored-link-with-text "* H1\n:PROPERTIES:\n:ID: abc\n:END:\n* H2\n** <point>Target\n" + (org-id-store-link-maybe t)))))) + \f ;;; Radio Targets -- 2.37.1 (Apple Git-137.1) ^ permalink raw reply related [flat|nested] 48+ messages in thread
* Re: [PATCH v2] org-id: allow using parent's existing id in links to headlines 2024-02-08 8:46 ` [PATCH v2] org-id: allow using parent's existing id in links to headlines Rick Lupton @ 2024-02-08 13:02 ` Ihor Radchenko 2024-02-08 22:30 ` Rick Lupton 0 siblings, 1 reply; 48+ messages in thread From: Ihor Radchenko @ 2024-02-08 13:02 UTC (permalink / raw) To: Rick Lupton; +Cc: Y. E. [-- Attachment #1: Type: text/plain, Size: 2958 bytes --] "Rick Lupton" <mail@ricklupton.name> writes: >> It looks like we cannot simply rely on narrowing to determine the >> created heading level. > > I think you're right. I have extended `org-link-search' to accept an optional argument describing the org element where newly created headings should go as subheadings. > > My thought was that this was not significantly more complicated than just passing the numeric level for new headings, but actually more flexible (e.g. you could if you wanted (with additional future elisp) create missing headings as part of a "To be filed" subtree within the file, rather than always at the end). > > Does that look ok? Yes. > [is it useful to keep attaching the unchanged first patch so they are available as a set?] Yes, it is useful. Makes it easier for my to batch-apply the patchset using https://git.kyleam.com/piem/about/ I have some thoughts about rewording your changes to the manual and ORG-NEWS. See the attached patch on top of yours. > Subject: [PATCH 1/2] lisp/org.el (org-insert-heading): allow specifying > heading level *Allow > * lisp/org.el (org-insert-heading): Change optional argument TOP to > LEVEL, accepting a number to force a specific heading level. > * testing/lisp/test-org.el (test-org/insert-heading): Add tests > * etc/ORG-NEWS: Document changes Please end sentences with period. > From d5759dd95bec88be38ddbde07fa4437c0528469a Mon Sep 17 00:00:00 2001 > From: Rick Lupton <mail@ricklupton.name> > Date: Sun, 19 Nov 2023 14:52:05 +0000 > Subject: [PATCH 2/2] org-id.el: Add search strings, inherit parent IDs > > ... > (org-link-try-link-store-functions): Extract logic to call external > link store functions. Pass them a new `interactive?' argument. > ... > (org-id-store-link): Consider IDs of parent headings as link targets > when current heading has no ID and `org-id-link-consider-parent-id' is > set. Add a search string to the link when enabled. Please, use two spaces between sentences. > * lisp/org-lint.el: add checker for "::" in ID properties. ... and start sentences from capital letter: *Add > -(defun org-link-search (s &optional avoid-pos stealth) > +(defun org-link-search (s &optional avoid-pos stealth new-heading-container) The new optional argument to a public function should be announced in ORG-NEWS. > + (new-heading-level (if new-heading-container > + (+ 1 (org-element-property :level new-heading-container)) What if new-heading-container is not a heading? > + 1))) > + (goto-char new-heading-position) This is err when container ends after narrowed region boundary. > +(defun org-link-precise-link-target () > ... > + (cond > + (name > + (list name > + name > + (org-element-begin element))) It would make sense to use #+caption as default description when available. [-- Warning: decoded text below may be mangled, UTF-8 assumed --] [-- Attachment #2: 0001-Amendments-to-org-manual.org-and-ORG-NEWS.patch --] [-- Type: text/x-patch, Size: 4327 bytes --] From 0f9a4503d95f7682229ae1c1ad8a4e2d069fc644 Mon Sep 17 00:00:00 2001 Message-ID: <0f9a4503d95f7682229ae1c1ad8a4e2d069fc644.1707396844.git.yantar92@posteo.net> From: Ihor Radchenko <yantar92@posteo.net> Date: Thu, 8 Feb 2024 13:53:44 +0100 Subject: [PATCH] Amendments to org-manual.org and ORG-NEWS --- doc/org-manual.org | 18 ++++++++++-------- etc/ORG-NEWS | 27 ++++++++++++++------------- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/doc/org-manual.org b/doc/org-manual.org index 49fce9113..e933a2d63 100644 --- a/doc/org-manual.org +++ b/doc/org-manual.org @@ -3489,14 +3489,16 @@ ** Handling Links #+vindex: org-id-link-consider-parent-id #+vindex: org-id-link-use-context - When ~org-id-link-consider-parent-id~ is ~t~ (and - ~org-link-context-for-files~ and ~org-id-link-use-context~ are both - enabled), parent =ID= properties are considered. This allows - linking to specific targets, named blocks, or headlines (which may - not have a globally unique =ID= themselves) within the context of a - parent headline or file which does. - - For example, given this org file with those variables set: + #+vindex: org-link-context-for-files + When ~org-id-link-consider-parent-id~ is ~t~[fn:: Also, + ~org-link-context-for-files~ and ~org-id-link-use-context~ should be + both enabled (which they are, by default).], parent =ID= properties + are considered. This allows linking to specific targets, named + blocks, or headlines (which may not have a globally unique =ID= + themselves) within the context of a parent headline or file which + does. + + For example, given this org file: #+begin_src org ,* Parent diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS index 84bbc5243..e29d2895f 100644 --- a/etc/ORG-NEWS +++ b/etc/ORG-NEWS @@ -477,22 +477,23 @@ The change is breaking when ~org-use-property-inheritance~ is set to ~t~. The =TEST= parameter is better served by Emacs debugging tools. -*** ~org-id-store-link~ now adds search strings for precise link targets +*** =id:= links support search options; ~org-id-store-link~ adds search option by default -This new behaviour can be disabled generally by setting -~org-id-link-use-context~ to ~nil~, or the setting can be toggled for -a single call to ~org-store-link~ with a universal argument. +Adding search option by ~org-id-store-link~ can be disabled by setting +~org-id-link-use-context~ to ~nil~, or toggled for a single call by +passing universal argument. When using this feature, IDs should not include =::=, which is used in links to indicate the start of the search string. For backwards compability, existing IDs including =::= will still be matched (but -cannot be used together with precise link targets). An org-lint -checker has been added to warn about this. +cannot be used together with search option). A new org-lint checker +has been added to warn about this. *** ~org-store-link~ behaviour storing additional =CUSTOM_ID= links has changed -As well as an =id:= link, ~org-store-link~ stores an additional "human -readable" link using a node's =CUSTOM_ID= property, if available. +Previously, when storing =id:= link, ~org-store-link~ stored an +additional "human readable" link using a node's =CUSTOM_ID= property. + This behaviour has been expanded to store an additional =CUSTOM_ID= link when storing any type of external link type in an Org file, not just =id:= links. @@ -778,11 +779,11 @@ For =id:= links, when this option is enabled, ~org-store-link~ will look for ids from parent/ancestor headlines, if the current headline does not have an id. -Combined with the new ability for =id:= links to use search strings -for precise link targets (when =org-id-link-use-context= is =t=, which -is the default), this allows linking to specific headlines without -requiring every headline to have an id property, as long as the -headline is unique within a subtree that does have an id property. +Combined with the new ability for =id:= links to use search options + [fn:: when =org-id-link-use-context= is =t=, which is the default], +this allows linking to specific headlines without requiring every +headline to have an id property, as long as the headline is unique +within a subtree that does have an id property. For example, given this org file: -- 2.43.0 [-- Attachment #3: Type: text/plain, Size: 224 bytes --] -- Ihor Radchenko // yantar92, Org mode contributor, Learn more about Org mode at <https://orgmode.org/>. Support Org development at <https://liberapay.com/org-mode>, or support my work at <https://liberapay.com/yantar92> ^ permalink raw reply related [flat|nested] 48+ messages in thread
* Re: [PATCH v2] org-id: allow using parent's existing id in links to headlines 2024-02-08 13:02 ` Ihor Radchenko @ 2024-02-08 22:30 ` Rick Lupton 2024-02-09 12:09 ` Ihor Radchenko 0 siblings, 1 reply; 48+ messages in thread From: Rick Lupton @ 2024-02-08 22:30 UTC (permalink / raw) To: Ihor Radchenko; +Cc: Y. E. [-- Attachment #1: Type: text/plain, Size: 1564 bytes --] On Thu, 8 Feb 2024, at 1:02 PM, Ihor Radchenko wrote: > I have some thoughts about rewording your changes to the manual and > ORG-NEWS. See the attached patch on top of yours. Thanks, makes sense -- wasn't sure whether to keep this as a separate patch or not, I have squashed into the attached updated version. > [minor points on commit messages] Fixed these. > The new optional argument to a public function should be announced in ORG-NEWS. Added. >> + (new-heading-level (if new-heading-container >> + (+ 1 (org-element-property :level new-heading-container)) > > What if new-heading-container is not a heading? > >> + 1))) >> + (goto-char new-heading-position) > > This is err when container ends after narrowed region boundary. Added checks for these. >> +(defun org-link-precise-link-target () >> ... >> + (cond >> + (name >> + (list name >> + name >> + (org-element-begin element))) > > It would make sense to use #+caption as default description when available. Maybe... But I had a little look and it seems complicated, since caption is a parsed property, it's not clear to me how to get a plain string in a simple way. And there could be a long and a short caption, over multiple lines. If the caption is long, it wouldn't make a good link description anyway. The current behaviour is the same as it was before, so maybe we can leave this as a future enhancement if wanted? [-- Attachment #2: 0001-lisp-org.el-org-insert-heading-Allow-specifying-head.patch --] [-- Type: application/octet-stream, Size: 5086 bytes --] From 3eb7515ab639e22ebb2ba3930dbbb049aba587e2 Mon Sep 17 00:00:00 2001 From: Rick Lupton <mail@ricklupton.name> Date: Wed, 3 Jan 2024 22:37:38 +0000 Subject: [PATCH 1/2] lisp/org.el (org-insert-heading): Allow specifying heading level * lisp/org.el (org-insert-heading): Change optional argument TOP to LEVEL, accepting a number to force a specific heading level. * testing/lisp/test-org.el (test-org/insert-heading): Add tests. * etc/ORG-NEWS: Document changes. --- etc/ORG-NEWS | 6 ++++++ lisp/org.el | 21 ++++++++++++++------- testing/lisp/test-org.el | 26 ++++++++++++++++++++++++-- 3 files changed, 44 insertions(+), 9 deletions(-) diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS index 92d363b80..9e68bcdcb 100644 --- a/etc/ORG-NEWS +++ b/etc/ORG-NEWS @@ -1033,6 +1033,12 @@ as the function can also act on objects. *** ~org-export-get-parent-element~ is renamed to ~org-element-parent-element~ and moved to =lisp/org-element.el= +*** ~org-insert-heading~ optional argument =TOP= is now =LEVEL= + +A numeric value forces a heading at that level to be inserted. For +backwards compatibility, non-numeric non-nil values insert level 1 +headings as before. + ** Miscellaneous *** =org-crypt.el= now applies initial visibility settings to decrypted entries diff --git a/lisp/org.el b/lisp/org.el index da315fccb..54748f495 100644 --- a/lisp/org.el +++ b/lisp/org.el @@ -6364,7 +6364,7 @@ headline instead of current one." (`(heading . ,value) value) (_ nil))) -(defun org-insert-heading (&optional arg invisible-ok top) +(defun org-insert-heading (&optional arg invisible-ok level) "Insert a new heading or an item with the same depth at point. If point is at the beginning of a heading, insert a new heading @@ -6393,12 +6393,19 @@ When INVISIBLE-OK is set, stop at invisible headlines when going back. This is important for non-interactive uses of the command. -When optional argument TOP is non-nil, insert a level 1 heading, -unconditionally." +When optional argument LEVEL is a number, insert a heading at +that level. For backwards compatibility, when LEVEL is non-nil +but not a number, insert a level-1 heading." (interactive "P") (let* ((blank? (org--blank-before-heading-p (equal arg '(16)))) - (level (org-current-level)) - (stars (make-string (if (and level (not top)) level 1) ?*))) + (current-level (org-current-level)) + (num-stars (or + ;; Backwards compat: if LEVEL non-nil, level is 1 + (and level (if (wholenump level) level 1)) + current-level + ;; This `1' is for when before first headline + 1)) + (stars (make-string num-stars ?*))) (cond ((or org-insert-heading-respect-content (member arg '((4) (16))) @@ -6407,7 +6414,7 @@ unconditionally." ;; Position point at the location of insertion. Make sure we ;; end up on a visible headline if INVISIBLE-OK is nil. (org-with-limited-levels - (if (not level) (outline-next-heading) ;before first headline + (if (not current-level) (outline-next-heading) ;before first headline (org-back-to-heading invisible-ok) (when (equal arg '(16)) (org-up-heading-safe)) (org-end-of-subtree invisible-ok 'to-heading))) @@ -6420,7 +6427,7 @@ unconditionally." (org-before-first-heading-p))) (insert "\n") (backward-char)) - (when (and (not level) (not (eobp)) (not (bobp))) + (when (and (not current-level) (not (eobp)) (not (bobp))) (when (org-at-heading-p) (insert "\n")) (backward-char)) (unless (and blank? (org-previous-line-empty-p)) diff --git a/testing/lisp/test-org.el b/testing/lisp/test-org.el index 822cbc67a..fc50dc787 100644 --- a/testing/lisp/test-org.el +++ b/testing/lisp/test-org.el @@ -1980,8 +1980,30 @@ CLOCK: [2022-09-17 sam. 11:00]--[2022-09-17 sam. 11:46] => 0:46" (let ((org-insert-heading-respect-content nil)) (org-insert-heading '(16))) (buffer-string)))) - ;; When optional TOP-LEVEL argument is non-nil, always insert - ;; a level 1 heading. + ;; When optional LEVEL argument is a number, insert a heading at + ;; that level. + (should + (equal "* H1\n** H2\n* " + (org-test-with-temp-text "* H1\n** H2<point>" + (org-insert-heading nil nil 1) + (buffer-string)))) + (should + (equal "* H1\n** H2\n** " + (org-test-with-temp-text "* H1\n** H2<point>" + (org-insert-heading nil nil 2) + (buffer-string)))) + (should + (equal "* H1\n** H2\n*** " + (org-test-with-temp-text "* H1\n** H2<point>" + (org-insert-heading nil nil 3) + (buffer-string)))) + (should + (equal "* H1\n- item\n* " + (org-test-with-temp-text "* H1\n- item<point>" + (org-insert-heading nil nil 1) + (buffer-string)))) + ;; When optional LEVEL argument is non-nil, always insert a level 1 + ;; heading. (should (equal "* H1\n** H2\n* " (org-test-with-temp-text "* H1\n** H2<point>" -- 2.37.1 (Apple Git-137.1) [-- Attachment #3: 0002-org-id.el-Add-search-strings-inherit-parent-IDs.patch --] [-- Type: application/octet-stream, Size: 59452 bytes --] From 8b4e721648d023686aeb650737f6f14feeefed13 Mon Sep 17 00:00:00 2001 From: Rick Lupton <mail@ricklupton.name> Date: Sun, 19 Nov 2023 14:52:05 +0000 Subject: [PATCH 2/2] org-id.el: Add search strings, inherit parent IDs * lisp/ol.el (org-store-link): Refactor org-id links to use standard `org-store-link-functions'. (org-link-search): Create new headings at appropriate level. (org-link-precise-link-target): New function extracting logic to identify a precise link target, e.g. a heading, named object, or text search. (org-link-try-link-store-functions): Extract logic to call external link store functions. Pass them a new `interactive?' argument. * lisp/ol-bbdb.el (org-bbdb-store-link): * lisp/ol-bibtex.el (org-bibtex-store-link): * lisp/ol-docview.el (org-docview-store-link): * lisp/ol-eshell.el (org-eshell-store-link): * lisp/ol-eww.el (org-eww-store-link): * lisp/ol-gnus.el (org-gnus-store-link): * lisp/ol-info.el (org-info-store-link): * lisp/ol-irc.el (org-irc-store-link): * lisp/ol-man.el (org-man-store-link): * lisp/ol-mhe.el (org-mhe-store-link): * lisp/ol-rmail.el (org-rmail-store-link): Accept optional arg. * lisp/org-id.el (org-id-link-consider-parent-id): New option to allow a parent heading with an id to be considered as a link target. (org-id-link-use-context): New option to add context to org-id links. (org-id-get): Add optional `inherit' argument which considers parents' IDs if the current entry does not have one. (org-id-store-link): Consider IDs of parent headings as link targets when current heading has no ID and `org-id-link-consider-parent-id' is set. Add a search string to the link when enabled. (org-id-store-link-maybe): Function set as :store option for custom id link property. Move logic from `org-store-link' here to determine when an org-id link should be stored using `org-id-store-link'. (org-id-open): Recognise search strings after "::" in org-id links. * lisp/org-lint.el: Add checker for "::" in ID properties. * testing/lisp/test-ol.el: Add tests for `org-link-precise-link-target' and `org-id-store-link' functions, testing new options. * doc/org-manual.org: Update documentation about links. * etc/ORG-NEWS: Document changes and new options. These feature allows for more precise links when using org-id to link to org headings, without requiring every single headline to have an id. Link: https://list.orgmode.org/118435e8-0b20-46fd-af6a-88de8e19fac6@app.fastmail.com/ --- doc/org-manual.org | 135 ++++++++++------ etc/ORG-NEWS | 72 +++++++++ lisp/ol-bbdb.el | 2 +- lisp/ol-bibtex.el | 2 +- lisp/ol-docview.el | 2 +- lisp/ol-eshell.el | 2 +- lisp/ol-eww.el | 2 +- lisp/ol-gnus.el | 2 +- lisp/ol-info.el | 2 +- lisp/ol-irc.el | 2 +- lisp/ol-man.el | 2 +- lisp/ol-mhe.el | 2 +- lisp/ol-rmail.el | 2 +- lisp/ol.el | 332 +++++++++++++++++++++++++--------------- lisp/org-id.el | 178 ++++++++++++++++++--- lisp/org-lint.el | 16 ++ testing/lisp/test-ol.el | 122 +++++++++++++++ 17 files changed, 673 insertions(+), 204 deletions(-) diff --git a/doc/org-manual.org b/doc/org-manual.org index 1a025a139..e933a2d63 100644 --- a/doc/org-manual.org +++ b/doc/org-manual.org @@ -3300,10 +3300,6 @@ Here is the full set of built-in link types: File links. File name may be remote, absolute, or relative. - Additionally, you can specify a line number, or a text search. - In Org files, you may link to a headline name, a custom ID, or a - code reference instead. - As a special case, "file" prefix may be omitted if the file name is complete, e.g., it starts with =./=, or =/=. @@ -3367,44 +3363,50 @@ Here is the full set of built-in link types: Execute a shell command upon activation. + +For =file:= and =id:= links, you can additionally specify a line +number, or a text search string, separated by =::=. In Org files, you +may link to a headline name, a custom ID, or a code reference instead. + The following table illustrates the link types above, along with their options: -| Link Type | Example | -|------------+----------------------------------------------------------| -| http | =http://staff.science.uva.nl/c.dominik/= | -| https | =https://orgmode.org/= | -| doi | =doi:10.1000/182= | -| file | =file:/home/dominik/images/jupiter.jpg= | -| | =/home/dominik/images/jupiter.jpg= (same as above) | -| | =file:papers/last.pdf= | -| | =./papers/last.pdf= (same as above) | -| | =file:/ssh:me@some.where:papers/last.pdf= (remote) | -| | =/ssh:me@some.where:papers/last.pdf= (same as above) | -| | =file:sometextfile::NNN= (jump to line number) | -| | =file:projects.org= | -| | =file:projects.org::some words= (text search)[fn:12] | -| | =file:projects.org::*task title= (headline search) | -| | =file:projects.org::#custom-id= (headline search) | -| attachment | =attachment:projects.org= | -| | =attachment:projects.org::some words= (text search) | -| docview | =docview:papers/last.pdf::NNN= | -| id | =id:B7423F4D-2E8A-471B-8810-C40F074717E9= | -| news | =news:comp.emacs= | -| mailto | =mailto:adent@galaxy.net= | -| mhe | =mhe:folder= (folder link) | -| | =mhe:folder#id= (message link) | -| rmail | =rmail:folder= (folder link) | -| | =rmail:folder#id= (message link) | -| gnus | =gnus:group= (group link) | -| | =gnus:group#id= (article link) | -| bbdb | =bbdb:R.*Stallman= (record with regexp) | -| irc | =irc:/irc.com/#emacs/bob= | -| help | =help:org-store-link= | -| info | =info:org#External links= | -| shell | =shell:ls *.org= | -| elisp | =elisp:(find-file "Elisp.org")= (Elisp form to evaluate) | -| | =elisp:org-agenda= (interactive Elisp command) | +| Link Type | Example | +|------------+--------------------------------------------------------------------| +| http | =http://staff.science.uva.nl/c.dominik/= | +| https | =https://orgmode.org/= | +| doi | =doi:10.1000/182= | +| file | =file:/home/dominik/images/jupiter.jpg= | +| | =/home/dominik/images/jupiter.jpg= (same as above) | +| | =file:papers/last.pdf= | +| | =./papers/last.pdf= (same as above) | +| | =file:/ssh:me@some.where:papers/last.pdf= (remote) | +| | =/ssh:me@some.where:papers/last.pdf= (same as above) | +| | =file:sometextfile::NNN= (jump to line number) | +| | =file:projects.org= | +| | =file:projects.org::some words= (text search)[fn:12] | +| | =file:projects.org::*task title= (headline search) | +| | =file:projects.org::#custom-id= (headline search) | +| attachment | =attachment:projects.org= | +| | =attachment:projects.org::some words= (text search) | +| docview | =docview:papers/last.pdf::NNN= | +| id | =id:B7423F4D-2E8A-471B-8810-C40F074717E9= | +| | =id:B7423F4D-2E8A-471B-8810-C40F074717E9::*task= (headline search) | +| news | =news:comp.emacs= | +| mailto | =mailto:adent@galaxy.net= | +| mhe | =mhe:folder= (folder link) | +| | =mhe:folder#id= (message link) | +| rmail | =rmail:folder= (folder link) | +| | =rmail:folder#id= (message link) | +| gnus | =gnus:group= (group link) | +| | =gnus:group#id= (article link) | +| bbdb | =bbdb:R.*Stallman= (record with regexp) | +| irc | =irc:/irc.com/#emacs/bob= | +| help | =help:org-store-link= | +| info | =info:org#External links= | +| shell | =shell:ls *.org= | +| elisp | =elisp:(find-file "Elisp.org")= (Elisp form to evaluate) | +| | =elisp:org-agenda= (interactive Elisp command) | #+cindex: VM links #+cindex: Wanderlust links @@ -3465,8 +3467,9 @@ current buffer: - /Org mode buffers/ :: For Org files, if there is a =<<target>>= at point, the link points - to the target. Otherwise it points to the current headline, which - is also the description. + to the target. If there is a named block (using =#+name:=) at + point, the link points to that name. Otherwise it points to the + current headline, which is also the description. #+vindex: org-id-link-to-org-use-id #+cindex: @samp{CUSTOM_ID}, property @@ -3484,6 +3487,32 @@ current buffer: timestamp, depending on ~org-id-method~. Later, when inserting the link, you need to decide which one to use. + #+vindex: org-id-link-consider-parent-id + #+vindex: org-id-link-use-context + #+vindex: org-link-context-for-files + When ~org-id-link-consider-parent-id~ is ~t~[fn:: Also, + ~org-link-context-for-files~ and ~org-id-link-use-context~ should be + both enabled (which they are, by default).], parent =ID= properties + are considered. This allows linking to specific targets, named + blocks, or headlines (which may not have a globally unique =ID= + themselves) within the context of a parent headline or file which + does. + + For example, given this org file: + + #+begin_src org + ,* Parent + :PROPERTIES: + :ID: abc + :END: + ,** Child 1 + ,** Child 2 + #+end_src + + Storing a link with point at "Child 1" will produce a link + =<id:abc::*Child 1>=, which precisely links to the "Child 1" + headline even though it does not have its own ID. + - /Email/News clients: VM, Rmail, Wanderlust, MH-E, Gnus/ :: #+vindex: org-link-email-description-format @@ -3763,7 +3792,9 @@ the link completion function like this: :ALT_TITLE: Search Options :END: #+cindex: search option in file links +#+cindex: search option in id links #+cindex: file links, searching +#+cindex: id links, searching #+cindex: attachment links, searching File links can contain additional information to make Emacs jump to a @@ -3775,8 +3806,8 @@ example, when the command ~org-store-link~ creates a link (see line as a search string that can be used to find this line back later when following the link with {{{kbd(C-c C-o)}}}. -Note that all search options apply for Attachment links in the same -way that they apply for File links. +Note that all search options apply for Attachment and ID links in the +same way that they apply for File links. Here is the syntax of the different ways to attach a search to a file link, together with explanations for each: @@ -21367,7 +21398,7 @@ The following =ol-man.el= file implements it PATH should be a topic that can be thrown at the man command." (funcall org-man-command path)) -(defun org-man-store-link () +(defun org-man-store-link (&optional _interactive?) "Store a link to a man page." (when (memq major-mode '(Man-mode woman-mode)) ;; This is a man page, we do make this link. @@ -21427,13 +21458,15 @@ A review of =ol-man.el=: For example, ~org-man-store-link~ is responsible for storing a link when ~org-store-link~ (see [[*Handling Links]]) is called from a buffer - displaying a man page. It first checks if the major mode is - appropriate. If check fails, the function returns ~nil~, which - means it isn't responsible for creating a link to the current - buffer. Otherwise the function makes a link string by combining - the =man:= prefix with the man topic. It also provides a default - description. The function ~org-insert-link~ can insert it back - into an Org buffer later on. + displaying a man page. It is passed an argument ~interactive?~ + which this function does not use, but other store functions use to + behave differently when a link is stored interactively by the user. + It first checks if the major mode is appropriate. If check fails, + the function returns ~nil~, which means it isn't responsible for + creating a link to the current buffer. Otherwise the function + makes a link string by combining the =man:= prefix with the man + topic. It also provides a default description. The function + ~org-insert-link~ can insert it back into an Org buffer later on. ** Adding Export Backends :PROPERTIES: diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS index 9e68bcdcb..532ede067 100644 --- a/etc/ORG-NEWS +++ b/etc/ORG-NEWS @@ -390,6 +390,14 @@ timestamp object. Possible values: ~timerange~, ~daterange~, ~nil~. ~org-element-timestamp-interpreter~ takes into account this property and returns an appropriate timestamp string. +**** =org-link= store functions are passed an ~interactive?~ argument + +The ~:store:~ functions set for link types using +~org-link-set-parameters~ are now passed an ~interactive?~ argument, +indicating whether ~org-store-link~ was called interactively. + +Existing store functions will continue to work. + *** ~org-priority=show~ command no longer adjusts for scheduled/deadline In agenda views, ~org-priority=show~ command previously displayed the @@ -468,6 +476,28 @@ The change is breaking when ~org-use-property-inheritance~ is set to ~t~. *** ~org-babel-lilypond-compile-lilyfile~ ignores optional second argument The =TEST= parameter is better served by Emacs debugging tools. + +*** =id:= links support search options; ~org-id-store-link~ adds search option by default + +Adding search option by ~org-id-store-link~ can be disabled by setting +~org-id-link-use-context~ to ~nil~, or toggled for a single call by +passing universal argument. + +When using this feature, IDs should not include =::=, which is used in +links to indicate the start of the search string. For backwards +compability, existing IDs including =::= will still be matched (but +cannot be used together with search option). A new org-lint checker +has been added to warn about this. + +*** ~org-store-link~ behaviour storing additional =CUSTOM_ID= links has changed + +Previously, when storing =id:= link, ~org-store-link~ stored an +additional "human readable" link using a node's =CUSTOM_ID= property. + +This behaviour has been expanded to store an additional =CUSTOM_ID= +link when storing any type of external link type in an Org file, not +just =id:= links. + ** New and changed options *** ~repeated-after-deadline~ value of ~org-agenda-skip-scheduled-repeats-after-deadline~ is moved to a new customization @@ -743,6 +773,35 @@ This option starts the agenda to automatically include archives, propagating the value for this variable to ~org-agenda-archives-mode~. For acceptable values and their meaning, see the value of that variable. +*** New option ~org-id-link-consider-parent-id~ to allow =id:= links to parent headlines + +For =id:= links, when this option is enabled, ~org-store-link~ will +look for ids from parent/ancestor headlines, if the current headline +does not have an id. + +Combined with the new ability for =id:= links to use search options + [fn:: when =org-id-link-use-context= is =t=, which is the default], +this allows linking to specific headlines without requiring every +headline to have an id property, as long as the headline is unique +within a subtree that does have an id property. + +For example, given this org file: + +#+begin_src org +,* Parent +:PROPERTIES: +:ID: abc +:END: +,** Child 1 +,** Child 2 +#+end_src + +Storing a link with point at "Child 1" will produce a link +=<id:abc::*Child 1>=, which precisely links to the "Child 1" headline +even though it does not have its own ID. By giving files top-level id +properties, links to headlines in the file can also be made more +robust by using the file id instead of the file path. + ** New features *** =ob-plantuml.el=: Support tikz file format output @@ -1039,6 +1098,19 @@ A numeric value forces a heading at that level to be inserted. For backwards compatibility, non-numeric non-nil values insert level 1 headings as before. +*** New optional argument for ~org-id-get~ + +New optional argument =INHERIT= means inherited ID properties from +parent entries are considered when getting an entry's ID (see +~org-id-link-consider-parent-id~ option). + +*** New optional argument for ~org-link-search~ + +If a missing heading is created to match the search string, the new +optional argument =NEW-HEADING-CONTAINER= specifies where in the +buffer it will be added. If not specified, new headings are created +at level 1 at the end of the accessible part of the buffer, as before. + ** Miscellaneous *** =org-crypt.el= now applies initial visibility settings to decrypted entries diff --git a/lisp/ol-bbdb.el b/lisp/ol-bbdb.el index be3924fc9..6ea060f70 100644 --- a/lisp/ol-bbdb.el +++ b/lisp/ol-bbdb.el @@ -226,7 +226,7 @@ date year)." ;;; Implementation -(defun org-bbdb-store-link () +(defun org-bbdb-store-link (&optional _interactive?) "Store a link to a BBDB database entry." (when (eq major-mode 'bbdb-mode) ;; This is BBDB, we make this link! diff --git a/lisp/ol-bibtex.el b/lisp/ol-bibtex.el index c5a950e2d..38468f32f 100644 --- a/lisp/ol-bibtex.el +++ b/lisp/ol-bibtex.el @@ -507,7 +507,7 @@ ARG, when non-nil, is a universal prefix argument. See `org-open-file' for details." (org-link-open-as-file path arg)) -(defun org-bibtex-store-link () +(defun org-bibtex-store-link (&optional _interactive?) "Store a link to a BibTeX entry." (when (eq major-mode 'bibtex-mode) (let* ((search (org-create-file-search-in-bibtex)) diff --git a/lisp/ol-docview.el b/lisp/ol-docview.el index b31f1ce5e..0907ddee1 100644 --- a/lisp/ol-docview.el +++ b/lisp/ol-docview.el @@ -83,7 +83,7 @@ (error "No such file: %s" path)) (when page (doc-view-goto-page page)))) -(defun org-docview-store-link () +(defun org-docview-store-link (&optional _interactive?) "Store a link to a docview buffer." (when (eq major-mode 'doc-view-mode) ;; This buffer is in doc-view-mode diff --git a/lisp/ol-eshell.el b/lisp/ol-eshell.el index 2c7ec6bef..595dd0ee0 100644 --- a/lisp/ol-eshell.el +++ b/lisp/ol-eshell.el @@ -60,7 +60,7 @@ followed by a colon." (insert command) (eshell-send-input))) -(defun org-eshell-store-link () +(defun org-eshell-store-link (&optional _interactive?) "Store eshell link. When opened, the link switches back to the current eshell buffer and the current working directory." diff --git a/lisp/ol-eww.el b/lisp/ol-eww.el index 40b820d2b..c13dbf339 100644 --- a/lisp/ol-eww.el +++ b/lisp/ol-eww.el @@ -62,7 +62,7 @@ "Open URL with Eww in the current buffer." (eww url)) -(defun org-eww-store-link () +(defun org-eww-store-link (&optional _interactive?) "Store a link to the url of an EWW buffer." (when (eq major-mode 'eww-mode) (org-link-store-props diff --git a/lisp/ol-gnus.el b/lisp/ol-gnus.el index e105fdb2c..b9ee8683f 100644 --- a/lisp/ol-gnus.el +++ b/lisp/ol-gnus.el @@ -123,7 +123,7 @@ If `org-store-link' was called with a prefix arg the meaning of (url-encode-url message-id)) (concat "gnus:" group "#" message-id))) -(defun org-gnus-store-link () +(defun org-gnus-store-link (&optional _interactive?) "Store a link to a Gnus folder or message." (pcase major-mode (`gnus-group-mode diff --git a/lisp/ol-info.el b/lisp/ol-info.el index 0edf9a13f..6062cab34 100644 --- a/lisp/ol-info.el +++ b/lisp/ol-info.el @@ -50,7 +50,7 @@ :insert-description #'org-info-description-as-command) ;; Implementation -(defun org-info-store-link () +(defun org-info-store-link (&optional _interactive?) "Store a link to an Info file and node." (when (eq major-mode 'Info-mode) (let ((link (concat "info:" diff --git a/lisp/ol-irc.el b/lisp/ol-irc.el index 78c4884b0..b263e52db 100644 --- a/lisp/ol-irc.el +++ b/lisp/ol-irc.el @@ -103,7 +103,7 @@ attributes that are found." parts)) ;;;###autoload -(defun org-irc-store-link () +(defun org-irc-store-link (&optional _interactive?) "Dispatch to the appropriate function to store a link to an IRC session." (cond ((eq major-mode 'erc-mode) diff --git a/lisp/ol-man.el b/lisp/ol-man.el index e3f13815e..42aacea81 100644 --- a/lisp/ol-man.el +++ b/lisp/ol-man.el @@ -82,7 +82,7 @@ matched strings in man buffer." (set-window-point window point) (set-window-start window point))))))) -(defun org-man-store-link () +(defun org-man-store-link (&optional _interactive?) "Store a link to a README file." (when (memq major-mode '(Man-mode woman-mode)) ;; This is a man page, we do make this link diff --git a/lisp/ol-mhe.el b/lisp/ol-mhe.el index 106cfedc9..a32481324 100644 --- a/lisp/ol-mhe.el +++ b/lisp/ol-mhe.el @@ -80,7 +80,7 @@ supported by MH-E." (org-link-set-parameters "mhe" :follow #'org-mhe-open :store #'org-mhe-store-link) ;; Implementation -(defun org-mhe-store-link () +(defun org-mhe-store-link (&optional _interactive?) "Store a link to an MH-E folder or message." (when (or (eq major-mode 'mh-folder-mode) (eq major-mode 'mh-show-mode)) diff --git a/lisp/ol-rmail.el b/lisp/ol-rmail.el index f6031ab52..f1f753b6f 100644 --- a/lisp/ol-rmail.el +++ b/lisp/ol-rmail.el @@ -51,7 +51,7 @@ :store #'org-rmail-store-link) ;; Implementation -(defun org-rmail-store-link () +(defun org-rmail-store-link (&optional _interactive?) "Store a link to an Rmail folder or message." (when (or (eq major-mode 'rmail-mode) (eq major-mode 'rmail-summary-mode)) diff --git a/lisp/ol.el b/lisp/ol.el index f8d911127..29b2d2bcf 100644 --- a/lisp/ol.el +++ b/lisp/ol.el @@ -57,13 +57,13 @@ (declare-function org-element-link-parser "org-element" ()) (declare-function org-element-property "org-element-ast" (property node)) (declare-function org-element-begin "org-element" (node)) +(declare-function org-element-end "org-element" (node)) (declare-function org-element-type-p "org-element-ast" (node types)) (declare-function org-element-update-syntax "org-element" ()) (declare-function org-entry-get "org" (pom property &optional inherit literal-nil)) (declare-function org-find-property "org" (property &optional value)) (declare-function org-get-heading "org" (&optional no-tags no-todo no-priority no-comment)) (declare-function org-id-find-id-file "org-id" (id)) -(declare-function org-id-store-link "org-id" ()) (declare-function org-insert-heading "org" (&optional arg invisible-ok top)) (declare-function org-load-modules-maybe "org" (&optional force)) (declare-function org-mark-ring-push "org" (&optional pos buffer)) @@ -818,6 +818,74 @@ spec." (org-with-point-at (car region) (not (org-in-regexp org-link-any-re)))) +(defun org-link--try-link-store-functions (interactive?) + "Try storing external links, prompting if more than one is possible. + +Each function returned by `org-store-link-functions' is called in +turn. If multiple functions return non-nil, prompt for which +link should be stored. + +Argument INTERACTIVE? indicates whether `org-store-link' was +called interactively and is passed to the link store functions. + +Return t when a link has been stored in `org-link-store-props'." + (let ((results-alist nil)) + (dolist (f (org-store-link-functions)) + (when (condition-case nil + (funcall f interactive?) + ;; FIXME: The store function used (< Org 9.7) to accept + ;; no arguments; provide backward compatibility support + ;; for them. + (wrong-number-of-arguments + (funcall f))) + ;; FIXME: return value is not link's plist, so we store the + ;; new value before it is modified. It would be cleaner to + ;; ask store link functions to return the plist instead. + (push (cons f (copy-sequence org-store-link-plist)) + results-alist))) + (pcase results-alist + (`nil nil) + (`((,_ . ,_)) t) ;single choice: nothing to do + (`((,name . ,_) . ,_) + ;; Reinstate link plist associated to the chosen + ;; function. + (apply #'org-link-store-props + (cdr (assoc-string + (completing-read + (format "Store link with (default %s): " name) + (mapcar #'car results-alist) + nil t nil nil (symbol-name name)) + results-alist))) + t)))) + +(defun org-link--add-to-stored-links (link desc) + "Add LINK to `org-stored-links' with description DESC." + (cond + ((not (member (list link desc) org-stored-links)) + (push (list link desc) org-stored-links) + (message "Stored: %s" (or desc link))) + ((equal (list link desc) (car org-stored-links)) + (message "This link has already been stored")) + (t + (setq org-stored-links + (delete (list link desc) org-stored-links)) + (push (list link desc) org-stored-links) + (message "Link moved to front: %s" (or desc link))))) + +(defun org-link--file-link-to-here () + "Return as (LINK . DESC) a file link with search string to here." + (let ((link (concat "file:" + (abbreviate-file-name + (buffer-file-name (buffer-base-buffer))))) + desc) + (when org-link-context-for-files + (pcase (org-link-precise-link-target) + (`nil nil) + (`(,search-string ,search-desc ,_position) + (setq link (format "%s::%s" link search-string)) + (setq desc search-desc)))) + (cons link desc))) + \f ;;; Public API @@ -1044,7 +1112,9 @@ LINK is escaped with backslashes for inclusion in buffer." "List of functions that are called to create and store a link. The functions are defined in the `:store' property of -`org-link-parameters'. +`org-link-parameters'. Each function should accept an argument +INTERACTIVE? which indicates whether the user has initiated +`org-store-link' interactively. Each function will be called in turn until one returns a non-nil value. Each function should check if it is responsible for @@ -1163,7 +1233,7 @@ Optional argument ARG is passed to `org-open-file' when S is a (`nil (user-error "No valid link in %S" s)) (link (org-link-open link arg)))) -(defun org-link-search (s &optional avoid-pos stealth) +(defun org-link-search (s &optional avoid-pos stealth new-heading-container) "Search for a search string S in the accessible part of the buffer. If S starts with \"#\", it triggers a custom ID search. @@ -1183,6 +1253,13 @@ When optional argument STEALTH is non-nil, do not modify visibility around point, thus ignoring `org-show-context-detail' variable. +When optional argument NEW-HEADING-CONTAINER is an element, any +new heading that is created (see +`org-link-search-must-match-exact-headline') will be added as a +subheading of NEW-HEADING-CONTAINER. Otherwise, new headings are +created at level 1 at the end of the accessible part of the +buffer. + Search is case-insensitive and ignores white spaces. Return type of matched result, which is either `dedicated' or `fuzzy'. Search respects buffer narrowing." @@ -1281,11 +1358,24 @@ respects buffer narrowing." ((and (derived-mode-p 'org-mode) (eq org-link-search-must-match-exact-headline 'query-to-create) (yes-or-no-p "No match - create this as a new heading? ")) - (goto-char (point-max)) - (unless (bolp) (newline)) - (org-insert-heading nil t t) - (insert s "\n") - (forward-line -1)) + (let* ((container-ok (and new-heading-container + (org-element-type-p new-heading-container '(headline)))) + (new-heading-position (if container-ok + (- (org-element-end new-heading-container) 1) + (point-max))) + (new-heading-level (if container-ok + (+ 1 (org-element-property :level new-heading-container)) + 1))) + ;; Need to widen when target is outside accessible portion of + ;; buffer, since the we want the user to end up there. + (unless (and (<= (point-min) new-heading-position) + (>= (point-max) new-heading-position)) + (widen)) + (goto-char new-heading-position) + (unless (bolp) (newline)) + (org-insert-heading nil t new-heading-level) + (insert (if starred (substring s 1) s) "\n") + (forward-line -1))) ;; Only headlines are looked after. No need to process ;; further: throw an error. ((and (derived-mode-p 'org-mode) @@ -1335,6 +1425,70 @@ priority cookie or tag." (org-link--normalize-string (or string (org-get-heading t t t t))))) +(defun org-link-precise-link-target () + "Determine search string and description for storing a link. + +If a search string (see `org-link-search') is found, return +list (SEARCH-STRING DESC POSITION). Otherwise, return nil. + +If there is an active region, the contents (or a part of it, see +`org-link-context-for-files') is used as the search string. + +In Org buffers, if point is at a named element (such as a source +block), the name is used for the search string. If at a heading, +its CUSTOM_ID is used to form a search string of the form +\"#id\", if present, otherwise the current heading text is used +in the form \"*Heading\". + +If none of those finds a suitable search string, the current line +is used as the search string. + +The description DESC is nil (meaning the user will be prompted +for a description when inserting the link) for search strings +based on a region or the current line. For other cases, DESC is +a cleaned-up version of the name or heading at point. + +POSITION is the buffer position at which the search string +matches." + (let* ((region (org-link--context-from-region)) + (result + (cond + (region + (list (org-link--normalize-string region t) + nil + (region-beginning))) + + ((derived-mode-p 'org-mode) + (let* ((element (org-element-at-point)) + (name (org-element-property :name element)) + (heading (org-element-lineage element '(headline inlinetask) t)) + (custom-id (org-entry-get heading "CUSTOM_ID"))) + (cond + (name + (list name + name + (org-element-begin element))) + ((org-before-first-heading-p) + (list (org-link--normalize-string (org-current-line-string) t) + nil + (line-beginning-position))) + (heading + (list (if custom-id (concat "#" custom-id) + (org-link-heading-search-string)) + (org-link--normalize-string + (org-get-heading t t t t)) + (org-element-begin heading)))))) + + ;; Not in an org-mode buffer, no region + (t + (list (org-link--normalize-string (org-current-line-string) t) + nil + (line-beginning-position)))))) + + ;; Only use search option if there is some text. + (when (org-string-nw-p (car result)) + result))) + (defun org-link-open-as-file (path in-emacs) "Pretend PATH is a file name and open it. @@ -1407,7 +1561,7 @@ PATH is a symbol name, as a string." ((and (pred boundp) variable) (describe-variable variable)) (name (user-error "Unknown function or variable: %s" name)))) -(defun org-link--store-help () +(defun org-link--store-help (&optional _interactive?) "Store \"help\" type link." (when (eq major-mode 'help-mode) (let ((symbol @@ -1542,7 +1696,12 @@ prefix ARG forces storing a link for each line in the active region. Assume the function is called interactively if INTERACTIVE? is -non-nil." +non-nil. + +In Org buffers, an additional \"human-readable\" simple file link +is stored as an alternative to persistent org-id or other links, +if at a heading with a CUSTOM_ID property or an element with a +NAME." (interactive "P\np") (org-load-modules-maybe) (if (and (equal arg '(64)) (org-region-active-p)) @@ -1557,36 +1716,19 @@ non-nil." (move-beginning-of-line 2) (set-mark (point))))) (setq org-store-link-plist nil) - (let (link cpltxt desc search custom-id agenda-link) ;; description + ;; Negate `org-context-in-file-links' when given a single universal arg. + (let ((org-link-context-for-files (org-xor org-link-context-for-files + (equal arg '(4)))) + link cpltxt desc search agenda-link) ;; description (cond ;; Store a link using an external link type, if any function is - ;; available. If more than one can generate a link from current - ;; location, ask which one to use. + ;; available, unless external link types are skipped for this + ;; call using two universal args. If more than one function + ;; can generate a link from current location, ask the user + ;; which one to use. ((and (not (equal arg '(16))) - (let ((results-alist nil)) - (dolist (f (org-store-link-functions)) - (when (funcall f) - ;; XXX: return value is not link's plist, so we - ;; store the new value before it is modified. It - ;; would be cleaner to ask store link functions to - ;; return the plist instead. - (push (cons f (copy-sequence org-store-link-plist)) - results-alist))) - (pcase results-alist - (`nil nil) - (`((,_ . ,_)) t) ;single choice: nothing to do - (`((,name . ,_) . ,_) - ;; Reinstate link plist associated to the chosen - ;; function. - (apply #'org-link-store-props - (cdr (assoc-string - (completing-read - (format "Store link with (default %s): " name) - (mapcar #'car results-alist) - nil t nil nil (symbol-name name)) - results-alist))) - t)))) - (setq link (plist-get org-store-link-plist :link)) + (org-link--try-link-store-functions interactive?)) + (setq link (plist-get org-store-link-plist :link)) ;; If store function actually set `:description' property, use ;; it, even if it is nil. Otherwise, fallback to nil (ask user). (setq desc (plist-get org-store-link-plist :description))) @@ -1637,6 +1779,7 @@ non-nil." (org-with-point-at m (setq agenda-link (org-store-link nil interactive?)))))) + ;; Calendar mode ((eq major-mode 'calendar-mode) (let ((cd (calendar-cursor-to-date))) (setq link @@ -1645,6 +1788,7 @@ non-nil." (org-encode-time 0 0 0 (nth 1 cd) (nth 0 cd) (nth 2 cd)))) (org-link-store-props :type "calendar" :date cd))) + ;; Image mode ((eq major-mode 'image-mode) (setq cpltxt (concat "file:" (abbreviate-file-name buffer-file-name)) @@ -1662,15 +1806,22 @@ non-nil." (setq cpltxt (concat "file:" file) link cpltxt))) + ;; Try `org-create-file-search-functions`. If any are + ;; successful, create a file link to the current buffer with + ;; the provided search string. (sets `link` and `cpltxt` to + ;; the same thing; it looks like the intention originally was + ;; that cpltxt was a description, which might have been set by + ;; the search-function (removed in switch to lexical binding)). ((setq search (run-hook-with-args-until-success 'org-create-file-search-functions)) (setq link (concat "file:" (abbreviate-file-name buffer-file-name) "::" search)) (setq cpltxt (or link))) ;; description + ;; Main logic for storing built-in link types in org-mode + ;; buffers ((and (buffer-file-name (buffer-base-buffer)) (derived-mode-p 'org-mode)) (org-with-limited-levels - (setq custom-id (org-entry-get nil "CUSTOM_ID")) (cond ;; Store a link using the target at point ((org-in-regexp "[^<]<<\\([^<>]+\\)>>[^>]" 1) @@ -1684,74 +1835,21 @@ non-nil." ;; links. Maybe the case of identical target and ;; description should be handled by `org-insert-link'. cpltxt nil - desc nil - ;; Do not append #CUSTOM_ID link below. - custom-id nil)) - ((and (featurep 'org-id) - (or (eq org-id-link-to-org-use-id t) - (and interactive? - (or (eq org-id-link-to-org-use-id 'create-if-interactive) - (and (eq org-id-link-to-org-use-id - 'create-if-interactive-and-no-custom-id) - (not custom-id)))) - (and org-id-link-to-org-use-id (org-entry-get nil "ID")))) - ;; Store a link using the ID at point - (setq link (condition-case nil - (prog1 (org-id-store-link) - (setq desc (plist-get org-store-link-plist :description))) - (error - ;; Probably before first headline, link only to file - (concat "file:" - (abbreviate-file-name - (buffer-file-name (buffer-base-buffer)))))))) - (t + desc nil)) + (t ;; Just link to current headline. - (setq cpltxt (concat "file:" - (abbreviate-file-name - (buffer-file-name (buffer-base-buffer))))) - ;; Add a context search string. - (when (org-xor org-link-context-for-files (equal arg '(4))) - (let* ((element (org-element-at-point)) - (name (org-element-property :name element)) - (context - (cond - ((let ((region (org-link--context-from-region))) - (and region (org-link--normalize-string region t)))) - (name) - ((org-before-first-heading-p) - (org-link--normalize-string (org-current-line-string) t)) - (t (org-link-heading-search-string))))) - (when (org-string-nw-p context) - (setq cpltxt (format "%s::%s" cpltxt context)) - (setq desc - (or name - ;; Although description is not a search - ;; string, use `org-link--normalize-string' - ;; to prettify it (contiguous white spaces) - ;; and remove volatile contents (statistics - ;; cookies). - (and (not (org-before-first-heading-p)) - (org-link--normalize-string - (org-get-heading t t t t))) - "NONE"))))) - (setq link cpltxt))))) + (let ((here (org-link--file-link-to-here))) + (setq cpltxt (car here)) + (setq desc (cdr here))) + (setq link cpltxt))))) + ;; Buffer linked to file, but not an org-mode buffer. ((buffer-file-name (buffer-base-buffer)) ;; Just link to this file here. - (setq cpltxt (concat "file:" - (abbreviate-file-name - (buffer-file-name (buffer-base-buffer))))) - ;; Add a context search string. - (when (org-xor org-link-context-for-files (equal arg '(4))) - (let ((context (org-link--normalize-string - (or (org-link--context-from-region) - (org-current-line-string)) - t))) - ;; Only use search option if there is some text. - (when (org-string-nw-p context) - (setq cpltxt (format "%s::%s" cpltxt context)) - (setq desc "NONE")))) - (setq link cpltxt)) + (let ((here (org-link--file-link-to-here))) + (setq cpltxt (car here)) + (setq desc (cdr here))) + (setq link cpltxt)) (interactive? (user-error "No method for storing a link from this buffer")) @@ -1767,24 +1865,18 @@ non-nil." ;; Store and return the link (if (not (and interactive? link)) (or agenda-link (and link (org-link-make-string link desc))) - (dotimes (_ (if custom-id 2 1)) ; Store 2 links when CUSTOM-ID is non-nil. - (cond - ((not (member (list link desc) org-stored-links)) - (push (list link desc) org-stored-links) - (message "Stored: %s" (or desc link))) - ((equal (list link desc) (car org-stored-links)) - (message "This link has already been stored")) - (t - (setq org-stored-links - (delete (list link desc) org-stored-links)) - (push (list link desc) org-stored-links) - (message "Link moved to front: %s" (or desc link)))) - (when custom-id - (setq link (concat "file:" - (abbreviate-file-name - (buffer-file-name (buffer-base-buffer))) - "::#" custom-id)))) - (car org-stored-links))))) + (org-link--add-to-stored-links link desc) + ;; In org buffers, store an additional "human-readable" link + ;; using custom id, if available. + (when (and (buffer-file-name (buffer-base-buffer)) + (derived-mode-p 'org-mode) + (org-entry-get nil "CUSTOM_ID")) + (let ((here (org-link--file-link-to-here))) + (setq link (car here)) + (setq desc (cdr here))) + (unless (equal (list link desc) (car org-stored-links)) + (org-link--add-to-stored-links link desc))) + (car org-stored-links))))) ;;;###autoload (defun org-insert-link (&optional complete-file link-location description) diff --git a/lisp/org-id.el b/lisp/org-id.el index 8647a57cc..58d51deca 100644 --- a/lisp/org-id.el +++ b/lisp/org-id.el @@ -129,6 +129,46 @@ nil Never use an ID to make a link, instead link using a text search for (const :tag "Only use existing" use-existing) (const :tag "Do not use ID to create link" nil))) +(defcustom org-id-link-consider-parent-id nil + "Non-nil means storing a link to an Org entry considers inherited IDs. + +When this option is non-nil and `org-id-link-use-context' is +enabled, ID properties inherited from parent entries will be +considered when storing an ID link. If no ID is found in this +way, a new one may be created as normal (see +`org-id-link-to-org-use-id'). + +For example, given this org file: + +* Parent +:PROPERTIES: +:ID: abc +:END: +** Child 1 +** Child 2 + +With `org-id-link-consider-parent-id' and +`org-id-link-use-context' both enabled, storing a link with point +at \"Child 1\" will produce a link \"<id:abc::*Child 1>\". This +allows linking to uniquely-named sub-entries within a parent +entry with an ID, without requiring every sub-entry to have its +own ID." + :group 'org-link-store + :group 'org-id + :package-version '(Org . "9.7") + :type 'boolean) + +(defcustom org-id-link-use-context t + "Non-nil means enables search string context in org-id links. + +Search strings are added by `org-id-store-link' when both the +general option `org-link-context-for-files' and the org-id option +`org-id-link-use-context' are non-nil." + :group 'org-link-store + :group 'org-id + :package-version '(Org . "9.7") + :type 'boolean) + (defcustom org-id-uuid-program "uuidgen" "The uuidgen program." :group 'org-id @@ -280,15 +320,21 @@ This is useful when working with contents in a temporary buffer that will be copied back to the original.") ;;;###autoload -(defun org-id-get (&optional epom create prefix) - "Get the ID property of the entry at EPOM. -EPOM is an element, marker, or buffer position. -If EPOM is nil, refer to the entry at point. -If the entry does not have an ID, the function returns nil. -However, when CREATE is non-nil, create an ID if none is present already. -PREFIX will be passed through to `org-id-new'. -In any case, the ID of the entry is returned." - (let ((id (org-entry-get epom "ID"))) +(defun org-id-get (&optional epom create prefix inherit) + "Get the ID of the entry at EPOM. + +EPOM is an element, marker, or buffer position. If EPOM is nil, +refer to the entry at point. + +If INHERIT is non-nil, ID properties inherited from parent +entries are considered. Otherwise, only ID properties on the +entry itself are considered. + +When CREATE is nil, return the ID of the entry if found, +otherwise nil. When CREATE is non-nil, create an ID if none has +been found, and return the new ID. PREFIX will be passed through +to `org-id-new'." + (let ((id (org-entry-get epom "ID" (and inherit t)))) (cond ((and id (stringp id) (string-match "\\S-" id)) id) @@ -700,21 +746,56 @@ optional argument MARKERP, return the position as a new marker." ;; id link type -;; Calling the following function is hard-coded into `org-store-link', -;; so we do have to add it to `org-store-link-functions'. +(defun org-id--get-id-to-store-link (&optional create) + "Get or create the relevant ID for storing a link. + +Optional argument CREATE is passed to `org-id-get'. + +Inherited IDs are only considered when +`org-id-link-consider-parent-id', `org-id-link-use-context' and +`org-link-context-for-files' are all enabled, since inherited IDs +are confusing without the additional search string context. + +Note that this function resets the +`org-entry-property-inherited-from' marker: it will either point +to nil (if the id was not inherited) or to the point it was +inherited from." + (let* ((inherit-id (and org-id-link-consider-parent-id + org-id-link-use-context + org-link-context-for-files))) + (move-marker org-entry-property-inherited-from nil) + (org-id-get nil create nil inherit-id))) ;;;###autoload (defun org-id-store-link () "Store a link to the current entry, using its ID. -If before first heading store first title-keyword as description -or filename if no title." +The link description is based on the heading, or if before the +first heading, the title keyword if available, or else the +filename. + +When `org-link-context-for-files' and `org-id-link-use-context' +are non-nil, add a search string to the link. The link +description is then based on the search string target. + +When in addition `org-id-link-consider-parent-id' is non-nil, the +ID can be inherited from a parent entry, with the search string +used to still link to the current location." (interactive) - (when (and (buffer-file-name (buffer-base-buffer)) (derived-mode-p 'org-mode)) - (let* ((link (concat "id:" (org-id-get-create))) + (when (and (buffer-file-name (buffer-base-buffer)) + (derived-mode-p 'org-mode)) + ;; Get the precise target first, in case looking for an id causes + ;; a properties drawer to be added at the current location. + (let* ((precise-target (and org-link-context-for-files + org-id-link-use-context + (org-link-precise-link-target))) + (link (concat "id:" (org-id--get-id-to-store-link 'create))) + (id-location (or (and org-entry-property-inherited-from + (marker-position org-entry-property-inherited-from)) + (save-excursion (org-back-to-heading-or-point-min t) (point)))) (case-fold-search nil) (desc (save-excursion - (org-back-to-heading-or-point-min t) + (goto-char id-location) (cond ((org-before-first-heading-p) (let ((keywords (org-collect-keywords '("TITLE")))) (if keywords @@ -726,14 +807,59 @@ or filename if no title." (match-string 4) (match-string 0))) (t link))))) + ;; Precise targets should be after id-location to avoid + ;; duplicating the current headline as a search string + (when (and precise-target + (> (nth 2 precise-target) id-location)) + (setq link (concat link "::" (nth 0 precise-target))) + (setq desc (nth 1 precise-target))) (org-link-store-props :link link :description desc :type "id") link))) -(defun org-id-open (id _) - "Go to the entry with id ID." - (org-mark-ring-push) - (let ((m (org-id-find id 'marker)) - cmd) +;;;###autoload +(defun org-id-store-link-maybe (&optional interactive?) + "Store a link to the current entry using its ID if enabled. + +The value of `org-id-link-to-org-use-id' determines whether an ID +link should be stored, using `org-id-store-link'. + +Assume the function is called interactively if INTERACTIVE? is +non-nil." + (when (and (buffer-file-name (buffer-base-buffer)) + (derived-mode-p 'org-mode) + (or (eq org-id-link-to-org-use-id t) + (and interactive? + (or (eq org-id-link-to-org-use-id 'create-if-interactive) + (and (eq org-id-link-to-org-use-id + 'create-if-interactive-and-no-custom-id) + (not (org-entry-get nil "CUSTOM_ID"))))) + ;; 'use-existing + (and org-id-link-to-org-use-id + (org-id--get-id-to-store-link)))) + (org-id-store-link))) + +(defun org-id-open (link _) + "Go to the entry indicated by id link LINK. + +The link can include a search string after \"::\", which is +passed to `org-link-search'. + +For backwards compatibility with IDs that contain \"::\", if no +match is found for the ID, the full link string including \"::\" +will be tried as an ID." + (let* ((option (and (string-match "::\\(.*\\)\\'" link) + (match-string 1 link))) + (id (if (not option) link + (substring link 0 (match-beginning 0)))) + m cmd) + (org-mark-ring-push) + (setq m (org-id-find id 'marker)) + (when (and (not m) option) + ;; Backwards compatibility: if id is not found, try treating + ;; whole link as an id. + (setq m (org-id-find link 'marker)) + (when m + (setq option nil))) (unless m (error "Cannot find entry with ID \"%s\"" id)) ;; Use a buffer-switching command in analogy to finding files @@ -750,9 +876,17 @@ or filename if no title." (funcall cmd (marker-buffer m))) (goto-char m) (move-marker m nil) + (when option + (save-restriction + (unless (org-before-first-heading-p) + (org-narrow-to-subtree)) + (org-link-search option nil nil + (org-element-lineage (org-element-at-point) 'headline t)))) (org-fold-show-context))) -(org-link-set-parameters "id" :follow #'org-id-open) +(org-link-set-parameters "id" + :follow #'org-id-open + :store #'org-id-store-link-maybe) (provide 'org-id) diff --git a/lisp/org-lint.el b/lisp/org-lint.el index 4d2a55d15..b23afcca3 100644 --- a/lisp/org-lint.el +++ b/lisp/org-lint.el @@ -65,6 +65,7 @@ ;; - special properties in properties drawers, ;; - obsolete syntax for properties drawers, ;; - invalid duration in EFFORT property, +;; - invalid ID property with a double colon, ;; - missing definition for footnote references, ;; - missing reference for footnote definitions, ;; - non-footnote definitions in footnote section, @@ -686,6 +687,16 @@ Use :header-args: instead" (list (org-element-begin p) (format "Invalid effort duration format: %S" value)))))))) +(defun org-lint-invalid-id-property (ast) + (org-element-map ast 'node-property + (lambda (p) + (when (equal "ID" (org-element-property :key p)) + (let ((value (org-element-property :value p))) + (and (org-string-nw-p value) + (string-match-p "::" value) + (list (org-element-begin p) + (format "IDs should not include \"::\": %S" value)))))))) + (defun org-lint-link-to-local-file (ast) (org-element-map ast 'link (lambda (l) @@ -1684,6 +1695,11 @@ AST is the buffer parse tree." #'org-lint-invalid-effort-property :categories '(properties)) +(org-lint-add-checker 'invalid-id-property + "Report search string delimiter \"::\" in ID property" + #'org-lint-invalid-id-property + :categories '(properties)) + (org-lint-add-checker 'undefined-footnote-reference "Report missing definition for footnote references" #'org-lint-undefined-footnote-reference diff --git a/testing/lisp/test-ol.el b/testing/lisp/test-ol.el index e0cec0854..3150b4e2f 100644 --- a/testing/lisp/test-ol.el +++ b/testing/lisp/test-ol.el @@ -381,6 +381,128 @@ See https://github.com/yantar92/org/issues/4." (equal (format "[[file:%s::*foo bar][foo bar]]" file file) (org-store-link nil))))))) +(ert-deftest test-org-link/precise-link-target () + "Test `org-link-precise-link-target` specifications." + (org-test-with-temp-text "* H1<point>\n* H2\n" + (should + (equal '("*H1" "H1" 1) + (org-link-precise-link-target)))) + (org-test-with-temp-text "* H1\n#+name: foo<point>\n#+begin_example\nhi\n#+end_example\n" + (should + (equal '("foo" "foo" 6) + (org-link-precise-link-target)))) + (org-test-with-temp-text "\nText<point>\n* H1\n" + (should + (equal '("Text" nil 2) + (org-link-precise-link-target)))) + (org-test-with-temp-text "\n<point>\n* H1\n" + (should + (equal nil (org-link-precise-link-target))))) + +(defmacro test-ol-stored-link-with-text (text &rest body) + "Return :link and :description from link stored in body." + (declare (indent 1)) + `(let (org-store-link-plist) + (org-test-with-temp-text-in-file ,text + ,@body + (list (plist-get org-store-link-plist :link) + (plist-get org-store-link-plist :description))))) + +(ert-deftest test-org-link/id-store-link () + "Test `org-id-store-link' specifications." + (let ((org-id-link-to-org-use-id nil)) + (should + (equal '(nil nil) + (test-ol-stored-link-with-text "* H1\n:PROPERTIES:\n:ID: abc\n:END:\n" + (org-id-store-link-maybe t))))) + ;; On a headline, link to that headline's ID. Use heading as the + ;; description of the link. + (let ((org-id-link-to-org-use-id t)) + (should + (equal '("id:abc" "H1") + (test-ol-stored-link-with-text "* H1\n:PROPERTIES:\n:ID: abc\n:END:\n" + (org-id-store-link-maybe t))))) + ;; Remove TODO keywords etc from description of the link. + (let ((org-id-link-to-org-use-id t)) + (should + (equal '("id:abc" "H1") + (test-ol-stored-link-with-text "* TODO [#A] H1 :tag:\n:PROPERTIES:\n:ID: abc\n:END:\n" + (org-id-store-link-maybe t))))) + ;; create-if-interactive + (let ((org-id-link-to-org-use-id 'create-if-interactive)) + (should + (equal '("id:abc" "H1") + (cl-letf (((symbol-function 'org-id-new) + (lambda (&rest _rest) "abc"))) + (test-ol-stored-link-with-text "* H1\n" + (org-id-store-link-maybe t))))) + (should + (equal '(nil nil) + (test-ol-stored-link-with-text "* H1\n" + (org-id-store-link-maybe nil))))) + ;; create-if-interactive-and-no-custom-id + (let ((org-id-link-to-org-use-id 'create-if-interactive-and-no-custom-id)) + (should + (equal '("id:abc" "H1") + (cl-letf (((symbol-function 'org-id-new) + (lambda (&rest _rest) "abc"))) + (test-ol-stored-link-with-text "* H1\n" + (org-id-store-link-maybe t))))) + (should + (equal '(nil nil) + (test-ol-stored-link-with-text "* H1\n:PROPERTIES:\n:CUSTOM_ID: xyz\n:END:\n" + (org-id-store-link-maybe t)))) + (should + (equal '(nil nil) + (test-ol-stored-link-with-text "* H1\n" + (org-id-store-link-maybe nil))))) + ;; use-context should have no effect when on the headline with an id + (let ((org-id-link-to-org-use-id t) + (org-id-link-use-context t)) + (should + (equal '("id:abc" "H2") + (test-ol-stored-link-with-text "* H1\n** H2<point>\n:PROPERTIES:\n:ID: abc\n:END:\n" + ;; simulate previously getting an inherited value + (move-marker org-entry-property-inherited-from 1) + (org-id-store-link-maybe t)))))) + +(ert-deftest test-org-link/id-store-link-using-parent () + "Test `org-id-store-link' specifications with `org-id-link-consider-parent-id` set." + ;; when using context to still find specific heading + (let ((org-id-link-to-org-use-id t) + (org-id-link-consider-parent-id t) + (org-id-link-use-context t)) + (should + (equal '("id:abc::*H2" "H2") + (test-ol-stored-link-with-text "* H1\n:PROPERTIES:\n:ID: abc\n:END:\n** H2\n<point>" + (org-id-store-link)))) + (should + (equal '("id:abc::name" "name") + (test-ol-stored-link-with-text "* H1\n:PROPERTIES:\n:ID: abc\n:END:\n\n#+name: name\n<point>#+begin_example\nhi\n#+end_example\n" + (org-id-store-link)))) + (should + (equal '("id:abc" "H1") + (test-ol-stored-link-with-text "* H1<point>\n:PROPERTIES:\n:ID: abc\n:END:\n** H2\n" + (org-id-store-link)))) + ;; should not use newly added ids as search string, e.g. in an empty file + (should + (let (name result) + (setq result + (cl-letf (((symbol-function 'org-id-new) + (lambda (&rest _rest) "abc"))) + (test-ol-stored-link-with-text "<point>" + (setq name (buffer-name)) + (org-id-store-link)))) + (equal `("id:abc" ,name) result)))) + ;; should not find targets in the next section + (let ((org-id-link-to-org-use-id 'use-existing) + (org-id-link-consider-parent-id t) + (org-id-link-use-context t)) + (should + (equal '(nil nil) + (test-ol-stored-link-with-text "* H1\n:PROPERTIES:\n:ID: abc\n:END:\n* H2\n** <point>Target\n" + (org-id-store-link-maybe t)))))) + \f ;;; Radio Targets -- 2.37.1 (Apple Git-137.1) ^ permalink raw reply related [flat|nested] 48+ messages in thread
* Re: [PATCH v2] org-id: allow using parent's existing id in links to headlines 2024-02-08 22:30 ` Rick Lupton @ 2024-02-09 12:09 ` Ihor Radchenko 2024-02-09 12:47 ` Rick Lupton 0 siblings, 1 reply; 48+ messages in thread From: Ihor Radchenko @ 2024-02-09 12:09 UTC (permalink / raw) To: Rick Lupton; +Cc: Y. E. "Rick Lupton" <mail@ricklupton.name> writes: > On Thu, 8 Feb 2024, at 1:02 PM, Ihor Radchenko wrote: >> I have some thoughts about rewording your changes to the manual and >> ORG-NEWS. See the attached patch on top of yours. > > Thanks, makes sense -- wasn't sure whether to keep this as a separate patch or not, I have squashed into the attached updated version. Right. That was intended for squash. I sent it as a separate patch to make it easier what exactly I proposed to change. >> It would make sense to use #+caption as default description when available. > > Maybe... But I had a little look and it seems complicated, since caption is a parsed property, it's not clear to me how to get a plain string in a simple way. And there could be a long and a short caption, over multiple lines. If the caption is long, it wouldn't make a good link description anyway. > > The current behaviour is the same as it was before, so maybe we can leave this as a future enhancement if wanted? No problem. I have no further comments on this version of the patch. It is ready for merging. May you please update on your FSF copyright assignment status? -- Ihor Radchenko // yantar92, Org mode contributor, Learn more about Org mode at <https://orgmode.org/>. Support Org development at <https://liberapay.com/org-mode>, or support my work at <https://liberapay.com/yantar92> ^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH v2] org-id: allow using parent's existing id in links to headlines 2024-02-09 12:09 ` Ihor Radchenko @ 2024-02-09 12:47 ` Rick Lupton 2024-02-09 12:57 ` Ihor Radchenko 0 siblings, 1 reply; 48+ messages in thread From: Rick Lupton @ 2024-02-09 12:47 UTC (permalink / raw) To: Ihor Radchenko; +Cc: Y. E. On Fri, 9 Feb 2024, at 12:09 PM, Ihor Radchenko wrote: > May you please update on your FSF copyright assignment status? I believe the agreement is all signed and completed. Thanks Rick ^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH v2] org-id: allow using parent's existing id in links to headlines 2024-02-09 12:47 ` Rick Lupton @ 2024-02-09 12:57 ` Ihor Radchenko 2024-02-24 10:48 ` Bastien Guerry 0 siblings, 1 reply; 48+ messages in thread From: Ihor Radchenko @ 2024-02-09 12:57 UTC (permalink / raw) To: Rick Lupton, Bastien; +Cc: Y. E. "Rick Lupton" <mail@ricklupton.name> writes: > On Fri, 9 Feb 2024, at 12:09 PM, Ihor Radchenko wrote: >> May you please update on your FSF copyright assignment status? > > I believe the agreement is all signed and completed. Bastien, may your please check FSF records? -- Ihor Radchenko // yantar92, Org mode contributor, Learn more about Org mode at <https://orgmode.org/>. Support Org development at <https://liberapay.com/org-mode>, or support my work at <https://liberapay.com/yantar92> ^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH v2] org-id: allow using parent's existing id in links to headlines 2024-02-09 12:57 ` Ihor Radchenko @ 2024-02-24 10:48 ` Bastien Guerry 2024-02-24 13:02 ` Ihor Radchenko 0 siblings, 1 reply; 48+ messages in thread From: Bastien Guerry @ 2024-02-24 10:48 UTC (permalink / raw) To: Ihor Radchenko; +Cc: Rick Lupton, Y. E. Hi, Ihor Radchenko <yantar92@posteo.net> writes: > "Rick Lupton" <mail@ricklupton.name> writes: > >> On Fri, 9 Feb 2024, at 12:09 PM, Ihor Radchenko wrote: >>> May you please update on your FSF copyright assignment status? >> >> I believe the agreement is all signed and completed. > > Bastien, may your please check FSF records? Done, and it is well in order. -- Bastien Guerry ^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH v2] org-id: allow using parent's existing id in links to headlines 2024-02-24 10:48 ` Bastien Guerry @ 2024-02-24 13:02 ` Ihor Radchenko 2024-02-24 15:57 ` Rick Lupton 2024-03-05 14:05 ` Stefan 0 siblings, 2 replies; 48+ messages in thread From: Ihor Radchenko @ 2024-02-24 13:02 UTC (permalink / raw) To: Bastien Guerry; +Cc: Rick Lupton, Y. E. Bastien Guerry <bzg@gnu.org> writes: >> "Rick Lupton" <mail@ricklupton.name> writes: >> >>> On Fri, 9 Feb 2024, at 12:09 PM, Ihor Radchenko wrote: >>>> May you please update on your FSF copyright assignment status? >>> >>> I believe the agreement is all signed and completed. >> >> Bastien, may your please check FSF records? > > Done, and it is well in order. Thanks for checking! Applied, onto main. https://git.savannah.gnu.org/cgit/emacs/org-mode.git/commit/?id=6e7e0b2cd https://git.savannah.gnu.org/cgit/emacs/org-mode.git/commit/?id=95554543b and added a record to the contributor list. https://git.sr.ht/~bzg/worg/commit/0ccaf58a Rick, thanks for your contribution! -- Ihor Radchenko // yantar92, Org mode contributor, Learn more about Org mode at <https://orgmode.org/>. Support Org development at <https://liberapay.com/org-mode>, or support my work at <https://liberapay.com/yantar92> ^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH v2] org-id: allow using parent's existing id in links to headlines 2024-02-24 13:02 ` Ihor Radchenko @ 2024-02-24 15:57 ` Rick Lupton 2024-03-05 14:05 ` Stefan 1 sibling, 0 replies; 48+ messages in thread From: Rick Lupton @ 2024-02-24 15:57 UTC (permalink / raw) To: Ihor Radchenko; +Cc: Y. E. Thanks for your help with it! On Sat, 24 Feb 2024, at 1:02 PM, Ihor Radchenko wrote: > Bastien Guerry <bzg@gnu.org> writes: > >>> "Rick Lupton" <mail@ricklupton.name> writes: >>> >>>> On Fri, 9 Feb 2024, at 12:09 PM, Ihor Radchenko wrote: >>>>> May you please update on your FSF copyright assignment status? >>>> >>>> I believe the agreement is all signed and completed. >>> >>> Bastien, may your please check FSF records? >> >> Done, and it is well in order. > > Thanks for checking! > Applied, onto main. > https://git.savannah.gnu.org/cgit/emacs/org-mode.git/commit/?id=6e7e0b2cd > https://git.savannah.gnu.org/cgit/emacs/org-mode.git/commit/?id=95554543b > > and added a record to the contributor list. > https://git.sr.ht/~bzg/worg/commit/0ccaf58a > > Rick, thanks for your contribution! > > -- > Ihor Radchenko // yantar92, > Org mode contributor, > Learn more about Org mode at <https://orgmode.org/>. > Support Org development at <https://liberapay.com/org-mode>, > or support my work at <https://liberapay.com/yantar92> ^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH v2] org-id: allow using parent's existing id in links to headlines 2024-02-24 13:02 ` Ihor Radchenko 2024-02-24 15:57 ` Rick Lupton @ 2024-03-05 14:05 ` Stefan 2024-03-05 14:51 ` Ihor Radchenko 1 sibling, 1 reply; 48+ messages in thread From: Stefan @ 2024-03-05 14:05 UTC (permalink / raw) To: emacs-orgmode Hi, since this patch was applied, there are now two functions in `org-store-link-functions` that feel responsible for id links from some buffers: the new `org-id-store-link-maybe` and `org-contacts-link-store`. This results in a prompt asking which one to use, which happens many times, e.g., when exporting agendas with org-contacts stuff in them. Not sure how to improve/avoid that, though. Best, Stefan ^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH v2] org-id: allow using parent's existing id in links to headlines 2024-03-05 14:05 ` Stefan @ 2024-03-05 14:51 ` Ihor Radchenko 0 siblings, 0 replies; 48+ messages in thread From: Ihor Radchenko @ 2024-03-05 14:51 UTC (permalink / raw) To: Stefan; +Cc: emacs-orgmode Stefan <org@stefan.failing.systems> writes: > since this patch was applied, there are now two functions in `org-store-link-functions` that feel responsible for id links from some buffers: the new `org-id-store-link-maybe` and `org-contacts-link-store`. > > This results in a prompt asking which one to use, which happens many times, e.g., when exporting agendas with org-contacts stuff in them. > > Not sure how to improve/avoid that, though. Org mode does not add the two functions there. It is likely something inside your config. -- Ihor Radchenko // yantar92, Org mode contributor, Learn more about Org mode at <https://orgmode.org/>. Support Org development at <https://liberapay.com/org-mode>, or support my work at <https://liberapay.com/yantar92> ^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH] org-id: allow using parent's existing id in links to headlines 2023-07-24 11:40 [PATCH] org-id: allow using parent's existing id in links to headlines Rick Lupton 2023-07-25 7:43 ` Ihor Radchenko @ 2023-11-04 23:01 ` Rick Lupton 2023-11-05 12:31 ` Ihor Radchenko 1 sibling, 1 reply; 48+ messages in thread From: Rick Lupton @ 2023-11-04 23:01 UTC (permalink / raw) To: Y. E. I realised there is another question here about how search strings are used in org-id links. Consider this example file: * Heading :PROPERTIES: :ID: 06E767E6-6145-45EB-B736-D350449126EC :END: #+name: named-thing #+begin_example Hi! #+end_example By default (`org-id-link-to-org-use-id` is nil), with point on `#+name: named-thing`, calling `org-store-link` will give a link like `[[file:test.org::named-thing][named-thing]]` which leads directly to the named example block. Different uses can also lead to search strings which link to headings, selected text in the region, or the current line's text. When `org-id-link-to-org-use-id` is non-nil, none of this happens -- calling `org-store-link` anywhere within the subtree will result in a link `[[id:06E767E6-6145-45EB-B736-D350449126EC][Heading]]` with no additional search string. My previous patch changes the behaviour when `org-id-link-to-org-use-id` has a new value (`inherit`) in two ways: (a) org-ids from parent headings are considered when choosing the ID to link to, and (b) search strings are added to the link But these are actually two independent things. So my question is: should search strings be added to all org-id links? This would make org-id links more powerful/precise (because you can link to more precise locations within the subtree), and simplifies the code in `org-store-link` in my patch (because point [b] above would apply to all org-id links, not just the new 'inherit ones), but it could change the behaviour when calling `org-store-link` with an active region or when point is on a named element. Depending on the answer, I can update the patch accordingly. Thanks, Rick ^ permalink raw reply [flat|nested] 48+ messages in thread
* Re: [PATCH] org-id: allow using parent's existing id in links to headlines 2023-11-04 23:01 ` [PATCH] " Rick Lupton @ 2023-11-05 12:31 ` Ihor Radchenko 0 siblings, 0 replies; 48+ messages in thread From: Ihor Radchenko @ 2023-11-05 12:31 UTC (permalink / raw) To: Rick Lupton; +Cc: Y. E. "Rick Lupton" <mail@ricklupton.name> writes: > My previous patch changes the behaviour when `org-id-link-to-org-use-id` has a new value (`inherit`) in two ways: > > (a) org-ids from parent headings are considered when choosing the ID to link to, and > (b) search strings are added to the link > > But these are actually two independent things. So my question is: should search strings be added to all org-id links? > This would make org-id links more powerful/precise (because you can link to more precise locations within the subtree), and simplifies the code in `org-store-link` in my patch (because point [b] above would apply to all org-id links, not just the new 'inherit ones), but it could change the behaviour when calling `org-store-link` with an active region or when point is on a named element. Sounds as a reasonable default, but users should have an option to revert to previous behaviour with heading id being stored. Otherwise, we may break the muscle memory for people used to what happens now. -- Ihor Radchenko // yantar92, Org mode contributor, Learn more about Org mode at <https://orgmode.org/>. Support Org development at <https://liberapay.com/org-mode>, or support my work at <https://liberapay.com/yantar92> ^ permalink raw reply [flat|nested] 48+ messages in thread
end of thread, other threads:[~2024-11-13 3:30 UTC | newest] Thread overview: 48+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2023-07-24 11:40 [PATCH] org-id: allow using parent's existing id in links to headlines Rick Lupton 2023-07-25 7:43 ` Ihor Radchenko 2023-07-25 15:16 ` Max Nikulin 2023-07-26 8:10 ` Ihor Radchenko 2023-07-27 0:16 ` Samuel Wales 2023-07-27 7:42 ` IDs below headline level (for paragraphs, lists, etc) (was: [PATCH] org-id: allow using parent's existing id in links to headlines) Ihor Radchenko 2023-07-28 20:00 ` Rick Lupton 2023-07-28 19:56 ` [PATCH] org-id: allow using parent's existing id in links to headlines Rick Lupton 2023-07-29 8:33 ` Ihor Radchenko 2023-11-09 20:56 ` Rick Lupton 2023-11-10 10:03 ` Ihor Radchenko 2023-11-19 15:21 ` Rick Lupton 2023-12-04 13:23 ` Rick Lupton 2023-12-10 13:35 ` Ihor Radchenko 2023-12-14 20:42 ` Rick Lupton 2023-12-15 12:55 ` Ihor Radchenko 2023-12-15 16:16 ` Rick Lupton 2023-12-16 14:20 ` Ihor Radchenko 2023-12-17 19:07 ` [PATCH v2] " Rick Lupton 2023-12-18 12:27 ` Ihor Radchenko 2024-01-02 16:13 ` Rick Lupton 2024-01-03 14:17 ` Ihor Radchenko 2024-01-28 22:47 ` Rick Lupton 2024-01-29 0:20 ` Samuel Wales 2024-01-29 13:06 ` Ihor Radchenko 2024-01-30 0:03 ` Samuel Wales 2024-02-03 15:08 ` Ihor Radchenko 2024-11-13 3:23 ` Samuel Wales 2024-01-29 13:00 ` Ihor Radchenko 2024-01-31 18:11 ` Rick Lupton 2024-02-01 12:13 ` Ihor Radchenko 2024-02-01 16:37 ` Rick Lupton 2024-02-03 13:10 ` Ihor Radchenko 2024-02-08 8:24 ` [PATCH] lisp/ol.el: Improve docstring Rick Lupton 2024-02-08 14:52 ` Ihor Radchenko 2024-02-08 8:46 ` [PATCH v2] org-id: allow using parent's existing id in links to headlines Rick Lupton 2024-02-08 13:02 ` Ihor Radchenko 2024-02-08 22:30 ` Rick Lupton 2024-02-09 12:09 ` Ihor Radchenko 2024-02-09 12:47 ` Rick Lupton 2024-02-09 12:57 ` Ihor Radchenko 2024-02-24 10:48 ` Bastien Guerry 2024-02-24 13:02 ` Ihor Radchenko 2024-02-24 15:57 ` Rick Lupton 2024-03-05 14:05 ` Stefan 2024-03-05 14:51 ` Ihor Radchenko 2023-11-04 23:01 ` [PATCH] " Rick Lupton 2023-11-05 12:31 ` Ihor Radchenko
Code repositories for project(s) associated with this public inbox https://git.savannah.gnu.org/cgit/emacs/org-mode.git This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox; as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).