0

Main Question

For a plugin I'm developing, I want to show a floating window somewhere within the buffer view, but want to work out exactly which rows and columns in the window are already taken up by content in the buffer so that I can place the floating window in a location where it does not obscure the code.

What I mean with that is that if I have a window that looks like the left, I am trying to determine that the window positions marked in green are free.

enter image description here

To accomplish this, my plan was to write some code that returns an exact copy of the contents of the window, so that I can then calculate the empty space available by subtracting the window width with the amount of characters that row contains.

Quickly, I ran into issues as there are many options that can be set to a multitude of values, each of which can alter the wrapping behaviour. See one such example in the extra context below.

If anyone knows a better way of achieving this (getting an exact copy of the contents of a window), or can point me in the right direction as to how I calculate the ACTUAL indentation level of a wrapping line (vim.fn.indent(lineNum) just returns the indentation level at the start of the line and doesn't care about wrapping lines), that would be nice.

Extra Context

Things become much more tricky and confusing when breakindent is enabled. The documentation doesn't match my expectations. For example, the documentation for breakindentopt:min reads

min:{n}     Minimum text width that will be kept after
                applying 'breakindent', even if the resulting
                text should normally be narrower. This prevents
                text indented almost to the right window border
                occupying lot of vertical space when broken.
                (default: 20)

But even when explicitly setting breakindentopt:min:20 and shift:0 lines start being shifted towards the left when the visible buffer width starts shrinking below a certain width, and that width doesn't seem to correlate to 20...

enter image description here

I've worked out that for some reason it starts shifting when the visible buffer width starts becoming smaller than the original indentation level + 18 characters, but where that value truly comes from, I don't understand.

Then there's many more options that are available, such as breakindentopt:column, which can influence the visible appearance of the buffer. Especially when windows get narrow, things start getting freaky.

I'll share my code that I currently have, but since it's quite a mouthful, here's a pseudocode rundown:

     - Get the line num of the first visible line of the buffer with vim.line('w0')
     - Calculate the line num of the last expected line given the window height
     - Grab those lines from the buffer
     - unwrap each line until we have enough to fill the window height
     1. to unwrap a line, we calculate the buffer_width (window width - gutter width)
     2. we take the line up to buffer_width, add it as our first line
     3. we take the rest of the line, pad it with spaces to match the right indent level, and repeat with the padded result


    -- unwrap a line into multiple lines if it spans multiple rows
    -- buffer_width: the width of the visible buffer area (window width - the gutter!)
    local function unwrap_line(line, buffer_width, indent_size)
       local unwrapped_lines = {}
       local indent_offset = 0
    
       -- perform some dark magic to determine the shift in indentation level
       -- that is applied to the rows containing the wrapped lines
    
       -- once the buffer_width shrinks below the magic_wrap_size, wrapped rows
       -- will be indented by -1 for each col smaller than the magic_wrap_size.
       -- for some reason unbeknownst to mankind this magic size is determined by taking
       -- the original indent size, and adding breakindentopt:min - 2
       local magic_wrap_size = indent_size + 20 - 2 -- 20 is the default breakindentopt:min
    
       -- if breakindent
       if vim.o.breakindent then
          -- if column is set,
          local column_value_string = vim.o.breakindentopt:match("column:([%-]?%d+)")
          if column_value_string then
             indent_offset = tonumber(column_value_string) - indent_size
          else
             -- start offset at the shift value
             local shift_value_string = vim.o.breakindentopt:match("shift:([%-]?%d+)")
             if shift_value_string then
                indent_offset = indent_offset + tonumber(shift_value_string)
             end
             -- adjust the magic_wrap_size if min is set
             local min_value_string = vim.o.breakindentopt:match("min:([%-]?%d+)")
             if min_value_string then
                magic_wrap_size = indent_size + indent_offset + tonumber(min_value_string) - 2
             end
          end
    
          -- take only negative offset values
          if magic_wrap_size > buffer_width then
             indent_offset = indent_offset + buffer_width - magic_wrap_size
          end
       else
          -- cancel out the indent size
          indent_offset = -indent_size
       end
    
       -- if a line is longer than the buffer width, split the line at buffer_width
       -- and add the first part to the unwrapped lines
       -- then pad the second part with spaces to the indent size and add it to the unwrapped lines
       -- then repeat until the line is fully unwrapped
       while #line > buffer_width do
          local first_part = string.sub(line, 1, buffer_width)
          table.insert(unwrapped_lines, first_part)
          local second_part = string.sub(line, buffer_width + 1)
          line = string.rep(" ", indent_size + indent_offset) .. second_part
       end
    
       -- insert (rest of) the line
       table.insert(unwrapped_lines, line)
       return unwrapped_lines
    end
    
    -- gets the lines that are visible in the buffer area, unwrapped
    -- (i.e. if a line spans multiple rows, each row is now it's own line)
    local function get_visible_lines_unwrapped(window_housing_buffer, visible_buffer_width, visible_buffer_height)
       -- get the 0-based index of the first line that is visible in the buffer area
       local index_first_line
       vim.api.nvim_win_call(window_housing_buffer, function()
          index_first_line = vim.fn.line("w0") - 1
       end)
       -- get the 0-based index of the line where we expect the last line to be, in the case that none of the lines are wrapped
       local expected_index_last_line = index_first_line + visible_buffer_height
       -- get the lines from the expected span
       local expected_lines = vim.api.nvim_buf_get_lines(
          vim.api.nvim_win_get_buf(window_housing_buffer),
          index_first_line,
          expected_index_last_line,
          false
       )
    
       local visible_lines = {}
       -- for each line in the expected span, unwrap the line and add it to the visible lines
       for i, line in ipairs(expected_lines) do
          -- satisfied when we have enough lines (or we could grab too many if the last line is wrapped)
          local satisfied = false
          local indent_size = 0
          vim.api.nvim_win_call(window_housing_buffer, function()
             indent_size = vim.fn.indent(index_first_line + i)
          end)
    
          -- if the indent_size
          local unwrapped_lines = unwrap_line(line, visible_buffer_width, indent_size)
          -- for each unwrapped line, add it to the visible lines
          for _, unwrapped_line in ipairs(unwrapped_lines) do
             table.insert(visible_lines, unwrapped_line)
             -- if we have enough lines, break
             if #visible_lines == visible_buffer_height then
                satisfied = true
                break
             end
          end
          if satisfied then
             break
          end
       end
       return visible_lines
    end

2
  • I won't comment on the Neovim functions, since I have basically no idea about it. But I have written some sample code to copy the content as it appears on the screen here: vi.stackexchange.com/a/5525/71 Commented Jun 19 at 15:38
  • Thanks! I'll check this out when I have time tomorrow and will report back.
    – KamielDev
    Commented Jun 19 at 19:59

1 Answer 1

0

I would use the screenchar() API and loop from the right border of the window to guess the free space.

For a given window number winnr you can loop:

  • from row: getwininfo()[winnr].winrow to getwininfo()[winnr].winrow + getwininfo()[winnr].height - 1
  • from column: getwininfo()[winnr].wincol + getwininfo()[winnr].width - 1 to getwininfo()[winnr].wincol You are searching for a screenchar() != char2nr(' ').

Here is a small code that build a list of pairs (row, room) for every row of a window:

let info = getwininfo()[winnr-1]
let rooms = []
for row in range(info.winrow, info.winrow + info.height -1)
  let room = info.width
  for c in range(info.wincol + info.width - 1, info.wincol, -1)
    if screenchar(row, c) != char2nr(' ')
      let room = info.wincol + info.width - 1 - c
      break
    endif
  endfor
  call add(rooms, [row, room])
endfor
echom rooms

Here is a lua version of the same:

info = vim.fn.getwininfo()[winnr]
rooms = {}
for row=info.winrow,info.winrow + info.height - 1 do
    room = info.width
    for c=info.wincol + info.width - 1, info.wincol, -1 do
        if (vim.fn.screenchar(row, c) ~= 32) then
          room = info.wincol + info.width - 1 - c
          break
        end
    end
    rooms[row] = {row=row, room=room}
end
2
  • Thanks! I'll check this out when I have time tomorrow and will report back.
    – KamielDev
    Commented Jun 19 at 19:59
  • Thanks for the feedback. Let us know if it fit your need :-) Commented Jun 19 at 20:03

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.