use agent_settings::AgentSettings;use collections::{HashMap, HashSet};use editor::{ ConflictsOurs, ConflictsOursMarker, ConflictsOuter, ConflictsTheirs, ConflictsTheirsMarker, Editor, MultiBuffer, RowHighlightOptions, display_map::{BlockContext, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId},};use gpui::{ App, ClickEvent, Context, Empty, Entity, InteractiveElement as _, ParentElement as _, Subscription, Task, WeakEntity,};use language::{Anchor, Buffer, BufferId};use project::{ ConflictRegion, ConflictSet, ConflictSetUpdate, Project, git_store::{GitStore, GitStoreEvent, RepositoryEvent},};use settings::Settings;use std::{ops::Range, sync::Arc};use ui::{ButtonLike, Divider, Tooltip, prelude::*};use util::debug_panic;use workspace::{HideStatusItem, StatusItemView, Workspace, item::ItemHandle};use zed_actions::agent::{ ConflictContent, ResolveConflictedFilesWithAgent, ResolveConflictsWithAgent,}; pub(crate) struct ConflictAddon { buffers: HashMap<BufferId, BufferConflicts>,} struct BufferConflicts { block_ids: Vec<(Range<Anchor>, CustomBlockId)>, conflict_set: Entity<ConflictSet>, _subscription: Subscription,} impl editor::Addon for ConflictAddon { fn to_any(&self) -> &dyn std::any::Any { self } fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> { Some(self) }} pub fn register_editor(editor: &mut Editor, buffer: Entity<MultiBuffer>, cx: &mut Context<Editor>) { let is_singleton = editor.buffer().read(cx).is_singleton(); if !editor.mode().is_full() || (!is_singleton && !editor.buffer().read(cx).all_diff_hunks_expanded()) || editor.read_only(cx) { return; } editor.register_addon(ConflictAddon { buffers: Default::default(), }); if is_singleton { let buffers = buffer.read(cx).all_buffers(); for buffer in buffers { open_conflict_set_for_buffer(editor, buffer, cx); } }} fn open_conflict_set_for_buffer( _editor: &mut Editor, buffer: Entity<Buffer>, cx: &mut Context<Editor>,) { let buffer = buffer.downgrade(); cx.spawn(async move |editor, cx| { let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id())?; if let Some(conflict_set) = editor.read_with(cx, |editor, _| { editor .addon::<ConflictAddon>() .and_then(|addon| addon.buffers.get(&buffer_id)) .map(|buffer_conflicts| buffer_conflicts.conflict_set.clone()) })? { editor.update(cx, |editor, cx| { buffer_ranges_updated(editor, conflict_set, cx); })?; return anyhow::Ok(()); } let Some(project) = editor.read_with(cx, |editor, _| editor.project().cloned())? else { return anyhow::Ok(()); }; let git_store = project.read_with(cx, |project, _| project.git_store().clone()); let Some(buffer) = buffer.upgrade() else { return Ok(()); }; let conflict_set = git_store .update(cx, |git_store, cx| { git_store.open_conflict_set(buffer.clone(), cx) }) .await; editor.update(cx, |editor, cx| { buffer_ranges_updated(editor, conflict_set, cx); })?; Ok(()) }) .detach();} pub(crate) fn buffer_ranges_updated( editor: &mut Editor, conflict_set: Entity<ConflictSet>, cx: &mut Context<Editor>,) { let buffer_id = conflict_set.read(cx).snapshot.buffer_id; if editor.buffer().read(cx).buffer(buffer_id).is_none() { return; } let Some(conflict_addon) = editor.addon_mut::<ConflictAddon>() else { return; }; let buffer_conflicts = conflict_addon.buffers.entry(buffer_id).or_insert_with(|| { let subscription = cx.subscribe(&conflict_set, conflicts_updated); BufferConflicts { block_ids: Vec::new(), conflict_set: conflict_set.clone(), _subscription: subscription, } }); let conflict_set = buffer_conflicts.conflict_set.clone(); let conflicts_len = conflict_set.read(cx).snapshot().conflicts.len(); let addon_conflicts_len = buffer_conflicts.block_ids.len(); conflicts_updated( editor, conflict_set, &ConflictSetUpdate { buffer_range: None, old_range: 0..addon_conflicts_len, new_range: 0..conflicts_len, }, cx, );} pub(crate) fn buffers_removed( editor: &mut Editor, removed_buffer_ids: &[BufferId], cx: &mut Context<Editor>,) { let mut removed_block_ids = HashSet::default(); let Some(conflict_addon) = editor.addon_mut::<ConflictAddon>() else { return; }; conflict_addon.buffers.retain(|buffer_id, buffer| { if removed_buffer_ids.contains(buffer_id) { removed_block_ids.extend(buffer.block_ids.iter().map(|(_, block_id)| *block_id)); false } else { true } }); editor.remove_blocks(removed_block_ids, None, cx);} #[ztracing::instrument(skip_all)]fn conflicts_updated( editor: &mut Editor, conflict_set: Entity<ConflictSet>, event: &ConflictSetUpdate, cx: &mut Context<Editor>,) { let buffer_id = conflict_set.read(cx).snapshot.buffer_id; let conflict_set = conflict_set.read(cx).snapshot(); let multibuffer = editor.buffer().read(cx); let snapshot = multibuffer.snapshot(cx); let old_range = { let Some(conflict_addon) = editor.addon_mut::<ConflictAddon>() else { return; }; let Some(buffer_conflicts) = conflict_addon.buffers.get(&buffer_id) else { return; }; match buffer_conflicts.block_ids.get(event.old_range.clone()) { Some(_) => Some(event.old_range.clone()), None => { debug_panic!( "conflicts updated event old range is invalid for buffer conflicts view (block_ids len is {:?}, old_range is {:?})", buffer_conflicts.block_ids.len(), event.old_range, ); if event.old_range.start <= event.old_range.end { Some( event.old_range.start.min(buffer_conflicts.block_ids.len()) ..event.old_range.end.min(buffer_conflicts.block_ids.len()), ) } else { None } } } }; // Remove obsolete highlights and blocks let Some(conflict_addon) = editor.addon_mut::<ConflictAddon>() else { return; }; if let Some((buffer_conflicts, old_range)) = conflict_addon .buffers .get_mut(&buffer_id) .zip(old_range.clone()) { let old_conflicts = buffer_conflicts.block_ids[old_range].to_owned(); let mut removed_highlighted_ranges = Vec::new(); let mut removed_block_ids = HashSet::default(); for (conflict_range, block_id) in old_conflicts { let Some(range) = snapshot.buffer_anchor_range_to_anchor_range(conflict_range) else { continue; }; removed_highlighted_ranges.push(range.clone()); removed_block_ids.insert(block_id); } editor.remove_gutter_highlights::<ConflictsOuter>(removed_highlighted_ranges.clone(), cx); editor.remove_highlighted_rows::<ConflictsOuter>(removed_highlighted_ranges.clone(), cx); editor.remove_highlighted_rows::<ConflictsOurs>(removed_highlighted_ranges.clone(), cx); editor .remove_highlighted_rows::<ConflictsOursMarker>(removed_highlighted_ranges.clone(), cx); editor.remove_highlighted_rows::<ConflictsTheirs>(removed_highlighted_ranges.clone(), cx); editor.remove_highlighted_rows::<ConflictsTheirsMarker>( removed_highlighted_ranges.clone(), cx, ); editor.remove_blocks(removed_block_ids, None, cx); } // Add new highlights and blocks let editor_handle = cx.weak_entity(); let new_conflicts = &conflict_set.conflicts[event.new_range.clone()]; let mut blocks = Vec::new(); for conflict in new_conflicts { update_conflict_highlighting(editor, conflict, &snapshot, cx); let Some(anchor) = snapshot.anchor_in_excerpt(conflict.range.start) else { continue; }; let editor_handle = editor_handle.clone(); blocks.push(BlockProperties { placement: BlockPlacement::Above(anchor), height: Some(1), style: BlockStyle::Sticky, render: Arc::new({ let conflict = conflict.clone(); move |cx| render_conflict_buttons(&conflict, editor_handle.clone(), cx) }), priority: 0, }) } let new_block_ids = editor.insert_blocks(blocks, None, cx); let Some(conflict_addon) = editor.addon_mut::<ConflictAddon>() else { return; }; if let Some((buffer_conflicts, old_range)) = conflict_addon.buffers.get_mut(&buffer_id).zip(old_range) { buffer_conflicts.block_ids.splice( old_range, new_conflicts .iter() .map(|conflict| conflict.range.clone()) .zip(new_block_ids), ); }} #[ztracing::instrument(skip_all)]fn update_conflict_highlighting( editor: &mut Editor, conflict: &ConflictRegion, buffer: &editor::MultiBufferSnapshot, cx: &mut Context<Editor>,) -> Option<()> { log::debug!("update conflict highlighting for {conflict:?}"); let outer = buffer.buffer_anchor_range_to_anchor_range(conflict.range.clone())?; let ours = buffer.buffer_anchor_range_to_anchor_range(conflict.ours.clone())?; let theirs = buffer.buffer_anchor_range_to_anchor_range(conflict.theirs.clone())?; let ours_background = |cx: &App| cx.theme().colors().version_control_conflict_marker_ours; let theirs_background = |cx: &App| cx.theme().colors().version_control_conflict_marker_theirs; let options = RowHighlightOptions { include_gutter: true, ..Default::default() }; editor.insert_gutter_highlight::<ConflictsOuter>( outer.start..theirs.end, |cx| cx.theme().colors().editor_background, cx, ); // Prevent diff hunk highlighting within the entire conflict region. editor.highlight_rows::<ConflictsOuter>(outer.clone(), theirs_background, options, cx); editor.highlight_rows::<ConflictsOurs>(ours.clone(), ours_background, options, cx); editor.highlight_rows::<ConflictsOursMarker>( outer.start..ours.start, ours_background, options, cx, ); editor.highlight_rows::<ConflictsTheirs>(theirs.clone(), theirs_background, options, cx); editor.highlight_rows::<ConflictsTheirsMarker>( theirs.end..outer.end, theirs_background, options, cx, ); Some(())} fn render_conflict_buttons( conflict: &ConflictRegion, editor: WeakEntity<Editor>, cx: &mut BlockContext,) -> AnyElement { let is_ai_enabled = AgentSettings::get_global(cx).enabled(cx); h_flex() .id(cx.block_id) .h(cx.line_height) .ml(cx.margins.gutter.width) .gap_1() .bg(cx.theme().colors().editor_background) .child( Button::new("head", format!("Use {}", conflict.ours_branch_name)) .label_size(LabelSize::Small) .on_click({ let editor = editor.clone(); let conflict = conflict.clone(); let ours = conflict.ours.clone(); move |_, window, cx| { resolve_conflict( editor.clone(), conflict.clone(), vec![ours.clone()], window, cx, ) .detach() } }), ) .child( Button::new("origin", format!("Use {}", conflict.theirs_branch_name)) .label_size(LabelSize::Small) .on_click({ let editor = editor.clone(); let conflict = conflict.clone(); let theirs = conflict.theirs.clone(); move |_, window, cx| { resolve_conflict( editor.clone(), conflict.clone(), vec![theirs.clone()], window, cx, ) .detach() } }), ) .child( Button::new("both", "Use Both") .label_size(LabelSize::Small) .on_click({ let editor = editor.clone(); let conflict = conflict.clone(); let ours = conflict.ours.clone(); let theirs = conflict.theirs.clone(); move |_, window, cx| { resolve_conflict( editor.clone(), conflict.clone(), vec![ours.clone(), theirs.clone()], window, cx, ) .detach() } }), ) .when(is_ai_enabled, |this| { this.child(Divider::vertical()).child( Button::new("resolve-with-agent", "Resolve with Agent") .label_size(LabelSize::Small) .start_icon( Icon::new(IconName::ZedAssistant) .size(IconSize::Small) .color(Color::Muted), ) .on_click({ let conflict = conflict.clone(); move |_, window, cx| { let content = editor .update(cx, |editor, cx| { let multibuffer = editor.buffer().read(cx); let buffer_id = conflict.ours.end.buffer_id; let buffer = multibuffer.buffer(buffer_id)?; let buffer_read = buffer.read(cx); let snapshot = buffer_read.snapshot(); let conflict_text = snapshot .text_for_range(conflict.range.clone()) .collect::<String>(); let file_path = buffer_read .file() .and_then(|file| file.as_local()) .map(|f| f.abs_path(cx).to_string_lossy().to_string()) .unwrap_or_default(); Some(ConflictContent { file_path, conflict_text, ours_branch_name: conflict.ours_branch_name.to_string(), theirs_branch_name: conflict.theirs_branch_name.to_string(), }) }) .ok() .flatten(); if let Some(content) = content { window.dispatch_action( Box::new(ResolveConflictsWithAgent { conflicts: vec![content], }), cx, ); } } }), ) }) .into_any()} fn collect_conflicted_file_paths(project: &Project, cx: &App) -> Vec<String> { let git_store = project.git_store().read(cx); let mut paths = Vec::new(); for repo in git_store.repositories().values() { let snapshot = repo.read(cx).snapshot(); for (repo_path, _) in snapshot.merge.merge_heads_by_conflicted_path.iter() { let is_currently_conflicted = snapshot .status_for_path(repo_path) .is_some_and(|entry| entry.status.is_conflicted()); if !is_currently_conflicted { continue; } if let Some(project_path) = repo.read(cx).repo_path_to_project_path(repo_path, cx) { paths.push( project_path .path .as_std_path() .to_string_lossy() .to_string(), ); } } } paths} pub(crate) fn resolve_conflict( editor: WeakEntity<Editor>, resolved_conflict: ConflictRegion, ranges: Vec<Range<Anchor>>, window: &mut Window, cx: &mut App,) -> Task<()> { window.spawn(cx, async move |cx| { editor .update(cx, |editor, cx| { let multibuffer = editor.buffer().clone(); let buffer_id = resolved_conflict.ours.end.buffer_id; let buffer = multibuffer.read(cx).buffer(buffer_id)?; resolved_conflict.resolve(buffer.clone(), &ranges, cx); let conflict_addon = editor.addon_mut::<ConflictAddon>()?; let snapshot = multibuffer.read(cx).snapshot(cx); let buffer_snapshot = buffer.read(cx).snapshot(); let state = conflict_addon .buffers .get_mut(&buffer_snapshot.remote_id())?; let ix = state .block_ids .binary_search_by(|(range, _)| { range .start .cmp(&resolved_conflict.range.start, &buffer_snapshot)Lines 1–500 of 698. Load more
use agent_settings::AgentSettings; use collections::{HashMap, HashSet}; use editor::{ ConflictsOurs, ConflictsOursMarker, ConflictsOuter, ConflictsTheirs, ConflictsTheirsMarker, Editor, MultiBuffer, RowHighlightOptions, display_map::{BlockContext, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId}, }; use gpui::{ App, ClickEvent, Context, Empty, Entity, InteractiveElement as _, ParentElement as _, Subscription, Task, WeakEntity, }; use language::{Anchor, Buffer, BufferId}; use project::{ ConflictRegion, ConflictSet, ConflictSetUpdate, Project, git_store::{GitStore, GitStoreEvent, RepositoryEvent}, }; use settings::Settings; use std::{ops::Range, sync::Arc}; use ui::{ButtonLike, Divider, Tooltip, prelude::*}; use util::debug_panic; use workspace::{HideStatusItem, StatusItemView, Workspace, item::ItemHandle}; use zed_actions::agent::{ ConflictContent, ResolveConflictedFilesWithAgent, ResolveConflictsWithAgent, }; pub(crate) struct ConflictAddon { buffers: HashMap<BufferId, BufferConflicts>, } struct BufferConflicts { block_ids: Vec<(Range<Anchor>, CustomBlockId)>, conflict_set: Entity<ConflictSet>, _subscription: Subscription, } impl editor::Addon for ConflictAddon { fn to_any(&self) -> &dyn std::any::Any { self } fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> { Some(self) } } pub fn register_editor(editor: &mut Editor, buffer: Entity<MultiBuffer>, cx: &mut Context<Editor>) { let is_singleton = editor.buffer().read(cx).is_singleton(); if !editor.mode().is_full() || (!is_singleton && !editor.buffer().read(cx).all_diff_hunks_expanded()) || editor.read_only(cx) { return; } editor.register_addon(ConflictAddon { buffers: Default::default(), }); if is_singleton { let buffers = buffer.read(cx).all_buffers(); for buffer in buffers { open_conflict_set_for_buffer(editor, buffer, cx); } } } fn open_conflict_set_for_buffer( _editor: &mut Editor, buffer: Entity<Buffer>, cx: &mut Context<Editor>, ) { let buffer = buffer.downgrade(); cx.spawn(async move |editor, cx| { let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id())?; if let Some(conflict_set) = editor.read_with(cx, |editor, _| { editor .addon::<ConflictAddon>() .and_then(|addon| addon.buffers.get(&buffer_id)) .map(|buffer_conflicts| buffer_conflicts.conflict_set.clone()) })? { editor.update(cx, |editor, cx| { buffer_ranges_updated(editor, conflict_set, cx); })?; return anyhow::Ok(()); } let Some(project) = editor.read_with(cx, |editor, _| editor.project().cloned())? else { return anyhow::Ok(()); }; let git_store = project.read_with(cx, |project, _| project.git_store().clone()); let Some(buffer) = buffer.upgrade() else { return Ok(()); }; let conflict_set = git_store .update(cx, |git_store, cx| { git_store.open_conflict_set(buffer.clone(), cx) }) .await; editor.update(cx, |editor, cx| { buffer_ranges_updated(editor, conflict_set, cx); })?; Ok(()) }) .detach(); } pub(crate) fn buffer_ranges_updated( editor: &mut Editor, conflict_set: Entity<ConflictSet>, cx: &mut Context<Editor>, ) { let buffer_id = conflict_set.read(cx).snapshot.buffer_id; if editor.buffer().read(cx).buffer(buffer_id).is_none() { return; } let Some(conflict_addon) = editor.addon_mut::<ConflictAddon>() else { return; }; let buffer_conflicts = conflict_addon.buffers.entry(buffer_id).or_insert_with(|| { let subscription = cx.subscribe(&conflict_set, conflicts_updated); BufferConflicts { block_ids: Vec::new(), conflict_set: conflict_set.clone(), _subscription: subscription, } }); let conflict_set = buffer_conflicts.conflict_set.clone(); let conflicts_len = conflict_set.read(cx).snapshot().conflicts.len(); let addon_conflicts_len = buffer_conflicts.block_ids.len(); conflicts_updated( editor, conflict_set, &ConflictSetUpdate { buffer_range: None, old_range: 0..addon_conflicts_len, new_range: 0..conflicts_len, }, cx, ); } pub(crate) fn buffers_removed( editor: &mut Editor, removed_buffer_ids: &[BufferId], cx: &mut Context<Editor>, ) { let mut removed_block_ids = HashSet::default(); let Some(conflict_addon) = editor.addon_mut::<ConflictAddon>() else { return; }; conflict_addon.buffers.retain(|buffer_id, buffer| { if removed_buffer_ids.contains(buffer_id) { removed_block_ids.extend(buffer.block_ids.iter().map(|(_, block_id)| *block_id)); false } else { true } }); editor.remove_blocks(removed_block_ids, None, cx); } #[ztracing::instrument(skip_all)] fn conflicts_updated( editor: &mut Editor, conflict_set: Entity<ConflictSet>, event: &ConflictSetUpdate, cx: &mut Context<Editor>, ) { let buffer_id = conflict_set.read(cx).snapshot.buffer_id; let conflict_set = conflict_set.read(cx).snapshot(); let multibuffer = editor.buffer().read(cx); let snapshot = multibuffer.snapshot(cx); let old_range = { let Some(conflict_addon) = editor.addon_mut::<ConflictAddon>() else { return; }; let Some(buffer_conflicts) = conflict_addon.buffers.get(&buffer_id) else { return; }; match buffer_conflicts.block_ids.get(event.old_range.clone()) { Some(_) => Some(event.old_range.clone()), None => { debug_panic!( "conflicts updated event old range is invalid for buffer conflicts view (block_ids len is {:?}, old_range is {:?})", buffer_conflicts.block_ids.len(), event.old_range, ); if event.old_range.start <= event.old_range.end { Some( event.old_range.start.min(buffer_conflicts.block_ids.len()) ..event.old_range.end.min(buffer_conflicts.block_ids.len()), ) } else { None } } } }; // Remove obsolete highlights and blocks let Some(conflict_addon) = editor.addon_mut::<ConflictAddon>() else { return; }; if let Some((buffer_conflicts, old_range)) = conflict_addon .buffers .get_mut(&buffer_id) .zip(old_range.clone()) { let old_conflicts = buffer_conflicts.block_ids[old_range].to_owned(); let mut removed_highlighted_ranges = Vec::new(); let mut removed_block_ids = HashSet::default(); for (conflict_range, block_id) in old_conflicts { let Some(range) = snapshot.buffer_anchor_range_to_anchor_range(conflict_range) else { continue; }; removed_highlighted_ranges.push(range.clone()); removed_block_ids.insert(block_id); } editor.remove_gutter_highlights::<ConflictsOuter>(removed_highlighted_ranges.clone(), cx); editor.remove_highlighted_rows::<ConflictsOuter>(removed_highlighted_ranges.clone(), cx); editor.remove_highlighted_rows::<ConflictsOurs>(removed_highlighted_ranges.clone(), cx); editor .remove_highlighted_rows::<ConflictsOursMarker>(removed_highlighted_ranges.clone(), cx); editor.remove_highlighted_rows::<ConflictsTheirs>(removed_highlighted_ranges.clone(), cx); editor.remove_highlighted_rows::<ConflictsTheirsMarker>( removed_highlighted_ranges.clone(), cx, ); editor.remove_blocks(removed_block_ids, None, cx); } // Add new highlights and blocks let editor_handle = cx.weak_entity(); let new_conflicts = &conflict_set.conflicts[event.new_range.clone()]; let mut blocks = Vec::new(); for conflict in new_conflicts { update_conflict_highlighting(editor, conflict, &snapshot, cx); let Some(anchor) = snapshot.anchor_in_excerpt(conflict.range.start) else { continue; }; let editor_handle = editor_handle.clone(); blocks.push(BlockProperties { placement: BlockPlacement::Above(anchor), height: Some(1), style: BlockStyle::Sticky, render: Arc::new({ let conflict = conflict.clone(); move |cx| render_conflict_buttons(&conflict, editor_handle.clone(), cx) }), priority: 0, }) } let new_block_ids = editor.insert_blocks(blocks, None, cx); let Some(conflict_addon) = editor.addon_mut::<ConflictAddon>() else { return; }; if let Some((buffer_conflicts, old_range)) = conflict_addon.buffers.get_mut(&buffer_id).zip(old_range) { buffer_conflicts.block_ids.splice( old_range, new_conflicts .iter() .map(|conflict| conflict.range.clone()) .zip(new_block_ids), ); } } #[ztracing::instrument(skip_all)] fn update_conflict_highlighting( editor: &mut Editor, conflict: &ConflictRegion, buffer: &editor::MultiBufferSnapshot, cx: &mut Context<Editor>, ) -> Option<()> { log::debug!("update conflict highlighting for {conflict:?}"); let outer = buffer.buffer_anchor_range_to_anchor_range(conflict.range.clone())?; let ours = buffer.buffer_anchor_range_to_anchor_range(conflict.ours.clone())?; let theirs = buffer.buffer_anchor_range_to_anchor_range(conflict.theirs.clone())?; let ours_background = |cx: &App| cx.theme().colors().version_control_conflict_marker_ours; let theirs_background = |cx: &App| cx.theme().colors().version_control_conflict_marker_theirs; let options = RowHighlightOptions { include_gutter: true, ..Default::default() }; editor.insert_gutter_highlight::<ConflictsOuter>( outer.start..theirs.end, |cx| cx.theme().colors().editor_background, cx, ); // Prevent diff hunk highlighting within the entire conflict region. editor.highlight_rows::<ConflictsOuter>(outer.clone(), theirs_background, options, cx); editor.highlight_rows::<ConflictsOurs>(ours.clone(), ours_background, options, cx); editor.highlight_rows::<ConflictsOursMarker>( outer.start..ours.start, ours_background, options, cx, ); editor.highlight_rows::<ConflictsTheirs>(theirs.clone(), theirs_background, options, cx); editor.highlight_rows::<ConflictsTheirsMarker>( theirs.end..outer.end, theirs_background, options, cx, ); Some(()) } fn render_conflict_buttons( conflict: &ConflictRegion, editor: WeakEntity<Editor>, cx: &mut BlockContext, ) -> AnyElement { let is_ai_enabled = AgentSettings::get_global(cx).enabled(cx); h_flex() .id(cx.block_id) .h(cx.line_height) .ml(cx.margins.gutter.width) .gap_1() .bg(cx.theme().colors().editor_background) .child( Button::new("head", format!("Use {}", conflict.ours_branch_name)) .label_size(LabelSize::Small) .on_click({ let editor = editor.clone(); let conflict = conflict.clone(); let ours = conflict.ours.clone(); move |_, window, cx| { resolve_conflict( editor.clone(), conflict.clone(), vec![ours.clone()], window, cx, ) .detach() } }), ) .child( Button::new("origin", format!("Use {}", conflict.theirs_branch_name)) .label_size(LabelSize::Small) .on_click({ let editor = editor.clone(); let conflict = conflict.clone(); let theirs = conflict.theirs.clone(); move |_, window, cx| { resolve_conflict( editor.clone(), conflict.clone(), vec![theirs.clone()], window, cx, ) .detach() } }), ) .child( Button::new("both", "Use Both") .label_size(LabelSize::Small) .on_click({ let editor = editor.clone(); let conflict = conflict.clone(); let ours = conflict.ours.clone(); let theirs = conflict.theirs.clone(); move |_, window, cx| { resolve_conflict( editor.clone(), conflict.clone(), vec![ours.clone(), theirs.clone()], window, cx, ) .detach() } }), ) .when(is_ai_enabled, |this| { this.child(Divider::vertical()).child( Button::new("resolve-with-agent", "Resolve with Agent") .label_size(LabelSize::Small) .start_icon( Icon::new(IconName::ZedAssistant) .size(IconSize::Small) .color(Color::Muted), ) .on_click({ let conflict = conflict.clone(); move |_, window, cx| { let content = editor .update(cx, |editor, cx| { let multibuffer = editor.buffer().read(cx); let buffer_id = conflict.ours.end.buffer_id; let buffer = multibuffer.buffer(buffer_id)?; let buffer_read = buffer.read(cx); let snapshot = buffer_read.snapshot(); let conflict_text = snapshot .text_for_range(conflict.range.clone()) .collect::<String>(); let file_path = buffer_read .file() .and_then(|file| file.as_local()) .map(|f| f.abs_path(cx).to_string_lossy().to_string()) .unwrap_or_default(); Some(ConflictContent { file_path, conflict_text, ours_branch_name: conflict.ours_branch_name.to_string(), theirs_branch_name: conflict.theirs_branch_name.to_string(), }) }) .ok() .flatten(); if let Some(content) = content { window.dispatch_action( Box::new(ResolveConflictsWithAgent { conflicts: vec![content], }), cx, ); } } }), ) }) .into_any() } fn collect_conflicted_file_paths(project: &Project, cx: &App) -> Vec<String> { let git_store = project.git_store().read(cx); let mut paths = Vec::new(); for repo in git_store.repositories().values() { let snapshot = repo.read(cx).snapshot(); for (repo_path, _) in snapshot.merge.merge_heads_by_conflicted_path.iter() { let is_currently_conflicted = snapshot .status_for_path(repo_path) .is_some_and(|entry| entry.status.is_conflicted()); if !is_currently_conflicted { continue; } if let Some(project_path) = repo.read(cx).repo_path_to_project_path(repo_path, cx) { paths.push( project_path .path .as_std_path() .to_string_lossy() .to_string(), ); } } } paths } pub(crate) fn resolve_conflict( editor: WeakEntity<Editor>, resolved_conflict: ConflictRegion, ranges: Vec<Range<Anchor>>, window: &mut Window, cx: &mut App, ) -> Task<()> { window.spawn(cx, async move |cx| { editor .update(cx, |editor, cx| { let multibuffer = editor.buffer().clone(); let buffer_id = resolved_conflict.ours.end.buffer_id; let buffer = multibuffer.read(cx).buffer(buffer_id)?; resolved_conflict.resolve(buffer.clone(), &ranges, cx); let conflict_addon = editor.addon_mut::<ConflictAddon>()?; let snapshot = multibuffer.read(cx).snapshot(cx); let buffer_snapshot = buffer.read(cx).snapshot(); let state = conflict_addon .buffers .get_mut(&buffer_snapshot.remote_id())?; let ix = state .block_ids .binary_search_by(|(range, _)| { range .start .cmp(&resolved_conflict.range.start, &buffer_snapshot)