15

This is what I get from the documentation: \zs "starts the highlighted part" after matching the preceding regex, and \@<= "starts the highlighted part" after matching the preceding atom. But I don't exactly understand the subtleties of this, so can anyone explain how they differ a bit more in depth?

This is what made me curious: If I run

/\_s\zsnnoremap

i.e. select nnoremap preceded by a space or a start-of-line (i.e. the newline from the preceding line, hence the \_ preceding the s) and then run gn to enter Visual Mode and visually select the next match, for some reason only the first column (i.e. the first n in nnoremap) is selected -- despite the fact that the entire nnoremap word is highlighted with :hlsearch turned on.

However, if I instead run the search

/\_s\@<=nnoremap

and then try gn, the entire nnoremap is properly selected. What could be going on here? Did I discover some obscure bug?

1
  • I think its in :h patterns but my memory suggests that regexs are composed of atoms, if that helps explain the difference.
    – D. Ben Knoble
    May 21, 2018 at 12:34

3 Answers 3

18

This looks like you indeed found an obscure bug. I have implemented the gn textobject back in 2012 for Vim 7.3 something. It basically works in the following way:

1) It searches backwards for the last match of the current regular expression.

2) It searches forward for the next match of the current regular expression.

This should make clear, that the cursor will be on start of the next match, even if it was there already at the start of 1). Finally

3) it searches for the end of the current regular expression. and puts the cursor there.

Now what happens here is that the search for the end of the current match wraps around and moves back to the end of the previous match (because wrapscan is already set, after being disabled for 1)). It then sets the Visual marker to the area from start (end of point 2) and the area moved to by the next search item 3).

I'll have a closer look at the problem and will probably submit a patch for Vim later.

[Update 22.05.2018] I have written and submitted a patch to fix this behaviour.

[Update2 22.05.2018] And the patch has been merged as patch level 8.1.0018

[Update 22.10.2019] As of Vim patch 8.1.629 the third step is not performed anymore. Instead Vim can now determine the end of the match when finding the beginning of the match (Step 2)

9

Christian has completely addressed the question of buggy behavior of gn, but there still are fundamental differences between \zs and \@<=. One of these being \@<= modifies a preceding atom, while \zs is an atom in itself.

Consider:

Xnnoremap

\%1cX\zsnnoremap     (regex 1)
\%1cX\@<=nnoremap    (regex 2)
\%2cX\@<=nnoremap    (regex 3)

Regex 1 matches, since \%1c matches in column 1 and there is an X there. \zs merely causes the primary capture to be restarted at a position after the X.

Regex 2 however does not match, because although \%1c matches in the first column, X\@<= is zero width as mentioned in the documentation. nnoremap starts at column 2, not 1, and besides, as @user938271 points out is no possible position before column 1.

Regex 3 matches since nnoremap starts at column 2, and is preceded by an X.

1
  • 1
    I don't think the regex 2 fails because there is nothing to make up the position difference between columns 1 and 2. If that was the issue, removing nnoremap from the regex would produce a match; but the regex still fails even without. I think it fails because \%1cX\@<= expresses a position which can't exist. \%1c matches the position at column 1, and X\@<= asks for a character X to match before that. But there can't be any character before the first column. Which is why, even if you replace X with a dot (any character), the regex \%1c.\@<= still fails.
    – user938271
    Sep 4, 2019 at 18:12
5

\zs applies to the entire regular expression, and sets the next character to be the first character of the entire match. Anything before the \zs will not be included as part of the matching text.

\@<=, on the other hand, only affects the atoms directly around it, allowing you to specify that the next atom will only match if it follows the preceding atom. So for example, the regular expression:

\vbar.*(foo)@<=bar

Will match all the text between two instances of bar (including the instances themselves), but only if the second is preceded by foo. i.e., it will match:

barbazfoobar

but not:

barbazbazbar

Because \@<= is localized in this way, you can even use \@<= multiple times in a single expression:

\vbar.*(foo)@<=bar.*(foo)@<=bar

The following will match three instances of bar, but only if the second two are each preceded by foo.

i.e. given the text:

barfoobarbazfoobar
barfoobarbazbazbar
barbazbarbazfoobar

It will match the first line only.

5
  • But you can exchange the first lookbehind with \zs, i.e., this should also work: \vfoo\zsbar.*(foo)@<=bar. May 22, 2018 at 6:48
  • @KarlYngveLervåg Good point. I've edited to make clearer the distinction, and to use examples where \zs cannot be substituted in at all.
    – Rich
    May 22, 2018 at 8:46
  • So, for my understanding, \zs and \ze can be replaced with look around regex patterns, and they're more powerful, right? More powerful cause they can be used more than once and can be grouped with \(\). And also because they work like perl's look around regex. Anything mistaken?
    – 3N4N
    Mar 20, 2019 at 10:36
  • 1
    @klaus That sounds about right to me (although I'm no expert). Note that you should use \zs/\ze when you can, though, because they're faster than look-arounds.
    – Rich
    Mar 29, 2019 at 9:04
  • Understood. And \zs and \ze are obviously more intuitive. Thanks for the explanations.
    – 3N4N
    Mar 29, 2019 at 9:24

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.