From f2d6be2e4910542615dc68af722ba0426863238f Mon Sep 17 00:00:00 2001 From: Julio Garcia Date: Tue, 3 Mar 2026 13:27:26 +0100 Subject: [PATCH] feat(diff): add toggle for side-by-side and inline layouts Adds a new keymap and action to toggle the diff layout between 'side-by-side' and 'inline' views. The toggle is available from both the explorer and diff view buffers, and updates the global config so subsequent file selections use the chosen layout. Prevents toggling in conflict mode and ensures proper buffer/window management during the transition. Improves usability for users who prefer different diff layouts. --- lua/codediff/config.lua | 1 + lua/codediff/ui/explorer/actions.lua | 116 +++++++++++++++++++++++++++ lua/codediff/ui/explorer/init.lua | 1 + lua/codediff/ui/explorer/keymaps.lua | 7 ++ lua/codediff/ui/view/keymaps.lua | 9 +++ 5 files changed, 134 insertions(+) diff --git a/lua/codediff/config.lua b/lua/codediff/config.lua index 0b52d3d..3bc4c1b 100644 --- a/lua/codediff/config.lua +++ b/lua/codediff/config.lua @@ -111,6 +111,7 @@ M.defaults = { restore = "X", -- Discard changes to file (restore to index/HEAD) toggle_changes = "gu", -- Toggle Changes (unstaged) group visibility toggle_staged = "gs", -- Toggle Staged Changes group visibility + toggle_layout = "gl", -- Toggle diff layout between 'side-by-side' and 'inline' }, history = { select = "", -- Select commit/file or toggle expand diff --git a/lua/codediff/ui/explorer/actions.lua b/lua/codediff/ui/explorer/actions.lua index 6c9b3fa..07111f2 100644 --- a/lua/codediff/ui/explorer/actions.lua +++ b/lua/codediff/ui/explorer/actions.lua @@ -297,6 +297,122 @@ function M.toggle_stage_entry(explorer, tree) end end +-- Toggle diff layout between 'side-by-side' and 'inline' +function M.toggle_layout(explorer) + local lifecycle = require("codediff.ui.lifecycle") + local layout_manager = require("codediff.ui.layout") + + local tabpage = explorer and explorer.tabpage or vim.api.nvim_get_current_tabpage() + local session = lifecycle.get_session(tabpage) + + if not session then + vim.notify("No active diff session", vim.log.levels.WARN) + return + end + + -- Don't toggle in conflict mode + 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 + end + + local current_layout = session.layout or "side-by-side" + local new_layout = current_layout == "inline" and "side-by-side" or "inline" + + -- Update global config so subsequent file selections use the new layout + config.options.diff.layout = new_layout + + -- Build session_config from current session state for re-rendering + 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, + } + + local is_placeholder = (session.original_path == "" and session.modified_path == "") + + if new_layout == "side-by-side" then + -- inline → side-by-side: create new window for the original side + local modified_win = session.modified_win + if not modified_win or not vim.api.nvim_win_is_valid(modified_win) then + return + end + + -- Clear inline decorations from the modified buffer + local inline_mod = require("codediff.ui.inline") + if session.modified_bufnr and vim.api.nvim_buf_is_valid(session.modified_bufnr) then + inline_mod.clear(session.modified_bufnr) + end + lifecycle.clear_highlights(session.modified_bufnr) + + -- Create original window (split on the appropriate side) + local split_cmd = config.options.diff.original_position == "right" and "rightbelow vsplit" or "leftabove vsplit" + vim.api.nvim_set_current_win(modified_win) + vim.cmd(split_cmd) + local original_win = vim.api.nvim_get_current_win() + vim.w[original_win].codediff_restore = 1 + + -- Update session window state + session.original_win = original_win + session.layout = nil -- nil means side-by-side (default) + + if is_placeholder then + -- Load scratch buffer into the new original window + local orig_scratch = vim.api.nvim_create_buf(false, true) + vim.bo[orig_scratch].buftype = "nofile" + vim.api.nvim_win_set_buf(original_win, orig_scratch) + session.original_bufnr = orig_scratch + layout_manager.arrange(tabpage) + else + vim.schedule(function() + require("codediff.ui.view.side_by_side").update(tabpage, session_config, false) + layout_manager.arrange(tabpage) + end) + end + else + -- side-by-side → inline: close original_win, keep modified_win + local original_win = session.original_win + local modified_win = session.modified_win + + if not modified_win or not vim.api.nvim_win_is_valid(modified_win) then + return + end + + -- Clear diff highlights from both buffers + lifecycle.clear_highlights(session.original_bufnr) + lifecycle.clear_highlights(session.modified_bufnr) + + -- Disable scrollbind on modified window before closing original + if vim.api.nvim_win_is_valid(modified_win) then + vim.wo[modified_win].scrollbind = false + end + + -- Close original window if it is distinct from modified + if original_win and vim.api.nvim_win_is_valid(original_win) and original_win ~= modified_win then + vim.api.nvim_set_current_win(modified_win) + pcall(vim.api.nvim_win_close, original_win, false) + end + + -- Collapse session to single window + session.original_win = modified_win + session.layout = "inline" + + if is_placeholder then + layout_manager.arrange(tabpage) + else + vim.schedule(function() + require("codediff.ui.view.inline_view").update(tabpage, session_config, false) + layout_manager.arrange(tabpage) + end) + end + end + + vim.notify("Layout: " .. new_layout, vim.log.levels.INFO) +end + -- Stage all files function M.stage_all(explorer) if not explorer or not explorer.git_root then diff --git a/lua/codediff/ui/explorer/init.lua b/lua/codediff/ui/explorer/init.lua index 63e50b1..77455fb 100644 --- a/lua/codediff/ui/explorer/init.lua +++ b/lua/codediff/ui/explorer/init.lua @@ -19,6 +19,7 @@ M.navigate_next = actions.navigate_next M.navigate_prev = actions.navigate_prev M.toggle_visibility = actions.toggle_visibility M.toggle_view_mode = actions.toggle_view_mode +M.toggle_layout = actions.toggle_layout M.toggle_stage_entry = actions.toggle_stage_entry M.toggle_stage_file = actions.toggle_stage_file M.stage_all = actions.stage_all diff --git a/lua/codediff/ui/explorer/keymaps.lua b/lua/codediff/ui/explorer/keymaps.lua index 2ed79a2..11a32f8 100644 --- a/lua/codediff/ui/explorer/keymaps.lua +++ b/lua/codediff/ui/explorer/keymaps.lua @@ -171,6 +171,13 @@ function M.setup(explorer) end, vim.tbl_extend("force", map_options, { buffer = split.bufnr, desc = "Toggle Staged Changes visibility" })) end + -- Toggle layout between 'side-by-side' and 'inline' (gl) + if explorer_keymaps.toggle_layout then + vim.keymap.set("n", explorer_keymaps.toggle_layout, function() + actions_module.toggle_layout(explorer) + end, vim.tbl_extend("force", map_options, { buffer = split.bufnr, desc = "Toggle side-by-side/inline layout" })) + end + -- Note: next_file/prev_file keymaps are set via view/keymaps.lua:setup_all_keymaps() -- which uses set_tab_keymap to set them on all buffers including explorer end diff --git a/lua/codediff/ui/view/keymaps.lua b/lua/codediff/ui/view/keymaps.lua index d72e46a..44be8cf 100644 --- a/lua/codediff/ui/view/keymaps.lua +++ b/lua/codediff/ui/view/keymaps.lua @@ -901,6 +901,15 @@ function M.setup_all_keymaps(tabpage, original_bufnr, modified_bufnr, is_explore if keymaps.align_move and not is_inline and config.options.diff.compute_moves then lifecycle.set_tab_keymap(tabpage, "n", keymaps.align_move, align_move, { desc = "Align moved code block" }) end + + -- Toggle layout (side-by-side ↔ inline) - tab-wide, available from any diff buffer + local explorer_keymaps = config.options.keymaps.explorer or {} + if explorer_keymaps.toggle_layout then + lifecycle.set_tab_keymap(tabpage, "n", explorer_keymaps.toggle_layout, function() + local explorer_actions = require("codediff.ui.explorer.actions") + explorer_actions.toggle_layout(nil) + end, { desc = "Toggle side-by-side/inline layout" }) + end end return M