From 511f12bd9e1ca1f53ee281b753a8757b5b0c703f Mon Sep 17 00:00:00 2001 From: Tamer Cuba Date: Tue, 8 Sep 2026 17:19:39 -0300 Subject: [PATCH 1/2] fix: reload open buffer via checktime when server edits a file shown in sidebar --- lua/eca/sidebar.lua | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/lua/eca/sidebar.lua b/lua/eca/sidebar.lua index 895f8dd..38857e3 100644 --- a/lua/eca/sidebar.lua +++ b/lua/eca/sidebar.lua @@ -1302,7 +1302,18 @@ function M:handle_chat_content_received(params) if details and type(details) == "table" and details.type == "fileChange" then local path = details.path if path and path ~= "" then - local filename = vim.fn.fnamemodify(path, ":t") + local absolute_path = vim.fn.fnamemodify(path, ":p") + + -- If this file is already loaded in a buffer, refresh it so users see + -- server-side edits immediately without needing :e or :checktime. + local bufnr = vim.fn.bufnr(absolute_path) + if bufnr ~= -1 and vim.api.nvim_buf_is_loaded(bufnr) then + vim.api.nvim_buf_call(bufnr, function() + vim.cmd("checktime") + end) + end + + local filename = vim.fn.fnamemodify(absolute_path, ":t") if filename and filename ~= "" then -- Avoid duplicating the filename if it is already present if tool_text and tool_text ~= "" then From d8bc02b1473138c08356ed063a2f23dbea364939 Mon Sep 17 00:00:00 2001 From: Tamer Cuba Date: Wed, 9 Sep 2026 09:02:21 -0300 Subject: [PATCH 2/2] feature: adding tests --- tests/test_sidebar_buffer_reload.lua | 192 +++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 tests/test_sidebar_buffer_reload.lua diff --git a/tests/test_sidebar_buffer_reload.lua b/tests/test_sidebar_buffer_reload.lua new file mode 100644 index 0000000..63a4971 --- /dev/null +++ b/tests/test_sidebar_buffer_reload.lua @@ -0,0 +1,192 @@ +local MiniTest = require("mini.test") +local eq = MiniTest.expect.equality +local child = MiniTest.new_child_neovim() + +local function flush(ms) + vim.uv.sleep(ms or 80) + child.api.nvim_eval("1") +end + +local function setup_env() + _G.Server = require("eca.server").new() + _G.State = require("eca.state").new() + _G.Mediator = require("eca.mediator").new(_G.Server, _G.State) + _G.Sidebar = require("eca.sidebar").new(1, _G.Mediator) + + _G.Sidebar:open() +end + +local T = MiniTest.new_set({ + hooks = { + pre_case = function() + child.restart({ "-u", "scripts/minimal_init.lua" }) + child.lua_func(setup_env) + end, + post_case = function() + child.lua([[ + if _G.Sidebar then _G.Sidebar:close() end + if _G.test_tmpfile then + os.remove(_G.test_tmpfile) + _G.test_tmpfile = nil + end + ]]) + end, + post_once = child.stop, + }, +}) + +local function report_file_change(path) + child.lua(string.format( + [[ + Sidebar:handle_chat_content_received({ + chatId = 'chat-reload', + content = { + type = 'toolCalled', + id = 'tool-reload', + name = 'write_file', + summary = 'Apply edit', + details = { type = 'fileChange', path = %q, diff = '+updated' }, + }, + }) + ]], + path + )) +end + +T["buffer reload on server file change"] = MiniTest.new_set() + +T["buffer reload on server file change"]["reloads a loaded buffer when the server edits its file"] = function() + child.lua([[ + local tmpfile = vim.fn.tempname() .. '.txt' + vim.fn.writefile({ 'original content' }, tmpfile) + _G.test_tmpfile = tmpfile + + vim.cmd('edit ' .. vim.fn.fnameescape(tmpfile)) + _G.test_bufnr = vim.api.nvim_get_current_buf() + + -- Simulate the server writing new content behind Neovim's back, and + -- force the mtime forward so :checktime reliably detects the change + -- even on filesystems with coarse mtime resolution. + vim.fn.writefile({ 'updated content from server' }, tmpfile) + local future = os.time() + 5 + vim.uv.fs_utime(tmpfile, future, future) + ]]) + + local tmpfile = child.lua_get("_G.test_tmpfile") + report_file_change(tmpfile) + flush(150) + + local lines = child.lua_get("vim.api.nvim_buf_get_lines(_G.test_bufnr, 0, -1, false)") + eq(lines, { "updated content from server" }) +end + +T["buffer reload on server file change"]["does not error when the changed file has no loaded buffer"] = function() + child.lua([[ + local tmpfile = vim.fn.tempname() .. '.txt' + vim.fn.writefile({ 'not open in any buffer' }, tmpfile) + _G.test_tmpfile = tmpfile + ]]) + + local tmpfile = child.lua_get("_G.test_tmpfile") + + local ok = child.lua_get(string.format( + [[(pcall(function() + Sidebar:handle_chat_content_received({ + chatId = 'chat-reload', + content = { + type = 'toolCalled', + id = 'tool-reload', + name = 'write_file', + summary = 'Apply edit', + details = { type = 'fileChange', path = %q, diff = '+updated' }, + }, + }) + end))]], + tmpfile + )) + + eq(ok, true) +end + +T["buffer reload on server file change"]["does not discard unsaved local edits in the buffer"] = function() + child.lua([[ + local tmpfile = vim.fn.tempname() .. '.txt' + vim.fn.writefile({ 'original content' }, tmpfile) + _G.test_tmpfile = tmpfile + + vim.cmd('edit ' .. vim.fn.fnameescape(tmpfile)) + _G.test_bufnr = vim.api.nvim_get_current_buf() + + -- Make an unsaved local edit before the server writes to the file. + vim.api.nvim_buf_set_lines(_G.test_bufnr, 0, -1, false, { 'local unsaved edit' }) + + vim.fn.writefile({ 'updated content from server' }, tmpfile) + local future = os.time() + 5 + vim.uv.fs_utime(tmpfile, future, future) + ]]) + + local tmpfile = child.lua_get("_G.test_tmpfile") + report_file_change(tmpfile) + flush(150) + + local lines = child.lua_get("vim.api.nvim_buf_get_lines(_G.test_bufnr, 0, -1, false)") + eq(lines, { "local unsaved edit" }) +end + +T["buffer reload on server file change"]["resolves a relative path to the loaded buffer's absolute path"] = function() + child.lua([[ + local tmpfile = vim.fn.tempname() .. '.txt' + vim.fn.writefile({ 'original content' }, tmpfile) + _G.test_tmpfile = tmpfile + + vim.cmd('edit ' .. vim.fn.fnameescape(tmpfile)) + _G.test_bufnr = vim.api.nvim_get_current_buf() + + vim.fn.writefile({ 'updated content from server' }, tmpfile) + local future = os.time() + 5 + vim.uv.fs_utime(tmpfile, future, future) + + -- The server may report a path relative to the current directory + -- rather than an absolute one. + _G.test_relative_path = vim.fn.fnamemodify(tmpfile, ':.') + ]]) + + local relative_path = child.lua_get("_G.test_relative_path") + report_file_change(relative_path) + flush(150) + + local lines = child.lua_get("vim.api.nvim_buf_get_lines(_G.test_bufnr, 0, -1, false)") + eq(lines, { "updated content from server" }) +end + +T["buffer reload on server file change"]["still appends the filename to the tool call summary"] = function() + child.lua([[ + local tmpfile = vim.fn.tempname() .. '.txt' + vim.fn.writefile({ 'original content' }, tmpfile) + _G.test_tmpfile = tmpfile + + vim.cmd('edit ' .. vim.fn.fnameescape(tmpfile)) + _G.test_bufnr = vim.api.nvim_get_current_buf() + + vim.fn.writefile({ 'updated content from server' }, tmpfile) + local future = os.time() + 5 + vim.uv.fs_utime(tmpfile, future, future) + ]]) + + local tmpfile = child.lua_get("_G.test_tmpfile") + report_file_change(tmpfile) + flush(150) + + child.lua([[ + local call = Sidebar._tool_calls[1] + local buf = Sidebar.containers.chat.bufnr + _G.file_summary_header = call and vim.api.nvim_buf_get_lines(buf, call.header_line - 1, call.header_line, false)[1] or '' + ]]) + + local header = child.lua_get("_G.file_summary_header") + local expected_name = child.lua_get(string.format("vim.fn.fnamemodify(%q, ':t')", tmpfile)) + local has_filename = child.lua_get(string.format("string.find(..., %q, 1, true) ~= nil", expected_name), { header }) + eq(has_filename, true) +end + +return T