I recently discovered to use context dependent keybindings. Keypress that does different thing depending on where the cursor — the point — is in the buffer.
The idea is not new. I learned how to use it from this blog post by Arthur Malabarba. Lispy and Worf, two packages by abo-abo, use it everywhere.
Lispy uses the peculiar syntax of LISPs or LISPs-like languages to
define keybindings that are aware of the context. An example :
pressing j
at a closing paren jumps to the next closing paren of the
same level in Lispy.
After using Lispy for a while, I recently had to edit some R code. I
grew frustrated of having to press C-c C-p
to evaluate a paragraph,
while I could just press p
after the last function call and be done
with it.
I thus defined :
(defvar ess-closing-delim ")\\|}\\|]"
"R closing delimiter")
which is a regex describing closing delimiters in the R syntax.
(defun lesspy-eval-function-or-paragraph (arg &optional vis)
(interactive "p")
(cond ((looking-back ess-closing-delim)
(ess-eval-region-or-function-or-paragraph vis))
(t (self-insert-command arg))))
I have bound this to p
with
(define-key ess-mode-map (kbd "p") #'lesspy-eval-function-or-paragraph)
The key part in this is the (looking-back ess-closing-delim)
part.
It will return t
(true) if the cursor is after a closing delimiter,
and nil
(false) otherwise. If the first cond
statement is false,
the last one is true by default and tells emacs to insert the arg
value, which corresponds to the (kbd "p")
command. In other words,
it inserts p
.
Now when I write R code, and the cursor is at the following position:
ggplot(mtcars, aes(cyl, mpg)) +
geom_point()|
I only have to press p
to evaluate the last two lines in the R repl.
But what if the cursor is here ?
ggplot(mtcars, aes(cyl, mpg)) +
geom_point()| +
theme_minimal()
The function will evaluate the whole paragraph. But frequently, when I
edit ggplot2 or dplyr code, I only want to evaluate from the beginning
of the paragraph to the cursor position, without the following
lines1. I thus modified the elisp code to take the +
sign into
account.
(defun lesspy-eval-function-or-paragraph (arg &optional vis)
(interactive "p")
(cond ((or (looking-at " %>%")
(and (looking-at " \\+") (looking-back ")")))
(save-excursion
(let ((end (point)))
(ess-goto-beginning-of-function-or-para)
(ess-eval-region (point) end vis "eval pipeline"))))
((looking-back ess-closing-delim)
(ess-eval-region-or-function-or-paragraph vis))
(t (self-insert-command arg))))
So now when I press p
at the following cursor position:
ggplot(mtcars, aes(cyl, mpg)) +
geom_point()| +
theme_minimal()
It only evaluates from ggplot
to geom_point()
.
This kind of keybindings is great, because it exploits the language syntax to define useful shortcuts. Since I learned how to use it, I’ve put it everywhere in my init file. For example, I’ve defined keybindings for when the cursor is after the punctuation sign of a sentence. Because one rarely put an alphabetic character right after the punctuation sign, this is a place in the buffer that you can exploit to create keybindings. An example :
(defvar textpy-punctuation-re "\\.\\|?\\|…\\|!\\|\\\"\\|^$"
"the punctuation sign regex")
(defun back-punctuation-p (&optional empty)
"if empty is t, returns t even on empty lines"
(and (or empty (not (looking-at "^")))
(looking-back textpy-punctuation-re)))
(defun textpy-save (arg)
(interactive "p")
(cond ((back-punctuation-p )
(save-buffer))
(t
(self-insert-command arg))))
(define-key text-mode-map "s" #'textpy-save)
Now when the cursor is after a .|
, pressing s
saves the buffer. If
the cursor is after . |
, pressing s
just inserts s
, as you would
expect.
The only difficulty in this kind of keybindings definition is to think
of all the use cases you can have for the target regexp and the overrided key (s
in this case).
- In ggplot2, you add elements to a plot layer by layer. So evaluating lines per line is a great way to isolate the effects of adding a layer. [return]