d12frosted / vulpea

A collection of functions for note taking based on `org` and `org-roam`.
GNU General Public License v3.0
236 stars 12 forks source link

org-roam-v2 parent node titles in vulpea-find? #178

Open iwooden opened 2 months ago

iwooden commented 2 months ago

Hi! First off, thanks for your work on this project - I'm interested in using vulpea over the native org-roam interactive functions primarily for performance reasons, though the tag/metadata stuff looks like it'll be useful if I dig into it more.

I'm using org-roam-v2 on Doom Emacs, and I heavily use the "headings as nodes" feature of v2 rather than "one node per file" like in v1. One thing that's really useful is that when I use org-roam-node-find, the candidates will show all their parent headers, and in fact the search text will match all child nodes even if just the parent node title matches:

Screenshot 2024-05-24 at 11 47 25 AM

This makes it very easy to disambiguate nodes that have similar titles by looking at their parent titles, jump to specific child nodes under a given heading, etc. If I use vulpea-find, it only matches against the node titles themselves:

Screenshot 2024-05-24 at 11 47 46 AM

Is there some way to config Vulpea to get similar behavior from vulpea-find? Keep in mind I'm a relatively new Emacs user, so I apologize if this question is trivial or I missed some easy config option for this.

Thanks again!

d12frosted commented 2 months ago

@iwooden Hey, all good! Thanks for reaching me out. There is no way to do it out of the box, but it can be implemented. I am OOO today and will not be able to share anything, but will try to do it tomorrow.

d12frosted commented 2 months ago

@iwooden can you please tell me how you include parent titles in org-roam? I wanted to check if behaviour I've coded is the same, but by default org-roam-node-find doesn't include anything 🤔 I guess you are using some custom org-roam-node-display-template, right?

I am also curious to know what you use to understand performance differences. Naïve approach would be slow on a huge collection of notes (assuming synchronous completion).

iwooden commented 2 months ago

Ah yep, you're totally right. As I mentioned, I'm using a (mostly) unmodified Doom Emacs, which apparently sets a custom org-roam-node-display-template as seen here. That function uses doom-hierarchy, which I'm pretty sure is defined here, which in turn uses org-roam-node-olp which... I'm not sure what's going on since I barely know any elisp and that goes out the window when it appears to be using some CLOS shim, lol. But I think it's referencing a "slot", which maybe means that the OLP is cached somehow instead of needing to be computed each time?

I'm not really using any profiling to determine performance differences (I know, heresy), I just noticed that org-roam-node-find was taking much longer as I got closer to a thousand nodes. My non-Emacs intuition says it shouldn't take that long to query 1k rows from sqlite, so I started looking for alternatives, found vulpea, and here we are!

In any case, it sounds like this is a major pain to do efficiently, but you've already helped quite a bit by pointing out org-roam-node-display-template. My rough thoughts at the moment are that the DB schema in sqlite might be extended to store the OLP info to avoid recalculation, and now I just need to level up my elisp skills enough to give it a shot. 🙂

d12frosted commented 2 months ago

I completely forgot about org-get-outline-path. In that case it should be fairly simple. I can add it to the vulpea table and there should be no penalty on read as there is no need to join multiple tables as it's done in org roam.

The only catch, it doesn't include the file title, i.e. title of the file-level note. But it's easy to get in performant way.

Let me add outline-path to vulpea-note.

d12frosted commented 2 months ago

The master branch now includes a commit that adds OLP to vulpea-note which you can use to include full outline path in the selection.

Outline path without file-level note title

(defun my-vulpea-select-describe-fn (note)
  "Describe NOTE for `vulpea-select'."
  (concat
   (propertize
    (string-join
     (nconc (vulpea-note-outline-path note) (list ""))
     " → ")
    'face 'completions-annotations)
   (vulpea-note-title note)))

;; this is just to test
(let ((vulpea-select-describe-fn #'my-vulpea-select-describe-fn))
  (vulpea-select "Note"))

;; you can configure vulpea to use this function by default:
(setq vulpea-select-describe-fn #'my-vulpea-select-describe-fn)

image

Outline path, including file-level note title

Currently there is no way to do in an optimal way without advice, but... well, it works 😅

(defun my-vulpea-select-from-wrapper (orig-fn &rest args)
  "Custom wrapper around `vulpea-select-from'.

The function calls ORIG-FN with ARGS, but optimises calls to `vulpea-db'
used by custom describe function for selection routine."
  (let ((title-by-path (make-hash-table :test #'equal)))
    (--each (vulpea-db-query
             (lambda (note)
               ;; here we just don't care about non-file-level notes and we don't care about aliases
               (and (= 0 (vulpea-note-level note))
                    (null (vulpea-note-primary-title note)))))
      (puthash (vulpea-note-path it) (vulpea-note-title it) title-by-path))
    (let ((vulpea-select-describe-fn (my-vulpea-select-describe-fn title-by-path)))
      (apply orig-fn args))))

(defun my-vulpea-select-describe-fn (title-by-path)
  "Return a describe function for `vulpea-select'.

It uses TITLE-BY-PATH to include file-level note title."
  (lambda (note)
    (concat
     (propertize
      (string-join
       (nconc
        (unless (= 0 (vulpea-note-level note))
          (or (gethash (vulpea-note-path note) title-by-path) "unknown file"))
        (vulpea-note-outline-path note)
        (when (= 0 (vulpea-note-level note)) (list "")))
       " → ")
      'face 'completions-annotations)
     (vulpea-note-title note))))

(advice-add 'vulpea-select-from :around #'my-vulpea-select-from-wrapper)

It's a little bit more slow as it requires extra query of all db, but at least it's not overly slow haha. In short it just dynamically builds the describe function every time you call vulpea-select-from (which is used by most of the functions in the vulpea library).

Feel free to play around with my-vulpea-select-describe-fn.

And of course, let me know if you need further assistance.

image

d12frosted commented 2 months ago

Now that I think about it... I actually love both options. Maybe I will change the default value of describe function 😅 Or at least, share both of the solutions in documentation 🤷

So really happy that you approached me 😅 And sorry for spoiling you the fun of implementing it yourself 😭

P.S. Reopened because I want to include it OOTB or share.