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.