mirror of
https://github.com/neovim/neovim.git
synced 2026-06-16 00:31:16 +00:00
fix(api): adjust Visual position after nvim_buf_set_text #30690
Problem:
Visual selection could end up in the wrong place after
nvim_buf_set_text or nvim_buf_set_lines. In some delete cases,
Visual.lnum was already clamped before the line shift happened, so the
adjustment got skipped.
Solution:
Split fix_cursor_cols into reusable fix_pos_col logic and reuse it
for Visual updates. Also adjust Visual.lnum before changed_lines so
the shift uses the original position before final clamping.
(cherry picked from commit 450ba41436)
This commit is contained in:
committed by
github-actions[bot]
parent
3d9c7431d2
commit
781c43ea05
@@ -439,6 +439,13 @@ void nvim_buf_set_lines(uint64_t channel_id, Buffer buf, Integer start, Integer
|
||||
mark_adjust_buf(b, (linenr_T)start, (linenr_T)(end - 1), adjust, (linenr_T)extra,
|
||||
true, kMarkAdjustApi, kExtmarkNOOP);
|
||||
|
||||
if (VIsual_active && b == curbuf && VIsual.lnum >= (linenr_T)start) {
|
||||
if (VIsual.lnum >= (linenr_T)end) {
|
||||
VIsual.lnum += (linenr_T)extra;
|
||||
}
|
||||
check_visual_pos();
|
||||
}
|
||||
|
||||
extmark_splice(b, (int)start - 1, 0, (int)(end - start), 0,
|
||||
deleted_bytes, (int)new_len, 0, inserted_bytes,
|
||||
kExtmarkUndo);
|
||||
@@ -666,6 +673,12 @@ void nvim_buf_set_text(uint64_t channel_id, Buffer buf, Integer start_row, Integ
|
||||
mark_adjust_buf(b, (linenr_T)start_row, (linenr_T)end_row - 1, adjust, (linenr_T)extra,
|
||||
true, kMarkAdjustApi, kExtmarkNOOP);
|
||||
|
||||
if (VIsual_active && b == curbuf && VIsual_mode != Ctrl_V) {
|
||||
fix_pos_col(b, &VIsual, (linenr_T)start_row, (colnr_T)start_col, (linenr_T)end_row,
|
||||
(colnr_T)end_col, (linenr_T)new_len, (colnr_T)last_item.size, 1);
|
||||
check_visual_pos();
|
||||
}
|
||||
|
||||
extmark_splice(b, (int)start_row - 1, (colnr_T)start_col,
|
||||
(int)(end_row - start_row), col_extent, old_byte,
|
||||
(int)new_len - 1, (colnr_T)last_item.size, new_byte,
|
||||
@@ -1279,6 +1292,83 @@ static void fix_cursor(win_T *win, linenr_T lo, linenr_T hi, linenr_T extra)
|
||||
}
|
||||
}
|
||||
|
||||
/// Adjust pos's col/lnum after text replacement between
|
||||
/// (start_row, start_col) and (end_row, end_col).
|
||||
static void fix_pos_col(buf_T *buf, pos_T *pos, linenr_T start_row, colnr_T start_col,
|
||||
linenr_T end_row, colnr_T end_col, linenr_T new_rows,
|
||||
colnr_T new_cols_at_end_row, colnr_T mode_col_adj)
|
||||
{
|
||||
if (pos->lnum < start_row) {
|
||||
return;
|
||||
}
|
||||
|
||||
linenr_T old_rows = end_row - start_row + 1;
|
||||
linenr_T lnum_shift = new_rows - old_rows;
|
||||
|
||||
if (pos->lnum > end_row) {
|
||||
pos->lnum += lnum_shift;
|
||||
return;
|
||||
}
|
||||
|
||||
colnr_T end_row_change_start = new_rows == 1 ? start_col : 0;
|
||||
colnr_T end_row_change_end = end_row_change_start + new_cols_at_end_row;
|
||||
|
||||
// check if pos is after replaced range or not
|
||||
if (pos->lnum == end_row && pos->col + mode_col_adj > end_col) {
|
||||
// if pos is after replaced range, it's shifted
|
||||
// to keep its position the same, relative to end_col
|
||||
pos->lnum += lnum_shift;
|
||||
pos->col += end_row_change_end - end_col;
|
||||
return;
|
||||
}
|
||||
|
||||
// if pos is inside replaced range
|
||||
// and the new range got smaller,
|
||||
// it's shifted to keep it inside the new range
|
||||
//
|
||||
// if pos is before range or range did not
|
||||
// get smaller, position is not changed
|
||||
|
||||
colnr_T old_coladd = pos->coladd;
|
||||
|
||||
// it's easier to work with a single value here.
|
||||
// col and coladd are fixed by a later call
|
||||
// to check_cursor_col when necessary
|
||||
pos->col += pos->coladd;
|
||||
pos->coladd = 0;
|
||||
|
||||
linenr_T new_end_row = start_row + new_rows - 1;
|
||||
|
||||
// make sure pos row is in the new row range
|
||||
if (pos->lnum > new_end_row) {
|
||||
pos->lnum = new_end_row;
|
||||
|
||||
// don't simply move pos up, but to the end
|
||||
// of new_end_row, if it's not at or after
|
||||
// it already (in case virtualedit is active)
|
||||
// column might be additionally adjusted below
|
||||
// to keep it inside col range if needed
|
||||
colnr_T len = ml_get_buf_len(buf, new_end_row);
|
||||
if (pos->col < len) {
|
||||
pos->col = len;
|
||||
}
|
||||
}
|
||||
|
||||
// if pos is at the last row and
|
||||
// it wasn't after eol before, move it exactly
|
||||
// to end_row_change_end
|
||||
if (pos->lnum == new_end_row
|
||||
&& pos->col > end_row_change_end && old_coladd == 0) {
|
||||
pos->col = end_row_change_end;
|
||||
|
||||
// make sure pos is inside range, not after it,
|
||||
// except when doing so would move it before new range
|
||||
if (pos->col - mode_col_adj >= end_row_change_start) {
|
||||
pos->col -= mode_col_adj;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fix cursor position after replacing text
|
||||
/// between (start_row, start_col) and (end_row, end_col).
|
||||
///
|
||||
@@ -1286,66 +1376,10 @@ static void fix_cursor(win_T *win, linenr_T lo, linenr_T hi, linenr_T extra)
|
||||
static void fix_cursor_cols(win_T *win, linenr_T start_row, colnr_T start_col, linenr_T end_row,
|
||||
colnr_T end_col, linenr_T new_rows, colnr_T new_cols_at_end_row)
|
||||
{
|
||||
colnr_T mode_col_adj = win == curwin && (State & MODE_INSERT) ? 0 : 1;
|
||||
|
||||
colnr_T end_row_change_start = new_rows == 1 ? start_col : 0;
|
||||
colnr_T end_row_change_end = end_row_change_start + new_cols_at_end_row;
|
||||
|
||||
// check if cursor is after replaced range or not
|
||||
if (win->w_cursor.lnum == end_row && win->w_cursor.col + mode_col_adj > end_col) {
|
||||
// if cursor is after replaced range, it's shifted
|
||||
// to keep it's position the same, relative to end_col
|
||||
|
||||
linenr_T old_rows = end_row - start_row + 1;
|
||||
win->w_cursor.lnum += new_rows - old_rows;
|
||||
win->w_cursor.col += end_row_change_end - end_col;
|
||||
} else {
|
||||
// if cursor is inside replaced range
|
||||
// and the new range got smaller,
|
||||
// it's shifted to keep it inside the new range
|
||||
//
|
||||
// if cursor is before range or range did not
|
||||
// got smaller, position is not changed
|
||||
|
||||
colnr_T old_coladd = win->w_cursor.coladd;
|
||||
|
||||
// it's easier to work with a single value here.
|
||||
// col and coladd are fixed by a later call
|
||||
// to check_cursor_col when necessary
|
||||
win->w_cursor.col += win->w_cursor.coladd;
|
||||
win->w_cursor.coladd = 0;
|
||||
|
||||
linenr_T new_end_row = start_row + new_rows - 1;
|
||||
|
||||
// make sure cursor row is in the new row range
|
||||
if (win->w_cursor.lnum > new_end_row) {
|
||||
win->w_cursor.lnum = new_end_row;
|
||||
|
||||
// don't simply move cursor up, but to the end
|
||||
// of new_end_row, if it's not at or after
|
||||
// it already (in case virtualedit is active)
|
||||
// column might be additionally adjusted below
|
||||
// to keep it inside col range if needed
|
||||
colnr_T len = ml_get_buf_len(win->w_buffer, new_end_row);
|
||||
if (win->w_cursor.col < len) {
|
||||
win->w_cursor.col = len;
|
||||
}
|
||||
}
|
||||
|
||||
// if cursor is at the last row and
|
||||
// it wasn't after eol before, move it exactly
|
||||
// to end_row_change_end
|
||||
if (win->w_cursor.lnum == new_end_row
|
||||
&& win->w_cursor.col > end_row_change_end && old_coladd == 0) {
|
||||
win->w_cursor.col = end_row_change_end;
|
||||
|
||||
// make sure cursor is inside range, not after it,
|
||||
// except when doing so would move it before new range
|
||||
if (win->w_cursor.col - mode_col_adj >= end_row_change_start) {
|
||||
win->w_cursor.col -= mode_col_adj;
|
||||
}
|
||||
}
|
||||
}
|
||||
colnr_T mode_col_adj = (win == curwin && (State & MODE_INSERT)) ? 0 : 1;
|
||||
fix_pos_col(win->w_buffer, &win->w_cursor,
|
||||
start_row, start_col, end_row, end_col,
|
||||
new_rows, new_cols_at_end_row, mode_col_adj);
|
||||
|
||||
check_cursor_col(win);
|
||||
changed_cline_bef_curs(win);
|
||||
|
||||
@@ -1005,6 +1005,110 @@ describe('api/buf', function()
|
||||
eq({ 1, 4 }, api.nvim_win_get_cursor(win2))
|
||||
end)
|
||||
|
||||
it('keep visual select position #29558', function()
|
||||
insert([[1234]])
|
||||
api.nvim_win_set_cursor(0, { 1, 1 })
|
||||
feed('vl')
|
||||
exec_lua([[
|
||||
vim.defer_fn(function() vim.api.nvim_buf_set_text(0, 0, 0, 0, 0, { '0' }) end, 50)
|
||||
vim.wait(80)
|
||||
]])
|
||||
local mode = api.nvim_get_mode().mode
|
||||
eq({ '23' }, fn.getregion(fn.getpos('.'), fn.getpos('v'), { type = mode }))
|
||||
eq({ 'v', { '01234' } }, { mode, api.nvim_buf_get_lines(0, 0, -1, false) })
|
||||
feed('<ESC>')
|
||||
|
||||
api.nvim_buf_set_lines(0, 0, -1, false, { '1', '2', '3' })
|
||||
api.nvim_win_set_cursor(0, { 2, 0 })
|
||||
feed('v')
|
||||
exec_lua([[
|
||||
vim.defer_fn(function() vim.api.nvim_buf_set_text(0, 0, 0, 0, 0, { '0', '' }) end, 50)
|
||||
vim.wait(80)
|
||||
]])
|
||||
mode = api.nvim_get_mode().mode
|
||||
eq({ '2' }, fn.getregion(fn.getpos('.'), fn.getpos('v'), { type = mode }))
|
||||
eq({ 'v', { '0', '1', '2', '3' } }, { mode, api.nvim_buf_get_lines(0, 0, -1, false) })
|
||||
feed('<ESC>')
|
||||
|
||||
api.nvim_buf_set_lines(0, 0, -1, false, { '1', '2' })
|
||||
api.nvim_win_set_cursor(0, { 1, 0 })
|
||||
feed('vj')
|
||||
exec_lua([[
|
||||
vim.defer_fn(function() vim.api.nvim_buf_set_text(0, 0, 0, 0, 0, { '0' }) end, 50)
|
||||
vim.wait(80)
|
||||
]])
|
||||
mode = api.nvim_get_mode().mode
|
||||
eq({ '1', '2' }, fn.getregion(fn.getpos('.'), fn.getpos('v'), { type = mode }))
|
||||
eq({ 'v', { '01', '2' } }, { mode, api.nvim_buf_get_lines(0, 0, -1, false) })
|
||||
feed('<ESC>')
|
||||
|
||||
api.nvim_buf_set_lines(0, 0, -1, false, { '123' })
|
||||
api.nvim_win_set_cursor(0, { 1, 1 })
|
||||
feed('v')
|
||||
exec_lua([[
|
||||
vim.defer_fn(function() vim.api.nvim_buf_set_text(0, 0, 0, 0, 0, { '', '' }) end, 50)
|
||||
vim.wait(80)
|
||||
]])
|
||||
mode = api.nvim_get_mode().mode
|
||||
eq({ '2' }, fn.getregion(fn.getpos('.'), fn.getpos('v'), { type = mode }))
|
||||
eq({ 'v', { '', '123' } }, { mode, api.nvim_buf_get_lines(0, 0, -1, false) })
|
||||
feed('<ESC>')
|
||||
|
||||
-- Visual block mode
|
||||
api.nvim_buf_set_lines(0, 0, -1, false, { '123', '456' })
|
||||
api.nvim_win_set_cursor(0, { 1, 0 })
|
||||
feed('<C-v>jl')
|
||||
exec_lua([[
|
||||
vim.defer_fn(function() vim.api.nvim_buf_set_text(0, 0, 0, 0, 0, { '0' }) end, 50)
|
||||
vim.wait(80)
|
||||
]])
|
||||
mode = api.nvim_get_mode().mode
|
||||
eq({ '01', '45' }, fn.getregion(fn.getpos('.'), fn.getpos('v'), { type = mode }))
|
||||
eq(
|
||||
{ string.char(0x16), { '0123', '456' } },
|
||||
{ mode, api.nvim_buf_get_lines(0, 0, -1, false) }
|
||||
)
|
||||
feed('<ESC>')
|
||||
-- also test nvim_buf_set_lines inserts line above visual selection
|
||||
api.nvim_buf_set_lines(0, 0, -1, false, { '1', '2', '3' })
|
||||
api.nvim_win_set_cursor(0, { 2, 0 })
|
||||
feed('v')
|
||||
exec_lua([[
|
||||
vim.defer_fn(function() vim.api.nvim_buf_set_lines(0, 0, 0, false, { 'new' }) end, 50)
|
||||
vim.wait(80)
|
||||
]])
|
||||
mode = api.nvim_get_mode().mode
|
||||
eq({ '2' }, fn.getregion(fn.getpos('.'), fn.getpos('v'), { type = mode }))
|
||||
eq({ 'v', { 'new', '1', '2', '3' } }, { mode, api.nvim_buf_get_lines(0, 0, -1, false) })
|
||||
feed('<ESC>')
|
||||
|
||||
api.nvim_buf_set_lines(0, 0, -1, false, { '1234' })
|
||||
api.nvim_win_set_cursor(0, { 1, 1 })
|
||||
feed('vl')
|
||||
exec_lua([[
|
||||
vim.defer_fn(function()
|
||||
vim.api.nvim_buf_set_text(0, 0, 0, 0, 0, { '0', 'foo' })
|
||||
end, 50)
|
||||
vim.wait(80)
|
||||
]])
|
||||
mode = api.nvim_get_mode().mode
|
||||
eq({ '23' }, fn.getregion(fn.getpos('.'), fn.getpos('v'), { type = mode }))
|
||||
eq({ 'v', { '0', 'foo1234' } }, { mode, api.nvim_buf_get_lines(0, 0, -1, false) })
|
||||
feed('<ESC>')
|
||||
|
||||
api.nvim_buf_set_lines(0, 0, -1, false, { '1', '2', '3', '4', '5', '6', '7', '8', '9', '10' })
|
||||
api.nvim_win_set_cursor(0, { 8, 0 })
|
||||
feed('v')
|
||||
exec_lua([[
|
||||
vim.defer_fn(function() vim.api.nvim_buf_set_lines(0, 0, 5, false, {}) end, 50)
|
||||
vim.wait(80)
|
||||
]])
|
||||
mode = api.nvim_get_mode().mode
|
||||
eq({ '8' }, fn.getregion(fn.getpos('.'), fn.getpos('v'), { type = mode }))
|
||||
eq({ 'v', { '6', '7', '8', '9', '10' } }, { mode, api.nvim_buf_get_lines(0, 0, -1, false) })
|
||||
feed('<ESC>')
|
||||
end)
|
||||
|
||||
describe('when text is being added right at cursor position #22526', function()
|
||||
it('updates the cursor position in NORMAL mode', function()
|
||||
insert([[
|
||||
|
||||
Reference in New Issue
Block a user