4

Context

Editing an arbitrary XML file in nxml-mode.

Need

Let's say I have a structure like this:

<a>
  <b>...</b>
</a>

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 '16 at 7:03
  • Yes, between </foo><bar> jump down to </bar>. If inside </foo>, jump up to <foo>. Same key shortcut. – Stéphane Gourichon Jun 28 '16 at 9:12
3

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))

    (save-excursion
      ;; 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)
            (backward-char))))

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

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

(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. – Stéphane Gourichon Jun 28 '16 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 '16 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. – Stéphane Gourichon Jun 28 '16 at 9:16
  • Just tried your edited answer. It does define one function and that's good. In most situations it works perfectly. – Stéphane Gourichon Jun 28 '16 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. – Stéphane Gourichon Jun 28 '16 at 9:27
0

Try the following:

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

(defun nxml-jump-to-openning-tag ()
  (interactive)
  (backward-char 2)
  (if (search-forward ">" nil t)
      (nxml-backward-element)
    (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. – Stéphane Gourichon May 30 '16 at 9:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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