12

In normal mode ( and ) move to the previous or next first character in the sentence, respectively. How do I move to the very last character.

If all of my sentences end with a period I can use )T. (first character of the next sentence, then backwards to the first period) but I want a more robust solution to use in a macro.

I know about as and is while in visual mode, so vis<ESC> works (visual mode, then select inside the sentence, then exit visual mode), but that seems pretty ugly to me, and I can't use it as a motion with other commands.

I'm at a loss. Help!

4
  • Why two <esc>s? You only need one.
    – Doorknob
    Feb 26, 2015 at 0:51
  • Oops, I was just getting mixed up because of the delay for vim to respond. I'll edit the question.
    – bsmith89
    Feb 26, 2015 at 0:56
  • 2
    I can only suggest )gE. I know you said you always end sentences with a period, but it's better to not even have to assume that.
    – superjer
    Feb 26, 2015 at 1:35
  • Also note that as and is do not only work in visual mode. You can do das in normal mode to Delete A Sentence.
    – superjer
    Feb 26, 2015 at 1:39

4 Answers 4

11

From my research into this, you need to define at least two custom motions. The first will be for just moving the cursor, and the second will be for use in operators. For a full functioning setup, it is much more complicated, and I would recommend looking at the code in CamelCaseMotion by Ingo Karkat which shows significantly more thought on doing this right than I do below. Most of my answer is based on quick information found in Vim help and experimentation.

In the following examples, I'm going to remap the ) and ( motions to do the each end of a sentence. You can use other characters as you wish by modifying the example. I'm also going your example of vis<ESC> as the more robust option (compared to )T.). In doing so, if you are already at the end of a sentence, you end up staying at the end instead of moving to the end after this one, because of the way is works. You can try replacing it with as for different results. This should actually work for turning either side of a text object into a motion.

First, the normal mode movement

Note This doesn't work in visual mode or operator pending mode

function! MoveToEnd(type, ...)
    exe "normal `]"
endfunction
map <silent> \) :set opfunc=MoveToEnd<CR>g@
nmap ) \)is

First, we define a new function called MoveToEnd whose sole job is to put the cursor at the the "last character of the previously changed or yanked text", which is the ] mark (see :help ']).

Next, we create a custom operator \) that calls our MoveToEnd function on whatever motion is given to it. In this way, the ] mark becomes the end of the selection given to our new operator. (see :help map-operator)

Finally, we map ) to call our custom operator with the is mostion, thereby effectively giving the equivalent of vis<ESC> without changing the last visual selection and without changing modes.

The reverse is similar, just using the other directions:

function! MoveToBeginning(type, ...)
    exe "normal `["
endfunction
map <silent> \( :set opfunc=MoveToBeginning<CR>g@
:nmap ( \(is

Second, the operator movement

This one is trickier. Our motion above destroys the start position, so operators like 'd' end up working against nothing. What we need is to be able to save our start position and add the end position. :help omap-info (operator pending mapping) gives a tip on doing this, which is to actually use visual mode.

What we'll do then, is save our position to one end of the visual selection register, use our normal mode motion from above, set the other visual selection register, and then activate visual mode:

function! SelectToEnd()
    normal m<  "Set start point of visual selection
    normal )   "Move to end of sentance (relies on mapping from earlier)
    normal m>  "Set end point of visual selection
    normal gv  " 'Go Visual' activate visual mode using selection marks
endfunction
omap ) :call SelectToEnd()<CR>

And for our back motion:

function! SelectToBeginning()
    normal m>  "Set end point to current position (since we are moving backwards)
    normal (   "Move to beginning of sentence (relies on mapping from earlier)
    normal m<  "Set start point
    normal gv  "Activate visual mode based on our new marks
endfunction
omap ( :call SelectToBeginning()

With this, you can do d) to delete to the end of the sentence (instead of the original of deleting to the beginning of the next one). All other operators should work as well.

4

Ninja Feet

I wrote a plugin called ninja-feet.vim that aims to help with this problem.

Motiony Operators

It defines some motiony operators1 that allow you to operate on text from your cursor to one end of a text object. Using [ or ] between the operator and text object causes the operator to operate on text from the cursor to the left or right (respectively) boundary of the text object.

There are also Normal mode operators for starting Insert mode at the beginning or end of a text object: z[ and z], respectively.

Example 1: Change from here to the end of the string

In this example, let | denote the cursor position.

foo("This is a string| with superfluous text at the end", bar);

Typing d]i" will change this to:

foo("This is a string", bar);
  1. d: use the delete operator
  2. ]: to the end of the following text object
  3. i": quoted string text object

Example 2: Start inserting after the current sentence

Again, let | denote the cursor position.

Here is |some sentence. And here is another.

Typing z]is will start Insert mode at the end of the first sentence.

  1. z]: start Insert mode after the following text object
  2. is: sentence text object

Caveats

  • These mappings are not repeatable with .. It's quite tricky and I'm trying to get it right.
  • The [ and ] operators (used between operator and text object) only work for text objects that begin with i or a. Technically, that's because they're really maps for [i, ]i, [a and ]a. I would love to improve this, but it's also very tricky.
  • There are no Normal mode motions to simply move your cursor to an end of a text object. I have gotten it to work, but at the price of the motion actually being an operator (meaning . will repeat the motion). I don't find that behaviour acceptable.

1 I call them motiony operators because they're operators mapped in Operator-pending mode, which is the mode you're in between typing an operator and a motion (e.g. between the d and w in dw).

1

If your sentences end with a period you can use just t. instead of )T. to move to the last character before the period. I find this the most convenient in everyday use and you can easily change to a question or exclamation mark as needed. It will move between sentences when repeated with ; (but not if typing out t. again). To move between hard wrapped lines with t you need the improvedft vim plugin, which makes this solution quite convenient.

If you want a single command compatible with all sentence ending characters, you could define a search similar to /\>[.?!]\(\s\|$\) (updated from comments). It searches for the end of of a word followed by a sentence ending character and then either a whitespace or the end of a line. You could bind it to a key if you don't want to type it in each time, something like noremap <leader>s /\>[.?!]\(\s\\|$\)<CR>.

3
  • 1
    The problem with t. is you have to already be on the line containing the period, which isn’t always true (esp. with hard-wrapped lines). I think your regex simplifies to \>[.?!]\(\s\|$\) (I used a word boundary rather than \w\ze, but either should work)
    – D. Ben Knoble
    Jan 10, 2021 at 15:14
  • Thank you @D.BenKnoble, that is a much nicer regex! Regarding t., I forgot to mention that the improvedft plugin lets you move across lines, added that now. Jan 10, 2021 at 21:10
  • Ah so \> matches end of a word. In other words, then: / (search for) \> (end of a word) [.?!] (where that word is followed by end of sentence punctuation) \(\s\|$\) (followed by a space or end-of-line).
    – markling
    Nov 6, 2021 at 10:47
0

In traditional vi you need 2 spaces after the punctuation -like the period- for the parens-jump to be effective.

Since many people no longer use the "2 spaces rule" when typing, the parens-sentence jump would fail.

1
  • 1
    For that reason, vim includes :help cpo-J to allow using the old behavior (backwards compatible). Though this doesn't actually answer the question posed.
    – D. Ben Knoble
    Oct 22, 2021 at 0:52

Your Answer

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

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