From 6438c613db6872861d006466eed162b6049d5701 Mon Sep 17 00:00:00 2001 From: Henry Jetmundsen Date: Thu, 18 Sep 2025 14:21:23 -0700 Subject: [PATCH] fix spam notifications on deleted files --- .luacheckrc | 93 +++++++++++++++++++++------------- lua/rovo-dev/file_refresh.lua | 44 +++++++++++++--- tests/test_rovo_dev_spec.lua | 95 ++++++++++++++++++++++++++++++++--- 3 files changed, 185 insertions(+), 47 deletions(-) diff --git a/.luacheckrc b/.luacheckrc index 409d258..d5d92a1 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -2,66 +2,91 @@ std = { globals = { - "vim", - "table", - "string", - "math", - "os", - "io", + 'vim', + 'table', + 'string', + 'math', + 'os', + 'io', + 'package', }, read_globals = { - "jit", - "require", - "pcall", - "type", - "ipairs", - "pairs", - "tostring", - "tonumber", - "error", - "assert", - "_VERSION", + 'jit', + 'require', + 'pcall', + 'type', + 'ipairs', + 'pairs', + 'tostring', + 'tonumber', + 'error', + 'assert', + '_VERSION', }, } -- Patterns for files to exclude exclude_files = { - ".luarocks/*", - "lua/plenary/*", - "tests/plenary/*", + '.luarocks/*', + 'lua/plenary/*', + 'tests/plenary/*', } -- Special configuration for scripts -files["scripts/**/*.lua"] = { +files['scripts/**/*.lua'] = { globals = { - "print", "arg", + 'print', + 'arg', }, } -- Special configuration for test files -files["tests/**/*.lua"] = { +files['tests/**/*.lua'] = { -- Allow common globals used in testing globals = { -- Common testing globals - "describe", "it", "before_each", "after_each", "teardown", "pending", "spy", "stub", "mock", + 'describe', + 'it', + 'before_each', + 'after_each', + 'teardown', + 'pending', + 'spy', + 'stub', + 'mock', -- Lua standard utilities used in tests - "print", "dofile", + 'print', + 'dofile', -- Test helpers - "test", "expect", + 'test', + 'expect', -- Global test state (allow modification) - "_G", + '_G', }, -- Define fields for assert from luassert read_globals = { assert = { fields = { - "is_true", "is_false", "is_nil", "is_not_nil", "equals", - "same", "near", "matches", "has_error", - "truthy", "falsy", "has", "has_no", "is_string", "is_number", - "is_function", "is_table" - } - } + 'is_true', + 'is_false', + 'is_nil', + 'is_not_nil', + 'equals', + 'same', + 'near', + 'matches', + 'has_error', + 'truthy', + 'falsy', + 'has', + 'has_no', + 'is_string', + 'is_number', + 'is_function', + 'is_table', + }, + }, }, -- For test files only, ignore unused arguments as they're often used for mock callbacks @@ -87,6 +112,6 @@ max_line_length = 120 max_cyclomatic_complexity = 20 -- Override settings for specific files -files["lua/rovo-dev/config.lua"] = { +files['lua/rovo-dev/config.lua'] = { max_cyclomatic_complexity = 30, -- The validate_config function has high complexity due to many validation checks } diff --git a/lua/rovo-dev/file_refresh.lua b/lua/rovo-dev/file_refresh.lua index 0f6372e..d9b7fc9 100644 --- a/lua/rovo-dev/file_refresh.lua +++ b/lua/rovo-dev/file_refresh.lua @@ -41,6 +41,23 @@ function M.setup(config) return false end + -- Reset deletion notification flag when the user writes the buffer (recreates file) + vim.api.nvim_create_autocmd('BufWritePost', { + group = augroup, + callback = function(args) + local buf = args.buf + local name = vim.api.nvim_buf_get_name(buf) + if not name or name == '' then + return + end + -- If the file now exists on disk, clear any previous deletion notification flag + if vim.fn.filereadable(name) == 1 then + vim.b[buf].rovo_dev_deletion_notified = nil + end + end, + desc = 'Rovo Dev: reset deletion notification on write', + }) + -- Notify when buffers were reloaded from disk vim.api.nvim_create_autocmd('FileChangedShellPost', { group = augroup, @@ -53,12 +70,27 @@ function M.setup(config) if not name or name == '' then return end - name = vim.fn.fnamemodify(name, ':~:.') - vim.notify( - ('Reloaded from disk: %s'):format(name), - vim.log.levels.INFO, - { title = 'Rovo Dev' } - ) + + -- Check if the file actually exists + if vim.fn.filereadable(name) == 1 then + -- Reset deletion notification flag since file exists again + vim.b[buf].rovo_dev_deletion_notified = nil + name = vim.fn.fnamemodify(name, ':~:.') + vim.notify( + ('Reloaded from disk: %s'):format(name), + vim.log.levels.INFO, + { title = 'Rovo Dev' } + ) + else + -- File was deleted - show a different notification only once + -- We use a buffer variable to track if we've already notified about deletion + local already_notified = vim.b[buf].rovo_dev_deletion_notified + if not already_notified then + vim.b[buf].rovo_dev_deletion_notified = true + name = vim.fn.fnamemodify(name, ':~:.') + vim.notify(('File deleted: %s'):format(name), vim.log.levels.WARN, { title = 'Rovo Dev' }) + end + end end, desc = 'Notify when a buffer is updated externally', }) diff --git a/tests/test_rovo_dev_spec.lua b/tests/test_rovo_dev_spec.lua index b7f2cf0..bb7edd8 100644 --- a/tests/test_rovo_dev_spec.lua +++ b/tests/test_rovo_dev_spec.lua @@ -13,7 +13,9 @@ local function reset_state() pcall(vim.cmd, 'silent! only') local ok, state = pcall(require, 'rovo-dev.state') if ok then - if state.has_win() then pcall(vim.api.nvim_win_close, state.win, true) end + if state.has_win() then + pcall(vim.api.nvim_win_close, state.win, true) + end pcall(state.reset_buf) pcall(state.clear_win) end @@ -24,7 +26,9 @@ local original_termopen local function stub_termopen(capture) original_termopen = original_termopen or vim.fn.termopen vim.fn.termopen = function(cmd, opts) - if capture then capture(cmd, opts) end + if capture then + capture(cmd, opts) + end return 4242 -- fake job id end end @@ -40,7 +44,9 @@ local function visible_windows_for_buf(buf) for _, win in ipairs(vim.api.nvim_list_wins()) do if vim.api.nvim_win_get_buf(win) == buf then local cfg = vim.api.nvim_win_get_config(win) - if not cfg or cfg.relative == '' then table.insert(wins, win) end + if not cfg or cfg.relative == '' then + table.insert(wins, win) + end end end return wins @@ -82,7 +88,9 @@ describe('rovo-dev.nvim', function() it('appends flags to cmd for list and string forms', function() local captured - stub_termopen(function(cmd) captured = cmd end) + stub_termopen(function(cmd) + captured = cmd + end) local rovo = require('rovo-dev') rovo.setup({ terminal = { cmd = { 'acli', 'rovodev', 'run' } } }) @@ -90,12 +98,16 @@ describe('rovo-dev.nvim', function() rovo.toggle({ '--verbose', '--restore' }) assert.is_true(type(captured) == 'table') - eq('--verbose', captured[#captured-1]) + eq('--verbose', captured[#captured - 1]) eq('--restore', captured[#captured]) -- string form - unload_rovo(); reset_state(); captured = nil - stub_termopen(function(cmd) captured = cmd end) + unload_rovo() + reset_state() + captured = nil + stub_termopen(function(cmd) + captured = cmd + end) rovo = require('rovo-dev') rovo.setup({ terminal = { cmd = 'acli rovodev run' } }) rovo.toggle({ '--shadow' }) @@ -132,4 +144,73 @@ describe('rovo-dev.nvim', function() vim.notify = old_notify end) + + it('handles deleted files correctly without repeated notifications', function() + stub_termopen() + local rovo = require('rovo-dev') + rovo.setup({}) + rovo.toggle() + + -- Create a temporary file + local test_file = 'tmp_rovodev_deleted_test.txt' + vim.fn.writefile({ 'test content' }, test_file) + + -- Open the file in a buffer + vim.cmd('edit ' .. test_file) + local file_buf = vim.api.nvim_get_current_buf() + + local notifications = {} + local old_notify = vim.notify + vim.notify = function(msg, level, opts) + table.insert(notifications, { msg = msg, level = level, opts = opts }) + end + + -- Delete the file externally + vim.fn.delete(test_file) + + -- Trigger FileChangedShellPost multiple times + vim.api.nvim_exec_autocmds('FileChangedShellPost', { buffer = file_buf }) + vim.api.nvim_exec_autocmds('FileChangedShellPost', { buffer = file_buf }) + vim.api.nvim_exec_autocmds('FileChangedShellPost', { buffer = file_buf }) + + -- Should only get one notification about deletion + eq(1, #notifications) + assert.is_truthy(notifications[1].msg:match('File deleted:')) + eq(vim.log.levels.WARN, notifications[1].level) + + -- Test recreation resets the flag via external write + notifications = {} + vim.fn.writefile({ 'recreated' }, test_file) + vim.api.nvim_exec_autocmds('FileChangedShellPost', { buffer = file_buf }) + + eq(1, #notifications) + assert.is_truthy(notifications[1].msg:match('Reloaded from disk:')) + eq(vim.log.levels.INFO, notifications[1].level) + + -- Delete again - should notify again + notifications = {} + vim.fn.delete(test_file) + vim.api.nvim_exec_autocmds('FileChangedShellPost', { buffer = file_buf }) + vim.api.nvim_exec_autocmds('FileChangedShellPost', { buffer = file_buf }) + + eq(1, #notifications) + assert.is_truthy(notifications[1].msg:match('File deleted:')) + + -- Now recreate by writing the buffer (simulates user :write) + notifications = {} + vim.api.nvim_buf_set_lines(file_buf, 0, -1, false, { 'recreated by buffer write' }) + vim.cmd('write') + -- Writing should clear the deletion flag via BufWritePost; no notification expected here + eq(0, #notifications) + + -- Delete again - should notify again after buffer write recreation + vim.fn.delete(test_file) + vim.api.nvim_exec_autocmds('FileChangedShellPost', { buffer = file_buf }) + + eq(1, #notifications) + assert.is_truthy(notifications[1].msg:match('File deleted:')) + + vim.notify = old_notify + vim.cmd('bdelete! ' .. file_buf) + end) end)