Match highlighting with Ivy

After this PR (fix)completions: use full string by jethrokuan · Pull Request #1754 · org-roam/org-roam · GitHub, which is super helpful as it lets you search the whole completion candidate while displaying a truncated version, I found that ivy highlighting wasn’t working, basically ivy does the highlighting on the candidate itself instead the display string. I found that the following code can update ivy to apply the highlighting to the display strings instead of the candidate. I’ve tested it with display strings that already have styling and it seems to be working. It also handles un-highlighting as you delete the search string. It does run a bit more code (two extra loop over the candidate string) but it is only applied to ivy-height candidates, not the whole list so I don’t think it is that bad.

;; Proper highlighting in org-roam search
(defun bl/map-text-properties (str prop fun)
  "Apply `fun' to all text properties of type `prop' in `str'.
If the output of `fun', called on the property value is `nil', the property is removed."
  (let ((start 0)
        (len (length str)))
    (while (and start (< start len))
      (let ((end (or (next-single-property-change start prop str) len)))
        (when-let ((value (get-text-property start prop str)))
          (let ((updated-value (funcall fun value)))
            (if updated-value
                (put-text-property start end prop updated-value str)
              (remove-text-properties start end (list prop 'nil) str))))
        (setq start end)))
    str))

(defun bl/remove-ivy-faces (value)
  "Remove any face added by `ivy' for value."
  (if (listp value)
      (seq-remove (lambda (v) (memq v ivy-minibuffer-faces)) value)
    (if (memq value ivy-minibuffer-faces)
        'nil
      value)))

(defun bl/ivy--highlight-ignore-order (str)
  "Highlight the display strings in ivy instead of the actual candidate."
  (if (text-property-any 0 (length str) 'display 'nil str)
      (bl/map-text-properties str 'display (lambda (s)
        (ivy--highlight-ignore-order (bl/map-text-properties s 'face 'bl/remove-ivy-faces))))
    (ivy--highlight-ignore-order str)))

(defun bl/org-roam-search-highlighting (fun &rest args)
  "This display string only custom highlighter breaks other finds so only do it for org-roam"
  (let ((ivy-highlight-functions-alist '((ivy--regex-ignore-order . bl/ivy--highlight-ignore-order)
                                         (ivy--regex-fuzzy . ivy--highlight-fuzzy)
                                         (ivy--regex-plus . ivy--highlight-default))))
    (apply fun args)))

 (advice-add 'org-roam-node-find :around 'bl/org-roam-search-highlighting)
 (advice-add 'org-roam-node-insert :around 'bl/org-roam-search-highlighting)
 (advice-add 'org-roam-capture :around 'bl/org-roam-search-highlighting)

It might make more sense to use advice to change the implementation of the highlighter instead of changing the alist in a let, but this was the first thing I got working lol.

I wanted to share this in-case anyone else was missing the match highlighting. It seems like this would be hard to fix in org-roam while supporting multiple completion backends. Also if anyone has any suggestions to improve my code, I’m all ears.

2 Likes

Good to know that it is possible. I thought you could not do this for the display property.