decorations: allow virt_text overlay at any column

This commit is contained in:
Björn Linse 2021-02-15 14:00:34 +01:00
parent d623400cad
commit 4781333a7a
5 changed files with 238 additions and 69 deletions

View File

@ -1425,6 +1425,11 @@ Array nvim_buf_get_extmarks(Buffer buffer, Integer ns_id,
/// - hl_group : name of the highlight group used to highlight
/// this mark.
/// - virt_text : virtual text to link to this mark.
/// - virt_text_pos : positioning of virtual text. Possible
/// values:
/// - "eol": right after eol character (default)
/// - "overlay": display over the specified column, without
/// shifting the underlying text.
/// - ephemeral : for use with |nvim_set_decoration_provider|
/// callbacks. The mark will only be used for the current
/// redraw cycle, and not be permantently stored in the
@ -1474,8 +1479,9 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id,
uint64_t id = 0;
int line2 = -1, hl_id = 0;
DecorPriority priority = DECOR_PRIORITY_BASE;
colnr_T col2 = 0;
colnr_T col2 = -1;
VirtText virt_text = KV_INITIAL_VALUE;
VirtTextPos virt_text_pos = kVTEndOfLine;
bool right_gravity = true;
bool end_right_gravity = false;
bool end_gravity_set = false;
@ -1544,6 +1550,22 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id,
if (ERROR_SET(err)) {
goto error;
}
} else if (strequal("virt_text_pos", k.data)) {
if (v->type != kObjectTypeString) {
api_set_error(err, kErrorTypeValidation,
"virt_text_pos is not a String");
goto error;
}
String str = v->data.string;
if (strequal("eol", str.data)) {
virt_text_pos = kVTEndOfLine;
} else if (strequal("overlay", str.data)) {
virt_text_pos = kVTOverlay;
} else {
api_set_error(err, kErrorTypeValidation,
"virt_text_pos: invalid value");
goto error;
}
} else if (strequal("ephemeral", k.data)) {
ephemeral = api_object_to_bool(*v, "ephemeral", false, err);
if (ERROR_SET(err)) {
@ -1585,7 +1607,7 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id,
// Only error out if they try to set end_right_gravity without
// setting end_col or end_line
if (line2 == -1 && col2 == 0 && end_gravity_set) {
if (line2 == -1 && col2 == -1 && end_gravity_set) {
api_set_error(err, kErrorTypeValidation,
"cannot set end_right_gravity "
"without setting end_line or end_col");
@ -1609,30 +1631,28 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id,
col2 = 0;
}
Decoration *decor = NULL, tmp = { 0 };
if (kv_size(virt_text) || priority != DECOR_PRIORITY_BASE) {
// TODO(bfredl): this is a bit sketchy. eventually we should
// have predefined decorations for both marks/ephemerals
decor = ephemeral ? &tmp : xcalloc(1, sizeof(*decor));
decor->hl_id = hl_id;
decor->virt_text = virt_text;
decor->priority = priority;
decor->virt_text_pos = virt_text_pos;
} else if (hl_id) {
decor = decor_hl(hl_id);
}
// TODO(bfredl): synergize these two branches even more
if (ephemeral && decor_state.buf == buf) {
int attr_id = hl_id > 0 ? syn_id2attr(hl_id) : 0;
VirtText *vt_allocated = NULL;
if (kv_size(virt_text)) {
vt_allocated = xmalloc(sizeof *vt_allocated);
*vt_allocated = virt_text;
}
decor_add_ephemeral(attr_id, (int)line, (colnr_T)col,
(int)line2, (colnr_T)col2, priority, vt_allocated);
decor_add_ephemeral((int)line, (int)col, line2, col2, decor, 0);
} else {
if (ephemeral) {
api_set_error(err, kErrorTypeException, "not yet implemented");
goto error;
}
Decoration *decor = NULL;
if (kv_size(virt_text)) {
decor = xcalloc(1, sizeof(*decor));
decor->hl_id = hl_id;
decor->virt_text = virt_text;
} else if (hl_id) {
decor = decor_hl(hl_id);
decor->priority = priority;
}
id = extmark_set(buf, (uint64_t)ns_id, id, (int)line, (colnr_T)col,
line2, col2, decor, right_gravity,

View File

@ -188,20 +188,21 @@ bool decor_redraw_start(buf_T *buf, int top_row, DecorState *state)
goto next_mark;
}
int attr_id = decor->hl_id > 0 ? syn_id2attr(decor->hl_id) : 0;
VirtText *vt = kv_size(decor->virt_text) ? &decor->virt_text : NULL;
HlRange range;
if (mark.id&MARKTREE_END_FLAG) {
range = (HlRange){ altpos.row, altpos.col, mark.row, mark.col,
attr_id, decor->priority, vt, false };
decor_add(state, altpos.row, altpos.col, mark.row, mark.col,
decor, false, 0);
} else {
range = (HlRange){ mark.row, mark.col, altpos.row,
altpos.col, attr_id, decor->priority, vt, false };
if (altpos.row == -1) {
altpos.row = mark.row;
altpos.col = mark.col;
}
decor_add(state, mark.row, mark.col, altpos.row, altpos.col,
decor, false, 0);
}
hlrange_activate(range, state);
next_mark:
if (marktree_itr_node_done(state->itr)) {
marktree_itr_next(buf->b_marktree, state->itr);
break;
}
marktree_itr_next(buf->b_marktree, state->itr);
@ -220,41 +221,39 @@ bool decor_redraw_line(buf_T *buf, int row, DecorState *state)
return true; // TODO(bfredl): be more precise
}
static void hlrange_activate(HlRange range, DecorState *state)
static void decor_add(DecorState *state, int start_row, int start_col,
int end_row, int end_col, Decoration *decor, bool owned,
DecorPriority priority)
{
// Get size before preparing the push, to have the number of elements
size_t s = kv_size(state->active);
int attr_id = decor->hl_id > 0 ? syn_id2attr(decor->hl_id) : 0;
HlRange range = { start_row, start_col, end_row, end_col,
attr_id, MAX(priority, decor->priority),
kv_size(decor->virt_text) ? &decor->virt_text : NULL,
decor->virt_text_pos,
kv_size(decor->virt_text) && owned, -1 };
kv_pushp(state->active);
size_t dest_index = 0;
// Determine insertion dest_index
while (dest_index < s) {
HlRange item = kv_A(state->active, dest_index);
if (item.priority > range.priority) {
size_t index;
for (index = kv_size(state->active)-1; index > 0; index--) {
HlRange item = kv_A(state->active, index-1);
if (item.priority <= range.priority) {
break;
}
dest_index++;
}
// Splice
for (size_t index = s; index > dest_index; index--) {
kv_A(state->active, index) = kv_A(state->active, index-1);
}
// Insert
kv_A(state->active, dest_index) = range;
kv_A(state->active, index) = range;
}
int decor_redraw_col(buf_T *buf, int col, DecorState *state)
int decor_redraw_col(buf_T *buf, int col, int virt_col, DecorState *state)
{
if (col <= state->col_until) {
return state->current;
}
state->col_until = MAXCOL;
while (true) {
// TODO(bfredl): check duplicate entry in "intersection"
// branch
mtmark_t mark = marktree_itr_current(state->itr);
if (mark.row < 0 || mark.row > state->row) {
break;
@ -278,6 +277,11 @@ int decor_redraw_col(buf_T *buf, int col, DecorState *state)
}
Decoration *decor = item->decor;
if (endpos.row == -1) {
endpos.row = mark.row;
endpos.col = mark.col;
}
if (endpos.row < mark.row
|| (endpos.row == mark.row && endpos.col <= mark.col)) {
if (!kv_size(decor->virt_text)) {
@ -285,12 +289,8 @@ int decor_redraw_col(buf_T *buf, int col, DecorState *state)
}
}
int attr_id = decor->hl_id > 0 ? syn_id2attr(decor->hl_id) : 0;
VirtText *vt = kv_size(decor->virt_text) ? &decor->virt_text : NULL;
hlrange_activate((HlRange){ mark.row, mark.col,
endpos.row, endpos.col,
attr_id, decor->priority,
vt, false }, state);
decor_add(state, mark.row, mark.col, endpos.row, endpos.col,
decor, false, 0);
next_mark:
marktree_itr_next(buf->b_marktree, state->itr);
@ -310,7 +310,7 @@ next_mark:
if (item.start_row < state->row
|| (item.start_row == state->row && item.start_col <= col)) {
active = true;
if (item.end_row == state->row) {
if (item.end_row == state->row && item.end_col > col) {
state->col_until = MIN(state->col_until, item.end_col-1);
}
} else {
@ -322,8 +322,12 @@ next_mark:
if (active && item.attr_id > 0) {
attr = hl_combine_attr(attr, item.attr_id);
}
if ((item.start_row == state->row && item.start_col <= col)
&& item.virt_text && item.virt_col == -1) {
item.virt_col = virt_col;
}
if (keep) {
kv_A(state->active, j++) = kv_A(state->active, i);
kv_A(state->active, j++) = item;
} else if (item.virt_text_owned) {
clear_virttext(item.virt_text);
xfree(item.virt_text);
@ -341,21 +345,20 @@ void decor_redraw_end(DecorState *state)
VirtText *decor_redraw_virt_text(buf_T *buf, DecorState *state)
{
decor_redraw_col(buf, MAXCOL, state);
decor_redraw_col(buf, MAXCOL, MAXCOL, state);
for (size_t i = 0; i < kv_size(state->active); i++) {
HlRange item = kv_A(state->active, i);
if (item.start_row == state->row && item.virt_text) {
if (item.start_row == state->row && item.virt_text
&& item.virt_text_pos == kVTEndOfLine) {
return item.virt_text;
}
}
return NULL;
}
void decor_add_ephemeral(int attr_id, int start_row, int start_col,
int end_row, int end_col, DecorPriority priority,
VirtText *virt_text)
void decor_add_ephemeral(int start_row, int start_col, int end_row, int end_col,
Decoration *decor, DecorPriority priority)
{
hlrange_activate(((HlRange){ start_row, start_col, end_row, end_col, attr_id,
priority, virt_text, virt_text != NULL }),
&decor_state);
decor_add(&decor_state, start_row, start_col, end_row, end_col, decor, true,
priority);
}

View File

@ -18,10 +18,16 @@ typedef kvec_t(VirtTextChunk) VirtText;
typedef uint16_t DecorPriority;
#define DECOR_PRIORITY_BASE 0x1000
typedef enum {
kVTEndOfLine,
kVTOverlay,
} VirtTextPos;
struct Decoration
{
int hl_id; // highlight group
VirtText virt_text;
VirtTextPos virt_text_pos;
// TODO(bfredl): style, signs, etc
DecorPriority priority;
bool shared; // shared decoration, don't free
@ -35,7 +41,9 @@ typedef struct {
int attr_id;
DecorPriority priority;
VirtText *virt_text;
VirtTextPos virt_text_pos;
bool virt_text_owned;
int virt_col;
} HlRange;
typedef struct {

View File

@ -143,7 +143,7 @@ long tab_page_click_defs_size = 0;
// for line_putchar. Contains the state that needs to be remembered from
// putting one character to the next.
typedef struct {
const char_u *p;
const char *p;
int prev_c; // previous Arabic character
int prev_c1; // first composing char for prev_c
} LineState;
@ -1857,7 +1857,7 @@ static int compute_foldcolumn(win_T *wp, int col)
/// Handles composing chars and arabic shaping state.
static int line_putchar(LineState *s, schar_T *dest, int maxcells, bool rl)
{
const char_u *p = s->p;
const char_u *p = (char_u *)s->p;
int cells = utf_ptr2cells(p);
int c_len = utfc_ptr2len(p);
int u8c, u8cc[MAX_MCO];
@ -2870,6 +2870,7 @@ static int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow,
&& vcol >= (long)wp->w_virtcol)
|| (number_only && draw_state > WL_NR))
&& filler_todo <= 0) {
draw_virt_text(buf, &col, grid->Columns);
grid_put_linebuf(grid, row, 0, col, -grid->Columns, wp->w_p_rl, wp,
wp->w_hl_attr_normal, false);
// Pretend we have finished updating the window. Except when
@ -3397,7 +3398,7 @@ static int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow,
}
if (has_decor && v > 0) {
int extmark_attr = decor_redraw_col(wp->w_buffer, (colnr_T)v-1,
int extmark_attr = decor_redraw_col(wp->w_buffer, (colnr_T)v-1, off,
&decor_state);
if (extmark_attr != 0) {
if (!attr_pri) {
@ -3919,7 +3920,7 @@ static int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow,
int i;
size_t virt_pos = 0;
LineState s = LINE_STATE((char_u *)"");
LineState s = LINE_STATE("");
int virt_attr = 0;
// Make sure alignment is the same regardless
@ -3963,7 +3964,7 @@ static int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow,
if (do_virttext && !delay_virttext) {
if (*s.p == NUL) {
if (virt_pos < virt_text.size) {
s.p = (char_u *)kv_A(virt_text, virt_pos).text;
s.p = kv_A(virt_text, virt_pos).text;
int hl_id = kv_A(virt_text, virt_pos).hl_id;
virt_attr = hl_id > 0 ? syn_id2attr(hl_id) : 0;
virt_pos++;
@ -4029,6 +4030,8 @@ static int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow,
col += n;
}
}
draw_virt_text(buf, &col, grid->Columns);
grid_put_linebuf(grid, row, 0, col, grid->Columns, wp->w_p_rl, wp,
wp->w_hl_attr_normal, false);
row++;
@ -4247,7 +4250,10 @@ static int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow,
&& (grid->Columns == Columns // Window spans the width of the screen,
|| ui_has(kUIMultigrid)) // or has dedicated grid.
&& !wp->w_p_rl; // Not right-to-left.
grid_put_linebuf(grid, row, 0, col - boguscols, grid->Columns, wp->w_p_rl,
int draw_col = col - boguscols;
draw_virt_text(buf, &draw_col, grid->Columns);
grid_put_linebuf(grid, row, 0, draw_col, grid->Columns, wp->w_p_rl,
wp, wp->w_hl_attr_normal, wrap);
if (wrap) {
ScreenGrid *current_grid = grid;
@ -4323,6 +4329,43 @@ static int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow,
return row;
}
void draw_virt_text(buf_T *buf, int *end_col, int max_col)
{
DecorState *state = &decor_state;
for (size_t i = 0; i < kv_size(state->active); i++) {
HlRange *item = &kv_A(state->active, i);
if (item->start_row == state->row && item->virt_text
&& item->virt_text_pos == kVTOverlay
&& item->virt_col >= 0) {
VirtText vt = *item->virt_text;
LineState s = LINE_STATE("");
int virt_attr = 0;
int col = item->virt_col;
size_t virt_pos = 0;
item->virt_col = -2; // deactivate
while (col < max_col) {
if (!*s.p) {
if (virt_pos == kv_size(vt)) {
break;
}
s.p = kv_A(vt, virt_pos).text;
int hl_id = kv_A(vt, virt_pos).hl_id;
virt_attr = hl_id > 0 ? syn_id2attr(hl_id) : 0;
virt_pos++;
continue;
}
int cells = line_putchar(&s, &linebuf_char[col], 2, false);
linebuf_attr[col++] = virt_attr;
if (cells > 1) {
linebuf_attr[col++] = virt_attr;
}
}
*end_col = MAX(*end_col, col);
}
}
}
/// Determine if dedicated window grid should be used or the default_grid
///
/// If UI did not request multigrid support, draw all windows on the

View File

@ -302,3 +302,98 @@ describe('decorations providers', function()
]]}
end)
end)
describe('extmark decorations', function()
local screen
before_each( function()
clear()
screen = Screen.new(50, 15)
screen:attach()
screen:set_default_attr_ids {
[1] = {bold=true, foreground=Screen.colors.Blue};
[2] = {foreground = Screen.colors.Brown};
[3] = {bold = true, foreground = Screen.colors.SeaGreen};
[4] = {background = Screen.colors.Red1, foreground = Screen.colors.Gray100};
}
end)
it('can have virtual text of overlay style', function()
insert [[
for _,item in ipairs(items) do
local text, hl_id_cell, count = unpack(item)
if hl_id_cell ~= nil then
hl_id = hl_id_cell
end
for _ = 1, (count or 1) do
local cell = line[colpos]
cell.text = text
cell.hl_id = hl_id
colpos = colpos+1
end
end]]
feed 'gg'
local ns = meths.create_namespace 'test'
for i = 1,9 do
meths.buf_set_extmark(0, ns, i, 0, { virt_text={{'|', 'LineNr'}}, virt_text_pos='overlay'})
if i == 3 or (i >= 6 and i <= 9) then
meths.buf_set_extmark(0, ns, i, 4, { virt_text={{'|', 'NonText'}}, virt_text_pos='overlay'})
end
end
meths.buf_set_extmark(0, ns, 9, 10, { virt_text={{'foo'}, {'bar', 'MoreMsg'}, {'!!', 'ErrorMsg'}}, virt_text_pos='overlay'})
-- can "float" beyond end of line
meths.buf_set_extmark(0, ns, 5, 28, { virt_text={{'loopy', 'ErrorMsg'}}, virt_text_pos='overlay'})
-- bound check: right edge of window
meths.buf_set_extmark(0, ns, 2, 26, { virt_text={{'bork bork bork ' }, {'bork bork bork', 'ErrorMsg'}}, virt_text_pos='overlay'})
screen:expect{grid=[[
^for _,item in ipairs(items) do |
{2:|} local text, hl_id_cell, count = unpack(item) |
{2:|} if hl_id_cell ~= nil tbork bork bork {4:bork bork}|
{2:|} {1:|} hl_id = hl_id_cell |
{2:|} end |
{2:|} for _ = 1, (count or 1) {4:loopy} |
{2:|} {1:|} local cell = line[colpos] |
{2:|} {1:|} cell.text = text |
{2:|} {1:|} cell.hl_id = hl_id |
{2:|} {1:|} cofoo{3:bar}{4:!!}olpos+1 |
end |
end |
{1:~ }|
{1:~ }|
|
]]}
-- handles broken lines
screen:try_resize(22, 25)
screen:expect{grid=[[
^for _,item in ipairs(i|
tems) do |
{2:|} local text, hl_id_|
cell, count = unpack(i|
tem) |
{2:|} if hl_id_cell ~= n|
il tbork bork bork {4:bor}|
{2:|} {1:|} hl_id = hl_id_|
cell |
{2:|} end |
{2:|} for _ = 1, (count |
or 1) {4:loopy} |
{2:|} {1:|} local cell = l|
ine[colpos] |
{2:|} {1:|} cell.text = te|
xt |
{2:|} {1:|} cell.hl_id = h|
l_id |
{2:|} {1:|} cofoo{3:bar}{4:!!}olpo|
s+1 |
end |
end |
{1:~ }|
{1:~ }|
|
]]}
end)
end)