···380380 Vars.ui_state.trigger_update $= ())
381381 else (
382382 Vars.set_rebase_preview_active true;
383383- Vars.set_rebase_preview_sources (Vars.get_active_revs ());
383383+ let sources = Vars.get_active_revs () in
384384+ Vars.set_rebase_preview_sources sources;
385385+ (* Default targets = parents of source roots; this preserves the
386386+ current tree shape rather than jumping to the hovered commit. *)
387387+ let fallback_targets () =
388388+ let selected = Vars.get_selected_revs () in
389389+ if List.length selected = 0
390390+ then [ Vars.get_hovered_rev () ]
391391+ else selected
392392+ in
384393 let targets =
385385- let selected = Vars.get_selected_revs () in
386386- if List.length selected = 0 then [ Vars.get_hovered_rev () ] else selected
394394+ try
395395+ let revset = Lwd.peek Vars.ui_state.revset in
396396+ let max_commits = (Vars.config |> Lwd.peek).max_commits in
397397+ let nodes, _ = get_graph_nodes ?revset max_commits in
398398+ Render_jj_graph.default_preview_targets ~sources nodes
399399+ with
400400+ | _ ->
401401+ (* If we cannot read the graph, keep preview usable instead of
402402+ failing the mode toggle outright. *)
403403+ fallback_targets ()
387404 in
388405 Vars.set_rebase_preview_targets targets;
389406 Vars.set_rebase_preview_invalid None;
+1-31
jj_tui/bin/graph_view.ml
···131131 x |> Ui.resize ~mw:10000 ~sw:1 |> Lwd.pure |> W.Box.box ~pad_w:0 ~pad_h:0)
132132 |> Option.value ~default:(Ui.empty |> Lwd.pure)
133133 in
134134- let mode_indicator =
135135- let$ active = Lwd.get Vars.ui_state.rebase_preview_active
136136- and$ mode = Lwd.get Vars.ui_state.rebase_preview_mode
137137- and$ source_mode = Lwd.get Vars.ui_state.rebase_preview_source_mode
138138- and$ invalid = Lwd.get Vars.ui_state.rebase_preview_invalid in
139139- if not active
140140- then Ui.empty
141141- else (
142142- let mode_str =
143143- match mode with
144144- | `Insert_before ->
145145- "insert-before"
146146- | `Insert_after ->
147147- "insert-after"
148148- | `Add_after ->
149149- "add-after"
150150- in
151151- let source_str =
152152- match source_mode with
153153- | `Revisions ->
154154- "revisions"
155155- | `Source ->
156156- "source"
157157- | `Branch ->
158158- "branch"
159159- in
160160- let base = Printf.sprintf "Preview: dest=%s source=%s" mode_str source_str in
161161- let label = match invalid with None -> base | Some msg -> base ^ " - " ^ msg in
162162- W.string label)
163163- in
164134 let items =
165135 let$ rendered_rows, rev_ids =
166136 (*TODO I think this ads a slight delay to everything becasue it makes things need to be renedered twice. maybe I could try getting rid of it*)
···329299 and$ error = Lwd.get error_var in
330300 match error with Some e -> e |> Ui.keyboard_area handleKeys | None -> list_ui
331301 in
332332- W.vbox [ revset_ui; mode_indicator; final_ui ]
302302+ W.vbox [ revset_ui; final_ui ]
333303 ;;
334304end
+114-30
jj_tui/bin/jj_ui.ml
···5757 event
5858 |> forward_events
5959 [
6060- custom;
6161- (fun x->
6262- (*Try to remap the key if needed *)
6363- match x with
6464- | `ASCII k, modifiers ->
6565- let key = Key.{ key = k; modifiers } in
6666- let key=Key_map.Key_Map.find_opt key (Lwd.peek ui_state.config).key_map.remap in
6767- ( match key with
6060+ custom
6161+ ; (fun x ->
6262+ (*Try to remap the key if needed *)
6363+ match x with
6464+ | `ASCII k, modifiers ->
6565+ let key = Key.{ key = k; modifiers } in
6666+ let key =
6767+ Key_map.Key_Map.find_opt key (Lwd.peek ui_state.config).key_map.remap
6868+ in
6969+ (match key with
6870 | Some remap ->
6971 (** This needs to be type converted so our limited variants get upcast into a full nottui event*)
7070- (`Remap (remap.remap, modifiers):> Nottui.Ui.may_handle)
7272+ (`Remap (remap.remap, modifiers) :> Nottui.Ui.may_handle)
7173 | None ->
7274 `Unhandled)
7373- | _ ->
7474- `Unhandled)
7575+ | _ ->
7676+ `Unhandled)
7577 ; (function
7678 | `ASCII 'q', _ ->
7779 Vars.quit $= true;
···150152 (** Makes a UI element responsive to terminal width and focus state
151153 - When focused: shows at full width if terminal is wide enough, or fills terminal if narrow
152154 - When unfocused: shows at normal width if terminal is wide enough, or collapses if narrow *)
153153- let responsive_view ?(shrunk_width=0) ?(shrink_on= `Focus) ~focus ui =
154154- let$* w, h = Lwd.get Vars.term_width_height in
155155- let$ ui = ui
156156-and$ focus = focus|>Focus.status in
157157-158158- let should_shrink = match shrink_on with
159159- | `Focus -> focus|>Focus.has_focus
160160- | `Unfocus -> not (focus|>Focus.has_focus)
161161-155155+ let responsive_view ?(shrunk_width = 0) ?(shrink_on = `Focus) ~focus ui =
156156+ let$* w, h = Lwd.get Vars.term_width_height in
157157+ let$ ui = ui
158158+ and$ focus = focus |> Focus.status in
159159+ let should_shrink =
160160+ match shrink_on with
161161+ | `Focus ->
162162+ focus |> Focus.has_focus
163163+ | `Unfocus ->
164164+ not (focus |> Focus.has_focus)
162165 in
163163- let threhold=(Lwd.peek Vars.config).single_pane_width_threshold in
166166+ let threhold = (Lwd.peek Vars.config).single_pane_width_threshold in
164167 if should_shrink
165165- then if w < threhold
166166- then ui |> Ui.resize ~w:w ~mw:w
167167- else ui
168168+ then if w < threhold then ui |> Ui.resize ~w ~mw:w else ui
168169 else if w < threhold
169169- then ui |> Ui.resize ~w:shrunk_width ~mw:shrunk_width
170170- else ui
170170+ then ui |> Ui.resize ~w:shrunk_width ~mw:shrunk_width
171171+ else ui
171172 ;;
172173173174 (** The primary view for the UI with the file_view graph_view and summary*)
···212213 ~mw:1000)
213214 |> W.Box.focusable ~focus:branch_focus ~pad_h:0 ~pad_w:1
214215 ]
215215- |> responsive_view ~focus:summary_focus ~shrink_on:`Unfocus ~shrunk_width:0
216216+ |> responsive_view ~focus:summary_focus ~shrink_on:`Unfocus ~shrunk_width:0
216217 ; (*Right side summary/status/fileinfo view*)
217218 (let ui =
218219 Show_view.render summary_focus
···222223 |> W.on_focus ~focus:summary_focus (Ui.resize ~sw:3 ~mw:1000)
223224 |> W.Box.focusable ~focus:summary_focus ~pad_h:0 ~pad_w:1
224225 in
225225- responsive_view ~focus:summary_focus ~shrunk_width:0 ui)
226226+ responsive_view ~focus:summary_focus ~shrunk_width:0 ui)
226227 ]
227228 (*These outer prompts can popup and show them selves over the main view*)
228229 |> W.Overlay.text_prompt ~char_count:true ~show_prompt_var:ui_state.show_prompt
229230 |> W.Overlay.popup ~show_popup_var:ui_state.show_popup
230231 |> W.Overlay.selection_list_prompt_filterable
231231- ~list_outline_focus_attr:A.(empty) (*highlighting the outline inside the propt is a bit over the top*)
232232+ ~list_outline_focus_attr:A.(empty)
233233+ (*highlighting the outline inside the propt is a bit over the top*)
232234 ~show_prompt_var:ui_state.show_string_selection_prompt
233235 |> inputs ~custom:(fun x -> Jj_commands.handleInputs Jj_commands.default_list x)
234236 ;;
···247249 |> inputs
248250 ;;
249251252252+ (** Tab bar like [W.keyboard_tabs] but renders preview status + hotkeys on the
253253+ right side when rebase preview is active, leaving tab switching intact. *)
254254+ let keyboard_tabs_with_preview (tabs : (string * (unit -> Ui.t Lwd.t)) list) :
255255+ Ui.t Lwd.t
256256+ =
257257+ let char_to_int c =
258258+ if c >= '0' && c <= '9' then Some (int_of_char c - int_of_char '0') else None
259259+ in
260260+ match tabs with
261261+ | [] ->
262262+ Lwd.return Ui.empty
263263+ | _ ->
264264+ let cur = Lwd.var 0 in
265265+ let$* idx_sel = Lwd.get cur in
266266+ let _, f = List.nth tabs idx_sel in
267267+ (* Build the static tab-label portion (depends only on current tab index). *)
268268+ let tab_labels =
269269+ tabs
270270+ |> List.mapi (fun i (s, _) ->
271271+ let attr = if i = idx_sel then A.(st underline) else A.empty in
272272+ let label = W.printf ~attr "%s [%d]" s (i + 1) in
273273+ if i = 0 then [ label ] else [ W.string " | "; label ])
274274+ |> List.concat
275275+ |> Ui.hcat
276276+ in
277277+ (* Reactive tab bar: combines labels with optional preview info on the right. *)
278278+ let tab_bar =
279279+ (let$ active = Lwd.get Vars.ui_state.rebase_preview_active
280280+ and$ mode = Lwd.get Vars.ui_state.rebase_preview_mode
281281+ and$ source_mode = Lwd.get Vars.ui_state.rebase_preview_source_mode
282282+ and$ invalid = Lwd.get Vars.ui_state.rebase_preview_invalid in
283283+ let preview_part =
284284+ if not active
285285+ then Ui.empty
286286+ else (
287287+ let mode_str =
288288+ match mode with
289289+ | `Insert_before ->
290290+ "insert-before"
291291+ | `Insert_after ->
292292+ "insert-after"
293293+ | `Add_after ->
294294+ "add-after"
295295+ in
296296+ let source_str =
297297+ match source_mode with
298298+ | `Revisions ->
299299+ "revisions"
300300+ | `Source ->
301301+ "source"
302302+ | `Branch ->
303303+ "branch"
304304+ in
305305+ let status =
306306+ Printf.sprintf "Preview: dest=%s source=%s" mode_str source_str
307307+ in
308308+ let status =
309309+ match invalid with None -> status | Some msg -> status ^ " - " ^ msg
310310+ in
311311+ W.string (status ^ " [y]apply [d]dest [Tab]dest [s]source [Esc]cancel"))
312312+ in
313313+ (* spacer pushes preview info to the far right *)
314314+ Ui.hcat [ tab_labels; Ui.resize ~sw:1 ~mw:10000 Ui.empty; preview_part ])
315315+ |> W.Box.box ~pad_w:1 ~pad_h:0
316316+ in
317317+ W.vbox [ tab_bar; f () ]
318318+ |>$ Ui.keyboard_area (function
319319+ | `ASCII key, _ ->
320320+ key
321321+ |> char_to_int
322322+ |> Option.map (fun i ->
323323+ if i >= 1 && i <= List.length tabs
324324+ then (
325325+ cur $= i - 1;
326326+ `Handled)
327327+ else `Unhandled)
328328+ |> Option.value ~default:`Unhandled
329329+ | _ ->
330330+ `Unhandled)
331331+ ;;
332332+250333 let mainUi () =
251334 (* first lets load the config*)
252335 Vars.config $= Config.load_config ();
···271354 | `RunCmd cmd ->
272355 Jj_widgets.interactive_process ("jj" :: cmd)
273356 | `Main ->
274274- W.keyboard_tabs [ ("Main", fun _ -> main_view ()); "Op log", log_view ])
357357+ keyboard_tabs_with_preview
358358+ [ ("Main", fun _ -> main_view ()); "Op log", log_view ])
275359 | (`CantStartProcess _ | `NotInRepo | `OtherError _) as other ->
276360 render_startup_error other
277361 ;;
+30
jj_tui/lib/render_jj_graph.ml
···173173 { source_ids; target_ids; source_set }
174174;;
175175176176+(** [default_preview_targets ~sources nodes]
177177+ Computes default rebase target revisions by collecting the parents of each
178178+ root in the source set (root = source commit whose parents are all outside
179179+ the source set). This preserves the existing tree shape when entering
180180+ preview mode instead of using the currently-hovered commit as the target. *)
181181+let default_preview_targets ~(sources : string list) (nodes : node list) : string list =
182182+ if sources = []
183183+ then []
184184+ else (
185185+ let source_ids = resolve_revs nodes sources in
186186+ let source_set = StringSet.of_list source_ids in
187187+ let seen = Hashtbl.create 8 in
188188+ (* Walk roots in graph order and keep parent order stable so the preview
189189+ starts from the commit's existing parent layout. *)
190190+ nodes
191191+ |> List.filter (fun n -> StringSet.mem n.commit_id source_set)
192192+ |> List.filter (fun n ->
193193+ not (List.exists (fun p -> StringSet.mem p.commit_id source_set) n.parents))
194194+ |> List.concat_map (fun root ->
195195+ root.parents
196196+ |> List.map (fun p -> p.commit_id)
197197+ |> List.filter (fun id -> not (StringSet.mem id source_set)))
198198+ |> List.filter (fun id ->
199199+ if Hashtbl.mem seen id
200200+ then false
201201+ else (
202202+ Hashtbl.add seen id ();
203203+ true)))
204204+;;
205205+176206let validate_preview_cycles
177207 ~(mode : preview_mode)
178208 ~(ancestors_of : string -> StringSet.t)
+54
jj_tui/lib/render_jj_graph_tests.ml
···10811081 Row 4: PadRow | node=child | graph=''
10821082 |}]
10831083;;
10841084+10851085+let%expect_test "default_preview_targets_single_commit" =
10861086+ (* c -> b -> a: select 'b'; default target should be 'c' (b's parent) *)
10871087+ let c = make_node "c" in
10881088+ let b = make_node ~parents:[ c ] "b" in
10891089+ let a = make_node ~parents:[ b ] "a" in
10901090+ let targets = default_preview_targets ~sources:[ "b" ] [ a; b; c ] in
10911091+ print_endline (String.concat "," targets);
10921092+ [%expect {| c |}]
10931093+;;
10941094+10951095+let%expect_test "default_preview_targets_chain_selected" =
10961096+ (* c -> b -> a: select both 'a' and 'b'; root of source set is 'b', whose
10971097+ parent is 'c'; so default target = 'c'. *)
10981098+ let c = make_node "c" in
10991099+ let b = make_node ~parents:[ c ] "b" in
11001100+ let a = make_node ~parents:[ b ] "a" in
11011101+ let targets = default_preview_targets ~sources:[ "a"; "b" ] [ a; b; c ] in
11021102+ print_endline (String.concat "," targets);
11031103+ [%expect {| c |}]
11041104+;;
11051105+11061106+let%expect_test "default_preview_targets_root_has_no_parents" =
11071107+ (* 'c' has no parents; selecting it yields no default targets. *)
11081108+ let c = make_node "c" in
11091109+ let b = make_node ~parents:[ c ] "b" in
11101110+ let targets = default_preview_targets ~sources:[ "c" ] [ b; c ] in
11111111+ print_endline (String.concat "," targets);
11121112+ [%expect {| |}]
11131113+;;
11141114+11151115+let%expect_test "default_preview_targets_multiple_roots" =
11161116+ (* Two independent commits d and b are both selected; their parents are
11171117+ e and c respectively (not in source set). *)
11181118+ let e = make_node "e" in
11191119+ let d = make_node ~parents:[ e ] "d" in
11201120+ let c = make_node "c" in
11211121+ let b = make_node ~parents:[ c ] "b" in
11221122+ let a = make_node ~parents:[ d; b ] "a" in
11231123+ let targets = default_preview_targets ~sources:[ "b"; "d" ] [ a; d; b; e; c ] in
11241124+ print_endline (String.concat "," targets);
11251125+ [%expect {| e,c |}]
11261126+;;
11271127+11281128+let%expect_test "default_preview_targets_preserve_parent_order" =
11291129+ (* Preserve merge-parent order instead of sorting alphabetically so preview
11301130+ starts from the current tree layout. *)
11311131+ let z = make_node "z" in
11321132+ let a = make_node "a" in
11331133+ let merge = make_node ~parents:[ z; a ] "merge" in
11341134+ let targets = default_preview_targets ~sources:[ "merge" ] [ merge; z; a ] in
11351135+ print_endline (String.concat "," targets);
11361136+ [%expect {| z,a |}]
11371137+;;