From 4e6f559b8c5f77924fdbe2e5abd9c6aa8efad13f Mon Sep 17 00:00:00 2001 From: Luuk van Baal Date: Tue, 24 Oct 2023 13:32:00 +0200 Subject: [PATCH] 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. --- runtime/doc/api.txt | 8 +- runtime/doc/news.txt | 3 + runtime/lua/vim/_meta/api.lua | 8 +- runtime/lua/vim/_meta/api_keysets.lua | 2 + src/nvim/api/deprecated.c | 2 +- src/nvim/api/extmark.c | 23 ++++-- src/nvim/api/keysets.h | 1 + src/nvim/decoration.c | 60 ++++++++++----- src/nvim/extmark.c | 103 ++++++++++++++------------ src/nvim/extmark.h | 5 ++ src/nvim/marktree.c | 11 ++- src/nvim/marktree.h | 27 +++++-- test/functional/api/extmark_spec.lua | 60 ++++++++++++++- 13 files changed, 229 insertions(+), 84 deletions(-) diff --git a/runtime/doc/api.txt b/runtime/doc/api.txt index 4430d97fc7..2f9e8228d2 100644 --- a/runtime/doc/api.txt +++ b/runtime/doc/api.txt @@ -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 when new text is inserted (true for right, false for 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 attribute. For example treesitter highlighting uses a 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 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 - redraw ). + redraw). Note: this function should not be called often. Rather, the callbacks themselves can be used to throttle unneeded callbacks. the `on_start` diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index e32e1aadb6..9ddb1e91b7 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -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" 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 content. Note that syntax highlighting of code examples requires a matching parser diff --git a/runtime/lua/vim/_meta/api.lua b/runtime/lua/vim/_meta/api.lua index 691da62f4f..6c6e11a0d3 100644 --- a/runtime/lua/vim/_meta/api.lua +++ b/runtime/lua/vim/_meta/api.lua @@ -554,6 +554,12 @@ function vim.api.nvim_buf_line_count(buffer) end --- the extmark end position (if it exists) will be shifted in --- when new text is inserted (true for right, false for --- 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 --- attribute. For example treesitter highlighting uses a --- 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 --- 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 ---- redraw ). +--- redraw). --- Note: this function should not be called often. Rather, the callbacks --- themselves can be used to throttle unneeded callbacks. the `on_start` --- callback can return `false` to disable the provider until the next redraw. diff --git a/runtime/lua/vim/_meta/api_keysets.lua b/runtime/lua/vim/_meta/api_keysets.lua index 467409505e..f69e5a92c7 100644 --- a/runtime/lua/vim/_meta/api_keysets.lua +++ b/runtime/lua/vim/_meta/api_keysets.lua @@ -227,6 +227,7 @@ error('Cannot require a meta file') --- @field virt_text_hide? boolean --- @field hl_eol? boolean --- @field hl_mode? string +--- @field invalidate? boolean --- @field ephemeral? boolean --- @field priority? integer --- @field right_gravity? boolean @@ -243,6 +244,7 @@ error('Cannot require a meta file') --- @field conceal? string --- @field spell? boolean --- @field ui_watched? boolean +--- @field undo_restore? boolean --- @class vim.api.keyset.user_command --- @field addr? any diff --git a/src/nvim/api/deprecated.c b/src/nvim/api/deprecated.c index 906edb7b44..8edb6dbd4f 100644 --- a/src/nvim/api/deprecated.c +++ b/src/nvim/api/deprecated.c @@ -172,7 +172,7 @@ Integer nvim_buf_set_virtual_text(Buffer buffer, Integer src_id, Integer line, A decor.virt_text_width = width; 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; } diff --git a/src/nvim/api/extmark.c b/src/nvim/api/extmark.c index 3f1745df40..aeecab6bd0 100644 --- a/src/nvim/api/extmark.c +++ b/src/nvim/api/extmark.c @@ -170,6 +170,13 @@ static Array extmark_to_array(const ExtmarkInfo *extmark, bool id, bool add_dict 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; if (decor->hl_id) { 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 /// 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 /// attribute. For example treesitter highlighting uses a /// value of 100. @@ -759,8 +769,6 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, Integer line, Integer goto error; }); - bool end_right_gravity = opts->end_right_gravity; - size_t len = 0; 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 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 { if (opts->ephemeral) { 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, - has_decor ? &decor : NULL, right_gravity, end_right_gravity, - !GET_BOOL_OR_TRUE(opts, set_extmark, undo_restore), err); + has_decor ? &decor : NULL, right_gravity, opts->end_right_gravity, + !GET_BOOL_OR_TRUE(opts, set_extmark, undo_restore), + opts->invalidate, err); if (ERROR_SET(err)) { 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, (int)line, (colnr_T)col_start, end_line, (colnr_T)col_end, - &decor, true, false, false, NULL); + &decor, true, false, false, false, NULL); 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 /// 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 -/// again for the next redraw ). +/// again for the next redraw). /// /// Note: this function should not be called often. Rather, the callbacks /// themselves can be used to throttle unneeded callbacks. the `on_start` diff --git a/src/nvim/api/keysets.h b/src/nvim/api/keysets.h index d435262ff3..0da2239847 100644 --- a/src/nvim/api/keysets.h +++ b/src/nvim/api/keysets.h @@ -32,6 +32,7 @@ typedef struct { Boolean virt_text_hide; Boolean hl_eol; String hl_mode; + Boolean invalidate; Boolean ephemeral; Integer priority; Boolean right_gravity; diff --git a/src/nvim/decoration.c b/src/nvim/decoration.c index 24f6693fe2..8341f29410 100644 --- a/src/nvim/decoration.c +++ b/src/nvim/decoration.c @@ -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, (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); 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) @@ -180,7 +205,7 @@ Decoration *decor_find_virttext(buf_T *buf, int row, uint64_t ns_id) MTKey mark = marktree_itr_current(itr); if (mark.pos.row < 0 || mark.pos.row > row) { break; - } else if (marktree_decor_level(mark) < kDecorLevelVisible) { + } else if (mt_invalid(mark) || marktree_decor_level(mark) < kDecorLevelVisible) { goto next_mark; } Decoration *decor = mark.decor_full; @@ -236,14 +261,14 @@ bool decor_redraw_start(win_T *wp, int top_row, DecorState *state) MTPair 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; } 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, false, pair.start.ns, pair.start.id); + 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); } 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); } -static void decor_add(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) +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) { 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; } - if (mt_end(mark) - || marktree_decor_level(mark) < kDecorLevelVisible) { + if (mt_invalid(mark) || mt_end(mark) || marktree_decor_level(mark) < kDecorLevelVisible) { 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; } - decor_add(state, mark.pos.row, mark.pos.col, endpos.row, endpos.col, - &decor, false, mark.ns, mark.id); + decor_push(state, mark.pos.row, mark.pos.col, endpos.row, endpos.col, + &decor, false, mark.ns, mark.id); next_mark: 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; 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; } @@ -444,7 +468,7 @@ void decor_redraw_signs(buf_T *buf, int row, int *num_signs, SignTextAttrs sattr 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; } @@ -605,14 +629,14 @@ bool decor_redraw_eol(win_T *wp, DecorState *state, int *eol_attr, int eol_col) return has_virttext; } -void decor_add_ephemeral(int start_row, int start_col, int end_row, int end_col, Decoration *decor, - uint64_t ns_id, uint64_t mark_id) +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) { if (end_row == -1) { end_row = start_row; 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 diff --git a/src/nvim/extmark.c b/src/nvim/extmark.c index 42ca99d706..434be75af0 100644 --- a/src/nvim/extmark.c +++ b/src/nvim/extmark.c @@ -56,11 +56,12 @@ /// 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, 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 id = idp ? *idp : 0; bool decor_full = false; + bool hl_eol = false; uint8_t decor_level = kDecorLevelNone; // no 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_level = kDecorLevelVisible; // decor affects redraw + hl_eol = decor->hl_eol; if (kv_size(decor->virt_lines)) { decor_level = kDecorLevelVirtLine; // decor affects horizontal size } } + uint16_t flags = mt_flags(right_gravity, hl_eol, no_undo, invalidate, decor_level); if (id == 0) { 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)); if (old_mark.pos.row == row && old_mark.pos.col == col) { 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.flags = 0; + old_mark.flags = flags; if (decor_full) { old_mark.decor_full = decor; } else if (decor) { 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; } marktree_revise(buf->b_marktree, itr, decor_level, old_mark); 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); } } 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, - mt_flags(right_gravity, decor_level, no_undo), 0, NULL }; + MTKey mark = { { row, col }, ns_id, id, 0, flags, 0, NULL }; if (decor_full) { mark.decor_full = decor; } else if (decor) { 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; } marktree_put(buf->b_marktree, mark, end_row, end_col, end_right_gravity); revised: - 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); - } - decor_redraw(buf, row, end_row > -1 ? end_row : row, decor); - } + decor_add(buf, row, end_row, decor, decor && decor->hl_id); if (idp) { *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) { - 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? @@ -348,6 +327,8 @@ static void push_mark(ExtmarkInfoArray *array, uint32_t ns_id, ExtmarkType type_ .row = mark.pos.row, .col = mark.pos.col, .end_row = end_pos.row, .end_col = end_pos.col, + .invalidate = mt_invalidate(mark), + .invalid = mt_invalid(mark), .right_gravity = mt_right(mark), .end_right_gravity = end_right, .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 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); if (!mark.id) { 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.end_right_gravity = mt_right(end); ret.no_undo = mt_no_undo(mark); + ret.invalidate = mt_invalidate(mark); + ret.invalid = mt_invalid(mark); ret.decor = get_decor(mark); return ret; @@ -408,20 +391,17 @@ void extmark_free_all(buf_T *buf) *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 -/// 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) +/// copying is useful when we cannot simply reverse the operation. This will do +/// nothing on redo, enforces correct position when undo. +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); - if (!uhp) { - return; - } - + MarkTreeIter itr[1] = { 0 }; ExtmarkUndoObject undo; - MarkTreeIter itr[1] = { 0 }; marktree_itr_get(buf->b_marktree, (int32_t)l_row, l_col, itr); while (true) { 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; } - 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; pos.mark = mt_lookup_key(mark); + pos.invalidated = invalidated; pos.old_row = mark.pos.row; pos.old_col = mark.pos.col; pos.row = -1; @@ -472,6 +476,14 @@ void extmark_apply_undo(ExtmarkUndoObject undo_info, bool undo) } else if (undo_info.type == kExtmarkSavePos) { ExtmarkSavePos pos = undo_info.data.savepos; 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) { 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; // TODO(bfredl): ej kasta? old_byte = (bcount_t)buf->deleted_bytes2; - new_row = amount_after + old_row; } else { // 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, new_row, new_col, new_byte); - if (undo == kExtmarkUndo && (old_row > 0 || old_col > 0)) { - // Copy marks that would be effected by delete + if (old_row > 0 || old_col > 0) { + // Copy and invalidate marks that would be effected by delete // TODO(bfredl): Be "smart" about gravity here, left-gravity at the // beginning and right-gravity at the end need not be preserved. // Also be smart about marks that already have been saved (important for // merge!) int end_row = start_row + old_row; 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, diff --git a/src/nvim/extmark.h b/src/nvim/extmark.h index 83c7a7cb69..6b7292465c 100644 --- a/src/nvim/extmark.h +++ b/src/nvim/extmark.h @@ -25,9 +25,13 @@ typedef struct { colnr_T end_col; bool right_gravity; bool end_right_gravity; + bool invalidate; + bool invalid; bool no_undo; Decoration decor; // TODO(bfredl): CHONKY } ExtmarkInfo; +#define EXTMARKINFO_INIT { 0, 0, -1, -1, -1, -1, false, false, false, false, false, \ + DECORATION_INIT }; typedef kvec_t(ExtmarkInfo) ExtmarkInfoArray; @@ -67,6 +71,7 @@ typedef struct { colnr_T old_col; int row; colnr_T col; + bool invalidated; } ExtmarkSavePos; typedef enum { diff --git a/src/nvim/marktree.c b/src/nvim/marktree.c index 3df659b8e1..214d228b2c 100644 --- a/src/nvim/marktree.c +++ b/src/nvim/marktree.c @@ -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 // 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_INVALID); rawkey(itr).flags = (uint16_t)(rawkey(itr).flags | (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).hl_id = key.hl_id; 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, int end_row, int end_col, bool end_right) { - MTKey key = { { row, col }, ns, id, 0, - mt_flags(right_gravity, 0, false), 0, NULL }; + uint16_t flags = mt_flags(right_gravity, false, false, false, 0); + MTKey key = { { row, col }, ns, id, 0, flags, 0, NULL }; marktree_put(b, key, end_row, end_col, end_right); } diff --git a/src/nvim/marktree.h b/src/nvim/marktree.h index 79e8f8f50e..82cb95d8e3 100644 --- a/src/nvim/marktree.h +++ b/src/nvim/marktree.h @@ -78,17 +78,19 @@ typedef struct { #define MT_FLAG_ORPHANED (((uint16_t)1) << 3) #define MT_FLAG_HL_EOL (((uint16_t)1) << 4) #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 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) // These _must_ be last to preserve ordering of marks #define MT_FLAG_RIGHT_GRAVITY (((uint16_t)1) << 14) #define MT_FLAG_LAST (((uint16_t)1) << 15) -#define MT_FLAG_EXTERNAL_MASK (MT_FLAG_DECOR_MASK | MT_FLAG_RIGHT_GRAVITY | \ - MT_FLAG_NO_UNDO | MT_FLAG_HL_EOL) +#define MT_FLAG_EXTERNAL_MASK (MT_FLAG_DECOR_MASK | MT_FLAG_RIGHT_GRAVITY | 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 #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; } +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) { 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); return (uint16_t)((right_gravity ? MT_FLAG_RIGHT_GRAVITY : 0) - | (decor_level << MT_FLAG_DECOR_OFFSET) - | (no_undo ? MT_FLAG_NO_UNDO : 0)); + | (hl_eol ? MT_FLAG_HL_EOL : 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; diff --git a/test/functional/api/extmark_spec.lua b/test/functional/api/extmark_spec.lua index 133ec67942..6e00be611d 100644 --- a/test/functional/api/extmark_spec.lua +++ b/test/functional/api/extmark_spec.lua @@ -1599,12 +1599,70 @@ describe('API/extmarks', function() set_extmark(ns, 3, 0, 0, { sign_text = '>>' }) set_extmark(ns, 4, 0, 0, { virt_text = {{'text', '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({{ 3, 0, 0 }}, get_extmarks(-1, 0, -1, { type = 'sign' })) eq({{ 4, 0, 0 }}, get_extmarks(-1, 0, -1, { type = 'virt_text' })) eq({{ 5, 0, 0 }}, get_extmarks(-1, 0, -1, { type = 'virt_lines' })) end) + + it("invalidated marks are deleted", function() + screen = Screen.new(40, 6) + screen:attach() + feed('dd6iaaa bbb cccgg') + 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) describe('Extmarks buffer api with many marks', function()