Elisp Posts

FULL example of org-mode links: internal links and search options

2022-04-27
/Tony Aldon/
comment on reddit
/
emacs revision: de7901abbc21
/
org-mode revision: af6f1298b6f6

Hey Emacser,

I hope you are doing well :)

In this post (2022-04-04) we talked about search options in file links, link abbreviations and the implementation of the command org-open-at-point.

My goal was to walk the path from pressing RET (if you have org-return-follows-link set to t) or C-c C-o on top of the abbreviated org link

[[emacs:lisp/simple.el::(defun next-error (&optional]]

to the file lisp/simple.el in the Emacs source code (cloned locally) with the point at the beginning of the function next-error.

We have seen that this path follows the following "call stack":

org-open-at-point
│
└> org-link-open
   │
   └> org-link-open-as-file
      │
      └> org-open-file
         │
         └> org-link-search

As the post has become bigger than I expected, I decided not to talk about the last call to the function org-link-search, despite my desire to do so.

Some time later, I decided to give it a chance. I looked org-link-search in the eyes and I saw that I won't be able to give a clear explanation without talking a least in one post about the catch/throw pattern in Elisp. So, I postponed it and wrote the post A tour of the catch/throw pattern in the Emacs source code (2022-04-13) in which we discuss the catch/throw pattern.

And today I'm trying to give a clear explanation of its implementation and I realized that the way to do it is not by showing bits of code and talking about those bits.

There is too much details to bring some value doing it like this (at least, I still haven't figured out how to do it that way for that function).

So let's try another approach that may give you some "tools" to examine its implementation if you are interested.

First, we look at a "full" example that features org links that, in the end, use org-link-search to make the jump when we call org-open-at-point on them.

Next, we give some practical examples demonstrating part of the Elisp API dealing with strings and regexps functions that are used in org-link-search.

With that done, just between us, let's say we are done with org-link-search function and we've completed the walk inside org-open-at-point function :)

Full example of org links that use org-link-search when followed

The following example features org links that, in the end, use org-link-search to make the jump when we call org-open-at-point on them.

Notes on the example:

For three of those links to work you need to cloned the org-mode repository under the directory /tmp/org-mode/. You can do this by running the following command:

cd /tmp/ && git clone git://git.sv.gnu.org/emacs.git

The example is written inside a unique source block (org-mode), so you have several options to take advantage of it:

  1. if you are reading this post on Reddit, copy/paste the block in an org-mode buffer and start playing with it,

  2. if you are reading this post from inside Emacs (using this document https://github.com/tonyaldon/posts) with the point inside the source block you can hit C-c ' (org-edit-special by default) and start playing with the example in a org-mode buffer.

For convenience and to make the following example almost selfcontained, we use for instance the link [[#custom-id-2]] that is of type custom-id instead of demonstrating the use of org-link-search with a link like this [[/path-to-file.org::#custom-id-2]] which is of type file with the search option component equal to #custom-id-2.

But it doesn't matter much because, in the end, both are treated the same way by the same function org-link-search, and the exact last call in both cases is (org-link-search "#custom-id-2").

In the example, the links are presented in the same order as they are treated in the cond special form in the body of org-link-search. If you finally decide to look at the implementation of org-link-search, you'll be able to follow the "flow" in the implementation following the "flow" of the example or vice-versa, maybe side by side using two different buffers (by the way this is how I built the example, side by side :-)).

Lastly, the info node related to this example are the following:

  • org#Internal Links,

  • org#Search Options,

  • org#Literal Examples,

  • org#Org Syntax.

Here is the example (if you're reading in your browser, you don't see the double brackets around links, for instance the first link is represented like this #custom-id-2, but is instead [[#custom-id-2]], moreover (ref:coderef) doesn't appear in the first source block):

* headline 1

With #custom-id-2 link we jump (with org-open-at-point) in this
document to the heading with the :CUSTOM_ID property equal to
custom-id-2.

With (coderef) link we jump (with org-open-at-point) to (ref:coderef)
in the source block below:

#+NAME: a named source block
#+BEGIN_SRC emacs-lisp
(let ((x 2))
  (1+ x))
#+END_SRC

Let assume we have the org repository cloned under the directory
/tmp/org-mode/.

With /tmp/org-mode/lisp/ol.el::/org-link-search/ we jump (with
org-open-at-point) to an Occur buffer like this one

#+BEGIN_SRC text
6 matches for "org-link-search" in buffer: ol.el
    340:(defcustom org-link-search-must-match-exact-headline 'query-to-create
   1093:       (org-link-search
   1132:(defun org-link-search (s &optional avoid-pos stealth)
   1245:     ;; `org-link-search-must-match-exact-headline'.
   1247:     (eq org-link-search-must-match-exact-headline 'query-to-create)
   1257:     (or starred org-link-search-must-match-exact-headline))
#+END_SRC

that matches the occurences of org-link-search~(the text surrounded by
two slashes / after the two colons :: in the previous link) in the
file /tmp/org-mode/lisp/ol.el.

With target link we jump (with org-open-at-point) to the target
<<target>> that is located in the first paragraph of the section
headline 2.

With a named source block link we jump (with org-open-at-point) to the
previous source block that is named a named source block via the
statment #+NAME: a named source block (because there is no target <<a
named source block>> in the document).

With headline 2 link we jump (with org-open-at-point) to the next
headline headline 2, whatever the value of the variable
org-link-search-must-match-exact-headline, because:

1) this headline exists,
2) there is no target <<headline 2>> in the document,
3) there is no named block, named paragagraph, etc. (see Affiliated
   Keywords in the org syntax) named with the statment #+NAME headline
   2.

#+NAME: headline 3
This paragagraph named headline 3 contains a target <<headline 3>>, so
we can't use the link [[headline 3]] to jump to the existing headline
headline 3.  But, we can use the following link * headline 3 (starting
with a star *) to jump to the existing headline headline 3.  And this
work whatever the value of the variable
org-link-search-must-match-exact-headline.

When org-link-search-must-match-exact-headline is set to
query-to-create (which is the default value)

#+BEGIN_SRC emacs-lisp
(setq org-link-search-must-match-exact-headline 'query-to-create)
#+END_SRC

calling org-open-at-point on headline 4 link offers to create a new
headline headline 4 at the end of this org document.  If we choose to
add it we will jump to that new headline and if not nothing happens
and we don't move.

When org-link-search-must-match-exact-headline is set to something
other than query-to-create, for instance t or nil like this:

#+BEGIN_SRC emacs-lisp
(setq org-link-search-must-match-exact-headline nil)
;; or
(setq org-link-search-must-match-exact-headline t)
#+END_SRC

calling org-open-at-point on the link * headline 5 (starting with a
star *) that points to a non existing headline raises the following
error:

: No match for fuzzy expression: * headline 5

When org-link-search-must-match-exact-headline is set t

#+BEGIN_SRC emacs-lisp
(setq org-link-search-must-match-exact-headline t)
#+END_SRC

calling org-open-at-point on the link headline 5 (and there is no
existing headline headline 5, there is no target <<headline 5>> and
there is no named element headline 5 in the document) raises the
following error:

: No match for fuzzy expression: * headline 5

When org-link-search-must-match-exact-headline is set nil

#+BEGIN_SRC emacs-lisp
(setq org-link-search-must-match-exact-headline nil)
#+END_SRC

calling org-open-at-point on the link headline 5 (and there is no
existing headline headline 5, there is no target <<headline 5>> and
there is no named element headline 5 in the document) jumps to first
occurence of headline 5 in the current document.

We still assume we have the org repository cloned under the directory
/tmp/org-mode/.

With /tmp/org-mode/lisp/ol.el::org-link-search we jump (with
org-open-at-point) to the first occurence of org-link-search in the
file /tmp/org-mode/lisp/ol.el which happens to be on the variable
org-link-search-must-match-exact-headline:

#+BEGIN_SRC emacs-lisp
(defcustom org-link-search-must-match-exact-headline 'query-to-create
  ...)
#+END_SRC

Note that as the file /tmp/org-mode/lisp/ol.el is not "open" in
org-mode, org-link-search does a fuzzy text search and doesn't look
for target, named elements or headlines.

If the search option (or internal links) we've used doesn't "match"
one of the previous search, org-link-search raises an error.

We still assume we have the org repository cloned under the directory
/tmp/org-mode/.

With /tmp/org-mode/lisp/ol.el::5 we jump (with org-open-at-point) to
jump to the line 5 in the file /tmp/org-mode/lisp/ol.el.

Although this link is of type file with its search option equal to 5,
the "jump" isn't done by org-link-search but by org-goto-line in the
function org-open-file.

* headline 2
:PROPERTIES:
:CUSTOM_ID: custom-id-2
:END:

I'm the <<target>>!

* headline 3

Elisp API dealing with strings and regexps

Remember that when we are working with Elisp we can obtain information about any symbols with the command describe-symbol bound by default to C-h o.

In org-link-search, the variable case-fold-search is set t which means that searches and matches should ignore case:

(let ((case-fold-search t))
  (string-match "FOO" "foo")) ; 0

(let ((case-fold-search nil))
  (string-match "FOO" "foo")) ; nil

In org-link-search, the search string s provided can contain newlines followed by any numbers of spaces or tabs. Those patterns are replaced by one space. This is done using the function replace-regexp-in-string like this:

(let ((s "search  \n  \t\t option"))
  (replace-regexp-in-string "\n[ \t]*" " " s))
;; "search   option"

The search option string s given to org-link-search can start:

  1. with a star * when we search specifically a headline (for instance in the link [[* headline 3]] used in the above example) or,

  2. with a hash # when we search a custom id (for instance in the link [[#custom-id-2]] used in the above example)

The function string-to-char returns the first character of a string and we can use like this:

(string-to-char "*foo") ; 42
?* ; 42
(string-to-char "#bar") ; 35
?# ; 35
(eq (string-to-char "* headline 3") ?*) ; t
(eq (string-to-char "#custom-id-2") ?#) ; t

In org-link-search, the searches are not done directly against its given string s but against different regexps depending on the context built from s.

The function split-string is used to split one of the string into substrings bounded by whitespace like this:

(split-string "foo bar baz") ; ("foo" "bar" "baz")

When the given string s starts with a star *, the star is removed using the function substring like this:

(substring "*foo" 1) ; "foo"

The searches are done using the function re-search-forward that searches in the current buffer for regular expression. So, we have to be careful when we give it a string to search for, that string must be a regexp.

We can ensure that the string we give to re-search-forward is a regexp using the function regexp-quote

that returns a regexp string which matches exactly the string we gave it and this is how org-link-search does it.

For instance, the characters ., ?, + and * are "special" in regexps, so if we want to match them in a regexp we must escape them and we can do it using the function regexp-quote like this:

(regexp-quote "foo.bar") ; "foo\\.bar"
(regexp-quote "foo+bar") ; "foo\\+bar"
(regexp-quote "foo?bar") ; "foo\\?bar"
(regexp-quote "foo*bar") ; "foo\\*bar"

We can go on and on but we won't, I think that's enought to get started :)

WE ARE DONE !!!

Acknowledgments

I take the opportunity of this post to thank Ihor Radchenko for his work on org-mode.

In addition to his contributions he always answers quickly in https://orgmode.org/worg/org-mailing-list.html.

Thank you Ihor Radchenko.