1

I can grab the number of lines of a buffer with line("$"), but this does not take into account if my terminal is small and I have set wrap and some lines wrap.

I wonder if it is possible to query this (greater) line count value? Basically it would be the # of times that you could gj from the top of the file (plus one). That's not a practical way to calculate it, neither is doing math based on the window dimension winwidth() and the buffer contents themselves.

  • for my application (sizing for small files to get their windows to auto-size upon opening) i actually can use gj in a loop to scan the height. it's very very ugly, but it will not be slow because i will always terminate it within 200 steps – Steven Lu Aug 7 '15 at 4:17
  • 2
    Don't count (pun!) too much on any number of wrapped lines you may find, as it will change if you modify a Vim setting such as showbreak, number (with relativenumber), foldcolumn (or when using signs). – VanLaser Aug 7 '15 at 7:13
1

It is better to employ strdisplaywidth() for this job. It takes things like number, fold and sign columns into account as well as &linebreak, &showbreak and &breakindent option effects. Problem is that you still have to somehow calculate width of the window available for text (i.e. exclude widths of number, fold and sign columns): strdisplaywidth() returns how many display cells are occupied, but it does not tell you how many display cells there are. The following code should do this:

function WinTextWidth()
    let winwidth = winwidth(0)
    let winwidth -= (max([len(line('$')), &numberwidth]) * (&number || &relativenumber))
    let winwidth -= &foldcolumn
    redir => signs
    execute 'silent sign place buffer=' . bufnr('%')
    redir END
    if signs !~# '^\n---[^\n]*\n$'
        let winwidth -= 2
    endif
    return winwidth
endfunction
function LineCount(...)
    let startlnr = get(a:000, 0, 1)
    let endlnr = get(a:000, 1, line('$'))
    let numlines = 0
    let winwidth = WinTextWidth()
    for lnr in range(startlnr, endlnr)
        let lwidth = strdisplaywidth(getline(lnr))
        let numlines += max([(lwidth - 1) / winwidth + 1, 1])
    endfor
    return numlines
endfunction

strdisplaywidth(getline(lnr)) may also be replaced with virtcol([lnr, '$']) - 1, this should be slightly faster and more compatible with older Vim versions.

| improve this answer | |
  • Thanks, this has prompted me to learn about showbreak and breakindent which do work quite nicely along with linebreak to make thin windows much more readable! – Steven Lu Apr 3 '16 at 17:00
  • I think this is perhaps possible to enhance to use for a window which is not the current window. But for right now my script does still use the technique of moving around with wincmd j and so forth so these functions are sufficient. – Steven Lu Apr 3 '16 at 21:05
  • This incorrectly counts lines that are empty (adding zero to numlines). I will edit when I have a code fix. – Steven Lu Apr 3 '16 at 21:52
  • I have applied the code change that makes it work well now. It does appear to (in my very few test cases) correctly determine the height. However I am really confused about how it is able to magically take into account the linebreak contributions. It must internally be aware of the window width and using that... it would be probably much more sensible if this function just went and did that division for us instead of giving us this otherwise useless column count. – Steven Lu Apr 3 '16 at 22:18
  • @StevenLu This function uses exactly the same code that is used by Vim to draw line on screen (you may see that f_linebreak from eval.c and win_line from screen.c both use win_lbr_chartabsize, though f_linebreak has a few more layers on top of that). And no, that internal function does not return a number of lines. Basically by introducing strdisplaywidth() Bram provided access to some rather internal function and I have no idea how much work it would be if this function did not exist: it existed at least since 7.0.1. – ZyX Apr 4 '16 at 1:57
3

The issue is that vim does not store the wrapped lines, it only displays those you have here. If you really want to do it you can do it a little bit more efficiently than using gj. The idea is to go to the beginning of the page, go to the end of the displayed window and use winline(), which gives you the displayed line number of the window and then scroll the page with z+.

function! CountLine()
  normal! gg
  let l:count = 0
  while 1
    normal! L$
    let l:count = l:count + winline()
    if line('.') == line('$')
      break
    else
      normal! z+
    endif
  endwhile
  return l:count
endfunction
| improve this answer | |
1

I find that the live buffer of the window is available in the python interface. This means I can use python to iterate the lines and compute for myself how many times each line wraps over, and then this can be summed to obtain an estimate of what the real working height of the buffer in the window is.

Here's an example of how you'd extricate some of this data from vimscript. I wrote this code here to inspect the data structures available inside vim.windows:

python << EOF                                                                
for win in vim.windows:                                                          
    print ", ".join([str(x) for x in [win.col, win.row, win.width, win.height]]);
    windir = dir(win)                                                            
    print 'dir: ' + str(windir)                                                  
    for method in windir:                                                        
        attr = getattr(win, method)                                              
        if method == 'buffer':                                                   
            print '    buffer: length ' + str(len(attr))                         
            print '    buffer[0]: ' + str(attr[0])                               
        elif method[0] != '_':                                                   
            print '    ' + method + ': ' + str(attr)                             
EOF

Some rather annoying things to keep in mind:

  1. this height i'm talking about depends on the width of the vim window (when wrapping)
  2. have to correctly deal with tabs and other chars which take up more space. I am unable to think of anything other than the Tab that has these properties (and even if I did use Unicode characters which I rarely do, I'm pretty sure i've never seen any that are supposed to take up more than one column), and with Tab the semantics seem pretty simple, it will take up exactly &tabstop columns.
  3. the width of the window includes the numberline. The numberline's width is dependent on the line count of the buffer, but also at least as wide as &numberwidth.
  4. the width of the window also includes the sign gutter. Luckily this is only ever a width of 0 or 2, right now all i can figure out is we need to use redir with sign place [bufnr] to find out for sure about the presence of this sign column.
  5. the type of wrapping (set linebreak) affects this (Maybe we replicate this wrap-by-word on the python side...)

There may be yet other factors that lead Vim to need more or less space than what we would naively compute in this way. Hopefully linebreak is one of the few remaining Vim features (maybe, hopefully it's the only one) that affect this. I can confirm that conceal does not interfere with wrapping offsets.

Obtaining the height this way should be many times more performant than using vimscript techniques, which invariably result in a lot more Vim frontend logic to execute.

| improve this answer | |

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.