Now that we have test coverage and a test runner, we can refactor the begin/end code.
First off, let’s namespace it. Instead of
defun begin-end-quote (), let’s make it
defun my/begin-end-quote ().
Why? There is another library with a function called
begin-end-quote: I’m not using it, but if I were, after namespacing I wouldn’t have to worry about anything breaking because of the name collision.
The coding conventions suggest namespacing packages for widespread use with
typopunct-insert-quotation-mark), but namespacing “less formal” code with
/ seems a useful distinction. Sacha Chua suggested a further convention for Emacs config files of
my/ instead of (say)
sean/, since .emacs.d code is so often copied and borrowed and it would make it look more consistent after mixing code from multiple sources.
Run the tests: they still pass. Hurrah.
Almost all of the code between
begin-end-quote is the same. I made them separate to begin with because I thought they would be more different, but they aren’t. So we can make everything but the outermost function take an argument of “quote” or “verse” and remove the duplication. (This will also let me quickly add variants like
#+begin_src emacs-lisp and
#+begin_example, which I’ve already needed while writing this blog.)
Again, run the tests, they still pass. Commit with this change.
Further quick points:
newline does insert a carriage return, but
insert “\n” would do the same thing, and feels more intuitive. Likewise, while
next-line do what they say, the more usual way would be to run
forward-line with a positive or negative integer argument.
There is also duplication between
my/begin-end-no-selected-region: they both print the
#+end tags, and if there’s a selected region it is reformatted, and if there isn’t, after printing the tags it moves the cursor back up between the tags. We can collapse the two methods into a single method with two conditionals.
(defun my/begin-end (variant)
(let ((cited-string "\n"))
(my/remove-old-citation-formatting (buffer-substring-no-properties (region-beginning) (region-end))))
(delete-region (region-beginning) (region-end)))
(insert "#+begin_" variant "\n"
"#+end_" variant "\n"))
The simpler conditional is at lines 11-12:
(use-region-p) returns true if there is a region selected, and if there isn’t, it moves the cursor up two lines.
The one in lines 3-10 is more complicated.
In lines 3-7, we’re using a
let statement to set
cited-string initially to
\n, which is what it should be if there’s no selection, and then we’re checking to see if there is a selected region: if there is, we’re resetting
cited-string to the value of the selected region with the old formatting code stripped out, and then we’re deleting the selected region.
At the beginning of line 8, then, we’ve deleted the selected region if there was one, and until the end of the body of the
let at the end of line 10,
cited-string contains either a return character (if there was no selection) or the reformatted contents of the selected region (if there was a selection). Because of that, we can insert it between the tags either way.
As part of this, we also modified the
my/fix-old-formatting method from one that dealt with the selected region in place to one which took a string and returned a modified string. All the selected region manipulation now takes place in the
Re-run the tests: they still pass. Excellent. Commit.
At this point, the refactoring is done, but it is now trivially easy to add new tests and implementations and key-bindings for
#+begin_src emacs-lisp /
The test for
#+begin_src emacs-lisp breaks because for
_src, the beginning tag and the end tag are different. The simplest way to get this passing is to pass in two arguments to
my/begin-end, which for all but
src will be identical, like so:
(defun my/begin-end-example ()
(my/begin-end "example" "example"))
(defun my/begin-end-src-emacs-lisp ()
(my/begin-end "src emacs-lisp" "src"))
(defun my/begin-end (begin-tag end-tag)
(insert "#+begin_" begin-tag "\n"
"#+end_" end-tag "\n"))
At this point, the code does everything we need, the tests pass, and we drove out the new functionality we needed with tests. We’re done. Final commit.