@@ -3048,6 +3048,21 @@ fn merge_change_indices(
30483048 . collect ( )
30493049}
30503050
3051+ /// Find line numbers of `<<<<<<<` conflict markers in a buffer.
3052+ fn find_conflict_markers ( buf : & TextBuffer ) -> Vec < usize > {
3053+ let text = buf. text ( & buf. start_iter ( ) , & buf. end_iter ( ) , false ) ;
3054+ text. lines ( )
3055+ . enumerate ( )
3056+ . filter_map ( |( i, line) | {
3057+ if line. starts_with ( "<<<<<<<" ) {
3058+ Some ( i)
3059+ } else {
3060+ None
3061+ }
3062+ } )
3063+ . collect ( )
3064+ }
3065+
30513066fn build_merge_view (
30523067 left_path : & Path ,
30533068 middle_path : & Path ,
@@ -3503,6 +3518,29 @@ fn build_merge_view(
35033518 nav_box. append ( & prev_btn) ;
35043519 nav_box. append ( & next_btn) ;
35053520
3521+ // Conflict navigation buttons
3522+ let prev_conflict_btn = Button :: from_icon_name ( "go-up-symbolic" ) ;
3523+ prev_conflict_btn. set_tooltip_text ( Some ( "Previous conflict (Ctrl+J)" ) ) ;
3524+ let next_conflict_btn = Button :: from_icon_name ( "go-down-symbolic" ) ;
3525+ next_conflict_btn. set_tooltip_text ( Some ( "Next conflict (Ctrl+K)" ) ) ;
3526+ let conflict_nav_box = GtkBox :: new ( Orientation :: Horizontal , 0 ) ;
3527+ conflict_nav_box. add_css_class ( "linked" ) ;
3528+ conflict_nav_box. append ( & prev_conflict_btn) ;
3529+ conflict_nav_box. append ( & next_conflict_btn) ;
3530+
3531+ let conflict_label = Label :: new ( None ) ;
3532+ conflict_label. add_css_class ( "chunk-label" ) ;
3533+ {
3534+ let n = find_conflict_markers ( & middle_buf) . len ( ) ;
3535+ if n == 0 {
3536+ conflict_label. set_label ( "No conflicts" ) ;
3537+ } else {
3538+ conflict_label. set_label ( & format ! ( "{n} conflicts" ) ) ;
3539+ }
3540+ }
3541+
3542+ let current_conflict: Rc < Cell < Option < usize > > > = Rc :: new ( Cell :: new ( None ) ) ;
3543+
35063544 // Undo/Redo buttons
35073545 let undo_btn = Button :: from_icon_name ( "edit-undo-symbolic" ) ;
35083546 undo_btn. set_tooltip_text ( Some ( "Undo (Ctrl+Z)" ) ) ;
@@ -3591,6 +3629,8 @@ fn build_merge_view(
35913629 toolbar. append ( & undo_redo_box) ;
35923630 toolbar. append ( & nav_box) ;
35933631 toolbar. append ( & chunk_label) ;
3632+ toolbar. append ( & conflict_nav_box) ;
3633+ toolbar. append ( & conflict_label) ;
35943634 toolbar. append ( & goto_entry) ;
35953635 toolbar. append ( & filter_box) ;
35963636 toolbar. append ( & merge_prefs_btn) ;
@@ -3703,6 +3743,101 @@ fn build_merge_view(
37033743 } ) ;
37043744 }
37053745
3746+ // Navigate conflict helper — jumps between `<<<<<<<` markers in middle buf.
3747+ let navigate_conflict = |cur : & Rc < Cell < Option < usize > > > ,
3748+ direction : i32 ,
3749+ mtv : & TextView ,
3750+ mb : & TextBuffer ,
3751+ ms : & ScrolledWindow | {
3752+ let markers = find_conflict_markers ( mb) ;
3753+ if markers. is_empty ( ) {
3754+ return ;
3755+ }
3756+ let next = match cur. get ( ) {
3757+ Some ( cur_line) => {
3758+ let pos = markers. iter ( ) . position ( |& l| l == cur_line) ;
3759+ match pos {
3760+ Some ( p) => {
3761+ if direction > 0 {
3762+ markers. get ( p + 1 ) . or ( markers. first ( ) )
3763+ } else if p > 0 {
3764+ markers. get ( p - 1 )
3765+ } else {
3766+ markers. last ( )
3767+ }
3768+ }
3769+ None => {
3770+ if direction > 0 {
3771+ markers. first ( )
3772+ } else {
3773+ markers. last ( )
3774+ }
3775+ }
3776+ }
3777+ }
3778+ None => {
3779+ if direction > 0 {
3780+ markers. first ( )
3781+ } else {
3782+ markers. last ( )
3783+ }
3784+ }
3785+ } ;
3786+ if let Some ( & line) = next {
3787+ cur. set ( Some ( line) ) ;
3788+ scroll_to_line ( mtv, mb, line, ms) ;
3789+ }
3790+ } ;
3791+
3792+ let update_conflict_label = |lbl : & Label , mb : & TextBuffer , cur : Option < usize > | {
3793+ let markers = find_conflict_markers ( mb) ;
3794+ let total = markers. len ( ) ;
3795+ if total == 0 {
3796+ lbl. set_label ( "No conflicts" ) ;
3797+ return ;
3798+ }
3799+ match cur {
3800+ Some ( cur_line) => {
3801+ if let Some ( pos) = markers. iter ( ) . position ( |& l| l == cur_line) {
3802+ lbl. set_label ( & format ! ( "Conflict {} of {}" , pos + 1 , total) ) ;
3803+ } else {
3804+ lbl. set_label ( & format ! ( "{total} conflicts" ) ) ;
3805+ }
3806+ }
3807+ None => lbl. set_label ( & format ! ( "{total} conflicts" ) ) ,
3808+ }
3809+ } ;
3810+
3811+ // Prev conflict
3812+ {
3813+ let cur = current_conflict. clone ( ) ;
3814+ let mtv = middle_pane. text_view . clone ( ) ;
3815+ let mb = middle_buf. clone ( ) ;
3816+ let ms = middle_pane. scroll . clone ( ) ;
3817+ let lbl = conflict_label. clone ( ) ;
3818+ let nav = navigate_conflict;
3819+ let upd = update_conflict_label;
3820+ prev_conflict_btn. connect_clicked ( move |_| {
3821+ nav ( & cur, -1 , & mtv, & mb, & ms) ;
3822+ upd ( & lbl, & mb, cur. get ( ) ) ;
3823+ } ) ;
3824+ }
3825+
3826+ // Next conflict
3827+ {
3828+ let cur = current_conflict. clone ( ) ;
3829+ let mtv = middle_pane. text_view . clone ( ) ;
3830+ let mb = middle_buf. clone ( ) ;
3831+ let ms = middle_pane. scroll . clone ( ) ;
3832+ let lbl = conflict_label. clone ( ) ;
3833+ let nav = navigate_conflict;
3834+ let upd = update_conflict_label;
3835+ next_conflict_btn. connect_clicked ( move |_| {
3836+ nav ( & cur, 1 , & mtv, & mb, & ms) ;
3837+ upd ( & lbl, & mb, cur. get ( ) ) ;
3838+ } ) ;
3839+ }
3840+
37063841 // ── Chunk maps for merge view ────────────────────────────────
37073842 let left_chunk_map = DrawingArea :: new ( ) ;
37083843 left_chunk_map. set_content_width ( 12 ) ;
@@ -3785,9 +3920,12 @@ fn build_merge_view(
37853920 let lcm = left_chunk_map. clone ( ) ;
37863921 let rcm = right_chunk_map. clone ( ) ;
37873922 let lbl = chunk_label. clone ( ) ;
3923+ let clbl = conflict_label. clone ( ) ;
37883924 let lch = left_chunks. clone ( ) ;
37893925 let rch = right_chunks. clone ( ) ;
3926+ let mb = middle_buf. clone ( ) ;
37903927 let cur = current_chunk. clone ( ) ;
3928+ let ccur = current_conflict. clone ( ) ;
37913929 let lf = left_pane. filler_overlay . clone ( ) ;
37923930 let mf = middle_pane. filler_overlay . clone ( ) ;
37933931 let rf = right_pane. filler_overlay . clone ( ) ;
@@ -3797,9 +3935,12 @@ fn build_merge_view(
37973935 let lcm = lcm. clone ( ) ;
37983936 let rcm = rcm. clone ( ) ;
37993937 let lbl = lbl. clone ( ) ;
3938+ let clbl = clbl. clone ( ) ;
38003939 let lch = lch. clone ( ) ;
38013940 let rch = rch. clone ( ) ;
3941+ let mb = mb. clone ( ) ;
38023942 let cur = cur. clone ( ) ;
3943+ let ccur = ccur. clone ( ) ;
38033944 let lf = lf. clone ( ) ;
38043945 let mf = mf. clone ( ) ;
38053946 let rf = rf. clone ( ) ;
@@ -3813,6 +3954,13 @@ fn build_merge_view(
38133954 rf. queue_draw ( ) ;
38143955 cur. set ( None ) ;
38153956 update_merge_label ( & lbl, & lch. borrow ( ) , & rch. borrow ( ) , None ) ;
3957+ ccur. set ( None ) ;
3958+ let n = find_conflict_markers ( & mb) . len ( ) ;
3959+ if n == 0 {
3960+ clbl. set_label ( "No conflicts" ) ;
3961+ } else {
3962+ clbl. set_label ( & format ! ( "{n} conflicts" ) ) ;
3963+ }
38163964 }
38173965 }
38183966 } ;
@@ -4214,6 +4362,32 @@ fn build_merge_view(
42144362 } ) ;
42154363 action_group. add_action ( & action) ;
42164364 }
4365+ {
4366+ let action = gio:: SimpleAction :: new ( "prev-conflict" , None ) ;
4367+ let cur = current_conflict. clone ( ) ;
4368+ let mtv = middle_pane. text_view . clone ( ) ;
4369+ let mb = middle_buf. clone ( ) ;
4370+ let ms = middle_pane. scroll . clone ( ) ;
4371+ let lbl = conflict_label. clone ( ) ;
4372+ action. connect_activate ( move |_, _| {
4373+ navigate_conflict ( & cur, -1 , & mtv, & mb, & ms) ;
4374+ update_conflict_label ( & lbl, & mb, cur. get ( ) ) ;
4375+ } ) ;
4376+ action_group. add_action ( & action) ;
4377+ }
4378+ {
4379+ let action = gio:: SimpleAction :: new ( "next-conflict" , None ) ;
4380+ let cur = current_conflict. clone ( ) ;
4381+ let mtv = middle_pane. text_view . clone ( ) ;
4382+ let mb = middle_buf. clone ( ) ;
4383+ let ms = middle_pane. scroll . clone ( ) ;
4384+ let lbl = conflict_label. clone ( ) ;
4385+ action. connect_activate ( move |_, _| {
4386+ navigate_conflict ( & cur, 1 , & mtv, & mb, & ms) ;
4387+ update_conflict_label ( & lbl, & mb, cur. get ( ) ) ;
4388+ } ) ;
4389+ action_group. add_action ( & action) ;
4390+ }
42174391 // Find action (Ctrl+F)
42184392 {
42194393 let action = gio:: SimpleAction :: new ( "find" , None ) ;
@@ -4478,6 +4652,8 @@ fn build_merge_window(
44784652 if let Some ( gtk_app) = window. application ( ) {
44794653 gtk_app. set_accels_for_action ( "diff.prev-chunk" , & [ "<Alt>Up" , "<Ctrl>e" ] ) ;
44804654 gtk_app. set_accels_for_action ( "diff.next-chunk" , & [ "<Alt>Down" , "<Ctrl>d" ] ) ;
4655+ gtk_app. set_accels_for_action ( "diff.prev-conflict" , & [ "<Ctrl>j" ] ) ;
4656+ gtk_app. set_accels_for_action ( "diff.next-conflict" , & [ "<Ctrl>k" ] ) ;
44814657 gtk_app. set_accels_for_action ( "diff.find" , & [ "<Ctrl>f" ] ) ;
44824658 gtk_app. set_accels_for_action ( "diff.find-replace" , & [ "<Ctrl>h" ] ) ;
44834659 gtk_app. set_accels_for_action ( "diff.find-next" , & [ "F3" ] ) ;
0 commit comments