Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions lua/markview/autocmds.lua
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,52 @@ autocmds.optionSet = function (args)
---|fE
end

---@diagnostic disable-next-line: undefined-field
autocmds.resize_timer = vim.uv.new_timer();

--[[ Window resized. ]]
---@param args vim.api.keyset.create_autocmd.callback_args
autocmds.resize = function (args)
---|fS

local state = require("markview.state");
local actions = require("markview.actions");

autocmds.resize_timer:stop();

--[[
`wrap` indents are placed at the wrap columns of the width they were
rendered at, so every buffer on screen needs to be rendered again for the
new width.

Resizes arrive in bursts(a mouse drag or a per-step terminal resize), so
the renders are *debounced* the same way cursor movements are.
]]
autocmds.resize_timer:start(50, 0, vim.schedule_wrap(function ()
if not state.enabled() then
return;
end

for _, buffer in ipairs(state.get_enabled_buffers()) do
--[[
Wrap indents are *window dependent*, so only buffers that are
displayed somewhere need to be rendered again.
]]
if vim.api.nvim_buf_is_valid(buffer) and vim.fn.win_findbuf(buffer)[1] then
if buffer == state.get_splitview_source() then
actions.splitview_render();
elseif actions.in_preview_mode() then
actions.render(buffer);
else
actions.clear(buffer);
end
end
end
end));

---|fE
end

---@diagnostic disable-next-line: undefined-field
autocmds.cursor_timer = vim.uv.new_timer();

Expand Down Expand Up @@ -507,6 +553,12 @@ autocmds.setup = function ()
callback = autocmds.optionSet
});

vim.api.nvim_create_autocmd({
"WinResized", "VimResized"
}, {
callback = autocmds.resize
});

vim.api.nvim_create_autocmd({
"BufAdd", "BufEnter",
"BufWinEnter"
Expand Down
35 changes: 30 additions & 5 deletions lua/markview/wrap.lua
Original file line number Diff line number Diff line change
Expand Up @@ -208,17 +208,42 @@ wrap.fine_wrap = function (buffer, win, row, ns, indent)
return;
end

local reallen = vim.fn.strdisplaywidth(
vim.api.nvim_buf_get_lines(buffer, row, row + 1, false)[1] or ""
);
local line = vim.api.nvim_buf_get_lines(buffer, row, row + 1, false)[1] or "";

--[[
IMPORTANT: `wrapcol`(from `virtcol2col()`) is a **byte index**, while
`strdisplaywidth()` returns a **display width**.

And, while `wrap` & `breakindent` are set, Neovim's `strdisplaywidth()` &
`virtcol()` are *inflated*(`72` spaces in a `40` column window measure as
`104` cells). Comparing one against the other made the
*"No enough text after indent"* check miss the last wrap of a line, so an
indent was inserted **before the line's last character**,

```
> quoted quoted quote▋ d
```

Both checks below therefore stay in **byte space**(`#line`) & require more
than a single character to be left after the wrap point.
]]
local line_len = #line;

--- Index(1-based) of the last byte that isn't just trailing whitespace.
local last_char = line_len;

while last_char > 0 and line:sub(last_char, last_char):match("%s") do
last_char = last_char - 1;
end

local virtlen = require("markview.utils").virt_len(indent);
local wraps = math.ceil(end_vcol / win_width);

for w = 1, wraps, 1 do
local wrapcol = vim.fn.virtcol2col(win, row + 1, (win_width * w) + 1);

if wrapcol >= end_vcol or wrapcol >= reallen then
-- Out of bounds.Or at end of line.
if wrapcol > line_len or wrapcol >= last_char then
-- Out of bounds. Or only the line's last character is left.
break;
elseif wrapcol + virtlen >= end_vcol then
-- No enough text after indent.
Expand Down
30 changes: 30 additions & 0 deletions test/minimal_init.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
--[[
Minimal init for smoke testing this plugin from a checkout(no `lazy.nvim`),

```sh
nvim -u test/minimal_init.lua test/wrap_tail_indent.md
```

It only enables the plugin, the wrap related options & the preview/hybrid
mode settings the wrapping issues are reported with.
]]

local root = vim.fn.fnamemodify(debug.getinfo(1, "S").source:sub(2), ":p:h:h");

vim.opt.runtimepath:prepend(root);

vim.opt.number = true;
vim.opt.termguicolors = true;
vim.opt.laststatus = 3;

-- Text wrapping, as reported.
vim.opt.wrap = true;
vim.opt.linebreak = true;
vim.opt.breakindent = true;

require("markview").setup({
preview = {
hybrid_modes = { "n", "no", "c" },
linewise_hybrid_mode = true,
},
});
60 changes: 60 additions & 0 deletions test/wrap_tail_indent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<!--
Manual checks for the wrap indent fixes (`wrap.fine_wrap()` and the
`WinResized`/`VimResized` re-render).

Run it from a checkout of this plugin,

```sh
nvim -u test/minimal_init.lua test/wrap_tail_indent.md
```

`test/minimal_init.lua` enables `wrap`/`linebreak`/`breakindent` & the
hybrid mode settings, so you only have to,

1. Narrow the window to ~40 columns (`:vertical resize 40`).
2. Put the cursor on the last line, so the lines above it render.
3. Check that no continuation row ends with a stray border(`-`) & that
the indent never splits the last word(`... quote- d`).
4. Widen back to ~100 columns. No indent may stay behind mid-line.

Every quoted/list line is padded to its stated byte length(36 & 46 are
the text widths of a 40 & 50 column window, so all of them wrap).
-->

# Wrap indent checks

## Block quotes

> case A: 72 bytes, a trailing space. lorem ipsum dolor sit amet consec

> case B: 72 bytes, no trailing space. lorem ipsum dolor sit amet consec

> case C: 73 bytes, wraps one character onto the last row. lorem ipsum do

> case D: 86 bytes, wraps one character onto the last row. lorem ipsum dolor sit ametx

## List items

- case E: 86 bytes, list item. lorem ipsum dolor sit amet consectetur adipiscing elit

- case F: 90 bytes, nested list item. lorem ipsum dolor sit amet consectetur adipiscing

- [ ] case G: 88 bytes, checkbox item. lorem ipsum dolor sit amet consectetur adipiscing

## Controls (must render exactly as before)

# A heading that is long enough to wrap in a narrow window

Plain paragraph, wrapped by Neovim itself, no indent is added here: lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua

| column a | column b | column c |
|---|---|---|
| lorem ipsum dolor sit am | x | y |

```lua
local x = 1 -- lorem ipsum dolor sit amet consectetur a
```

---

> case H: 71 bytes, no trailing space. lorem ipsum dolor sit amet conse