From 93f8ece0fdca3216229bf66a44afbe511feb22a9 Mon Sep 17 00:00:00 2001 From: tk <49250442+tkr-sh@users.noreply.github.com> Date: Sun, 16 Aug 2026 20:56:53 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E2=9C=A8=20feat:=20add=20open=5Fon=5Fempty?= =?UTF-8?q?=20option?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit By default, :CodeDiff bails out with "No changes to show" when the working tree is clean. That's unfortunate: there may be no changes now, but there could be in the future. With `explorer.open_on_empty = true`, the explorer still opens with empty group headers, empty file list, and the welcome banner in the diff panes. This matches the state already shown when all staged changes are committed and nothing remains. Defaults to false, so existing behavior is unchanged. --- README.md | 1 + doc/codediff.txt | 1 + lua/codediff/commands.lua | 12 ++- lua/codediff/config.lua | 1 + lua/codediff/ui/explorer/render.lua | 9 ++ tests/command_e2e_spec.lua | 139 ++++++++++++++++++++++++++++ 6 files changed, 160 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 72fa369c..c89c22d0 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,7 @@ https://github.com/user-attachments/assets/64c41f01-dffe-4318-bce4-16eec8de356e width = 40, -- Width when position is "left" (columns) height = 15, -- Height when position is "bottom" (lines) auto_refresh = true, -- Auto-refresh file list on focus / git index changes (set false to avoid lag in huge repos; R still refreshes manually) + open_on_empty = false, -- Open the explorer even when the working tree is clean (empty file list + diff panes) indent_markers = true, -- Show indent markers in tree view (│, ├, └) initial_focus = "explorer", -- Initial focus: "explorer", "original", or "modified" icons = { diff --git a/doc/codediff.txt b/doc/codediff.txt index 2b68e119..59332f24 100644 --- a/doc/codediff.txt +++ b/doc/codediff.txt @@ -259,6 +259,7 @@ Setup entry point: width = 40, height = 15, auto_refresh = true, + open_on_empty = false, -- Open the explorer even when the working tree is clean (empty file list + diff panes) auto_open_on_cursor = false, indent_markers = true, icons = { diff --git a/lua/codediff/commands.lua b/lua/codediff/commands.lua index 4dfa55ee..419ad62f 100644 --- a/lua/codediff/commands.lua +++ b/lua/codediff/commands.lua @@ -372,12 +372,18 @@ local function handle_explorer(revision, revision2, global_opts, pathspec) -- Check if there are any changes (including conflicts) local has_conflicts = status_result.conflicts and #status_result.conflicts > 0 - if #status_result.unstaged == 0 and #status_result.staged == 0 and not has_conflicts then + local is_empty = #status_result.unstaged == 0 + and #status_result.staged == 0 + and not has_conflicts + if is_empty and not config.options.explorer.open_on_empty then vim.notify("No changes to show", vim.log.levels.INFO) return end - -- Create explorer view with empty diff panes initially + -- Create explorer view with empty diff panes initially. + -- When open_on_empty is true and is_empty, status_result still has its + -- (empty) unstaged/staged/conflicts arrays, so the explorer renders + -- empty groups rather than nil-ing out. ---@type SessionConfig local session_config = { @@ -486,7 +492,7 @@ local function handle_explorer_staged(revision, global_opts, pathspec) return end - if #status_result.staged == 0 then + if #status_result.staged == 0 and not config.options.explorer.open_on_empty then vim.notify("No staged changes to show", vim.log.levels.INFO) return end diff --git a/lua/codediff/config.lua b/lua/codediff/config.lua index bf0cf35b..306d56c0 100644 --- a/lua/codediff/config.lua +++ b/lua/codediff/config.lua @@ -60,6 +60,7 @@ M.defaults = { width = 40, -- Width when position is "left" (columns) height = 15, -- Height when position is "bottom" (lines) auto_refresh = true, -- Enable automatic explorer refresh (BufEnter + git watcher) + open_on_empty = false, -- Open the explorer even when the working tree is clean (empty file list + diff panes) view_mode = "list", -- "list" (flat file list) or "tree" (directory tree) indent_markers = true, -- Show indent markers in tree view (│, ├, └) initial_focus = "explorer", -- Initial focus: "explorer", "original", or "modified" diff --git a/lua/codediff/ui/explorer/render.lua b/lua/codediff/ui/explorer/render.lua index 8bd6cc39..72af31d8 100644 --- a/lua/codediff/ui/explorer/render.lua +++ b/lua/codediff/ui/explorer/render.lua @@ -628,6 +628,15 @@ function M.create(status_result, git_root, tabpage, width, base_revision, target -- Setup auto-refresh refresh_module.setup_auto_refresh(explorer, tabpage) +-- Empty working tree (explorer.open_on_empty): the explorer opened with zero + -- files. Render the welcome banner in the diff panes (instead of blank + -- scratch buffers) so the state is legible. This mirrors what a refresh + -- shows when the tree later becomes empty. should_show_welcome() also rejects + -- dir mode, so directory comparisons are unaffected. + if should_show_welcome(explorer) then + show_welcome_page(explorer) + end + -- Re-render on window resize for dynamic width vim.api.nvim_create_autocmd("WinResized", { callback = function() diff --git a/tests/command_e2e_spec.lua b/tests/command_e2e_spec.lua index 83f95af1..513045f1 100644 --- a/tests/command_e2e_spec.lua +++ b/tests/command_e2e_spec.lua @@ -27,6 +27,14 @@ describe("Command E2E (real dispatch + render)", function() end, { nargs = "*", bang = true, range = true }) end + -- config.setup() merges into the *current* options, so it accumulates across + -- calls. Reset to defaults first so every scenario starts from a known state. + local function reset_config(opts) + local config = require("codediff.config") + config.options = vim.deepcopy(config.defaults) + require("codediff").setup(opts or {}) + end + local function find_explorer_buf() for _, tp in ipairs(vim.api.nvim_list_tabpages()) do for _, win in ipairs(vim.api.nvim_tabpage_list_wins(tp)) do @@ -229,6 +237,137 @@ describe("Command E2E (real dispatch + render)", function() h.assert_contains(notified, "No staged changes") end) + -- ── Empty working tree (explorer.open_on_empty) ─────────────────────────── + -- A fresh, fully-committed repo is clean: nothing untracked, unstaged, + -- staged, or in conflict. `--repo` pins the target so the current buffer is + -- irrelevant. + + it(":CodeDiff on a clean repo: notifies and opens no tab (default)", function() + reset_config() + local repo2 = h.create_temp_git_repo() + repo2.write_file("sit.txt", { "x" }) + repo2.git("add sit.txt") + repo2.git("commit -m clean") + local notified + local orig_notify = vim.notify + vim.notify = function(msg, level) + if level == vim.log.levels.INFO then + notified = tostring(msg) + end + end + vim.cmd("CodeDiff --repo " .. repo2.dir) + vim.wait(2000, function() + return notified ~= nil + end) + vim.notify = orig_notify + local tabs = #session_tabs() + repo2.cleanup() + assert.is_not_nil(notified, "a notification is shown on a clean repo") + h.assert_contains(notified, "No changes to show") + assert.equals(0, tabs, "no explorer tab opens on a clean repo by default") + end) + + it(":CodeDiff on a clean repo: open_on_empty opens an empty explorer", function() + reset_config({ explorer = { open_on_empty = true } }) + local repo2 = h.create_temp_git_repo() + repo2.write_file("sit.txt", { "x" }) + repo2.git("add sit.txt") + repo2.git("commit -m clean") + local notified + local orig_notify = vim.notify + vim.notify = function(msg, level) + if level == vim.log.levels.INFO then + notified = tostring(msg) + end + end + vim.cmd("CodeDiff --repo " .. repo2.dir) + local tabs_exist = vim.wait(5000, function() + return #session_tabs() > 0 + end, 50) + vim.notify = orig_notify + assert.is_true(tabs_exist, "a session tab is opened with open_on_empty") + assert.is_nil(notified, "no 'No changes to show' notification with open_on_empty") + + local text = explorer_text() + h.assert_contains(text, "Changes (0)", "explorer shows the empty Changes group") + h.assert_contains(text, "Staged Changes (0)", "explorer shows the empty Staged Changes group") + + local welcome = require("codediff.ui.welcome") + local tab = find_explorer_tab() + local session = lifecycle.get_session(tab) + assert.is_not_nil(session, "session exists for the empty explorer") + local show_welcome = vim.wait(5000, function() + return welcome.is_welcome_buffer(session.modified_bufnr) + end, 50) + assert.is_true(show_welcome, "empty explorer shows the welcome banner in the diff pane") + repo2.cleanup() + end) + + it(":CodeDiff --staged on a clean repo: open_on_empty opens an empty staged explorer", function() + reset_config({ explorer = { open_on_empty = true } }) + local repo2 = h.create_temp_git_repo() + repo2.write_file("sit.txt", { "x" }) + repo2.git("add sit.txt") + repo2.git("commit -m clean") + local notified + local orig_notify = vim.notify + vim.notify = function(msg, level) + if level == vim.log.levels.INFO then + notified = tostring(msg) + end + end + vim.cmd("CodeDiff --repo " .. repo2.dir .. " --staged") + local tabs_exist = vim.wait(5000, function() + return #session_tabs() > 0 + end, 50) + vim.notify = orig_notify + assert.is_true(tabs_exist, "a session tab is opened with open_on_empty") + assert.is_nil(notified, "no 'No staged changes to show' notification with open_on_empty") + repo2.cleanup() + end) + + it(":CodeDiff on a clean repo: open_on_empty explorer repopulates on refresh", function() + reset_config({ explorer = { open_on_empty = true } }) + local repo2 = h.create_temp_git_repo() + repo2.write_file("sit.txt", { "x" }) + repo2.git("add sit.txt") + repo2.git("commit -m clean") + vim.cmd("CodeDiff --repo " .. repo2.dir) + local tabs_exist = vim.wait(5000, function() + return #session_tabs() > 0 + end, 50) + assert.is_true(tabs_exist, "an empty explorer tab is opened") + + -- Externally create a change, then refresh through the real code path. + repo2.write_file("sit.txt", { "x changed" }) + local explorer = lifecycle.get_explorer(find_explorer_tab()) + assert.is_not_nil(explorer, "explorer object exists") + require("codediff.ui.explorer.refresh").refresh(explorer) + + local refreshed = vim.wait(10000, function() + local files = require("codediff.ui.explorer.refresh").get_all_files(explorer.tree) + return #files > 0 + end, 50) + assert.is_true(refreshed, "refresh populates the explorer with the new change") + assert.is_true(vim.api.nvim_tabpage_is_valid(find_explorer_tab()), "the tab stays open after refresh") + + -- Refresh does not auto-select (existing no-auto-select-on-refresh design); + -- a manual select replaces the welcome banner with the file diff. + local session = lifecycle.get_session(find_explorer_tab()) + explorer.on_file_select({ + path = repo2.path("sit.txt"), + status = "M", + git_root = repo2.dir, + group = "unstaged", + }) + local welcome = require("codediff.ui.welcome") + local restored = vim.wait(10000, function() + return not welcome.is_welcome_buffer(session.modified_bufnr) + end, 50) + assert.is_true(restored, "selecting the file replaces the welcome banner with the diff") + repo2.cleanup() + end) + -- ── Single-file diff (two real files) ───────────────────────────────────── it(":CodeDiff file — renders both files in a diff", function() From 9402e881959f8d51e074a74a40b11b0de76abc39 Mon Sep 17 00:00:00 2001 From: tk <49250442+tkr-sh@users.noreply.github.com> Date: Sun, 16 Aug 2026 21:06:20 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20fix:=20rephrase?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- doc/codediff.txt | 2 +- lua/codediff/config.lua | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c89c22d0..2c47c4e1 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,7 @@ https://github.com/user-attachments/assets/64c41f01-dffe-4318-bce4-16eec8de356e width = 40, -- Width when position is "left" (columns) height = 15, -- Height when position is "bottom" (lines) auto_refresh = true, -- Auto-refresh file list on focus / git index changes (set false to avoid lag in huge repos; R still refreshes manually) - open_on_empty = false, -- Open the explorer even when the working tree is clean (empty file list + diff panes) + open_on_empty = false, -- When true, open the explorer even when the working tree is clean indent_markers = true, -- Show indent markers in tree view (│, ├, └) initial_focus = "explorer", -- Initial focus: "explorer", "original", or "modified" icons = { diff --git a/doc/codediff.txt b/doc/codediff.txt index 59332f24..c5e1a952 100644 --- a/doc/codediff.txt +++ b/doc/codediff.txt @@ -259,7 +259,7 @@ Setup entry point: width = 40, height = 15, auto_refresh = true, - open_on_empty = false, -- Open the explorer even when the working tree is clean (empty file list + diff panes) + open_on_empty = false, -- When true, open the explorer even when the working tree is clean auto_open_on_cursor = false, indent_markers = true, icons = { diff --git a/lua/codediff/config.lua b/lua/codediff/config.lua index 306d56c0..bdabb9ca 100644 --- a/lua/codediff/config.lua +++ b/lua/codediff/config.lua @@ -60,7 +60,7 @@ M.defaults = { width = 40, -- Width when position is "left" (columns) height = 15, -- Height when position is "bottom" (lines) auto_refresh = true, -- Enable automatic explorer refresh (BufEnter + git watcher) - open_on_empty = false, -- Open the explorer even when the working tree is clean (empty file list + diff panes) + open_on_empty = false, -- When true, open the explorer even when the working tree is clean view_mode = "list", -- "list" (flat file list) or "tree" (directory tree) indent_markers = true, -- Show indent markers in tree view (│, ├, └) initial_focus = "explorer", -- Initial focus: "explorer", "original", or "modified" From b26ed91d7f8ceb4e08e09ecc479decd068f5c4ae Mon Sep 17 00:00:00 2001 From: tk <49250442+tkr-sh@users.noreply.github.com> Date: Sun, 16 Aug 2026 21:07:44 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=92=85=20style:=20fmt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lua/codediff/ui/explorer/render.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/codediff/ui/explorer/render.lua b/lua/codediff/ui/explorer/render.lua index 72af31d8..cf08d36f 100644 --- a/lua/codediff/ui/explorer/render.lua +++ b/lua/codediff/ui/explorer/render.lua @@ -628,7 +628,7 @@ function M.create(status_result, git_root, tabpage, width, base_revision, target -- Setup auto-refresh refresh_module.setup_auto_refresh(explorer, tabpage) --- Empty working tree (explorer.open_on_empty): the explorer opened with zero + -- Empty working tree (explorer.open_on_empty): the explorer opened with zero -- files. Render the welcome banner in the diff panes (instead of blank -- scratch buffers) so the state is legible. This mirrors what a refresh -- shows when the tree later becomes empty. should_show_welcome() also rejects