aboutsummaryrefslogtreecommitdiff
path: root/test/functional/lua/buffer_updates_spec.lua
diff options
context:
space:
mode:
authorRiccardo Mazzarini <me@noib3.dev>2025-11-21 06:40:08 +0100
committerGitHub <noreply@github.com>2025-11-20 21:40:08 -0800
commit7da4d6abe2491b165e8cb94d4d774d8be7b23da1 (patch)
treebd032d81f2bfe193dcd90b6fcdcc07e840f2e732 /test/functional/lua/buffer_updates_spec.lua
parent7be031f397da2555e950a4acf753b1219ef7dfe1 (diff)
downloadrneovim-7da4d6abe2491b165e8cb94d4d774d8be7b23da1.tar.gz
rneovim-7da4d6abe2491b165e8cb94d4d774d8be7b23da1.tar.bz2
rneovim-7da4d6abe2491b165e8cb94d4d774d8be7b23da1.zip
fix(api): on_bytes gets stale data on :substitute #36487
Problem: `extmark_splice()` was being called before `ml_replace()`, which caused the on_bytes callback to be invoked with the old buffer text instead of the new text. Solution: store metadata for each match in a growing array, call `ml_replace()` once to update the buffer, then call `extmark_splice()` once per match. Closes https://github.com/neovim/neovim/issues/36370.
Diffstat (limited to 'test/functional/lua/buffer_updates_spec.lua')
-rw-r--r--test/functional/lua/buffer_updates_spec.lua249
1 files changed, 249 insertions, 0 deletions
diff --git a/test/functional/lua/buffer_updates_spec.lua b/test/functional/lua/buffer_updates_spec.lua
index 443b1ccc19..c5702bc624 100644
--- a/test/functional/lua/buffer_updates_spec.lua
+++ b/test/functional/lua/buffer_updates_spec.lua
@@ -1250,6 +1250,255 @@ describe('lua: nvim_buf_attach on_bytes', function()
}
end)
+ it('on_bytes sees modified buffer after substitute', function()
+ api.nvim_buf_set_lines(0, 0, -1, true, { 'Hello' })
+
+ local buffer_lines = exec_lua(function()
+ local lines
+ vim.api.nvim_buf_attach(0, false, {
+ on_bytes = function()
+ lines = vim.api.nvim_buf_get_lines(0, 0, -1, true)
+ end,
+ })
+ vim.cmd('s/llo/y/')
+ return lines
+ end)
+
+ -- Make sure on_bytes is called after the buffer is modified.
+ eq({ 'Hey' }, buffer_lines)
+ end)
+
+ it('on_bytes called multiple times for multiple substitutions on same line', function()
+ api.nvim_buf_set_lines(0, 0, -1, true, { 'Hello Hello' })
+
+ local call_count, args = exec_lua(function()
+ local count = 0
+ local args = {}
+ vim.api.nvim_buf_attach(0, false, {
+ on_bytes = function(
+ _,
+ _,
+ _,
+ start_row,
+ start_col,
+ start_byte,
+ old_row,
+ old_col,
+ old_byte,
+ new_row,
+ new_col,
+ new_byte
+ )
+ count = count + 1
+ table.insert(args, {
+ start_row = start_row,
+ start_col = start_col,
+ start_byte = start_byte,
+ old_row = old_row,
+ old_col = old_col,
+ old_byte = old_byte,
+ new_row = new_row,
+ new_col = new_col,
+ new_byte = new_byte,
+ buffer_lines = vim.api.nvim_buf_get_lines(0, 0, -1, true),
+ })
+ end,
+ })
+ vim.cmd('s/llo/y/g')
+ return count, args
+ end)
+
+ -- Should be called twice, once for each match.
+ eq(2, call_count)
+
+ -- First match: "llo" at column 2 -> "y".
+ eq({
+ start_row = 0,
+ start_col = 2,
+ start_byte = 2,
+ old_row = 0,
+ old_col = 3,
+ old_byte = 3,
+ new_row = 0,
+ new_col = 1,
+ new_byte = 1,
+ buffer_lines = { 'Hey Hey' },
+ }, args[1])
+
+ -- Second match: "llo" at column 8 (in original) -> column 6 (after first substitution).
+ eq({
+ start_row = 0,
+ start_col = 6, -- Adjusted position after first substitution.
+ start_byte = 6,
+ old_row = 0,
+ old_col = 3,
+ old_byte = 3,
+ new_row = 0,
+ new_col = 1,
+ new_byte = 1,
+ buffer_lines = { 'Hey Hey' },
+ }, args[2])
+ end)
+
+ it('on_bytes called correctly for multi-line substitutions', function()
+ api.nvim_buf_set_lines(0, 0, -1, true, { 'foo bar', 'baz qux' })
+
+ local call_count, args = exec_lua(function()
+ local count = 0
+ local args = {}
+ vim.api.nvim_buf_attach(0, false, {
+ on_bytes = function(
+ _,
+ _,
+ _,
+ start_row,
+ start_col,
+ start_byte,
+ old_row,
+ old_col,
+ old_byte,
+ new_row,
+ new_col,
+ new_byte
+ )
+ count = count + 1
+ table.insert(args, {
+ start_row = start_row,
+ start_col = start_col,
+ start_byte = start_byte,
+ old_row = old_row,
+ old_col = old_col,
+ old_byte = old_byte,
+ new_row = new_row,
+ new_col = new_col,
+ new_byte = new_byte,
+ buffer_lines = vim.api.nvim_buf_get_lines(0, 0, -1, true),
+ })
+ end,
+ })
+ vim.cmd('s/bar/X\\rY/')
+ return count, args
+ end)
+
+ -- Should be called once for the substitution.
+ eq(1, call_count)
+
+ eq({
+ start_row = 0,
+ start_col = 4,
+ start_byte = 4,
+ old_row = 0,
+ old_col = 3,
+ old_byte = 3,
+ new_row = 1,
+ new_col = 1,
+ new_byte = 3,
+ buffer_lines = { 'foo X', 'Y', 'baz qux' },
+ }, args[1])
+ end)
+
+ it('on_bytes called multiple times for global substitution creating multiple lines', function()
+ api.nvim_buf_set_lines(0, 0, -1, true, { 'foo bar baz' })
+
+ local call_count, args = exec_lua(function()
+ local count = 0
+ local args = {}
+ vim.api.nvim_buf_attach(0, false, {
+ on_bytes = function(
+ _,
+ _,
+ _,
+ start_row,
+ start_col,
+ start_byte,
+ old_row,
+ old_col,
+ old_byte,
+ new_row,
+ new_col,
+ new_byte
+ )
+ count = count + 1
+ table.insert(args, {
+ start_row = start_row,
+ start_col = start_col,
+ start_byte = start_byte,
+ old_row = old_row,
+ old_col = old_col,
+ old_byte = old_byte,
+ new_row = new_row,
+ new_col = new_col,
+ new_byte = new_byte,
+ buffer_lines = vim.api.nvim_buf_get_lines(0, 0, -1, true),
+ })
+ end,
+ })
+ -- Global substitution with newlines in replacement.
+ vim.cmd([[s/ /\r/g]])
+ return count, args
+ end)
+
+ -- Should be called once per space replacement.
+ eq(2, call_count)
+
+ eq({
+ start_row = 0,
+ start_col = 3,
+ start_byte = 3,
+ old_row = 0,
+ old_col = 1,
+ old_byte = 1,
+ new_row = 1,
+ new_col = 0,
+ new_byte = 1,
+ buffer_lines = { 'foo', 'bar', 'baz' },
+ }, args[1])
+
+ eq({
+ start_row = 1,
+ start_col = 3,
+ start_byte = 7,
+ old_row = 0,
+ old_col = 1,
+ old_byte = 1,
+ new_row = 1,
+ new_col = 0,
+ new_byte = 1,
+ buffer_lines = { 'foo', 'bar', 'baz' },
+ }, args[2])
+ end)
+
+ it(
+ 'no buffer update event is emitted while editing substitute command, only after confirmation',
+ function()
+ api.nvim_buf_set_lines(0, 0, -1, true, { 'Hello world', 'Hello Neovim' })
+
+ exec_lua(function()
+ _G.num_buffer_updates = 0
+ vim.api.nvim_buf_attach(0, false, {
+ on_bytes = function()
+ _G.num_buffer_updates = _G.num_buffer_updates + 1
+ end,
+ })
+ end)
+
+ -- Start typing the substitute command - no events should be emitted yet.
+ feed(':%s/Hello/Hi')
+ eq(0, exec_lua('return _G.num_buffer_updates'))
+
+ -- Continue editing the command - still no events.
+ feed('<BS><BS>Hey')
+ eq(0, exec_lua('return _G.num_buffer_updates'))
+
+ -- After confirming the substitution, two events should be emitted (one per line).
+ feed('<CR>')
+ eq(2, exec_lua('return _G.num_buffer_updates'))
+
+ -- Verify the buffer was actually modified.
+ eq({ 'Hey world', 'Hey Neovim' }, api.nvim_buf_get_lines(0, 0, -1, true))
+ end
+ )
+
it('flushes delbytes on join', function()
local check_events = setup_eventcheck(verify, { 'AAA', 'BBB', 'CCC' })