feat(extmarks): add 'invalidate' property to extmarks

Problem:  No way to have extmarks automatically removed when the range it
          is attached to is deleted.
Solution: Add new 'invalidate' property that will hide a mark when the
          entirety of its range is deleted. When "undo_restore" is set
          to false, delete the mark from the buffer instead.
This commit is contained in:
Luuk van Baal 2023-10-24 13:32:00 +02:00
parent 324fad1e88
commit 4e6f559b8c
13 changed files with 229 additions and 84 deletions

View File

@ -2707,6 +2707,12 @@ nvim_buf_set_extmark({buffer}, {ns_id}, {line}, {col}, {*opts})
the extmark end position (if it exists) will be shifted in the extmark end position (if it exists) will be shifted in
when new text is inserted (true for right, false for when new text is inserted (true for right, false for
left). Defaults to false. left). Defaults to false.
• undo_restore : Restore the exact position of the mark if
text around the mark was deleted and then restored by
undo. Defaults to true.
• invalidate : boolean that indicates whether to hide the
extmark if the entirety of its range is deleted. If
"undo_restore" is false, the extmark is deleted instead.
• priority: a priority value for the highlight group or sign • priority: a priority value for the highlight group or sign
attribute. For example treesitter highlighting uses a attribute. For example treesitter highlighting uses a
value of 100. value of 100.
@ -2777,7 +2783,7 @@ nvim_set_decoration_provider({ns_id}, {*opts})
|nvim_buf_set_extmark()| can be called to add marks on a per-window or |nvim_buf_set_extmark()| can be called to add marks on a per-window or
per-lines basis. Use the `ephemeral` key to only use the mark for the per-lines basis. Use the `ephemeral` key to only use the mark for the
current screen redraw (the callback will be called again for the next current screen redraw (the callback will be called again for the next
redraw ). redraw).
Note: this function should not be called often. Rather, the callbacks Note: this function should not be called often. Rather, the callbacks
themselves can be used to throttle unneeded callbacks. the `on_start` themselves can be used to throttle unneeded callbacks. the `on_start`

View File

@ -273,6 +273,9 @@ The following changes to existing APIs or features add new behavior.
• Extmarks can opt-out of precise undo tracking using the new "undo_restore" • Extmarks can opt-out of precise undo tracking using the new "undo_restore"
flag to |nvim_buf_set_extmark()| flag to |nvim_buf_set_extmark()|
• Extmarks can be automatically hidden or removed using the new "invalidate"
flag to |nvim_buf_set_extmark()|
• LSP hover and signature help now use Treesitter for highlighting of Markdown • LSP hover and signature help now use Treesitter for highlighting of Markdown
content. content.
Note that syntax highlighting of code examples requires a matching parser Note that syntax highlighting of code examples requires a matching parser

View File

@ -554,6 +554,12 @@ function vim.api.nvim_buf_line_count(buffer) end
--- the extmark end position (if it exists) will be shifted in --- the extmark end position (if it exists) will be shifted in
--- when new text is inserted (true for right, false for --- when new text is inserted (true for right, false for
--- left). Defaults to false. --- left). Defaults to false.
--- • undo_restore : Restore the exact position of the mark if
--- text around the mark was deleted and then restored by
--- undo. Defaults to true.
--- • invalidate : boolean that indicates whether to hide the
--- extmark if the entirety of its range is deleted. If
--- "undo_restore" is false, the extmark is deleted instead.
--- • priority: a priority value for the highlight group or sign --- • priority: a priority value for the highlight group or sign
--- attribute. For example treesitter highlighting uses a --- attribute. For example treesitter highlighting uses a
--- value of 100. --- value of 100.
@ -1812,7 +1818,7 @@ function vim.api.nvim_set_current_win(window) end
--- `nvim_buf_set_extmark()` can be called to add marks on a per-window or --- `nvim_buf_set_extmark()` can be called to add marks on a per-window or
--- per-lines basis. Use the `ephemeral` key to only use the mark for the --- per-lines basis. Use the `ephemeral` key to only use the mark for the
--- current screen redraw (the callback will be called again for the next --- current screen redraw (the callback will be called again for the next
--- redraw ). --- redraw).
--- Note: this function should not be called often. Rather, the callbacks --- Note: this function should not be called often. Rather, the callbacks
--- themselves can be used to throttle unneeded callbacks. the `on_start` --- themselves can be used to throttle unneeded callbacks. the `on_start`
--- callback can return `false` to disable the provider until the next redraw. --- callback can return `false` to disable the provider until the next redraw.

View File

@ -227,6 +227,7 @@ error('Cannot require a meta file')
--- @field virt_text_hide? boolean --- @field virt_text_hide? boolean
--- @field hl_eol? boolean --- @field hl_eol? boolean
--- @field hl_mode? string --- @field hl_mode? string
--- @field invalidate? boolean
--- @field ephemeral? boolean --- @field ephemeral? boolean
--- @field priority? integer --- @field priority? integer
--- @field right_gravity? boolean --- @field right_gravity? boolean
@ -243,6 +244,7 @@ error('Cannot require a meta file')
--- @field conceal? string --- @field conceal? string
--- @field spell? boolean --- @field spell? boolean
--- @field ui_watched? boolean --- @field ui_watched? boolean
--- @field undo_restore? boolean
--- @class vim.api.keyset.user_command --- @class vim.api.keyset.user_command
--- @field addr? any --- @field addr? any

View File

@ -172,7 +172,7 @@ Integer nvim_buf_set_virtual_text(Buffer buffer, Integer src_id, Integer line, A
decor.virt_text_width = width; decor.virt_text_width = width;
decor.priority = 0; decor.priority = 0;
extmark_set(buf, ns_id, NULL, (int)line, 0, -1, -1, &decor, true, false, false, NULL); extmark_set(buf, ns_id, NULL, (int)line, 0, -1, -1, &decor, true, false, false, false, NULL);
return src_id; return src_id;
} }

