1

One can find several examples of formatting one sentence per line such as right here: How to set up Vim to work with one sentence per line?

The question states that: My current solution is to join a paragraph with J, then run :'<,'>s/\. /.\r/g.

However if there are hard breaks in the text, the provided solution still requires joining the paragraph (or selected text) with J before gq.

I tried unsucessfully incorporating this into the function like this among other attempts:

function! OneSentencePerLine(start, end)
   silent execute a:start.','.a:end.'join'
   silent execute a:start.','.a:end.'s/[.!?]\zs /\r/g' 
endfunction

set formatexpr=MyFormatExpr(v:lnum,v:lnum+v:count-1)

Is there a way we can do this in one shot?

1

1 Answer 1

1

One issue with your current approach is that after joining the lines, what you have is a single long line, so the range on the second command doesn't make sense anymore. You could fix that fairly easily by dropping it from the second command.

I'd also match a final \s*\ze\S in the substitute regex, both to remove all trailing/leading spaces and also to prevent it from adding an extra blank line at the end of the block (if it ends with a period.)

This 'formatexpr' is also pretty incompatible with a non-zero 'textwidth'. As Vim tries to reformat lines after the 'textwidth' is reached, whenever this ends up splitting it into two or more lines, the cursor will end up at the start of the last line and you'll be most probably inserting text at the wrong location.

You can work around that by simply setting 'textwidth' to 0 whenever you use this 'formatexpr' or by actually making it ignore a 'textwidth' setting and refuse to break lines while you're inserting text. You can do the latter by checking mode() and seeing if you're in an Insert or Replace mode. (The docs at :help 'formatexpr' detail this option.)

In any case above, this will only trigger when you use a gq command, such as gqip to reflow the current paragraph, at one sentence per line.

This is the final function I got that does the job:

function! OneSentencePerLine()
  if mode() =~# '^[iR]'
    return
  endif
  let start = v:lnum
  let end = start + v:count - 1
  execute start.','.end.'join'
  s/[.!?]\zs\s*\ze\S/\r/g
endfunction
set formatexpr=OneSentencePerLine()

It's actually possible to have it reformat the text on-the-fly and have 'textwidth' trigger it. (In that case, you might even want to use a very short 'textwidth' to trigger it early enough to catch even short sequences.) But the tricky part of that is keeping track of the cursor position and ensuring the cursor will be at the same location (on the same word, same insertion position) after reformatting the text. That's far from trivial. Possibly not something for an answer here... (But we'll see, I've seen answers with long pieces of code before. Or someone might take inspiration and write a plug-in to implement such a smart format expression.)

2
  • 1
    That is super awesome!!! Thank you so much for the detailed answer. It works perfectly. Hopefully that will be useful for the asciidoc/latex and other git users who need to keep track of sentence changes! FYI I set textwidth=0 anyways for asciidoc. (with wrap,showbreak,breakindent)
    – Boyd
    Jan 2, 2021 at 11:56
  • 1
    I've been using that ever since but changed the regex to s/[.:;?!'"”»][ ]\zs\s*\ze\S/\r/g in order to accomodate dumb, smart, and French quotes with punctionation inside or outside. I added the simple [ ] to handle that instead of \s. It seems to work great for my useage. But still open to more suggestions to handle any other edge cases.
    – Boyd
    Feb 1 at 17:51

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.