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?

5
  • 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
    Commented Feb 6, 2018 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
    Commented Feb 6, 2018 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
    Commented Feb 6, 2018 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
    Commented Feb 6, 2018 at 20:13
  • See my answer 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
    Commented Feb 8, 2018 at 9:26

1 Answer 1

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!
4
  • what hook would you use to re-fontify org-mode links, after a heading has been modified? Thnx!
    – Adam
    Commented Feb 9, 2018 at 6: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
    Commented Feb 9, 2018 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
    Commented Feb 16, 2018 at 0:20
  • @Adam I've added save-match-data. Could you try again?
    – Tobias
    Commented Feb 16, 2018 at 12:29

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.