View File

@ -170,6 +170,13 @@ static Array extmark_to_array(const ExtmarkInfo *extmark, bool id, bool add_dict
PUT(dict, "undo_restore", BOOLEAN_OBJ(false)); PUT(dict, "undo_restore", BOOLEAN_OBJ(false));
} }
if (extmark->invalidate) {
PUT(dict, "invalidate", BOOLEAN_OBJ(true));
}
if (extmark->invalid) {
PUT(dict, "invalid", BOOLEAN_OBJ(true));
}
const Decoration *decor = &extmark->decor; const Decoration *decor = &extmark->decor;
if (decor->hl_id) { if (decor->hl_id) {
PUT(dict, "hl_group", hl_group_name(decor->hl_id, hl_name)); PUT(dict, "hl_group", hl_group_name(decor->hl_id, hl_name));
@ -526,6 +533,9 @@ Array nvim_buf_get_extmarks(Buffer buffer, Integer ns_id, Object start, Object e
/// - undo_restore : Restore the exact position of the mark /// - undo_restore : Restore the exact position of the mark
/// if text around the mark was deleted and then restored by undo. /// if text around the mark was deleted and then restored by undo.
/// Defaults to true. /// Defaults to true.
/// - invalidate : boolean that indicates whether to hide the
/// extmark if the entirety of its range is deleted. If
/// "undo_restore" is false, the extmark is deleted instead.
/// - priority: a priority value for the highlight group or sign /// - priority: a priority value for the highlight group or sign
/// attribute. For example treesitter highlighting uses a /// attribute. For example treesitter highlighting uses a
/// value of 100. /// value of 100.
@ -759,8 +769,6 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, Integer line, Integer
goto error; goto error;
}); });
bool end_right_gravity = opts->end_right_gravity;
size_t len = 0; size_t len = 0;
if (!HAS_KEY(opts, set_extmark, spell)) { if (!HAS_KEY(opts, set_extmark, spell)) {
@ -823,7 +831,7 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, Integer line, Integer
// TODO(bfredl): synergize these two branches even more // TODO(bfredl): synergize these two branches even more
if (opts->ephemeral && decor_state.win && decor_state.win->w_buffer == buf) { if (opts->ephemeral && decor_state.win && decor_state.win->w_buffer == buf) {
decor_add_ephemeral((int)line, (int)col, line2, col2, &decor, (uint64_t)ns_id, id); decor_push_ephemeral((int)line, (int)col, line2, col2, &decor, (uint64_t)ns_id, id);
} else { } else {
if (opts->ephemeral) { if (opts->ephemeral) {
api_set_error(err, kErrorTypeException, "not yet implemented"); api_set_error(err, kErrorTypeException, "not yet implemented");
@ -831,8 +839,9 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, Integer line, Integer
} }
extmark_set(buf, (uint32_t)ns_id, &id, (int)line, (colnr_T)col, line2, col2, extmark_set(buf, (uint32_t)ns_id, &id, (int)line, (colnr_T)col, line2, col2,
has_decor ? &decor : NULL, right_gravity, end_right_gravity, has_decor ? &decor : NULL, right_gravity, opts->end_right_gravity,
!GET_BOOL_OR_TRUE(opts, set_extmark, undo_restore), err); !GET_BOOL_OR_TRUE(opts, set_extmark, undo_restore),
opts->invalidate, err);
if (ERROR_SET(err)) { if (ERROR_SET(err)) {
goto error; goto error;
} }
@ -959,7 +968,7 @@ Integer nvim_buf_add_highlight(Buffer buffer, Integer ns_id, String hl_group, In
extmark_set(buf, ns, NULL, extmark_set(buf, ns, NULL,
(int)line, (colnr_T)col_start, (int)line, (colnr_T)col_start,
end_line, (colnr_T)col_end, end_line, (colnr_T)col_end,
&decor, true, false, false, NULL); &decor, true, false, false, false, NULL);
return ns_id; return ns_id;
} }
@ -1010,7 +1019,7 @@ void nvim_buf_clear_namespace(Buffer buffer, Integer ns_id, Integer line_start,
/// redrawn buffer. |nvim_buf_set_extmark()| can be called to add marks /// redrawn buffer. |nvim_buf_set_extmark()| can be called to add marks
/// on a per-window or per-lines basis. Use the `ephemeral` key to only /// on a per-window or per-lines basis. Use the `ephemeral` key to only
/// use the mark for the current screen redraw (the callback will be called /// use the mark for the current screen redraw (the callback will be called
/// again for the next redraw ). /// again for the next redraw).
/// ///
/// Note: this function should not be called often. Rather, the callbacks /// Note: this function should not be called often. Rather, the callbacks
/// themselves can be used to throttle unneeded callbacks. the `on_start` /// themselves can be used to throttle unneeded callbacks. the `on_start`

View File

@ -32,6 +32,7 @@ typedef struct {
Boolean virt_text_hide; Boolean virt_text_hide;
Boolean hl_eol; Boolean hl_eol;
String hl_mode; String hl_mode;
Boolean invalidate;
Boolean ephemeral; Boolean ephemeral;
Integer priority; Integer priority;
Boolean right_gravity; Boolean right_gravity;

View File

@ -66,7 +66,7 @@ void bufhl_add_hl_pos_offset(buf_T *buf, int src_id, int hl_id, lpos_T pos_start
} }
extmark_set(buf, (uint32_t)src_id, NULL, extmark_set(buf, (uint32_t)src_id, NULL,
(int)lnum - 1, hl_start, (int)lnum - 1 + end_off, hl_end, (int)lnum - 1, hl_start, (int)lnum - 1 + end_off, hl_end,
&decor, true, false, true, NULL); &decor, true, false, true, false, NULL);
} }
} }
@ -95,7 +95,30 @@ void decor_redraw(buf_T *buf, int row1, int row2, Decoration *decor)
} }
} }
void decor_remove(buf_T *buf, int row, int row2, Decoration *decor) void decor_add(buf_T *buf, int row, int row2, Decoration *decor, bool hl_id)
{
if (decor) {
if (kv_size(decor->virt_text) && decor->virt_text_pos == kVTInline) {
buf->b_virt_text_inline++;
}
if (kv_size(decor->virt_lines)) {
buf->b_virt_line_blocks++;
}
if (decor_has_sign(decor)) {
buf->b_signs++;
}
if (decor->sign_text) {
buf->b_signs_with_text++;
// TODO(lewis6991): smarter invalidation
buf_signcols_add_check(buf, NULL);
}
}
if (decor || hl_id) {
decor_redraw(buf, row, row2 > -1 ? row2 : row, decor);
}
}
void decor_remove(buf_T *buf, int row, int row2, Decoration *decor, bool invalidate)
{ {
decor_redraw(buf, row, row2, decor); decor_redraw(buf, row, row2, decor);
if (decor) { if (decor) {
@ -119,7 +142,9 @@ void decor_remove(buf_T *buf, int row, int row2, Decoration *decor)
} }
} }
} }
decor_free(decor); if (!invalidate) {
decor_free(decor);
}
} }
void decor_clear(Decoration *decor) void decor_clear(Decoration *decor)
@ -180,7 +205,7 @@ Decoration *decor_find_virttext(buf_T *buf, int row, uint64_t ns_id)
MTKey mark = marktree_itr_current(itr); MTKey mark = marktree_itr_current(itr);
if (mark.pos.row < 0 || mark.pos.row > row) { if (mark.pos.row < 0 || mark.pos.row > row) {
break; break;
} else if (marktree_decor_level(mark) < kDecorLevelVisible) { } else if (mt_invalid(mark) || marktree_decor_level(mark) < kDecorLevelVisible) {
goto next_mark; goto next_mark;
} }
Decoration *decor = mark.decor_full; Decoration *decor = mark.decor_full;
@ -236,14 +261,14 @@ bool decor_redraw_start(win_T *wp, int top_row, DecorState *state)
MTPair pair; MTPair pair;
while (marktree_itr_step_overlap(buf->b_marktree, state->itr, &pair)) { while (marktree_itr_step_overlap(buf->b_marktree, state->itr, &pair)) {
if (marktree_decor_level(pair.start) < kDecorLevelVisible) { if (mt_invalid(pair.start) || marktree_decor_level(pair.start) < kDecorLevelVisible) {
continue; continue;
} }
Decoration decor = get_decor(pair.start); Decoration decor = get_decor(pair.start);
decor_add(state, pair.start.pos.row, pair.start.pos.col, pair.end_pos.row, pair.end_pos.col, decor_push(state, pair.start.pos.row, pair.start.pos.col, pair.end_pos.row, pair.end_pos.col,
&decor, false, pair.start.ns, pair.start.id); &decor, false, pair.start.ns, pair.start.id);
} }
return true; // TODO(bfredl): check if available in the region return true; // TODO(bfredl): check if available in the region
@ -266,8 +291,8 @@ bool decor_redraw_line(win_T *wp, int row, DecorState *state)
return (k.pos.row >= 0 && k.pos.row <= row); return (k.pos.row >= 0 && k.pos.row <= row);
} }
static void decor_add(DecorState *state, int start_row, int start_col, int end_row, int end_col, static void decor_push(DecorState *state, int start_row, int start_col, int end_row, int end_col,
Decoration *decor, bool owned, uint64_t ns_id, uint64_t mark_id) Decoration *decor, bool owned, uint64_t ns_id, uint64_t mark_id)
{ {
int attr_id = decor->hl_id > 0 ? syn_id2attr(decor->hl_id) : 0; int attr_id = decor->hl_id > 0 ? syn_id2attr(decor->hl_id) : 0;
@ -327,8 +352,7 @@ int decor_redraw_col(win_T *wp, int col, int win_col, bool hidden, DecorState *s
break; break;
} }
if (mt_end(mark) if (mt_invalid(mark) || mt_end(mark) || marktree_decor_level(mark) < kDecorLevelVisible) {
|| marktree_decor_level(mark) < kDecorLevelVisible) {
goto next_mark; goto next_mark;
} }
@ -339,8 +363,8 @@ int decor_redraw_col(win_T *wp, int col, int win_col, bool hidden, DecorState *s
endpos = mark.pos; endpos = mark.pos;
} }
decor_add(state, mark.pos.row, mark.pos.col, endpos.row, endpos.col, decor_push(state, mark.pos.row, mark.pos.col, endpos.row, endpos.col,
&decor, false, mark.ns, mark.id); &decor, false, mark.ns, mark.id);
next_mark: next_mark:
marktree_itr_next(buf->b_marktree, state->itr); marktree_itr_next(buf->b_marktree, state->itr);
@ -425,7 +449,7 @@ void decor_redraw_signs(buf_T *buf, int row, int *num_signs, SignTextAttrs sattr
MTPair pair; MTPair pair;
while (marktree_itr_step_overlap(buf->b_marktree, itr, &pair)) { while (marktree_itr_step_overlap(buf->b_marktree, itr, &pair)) {
if (marktree_decor_level(pair.start) < kDecorLevelVisible) { if (mt_invalid(pair.start) || marktree_decor_level(pair.start) < kDecorLevelVisible) {
continue; continue;
} }
@ -444,7 +468,7 @@ void decor_redraw_signs(buf_T *buf, int row, int *num_signs, SignTextAttrs sattr
break; break;
} }
if (mt_end(mark) || marktree_decor_level(mark) < kDecorLevelVisible) { if (mt_end(mark) || mt_invalid(mark) || marktree_decor_level(mark) < kDecorLevelVisible) {
goto next_mark; goto next_mark;
} }
@ -605,14 +629,14 @@ bool decor_redraw_eol(win_T *wp, DecorState *state, int *eol_attr, int eol_col)
return has_virttext; return has_virttext;
} }
void decor_add_ephemeral(int start_row, int start_col, int end_row, int end_col, Decoration *decor, void decor_push_ephemeral(int start_row, int start_col, int end_row, int end_col, Decoration *decor,
uint64_t ns_id, uint64_t mark_id) uint64_t ns_id, uint64_t mark_id)
{ {
if (end_row == -1) { if (end_row == -1) {
end_row = start_row; end_row = start_row;
end_col = start_col; end_col = start_col;
} }
decor_add(&decor_state, start_row, start_col, end_row, end_col, decor, true, ns_id, mark_id); decor_push(&decor_state, start_row, start_col, end_row, end_col, decor, true, ns_id, mark_id);
} }
/// @param has_fold whether line "lnum" has a fold, or kNone when not calculated yet /// @param has_fold whether line "lnum" has a fold, or kNone when not calculated yet

