1

Say the point is on the 0 here:

as0sasasa
asasasasa
as1sasasa
    asa
as3sasasa
asasasasa
as2sasasa

I'd like to have a command – like a vertical forward-word – that when run repeatedly would jump first to point 1, then to point 2. And another command – a vertical backward-word – that would then jump back to 3, and finally to 0 again.


Use cases (see comments)

I have some tables in LaTeX that are structured like this:

header    text    text    text    
          text    text    text    
          text    text    text    
header    text    text    text    
          text    text    text    
          text    text    text    

This command would be useful to jump to the different instances of header. Also, I often format plain text like this

texttexttexttexttexttext
  texttexttexttexttexttext
  texttexttexttexttext
  texttexttexttexttexttexttext
  texttexttexttexttexttext
  texttexttexttexttext
texttexttexttext
  texttext
    texttexttext
    texttexttext
texttext

And I'd use this command to skip the more deeply indented parts of the text (the “stops” would be at the bold italic letters if starting from the x).


Update

I've been given a solution to a similar request on Jump to non-whitespace characters along a column.

  • 1
    This seems like a variation of emacs.stackexchange.com/questions/22091/…. – glucas Mar 1 '18 at 20:24
  • @glucas Thanks for the link. That function does quite what I need, it only falls short in that it doesn't skip non-whitespace blocks, only whitespace. Definitely useful anyway, for me. – Arch Stanton Mar 5 '18 at 16:12
  • May I ask what such a command would be good for? I can't think of any situation where I would find that command useful. – Qudit Mar 19 '18 at 1:32
  • @Qudit See the updated question. I didn't expect your question as it happens quite often to me to be in need of such movement. For Vim there's even a plugin for that. Is there some command (other than forward-paragraph) I'm missing, perhaps? – Arch Stanton Mar 20 '18 at 10:15
2

Here's another approach.

(defun my-down ()
  (interactive)
  (when (eq this-command last-command) (line-move-visual 1))
  (while (not (looking-at "[[:digit:]]")) (line-move-visual 1)))

(defun my-up ()
  (interactive)
  (when (eq this-command last-command) (line-move-visual -1))
  (while (not (looking-at "[[:digit:]]")) (line-move-visual -1)))

