Recently, as I replaced more of my Emacs-helm configuration with counsel and ivy, I noticed that ivy-switch-buffer, when augmented by ivy-rich, was not showing the directories of the buffers it was listing.

After some Lisp spelunking, I discovered that it was because ivy-rich relies on the presence of either the projectile package, something I do not wish to have in my Emacs configuration, or on project.el, which I also do not use.

In this post, I show how you can get full buffer filenames and project names with the lighter-than-projectile and more-robust-than-project.el find-file-in-project, or how you can bypass the project name functionality completely and just get buffer filenames with no extra packages.

(There are a number of long-standing PRs on ivy-rich, so I decided against investing my time there, instead making the fix available directly on this blog.)

Updates

Wednesday 2020-11-18

Indeed Yevgnen (ivy-rich’s author) merged my PR, so everything in this post should be fixed if you update your ivy-rich from melpa.

Tuesday 2020-11-17

Probably due to my post on reddit.com/r/emacs/, ivy-rich’s author added find-file-in-project support with this commit!

On its part, this motivated me to create a PR to ivy-rich with my improved buffer directory extraction.

If that PR gets merged, you can ignore the rest of this post.

The result

We want the ivy-switch-buffer display to look like the one in the screenshot below. Each buffer has its full filename, if available, on the right.

Here’s another screenshot with all-the-icons-ivy-rich installed and active:

The code

Note: This section is here purely for reference purposes. The fixes documented here have now been merged into the ivy-rich package.

I have the following code in my init.el.

In it, two of the ivy-rich functions are overwritten with improved versions.

Please see the inline comments for explanation.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
(use-package ivy-rich
  :ensure t
  :config (ivy-rich-mode 1)

  ;; abbreviate turns home into ~ (for example)
  ;; buffers still only get the buffer basename
  (setq ivy-virtual-abbreviate 'abbreviate
        ivy-rich-path-style 'abbrev)

  ;; use buffer-file-name and list-buffers-directory instead of default-directory
  ;; so that special buffers, e.g. *scratch* don't get a directory (we return nil in those cases)
  (defun ivy-rich--switch-buffer-directory (candidate)
    "Return directory of file visited by buffer named CANDIDATE, or nil if no file."
    (let* ((buffer (get-buffer candidate))
           (fn (buffer-file-name buffer)))
      ;; if valid filename, i.e. buffer visiting file:
      (if fn
          ;; return containing directory
          (directory-file-name fn)
        ;; else if mode explicitly offering list-buffers-directory, return that; else nil.
        ;; buffers that don't explicitly visit files, but would like to show a filename,
        ;; e.g. magit or dired, set the list-buffers-directory variable
        (buffer-local-value 'list-buffers-directory buffer))))

  ;; override ivy-rich project root finding to use FFIP or to skip completely
  (defun ivy-rich-switch-buffer-root (candidate)
    ;; 1. changed let* to when-let*; if our directory func above returns nil,
    ;;    we don't want to try and find project root
    (when-let* ((dir (ivy-rich--switch-buffer-directory candidate)))
      (unless (or (and (file-remote-p dir)
                       (not ivy-rich-parse-remote-buffer))
                  ;; Workaround for `browse-url-emacs' buffers , it changes
                  ;; `default-directory' to "http://" (#25)
                  (string-match "https?://" dir))
        (cond
         ;; 2. replace the project-root-finding
         ;; a. add FFIP for projectile-less project-root finding (on my setup much faster) ...
         ((require 'find-file-in-project nil t)
          (let ((default-directory dir))
            (ffip-project-root)))
         ;; b. OR disable project-root-finding altogether
         (t "")
         ((bound-and-true-p projectile-mode)
          (let ((project (or (ivy-rich--local-values
                              candidate 'projectile-project-root)
                             (projectile-project-root dir))))
            (unless (string= project "-")
              project)))
         ((require 'project nil t)
          (when-let ((project (project-current nil dir)))
            (car (project-roots project))))
         )))))