diff --git a/src/nvim/edit.c b/src/nvim/edit.c index df0c075306..0cbc895328 100644 --- a/src/nvim/edit.c +++ b/src/nvim/edit.c @@ -3673,23 +3673,6 @@ static void ins_del(void) AppendCharToRedobuff(K_DEL); } -// Delete one character for ins_bs(). -static void ins_bs_one(colnr_T *vcolp) -{ - dec_cursor(); - getvcol(curwin, &curwin->w_cursor, vcolp, NULL, NULL); - if (State & REPLACE_FLAG) { - // Don't delete characters before the insert point when in - // Replace mode - if (curwin->w_cursor.lnum != Insstart.lnum - || curwin->w_cursor.col >= Insstart.col) { - replace_do_bs(-1); - } - } else { - del_char(false); - } -} - /// Handle Backspace, delete-word and delete-line in Insert mode. /// /// @param c character that was typed @@ -3846,42 +3829,69 @@ static bool ins_bs(int c, int mode, int *inserted_space_p) // Handle deleting one 'shiftwidth' or 'softtabstop'. if (mode == BACKSPACE_CHAR && ((p_sta && in_indent) - || ((get_sts_value() != 0 - || tabstop_count(curbuf->b_p_vsts_array)) + || ((get_sts_value() != 0 || tabstop_count(curbuf->b_p_vsts_array)) && curwin->w_cursor.col > 0 && (*(get_cursor_pos_ptr() - 1) == TAB || (*(get_cursor_pos_ptr() - 1) == ' ' && (!*inserted_space_p || arrow_used)))))) { - colnr_T vcol; - colnr_T want_vcol; - *inserted_space_p = false; - // Compute the virtual column where we want to be. Since - // 'showbreak' may get in the way, need to get the last column of - // the previous character. - getvcol(curwin, &curwin->w_cursor, &vcol, NULL, NULL); - colnr_T start_vcol = vcol; - dec_cursor(); - getvcol(curwin, &curwin->w_cursor, NULL, NULL, &want_vcol); - inc_cursor(); - if (p_sta && in_indent) { - int ts = get_sw_value(curbuf); - want_vcol = (want_vcol / ts) * ts; + + bool const use_ts = !curwin->w_p_list || curwin->w_p_lcs_chars.tab1; + char *const line = ml_get_buf(curbuf, curwin->w_cursor.lnum); + char *const end_ptr = line + curwin->w_cursor.col; + + colnr_T vcol = 0; + colnr_T space_vcol = 0; + StrCharInfo sci = utf_ptr2StrCharInfo(line); + StrCharInfo space_sci = sci; + bool prev_space = false; + while (sci.ptr < end_ptr) { + bool cur_space = ascii_iswhite(sci.chr.value); + if (!prev_space && cur_space) { + space_sci = sci; + space_vcol = vcol; + } + vcol += charsize_nowrap(curbuf, use_ts, vcol, sci.chr.value); + sci = utfc_next(sci); + prev_space = cur_space; + } + + colnr_T want_vcol = vcol - 1; + if (want_vcol <= 0) { + want_vcol = 0; + } else if (p_sta && in_indent) { + want_vcol = want_vcol - want_vcol % get_sw_value(curbuf); } else { - want_vcol = tabstop_start(want_vcol, - get_sts_value(), - curbuf->b_p_vsts_array); + want_vcol = tabstop_start(want_vcol, get_sts_value(), curbuf->b_p_vsts_array); } - // delete characters until we are at or before want_vcol - while (vcol > want_vcol && curwin->w_cursor.col > 0 - && (cc = (uint8_t)(*(get_cursor_pos_ptr() - 1)), ascii_iswhite(cc))) { - ins_bs_one(&vcol); + while (true) { + int size = charsize_nowrap(curbuf, use_ts, space_vcol, space_sci.chr.value); + if (space_vcol + size > want_vcol) { + break; + } + space_vcol += size; + space_sci = utfc_next(space_sci); + } + colnr_T const want_col = (int)(space_sci.ptr - line); + + // Delete characters until we are at or before want_col. + while (curwin->w_cursor.col > want_col) { + dec_cursor(); + if (State & REPLACE_FLAG) { + // Don't delete characters before the insert point when in Replace mode. + if (curwin->w_cursor.lnum != Insstart.lnum + || curwin->w_cursor.col >= Insstart.col) { + replace_do_bs(-1); + } + } else { + del_char(false); + } } - // insert extra spaces until we are at want_vcol - while (vcol < want_vcol) { - // Remember the first char we inserted + // Insert extra spaces until we are at want_vcol. + for (; space_vcol < want_vcol; space_vcol++) { + // Remember the first char we inserted. if (curwin->w_cursor.lnum == Insstart_orig.lnum && curwin->w_cursor.col < Insstart_orig.col) { Insstart_orig.col = curwin->w_cursor.col; @@ -3895,13 +3905,6 @@ static bool ins_bs(int c, int mode, int *inserted_space_p) replace_push(NUL); } } - getvcol(curwin, &curwin->w_cursor, &vcol, NULL, NULL); - } - - // If we are now back where we started delete one character. Can - // happen when using 'sts' and 'linebreak'. - if (vcol >= start_vcol) { - ins_bs_one(&vcol); } } else { // Delete up to starting point, start of line or previous word. diff --git a/src/nvim/plines.c b/src/nvim/plines.c index 44b06a0403..3c1297968f 100644 --- a/src/nvim/plines.c +++ b/src/nvim/plines.c @@ -375,6 +375,18 @@ CharSize charsize_fast(CharsizeArg *csarg, colnr_T const vcol, int32_t const cur return charsize_fast_impl(csarg->win, csarg->use_tabstop, vcol, cur_char); } +/// Get the number of cells taken up on the screen at given virtual column. +int charsize_nowrap(buf_T *buf, bool use_tabstop, colnr_T vcol, int32_t cur_char) +{ + if (cur_char == TAB && use_tabstop) { + return tabstop_padding(vcol, buf->b_p_ts, buf->b_p_vts_array); + } else if (cur_char < 0) { + return kInvalidByteCells; + } else { + return char2cells(cur_char); + } +} + /// Check that virtual column "vcol" is in the rightmost column of window "wp". /// /// @param wp window diff --git a/test/functional/editor/mode_insert_spec.lua b/test/functional/editor/mode_insert_spec.lua index e96813b6f7..ebf118736f 100644 --- a/test/functional/editor/mode_insert_spec.lua +++ b/test/functional/editor/mode_insert_spec.lua @@ -8,6 +8,7 @@ local command = helpers.command local eq = helpers.eq local eval = helpers.eval local curbuf_contents = helpers.curbuf_contents +local api = helpers.api describe('insert-mode', function() before_each(function() @@ -221,4 +222,146 @@ describe('insert-mode', function() ]], } end) + + describe('backspace', function() + local function set_lines(line_b, line_e, ...) + api.nvim_buf_set_lines(0, line_b, line_e, true, { ... }) + end + local function s(count) + return (' '):rep(count) + end + + local function test_cols(expected_cols) + local cols = { { helpers.fn.col('.'), helpers.fn.virtcol('.') } } + for _ = 2, #expected_cols do + feed('') + table.insert(cols, { helpers.fn.col('.'), helpers.fn.virtcol('.') }) + end + eq(expected_cols, cols) + end + + it('works with tabs and spaces', function() + local screen = Screen.new(30, 2) + screen:attach() + command('setl ts=4 sw=4') + set_lines(0, 1, '\t' .. s(4) .. '\t' .. s(9) .. '\t a') + feed('$i') + test_cols({ + { 18, 26 }, + { 17, 25 }, + { 15, 21 }, + { 11, 17 }, + { 7, 13 }, + { 6, 9 }, + { 2, 5 }, + { 1, 1 }, + }) + end) + + it('works with varsofttabstop', function() + local screen = Screen.new(30, 2) + screen:attach() + command('setl vsts=6,2,5,3') + set_lines(0, 1, 'a\t' .. s(4) .. '\t a') + feed('$i') + test_cols({ + { 9, 18 }, + { 8, 17 }, + { 8, 14 }, + { 3, 9 }, + { 7, 7 }, + { 2, 2 }, + { 1, 1 }, + }) + end) + + it('works with tab as ^I', function() + local screen = Screen.new(30, 2) + screen:attach() + command('set list listchars=space:.') + command('setl ts=4 sw=4') + set_lines(0, 1, '\t' .. s(4) .. '\t' .. s(9) .. '\t a') + feed('$i') + test_cols({ + { 18, 21 }, + { 15, 17 }, + { 11, 13 }, + { 7, 9 }, + { 4, 5 }, + { 1, 1 }, + }) + end) + + it('works in replace mode', function() + local screen = Screen.new(50, 2) + screen:attach() + command('setl ts=8 sw=8 sts=8') + set_lines(0, 1, '\t' .. s(4) .. '\t' .. s(9) .. '\t a') + feed('$R') + test_cols({ + { 18, 34 }, + { 17, 33 }, + { 15, 25 }, + { 7, 17 }, + { 2, 9 }, + { 1, 8 }, -- last screen cell of first tab is at vcol 8 + }) + end) + + it('works with breakindent', function() + local screen = Screen.new(17, 4) + screen:attach() + command('setl ts=4 sw=4 bri briopt=min:5') + set_lines(0, 1, '\t' .. s(4) .. '\t' .. s(9) .. '\t a') + feed('$i') + test_cols({ + { 18, 50 }, + { 17, 49 }, + { 15, 33 }, + { 11, 17 }, + { 7, 13 }, + { 6, 9 }, + { 2, 5 }, + { 1, 1 }, + }) + end) + + it('works with inline virtual text', function() + local screen = Screen.new(50, 2) + screen:attach() + command('setl ts=4 sw=4') + set_lines(0, 1, '\t' .. s(4) .. '\t' .. s(9) .. '\t a') + local ns = api.nvim_create_namespace('') + local vt_opts = { virt_text = { { 'text' } }, virt_text_pos = 'inline' } + api.nvim_buf_set_extmark(0, ns, 0, 2, vt_opts) + feed('$i') + test_cols({ + { 18, 30 }, + { 17, 29 }, + { 15, 25 }, + { 11, 21 }, + { 7, 17 }, + { 6, 13 }, + { 2, 9 }, + { 1, 5 }, + }) + end) + + it("works with 'revins'", function() + local screen = Screen.new(30, 3) + screen:attach() + command('setl ts=4 sw=4 revins') + set_lines(0, 1, ('a'):rep(16), s(3) .. '\t' .. s(4) .. '\t a') + feed('j$i') + test_cols({ + { 11, 14 }, + { 10, 13 }, + { 9, 9 }, + { 5, 5 }, + { 1, 1 }, + { 1, 1 }, -- backspace on empty line does nothing + }) + eq(2, api.nvim_win_get_cursor(0)[1]) + end) + end) end)