How to insert a prefix to the description when using completion at point?

As a follow-up to my previous question, I want to ask how to insert a prefix to the description when using completion at point. Upon digging the source code of org-roam, I found a function that replaces roam: with id: upon saving the buffer. I modified that function to solve my use case-

  (defun sg/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")))))   ;; Add this text in the properties

  (defun sg/org-roam-link-replace-at-point-a (&optional link)
    "Replace \"roam:\" LINK at point with an \"id:\" link."
    (save-excursion
      (save-match-data
        (let* ((link (or link (org-element-context)))
               (type (org-element-property :type link))
               (path (org-element-property :path link))
               (desc (and (org-element-property :contents-begin link)
                          (org-element-property :contents-end link)
                          (buffer-substring-no-properties
                           (org-element-property :contents-begin link)
                           (org-element-property :contents-end link))))
               node)
          (goto-char (org-element-property :begin link))
          (when (and (org-in-regexp org-link-any-re 1)
                     (string-equal type "roam")
                     ;; Added `desc' ref here
                     (setq node (save-match-data
                                  (org-roam-node-from-title-or-alias
                                   (if (string-empty-p path)
                                       desc
                                     path)))))

            (setq prefix (sg/org-roam-get-prefix node))    ;; Added this
            (replace-match (org-link-make-string
                            (concat "id:" (org-roam-node-id node))
                            (or desc path))))))))  ;; `prefix` will be used here (currently omitted that part)

  (advice-add #'org-roam-link-replace-at-point :override #'sg/org-roam-link-replace-at-point-a)

But, using this changes the behaviour of the function and random text seems to be replaced with the id:... part. I think replace-match is causing the issue, but, I am unable to fix it. Please help.

How about something like this?

I think you just need to manipulate desc but only after node has been set. I don’t think you need any other change for your purpose (?)

  1. I am assuming sg/org-roam-get-prefix returns nil when there is no prefix.
  2. Removed the part for Added desc’ ref here` does; the purpose is not clear.
(defun org-roam-link-replace-at-point (&optional link)
  "Replace \"roam:\" LINK at point with an \"id:\" link."
  (save-excursion
    (save-match-data
      (let* ((link (or link (org-element-context)))
             (type (org-element-property :type link))
             (path (org-element-property :path link))
             (desc (and (org-element-property :contents-begin link)
                        (org-element-property :contents-end link)
                        (buffer-substring-no-properties
                         (org-element-property :contents-begin link)
                         (org-element-property :contents-end link))))
             node)
        (goto-char (org-element-property :begin link))
         (when (and (org-in-regexp org-link-any-re 1)
                    (string-equal type "roam")
                    (setq node (save-match-data (org-roam-node-from-title-or-alias path))))
+          (let ((prefix (sg/org-roam-get-prefix node))) ; use let instead of setq; otherwise, prefix will be a global var
+            (when (and prefix desc) ; prefix and desc may be nil, right?
+              (setq desc (concat prefix ; this setq is fine because desc is let-bound above.
+                                 ":"    ; change this separator between prefix and desc as you see fit.
+                                 desc))))
           (replace-match (org-link-make-string
                           (concat "id:" (org-roam-node-id node))
                           (or desc path))))))))

I don’t have the data to test my suggestion (sorry).

One thing I am not sure about is that my inserted code separates save-match-data and replace-match – depending on the code in sg/org-roam-get-prefix, the matched data may be altered.

Tested my code and fixed problems.

  • For :roam, we need to use path instead of desc for the target title (your original probably does this; my bad).
  • Surround my additional code with save-match-data so that replace-match works correctly.

I didn’t test your original, but it looks like you and I both needed to use save-match-data; otherwise, the point goes to some random location and does not correctly replace the :roam link being worked on. Now I know how to use save-match-data :slight_smile:

(defun org-roam-link-replace-at-point (&optional link)
  "Replace \"roam:\" LINK at point with an \"id:\" link."
  (save-excursion
    (save-match-data
      (let* ((link (or link (org-element-context)))
             (type (org-element-property :type link))
             (path (org-element-property :path link))
             (desc (and (org-element-property :contents-begin link)
                        (org-element-property :contents-end link)
                        (buffer-substring-no-properties
                         (org-element-property :contents-begin link)
                         (org-element-property :contents-end link))))
             node)
        (goto-char (org-element-property :begin link))
         (when (and (org-in-regexp org-link-any-re 1)
                    (string-equal type "roam")
                    (setq node (save-match-data (org-roam-node-from-title-or-alias path))))
+          (save-match-data
+            (let ((prefix (sg/org-roam-get-prefix node))) ; use let instead of setq; otherwise, prefix will be a global var
+              (when (and prefix path) ; prefix may be nil, right? Corrected to path, instead of desc
+                (setq path (concat prefix ; this setq is fine because path is let-bound above.
+                                   ":"    ; change this separator between prefix and path as you see fit.
+                                   path)))))
           (replace-match (org-link-make-string
                           (concat "id:" (org-roam-node-id node))
                           (or desc path))))))))

2 Likes

Thank you so much that works well. I also need to learn about save-match-data.

1 Like

If I am not mistaken – save-match-data preserves results of regexp operations outside the block – each regexp changes the state of a few variables;

for a quick example – consider this – we consider the function match-string that returns the result of the previous regex search.

;; Set Variable
(setq my-string "Hello 123 World")

;; Do regex
(string-match "[0-9]+" my-string)
;; Show the result
(message "Number: %s" (match-string 0 my-string))

;; Do another regex
(string-match "\\b\\w+\\b" my-string)
;; Show the result
(message "Word: %s" (match-string 0 my-string))

;; Problem? How to refer to the first result?
;; Second search has modified the state of match-string
;; you will get wrong result trying to access the result
(message "Number: %s" (match-string 0 my-string))

;; Solution
;; self-Contain regexp operations 
(save-match-data
  (string-match "[0-9]+" my-string)
  (message "Number: %s" (match-string 0 my-string)))

(save-match-data
  (string-match "\\b\\w+\\b" my-string)
  (message "Word: %s" (match-string 0 my-string)))

so save-match-data is useful when trying not contaminate the variables for future use – by intermediate regexp operations.

Hope this is helpful and intuitive.

Edit:

35.6 The Match Data

Emacs keeps track of the start and end positions of the segments of text found during a search; this is called the match data. Thanks to the match data, you can search for a complex pattern, such as a date in a mail message, and then extract parts of the match under control of the pattern.

Because the match data normally describe the most recent search only, you must be careful not to do another search inadvertently between the search you wish to refer back to and the use of the match data. If you can’t avoid another intervening search, you must save and restore the match data around it, to prevent it from being overwritten.

Notice that all functions are allowed to overwrite the match data unless they’re explicitly documented not to do so. A consequence is that functions that are run implicitly in the background (see Timers for Delayed Execution, and Idle Timers) should likely save and restore the match data explicitly. 

From the Elisp Manual : Match Data (GNU Emacs Lisp Reference Manual)

2 Likes