>> 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?