While I was studying Tpope's vim-commentary, I was struck by a line that contains the topic of this question. I am referring to one of the lines in Tpope's vim-commentary: https://github.com/tpope/vim-commentary/blob/f8238d70f873969fb41bf6a6b07ca63a4c0b82b1/plugin/commentary.vim#L101

nnoremap <expr>   <Plug>CommentaryLine <SID>go() . '_'

What does . '_' mean in the context where the statement is followed by a function call?

<SID>go() function has some side effect of transforming a text(i.e. commenting a line) and returns an empty string, does that mean <SID>go() evaluates to empty string which is concatenated by '_' due to . '_'? I am not so sure because of the name of the mapping <Plug>CommentaryLine.


It turns out that, when called without arguments, go() actually sets 'operatorfunc' and returns 'g@'. (See this code snippet.)

So the concatenation of 'g@' . '_' results in the g@_ command being executed in normal mode. The <expr> in the mapping makes Vim evaluate it as an expression and finally execute the string result as a normal mode command (similar to how :execute normal! would do, in this particular case.) See :help :map-<expr> for more details.

The g@ command will wait for a motion and then apply the 'operatorfunc' to that motion. See :help g@. So you could use, for example, g@} to apply it to the block going from the current line until the next blank line, or g@ip to apply it to the paragraph under the cursor.

The _ motion moves to the beginning of the current line, so in that sense it's somewhat similar to the ^ motion, but it has two very important differences from the latter.

First, it's a linewise motion, so even though it will stay on the same line, when used as a motion for an operator, it will apply to the whole line and not just the characters from the cursor position to the beginning of the line. (For illustration, place the cursor in the middle of a line and see the difference between d^ and d_.)

Second, when used with a count, it will move to the beginning of the [count] - 1 line below the current one. So 5_ will move down 4 lines and go to the beginning of that line (similar to 4j^ or 4+.) But that's very useful in a motion for an operator, since when you include the current line up to 4 lines down, you end up with 5 lines, so 5_ is actually selecting a block of 5 lines, starting with the current line. (For illustration, see how 5d_ will delete 5 lines. That's equivalent to d5_, the count may precede the operator-pending command as well.)

In effect, many Vi shortcuts doubling a letter are in fact equivalent to using _. For example, dd is the same as d_ and yy is the same as y_.

See :help _. (Though, it's really brief, won't give you all these details and really draw attention to the two important details I highlighted here.)

So, in short, g@_ here will apply the 'operatorfunc' to the current line. It will work on a line, since _ is a linewise motion. It will also take a count and act on a block of [count] lines, due to the way _ behaves with a count. That's it!

We haven't really covered the 'operatorfunc' being used here. (It's a bit out of scope of your question, but hopefully still on topic.) It turns out the go() function is using a slightly convoluted way to set 'operatorfunc' to itself. (See :help <sfile> to see how you get to the function name.)

When g@ calls an 'operatorfunc' it passes it a single argument, which indicates whether the motion was linewise, characterwise or blockwise (see :help g@ again for details.) So in case of g@_, it will call go("line"). Being called with a single argument will arrive at this codepath, which will make it act on the lines going from '[ to '], which are the marks set by g@.

And, as advertised in the help of the commentary plug-in, the gcc (comment line) binding will work with a count.

  • 1
    Thank you for the comprehensive answer @filbranden! Your answer makes sense, except for one thing. If "g@_" is a string, wouldn't you want to run execute "normal" . "g@_"? Or is a string in a nnoremap context automatically interpreted as a sequence of normal commands?
    – Alby
    Dec 5 '19 at 17:16
  • @Alby That's what the <expr> does. See :help :map-<expr>. I'll update the answer to explain that bit as well.
    – filbranden
    Dec 5 '19 at 17:21
  • 1
    Thank you @filbranden . I think your had another post that is a great supplementary material to this question as well. vi.stackexchange.com/questions/20523/…
    – Alby
    Dec 5 '19 at 17:30
  • 1
    Really happy to help @Alby! Welcome in the journey of learning Vimscript in detail. Studying good quality plug-ins is a great way to learn and tpope's plug-ins are definitely very high quality ones. Good luck! And keep asking questions, that way we'll all learn more together.
    – filbranden
    Dec 5 '19 at 17:33

does that mean go() evaluates to empty string which is concatenated by "_"?

In short, yes, it does. _ is a normal mode command to move cursor on the first nonblank character of the current line (when used without count). It gets executed due to :h map-<expr>

  • Since <SID>go() is executed in normal mode, does . mean "repeat" in this normal context? In other words, execute <SID>go, repeat, and go to the first nonblank character?
    – Alby
    Dec 5 '19 at 17:08
  • @Alby <SID>go() is an :h expression. "Dot" here stands for string concatenation operator.
    – Matt
    Dec 5 '19 at 17:58

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.