How to insert a prefix to the description when using org-roam-node-insert?

I want to automatically insert a prefix to the description of the link whenever I insert a link using org-roam-node-insert. For eg.-
Whenever I insert a link to one of my projects, I should have proj:project1_name as the link description instead of just project1_name. Similarly after selecting project2_name in org-roam-node-insert, I should get the link description as proj:project2_name. Also, if there is some other category of things like a “plan”, I should get plan:plan1_name and so on. Of course, I would first need to define the “proj” variable for the projects and the “plan” variable for the plans. Also, if there is no such “variable” defined for some entity, the prefix should not be inserted.
Can someone guide me on how to achieve this? I looked into aliases but, I don’t think they are suitable for my use case.

Could be done trivially - depends on how you want to define the variable – would it be defined automatically based on the folder or how? The way to solve a problem is to first delineate clear steps from reaching point a to point b – unless the steps could be clarified, no algorithm could be built to automate the process - try to think in terms of steps.

I intentionally didn’t specify where I would put the “prefix” (thinking if there was a better way than mine, then, someone could help me out). What I was thinking was putting a different prefix for each file (maybe in the properties drawer at the top of the file). Maybe this could be extended to the headings with the property drawer. I don’t know how I would go about extracting that. If there is some better way/location to hold the “prefix” word, then I would be happy to hear it.

