Auto-closing Django template tags in Emacs
July 7th, 2008 by Eddie SullivanI have written two previous articles about how I edit Django template files in Emacs and XEmacs. Here is How I edit Django templates. And here is More on editing Django templates in XEmacs. Here today is another little tip that can be used in conjunction with those two other posts or independently.
Django templates involve a lot of punctuation. Between the angle brackets and slashes of HTML and the curly braces and percent signs of the Django template language, it's enough to make your pinky fingers hurt just thinking about it. Therefore any little trick to reduce some of this typing burden can be helpful. Presented here is some Emacs Lisp code to provide auto-closing of Django template tags. So even if you still have to type things like curly-brace percent-sign space ifequal blah blah2 percent-sign close-curly-brace, you won't have to type the {% endifequal %}. (Of course, if you're using the abbrev tips I gave previously, you won't even need to type the opening tag very often, but sometimes you still do.)
The code
Ok, here it is. You should be able to copy and paste this into your .emacs file. Then anywhere in your template file, simply call the new function django-close-tag and it will find the last open tag and close it. In the sample code below, I have it bound to C-c]; that is, hold down the control key and press C, then let go of the control key and press the right square bracket. Of course, you can change the code to bind the function to any key combination you want.
(defvar django-closable-tags '("for" "block" "comment" "filter" "ifchanged" "ifequal" "ifnotequal" "spaceless" "if")) (defvar django-tag-re (concat "{%\\s *\\(end\\)?\\(" (mapconcat 'identity django-closable-tags "\\|") "\\)[^%]*%}")) (defun django-find-open-tag () (if (search-backward-regexp django-tag-re nil t) (if (match-string 1) ; If it's an end tag (if (not (string= (match-string 2) (django-find-open-tag))) (error "Unmatched Django tag") (django-find-open-tag)) (match-string 2)) ; Otherwise, return the match nil)) (defun django-close-tag () (interactive) (let ((open-tag (save-excursion (django-find-open-tag)))) (if open-tag (insert "{% end" open-tag " %}") (error "Nothing to close")))) (define-key html-mode-map "\C-c]" 'django-close-tag)
How it works
The variable django-closable-tags is a list of all the Django tags that require closing. As more tags get added, this list can be expanded.
The meat of the work is done in django-find-open-tag. The first thing it does is search backwards through the buffer for the last Django tag. If the last tag is an end-tag, the function calls itself recursively to skip over that block. This continues until it finds an unclosed tag, which it then returns as a string.
The function you actually call is django-close-tag. This calls django-find-open-tag to find a tag to close, then inserts the appropriate end-tag text.
Hopefully that is pretty straightforward. The only tricky part for a Lisp newbie could be the use of recursion. For a situation like this, recursion is the perfect approach. In a sense, the function calls build a list of closed tags on the call-stack, which then unravels one at a time until an unmatched tag is found.
Other than that, there is some pretty straightforward regular expression matching. These are extremely powerful, and you should read up on them if you haven't already.
July 8th, 2008 at 4:04 pm I've made one small correction in the text above. Be sure "if" comes after "ifequal", "ifchanged", and "ifnotequal" in django-closable-tags, so that the matching can work correctly.
August 22nd, 2008 at 10:34 am I've added your auto-closing elisp to my fork of the django-html-mode on https://code.edge.launchpad.net/~eopadoan/+junk/django-html-mode