Editing an arbitrary XML file in nxml-mode.


Let's say I have a structure like this:


Let's say the cursor is at the <a>. I'm looking for one shortcut that brings the cursor directly from <a> to </a>, and back if called again.

Search before you ask

This question requests one-way jump and gets an answer: C-M-n to jump forward, C-u -1 C-M-n to jump backwards -- Emacs, nxhtml how to highlight or jump to the closing html tag? - Stack Overflow.

But this is inconvenient because to jump back one has to:

  1. position the cursor exactly at end of closing tag, and
  2. use a long and convoluted key shortcut C-u -1 C-M-n.

There's room for more convenience: one shortcut to do both directions.

How to do that?

If this does not already exists, I'd like to it. I'm a seasoned developer in a number of languages and environments but not proficient enough in emacs-lisp to make my own solution. Still, it might be easy enough to do for an emacs-lisp wizard.

Technically, one function with this behavior would do the trick:

  • when cursor is on an opening tag, jump forwards (call (nxml-forward-element 1))
  • when cursor is on a closing tag, jump backwards (call (nxml-forward-element -1))
  • bonus points if, when cursor is anywhere between < and > (not only just before < as nxml-forward-element requires), the function still works
  • bonus points if, when cursor is anywhere inside text or whatever, then jump backwards to opening tag.
  • What do you expect between </foo><bar> ? I guess : jump to </bar>.
    – YoungFrog
    Jun 28, 2016 at 7:03
  • Yes, between </foo><bar> jump down to </bar>. If inside </foo>, jump up to <foo>. Same key shortcut. Jun 28, 2016 at 9:12

2 Answers 2


If you use evil-mode, you can install https://github.com/redguardtoo/evil-matchit and press %

If you prefer original Emacs,

(autoload 'sgml-skip-tag-backward "sgml-mode" nil t)
(autoload 'sgml-skip-tag-forward "sgml-mode" nil t)
(defun html-get-tag ()
  (let ((b (line-beginning-position))
        (e (line-end-position))
        (looping t)
        (html-tag-char (string-to-char "<"))
        (char (following-char))
        (p (point))
        (found_tag -1))

      ;; search backward
      (unless (= char html-tag-char)
        (while (and looping (<= b (point)) (not (= char 60)))
          (setq char (following-char))
          (setq p (point))
          (if (= p (point-min))
              ;; need get out of loop anyway
              (setq looping nil)

      ;; search forward
      (if (not (= char html-tag-char))
            (while (and (>= e (point)) (not (= char 60)))
              (setq char (following-char))
              (setq p (point))

      ;; is end tag?
      (when (and (= char html-tag-char) (< p e))
        (goto-char p)
        (if (= (following-char) 47)
              ;; </
              (skip-chars-forward "^>")
              (setq p (point))
              (setq found_tag 1))
            ;; < , looks fine
            (setq found_tag 0)))))

(defun html-jump(&optional num)
  "Jump forward from html open tag"
  (interactive "P")
  (unless num (setq num 1))
  ;; web-mode-forward-sexp is assigned to forward-sexp-function
  ;; it's buggy in web-mode v11, here is the workaround
  (let ((backup-forward-sexp-function forward-sexp-function))
    (if (= (html-get-tag) 0)
        (sgml-skip-tag-forward num)
      (sgml-skip-tag-backward num))))

Usage: M-x html-jump, it works in any html related major mode (php-mode, xml-mode, web-mode, nxml-mode ....)

  • Thanks @chenbin though I can't have it work. (1) The first option seemed worth a try, so I installed evil and evil-matchit from melpa, enabled Evil-Matchit minor mode on a HTML document. Pressing % just inserts the character, even when on a HTML opening or closing tag. (2) Original emacs: how can we have one function automatically call the forward or backward variants you just define, depending on the cursor position? That's a major point of the question. Thanks again. Jun 28, 2016 at 6:31
  • for option 1, you need enable both modes. insert (evil-mode 1) (global-evil-matchit-mode 1) into ~/.emacs. evil-mode is vim emulate, so unless you use vim key bindings, you can't use evil-matchit.
    – chen bin
    Jun 28, 2016 at 6:37
  • I just did M-x evil-mode M-x evil-match-mode and yes, typing % does it: one key shortcut to go back and forth. Alas, as you write cursor navigation and all behaves like vi. That's too much a change to bear. Just isolating the one shortcut while keeping standard emacs behavior would be perfect. Jun 28, 2016 at 9:16
  • Just tried your edited answer. It does define one function and that's good. In most situations it works perfectly. Jun 28, 2016 at 9:27
  • One case remains. I tried a few changes in the code, without success so I stick to your version and ask. Observed: when jumping from <foo> to </foo><bar> it jumps down to right after the closing bracket of </foo> which sometimes happens to be the opening bracket of <bar>. As a result, calling again jumps down to </bar>. Wished: when jumping from <foo> to </foo><bar> it should stop one char back inside </foo> to ensure the trip back to <foo> when calling again. Thanks if you can adjust this. Jun 28, 2016 at 9:27

Try the following:

(defun nxml-jump-to-closing-tag ()
  (forward-char 2)
  (if (search-backward "<" nil t)
    (message "No more elements")

(defun nxml-jump-to-openning-tag ()
  (backward-char 2)
  (if (search-forward ">" nil t)
    (message "No more elements")

(define-key nxml-mode-map (kbd "<C-M-right>") 'nxml-jump-to-closing-tag)
(define-key nxml-mode-map (kbd "<C-M-left>")  'nxml-jump-to-openning-tag)

Press C-M-←. Press C-M-→ to get back.

  • Thank you for this starting point. If I understand correctly, their main value is to cope with the cursor being near (instead of exactly) at the expected position. Can these two functions be merged into (or called by) one, as in the original question, "one shortcut to do both directions"? The function would have to detect if cursor is near a closing or opening tag to choose the correct function to call. May 30, 2016 at 9:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.