(defun 0/org-roam-node-insert (&optional filter-fn &key templates info)
  "Find an Org-roam node and insert (where the point is) an \"id:\" link to it.
FILTER-FN is a function to filter out nodes: it takes an `org-roam-node',
and when nil is returned the node will be filtered out.
The TEMPLATES, if provided, override the list of capture templates (see
`org-roam-capture-'.)
The INFO, if provided, is passed to the underlying `org-roam-capture-'."
  (interactive)
  (unwind-protect
      ;; Group functions together to avoid inconsistent state on quit
      (atomic-change-group
        (let* ((region-text (if (region-active-p)
                                (buffer-substring-no-properties (region-beginning) (region-end))))
               (node (org-roam-node-read region-text filter-fn))
               (description (or region-text
                                (org-roam-node-formatted node)))
               (id (org-roam-node-id node))
               (prefix (org-entry-get (point) "prefix"))) ;; Change "prefix" to the name of your property
          (if id
              (progn
                (when region-text
                  (delete-region (region-beginning) (region-end)))
                (insert (org-link-make-string
                         (concat "id:" id)
                         (concat prefix (when prefix ":") description)))
                (run-hook-with-args 'org-roam-post-node-insert-hook id description))
            (org-roam-capture-
             :node node
             :info info
             :templates templates
             :props (list :region (when (region-active-p)
                                     (cons (region-beginning) (region-end)))
                          :link-description description
                          :finalize 'insert-link)))))
    (deactivate-mark)))

The value of the property :prefix: will be automatically taken into consideration – let me know if you need anything else

1 Like

Hi, sorry for not replying earlier as I was understanding and trying out the code snippet shared by you. Thank you so much for the code snippet.
One issue I am facing is that the “prefix” variable is empty, even, after adding the property :prefix: project1_name. Do you know what might be causing the issue?

You should add the prefix to the headline – and then save the file before inserting it. Is it not working for that? Or are you adding the property to the file property then want the headline to just grab it?

For simplicity I have considered the case where each headline could be configured with its own prefix

I tried inserting this (with different uuids) for a heading as well as on top of a file-

:PROPERTIES:
:ID:       f896f7c2-145a-4229-ab2c-9ea6a5cb08e5
:prefix:    project1_name
:END:

After that, I call M-x 0/org-roam-node-insert from another file to insert the link. But, I still get an empty prefix value.

My mistake – I did not test it out thoroughly, we need to dynamically get the property value – I tested it in the current file only

(defun 0/org-roam-get-prefix (node)
  "Determine the prefix for an Org-roam node."
  (let ((file (org-roam-node-file node)))
    (with-current-buffer (find-file-noselect file)
      (org-with-point-at (org-roam-node-point node)
        (org-entry-get nil "prefix")))))

(defun 0/org-roam-node-insert (&optional filter-fn &key templates info)
  "Find an Org-roam node and insert (where the point is) an \"id:\" link to it.
FILTER-FN is a function to filter out nodes: it takes an `org-roam-node',
and when nil is returned the node will be filtered out.
The TEMPLATES, if provided, override the list of capture templates (see
`org-roam-capture-'.)
The INFO, if provided, is passed to the underlying `org-roam-capture-'."
  (interactive)
  (unwind-protect
      ;; Group functions together to avoid inconsistent state on quit
      (atomic-change-group
        (let* ((region-text (if (region-active-p)
                                (buffer-substring-no-properties (region-beginning) (region-end))))
               (node (org-roam-node-read region-text filter-fn))
               (description (or region-text
                                (org-roam-node-formatted node)))
               (id (org-roam-node-id node))
               (prefix (0/org-roam-get-prefix node))) ;; Get prefix dynamically
          (if id
              (progn
                (when region-text
                  (delete-region (region-beginning) (region-end)))
                (insert (org-link-make-string
                         (concat "id:" id)
                         (concat prefix (when prefix ":") description)))
                (run-hook-with-args 'org-roam-post-node-insert-hook id description))
            (org-roam-capture-
             :node node
             :info info
             :templates templates
             :props (list :region (when (region-active-p)
                                     (cons (region-beginning) (region-end)))
                          :link-description description
                          :finalize 'insert-link)))))
    (deactivate-mark)))

I have defined a helper function to get the property correctly this time, let me know if it works

1 Like

Hey, that works perfectly as I want even from another file. Thank you so much.
One question I want to ask is why the previous solution didn’t work?

1 Like

Because it was looking for the property in the file you were inserting in rather than in the file the node is defined at, I made a silly mistake.

1 Like

Just an update - We are being highly inefficient here – the node struct contains all the PROPERTIES of a node – we dont need to visit the file to get it.

(defun 0/org-roam-get-prefix (node)
  "Determine the prefix for an Org-roam node."
  (cdr (assoc "PREFIX" (org-roam-node-properties node))))

This is a much more efficient and correct way to do it.

Finally we should define the insert func with cl-defun ELSE we cannot capture new nodes while doing an insert.

(cl-defun org-roam-node-insert+ (&optional filter-fn &key templates info)
  (interactive)
  (unwind-protect
      ;; Group functions together to avoid inconsistent state on quit
      (atomic-change-group
        (let* (region-text
               beg end
               (_ (when (region-active-p)
                    (setq beg (set-marker (make-marker) (region-beginning)))
                    (setq end (set-marker (make-marker) (region-end)))
                    (setq region-text (org-link-display-format (buffer-substring-no-properties beg end)))))
               (node (org-roam-node-read region-text filter-fn))
               (description (or region-text
                                (org-roam-node-formatted node))))
          (if (org-roam-node-id node)
              (progn
                (when region-text
                  (delete-region beg end)
                  (set-marker beg nil)
                  (set-marker end nil))
                (let ((id (org-roam-node-id node))
		      (prefix (cdr (assoc "PREFIX" (org-roam-node-properties node)))))
                  (insert (org-link-make-string
                           (concat "id:" id)
			   (concat prefix (when prefix ":") description)))
                  (run-hook-with-args 'org-roam-post-node-insert-hook
                                      id
                                      description)))
            (org-roam-capture-
             :node node
             :info info
             :templates templates
             :props (append
                     (when (and beg end)
                       (list :region (cons beg end)))
                     (list :link-description description
                           :finalize 'insert-link))))))
    (deactivate-mark)))
(advice-add 'org-roam-node-insert :override #'org-roam-node-insert+)

Since the prefix determination has been simplified - we altogether don’t need a helper fn.

One limitation ofcourse remains we cannot use this prefix while doing capture – that is we cannot set a prefix during capture - the prefix would only work for nodes that exist already – because of the structure of the insert function itself.

Best

@nobiot I want to ask for a help - I cant get my head around this
In the code to org-roam-node-insert we use the following

 (let* (region-text
               beg end
               (_ (when (region-active-p)
                    (setq beg (set-marker (make-marker) (region-beginning)))
                    (setq end (set-marker (make-marker) (region-end)))
                    (setq region-text (org-link-display-format (buffer-substring-no-properties beg end)))))
               (node (org-roam-node-read region-text filter-fn))
               (description (or region-text
                                (org-roam-node-formatted node))))

What is the difference between (let (( and let ( that is being used here
I tried edebug to understand the values being generated but I still cannot understand this – what is this syntax?

Thanks.

The docstring of let* says this:

Each element of VARLIST is a symbol (which is bound to nil)
or a list (SYMBOL VALUEFORM) (which binds SYMBOL to the value of VALUEFORM).

I think it should be formatted like this below.

  (let* (region-text
         beg
         end
         (_ (when (region-active-p)
              (setq beg (set-marker (make-marker) (region-beginning)))
              (setq end (set-marker (make-marker) (region-end)))
              (setq region-text (org-link-display-format (buffer-substring-no-properties beg end)))))
         (node (org-roam-node-read region-text filter-fn))
         (description (or region-text
                          (org-roam-node-formatted node)))))

From how it runs, I believe:

  1. First it defines variables region-text, beg, end, and they are all bound to nil.

  2. “_” does not define any variable, but is intended to be like “progn” to evaluate the when form and set beg, end, region-text conditionally for (region-active-p). I think that technically, this part defines variable _, but the underscore tells the compiler that it is to be ignored (no warning issued when the compiler sees it unused).

  3. Lastly it defines node and description and bind them according to the forms, (org-roam-node-read ...) and (or ...), respectively.

1 Like