View File

@ -56,11 +56,12 @@
/// must not be used during iteration! /// must not be used during iteration!
void extmark_set(buf_T *buf, uint32_t ns_id, uint32_t *idp, int row, colnr_T col, int end_row, void extmark_set(buf_T *buf, uint32_t ns_id, uint32_t *idp, int row, colnr_T col, int end_row,
colnr_T end_col, Decoration *decor, bool right_gravity, bool end_right_gravity, colnr_T end_col, Decoration *decor, bool right_gravity, bool end_right_gravity,
bool no_undo, Error *err) bool no_undo, bool invalidate, Error *err)
{ {
uint32_t *ns = map_put_ref(uint32_t, uint32_t)(buf->b_extmark_ns, ns_id, NULL, NULL); uint32_t *ns = map_put_ref(uint32_t, uint32_t)(buf->b_extmark_ns, ns_id, NULL, NULL);
uint32_t id = idp ? *idp : 0; uint32_t id = idp ? *idp : 0;
bool decor_full = false; bool decor_full = false;
bool hl_eol = false;
uint8_t decor_level = kDecorLevelNone; // no decor uint8_t decor_level = kDecorLevelNone; // no decor
if (decor) { if (decor) {
@ -74,10 +75,12 @@ void extmark_set(buf_T *buf, uint32_t ns_id, uint32_t *idp, int row, colnr_T col
decor = xmemdup(decor, sizeof *decor); decor = xmemdup(decor, sizeof *decor);
} }
decor_level = kDecorLevelVisible; // decor affects redraw decor_level = kDecorLevelVisible; // decor affects redraw
hl_eol = decor->hl_eol;
if (kv_size(decor->virt_lines)) { if (kv_size(decor->virt_lines)) {
decor_level = kDecorLevelVirtLine; // decor affects horizontal size decor_level = kDecorLevelVirtLine; // decor affects horizontal size
} }
} }
uint16_t flags = mt_flags(right_gravity, hl_eol, no_undo, invalidate, decor_level);
if (id == 0) { if (id == 0) {
id = ++*ns; id = ++*ns;
@ -99,25 +102,20 @@ void extmark_set(buf_T *buf, uint32_t ns_id, uint32_t *idp, int row, colnr_T col
assert(marktree_itr_valid(itr)); assert(marktree_itr_valid(itr));
if (old_mark.pos.row == row && old_mark.pos.col == col) { if (old_mark.pos.row == row && old_mark.pos.col == col) {
if (marktree_decor_level(old_mark) > kDecorLevelNone) { if (marktree_decor_level(old_mark) > kDecorLevelNone) {
decor_remove(buf, row, row, old_mark.decor_full); decor_remove(buf, row, row, old_mark.decor_full, false);
old_mark.decor_full = NULL; old_mark.decor_full = NULL;
} }
old_mark.flags = 0; old_mark.flags = flags;
if (decor_full) { if (decor_full) {
old_mark.decor_full = decor; old_mark.decor_full = decor;
} else if (decor) { } else if (decor) {
old_mark.hl_id = decor->hl_id; old_mark.hl_id = decor->hl_id;
// Workaround: the gcc compiler of functionaltest-lua build
// apparently incapable of handling basic integer constants.
// This can be underanged as soon as we bump minimal gcc version.
old_mark.flags = (uint16_t)(old_mark.flags
| (decor->hl_eol ? (uint16_t)MT_FLAG_HL_EOL : (uint16_t)0));
old_mark.priority = decor->priority; old_mark.priority = decor->priority;
} }
marktree_revise(buf->b_marktree, itr, decor_level, old_mark); marktree_revise(buf->b_marktree, itr, decor_level, old_mark);
goto revised; goto revised;
} }
decor_remove(buf, old_mark.pos.row, old_mark.pos.row, old_mark.decor_full); decor_remove(buf, old_mark.pos.row, old_mark.pos.row, old_mark.decor_full, false);
marktree_del_itr(buf->b_marktree, itr, false); marktree_del_itr(buf->b_marktree, itr, false);
} }
} else { } else {
@ -125,37 +123,18 @@ void extmark_set(buf_T *buf, uint32_t ns_id, uint32_t *idp, int row, colnr_T col
} }
} }
MTKey mark = { { row, col }, ns_id, id, 0, MTKey mark = { { row, col }, ns_id, id, 0, flags, 0, NULL };
mt_flags(right_gravity, decor_level, no_undo), 0, NULL };
if (decor_full) { if (decor_full) {
mark.decor_full = decor; mark.decor_full = decor;
} else if (decor) { } else if (decor) {
mark.hl_id = decor->hl_id; mark.hl_id = decor->hl_id;
// workaround: see above
mark.flags = (uint16_t)(mark.flags | (decor->hl_eol ? (uint16_t)MT_FLAG_HL_EOL : (uint16_t)0));
mark.priority = decor->priority; mark.priority = decor->priority;
} }
marktree_put(buf->b_marktree, mark, end_row, end_col, end_right_gravity); marktree_put(buf->b_marktree, mark, end_row, end_col, end_right_gravity);
revised: revised:
if (decor) { decor_add(buf, row, end_row, decor, decor && decor->hl_id);
if (kv_size(decor->virt_text) && decor->virt_text_pos == kVTInline) {
buf->b_virt_text_inline++;
}
if (kv_size(decor->virt_lines)) {
buf->b_virt_line_blocks++;
}
if (decor_has_sign(decor)) {
buf->b_signs++;
}
if (decor->sign_text) {
buf->b_signs_with_text++;
// TODO(lewis6991): smarter invalidation
buf_signcols_add_check(buf, NULL);
}
decor_redraw(buf, row, end_row > -1 ? end_row : row, decor);
}
if (idp) { if (idp) {
*idp = id; *idp = id;
@ -215,7 +194,7 @@ linenr_T extmark_del(buf_T *buf, MarkTreeIter *itr, MTKey key, bool restore)
} }
if (marktree_decor_level(key) > kDecorLevelNone) { if (marktree_decor_level(key) > kDecorLevelNone) {
decor_remove(buf, key.pos.row, key2.pos.row, key.decor_full); decor_remove(buf, key.pos.row, key2.pos.row, key.decor_full, false);
} }
// TODO(bfredl): delete it from current undo header, opportunistically? // TODO(bfredl): delete it from current undo header, opportunistically?
@ -348,6 +327,8 @@ static void push_mark(ExtmarkInfoArray *array, uint32_t ns_id, ExtmarkType type_
.row = mark.pos.row, .col = mark.pos.col, .row = mark.pos.row, .col = mark.pos.col,
.end_row = end_pos.row, .end_row = end_pos.row,
.end_col = end_pos.col, .end_col = end_pos.col,
.invalidate = mt_invalidate(mark),
.invalid = mt_invalid(mark),
.right_gravity = mt_right(mark), .right_gravity = mt_right(mark),
.end_right_gravity = end_right, .end_right_gravity = end_right,
.no_undo = mt_no_undo(mark), .no_undo = mt_no_undo(mark),
@ -357,7 +338,7 @@ static void push_mark(ExtmarkInfoArray *array, uint32_t ns_id, ExtmarkType type_
/// Lookup an extmark by id /// Lookup an extmark by id
ExtmarkInfo extmark_from_id(buf_T *buf, uint32_t ns_id, uint32_t id) ExtmarkInfo extmark_from_id(buf_T *buf, uint32_t ns_id, uint32_t id)
{ {
ExtmarkInfo ret = { 0, 0, -1, -1, -1, -1, false, false, false, DECORATION_INIT }; ExtmarkInfo ret = EXTMARKINFO_INIT;
MTKey mark = marktree_lookup_ns(buf->b_marktree, ns_id, id, false, NULL); MTKey mark = marktree_lookup_ns(buf->b_marktree, ns_id, id, false, NULL);
if (!mark.id) { if (!mark.id) {
return ret; return ret;
@ -374,6 +355,8 @@ ExtmarkInfo extmark_from_id(buf_T *buf, uint32_t ns_id, uint32_t id)
ret.right_gravity = mt_right(mark); ret.right_gravity = mt_right(mark);
ret.end_right_gravity = mt_right(end); ret.end_right_gravity = mt_right(end);
ret.no_undo = mt_no_undo(mark); ret.no_undo = mt_no_undo(mark);
ret.invalidate = mt_invalidate(mark);
ret.invalid = mt_invalid(mark);
ret.decor = get_decor(mark); ret.decor = get_decor(mark);
return ret; return ret;
@ -408,20 +391,17 @@ void extmark_free_all(buf_T *buf)
*buf->b_extmark_ns = (Map(uint32_t, uint32_t)) MAP_INIT; *buf->b_extmark_ns = (Map(uint32_t, uint32_t)) MAP_INIT;
} }
/// copy extmarks data between range /// invalidate extmarks between range and copy to undo header
/// ///
/// useful when we cannot simply reverse the operation. This will do nothing on /// copying is useful when we cannot simply reverse the operation. This will do
/// redo, enforces correct position when undo. /// nothing on redo, enforces correct position when undo.
void u_extmark_copy(buf_T *buf, int l_row, colnr_T l_col, int u_row, colnr_T u_col) void extmark_splice_delete(buf_T *buf, int l_row, colnr_T l_col, int u_row, colnr_T u_col,
ExtmarkOp op)
{ {
u_header_T *uhp = u_force_get_undo_header(buf); u_header_T *uhp = u_force_get_undo_header(buf);
if (!uhp) { MarkTreeIter itr[1] = { 0 };
return;
}
ExtmarkUndoObject undo; ExtmarkUndoObject undo;
MarkTreeIter itr[1] = { 0 };
marktree_itr_get(buf->b_marktree, (int32_t)l_row, l_col, itr); marktree_itr_get(buf->b_marktree, (int32_t)l_row, l_col, itr);
while (true) { while (true) {
MTKey mark = marktree_itr_current(itr); MTKey mark = marktree_itr_current(itr);
@ -431,9 +411,33 @@ void u_extmark_copy(buf_T *buf, int l_row, colnr_T l_col, int u_row, colnr_T u_c
break; break;
} }
if (!mt_no_undo(mark)) { bool invalidated = false;
// Invalidate/delete mark
if (!mt_invalid(mark) && mt_invalidate(mark) && !mt_end(mark)) {
MTPos endpos = marktree_get_altpos(buf->b_marktree, mark, NULL);
if (endpos.row < 0) {
endpos = mark.pos;
}
if ((endpos.col <= u_col || (!u_col && endpos.row == mark.pos.row))
&& mark.pos.col >= l_col
&& mark.pos.row >= l_row && endpos.row <= u_row - (u_col ? 0 : 1)) {
if (mt_no_undo(mark)) {
extmark_del(buf, itr, mark, true);
continue;
} else {
invalidated = true;
mark.flags |= MT_FLAG_INVALID;
marktree_revise(curbuf->b_marktree, itr, marktree_decor_level(mark), mark);
decor_remove(buf, mark.pos.row, endpos.row, mark.decor_full, true);
}
}
}
// Push mark to undo header
if (uhp && op == kExtmarkUndo && !mt_no_undo(mark)) {
ExtmarkSavePos pos; ExtmarkSavePos pos;
pos.mark = mt_lookup_key(mark); pos.mark = mt_lookup_key(mark);
pos.invalidated = invalidated;
pos.old_row = mark.pos.row; pos.old_row = mark.pos.row;
pos.old_col = mark.pos.col; pos.old_col = mark.pos.col;
pos.row = -1; pos.row = -1;
@ -472,6 +476,14 @@ void extmark_apply_undo(ExtmarkUndoObject undo_info, bool undo)
} else if (undo_info.type == kExtmarkSavePos) { } else if (undo_info.type == kExtmarkSavePos) {
ExtmarkSavePos pos = undo_info.data.savepos; ExtmarkSavePos pos = undo_info.data.savepos;
if (undo) { if (undo) {
if (pos.invalidated) {
MarkTreeIter itr[1] = { 0 };
MTKey mark = marktree_lookup(curbuf->b_marktree, pos.mark, itr);
MTKey end = marktree_get_alt(curbuf->b_marktree, mark, NULL);
mark.flags &= (uint16_t) ~MT_FLAG_INVALID;
marktree_revise(curbuf->b_marktree, itr, marktree_decor_level(mark), mark);
decor_add(curbuf, mark.pos.row, end.pos.row, mark.decor_full, mark.hl_id);
}
if (pos.old_row >= 0) { if (pos.old_row >= 0) {
extmark_setraw(curbuf, pos.mark, pos.old_row, pos.old_col); extmark_setraw(curbuf, pos.mark, pos.old_row, pos.old_col);
} }
@ -513,7 +525,6 @@ void extmark_adjust(buf_T *buf, linenr_T line1, linenr_T line2, linenr_T amount,
old_row = line2 - line1 + 1; old_row = line2 - line1 + 1;
// TODO(bfredl): ej kasta? // TODO(bfredl): ej kasta?
old_byte = (bcount_t)buf->deleted_bytes2; old_byte = (bcount_t)buf->deleted_bytes2;
new_row = amount_after + old_row; new_row = amount_after + old_row;
} else { } else {
// A region is either deleted (amount == MAXLNUM) or // A region is either deleted (amount == MAXLNUM) or
@ -579,15 +590,15 @@ void extmark_splice_impl(buf_T *buf, int start_row, colnr_T start_col, bcount_t
old_row, old_col, old_byte, old_row, old_col, old_byte,
new_row, new_col, new_byte); new_row, new_col, new_byte);
if (undo == kExtmarkUndo && (old_row > 0 || old_col > 0)) { if (old_row > 0 || old_col > 0) {
// Copy marks that would be effected by delete // Copy and invalidate marks that would be effected by delete
// TODO(bfredl): Be "smart" about gravity here, left-gravity at the // TODO(bfredl): Be "smart" about gravity here, left-gravity at the
// beginning and right-gravity at the end need not be preserved. // beginning and right-gravity at the end need not be preserved.
// Also be smart about marks that already have been saved (important for // Also be smart about marks that already have been saved (important for
// merge!) // merge!)
int end_row = start_row + old_row; int end_row = start_row + old_row;
int end_col = (old_row ? 0 : start_col) + old_col; int end_col = (old_row ? 0 : start_col) + old_col;
u_extmark_copy(buf, start_row, start_col, end_row, end_col); extmark_splice_delete(buf, start_row, start_col, end_row, end_col, undo);
} }
marktree_splice(buf->b_marktree, (int32_t)start_row, start_col, marktree_splice(buf->b_marktree, (int32_t)start_row, start_col,

View File

@ -25,9 +25,13 @@ typedef struct {
colnr_T end_col; colnr_T end_col;
bool right_gravity; bool right_gravity;
bool end_right_gravity; bool end_right_gravity;
bool invalidate;
bool invalid;
bool no_undo; bool no_undo;
Decoration decor; // TODO(bfredl): CHONKY Decoration decor; // TODO(bfredl): CHONKY
} ExtmarkInfo; } ExtmarkInfo;
#define EXTMARKINFO_INIT { 0, 0, -1, -1, -1, -1, false, false, false, false, false, \
DECORATION_INIT };
typedef kvec_t(ExtmarkInfo) ExtmarkInfoArray; typedef kvec_t(ExtmarkInfo) ExtmarkInfoArray;
@ -67,6 +71,7 @@ typedef struct {
colnr_T old_col; colnr_T old_col;
int row; int row;
colnr_T col; colnr_T col;
bool invalidated;
} ExtmarkSavePos; } ExtmarkSavePos;
typedef enum { typedef enum {

View File

@ -1146,9 +1146,14 @@ void marktree_revise(MarkTree *b, MarkTreeIter *itr, uint8_t decor_level, MTKey
// TODO(bfredl): clean up this mess and re-instantiate &= and |= forms // TODO(bfredl): clean up this mess and re-instantiate &= and |= forms
// once we upgrade to a non-broken version of gcc in functionaltest-lua CI // once we upgrade to a non-broken version of gcc in functionaltest-lua CI
rawkey(itr).flags = (uint16_t)(rawkey(itr).flags & (uint16_t) ~MT_FLAG_DECOR_MASK); rawkey(itr).flags = (uint16_t)(rawkey(itr).flags & (uint16_t) ~MT_FLAG_DECOR_MASK);
rawkey(itr).flags = (uint16_t)(rawkey(itr).flags & (uint16_t) ~MT_FLAG_INVALID);
rawkey(itr).flags = (uint16_t)(rawkey(itr).flags rawkey(itr).flags = (uint16_t)(rawkey(itr).flags
| (uint16_t)(decor_level << MT_FLAG_DECOR_OFFSET) | (uint16_t)(decor_level << MT_FLAG_DECOR_OFFSET)
| (uint16_t)(key.flags & MT_FLAG_DECOR_MASK)); | (uint16_t)(key.flags & MT_FLAG_DECOR_MASK)
| (uint16_t)(key.flags & MT_FLAG_HL_EOL)
| (uint16_t)(key.flags & MT_FLAG_NO_UNDO)
| (uint16_t)(key.flags & MT_FLAG_INVALID)
| (uint16_t)(key.flags & MT_FLAG_INVALIDATE));
rawkey(itr).decor_full = key.decor_full; rawkey(itr).decor_full = key.decor_full;
rawkey(itr).hl_id = key.hl_id; rawkey(itr).hl_id = key.hl_id;
rawkey(itr).priority = key.priority; rawkey(itr).priority = key.priority;
@ -2006,8 +2011,8 @@ static void marktree_itr_fix_pos(MarkTree *b, MarkTreeIter *itr)
void marktree_put_test(MarkTree *b, uint32_t ns, uint32_t id, int row, int col, bool right_gravity, void marktree_put_test(MarkTree *b, uint32_t ns, uint32_t id, int row, int col, bool right_gravity,
int end_row, int end_col, bool end_right) int end_row, int end_col, bool end_right)
{ {
MTKey key = { { row, col }, ns, id, 0, uint16_t flags = mt_flags(right_gravity, false, false, false, 0);
mt_flags(right_gravity, 0, false), 0, NULL }; MTKey key = { { row, col }, ns, id, 0, flags, 0, NULL };
marktree_put(b, key, end_row, end_col, end_right); marktree_put(b, key, end_row, end_col, end_right);
} }

View File

@ -78,17 +78,19 @@ typedef struct {
#define MT_FLAG_ORPHANED (((uint16_t)1) << 3) #define MT_FLAG_ORPHANED (((uint16_t)1) << 3)
#define MT_FLAG_HL_EOL (((uint16_t)1) << 4) #define MT_FLAG_HL_EOL (((uint16_t)1) << 4)
#define MT_FLAG_NO_UNDO (((uint16_t)1) << 5) #define MT_FLAG_NO_UNDO (((uint16_t)1) << 5)
#define MT_FLAG_INVALIDATE (((uint16_t)1) << 6)
#define MT_FLAG_INVALID (((uint16_t)1) << 7)
#define DECOR_LEVELS 4 #define DECOR_LEVELS 4
#define MT_FLAG_DECOR_OFFSET 6 #define MT_FLAG_DECOR_OFFSET 8
#define MT_FLAG_DECOR_MASK (((uint16_t)(DECOR_LEVELS - 1)) << MT_FLAG_DECOR_OFFSET) #define MT_FLAG_DECOR_MASK (((uint16_t)(DECOR_LEVELS - 1)) << MT_FLAG_DECOR_OFFSET)
// These _must_ be last to preserve ordering of marks // These _must_ be last to preserve ordering of marks
#define MT_FLAG_RIGHT_GRAVITY (((uint16_t)1) << 14) #define MT_FLAG_RIGHT_GRAVITY (((uint16_t)1) << 14)
#define MT_FLAG_LAST (((uint16_t)1) << 15) #define MT_FLAG_LAST (((uint16_t)1) << 15)
#define MT_FLAG_EXTERNAL_MASK (MT_FLAG_DECOR_MASK | MT_FLAG_RIGHT_GRAVITY | \ #define MT_FLAG_EXTERNAL_MASK (MT_FLAG_DECOR_MASK | MT_FLAG_RIGHT_GRAVITY | MT_FLAG_HL_EOL \
MT_FLAG_NO_UNDO | MT_FLAG_HL_EOL) | MT_FLAG_NO_UNDO | MT_FLAG_INVALIDATE | MT_FLAG_INVALID)
// this is defined so that start and end of the same range have adjacent ids // this is defined so that start and end of the same range have adjacent ids
#define MARKTREE_END_FLAG ((uint64_t)1) #define MARKTREE_END_FLAG ((uint64_t)1)
@ -132,17 +134,30 @@ static inline bool mt_no_undo(MTKey key)
return key.flags & MT_FLAG_NO_UNDO; return key.flags & MT_FLAG_NO_UNDO;
} }
static inline bool mt_invalidate(MTKey key)
{
return key.flags & MT_FLAG_INVALIDATE;
}
static inline bool mt_invalid(MTKey key)
{
return key.flags & MT_FLAG_INVALID;
}
static inline uint8_t marktree_decor_level(MTKey key) static inline uint8_t marktree_decor_level(MTKey key)
{ {
return (uint8_t)((key.flags&MT_FLAG_DECOR_MASK) >> MT_FLAG_DECOR_OFFSET); return (uint8_t)((key.flags&MT_FLAG_DECOR_MASK) >> MT_FLAG_DECOR_OFFSET);
} }
static inline uint16_t mt_flags(bool right_gravity, uint8_t decor_level, bool no_undo) static inline uint16_t mt_flags(bool right_gravity, bool hl_eol, bool no_undo, bool invalidate,
uint8_t decor_level)
{ {
assert(decor_level < DECOR_LEVELS); assert(decor_level < DECOR_LEVELS);
return (uint16_t)((right_gravity ? MT_FLAG_RIGHT_GRAVITY : 0) return (uint16_t)((right_gravity ? MT_FLAG_RIGHT_GRAVITY : 0)
| (decor_level << MT_FLAG_DECOR_OFFSET) | (hl_eol ? MT_FLAG_HL_EOL : 0)
| (no_undo ? MT_FLAG_NO_UNDO : 0)); | (no_undo ? MT_FLAG_NO_UNDO : 0)
| (invalidate ? MT_FLAG_INVALIDATE : 0)
| (decor_level << MT_FLAG_DECOR_OFFSET));
} }
typedef kvec_withinit_t(uint64_t, 4) Intersection; typedef kvec_withinit_t(uint64_t, 4) Intersection;

View File

@ -1599,12 +1599,70 @@ describe('API/extmarks', function()
set_extmark(ns, 3, 0, 0, { sign_text = '>>' }) set_extmark(ns, 3, 0, 0, { sign_text = '>>' })
set_extmark(ns, 4, 0, 0, { virt_text = {{'text', 'Normal'}}}) set_extmark(ns, 4, 0, 0, { virt_text = {{'text', 'Normal'}}})
set_extmark(ns, 5, 0, 0, { virt_lines = {{{ 'line', 'Normal' }}}}) set_extmark(ns, 5, 0, 0, { virt_lines = {{{ 'line', 'Normal' }}}})
eq(5, #get_extmarks(-1, 0, -1, { details = true })) eq(5, #get_extmarks(-1, 0, -1, {}))
eq({{ 2, 0, 0 }}, get_extmarks(-1, 0, -1, { type = 'highlight' })) eq({{ 2, 0, 0 }}, get_extmarks(-1, 0, -1, { type = 'highlight' }))
eq({{ 3, 0, 0 }}, get_extmarks(-1, 0, -1, { type = 'sign' })) eq({{ 3, 0, 0 }}, get_extmarks(-1, 0, -1, { type = 'sign' }))
eq({{ 4, 0, 0 }}, get_extmarks(-1, 0, -1, { type = 'virt_text' })) eq({{ 4, 0, 0 }}, get_extmarks(-1, 0, -1, { type = 'virt_text' }))
eq({{ 5, 0, 0 }}, get_extmarks(-1, 0, -1, { type = 'virt_lines' })) eq({{ 5, 0, 0 }}, get_extmarks(-1, 0, -1, { type = 'virt_lines' }))
end) end)
it("invalidated marks are deleted", function()
screen = Screen.new(40, 6)
screen:attach()
feed('dd6iaaa bbb ccc<CR><ESC>gg')
set_extmark(ns, 1, 0, 0, { invalidate = true, sign_text = 'S1' })
set_extmark(ns, 2, 1, 0, { invalidate = true, sign_text = 'S2' })
-- mark with invalidate is removed
command('d')
screen:expect([[
S2^aaa bbb ccc |
aaa bbb ccc |
aaa bbb ccc |
aaa bbb ccc |
aaa bbb ccc |
|
]])
-- mark is restored with undo_restore == true
command('silent undo')
screen:expect([[
S1^aaa bbb ccc |
S2aaa bbb ccc |
aaa bbb ccc |
aaa bbb ccc |
aaa bbb ccc |
|
]])
-- mark is deleted with undo_restore == false
set_extmark(ns, 1, 0, 0, { invalidate = true, undo_restore = false, sign_text = 'S1' })
set_extmark(ns, 2, 1, 0, { invalidate = true, undo_restore = false, sign_text = 'S2' })
command('1d 2')
eq(0, #get_extmarks(-1, 0, -1, {}))
-- mark is not removed when deleting bytes before the range
set_extmark(ns, 3, 0, 4, { invalidate = true, undo_restore = false,
hl_group = 'Error', end_col = 7 })
feed('dw')
eq(3, get_extmark_by_id(ns, 3, { details = true })[3].end_col)
-- mark is not removed when deleting bytes at the start of the range
feed('x')
eq(2, get_extmark_by_id(ns, 3, { details = true })[3].end_col)
-- mark is not removed when deleting bytes from the end of the range
feed('lx')
eq(1, get_extmark_by_id(ns, 3, { details = true})[3].end_col)
-- mark is not removed when deleting bytes beyond end of the range
feed('x')
eq(1, get_extmark_by_id(ns, 3, { details = true})[3].end_col)
-- mark is removed when all bytes in the range are deleted
feed('hx')
eq({}, get_extmark_by_id(ns, 3, {}))
-- multiline mark is not removed when start of its range is deleted
set_extmark(ns, 4, 1, 4, { undo_restore = false, invalidate = true,
hl_group = 'Error', end_col = 7, end_row = 3 })
feed('ddDdd')
eq({0, 0}, get_extmark_by_id(ns, 4, {}))
-- multiline mark is removed when entirety of its range is deleted
feed('vj2ed')
eq({}, get_extmark_by_id(ns, 4, {}))
end)
end) end)
describe('Extmarks buffer api with many marks', function() describe('Extmarks buffer api with many marks', function()