From d9aa06eed8e38f45f6f60158a0dc5d25717a91dd Mon Sep 17 00:00:00 2001 From: jreidx Date: Tue, 2 Jun 2026 18:05:51 -0400 Subject: [PATCH] feat(extmark): virt_lines_overflow "wrap" and "auto" Problem: Extmark has support for horizontal scrolling and truncating, but not wrapping. Solution: Extend virt_lines_overflow flags to support "wrap" and "auto" based on proposed changes in #18282. --- runtime/doc/api.txt | 3 + runtime/doc/news.txt | 3 + runtime/lua/vim/_meta/api.gen.lua | 2 + runtime/lua/vim/_meta/api_keysets.gen.lua | 2 +- src/nvim/api/extmark.c | 12 +- src/nvim/api/keysets_defs.h | 2 +- src/nvim/decoration.c | 83 ++++++- src/nvim/decoration_defs.h | 11 +- src/nvim/drawline.c | 51 +++- test/functional/api/extmark_spec.lua | 44 ++++ test/functional/ui/decorations_spec.lua | 285 ++++++++++++++++++++++ 11 files changed, 473 insertions(+), 25 deletions(-) diff --git a/runtime/doc/api.txt b/runtime/doc/api.txt index 44c8b036c9..1df7571476 100644 --- a/runtime/doc/api.txt +++ b/runtime/doc/api.txt @@ -3332,6 +3332,9 @@ nvim_buf_set_extmark({buf}, {ns_id}, {line}, {col}, {opts}) • "trunc": truncate virtual lines on the right (default). • "scroll": virtual lines can scroll horizontally with 'nowrap', otherwise the same as "trunc". + • "wrap": virtual lines can wrap onto extra lines. + • "auto": virtual lines wrap with 'wrap' and scroll + horizontally with 'nowrap'. • virt_text : *virtual-text* to link to this mark. A list of `[text, highlight]` tuples, each representing a text chunk with specified highlight. `highlight` element can either be diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 7bea2f2cad..0d7aa7fc8f 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -127,6 +127,9 @@ API • |nvim_get_option_info2()| • |nvim_get_option_value()| • |nvim_set_option_value()| +• |nvim_buf_set_extmark()| `virt_lines_overflow` accepts "wrap" to enable + wrapping onto extra rows and "auto" which enables horizontal scrolling when + 'nowrap' is set and wrapping when 'wrap' is set. BUILD diff --git a/runtime/lua/vim/_meta/api.gen.lua b/runtime/lua/vim/_meta/api.gen.lua index 384bbc0add..426b08451c 100644 --- a/runtime/lua/vim/_meta/api.gen.lua +++ b/runtime/lua/vim/_meta/api.gen.lua @@ -696,6 +696,8 @@ function vim.api.nvim_buf_line_count(buf) end --- - "trunc": truncate virtual lines on the right (default). --- - "scroll": virtual lines can scroll horizontally with 'nowrap', --- otherwise the same as "trunc". +--- - "wrap": virtual lines can wrap onto extra lines. +--- - "auto": virtual lines wrap with 'wrap' and scroll horizontally with 'nowrap'. --- - virt_text : [](virtual-text) to link to this mark. --- A list of `[text, highlight]` tuples, each representing a --- text chunk with specified highlight. `highlight` element diff --git a/runtime/lua/vim/_meta/api_keysets.gen.lua b/runtime/lua/vim/_meta/api_keysets.gen.lua index 9c0137de66..2e4c241b70 100644 --- a/runtime/lua/vim/_meta/api_keysets.gen.lua +++ b/runtime/lua/vim/_meta/api_keysets.gen.lua @@ -441,7 +441,7 @@ error('Cannot require a meta file') --- @field virt_lines? any[] --- @field virt_lines_above? boolean --- @field virt_lines_leftcol? boolean ---- @field virt_lines_overflow? "trunc"|"scroll" +--- @field virt_lines_overflow? "trunc"|"scroll"|"wrap"|"auto" --- @field virt_text? any[] --- @field virt_text_hide? boolean --- @field virt_text_pos? "eol"|"eol_right_align"|"overlay"|"right_align"|"inline" diff --git a/src/nvim/api/extmark.c b/src/nvim/api/extmark.c index a9b9d1a29f..6e29d72ef7 100644 --- a/src/nvim/api/extmark.c +++ b/src/nvim/api/extmark.c @@ -495,6 +495,8 @@ ArrayOf(DictAs(get_extmark_item)) nvim_buf_get_extmarks(Buffer buf, Integer ns_i /// - "trunc": truncate virtual lines on the right (default). /// - "scroll": virtual lines can scroll horizontally with 'nowrap', /// otherwise the same as "trunc". +/// - "wrap": virtual lines can wrap onto extra lines. +/// - "auto": virtual lines wrap with 'wrap' and scroll horizontally with 'nowrap'. /// - virt_text : [](virtual-text) to link to this mark. /// A list of `[text, highlight]` tuples, each representing a /// text chunk with specified highlight. `highlight` element @@ -724,10 +726,15 @@ Integer nvim_buf_set_extmark(Buffer buf, Integer ns_id, Integer line, Integer co } int virt_lines_flags = opts->virt_lines_leftcol ? kVLLeftcol : 0; + VirtLineOverflow virt_lines_overflow = kVLOverflowTrunc; if (HAS_KEY(opts, set_extmark, virt_lines_overflow)) { String str = opts->virt_lines_overflow; if (strequal("scroll", str.data)) { - virt_lines_flags |= kVLScroll; + virt_lines_overflow = kVLOverflowScroll; + } else if (strequal("wrap", str.data)) { + virt_lines_overflow = kVLOverflowWrap; + } else if (strequal("auto", str.data)) { + virt_lines_overflow = kVLOverflowAuto; } else if (!strequal("trunc", str.data)) { VALIDATE_S(false, "virt_lines_overflow", str.data, { goto error; @@ -743,7 +750,8 @@ Integer nvim_buf_set_extmark(Buffer buf, Integer ns_id, Integer line, Integer co }); int dummig; VirtText jtem = parse_virt_text(a.items[j].data.array, err, &dummig, false); - kv_push(virt_lines.data.virt_lines, ((struct virt_line){ jtem, virt_lines_flags })); + kv_push(virt_lines.data.virt_lines, + ((struct virt_line){ jtem, virt_lines_flags, virt_lines_overflow })); if (ERROR_SET(err)) { goto error; } diff --git a/src/nvim/api/keysets_defs.h b/src/nvim/api/keysets_defs.h index 399e5aa6bb..2f3049d731 100644 --- a/src/nvim/api/keysets_defs.h +++ b/src/nvim/api/keysets_defs.h @@ -48,7 +48,7 @@ typedef struct { Array virt_lines; Boolean virt_lines_above; Boolean virt_lines_leftcol; - Enum("trunc", "scroll") virt_lines_overflow; + Enum("trunc", "scroll", "wrap", "auto") virt_lines_overflow; Boolean strict; String sign_text; HLGroupID sign_hl_group; diff --git a/src/nvim/decoration.c b/src/nvim/decoration.c index 114e845eec..9dee121519 100644 --- a/src/nvim/decoration.c +++ b/src/nvim/decoration.c @@ -20,7 +20,9 @@ #include "nvim/grid.h" #include "nvim/highlight.h" #include "nvim/highlight_group.h" +#include "nvim/indent.h" #include "nvim/marktree.h" +#include "nvim/mbyte.h" #include "nvim/memory.h" #include "nvim/memory_defs.h" #include "nvim/move.h" @@ -1122,7 +1124,60 @@ bool decor_redraw_eol(win_T *wp, DecorState *state, int *eol_attr, int eol_col) static const uint32_t lines_filter[kMTMetaCount] = {[kMTMetaLines] = kMTFilterSelect }; +/// Counts the number of rows occupied by a virtual line. When skip_cells is non-NULL, sets it to +/// the cell offset where target_row begins. +/// +/// @param target_row Row relative to the virtual line that skip_cells is computed for. +/// @param skip_cells Cell offset for the target_row. Pass NULL when only the row count is needed. +/// +/// @return Number of rows occupied by the virtual line. +int decor_virt_line_rows(win_T *wp, const struct virt_line *vl, int target_row, int *skip_cells) +{ + VirtLineOverflow overflow = vl->overflow; + bool wrap = (overflow == kVLOverflowWrap) || ((overflow == kVLOverflowAuto) && wp->w_p_wrap); + int row_width = wp->w_view_width - ((vl->flags & kVLLeftcol) ? 0 : win_col_off(wp)); + if (skip_cells != NULL) { + *skip_cells = 0; + } + if (!wrap || row_width <= 0) { + return 1; + } + VirtText vt = vl->line; + buf_T *buf = wp->w_buffer; + int vcol = 0; + int row_cells = 0; + int row = 0; + for (int i = 0; i < (int)kv_size(vt); i++) { + const char *virt_str = kv_A(vt, i).text; + if (virt_str == NULL) { + continue; + } + while (*virt_str != NUL) { + int bytes_to_next_char = utfc_ptr2len(virt_str); + // Need to compute cell width due to TAB & double width cell chars + int cells = (*virt_str == TAB) + ? tabstop_padding(vcol, buf->b_p_ts, buf->b_p_vts_array) + : utf_ptr2cells(virt_str); + virt_str += bytes_to_next_char; + // Wrap before total row cell length would exceed the window size + // to protect against double width cell chars being split at the boundary + if (row_cells + cells > row_width) { + row++; + if (skip_cells != NULL && row == target_row) { + *skip_cells = vcol; + } + row_cells = 0; + } + row_cells += cells; + vcol += cells; + } + } + + return row + 1; +} + /// @param apply_folds Only count virtual lines that are not in folds. +/// @return Number of rows occupied by the virtual lines. int decor_virt_lines(win_T *wp, int start_row, int end_row, int *num_below, VirtLines *lines, bool apply_folds) { @@ -1154,13 +1209,18 @@ int decor_virt_lines(win_T *wp, int start_row, int end_row, int *num_below, Virt if (draw_row >= start_row && draw_row < end_row && (!apply_folds || !(hasFolding(wp, mrow + 1, NULL, NULL) || decor_conceal_line(wp, mrow, false)))) { - virt_lines += (int)kv_size(vt->data.virt_lines); + // Iterates over each virtual line summing number of rows. + // Rows belonging to previous line are accumulated in num_below. + for (int i = 0; i < (int)kv_size(vt->data.virt_lines); i++) { + int rows = decor_virt_line_rows(wp, &kv_A(vt->data.virt_lines, i), 0, NULL); + virt_lines += rows; + if (num_below && !above) { + (*num_below) += rows; + } + } if (lines) { kv_splice(*lines, vt->data.virt_lines); } - if (num_below && !above) { - (*num_below) += (int)kv_size(vt->data.virt_lines); - } } } vt = vt->next; @@ -1262,16 +1322,27 @@ void decor_to_dict_legacy(Dict *dict, DecorInline decor, bool hl_name, Arena *ar if (virt_lines) { Array all_chunks = arena_array(arena, kv_size(virt_lines->data.virt_lines)); int virt_lines_flags = 0; + VirtLineOverflow virt_lines_overflow = kVLOverflowTrunc; for (size_t i = 0; i < kv_size(virt_lines->data.virt_lines); i++) { virt_lines_flags = kv_A(virt_lines->data.virt_lines, i).flags; + virt_lines_overflow = kv_A(virt_lines->data.virt_lines, i).overflow; Array chunks = virt_text_to_array(kv_A(virt_lines->data.virt_lines, i).line, hl_name, arena); ADD(all_chunks, ARRAY_OBJ(chunks)); } PUT_C(*dict, "virt_lines", ARRAY_OBJ(all_chunks)); PUT_C(*dict, "virt_lines_above", BOOLEAN_OBJ(virt_lines->flags & kVTLinesAbove)); PUT_C(*dict, "virt_lines_leftcol", BOOLEAN_OBJ(virt_lines_flags & kVLLeftcol)); - PUT_C(*dict, "virt_lines_overflow", - CSTR_AS_OBJ(virt_lines_flags & kVLScroll ? "scroll" : "trunc")); + + char *overflow = "trunc"; + if (virt_lines_overflow == kVLOverflowScroll) { + overflow = "scroll"; + } else if (virt_lines_overflow == kVLOverflowWrap) { + overflow = "wrap"; + } else if (virt_lines_overflow == kVLOverflowAuto) { + overflow = "auto"; + } + + PUT_C(*dict, "virt_lines_overflow", CSTR_AS_OBJ(overflow)); priority = virt_lines->priority; } diff --git a/src/nvim/decoration_defs.h b/src/nvim/decoration_defs.h index 56e8eb634a..d24fb9d29b 100644 --- a/src/nvim/decoration_defs.h +++ b/src/nvim/decoration_defs.h @@ -29,11 +29,16 @@ typedef enum { /// Flags for virtual lines enum { kVLLeftcol = 1, ///< Start at left window edge, ignoring number column, etc. - kVLScroll = 2, ///< Can scroll horizontally with 'nowrap' - // kVLWrap = 4, }; -typedef kvec_t(struct virt_line { VirtText line; int flags; }) VirtLines; +typedef enum { + kVLOverflowTrunc, ///< Truncate with 'nowrap' + kVLOverflowScroll, ///< Scroll horizontally with 'nowrap' + kVLOverflowWrap, ///< Wrap onto extra rows + kVLOverflowAuto, ///< Scroll with 'nowrap'; wrap with 'wrap' +} VirtLineOverflow; + +typedef kvec_t(struct virt_line { VirtText line; int flags; VirtLineOverflow overflow; }) VirtLines; typedef uint16_t DecorPriority; typedef uint32_t DecorPriorityInternal; diff --git a/src/nvim/drawline.c b/src/nvim/drawline.c index 1788a278b0..daf44bb137 100644 --- a/src/nvim/drawline.c +++ b/src/nvim/drawline.c @@ -1322,6 +1322,8 @@ int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, int col_rows, b } VirtLines virt_lines = KV_INITIAL_VALUE; wlv.n_virt_lines = decor_virt_lines(wp, lnum - 1, lnum, &wlv.n_virt_below, &virt_lines, true); + // Preserving count of virt_lines for topline visibility + int total_virt_rows = wlv.n_virt_lines; wlv.filler_lines += wlv.n_virt_lines; if (lnum == wp->w_topline) { wlv.filler_lines = wp->w_topfill; @@ -1674,8 +1676,13 @@ int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, int col_rows, b const bool may_have_inline_virt = !has_foldtext && buf_meta_total(wp->w_buffer, kMTMetaInline) > 0; - int virt_line_index = -1; + bool has_virt_line = false; int virt_line_flags = 0; + // Index into virt_lines and the starting row of that line (virt_line_start_row). + // Both persist across iterations to avoid traversing previously visited rows. + int virt_line_index = 0; + int virt_line_start_row = 0; + int virt_line_skip_cells = 0; // Repeat for each cell in the displayed line. while (true) { @@ -1715,15 +1722,29 @@ int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, int col_rows, b } if (wlv.filler_todo > 0) { - int index = wlv.filler_todo - (wlv.filler_lines - wlv.n_virt_lines); - if (index > 0) { - virt_line_index = (int)kv_size(virt_lines) - index; - assert(virt_line_index >= 0); - virt_line_flags = kv_A(virt_lines, virt_line_index).flags; + // nvim_buf_set_extmark: virt_lines_overflow + int virt_rows_todo = wlv.filler_todo - (wlv.filler_lines - wlv.n_virt_lines); + if (virt_rows_todo > 0) { + int target_row = total_virt_rows - virt_rows_todo; + // Count number of rows spanned by virtual line. + // Find the virtual line containing the target row. + // Set skip cells with the same row-counting function so offset calculation stays in sync + for (; virt_line_index < (int)kv_size(virt_lines); virt_line_index++) { + int line_rows = decor_virt_line_rows(wp, &kv_A(virt_lines, virt_line_index), 0, NULL); + if (target_row < virt_line_start_row + line_rows) { + has_virt_line = true; + virt_line_flags = kv_A(virt_lines, virt_line_index).flags; + decor_virt_line_rows(wp, &kv_A(virt_lines, virt_line_index), + target_row - virt_line_start_row, + &virt_line_skip_cells); + break; + } + virt_line_start_row += line_rows; + } } } - if (virt_line_index >= 0 && (virt_line_flags & kVLLeftcol)) { + if (has_virt_line && (virt_line_flags & kVLLeftcol)) { // skip columns } else if (statuscol.draw) { // Draw 'statuscolumn' if it is set. @@ -1768,7 +1789,7 @@ int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, int col_rows, b break; } wlv.filler_todo--; - virt_line_index = -1; + has_virt_line = false; if (wlv.filler_todo == 0 && (wp->w_botfill || !draw_text)) { break; } @@ -3149,14 +3170,20 @@ end_check: } } - if (virt_line_index >= 0) { + if (has_virt_line) { + VirtLineOverflow overflow = kv_A(virt_lines, virt_line_index).overflow; + bool virt_row_scroll = (overflow == kVLOverflowScroll) + || ((overflow == kVLOverflowAuto) && !wp->w_p_wrap); + if (virt_row_scroll) { + virt_line_skip_cells = wp->w_leftcol; + } draw_virt_text_item(buf, - virt_line_flags & kVLLeftcol ? 0 : win_col_offset, + (virt_line_flags & kVLLeftcol) ? 0 : win_col_offset, kv_A(virt_lines, virt_line_index).line, kHlModeReplace, view_width, 0, - virt_line_flags & kVLScroll ? wp->w_leftcol : 0); + virt_line_skip_cells); } else if (wlv.filler_todo <= 0) { draw_virt_text(wp, buf, win_col_offset, &draw_col, wlv.row); } @@ -3205,7 +3232,7 @@ end_check: statuscol.draw = false; // don't draw status column if "n" is in 'cpo' } wlv.filler_todo--; - virt_line_index = -1; + has_virt_line = false; virt_line_flags = 0; // When the filler lines are actually below the last line of the // file, or we are not drawing text for this line, break here. diff --git a/test/functional/api/extmark_spec.lua b/test/functional/api/extmark_spec.lua index 911d9fb0bf..f335ab15a5 100644 --- a/test/functional/api/extmark_spec.lua +++ b/test/functional/api/extmark_spec.lua @@ -1738,6 +1738,50 @@ describe('API/extmarks', function() }, }, get_extmark_by_id(ns, marks[3], { details = true })) + -- verify 'wrap' is returned through get_extmark_by_id to validate flag comparison + set_extmark(ns, marks[4], 0, 0, { + priority = 0, + ui_watched = true, + virt_lines = { { { '', 'Macro' }, { '' }, { '', '' } } }, + virt_lines_overflow = 'wrap', + }) + eq({ + 0, + 0, + { + ns_id = ns, + right_gravity = true, + ui_watched = true, + priority = 0, + virt_lines = { { { '', 'Macro' }, { '' }, { '', '' } } }, + virt_lines_above = false, + virt_lines_leftcol = false, + virt_lines_overflow = 'wrap', + }, + }, get_extmark_by_id(ns, marks[4], { details = true })) + + -- verify 'auto' is returned through get_extmark_by_id to validate flag comparison + set_extmark(ns, marks[5], 0, 0, { + priority = 0, + ui_watched = true, + virt_lines = { { { '', 'Macro' }, { '' }, { '', '' } } }, + virt_lines_overflow = 'auto', + }) + eq({ + 0, + 0, + { + ns_id = ns, + right_gravity = true, + ui_watched = true, + priority = 0, + virt_lines = { { { '', 'Macro' }, { '' }, { '', '' } } }, + virt_lines_above = false, + virt_lines_leftcol = false, + virt_lines_overflow = 'auto', + }, + }, get_extmark_by_id(ns, marks[5], { details = true })) + set_extmark(ns, marks[4], 0, 0, { cursorline_hl_group = 'Statement' }) eq({ 0, diff --git a/test/functional/ui/decorations_spec.lua b/test/functional/ui/decorations_spec.lua index 95c9421595..d376e4161f 100644 --- a/test/functional/ui/decorations_spec.lua +++ b/test/functional/ui/decorations_spec.lua @@ -6893,6 +6893,291 @@ if (h->n_buckets < new_n_buckets) { // expand ]]) end) + it('virt_lines_overflow=wrap', function() + command('set wrap signcolumn=yes') + insert('line1\nline2\nline3\n') + api.nvim_buf_set_extmark(0, ns, 0, 0, { + virt_lines = { + { + { + string.rep( + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean non felis dapibus, semper sapien vitae, fringilla orci.', + 5 + ), + 'Special', + }, + }, + }, + virt_lines_overflow = 'wrap', + }) + screen:expect([[ + {7: }{16:rem ipsum dolor sit amet, consectetur adipiscing}| + {7: }{16: elit. Aenean non felis dapibus, semper sapien v}| + {7: }{16:itae, fringilla orci.Lorem ipsum dolor sit amet,}| + {7: }{16: consectetur adipiscing elit. Aenean non felis d}| + {7: }{16:apibus, semper sapien vitae, fringilla orci.Lore}| + {7: }{16:m ipsum dolor sit amet, consectetur adipiscing e}| + {7: }{16:lit. Aenean non felis dapibus, semper sapien vit}| + {7: }{16:ae, fringilla orci.} | + {7: }line2 | + {7: }line3 | + {7: }^ | + | + ]]) + feed('') + screen:expect([[ + {7: }{16: elit. Aenean non felis dapibus, semper sapien v}| + {7: }{16:itae, fringilla orci.Lorem ipsum dolor sit amet,}| + {7: }{16: consectetur adipiscing elit. Aenean non felis d}| + {7: }{16:apibus, semper sapien vitae, fringilla orci.Lore}| + {7: }{16:m ipsum dolor sit amet, consectetur adipiscing e}| + {7: }{16:lit. Aenean non felis dapibus, semper sapien vit}| + {7: }{16:ae, fringilla orci.} | + {7: }line2 | + {7: }line3 | + {7: }^ | + {1:~ }| + | + ]]) + feed('') + screen:expect([[ + {7: }{16:itae, fringilla orci.Lorem ipsum dolor sit amet,}| + {7: }{16: consectetur adipiscing elit. Aenean non felis d}| + {7: }{16:apibus, semper sapien vitae, fringilla orci.Lore}| + {7: }{16:m ipsum dolor sit amet, consectetur adipiscing e}| + {7: }{16:lit. Aenean non felis dapibus, semper sapien vit}| + {7: }{16:ae, fringilla orci.} | + {7: }line2 | + {7: }line3 | + {7: }^ | + {1:~ }|*2 + | + ]]) + end) + + it('virt_lines_overflow=wrap with 2 cell character', function() + command('set wrap signcolumn=yes') + insert('line1\nline2\nline3\n') + api.nvim_buf_set_extmark(0, ns, 0, 0, { + virt_lines = { + { { string.rep('12345678古', 5), 'Special' } }, + }, + virt_lines_overflow = 'wrap', + }) + screen:expect([[ + {7: }line1 | + {7: }{16:12345678古12345678古12345678古12345678古12345678}| + {7: }{16:古} | + {7: }line2 | + {7: }line3 | + {7: }^ | + {1:~ }|*5 + | + ]]) + feed('') + + screen:expect([[ + {7: }{16:12345678古12345678古12345678古12345678古12345678}| + {7: }{16:古} | + {7: }line2 | + {7: }line3 | + {7: }^ | + {1:~ }|*6 + | + ]]) + + feed('') + screen:expect([[ + {7: }{16:古} | + {7: }line2 | + {7: }line3 | + {7: }^ | + {1:~ }|*7 + | + ]]) + end) + + it('virt_lines_overflow=wrap with tabs', function() + command('set wrap signcolumn=yes') + api.nvim_buf_set_extmark(0, ns, 0, 0, { + virt_lines = { + { { string.rep('1234\t\t\t5\t6\t78古', 10), 'Special' } }, + }, + virt_lines_overflow = 'wrap', + }) + screen:expect([[ + {7: }^ | + {7: }{16:1234 5 6 78古1234}| + {7: }{16: 5 6 78古1234}|*8 + {7: }{16: 5 6 78古} | + | + ]]) + end) + + it('virt_lines_overflow=wrap with 2 cell character at the boundary', function() + insert('x') + api.nvim_buf_set_extmark(0, ns, 0, 0, { + virt_lines = { { { string.rep('a', 49) .. string.rep('古', 4), 'Special' } } }, + virt_lines_overflow = 'wrap', + }) + screen:expect([[ + ^x | + {16:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa }| + {16:古古古古} | + {1:~ }|*8 + | + ]]) + api.nvim_buf_clear_namespace(0, ns, 0, -1) + screen:try_resize(13, 10) + api.nvim_buf_set_extmark(0, ns, 0, 0, { + virt_lines = { + { { 'AB', 'Special' } }, + { { string.rep('古', 13), 'Special' } }, + }, + virt_lines_overflow = 'wrap', + }) + screen:expect([[ + ^x | + {16:AB} | + {16:古古古古古古 }|*2 + {16:古} | + {1:~ }|*4 + | + ]]) + end) + + it('virt_lines_overflow=wrap with nowrap', function() + command('set nowrap signcolumn=yes') + insert(string.rep('abcdefghij', 6) .. 'line1\nline2\nline3\n') + api.nvim_buf_set_extmark(0, ns, 0, 0, { + virt_lines = { + { { string.rep('12345678古', 5), 'Special' } }, + }, + virt_lines_overflow = 'wrap', + }) + feed('ggzlzl') + screen:expect([[ + {7: }^cdefghijabcdefghijabcdefghijabcdefghijabcdefghij| + {7: }{16:12345678古12345678古12345678古12345678古12345678}| + {7: }{16:古} | + {7: }ne2 | + {7: }ne3 | + {7: } | + {1:~ }|*5 + | + ]]) + end) + + it('virt_lines_overflow=auto with wrap', function() + command('set wrap signcolumn=yes') + insert('line1\nline2\nline3\n') + api.nvim_buf_set_extmark(0, ns, 0, 0, { + virt_lines = { + { { string.rep('Thequickbrownfoxjumpsoverthelazydog. The quick brown fox jumps over the lazy dog', 20), 'Special' } }, + }, + virt_lines_overflow = 'auto', + }) + screen:expect([[ + {7: }{16:rown fox jumps over the lazy dogThequickbrownfox}| + {7: }{16:jumpsoverthelazydog. The quick brown fox jumps o}| + {7: }{16:ver the lazy dogThequickbrownfoxjumpsoverthelazy}| + {7: }{16:dog. The quick brown fox jumps over the lazy dog}| + {7: }{16:Thequickbrownfoxjumpsoverthelazydog. The quick b}| + {7: }{16:rown fox jumps over the lazy dogThequickbrownfox}| + {7: }{16:jumpsoverthelazydog. The quick brown fox jumps o}| + {7: }{16:ver the lazy dog} | + {7: }line2 | + {7: }line3 | + {7: }^ | + | + ]]) + end) + + it('virt_lines_overflow=auto and nowrap scrolls horizontally', function() + command('set nowrap signcolumn=yes') + insert('abcdefghijklmnopqrstuvwxyz') + api.nvim_buf_set_extmark(0, ns, 0, 0, { + virt_lines = { + { { '12α口β̳γ̲=', 'Special' }, { '❤️345678', 'Special' } }, + { { '123\t45\t678', 'NonText' } }, + }, + virt_lines_overflow = 'auto', + }) + screen:expect([[ + {7: }abcdefghijklmnopqrstuvwxy^z | + {7: }{16:12α口β̳γ̲=❤️345678} | + {7: }{1:123 45 678} | + {1:~ }|*8 + | + ]]) + feed('zl') + screen:expect([[ + {7: }bcdefghijklmnopqrstuvwxy^z | + {7: }{16:2α口β̳γ̲=❤️345678} | + {7: }{1:23 45 678} | + {1:~ }|*8 + | + ]]) + end) + + it('virt_lines_overflow=wrap with virt_lines_above', function() + command('set wrap signcolumn=yes') + insert('line1\nline2\nline3\n') + feed('2gg') + api.nvim_buf_set_extmark(0, ns, 1, 0, { + virt_lines = { + { { string.rep('Lorem ipsum dolor sit amet, consectetur. ', 4), 'Special' } }, + }, + virt_lines_overflow = 'wrap', + virt_lines_above = true, + }) + screen:expect([[ + {7: }line1 | + {7: }{16:Lorem ipsum dolor sit amet, consectetur. Lorem i}| + {7: }{16:psum dolor sit amet, consectetur. Lorem ipsum do}| + {7: }{16:lor sit amet, consectetur. Lorem ipsum dolor sit}| + {7: }{16: amet, consectetur. } | + {7: }^line2 | + {7: }line3 | + {7: } | + {1:~ }|*3 + | + ]]) + end) + + it('virt_lines_overflow=wrap with wrapped rows for statuscolumn', function() + command('set wrap statuscolumn=[%{v:lnum}]') + insert('line1\nline2\nline3\n') + feed('2gg') + api.nvim_buf_set_extmark(0, ns, 0, 0, { + virt_lines = { + { { 'BELOW ' .. string.rep('Lorem ipsum dolor sit amet, consectetur. ', 4), 'Special' } }, + }, + virt_lines_overflow = 'wrap', + }) + api.nvim_buf_set_extmark(0, ns, 1, 0, { + virt_lines = { + { { 'ABOVE ' .. string.rep('Lorem ipsum dolor sit amet, consectetur. ', 4), 'Special' } }, + }, + virt_lines_overflow = 'wrap', + virt_lines_above = true, + }) + screen:expect([[ + {8:[1]}line1 | + {8:[1]}{16:BELOW Lorem ipsum dolor sit amet, consectetur. }| + {8:[1]}{16:Lorem ipsum dolor sit amet, consectetur. Lorem }| + {8:[1]}{16:ipsum dolor sit amet, consectetur. Lorem ipsum }| + {8:[1]}{16:dolor sit amet, consectetur. } | + {8:[2]}{16:ABOVE Lorem ipsum dolor sit amet, consectetur. }| + {8:[2]}{16:Lorem ipsum dolor sit amet, consectetur. Lorem }| + {8:[2]}{16:ipsum dolor sit amet, consectetur. Lorem ipsum }| + {8:[2]}{16:dolor sit amet, consectetur. } | + {8:[2]}^line2 | + {8:[3]}line3 | + | + ]]) + end) + it('does not show twice if end_row or end_col is specified #18622', function() screen:try_resize(50, 8) insert([[