20130222

Creating New Text Objects in Evil-Mode (Vim emulation layer in emacs)


The links to Practical Vim are affiliate links to Amazon. Beware!

So... last January I was in a flight to London, preparing for an intense, 12 days course on traditional shoemaking (English hand-welted shoes, improving our knowledge at The Fancy Puffin.) And my flight read was Practical Vim. Most of my readers are already aware I'm an emacs guy, so the main question is why?

I love knowing many tools. Programming in many languages, knowing enough about any thing. I couldn't stand all these people saying Vim was superior (even though most had never touched emacs,) and since knowledge is power, I thought a little more (for me) could not hurt (mwahahahaha!) I picked Practical Vim and started reading it a few days before leaving, and finished it during the few non-shoemaking moments of that course.

When I was back to having a computer, I was torn between what I learnt about Vim and looked cool and all those things I already loved about emacs. I have MacVim a few tries, writing short pieces, writing, testing, rewriting. Clearly there was no way I could give up gnus, org-mode (even though I'm not using it that much lately) and all my complex configuration options. So... how come? Even though modal editing feels weird, text objects are cool. Being able to "say" di( and get it to delete inside a pair of parentheses is way cool. Basically this is what sold me as a great feature.

I decided to use evil-mode in emacs. Best of both worlds: I still had all my emacs tools AND text objects and some modal editing. I missed having numbered lines, although I just googled for it... And there's linum-mode (since emacs 22!!) which gives them. But I miss ex commands: being able to move any line anywhere just by issuing :source m destination is powerful. Evil-mode supports some ex commands, just not copy or move. Sigh!

Today was the first day I added evil-mode to my .emacs file, and the day started by working in a LaTeX file. In a LaTeX file, most things are enclosed in pairs of dollars, like $x=1$. These dollar delimited expressions are what TeX parses as formulas, and they appear as neat images in your target (usually PDF) file. So, most of the things I need to change, delete or move around are enclosed by dollars. So:
  • change what's inside a pair with ci$
  • delete the whole formula with da$
  • delete just the contents of the formula with di$
  • copy the formula with yi$
Useful, isn't it? Problem is, evil-mode does not have $ as a text object! Luckily it offers the tools to make new text objects... Getting it to work was harder than expected, though. I was given several choices of functions I could use to create my text object. The best option seemed to be using evil-regexp-range that allows you to give a regexp for the first delimiter and another one for the second. But all tries I have done resulted in it marking the first occurrence in the buffer. No luck. After trying repeteadly with it, I gave up and used instead evil-inner-object-range, where I had to wrestle with point, mark, excursions and the like. When I managed to get it working I just decided I would not touch it again (even if I'm pretty sure the save-excursion calls are redundant, as well as most move and mark commands...) It works, don't touch it.

(defun rb-first-dollar-exclusive (&optional arg)
(interactive)
(save-excursion
(setq rb-temp-first (re-search-backward "\\$" nil t 1))
(message "First %s" rb-temp-first))
(goto-char (+ 1 rb-temp-first)))

(defun rb-second-dollar-exclusive (&optional arg)
(interactive)
(save-excursion
(setq rb-temp-second (re-search-forward "\\$" nil t 1))
(message "Second %s" rb-temp-second))
(goto-char (- rb-temp-second 1)))

(evil-define-text-object evil-inner-dollar (count &optional beg end type)
(evil-inner-object-range count beg end type
#'rb-second-dollar-exclusive
#'rb-first-dollar-exclusive))

(define-key evil-inner-text-objects-map "$" 'evil-inner-dollar)

(defun rb-first-dollar-inclusive (&optional arg)
(interactive)
(save-excursion
(setq rb-temp-first (re-search-backward "\\$" nil t 2))
(message "First %s" rb-temp-first))
(goto-char rb-temp-first))

(defun rb-second-dollar-inclusive (&optional arg)
(interactive)
(save-excursion
(setq rb-temp-second (re-search-forward "\\$" nil t 1))
(message "Second %s" rb-temp-second))
(goto-char rb-temp-second))

(evil-define-text-object evil-outer-dollar (count &optional beg end type)
(evil-inner-object-range count beg end type
#'rb-second-dollar-inclusive
#'rb-first-dollar-inclusive))

(define-key evil-outer-text-objects-map "$" 'evil-outer-dollar)


(Blogger is killing indentation, damn) Enjoy, though!

Written by Ruben Berenguel