>> Idea

I cannot live without projectile and probably helm and projectile-find-files is my bread-and-butter command. But they say, familiarity breeds contempt and once you been using it so much; you want some customization. But which part? For my contempt, I really just want to change how the helm-completion buffer displays the options.

What I really want is rather to display the filename in the front and display the file path in reverse order since my eyes naturally scan from left to right. And so if you want proof of how this looks like to me, I used the projectile-find-files on my emacs directory and this is what it looks like with just helm.

 

After some hacking and discussion, this is what I settled on.

 

So if you think this is weird or offensive, feel free to do something more meaningful in your life. But if you are curious, keep reading.

Or if you just want to know the code, here it is for your consumption.

(require 'dash)
(require 's)
(require 'f)

(defun fn/custom-helm-completion (prompt choices)
  "Just a custom helm completion for projection"
  (lexical-let*
      ((fn-separator "..")
       (fn-notation
        (lambda (path)
          (lexical-let ((fn-pieces (f-split path)))
            (string-join (reverse fn-pieces) fn-separator))))
       (relative-parent-path
        (lambda (path relative-path)
          (lexical-let
              ((split-path (f-split path))
               (split-relative-path (f-split relative-path)))
            (string-join
             (-drop-last (length split-relative-path) split-path)
             (f-path-separator)))))

       (as-pair (lambda (ish)
                  (if (listp ish)
                      ish (cons ish ish))))
       (swap-pair (lambda (pair)
                    (cons (cdr pair) (car pair))))
       (map-car (lambda (f pair)
                  (cons (funcall f (car pair))
                        (cdr pair))))

       (pair-as-label
        (lambda (pairs)
          (lexical-let* ((labels
                             (mapcar #'cdr pairs))
                         (label-lengths
                          (mapcar
                           (-compose
                            #'length
                            fn-notation)
                           labels))

                         (max-label-length
                          (apply #'max label-lengths))


                         (label-format
                          "%-s")

                         (description-format
                          "%-s")

                         (display-formatter
                          (lambda (name description)
                            (concat
                             (format label-format name)
                             "  |->  "
                             (format description-format description)
                             "  <-|"))))
            (lambda (pair)
              (lexical-let*
                  ((unique-path (car pair))
                   (full-path (cdr pair))
                   (parent-path
                    (funcall relative-parent-path
                             full-path
                             unique-path))

                   (display-name
                    (funcall fn-notation unique-path))
                   (display-description
                    (funcall fn-notation parent-path))

                   (display-label
                    (funcall display-formatter
                             display-name
                             display-description)))
                (cons display-label (cdr pair)))))))
       (refined-choices (f-uniquify-alist choices))
       (mapped-choices
        (mapcar (-compose
                 (funcall pair-as-label refined-choices)
                 swap-pair
                 as-pair)
                refined-choices)))
    (helm-comp-read prompt mapped-choices
                    :must-match 'confirm)))

(setq projectile-completion-system #'fn/custom-helm-completion)

I admit the code looks long, but I think it follows my functional and aesthetic instincts.

>> Beginning

As you can see from the screenshot taken with the useful emacsshot, it focuses on the filename rather than the whole path. This reminds when I used ido with flex matching, the search is more tuned with the filename which I still miss although helm is different in this regard, a small concession would be nice. So thus my journey of modifying the completion.

Actually, I just wanted to see the filename but the joy of exploring the project structure might be lost if I don't include the whole path but optional. So let's start with the primary feature: filenames.

>> Filenames

Seems easy enough with the f library and f-filename function. So let's see how this factors in with projectile-current-project-files.

(defun out (value)
  (message "%s" value))

(mapc #'out (projectile-current-project-files))
;; Trimmed output
;; elisp/custom-zone/zone-end-of-buffer.el
;; elisp/custom-zone/zone-waves.el
;; .gitignore
;; .projectile
;; README.org
;; config.or
;; elisp/promise/.gitignore
;; elisp/promise/LICENSE
;; elisp/promise/promise-test.el
;; elisp/promise/promise.el
;; init-standard.el
;; init.el
;; personal.el

(mapc (-compose #'out #'f-filename) (projectile-current-project-files))

;; zone-end-of-buffer.el
;; zone-waves.el
;; .gitignore
;; .projectile
;; README.org
;; config.org
;; .gitignore
;; LICENSE
;; promise-test.el
;; promise.el
;; init-standard.el
;; init.el
;; personal.el

That was easy, but the astute reader will notice that there are two .gitignore files, one from my .emacs.d and one from my shiv promise.el implementation. This raises the question: if there are files with the same name, how do you differentiate between the two?

Well Emacs already has a nice term for this with buffers and its uniquify. The idea to resolve this is to add the least many parents to make them unique and thankfully f already implements this with f-uniquify-alist so one does not need to worry about it. Crisis averted.

(mapc #'out  (f-uniquify-alist (projectile-current-project-files)))

;; Trimmed output
;; (elisp/custom-zone/zone-end-of-buffer.el . zone-end-of-buffer.el)
;; (elisp/custom-zone/zone-waves.el . zone-waves.el)
;; (.gitignore . /.gitignore)  ;; <-- The first gitignore
;; (elisp/promise/.gitignore . promise/.gitignore)
;; (.projectile . .projectile)
;; (README.org . README.org)
;; (config.org . config.org)
;; (elisp/custom-zone/end-of-buffer.el . end-of-buffer.el)
;; (elisp/custom-zone/waves.el . waves.el)
;; (elisp/promise/.gitignore . promise/.gitignore) ;; <-- The other gitignore
;; (elisp/promise/LICENSE . LICENSE)
;; (elisp/promise/promise-test.el . promise-test.el)
;; (elisp/promise/promise.el . promise.el)
;; (init-standard.el . init-standard.el)
;; (init.el . init.el)
;; (personal.el . personal.el)

Aside from using the alist version which shows the original value and the uniquified value, notice the cdr of the same filenames are now unique. So if we are given a list of project files we can just use that function and we are near the mark.

Now, let's see what we can do about exposing the path now that we have this.

>> Project Path

So with the previous concept, I want to display the path after the label almost like a two column table. Seems easy enough with f-dirname but taking with the uniquified issue as above.

(defun out-pair (pair)
  (pcase-let ((`(,value . ,unique-value) pair))
    (message "%s | %s" unique-value (f-dirname value))))

;; Remember the original
;; Given (.gitignore . /.gitignore)
(out-pair '(".gitignore" . "/.gitignore"))
".gitignore | /"

;; Given (elisp/promise/.gitignore . promise/.gitignore)
(out-pair '("elisp/promise/.gitignore" . "promise/.gitignore"))
"promise/.gitignore | elisp/promise/"

;; Combining them as an hypothetical display
".gitignore | /.gitignore"
"promise/.gitignore | elisp/promise/"

So nothing is really wrong with this but there is a redundancy in path with promise/.gitignore. It would be nice if the path could be trimmed from the highest point like so.

(defun relative-parent-path (path relative-path)
  "Check this out in the implementation
   Again it is only for this problem"
  path)

(defun out-pair2 (pair)
  (pcase-let ((`(,value . ,unique-value) pair))
    (message "%s | %s" unique-value (relative-parent-path value unique-value))))


;; Instead of
"promise/.gitignore | elisp/promise/"

;; Would be nice if
"promise/.gitignore | elisp/"
(out-pair2 '("elisp/promise/.gitignore" . "promise/.gitignore"))

Sadly, the code, relative-parent-path, for that isn't worth reviewing as it is only meant for this problem. Now it does look better, which is good enough for me: unique path combined with completing path.

But there is one more enhancement I would love to see: reversed paths

>> Reversing Paths

Instead of reading /a/b/c/d, I find it curiously interesting if it could be written as d..c..b..a which is easy enough to do with f-split and reverse.

(defconst fn-separator "..")

(defun fn-notation (path)
  (lexical-let ((fn-pieces (f-split path)))
    (string-join (reverse fn-pieces) fn-separator)))

;; How does it look here?
"promise/.gitignore | elisp/"

;; Ah much better
".gitignore..promise | elisp"

The choice of separator is yours but the idea of reversing it allows me to home in to the file I am looking for and its contextualized parent. Minor enhancement are really important sometimes.

Again this notation is optional but I really like it and it makes me wonder why there isn't this option available. And with that, we can now start hacking after trying out some stuff.

>> Really Hacking

There is really one question that should have been answered in the first place: why the completion system?

Well, I thought about advicing helm-projectile and projectile-current-project-files but I thought that it might do more damage as the latter is the source, not the display, while the former is a bit harder to track down cleanly. I settled on the projectile-completion-system as it is a natural point to hook in. So that's why.

Actually, my problem is that there is no hook or mapping function that one can use being helm and all. After the next section, there are references in the helm-projectile code that assumes a direct source to label mapping; meaning making this change might break other functionality which isn't needed. I really just want projectile-find-file to have a cool display, not blow up describe-variable or something.

Here is the ticket I filed for that. If that has a good answer, then much of the code I written could be easily adapted to use that instead of my customization.

>> Helm Completion

Okay, so now that we can turn the source into a desirable output. How does the actual completion engine or helm work? Or more precisely helm-comp-read, which is used by projectile-completion-system? Let's see the code where this happens.

((eq projectile-completion-system 'helm)
 (if (fboundp 'helm-comp-read)
     (helm-comp-read prompt choices
                     :initial-input initial-input
                     :candidates-in-buffer t
                     :must-match 'confirm)
   (user-error "Please install helm from \
https://github.com/emacs-helm/helm")))

So helm-comp-read takes a prompt and a collection of choices? So the question is can collection be an alist instead of key value pairs? I was skeptic at first but it actually does work and yet it did. I am thankful it is because if it isn't we have to create a value-label-value mapping which just extra glue. So how does this look like? I can write some code but it is better if you try it out yourself.

(setq my-prompt "What door do you want to open: ")

(helm-comp-read my-prompt (list
                           (cons "A" 'loser)
                           (cons "B" 'winner)
                           (cons "C" 'loser)))

;; vs
(let ((choice (helm-comp-read my-prompt (list "A" "B" "C"))))
  (pcase choice
    ("A" 'loser)
    ("B" 'winner)
    ("C" 'loser)))

So with that feature, we hook up our f-uniquify-alist and viola.

;; Remmber the f-uniquify-alist is (value, label)
;; So we create a swapper to make it appropriate
(defun swap-pair (pair)
  (cons (cdr pair) (car pair)))

(helm-comp-read "So what file do you want? "
                (mapcar #'swap-pair ;; Just swap the fields before display
                        (f-uniquify-alist
                         (projectile-current-project-files))))

And with that you have an uniquified projectile file list. Everything else is just composing more functions after swap-pair specifically the car or label of it. So if one intends to create an hook, you now know where it is.

The key display function is display-formatter in the implementation, it is pretty much just a format. So there really isn't much to discuss or do you want to discuss functional style which I've taken? Either way, one could do it very easily after this.

>> Wish List

So our discussion led us to a simple implementation of a projection completion. But there are some things I wanted after implementing this and maybe somebody can do this.

Uniquified paths are bold : They have some face configuration that makes files bold while the paths smaller and differently colored if possible. I haven't checked out face options yet

Value and path are in two lines : It would be nice to see the path to be below the file as it can be easier to read but this is harder to implement with just spaces and how helm is built on. I tried adding newlines to each choice but this makes the selection a little bit more tricky and delicate. Rough one line display is good enough.

There is some inkling of this implementation with the variable
`max-text-length` which is a stab at guessing the completion
buffer length and determine how many spaces to put or whether to
align the value to the path. Some ideas remain in making it more
aesthetically pleasing.

Performance concern : While making this code I spotted a performance issue with f-uniquify-alist with large projects which causes the UI to hang. I filed a issue regarding this but I feel crafting a personalized uniquified function might be the real solution

>> Conclusion

There might be more wishes but the intention is complete. So I hope you found that story entertaining and that you try it out yourself or maybe you taught I was crazy doing this. Ah, I prefer to think of the latter as Emacs has caused me severe impairment. Cheers

>> 2016-08-03 Update

So after hacking with font-lock, I can finally settle on this.

 

So I made the value indeed bolder, changing the size is safer than changing the color, while I made the relative path much thinner to separate them. It is compact and different enough, with this I don't need the tabular arrangement of space filling so good. It looks fine if I do say so myself.

Now I wonder what somebody else has done?