aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSergey Slipchenko <faergeek@gmail.com>2023-09-11 08:16:03 +0400
committerSergey Slipchenko <faergeek@gmail.com>2023-09-11 08:16:03 +0400
commitd22172f36bbe147f3aa6b76a1c43ae445f481c2e (patch)
tree882356fe01d9dde4447ecd2bfe43bcfc44e6c3d1
parent2b475cb5cc2196a32085fbbdfd7357cbb02a1cb0 (diff)
downloadrneovim-d22172f36bbe147f3aa6b76a1c43ae445f481c2e.tar.gz
rneovim-d22172f36bbe147f3aa6b76a1c43ae445f481c2e.tar.bz2
rneovim-d22172f36bbe147f3aa6b76a1c43ae445f481c2e.zip
fix(api): more intuitive cursor updates in nvim_buf_set_text
Fixes #22526
-rw-r--r--runtime/doc/api.txt3
-rw-r--r--runtime/lua/vim/_meta/api.lua1
-rw-r--r--runtime/lua/vim/lsp/util.lua42
-rw-r--r--src/nvim/api/buffer.c85
-rw-r--r--test/functional/api/buffer_spec.lua704
-rw-r--r--test/functional/plugin/lsp_spec.lua2
6 files changed, 790 insertions, 47 deletions
diff --git a/runtime/doc/api.txt b/runtime/doc/api.txt
index 7dd760b6a5..c0bea52513 100644
--- a/runtime/doc/api.txt
+++ b/runtime/doc/api.txt
@@ -2438,6 +2438,8 @@ nvim_buf_set_text({buffer}, {start_row}, {start_col}, {end_row}, {end_col},
Prefer |nvim_buf_set_lines()| if you are only adding or deleting entire
lines.
+ Prefer |nvim_put()| if you want to insert text at the cursor position.
+
Attributes: ~
not allowed when |textlock| is active
@@ -2451,6 +2453,7 @@ nvim_buf_set_text({buffer}, {start_row}, {start_col}, {end_row}, {end_col},
See also: ~
• |nvim_buf_set_lines()|
+ • |nvim_put()|
nvim_buf_set_var({buffer}, {name}, {value}) *nvim_buf_set_var()*
Sets a buffer-scoped (b:) variable
diff --git a/runtime/lua/vim/_meta/api.lua b/runtime/lua/vim/_meta/api.lua
index c46b604b90..6c4e6c04d9 100644
--- a/runtime/lua/vim/_meta/api.lua
+++ b/runtime/lua/vim/_meta/api.lua
@@ -621,6 +621,7 @@ function vim.api.nvim_buf_set_option(buffer, name, value) end
--- range, use `replacement = {}`.
--- Prefer `nvim_buf_set_lines()` if you are only adding or deleting entire
--- lines.
+--- Prefer `nvim_put()` if you want to insert text at the cursor position.
---
--- @param buffer integer Buffer handle, or 0 for current buffer
--- @param start_row integer First line index
diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua
index e76fd15612..54721865b7 100644
--- a/runtime/lua/vim/lsp/util.lua
+++ b/runtime/lua/vim/lsp/util.lua
@@ -454,23 +454,6 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding)
end
end)
- -- Some LSP servers are depending on the VSCode behavior.
- -- The VSCode will re-locate the cursor position after applying TextEdit so we also do it.
- local is_current_buf = api.nvim_get_current_buf() == bufnr or bufnr == 0
- local cursor = (function()
- if not is_current_buf then
- return {
- row = -1,
- col = -1,
- }
- end
- local cursor = api.nvim_win_get_cursor(0)
- return {
- row = cursor[1] - 1,
- col = cursor[2],
- }
- end)()
-
-- save and restore local marks since they get deleted by nvim_buf_set_lines
local marks = {}
for _, m in pairs(vim.fn.getmarklist(bufnr or vim.api.nvim_get_current_buf())) do
@@ -480,7 +463,6 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding)
end
-- Apply text edits.
- local is_cursor_fixed = false
local has_eol_text_edit = false
for _, text_edit in ipairs(text_edits) do
-- Normalize line ending
@@ -527,20 +509,6 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding)
e.end_col = math.min(last_line_len, e.end_col)
api.nvim_buf_set_text(bufnr, e.start_row, e.start_col, e.end_row, e.end_col, e.text)
-
- -- Fix cursor position.
- local row_count = (e.end_row - e.start_row) + 1
- if e.end_row < cursor.row then
- cursor.row = cursor.row + (#e.text - row_count)
- is_cursor_fixed = true
- elseif e.end_row == cursor.row and e.end_col <= cursor.col then
- cursor.row = cursor.row + (#e.text - row_count)
- cursor.col = #e.text[#e.text] + (cursor.col - e.end_col)
- if #e.text == 1 then
- cursor.col = cursor.col + e.start_col
- end
- is_cursor_fixed = true
- end
end
end
@@ -560,16 +528,6 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding)
end
end
- -- Apply fixed cursor position.
- if is_cursor_fixed then
- local is_valid_cursor = true
- is_valid_cursor = is_valid_cursor and cursor.row < max
- is_valid_cursor = is_valid_cursor and cursor.col <= #(get_line(bufnr, cursor.row) or '')
- if is_valid_cursor then
- api.nvim_win_set_cursor(0, { cursor.row + 1, cursor.col })
- end
- end
-
-- Remove final line if needed
local fix_eol = has_eol_text_edit
fix_eol = fix_eol and (vim.bo[bufnr].eol or (vim.bo[bufnr].fixeol and not vim.bo[bufnr].binary))
diff --git a/src/nvim/api/buffer.c b/src/nvim/api/buffer.c
index b8cb09ceb3..baac694848 100644
--- a/src/nvim/api/buffer.c
+++ b/src/nvim/api/buffer.c
@@ -504,7 +504,10 @@ end:
///
/// Prefer |nvim_buf_set_lines()| if you are only adding or deleting entire lines.
///
+/// Prefer |nvim_put()| if you want to insert text at the cursor position.
+///
/// @see |nvim_buf_set_lines()|
+/// @see |nvim_put()|
///
/// @param channel_id
/// @param buffer Buffer handle, or 0 for current buffer
@@ -725,11 +728,12 @@ void nvim_buf_set_text(uint64_t channel_id, Buffer buffer, Integer start_row, In
FOR_ALL_TAB_WINDOWS(tp, win) {
if (win->w_buffer == buf) {
- // adjust cursor like an extmark ( i e it was inside last_part_len)
- if (win->w_cursor.lnum == end_row && win->w_cursor.col > end_col) {
- win->w_cursor.col -= col_extent - (colnr_T)last_item.size;
+ if (win->w_cursor.lnum >= start_row && win->w_cursor.lnum <= end_row) {
+ fix_cursor_cols(win, (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);
+ } else {
+ fix_cursor(win, (linenr_T)start_row, (linenr_T)end_row, (linenr_T)extra);
}
- fix_cursor(win, (linenr_T)start_row, (linenr_T)end_row, (linenr_T)extra);
}
}
@@ -1339,6 +1343,79 @@ static void fix_cursor(win_T *win, linenr_T lo, linenr_T hi, linenr_T extra)
invalidate_botline(win);
}
+/// Fix cursor position after replacing text
+/// between (start_row, start_col) and (end_row, end_col).
+///
+/// win->w_cursor.lnum is assumed to be >= start_row and <= end_row.
+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_win 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 = (colnr_T)strlen(ml_get_buf(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;
+ }
+ }
+ }
+
+ check_cursor_col_win(win);
+ changed_cline_bef_curs(win);
+ invalidate_botline(win);
+}
+
/// Initialise a string array either:
/// - on the Lua stack (as a table) (if lstate is not NULL)
/// - as an API array object (if lstate is NULL).
diff --git a/test/functional/api/buffer_spec.lua b/test/functional/api/buffer_spec.lua
index 292e5a2d56..9833ebee4c 100644
--- a/test/functional/api/buffer_spec.lua
+++ b/test/functional/api/buffer_spec.lua
@@ -848,6 +848,710 @@ describe('api/buf', function()
eq({1, 4}, meths.win_get_cursor(win2))
end)
+ describe('when text is being added right at cursor position #22526', function()
+ it('updates the cursor position in NORMAL mode', function()
+ insert([[
+ abcd]])
+
+ -- position the cursor on 'c'
+ curwin('set_cursor', {1, 2})
+ -- add 'xxx' before 'c'
+ set_text(0, 2, 0, 2, {'xxx'})
+ eq({'abxxxcd'}, get_lines(0, -1, true))
+ -- cursor should be on 'c'
+ eq({1, 5}, curwin('get_cursor'))
+ end)
+
+ it('updates the cursor position only in non-current window when in INSERT mode', function()
+ insert([[
+ abcd]])
+
+ -- position the cursor on 'c'
+ curwin('set_cursor', {1, 2})
+ -- open vertical split
+ feed('<c-w>v')
+ -- get into INSERT mode to treat cursor
+ -- as being after 'b', not on 'c'
+ feed('i')
+ -- add 'xxx' between 'b' and 'c'
+ set_text(0, 2, 0, 2, {'xxx'})
+ eq({'abxxxcd'}, get_lines(0, -1, true))
+ -- in the current window cursor should stay after 'b'
+ eq({1, 2}, curwin('get_cursor'))
+ -- quit INSERT mode
+ feed('<esc>')
+ -- close current window
+ feed('<c-w>c')
+ -- in another window cursor should be on 'c'
+ eq({1, 5}, curwin('get_cursor'))
+ end)
+ end)
+
+ describe('when text is being deleted right at cursor position', function()
+ it('leaves cursor at the same position in NORMAL mode', function()
+ insert([[
+ abcd]])
+
+ -- position the cursor on 'b'
+ curwin('set_cursor', {1, 1})
+ -- delete 'b'
+ set_text(0, 1, 0, 2, {})
+ eq({'acd'}, get_lines(0, -1, true))
+ -- cursor is now on 'c'
+ eq({1, 1}, curwin('get_cursor'))
+ end)
+
+ it('leaves cursor at the same position in INSERT mode in current and non-current window', function()
+ insert([[
+ abcd]])
+
+ -- position the cursor on 'b'
+ curwin('set_cursor', {1, 1})
+ -- open vertical split
+ feed('<c-w>v')
+ -- get into INSERT mode to treat cursor
+ -- as being after 'a', not on 'b'
+ feed('i')
+ -- delete 'b'
+ set_text(0, 1, 0, 2, {})
+ eq({'acd'}, get_lines(0, -1, true))
+ -- cursor in the current window should stay after 'a'
+ eq({1, 1}, curwin('get_cursor'))
+ -- quit INSERT mode
+ feed('<esc>')
+ -- close current window
+ feed('<c-w>c')
+ -- cursor in non-current window should stay on 'c'
+ eq({1, 1}, curwin('get_cursor'))
+ end)
+ end)
+
+ describe('when cursor is inside replaced row range', function()
+ it('keeps cursor at the same position if cursor is at start_row, but before start_col', function()
+ insert([[
+ This should be first
+ then there is a line we do not want
+ and finally the last one]])
+
+ -- position the cursor on ' ' before 'first'
+ curwin('set_cursor', {1, 14})
+
+ set_text(0, 15, 2, 11, {
+ 'the line we do not want',
+ 'but hopefully',
+ })
+
+ eq({
+ 'This should be the line we do not want',
+ 'but hopefully the last one',
+ }, get_lines(0, -1, true))
+ -- cursor should stay at the same position
+ eq({1, 14}, curwin('get_cursor'))
+ end)
+
+ it('keeps cursor at the same position if cursor is at start_row and column is still valid', function()
+ insert([[
+ This should be first
+ then there is a line we do not want
+ and finally the last one]])
+
+ -- position the cursor on 'f' in 'first'
+ curwin('set_cursor', {1, 15})
+
+ set_text(0, 15, 2, 11, {
+ 'the line we do not want',
+ 'but hopefully',
+ })
+
+ eq({
+ 'This should be the line we do not want',
+ 'but hopefully the last one',
+ }, get_lines(0, -1, true))
+ -- cursor should stay at the same position
+ eq({1, 15}, curwin('get_cursor'))
+ end)
+
+ it('adjusts cursor column to keep it valid if start_row got smaller', function()
+ insert([[
+ This should be first
+ then there is a line we do not want
+ and finally the last one]])
+
+ -- position the cursor on 't' in 'first'
+ curwin('set_cursor', {1, 19})
+
+ local cursor = exec_lua([[
+ vim.api.nvim_buf_set_text(0, 0, 15, 2, 24, {'last'})
+ return vim.api.nvim_win_get_cursor(0)
+ ]])
+
+ eq({ 'This should be last' }, get_lines(0, -1, true))
+ -- cursor should end up on 't' in 'last'
+ eq({1, 18}, curwin('get_cursor'))
+ -- immediate call to nvim_win_get_cursor should have returned the same position
+ eq({1, 18}, cursor)
+ end)
+
+ it('adjusts cursor column to keep it valid if start_row got smaller in INSERT mode', function()
+ insert([[
+ This should be first
+ then there is a line we do not want
+ and finally the last one]])
+
+ -- position the cursor on 't' in 'first'
+ curwin('set_cursor', {1, 19})
+ -- enter INSERT mode to treat cursor as being after 't'
+ feed('a')
+
+ local cursor = exec_lua([[
+ vim.api.nvim_buf_set_text(0, 0, 15, 2, 24, {'last'})
+ return vim.api.nvim_win_get_cursor(0)
+ ]])
+
+ eq({ 'This should be last' }, get_lines(0, -1, true))
+ -- cursor should end up after 't' in 'last'
+ eq({1, 19}, curwin('get_cursor'))
+ -- immediate call to nvim_win_get_cursor should have returned the same position
+ eq({1, 19}, cursor)
+ end)
+
+ it('adjusts cursor column to keep it valid in a row after start_row if it got smaller', function()
+ insert([[
+ This should be first
+ then there is a line we do not want
+ and finally the last one]])
+
+ -- position the cursor on 'w' in 'want'
+ curwin('set_cursor', {2, 31})
+
+ local cursor = exec_lua([[
+ vim.api.nvim_buf_set_text(0, 0, 15, 2, 11, {
+ '1',
+ 'then 2',
+ 'and then',
+ })
+ return vim.api.nvim_win_get_cursor(0)
+ ]])
+
+ eq({
+ 'This should be 1',
+ 'then 2',
+ 'and then the last one',
+ }, get_lines(0, -1, true))
+ -- cursor column should end up at the end of a row
+ eq({2, 5}, curwin('get_cursor'))
+ -- immediate call to nvim_win_get_cursor should have returned the same position
+ eq({2, 5}, cursor)
+ end)
+
+ it('adjusts cursor column to keep it valid in a row after start_row if it got smaller in INSERT mode', function()
+ insert([[
+ This should be first
+ then there is a line we do not want
+ and finally the last one]])
+
+ -- position the cursor on 'w' in 'want'
+ curwin('set_cursor', {2, 31})
+ -- enter INSERT mode
+ feed('a')
+
+ local cursor = exec_lua([[
+ vim.api.nvim_buf_set_text(0, 0, 15, 2, 11, {
+ '1',
+ 'then 2',
+ 'and then',
+ })
+ return vim.api.nvim_win_get_cursor(0)
+ ]])
+
+ eq({
+ 'This should be 1',
+ 'then 2',
+ 'and then the last one',
+ }, get_lines(0, -1, true))
+ -- cursor column should end up at the end of a row
+ eq({2, 6}, curwin('get_cursor'))
+ -- immediate call to nvim_win_get_cursor should have returned the same position
+ eq({2, 6}, cursor)
+ end)
+
+ it('adjusts cursor line and column to keep it inside replacement range', function()
+ insert([[
+ This should be first
+ then there is a line we do not want
+ and finally the last one]])
+
+ -- position the cursor on 'n' in 'finally'
+ curwin('set_cursor', {3, 6})
+
+ local cursor = exec_lua([[
+ vim.api.nvim_buf_set_text(0, 0, 15, 2, 11, {
+ 'the line we do not want',
+ 'but hopefully',
+ })
+ return vim.api.nvim_win_get_cursor(0)
+ ]])
+
+ eq({
+ 'This should be the line we do not want',
+ 'but hopefully the last one',
+ }, get_lines(0, -1, true))
+ -- cursor should end up on 'y' in 'hopefully'
+ -- to stay in the range, because it got smaller
+ eq({2, 12}, curwin('get_cursor'))
+ -- immediate call to nvim_win_get_cursor should have returned the same position
+ eq({2, 12}, cursor)
+ end)
+
+ it('adjusts cursor line and column if replacement is empty', function()
+ insert([[
+ This should be first
+ then there is a line we do not want
+ and finally the last one]])
+
+ -- position the cursor on 'r' in 'there'
+ curwin('set_cursor', {2, 8})
+
+ local cursor = exec_lua([[
+ vim.api.nvim_buf_set_text(0, 0, 15, 2, 12, {})
+ return vim.api.nvim_win_get_cursor(0)
+ ]])
+
+ eq({ 'This should be the last one' }, get_lines(0, -1, true))
+ -- cursor should end up on the next column after deleted range
+ eq({1, 15}, curwin('get_cursor'))
+ -- immediate call to nvim_win_get_cursor should have returned the same position
+ eq({1, 15}, cursor)
+ end)
+
+ it('adjusts cursor line and column if replacement is empty and start_col == 0', function()
+ insert([[
+ This should be first
+ then there is a line we do not want
+ and finally the last one]])
+
+ -- position the cursor on 'r' in 'there'
+ curwin('set_cursor', {2, 8})
+
+ local cursor = exec_lua([[
+ vim.api.nvim_buf_set_text(0, 0, 0, 2, 4, {})
+ return vim.api.nvim_win_get_cursor(0)
+ ]])
+
+ eq({ 'finally the last one' }, get_lines(0, -1, true))
+ -- cursor should end up in column 0
+ eq({1, 0}, curwin('get_cursor'))
+ -- immediate call to nvim_win_get_cursor should have returned the same position
+ eq({1, 0}, cursor)
+ end)
+
+ it('adjusts cursor column if replacement ends at cursor row, after cursor column', function()
+ insert([[
+ This should be first
+ then there is a line we do not want
+ and finally the last one]])
+
+ -- position the cursor on 'y' in 'finally'
+ curwin('set_cursor', {3, 10})
+ set_text(0, 15, 2, 11, { '1', 'this 2', 'and then' })
+
+ eq({
+ 'This should be 1',
+ 'this 2',
+ 'and then the last one',
+ }, get_lines(0, -1, true))
+ -- cursor should end up on 'n' in 'then'
+ eq({3, 7}, curwin('get_cursor'))
+ end)
+
+ it('adjusts cursor column if replacement ends at cursor row, at cursor column in INSERT mode', function()
+ insert([[
+ This should be first
+ then there is a line we do not want
+ and finally the last one]])
+
+ -- position the cursor on 'y' at 'finally'
+ curwin('set_cursor', {3, 10})
+ -- enter INSERT mode to treat cursor as being between 'l' and 'y'
+ feed('i')
+ set_text(0, 15, 2, 11, { '1', 'this 2', 'and then' })
+
+ eq({
+ 'This should be 1',
+ 'this 2',
+ 'and then the last one',
+ }, get_lines(0, -1, true))
+ -- cursor should end up after 'n' in 'then'
+ eq({3, 8}, curwin('get_cursor'))
+ end)
+
+ it('adjusts cursor column if replacement is inside of a single line', function()
+ insert([[
+ This should be first
+ then there is a line we do not want
+ and finally the last one]])
+
+ -- position the cursor on 'y' in 'finally'
+ curwin('set_cursor', {3, 10})
+ set_text(2, 4, 2, 11, { 'then' })
+
+ eq({
+ 'This should be first',
+ 'then there is a line we do not want',
+ 'and then the last one',
+ }, get_lines(0, -1, true))
+ -- cursor should end up on 'n' in 'then'
+ eq({3, 7}, curwin('get_cursor'))
+ end)
+
+ it('does not move cursor column after end of a line', function()
+ insert([[
+ This should be the only line here
+ !!!]])
+
+ -- position cursor on the last '1'
+ curwin('set_cursor', {2, 2})
+
+ local cursor = exec_lua([[
+ vim.api.nvim_buf_set_text(0, 0, 33, 1, 3, {})
+ return vim.api.nvim_win_get_cursor(0)
+ ]])
+
+ eq({ 'This should be the only line here' }, get_lines(0, -1, true))
+ -- cursor should end up on '!'
+ eq({1, 32}, curwin('get_cursor'))
+ -- immediate call to nvim_win_get_cursor should have returned the same position
+ eq({1, 32}, cursor)
+ end)
+
+ it('does not move cursor column before start of a line', function()
+ insert('\n!!!')
+
+ -- position cursor on the last '1'
+ curwin('set_cursor', {2, 2})
+
+ local cursor = exec_lua([[
+ vim.api.nvim_buf_set_text(0, 0, 0, 1, 3, {})
+ return vim.api.nvim_win_get_cursor(0)
+ ]])
+
+ eq({ '' }, get_lines(0, -1, true))
+ -- cursor should end up on '!'
+ eq({1, 0}, curwin('get_cursor'))
+ -- immediate call to nvim_win_get_cursor should have returned the same position
+ eq({1, 0}, cursor)
+ end)
+
+ describe('with virtualedit', function()
+ it('adjusts cursor line and column to keep it inside replacement range if cursor is not after eol', function()
+ insert([[
+ This should be first
+ then there is a line we do not want
+ and finally the last one]])
+
+ -- position cursor on 't' in 'want'
+ curwin('set_cursor', {2, 34})
+ -- turn on virtualedit
+ command('set virtualedit=all')
+
+ local cursor = exec_lua([[
+ vim.api.nvim_buf_set_text(0, 0, 15, 2, 11, {
+ 'the line we do not want',
+ 'but hopefully',
+ })
+ return vim.api.nvim_win_get_cursor(0)
+ ]])
+
+ eq({
+ 'This should be the line we do not want',
+ 'but hopefully the last one',
+ }, get_lines(0, -1, true))
+ -- cursor should end up on 'y' in 'hopefully'
+ -- to stay in the range
+ eq({2, 12}, curwin('get_cursor'))
+ -- immediate call to nvim_win_get_cursor should have returned the same position
+ eq({2, 12}, cursor)
+ -- coladd should be 0
+ eq(0, exec_lua([[
+ return vim.fn.winsaveview().coladd
+ ]]))
+ end)
+
+ it('does not change cursor screen column when cursor is after eol and row got shorter', function()
+ insert([[
+ This should be first
+ then there is a line we do not want
+ and finally the last one]])
+
+ -- position cursor on 't' in 'want'
+ curwin('set_cursor', {2, 34})
+ -- turn on virtualedit
+ command('set virtualedit=all')
+ -- move cursor after eol
+ exec_lua([[
+ vim.fn.winrestview({ coladd = 5 })
+ ]])
+
+ local cursor = exec_lua([[
+ vim.api.nvim_buf_set_text(0, 0, 15, 2, 11, {
+ 'the line we do not want',
+ 'but hopefully',
+ })
+ return vim.api.nvim_win_get_cursor(0)
+ ]])
+
+ eq({
+ 'This should be the line we do not want',
+ 'but hopefully the last one',
+ }, get_lines(0, -1, true))
+ -- cursor should end up at eol of a new row
+ eq({2, 26}, curwin('get_cursor'))
+ -- immediate call to nvim_win_get_cursor should have returned the same position
+ eq({2, 26}, cursor)
+ -- coladd should be increased so that cursor stays in the same screen column
+ eq(13, exec_lua([[
+ return vim.fn.winsaveview().coladd
+ ]]))
+ end)
+
+ it('does not change cursor screen column when cursor is after eol and row got longer', function()
+ insert([[
+ This should be first
+ then there is a line we do not want
+ and finally the last one]])
+
+ -- position cursor on 't' in 'first'
+ curwin('set_cursor', {1, 19})
+ -- turn on virtualedit
+ command('set virtualedit=all')
+ -- move cursor after eol
+ exec_lua([[
+ vim.fn.winrestview({ coladd = 21 })
+ ]])
+
+ local cursor = exec_lua([[
+ vim.api.nvim_buf_set_text(0, 0, 15, 2, 11, {
+ 'the line we do not want',
+ 'but hopefully',
+ })
+ return vim.api.nvim_win_get_cursor(0)
+ ]])
+
+ eq({
+ 'This should be the line we do not want',
+ 'but hopefully the last one',
+ }, get_lines(0, -1, true))
+ -- cursor should end up at eol of a new row
+ eq({1, 38}, curwin('get_cursor'))
+ -- immediate call to nvim_win_get_cursor should have returned the same position
+ eq({1, 38}, cursor)
+ -- coladd should be increased so that cursor stays in the same screen column
+ eq(2, exec_lua([[
+ return vim.fn.winsaveview().coladd
+ ]]))
+ end)
+
+ it('does not change cursor screen column when cursor is after eol and row extended past cursor column', function()
+ insert([[
+ This should be first
+ then there is a line we do not want
+ and finally the last one]])
+
+ -- position cursor on 't' in 'first'
+ curwin('set_cursor', {1, 19})
+ -- turn on virtualedit
+ command('set virtualedit=all')
+ -- move cursor after eol just a bit
+ exec_lua([[
+ vim.fn.winrestview({ coladd = 3 })
+ ]])
+
+ local cursor = exec_lua([[
+ vim.api.nvim_buf_set_text(0, 0, 15, 2, 11, {
+ 'the line we do not want',
+ 'but hopefully',
+ })
+ return vim.api.nvim_win_get_cursor(0)
+ ]])
+
+ eq({
+ 'This should be the line we do not want',
+ 'but hopefully the last one',
+ }, get_lines(0, -1, true))
+ -- cursor should stay at the same screen column
+ eq({1, 22}, curwin('get_cursor'))
+ -- immediate call to nvim_win_get_cursor should have returned the same position
+ eq({1, 22}, cursor)
+ -- coladd should become 0
+ eq(0, exec_lua([[
+ return vim.fn.winsaveview().coladd
+ ]]))
+ end)
+
+ it('does not change cursor screen column when cursor is after eol and row range decreased', function()
+ insert([[
+ This should be first
+ then there is a line we do not want
+ and one more
+ and finally the last one]])
+
+ -- position cursor on 'e' in 'more'
+ curwin('set_cursor', {3, 11})
+ -- turn on virtualedit
+ command('set virtualedit=all')
+ -- move cursor after eol
+ exec_lua([[
+ vim.fn.winrestview({ coladd = 28 })
+ ]])
+
+ local cursor = exec_lua([[
+ vim.api.nvim_buf_set_text(0, 0, 15, 3, 11, {
+ 'the line we do not want',
+ 'but hopefully',
+ })
+ return vim.api.nvim_win_get_cursor(0)
+ ]])
+
+ eq({
+ 'This should be the line we do not want',
+ 'but hopefully the last one',
+ }, get_lines(0, -1, true))
+ -- cursor should end up at eol of a new row
+ eq({2, 26}, curwin('get_cursor'))
+ -- immediate call to nvim_win_get_cursor should have returned the same position
+ eq({2, 26}, cursor)
+ -- coladd should be increased so that cursor stays in the same screen column
+ eq(13, exec_lua([[
+ return vim.fn.winsaveview().coladd
+ ]]))
+ end)
+ end)
+ end)
+
+ describe('when cursor is at end_row and after end_col', function()
+ it('adjusts cursor column when only a newline is added or deleted', function()
+ insert([[
+ first line
+ second
+ line]])
+
+ -- position the cursor on 'i'
+ curwin('set_cursor', {3, 2})
+ set_text(1, 6, 2, 0, {})
+ eq({'first line', 'second line'}, get_lines(0, -1, true))
+ -- cursor should stay on 'i'
+ eq({2, 8}, curwin('get_cursor'))
+
+ -- add a newline back
+ set_text(1, 6, 1, 6, {'', ''})
+ eq({'first line', 'second', ' line'}, get_lines(0, -1, true))
+ -- cursor should return back to the original position
+ eq({3, 2}, curwin('get_cursor'))
+ end)
+
+ it('adjusts cursor column if the range is not bound to either start or end of a line', function()
+ insert([[
+ This should be first
+ then there is a line we do not want
+ and finally the last one]])
+
+ -- position the cursor on 'h' in 'the'
+ curwin('set_cursor', {3, 13})
+ set_text(0, 14, 2, 11, {})
+ eq({'This should be the last one'}, get_lines(0, -1, true))
+ -- cursor should stay on 'h'
+ eq({1, 16}, curwin('get_cursor'))
+ -- add deleted lines back
+ set_text(0, 14, 0, 14, {
+ ' first',
+ 'then there is a line we do not want',
+ 'and finally',
+ })
+ eq({
+ 'This should be first',
+ 'then there is a line we do not want',
+ 'and finally the last one',
+ }, get_lines(0, -1, true))
+ -- cursor should return back to the original position
+ eq({3, 13}, curwin('get_cursor'))
+ end)
+
+ it('adjusts cursor column if replacing lines in range, not just deleting and adding', function()
+ insert([[
+ This should be first
+ then there is a line we do not want
+ and finally the last one]])
+
+ -- position the cursor on 's' in 'last'
+ curwin('set_cursor', {3, 18})
+ set_text(0, 15, 2, 11, {
+ 'the line we do not want',
+ 'but hopefully',
+ })
+
+ eq({
+ 'This should be the line we do not want',
+ 'but hopefully the last one',
+ }, get_lines(0, -1, true))
+ -- cursor should stay on 's'
+ eq({2, 20}, curwin('get_cursor'))
+
+ set_text(0, 15, 1, 13, {
+ 'first',
+ 'then there is a line we do not want',
+ 'and finally',
+ })
+
+ eq({
+ 'This should be first',
+ 'then there is a line we do not want',
+ 'and finally the last one',
+ }, get_lines(0, -1, true))
+ -- cursor should return back to the original position
+ eq({3, 18}, curwin('get_cursor'))
+ end)
+
+ it('does not move cursor column after end of a line', function()
+ insert([[
+ This should be the only line here
+ ]])
+
+ -- position cursor at the empty line
+ curwin('set_cursor', {2, 0})
+
+ local cursor = exec_lua([[
+ vim.api.nvim_buf_set_text(0, 0, 33, 1, 0, {'!'})
+ return vim.api.nvim_win_get_cursor(0)
+ ]])
+
+ eq({ 'This should be the only line here!' }, get_lines(0, -1, true))
+ -- cursor should end up on '!'
+ eq({1, 33}, curwin('get_cursor'))
+ -- immediate call to nvim_win_get_cursor should have returned the same position
+ eq({1, 33}, cursor)
+ end)
+
+ it('does not move cursor column before start of a line', function()
+ insert('\n')
+
+ eq({ '', '' }, get_lines(0, -1, true))
+
+ -- position cursor on the last '1'
+ curwin('set_cursor', {2, 2})
+
+ local cursor = exec_lua([[
+ vim.api.nvim_buf_set_text(0, 0, 0, 1, 0, {''})
+ return vim.api.nvim_win_get_cursor(0)
+ ]])
+
+ eq({ '' }, get_lines(0, -1, true))
+ -- cursor should end up on '!'
+ eq({1, 0}, curwin('get_cursor'))
+ -- immediate call to nvim_win_get_cursor should have returned the same position
+ eq({1, 0}, cursor)
+ end)
+ end)
+
it('can handle NULs', function()
set_text(0, 0, 0, 0, {'ab\0cd'})
eq('ab\0cd', curbuf_depr('get_line', 0))
diff --git a/test/functional/plugin/lsp_spec.lua b/test/functional/plugin/lsp_spec.lua
index 3eb89b4556..e0a8badb67 100644
--- a/test/functional/plugin/lsp_spec.lua
+++ b/test/functional/plugin/lsp_spec.lua
@@ -1787,7 +1787,7 @@ describe('LSP', function()
eq({
'First line of text';
}, buf_lines(1))
- eq({ 1, 6 }, funcs.nvim_win_get_cursor(0))
+ eq({ 1, 17 }, funcs.nvim_win_get_cursor(0))
end)
it('fix the cursor row', function()