Where parallels cross

Interesting bits of life

A way to fix Helm handling of symlink (/tmp dir) in Mac OS

I found myself upset about using Helm on Mac OS for a while because it wouldn't show me the contents of my /tmp directory. The issue would always become apparent in the most disappointing times. For example, the Emacs mail client mu4e requires a file to exist in order to set it as an attachment. Bet where is the file I want to attach? Right, in the /tmp directory that for some reason my Helm sees (mostly) empty. That makes adding an attachment to a mail more painful than needed.

This is how today I studied the issue and fixed it once and for all for myself.

The thing is that Helm creates a cache of directories contents to be super quick. Any time you create a file in a directory that Helm is keeping a cache for, Helm receives a notification and the cache is updated. Somehow that doesn't work well for symlinks on Mac OS (tmp is a symlink in Mac OS). So the cache doesn't get updated and I don't see my files!

There is a relating issue on the Helm repo is https://github.com/emacs-helm/helm/issues/2542 and the immediate solution would be to do C-c C-u to force the refresh of the cache.

For a microsecond I really thought I would remember C-c C-u every time I searched in /tmp. The ashamed of myself, I fixed it once and for all. Inelegantly, but still a hack worth sharing.

In my Emacs configuration dedicated to Mac OS I added the following:

(defcustom my/extra-checks-for-helm-dirs-cache (lambda () (or (s-starts-with-p "/tmp" default-directory))) "")

(when (eq 'darwin system-type)
  (with-eval-after-load 'helm-files
    (defun helm-ff-directory-files (directory &optional force-update)
      "List contents of DIRECTORY.
Argument FULL mean absolute path.
It is same as `directory-files' but always returns the dotted
filename \\='.' and \\='..' even on root directories in Windows
When FORCE-UPDATE is non nil recompute candidates even if DIRECTORY is
in cache."
      (let ((method (file-remote-p directory 'method)))
        (setq directory (file-name-as-directory
                         (expand-file-name directory)))
        (or (and (funcall my/extra-checks-for-helm-dirs-cache) (not force-update)
                 (gethash directory helm-ff--list-directory-cache))
            (let* (file-error
                   (ls   (condition-case err
                             (helm-list-directory directory)
                           ;; Handle file-error from here for Windows
                           ;; because predicates like `file-readable-p' and friends
                           ;; seem broken on emacs for Windows systems (always returns t).
                           ;; This should never be called on GNU/Linux/Unix
                           ;; as the error is properly intercepted in
                           ;; `helm-find-files-get-candidates' by `file-readable-p'.
                                ;; Prefix error message with @@@@ for safety
                                ;; (some files may match file-error See bug#2400) 
                                (list (format "@@@@%s:%s"
                                              (car err)
                                              (mapconcat 'identity (cdr err) " ")))
                              (setq file-error t)))))
                   (dot  (concat directory "."))
                   (dot2 (concat directory ".."))
                   (candidates (append (and (not file-error) (list dot dot2)) ls)))
              (puthash directory (+ (length ls) 2) helm-ff--directory-files-length)
                  (puthash directory
                           (cl-loop for f in candidates
                                    when (helm-ff-filter-candidate-one-by-one f)
                                    collect it)
                ;; Put an inotify watcher to check directory modifications.
                (unless (or (null helm-ff-use-notify)
                            (member method helm-ff-inotify-unsupported-methods)
                            (gethash directory helm-ff--file-notify-watchers))
                  (condition-case-unless-debug err
                      (puthash directory
                                '(change attribute-change)
                                (helm-ff--inotify-make-callback directory))
                    (file-notify-error (user-error "Error: %S %S" (car err) (cdr err))))))))))))

This redefines helm-ff-directory-files to have an alternative way to force the full refresh of the cache.

You can see I define a predicate I want to run:

(defcustom my/extra-checks-for-helm-dirs-cache (lambda () (or (s-starts-with-p "/tmp" default-directory))) "")

This checks if we are in /tmp/. Then I inject that predicate next to the force-update bit:

(and (funcall my/extra-checks-for-helm-dirs-cache) (not force-update)
                 (gethash directory helm-ff--list-directory-cache))

With that I can extend the predicate and automatically refresh the cache without me remembering extra keybindings.

Happy tmp/ing!