(global-set-key "\C-o" 'my-down)
(global-set-key "\C-t" 'my-up)

This will also work if you have a short line, such as the next-to-last one here:

as0sasasa
asasasasa
as1sasasa
    asa
as3sasasa
a
as2sasasa
  • Thanks Drew, but you are focusing on my "targets" being digits, while that was only an aid for explaning the question. The only distinction should be between whitespace and non-whitespace characters. I mean, the command I'd like to have should work the same in any column of text as long as it contains the same succession of whitespace and and non-whitespace blocks vertically. I made a different example on the question in vi.sx that is still pertinent for this one, maybe that's better. – Arch Stanton Mar 2 '18 at 9:29
  • 1
    @ArchStanton No, forward-word works not on the basis of whitespace but on the basis of word syntax or a self-defined function. See the doc of foward-word. – Tobias Mar 20 '18 at 8:26
  • @Tobias the analogy with forward-word was limited to the functionality, just to make the question hopefully a bit clearer. – Arch Stanton Mar 20 '18 at 8:38
  • 1
    @ArchStanton But foward-word in vertical direction is the only clear indisputable specification in your question. – Tobias Mar 20 '18 at 8:41
  • @Tobias I hoped the example would have covered such role. – Arch Stanton Mar 20 '18 at 8:54
1

The following lisp code tries to be as close to forward-word / backward-word as possible without the implementation of find-word-boundary-function-table.

It works with word boundaries defined by the syntax table of the current mode and it tries to stay on the current line throughout repeated application of vertical word motion commands.

Since the code is no lisp package yet the vertical word motion commands are bound to user-reserved key sequences. vwm-down-word is bound to C-c w ↓ and vwm-up-word is bound to C-c w ↑.

The code works with the main ideas from Drew's answer https://emacs.stackexchange.com/a/39168/2370 (+1).

;;; vertical word motion: wvm
(defvar-local vwm-current-column nil
  "Current column for vertical word motion commands.")

(defconst vwm-commands '(vwm-down-word vwm-up-word)
  "Vertical word motion commands keeping `vwm-current-column'.")

(defmacro vwm-repeat-command (arg neg-fun pos-fun &rest body)
  "If ARG is negative call NEG-FUN with ARG.
If ARG is larger than 1 call POS-FUN with 1 ARG times.
If ARG is 1 eval BODY like `progn'.
Also takes care of `wvm-current-column'."
  (declare (debug (integerp symbolp symbolp body)) (indent 3))
  `(progn
     (message "Last command: %S" last-command)
     (unless (and (memq last-command vwm-commands)
                  (numberp vwm-current-column))
       (setq vwm-current-column (current-column)))
     (unless arg
       (setq arg 1))
     (cl-assert (integerp arg) nil
                "Expected integer number as argument of `down-word'")
     (prog1
         (cond
          ((< arg 0)
           (,neg-fun (- arg)))
          ((> arg 1)
           (while (and (> arg 0)
                       (,pos-fun 1))
             (cl-decf arg))
           (eq arg 0))
          ((eq arg 1)
           ,@body)
          (t ;; case arg == 0 does nothing
           nil))
       (when (numberp vwm-current-column)
         (line-move-to-column vwm-current-column)))))

(defun vwm-down-one-word (arg)
  "Move down one word if ARG is positive.
Move up one word if ARG is negative."
  (cl-assert (and (numberp arg)
                  (/= arg 0))
             nil "Argument of `down-one-word' must be a non-zero number")
  (setq arg (cl-signum arg))
  (let ((ret t))
    ;; skip whitespace
    (while (and (null (looking-at-p "\\sw"))
                (condition-case nil
                    (progn
                      (when (numberp vwm-current-column)
                        (line-move-to-column vwm-current-column))
                      (line-move-visual arg)
                      t)
                  (error (setq ret nil)))))
    ;; move downwards to end of word
    (when ret
      (while (and (looking-at-p "\\sw")
                  (condition-case nil
                      (progn
                        (when (numberp vwm-current-column)
                          (line-move-to-column vwm-current-column))
                        (line-move-visual arg)
                        t)
                    (error nil)))))
    ret))

(defun vwm-down-word (&optional arg)
  "Move point downwards ARG vertical words (upwards if ARG is negative).
If ARG is omitted or nil, move point downwards one word.
Normally returns t.
If the bottom of the buffer is reached, point is
left there and the function returns nil.

The word boundaries are normally by the buffer’s syntax
table."
  (interactive "p")
  (vwm-repeat-command arg vwm-up-word vwm-down-word
    (vwm-down-one-word 1)))

(defun vwm-up-word (&optional arg)
  "Move upwards until encountering the beginning of a vertical word.
With argument ARG, do this that many times.
If ARG is omitted or nil, move point backward one word.

The word boundaries are determined by the buffer’s syntax
table."
  (interactive "p")
  (vwm-repeat-command arg vwm-down-word vwm-up-word
    (vwm-down-one-word -1)))

(global-set-key (kbd "C-c w <down>") 'vwm-down-word)
(global-set-key (kbd "C-c w <up>") 'vwm-up-word)
  • +1. Sorry for the late comment, I got my degree yesterday and the last few days have been quite intense. This works basically good, except for two things: 1st, it moves one line beyond the desired "target", 2nd, it skips over parentheses and other symbols (I should have been more precise and said that it should jump over whitespace or non-whitespace blocks, more than ws / text blocks). – Arch Stanton Mar 24 '18 at 11:13
1

(The OP question is apparently not expressed very clearly, and it seems to morph...)

Apparently the aim now is to do a downward (vertical) version of forward-word.

For that you can use command gw-downward-word (or gw-upward-word, for a vertical backward-word), from library Go Where (gowhere.el).

gw-downward-word is defined like this:

  1. While not on a word-constituent character, go to the next word char.
  2. Then go to the next non-word char.

Step 1 moves to the beginning of the next word. Step 2 moves (just) past that word.

(defun gw-downward-word (_pos &optional n)
  "Like `forward-word', but move down, not across."
  (interactive "i\np")
  (setq n  (or n  1))
  (dotimes (_i  n)
    (if (and (not (eobp))  (gw-word-char-after-p (point)))
        (gw-to-next-where-vertical #'gw-not-word-char-after-p)
      (let ((pos  (point)))
        (condition-case err
            (progn (gw-to-next-where-vertical #'gw-word-char-after-p)
                   (gw-to-next-where-vertical #'gw-not-word-char-after-p))
          (error (progn (goto-char pos) (error "%s" (error-message-string err)))))))))
  • I don't think my question is unclear, except for the text vs non-whitespace ambiguity. It's kind of interesting that I've found much less difficulty explaining it on vi.sx than here, and that the only partially working solution (that is now linked in the question) has the "evil" tag. It seems like vim and emacs users think about motion in different ways. – Arch Stanton Mar 25 '18 at 10:13
  • Anyway, this answer basically works, but similarly to Tobias's it distinguishes between text and symbols and it ends up one line beyond the "desired target". – Arch Stanton Mar 25 '18 at 10:19
  • Even your "ends up one line beyond the desired target" points to how unclear you've been - you haven't been able to clearly specify what the desired target is. M-f does exactly this: stops at the first non-word char after the word. In Emacs, word syntax means something, and you (and any mode) can define just which chars are word(-constituent) chars. Symbol syntax is, a priori, different. See [Syntax Desciptors]()gnu.org/software/emacs/manual/html_node/elisp/…. If you want _ or whatever to have word syntax then define it that way. – Drew Mar 25 '18 at 14:02
  • "My" vertical forward-word should stop at the last line of a vertical block of non-whitespace characters. The vertical analogue of backward-word should stop at the first line of a block of non-whitespace characters. – Arch Stanton Mar 25 '18 at 14:32
0

I think this pretty much does what you ask, even if it has a slightly different flavor. For this you need library Isearch+ (isearch+.el).

  1. Select the text for that column:

    a. Set the region corners before the 0 in the first line and after the 2 in the last line.

    b. Use C-x SPC, to turn on rectangle-mark-mode.

  2. Use C-M-s to search for the regexp [[:digit:]] (or whatever else you want to match in that column). Repeat C-M-s to search forward; repeat C-M-r to search backward.

Provided option isearchp-restrict-to-region-flag is non-nil, which it is by default, when the region is active Isearch+ restricts searching to the region. (You can also search only outside the region.) When the region is rectangular (i.e., it satisfies region-noncontiguous-p), search is restricted to the rectangular region.

In this case, the rectangular region is a single column of characters.


Alternatively, if you don't want to select the current column as a rectangle, you can use C-z c during Isearch to tell it to limit searching between two columns - in this case the values of (current-column) and (1+ (current-column)). You are prompted for the column values, so you would first use M-: (current-column) to get the column number.

In your case it is column 2 that you want to search, so at the prompt from C-z c you would enter 2 and 3 for the min and max columns to search.

Or you could define your own Isearch filter-predicate-addition command for this, and bind it to a key (say C-z C) in keymap isearchp-filter-map:

(defun isearchp-this-column (&optional flip-read-name-p flip-read-prefix-p msgp)
  "Add predicate that restricts search to this column.
You might be prompted for a predicate name or an Isearch prompt
prefix.  See `isearchp-add-filter-predicate' for info about this and
about the use of a prefix argument."
  (interactive
    (list (and current-prefix-arg  (<= (prefix-numeric-value current-prefix-arg) 0))
          (and current-prefix-arg  (>= (prefix-numeric-value current-prefix-arg) 0))
          t))
  (let ((col  (current-column)))
    (isearchp-add-filter-predicate
      `(lambda (beg end)
          (and (= (save-excursion (goto-char beg) (current-column)) ,(1- col))
               (= (save-excursion (goto-char end) (current-column)) ,col)))
          flip-read-name-p flip-read-prefix-p msgp)))

(define-key isearchp-filter-map (kbd "C") 'isearchp-this-column) ; `C-z C'

Or if you don't really care about adding and removing filter predicates dynamically (and you probably don't, here) then you could just define your own search command for this, using a filter predicate defined for the column where you start the search. (And then search for [[:digit]], as in the other cases above.)

(defun search-this-column ()
  "Regexp-search current column only, using filter predicate `this-col-p'."
  (interactive)
  (let ((isearch-filter-predicate  (this-col-p (current-column))))
    (isearch-forward-regexp)))

(defun this-col-p (column)
  "Return filter predicate that returns non-nil if search hit is in COLUMN."
  `(lambda (beg end)
     (and (= (save-excursion (goto-char beg) (current-column)) ,column)
          (= (save-excursion (goto-char end) (current-column)) ,(1+ column)))))
0

If you want to search for a whole string vertically then things are trickier. Emacs is not really super amenable to that. For one thing, what to do about a line that is shorter than the column you are trying to search? It could be ignored or it could mean that search fails.

This reads a search pattern (literal or, with prefix arg, regexp) and searches down the current column for it. Regexp matching is non-greedy - the shortest match ends the search. And short lines encountered mean the search fails.

(defun search-downward (pattern &optional regexp-p)
  "Seach for PATTERN vertically downward.
With a prefix arg, search for PATTERN as a regexp.
Otherwise, search for it literally."
  (interactive (let ((prompt  (if current-prefix-arg "Regexp search: " "Search: ")))
                 (list (read-string prompt) current-prefix-arg)))
  (unless regexp-p (setq pattern  (regexp-quote pattern)))
  (let ((opoint  (point))
        (col     (current-column))
        (chrs    "")
        chr fail)
    (setq fail  (catch 'search-downward
                  (while (not (= (line-end-position) (point-max)))
                    (line-move-visual 1)
                    (when (and (< (current-column) col)  (not (eobp)))
                      (throw 'search-downward "- short line encountered"))
                    (setq chr   (char-after))
                    (when chr (setq chrs  (concat chrs (list chr))))
                    (when (string-match-p pattern chrs) (throw 'search-downward nil)))
                  ""))
    (when fail
      (goto-char opoint)
      (message "No match%s" fail))))
  • Thanks Drew for helping me. What I need though is something that quickly moves over the text/whitespace block. It should behave like a movement command, not like a search command (it shouldn't prompt for a search query). The answer to How to jump up or down to first non-whitespace character in same column actually does half of the job right: it skips blocks of whitespace, but not blocks of text. – Arch Stanton Mar 20 '18 at 10:11
  • If a line is shorter, it should skip it. It's debatable what it should do if there's whitespace in a line at exactly the same column I'm on, but text on both sides of it (as might happen for a short line with a comment further on the right: blablabla | #comment, where | indicates the cursor column). I'd say it should stop on it. – Arch Stanton Mar 20 '18 at 10:21

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.