My first Emacs Lisp

Or: How I can't let Vim go
An Emacs Lisp function called `learn-some-elisp`. The content of the function is just filler to make it look like it's doing something.

Not as hard as you might think.

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 C-r (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-repeat and 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 (()).

Comments

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 current~prefix-arg.

~(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 M-x. Similarly, 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 BEG and END to know what region to operate on. When called interactively, BEG and 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~

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 needs an else-clause to run if the provided condition doesn't hold true. In languages like Python, Rust, JavaScript, etc., it's the equivalent of just using an if expression/statement without an else-clause.

~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 (4) to 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.

Footnotes

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


Thomas Heartman is a developer, writer, speaker, and one of those odd people who enjoy lifting heavy things and putting them back down again. Preferably with others. Doing his best to gain and share as much knowledge as possible.