nXML mode is the standard XML editing mode for Emacs. It’s alright, but the ergonomics could be improved: authoring XML is a pain, and you need powerful completion to make it tolerable.
nXML’s main quality of life feature is automatically completing end tags. That
is, if you type </
, it automatically completes this to </foo>
, by searching
backwards to find which tag you’re in.
Many XML editors have a feature where typing the start tag causes them to
complete the end tag, and leave the cursor between the two. So if you have
<foo|
(where the pipe character is the point, or cursor), typing the
right angle bracket completes this to <foo>|</foo>
, leaving the point between
the two elements.
nXML can do this, but you have to input an awkward key combination. We can instead do this automatically:
(require 'nxml-mode)
(defun my-in-start-tag-p ()
;; Check that we're at the end of a start tag. From the source code of
;; `nxml-balanced-close-start-tag`.
(let ((token-end (nxml-token-before))
(pos (1+ (point)))
(token-start xmltok-start))
(or (eq xmltok-type 'partial-start-tag)
(and (memq xmltok-type '(start-tag
empty-element
partial-empty-element))
(>= token-end pos)))))
(defun my-finish-element ()
(interactive)
(if (my-in-start-tag-p)
;; If we're at the end of a start tag like `<foo`, complete this to
;; `<foo></foo>`, then move the point between the start and end tags.
(nxml-balanced-close-start-tag-inline)
;; Otherwise insert an angle bracket.
(insert ">")))
(define-key nxml-mode-map (kbd ">") 'my-finish-element)
Another complaint I have is indentation. By default, if you have:
<foo>
<bar>|</bar>
</foo>
And you press enter, you get:
<foo>
<bar>
|</bar>
</foo>
But this isn’t what I want. I want to leave the point in an indented blank line between the two elements:
<foo>
<bar>
|
</bar>
</foo>
We can do this very straightforwardly:
(defun my-nxml-newline ()
"Insert a newline, indenting the current line and the newline appropriately in nxml-mode."
(interactive)
;; Are we between an open and closing tag?
(if (and (char-before) (char-after)
(char-equal (char-before) ?>)
(char-equal (char-after) ?<))
;; If so, indent it properly.
(let ((indentation (current-indentation)))
(newline)
(indent-line-to (+ indentation 4))
(newline)
(indent-line-to indentation)
(previous-line)
(end-of-line))
;; Otherwise just insert a regular newline.
(newline)))
(define-key nxml-mode-map (kbd "RET") 'my-nxml-newline)
I don’t know the first thing about Emacs Lisp, but I could specify, algorithmically, the kind of thing I wanted to see happen, so I asked ChatGPT how to do it:
I made two of mistakes (forgetting one step of the algorithm, and forgetting to
specify a necessary precondition). ChatGPT made one: it suggested
beginning-of-line
rather than end-of-line
, where the latter is required to
put the cursor after the whitespace for indentation.