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.
To accomplish this, my plan was to write some code that returns an exact copy of the contents of the visible buffer, so that I can then calculate the empty space available by subtracting the buffer width with the amount of characters that row contains.
This is quite trivial for situations where breakindent
is disabled, as when a line spans multiple rows, the wrapped content just starts at the very start of the buffer.
But, this becomes much more tricky 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…
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.
I then also tried supporting breakindentopt:column
but that got me even more confused.
If anyone knows a better way of achieving this, 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.
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