2

When I enable the wrap option, the first line of the viewport remains fixed and the lines below it are all shunted down as they are wrapped. This means the cursor line jumps down too, which is distracting for my use case (I need to rapidly toggle between wrap and nowrap using a shortcut key).

Is there a way to prevent this, so that the viewport is anchored on the current line and any newly wrapped text above this is line is shunted up?

I found winline() which gives me the cursor row number in the viewport, but there is no corresponding setwinline() function. Furthermore, you can't just scroll up or down by the change in winline() because some of those lines may be wrapped.

Related: scroll line to specific screen line

2
  • Does the solution you found work with 'smoothscroll' set?
    – Rich
    Commented Sep 18, 2023 at 20:05
  • I'm using vim 8.2, so this option isn't available. Although I could upgrade myself, I'd like to support older versions as this feature is part of a plugin: github.com/rogerkeays/vimjournal Commented Sep 20, 2023 at 14:16

1 Answer 1

1

It is not always possible to achieve the desired output since the first window row is always the start of a line (if smoothscroll is not set)

Suppose you have that text (with cursor at row 2):

 2 line 2 foo bar tom foo bar tom foo
>3 line 3 foo bar tom foo bar tom foo
 4 line 4 foo bar tom foo bar tom foo

When you toggle wrap:

You can have (with cursor at row 3):

 2 line 2 foo bar tom
   foo bar tom foo
>3 line 3 foo bar tom
   foo bar tom foo
 4 line 4 foo bar tom
   foo bar tom foo

or (with cursor at row 1):

>3 line 3 foo bar tom
   foo bar tom foo
 4 line 4 foo bar tom
   foo bar tom foo

But not (with cursor at row 2 as you which):

   foo bar tom foo
>3 line 3 foo bar tom
   foo bar tom foo
 4 line 4 foo bar tom
   foo bar tom foo

Another limitation is that the start of the text is always at row 1

Suppose you start with (cursor at row 6)

 2 line 2 foo bar tom
   foo bar tom foo
 3 line 3 foo bar tom
   foo bar tom foo
 4 line 4 foo bar tom
>  foo bar tom foo

When you toggle wrap the best you can have is (with cursor at row 4):

 1 line 1 foo bar tom foo bar tom foo
 2 line 2 foo bar tom foo bar tom foo
 3 line 3 foo bar tom foo bar tom foo
>4 line 4 foo bar tom foo bar tom foo

That being said here is an approximation of what you would like to have:

function GetWinWidth()
  let ret = winwidth(0)

  if &signcolumn !=# 'no'
    let ret = ret - 2
  endif

  if &number
    let ret = ret - max([&numberwidth, float2nr(log10(line('$'))) + 2])
  endif

  return ret
endfunction

function WrapRowDelta(line1, line2)
  " This is an approximation that could be improved :-)
  let ret = 0
  let max_width = GetWinWidth()
  for i in range(a:line1, a:line2 - 1)
    if foldclosed(i) < 0
      let ret = ret + float2nr(ceil(str2float(len(getline(i)) + 1) / max_width))
    elseif foldclosed(i) == i
      let ret = ret + 1
    endif
  endfor
  return ret
endfunction

function! NoWrapRowDelta(line1, line2)
  " return a:line2 - a:line1
  let ret = 0
  for i in range(a:line1, a:line2 - 1)
    if foldclosed(i) < 0
      let ret = ret + 1
    elseif foldclosed(i) == i
      let ret = ret + 1
    endif
  endfor
  return ret
endfunction

function! CorrectCursorScroll()
  if &wrap 
    " Estimation of the previous space:
    let nonwrap_row_delta = NoWrapRowDelta(line('w0'), line('.'))

    " Value of the current space
    let wrap_row_delta = screenpos(0, line('.'), col('.')).row - 1
    while wrap_row_delta > nonwrap_row_delta
      exe "normal! \<C-e>"
      let r = screenpos(0, line('.'), col('.')).row - 1
      if wrap_row_delta == r
        break
      endif
      let wrap_row_delta = r
    endwhile
    if wrap_row_delta < nonwrap_row_delta
      exe "normal! \<C-y>"
    endif
  else
    " Estimation of the previous space:
    let wrap_row_delta = WrapRowDelta(line('w0'), line('.'))

    " Value of the current space
    let nonwrap_row_delta = NoWrapRowDelta(line('w0'), line('.'))
    while wrap_row_delta > nonwrap_row_delta
      exe "normal! \<C-y>"
      let r = NoWrapRowDelta(line('w0'), line('.'))
      if nonwrap_row_delta == r
        break
      endif
      let nonwrap_row_delta = r
    endwhile
    if wrap_row_delta < nonwrap_row_delta
      exe "normal! \<C-e>"
    endif
  endif
endfunction

autocmd! OptionSet wrap call CorrectCursorScroll()
10
  • 1
    Hmm... this solution seems to have some problems with code folding. Maybe winline() can help? Commented Sep 20, 2023 at 14:26
  • Thanks for the feedback :-) If you could give me more information about the code folding problem you have I would see if I can improve it ;-) Commented Sep 20, 2023 at 14:35
  • 1
    I see indeed an obvious problem (I'll try to fix it when I have some time). I'll keep you posted :-) Commented Sep 20, 2023 at 14:41
  • When there is at least one visible closed fold above the cursor, the cursor line will keep moving down the viewport when you toggle wrap, until it hits the bottom. If there are closed folds below the cursor, or if the fold above is scrolled out of view it works fine. Commented Sep 20, 2023 at 14:44
  • I have provided a new version that should work better with fold. Let me know if it works for you :-) Commented Sep 20, 2023 at 15:26

Your Answer

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

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