In my previous post on reading the Emacs manual, I mentioned that there were a couple of things that I was missing from my editing workflow when using regular Emacs bindings. The most notable of which was the ability to kill up until a search hit. I knew that it'd be possible to write a function to do it, but I didn't really know where to start, so I figured I'd just do it later.
In the Reddit thread about the post, user e17i gave me a little snippet to get me started for writing such a function. Turns out that was all I needed to get started. I put aside my fear of Lisp and went to work.
I ended up with three functions for Vim-like search movement: just exiting a search at a result, killing up to a search result, and copying up to a search result. Not particularly complicated, but I was so proud of myself when I got it working. The functions are included below for your viewing pleasure1:
(defun isearch-vim-style-exit () "Move point to the start of the matched string, regardless of search direction." (interactive) (when (eq isearch-forward t) (goto-char isearch-other-end)) (isearch-exit)) (defun isearch-vim-style-kill () "Kill up to the search match when searching forward. When searching backward, kill to the beginning of the match." (interactive) (isearch-vim-style-exit) (call-interactively 'kill-region)) (defun isearch-vim-style-copy () "Copy up to the search match when searching forward. When searching backward, copy to the start of the search match." (interactive) (isearch-vim-style-exit) (call-interactively 'kill-ring-save) (exchange-point-and-mark))
I've mapped the functions to three separate key bindings in
isearch-mode-map to make them easily accessible while searching:
(define-key isearch-mode-map (kbd "<C-return>") 'isearch-vim-style-exit) (define-key isearch-mode-map (kbd "<M-return>") 'isearch-vim-style-kill) (define-key isearch-mode-map (kbd "<C-M-return>") 'isearch-vim-style-copy)
Justification and motivation
In Emacs, when searching with
isearch, when you 'accept' a match and move point there, Emacs will put you at the end of your matched text. Sometimes this is exactly what you want. Often, though, I find that I'd rather have point move to just before where the search string matches. This is how it works in Vim, and I have convinced myself that it's also the standard way of moving cursors to searches in other text editors. In addition to just moving to a search result, I want the same pattern to apply for killing and for copying the text between your original cursor position and the search match. In short, these functions act on everything between your original cursor position and the start of the selected match, regardless of whether you search forwards or backwards.
This functionality is the same as regular isearch when searching backward, but when searching forward it's the same as adding an extra
isearch-backward) after picking a match.
The Reddit snippet
The tip I got on Reddit gave me a little code snippet to start me off. At first I thought it was just what I wanted, but I realized later that it wasn't quite what I was looking for. The original snippet is very symmetrical in that it goes to the end of a match when searching backward and to the start of a match when searching forward. However, I have found that I always want to move to the start of a match, no matter what side I come at it from. This may seem asymmetrical, but one nice thing about it is that it'll work the same on any match, regardless of whether you're searching forward or backward. This is especially useful if your search has wrapped.
The original snippet was:
(define-key isearch-mode-map (kbd "<C-return>") (lambda () (interactive) (isearch-repeat(if (eq isearch-forward nil) 'forward 'backward)) (isearch-exit)))
So even if it wasn't quite what I needed, it gave me the tools necessary to start working on my own implementation, namely the
isearch-exit functions and the
isearch-forward variable. With this I had all I needed to start playing around with the functionality myself.
Elisp crash course
If you've never encountered Lisp before, here's a short (and very incomplete) introduction to Emacs Lisp. Do bear in mind that this is the first Emacs Lisp code I've written myself, so I'm probably missing a lot of context and nuance. If you find any errors, please do tell me; I'll be very grateful. For a more complete introduction to the language, check out the Emacs Lisp manual.
- ~defun~ [name] (args)
The first line of the function contains the keyword
defun, signifying that we're defining a function, the name of the function, and a list of parameters. In all the above functions, the parameter list is empty, so it's just a set of empty parentheses (
After the first line of each function, I've added a string describing what the function does. This works as documentation. When looking up the function in Emacs (
C-h f <name of function>), this text gets displayed. It's not a requirement to put into a function, but it's nice to have when you need to look things up.
Furthermore, lines starting with
; are standard code comments, such as the one about setting
- ~(interactive)~ and ~call-interactively~
In Emacs Lisp,
(interactive) turns a Lisp function into a command. In short, this means that you can assign it to a key sequence and call it from anywhere in Emacs by using
call-interactively is used to call interactive commands that take arguments. These commands can either be given explicit arguments and called like normal functions, or we can use
call-interactively. When doing the latter, certain parameters can be passed implicitly. For instance
kill-region needs two arguments
END to know what region to operate on. When called interactively,
END get the values of point and mark, so we don't need to pass them explicitly. For more information about the commands and
interactive, check out the Emacs Lisp manual, chapter 21.2.
- What's with the quotes?
Again, the manual (chapter 10.3) has info on what the single quotes do on a deeper level, but in our case, what we want is simply to pass the quoted function as an argument, and not the result of evaluating that function. In short, it's passing a function pointer, rather than the result of a function.
when is the Lisp way to only evaluate some code when a condition holds true. It's similar to an
if expression, but an
if expression/statement without an
- ~let~ and ~current-prefix-arg~
There's not a whole lot of variable binding going on in these functions. The one place it's done is in the
isearch-vim-style-copy function. As you might expect, the
let keyword assigns a value to a variable (in this case
current-prefix-arg). In Emacs Lisp, variables can have global scope, so the
let binding ensures that the variable is only bound to this value within this scope. The variable
current-prefix-arg is used to augment
set-mark-command. We do this to move point back to where the search started after copying the region.
And that's the story of how I got started writing Lisp. It's dead simple, but I've got a taste for it now, and I think I like it. ... yeah. I think I like it.
The code above has been modified from its original published state after feedback from Reddit and working with it some more. The original snippet looked like this:
(defun isearch-vim-style-exit () "Move point to the start of the matched string, regardless of search direction." (interactive) (when (eq isearch-forward t) (isearch-repeat 'backward)) (isearch-exit)) (defun isearch-vim-style-kill () "Kill up to the search match when searching forward. When searching backward, kill to the beginning of the match." (interactive) (isearch-vim-style-exit) (call-interactively 'kill-region)) (defun isearch-vim-style-copy () "Copy up to the search match when searching forward. When searching backward, copy to the start of the search match." (interactive) (isearch-vim-style-exit) (call-interactively 'kill-ring-save) ;; set prefix arg to move point back to where search started (let ((current-prefix-arg '(4))) (call-interactively 'set-mark-command)))