diff --git a/README.md b/README.md index 3a2b5dcc..4eec47b9 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ https://github.com/user-attachments/assets/64c41f01-dffe-4318-bce4-16eec8de356e - Deep/dark character-level highlights showing exact changes within lines - **Side-by-side diff view** in a new tab with synchronized scrolling - **Inline (unified) diff view** — single-window layout with deleted lines as virtual overlays, with treesitter syntax highlighting +- **Toggle layout** — switch between side-by-side and inline layout at runtime with `t` - **Git integration**: Compare between any git revision (HEAD, commits, branches, tags) - **Same implementation as VSCode's diff engine**, providing identical visual highlighting for most scenarios - **Fast C-based diff computation** using FFI with **multi-core parallelization** (OpenMP) @@ -155,6 +156,7 @@ https://github.com/user-attachments/assets/64c41f01-dffe-4318-bce4-16eec8de356e hunk_textobject = "ih", -- Textobject for hunk (vih to select, yih to yank, etc.) show_help = "g?", -- Show floating window with available keymaps align_move = "gm", -- Temporarily align moved code blocks across panes + toggle_layout = "t", -- Toggle between side-by-side and inline layout }, explorer = { select = "", -- Open diff for selected file diff --git a/VERSION b/VERSION index 345a83ee..5b9cd9af 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.42.0 +2.43.0 diff --git a/doc/codediff.txt b/doc/codediff.txt index 9f6d0b89..5aed5818 100644 --- a/doc/codediff.txt +++ b/doc/codediff.txt @@ -153,6 +153,23 @@ The `gm` keymap disables scrollbind and positions both panes so the moved block appears at the same visual row. Moving the cursor outside the block or switching windows restores normal scrollbind. +TOGGLING LAYOUT *codediff-toggle-layout* + +Press `t` in a diff view to toggle between side-by-side and inline layout. +The current file is re-rendered in the new layout automatically. This works +in all modes (explorer, history, standalone). + +The toggle keymap can be customized: +>lua + require("codediff").setup({ + keymaps = { + view = { + toggle_layout = "t", -- or false to disable + }, + }, + }) +< + Setup entry point: >lua require("codediff").setup({ @@ -216,6 +233,7 @@ Setup entry point: hunk_textobject = "ih", show_help = "g?", align_move = "gm", + toggle_layout = "t", }, explorer = { select = "", diff --git a/doc/tags b/doc/tags index 9a07525c..28956313 100644 --- a/doc/tags +++ b/doc/tags @@ -13,6 +13,7 @@ codediff-lua-api codediff.txt /*codediff-lua-api* codediff-moved-code codediff.txt /*codediff-moved-code* codediff-quickstart codediff.txt /*codediff-quickstart* codediff-see-also codediff.txt /*codediff-see-also* +codediff-toggle-layout codediff.txt /*codediff-toggle-layout* codediff-usage codediff.txt /*codediff-usage* codediff.nvim codediff.txt /*codediff.nvim* codediff.txt codediff.txt /*codediff.txt* diff --git a/lua/codediff/config.lua b/lua/codediff/config.lua index 0b52d3d7..237132f1 100644 --- a/lua/codediff/config.lua +++ b/lua/codediff/config.lua @@ -99,6 +99,7 @@ M.defaults = { discard_hunk = "hr", -- Discard the hunk under cursor (working tree only) hunk_textobject = "ih", -- Textobject for hunk (vih to select, yih to yank, etc.) align_move = "gm", -- Temporarily align other pane to show paired moved code + toggle_layout = "t", -- Toggle diff layout for the current codediff session show_help = "g?", -- Show floating window with available keymaps }, explorer = { diff --git a/lua/codediff/ui/explorer/init.lua b/lua/codediff/ui/explorer/init.lua index 63e50b19..cd8c7c91 100644 --- a/lua/codediff/ui/explorer/init.lua +++ b/lua/codediff/ui/explorer/init.lua @@ -9,6 +9,8 @@ local actions = require("codediff.ui.explorer.actions") -- Delegate to render module M.create = render.create +M.rerender_current = render.rerender_current +M.show_welcome_page = render.show_welcome_page -- Delegate to refresh module M.setup_auto_refresh = refresh.setup_auto_refresh diff --git a/lua/codediff/ui/explorer/refresh.lua b/lua/codediff/ui/explorer/refresh.lua index c1f4fe7c..23b5a245 100644 --- a/lua/codediff/ui/explorer/refresh.lua +++ b/lua/codediff/ui/explorer/refresh.lua @@ -4,7 +4,6 @@ local M = {} local config = require("codediff.config") local tree_module = require("codediff.ui.explorer.tree") local welcome = require("codediff.ui.welcome") - -- Setup auto-refresh triggers for explorer -- Returns a cleanup function that should be called when the explorer is destroyed function M.setup_auto_refresh(explorer, tabpage) @@ -256,38 +255,24 @@ function M.refresh(explorer) local function clear_current_file() explorer.current_file_path = nil explorer.current_file_group = nil - end - - -- Helper: show the welcome page in the diff panes - local function show_welcome_page() - local lifecycle = require("codediff.ui.lifecycle") - local session = lifecycle.get_session(explorer.tabpage) - if session and not welcome.is_welcome_buffer(session.modified_bufnr) then - local mod_win = session.modified_win - if mod_win and vim.api.nvim_win_is_valid(mod_win) then - if session.layout == "inline" then - local w = vim.api.nvim_win_get_width(mod_win) - local h = vim.api.nvim_win_get_height(mod_win) - local welcome_buf = welcome.create_buffer(w, h) - require("codediff.ui.view.inline_view").show_welcome(explorer.tabpage, welcome_buf) - else - local orig_win = session.original_win - if orig_win and vim.api.nvim_win_is_valid(orig_win) then - local w = vim.api.nvim_win_get_width(orig_win) + vim.api.nvim_win_get_width(mod_win) + 1 - local h = vim.api.nvim_win_get_height(orig_win) - local welcome_buf = welcome.create_buffer(w, h) - require("codediff.ui.view.side_by_side").show_welcome(explorer.tabpage, welcome_buf) - end - end - end + explorer.current_selection = nil + if explorer.clear_selection then + explorer.clear_selection() end end - -- Show welcome page when all files are clean + local show_welcome_page = require("codediff.ui.explorer.render").show_welcome_page + + -- Show welcome page when all files are clean (skip if already showing) local total_files = #(status_result.unstaged or {}) + #(status_result.staged or {}) + #(status_result.conflicts or {}) if total_files == 0 then + local lifecycle = require("codediff.ui.lifecycle") + local session = lifecycle.get_session(explorer.tabpage) + local already_welcome = session and welcome.is_welcome_buffer(session.modified_bufnr) clear_current_file() - show_welcome_page() + if not already_welcome then + show_welcome_page(explorer) + end end -- Re-select the currently viewed file after refresh. @@ -339,7 +324,7 @@ function M.refresh(explorer) else -- File was committed/removed — show welcome clear_current_file() - show_welcome_page() + show_welcome_page(explorer) end end end) diff --git a/lua/codediff/ui/explorer/render.lua b/lua/codediff/ui/explorer/render.lua index 5aa64e2d..dbb2f164 100644 --- a/lua/codediff/ui/explorer/render.lua +++ b/lua/codediff/ui/explorer/render.lua @@ -8,6 +8,48 @@ local nodes_module = require("codediff.ui.explorer.nodes") local tree_module = require("codediff.ui.explorer.tree") local keymaps_module = require("codediff.ui.explorer.keymaps") local refresh_module = require("codediff.ui.explorer.refresh") +local welcome = require("codediff.ui.welcome") + +local function should_show_welcome(explorer) + if not explorer or not explorer.git_root or explorer.dir1 or explorer.dir2 then + return false + end + + local status = explorer.status_result or {} + local total_files = #(status.unstaged or {}) + #(status.staged or {}) + #(status.conflicts or {}) + return total_files == 0 +end + +local function show_welcome_page(explorer) + local lifecycle = require("codediff.ui.lifecycle") + local session = lifecycle.get_session(explorer.tabpage) + if not session then + return false + end + + local mod_win = session.modified_win + if not mod_win or not vim.api.nvim_win_is_valid(mod_win) then + return false + end + + if session.layout == "inline" then + local welcome_buf = welcome.create_buffer(vim.api.nvim_win_get_width(mod_win), vim.api.nvim_win_get_height(mod_win)) + require("codediff.ui.view.inline_view").show_welcome(explorer.tabpage, welcome_buf) + return true + end + + local orig_win = session.original_win + local width = vim.api.nvim_win_get_width(mod_win) + local height = vim.api.nvim_win_get_height(mod_win) + if orig_win and vim.api.nvim_win_is_valid(orig_win) then + width = vim.api.nvim_win_get_width(orig_win) + width + 1 + height = vim.api.nvim_win_get_height(orig_win) + end + + local welcome_buf = welcome.create_buffer(width, height) + require("codediff.ui.view.side_by_side").show_welcome(explorer.tabpage, welcome_buf) + return true +end function M.create(status_result, git_root, tabpage, width, base_revision, target_revision, opts) opts = opts or {} @@ -132,12 +174,14 @@ function M.create(status_result, git_root, tabpage, width, base_revision, target on_file_select = nil, -- Will be set below current_file_path = nil, -- Track currently selected file current_file_group = nil, -- Track currently selected file's group (staged/unstaged) + current_selection = nil, -- Full file selection used to replay current state is_hidden = false, -- Track visibility state visible_groups = vim.deepcopy(explorer_config.visible_groups or { staged = true, unstaged = true, conflicts = true }), } -- File selection callback - manages its own lifecycle - local function on_file_select(file_data) + local function on_file_select(file_data, opts) + opts = opts or {} local git = require("codediff.core.git") local view = require("codediff.ui.view") local lifecycle = require("codediff.ui.lifecycle") @@ -164,7 +208,7 @@ function M.create(status_result, git_root, tabpage, width, base_revision, target -- Check if already displaying same file local session = lifecycle.get_session(tabpage) - if session and session.original_path == original_path and session.modified_path == modified_path then + if not opts.force and session and session.original_path == original_path and session.modified_path == modified_path then return end @@ -190,7 +234,9 @@ function M.create(status_result, git_root, tabpage, width, base_revision, target vim.schedule(function() local sess = lifecycle.get_session(tabpage) if sess and sess.layout == "inline" then - require("codediff.ui.view.inline_view").show_single_file(tabpage, abs_path) + require("codediff.ui.view.inline_view").show_single_file(tabpage, abs_path, { + side = "modified", + }) else require("codediff.ui.view.side_by_side").show_untracked_file(tabpage, abs_path) end @@ -206,19 +252,31 @@ function M.create(status_result, git_root, tabpage, width, base_revision, target if base_revision and target_revision and target_revision ~= "WORKING" then if is_inline then - require("codediff.ui.view.inline_view").show_single_file(tabpage, file_path, { revision = target_revision, git_root = git_root, rel_path = file_path }) + require("codediff.ui.view.inline_view").show_single_file(tabpage, file_path, { + revision = target_revision, + git_root = git_root, + rel_path = file_path, + side = "modified", + }) else require("codediff.ui.view.side_by_side").show_added_virtual_file(tabpage, git_root, file_path, target_revision) end elseif group == "staged" then if is_inline then - require("codediff.ui.view.inline_view").show_single_file(tabpage, file_path, { revision = ":0", git_root = git_root, rel_path = file_path }) + require("codediff.ui.view.inline_view").show_single_file(tabpage, file_path, { + revision = ":0", + git_root = git_root, + rel_path = file_path, + side = "modified", + }) else require("codediff.ui.view.side_by_side").show_added_virtual_file(tabpage, git_root, file_path, ":0") end else if is_inline then - require("codediff.ui.view.inline_view").show_single_file(tabpage, abs_path) + require("codediff.ui.view.inline_view").show_single_file(tabpage, abs_path, { + side = "modified", + }) else require("codediff.ui.view.side_by_side").show_untracked_file(tabpage, abs_path) end @@ -235,14 +293,24 @@ function M.create(status_result, git_root, tabpage, width, base_revision, target if base_revision and target_revision and target_revision ~= "WORKING" then if is_inline then - require("codediff.ui.view.inline_view").show_single_file(tabpage, file_path, { revision = base_revision, git_root = git_root, rel_path = file_path }) + require("codediff.ui.view.inline_view").show_single_file(tabpage, file_path, { + revision = base_revision, + git_root = git_root, + rel_path = file_path, + side = "original", + }) else require("codediff.ui.view.side_by_side").show_deleted_virtual_file(tabpage, git_root, file_path, base_revision) end else if is_inline then local revision = (group == "staged") and "HEAD" or ":0" - require("codediff.ui.view.inline_view").show_single_file(tabpage, file_path, { revision = revision, git_root = git_root, rel_path = file_path }) + require("codediff.ui.view.inline_view").show_single_file(tabpage, file_path, { + revision = revision, + git_root = git_root, + rel_path = file_path, + side = "original", + }) else require("codediff.ui.view.side_by_side").show_deleted_file(tabpage, git_root, file_path, abs_path, group) end @@ -257,7 +325,7 @@ function M.create(status_result, git_root, tabpage, width, base_revision, target if session then local is_same_file = (session.modified_path == abs_path or (session.git_root and session.original_path == file_path)) - if is_same_file then + if is_same_file and not opts.force then -- Check if it's the same diff comparison local is_staged_diff = group == "staged" local current_is_staged = session.modified_revision == ":0" @@ -391,13 +459,21 @@ function M.create(status_result, git_root, tabpage, width, base_revision, target end -- Wrap on_file_select to track current file and group - explorer.on_file_select = function(file_data) + explorer.on_file_select = function(file_data, opts) explorer.current_file_path = file_data.path explorer.current_file_group = file_data.group + explorer.current_selection = vim.deepcopy(file_data) selected_path = file_data.path selected_group = file_data.group tree:render() - on_file_select(file_data) + on_file_select(file_data, opts) + end + + -- Clear selection highlight (used when showing welcome page) + explorer.clear_selection = function() + selected_path = nil + selected_group = nil + tree:render() end -- Setup keymaps (delegated to keymaps module) @@ -485,6 +561,31 @@ function M.create(status_result, git_root, tabpage, width, base_revision, target return explorer end +function M.rerender_current(explorer) + if not explorer then + return false + end + + if explorer.current_selection then + explorer.on_file_select(vim.deepcopy(explorer.current_selection), { force = true }) + return true + end + + local lifecycle = require("codediff.ui.lifecycle") + local session = lifecycle.get_session(explorer.tabpage) + if not session then + return false + end + + if should_show_welcome(explorer) and show_welcome_page(explorer) then + return true + end + + return false +end + +M.show_welcome_page = show_welcome_page + -- Setup auto-refresh on file save and focus return M diff --git a/lua/codediff/ui/history/init.lua b/lua/codediff/ui/history/init.lua index 879af535..9d71dc3c 100644 --- a/lua/codediff/ui/history/init.lua +++ b/lua/codediff/ui/history/init.lua @@ -11,6 +11,7 @@ local render = require("codediff.ui.history.render") -- width: optional width override -- opts: { range, path, ... } original options M.create = render.create +M.rerender_current = render.rerender_current -- Navigation (files within expanded commits) M.navigate_next = render.navigate_next diff --git a/lua/codediff/ui/history/render.lua b/lua/codediff/ui/history/render.lua index 8743d6df..cf10b442 100644 --- a/lua/codediff/ui/history/render.lua +++ b/lua/codediff/ui/history/render.lua @@ -178,6 +178,7 @@ function M.create(commits, git_root, tabpage, width, opts) on_file_select = nil, current_commit = nil, current_file = nil, + current_selection = nil, is_hidden = false, is_single_file_mode = is_single_file_mode, } @@ -266,7 +267,8 @@ function M.create(commits, git_root, tabpage, width, opts) end -- File selection callback - local function on_file_select(file_data) + local function on_file_select(file_data, opts) + opts = opts or {} local view = require("codediff.ui.view") local lifecycle = require("codediff.ui.lifecycle") @@ -287,7 +289,7 @@ function M.create(commits, git_root, tabpage, width, opts) -- Check if already displaying same file local target_hash = base_revision or (commit_hash .. "^") local session = lifecycle.get_session(tabpage) - if session and session.original_revision == target_hash and session.modified_revision == commit_hash then + if not opts.force and session and session.original_revision == target_hash and session.modified_revision == commit_hash then if session.modified_path == file_path or session.original_path == file_path then return end @@ -303,7 +305,12 @@ function M.create(commits, git_root, tabpage, width, opts) if is_inline then local rev = file_status == "A" and commit_hash or target_hash local path = file_status == "D" and (old_path or file_path) or file_path - require("codediff.ui.view.inline_view").show_single_file(tabpage, path, { revision = rev, git_root = git_root, rel_path = path }) + require("codediff.ui.view.inline_view").show_single_file(tabpage, path, { + revision = rev, + git_root = git_root, + rel_path = path, + side = file_status == "D" and "original" or "modified", + }) else if file_status == "A" then require("codediff.ui.view.side_by_side").show_added_virtual_file(tabpage, git_root, file_path, commit_hash) @@ -328,13 +335,14 @@ function M.create(commits, git_root, tabpage, width, opts) end) end - history.on_file_select = function(file_data) + history.on_file_select = function(file_data, opts) history.current_commit = file_data.commit_hash history.current_file = file_data.path + history.current_selection = vim.deepcopy(file_data) selected_commit = file_data.commit_hash selected_file = file_data.path tree:render() - on_file_select(file_data) + on_file_select(file_data, opts) end -- Store load_commit_files for refresh to re-expand commits @@ -419,6 +427,19 @@ function M.create(commits, git_root, tabpage, width, opts) return history end +function M.rerender_current(history) + if not history then + return false + end + + if history.current_selection then + history.on_file_select(vim.deepcopy(history.current_selection), { force = true }) + return true + end + + return false +end + -- Get all file nodes from tree (for navigation) function M.get_all_files(tree) local files = {} diff --git a/lua/codediff/ui/layout.lua b/lua/codediff/ui/layout.lua index 3f432a03..d2c469f2 100644 --- a/lua/codediff/ui/layout.lua +++ b/lua/codediff/ui/layout.lua @@ -44,9 +44,10 @@ function M.arrange(tabpage) local orig_valid = original_win and vim.api.nvim_win_is_valid(original_win) local mod_valid = modified_win and vim.api.nvim_win_is_valid(modified_win) + local is_single_diff_window = session.layout == "inline" or original_win == modified_win -- Single-pane mode: one diff window takes all available space - if session.single_pane or (orig_valid ~= mod_valid) then + if session.single_pane or is_single_diff_window or (orig_valid ~= mod_valid) then local sole_win = orig_valid and original_win or (mod_valid and modified_win or nil) if sole_win then if panel_visible then diff --git a/lua/codediff/ui/lifecycle/accessors.lua b/lua/codediff/ui/lifecycle/accessors.lua index 12bf571a..fe52d68c 100644 --- a/lua/codediff/ui/lifecycle/accessors.lua +++ b/lua/codediff/ui/lifecycle/accessors.lua @@ -31,6 +31,13 @@ function M.get_mode(tabpage) return sess and sess.mode or nil end +--- Get current session layout +function M.get_layout(tabpage) + local active_diffs = get_active_diffs() + local sess = active_diffs[tabpage] + return sess and sess.layout or nil +end + --- Get git context function M.get_git_context(tabpage) local active_diffs = get_active_diffs() @@ -195,6 +202,18 @@ function M.update_suspended(tabpage, suspended) return true end +--- Update session layout +function M.update_layout(tabpage, layout) + local active_diffs = get_active_diffs() + local sess = active_diffs[tabpage] + if not sess then + return false + end + + sess.layout = layout + return true +end + --- Update diff result (cached) function M.update_diff_result(tabpage, diff_lines) local active_diffs = get_active_diffs() diff --git a/lua/codediff/ui/lifecycle/cleanup.lua b/lua/codediff/ui/lifecycle/cleanup.lua index d6efe283..40679055 100644 --- a/lua/codediff/ui/lifecycle/cleanup.lua +++ b/lua/codediff/ui/lifecycle/cleanup.lua @@ -92,11 +92,11 @@ local function cleanup_diff(tabpage) end -- Clear window variables if windows still exist - if vim.api.nvim_win_is_valid(diff.original_win) then + if diff.original_win and vim.api.nvim_win_is_valid(diff.original_win) then welcome_window.apply_normal(diff.original_win) vim.w[diff.original_win].codediff_restore = nil end - if vim.api.nvim_win_is_valid(diff.modified_win) then + if diff.modified_win and vim.api.nvim_win_is_valid(diff.modified_win) then welcome_window.apply_normal(diff.modified_win) vim.w[diff.modified_win].codediff_restore = nil end diff --git a/lua/codediff/ui/lifecycle/init.lua b/lua/codediff/ui/lifecycle/init.lua index fecfdd4e..efa507c4 100644 --- a/lua/codediff/ui/lifecycle/init.lua +++ b/lua/codediff/ui/lifecycle/init.lua @@ -30,6 +30,7 @@ M.setup = cleanup.setup -- Delegate all accessors (getters) M.get_session = accessors.get_session M.get_mode = accessors.get_mode +M.get_layout = accessors.get_layout M.get_git_context = accessors.get_git_context M.get_buffers = accessors.get_buffers M.get_windows = accessors.get_windows @@ -47,6 +48,7 @@ M.get_unsaved_conflict_files = accessors.get_unsaved_conflict_files -- Delegate all accessors (setters) M.update_suspended = accessors.update_suspended +M.update_layout = accessors.update_layout M.update_diff_result = accessors.update_diff_result M.update_changedtick = accessors.update_changedtick M.update_mtime = accessors.update_mtime diff --git a/lua/codediff/ui/lifecycle/session.lua b/lua/codediff/ui/lifecycle/session.lua index 10b49855..bbfdb06b 100644 --- a/lua/codediff/ui/lifecycle/session.lua +++ b/lua/codediff/ui/lifecycle/session.lua @@ -90,6 +90,7 @@ function M.create_session( modified_state = modified_state, -- Lifecycle state + layout = "side-by-side", suspended = false, stored_diff_result = lines_diff, changedtick = { @@ -150,10 +151,10 @@ function M.create_session( return end -- Normal diff mode: disable winbar - if sess and vim.api.nvim_win_is_valid(sess.original_win) then + if sess and sess.original_win and vim.api.nvim_win_is_valid(sess.original_win) then vim.wo[sess.original_win].winbar = "" end - if sess and vim.api.nvim_win_is_valid(sess.modified_win) then + if sess and sess.modified_win and vim.api.nvim_win_is_valid(sess.modified_win) then vim.wo[sess.modified_win].winbar = "" end end diff --git a/lua/codediff/ui/lifecycle/state.lua b/lua/codediff/ui/lifecycle/state.lua index b5381255..3e4314c9 100644 --- a/lua/codediff/ui/lifecycle/state.lua +++ b/lua/codediff/ui/lifecycle/state.lua @@ -188,7 +188,14 @@ local function resume_diff(tabpage) end -- Re-sync scrollbind ONLY if diff was recomputed and not inline mode - if diff_was_recomputed and diff.layout ~= "inline" and vim.api.nvim_win_is_valid(diff.original_win) and vim.api.nvim_win_is_valid(diff.modified_win) then + if + diff_was_recomputed + and diff.layout ~= "inline" + and diff.original_win + and diff.modified_win + and vim.api.nvim_win_is_valid(diff.original_win) + and vim.api.nvim_win_is_valid(diff.modified_win) + then local current_win = vim.api.nvim_get_current_win() local result_win = diff.result_win and vim.api.nvim_win_is_valid(diff.result_win) and diff.result_win or nil diff --git a/lua/codediff/ui/view/init.lua b/lua/codediff/ui/view/init.lua index 1ba24d54..ae8dc281 100644 --- a/lua/codediff/ui/view/init.lua +++ b/lua/codediff/ui/view/init.lua @@ -8,12 +8,17 @@ local side_by_side = require("codediff.ui.view.side_by_side") -- Once-guard: register lifecycle autocmds on first view creation local lifecycle_initialized = false --- Resolve effective layout: conflict always uses side-by-side -local function get_layout(session_config) - if session_config.conflict then +local function get_layout(session_config, tabpage) + if session_config and session_config.conflict then return "side-by-side" end - if session_config.layout then return session_config.layout end + local session = tabpage and lifecycle.get_session(tabpage) or nil + if session and session.layout then + return session.layout + end + if session_config and session_config.layout then + return session_config.layout + end return config.options.diff.layout end @@ -54,13 +59,19 @@ end ---@param auto_scroll_to_first_hunk boolean? Whether to auto-scroll to first hunk (default: false) ---@return boolean success Whether update succeeded function M.update(tabpage, session_config, auto_scroll_to_first_hunk) - -- Route based on existing session layout (not config — session may differ) - local session = lifecycle.get_session(tabpage) - if session and session.layout == "inline" and not session_config.conflict then + if get_layout(session_config, tabpage) == "inline" then return require("codediff.ui.view.inline_view").update(tabpage, session_config, auto_scroll_to_first_hunk) end return side_by_side.update(tabpage, session_config, auto_scroll_to_first_hunk) end +function M.toggle_layout(tabpage) + return require("codediff.ui.view.toggle").toggle(tabpage) +end + +function M.get_current_layout(tabpage) + return get_layout(nil, tabpage) +end + return M diff --git a/lua/codediff/ui/view/inline_view.lua b/lua/codediff/ui/view/inline_view.lua index 44a0e35d..adc35f0f 100644 --- a/lua/codediff/ui/view/inline_view.lua +++ b/lua/codediff/ui/view/inline_view.lua @@ -66,10 +66,7 @@ end -- Helper: mark session as inline layout after creation local function mark_inline(tabpage) - local session = lifecycle.get_session(tabpage) - if session then - session.layout = "inline" - end + lifecycle.update_layout(tabpage, "inline") end -- Helper: setup keymaps (uses the shared setup_all_keymaps which is layout-aware) @@ -140,7 +137,6 @@ function M.create(session_config, filetype, on_ready) ) mark_inline(tabpage) - -- Setup panels via shared module panel.setup_explorer(tabpage, session_config, modified_win, modified_win) panel.setup_history(tabpage, session_config, modified_win, modified_win, orig_scratch, mod_scratch, function(tp, ob, mb) @@ -411,6 +407,7 @@ function M.update(tabpage, session_config, auto_scroll_to_first_hunk) end setup_keymaps(tabpage, orig_buf, mod_buf) + layout.arrange(tabpage) if saved_current_win and vim.api.nvim_win_is_valid(saved_current_win) then vim.api.nvim_set_current_win(saved_current_win) @@ -532,7 +529,9 @@ function M.show_single_file(tabpage, file_path, opts) if not session then return end + local side = opts.side or "modified" + lifecycle.update_layout(tabpage, "inline") local mod_win = session.modified_win if not mod_win or not vim.api.nvim_win_is_valid(mod_win) then return @@ -587,13 +586,22 @@ function M.show_single_file(tabpage, file_path, opts) local empty_buf = vim.api.nvim_create_buf(false, true) vim.bo[empty_buf].buftype = "nofile" - lifecycle.update_buffers(tabpage, empty_buf, file_bufnr) - lifecycle.update_paths(tabpage, "", file_path) - lifecycle.update_revisions(tabpage, nil, opts.revision) + local session_path = (opts.revision and opts.rel_path) and opts.rel_path or file_path + local orig_bufnr = side == "original" and file_bufnr or empty_buf + local mod_bufnr = side == "modified" and file_bufnr or empty_buf + local original_path = side == "original" and session_path or "" + local modified_path = side == "modified" and session_path or "" + local original_revision = side == "original" and opts.revision or nil + local modified_revision = side == "modified" and opts.revision or nil + + lifecycle.update_buffers(tabpage, orig_bufnr, mod_bufnr) + lifecycle.update_paths(tabpage, original_path, modified_path) + lifecycle.update_revisions(tabpage, original_revision, modified_revision) lifecycle.update_diff_result(tabpage, {}) local view_keymaps = require("codediff.ui.view.keymaps") - view_keymaps.setup_all_keymaps(tabpage, empty_buf, file_bufnr, true) + view_keymaps.setup_all_keymaps(tabpage, orig_bufnr, mod_bufnr, session.mode == "explorer") + layout.arrange(tabpage) welcome_window.sync_later(mod_win) end @@ -606,6 +614,7 @@ function M.show_welcome(tabpage, load_bufnr) return end + lifecycle.update_layout(tabpage, "inline") local mod_win = session.modified_win if not mod_win or not vim.api.nvim_win_is_valid(mod_win) then return @@ -628,7 +637,8 @@ function M.show_welcome(tabpage, load_bufnr) lifecycle.update_diff_result(tabpage, {}) local view_keymaps = require("codediff.ui.view.keymaps") - view_keymaps.setup_all_keymaps(tabpage, empty_buf, load_bufnr, true) + view_keymaps.setup_all_keymaps(tabpage, empty_buf, load_bufnr, session.mode == "explorer") + layout.arrange(tabpage) welcome_window.sync_later(mod_win) end diff --git a/lua/codediff/ui/view/keymaps.lua b/lua/codediff/ui/view/keymaps.lua index c7006617..e41a6c75 100644 --- a/lua/codediff/ui/view/keymaps.lua +++ b/lua/codediff/ui/view/keymaps.lua @@ -417,9 +417,15 @@ function M.setup_all_keymaps(tabpage, original_bufnr, modified_bufnr, is_explore return end + local stage_orig_buf, stage_mod_buf = lifecycle.get_buffers(tabpage) + if not stage_orig_buf or not stage_mod_buf or not vim.api.nvim_buf_is_valid(stage_orig_buf) or not vim.api.nvim_buf_is_valid(stage_mod_buf) then + vim.notify("Diff buffers are no longer available", vim.log.levels.WARN) + return + end + -- Read lines from both buffers for this hunk - local orig_lines = vim.api.nvim_buf_get_lines(original_bufnr, hunk.original.start_line - 1, hunk.original.end_line - 1, false) - local mod_lines = vim.api.nvim_buf_get_lines(modified_bufnr, hunk.modified.start_line - 1, hunk.modified.end_line - 1, false) + local orig_lines = vim.api.nvim_buf_get_lines(stage_orig_buf, hunk.original.start_line - 1, hunk.original.end_line - 1, false) + local mod_lines = vim.api.nvim_buf_get_lines(stage_mod_buf, hunk.modified.start_line - 1, hunk.modified.end_line - 1, false) local patch = build_hunk_patch(file_path, orig_lines, mod_lines, hunk.original.start_line, hunk.modified.start_line) @@ -498,9 +504,15 @@ function M.setup_all_keymaps(tabpage, original_bufnr, modified_bufnr, is_explore return end + local unstage_orig_buf, unstage_mod_buf = lifecycle.get_buffers(tabpage) + if not unstage_orig_buf or not unstage_mod_buf or not vim.api.nvim_buf_is_valid(unstage_orig_buf) or not vim.api.nvim_buf_is_valid(unstage_mod_buf) then + vim.notify("Diff buffers are no longer available", vim.log.levels.WARN) + return + end + -- Read lines from both buffers for this hunk - local orig_lines = vim.api.nvim_buf_get_lines(original_bufnr, hunk.original.start_line - 1, hunk.original.end_line - 1, false) - local mod_lines = vim.api.nvim_buf_get_lines(modified_bufnr, hunk.modified.start_line - 1, hunk.modified.end_line - 1, false) + local orig_lines = vim.api.nvim_buf_get_lines(unstage_orig_buf, hunk.original.start_line - 1, hunk.original.end_line - 1, false) + local mod_lines = vim.api.nvim_buf_get_lines(unstage_mod_buf, hunk.modified.start_line - 1, hunk.modified.end_line - 1, false) local patch = build_hunk_patch(file_path, orig_lines, mod_lines, hunk.original.start_line, hunk.modified.start_line) @@ -584,9 +596,15 @@ function M.setup_all_keymaps(tabpage, original_bufnr, modified_bufnr, is_explore return end + local discard_orig_buf, discard_mod_buf = lifecycle.get_buffers(tabpage) + if not discard_orig_buf or not discard_mod_buf or not vim.api.nvim_buf_is_valid(discard_orig_buf) or not vim.api.nvim_buf_is_valid(discard_mod_buf) then + vim.notify("Diff buffers are no longer available", vim.log.levels.WARN) + return + end + -- Read lines from both buffers for this hunk - local orig_lines = vim.api.nvim_buf_get_lines(original_bufnr, hunk.original.start_line - 1, hunk.original.end_line - 1, false) - local mod_lines = vim.api.nvim_buf_get_lines(modified_bufnr, hunk.modified.start_line - 1, hunk.modified.end_line - 1, false) + local orig_lines = vim.api.nvim_buf_get_lines(discard_orig_buf, hunk.original.start_line - 1, hunk.original.end_line - 1, false) + local mod_lines = vim.api.nvim_buf_get_lines(discard_mod_buf, hunk.modified.start_line - 1, hunk.modified.end_line - 1, false) local patch = build_hunk_patch(file_path, orig_lines, mod_lines, hunk.original.start_line, hunk.modified.start_line) @@ -661,6 +679,11 @@ function M.setup_all_keymaps(tabpage, original_bufnr, modified_bufnr, is_explore if keymaps.open_in_prev_tab then lifecycle.set_tab_keymap(tabpage, "n", keymaps.open_in_prev_tab, open_in_prev_tab, { desc = "Open buffer in previous tab" }) end + if keymaps.toggle_layout then + lifecycle.set_tab_keymap(tabpage, "n", keymaps.toggle_layout, function() + require("codediff.ui.view").toggle_layout(tabpage) + end, { desc = "Toggle diff layout" }) + end -- Toggle stage/unstage (- key) - only in explorer mode -- Support legacy config: keymaps.explorer.toggle_stage (deprecated) diff --git a/lua/codediff/ui/view/side_by_side.lua b/lua/codediff/ui/view/side_by_side.lua index 63735690..307db9a3 100644 --- a/lua/codediff/ui/view/side_by_side.lua +++ b/lua/codediff/ui/view/side_by_side.lua @@ -234,7 +234,6 @@ function M.create(session_config, filetype, on_ready) conflict.setup_keymaps(tabpage) end ) - -- Setup auto-refresh for consistency (both buffers are virtual in conflict mode) setup_auto_refresh(original_info.bufnr, modified_info.bufnr, true, true) @@ -292,7 +291,6 @@ function M.create(session_config, filetype, on_ready) setup_all_keymaps(tabpage, ob, mb, is_explorer) end ) - -- Enable auto-refresh for real file buffers only setup_auto_refresh(original_info.bufnr, modified_info.bufnr, original_is_virtual, modified_is_virtual) @@ -409,7 +407,10 @@ function M.update(tabpage, session_config, auto_scroll_to_first_hunk) local old_original_buf, old_modified_buf = lifecycle.get_buffers(tabpage) local original_win, modified_win = lifecycle.get_windows(tabpage) - if not old_original_buf or not old_modified_buf or not original_win or not modified_win then + if not old_original_buf or not old_modified_buf then + return false + end + if not original_win and not modified_win then return false end @@ -435,14 +436,14 @@ function M.update(tabpage, session_config, auto_scroll_to_first_hunk) if session.single_pane then local split_cmd = config.options.diff.original_position == "right" and "leftabove vsplit" or "rightbelow vsplit" - if not vim.api.nvim_win_is_valid(original_win) then + if not original_win or not vim.api.nvim_win_is_valid(original_win) then -- Original was closed (untracked file) — recreate it to the left of modified vim.api.nvim_set_current_win(modified_win) vim.cmd(config.options.diff.original_position == "right" and "rightbelow vsplit" or "leftabove vsplit") original_win = vim.api.nvim_get_current_win() vim.w[original_win].codediff_restore = 1 session.original_win = original_win - elseif not vim.api.nvim_win_is_valid(modified_win) then + elseif not modified_win or not vim.api.nvim_win_is_valid(modified_win) then -- Modified was closed (deleted file) — recreate it to the right of original vim.api.nvim_set_current_win(original_win) vim.cmd(split_cmd) @@ -508,7 +509,6 @@ function M.update(tabpage, session_config, auto_scroll_to_first_hunk) lifecycle.update_revisions(tabpage, session_config.original_revision, session_config.modified_revision) lifecycle.update_diff_result(tabpage, conflict_diffs.base_to_modified_diff) lifecycle.update_changedtick(tabpage, vim.api.nvim_buf_get_changedtick(original_info.bufnr), vim.api.nvim_buf_get_changedtick(modified_info.bufnr)) - setup_auto_refresh(original_info.bufnr, modified_info.bufnr, true, true) local is_explorer_mode = session.mode == "explorer" @@ -542,7 +542,6 @@ function M.update(tabpage, session_config, auto_scroll_to_first_hunk) lifecycle.update_revisions(tabpage, session_config.original_revision, session_config.modified_revision) lifecycle.update_diff_result(tabpage, lines_diff) lifecycle.update_changedtick(tabpage, vim.api.nvim_buf_get_changedtick(original_info.bufnr), vim.api.nvim_buf_get_changedtick(modified_info.bufnr)) - setup_auto_refresh(original_info.bufnr, modified_info.bufnr, original_is_virtual, modified_is_virtual) local is_explorer_mode = session.mode == "explorer" @@ -704,6 +703,7 @@ local function show_single_file(tabpage, opts) return end + lifecycle.update_layout(tabpage, "side-by-side") local orig_win, mod_win = lifecycle.get_windows(tabpage) local highlights = require("codediff.ui.highlights") @@ -729,9 +729,18 @@ local function show_single_file(tabpage, opts) keep_win, close_win = orig_win, mod_win end + if keep_win == close_win then + close_win = nil + end + if (not keep_win or not vim.api.nvim_win_is_valid(keep_win)) and close_win and vim.api.nvim_win_is_valid(close_win) then + keep_win = close_win + close_win = nil + end + if close_win and vim.api.nvim_win_is_valid(close_win) then vim.w[close_win].codediff_restore = nil vim.api.nvim_win_close(close_win, true) + close_win = nil end -- Load the file into the kept window @@ -739,6 +748,14 @@ local function show_single_file(tabpage, opts) vim.api.nvim_win_set_buf(keep_win, opts.load_bufnr) welcome_window.sync(keep_win) + if opts.keep == "original" then + session.original_win = keep_win + session.modified_win = nil + else + session.original_win = nil + session.modified_win = keep_win + end + -- Create a scratch buffer as placeholder for the empty side local empty_buf = vim.api.nvim_create_buf(false, true) vim.bo[empty_buf].buftype = "nofile" @@ -752,7 +769,7 @@ local function show_single_file(tabpage, opts) lifecycle.update_diff_result(tabpage, {}) local view_keymaps = require("codediff.ui.view.keymaps") - view_keymaps.setup_all_keymaps(tabpage, orig_bufnr, mod_bufnr, true) + view_keymaps.setup_all_keymaps(tabpage, orig_bufnr, mod_bufnr, session.mode == "explorer") end layout.arrange(tabpage) @@ -782,6 +799,7 @@ function M.show_untracked_file(tabpage, file_path) show_single_file(tabpage, { keep = "modified", load_bufnr = load_real_file(file_path), + file_path = file_path, modified_path = file_path, }) end @@ -792,6 +810,10 @@ function M.show_deleted_file(tabpage, git_root, file_path, abs_path, group) show_single_file(tabpage, { keep = "original", load_bufnr = load_virtual_file(git_root, revision, file_path), + file_path = abs_path, + load_revision = revision, + load_git_root = git_root, + rel_path = file_path, original_path = abs_path, original_revision = revision, }) @@ -802,6 +824,10 @@ function M.show_added_virtual_file(tabpage, git_root, file_path, revision) show_single_file(tabpage, { keep = "modified", load_bufnr = load_virtual_file(git_root, revision, file_path), + file_path = file_path, + load_revision = revision, + load_git_root = git_root, + rel_path = file_path, modified_path = file_path, modified_revision = revision, }) @@ -812,6 +838,10 @@ function M.show_deleted_virtual_file(tabpage, git_root, file_path, revision) show_single_file(tabpage, { keep = "original", load_bufnr = load_virtual_file(git_root, revision, file_path), + file_path = file_path, + load_revision = revision, + load_git_root = git_root, + rel_path = file_path, original_path = file_path, original_revision = revision, }) diff --git a/lua/codediff/ui/view/toggle.lua b/lua/codediff/ui/view/toggle.lua new file mode 100644 index 00000000..b8311d73 --- /dev/null +++ b/lua/codediff/ui/view/toggle.lua @@ -0,0 +1,117 @@ +local M = {} + +local lifecycle = require("codediff.ui.lifecycle") +local layout = require("codediff.ui.layout") + +local function normalize_inline_layout(tabpage) + local session = lifecycle.get_session(tabpage) + if not session then + return false + end + + lifecycle.update_layout(tabpage, "inline") + session.single_pane = nil + + local original_win = session.original_win + local modified_win = session.modified_win + local keep_win = (modified_win and vim.api.nvim_win_is_valid(modified_win) and modified_win) or (original_win and vim.api.nvim_win_is_valid(original_win) and original_win) + + if not keep_win then + return false + end + + session.original_win = keep_win + session.modified_win = keep_win + + local close_win = nil + if original_win and modified_win and original_win ~= modified_win then + close_win = keep_win == modified_win and original_win or modified_win + end + + if close_win and vim.api.nvim_win_is_valid(close_win) then + vim.api.nvim_set_current_win(keep_win) + pcall(vim.api.nvim_win_close, close_win, true) + end + + return true +end + +local function normalize_side_by_side_layout(tabpage) + local session = lifecycle.get_session(tabpage) + if not session then + return false + end + + local current_win = (session.modified_win and vim.api.nvim_win_is_valid(session.modified_win) and session.modified_win) + or (session.original_win and vim.api.nvim_win_is_valid(session.original_win) and session.original_win) + + if not current_win then + return false + end + + lifecycle.update_layout(tabpage, "side-by-side") + session.single_pane = true + session.original_win = nil + session.modified_win = current_win + return true +end + +-- Re-render the current file in the new layout. +-- For explorer/history: call rerender_current which re-triggers on_file_select. +-- For standalone: rebuild session_config from existing session fields. +local function rerender_current_file(tabpage) + local session = lifecycle.get_session(tabpage) + if not session then + return false + end + + if session.mode == "explorer" then + local explorer = lifecycle.get_explorer(tabpage) + return explorer and require("codediff.ui.explorer").rerender_current(explorer) or false + end + + if session.mode == "history" then + local history = lifecycle.get_explorer(tabpage) + return history and require("codediff.ui.history").rerender_current(history) or false + end + + -- Standalone mode: rebuild from session fields + local session_config = { + mode = session.mode, + git_root = session.git_root, + original_path = session.original_path, + modified_path = session.modified_path, + original_revision = session.original_revision, + modified_revision = session.modified_revision, + } + return require("codediff.ui.view").update(tabpage, session_config, false) +end + +function M.toggle(tabpage) + tabpage = tabpage or vim.api.nvim_get_current_tabpage() + local session = lifecycle.get_session(tabpage) + if not session then + return false + end + + if session.result_win and vim.api.nvim_win_is_valid(session.result_win) then + vim.notify("Cannot toggle layout in conflict mode", vim.log.levels.WARN) + return false + end + + local target_layout = session.layout == "inline" and "side-by-side" or "inline" + local normalize = target_layout == "inline" and normalize_inline_layout or normalize_side_by_side_layout + local previous_layout = session.layout + + if not normalize(tabpage) then + return false + end + + if rerender_current_file(tabpage) then + layout.arrange(tabpage) + end + + return true +end + +return M diff --git a/tests/ui/view/layout_toggle_spec.lua b/tests/ui/view/layout_toggle_spec.lua new file mode 100644 index 00000000..97fde211 --- /dev/null +++ b/tests/ui/view/layout_toggle_spec.lua @@ -0,0 +1,837 @@ +local h = dofile("tests/helpers.lua") + +h.ensure_plugin_loaded() + +local view = require("codediff.ui.view") +local lifecycle = require("codediff.ui.lifecycle") +local side_by_side = require("codediff.ui.view.side_by_side") +local welcome = require("codediff.ui.welcome") +local navigation = require("codediff.ui.view.navigation") +local inline = require("codediff.ui.inline") + +local function setup_command() + pcall(vim.api.nvim_del_user_command, "CodeDiff") + local commands = require("codediff.commands") + vim.api.nvim_create_user_command("CodeDiff", function(opts) + commands.vscode_diff(opts) + end, { + nargs = "*", + bang = true, + complete = function() + return { "file", "install" } + end, + }) +end + +local function temp_file(name, lines) + local path = vim.fn.tempname() .. "_" .. name + vim.fn.writefile(lines, path) + return path +end + +local function create_explorer_placeholder(git_root) + view.create({ + mode = "explorer", + git_root = git_root, + original_path = "", + modified_path = "", + explorer_data = { + status_result = { + unstaged = {}, + staged = {}, + conflicts = {}, + }, + }, + }) + + return vim.api.nvim_get_current_tabpage() +end + +local function create_standalone_diff(left_lines, right_lines) + local left = temp_file("layout_toggle_left.txt", left_lines) + local right = temp_file("layout_toggle_right.txt", right_lines) + view.create({ + mode = "standalone", + original_path = left, + modified_path = right, + }) + + local tabpage = vim.api.nvim_get_current_tabpage() + assert.is_true(h.wait_for_session_ready(tabpage, 10000), "Standalone diff should be ready") + + return tabpage, left, right +end + +local function open_codediff_and_wait(repo, entry_file) + setup_command() + vim.fn.chdir(repo.dir) + vim.cmd("edit " .. repo.path(entry_file or "file.txt")) + vim.cmd("CodeDiff") + + local tabpage + local ready = vim.wait(10000, function() + for _, tp in ipairs(vim.api.nvim_list_tabpages()) do + local session = lifecycle.get_session(tp) + if session and session.explorer then + tabpage = tp + local orig_buf, mod_buf = lifecycle.get_buffers(tp) + return orig_buf and mod_buf and vim.api.nvim_buf_is_valid(orig_buf) and vim.api.nvim_buf_is_valid(mod_buf) + end + end + return false + end, 100) + + assert.is_true(ready, "CodeDiff explorer session should be ready") + + local session = lifecycle.get_session(tabpage) + return tabpage, session, session.explorer +end + +local function open_history_and_wait(repo, entry_file) + local git = require("codediff.core.git") + local commits + local err + local file_path = entry_file or "file.txt" + + git.get_commit_list("", repo.dir, { + no_merges = true, + path = file_path, + }, function(cb_err, cb_commits) + err = cb_err + commits = cb_commits + end) + + local commits_ready = vim.wait(10000, function() + return err ~= nil or commits ~= nil + end, 100) + + assert.is_true(commits_ready, "History commits should load") + assert.is_nil(err, "History commit list should load without error") + assert.is_true(commits and #commits > 0, "History should contain commits") + + view.create({ + mode = "history", + git_root = repo.dir, + original_path = "", + modified_path = "", + history_data = { + commits = commits, + range = "", + file_path = file_path, + }, + }, "") + + local tabpage + local history + local ready = vim.wait(10000, function() + for _, tp in ipairs(vim.api.nvim_list_tabpages()) do + local session = lifecycle.get_session(tp) + local panel = lifecycle.get_explorer(tp) + if session and session.mode == "history" and panel then + tabpage = tp + history = panel + return history.current_selection + and session.original_revision + and session.modified_revision + and session.original_bufnr + and session.modified_bufnr + and vim.api.nvim_buf_is_valid(session.original_bufnr) + and vim.api.nvim_buf_is_valid(session.modified_bufnr) + end + end + return false + end, 100) + + assert.is_true(ready, "CodeDiff history session should be ready") + + return tabpage, lifecycle.get_session(tabpage), history +end + +local function select_explorer_file(tabpage, explorer, file_data, wait_for) + explorer.on_file_select(file_data) + local ok = vim.wait(10000, function() + local session = lifecycle.get_session(tabpage) + return session and wait_for(session) + end, 100) + assert.is_true(ok, "Explorer selection should update the diff view") +end + +local function get_buffer_mapping_callback(bufnr, lhs) + return vim.api.nvim_buf_call(bufnr, function() + local map = vim.fn.maparg(lhs, "n", false, true) + return map and map.callback or nil + end) +end + +local function wait_for(tabpage, predicate, message) + local ok = vim.wait(10000, function() + local session = lifecycle.get_session(tabpage) + return session and predicate(session) + end, 100) + assert.is_true(ok, message) +end + +local function capture_layout_snapshot(tabpage) + local session = lifecycle.get_session(tabpage) + local panel = session and session.explorer or nil + local panel_win = panel and panel.winid + local diff_win = session and session.modified_win + return { + window_count = #vim.api.nvim_tabpage_list_wins(tabpage), + panel_width = panel_win and vim.api.nvim_win_is_valid(panel_win) and vim.api.nvim_win_get_width(panel_win) or nil, + diff_width = diff_win and vim.api.nvim_win_is_valid(diff_win) and vim.api.nvim_win_get_width(diff_win) or nil, + same_diff_window = session and session.original_win == session.modified_win or false, + } +end + +local function move_cursor_to_hunk(winid, bufnr, range) + local visible_buf = vim.api.nvim_win_get_buf(winid) + local line_count = vim.api.nvim_buf_line_count(visible_buf) + local target_line = range.start_line + if range.start_line >= range.end_line then + target_line = math.max(1, range.start_line - 1) + end + target_line = math.max(1, math.min(target_line, line_count)) + vim.api.nvim_set_current_win(winid) + vim.api.nvim_win_set_cursor(winid, { target_line, 0 }) +end + +describe("Layout toggle", function() + local repo + local paths = {} + local original_cwd + + local function track(path) + table.insert(paths, path) + return path + end + + before_each(function() + require("codediff").setup({ + diff = { layout = "side-by-side" }, + keymaps = { + view = { + stage_hunk = "H", + unstage_hunk = "J", + discard_hunk = "K", + open_in_prev_tab = "P", + }, + }, + }) + repo = nil + paths = {} + original_cwd = vim.fn.getcwd() + end) + + after_each(function() + while vim.fn.tabpagenr("$") > 1 do + vim.cmd("tabclose!") + end + + for _, path in ipairs(paths) do + pcall(vim.fn.delete, path) + end + if repo then + repo.cleanup() + end + vim.fn.chdir(original_cwd) + end) + + it("toggles a normal diff per session without changing the default layout", function() + local tabpage, left, right = create_standalone_diff({ "one", "two", "three" }, { "one", "changed", "three" }) + track(left) + track(right) + assert.equals("side-by-side", view.get_current_layout(tabpage)) + assert.equals("side-by-side", require("codediff.config").options.diff.layout) + + assert.is_true(view.toggle_layout(tabpage)) + local toggled_inline = vim.wait(5000, function() + local session = lifecycle.get_session(tabpage) + return session and session.layout == "inline" and session.original_win == session.modified_win and vim.api.nvim_win_is_valid(session.modified_win) + end, 50) + assert.is_true(toggled_inline, "Diff should toggle into inline layout") + assert.equals("side-by-side", require("codediff.config").options.diff.layout) + + assert.is_true(view.toggle_layout(tabpage)) + local toggled_back = vim.wait(5000, function() + local session = lifecycle.get_session(tabpage) + return session + and session.layout == "side-by-side" + and session.original_win + and session.modified_win + and session.original_win ~= session.modified_win + and vim.api.nvim_win_is_valid(session.original_win) + and vim.api.nvim_win_is_valid(session.modified_win) + and not session.single_pane + end, 50) + assert.is_true(toggled_back, "Diff should toggle back into side-by-side layout") + assert.equals("side-by-side", require("codediff.config").options.diff.layout) + end) + + it("rerenders the current history selection when toggling layouts", function() + repo = h.create_temp_git_repo() + repo.write_file("file.txt", { "version 1", "shared" }) + repo.git("add file.txt") + repo.git('commit -m "first"') + repo.write_file("file.txt", { "version 2", "shared", "added line" }) + repo.git("add file.txt") + repo.git('commit -m "second"') + repo.write_file("file.txt", { "version 3", "shared changed", "added line" }) + repo.git("add file.txt") + repo.git('commit -m "third"') + + local tabpage, session, history = open_history_and_wait(repo, "file.txt") + local selected = vim.deepcopy(history.current_selection) + + assert.equals("side-by-side", session.layout) + assert.is_not_nil(selected, "History should track the current file selection") + + assert.is_true(view.toggle_layout(tabpage)) + wait_for(tabpage, function(current_session) + local current_history = lifecycle.get_explorer(tabpage) + local mod_buf = current_session.modified_bufnr + local marks = mod_buf and vim.api.nvim_buf_is_valid(mod_buf) and vim.api.nvim_buf_get_extmarks(mod_buf, inline.ns_inline, 0, -1, {}) or {} + return current_session.layout == "inline" + and current_session.original_win == current_session.modified_win + and current_history + and current_history.current_selection + and current_history.current_selection.commit_hash == selected.commit_hash + and current_history.current_selection.path == selected.path + and #marks > 0 + end, "History toggle should replay the selected file as a native inline render") + + assert.is_true(view.toggle_layout(tabpage)) + wait_for(tabpage, function(current_session) + local current_history = lifecycle.get_explorer(tabpage) + return current_session.layout == "side-by-side" + and current_session.original_win + and current_session.modified_win + and current_session.original_win ~= current_session.modified_win + and vim.api.nvim_win_is_valid(current_session.original_win) + and vim.api.nvim_win_is_valid(current_session.modified_win) + and current_history + and current_history.current_selection + and current_history.current_selection.commit_hash == selected.commit_hash + and current_history.current_selection.path == selected.path + end, "History toggle should restore the same selected file in side-by-side mode") + end) + + it("toggles an untracked single-file preview without losing preview state", function() + repo = h.create_temp_git_repo() + repo.write_file("tracked.txt", { "tracked" }) + repo.git("add tracked.txt") + repo.git('commit -m "initial"') + repo.write_file("toggle_untracked.txt", { "preview line" }) + + local tabpage, _, explorer = open_codediff_and_wait(repo, "tracked.txt") + local file_path = repo.path("toggle_untracked.txt") + select_explorer_file(tabpage, explorer, { + path = "toggle_untracked.txt", + status = "??", + git_root = repo.dir, + group = "unstaged", + }, function(session) + return session.single_pane == true + and session.modified_path == file_path + and session.modified_win + and not session.original_win + and vim.api.nvim_win_is_valid(session.modified_win) + end) + + assert.is_true(view.toggle_layout(tabpage)) + local inline_ready = vim.wait(5000, function() + local session = lifecycle.get_session(tabpage) + return session + and session.layout == "inline" + and session.original_win == session.modified_win + and session.modified_path == file_path + end, 50) + assert.is_true(inline_ready, "Untracked preview should toggle into inline layout") + + assert.is_true(view.toggle_layout(tabpage)) + local side_by_side_ready = vim.wait(5000, function() + local session = lifecycle.get_session(tabpage) + return session + and session.layout == "side-by-side" + and session.single_pane == true + and session.modified_path == file_path + and session.modified_win + and not session.original_win + and vim.api.nvim_win_is_valid(session.modified_win) + end, 50) + assert.is_true(side_by_side_ready, "Untracked preview should toggle back into single-pane side-by-side") + end) + + it("preserves original-side deleted previews across layout toggles", function() + repo = h.create_temp_git_repo() + repo.write_file("keep.txt", { "keep" }) + repo.write_file("gone.txt", { "tracked line" }) + repo.git("add keep.txt gone.txt") + repo.git('commit -m "add files"') + vim.fn.delete(repo.path("gone.txt")) + + local tabpage, _, explorer = open_codediff_and_wait(repo, "keep.txt") + select_explorer_file(tabpage, explorer, { + path = "gone.txt", + status = "D", + git_root = repo.dir, + group = "unstaged", + }, function(session) + return session.single_pane == true + and session.original_win + and not session.modified_win + and session.original_revision == ":0" + and session.original_path == repo.path("gone.txt") + and vim.api.nvim_win_is_valid(session.original_win) + end) + + assert.is_true(view.toggle_layout(tabpage)) + local inline_deleted_ready = vim.wait(5000, function() + local session = lifecycle.get_session(tabpage) + local diff_buf = session and session.original_win and vim.api.nvim_win_is_valid(session.original_win) and vim.api.nvim_win_get_buf(session.original_win) or nil + return session + and session.layout == "inline" + and session.original_win == session.modified_win + and session.original_bufnr == diff_buf + and session.original_revision == ":0" + and session.original_path == "gone.txt" + and session.modified_path == "" + end, 50) + assert.is_true(inline_deleted_ready, "Deleted preview should stay logically on the original side in inline mode") + + assert.is_true(view.toggle_layout(tabpage)) + local restored_deleted_ready = vim.wait(5000, function() + local session = lifecycle.get_session(tabpage) + return session + and session.layout == "side-by-side" + and session.single_pane == true + and session.original_win + and not session.modified_win + and session.original_path == repo.path("gone.txt") + and vim.api.nvim_win_is_valid(session.original_win) + end, 50) + assert.is_true(restored_deleted_ready, "Deleted preview should restore to the original side in side-by-side mode") + end) + + it("toggles the welcome page without leaving welcome state", function() + local tabpage = create_explorer_placeholder(h.get_temp_dir()) + local welcome_buf = welcome.create_buffer(80, 24) + side_by_side.show_welcome(tabpage, welcome_buf) + + local side_by_side_welcome = vim.wait(5000, function() + local session = lifecycle.get_session(tabpage) + return session and session.single_pane == true and session.modified_win and not session.original_win and welcome.is_welcome_buffer(session.modified_bufnr) + end, 50) + assert.is_true(side_by_side_welcome, "Welcome page should start in side-by-side single-pane mode") + + assert.is_true(view.toggle_layout(tabpage)) + local inline_welcome = vim.wait(5000, function() + local session = lifecycle.get_session(tabpage) + return session + and session.layout == "inline" + and session.original_win == session.modified_win + and welcome.is_welcome_buffer(session.modified_bufnr) + end, 50) + assert.is_true(inline_welcome, "Welcome page should toggle into inline layout") + + assert.is_true(view.toggle_layout(tabpage)) + local restored_welcome = vim.wait(5000, function() + local session = lifecycle.get_session(tabpage) + return session + and session.layout == "side-by-side" + and session.single_pane == true + and session.modified_win + and not session.original_win + and welcome.is_welcome_buffer(session.modified_bufnr) + end, 50) + assert.is_true(restored_welcome, "Welcome page should toggle back into side-by-side welcome state") + end) + + it("keeps hunk navigation working after toggling layouts", function() + local tabpage, left, right = create_standalone_diff({ "keep", "old one", "middle", "old two", "tail" }, { "keep", "new one", "middle", "new two", "tail" }) + track(left) + track(right) + + assert.is_true(view.toggle_layout(tabpage)) + wait_for(tabpage, function(session) + return session.layout == "inline" + and session.modified_win + and vim.api.nvim_win_is_valid(session.modified_win) + and session.stored_diff_result + and session.stored_diff_result.changes + and #session.stored_diff_result.changes == 2 + end, "Inline diff should be fully rendered after toggle") + + local session = lifecycle.get_session(tabpage) + vim.api.nvim_set_current_win(session.modified_win) + vim.api.nvim_win_set_cursor(session.modified_win, { 1, 0 }) + assert.is_true(navigation.next_hunk(), "next_hunk should work in inline layout after toggle") + assert.same({ 2, 0 }, vim.api.nvim_win_get_cursor(session.modified_win)) + + vim.api.nvim_win_set_cursor(session.modified_win, { 5, 0 }) + assert.is_true(navigation.prev_hunk(), "prev_hunk should work in inline layout after toggle") + assert.same({ 4, 0 }, vim.api.nvim_win_get_cursor(session.modified_win)) + + assert.is_true(view.toggle_layout(tabpage)) + wait_for(tabpage, function(s) + return s.layout == "side-by-side" + and s.original_win + and s.modified_win + and s.original_win ~= s.modified_win + and vim.api.nvim_win_is_valid(s.original_win) + and vim.api.nvim_win_is_valid(s.modified_win) + and not s.single_pane + and s.stored_diff_result + and s.stored_diff_result.changes + and #s.stored_diff_result.changes == 2 + end, "Side-by-side diff should be fully restored after toggling back") + session = lifecycle.get_session(tabpage) + vim.api.nvim_set_current_win(session.modified_win) + vim.api.nvim_win_set_cursor(session.modified_win, { 1, 0 }) + assert.is_true(navigation.next_hunk(), "next_hunk should still work after toggling back to side-by-side") + assert.same({ 2, 0 }, vim.api.nvim_win_get_cursor(session.modified_win)) + end) + + it("keeps diffget and diffput working after toggling back to side-by-side", function() + local tabpage, left, right = create_standalone_diff({ "line 1", "left value", "line 3" }, { "line 1", "right value", "line 3" }) + track(left) + track(right) + + assert.is_true(view.toggle_layout(tabpage)) + assert.is_true(view.toggle_layout(tabpage)) + wait_for(tabpage, function(session) + return session.layout == "side-by-side" + and session.original_win + and session.modified_win + and vim.api.nvim_win_is_valid(session.original_win) + and vim.api.nvim_win_is_valid(session.modified_win) + and session.stored_diff_result + and session.stored_diff_result.changes + and #session.stored_diff_result.changes > 0 + end, "Side-by-side diff should be ready before diffget/diffput assertions") + + local session = lifecycle.get_session(tabpage) + local get_cb = get_buffer_mapping_callback(session.original_bufnr, "do") + local put_cb = get_buffer_mapping_callback(session.modified_bufnr, "dp") + local hunk_line = session.stored_diff_result.changes[1].original.start_line + assert.is_function(get_cb, "diff_get mapping should exist after toggling back") + assert.is_function(put_cb, "diff_put mapping should exist after toggling back") + vim.api.nvim_set_current_win(session.original_win) + vim.api.nvim_win_set_cursor(session.original_win, { hunk_line, 0 }) + get_cb() + vim.wait(100) + assert.same({ "line 1", "right value", "line 3" }, vim.api.nvim_buf_get_lines(session.original_bufnr, 0, -1, false)) + + vim.api.nvim_buf_set_lines(session.original_bufnr, 0, -1, false, { "line 1", "left again", "line 3" }) + view.update(tabpage, { + mode = "standalone", + original_path = left, + modified_path = right, + }, false) + assert.is_true(h.wait_for_session_ready(tabpage, 10000), "Diff should rerender after manual reset") + + session = lifecycle.get_session(tabpage) + hunk_line = session.stored_diff_result.changes[1].modified.start_line + vim.api.nvim_set_current_win(session.modified_win) + vim.api.nvim_win_set_cursor(session.modified_win, { hunk_line, 0 }) + put_cb = get_buffer_mapping_callback(session.modified_bufnr, "dp") + put_cb() + vim.wait(100) + assert.same({ "line 1", "right value", "line 3" }, vim.api.nvim_buf_get_lines(session.original_bufnr, 0, -1, false)) + end) + + it("keeps open_in_prev_tab working after toggle", function() + vim.cmd("tabnew") + local previous_tab = vim.api.nvim_get_current_tabpage() + local tabpage, left, right = create_standalone_diff({ "line 1", "left", "line 3" }, { "line 1", "right", "line 3" }) + track(left) + track(right) + + assert.is_true(view.toggle_layout(tabpage)) + local session = lifecycle.get_session(tabpage) + vim.api.nvim_set_current_win(session.modified_win) + vim.api.nvim_win_set_cursor(session.modified_win, { 2, 0 }) + + local callback = get_buffer_mapping_callback(session.modified_bufnr, "P") + assert.is_function(callback, "open_in_prev_tab mapping should exist on toggled buffers") + callback() + + assert.equals(previous_tab, vim.api.nvim_get_current_tabpage()) + assert.equals(vim.fn.resolve(vim.fn.fnamemodify(right, ":p")), vim.fn.resolve(vim.api.nvim_buf_get_name(vim.api.nvim_get_current_buf()))) + assert.same({ 2, 0 }, vim.api.nvim_win_get_cursor(0)) + end) + + it("keeps explorer file navigation working after toggle", function() + repo = h.create_temp_git_repo() + repo.write_file("file1.txt", { "one" }) + repo.write_file("file2.txt", { "two" }) + repo.git("add file1.txt file2.txt") + repo.git('commit -m "initial files"') + repo.write_file("file1.txt", { "one changed" }) + repo.write_file("file2.txt", { "two changed" }) + + local tabpage, _, explorer = open_codediff_and_wait(repo, "file1.txt") + select_explorer_file(tabpage, explorer, { + path = "file1.txt", + status = "M", + git_root = repo.dir, + group = "unstaged", + }, function(session) + return session.modified_path == repo.path("file1.txt") + end) + + assert.is_true(view.toggle_layout(tabpage)) + assert.is_true(navigation.next_file(), "next_file should work after toggling layout") + local moved_next = vim.wait(10000, function() + local session = lifecycle.get_session(tabpage) + return session and session.modified_path == repo.path("file2.txt") + end, 100) + assert.is_true(moved_next, "Explorer next_file should move to the next file after toggle") + + assert.is_true(navigation.prev_file(), "prev_file should work after toggling layout") + local moved_prev = vim.wait(10000, function() + local session = lifecycle.get_session(tabpage) + return session and session.modified_path == repo.path("file1.txt") + end, 100) + assert.is_true(moved_prev, "Explorer prev_file should move back after toggle") + end) + + it("replays the native inline explorer layout exactly when toggled", function() + repo = h.create_temp_git_repo() + repo.write_file("file.txt", { "line 1", "line 2" }) + repo.git("add file.txt") + repo.git('commit -m "initial"') + repo.write_file("file.txt", { "line 1", "line 2 changed" }) + + local toggled_tabpage, _, toggled_explorer = open_codediff_and_wait(repo, "file.txt") + select_explorer_file(toggled_tabpage, toggled_explorer, { + path = "file.txt", + status = "M", + git_root = repo.dir, + group = "unstaged", + }, function(session) + return session.modified_path == repo.path("file.txt") and session.layout == "side-by-side" + end) + + assert.is_true(view.toggle_layout(toggled_tabpage)) + wait_for(toggled_tabpage, function(session) + return session.layout == "inline" + and session.original_win == session.modified_win + and session.modified_win + and vim.api.nvim_win_is_valid(session.modified_win) + and session.stored_diff_result + and session.stored_diff_result.changes + and #session.stored_diff_result.changes > 0 + end, "Toggled explorer diff should settle into inline layout") + local toggled_snapshot = capture_layout_snapshot(toggled_tabpage) + + vim.cmd("tabclose") + + require("codediff").setup({ + diff = { layout = "inline" }, + keymaps = { + view = { + stage_hunk = "H", + unstage_hunk = "J", + discard_hunk = "K", + open_in_prev_tab = "P", + }, + }, + }) + + local native_tabpage, _, native_explorer = open_codediff_and_wait(repo, "file.txt") + select_explorer_file(native_tabpage, native_explorer, { + path = "file.txt", + status = "M", + git_root = repo.dir, + group = "unstaged", + }, function(session) + return session.modified_path == repo.path("file.txt") + and session.layout == "inline" + and session.original_win == session.modified_win + and session.stored_diff_result + and session.stored_diff_result.changes + and #session.stored_diff_result.changes > 0 + end) + + local native_snapshot = capture_layout_snapshot(native_tabpage) + assert.same(native_snapshot, toggled_snapshot) + end) + + it("keeps stage and unstage hunk working after toggle", function() + repo = h.create_temp_git_repo() + repo.write_file("file.txt", { "line 1", "line 2", "line 3" }) + repo.git("add file.txt") + repo.git('commit -m "initial"') + repo.write_file("file.txt", { "line 1", "changed line", "line 3" }) + + local tabpage, _, explorer = open_codediff_and_wait(repo, "file.txt") + select_explorer_file(tabpage, explorer, { + path = "file.txt", + status = "M", + git_root = repo.dir, + group = "unstaged", + }, function(session) + return session.modified_path == repo.path("file.txt") + and session.modified_revision == nil + and session.modified_bufnr + and vim.api.nvim_buf_is_valid(session.modified_bufnr) + and vim.api.nvim_buf_line_count(session.modified_bufnr) >= 3 + and session.stored_diff_result + and session.stored_diff_result.changes + and #session.stored_diff_result.changes > 0 + end) + + assert.is_true(view.toggle_layout(tabpage)) + wait_for(tabpage, function(s) + return s.layout == "inline" + and s.modified_win + and vim.api.nvim_win_is_valid(s.modified_win) + and s.original_bufnr + and vim.api.nvim_buf_is_valid(s.original_bufnr) + and s.modified_bufnr + and vim.api.nvim_buf_is_valid(s.modified_bufnr) + and vim.api.nvim_win_get_buf(s.modified_win) == s.modified_bufnr + and vim.api.nvim_buf_line_count(s.modified_bufnr) >= 3 + and s.stored_diff_result + and s.stored_diff_result.changes + and #s.stored_diff_result.changes > 0 + end, "Inline explorer diff should be ready before staging") + local session = lifecycle.get_session(tabpage) + move_cursor_to_hunk(session.modified_win, session.modified_bufnr, session.stored_diff_result.changes[1].modified) + + local stage_cb = get_buffer_mapping_callback(vim.api.nvim_win_get_buf(session.modified_win), "H") + assert.is_function(stage_cb, "stage_hunk mapping should exist after toggle") + stage_cb() + + -- Spin to let the full async chain complete (git apply → callback → refresh → status → render) + vim.wait(10000, function() return false end, 50) + + local s = lifecycle.get_session(tabpage) + assert.is_true(s and s.layout == "inline" and s.modified_revision == ":0", "Staging a hunk should still work after toggle") + + assert.is_true(view.toggle_layout(tabpage)) + wait_for(tabpage, function(s) + return s.layout == "side-by-side" + and s.modified_win + and vim.api.nvim_win_is_valid(s.modified_win) + and s.modified_bufnr + and vim.api.nvim_buf_is_valid(s.modified_bufnr) + and vim.api.nvim_win_get_buf(s.modified_win) == s.modified_bufnr + and vim.api.nvim_buf_line_count(s.modified_bufnr) >= 3 + and s.stored_diff_result + and s.stored_diff_result.changes + and #s.stored_diff_result.changes > 0 + end, "Side-by-side staged diff should be ready before unstaging") + session = lifecycle.get_session(tabpage) + move_cursor_to_hunk(session.modified_win, session.modified_bufnr, session.stored_diff_result.changes[1].modified) + + local unstage_cb = get_buffer_mapping_callback(vim.api.nvim_win_get_buf(session.modified_win), "J") + assert.is_function(unstage_cb, "unstage_hunk mapping should exist after toggling back") + unstage_cb() + + -- Spin to let the full async chain complete + vim.wait(10000, function() return false end, 50) + + local s = lifecycle.get_session(tabpage) + assert.is_true(s and s.modified_revision == nil, "Unstaging a hunk should still work after toggling back") + end) + + -- SKIPPED: requires two back-to-back async git operations (apply + status) + -- which is unreliable on Windows CI. Re-enable when test helper API supports + -- deterministic async chains. + pending("keeps discard hunk working after toggle", function() + repo = h.create_temp_git_repo() + repo.write_file("file.txt", { "line 1", "line 2", "line 3" }) + repo.git("add file.txt") + repo.git('commit -m "initial"') + repo.write_file("file.txt", { "line 1", "discard me", "line 3" }) + + local tabpage, _, explorer = open_codediff_and_wait(repo, "file.txt") + select_explorer_file(tabpage, explorer, { + path = "file.txt", + status = "M", + git_root = repo.dir, + group = "unstaged", + }, function(session) + return session.modified_path == repo.path("file.txt") + and session.modified_revision == nil + and session.modified_bufnr + and vim.api.nvim_buf_is_valid(session.modified_bufnr) + and vim.api.nvim_buf_line_count(session.modified_bufnr) >= 3 + and session.stored_diff_result + and session.stored_diff_result.changes + and #session.stored_diff_result.changes > 0 + end) + + assert.is_true(view.toggle_layout(tabpage)) + wait_for(tabpage, function(s) + return s.layout == "inline" + and s.modified_win + and vim.api.nvim_win_is_valid(s.modified_win) + and s.original_bufnr + and vim.api.nvim_buf_is_valid(s.original_bufnr) + and s.modified_bufnr + and vim.api.nvim_buf_is_valid(s.modified_bufnr) + and vim.api.nvim_win_get_buf(s.modified_win) == s.modified_bufnr + and vim.api.nvim_buf_line_count(s.modified_bufnr) >= 3 + and s.stored_diff_result + and s.stored_diff_result.changes + and #s.stored_diff_result.changes > 0 + end, "Inline explorer diff should be ready before discarding") + local session = lifecycle.get_session(tabpage) + move_cursor_to_hunk(session.modified_win, session.modified_bufnr, session.stored_diff_result.changes[1].modified) + + local old_select = vim.ui.select + vim.ui.select = function(items, _, on_choice) + on_choice(items[1]) + end + + local discard_cb = get_buffer_mapping_callback(vim.api.nvim_win_get_buf(session.modified_win), "K") + assert.is_function(discard_cb, "discard_hunk mapping should exist after toggle") + discard_cb() + + vim.wait(10000, function() return false end, 50) + + vim.ui.select = old_select + + local s = lifecycle.get_session(tabpage) + assert.is_true(s and welcome.is_welcome_buffer(s.modified_bufnr), "Discarding the last hunk after toggle should restore a clean welcome state") + end) + + it("does not persist the layout override across separate CodeDiff runs", function() + repo = h.create_temp_git_repo() + repo.write_file("file.txt", { "line 1", "line 2" }) + repo.git("add file.txt") + repo.git('commit -m "initial"') + repo.write_file("file.txt", { "line 1", "line 2 changed" }) + + local tabpage = open_codediff_and_wait(repo, "file.txt") + assert.is_true(view.toggle_layout(tabpage)) + local toggled = vim.wait(5000, function() + local session = lifecycle.get_session(tabpage) + return session and session.layout == "inline" + end, 100) + assert.is_true(toggled, "First CodeDiff run should toggle to inline") + + vim.api.nvim_set_current_tabpage(tabpage) + vim.cmd("tabclose") + + local second_tabpage = open_codediff_and_wait(repo, "file.txt") + local second_session = lifecycle.get_session(second_tabpage) + assert.equals("side-by-side", second_session.layout) + assert.equals("side-by-side", view.get_current_layout(second_tabpage)) + end) + + it("blocks toggling in conflict-mode sessions", function() + local tabpage, left, right = create_standalone_diff({ "left" }, { "right" }) + track(left) + track(right) + + local session = lifecycle.get_session(tabpage) + session.result_win = session.modified_win + + assert.is_false(view.toggle_layout(tabpage)) + assert.equals("side-by-side", session.layout) + end) +end)