@@ -70,6 +70,8 @@ local hunk_keymaps = {}
7070--- @type table<integer , integer>
7171local hunk_keymap_autocmds = {}
7272
73+ --- @alias diffs.ReviewMapLayout " unified" | " stacked"
74+
7375--- @class diffs.ReviewSplitState
7476--- @field left_buf integer
7577--- @field right_buf integer
@@ -80,6 +82,7 @@ local hunk_keymap_autocmds = {}
8082--- @field display string
8183--- @field review_lines string[]
8284--- @field list_opts table ?
85+ --- @field map_layout diffs.ReviewMapLayout
8386--- @field selected_file string
8487--- @field selected_key string ?
8588--- @field selected_diff_spec diffs.DiffSpec
424427
425428--- @param bufnr integer
426429--- @param vertical ? boolean
427- local function show_generated_diff_buffer (bufnr , vertical )
428- local existing_win = M .find_diffs_window ()
430+ --- @param replace_win ? integer
431+ --- @return boolean
432+ local function show_generated_diff_buffer (bufnr , vertical , replace_win )
433+ local replace_requested = type (replace_win ) == ' number'
434+ if replace_requested and not vim .api .nvim_win_is_valid (replace_win ) then
435+ notify (' replacement diff window is no longer valid' , vim .log .levels .WARN )
436+ return false
437+ end
438+
439+ local existing_win = replace_requested and replace_win or M .find_diffs_window ()
429440 if existing_win then
430441 vim .api .nvim_set_current_win (existing_win )
431442 split .release_pair_window_options (existing_win )
@@ -436,6 +447,7 @@ local function show_generated_diff_buffer(bufnr, vertical)
436447 clear_generated_diff_window_bindings (vim .api .nvim_get_current_win ())
437448 vim .api .nvim_win_set_buf (0 , bufnr )
438449 end
450+ return true
439451end
440452
441453--- @param bufnr integer
@@ -502,17 +514,50 @@ end
502514
503515--- @class diffs.ReviewDepsOpts
504516--- @field rail_style ? diffs.RailStyle
517+ --- @field review_layout ? diffs.ReviewMapLayout
518+ --- @field replace_win ? integer
519+
520+ --- @param layout ? string
521+ --- @return diffs.ReviewMapLayout
522+ local function normalize_review_map_layout (layout )
523+ return layout == ' stacked' and ' stacked' or ' unified'
524+ end
525+
526+ --- @param rail_style ? diffs.RailStyle
527+ --- @return diffs.ReviewMapLayout
528+ local function review_layout_for_rail_style (rail_style )
529+ return rail_style == ' single' and ' stacked' or ' unified'
530+ end
531+
532+ --- @param bufnr integer
533+ --- @param display string
534+ --- @param layout diffs.ReviewMapLayout
535+ local function setup_review_map_buffer (bufnr , display , layout )
536+ vim .b [bufnr ].diffs_review = { display = display , layout = layout }
537+ if not get_buffer_keymap (bufnr , ' n' , ' gs' ) then
538+ vim .keymap .set (
539+ ' n' ,
540+ ' gs' ,
541+ ' <Plug>(diffs-review-toggle-layout)' ,
542+ { buffer = bufnr , remap = true }
543+ )
544+ end
545+ end
505546
506547--- @param opts ? diffs.ReviewDepsOpts
507548--- @return diffs.ReviewDeps
508549local function review_deps (opts )
509550 opts = opts or {}
510551 return {
511552 create_generated_diff_buffer = create_generated_diff_buffer ,
512- show_generated_diff_buffer = show_generated_diff_buffer ,
553+ show_generated_diff_buffer = function (bufnr , vertical )
554+ return show_generated_diff_buffer (bufnr , vertical , opts .replace_win )
555+ end ,
513556 attach_generated_diff_buffer = attach_generated_diff_buffer ,
514557 replace_combined_diffs = replace_combined_diffs ,
515558 rail_style = opts .rail_style ,
559+ review_layout = opts .review_layout or review_layout_for_rail_style (opts .rail_style ),
560+ setup_review_map_buffer = setup_review_map_buffer ,
516561 }
517562end
518563
@@ -864,7 +909,12 @@ local function complete_diff_command(arglead, cmdline, cursorpos)
864909 return matches
865910end
866911
867- --- @type fun ( spec ?: diffs.ReviewSpec , opts ?: { selection ?: diffs.GeneratedFileSelection , replace_win ?: integer }): integer ?
912+ --- @class diffs.OpenReviewSplitOpts
913+ --- @field selection ? diffs.GeneratedFileSelection
914+ --- @field replace_win ? integer
915+ --- @field map_layout ? diffs.ReviewMapLayout
916+
917+ --- @type fun ( spec ?: diffs.ReviewSpec , opts ?: diffs.OpenReviewSplitOpts ): integer ?
868918local open_review_split
869919
870920--- @param args ? string
@@ -889,6 +939,7 @@ function M.review_command(args, vertical, opts)
889939 parsed .spec .vertical = vertical or false
890940 local bufnr = M .review (parsed .spec , {
891941 rail_style = rail_style_for_layout (parsed .layout ),
942+ review_layout = normalize_review_map_layout (parsed .layout ),
892943 })
893944 return bufnr
894945end
@@ -1061,6 +1112,7 @@ end
10611112--- @field replace_win integer
10621113--- @field selected diffs.GeneratedFileSelection
10631114--- @field repo_root string
1115+ --- @field map_layout diffs.ReviewMapLayout
10641116
10651117--- @param opts ? diffs.ReviewSplitOpts
10661118--- @return diffs.ReviewSplitContext ?, string ?, integer ?, integer ?
@@ -1093,6 +1145,8 @@ local function review_split_context(opts)
10931145 if not source or source .kind ~= ' review' then
10941146 return nil , ' selected file is not from a review buffer' , vim .log .levels .WARN , nil
10951147 end
1148+ local marker = vim .b [review_buf ].diffs_review
1149+ local map_layout = normalize_review_map_layout (type (marker ) == ' table' and marker .layout or nil )
10961150
10971151 local review_win = first_window_for_buffer (review_buf )
10981152 if not review_win then
@@ -1107,6 +1161,7 @@ local function review_split_context(opts)
11071161 replace_win = review_win ,
11081162 selected = selected ,
11091163 repo_root = source .repo_root ,
1164+ map_layout = map_layout ,
11101165 },
11111166 nil ,
11121167 nil ,
@@ -1231,6 +1286,14 @@ local function setup_review_split_panes(state)
12311286 { buffer = buf , remap = true }
12321287 )
12331288 end
1289+ if not get_buffer_keymap (buf , ' n' , ' gs' ) then
1290+ vim .keymap .set (
1291+ ' n' ,
1292+ ' gs' ,
1293+ ' <Plug>(diffs-review-toggle-layout)' ,
1294+ { buffer = buf , remap = true }
1295+ )
1296+ end
12341297 end
12351298 end
12361299end
@@ -1597,7 +1660,7 @@ local function close_existing_review_splits()
15971660end
15981661
15991662--- @param spec diffs.ReviewSpec ?
1600- --- @param opts ? { selection ?: diffs.GeneratedFileSelection , replace_win ?: integer }
1663+ --- @param opts ? diffs.OpenReviewSplitOpts
16011664--- @return integer ?
16021665open_review_split = function (spec , opts )
16031666 opts = opts or {}
@@ -1684,6 +1747,7 @@ open_review_split = function(spec, opts)
16841747 display = normalized .display ,
16851748 review_lines = review_lines ,
16861749 list_opts = list_opts ,
1750+ map_layout = normalize_review_map_layout (opts .map_layout ),
16871751 selected_file = first .file ,
16881752 selected_key = first .key or first .file ,
16891753 selected_diff_spec = diff_spec ,
@@ -1734,9 +1798,107 @@ function M.review_split(opts)
17341798 return open_review_split (review_spec , {
17351799 selection = context .selected ,
17361800 replace_win = context .replace_win ,
1801+ map_layout = context .map_layout ,
17371802 })
17381803end
17391804
1805+ --- @param state diffs.ReviewSplitState
1806+ --- @return integer ?
1807+ local function review_split_keep_window (state )
1808+ local current_win = vim .api .nvim_get_current_win ()
1809+ local current_buf = vim .api .nvim_get_current_buf ()
1810+ if
1811+ (current_buf == state .left_buf or current_buf == state .right_buf )
1812+ and vim .api .nvim_win_is_valid (current_win )
1813+ then
1814+ return current_win
1815+ end
1816+ for _ , win in ipairs ({ state .left_win , state .right_win }) do
1817+ if vim .api .nvim_win_is_valid (win ) then
1818+ local buf = vim .api .nvim_win_get_buf (win )
1819+ if buf == state .left_buf or buf == state .right_buf then
1820+ return win
1821+ end
1822+ end
1823+ end
1824+ return first_window_for_buffer (state .left_buf ) or first_window_for_buffer (state .right_buf )
1825+ end
1826+
1827+ --- @param bufnr integer
1828+ --- @param selected_file string
1829+ --- @param selected_key string ?
1830+ --- @return boolean
1831+ local function goto_review_map_selection (bufnr , selected_file , selected_key )
1832+ local win = first_window_for_buffer (bufnr )
1833+ if not win then
1834+ return false
1835+ end
1836+ for _ , item in ipairs (vim .fn .getqflist ({ items = 0 }).items ) do
1837+ local data = item .user_data and item .user_data .diffs
1838+ if item .bufnr == bufnr and type (data ) == ' table' then
1839+ local matches = selected_key and data .key == selected_key
1840+ or (not selected_key and data .file == selected_file )
1841+ if matches then
1842+ vim .api .nvim_set_current_win (win )
1843+ vim .api .nvim_win_set_cursor (win , { math.max (1 , item .lnum or 1 ), 0 })
1844+ vim .api .nvim_exec_autocmds (' CursorMoved' , { buffer = bufnr })
1845+ return true
1846+ end
1847+ end
1848+ end
1849+ return false
1850+ end
1851+
1852+ --- @param state diffs.ReviewSplitState
1853+ --- @param layout diffs.ReviewMapLayout
1854+ --- @return integer ?
1855+ local function open_review_map_from_split (state , layout )
1856+ local keep_win = review_split_keep_window (state )
1857+ if not keep_win then
1858+ notify (' review split window is no longer visible' , vim .log .levels .WARN )
1859+ return nil
1860+ end
1861+ local source_buf = vim .api .nvim_win_get_buf (keep_win )
1862+ local selected_file = state .selected_file
1863+ local selected_key = state .selected_key
1864+ local review_spec = vim .deepcopy (state .review )
1865+ review_spec .repo = state .repo_root
1866+ forget_review_split (state )
1867+ local split_buffers = split .close_pair_into_window (source_buf , keep_win )
1868+ if not split_buffers then
1869+ review_split_states [state .left_buf ] = state
1870+ review_split_states [state .right_buf ] = state
1871+ attach_review_split_autocmds (state )
1872+ notify (' cannot close review split before opening review map' , vim .log .levels .WARN )
1873+ return nil
1874+ end
1875+ local bufnr = M .review (review_spec , {
1876+ rail_style = rail_style_for_layout (layout ),
1877+ review_layout = layout ,
1878+ replace_win = keep_win ,
1879+ })
1880+ split .delete_pair_buffers (split_buffers )
1881+ if bufnr then
1882+ goto_review_map_selection (bufnr , selected_file , selected_key )
1883+ end
1884+ return bufnr
1885+ end
1886+
1887+ --- @class diffs.ReviewToggleLayoutOpts
1888+ --- @field bufnr ? integer
1889+
1890+ --- @param opts ? diffs.ReviewToggleLayoutOpts
1891+ --- @return integer ?
1892+ function M .review_toggle_layout (opts )
1893+ opts = opts or {}
1894+ local bufnr = opts .bufnr or vim .api .nvim_get_current_buf ()
1895+ local state = resolve_review_split (bufnr )
1896+ if state then
1897+ return open_review_map_from_split (state , state .map_layout )
1898+ end
1899+ return M .review_split ({ bufnr = bufnr })
1900+ end
1901+
17401902--- @param args ? string
17411903--- @param vertical ? boolean
17421904--- @param opts ? { warn_vertical_split ?: boolean }
0 commit comments