[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