diff --git a/src/nvim/buffer.c b/src/nvim/buffer.c index b29e87e91b..da1e27aebf 100644 --- a/src/nvim/buffer.c +++ b/src/nvim/buffer.c @@ -757,8 +757,6 @@ void buf_clear(void) { linenr_T line_count = curbuf->b_ml.ml_line_count; extmark_free_all(curbuf); // delete any extmarks - map_destroy(int, curbuf->b_signcols.invalid); - *curbuf->b_signcols.invalid = (Map(int, SignRange)) MAP_INIT; while (!(curbuf->b_ml.ml_flags & ML_EMPTY)) { ml_delete(1, false); } @@ -929,8 +927,6 @@ static void free_buffer_stuff(buf_T *buf, int free_flags) } uc_clear(&buf->b_ucmds); // clear local user commands extmark_free_all(buf); // delete any extmarks - map_destroy(int, buf->b_signcols.invalid); - *buf->b_signcols.invalid = (Map(int, SignRange)) MAP_INIT; map_clear_mode(buf, MAP_ALL_MODES, true, false); // clear local mappings map_clear_mode(buf, MAP_ALL_MODES, true, true); // clear local abbrevs XFREE_CLEAR(buf->b_start_fenc); diff --git a/src/nvim/buffer_defs.h b/src/nvim/buffer_defs.h index f09aa70ef4..145a8435aa 100644 --- a/src/nvim/buffer_defs.h +++ b/src/nvim/buffer_defs.h @@ -685,10 +685,12 @@ struct file_buffer { // may use a different synblock_T. struct { - int max; // maximum number of signs on a single line - int max_count; // number of lines with max number of signs - bool resized; // whether max changed at start of redraw - Map(int, SignRange) invalid[1]; // map of invalid ranges to be checked + int max; // maximum number of signs on a single line + int count[SIGN_SHOW_MAX]; // number of lines with number of signs + bool resized; // whether max changed at start of redraw + bool autom; // whether 'signcolumn' is displayed in "auto:n>1" + // configured window. "b_signcols" calculation + // is skipped if false. } b_signcols; Terminal *terminal; // Terminal instance associated with the buffer diff --git a/src/nvim/decoration.c b/src/nvim/decoration.c index a3df5e704e..ddc4708740 100644 --- a/src/nvim/decoration.c +++ b/src/nvim/decoration.c @@ -207,7 +207,7 @@ void buf_put_decor_sh(buf_T *buf, DecorSignHighlight *sh, int row1, int row2) buf->b_signs++; if (sh->text[0]) { buf->b_signs_with_text++; - buf_signcols_invalidate_range(buf, row1, row2, 1); + buf_signcols_count_range(buf, row1, row2, 1, kFalse); } } } @@ -254,8 +254,11 @@ void buf_remove_decor_sh(buf_T *buf, int row1, int row2, DecorSignHighlight *sh) if (sh->text[0]) { assert(buf->b_signs_with_text > 0); buf->b_signs_with_text--; - if (row2 >= row1) { - buf_signcols_invalidate_range(buf, row1, row2, -1); + if (buf->b_signs_with_text) { + buf_signcols_count_range(buf, row1, row2, -1, kFalse); + } else { + buf->b_signcols.resized = true; + buf->b_signcols.max = buf->b_signcols.count[0] = 0; } } } @@ -785,42 +788,36 @@ DecorSignHighlight *decor_find_sign(DecorInline decor) } } -static void buf_signcols_validate_row(buf_T *buf, int count, int add) +/// Count the number of signs in a range after adding/removing a sign, or to +/// (re-)initialize a range in "b_signcols.count". +/// +/// @param add 1, -1 or 0 for an added, deleted or initialized range. +/// @param clear kFalse, kTrue or kNone for an, added/deleted, cleared, or initialized range. +void buf_signcols_count_range(buf_T *buf, int row1, int row2, int add, TriState clear) { - // If "count" is greater than current max, set it and reset "max_count". - if (count > buf->b_signcols.max) { - buf->b_signcols.max = count; - buf->b_signcols.max_count = 0; - buf->b_signcols.resized = true; - } - // If row has or had "max" signs, adjust "max_count" with sign of "add". - if (count == buf->b_signcols.max - (add < 0 ? -add : 0)) { - buf->b_signcols.max_count += (add > 0) - (add < 0); - } -} - -/// Validate a range by counting the number of overlapping signs and adjusting -/// "b_signcols" accordingly. -static void buf_signcols_validate_range(buf_T *buf, int row1, int row2, int add) -{ - if (-add == buf->b_signcols.max) { - buf->b_signcols.max_count -= (row2 + 1 - row1); - return; // max signs were removed from the range, no need to count. + if (!buf->b_signcols.autom || !buf->b_signs_with_text) { + return; } - int currow = row1; - MTPair pair = { 0 }; - MarkTreeIter itr[1]; + static int nested = 0; + // An undo/redo may trigger subsequent calls before its own kNone call. + if ((nested += clear) > (0 + (clear == kTrue))) { + return; // Avoid adding signs more than once. + } - // Allocate an array of integers holding the overlapping signs in the range. + // Allocate an array of integers holding the number of signs in the range. assert(row2 >= row1); - int *overlap = xcalloc(sizeof(int), (size_t)(row2 + 1 - row1)); + int *count = xcalloc(sizeof(int), (size_t)(row2 + 1 - row1)); + MarkTreeIter itr[1]; + MTPair pair = { 0 }; - // First find the number of overlapping signs at "row1". - marktree_itr_get_overlap(buf->b_marktree, currow, 0, itr); + // Increment count array for signs that start before "row1" but do overlap the range. + marktree_itr_get_overlap(buf->b_marktree, row1, 0, itr); while (marktree_itr_step_overlap(buf->b_marktree, itr, &pair)) { - if (!mt_invalid(pair.start) && pair.start.flags & MT_FLAG_DECOR_SIGNTEXT) { - overlap[0]++; + if ((pair.start.flags & MT_FLAG_DECOR_SIGNTEXT) && !mt_invalid(pair.start)) { + for (int i = row1; i <= MIN(row2, pair.end_pos.row); i++) { + count[i - row1]++; + } } } @@ -830,84 +827,37 @@ static void buf_signcols_validate_range(buf_T *buf, int row1, int row2, int add) if (mark.pos.row > row2) { break; } - // Finish the count at the previous row. - if (mark.pos.row != currow) { - buf_signcols_validate_row(buf, overlap[currow - row1], add); - currow = mark.pos.row; - } - // Increment overlap array for the start and range of a paired sign mark. - if (!mt_invalid(mark) && !mt_end(mark) && (mark.flags & MT_FLAG_DECOR_SIGNTEXT)) { + if ((mark.flags & MT_FLAG_DECOR_SIGNTEXT) && !mt_invalid(mark) && !mt_end(mark)) { + // Increment count array for the range of a paired sign mark. MTPos end = marktree_get_altpos(buf->b_marktree, mark, NULL); - for (int i = currow; i <= MIN(row2, end.row); i++) { - overlap[i - row1]++; + for (int i = mark.pos.row; i <= MIN(row2, end.row); i++) { + count[i - row1]++; } } marktree_itr_next(buf->b_marktree, itr); } - buf_signcols_validate_row(buf, overlap[currow - row1], add); - xfree(overlap); -} -int buf_signcols_validate(win_T *wp, buf_T *buf, bool stc_check) -{ - if (!map_size(buf->b_signcols.invalid)) { - return buf->b_signcols.max; - } - - int start; - SignRange range; - map_foreach(buf->b_signcols.invalid, start, range, { - // Leave rest of the ranges invalid if max is already at configured - // maximum or resize is detected for a 'statuscolumn' rebuild. - if ((stc_check && buf->b_signcols.resized) - || (!stc_check && range.add > 0 && buf->b_signcols.max >= wp->w_maxscwidth)) { - return wp->w_maxscwidth; + // For each row increment "b_signcols.count" at the number of counted signs, + // and decrement at the previous number of signs. These two operations are + // split in separate calls if "clear" is not kNone (surrounding a marktree splice). + for (int i = 0; i < row2 + 1 - row1; i++) { + int width = MIN(SIGN_SHOW_MAX, count[i] - add); + if (clear != kNone && width > 0) { + buf->b_signcols.count[width - 1]--; + assert(buf->b_signcols.count[width - 1] >= 0); } - buf_signcols_validate_range(buf, start, range.end, range.add); - }); - - // Check if we need to scan the entire buffer. - if (buf->b_signcols.max_count == 0) { - buf->b_signcols.max = 0; - buf->b_signcols.resized = true; - buf_signcols_validate_range(buf, 0, buf->b_ml.ml_line_count, 1); - } - - map_clear(int, buf->b_signcols.invalid); - return buf->b_signcols.max; -} - -static void buf_signcols_invalidate_range(buf_T *buf, int row1, int row2, int add) -{ - if (!buf->b_signs_with_text) { - buf->b_signcols.max = buf->b_signcols.max_count = 0; - buf->b_signcols.resized = true; - map_clear(int, buf->b_signcols.invalid); - return; - } - - // Remove an invalid range if sum of added/removed signs is now 0. - SignRange *srp = map_ref(int, SignRange)(buf->b_signcols.invalid, row1, NULL); - if (srp && srp->end == row2 && srp->add + add == 0) { - map_del(int, SignRange)(buf->b_signcols.invalid, row1, NULL); - return; - } - - // Merge with overlapping invalid range. - int start; - SignRange range; - map_foreach(buf->b_signcols.invalid, start, range, { - if (row1 <= range.end && start <= row2) { - row1 = MIN(row1, start); - row2 = MAX(row2, range.end); - break; + width = MIN(SIGN_SHOW_MAX, count[i]); + if (clear != kTrue && width > 0) { + buf->b_signcols.count[width - 1]++; + if (width > buf->b_signcols.max) { + buf->b_signcols.resized = true; + buf->b_signcols.max = width; + } } - }); + } - srp = map_put_ref(int, SignRange)(buf->b_signcols.invalid, row1, NULL, NULL); - srp->end = row2; - srp->add += add; + xfree(count); } void decor_redraw_end(DecorState *state) diff --git a/src/nvim/drawscreen.c b/src/nvim/drawscreen.c index 7f794a58d2..b49de19349 100644 --- a/src/nvim/drawscreen.c +++ b/src/nvim/drawscreen.c @@ -1201,27 +1201,30 @@ void comp_col(void) /// Redraw entire window "wp" if configured 'signcolumn' width changes. static bool win_redraw_signcols(win_T *wp) { - int width; - bool rebuild_stc = false; buf_T *buf = wp->w_buffer; - if (wp->w_minscwidth <= SCL_NO) { - if (*wp->w_p_stc) { - buf_signcols_validate(wp, buf, true); - if (buf->b_signcols.resized) { - rebuild_stc = true; - wp->w_nrwidth_line_count = 0; - } - } - width = 0; - } else if (wp->w_maxscwidth <= 1 && buf->b_signs_with_text >= (size_t)wp->w_maxscwidth) { - width = wp->w_maxscwidth; - } else { - width = MIN(wp->w_maxscwidth, buf_signcols_validate(wp, buf, false)); + if (!buf->b_signcols.autom + && (*wp->w_p_stc != NUL || (wp->w_maxscwidth > 1 && wp->w_minscwidth != wp->w_maxscwidth))) { + buf->b_signcols.autom = true; + buf_signcols_count_range(buf, 0, buf->b_ml.ml_line_count, MAXLNUM, kNone); + } + + while (buf->b_signcols.max > 0 && buf->b_signcols.count[buf->b_signcols.max - 1] == 0) { + buf->b_signcols.resized = true; + buf->b_signcols.max--; + } + + int width = MIN(wp->w_maxscwidth, buf->b_signcols.max); + bool rebuild_stc = buf->b_signcols.resized && *wp->w_p_stc != NUL; + + if (rebuild_stc) { + wp->w_nrwidth_line_count = 0; + } else if (wp->w_minscwidth == 0 && wp->w_maxscwidth == 1) { + width = buf->b_signs_with_text > 0; } int scwidth = wp->w_scwidth; - wp->w_scwidth = MAX(wp->w_minscwidth, width); + wp->w_scwidth = MAX(MAX(0, wp->w_minscwidth), width); return (wp->w_scwidth != scwidth || rebuild_stc); } diff --git a/src/nvim/extmark.c b/src/nvim/extmark.c index a5f669acde..0321a11b0f 100644 --- a/src/nvim/extmark.c +++ b/src/nvim/extmark.c @@ -521,10 +521,19 @@ void extmark_splice_impl(buf_T *buf, int start_row, colnr_T start_col, bcount_t extmark_splice_delete(buf, start_row, start_col, end_row, end_col, uvp, false, undo); } + // Remove signs inside edited region from "b_signcols.count", add after splicing. + if (old_row > 0 || new_row > 0) { + buf_signcols_count_range(buf, start_row, start_row + old_row + 1, 0, kTrue); + } + marktree_splice(buf->b_marktree, (int32_t)start_row, start_col, old_row, old_col, new_row, new_col); + if (old_row > 0 || new_row > 0) { + buf_signcols_count_range(buf, start_row, start_row + new_row + 1, 0, kNone); + } + if (undo == kExtmarkUndo) { u_header_T *uhp = u_force_get_undo_header(buf); if (!uhp) { @@ -603,10 +612,16 @@ void extmark_move_region(buf_T *buf, int start_row, colnr_T start_col, bcount_t extent_row, extent_col, extent_byte, 0, 0, 0); + int row1 = MIN(start_row, new_row); + int row2 = MAX(start_row, new_row) + extent_row; + buf_signcols_count_range(buf, row1, row2, 0, kTrue); + marktree_move_region(buf->b_marktree, start_row, start_col, extent_row, extent_col, new_row, new_col); + buf_signcols_count_range(buf, row1, row2, 0, kNone); + buf_updates_send_splice(buf, new_row, new_col, new_byte, 0, 0, 0, extent_row, extent_col, extent_byte); diff --git a/src/nvim/map.c b/src/nvim/map.c index 0011c97f9b..be6bf58daa 100644 --- a/src/nvim/map.c +++ b/src/nvim/map.c @@ -111,9 +111,6 @@ void mh_clear(MapHash *h) #define VAL_NAME(x) quasiquote(x, String) #include "nvim/map_value_impl.c.h" #undef VAL_NAME -#define VAL_NAME(x) quasiquote(x, SignRange) -#include "nvim/map_value_impl.c.h" -#undef VAL_NAME #undef KEY_NAME #define KEY_NAME(x) x##ptr_t diff --git a/src/nvim/map_defs.h b/src/nvim/map_defs.h index b85ba5acaf..f3c4e4ea95 100644 --- a/src/nvim/map_defs.h +++ b/src/nvim/map_defs.h @@ -48,7 +48,6 @@ static const uint64_t value_init_uint64_t = 0; static const int64_t value_init_int64_t = 0; static const String value_init_String = STRING_INIT; static const ColorItem value_init_ColorItem = COLOR_ITEM_INITIALIZER; -static const SignRange value_init_SignRange = SIGNRANGE_INIT; // layer 0: type non-specific code @@ -151,7 +150,6 @@ KEY_DECLS(uint32_t) KEY_DECLS(String) KEY_DECLS(HlEntry) KEY_DECLS(ColorKey) -KEY_DECLS(SignRange) MAP_DECLS(int, int) MAP_DECLS(int, ptr_t) @@ -168,7 +166,6 @@ MAP_DECLS(uint32_t, uint32_t) MAP_DECLS(String, int) MAP_DECLS(int, String) MAP_DECLS(ColorKey, ColorItem) -MAP_DECLS(int, SignRange) #define set_has(T, set, key) set_has_##T(set, key) #define set_put(T, set, key) set_put_##T(set, key, NULL) diff --git a/src/nvim/types_defs.h b/src/nvim/types_defs.h index 99f448d8a1..934159b9d9 100644 --- a/src/nvim/types_defs.h +++ b/src/nvim/types_defs.h @@ -48,13 +48,6 @@ typedef enum { typedef int64_t OptInt; -// Range entry for the "b_signcols.invalid" map in which the keys are the range start. -typedef struct { - int end; // End of the invalid range. - int add; // Number of signs added in the invalid range, negative for deleted signs. -} SignRange; -#define SIGNRANGE_INIT { 0, 0 } - enum { SIGN_WIDTH = 2, }; ///< Number of display cells for a sign in the signcolumn typedef struct file_buffer buf_T; diff --git a/src/nvim/undo.c b/src/nvim/undo.c index b08ce96568..d343fb5fa0 100644 --- a/src/nvim/undo.c +++ b/src/nvim/undo.c @@ -89,6 +89,7 @@ #include "nvim/buffer_updates.h" #include "nvim/change.h" #include "nvim/cursor.h" +#include "nvim/decoration.h" #include "nvim/drawscreen.h" #include "nvim/edit.h" #include "nvim/eval/funcs.h" @@ -2423,17 +2424,38 @@ static void u_undoredo(bool undo, bool do_buf_event) curbuf->b_op_end.lnum = curbuf->b_ml.ml_line_count; } + int row1 = MAXLNUM; + int row2 = -1; + int row3 = -1; + // Tricky: ExtmarkSavePos may come after ExtmarkSplice which does call + // buf_signcols_count_range() but then misses the yet unrestored marks. + if (curbuf->b_signcols.autom && curbuf->b_signs_with_text) { + for (int i = 0; i < (int)kv_size(curhead->uh_extmark); i++) { + ExtmarkUndoObject undo_info = kv_A(curhead->uh_extmark, i); + if (undo_info.type == kExtmarkSplice) { + ExtmarkSplice s = undo_info.data.splice; + if (s.old_row > 0 || s.new_row > 0) { + row1 = MIN(row1, s.start_row); + row2 = MAX(row2, s.start_row + (undo ? s.new_row : s.old_row) + 1); + row3 = MAX(row3, s.start_row + (undo ? s.old_row : s.new_row) + 1); + } + } + } + if (row2 != -1) { + // Remove signs inside edited region from "b_signcols.count". + buf_signcols_count_range(curbuf, row1, row2, 0, kTrue); + } + } // Adjust Extmarks - ExtmarkUndoObject undo_info; if (undo) { for (int i = (int)kv_size(curhead->uh_extmark) - 1; i > -1; i--) { - undo_info = kv_A(curhead->uh_extmark, i); + ExtmarkUndoObject undo_info = kv_A(curhead->uh_extmark, i); extmark_apply_undo(undo_info, undo); } // redo } else { for (int i = 0; i < (int)kv_size(curhead->uh_extmark); i++) { - undo_info = kv_A(curhead->uh_extmark, i); + ExtmarkUndoObject undo_info = kv_A(curhead->uh_extmark, i); extmark_apply_undo(undo_info, undo); } } @@ -2442,7 +2464,10 @@ static void u_undoredo(bool undo, bool do_buf_event) // should have all info to send a buffer-reloaing on_lines/on_bytes event buf_updates_unload(curbuf, true); } - // finish Adjusting extmarks + // Finish adjusting extmarks: add signs inside edited region to "b_signcols.count". + if (row2 != -1) { + buf_signcols_count_range(curbuf, row1, row3, 0, kNone); + } curhead->uh_entry = newlist; curhead->uh_flags = new_flags; diff --git a/test/functional/ui/decorations_spec.lua b/test/functional/ui/decorations_spec.lua index 186bf19214..0844ddf249 100644 --- a/test/functional/ui/decorations_spec.lua +++ b/test/functional/ui/decorations_spec.lua @@ -4898,14 +4898,103 @@ l5 it('correct width with multiple overlapping signs', function() screen:try_resize(20, 4) insert(example_test3) - api.nvim_buf_set_extmark(0, ns, 0, -1, {sign_text='S1', end_row=2}) - api.nvim_buf_set_extmark(0, ns, 1, -1, {sign_text='S2', end_row=2}) + api.nvim_buf_set_extmark(0, ns, 0, -1, {sign_text='S1'}) + api.nvim_buf_set_extmark(0, ns, 0, -1, {sign_text='S2', end_row=2}) + api.nvim_buf_set_extmark(0, ns, 1, -1, {sign_text='S3', end_row=2}) feed('gg') + local s1 = [[ + S1S2^l1 | + S2S3l2 | + S2S3l3 | + | + ]] + screen:expect{grid=s1} + -- Correct width when :move'ing a line with signs + command('move2') screen:expect{grid=[[ - S1{1: }^l1 | - S1S2l2 | - S1S2l3 | + S3{1: }l2 | + S1S2S3^l1 | + {1: }l3 | + | + ]]} + command('silent undo') + screen:expect{grid=s1} + command('d') + screen:expect{grid=[[ + S1S2S3^l2 | + S2S3{1: }l3 | + {1: }l4 | + | + ]]} + command('d') + screen:expect{grid=[[ + S1S2S3^l3 | + {1: }l4 | + {1: }l5 | + | + ]]} + end) + + it('correct width when adding and removing multiple signs', function() + screen:try_resize(20, 4) + insert(example_test3) + feed('gg') + command([[ + let ns = nvim_create_namespace('') + call nvim_buf_set_extmark(0, ns, 0, 0, {'sign_text':'S1', 'end_row':3}) + let s1 = nvim_buf_set_extmark(0, ns, 2, 0, {'sign_text':'S2', 'end_row':4}) + let s2 = nvim_buf_set_extmark(0, ns, 5, 0, {'sign_text':'S3'}) + let s3 = nvim_buf_set_extmark(0, ns, 6, 0, {'sign_text':'S3'}) + let s4 = nvim_buf_set_extmark(0, ns, 5, 0, {'sign_text':'S3'}) + let s5 = nvim_buf_set_extmark(0, ns, 6, 0, {'sign_text':'S3'}) + redraw! + call nvim_buf_del_extmark(0, ns, s2) + call nvim_buf_del_extmark(0, ns, s3) + call nvim_buf_del_extmark(0, ns, s4) + call nvim_buf_del_extmark(0, ns, s5) + redraw! + call nvim_buf_del_extmark(0, ns, s1) + ]]) + screen:expect{grid=[[ + S1^l1 | + S1l2 | + S1l3 | + | + ]]} + end) + + it('correct width when deleting lines', function() + screen:try_resize(20, 4) + insert(example_test3) + feed('gg') + command([[ + let ns = nvim_create_namespace('') + call nvim_buf_set_extmark(0, ns, 4, 0, {'sign_text':'S1'}) + call nvim_buf_set_extmark(0, ns, 4, 0, {'sign_text':'S2'}) + let s3 = nvim_buf_set_extmark(0, ns, 5, 0, {'sign_text':'S3'}) + call nvim_buf_del_extmark(0, ns, s3) + norm 4Gdd + ]]) + screen:expect{grid=[[ + {1: }l3 | + S1S2l5 | + {1: }^ | + | + ]]} + end) + + it('correct width when splitting lines with signs on different columns', function() + screen:try_resize(20, 4) + insert(example_test3) + feed('gg') + api.nvim_buf_set_extmark(0, ns, 0, 0, {sign_text='S1'}) + api.nvim_buf_set_extmark(0, ns, 0, 1, {sign_text='S2'}) + feed('a') + screen:expect{grid=[[ + S1l | + S2^1 | + {1: }l2 | | ]]} end) diff --git a/test/functional/ui/statuscolumn_spec.lua b/test/functional/ui/statuscolumn_spec.lua index dec696d3c3..d5aeb2c51a 100644 --- a/test/functional/ui/statuscolumn_spec.lua +++ b/test/functional/ui/statuscolumn_spec.lua @@ -521,8 +521,8 @@ describe('statuscolumn', function() command([[set stc=%6s\ %l]]) exec_lua('vim.api.nvim_buf_set_extmark(0, ns, 7, 0, {sign_text = "ð’€€"})') screen:expect([[ - {0: ð’€€ 8}^aaaaa | - {0: }{1: }{0: 9}aaaaa | + {0: ð’€€ 8 }^aaaaa | + {0: }{1: }{0: 9 }aaaaa | | ]]) end)