3

Is there a function that will validate internal fuzzy links in org-mode? I would like to see which links resolve to headers (fontified in blue) and which ones are broke (fontified in red).

John Kitchin had a good solution for external links:

(org-link-set-parameters
 "file"
 :face (lambda (path) (if (file-exists-p path) 'org-link 'org-warning)))

and I found this code, as part of org-lint:

  (defun org-lint-invalid-fuzzy-link (ast)
    (let ((info (list :parse-tree ast)))
      (org-element-map ast 'link
        (lambda (link)
          (and (equal (org-element-property :type link) "fuzzy")
               (not (ignore-errors (org-export-resolve-fuzzy-link link info)))
               (list (org-element-property :begin link)
                     (format "Unknown fuzzy location \"%s\""
                             (let ((path (org-element-property :path link)))
                               (if (string-prefix-p "*" path)
                                   (substring path 1)
                                 path)))))))))

Is there is a way to marry these together?

Drew
  • 75,699
  • 9
  • 109
  • 225
Adam
  • 1,857
  • 11
  • 32
  • Pityingly `org-set-link-parameters` not the right path. `org-link-set-parameters` is for named link types. Fuzzy links don't belong to that class. `org-open-at-point` uses `org-element-type` to identify fuzzy links and `org-link-search` with `(org-link-unescape (org-element-property :path context))` as search string and `AVOID-POS = (+ 2 (org-element-property :begin context))` to determine the destination. – Tobias Feb 06 '18 at 14:50
  • Your suggested solution ansatz with `org-lint-invalid-fuzzy-link` would need `org-element-parse-buffer` to calculate the argument `ast` ("ast" == "abstract syntax tree" of the org buffer). That function is not appropriate for just-in-time font locking because it is too heavy. – Tobias Feb 06 '18 at 15:01
  • I think `org-link-search` would also be too slow for jit. One needs to cache the search results somehow and to keep the cache up-to-date when the **destination** is edited. That task is not so easy. Note that I did not ignore your question after you asked it. But that task is not as easy as it may appear at first sight of the question. – Tobias Feb 06 '18 at 15:21
  • Thanks @Tobias. Would `org-link-search` be too slow if it is only triggered when a link (or header) is created or updated? – Adam Feb 06 '18 at 20:13
  • See [my answer](https://emacs.stackexchange.com/a/38680/2370) also as answer to your last comment. It is almost always better to measure the performance than to guess from the complexity of the function. – Tobias Feb 08 '18 at 09:26

1 Answers1

3

Links in org-files are font-locked by function org-activate-links. That function also looks for "fuzzy" link types in the alist org-link-parameters. So we are lucky and can also use org-link-set-parameters for fuzzy links.

As discussed in the comments to the question org-link-search can be exploited to search for fuzzy link targets.

One has to take several measures to protect font lock from the actions of org-link-search but finally I got a working version.

You have to test for yourself whether this slows down org-mode in big org files.

(require 'org)

(org-link-set-parameters
 "fuzzy"
 :face (lambda (path)
         (let ((org-link-search-inhibit-query t))
           (if (condition-case nil
                   (save-excursion
                     (save-match-data
                       (org-link-search path (point) t)))
                 (error nil))
               'org-link 'org-warning))))

(defvar org-fuzzy-link-fontification-required nil
  "Set to t when fontification of org fuzzy links is required.")

(defun org-check-headline-modifications (beg end &optional pre-length)
  "Re-fontify after a change of headlines in region from BEG to END.
This function can be used for the hooks `before-change-functions'
and `after-change-functions'.
A call from `after-change-functions' is identified by PRE-LENGTH."
  (save-excursion
    (save-restriction
      (save-match-data
        (goto-char beg) (forward-line 0)
        (if (or org-fuzzy-link-fontification-required
                (org-at-heading-p)
                (search-forward (concat "^" outline-regexp) end t))
            (if pre-length
                (progn
                  (setq org-fuzzy-link-fontification-required nil)
                  (font-lock-flush))
              (setq org-fuzzy-link-fontification-required t))
          )))))

(defun org-setup-check-headline-modifications ()
  "Setup `org-mode' to check headline modifications."
  (add-hook 'before-change-functions #'org-check-headline-modifications nil t)
  (add-hook 'after-change-functions #'org-check-headline-modifications nil t))

(add-hook 'org-mode-hook #'org-setup-check-headline-modifications)

Tested with:

  • emacs-version: 25.3.2
  • org-version: 9.1.6

Possible improvements:

  • DONE: If a heading is modified links should be re-fontified. The easiest way is to invalidate font-lock for the whole buffer (font-lock-flush (point-min) (point-max)). Do that only when leaving the modified heading to avoid a performance problems!
Tobias
  • 32,569
  • 1
  • 34
  • 75
  • what hook would you use to re-fontify org-mode links, after a heading has been modified? Thnx! – Adam Feb 09 '18 at 06:22
  • @Adam `before-change-functions` and `after-change-functions` are natural candidates. `before-change-functions` is needed because links can become invalid through deletion of headlines. The additional need of `after-change-functions` is quite clear (any kind of newly introduced headlines). I've added a prototype implementation. – Tobias Feb 09 '18 at 11:36
  • Hi @Tobias I've been using this code successfully for about a week now. However... I recently created an org-capture template that posts to a journal using file+datetree. Something in this code is "clobbering" that template. Here is the link to the error message: emacs.stackexchange.com/questions/38800/… Any ideas on how to make this compatible? thanks! – Adam Feb 16 '18 at 00:20
  • @Adam I've added `save-match-data`. Could you try again? – Tobias Feb 16 '18 at 12:29