feat(decorations): support signs

Add the following options to extmarks:
  - sign_text
  - sign_hl_group
  - number_hl_group
  - line_hl_group
  - cursorline_hl_group

Note: ranges are unsupported and decorations are only applied to
start_row
This commit is contained in:
Lewis Russell 2022-01-03 12:22:13 +00:00
parent 83fc914337
commit 30e4cc3b3f
13 changed files with 589 additions and 17 deletions

View File

@ -445,6 +445,27 @@ Array nvim_buf_get_extmarks(Buffer buffer, Integer ns_id, Object start, Object e
/// - strict: boolean that indicates extmark should not be placed
/// if the line or column value is past the end of the
/// buffer or end of the line respectively. Defaults to true.
/// - sign_text: string of length 1-2 used to display in the
/// sign column.
/// Note: ranges are unsupported and decorations are only
/// applied to start_row
/// - sign_hl_group: name of the highlight group used to
/// highlight the sign column text.
/// Note: ranges are unsupported and decorations are only
/// applied to start_row
/// - number_hl_group: name of the highlight group used to
/// highlight the number column.
/// Note: ranges are unsupported and decorations are only
/// applied to start_row
/// - line_hl_group: name of the highlight group used to
/// highlight the whole line.
/// Note: ranges are unsupported and decorations are only
/// applied to start_row
/// - cursorline_hl_group: name of the highlight group used to
/// highlight the line when the cursor is on the same line
/// as the mark and 'cursorline' is enabled.
/// Note: ranges are unsupported and decorations are only
/// applied to start_row
///
/// @param[out] err Error details, if any
/// @return Id of the created/updated extmark
@ -519,10 +540,25 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, Integer line, Integer
goto error;
}
if (HAS_KEY(opts->hl_group)) {
decor.hl_id = object_to_hl_id(opts->hl_group, "hl_group", err);
if (ERROR_SET(err)) {
goto error;
struct {
const char *name;
Object *opt;
int *dest;
} hls[] = {
{ "hl_group" , &opts->hl_group , &decor.hl_id },
{ "sign_hl_group" , &opts->sign_hl_group , &decor.sign_hl_id },
{ "number_hl_group" , &opts->number_hl_group , &decor.number_hl_id },
{ "line_hl_group" , &opts->line_hl_group , &decor.line_hl_id },
{ "cursorline_hl_group", &opts->cursorline_hl_group, &decor.cursorline_hl_id },
{ NULL, NULL, NULL },
};
for (int j = 0; hls[j].name && hls[j].dest; j++) {
if (HAS_KEY(*hls[j].opt)) {
*hls[j].dest = object_to_hl_id(*hls[j].opt, hls[j].name, err);
if (ERROR_SET(err)) {
goto error;
}
}
}
@ -622,6 +658,17 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, Integer line, Integer
goto error;
}
if (opts->sign_text.type == kObjectTypeString) {
if (!init_sign_text(&decor.sign_text,
(char_u *)opts->sign_text.data.string.data)) {
api_set_error(err, kErrorTypeValidation, "sign_text is not a valid value");
goto error;
}
} else if (HAS_KEY(opts->sign_text)) {
api_set_error(err, kErrorTypeValidation, "sign_text is not a String");
goto error;
}
bool right_gravity = true;
OPTION_TO_BOOL(right_gravity, right_gravity, true);
@ -709,6 +756,7 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, Integer line, Integer
error:
clear_virttext(&decor.virt_text);
xfree(decor.sign_text);
return 0;
}

View File

@ -22,6 +22,11 @@ return {
"virt_lines_above";
"virt_lines_leftcol";
"strict";
"sign_text";
"sign_hl_group";
"number_hl_group";
"line_hl_group";
"cursorline_hl_group";
};
keymap = {
"noremap";

View File

@ -1643,3 +1643,39 @@ sctx_T api_set_sctx(uint64_t channel_id)
}
return old_current_sctx;
}
// adapted from sign.c:sign_define_init_text.
// TODO(lewis6991): Consider merging
int init_sign_text(char_u **sign_text, char_u *text)
{
char_u *s;
char_u *endp = text + (int)STRLEN(text);
// Count cells and check for non-printable chars
int cells = 0;
for (s = text; s < endp; s += utfc_ptr2len(s)) {
if (!vim_isprintc(utf_ptr2char(s))) {
break;
}
cells += utf_ptr2cells(s);
}
// Currently must be empty, one or two display cells
if (s != endp || cells > 2) {
return FAIL;
}
if (cells < 1) {
return OK;
}
// Allocate one byte more if we need to pad up
// with a space.
size_t len = (size_t)(endp - text + ((cells == 1) ? 1 : 0));
*sign_text = vim_strnsave(text, len);
if (cells == 1) {
STRCPY(*sign_text + len - 1, " ");
}
return OK;
}

