Highlight merge conflicts and provide for resolving them (#28065)
TODO: - [x] Make it work in the project diff: - [x] Support non-singleton buffers - [x] Adjust excerpt boundaries to show full conflicts - [x] Write tests for conflict-related events and state management - [x] Prevent hunk buttons from appearing inside conflicts - [x] Make sure it works over SSH, collab - [x] Allow separate theming of markers Bonus: - [ ] Count of conflicts in toolbar - [ ] Keyboard-driven navigation and resolution - [ ] ~~Inlay hints to contextualize "ours"/"theirs"~~ Release Notes: - Implemented initial support for resolving merge conflicts. --------- Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
This commit is contained in:
parent
ef54b58346
commit
724c935196
24 changed files with 1626 additions and 184 deletions
|
@ -1328,7 +1328,7 @@ impl InlineAssistant {
|
||||||
editor.highlight_rows::<InlineAssist>(
|
editor.highlight_rows::<InlineAssist>(
|
||||||
row_range,
|
row_range,
|
||||||
cx.theme().status().info_background,
|
cx.theme().status().info_background,
|
||||||
false,
|
Default::default(),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1393,7 +1393,7 @@ impl InlineAssistant {
|
||||||
editor.highlight_rows::<DeletedLines>(
|
editor.highlight_rows::<DeletedLines>(
|
||||||
Anchor::min()..Anchor::max(),
|
Anchor::min()..Anchor::max(),
|
||||||
cx.theme().status().deleted_background,
|
cx.theme().status().deleted_background,
|
||||||
false,
|
Default::default(),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
editor
|
editor
|
||||||
|
|
|
@ -1226,7 +1226,7 @@ impl InlineAssistant {
|
||||||
editor.highlight_rows::<InlineAssist>(
|
editor.highlight_rows::<InlineAssist>(
|
||||||
row_range,
|
row_range,
|
||||||
cx.theme().status().info_background,
|
cx.theme().status().info_background,
|
||||||
false,
|
Default::default(),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1291,7 +1291,7 @@ impl InlineAssistant {
|
||||||
editor.highlight_rows::<DeletedLines>(
|
editor.highlight_rows::<DeletedLines>(
|
||||||
Anchor::min()..Anchor::max(),
|
Anchor::min()..Anchor::max(),
|
||||||
cx.theme().status().deleted_background,
|
cx.theme().status().deleted_background,
|
||||||
false,
|
Default::default(),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
editor
|
editor
|
||||||
|
|
|
@ -269,6 +269,12 @@ enum DocumentHighlightWrite {}
|
||||||
enum InputComposition {}
|
enum InputComposition {}
|
||||||
enum SelectedTextHighlight {}
|
enum SelectedTextHighlight {}
|
||||||
|
|
||||||
|
pub enum ConflictsOuter {}
|
||||||
|
pub enum ConflictsOurs {}
|
||||||
|
pub enum ConflictsTheirs {}
|
||||||
|
pub enum ConflictsOursMarker {}
|
||||||
|
pub enum ConflictsTheirsMarker {}
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||||
pub enum Navigated {
|
pub enum Navigated {
|
||||||
Yes,
|
Yes,
|
||||||
|
@ -694,6 +700,10 @@ pub trait Addon: 'static {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_any(&self) -> &dyn std::any::Any;
|
fn to_any(&self) -> &dyn std::any::Any;
|
||||||
|
|
||||||
|
fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A set of caret positions, registered when the editor was edited.
|
/// A set of caret positions, registered when the editor was edited.
|
||||||
|
@ -1083,11 +1093,27 @@ impl SelectionHistory {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct RowHighlightOptions {
|
||||||
|
pub autoscroll: bool,
|
||||||
|
pub include_gutter: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for RowHighlightOptions {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
autoscroll: Default::default(),
|
||||||
|
include_gutter: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct RowHighlight {
|
struct RowHighlight {
|
||||||
index: usize,
|
index: usize,
|
||||||
range: Range<Anchor>,
|
range: Range<Anchor>,
|
||||||
color: Hsla,
|
color: Hsla,
|
||||||
should_autoscroll: bool,
|
options: RowHighlightOptions,
|
||||||
|
type_id: TypeId,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
@ -5942,7 +5968,10 @@ impl Editor {
|
||||||
self.highlight_rows::<EditPredictionPreview>(
|
self.highlight_rows::<EditPredictionPreview>(
|
||||||
target..target,
|
target..target,
|
||||||
cx.theme().colors().editor_highlighted_line_background,
|
cx.theme().colors().editor_highlighted_line_background,
|
||||||
true,
|
RowHighlightOptions {
|
||||||
|
autoscroll: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
self.request_autoscroll(Autoscroll::fit(), cx);
|
self.request_autoscroll(Autoscroll::fit(), cx);
|
||||||
|
@ -13449,7 +13478,7 @@ impl Editor {
|
||||||
start..end,
|
start..end,
|
||||||
highlight_color
|
highlight_color
|
||||||
.unwrap_or_else(|| cx.theme().colors().editor_highlighted_line_background),
|
.unwrap_or_else(|| cx.theme().colors().editor_highlighted_line_background),
|
||||||
false,
|
Default::default(),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
self.request_autoscroll(Autoscroll::center().for_anchor(start), cx);
|
self.request_autoscroll(Autoscroll::center().for_anchor(start), cx);
|
||||||
|
@ -16765,7 +16794,7 @@ impl Editor {
|
||||||
&mut self,
|
&mut self,
|
||||||
range: Range<Anchor>,
|
range: Range<Anchor>,
|
||||||
color: Hsla,
|
color: Hsla,
|
||||||
should_autoscroll: bool,
|
options: RowHighlightOptions,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let snapshot = self.buffer().read(cx).snapshot(cx);
|
let snapshot = self.buffer().read(cx).snapshot(cx);
|
||||||
|
@ -16797,7 +16826,7 @@ impl Editor {
|
||||||
merged = true;
|
merged = true;
|
||||||
prev_highlight.index = index;
|
prev_highlight.index = index;
|
||||||
prev_highlight.color = color;
|
prev_highlight.color = color;
|
||||||
prev_highlight.should_autoscroll = should_autoscroll;
|
prev_highlight.options = options;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16808,7 +16837,8 @@ impl Editor {
|
||||||
range: range.clone(),
|
range: range.clone(),
|
||||||
index,
|
index,
|
||||||
color,
|
color,
|
||||||
should_autoscroll,
|
options,
|
||||||
|
type_id: TypeId::of::<T>(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -16914,7 +16944,15 @@ impl Editor {
|
||||||
used_highlight_orders.entry(row).or_insert(highlight.index);
|
used_highlight_orders.entry(row).or_insert(highlight.index);
|
||||||
if highlight.index >= *used_index {
|
if highlight.index >= *used_index {
|
||||||
*used_index = highlight.index;
|
*used_index = highlight.index;
|
||||||
unique_rows.insert(DisplayRow(row), highlight.color.into());
|
unique_rows.insert(
|
||||||
|
DisplayRow(row),
|
||||||
|
LineHighlight {
|
||||||
|
include_gutter: highlight.options.include_gutter,
|
||||||
|
border: None,
|
||||||
|
background: highlight.color.into(),
|
||||||
|
type_id: Some(highlight.type_id),
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
unique_rows
|
unique_rows
|
||||||
|
@ -16930,7 +16968,7 @@ impl Editor {
|
||||||
.values()
|
.values()
|
||||||
.flat_map(|highlighted_rows| highlighted_rows.iter())
|
.flat_map(|highlighted_rows| highlighted_rows.iter())
|
||||||
.filter_map(|highlight| {
|
.filter_map(|highlight| {
|
||||||
if highlight.should_autoscroll {
|
if highlight.options.autoscroll {
|
||||||
Some(highlight.range.start.to_display_point(snapshot).row())
|
Some(highlight.range.start.to_display_point(snapshot).row())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
@ -17405,13 +17443,19 @@ impl Editor {
|
||||||
});
|
});
|
||||||
self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
|
self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
|
||||||
}
|
}
|
||||||
multi_buffer::Event::ExcerptsRemoved { ids } => {
|
multi_buffer::Event::ExcerptsRemoved {
|
||||||
|
ids,
|
||||||
|
removed_buffer_ids,
|
||||||
|
} => {
|
||||||
self.refresh_inlay_hints(InlayHintRefreshReason::ExcerptsRemoved(ids.clone()), cx);
|
self.refresh_inlay_hints(InlayHintRefreshReason::ExcerptsRemoved(ids.clone()), cx);
|
||||||
let buffer = self.buffer.read(cx);
|
let buffer = self.buffer.read(cx);
|
||||||
self.registered_buffers
|
self.registered_buffers
|
||||||
.retain(|buffer_id, _| buffer.buffer(*buffer_id).is_some());
|
.retain(|buffer_id, _| buffer.buffer(*buffer_id).is_some());
|
||||||
jsx_tag_auto_close::refresh_enabled_in_any_buffer(self, multibuffer, cx);
|
jsx_tag_auto_close::refresh_enabled_in_any_buffer(self, multibuffer, cx);
|
||||||
cx.emit(EditorEvent::ExcerptsRemoved { ids: ids.clone() })
|
cx.emit(EditorEvent::ExcerptsRemoved {
|
||||||
|
ids: ids.clone(),
|
||||||
|
removed_buffer_ids: removed_buffer_ids.clone(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
multi_buffer::Event::ExcerptsEdited {
|
multi_buffer::Event::ExcerptsEdited {
|
||||||
excerpt_ids,
|
excerpt_ids,
|
||||||
|
@ -18219,6 +18263,13 @@ impl Editor {
|
||||||
.and_then(|item| item.to_any().downcast_ref::<T>())
|
.and_then(|item| item.to_any().downcast_ref::<T>())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn addon_mut<T: Addon>(&mut self) -> Option<&mut T> {
|
||||||
|
let type_id = std::any::TypeId::of::<T>();
|
||||||
|
self.addons
|
||||||
|
.get_mut(&type_id)
|
||||||
|
.and_then(|item| item.to_any_mut()?.downcast_mut::<T>())
|
||||||
|
}
|
||||||
|
|
||||||
fn character_size(&self, window: &mut Window) -> gpui::Size<Pixels> {
|
fn character_size(&self, window: &mut Window) -> gpui::Size<Pixels> {
|
||||||
let text_layout_details = self.text_layout_details(window);
|
let text_layout_details = self.text_layout_details(window);
|
||||||
let style = &text_layout_details.editor_style;
|
let style = &text_layout_details.editor_style;
|
||||||
|
@ -19732,6 +19783,7 @@ pub enum EditorEvent {
|
||||||
},
|
},
|
||||||
ExcerptsRemoved {
|
ExcerptsRemoved {
|
||||||
ids: Vec<ExcerptId>,
|
ids: Vec<ExcerptId>,
|
||||||
|
removed_buffer_ids: Vec<BufferId>,
|
||||||
},
|
},
|
||||||
BufferFoldToggled {
|
BufferFoldToggled {
|
||||||
ids: Vec<ExcerptId>,
|
ids: Vec<ExcerptId>,
|
||||||
|
@ -20672,24 +20724,8 @@ impl Render for MissingEditPredictionKeybindingTooltip {
|
||||||
pub struct LineHighlight {
|
pub struct LineHighlight {
|
||||||
pub background: Background,
|
pub background: Background,
|
||||||
pub border: Option<gpui::Hsla>,
|
pub border: Option<gpui::Hsla>,
|
||||||
}
|
pub include_gutter: bool,
|
||||||
|
pub type_id: Option<TypeId>,
|
||||||
impl From<Hsla> for LineHighlight {
|
|
||||||
fn from(hsla: Hsla) -> Self {
|
|
||||||
Self {
|
|
||||||
background: hsla.into(),
|
|
||||||
border: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Background> for LineHighlight {
|
|
||||||
fn from(background: Background) -> Self {
|
|
||||||
Self {
|
|
||||||
background,
|
|
||||||
border: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_diff_hunk_controls(
|
fn render_diff_hunk_controls(
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
ActiveDiagnostic, BlockId, COLUMNAR_SELECTION_MODIFIERS, CURSORS_VISIBLE_FOR,
|
ActiveDiagnostic, BlockId, COLUMNAR_SELECTION_MODIFIERS, CURSORS_VISIBLE_FOR,
|
||||||
ChunkRendererContext, ChunkReplacement, ContextMenuPlacement, CursorShape, CustomBlockId,
|
ChunkRendererContext, ChunkReplacement, ConflictsOurs, ConflictsOursMarker, ConflictsOuter,
|
||||||
|
ConflictsTheirs, ConflictsTheirsMarker, ContextMenuPlacement, CursorShape, CustomBlockId,
|
||||||
DisplayDiffHunk, DisplayPoint, DisplayRow, DocumentHighlightRead, DocumentHighlightWrite,
|
DisplayDiffHunk, DisplayPoint, DisplayRow, DocumentHighlightRead, DocumentHighlightWrite,
|
||||||
EditDisplayMode, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle,
|
EditDisplayMode, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle,
|
||||||
FILE_HEADER_HEIGHT, FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput,
|
FILE_HEADER_HEIGHT, FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput,
|
||||||
|
@ -4036,6 +4037,7 @@ impl EditorElement {
|
||||||
line_height: Pixels,
|
line_height: Pixels,
|
||||||
scroll_pixel_position: gpui::Point<Pixels>,
|
scroll_pixel_position: gpui::Point<Pixels>,
|
||||||
display_hunks: &[(DisplayDiffHunk, Option<Hitbox>)],
|
display_hunks: &[(DisplayDiffHunk, Option<Hitbox>)],
|
||||||
|
highlighted_rows: &BTreeMap<DisplayRow, LineHighlight>,
|
||||||
editor: Entity<Editor>,
|
editor: Entity<Editor>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
|
@ -4064,6 +4066,22 @@ impl EditorElement {
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if highlighted_rows
|
||||||
|
.get(&display_row_range.start)
|
||||||
|
.and_then(|highlight| highlight.type_id)
|
||||||
|
.is_some_and(|type_id| {
|
||||||
|
[
|
||||||
|
TypeId::of::<ConflictsOuter>(),
|
||||||
|
TypeId::of::<ConflictsOursMarker>(),
|
||||||
|
TypeId::of::<ConflictsOurs>(),
|
||||||
|
TypeId::of::<ConflictsTheirs>(),
|
||||||
|
TypeId::of::<ConflictsTheirsMarker>(),
|
||||||
|
]
|
||||||
|
.contains(&type_id)
|
||||||
|
})
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let row_ix = (display_row_range.start - row_range.start).0 as usize;
|
let row_ix = (display_row_range.start - row_range.start).0 as usize;
|
||||||
if row_infos[row_ix].diff_status.is_none() {
|
if row_infos[row_ix].diff_status.is_none() {
|
||||||
continue;
|
continue;
|
||||||
|
@ -4258,14 +4276,21 @@ impl EditorElement {
|
||||||
highlight_row_end: DisplayRow,
|
highlight_row_end: DisplayRow,
|
||||||
highlight: crate::LineHighlight,
|
highlight: crate::LineHighlight,
|
||||||
edges| {
|
edges| {
|
||||||
|
let mut origin_x = layout.hitbox.left();
|
||||||
|
let mut width = layout.hitbox.size.width;
|
||||||
|
if !highlight.include_gutter {
|
||||||
|
origin_x += layout.gutter_hitbox.size.width;
|
||||||
|
width -= layout.gutter_hitbox.size.width;
|
||||||
|
}
|
||||||
|
|
||||||
let origin = point(
|
let origin = point(
|
||||||
layout.hitbox.origin.x,
|
origin_x,
|
||||||
layout.hitbox.origin.y
|
layout.hitbox.origin.y
|
||||||
+ (highlight_row_start.as_f32() - scroll_top)
|
+ (highlight_row_start.as_f32() - scroll_top)
|
||||||
* layout.position_map.line_height,
|
* layout.position_map.line_height,
|
||||||
);
|
);
|
||||||
let size = size(
|
let size = size(
|
||||||
layout.hitbox.size.width,
|
width,
|
||||||
layout.position_map.line_height
|
layout.position_map.line_height
|
||||||
* highlight_row_end.next_row().minus(highlight_row_start) as f32,
|
* highlight_row_end.next_row().minus(highlight_row_start) as f32,
|
||||||
);
|
);
|
||||||
|
@ -6789,10 +6814,16 @@ impl Element for EditorElement {
|
||||||
} else {
|
} else {
|
||||||
background_color.opacity(0.36)
|
background_color.opacity(0.36)
|
||||||
}),
|
}),
|
||||||
|
include_gutter: true,
|
||||||
|
type_id: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let filled_highlight =
|
let filled_highlight = LineHighlight {
|
||||||
solid_background(background_color.opacity(hunk_opacity)).into();
|
background: solid_background(background_color.opacity(hunk_opacity)),
|
||||||
|
border: None,
|
||||||
|
include_gutter: true,
|
||||||
|
type_id: None,
|
||||||
|
};
|
||||||
|
|
||||||
let background = if Self::diff_hunk_hollow(diff_status, cx) {
|
let background = if Self::diff_hunk_hollow(diff_status, cx) {
|
||||||
hollow_highlight
|
hollow_highlight
|
||||||
|
@ -7551,6 +7582,7 @@ impl Element for EditorElement {
|
||||||
line_height,
|
line_height,
|
||||||
scroll_pixel_position,
|
scroll_pixel_position,
|
||||||
&display_hunks,
|
&display_hunks,
|
||||||
|
&highlighted_rows,
|
||||||
self.editor.clone(),
|
self.editor.clone(),
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
|
|
@ -288,7 +288,7 @@ impl FollowableItem for Editor {
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
EditorEvent::ExcerptsRemoved { ids } => {
|
EditorEvent::ExcerptsRemoved { ids, .. } => {
|
||||||
update
|
update
|
||||||
.deleted_excerpts
|
.deleted_excerpts
|
||||||
.extend(ids.iter().map(ExcerptId::to_proto));
|
.extend(ids.iter().map(ExcerptId::to_proto));
|
||||||
|
|
|
@ -34,6 +34,7 @@ pub struct FakeGitRepositoryState {
|
||||||
pub blames: HashMap<RepoPath, Blame>,
|
pub blames: HashMap<RepoPath, Blame>,
|
||||||
pub current_branch_name: Option<String>,
|
pub current_branch_name: Option<String>,
|
||||||
pub branches: HashSet<String>,
|
pub branches: HashSet<String>,
|
||||||
|
pub merge_head_shas: Vec<String>,
|
||||||
pub simulated_index_write_error_message: Option<String>,
|
pub simulated_index_write_error_message: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,12 +48,20 @@ impl FakeGitRepositoryState {
|
||||||
blames: Default::default(),
|
blames: Default::default(),
|
||||||
current_branch_name: Default::default(),
|
current_branch_name: Default::default(),
|
||||||
branches: Default::default(),
|
branches: Default::default(),
|
||||||
|
merge_head_shas: Default::default(),
|
||||||
simulated_index_write_error_message: Default::default(),
|
simulated_index_write_error_message: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FakeGitRepository {
|
impl FakeGitRepository {
|
||||||
|
fn with_state<F, T>(&self, write: bool, f: F) -> Result<T>
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut FakeGitRepositoryState) -> T,
|
||||||
|
{
|
||||||
|
self.fs.with_git_state(&self.dot_git_path, write, f)
|
||||||
|
}
|
||||||
|
|
||||||
fn with_state_async<F, T>(&self, write: bool, f: F) -> BoxFuture<'static, Result<T>>
|
fn with_state_async<F, T>(&self, write: bool, f: F) -> BoxFuture<'static, Result<T>>
|
||||||
where
|
where
|
||||||
F: 'static + Send + FnOnce(&mut FakeGitRepositoryState) -> Result<T>,
|
F: 'static + Send + FnOnce(&mut FakeGitRepositoryState) -> Result<T>,
|
||||||
|
@ -137,11 +146,18 @@ impl GitRepository for FakeGitRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn merge_head_shas(&self) -> Vec<String> {
|
fn merge_head_shas(&self) -> Vec<String> {
|
||||||
vec![]
|
self.with_state(false, |state| state.merge_head_shas.clone())
|
||||||
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show(&self, _commit: String) -> BoxFuture<Result<CommitDetails>> {
|
fn show(&self, commit: String) -> BoxFuture<Result<CommitDetails>> {
|
||||||
unimplemented!()
|
async {
|
||||||
|
Ok(CommitDetails {
|
||||||
|
sha: commit.into(),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
.boxed()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reset(
|
fn reset(
|
||||||
|
|
|
@ -133,7 +133,7 @@ pub struct CommitSummary {
|
||||||
pub has_parent: bool,
|
pub has_parent: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
#[derive(Clone, Debug, Default, Hash, PartialEq, Eq)]
|
||||||
pub struct CommitDetails {
|
pub struct CommitDetails {
|
||||||
pub sha: SharedString,
|
pub sha: SharedString,
|
||||||
pub message: SharedString,
|
pub message: SharedString,
|
||||||
|
|
473
crates/git_ui/src/conflict_view.rs
Normal file
473
crates/git_ui/src/conflict_view.rs
Normal file
|
@ -0,0 +1,473 @@
|
||||||
|
use collections::{HashMap, HashSet};
|
||||||
|
use editor::{
|
||||||
|
ConflictsOurs, ConflictsOursMarker, ConflictsOuter, ConflictsTheirs, ConflictsTheirsMarker,
|
||||||
|
Editor, EditorEvent, ExcerptId, MultiBuffer, RowHighlightOptions,
|
||||||
|
display_map::{BlockContext, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId},
|
||||||
|
};
|
||||||
|
use gpui::{
|
||||||
|
App, Context, Entity, InteractiveElement as _, ParentElement as _, Subscription, WeakEntity,
|
||||||
|
};
|
||||||
|
use language::{Anchor, Buffer, BufferId};
|
||||||
|
use project::{ConflictRegion, ConflictSet, ConflictSetUpdate};
|
||||||
|
use std::{ops::Range, sync::Arc};
|
||||||
|
use ui::{
|
||||||
|
ActiveTheme, AnyElement, Element as _, StatefulInteractiveElement, Styled,
|
||||||
|
StyledTypography as _, div, h_flex, rems,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(crate) struct ConflictAddon {
|
||||||
|
buffers: HashMap<BufferId, BufferConflicts>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConflictAddon {
|
||||||
|
pub(crate) fn conflict_set(&self, buffer_id: BufferId) -> Option<Entity<ConflictSet>> {
|
||||||
|
self.buffers
|
||||||
|
.get(&buffer_id)
|
||||||
|
.map(|entry| entry.conflict_set.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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>) {
|
||||||
|
// Only show conflict UI for singletons and in the project diff.
|
||||||
|
if !editor.buffer().read(cx).is_singleton()
|
||||||
|
&& !editor.buffer().read(cx).all_diff_hunks_expanded()
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.register_addon(ConflictAddon {
|
||||||
|
buffers: Default::default(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let buffers = buffer.read(cx).all_buffers().clone();
|
||||||
|
for buffer in buffers {
|
||||||
|
buffer_added(editor, buffer, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.subscribe(&cx.entity(), |editor, _, event, cx| match event {
|
||||||
|
EditorEvent::ExcerptsAdded { buffer, .. } => buffer_added(editor, buffer.clone(), cx),
|
||||||
|
EditorEvent::ExcerptsExpanded { ids } => {
|
||||||
|
let multibuffer = editor.buffer().read(cx).snapshot(cx);
|
||||||
|
for excerpt_id in ids {
|
||||||
|
let Some(buffer) = multibuffer.buffer_for_excerpt(*excerpt_id) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let addon = editor.addon::<ConflictAddon>().unwrap();
|
||||||
|
let Some(conflict_set) = addon.conflict_set(buffer.remote_id()).clone() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
excerpt_for_buffer_updated(editor, conflict_set, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EditorEvent::ExcerptsRemoved {
|
||||||
|
removed_buffer_ids, ..
|
||||||
|
} => buffers_removed(editor, removed_buffer_ids, cx),
|
||||||
|
_ => {}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn excerpt_for_buffer_updated(
|
||||||
|
editor: &mut Editor,
|
||||||
|
conflict_set: Entity<ConflictSet>,
|
||||||
|
cx: &mut Context<Editor>,
|
||||||
|
) {
|
||||||
|
let conflicts_len = conflict_set.read(cx).snapshot().conflicts.len();
|
||||||
|
conflicts_updated(
|
||||||
|
editor,
|
||||||
|
conflict_set,
|
||||||
|
&ConflictSetUpdate {
|
||||||
|
buffer_range: None,
|
||||||
|
old_range: 0..conflicts_len,
|
||||||
|
new_range: 0..conflicts_len,
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn buffer_added(editor: &mut Editor, buffer: Entity<Buffer>, cx: &mut Context<Editor>) {
|
||||||
|
let Some(project) = &editor.project else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let git_store = project.read(cx).git_store().clone();
|
||||||
|
|
||||||
|
let buffer_conflicts = editor
|
||||||
|
.addon_mut::<ConflictAddon>()
|
||||||
|
.unwrap()
|
||||||
|
.buffers
|
||||||
|
.entry(buffer.read(cx).remote_id())
|
||||||
|
.or_insert_with(|| {
|
||||||
|
let conflict_set = git_store.update(cx, |git_store, cx| {
|
||||||
|
git_store.open_conflict_set(buffer.clone(), cx)
|
||||||
|
});
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn buffers_removed(editor: &mut Editor, removed_buffer_ids: &[BufferId], cx: &mut Context<Editor>) {
|
||||||
|
let mut removed_block_ids = HashSet::default();
|
||||||
|
editor
|
||||||
|
.addon_mut::<ConflictAddon>()
|
||||||
|
.unwrap()
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 excerpts = multibuffer.excerpts_for_buffer(buffer_id, cx);
|
||||||
|
let Some(buffer_snapshot) = excerpts
|
||||||
|
.first()
|
||||||
|
.and_then(|(excerpt_id, _)| snapshot.buffer_for_excerpt(*excerpt_id))
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove obsolete highlights and blocks
|
||||||
|
let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
|
||||||
|
if let Some(buffer_conflicts) = conflict_addon.buffers.get_mut(&buffer_id) {
|
||||||
|
let old_conflicts = buffer_conflicts.block_ids[event.old_range.clone()].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((excerpt_id, _)) = excerpts.iter().find(|(_, range)| {
|
||||||
|
let precedes_start = range
|
||||||
|
.context
|
||||||
|
.start
|
||||||
|
.cmp(&conflict_range.start, &buffer_snapshot)
|
||||||
|
.is_le();
|
||||||
|
let follows_end = range
|
||||||
|
.context
|
||||||
|
.end
|
||||||
|
.cmp(&conflict_range.start, &buffer_snapshot)
|
||||||
|
.is_ge();
|
||||||
|
precedes_start && follows_end
|
||||||
|
}) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let excerpt_id = *excerpt_id;
|
||||||
|
let Some(range) = snapshot
|
||||||
|
.anchor_in_excerpt(excerpt_id, conflict_range.start)
|
||||||
|
.zip(snapshot.anchor_in_excerpt(excerpt_id, conflict_range.end))
|
||||||
|
.map(|(start, end)| start..end)
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
removed_highlighted_ranges.push(range.clone());
|
||||||
|
removed_block_ids.insert(block_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
let Some((excerpt_id, _)) = excerpts.iter().find(|(_, range)| {
|
||||||
|
let precedes_start = range
|
||||||
|
.context
|
||||||
|
.start
|
||||||
|
.cmp(&conflict.range.start, &buffer_snapshot)
|
||||||
|
.is_le();
|
||||||
|
let follows_end = range
|
||||||
|
.context
|
||||||
|
.end
|
||||||
|
.cmp(&conflict.range.start, &buffer_snapshot)
|
||||||
|
.is_ge();
|
||||||
|
precedes_start && follows_end
|
||||||
|
}) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let excerpt_id = *excerpt_id;
|
||||||
|
|
||||||
|
update_conflict_highlighting(editor, conflict, &snapshot, excerpt_id, cx);
|
||||||
|
|
||||||
|
let Some(anchor) = snapshot.anchor_in_excerpt(excerpt_id, conflict.range.start) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let editor_handle = editor_handle.clone();
|
||||||
|
blocks.push(BlockProperties {
|
||||||
|
placement: BlockPlacement::Above(anchor),
|
||||||
|
height: Some(1),
|
||||||
|
style: BlockStyle::Fixed,
|
||||||
|
render: Arc::new({
|
||||||
|
let conflict = conflict.clone();
|
||||||
|
move |cx| render_conflict_buttons(&conflict, excerpt_id, editor_handle.clone(), cx)
|
||||||
|
}),
|
||||||
|
priority: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
let new_block_ids = editor.insert_blocks(blocks, None, cx);
|
||||||
|
|
||||||
|
let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
|
||||||
|
if let Some(buffer_conflicts) = conflict_addon.buffers.get_mut(&buffer_id) {
|
||||||
|
buffer_conflicts.block_ids.splice(
|
||||||
|
event.old_range.clone(),
|
||||||
|
new_conflicts
|
||||||
|
.iter()
|
||||||
|
.map(|conflict| conflict.range.clone())
|
||||||
|
.zip(new_block_ids),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_conflict_highlighting(
|
||||||
|
editor: &mut Editor,
|
||||||
|
conflict: &ConflictRegion,
|
||||||
|
buffer: &editor::MultiBufferSnapshot,
|
||||||
|
excerpt_id: editor::ExcerptId,
|
||||||
|
cx: &mut Context<Editor>,
|
||||||
|
) {
|
||||||
|
log::debug!("update conflict highlighting for {conflict:?}");
|
||||||
|
let theme = cx.theme().clone();
|
||||||
|
let colors = theme.colors();
|
||||||
|
let outer_start = buffer
|
||||||
|
.anchor_in_excerpt(excerpt_id, conflict.range.start)
|
||||||
|
.unwrap();
|
||||||
|
let outer_end = buffer
|
||||||
|
.anchor_in_excerpt(excerpt_id, conflict.range.end)
|
||||||
|
.unwrap();
|
||||||
|
let our_start = buffer
|
||||||
|
.anchor_in_excerpt(excerpt_id, conflict.ours.start)
|
||||||
|
.unwrap();
|
||||||
|
let our_end = buffer
|
||||||
|
.anchor_in_excerpt(excerpt_id, conflict.ours.end)
|
||||||
|
.unwrap();
|
||||||
|
let their_start = buffer
|
||||||
|
.anchor_in_excerpt(excerpt_id, conflict.theirs.start)
|
||||||
|
.unwrap();
|
||||||
|
let their_end = buffer
|
||||||
|
.anchor_in_excerpt(excerpt_id, conflict.theirs.end)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let ours_background = colors.version_control_conflict_ours_background;
|
||||||
|
let ours_marker = colors.version_control_conflict_ours_marker_background;
|
||||||
|
let theirs_background = colors.version_control_conflict_theirs_background;
|
||||||
|
let theirs_marker = colors.version_control_conflict_theirs_marker_background;
|
||||||
|
let divider_background = colors.version_control_conflict_divider_background;
|
||||||
|
|
||||||
|
let options = RowHighlightOptions {
|
||||||
|
include_gutter: false,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Prevent diff hunk highlighting within the entire conflict region.
|
||||||
|
editor.highlight_rows::<ConflictsOuter>(
|
||||||
|
outer_start..outer_end,
|
||||||
|
divider_background,
|
||||||
|
options,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
editor.highlight_rows::<ConflictsOurs>(our_start..our_end, ours_background, options, cx);
|
||||||
|
editor.highlight_rows::<ConflictsOursMarker>(outer_start..our_start, ours_marker, options, cx);
|
||||||
|
editor.highlight_rows::<ConflictsTheirs>(
|
||||||
|
their_start..their_end,
|
||||||
|
theirs_background,
|
||||||
|
options,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
editor.highlight_rows::<ConflictsTheirsMarker>(
|
||||||
|
their_end..outer_end,
|
||||||
|
theirs_marker,
|
||||||
|
options,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_conflict_buttons(
|
||||||
|
conflict: &ConflictRegion,
|
||||||
|
excerpt_id: ExcerptId,
|
||||||
|
editor: WeakEntity<Editor>,
|
||||||
|
cx: &mut BlockContext,
|
||||||
|
) -> AnyElement {
|
||||||
|
h_flex()
|
||||||
|
.h(cx.line_height)
|
||||||
|
.items_end()
|
||||||
|
.ml(cx.gutter_dimensions.width)
|
||||||
|
.id(cx.block_id)
|
||||||
|
.gap_0p5()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.id("ours")
|
||||||
|
.px_1()
|
||||||
|
.child("Take Ours")
|
||||||
|
.rounded_t(rems(0.2))
|
||||||
|
.text_ui_sm(cx)
|
||||||
|
.hover(|this| this.bg(cx.theme().colors().element_background))
|
||||||
|
.cursor_pointer()
|
||||||
|
.on_click({
|
||||||
|
let editor = editor.clone();
|
||||||
|
let conflict = conflict.clone();
|
||||||
|
let ours = conflict.ours.clone();
|
||||||
|
move |_, _, cx| {
|
||||||
|
resolve_conflict(editor.clone(), excerpt_id, &conflict, &[ours.clone()], cx)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.id("theirs")
|
||||||
|
.px_1()
|
||||||
|
.child("Take Theirs")
|
||||||
|
.rounded_t(rems(0.2))
|
||||||
|
.text_ui_sm(cx)
|
||||||
|
.hover(|this| this.bg(cx.theme().colors().element_background))
|
||||||
|
.cursor_pointer()
|
||||||
|
.on_click({
|
||||||
|
let editor = editor.clone();
|
||||||
|
let conflict = conflict.clone();
|
||||||
|
let theirs = conflict.theirs.clone();
|
||||||
|
move |_, _, cx| {
|
||||||
|
resolve_conflict(
|
||||||
|
editor.clone(),
|
||||||
|
excerpt_id,
|
||||||
|
&conflict,
|
||||||
|
&[theirs.clone()],
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.id("both")
|
||||||
|
.px_1()
|
||||||
|
.child("Take Both")
|
||||||
|
.rounded_t(rems(0.2))
|
||||||
|
.text_ui_sm(cx)
|
||||||
|
.hover(|this| this.bg(cx.theme().colors().element_background))
|
||||||
|
.cursor_pointer()
|
||||||
|
.on_click({
|
||||||
|
let editor = editor.clone();
|
||||||
|
let conflict = conflict.clone();
|
||||||
|
let ours = conflict.ours.clone();
|
||||||
|
let theirs = conflict.theirs.clone();
|
||||||
|
move |_, _, cx| {
|
||||||
|
resolve_conflict(
|
||||||
|
editor.clone(),
|
||||||
|
excerpt_id,
|
||||||
|
&conflict,
|
||||||
|
&[ours.clone(), theirs.clone()],
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.into_any()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_conflict(
|
||||||
|
editor: WeakEntity<Editor>,
|
||||||
|
excerpt_id: ExcerptId,
|
||||||
|
resolved_conflict: &ConflictRegion,
|
||||||
|
ranges: &[Range<Anchor>],
|
||||||
|
cx: &mut App,
|
||||||
|
) {
|
||||||
|
let Some(editor) = editor.upgrade() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let multibuffer = editor.read(cx).buffer().read(cx);
|
||||||
|
let snapshot = multibuffer.snapshot(cx);
|
||||||
|
let Some(buffer) = resolved_conflict
|
||||||
|
.ours
|
||||||
|
.end
|
||||||
|
.buffer_id
|
||||||
|
.and_then(|buffer_id| multibuffer.buffer(buffer_id))
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||||
|
|
||||||
|
resolved_conflict.resolve(buffer, ranges, cx);
|
||||||
|
|
||||||
|
editor.update(cx, |editor, cx| {
|
||||||
|
let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
|
||||||
|
let Some(state) = conflict_addon.buffers.get_mut(&buffer_snapshot.remote_id()) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Ok(ix) = state.block_ids.binary_search_by(|(range, _)| {
|
||||||
|
range
|
||||||
|
.start
|
||||||
|
.cmp(&resolved_conflict.range.start, &buffer_snapshot)
|
||||||
|
}) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let &(_, block_id) = &state.block_ids[ix];
|
||||||
|
let start = snapshot
|
||||||
|
.anchor_in_excerpt(excerpt_id, resolved_conflict.range.start)
|
||||||
|
.unwrap();
|
||||||
|
let end = snapshot
|
||||||
|
.anchor_in_excerpt(excerpt_id, resolved_conflict.range.end)
|
||||||
|
.unwrap();
|
||||||
|
editor.remove_highlighted_rows::<ConflictsOuter>(vec![start..end], cx);
|
||||||
|
editor.remove_highlighted_rows::<ConflictsOurs>(vec![start..end], cx);
|
||||||
|
editor.remove_highlighted_rows::<ConflictsTheirs>(vec![start..end], cx);
|
||||||
|
editor.remove_highlighted_rows::<ConflictsOursMarker>(vec![start..end], cx);
|
||||||
|
editor.remove_highlighted_rows::<ConflictsTheirsMarker>(vec![start..end], cx);
|
||||||
|
editor.remove_blocks(HashSet::from_iter([block_id]), None, cx);
|
||||||
|
})
|
||||||
|
}
|
|
@ -447,7 +447,7 @@ impl GitPanel {
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
GitStoreEvent::RepositoryUpdated(_, _, _) => {}
|
GitStoreEvent::RepositoryUpdated(_, _, _) => {}
|
||||||
GitStoreEvent::JobsUpdated => {}
|
GitStoreEvent::JobsUpdated | GitStoreEvent::ConflictsUpdated => {}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.detach();
|
.detach();
|
||||||
|
@ -1650,7 +1650,7 @@ impl GitPanel {
|
||||||
if let Some(merge_message) = self
|
if let Some(merge_message) = self
|
||||||
.active_repository
|
.active_repository
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|repo| repo.read(cx).merge_message.as_ref())
|
.and_then(|repo| repo.read(cx).merge.message.as_ref())
|
||||||
{
|
{
|
||||||
return Some(merge_message.to_string());
|
return Some(merge_message.to_string());
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ use std::any::Any;
|
||||||
use ::settings::Settings;
|
use ::settings::Settings;
|
||||||
use command_palette_hooks::CommandPaletteFilter;
|
use command_palette_hooks::CommandPaletteFilter;
|
||||||
use commit_modal::CommitModal;
|
use commit_modal::CommitModal;
|
||||||
|
use editor::Editor;
|
||||||
mod blame_ui;
|
mod blame_ui;
|
||||||
use git::{
|
use git::{
|
||||||
repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus},
|
repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus},
|
||||||
|
@ -20,6 +21,7 @@ pub mod branch_picker;
|
||||||
mod commit_modal;
|
mod commit_modal;
|
||||||
pub mod commit_tooltip;
|
pub mod commit_tooltip;
|
||||||
mod commit_view;
|
mod commit_view;
|
||||||
|
mod conflict_view;
|
||||||
pub mod git_panel;
|
pub mod git_panel;
|
||||||
mod git_panel_settings;
|
mod git_panel_settings;
|
||||||
pub mod onboarding;
|
pub mod onboarding;
|
||||||
|
@ -35,6 +37,11 @@ pub fn init(cx: &mut App) {
|
||||||
|
|
||||||
editor::set_blame_renderer(blame_ui::GitBlameRenderer, cx);
|
editor::set_blame_renderer(blame_ui::GitBlameRenderer, cx);
|
||||||
|
|
||||||
|
cx.observe_new(|editor: &mut Editor, _, cx| {
|
||||||
|
conflict_view::register_editor(editor, editor.buffer().clone(), cx);
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
cx.observe_new(|workspace: &mut Workspace, _, cx| {
|
cx.observe_new(|workspace: &mut Workspace, _, cx| {
|
||||||
ProjectDiff::register(workspace, cx);
|
ProjectDiff::register(workspace, cx);
|
||||||
CommitModal::register(workspace);
|
CommitModal::register(workspace);
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
|
conflict_view::ConflictAddon,
|
||||||
git_panel::{GitPanel, GitPanelAddon, GitStatusEntry},
|
git_panel::{GitPanel, GitPanelAddon, GitStatusEntry},
|
||||||
remote_button::{render_publish_button, render_push_button},
|
remote_button::{render_publish_button, render_push_button},
|
||||||
};
|
};
|
||||||
|
@ -26,7 +27,10 @@ use project::{
|
||||||
Project, ProjectPath,
|
Project, ProjectPath,
|
||||||
git_store::{GitStore, GitStoreEvent, RepositoryEvent},
|
git_store::{GitStore, GitStoreEvent, RepositoryEvent},
|
||||||
};
|
};
|
||||||
use std::any::{Any, TypeId};
|
use std::{
|
||||||
|
any::{Any, TypeId},
|
||||||
|
ops::Range,
|
||||||
|
};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::{KeyBinding, Tooltip, prelude::*, vertical_divider};
|
use ui::{KeyBinding, Tooltip, prelude::*, vertical_divider};
|
||||||
use util::ResultExt as _;
|
use util::ResultExt as _;
|
||||||
|
@ -48,7 +52,6 @@ pub struct ProjectDiff {
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
update_needed: postage::watch::Sender<()>,
|
update_needed: postage::watch::Sender<()>,
|
||||||
pending_scroll: Option<PathKey>,
|
pending_scroll: Option<PathKey>,
|
||||||
current_branch: Option<Branch>,
|
|
||||||
_task: Task<Result<()>>,
|
_task: Task<Result<()>>,
|
||||||
_subscription: Subscription,
|
_subscription: Subscription,
|
||||||
}
|
}
|
||||||
|
@ -61,9 +64,9 @@ struct DiffBuffer {
|
||||||
file_status: FileStatus,
|
file_status: FileStatus,
|
||||||
}
|
}
|
||||||
|
|
||||||
const CONFLICT_NAMESPACE: u32 = 0;
|
const CONFLICT_NAMESPACE: u32 = 1;
|
||||||
const TRACKED_NAMESPACE: u32 = 1;
|
const TRACKED_NAMESPACE: u32 = 2;
|
||||||
const NEW_NAMESPACE: u32 = 2;
|
const NEW_NAMESPACE: u32 = 3;
|
||||||
|
|
||||||
impl ProjectDiff {
|
impl ProjectDiff {
|
||||||
pub(crate) fn register(workspace: &mut Workspace, cx: &mut Context<Workspace>) {
|
pub(crate) fn register(workspace: &mut Workspace, cx: &mut Context<Workspace>) {
|
||||||
|
@ -154,7 +157,8 @@ impl ProjectDiff {
|
||||||
window,
|
window,
|
||||||
move |this, _git_store, event, _window, _cx| match event {
|
move |this, _git_store, event, _window, _cx| match event {
|
||||||
GitStoreEvent::ActiveRepositoryChanged(_)
|
GitStoreEvent::ActiveRepositoryChanged(_)
|
||||||
| GitStoreEvent::RepositoryUpdated(_, RepositoryEvent::Updated { .. }, true) => {
|
| GitStoreEvent::RepositoryUpdated(_, RepositoryEvent::Updated { .. }, true)
|
||||||
|
| GitStoreEvent::ConflictsUpdated => {
|
||||||
*this.update_needed.borrow_mut() = ();
|
*this.update_needed.borrow_mut() = ();
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
@ -178,7 +182,6 @@ impl ProjectDiff {
|
||||||
multibuffer,
|
multibuffer,
|
||||||
pending_scroll: None,
|
pending_scroll: None,
|
||||||
update_needed: send,
|
update_needed: send,
|
||||||
current_branch: None,
|
|
||||||
_task: worker,
|
_task: worker,
|
||||||
_subscription: git_store_subscription,
|
_subscription: git_store_subscription,
|
||||||
}
|
}
|
||||||
|
@ -395,11 +398,25 @@ impl ProjectDiff {
|
||||||
let buffer = diff_buffer.buffer;
|
let buffer = diff_buffer.buffer;
|
||||||
let diff = diff_buffer.diff;
|
let diff = diff_buffer.diff;
|
||||||
|
|
||||||
|
let conflict_addon = self
|
||||||
|
.editor
|
||||||
|
.read(cx)
|
||||||
|
.addon::<ConflictAddon>()
|
||||||
|
.expect("project diff editor should have a conflict addon");
|
||||||
|
|
||||||
let snapshot = buffer.read(cx).snapshot();
|
let snapshot = buffer.read(cx).snapshot();
|
||||||
let diff = diff.read(cx);
|
let diff = diff.read(cx);
|
||||||
let diff_hunk_ranges = diff
|
let diff_hunk_ranges = diff
|
||||||
.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx)
|
.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx)
|
||||||
.map(|diff_hunk| diff_hunk.buffer_range.to_point(&snapshot))
|
.map(|diff_hunk| diff_hunk.buffer_range.clone());
|
||||||
|
let conflicts = conflict_addon
|
||||||
|
.conflict_set(snapshot.remote_id())
|
||||||
|
.map(|conflict_set| conflict_set.read(cx).snapshot().conflicts.clone())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let conflicts = conflicts.iter().map(|conflict| conflict.range.clone());
|
||||||
|
|
||||||
|
let excerpt_ranges = merge_anchor_ranges(diff_hunk_ranges, conflicts, &snapshot)
|
||||||
|
.map(|range| range.to_point(&snapshot))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let (was_empty, is_excerpt_newly_added) = self.multibuffer.update(cx, |multibuffer, cx| {
|
let (was_empty, is_excerpt_newly_added) = self.multibuffer.update(cx, |multibuffer, cx| {
|
||||||
|
@ -407,7 +424,7 @@ impl ProjectDiff {
|
||||||
let (_, is_newly_added) = multibuffer.set_excerpts_for_path(
|
let (_, is_newly_added) = multibuffer.set_excerpts_for_path(
|
||||||
path_key.clone(),
|
path_key.clone(),
|
||||||
buffer,
|
buffer,
|
||||||
diff_hunk_ranges,
|
excerpt_ranges,
|
||||||
editor::DEFAULT_MULTIBUFFER_CONTEXT,
|
editor::DEFAULT_MULTIBUFFER_CONTEXT,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
@ -450,18 +467,6 @@ impl ProjectDiff {
|
||||||
cx: &mut AsyncWindowContext,
|
cx: &mut AsyncWindowContext,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
while let Some(_) = recv.next().await {
|
while let Some(_) = recv.next().await {
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
let new_branch = this
|
|
||||||
.git_store
|
|
||||||
.read(cx)
|
|
||||||
.active_repository()
|
|
||||||
.and_then(|active_repository| active_repository.read(cx).branch.clone());
|
|
||||||
if new_branch != this.current_branch {
|
|
||||||
this.current_branch = new_branch;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let buffers_to_load = this.update(cx, |this, cx| this.load_buffers(cx))?;
|
let buffers_to_load = this.update(cx, |this, cx| this.load_buffers(cx))?;
|
||||||
for buffer_to_load in buffers_to_load {
|
for buffer_to_load in buffers_to_load {
|
||||||
if let Some(buffer) = buffer_to_load.await.log_err() {
|
if let Some(buffer) = buffer_to_load.await.log_err() {
|
||||||
|
@ -1127,47 +1132,6 @@ impl RenderOnce for ProjectDiffEmptyState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// .when(self.can_push_and_pull, |this| {
|
|
||||||
// let remote_button = crate::render_remote_button(
|
|
||||||
// "project-diff-remote-button",
|
|
||||||
// &branch,
|
|
||||||
// self.focus_handle.clone(),
|
|
||||||
// false,
|
|
||||||
// );
|
|
||||||
|
|
||||||
// match remote_button {
|
|
||||||
// Some(button) => {
|
|
||||||
// this.child(h_flex().justify_around().child(button))
|
|
||||||
// }
|
|
||||||
// None => this.child(
|
|
||||||
// h_flex()
|
|
||||||
// .justify_around()
|
|
||||||
// .child(Label::new("Remote up to date")),
|
|
||||||
// ),
|
|
||||||
// }
|
|
||||||
// }),
|
|
||||||
//
|
|
||||||
// // .map(|this| {
|
|
||||||
// this.child(h_flex().justify_around().mt_1().child(
|
|
||||||
// Button::new("project-diff-close-button", "Close").when_some(
|
|
||||||
// self.focus_handle.clone(),
|
|
||||||
// |this, focus_handle| {
|
|
||||||
// this.key_binding(KeyBinding::for_action_in(
|
|
||||||
// &CloseActiveItem::default(),
|
|
||||||
// &focus_handle,
|
|
||||||
// window,
|
|
||||||
// cx,
|
|
||||||
// ))
|
|
||||||
// .on_click(move |_, window, cx| {
|
|
||||||
// window.focus(&focus_handle);
|
|
||||||
// window
|
|
||||||
// .dispatch_action(Box::new(CloseActiveItem::default()), cx);
|
|
||||||
// })
|
|
||||||
// },
|
|
||||||
// ),
|
|
||||||
// ))
|
|
||||||
// }),
|
|
||||||
|
|
||||||
mod preview {
|
mod preview {
|
||||||
use git::repository::{
|
use git::repository::{
|
||||||
Branch, CommitSummary, Upstream, UpstreamTracking, UpstreamTrackingStatus,
|
Branch, CommitSummary, Upstream, UpstreamTracking, UpstreamTrackingStatus,
|
||||||
|
@ -1293,6 +1257,53 @@ mod preview {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn merge_anchor_ranges<'a>(
|
||||||
|
left: impl 'a + Iterator<Item = Range<Anchor>>,
|
||||||
|
right: impl 'a + Iterator<Item = Range<Anchor>>,
|
||||||
|
snapshot: &'a language::BufferSnapshot,
|
||||||
|
) -> impl 'a + Iterator<Item = Range<Anchor>> {
|
||||||
|
let mut left = left.fuse().peekable();
|
||||||
|
let mut right = right.fuse().peekable();
|
||||||
|
|
||||||
|
std::iter::from_fn(move || {
|
||||||
|
let Some(left_range) = left.peek() else {
|
||||||
|
return right.next();
|
||||||
|
};
|
||||||
|
let Some(right_range) = right.peek() else {
|
||||||
|
return left.next();
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut next_range = if left_range.start.cmp(&right_range.start, snapshot).is_lt() {
|
||||||
|
left.next().unwrap()
|
||||||
|
} else {
|
||||||
|
right.next().unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Extend the basic range while there's overlap with a range from either stream.
|
||||||
|
loop {
|
||||||
|
if let Some(left_range) = left
|
||||||
|
.peek()
|
||||||
|
.filter(|range| range.start.cmp(&next_range.end, &snapshot).is_le())
|
||||||
|
.cloned()
|
||||||
|
{
|
||||||
|
left.next();
|
||||||
|
next_range.end = left_range.end;
|
||||||
|
} else if let Some(right_range) = right
|
||||||
|
.peek()
|
||||||
|
.filter(|range| range.start.cmp(&next_range.end, &snapshot).is_le())
|
||||||
|
.cloned()
|
||||||
|
{
|
||||||
|
right.next();
|
||||||
|
next_range.end = right_range.end;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(next_range)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
|
@ -2,7 +2,8 @@ pub mod cursor_position;
|
||||||
|
|
||||||
use cursor_position::{LineIndicatorFormat, UserCaretPosition};
|
use cursor_position::{LineIndicatorFormat, UserCaretPosition};
|
||||||
use editor::{
|
use editor::{
|
||||||
Anchor, Editor, MultiBufferSnapshot, ToOffset, ToPoint, actions::Tab, scroll::Autoscroll,
|
Anchor, Editor, MultiBufferSnapshot, RowHighlightOptions, ToOffset, ToPoint, actions::Tab,
|
||||||
|
scroll::Autoscroll,
|
||||||
};
|
};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, SharedString, Styled,
|
App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, SharedString, Styled,
|
||||||
|
@ -180,7 +181,10 @@ impl GoToLine {
|
||||||
editor.highlight_rows::<GoToLineRowHighlights>(
|
editor.highlight_rows::<GoToLineRowHighlights>(
|
||||||
start..end,
|
start..end,
|
||||||
cx.theme().colors().editor_highlighted_line_background,
|
cx.theme().colors().editor_highlighted_line_background,
|
||||||
true,
|
RowHighlightOptions {
|
||||||
|
autoscroll: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
editor.request_autoscroll(Autoscroll::center(), cx);
|
editor.request_autoscroll(Autoscroll::center(), cx);
|
||||||
|
|
|
@ -95,6 +95,7 @@ pub enum Event {
|
||||||
},
|
},
|
||||||
ExcerptsRemoved {
|
ExcerptsRemoved {
|
||||||
ids: Vec<ExcerptId>,
|
ids: Vec<ExcerptId>,
|
||||||
|
removed_buffer_ids: Vec<BufferId>,
|
||||||
},
|
},
|
||||||
ExcerptsExpanded {
|
ExcerptsExpanded {
|
||||||
ids: Vec<ExcerptId>,
|
ids: Vec<ExcerptId>,
|
||||||
|
@ -2021,7 +2022,12 @@ impl MultiBuffer {
|
||||||
pub fn clear(&mut self, cx: &mut Context<Self>) {
|
pub fn clear(&mut self, cx: &mut Context<Self>) {
|
||||||
self.sync(cx);
|
self.sync(cx);
|
||||||
let ids = self.excerpt_ids();
|
let ids = self.excerpt_ids();
|
||||||
self.buffers.borrow_mut().clear();
|
let removed_buffer_ids = self
|
||||||
|
.buffers
|
||||||
|
.borrow_mut()
|
||||||
|
.drain()
|
||||||
|
.map(|(id, _)| id)
|
||||||
|
.collect();
|
||||||
self.excerpts_by_path.clear();
|
self.excerpts_by_path.clear();
|
||||||
self.paths_by_excerpt.clear();
|
self.paths_by_excerpt.clear();
|
||||||
let mut snapshot = self.snapshot.borrow_mut();
|
let mut snapshot = self.snapshot.borrow_mut();
|
||||||
|
@ -2046,7 +2052,10 @@ impl MultiBuffer {
|
||||||
singleton_buffer_edited: false,
|
singleton_buffer_edited: false,
|
||||||
edited_buffer: None,
|
edited_buffer: None,
|
||||||
});
|
});
|
||||||
cx.emit(Event::ExcerptsRemoved { ids });
|
cx.emit(Event::ExcerptsRemoved {
|
||||||
|
ids,
|
||||||
|
removed_buffer_ids,
|
||||||
|
});
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2310,9 +2319,9 @@ impl MultiBuffer {
|
||||||
new_excerpts.append(suffix, &());
|
new_excerpts.append(suffix, &());
|
||||||
drop(cursor);
|
drop(cursor);
|
||||||
snapshot.excerpts = new_excerpts;
|
snapshot.excerpts = new_excerpts;
|
||||||
for buffer_id in removed_buffer_ids {
|
for buffer_id in &removed_buffer_ids {
|
||||||
self.diffs.remove(&buffer_id);
|
self.diffs.remove(buffer_id);
|
||||||
snapshot.diffs.remove(&buffer_id);
|
snapshot.diffs.remove(buffer_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if changed_trailing_excerpt {
|
if changed_trailing_excerpt {
|
||||||
|
@ -2325,7 +2334,10 @@ impl MultiBuffer {
|
||||||
singleton_buffer_edited: false,
|
singleton_buffer_edited: false,
|
||||||
edited_buffer: None,
|
edited_buffer: None,
|
||||||
});
|
});
|
||||||
cx.emit(Event::ExcerptsRemoved { ids });
|
cx.emit(Event::ExcerptsRemoved {
|
||||||
|
ids,
|
||||||
|
removed_buffer_ids,
|
||||||
|
});
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -635,7 +635,7 @@ fn test_excerpt_events(cx: &mut App) {
|
||||||
predecessor,
|
predecessor,
|
||||||
excerpts,
|
excerpts,
|
||||||
} => follower.insert_excerpts_with_ids_after(predecessor, buffer, excerpts, cx),
|
} => follower.insert_excerpts_with_ids_after(predecessor, buffer, excerpts, cx),
|
||||||
Event::ExcerptsRemoved { ids } => follower.remove_excerpts(ids, cx),
|
Event::ExcerptsRemoved { ids, .. } => follower.remove_excerpts(ids, cx),
|
||||||
Event::Edited { .. } => {
|
Event::Edited { .. } => {
|
||||||
*follower_edit_event_count.write() += 1;
|
*follower_edit_event_count.write() += 1;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ use std::{
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use editor::RowHighlightOptions;
|
||||||
use editor::{Anchor, AnchorRangeExt, Editor, scroll::Autoscroll};
|
use editor::{Anchor, AnchorRangeExt, Editor, scroll::Autoscroll};
|
||||||
use fuzzy::StringMatch;
|
use fuzzy::StringMatch;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
|
@ -171,7 +172,10 @@ impl OutlineViewDelegate {
|
||||||
active_editor.highlight_rows::<OutlineRowHighlights>(
|
active_editor.highlight_rows::<OutlineRowHighlights>(
|
||||||
outline_item.range.start..outline_item.range.end,
|
outline_item.range.start..outline_item.range.end,
|
||||||
cx.theme().colors().editor_highlighted_line_background,
|
cx.theme().colors().editor_highlighted_line_background,
|
||||||
true,
|
RowHighlightOptions {
|
||||||
|
autoscroll: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
active_editor.request_autoscroll(Autoscroll::center(), cx);
|
active_editor.request_autoscroll(Autoscroll::center(), cx);
|
||||||
|
|
|
@ -5028,7 +5028,7 @@ fn subscribe_for_editor_events(
|
||||||
.extend(excerpts.iter().map(|&(excerpt_id, _)| excerpt_id));
|
.extend(excerpts.iter().map(|&(excerpt_id, _)| excerpt_id));
|
||||||
outline_panel.update_fs_entries(editor.clone(), debounce, window, cx);
|
outline_panel.update_fs_entries(editor.clone(), debounce, window, cx);
|
||||||
}
|
}
|
||||||
EditorEvent::ExcerptsRemoved { ids } => {
|
EditorEvent::ExcerptsRemoved { ids, .. } => {
|
||||||
let mut ids = ids.iter().collect::<HashSet<_>>();
|
let mut ids = ids.iter().collect::<HashSet<_>>();
|
||||||
for excerpts in outline_panel.excerpts.values_mut() {
|
for excerpts in outline_panel.excerpts.values_mut() {
|
||||||
excerpts.retain(|excerpt_id, _| !ids.remove(excerpt_id));
|
excerpts.retain(|excerpt_id, _| !ids.remove(excerpt_id));
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
mod conflict_set;
|
||||||
pub mod git_traversal;
|
pub mod git_traversal;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -10,11 +11,12 @@ use askpass::AskPassDelegate;
|
||||||
use buffer_diff::{BufferDiff, BufferDiffEvent};
|
use buffer_diff::{BufferDiff, BufferDiffEvent};
|
||||||
use client::ProjectId;
|
use client::ProjectId;
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
|
pub use conflict_set::{ConflictRegion, ConflictSet, ConflictSetSnapshot, ConflictSetUpdate};
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use futures::{
|
use futures::{
|
||||||
FutureExt as _, StreamExt as _,
|
FutureExt, StreamExt as _,
|
||||||
channel::{mpsc, oneshot},
|
channel::{mpsc, oneshot},
|
||||||
future::{self, Shared},
|
future::{self, Shared, try_join_all},
|
||||||
};
|
};
|
||||||
use git::{
|
use git::{
|
||||||
BuildPermalinkParams, GitHostingProviderRegistry, WORK_DIRECTORY_REPO_PATH,
|
BuildPermalinkParams, GitHostingProviderRegistry, WORK_DIRECTORY_REPO_PATH,
|
||||||
|
@ -74,7 +76,7 @@ pub struct GitStore {
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
loading_diffs:
|
loading_diffs:
|
||||||
HashMap<(BufferId, DiffKind), Shared<Task<Result<Entity<BufferDiff>, Arc<anyhow::Error>>>>>,
|
HashMap<(BufferId, DiffKind), Shared<Task<Result<Entity<BufferDiff>, Arc<anyhow::Error>>>>>,
|
||||||
diffs: HashMap<BufferId, Entity<BufferDiffState>>,
|
diffs: HashMap<BufferId, Entity<BufferGitState>>,
|
||||||
shared_diffs: HashMap<proto::PeerId, HashMap<BufferId, SharedDiffs>>,
|
shared_diffs: HashMap<proto::PeerId, HashMap<BufferId, SharedDiffs>>,
|
||||||
_subscriptions: Vec<Subscription>,
|
_subscriptions: Vec<Subscription>,
|
||||||
}
|
}
|
||||||
|
@ -85,12 +87,15 @@ struct SharedDiffs {
|
||||||
uncommitted: Option<Entity<BufferDiff>>,
|
uncommitted: Option<Entity<BufferDiff>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct BufferDiffState {
|
struct BufferGitState {
|
||||||
unstaged_diff: Option<WeakEntity<BufferDiff>>,
|
unstaged_diff: Option<WeakEntity<BufferDiff>>,
|
||||||
uncommitted_diff: Option<WeakEntity<BufferDiff>>,
|
uncommitted_diff: Option<WeakEntity<BufferDiff>>,
|
||||||
|
conflict_set: Option<WeakEntity<ConflictSet>>,
|
||||||
recalculate_diff_task: Option<Task<Result<()>>>,
|
recalculate_diff_task: Option<Task<Result<()>>>,
|
||||||
|
reparse_conflict_markers_task: Option<Task<Result<()>>>,
|
||||||
language: Option<Arc<Language>>,
|
language: Option<Arc<Language>>,
|
||||||
language_registry: Option<Arc<LanguageRegistry>>,
|
language_registry: Option<Arc<LanguageRegistry>>,
|
||||||
|
conflict_updated_futures: Vec<oneshot::Sender<()>>,
|
||||||
recalculating_tx: postage::watch::Sender<bool>,
|
recalculating_tx: postage::watch::Sender<bool>,
|
||||||
|
|
||||||
/// These operation counts are used to ensure that head and index text
|
/// These operation counts are used to ensure that head and index text
|
||||||
|
@ -224,17 +229,26 @@ impl sum_tree::KeyedItem for StatusEntry {
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
pub struct RepositoryId(pub u64);
|
pub struct RepositoryId(pub u64);
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||||
|
pub struct MergeDetails {
|
||||||
|
pub conflicted_paths: TreeSet<RepoPath>,
|
||||||
|
pub message: Option<SharedString>,
|
||||||
|
pub apply_head: Option<CommitDetails>,
|
||||||
|
pub cherry_pick_head: Option<CommitDetails>,
|
||||||
|
pub merge_heads: Vec<CommitDetails>,
|
||||||
|
pub rebase_head: Option<CommitDetails>,
|
||||||
|
pub revert_head: Option<CommitDetails>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct RepositorySnapshot {
|
pub struct RepositorySnapshot {
|
||||||
pub id: RepositoryId,
|
pub id: RepositoryId,
|
||||||
pub merge_message: Option<SharedString>,
|
|
||||||
pub statuses_by_path: SumTree<StatusEntry>,
|
pub statuses_by_path: SumTree<StatusEntry>,
|
||||||
pub work_directory_abs_path: Arc<Path>,
|
pub work_directory_abs_path: Arc<Path>,
|
||||||
pub branch: Option<Branch>,
|
pub branch: Option<Branch>,
|
||||||
pub head_commit: Option<CommitDetails>,
|
pub head_commit: Option<CommitDetails>,
|
||||||
pub merge_conflicts: TreeSet<RepoPath>,
|
|
||||||
pub merge_head_shas: Vec<SharedString>,
|
|
||||||
pub scan_id: u64,
|
pub scan_id: u64,
|
||||||
|
pub merge: MergeDetails,
|
||||||
}
|
}
|
||||||
|
|
||||||
type JobId = u64;
|
type JobId = u64;
|
||||||
|
@ -297,6 +311,7 @@ pub enum GitStoreEvent {
|
||||||
RepositoryRemoved(RepositoryId),
|
RepositoryRemoved(RepositoryId),
|
||||||
IndexWriteError(anyhow::Error),
|
IndexWriteError(anyhow::Error),
|
||||||
JobsUpdated,
|
JobsUpdated,
|
||||||
|
ConflictsUpdated,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventEmitter<RepositoryEvent> for Repository {}
|
impl EventEmitter<RepositoryEvent> for Repository {}
|
||||||
|
@ -681,10 +696,11 @@ impl GitStore {
|
||||||
let text_snapshot = buffer.text_snapshot();
|
let text_snapshot = buffer.text_snapshot();
|
||||||
this.loading_diffs.remove(&(buffer_id, kind));
|
this.loading_diffs.remove(&(buffer_id, kind));
|
||||||
|
|
||||||
|
let git_store = cx.weak_entity();
|
||||||
let diff_state = this
|
let diff_state = this
|
||||||
.diffs
|
.diffs
|
||||||
.entry(buffer_id)
|
.entry(buffer_id)
|
||||||
.or_insert_with(|| cx.new(|_| BufferDiffState::default()));
|
.or_insert_with(|| cx.new(|_| BufferGitState::new(git_store)));
|
||||||
|
|
||||||
let diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx));
|
let diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx));
|
||||||
|
|
||||||
|
@ -737,6 +753,62 @@ impl GitStore {
|
||||||
diff_state.read(cx).uncommitted_diff.as_ref()?.upgrade()
|
diff_state.read(cx).uncommitted_diff.as_ref()?.upgrade()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn open_conflict_set(
|
||||||
|
&mut self,
|
||||||
|
buffer: Entity<Buffer>,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Entity<ConflictSet> {
|
||||||
|
log::debug!("open conflict set");
|
||||||
|
let buffer_id = buffer.read(cx).remote_id();
|
||||||
|
|
||||||
|
if let Some(git_state) = self.diffs.get(&buffer_id) {
|
||||||
|
if let Some(conflict_set) = git_state
|
||||||
|
.read(cx)
|
||||||
|
.conflict_set
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|weak| weak.upgrade())
|
||||||
|
{
|
||||||
|
let conflict_set = conflict_set.clone();
|
||||||
|
let buffer_snapshot = buffer.read(cx).text_snapshot();
|
||||||
|
|
||||||
|
git_state.update(cx, |state, cx| {
|
||||||
|
let _ = state.reparse_conflict_markers(buffer_snapshot, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
return conflict_set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let is_unmerged = self
|
||||||
|
.repository_and_path_for_buffer_id(buffer_id, cx)
|
||||||
|
.map_or(false, |(repo, path)| {
|
||||||
|
repo.read(cx)
|
||||||
|
.snapshot
|
||||||
|
.merge
|
||||||
|
.conflicted_paths
|
||||||
|
.contains(&path)
|
||||||
|
});
|
||||||
|
let git_store = cx.weak_entity();
|
||||||
|
let buffer_git_state = self
|
||||||
|
.diffs
|
||||||
|
.entry(buffer_id)
|
||||||
|
.or_insert_with(|| cx.new(|_| BufferGitState::new(git_store)));
|
||||||
|
let conflict_set = cx.new(|cx| ConflictSet::new(buffer_id, is_unmerged, cx));
|
||||||
|
|
||||||
|
self._subscriptions
|
||||||
|
.push(cx.subscribe(&conflict_set, |_, _, _, cx| {
|
||||||
|
cx.emit(GitStoreEvent::ConflictsUpdated);
|
||||||
|
}));
|
||||||
|
|
||||||
|
buffer_git_state.update(cx, |state, cx| {
|
||||||
|
state.conflict_set = Some(conflict_set.downgrade());
|
||||||
|
let buffer_snapshot = buffer.read(cx).text_snapshot();
|
||||||
|
let _ = state.reparse_conflict_markers(buffer_snapshot, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
conflict_set
|
||||||
|
}
|
||||||
|
|
||||||
pub fn project_path_git_status(
|
pub fn project_path_git_status(
|
||||||
&self,
|
&self,
|
||||||
project_path: &ProjectPath,
|
project_path: &ProjectPath,
|
||||||
|
@ -1079,6 +1151,35 @@ impl GitStore {
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let id = repo.read(cx).id;
|
let id = repo.read(cx).id;
|
||||||
|
let merge_conflicts = repo.read(cx).snapshot.merge.conflicted_paths.clone();
|
||||||
|
for (buffer_id, diff) in self.diffs.iter() {
|
||||||
|
if let Some((buffer_repo, repo_path)) =
|
||||||
|
self.repository_and_path_for_buffer_id(*buffer_id, cx)
|
||||||
|
{
|
||||||
|
if buffer_repo == repo {
|
||||||
|
diff.update(cx, |diff, cx| {
|
||||||
|
if let Some(conflict_set) = &diff.conflict_set {
|
||||||
|
let conflict_status_changed =
|
||||||
|
conflict_set.update(cx, |conflict_set, cx| {
|
||||||
|
let has_conflict = merge_conflicts.contains(&repo_path);
|
||||||
|
conflict_set.set_has_conflict(has_conflict, cx)
|
||||||
|
})?;
|
||||||
|
if conflict_status_changed {
|
||||||
|
let buffer_store = self.buffer_store.read(cx);
|
||||||
|
if let Some(buffer) = buffer_store.get(*buffer_id) {
|
||||||
|
let _ = diff.reparse_conflict_markers(
|
||||||
|
buffer.read(cx).text_snapshot(),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
anyhow::Ok(())
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
cx.emit(GitStoreEvent::RepositoryUpdated(
|
cx.emit(GitStoreEvent::RepositoryUpdated(
|
||||||
id,
|
id,
|
||||||
event.clone(),
|
event.clone(),
|
||||||
|
@ -1218,9 +1319,15 @@ impl GitStore {
|
||||||
if let Some(diff_state) = self.diffs.get_mut(&buffer.read(cx).remote_id()) {
|
if let Some(diff_state) = self.diffs.get_mut(&buffer.read(cx).remote_id()) {
|
||||||
let buffer = buffer.read(cx).text_snapshot();
|
let buffer = buffer.read(cx).text_snapshot();
|
||||||
diff_state.update(cx, |diff_state, cx| {
|
diff_state.update(cx, |diff_state, cx| {
|
||||||
diff_state.recalculate_diffs(buffer, cx);
|
diff_state.recalculate_diffs(buffer.clone(), cx);
|
||||||
futures.extend(diff_state.wait_for_recalculation());
|
futures.extend(diff_state.wait_for_recalculation().map(FutureExt::boxed));
|
||||||
});
|
});
|
||||||
|
futures.push(diff_state.update(cx, |diff_state, cx| {
|
||||||
|
diff_state
|
||||||
|
.reparse_conflict_markers(buffer, cx)
|
||||||
|
.map(|_| {})
|
||||||
|
.boxed()
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async move {
|
async move {
|
||||||
|
@ -2094,13 +2201,86 @@ impl GitStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BufferDiffState {
|
impl BufferGitState {
|
||||||
|
fn new(_git_store: WeakEntity<GitStore>) -> Self {
|
||||||
|
Self {
|
||||||
|
unstaged_diff: Default::default(),
|
||||||
|
uncommitted_diff: Default::default(),
|
||||||
|
recalculate_diff_task: Default::default(),
|
||||||
|
language: Default::default(),
|
||||||
|
language_registry: Default::default(),
|
||||||
|
recalculating_tx: postage::watch::channel_with(false).0,
|
||||||
|
hunk_staging_operation_count: 0,
|
||||||
|
hunk_staging_operation_count_as_of_write: 0,
|
||||||
|
head_text: Default::default(),
|
||||||
|
index_text: Default::default(),
|
||||||
|
head_changed: Default::default(),
|
||||||
|
index_changed: Default::default(),
|
||||||
|
language_changed: Default::default(),
|
||||||
|
conflict_updated_futures: Default::default(),
|
||||||
|
conflict_set: Default::default(),
|
||||||
|
reparse_conflict_markers_task: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn buffer_language_changed(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
|
fn buffer_language_changed(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
|
||||||
self.language = buffer.read(cx).language().cloned();
|
self.language = buffer.read(cx).language().cloned();
|
||||||
self.language_changed = true;
|
self.language_changed = true;
|
||||||
let _ = self.recalculate_diffs(buffer.read(cx).text_snapshot(), cx);
|
let _ = self.recalculate_diffs(buffer.read(cx).text_snapshot(), cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn reparse_conflict_markers(
|
||||||
|
&mut self,
|
||||||
|
buffer: text::BufferSnapshot,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> oneshot::Receiver<()> {
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
|
||||||
|
let Some(conflict_set) = self
|
||||||
|
.conflict_set
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|conflict_set| conflict_set.upgrade())
|
||||||
|
else {
|
||||||
|
return rx;
|
||||||
|
};
|
||||||
|
|
||||||
|
let old_snapshot = conflict_set.read_with(cx, |conflict_set, _| {
|
||||||
|
if conflict_set.has_conflict {
|
||||||
|
Some(conflict_set.snapshot())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(old_snapshot) = old_snapshot {
|
||||||
|
self.conflict_updated_futures.push(tx);
|
||||||
|
self.reparse_conflict_markers_task = Some(cx.spawn(async move |this, cx| {
|
||||||
|
let (snapshot, changed_range) = cx
|
||||||
|
.background_spawn(async move {
|
||||||
|
let new_snapshot = ConflictSet::parse(&buffer);
|
||||||
|
let changed_range = old_snapshot.compare(&new_snapshot, &buffer);
|
||||||
|
(new_snapshot, changed_range)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
if let Some(conflict_set) = &this.conflict_set {
|
||||||
|
conflict_set
|
||||||
|
.update(cx, |conflict_set, cx| {
|
||||||
|
conflict_set.set_snapshot(snapshot, changed_range, cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
let futures = std::mem::take(&mut this.conflict_updated_futures);
|
||||||
|
for tx in futures {
|
||||||
|
tx.send(()).ok();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
rx
|
||||||
|
}
|
||||||
|
|
||||||
fn unstaged_diff(&self) -> Option<Entity<BufferDiff>> {
|
fn unstaged_diff(&self) -> Option<Entity<BufferDiff>> {
|
||||||
self.unstaged_diff.as_ref().and_then(|set| set.upgrade())
|
self.unstaged_diff.as_ref().and_then(|set| set.upgrade())
|
||||||
}
|
}
|
||||||
|
@ -2335,26 +2515,6 @@ impl BufferDiffState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for BufferDiffState {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
unstaged_diff: Default::default(),
|
|
||||||
uncommitted_diff: Default::default(),
|
|
||||||
recalculate_diff_task: Default::default(),
|
|
||||||
language: Default::default(),
|
|
||||||
language_registry: Default::default(),
|
|
||||||
recalculating_tx: postage::watch::channel_with(false).0,
|
|
||||||
hunk_staging_operation_count: 0,
|
|
||||||
hunk_staging_operation_count_as_of_write: 0,
|
|
||||||
head_text: Default::default(),
|
|
||||||
index_text: Default::default(),
|
|
||||||
head_changed: Default::default(),
|
|
||||||
index_changed: Default::default(),
|
|
||||||
language_changed: Default::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn make_remote_delegate(
|
fn make_remote_delegate(
|
||||||
this: Entity<GitStore>,
|
this: Entity<GitStore>,
|
||||||
project_id: u64,
|
project_id: u64,
|
||||||
|
@ -2397,14 +2557,12 @@ impl RepositorySnapshot {
|
||||||
fn empty(id: RepositoryId, work_directory_abs_path: Arc<Path>) -> Self {
|
fn empty(id: RepositoryId, work_directory_abs_path: Arc<Path>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
merge_message: None,
|
|
||||||
statuses_by_path: Default::default(),
|
statuses_by_path: Default::default(),
|
||||||
work_directory_abs_path,
|
work_directory_abs_path,
|
||||||
branch: None,
|
branch: None,
|
||||||
head_commit: None,
|
head_commit: None,
|
||||||
merge_conflicts: Default::default(),
|
|
||||||
merge_head_shas: Default::default(),
|
|
||||||
scan_id: 0,
|
scan_id: 0,
|
||||||
|
merge: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2419,7 +2577,8 @@ impl RepositorySnapshot {
|
||||||
.collect(),
|
.collect(),
|
||||||
removed_statuses: Default::default(),
|
removed_statuses: Default::default(),
|
||||||
current_merge_conflicts: self
|
current_merge_conflicts: self
|
||||||
.merge_conflicts
|
.merge
|
||||||
|
.conflicted_paths
|
||||||
.iter()
|
.iter()
|
||||||
.map(|repo_path| repo_path.to_proto())
|
.map(|repo_path| repo_path.to_proto())
|
||||||
.collect(),
|
.collect(),
|
||||||
|
@ -2480,7 +2639,8 @@ impl RepositorySnapshot {
|
||||||
updated_statuses,
|
updated_statuses,
|
||||||
removed_statuses,
|
removed_statuses,
|
||||||
current_merge_conflicts: self
|
current_merge_conflicts: self
|
||||||
.merge_conflicts
|
.merge
|
||||||
|
.conflicted_paths
|
||||||
.iter()
|
.iter()
|
||||||
.map(|path| path.as_ref().to_proto())
|
.map(|path| path.as_ref().to_proto())
|
||||||
.collect(),
|
.collect(),
|
||||||
|
@ -2515,7 +2675,7 @@ impl RepositorySnapshot {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn has_conflict(&self, repo_path: &RepoPath) -> bool {
|
pub fn has_conflict(&self, repo_path: &RepoPath) -> bool {
|
||||||
self.merge_conflicts.contains(repo_path)
|
self.merge.conflicted_paths.contains(repo_path)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This is the name that will be displayed in the repository selector for this repository.
|
/// This is the name that will be displayed in the repository selector for this repository.
|
||||||
|
@ -2529,7 +2689,77 @@ impl RepositorySnapshot {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl MergeDetails {
|
||||||
|
async fn load(
|
||||||
|
backend: &Arc<dyn GitRepository>,
|
||||||
|
status: &SumTree<StatusEntry>,
|
||||||
|
prev_snapshot: &RepositorySnapshot,
|
||||||
|
) -> Result<(MergeDetails, bool)> {
|
||||||
|
fn sha_eq<'a>(
|
||||||
|
l: impl IntoIterator<Item = &'a CommitDetails>,
|
||||||
|
r: impl IntoIterator<Item = &'a CommitDetails>,
|
||||||
|
) -> bool {
|
||||||
|
l.into_iter()
|
||||||
|
.map(|commit| &commit.sha)
|
||||||
|
.eq(r.into_iter().map(|commit| &commit.sha))
|
||||||
|
}
|
||||||
|
|
||||||
|
let merge_heads = try_join_all(
|
||||||
|
backend
|
||||||
|
.merge_head_shas()
|
||||||
|
.into_iter()
|
||||||
|
.map(|sha| backend.show(sha)),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let cherry_pick_head = backend.show("CHERRY_PICK_HEAD".into()).await.ok();
|
||||||
|
let rebase_head = backend.show("REBASE_HEAD".into()).await.ok();
|
||||||
|
let revert_head = backend.show("REVERT_HEAD".into()).await.ok();
|
||||||
|
let apply_head = backend.show("APPLY_HEAD".into()).await.ok();
|
||||||
|
let message = backend.merge_message().await.map(SharedString::from);
|
||||||
|
let merge_heads_changed = !sha_eq(
|
||||||
|
merge_heads.as_slice(),
|
||||||
|
prev_snapshot.merge.merge_heads.as_slice(),
|
||||||
|
) || !sha_eq(
|
||||||
|
cherry_pick_head.as_ref(),
|
||||||
|
prev_snapshot.merge.cherry_pick_head.as_ref(),
|
||||||
|
) || !sha_eq(
|
||||||
|
apply_head.as_ref(),
|
||||||
|
prev_snapshot.merge.apply_head.as_ref(),
|
||||||
|
) || !sha_eq(
|
||||||
|
rebase_head.as_ref(),
|
||||||
|
prev_snapshot.merge.rebase_head.as_ref(),
|
||||||
|
) || !sha_eq(
|
||||||
|
revert_head.as_ref(),
|
||||||
|
prev_snapshot.merge.revert_head.as_ref(),
|
||||||
|
);
|
||||||
|
let conflicted_paths = if merge_heads_changed {
|
||||||
|
TreeSet::from_ordered_entries(
|
||||||
|
status
|
||||||
|
.iter()
|
||||||
|
.filter(|entry| entry.status.is_conflicted())
|
||||||
|
.map(|entry| entry.repo_path.clone()),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
prev_snapshot.merge.conflicted_paths.clone()
|
||||||
|
};
|
||||||
|
let details = MergeDetails {
|
||||||
|
conflicted_paths,
|
||||||
|
message,
|
||||||
|
apply_head,
|
||||||
|
cherry_pick_head,
|
||||||
|
merge_heads,
|
||||||
|
rebase_head,
|
||||||
|
revert_head,
|
||||||
|
};
|
||||||
|
Ok((details, merge_heads_changed))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Repository {
|
impl Repository {
|
||||||
|
pub fn snapshot(&self) -> RepositorySnapshot {
|
||||||
|
self.snapshot.clone()
|
||||||
|
}
|
||||||
|
|
||||||
fn local(
|
fn local(
|
||||||
id: RepositoryId,
|
id: RepositoryId,
|
||||||
work_directory_abs_path: Arc<Path>,
|
work_directory_abs_path: Arc<Path>,
|
||||||
|
@ -3731,7 +3961,7 @@ impl Repository {
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(proto_to_commit_details);
|
.map(proto_to_commit_details);
|
||||||
|
|
||||||
self.snapshot.merge_conflicts = conflicted_paths;
|
self.snapshot.merge.conflicted_paths = conflicted_paths;
|
||||||
|
|
||||||
let edits = update
|
let edits = update
|
||||||
.removed_statuses
|
.removed_statuses
|
||||||
|
@ -4321,16 +4551,6 @@ async fn compute_snapshot(
|
||||||
let branches = backend.branches().await?;
|
let branches = backend.branches().await?;
|
||||||
let branch = branches.into_iter().find(|branch| branch.is_head);
|
let branch = branches.into_iter().find(|branch| branch.is_head);
|
||||||
let statuses = backend.status(&[WORK_DIRECTORY_REPO_PATH.clone()]).await?;
|
let statuses = backend.status(&[WORK_DIRECTORY_REPO_PATH.clone()]).await?;
|
||||||
let merge_message = backend
|
|
||||||
.merge_message()
|
|
||||||
.await
|
|
||||||
.and_then(|msg| Some(msg.lines().nth(0)?.to_owned().into()));
|
|
||||||
let merge_head_shas = backend
|
|
||||||
.merge_head_shas()
|
|
||||||
.into_iter()
|
|
||||||
.map(SharedString::from)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let statuses_by_path = SumTree::from_iter(
|
let statuses_by_path = SumTree::from_iter(
|
||||||
statuses
|
statuses
|
||||||
.entries
|
.entries
|
||||||
|
@ -4341,47 +4561,36 @@ async fn compute_snapshot(
|
||||||
}),
|
}),
|
||||||
&(),
|
&(),
|
||||||
);
|
);
|
||||||
|
let (merge_details, merge_heads_changed) =
|
||||||
|
MergeDetails::load(&backend, &statuses_by_path, &prev_snapshot).await?;
|
||||||
|
|
||||||
let merge_head_shas_changed = merge_head_shas != prev_snapshot.merge_head_shas;
|
if merge_heads_changed
|
||||||
|
|
||||||
if merge_head_shas_changed
|
|
||||||
|| branch != prev_snapshot.branch
|
|| branch != prev_snapshot.branch
|
||||||
|| statuses_by_path != prev_snapshot.statuses_by_path
|
|| statuses_by_path != prev_snapshot.statuses_by_path
|
||||||
{
|
{
|
||||||
events.push(RepositoryEvent::Updated { full_scan: true });
|
events.push(RepositoryEvent::Updated { full_scan: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut current_merge_conflicts = TreeSet::default();
|
|
||||||
for (repo_path, status) in statuses.entries.iter() {
|
|
||||||
if status.is_conflicted() {
|
|
||||||
current_merge_conflicts.insert(repo_path.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cache merge conflict paths so they don't change from staging/unstaging,
|
// Cache merge conflict paths so they don't change from staging/unstaging,
|
||||||
// until the merge heads change (at commit time, etc.).
|
// until the merge heads change (at commit time, etc.).
|
||||||
let mut merge_conflicts = prev_snapshot.merge_conflicts.clone();
|
if merge_heads_changed {
|
||||||
if merge_head_shas_changed {
|
|
||||||
merge_conflicts = current_merge_conflicts;
|
|
||||||
events.push(RepositoryEvent::MergeHeadsChanged);
|
events.push(RepositoryEvent::MergeHeadsChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Useful when branch is None in detached head state
|
// Useful when branch is None in detached head state
|
||||||
let head_commit = match backend.head_sha() {
|
let head_commit = match backend.head_sha() {
|
||||||
Some(head_sha) => backend.show(head_sha).await.ok(),
|
Some(head_sha) => backend.show(head_sha).await.log_err(),
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let snapshot = RepositorySnapshot {
|
let snapshot = RepositorySnapshot {
|
||||||
id,
|
id,
|
||||||
merge_message,
|
|
||||||
statuses_by_path,
|
statuses_by_path,
|
||||||
work_directory_abs_path,
|
work_directory_abs_path,
|
||||||
scan_id: prev_snapshot.scan_id + 1,
|
scan_id: prev_snapshot.scan_id + 1,
|
||||||
branch,
|
branch,
|
||||||
head_commit,
|
head_commit,
|
||||||
merge_conflicts,
|
merge: merge_details,
|
||||||
merge_head_shas,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok((snapshot, events))
|
Ok((snapshot, events))
|
||||||
|
|
560
crates/project/src/git_store/conflict_set.rs
Normal file
560
crates/project/src/git_store/conflict_set.rs
Normal file
|
@ -0,0 +1,560 @@
|
||||||
|
use gpui::{App, Context, Entity, EventEmitter};
|
||||||
|
use std::{cmp::Ordering, ops::Range, sync::Arc};
|
||||||
|
use text::{Anchor, BufferId, OffsetRangeExt as _};
|
||||||
|
|
||||||
|
pub struct ConflictSet {
|
||||||
|
pub has_conflict: bool,
|
||||||
|
pub snapshot: ConflictSetSnapshot,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct ConflictSetUpdate {
|
||||||
|
pub buffer_range: Option<Range<Anchor>>,
|
||||||
|
pub old_range: Range<usize>,
|
||||||
|
pub new_range: Range<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ConflictSetSnapshot {
|
||||||
|
pub buffer_id: BufferId,
|
||||||
|
pub conflicts: Arc<[ConflictRegion]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConflictSetSnapshot {
|
||||||
|
pub fn conflicts_in_range(
|
||||||
|
&self,
|
||||||
|
range: Range<Anchor>,
|
||||||
|
buffer: &text::BufferSnapshot,
|
||||||
|
) -> &[ConflictRegion] {
|
||||||
|
let start_ix = self
|
||||||
|
.conflicts
|
||||||
|
.binary_search_by(|conflict| {
|
||||||
|
conflict
|
||||||
|
.range
|
||||||
|
.end
|
||||||
|
.cmp(&range.start, buffer)
|
||||||
|
.then(Ordering::Greater)
|
||||||
|
})
|
||||||
|
.unwrap_err();
|
||||||
|
let end_ix = start_ix
|
||||||
|
+ self.conflicts[start_ix..]
|
||||||
|
.binary_search_by(|conflict| {
|
||||||
|
conflict
|
||||||
|
.range
|
||||||
|
.start
|
||||||
|
.cmp(&range.end, buffer)
|
||||||
|
.then(Ordering::Less)
|
||||||
|
})
|
||||||
|
.unwrap_err();
|
||||||
|
&self.conflicts[start_ix..end_ix]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn compare(&self, other: &Self, buffer: &text::BufferSnapshot) -> ConflictSetUpdate {
|
||||||
|
let common_prefix_len = self
|
||||||
|
.conflicts
|
||||||
|
.iter()
|
||||||
|
.zip(other.conflicts.iter())
|
||||||
|
.take_while(|(old, new)| old == new)
|
||||||
|
.count();
|
||||||
|
let common_suffix_len = self.conflicts[common_prefix_len..]
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.zip(other.conflicts[common_prefix_len..].iter().rev())
|
||||||
|
.take_while(|(old, new)| old == new)
|
||||||
|
.count();
|
||||||
|
let old_conflicts =
|
||||||
|
&self.conflicts[common_prefix_len..(self.conflicts.len() - common_suffix_len)];
|
||||||
|
let new_conflicts =
|
||||||
|
&other.conflicts[common_prefix_len..(other.conflicts.len() - common_suffix_len)];
|
||||||
|
let old_range = common_prefix_len..(common_prefix_len + old_conflicts.len());
|
||||||
|
let new_range = common_prefix_len..(common_prefix_len + new_conflicts.len());
|
||||||
|
let start = match (old_conflicts.first(), new_conflicts.first()) {
|
||||||
|
(None, None) => None,
|
||||||
|
(None, Some(conflict)) => Some(conflict.range.start),
|
||||||
|
(Some(conflict), None) => Some(conflict.range.start),
|
||||||
|
(Some(first), Some(second)) => Some(first.range.start.min(&second.range.start, buffer)),
|
||||||
|
};
|
||||||
|
let end = match (old_conflicts.last(), new_conflicts.last()) {
|
||||||
|
(None, None) => None,
|
||||||
|
(None, Some(conflict)) => Some(conflict.range.end),
|
||||||
|
(Some(first), None) => Some(first.range.end),
|
||||||
|
(Some(first), Some(second)) => Some(first.range.end.max(&second.range.end, buffer)),
|
||||||
|
};
|
||||||
|
ConflictSetUpdate {
|
||||||
|
buffer_range: start.zip(end).map(|(start, end)| start..end),
|
||||||
|
old_range,
|
||||||
|
new_range,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ConflictRegion {
|
||||||
|
pub range: Range<Anchor>,
|
||||||
|
pub ours: Range<Anchor>,
|
||||||
|
pub theirs: Range<Anchor>,
|
||||||
|
pub base: Option<Range<Anchor>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConflictRegion {
|
||||||
|
pub fn resolve(
|
||||||
|
&self,
|
||||||
|
buffer: Entity<language::Buffer>,
|
||||||
|
ranges: &[Range<Anchor>],
|
||||||
|
cx: &mut App,
|
||||||
|
) {
|
||||||
|
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||||
|
let mut deletions = Vec::new();
|
||||||
|
let empty = "";
|
||||||
|
let outer_range = self.range.to_offset(&buffer_snapshot);
|
||||||
|
let mut offset = outer_range.start;
|
||||||
|
for kept_range in ranges {
|
||||||
|
let kept_range = kept_range.to_offset(&buffer_snapshot);
|
||||||
|
if kept_range.start > offset {
|
||||||
|
deletions.push((offset..kept_range.start, empty));
|
||||||
|
}
|
||||||
|
offset = kept_range.end;
|
||||||
|
}
|
||||||
|
if outer_range.end > offset {
|
||||||
|
deletions.push((offset..outer_range.end, empty));
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.update(cx, |buffer, cx| {
|
||||||
|
buffer.edit(deletions, None, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConflictSet {
|
||||||
|
pub fn new(buffer_id: BufferId, has_conflict: bool, _: &mut Context<Self>) -> Self {
|
||||||
|
Self {
|
||||||
|
has_conflict,
|
||||||
|
snapshot: ConflictSetSnapshot {
|
||||||
|
buffer_id,
|
||||||
|
conflicts: Default::default(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_has_conflict(&mut self, has_conflict: bool, cx: &mut Context<Self>) -> bool {
|
||||||
|
if has_conflict != self.has_conflict {
|
||||||
|
self.has_conflict = has_conflict;
|
||||||
|
if !self.has_conflict {
|
||||||
|
cx.emit(ConflictSetUpdate {
|
||||||
|
buffer_range: None,
|
||||||
|
old_range: 0..self.snapshot.conflicts.len(),
|
||||||
|
new_range: 0..0,
|
||||||
|
});
|
||||||
|
self.snapshot.conflicts = Default::default();
|
||||||
|
}
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn snapshot(&self) -> ConflictSetSnapshot {
|
||||||
|
self.snapshot.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_snapshot(
|
||||||
|
&mut self,
|
||||||
|
snapshot: ConflictSetSnapshot,
|
||||||
|
update: ConflictSetUpdate,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.snapshot = snapshot;
|
||||||
|
cx.emit(update);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(buffer: &text::BufferSnapshot) -> ConflictSetSnapshot {
|
||||||
|
let mut conflicts = Vec::new();
|
||||||
|
|
||||||
|
let mut line_pos = 0;
|
||||||
|
let mut lines = buffer.text_for_range(0..buffer.len()).lines();
|
||||||
|
|
||||||
|
let mut conflict_start: Option<usize> = None;
|
||||||
|
let mut ours_start: Option<usize> = None;
|
||||||
|
let mut ours_end: Option<usize> = None;
|
||||||
|
let mut base_start: Option<usize> = None;
|
||||||
|
let mut base_end: Option<usize> = None;
|
||||||
|
let mut theirs_start: Option<usize> = None;
|
||||||
|
|
||||||
|
while let Some(line) = lines.next() {
|
||||||
|
let line_end = line_pos + line.len();
|
||||||
|
|
||||||
|
if line.starts_with("<<<<<<< ") {
|
||||||
|
// If we see a new conflict marker while already parsing one,
|
||||||
|
// abandon the previous one and start a new one
|
||||||
|
conflict_start = Some(line_pos);
|
||||||
|
ours_start = Some(line_end + 1);
|
||||||
|
} else if line.starts_with("||||||| ")
|
||||||
|
&& conflict_start.is_some()
|
||||||
|
&& ours_start.is_some()
|
||||||
|
{
|
||||||
|
ours_end = Some(line_pos);
|
||||||
|
base_start = Some(line_end + 1);
|
||||||
|
} else if line.starts_with("=======")
|
||||||
|
&& conflict_start.is_some()
|
||||||
|
&& ours_start.is_some()
|
||||||
|
{
|
||||||
|
// Set ours_end if not already set (would be set if we have base markers)
|
||||||
|
if ours_end.is_none() {
|
||||||
|
ours_end = Some(line_pos);
|
||||||
|
} else if base_start.is_some() {
|
||||||
|
base_end = Some(line_pos);
|
||||||
|
}
|
||||||
|
theirs_start = Some(line_end + 1);
|
||||||
|
} else if line.starts_with(">>>>>>> ")
|
||||||
|
&& conflict_start.is_some()
|
||||||
|
&& ours_start.is_some()
|
||||||
|
&& ours_end.is_some()
|
||||||
|
&& theirs_start.is_some()
|
||||||
|
{
|
||||||
|
let theirs_end = line_pos;
|
||||||
|
let conflict_end = line_end + 1;
|
||||||
|
|
||||||
|
let range = buffer.anchor_after(conflict_start.unwrap())
|
||||||
|
..buffer.anchor_before(conflict_end);
|
||||||
|
let ours = buffer.anchor_after(ours_start.unwrap())
|
||||||
|
..buffer.anchor_before(ours_end.unwrap());
|
||||||
|
let theirs =
|
||||||
|
buffer.anchor_after(theirs_start.unwrap())..buffer.anchor_before(theirs_end);
|
||||||
|
|
||||||
|
let base = base_start
|
||||||
|
.zip(base_end)
|
||||||
|
.map(|(start, end)| buffer.anchor_after(start)..buffer.anchor_before(end));
|
||||||
|
|
||||||
|
conflicts.push(ConflictRegion {
|
||||||
|
range,
|
||||||
|
ours,
|
||||||
|
theirs,
|
||||||
|
base,
|
||||||
|
});
|
||||||
|
|
||||||
|
conflict_start = None;
|
||||||
|
ours_start = None;
|
||||||
|
ours_end = None;
|
||||||
|
base_start = None;
|
||||||
|
base_end = None;
|
||||||
|
theirs_start = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
line_pos = line_end + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ConflictSetSnapshot {
|
||||||
|
conflicts: conflicts.into(),
|
||||||
|
buffer_id: buffer.remote_id(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<ConflictSetUpdate> for ConflictSet {}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::sync::mpsc;
|
||||||
|
|
||||||
|
use crate::{Project, project_settings::ProjectSettings};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use fs::FakeFs;
|
||||||
|
use git::status::{UnmergedStatus, UnmergedStatusCode};
|
||||||
|
use gpui::{BackgroundExecutor, TestAppContext};
|
||||||
|
use language::language_settings::AllLanguageSettings;
|
||||||
|
use serde_json::json;
|
||||||
|
use settings::Settings as _;
|
||||||
|
use text::{Buffer, BufferId, ToOffset as _};
|
||||||
|
use unindent::Unindent as _;
|
||||||
|
use util::path;
|
||||||
|
use worktree::WorktreeSettings;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_conflicts_in_buffer() {
|
||||||
|
// Create a buffer with conflict markers
|
||||||
|
let test_content = r#"
|
||||||
|
This is some text before the conflict.
|
||||||
|
<<<<<<< HEAD
|
||||||
|
This is our version
|
||||||
|
=======
|
||||||
|
This is their version
|
||||||
|
>>>>>>> branch-name
|
||||||
|
|
||||||
|
Another conflict:
|
||||||
|
<<<<<<< HEAD
|
||||||
|
Our second change
|
||||||
|
||||||| merged common ancestors
|
||||||
|
Original content
|
||||||
|
=======
|
||||||
|
Their second change
|
||||||
|
>>>>>>> branch-name
|
||||||
|
"#
|
||||||
|
.unindent();
|
||||||
|
|
||||||
|
let buffer_id = BufferId::new(1).unwrap();
|
||||||
|
let buffer = Buffer::new(0, buffer_id, test_content);
|
||||||
|
let snapshot = buffer.snapshot();
|
||||||
|
|
||||||
|
let conflict_snapshot = ConflictSet::parse(&snapshot);
|
||||||
|
assert_eq!(conflict_snapshot.conflicts.len(), 2);
|
||||||
|
|
||||||
|
let first = &conflict_snapshot.conflicts[0];
|
||||||
|
assert!(first.base.is_none());
|
||||||
|
let our_text = snapshot
|
||||||
|
.text_for_range(first.ours.clone())
|
||||||
|
.collect::<String>();
|
||||||
|
let their_text = snapshot
|
||||||
|
.text_for_range(first.theirs.clone())
|
||||||
|
.collect::<String>();
|
||||||
|
assert_eq!(our_text, "This is our version\n");
|
||||||
|
assert_eq!(their_text, "This is their version\n");
|
||||||
|
|
||||||
|
let second = &conflict_snapshot.conflicts[1];
|
||||||
|
assert!(second.base.is_some());
|
||||||
|
let our_text = snapshot
|
||||||
|
.text_for_range(second.ours.clone())
|
||||||
|
.collect::<String>();
|
||||||
|
let their_text = snapshot
|
||||||
|
.text_for_range(second.theirs.clone())
|
||||||
|
.collect::<String>();
|
||||||
|
let base_text = snapshot
|
||||||
|
.text_for_range(second.base.as_ref().unwrap().clone())
|
||||||
|
.collect::<String>();
|
||||||
|
assert_eq!(our_text, "Our second change\n");
|
||||||
|
assert_eq!(their_text, "Their second change\n");
|
||||||
|
assert_eq!(base_text, "Original content\n");
|
||||||
|
|
||||||
|
// Test conflicts_in_range
|
||||||
|
let range = snapshot.anchor_before(0)..snapshot.anchor_before(snapshot.len());
|
||||||
|
let conflicts_in_range = conflict_snapshot.conflicts_in_range(range, &snapshot);
|
||||||
|
assert_eq!(conflicts_in_range.len(), 2);
|
||||||
|
|
||||||
|
// Test with a range that includes only the first conflict
|
||||||
|
let first_conflict_end = conflict_snapshot.conflicts[0].range.end;
|
||||||
|
let range = snapshot.anchor_before(0)..first_conflict_end;
|
||||||
|
let conflicts_in_range = conflict_snapshot.conflicts_in_range(range, &snapshot);
|
||||||
|
assert_eq!(conflicts_in_range.len(), 1);
|
||||||
|
|
||||||
|
// Test with a range that includes only the second conflict
|
||||||
|
let second_conflict_start = conflict_snapshot.conflicts[1].range.start;
|
||||||
|
let range = second_conflict_start..snapshot.anchor_before(snapshot.len());
|
||||||
|
let conflicts_in_range = conflict_snapshot.conflicts_in_range(range, &snapshot);
|
||||||
|
assert_eq!(conflicts_in_range.len(), 1);
|
||||||
|
|
||||||
|
// Test with a range that doesn't include any conflicts
|
||||||
|
let range = buffer.anchor_after(first_conflict_end.to_offset(&buffer) + 1)
|
||||||
|
..buffer.anchor_before(second_conflict_start.to_offset(&buffer) - 1);
|
||||||
|
let conflicts_in_range = conflict_snapshot.conflicts_in_range(range, &snapshot);
|
||||||
|
assert_eq!(conflicts_in_range.len(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_nested_conflict_markers() {
|
||||||
|
// Create a buffer with nested conflict markers
|
||||||
|
let test_content = r#"
|
||||||
|
This is some text before the conflict.
|
||||||
|
<<<<<<< HEAD
|
||||||
|
This is our version
|
||||||
|
<<<<<<< HEAD
|
||||||
|
This is a nested conflict marker
|
||||||
|
=======
|
||||||
|
This is their version in a nested conflict
|
||||||
|
>>>>>>> branch-nested
|
||||||
|
=======
|
||||||
|
This is their version
|
||||||
|
>>>>>>> branch-name
|
||||||
|
"#
|
||||||
|
.unindent();
|
||||||
|
|
||||||
|
let buffer_id = BufferId::new(1).unwrap();
|
||||||
|
let buffer = Buffer::new(0, buffer_id, test_content.to_string());
|
||||||
|
let snapshot = buffer.snapshot();
|
||||||
|
|
||||||
|
let conflict_snapshot = ConflictSet::parse(&snapshot);
|
||||||
|
|
||||||
|
assert_eq!(conflict_snapshot.conflicts.len(), 1);
|
||||||
|
|
||||||
|
// The conflict should have our version, their version, but no base
|
||||||
|
let conflict = &conflict_snapshot.conflicts[0];
|
||||||
|
assert!(conflict.base.is_none());
|
||||||
|
|
||||||
|
// Check that the nested conflict was detected correctly
|
||||||
|
let our_text = snapshot
|
||||||
|
.text_for_range(conflict.ours.clone())
|
||||||
|
.collect::<String>();
|
||||||
|
assert_eq!(our_text, "This is a nested conflict marker\n");
|
||||||
|
let their_text = snapshot
|
||||||
|
.text_for_range(conflict.theirs.clone())
|
||||||
|
.collect::<String>();
|
||||||
|
assert_eq!(their_text, "This is their version in a nested conflict\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_conflicts_in_range() {
|
||||||
|
// Create a buffer with conflict markers
|
||||||
|
let test_content = r#"
|
||||||
|
one
|
||||||
|
<<<<<<< HEAD1
|
||||||
|
two
|
||||||
|
=======
|
||||||
|
three
|
||||||
|
>>>>>>> branch1
|
||||||
|
four
|
||||||
|
five
|
||||||
|
<<<<<<< HEAD2
|
||||||
|
six
|
||||||
|
=======
|
||||||
|
seven
|
||||||
|
>>>>>>> branch2
|
||||||
|
eight
|
||||||
|
nine
|
||||||
|
<<<<<<< HEAD3
|
||||||
|
ten
|
||||||
|
=======
|
||||||
|
eleven
|
||||||
|
>>>>>>> branch3
|
||||||
|
twelve
|
||||||
|
<<<<<<< HEAD4
|
||||||
|
thirteen
|
||||||
|
=======
|
||||||
|
fourteen
|
||||||
|
>>>>>>> branch4
|
||||||
|
fifteen
|
||||||
|
"#
|
||||||
|
.unindent();
|
||||||
|
|
||||||
|
let buffer_id = BufferId::new(1).unwrap();
|
||||||
|
let buffer = Buffer::new(0, buffer_id, test_content.clone());
|
||||||
|
let snapshot = buffer.snapshot();
|
||||||
|
|
||||||
|
let conflict_snapshot = ConflictSet::parse(&snapshot);
|
||||||
|
assert_eq!(conflict_snapshot.conflicts.len(), 4);
|
||||||
|
|
||||||
|
let range = test_content.find("seven").unwrap()..test_content.find("eleven").unwrap();
|
||||||
|
let range = buffer.anchor_before(range.start)..buffer.anchor_after(range.end);
|
||||||
|
assert_eq!(
|
||||||
|
conflict_snapshot.conflicts_in_range(range, &snapshot),
|
||||||
|
&conflict_snapshot.conflicts[1..=2]
|
||||||
|
);
|
||||||
|
|
||||||
|
let range = test_content.find("one").unwrap()..test_content.find("<<<<<<< HEAD2").unwrap();
|
||||||
|
let range = buffer.anchor_before(range.start)..buffer.anchor_after(range.end);
|
||||||
|
assert_eq!(
|
||||||
|
conflict_snapshot.conflicts_in_range(range, &snapshot),
|
||||||
|
&conflict_snapshot.conflicts[0..=1]
|
||||||
|
);
|
||||||
|
|
||||||
|
let range =
|
||||||
|
test_content.find("eight").unwrap() - 1..test_content.find(">>>>>>> branch3").unwrap();
|
||||||
|
let range = buffer.anchor_before(range.start)..buffer.anchor_after(range.end);
|
||||||
|
assert_eq!(
|
||||||
|
conflict_snapshot.conflicts_in_range(range, &snapshot),
|
||||||
|
&conflict_snapshot.conflicts[1..=2]
|
||||||
|
);
|
||||||
|
|
||||||
|
let range = test_content.find("thirteen").unwrap() - 1..test_content.len();
|
||||||
|
let range = buffer.anchor_before(range.start)..buffer.anchor_after(range.end);
|
||||||
|
assert_eq!(
|
||||||
|
conflict_snapshot.conflicts_in_range(range, &snapshot),
|
||||||
|
&conflict_snapshot.conflicts[3..=3]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_conflict_updates(executor: BackgroundExecutor, cx: &mut TestAppContext) {
|
||||||
|
env_logger::try_init().ok();
|
||||||
|
cx.update(|cx| {
|
||||||
|
settings::init(cx);
|
||||||
|
WorktreeSettings::register(cx);
|
||||||
|
ProjectSettings::register(cx);
|
||||||
|
AllLanguageSettings::register(cx);
|
||||||
|
});
|
||||||
|
let initial_text = "
|
||||||
|
one
|
||||||
|
two
|
||||||
|
three
|
||||||
|
four
|
||||||
|
five
|
||||||
|
"
|
||||||
|
.unindent();
|
||||||
|
let fs = FakeFs::new(executor);
|
||||||
|
fs.insert_tree(
|
||||||
|
path!("/project"),
|
||||||
|
json!({
|
||||||
|
".git": {},
|
||||||
|
"a.txt": initial_text,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
|
||||||
|
let (git_store, buffer) = project.update(cx, |project, cx| {
|
||||||
|
(
|
||||||
|
project.git_store().clone(),
|
||||||
|
project.open_local_buffer(path!("/project/a.txt"), cx),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let buffer = buffer.await.unwrap();
|
||||||
|
let conflict_set = git_store.update(cx, |git_store, cx| {
|
||||||
|
git_store.open_conflict_set(buffer.clone(), cx)
|
||||||
|
});
|
||||||
|
let (events_tx, events_rx) = mpsc::channel::<ConflictSetUpdate>();
|
||||||
|
let _conflict_set_subscription = cx.update(|cx| {
|
||||||
|
cx.subscribe(&conflict_set, move |_, event, _| {
|
||||||
|
events_tx.send(event.clone()).ok();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
let conflicts_snapshot = conflict_set.update(cx, |conflict_set, _| conflict_set.snapshot());
|
||||||
|
assert!(conflicts_snapshot.conflicts.is_empty());
|
||||||
|
|
||||||
|
buffer.update(cx, |buffer, cx| {
|
||||||
|
buffer.edit(
|
||||||
|
[
|
||||||
|
(4..4, "<<<<<<< HEAD\n"),
|
||||||
|
(14..14, "=======\nTWO\n>>>>>>> branch\n"),
|
||||||
|
],
|
||||||
|
None,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.run_until_parked();
|
||||||
|
events_rx.try_recv().expect_err(
|
||||||
|
"no conflicts should be registered as long as the file's status is unchanged",
|
||||||
|
);
|
||||||
|
|
||||||
|
fs.with_git_state(path!("/project/.git").as_ref(), true, |state| {
|
||||||
|
state.unmerged_paths.insert(
|
||||||
|
"a.txt".into(),
|
||||||
|
UnmergedStatus {
|
||||||
|
first_head: UnmergedStatusCode::Updated,
|
||||||
|
second_head: UnmergedStatusCode::Updated,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
// Cause the repository to emit MergeHeadsChanged.
|
||||||
|
state.merge_head_shas = vec!["abc".into(), "def".into()]
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
cx.run_until_parked();
|
||||||
|
let update = events_rx
|
||||||
|
.try_recv()
|
||||||
|
.expect("status change should trigger conflict parsing");
|
||||||
|
assert_eq!(update.old_range, 0..0);
|
||||||
|
assert_eq!(update.new_range, 0..1);
|
||||||
|
|
||||||
|
let conflict = conflict_set.update(cx, |conflict_set, _| {
|
||||||
|
conflict_set.snapshot().conflicts[0].clone()
|
||||||
|
});
|
||||||
|
cx.update(|cx| {
|
||||||
|
conflict.resolve(buffer.clone(), &[conflict.theirs.clone()], cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.run_until_parked();
|
||||||
|
let update = events_rx
|
||||||
|
.try_recv()
|
||||||
|
.expect("conflicts should be removed after resolution");
|
||||||
|
assert_eq!(update.old_range, 0..1);
|
||||||
|
assert_eq!(update.new_range, 0..0);
|
||||||
|
}
|
||||||
|
}
|
|
@ -29,7 +29,10 @@ pub mod search_history;
|
||||||
mod yarn;
|
mod yarn;
|
||||||
|
|
||||||
use crate::git_store::GitStore;
|
use crate::git_store::GitStore;
|
||||||
pub use git_store::git_traversal::{ChildEntriesGitIter, GitEntry, GitEntryRef, GitTraversal};
|
pub use git_store::{
|
||||||
|
ConflictRegion, ConflictSet, ConflictSetSnapshot, ConflictSetUpdate,
|
||||||
|
git_traversal::{ChildEntriesGitIter, GitEntry, GitEntryRef, GitTraversal},
|
||||||
|
};
|
||||||
|
|
||||||
use anyhow::{Context as _, Result, anyhow};
|
use anyhow::{Context as _, Result, anyhow};
|
||||||
use buffer_store::{BufferStore, BufferStoreEvent};
|
use buffer_store::{BufferStore, BufferStoreEvent};
|
||||||
|
|
|
@ -143,6 +143,11 @@ impl ThemeColors {
|
||||||
version_control_renamed: MODIFIED_COLOR,
|
version_control_renamed: MODIFIED_COLOR,
|
||||||
version_control_conflict: orange().light().step_12(),
|
version_control_conflict: orange().light().step_12(),
|
||||||
version_control_ignored: gray().light().step_12(),
|
version_control_ignored: gray().light().step_12(),
|
||||||
|
version_control_conflict_ours_background: green().light().step_10().alpha(0.5),
|
||||||
|
version_control_conflict_theirs_background: blue().light().step_10().alpha(0.5),
|
||||||
|
version_control_conflict_ours_marker_background: green().light().step_10().alpha(0.7),
|
||||||
|
version_control_conflict_theirs_marker_background: blue().light().step_10().alpha(0.7),
|
||||||
|
version_control_conflict_divider_background: Hsla::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -258,6 +263,11 @@ impl ThemeColors {
|
||||||
version_control_renamed: MODIFIED_COLOR,
|
version_control_renamed: MODIFIED_COLOR,
|
||||||
version_control_conflict: orange().dark().step_12(),
|
version_control_conflict: orange().dark().step_12(),
|
||||||
version_control_ignored: gray().dark().step_12(),
|
version_control_ignored: gray().dark().step_12(),
|
||||||
|
version_control_conflict_ours_background: green().dark().step_10().alpha(0.5),
|
||||||
|
version_control_conflict_theirs_background: blue().dark().step_10().alpha(0.5),
|
||||||
|
version_control_conflict_ours_marker_background: green().dark().step_10().alpha(0.7),
|
||||||
|
version_control_conflict_theirs_marker_background: blue().dark().step_10().alpha(0.7),
|
||||||
|
version_control_conflict_divider_background: Hsla::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -201,6 +201,23 @@ pub(crate) fn zed_default_dark() -> Theme {
|
||||||
version_control_renamed: MODIFIED_COLOR,
|
version_control_renamed: MODIFIED_COLOR,
|
||||||
version_control_conflict: crate::orange().light().step_12(),
|
version_control_conflict: crate::orange().light().step_12(),
|
||||||
version_control_ignored: crate::gray().light().step_12(),
|
version_control_ignored: crate::gray().light().step_12(),
|
||||||
|
version_control_conflict_ours_background: crate::green()
|
||||||
|
.light()
|
||||||
|
.step_12()
|
||||||
|
.alpha(0.5),
|
||||||
|
version_control_conflict_theirs_background: crate::blue()
|
||||||
|
.light()
|
||||||
|
.step_12()
|
||||||
|
.alpha(0.5),
|
||||||
|
version_control_conflict_ours_marker_background: crate::green()
|
||||||
|
.light()
|
||||||
|
.step_12()
|
||||||
|
.alpha(0.7),
|
||||||
|
version_control_conflict_theirs_marker_background: crate::blue()
|
||||||
|
.light()
|
||||||
|
.step_12()
|
||||||
|
.alpha(0.7),
|
||||||
|
version_control_conflict_divider_background: Hsla::default(),
|
||||||
},
|
},
|
||||||
status: StatusColors {
|
status: StatusColors {
|
||||||
conflict: yellow,
|
conflict: yellow,
|
||||||
|
|
|
@ -586,6 +586,26 @@ pub struct ThemeColorsContent {
|
||||||
/// Ignored version control color.
|
/// Ignored version control color.
|
||||||
#[serde(rename = "version_control.ignored")]
|
#[serde(rename = "version_control.ignored")]
|
||||||
pub version_control_ignored: Option<String>,
|
pub version_control_ignored: Option<String>,
|
||||||
|
|
||||||
|
/// Background color for row highlights of "ours" regions in merge conflicts.
|
||||||
|
#[serde(rename = "version_control.conflict.ours_background")]
|
||||||
|
pub version_control_conflict_ours_background: Option<String>,
|
||||||
|
|
||||||
|
/// Background color for row highlights of "theirs" regions in merge conflicts.
|
||||||
|
#[serde(rename = "version_control.conflict.theirs_background")]
|
||||||
|
pub version_control_conflict_theirs_background: Option<String>,
|
||||||
|
|
||||||
|
/// Background color for row highlights of "ours" conflict markers in merge conflicts.
|
||||||
|
#[serde(rename = "version_control.conflict.ours_marker_background")]
|
||||||
|
pub version_control_conflict_ours_marker_background: Option<String>,
|
||||||
|
|
||||||
|
/// Background color for row highlights of "theirs" conflict markers in merge conflicts.
|
||||||
|
#[serde(rename = "version_control.conflict.theirs_marker_background")]
|
||||||
|
pub version_control_conflict_theirs_marker_background: Option<String>,
|
||||||
|
|
||||||
|
/// Background color for row highlights of the "ours"/"theirs" divider in merge conflicts.
|
||||||
|
#[serde(rename = "version_control.conflict.divider_background")]
|
||||||
|
pub version_control_conflict_divider_background: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ThemeColorsContent {
|
impl ThemeColorsContent {
|
||||||
|
@ -1037,6 +1057,26 @@ impl ThemeColorsContent {
|
||||||
.and_then(|color| try_parse_color(color).ok())
|
.and_then(|color| try_parse_color(color).ok())
|
||||||
// Fall back to `conflict`, for backwards compatibility.
|
// Fall back to `conflict`, for backwards compatibility.
|
||||||
.or(status_colors.ignored),
|
.or(status_colors.ignored),
|
||||||
|
version_control_conflict_ours_background: self
|
||||||
|
.version_control_conflict_ours_background
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|color| try_parse_color(color).ok()),
|
||||||
|
version_control_conflict_theirs_background: self
|
||||||
|
.version_control_conflict_theirs_background
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|color| try_parse_color(color).ok()),
|
||||||
|
version_control_conflict_ours_marker_background: self
|
||||||
|
.version_control_conflict_ours_marker_background
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|color| try_parse_color(color).ok()),
|
||||||
|
version_control_conflict_theirs_marker_background: self
|
||||||
|
.version_control_conflict_theirs_marker_background
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|color| try_parse_color(color).ok()),
|
||||||
|
version_control_conflict_divider_background: self
|
||||||
|
.version_control_conflict_divider_background
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|color| try_parse_color(color).ok()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -261,6 +261,14 @@ pub struct ThemeColors {
|
||||||
pub version_control_conflict: Hsla,
|
pub version_control_conflict: Hsla,
|
||||||
/// Represents an ignored entry in version control systems.
|
/// Represents an ignored entry in version control systems.
|
||||||
pub version_control_ignored: Hsla,
|
pub version_control_ignored: Hsla,
|
||||||
|
|
||||||
|
/// Represents the "ours" region of a merge conflict.
|
||||||
|
pub version_control_conflict_ours_background: Hsla,
|
||||||
|
/// Represents the "theirs" region of a merge conflict.
|
||||||
|
pub version_control_conflict_theirs_background: Hsla,
|
||||||
|
pub version_control_conflict_ours_marker_background: Hsla,
|
||||||
|
pub version_control_conflict_theirs_marker_background: Hsla,
|
||||||
|
pub version_control_conflict_divider_background: Hsla,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(EnumIter, Debug, Clone, Copy, AsRefStr)]
|
#[derive(EnumIter, Debug, Clone, Copy, AsRefStr)]
|
||||||
|
|
|
@ -1500,7 +1500,7 @@ impl ShellExec {
|
||||||
editor.highlight_rows::<ShellExec>(
|
editor.highlight_rows::<ShellExec>(
|
||||||
input_range.clone().unwrap(),
|
input_range.clone().unwrap(),
|
||||||
cx.theme().status().unreachable_background,
|
cx.theme().status().unreachable_background,
|
||||||
false,
|
Default::default(),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue