Backlink counters

For a long time I wanted to add a thing similar to what’s known to anyone who ever used lsp-mode in Emacs with lenses feature enabled. lsp-mode can show number of references for any given function.

I wanted a similar thing in Org-Roam - to see number of backlinks next to a heading.

Finally, I’ve got to figure that out. I borrowed the initial implementation of the idea from here: My Doom Emacs configurations · Hieu Phay

Although there, the author doing something similar, yet not the exact thing - they are displaying number of references (backlinks) next to each link. I wanted to see the numbers for headings.

Another departure from the original implementation - instead of using (org-element-map), I decided to use regex-based approach - org-element-map has to scan the entire buffer and could be costly, regex should be much faster. Too bad there isn’t yet solid org-mode treesitter grammar - that would’ve even better.

Here’s the code:

    (defface org-roam-count-overlay-face
      '((t :inherit org-list-dt :height 0.6 :underline nil :weight light))
      "Face for Org Roam count overlay.")

    (defun org-roam--count-overlay-make (pos count)
      (let* ((overlay-value (propertize
                             (format "│%d│ " count)
                             'face 'org-roam-count-overlay-face
                             'display '(raise 0.3)))
             (ov (make-overlay pos pos (current-buffer) nil t)))
        (overlay-put ov 'roam-backlinks-count count)
        (overlay-put ov 'priority 1)
        (overlay-put ov 'after-string overlay-value)))

    (defun org-roam--count-overlay-remove-all ()
      (dolist (ov (overlays-in (point-min) (point-max)))
        (when (overlay-get ov 'roam-backlinks-count)
          (delete-overlay ov))))

    (defun org-roam--count-overlay-make-all ()
      (save-excursion
        (goto-char (point-min))
        (org-roam--count-overlay-remove-all)
        (while (re-search-forward "^\\(*+ \\)\\(.*$\\)\n\\(:properties:\\)\n\\(?:.*\n\\)*?:id:\\s-+\\([^[:space:]\n]+\\)" nil :no-error)
          (when-let* ((pos (match-beginning 2))
                      (id (string-trim (substring-no-properties (match-string 4))))
                      (count (caar
                              (org-roam-db-query
                               [:select (funcall count source)
                                :from links
                                :where (= dest $s1)
                                :and (= type "id")]
                               id))))
            (when (< 0 count)
              (org-roam--count-overlay-make pos count))))))

    ;;;###autoload
    (define-minor-mode org-roam-count-overlay-mode
      "Display backlink count for org-roam links."
      :after-hook
      (if org-roam-count-overlay-mode
          (progn
            (org-roam--count-overlay-make-all)
            (add-hook 'after-save-hook #'org-roam--count-overlay-make-all nil t))
        (org-roam--count-overlay-remove-all)
        (remove-hook 'after-save-hook #'org-roam--count-overlay-remove-all t)))

You may want to grab it directly from my config, most likely I won’t be updating it here every time I make changes.

Here’s a screenshot:

Note that I couldn’t figure out how to place the overlay at the end of the heading - it only works when the heading is not collapsed, so I had to add the at the front of the heading.

Hope someone finds this helpful.

2 Likes

Darn, I just realized that I probably want to have the counters in file-nodes as well, not only the headings.

This is awesome!

One of the features I’ve missed since switching from Semantic Synchrony to org-roam was the counts of parents and children. If I have time I’ll look at your code to see if I can work out how to extend it to count children too.

If you change how you’re doing it could you please post a note to that effect in this thread?

What would be “parents” and what would be “children” here?

e.g., using my screenshot as the example: “Concurrency” note is linked from 3 other locations - that’s number of backlinks.

If I understand it right, you also want to see the number of links inside “Concurrency” heading and subheadings, i.e., number of "forward-links’?

The problem with that is that Org-Roam treats each node independently in its database. When a subheading is a node itself (has an :ID: property), it’s treated as a separate entity and its links are associated with that node’s ID, not the parent node. i.e., for:

 * Foo
 :PROPERTIES:
 :ID: DCBC12CD-4E28-4F2C-A2F5-702284EBDCC6
 :END:

 [[id:D742812E-6523-4731-9CC1-8DCD6253A210][Link 1]]
 [[id:D9F71D4B-5F06-4A53-977D-534FE0CB82C4][Link 2]]


 ** Bar
 [[id:0FBF54D1-87EE-4DBC-831E-B2EF0C335096][Link 3]]

(let ((id "DCBC12CD-4E28-4F2C-A2F5-702284EBDCC6"))
 (org-roam-db-query
  [:select (funcall count dest)
   :from links
   :where (= source $s1)
   :and (= type "id")]
  id))

Would return 3.

But if you change the **Bar to have its own ID - same query would return 2, and that’s not great.

You’re right, that was unclear, thanks. I just meant the number of branches under a given headline in the org tree, ignoring org links.

Now that you make me think about it, though, links in the text immediately under the headline also ought to count as children. Which suggests there are actually four additional kinds of counts you could want:

  • Number of sub-headlines
  • Number of sub-headlines, recursively (sub-sub-headlines too, etc.)
  • Number of links in the text immediately under the headline
  • Number of links under the headline, recursively

Which would kind of be a lot of clutter. If I had the possibility of showing all of those things, I would want to be able to toggle it.

But the number of backlinks, which you implemented, is definitely the most important of those things to show. The others can be seen by just opening the headline (albeit maybe doing a lot of scrolling and counting in your head).

Direct subheadings, nested subheadings, immediate links, etc. I personally don’t see any value of having any of them shown all the time. Sounds like a visual mess and confusion.

I can see the value of showing the number of Org-Roam backlinks. I also can be convinced to show the number of direct (forward) links as well (if there’s a sensible workaround for the issue I described earlier)

All those other things you have mentioned can be calculated and displayed on demand - it simply is impractical and surely will have performance problems with larger files to have them calculated in real-time.

Perhaps it can be customized to work in Column View (The Org Manual)