diff --git a/README.md b/README.md index 72fa369c..2c47c4e1 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, -- 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 2b68e119..c5e1a952 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, -- 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/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..bdabb9ca 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, -- 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" diff --git a/lua/codediff/ui/explorer/render.lua b/lua/codediff/ui/explorer/render.lua index 8bd6cc39..cf08d36f 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()