···18181919 (* Use the library's render function for commit content *)
2020 let render_commit_content = Commit_render.render_commit_content
2121-2222- (** Group rows by their owning node. Each group is (node_row, continuation_rows).
2323- Each NodeRow starts a new group containing it and all following non-NodeRows until
2424- the next NodeRow. *)
2525- let group_rows_by_node (rows : Render_jj_graph.graph_row_output list) :
2626- (Render_jj_graph.graph_row_output * Render_jj_graph.graph_row_output list) list
2727- =
2828- let open Render_jj_graph in
2929- let rec loop acc current_group = function
3030- | [] ->
3131- List.rev (match current_group with Some g -> g :: acc | None -> acc)
3232- | row :: rest ->
3333- (match row.row_type with
3434- | NodeRow ->
3535- let acc = match current_group with Some g -> g :: acc | None -> acc in
3636- loop acc (Some (row, [])) rest
3737- | _ ->
3838- (match current_group with
3939- | Some (node_row, conts) ->
4040- loop acc (Some (node_row, conts @ [ row ])) rest
4141- | None ->
4242- (* Orphan row, shouldn't happen, but skip it *)
4343- loop acc None rest))
4444- in
4545- loop [] None rows
4646- ;;
4747-4848- (** Render a node group by distributing content lines across available rows.
4949- Returns a list of (row, rendered_image) pairs. *)
5050- let render_node_group
5151- ((node_row, continuation_rows) :
5252- Render_jj_graph.graph_row_output * Render_jj_graph.graph_row_output list)
5353- ~(render_content : Render_jj_graph.node -> Notty.image list) :
5454- (Render_jj_graph.graph_row_output * Notty.image) list
5555- =
5656- let open Notty in
5757- let open Render_jj_graph in
5858- let content_lines = render_content node_row.node in
5959- let available_rows = node_row :: continuation_rows in
6060- let result = ref [] in
6161- (* Distribute content lines across available rows *)
6262- List.iteri
6363- (fun i row ->
6464- let graph_img = row.graph_image in
6565- let combined =
6666- if i < List.length content_lines
6767- then I.hcat [ graph_img; List.nth content_lines i ]
6868- else graph_img
6969- in
7070- result := (row, combined) :: !result)
7171- available_rows;
7272- (* If content needs more lines than available, add synthetic continuation rows *)
7373- if List.length content_lines > List.length available_rows
7474- then (
7575- let node_glyphs = [ "○"; "@"; "◌"; "◆"; "×" ] in
7676- let synthetic_graph =
7777- let chars = node_row.graph_chars in
7878- let replaced = ref chars in
7979- List.iter
8080- (fun glyph ->
8181- replaced := Str.global_replace (Str.regexp_string glyph) "│" !replaced)
8282- node_glyphs;
8383- I.string A.empty !replaced
8484- in
8585- for i = List.length available_rows to List.length content_lines - 1 do
8686- let line_img = List.nth content_lines i in
8787- result := (node_row, I.hcat [ synthetic_graph; line_img ]) :: !result
8888- done);
8989- List.rev !result
9090- ;;
2121+ let group_rows_by_node = Graph_row_layout.group_rows_by_node
2222+ let render_node_group = Graph_row_layout.render_node_group
91239224 let bookmark_select_prompt get_bookmark_list name func =
9325 Selection_prompt
+164
jj_tui/lib/graph_row_layout.ml
···11+open Notty
22+33+(** Group rows by their owning node, preserving graph-only rows that can appear
44+ before a visible node after elision. Native jj emits those rows before the node,
55+ so the layout layer needs to keep that ordering instead of forcing everything to
66+ trail the node row. *)
77+88+type node_group = {
99+ pre_rows : Render_jj_graph.graph_row_output list
1010+ ; node_row : Render_jj_graph.graph_row_output
1111+ ; continuation_rows : Render_jj_graph.graph_row_output list
1212+}
1313+1414+let group_rows_by_node_raw (rows : Render_jj_graph.graph_row_output list) :
1515+ node_group list
1616+ =
1717+ let open Render_jj_graph in
1818+ let rec loop acc pending_rows current_group = function
1919+ | [] ->
2020+ let acc = match current_group with Some g -> g :: acc | None -> acc in
2121+ List.rev acc
2222+ | row :: rest ->
2323+ (match row.row_type with
2424+ | NodeRow ->
2525+ let acc = match current_group with Some g -> g :: acc | None -> acc in
2626+ loop
2727+ acc
2828+ []
2929+ (Some { pre_rows = pending_rows; node_row = row; continuation_rows = [] })
3030+ rest
3131+ | _ ->
3232+ (match current_group with
3333+ | Some ({ continuation_rows; _ } as group) ->
3434+ loop
3535+ acc
3636+ pending_rows
3737+ (Some { group with continuation_rows = continuation_rows @ [ row ] })
3838+ rest
3939+ | None ->
4040+ (* Native jj can emit graph-only rows like `│` or `~` before the next
4141+ visible node after an elision gap. Keep them and attach them before
4242+ that node instead of dropping or reordering them. *)
4343+ loop acc (pending_rows @ [ row ]) None rest))
4444+ in
4545+ loop [] [] None rows
4646+;;
4747+4848+let contains_str s substr =
4949+ try
5050+ let _ = Str.search_forward (Str.regexp_string substr) s 0 in
5151+ true
5252+ with
5353+ | Not_found ->
5454+ false
5555+;;
5656+5757+let has_leading_branch_node (row : Render_jj_graph.graph_row_output) =
5858+ (contains_str row.graph_chars "○"
5959+ || contains_str row.graph_chars "@"
6060+ || contains_str row.graph_chars "◌"
6161+ || contains_str row.graph_chars "◆"
6262+ || contains_str row.graph_chars "×")
6363+ && contains_str row.graph_chars "│"
6464+;;
6565+6666+let is_plain_vertical_pad (row : Render_jj_graph.graph_row_output) =
6767+ row.row_type = Render_jj_graph.PadRow && String.trim row.graph_chars = "│"
6868+;;
6969+7070+let is_branch_continuation_pad (row : Render_jj_graph.graph_row_output) =
7171+ row.row_type = Render_jj_graph.PadRow && String.trim row.graph_chars = "│ │"
7272+;;
7373+7474+let normalize_join_rows (groups : node_group list) : node_group list =
7575+ let rec loop acc = function
7676+ | ({ continuation_rows = prev_conts; _ } as prev_group)
7777+ :: ({ node_row = next_node; continuation_rows = next_conts; _ } as next_group)
7878+ :: rest ->
7979+ (match List.rev prev_conts with
8080+ | (trailing_link : Render_jj_graph.graph_row_output) :: prev_rev_rest
8181+ when trailing_link.row_type = Render_jj_graph.LinkRow
8282+ && has_leading_branch_node next_node
8383+ && List.exists is_plain_vertical_pad next_conts
8484+ && not (List.exists is_branch_continuation_pad next_conts) ->
8585+ let prev_group =
8686+ { prev_group with continuation_rows = List.rev prev_rev_rest }
8787+ in
8888+ let next_group =
8989+ { next_group with continuation_rows = trailing_link :: next_conts }
9090+ in
9191+ loop (prev_group :: acc) (next_group :: rest)
9292+ | _ ->
9393+ loop (prev_group :: acc) (next_group :: rest))
9494+ | [ group ] ->
9595+ List.rev (group :: acc)
9696+ | [] ->
9797+ List.rev acc
9898+ in
9999+ loop [] groups
100100+;;
101101+102102+let group_rows_by_node rows = rows |> group_rows_by_node_raw |> normalize_join_rows
103103+104104+let render_node_group
105105+ ({ pre_rows; node_row; continuation_rows } : node_group)
106106+ ~(render_content : Render_jj_graph.node -> Notty.image list) :
107107+ (Render_jj_graph.graph_row_output * Notty.image) list
108108+ =
109109+ let content_lines = render_content node_row.node in
110110+ let content_rows, trailing_graph_only_rows =
111111+ let available_rows = node_row :: continuation_rows in
112112+ available_rows
113113+ |> List.partition (fun (row : Render_jj_graph.graph_row_output) ->
114114+ row.row_type <> Render_jj_graph.TermRow)
115115+ in
116116+ let result = ref [] in
117117+ (* Distribute content lines across node/link/pad rows only. Term rows such as
118118+ `~ (elided revisions)` must remain graph-only so commit descriptions stay on
119119+ a vertical continuation instead of being glued to the elision marker. *)
120120+ List.iteri
121121+ (fun i (row : Render_jj_graph.graph_row_output) ->
122122+ let combined =
123123+ if i < List.length content_lines
124124+ then I.hcat [ row.graph_image; List.nth content_lines i ]
125125+ else row.graph_image
126126+ in
127127+ result := (row, combined) :: !result)
128128+ content_rows;
129129+ (* When native jj doesn't provide enough content-bearing rows for a two-line
130130+ commit, keep the description visually attached by replacing the node glyph
131131+ with a vertical line before appending any graph-only term rows. *)
132132+ if List.length content_lines > List.length content_rows
133133+ then (
134134+ let node_glyphs = [ "○"; "@"; "◌"; "◆"; "×" ] in
135135+ let synthetic_graph =
136136+ let chars = node_row.graph_chars in
137137+ let replaced = ref chars in
138138+ List.iter
139139+ (fun glyph ->
140140+ replaced := Str.global_replace (Str.regexp_string glyph) "│" !replaced)
141141+ node_glyphs;
142142+ I.string A.empty !replaced
143143+ in
144144+ for i = List.length content_rows to List.length content_lines - 1 do
145145+ let line_img = List.nth content_lines i in
146146+ result := (node_row, I.hcat [ synthetic_graph; line_img ]) :: !result
147147+ done);
148148+ pre_rows
149149+ |> List.iter (fun (row : Render_jj_graph.graph_row_output) ->
150150+ result := (row, row.graph_image) :: !result);
151151+ trailing_graph_only_rows
152152+ |> List.iter (fun (row : Render_jj_graph.graph_row_output) ->
153153+ result := (row, row.graph_image) :: !result);
154154+ let rendered_rows = List.rev !result in
155155+ let is_node_row ((row, _img) : Render_jj_graph.graph_row_output * Notty.image) =
156156+ row == node_row
157157+ in
158158+ match List.find_opt is_node_row rendered_rows with
159159+ | None ->
160160+ rendered_rows
161161+ | Some node_entry ->
162162+ let other_rows = List.filter (fun entry -> not (is_node_row entry)) rendered_rows in
163163+ node_entry :: other_rows
164164+;;
+27-27
jj_tui/lib/process_wrappers.ml
···163163 let native_graph_output ?revset limit =
164164 let args = [ "log"; "-T"; native_graph_template; "--limit"; string_of_int limit ] in
165165 let args = match revset with Some r -> args @ [ "-r"; r ] | None -> args in
166166- jj_no_log args ~color:false
166166+ jj_no_log args
167167 ;;
168168169169 let line_before_marker line marker =
···182182 search 0
183183 ;;
184184185185- let make_graph_row_output ~graph_chars ~row_type () =
186186- let open Notty in
185185+ let make_graph_row_output ~graph_raw ~graph_chars ~row_type () =
187186 Render_jj_graph.
188187 {
189188 graph_chars
190190- ; graph_image = I.string A.empty graph_chars
189189+ ; graph_image = AnsiReverse.colored_string graph_raw
191190 ; node = Render_jj_graph.make_elided_node ()
192191 ; row_type
193192 }
···202201 List.fold_left
203202 (fun (acc, current_group) line ->
204203 match line_before_marker line node_row_marker with
205205- | Some graph_chars ->
204204+ | Some graph_raw ->
206205 let acc = flush_group current_group acc in
206206+ let graph_chars = remove_ansi graph_raw in
207207 let node_row =
208208- make_graph_row_output ~graph_chars ~row_type:Render_jj_graph.NodeRow ()
208208+ make_graph_row_output
209209+ ~graph_raw
210210+ ~graph_chars
211211+ ~row_type:Render_jj_graph.NodeRow
212212+ ()
209213 in
210214 acc, Some { node_row; continuation_rows = [] }
211215 | None ->
212212- let graph_chars = remove_ansi line in
213213- if graph_chars = ""
216216+ let graph_raw =
217217+ match line_before_marker line info_row_marker with
218218+ | Some graph_raw ->
219219+ graph_raw
220220+ | None ->
221221+ line
222222+ in
223223+ let graph_chars = remove_ansi graph_raw in
224224+ if String.trim graph_chars = ""
214225 then acc, current_group
215226 else (
216216- let row_type =
217217- match line_before_marker line info_row_marker with
218218- | Some _ ->
219219- Render_jj_graph.PadRow
220220- | None ->
221221- Render_jj_graph.classify_row_type graph_chars
222222- in
223223- let graph_chars =
224224- match line_before_marker line info_row_marker with
225225- | Some chars ->
226226- chars
227227- | None ->
228228- graph_chars
229229- in
227227+ let row_type = Render_jj_graph.classify_row_type graph_chars in
230228 match current_group with
231229 | Some group ->
232232- let row = make_graph_row_output ~graph_chars ~row_type () in
230230+ let row = make_graph_row_output ~graph_raw ~graph_chars ~row_type () in
233231 ( acc
234232 , Some
235233 { group with continuation_rows = group.continuation_rows @ [ row ] }
236234 )
237235 | None ->
238238- let node_row = make_graph_row_output ~graph_chars ~row_type () in
236236+ let node_row =
237237+ make_graph_row_output ~graph_raw ~graph_chars ~row_type ()
238238+ in
239239 acc, Some { node_row; continuation_rows = [] }))
240240 ([], None)
241241 lines
···247247 if List.length groups <> List.length nodes
248248 then None
249249 else (
250250- let rows_rev =
250250+ let rows =
251251 List.fold_left2
252252 (fun acc group node ->
253253 let node_row : Render_jj_graph.graph_row_output =
···258258 (fun (row : Render_jj_graph.graph_row_output) -> { row with node })
259259 group.continuation_rows
260260 in
261261- List.rev_append (List.rev (node_row :: continuation_rows)) acc)
261261+ acc @ (node_row :: continuation_rows))
262262 []
263263 groups
264264 nodes
265265 in
266266- Some (List.rev rows_rev))
266266+ Some rows)
267267 ;;
268268269269 let get_graph_info node_template revset_arg limit =