terminal user interface to jujutsu. Focused on speed and clarity
9
fork

Configure Feed

Select the types of activity you want to include in your feed.

improve rebase preview rendering so it doesn't break

+219 -64
+20 -3
jj_tui/bin/graph_commands.ml
··· 380 380 Vars.ui_state.trigger_update $= ()) 381 381 else ( 382 382 Vars.set_rebase_preview_active true; 383 - Vars.set_rebase_preview_sources (Vars.get_active_revs ()); 383 + let sources = Vars.get_active_revs () in 384 + Vars.set_rebase_preview_sources sources; 385 + (* Default targets = parents of source roots; this preserves the 386 + current tree shape rather than jumping to the hovered commit. *) 387 + let fallback_targets () = 388 + let selected = Vars.get_selected_revs () in 389 + if List.length selected = 0 390 + then [ Vars.get_hovered_rev () ] 391 + else selected 392 + in 384 393 let targets = 385 - let selected = Vars.get_selected_revs () in 386 - if List.length selected = 0 then [ Vars.get_hovered_rev () ] else selected 394 + try 395 + let revset = Lwd.peek Vars.ui_state.revset in 396 + let max_commits = (Vars.config |> Lwd.peek).max_commits in 397 + let nodes, _ = get_graph_nodes ?revset max_commits in 398 + Render_jj_graph.default_preview_targets ~sources nodes 399 + with 400 + | _ -> 401 + (* If we cannot read the graph, keep preview usable instead of 402 + failing the mode toggle outright. *) 403 + fallback_targets () 387 404 in 388 405 Vars.set_rebase_preview_targets targets; 389 406 Vars.set_rebase_preview_invalid None;
+1 -31
jj_tui/bin/graph_view.ml
··· 131 131 x |> Ui.resize ~mw:10000 ~sw:1 |> Lwd.pure |> W.Box.box ~pad_w:0 ~pad_h:0) 132 132 |> Option.value ~default:(Ui.empty |> Lwd.pure) 133 133 in 134 - let mode_indicator = 135 - let$ active = Lwd.get Vars.ui_state.rebase_preview_active 136 - and$ mode = Lwd.get Vars.ui_state.rebase_preview_mode 137 - and$ source_mode = Lwd.get Vars.ui_state.rebase_preview_source_mode 138 - and$ invalid = Lwd.get Vars.ui_state.rebase_preview_invalid in 139 - if not active 140 - then Ui.empty 141 - else ( 142 - let mode_str = 143 - match mode with 144 - | `Insert_before -> 145 - "insert-before" 146 - | `Insert_after -> 147 - "insert-after" 148 - | `Add_after -> 149 - "add-after" 150 - in 151 - let source_str = 152 - match source_mode with 153 - | `Revisions -> 154 - "revisions" 155 - | `Source -> 156 - "source" 157 - | `Branch -> 158 - "branch" 159 - in 160 - let base = Printf.sprintf "Preview: dest=%s source=%s" mode_str source_str in 161 - let label = match invalid with None -> base | Some msg -> base ^ " - " ^ msg in 162 - W.string label) 163 - in 164 134 let items = 165 135 let$ rendered_rows, rev_ids = 166 136 (*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*) ··· 329 299 and$ error = Lwd.get error_var in 330 300 match error with Some e -> e |> Ui.keyboard_area handleKeys | None -> list_ui 331 301 in 332 - W.vbox [ revset_ui; mode_indicator; final_ui ] 302 + W.vbox [ revset_ui; final_ui ] 333 303 ;; 334 304 end
+114 -30
jj_tui/bin/jj_ui.ml
··· 57 57 event 58 58 |> forward_events 59 59 [ 60 - custom; 61 - (fun x-> 62 - (*Try to remap the key if needed *) 63 - match x with 64 - | `ASCII k, modifiers -> 65 - let key = Key.{ key = k; modifiers } in 66 - let key=Key_map.Key_Map.find_opt key (Lwd.peek ui_state.config).key_map.remap in 67 - ( match key with 60 + custom 61 + ; (fun x -> 62 + (*Try to remap the key if needed *) 63 + match x with 64 + | `ASCII k, modifiers -> 65 + let key = Key.{ key = k; modifiers } in 66 + let key = 67 + Key_map.Key_Map.find_opt key (Lwd.peek ui_state.config).key_map.remap 68 + in 69 + (match key with 68 70 | Some remap -> 69 71 (** This needs to be type converted so our limited variants get upcast into a full nottui event*) 70 - (`Remap (remap.remap, modifiers):> Nottui.Ui.may_handle) 72 + (`Remap (remap.remap, modifiers) :> Nottui.Ui.may_handle) 71 73 | None -> 72 74 `Unhandled) 73 - | _ -> 74 - `Unhandled) 75 + | _ -> 76 + `Unhandled) 75 77 ; (function 76 78 | `ASCII 'q', _ -> 77 79 Vars.quit $= true; ··· 150 152 (** Makes a UI element responsive to terminal width and focus state 151 153 - When focused: shows at full width if terminal is wide enough, or fills terminal if narrow 152 154 - When unfocused: shows at normal width if terminal is wide enough, or collapses if narrow *) 153 - let responsive_view ?(shrunk_width=0) ?(shrink_on= `Focus) ~focus ui = 154 - let$* w, h = Lwd.get Vars.term_width_height in 155 - let$ ui = ui 156 - and$ focus = focus|>Focus.status in 157 - 158 - let should_shrink = match shrink_on with 159 - | `Focus -> focus|>Focus.has_focus 160 - | `Unfocus -> not (focus|>Focus.has_focus) 161 - 155 + let responsive_view ?(shrunk_width = 0) ?(shrink_on = `Focus) ~focus ui = 156 + let$* w, h = Lwd.get Vars.term_width_height in 157 + let$ ui = ui 158 + and$ focus = focus |> Focus.status in 159 + let should_shrink = 160 + match shrink_on with 161 + | `Focus -> 162 + focus |> Focus.has_focus 163 + | `Unfocus -> 164 + not (focus |> Focus.has_focus) 162 165 in 163 - let threhold=(Lwd.peek Vars.config).single_pane_width_threshold in 166 + let threhold = (Lwd.peek Vars.config).single_pane_width_threshold in 164 167 if should_shrink 165 - then if w < threhold 166 - then ui |> Ui.resize ~w:w ~mw:w 167 - else ui 168 + then if w < threhold then ui |> Ui.resize ~w ~mw:w else ui 168 169 else if w < threhold 169 - then ui |> Ui.resize ~w:shrunk_width ~mw:shrunk_width 170 - else ui 170 + then ui |> Ui.resize ~w:shrunk_width ~mw:shrunk_width 171 + else ui 171 172 ;; 172 173 173 174 (** The primary view for the UI with the file_view graph_view and summary*) ··· 212 213 ~mw:1000) 213 214 |> W.Box.focusable ~focus:branch_focus ~pad_h:0 ~pad_w:1 214 215 ] 215 - |> responsive_view ~focus:summary_focus ~shrink_on:`Unfocus ~shrunk_width:0 216 + |> responsive_view ~focus:summary_focus ~shrink_on:`Unfocus ~shrunk_width:0 216 217 ; (*Right side summary/status/fileinfo view*) 217 218 (let ui = 218 219 Show_view.render summary_focus ··· 222 223 |> W.on_focus ~focus:summary_focus (Ui.resize ~sw:3 ~mw:1000) 223 224 |> W.Box.focusable ~focus:summary_focus ~pad_h:0 ~pad_w:1 224 225 in 225 - responsive_view ~focus:summary_focus ~shrunk_width:0 ui) 226 + responsive_view ~focus:summary_focus ~shrunk_width:0 ui) 226 227 ] 227 228 (*These outer prompts can popup and show them selves over the main view*) 228 229 |> W.Overlay.text_prompt ~char_count:true ~show_prompt_var:ui_state.show_prompt 229 230 |> W.Overlay.popup ~show_popup_var:ui_state.show_popup 230 231 |> W.Overlay.selection_list_prompt_filterable 231 - ~list_outline_focus_attr:A.(empty) (*highlighting the outline inside the propt is a bit over the top*) 232 + ~list_outline_focus_attr:A.(empty) 233 + (*highlighting the outline inside the propt is a bit over the top*) 232 234 ~show_prompt_var:ui_state.show_string_selection_prompt 233 235 |> inputs ~custom:(fun x -> Jj_commands.handleInputs Jj_commands.default_list x) 234 236 ;; ··· 247 249 |> inputs 248 250 ;; 249 251 252 + (** Tab bar like [W.keyboard_tabs] but renders preview status + hotkeys on the 253 + right side when rebase preview is active, leaving tab switching intact. *) 254 + let keyboard_tabs_with_preview (tabs : (string * (unit -> Ui.t Lwd.t)) list) : 255 + Ui.t Lwd.t 256 + = 257 + let char_to_int c = 258 + if c >= '0' && c <= '9' then Some (int_of_char c - int_of_char '0') else None 259 + in 260 + match tabs with 261 + | [] -> 262 + Lwd.return Ui.empty 263 + | _ -> 264 + let cur = Lwd.var 0 in 265 + let$* idx_sel = Lwd.get cur in 266 + let _, f = List.nth tabs idx_sel in 267 + (* Build the static tab-label portion (depends only on current tab index). *) 268 + let tab_labels = 269 + tabs 270 + |> List.mapi (fun i (s, _) -> 271 + let attr = if i = idx_sel then A.(st underline) else A.empty in 272 + let label = W.printf ~attr "%s [%d]" s (i + 1) in 273 + if i = 0 then [ label ] else [ W.string " | "; label ]) 274 + |> List.concat 275 + |> Ui.hcat 276 + in 277 + (* Reactive tab bar: combines labels with optional preview info on the right. *) 278 + let tab_bar = 279 + (let$ active = Lwd.get Vars.ui_state.rebase_preview_active 280 + and$ mode = Lwd.get Vars.ui_state.rebase_preview_mode 281 + and$ source_mode = Lwd.get Vars.ui_state.rebase_preview_source_mode 282 + and$ invalid = Lwd.get Vars.ui_state.rebase_preview_invalid in 283 + let preview_part = 284 + if not active 285 + then Ui.empty 286 + else ( 287 + let mode_str = 288 + match mode with 289 + | `Insert_before -> 290 + "insert-before" 291 + | `Insert_after -> 292 + "insert-after" 293 + | `Add_after -> 294 + "add-after" 295 + in 296 + let source_str = 297 + match source_mode with 298 + | `Revisions -> 299 + "revisions" 300 + | `Source -> 301 + "source" 302 + | `Branch -> 303 + "branch" 304 + in 305 + let status = 306 + Printf.sprintf "Preview: dest=%s source=%s" mode_str source_str 307 + in 308 + let status = 309 + match invalid with None -> status | Some msg -> status ^ " - " ^ msg 310 + in 311 + W.string (status ^ " [y]apply [d]dest [Tab]dest [s]source [Esc]cancel")) 312 + in 313 + (* spacer pushes preview info to the far right *) 314 + Ui.hcat [ tab_labels; Ui.resize ~sw:1 ~mw:10000 Ui.empty; preview_part ]) 315 + |> W.Box.box ~pad_w:1 ~pad_h:0 316 + in 317 + W.vbox [ tab_bar; f () ] 318 + |>$ Ui.keyboard_area (function 319 + | `ASCII key, _ -> 320 + key 321 + |> char_to_int 322 + |> Option.map (fun i -> 323 + if i >= 1 && i <= List.length tabs 324 + then ( 325 + cur $= i - 1; 326 + `Handled) 327 + else `Unhandled) 328 + |> Option.value ~default:`Unhandled 329 + | _ -> 330 + `Unhandled) 331 + ;; 332 + 250 333 let mainUi () = 251 334 (* first lets load the config*) 252 335 Vars.config $= Config.load_config (); ··· 271 354 | `RunCmd cmd -> 272 355 Jj_widgets.interactive_process ("jj" :: cmd) 273 356 | `Main -> 274 - W.keyboard_tabs [ ("Main", fun _ -> main_view ()); "Op log", log_view ]) 357 + keyboard_tabs_with_preview 358 + [ ("Main", fun _ -> main_view ()); "Op log", log_view ]) 275 359 | (`CantStartProcess _ | `NotInRepo | `OtherError _) as other -> 276 360 render_startup_error other 277 361 ;;
+30
jj_tui/lib/render_jj_graph.ml
··· 173 173 { source_ids; target_ids; source_set } 174 174 ;; 175 175 176 + (** [default_preview_targets ~sources nodes] 177 + Computes default rebase target revisions by collecting the parents of each 178 + root in the source set (root = source commit whose parents are all outside 179 + the source set). This preserves the existing tree shape when entering 180 + preview mode instead of using the currently-hovered commit as the target. *) 181 + let default_preview_targets ~(sources : string list) (nodes : node list) : string list = 182 + if sources = [] 183 + then [] 184 + else ( 185 + let source_ids = resolve_revs nodes sources in 186 + let source_set = StringSet.of_list source_ids in 187 + let seen = Hashtbl.create 8 in 188 + (* Walk roots in graph order and keep parent order stable so the preview 189 + starts from the commit's existing parent layout. *) 190 + nodes 191 + |> List.filter (fun n -> StringSet.mem n.commit_id source_set) 192 + |> List.filter (fun n -> 193 + not (List.exists (fun p -> StringSet.mem p.commit_id source_set) n.parents)) 194 + |> List.concat_map (fun root -> 195 + root.parents 196 + |> List.map (fun p -> p.commit_id) 197 + |> List.filter (fun id -> not (StringSet.mem id source_set))) 198 + |> List.filter (fun id -> 199 + if Hashtbl.mem seen id 200 + then false 201 + else ( 202 + Hashtbl.add seen id (); 203 + true))) 204 + ;; 205 + 176 206 let validate_preview_cycles 177 207 ~(mode : preview_mode) 178 208 ~(ancestors_of : string -> StringSet.t)
+54
jj_tui/lib/render_jj_graph_tests.ml
··· 1081 1081 Row 4: PadRow | node=child | graph='' 1082 1082 |}] 1083 1083 ;; 1084 + 1085 + let%expect_test "default_preview_targets_single_commit" = 1086 + (* c -> b -> a: select 'b'; default target should be 'c' (b's parent) *) 1087 + let c = make_node "c" in 1088 + let b = make_node ~parents:[ c ] "b" in 1089 + let a = make_node ~parents:[ b ] "a" in 1090 + let targets = default_preview_targets ~sources:[ "b" ] [ a; b; c ] in 1091 + print_endline (String.concat "," targets); 1092 + [%expect {| c |}] 1093 + ;; 1094 + 1095 + let%expect_test "default_preview_targets_chain_selected" = 1096 + (* c -> b -> a: select both 'a' and 'b'; root of source set is 'b', whose 1097 + parent is 'c'; so default target = 'c'. *) 1098 + let c = make_node "c" in 1099 + let b = make_node ~parents:[ c ] "b" in 1100 + let a = make_node ~parents:[ b ] "a" in 1101 + let targets = default_preview_targets ~sources:[ "a"; "b" ] [ a; b; c ] in 1102 + print_endline (String.concat "," targets); 1103 + [%expect {| c |}] 1104 + ;; 1105 + 1106 + let%expect_test "default_preview_targets_root_has_no_parents" = 1107 + (* 'c' has no parents; selecting it yields no default targets. *) 1108 + let c = make_node "c" in 1109 + let b = make_node ~parents:[ c ] "b" in 1110 + let targets = default_preview_targets ~sources:[ "c" ] [ b; c ] in 1111 + print_endline (String.concat "," targets); 1112 + [%expect {| |}] 1113 + ;; 1114 + 1115 + let%expect_test "default_preview_targets_multiple_roots" = 1116 + (* Two independent commits d and b are both selected; their parents are 1117 + e and c respectively (not in source set). *) 1118 + let e = make_node "e" in 1119 + let d = make_node ~parents:[ e ] "d" in 1120 + let c = make_node "c" in 1121 + let b = make_node ~parents:[ c ] "b" in 1122 + let a = make_node ~parents:[ d; b ] "a" in 1123 + let targets = default_preview_targets ~sources:[ "b"; "d" ] [ a; d; b; e; c ] in 1124 + print_endline (String.concat "," targets); 1125 + [%expect {| e,c |}] 1126 + ;; 1127 + 1128 + let%expect_test "default_preview_targets_preserve_parent_order" = 1129 + (* Preserve merge-parent order instead of sorting alphabetically so preview 1130 + starts from the current tree layout. *) 1131 + let z = make_node "z" in 1132 + let a = make_node "a" in 1133 + let merge = make_node ~parents:[ z; a ] "merge" in 1134 + let targets = default_preview_targets ~sources:[ "merge" ] [ merge; z; a ] in 1135 + print_endline (String.concat "," targets); 1136 + [%expect {| z,a |}] 1137 + ;;