counsel-git-grep, in async
04 Jun 2015Introduction
It took me quite a while to figure out, but finally
counsel-git-grep
, which I've described
before works completely
asynchronously, which means it's very fast and smooth even for huge
repositories.
If you don't know what git grep
is: it allows you to search for a
regular expression in your Git repository. In a way, it's just a
find
/ grep
combo, but very concise and fast. It's particularly
great for finding all uses and references to a symbol.
It can be useful even for non-programmers. One great application is to stick all your org-mode files into a Git repository. Then you can search all your files at once very fast: all my org stuff takes more than 1000,000 lines and there's no lag while searching. I really should remove some pdfs from the repository at some point.
The Video Demo
Check out the speed and the various command in this Video Demo. Each key stroke starts two asynchronous shell calls, while canceling the current ones if they are still running. Here are the example calls:
$ git --no-pager grep --full-name -n --no-color \
-i -e "forward-line 1" | head -n 200
$ git grep -i -c 'forward-line 1' \
| sed 's/.*:\\(.*\\)/\\1/g' \
| awk '{s+=$1} END {print s}'
The Elisp side
I figure since I was interested in this topic, probably a few more people are as well. So I'll explain below how async processing is done (in this case):
(defun counsel--gg-count (regex &optional no-async)
"Quickly count the amount of git grep REGEX matches.
When NO-ASYNC is non-nil, do it synchronously."
(let ((default-directory counsel--git-grep-dir)
(cmd (concat (format "git grep -i -c '%s'"
regex)
" | sed 's/.*:\\(.*\\)/\\1/g'"
" | awk '{s+=$1} END {print s}'"))
(counsel-ggc-process " *counsel-gg-count*"))
(if no-async
(string-to-number (shell-command-to-string cmd))
(let ((proc (get-process counsel-ggc-process))
(buff (get-buffer counsel-ggc-process)))
(when proc
(delete-process proc))
(when buff
(kill-buffer buff))
(setq proc (start-process-shell-command
counsel-ggc-process
counsel-ggc-process
cmd))
(set-process-sentinel
proc
#'(lambda (process event)
(when (string= event "finished\n")
(with-current-buffer
(process-buffer process)
(setq ivy--full-length
(string-to-number
(buffer-string))))
(ivy--insert-minibuffer
(ivy--format ivy--all-candidates)))))))))
- Since we're calling a shell command, it's important to set
default-directory
to a proper value: the shell command will be run there. - The output from the process will be channeled into a buffer. I start the buffer name with a space to make it hidden: it will not be shown in the buffer list and
switch-to-buffer
completion. - I leave the sync option and use
shell-command-to-string
for that. Sometimes it's necessary to know the amount of candidates without a delay. - If
get-process
andget-buffer
return something it means that the shell command is still running from the previous input. But the input has changed and that data has become useless, so I kill both the process and the buffer. - Then I start a new process with
start-process-shell-command
- a convenience wrapper around the basicstart-process
that's useful for passing a full shell command with arguments. - Finally, I let
counsel--gg-count
return without waiting forgit grep
to finish. But something needs to be done when it finishes, so I useset-process-sentinel
. Emacs will call my lambda when there's a new event from the process.
Outro
To check out the new stuff, just install or upgrade counsel
from MELPA. Or clone the source. I hope that my Elisp explanations were useful, async stuff is awesome, and I hope to see more of it in future Emacs packages.