4

I'm trying to write a plugin that highlights overused words.

After much effort I managed to write a function (and a command, :Ditto) that will highlight the most used word in the file or in a specific range. So I can select a paragraph and call that function and the most frequent word in that paragraph will be highlighted.

This is the result of that first attempt.

Now I want to be able to call that function/command once for every paragraph in the file. So instead of selecting each paragraph and calling it I'd be able to do it just once and the most frequent word in each paragraph would be highlighted in that paragraph.

How could I go about that?


PS: Any other ideas/suggestions/recommendations are more than welcome. I know nearly nothing about vimscript. Also it would be great if besides running the function once for each paragraph the plugin could do it once per sentence and maybe other scopes… and after that I hope to be able to cycle through the most used words, so I could see the second most used word, then the third, and so on. I thought I'd start with my current problem, which is just running that function once for each paragraph, but if anybody can help with the rest that would be awesome too. Thanks! :)

  • I guess you can try macros. Find a way to reach a paragraph, either through text motions or regex patterns. Then include that one and operations to be done, in a register. Play it as a recursive macro. Done! – SibiCoder Jul 25 '16 at 5:05
  • You can put a global command in a function in vimscript just fine. Just leave out the leading :. – Tumbler41 Jul 25 '16 at 14:53
  • @Tumbler41 Ah, thank you! I was trying to wrap the whole thing into another execute "normal …", didn't realize I could just leave the colon out. Now I just have to figure out why my regex isn't matching paragraphs perfectly. – dbmrq Jul 25 '16 at 18:33
1

With the following command (for testing):

com! -range MyCom <line1>,<line2> s/^/<line1>,<line2>: /

You can pass each paragraph in a buffer to MyCom with:

g/\v.(\n\n|\n*%$)/ '{,. MyCom

It searches for the last line of a paragraph, then uses from the previous paragraph mark to that point as the range.

You'll get a single leading blank line as part of the paragraph though (if there is one).

To have a function handle a range, append the range keyword to the definition (e.g. function Foo() range).

When it's called with a range (e.g. :1,5 call Foo()) it will be invoked once, rather than once for each line. You also have access to the magic arguments a:firstline and a:lastline within the function body so you can handle the range yourself.

See :help a:firstline.

  • Nice! I thought of another option too, using gN, I added it as a new answer, it doesn't seem to have that problem with the blank lines and it selects just the text of each paragraph. Your answer has the advantage of using Vim's own definition of a paragraph with '{ though, instead of defining it again (although just for the start of the paragraph, not the end). So it could be better in some cases. – dbmrq Jul 26 '16 at 20:55
2

Ok, with some help from @Tumbler41 I arrived at this pattern:

\(^\n\|\%1l\)\s*\S\_.\{-}\n$

Which seems to match paragraphs exactly, even when they're right at the start or right at the end of a file.

So now I can do

:g/\(^\n\|\%1l\)\s*\S\_.\{-}\n$/execute "normal! vap:MyCommand\<cr>"

And it'll run that command once for every paragraph. The only caveat is that it won't work with vip, only with vap, because the match starts at at the new line right before a paragraph, so that line is all that gets selected with vip. I tried to fix that by adding \zs and \ze to the pattern, but it doesn't work properly when I run it with global (using normal vipd as a command, for instance), I don't know why. If anyone can fix that I'll gladly accept their answer. :)


Edit: Since I'm using vap anyway I changed the pattern to this:

\(^\n\|\%1l\).\_.\{-}\n$

Here's a breakdown:

  • The match must start with an empty line (^\n) or the first line (\%1l) (so it'll match the first paragraph even if there's no new line before it)

  • Then there must be anything except a new line (.)

  • Then there must be absolutely anything (\_.) as many times as necessary up until (\{-}) a new line (\n) and the end of a line ($)

The \s*\S part was necessary because otherwise, if a file had many empty lines one after the other, the pattern would find matches starting on each of those blank lines (doing n would stop at each one), and my command would run many times on the same paragraph. The \s* would match white spaces so the paragraph could be indented, and then there had to be anything but a white space \S to make sure it wasn't another empty line right after that.

A blank line isn't the same as an empty line though, and that pattern wouldn't match a paragraph that had a blank line (besides the empty line) right before it, so I changed that part to . (anything but a new line).

Note also that \n\n at the end wouldn't match a paragraph that ends right at the end of the file, without any lines below it, but \n$ does.

  • Sorry, forgot to look at the \zs thing in chat. I tried this and it worked for me on your data set: \(^\n\|\%1l\)\zs\s*\S\_.\{-}\n$ – Tumbler41 Jul 25 '16 at 21:29
  • 1
    @Tumbler41 It works when searching, but if I do :g/\(^\n\|\%1l\)\zs\s*\S\_.\{-}\n$/normal vipd it deletes the blank lines and the paragraphs are still there. It's like the global command ignores \zs and \ze. – dbmrq Jul 25 '16 at 21:39
  • Alright one more attempt: :g/\(^\n\|\%1l\)\s*\S\_.\{-}\n$/+1 execute "vip:MyCommand()\<CR>" This should find the blank lines but then go to the next character with the +1. Allowing you to start on a line with the first of the paragraph text. – Tumbler41 Jul 25 '16 at 21:45
  • @Tumbler41 Ah, now we're talking! There's still a tiny problem, though… if the file starts with a single line of text followed by empty lines, it'll skip that first line and do vip in an empty line. I can work around that by doing two passes, though, one for just the first paragraph without the +1 and one for the rest of the file. – dbmrq Jul 25 '16 at 22:31
  • 1
    @nobe4 There you go. :) – dbmrq Jul 26 '16 at 9:53
0

I already answered this question with one approach to solve it, and @Antony's answer is very nice too, but I kept looking around and thought of another possibility that looks even better to me:

:g/\(^\n\|\%1l\)\_s*\zs\S\_.\{-}\ze\n$/execute "normal! gN:MyCommand\<cr>"

That pattern seems to perfectly match just the text of each paragraph, and then gN visually selects each match and runs the command with it.


At first I was trying this with gn, but most of the time it seems to select two paragraphs, even if te pattern matches just one. If I do /\(^\n\|\%1l\)\_s*\zs\S\_.\{-}\ze\n$ and go like gggn<esc>ngn<esc>ngn<esc>ngn<esc>, each gn usually selects more than one paragraph and sometimes even more, I don't know why.

With gN, on the other hand, that command seems to work perfectly.


Edit: Uhm, I found a problem with this. If the paragraphs aren't indented, the command won't be run on the last one, I don't know why. It looks like the match has to start after the first character in the paragraph for some reason, or the last paragraph won't match…

So to work around that I can do this:

:g/\(^\n\|\%1l\).\zs\_.\{-}\ze\n$/execute "normal! gNV:MyCommand\<cr>"

The match starts after the first character but then the selection is turned into linewise visual mode with V.

It still works, but now it's getting a little confusing, so I'm not sure if this is the best option anymore.

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.