View File

@ -36,6 +36,7 @@
#include "nvim/cursor.h"
#include "nvim/diff.h"
#include "nvim/digraph.h"
#include "nvim/decoration.h"
#include "nvim/eval.h"
#include "nvim/ex_cmds.h"
#include "nvim/ex_cmds2.h"
@ -5469,6 +5470,11 @@ static int buf_signcols_inner(buf_T *buf, int maximum)
FOR_ALL_SIGNS_IN_BUF(buf, sign) {
if (sign->se_lnum > curline) {
// Counted all signs, now add extmark signs
if (curline > 0) {
linesum += decor_signcols(buf, &decor_state, (int)curline-1, (int)curline-1,
maximum-linesum);
}
if (linesum > signcols) {
signcols = linesum;
if (signcols >= maximum) {
@ -5483,6 +5489,19 @@ static int buf_signcols_inner(buf_T *buf, int maximum)
}
}
if (curline > 0) {
linesum += decor_signcols(buf, &decor_state, (int)curline-1, (int)curline-1, maximum-linesum);
}
if (linesum > signcols) {
signcols = linesum;
if (signcols >= maximum) {
return maximum;
}
}
// Check extmarks between signs
linesum = decor_signcols(buf, &decor_state, 0, (int)buf->b_ml.ml_line_count-1, maximum);
if (linesum > signcols) {
signcols = linesum;
if (signcols >= maximum) {

View File

@ -875,6 +875,7 @@ struct file_buffer {
MarkTree b_marktree[1];
Map(uint32_t, uint32_t) b_extmark_ns[1]; // extmark namespaces
size_t b_virt_line_blocks; // number of virt_line blocks
size_t b_signs; // number of sign extmarks
// array of channel_id:s which have asked to receive updates for this
// buffer.

View File

@ -5,6 +5,7 @@
#include "nvim/extmark.h"
#include "nvim/highlight.h"
#include "nvim/lua/executor.h"
#include "nvim/move.h"
#include "nvim/screen.h"
#include "nvim/syntax.h"
#include "nvim/vim.h"
@ -65,8 +66,15 @@ void bufhl_add_hl_pos_offset(buf_T *buf, int src_id, int hl_id, lpos_T pos_start
void decor_redraw(buf_T *buf, int row1, int row2, Decoration *decor)
{
if ((!decor || decor->hl_id) && row2 >= row1) {
redraw_buf_range_later(buf, row1+1, row2+1);
if (row2 >= row1) {
if (decor && decor->sign_text) {
buf->b_signcols_valid = false;
changed_line_abv_curs();
}
if (!decor || decor->hl_id || decor_has_sign(decor)) {
redraw_buf_range_later(buf, row1+1, row2+1);
}
}
if (decor && kv_size(decor->virt_text)) {
@ -82,9 +90,15 @@ void decor_redraw(buf_T *buf, int row1, int row2, Decoration *decor)
void decor_remove(buf_T *buf, int row, int row2, Decoration *decor)
{
decor_redraw(buf, row, row2, decor);
if (decor && kv_size(decor->virt_lines)) {
assert(buf->b_virt_line_blocks > 0);
buf->b_virt_line_blocks--;
if (decor) {
if (kv_size(decor->virt_lines)) {
assert(buf->b_virt_line_blocks > 0);
buf->b_virt_line_blocks--;
}
if (decor_has_sign(decor)) {
assert(buf->b_signs > 0);
buf->b_signs--;
}
}
decor_free(decor);
}
@ -97,6 +111,7 @@ void decor_free(Decoration *decor)
clear_virttext(&kv_A(decor->virt_lines, i).line);
}
kv_destroy(decor->virt_lines);
xfree(decor->sign_text);
xfree(decor);
}
}
@ -136,6 +151,7 @@ bool decor_redraw_reset(buf_T *buf, DecorState *state)
{
state->row = -1;
state->buf = buf;
state->has_sign_decor = false;
for (size_t i = 0; i < kv_size(state->active); i++) {
DecorRange item = kv_A(state->active, i);
if (item.virt_text_owned) {
@ -180,9 +196,23 @@ bool decor_redraw_start(buf_T *buf, int top_row, DecorState *state)
Decoration decor = get_decor(mark);
// Exclude non-paired marks unless they contain virt_text or a sign
if (!mt_paired(mark)
&& !kv_size(decor.virt_text)
&& !decor_has_sign(&decor)) {
goto next_mark;
}
// Don't add signs for end marks as the start mark has already been added.
if (mt_end(mark) && decor_has_sign(&decor)) {
goto next_mark;
}
mtpos_t altpos = marktree_get_altpos(buf->b_marktree, mark, NULL);
if ((!mt_end(mark) && altpos.row < top_row
// Exclude start marks if the end mark position is above the top row
// Exclude end marks if we have already added the start mark
if ((mt_start(mark) && altpos.row < top_row
&& !kv_size(decor.virt_text))
|| (mt_end(mark) && altpos.row >= top_row)) {
goto next_mark;
@ -241,6 +271,10 @@ static void decor_add(DecorState *state, int start_row, int start_col, int end_r
kv_A(state->active, index) = kv_A(state->active, index-1);
}
kv_A(state->active, index) = range;
if (decor_has_sign(decor)) {
state->has_sign_decor = true;
}
}
int decor_redraw_col(buf_T *buf, int col, int win_col, bool hidden, DecorState *state)
@ -294,7 +328,8 @@ next_mark:
bool active = false, keep = true;
if (item.end_row < state->row
|| (item.end_row == state->row && item.end_col <= col)) {
if (!(item.start_row >= state->row && kv_size(item.decor.virt_text))) {
if (!(item.start_row >= state->row && kv_size(item.decor.virt_text))
&& !decor_has_sign(&item.decor)) {
keep = false;
}
} else {
@ -329,6 +364,131 @@ next_mark:
return attr;
}
void decor_redraw_signs(buf_T *buf, DecorState *state, int row,
int *num_signs, sign_attrs_T sattrs[])
{
for (size_t i = 0; i < kv_size(state->active); i++) {
DecorRange item = kv_A(state->active, i);
Decoration *decor = &item.decor;
if (!decor_has_sign(decor)) {
continue;
}
if (state->row != item.start_row) {
continue;
}
int j;
for (j = (*num_signs); j > 0; j--) {
if (sattrs[j].sat_prio <= decor->priority) {
break;
}
sattrs[j] = sattrs[j-1];
}
if (j < SIGN_SHOW_MAX) {
memset(&sattrs[j], 0, sizeof(sign_attrs_T));
sattrs[j].sat_text = decor->sign_text;
if (decor->sign_hl_id != 0) {
sattrs[j].sat_texthl = syn_id2attr(decor->sign_hl_id);
}
if (decor->number_hl_id != 0) {
sattrs[j].sat_numhl = syn_id2attr(decor->number_hl_id);
}
if (decor->line_hl_id != 0) {
sattrs[j].sat_linehl = syn_id2attr(decor->line_hl_id);
}
if (decor->cursorline_hl_id != 0) {
sattrs[j].sat_culhl = syn_id2attr(decor->cursorline_hl_id);
}
sattrs[j].sat_prio = decor->priority;
(*num_signs)++;
}
}
}
// Get the maximum required amount of sign columns needed between row and
// end_row.
int decor_signcols(buf_T *buf, DecorState *state, int row, int end_row, int max)
{
int count = 0; // count for the number of signs on a given row
int count_remove = 0; // how much to decrement count by when iterating marks for a new row
int signcols = 0; // highest value of count
int currow = -1; // current row
if (max <= 1 && buf->b_signs >= (size_t)max) {
return max;
}
if (buf->b_signs == 0) {
return 0;
}
MarkTreeIter itr[1] = { 0 };
marktree_itr_get(buf->b_marktree, 0, -1, itr);
while (true) {
mtkey_t mark = marktree_itr_current(itr);
if (mark.pos.row < 0 || mark.pos.row > end_row) {
break;
}
if ((mark.pos.row < row && mt_end(mark))
|| marktree_decor_level(mark) < kDecorLevelVisible
|| !mark.decor_full) {
goto next_mark;
}
Decoration decor = get_decor(mark);
if (!decor.sign_text) {
goto next_mark;
}
if (mark.pos.row > currow) {
count -= count_remove;
count_remove = 0;
currow = mark.pos.row;
}
if (!mt_paired(mark)) {
if (mark.pos.row >= row) {
count++;
if (count > signcols) {
signcols = count;
if (signcols >= max) {
return max;
}
}
count_remove++;
}
goto next_mark;
}
mtpos_t altpos = marktree_get_altpos(buf->b_marktree, mark, NULL);
if (mt_end(mark)) {
if (mark.pos.row >= row && altpos.row <= end_row) {
count_remove++;
}
} else {
if (altpos.row >= row) {
count++;
if (count > signcols) {
signcols = count;
if (signcols >= max) {
return max;
}
}
}
}
next_mark:
marktree_itr_next(buf->b_marktree, itr);
}
return signcols;
}
void decor_redraw_end(DecorState *state)
{
state->buf = NULL;

View File

@ -47,13 +47,18 @@ struct Decoration {
bool virt_text_hide;
bool hl_eol;
bool virt_lines_above;
// TODO(bfredl): style, signs, etc
// TODO(bfredl): style, etc
DecorPriority priority;
int col; // fixed col value, like win_col
int virt_text_width; // width of virt_text
char_u *sign_text;
int sign_hl_id;
int number_hl_id;
int line_hl_id;
int cursorline_hl_id;
};
#define DECORATION_INIT { KV_INITIAL_VALUE, KV_INITIAL_VALUE, 0, kVTEndOfLine, kHlModeUnknown, \
false, false, false, DECOR_PRIORITY_BASE, 0, 0 }
false, false, false, DECOR_PRIORITY_BASE, 0, 0, NULL, 0, 0, 0, 0 }
typedef struct {
int start_row;
@ -75,6 +80,7 @@ typedef struct {
int col_until;
int current;
int eol_col;
bool has_sign_decor;
} DecorState;
typedef struct {
@ -98,6 +104,15 @@ EXTERN bool provider_active INIT(= false);
LUA_NOREF, LUA_NOREF, LUA_NOREF, \
LUA_NOREF, -1 }
static inline bool decor_has_sign(Decoration *decor)
{
return decor->sign_text
|| decor->sign_hl_id
|| decor->number_hl_id
|| decor->line_hl_id
|| decor->cursorline_hl_id;
}
#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "decoration.h.generated.h"
#endif

View File

@ -67,7 +67,9 @@ void extmark_set(buf_T *buf, uint32_t ns_id, uint32_t *idp, int row, colnr_T col
uint8_t decor_level = kDecorLevelNone; // no decor
if (decor) {
if (kv_size(decor->virt_text) || kv_size(decor->virt_lines)) {
if (kv_size(decor->virt_text)
|| kv_size(decor->virt_lines)
|| decor_has_sign(decor)) {
decor_full = true;
decor = xmemdup(decor, sizeof *decor);
}
@ -142,6 +144,9 @@ revised:
if (kv_size(decor->virt_lines)) {
buf->b_virt_line_blocks++;
}
if (decor_has_sign(decor)) {
buf->b_signs++;
}
decor_redraw(buf, row, end_row > -1 ? end_row : row, decor);
}

View File

@ -87,6 +87,11 @@ static inline bool mt_end(mtkey_t key)
return key.flags & MT_FLAG_END;
}
static inline bool mt_start(mtkey_t key)
{
return mt_paired(key) && !mt_end(key);
}
static inline bool mt_right(mtkey_t key)
{
return key.flags & MT_FLAG_RIGHT_GRAVITY;

View File

@ -2208,6 +2208,8 @@ static int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, bool noc
buf_T *buf = wp->w_buffer;
bool end_fill = (lnum == buf->b_ml.ml_line_count+1);
has_decor = decor_redraw_line(buf, lnum-1, &decor_state);
if (!number_only) {
// To speed up the loop below, set extra_check when there is linebreak,
// trailing white space and/or syntax processing to be done.
@ -2229,9 +2231,6 @@ static int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, bool noc
}
}
has_decor = decor_redraw_line(wp->w_buffer, lnum-1,
&decor_state);
for (size_t k = 0; k < kv_size(*providers); k++) {
DecorProvider *p = kv_A(*providers, k);
if (p && p->redraw_line != LUA_NOREF) {
@ -2460,6 +2459,9 @@ static int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, bool noc
memset(sattrs, 0, sizeof(sattrs));
num_signs = buf_get_signattrs(wp->w_buffer, lnum, sattrs);
if (decor_state.has_sign_decor) {
decor_redraw_signs(buf, &decor_state, lnum-1, &num_signs, sattrs);
}
// If this line has a sign with line highlighting set line_attr.
// TODO(bfredl, vigoux): this should not take priority over decoration!

View File

@ -506,6 +506,8 @@ int buf_get_signattrs(buf_T *buf, linenr_T lnum, sign_attrs_T sattrs[])
if (sp->sn_num_hl != 0) {
sattr.sat_numhl = syn_id2attr(sp->sn_num_hl);
}
// Store the priority so we can mesh in extmark signs later
sattr.sat_prio = sign->se_priority;
}
sattrs[nr_matches] = sattr;

View File

@ -40,6 +40,7 @@ typedef struct sign_attrs_S {
int sat_linehl;
int sat_culhl;
int sat_numhl;
int sat_prio; // Used for inserting extmark signs
} sign_attrs_T;
#define SIGN_SHOW_MAX 9

View File

@ -1362,3 +1362,276 @@ if (h->n_buckets < new_n_buckets) { // expand
end)
end)
describe('decorations: signs', function()
local screen, ns
before_each(function()
clear()
screen = Screen.new(50, 10)
screen:attach()
screen:set_default_attr_ids {
[1] = {foreground = Screen.colors.Blue4, background = Screen.colors.Grey};
[2] = {foreground = Screen.colors.Blue1, bold = true};
}
ns = meths.create_namespace 'test'
meths.win_set_option(0, 'signcolumn', 'auto:9')
end)
local example_text = [[
l1
l2
l3
l4
l5
]]
it('can add a single sign (no end row)', function()
insert(example_text)
feed 'gg'
meths.buf_set_extmark(0, ns, 1, -1, {sign_text='S'})
screen:expect{grid=[[
{1: }^l1 |
S l2 |
{1: }l3 |
{1: }l4 |
{1: }l5 |
{1: } |
{2:~ }|
{2:~ }|
{2:~ }|
|
]]}
end)
it('can add a single sign (with end row)', function()
insert(example_text)
feed 'gg'
meths.buf_set_extmark(0, ns, 1, -1, {sign_text='S', end_row=1})
screen:expect{grid=[[
{1: }^l1 |
S l2 |
{1: }l3 |
{1: }l4 |
{1: }l5 |
{1: } |
{2:~ }|
{2:~ }|
{2:~ }|
|
]]}
end)
it('can add multiple signs (single extmark)', function()
pending('TODO(lewis6991): Support ranged signs')
insert(example_text)
feed 'gg'
meths.buf_set_extmark(0, ns, 1, -1, {sign_text='S', end_row = 2})
screen:expect{grid=[[
{1: }^l1 |
S l2 |
S l3 |
{1: }l4 |
{1: }l5 |
{1: } |
{2:~ }|
{2:~ }|
{2:~ }|
|
]]}
end)
it('can add multiple signs (multiple extmarks)', function()
pending('TODO(lewis6991): Support ranged signs')
insert(example_text)
feed'gg'
meths.buf_set_extmark(0, ns, 1, -1, {sign_text='S1'})
meths.buf_set_extmark(0, ns, 3, -1, {sign_text='S2', end_row = 4})
screen:expect{grid=[[
{1: }^l1 |
S1l2 |
{1: }l3 |
S2l4 |
S2l5 |
{1: } |
{2:~ }|
{2:~ }|
{2:~ }|
|
]]}
end)
it('can add multiple signs (multiple extmarks) 2', function()
insert(example_text)
feed 'gg'
meths.buf_set_extmark(0, ns, 1, -1, {sign_text='S1'})
meths.buf_set_extmark(0, ns, 1, -1, {sign_text='S2'})
screen:expect{grid=[[
{1: }^l1 |
S2S1l2 |
{1: }l3 |
{1: }l4 |
{1: }l5 |
{1: } |
{2:~ }|
{2:~ }|
{2:~ }|
|
]]}
-- TODO(lewis6991): Support ranged signs
-- meths.buf_set_extmark(1, ns, 1, -1, {sign_text='S3', end_row = 2})
-- screen:expect{grid=[[
-- {1: }^l1 |
-- S3S2S1l2 |
-- S3{1: }l3 |
-- {1: }l4 |
-- {1: }l5 |
-- {1: } |
-- {2:~ }|
-- {2:~ }|
-- {2:~ }|
-- |
-- ]]}
end)
it('can add multiple signs (multiple extmarks) 3', function()
pending('TODO(lewis6991): Support ranged signs')
insert(example_text)
feed 'gg'
meths.buf_set_extmark(0, ns, 1, -1, {sign_text='S1', end_row=2})
meths.buf_set_extmark(0, ns, 2, -1, {sign_text='S2', end_row=3})
screen:expect{grid=[[
{1: }^l1 |
S1{1: }l2 |
S2S1l3 |
S2{1: }l4 |
{1: }l5 |
{1: } |
{2:~ }|
{2:~ }|
{2:~ }|
|
]]}
end)
it('can add multiple signs (multiple extmarks) 4', function()
insert(example_text)
feed 'gg'
meths.buf_set_extmark(0, ns, 0, -1, {sign_text='S1', end_row=0})
meths.buf_set_extmark(0, ns, 1, -1, {sign_text='S2', end_row=1})
screen:expect{grid=[[
S1^l1 |
S2l2 |
{1: }l3 |
{1: }l4 |
{1: }l5 |
{1: } |
{2:~ }|
{2:~ }|
{2:~ }|
|
]]}
end)
it('works with old signs', function()
insert(example_text)
feed 'gg'
helpers.command('sign define Oldsign text=x')
helpers.command([[exe 'sign place 42 line=2 name=Oldsign buffer=' . bufnr('')]])
meths.buf_set_extmark(0, ns, 0, -1, {sign_text='S1'})
meths.buf_set_extmark(0, ns, 1, -1, {sign_text='S2'})
meths.buf_set_extmark(0, ns, 0, -1, {sign_text='S4'})
meths.buf_set_extmark(0, ns, 2, -1, {sign_text='S5'})
screen:expect{grid=[[
S4S1^l1 |
S2x l2 |
S5{1: }l3 |
{1: }l4 |
{1: }l5 |
{1: } |
{2:~ }|
{2:~ }|
{2:~ }|
|
]]}
end)
it('works with old signs (with range)', function()
pending('TODO(lewis6991): Support ranged signs')
insert(example_text)
feed 'gg'
helpers.command('sign define Oldsign text=x')
helpers.command([[exe 'sign place 42 line=2 name=Oldsign buffer=' . bufnr('')]])
meths.buf_set_extmark(0, ns, 0, -1, {sign_text='S1'})
meths.buf_set_extmark(0, ns, 1, -1, {sign_text='S2'})
meths.buf_set_extmark(0, ns, 0, -1, {sign_text='S3', end_row = 4})
meths.buf_set_extmark(0, ns, 0, -1, {sign_text='S4'})
meths.buf_set_extmark(0, ns, 2, -1, {sign_text='S5'})
screen:expect{grid=[[
S3S4S1^l1 |
S2S3x l2 |
S5S3{1: }l3 |
S3{1: }l4 |
S3{1: }l5 |
{1: } |
{2:~ }|
{2:~ }|
{2:~ }|
|
]]}
end)
it('can add a ranged sign (with start out of view)', function()
pending('TODO(lewis6991): Support ranged signs')
insert(example_text)
command 'set signcolumn=yes:2'
feed 'gg'
feed '2<C-e>'
meths.buf_set_extmark(0, ns, 1, -1, {sign_text='X', end_row=3})
screen:expect{grid=[[
X {1: }^l3 |
X {1: }l4 |
{1: }l5 |
{1: } |
{2:~ }|
{2:~ }|
{2:~ }|
{2:~ }|
{2:~ }|
|
]]}
end)
end)