emacs-elpa-diffs
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[elpa] externals/org 95554543b9 2/2: org-id.el: Add search strings, inhe


From: ELPA Syncer
Subject: [elpa] externals/org 95554543b9 2/2: org-id.el: Add search strings, inherit parent IDs
Date: Sat, 24 Feb 2024 09:58:40 -0500 (EST)

branch: externals/org
commit 95554543b98513fb807a72a9fc5256e92c4cece0
Author: Rick Lupton <mail@ricklupton.name>
Commit: Ihor Radchenko <yantar92@posteo.net>

    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 2be6c92cd6..89592b12da 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:
@@ -21522,7 +21553,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.
@@ -21582,13 +21613,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 c4e1263bf4..b79f275c4d 100644
--- a/etc/ORG-NEWS
+++ b/etc/ORG-NEWS
@@ -460,6 +460,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
@@ -538,6 +546,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
 *** New option ~org-beamer-frame-environment~
 
@@ -868,6 +898,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
 
@@ -1164,6 +1223,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 be3924fc91..6ea060f701 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 c5a950e2d3..38468f32f1 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 b31f1ce5eb..0907ddee1d 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 2c7ec6bef5..595dd0ee0f 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 40b820d2b1..c13dbf339e 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 e105fdb2c9..b9ee8683fa 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 0edf9a13fe..6062cab34d 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 78c4884b00..b263e52db6 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 e3f13815e2..42aacea815 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 106cfedc97..a32481324f 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 f6031ab52c..f1f753b6f3 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 a680c43f3f..22782578c8 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)))
+
 
 ;;; 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 8647a57cc0..58d51decac 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 e65f9a7eb2..9e9d562535 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)
@@ -1697,6 +1708,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 e0cec0854a..3150b4e2f1 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))))))
+
 
 ;;; Radio Targets
 



reply via email to

[Prev in Thread] Current Thread [Next in Thread]