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>(
|
||||
row_range,
|
||||
cx.theme().status().info_background,
|
||||
false,
|
||||
Default::default(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
@ -1393,7 +1393,7 @@ impl InlineAssistant {
|
|||
editor.highlight_rows::<DeletedLines>(
|
||||
Anchor::min()..Anchor::max(),
|
||||
cx.theme().status().deleted_background,
|
||||
false,
|
||||
Default::default(),
|
||||
cx,
|
||||
);
|
||||
editor
|
||||
|
|
|
@ -1226,7 +1226,7 @@ impl InlineAssistant {
|
|||
editor.highlight_rows::<InlineAssist>(
|
||||
row_range,
|
||||
cx.theme().status().info_background,
|
||||
false,
|
||||
Default::default(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
@ -1291,7 +1291,7 @@ impl InlineAssistant {
|
|||
editor.highlight_rows::<DeletedLines>(
|
||||
Anchor::min()..Anchor::max(),
|
||||
cx.theme().status().deleted_background,
|
||||
false,
|
||||
Default::default(),
|
||||
cx,
|
||||
);
|
||||
editor
|
||||
|
|
|
@ -269,6 +269,12 @@ enum DocumentHighlightWrite {}
|
|||
enum InputComposition {}
|
||||
enum SelectedTextHighlight {}
|
||||
|
||||
pub enum ConflictsOuter {}
|
||||
pub enum ConflictsOurs {}
|
||||
pub enum ConflictsTheirs {}
|
||||
pub enum ConflictsOursMarker {}
|
||||
pub enum ConflictsTheirsMarker {}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum Navigated {
|
||||
Yes,
|
||||
|
@ -694,6 +700,10 @@ pub trait Addon: 'static {
|
|||
}
|
||||
|
||||
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.
|
||||
|
@ -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 {
|
||||
index: usize,
|
||||
range: Range<Anchor>,
|
||||
color: Hsla,
|
||||
should_autoscroll: bool,
|
||||
options: RowHighlightOptions,
|
||||
type_id: TypeId,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
@ -5942,7 +5968,10 @@ impl Editor {
|
|||
self.highlight_rows::<EditPredictionPreview>(
|
||||
target..target,
|
||||
cx.theme().colors().editor_highlighted_line_background,
|
||||
true,
|
||||
RowHighlightOptions {
|
||||
autoscroll: true,
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
);
|
||||
self.request_autoscroll(Autoscroll::fit(), cx);
|
||||
|
@ -13449,7 +13478,7 @@ impl Editor {
|
|||
start..end,
|
||||
highlight_color
|
||||
.unwrap_or_else(|| cx.theme().colors().editor_highlighted_line_background),
|
||||
false,
|
||||
Default::default(),
|
||||
cx,
|
||||
);
|
||||
self.request_autoscroll(Autoscroll::center().for_anchor(start), cx);
|
||||
|
@ -16765,7 +16794,7 @@ impl Editor {
|
|||
&mut self,
|
||||
range: Range<Anchor>,
|
||||
color: Hsla,
|
||||
should_autoscroll: bool,
|
||||
options: RowHighlightOptions,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let snapshot = self.buffer().read(cx).snapshot(cx);
|
||||
|
@ -16797,7 +16826,7 @@ impl Editor {
|
|||
merged = true;
|
||||
prev_highlight.index = index;
|
||||
prev_highlight.color = color;
|
||||
prev_highlight.should_autoscroll = should_autoscroll;
|
||||
prev_highlight.options = options;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -16808,7 +16837,8 @@ impl Editor {
|
|||
range: range.clone(),
|
||||
index,
|
||||
color,
|
||||
should_autoscroll,
|
||||
options,
|
||||
type_id: TypeId::of::<T>(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -16914,7 +16944,15 @@ impl Editor {
|
|||
used_highlight_orders.entry(row).or_insert(highlight.index);
|
||||
if highlight.index >= *used_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
|
||||
|
@ -16930,7 +16968,7 @@ impl Editor {
|
|||
.values()
|
||||
.flat_map(|highlighted_rows| highlighted_rows.iter())
|
||||
.filter_map(|highlight| {
|
||||
if highlight.should_autoscroll {
|
||||
if highlight.options.autoscroll {
|
||||
Some(highlight.range.start.to_display_point(snapshot).row())
|
||||
} else {
|
||||
None
|
||||
|
@ -17405,13 +17443,19 @@ impl Editor {
|
|||
});
|
||||
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);
|
||||
let buffer = self.buffer.read(cx);
|
||||
self.registered_buffers
|
||||
.retain(|buffer_id, _| buffer.buffer(*buffer_id).is_some());
|
||||
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 {
|
||||
excerpt_ids,
|
||||
|
@ -18219,6 +18263,13 @@ impl Editor {
|
|||
.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> {
|
||||
let text_layout_details = self.text_layout_details(window);
|
||||
let style = &text_layout_details.editor_style;
|
||||
|
@ -19732,6 +19783,7 @@ pub enum EditorEvent {
|
|||
},
|
||||
ExcerptsRemoved {
|
||||
ids: Vec<ExcerptId>,
|
||||
removed_buffer_ids: Vec<BufferId>,
|
||||
},
|
||||
BufferFoldToggled {
|
||||
ids: Vec<ExcerptId>,
|
||||
|
@ -20672,24 +20724,8 @@ impl Render for MissingEditPredictionKeybindingTooltip {
|
|||
pub struct LineHighlight {
|
||||
pub background: Background,
|
||||
pub border: Option<gpui::Hsla>,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
pub include_gutter: bool,
|
||||
pub type_id: Option<TypeId>,
|
||||
}
|
||||
|
||||
fn render_diff_hunk_controls(
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use crate::{
|
||||
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,
|
||||
EditDisplayMode, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle,
|
||||
FILE_HEADER_HEIGHT, FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput,
|
||||
|
@ -4036,6 +4037,7 @@ impl EditorElement {
|
|||
line_height: Pixels,
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
display_hunks: &[(DisplayDiffHunk, Option<Hitbox>)],
|
||||
highlighted_rows: &BTreeMap<DisplayRow, LineHighlight>,
|
||||
editor: Entity<Editor>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
|
@ -4064,6 +4066,22 @@ impl EditorElement {
|
|||
{
|
||||
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;
|
||||
if row_infos[row_ix].diff_status.is_none() {
|
||||
continue;
|
||||
|
@ -4258,14 +4276,21 @@ impl EditorElement {
|
|||
highlight_row_end: DisplayRow,
|
||||
highlight: crate::LineHighlight,
|
||||
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(
|
||||
layout.hitbox.origin.x,
|
||||
origin_x,
|
||||
layout.hitbox.origin.y
|
||||
+ (highlight_row_start.as_f32() - scroll_top)
|
||||
* layout.position_map.line_height,
|
||||
);
|
||||
let size = size(
|
||||
layout.hitbox.size.width,
|
||||
width,
|
||||
layout.position_map.line_height
|
||||
* highlight_row_end.next_row().minus(highlight_row_start) as f32,
|
||||
);
|
||||
|
@ -6789,10 +6814,16 @@ impl Element for EditorElement {
|
|||
} else {
|
||||
background_color.opacity(0.36)
|
||||
}),
|
||||
include_gutter: true,
|
||||
type_id: None,
|
||||
};
|
||||
|
||||
let filled_highlight =
|
||||
solid_background(background_color.opacity(hunk_opacity)).into();
|
||||
let filled_highlight = LineHighlight {
|
||||
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) {
|
||||
hollow_highlight
|
||||
|
@ -7551,6 +7582,7 @@ impl Element for EditorElement {
|
|||
line_height,
|
||||
scroll_pixel_position,
|
||||
&display_hunks,
|
||||
&highlighted_rows,
|
||||
self.editor.clone(),
|
||||
window,
|
||||
cx,
|
||||
|
|
|
@ -288,7 +288,7 @@ impl FollowableItem for Editor {
|
|||
}
|
||||
true
|
||||
}
|
||||
EditorEvent::ExcerptsRemoved { ids } => {
|
||||
EditorEvent::ExcerptsRemoved { ids, .. } => {
|
||||
update
|
||||
.deleted_excerpts
|
||||
.extend(ids.iter().map(ExcerptId::to_proto));
|
||||
|
|
|
@ -34,6 +34,7 @@ pub struct FakeGitRepositoryState {
|
|||
pub blames: HashMap<RepoPath, Blame>,
|
||||
pub current_branch_name: Option<String>,
|
||||
pub branches: HashSet<String>,
|
||||
pub merge_head_shas: Vec<String>,
|
||||
pub simulated_index_write_error_message: Option<String>,
|
||||
}
|
||||
|
||||
|
@ -47,12 +48,20 @@ impl FakeGitRepositoryState {
|
|||
blames: Default::default(),
|
||||
current_branch_name: Default::default(),
|
||||
branches: Default::default(),
|
||||
merge_head_shas: Default::default(),
|
||||
simulated_index_write_error_message: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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>>
|
||||
where
|
||||
F: 'static + Send + FnOnce(&mut FakeGitRepositoryState) -> Result<T>,
|
||||
|
@ -137,11 +146,18 @@ impl GitRepository for FakeGitRepository {
|
|||
}
|
||||
|
||||
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>> {
|
||||
unimplemented!()
|
||||
fn show(&self, commit: String) -> BoxFuture<Result<CommitDetails>> {
|
||||
async {
|
||||
Ok(CommitDetails {
|
||||
sha: commit.into(),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn reset(
|
||||
|
|
|
@ -133,7 +133,7 @@ pub struct CommitSummary {
|
|||
pub has_parent: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, Default, Hash, PartialEq, Eq)]
|
||||
pub struct CommitDetails {
|
||||
pub sha: 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();
|
||||
}
|
||||
GitStoreEvent::RepositoryUpdated(_, _, _) => {}
|
||||
GitStoreEvent::JobsUpdated => {}
|
||||
GitStoreEvent::JobsUpdated | GitStoreEvent::ConflictsUpdated => {}
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
|
@ -1650,7 +1650,7 @@ impl GitPanel {
|
|||
if let Some(merge_message) = self
|
||||
.active_repository
|
||||
.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());
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ use std::any::Any;
|
|||
use ::settings::Settings;
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use commit_modal::CommitModal;
|
||||
use editor::Editor;
|
||||
mod blame_ui;
|
||||
use git::{
|
||||
repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus},
|
||||
|
@ -20,6 +21,7 @@ pub mod branch_picker;
|
|||
mod commit_modal;
|
||||
pub mod commit_tooltip;
|
||||
mod commit_view;
|
||||
mod conflict_view;
|
||||
pub mod git_panel;
|
||||
mod git_panel_settings;
|
||||
pub mod onboarding;
|
||||
|
@ -35,6 +37,11 @@ pub fn init(cx: &mut App) {
|
|||
|
||||
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| {
|
||||
ProjectDiff::register(workspace, cx);
|
||||
CommitModal::register(workspace);
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use crate::{
|
||||
conflict_view::ConflictAddon,
|
||||
git_panel::{GitPanel, GitPanelAddon, GitStatusEntry},
|
||||
remote_button::{render_publish_button, render_push_button},
|
||||
};
|
||||
|
@ -26,7 +27,10 @@ use project::{
|
|||
Project, ProjectPath,
|
||||
git_store::{GitStore, GitStoreEvent, RepositoryEvent},
|
||||
};
|
||||
use std::any::{Any, TypeId};
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
ops::Range,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use ui::{KeyBinding, Tooltip, prelude::*, vertical_divider};
|
||||
use util::ResultExt as _;
|
||||
|
@ -48,7 +52,6 @@ pub struct ProjectDiff {
|
|||
focus_handle: FocusHandle,
|
||||
update_needed: postage::watch::Sender<()>,
|
||||
pending_scroll: Option<PathKey>,
|
||||
current_branch: Option<Branch>,
|
||||
_task: Task<Result<()>>,
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
@ -61,9 +64,9 @@ struct DiffBuffer {
|
|||
file_status: FileStatus,
|
||||
}
|
||||
|
||||
const CONFLICT_NAMESPACE: u32 = 0;
|
||||
const TRACKED_NAMESPACE: u32 = 1;
|
||||
const NEW_NAMESPACE: u32 = 2;
|
||||
const CONFLICT_NAMESPACE: u32 = 1;
|
||||
const TRACKED_NAMESPACE: u32 = 2;
|
||||
const NEW_NAMESPACE: u32 = 3;
|
||||
|
||||
impl ProjectDiff {
|
||||
pub(crate) fn register(workspace: &mut Workspace, cx: &mut Context<Workspace>) {
|
||||
|
@ -154,7 +157,8 @@ impl ProjectDiff {
|
|||
window,
|
||||
move |this, _git_store, event, _window, _cx| match event {
|
||||
GitStoreEvent::ActiveRepositoryChanged(_)
|
||||
| GitStoreEvent::RepositoryUpdated(_, RepositoryEvent::Updated { .. }, true) => {
|
||||
| GitStoreEvent::RepositoryUpdated(_, RepositoryEvent::Updated { .. }, true)
|
||||
| GitStoreEvent::ConflictsUpdated => {
|
||||
*this.update_needed.borrow_mut() = ();
|
||||
}
|
||||
_ => {}
|
||||
|
@ -178,7 +182,6 @@ impl ProjectDiff {
|
|||
multibuffer,
|
||||
pending_scroll: None,
|
||||
update_needed: send,
|
||||
current_branch: None,
|
||||
_task: worker,
|
||||
_subscription: git_store_subscription,
|
||||
}
|
||||
|
@ -395,11 +398,25 @@ impl ProjectDiff {
|
|||
let buffer = diff_buffer.buffer;
|
||||
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 diff = diff.read(cx);
|
||||
let diff_hunk_ranges = diff
|
||||
.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<_>>();
|
||||
|
||||
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(
|
||||
path_key.clone(),
|
||||
buffer,
|
||||
diff_hunk_ranges,
|
||||
excerpt_ranges,
|
||||
editor::DEFAULT_MULTIBUFFER_CONTEXT,
|
||||
cx,
|
||||
);
|
||||
|
@ -450,18 +467,6 @@ impl ProjectDiff {
|
|||
cx: &mut AsyncWindowContext,
|
||||
) -> Result<()> {
|
||||
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))?;
|
||||
for buffer_to_load in buffers_to_load {
|
||||
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 {
|
||||
use git::repository::{
|
||||
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(test)]
|
||||
mod tests {
|
||||
|
|
|
@ -2,7 +2,8 @@ pub mod cursor_position;
|
|||
|
||||
use cursor_position::{LineIndicatorFormat, UserCaretPosition};
|
||||
use editor::{
|
||||
Anchor, Editor, MultiBufferSnapshot, ToOffset, ToPoint, actions::Tab, scroll::Autoscroll,
|
||||
Anchor, Editor, MultiBufferSnapshot, RowHighlightOptions, ToOffset, ToPoint, actions::Tab,
|
||||
scroll::Autoscroll,
|
||||
};
|
||||
use gpui::{
|
||||
App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, SharedString, Styled,
|
||||
|
@ -180,7 +181,10 @@ impl GoToLine {
|
|||
editor.highlight_rows::<GoToLineRowHighlights>(
|
||||
start..end,
|
||||
cx.theme().colors().editor_highlighted_line_background,
|
||||
true,
|
||||
RowHighlightOptions {
|
||||
autoscroll: true,
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
);
|
||||
editor.request_autoscroll(Autoscroll::center(), cx);
|
||||
|
|
|
@ -95,6 +95,7 @@ pub enum Event {
|
|||
},
|
||||
ExcerptsRemoved {
|
||||
ids: Vec<ExcerptId>,
|
||||
removed_buffer_ids: Vec<BufferId>,
|
||||
},
|
||||
ExcerptsExpanded {
|
||||
ids: Vec<ExcerptId>,
|
||||
|
@ -2021,7 +2022,12 @@ impl MultiBuffer {
|
|||
pub fn clear(&mut self, cx: &mut Context<Self>) {
|
||||
self.sync(cx);
|
||||
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.paths_by_excerpt.clear();
|
||||
let mut snapshot = self.snapshot.borrow_mut();
|
||||
|
@ -2046,7 +2052,10 @@ impl MultiBuffer {
|
|||
singleton_buffer_edited: false,
|
||||
edited_buffer: None,
|
||||
});
|
||||
cx.emit(Event::ExcerptsRemoved { ids });
|
||||
cx.emit(Event::ExcerptsRemoved {
|
||||
ids,
|
||||
removed_buffer_ids,
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
|
@ -2310,9 +2319,9 @@ impl MultiBuffer {
|
|||
new_excerpts.append(suffix, &());
|
||||
drop(cursor);
|
||||
snapshot.excerpts = new_excerpts;
|
||||
for buffer_id in removed_buffer_ids {
|
||||
self.diffs.remove(&buffer_id);
|
||||
snapshot.diffs.remove(&buffer_id);
|
||||
for buffer_id in &removed_buffer_ids {
|
||||
self.diffs.remove(buffer_id);
|
||||
snapshot.diffs.remove(buffer_id);
|
||||
}
|
||||
|
||||
if changed_trailing_excerpt {
|
||||
|
@ -2325,7 +2334,10 @@ impl MultiBuffer {
|
|||
singleton_buffer_edited: false,
|
||||
edited_buffer: None,
|
||||
});
|
||||
cx.emit(Event::ExcerptsRemoved { ids });
|
||||
cx.emit(Event::ExcerptsRemoved {
|
||||
ids,
|
||||
removed_buffer_ids,
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
|
|
|
@ -635,7 +635,7 @@ fn test_excerpt_events(cx: &mut App) {
|
|||
predecessor,
|
||||
excerpts,
|
||||
} => 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 { .. } => {
|
||||
*follower_edit_event_count.write() += 1;
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ use std::{
|
|||
sync::Arc,
|
||||
};
|
||||
|
||||
use editor::RowHighlightOptions;
|
||||
use editor::{Anchor, AnchorRangeExt, Editor, scroll::Autoscroll};
|
||||
use fuzzy::StringMatch;
|
||||
use gpui::{
|
||||
|
@ -171,7 +172,10 @@ impl OutlineViewDelegate {
|
|||
active_editor.highlight_rows::<OutlineRowHighlights>(
|
||||
outline_item.range.start..outline_item.range.end,
|
||||
cx.theme().colors().editor_highlighted_line_background,
|
||||
true,
|
||||
RowHighlightOptions {
|
||||
autoscroll: true,
|
||||
..Default::default()
|
||||
},
|
||||
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));
|
||||
outline_panel.update_fs_entries(editor.clone(), debounce, window, cx);
|
||||
}
|
||||
EditorEvent::ExcerptsRemoved { ids } => {
|
||||
EditorEvent::ExcerptsRemoved { ids, .. } => {
|
||||
let mut ids = ids.iter().collect::<HashSet<_>>();
|
||||
for excerpts in outline_panel.excerpts.values_mut() {
|
||||
excerpts.retain(|excerpt_id, _| !ids.remove(excerpt_id));
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
mod conflict_set;
|
||||
pub mod git_traversal;
|
||||
|
||||
use crate::{
|
||||
|
@ -10,11 +11,12 @@ use askpass::AskPassDelegate;
|
|||
use buffer_diff::{BufferDiff, BufferDiffEvent};
|
||||
use client::ProjectId;
|
||||
use collections::HashMap;
|
||||
pub use conflict_set::{ConflictRegion, ConflictSet, ConflictSetSnapshot, ConflictSetUpdate};
|
||||
use fs::Fs;
|
||||
use futures::{
|
||||
FutureExt as _, StreamExt as _,
|
||||
FutureExt, StreamExt as _,
|
||||
channel::{mpsc, oneshot},
|
||||
future::{self, Shared},
|
||||
future::{self, Shared, try_join_all},
|
||||
};
|
||||
use git::{
|
||||
BuildPermalinkParams, GitHostingProviderRegistry, WORK_DIRECTORY_REPO_PATH,
|
||||
|
@ -74,7 +76,7 @@ pub struct GitStore {
|
|||
#[allow(clippy::type_complexity)]
|
||||
loading_diffs:
|
||||
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>>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
@ -85,12 +87,15 @@ struct SharedDiffs {
|
|||
uncommitted: Option<Entity<BufferDiff>>,
|
||||
}
|
||||
|
||||
struct BufferDiffState {
|
||||
struct BufferGitState {
|
||||
unstaged_diff: Option<WeakEntity<BufferDiff>>,
|
||||
uncommitted_diff: Option<WeakEntity<BufferDiff>>,
|
||||
conflict_set: Option<WeakEntity<ConflictSet>>,
|
||||
recalculate_diff_task: Option<Task<Result<()>>>,
|
||||
reparse_conflict_markers_task: Option<Task<Result<()>>>,
|
||||
language: Option<Arc<Language>>,
|
||||
language_registry: Option<Arc<LanguageRegistry>>,
|
||||
conflict_updated_futures: Vec<oneshot::Sender<()>>,
|
||||
recalculating_tx: postage::watch::Sender<bool>,
|
||||
|
||||
/// 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)]
|
||||
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)]
|
||||
pub struct RepositorySnapshot {
|
||||
pub id: RepositoryId,
|
||||
pub merge_message: Option<SharedString>,
|
||||
pub statuses_by_path: SumTree<StatusEntry>,
|
||||
pub work_directory_abs_path: Arc<Path>,
|
||||
pub branch: Option<Branch>,
|
||||
pub head_commit: Option<CommitDetails>,
|
||||
pub merge_conflicts: TreeSet<RepoPath>,
|
||||
pub merge_head_shas: Vec<SharedString>,
|
||||
pub scan_id: u64,
|
||||
pub merge: MergeDetails,
|
||||
}
|
||||
|
||||
type JobId = u64;
|
||||
|
@ -297,6 +311,7 @@ pub enum GitStoreEvent {
|
|||
RepositoryRemoved(RepositoryId),
|
||||
IndexWriteError(anyhow::Error),
|
||||
JobsUpdated,
|
||||
ConflictsUpdated,
|
||||
}
|
||||
|
||||
impl EventEmitter<RepositoryEvent> for Repository {}
|
||||
|
@ -681,10 +696,11 @@ impl GitStore {
|
|||
let text_snapshot = buffer.text_snapshot();
|
||||
this.loading_diffs.remove(&(buffer_id, kind));
|
||||
|
||||
let git_store = cx.weak_entity();
|
||||
let diff_state = this
|
||||
.diffs
|
||||
.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));
|
||||
|
||||
|
@ -737,6 +753,62 @@ impl GitStore {
|
|||
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(
|
||||
&self,
|
||||
project_path: &ProjectPath,
|
||||
|
@ -1079,6 +1151,35 @@ impl GitStore {
|
|||
cx: &mut Context<Self>,
|
||||
) {
|
||||
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(
|
||||
id,
|
||||
event.clone(),
|
||||
|
@ -1218,9 +1319,15 @@ impl GitStore {
|
|||
if let Some(diff_state) = self.diffs.get_mut(&buffer.read(cx).remote_id()) {
|
||||
let buffer = buffer.read(cx).text_snapshot();
|
||||
diff_state.update(cx, |diff_state, cx| {
|
||||
diff_state.recalculate_diffs(buffer, cx);
|
||||
futures.extend(diff_state.wait_for_recalculation());
|
||||
diff_state.recalculate_diffs(buffer.clone(), cx);
|
||||
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 {
|
||||
|
@ -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>) {
|
||||
self.language = buffer.read(cx).language().cloned();
|
||||
self.language_changed = true;
|
||||
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>> {
|
||||
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(
|
||||
this: Entity<GitStore>,
|
||||
project_id: u64,
|
||||
|
@ -2397,14 +2557,12 @@ impl RepositorySnapshot {
|
|||
fn empty(id: RepositoryId, work_directory_abs_path: Arc<Path>) -> Self {
|
||||
Self {
|
||||
id,
|
||||
merge_message: None,
|
||||
statuses_by_path: Default::default(),
|
||||
work_directory_abs_path,
|
||||
branch: None,
|
||||
head_commit: None,
|
||||
merge_conflicts: Default::default(),
|
||||
merge_head_shas: Default::default(),
|
||||
scan_id: 0,
|
||||
merge: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2419,7 +2577,8 @@ impl RepositorySnapshot {
|
|||
.collect(),
|
||||
removed_statuses: Default::default(),
|
||||
current_merge_conflicts: self
|
||||
.merge_conflicts
|
||||
.merge
|
||||
.conflicted_paths
|
||||
.iter()
|
||||
.map(|repo_path| repo_path.to_proto())
|
||||
.collect(),
|
||||
|
@ -2480,7 +2639,8 @@ impl RepositorySnapshot {
|
|||
updated_statuses,
|
||||
removed_statuses,
|
||||
current_merge_conflicts: self
|
||||
.merge_conflicts
|
||||
.merge
|
||||
.conflicted_paths
|
||||
.iter()
|
||||
.map(|path| path.as_ref().to_proto())
|
||||
.collect(),
|
||||
|
@ -2515,7 +2675,7 @@ impl RepositorySnapshot {
|
|||
}
|
||||
|
||||
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.
|
||||
|
@ -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 {
|
||||
pub fn snapshot(&self) -> RepositorySnapshot {
|
||||
self.snapshot.clone()
|
||||
}
|
||||
|
||||
fn local(
|
||||
id: RepositoryId,
|
||||
work_directory_abs_path: Arc<Path>,
|
||||
|
@ -3731,7 +3961,7 @@ impl Repository {
|
|||
.as_ref()
|
||||
.map(proto_to_commit_details);
|
||||
|
||||
self.snapshot.merge_conflicts = conflicted_paths;
|
||||
self.snapshot.merge.conflicted_paths = conflicted_paths;
|
||||
|
||||
let edits = update
|
||||
.removed_statuses
|
||||
|
@ -4321,16 +4551,6 @@ async fn compute_snapshot(
|
|||
let branches = backend.branches().await?;
|
||||
let branch = branches.into_iter().find(|branch| branch.is_head);
|
||||
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(
|
||||
statuses
|
||||
.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_head_shas_changed
|
||||
if merge_heads_changed
|
||||
|| branch != prev_snapshot.branch
|
||||
|| statuses_by_path != prev_snapshot.statuses_by_path
|
||||
{
|
||||
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,
|
||||
// until the merge heads change (at commit time, etc.).
|
||||
let mut merge_conflicts = prev_snapshot.merge_conflicts.clone();
|
||||
if merge_head_shas_changed {
|
||||
merge_conflicts = current_merge_conflicts;
|
||||
if merge_heads_changed {
|
||||
events.push(RepositoryEvent::MergeHeadsChanged);
|
||||
}
|
||||
|
||||
// Useful when branch is None in detached head state
|
||||
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,
|
||||
};
|
||||
|
||||
let snapshot = RepositorySnapshot {
|
||||
id,
|
||||
merge_message,
|
||||
statuses_by_path,
|
||||
work_directory_abs_path,
|
||||
scan_id: prev_snapshot.scan_id + 1,
|
||||
branch,
|
||||
head_commit,
|
||||
merge_conflicts,
|
||||
merge_head_shas,
|
||||
merge: merge_details,
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
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 buffer_store::{BufferStore, BufferStoreEvent};
|
||||
|
|
|
@ -143,6 +143,11 @@ impl ThemeColors {
|
|||
version_control_renamed: MODIFIED_COLOR,
|
||||
version_control_conflict: orange().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_conflict: orange().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_conflict: crate::orange().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 {
|
||||
conflict: yellow,
|
||||
|
|
|
@ -586,6 +586,26 @@ pub struct ThemeColorsContent {
|
|||
/// Ignored version control color.
|
||||
#[serde(rename = "version_control.ignored")]
|
||||
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 {
|
||||
|
@ -1037,6 +1057,26 @@ impl ThemeColorsContent {
|
|||
.and_then(|color| try_parse_color(color).ok())
|
||||
// Fall back to `conflict`, for backwards compatibility.
|
||||
.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,
|
||||
/// Represents an ignored entry in version control systems.
|
||||
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)]
|
||||
|
|
|
@ -1500,7 +1500,7 @@ impl ShellExec {
|
|||
editor.highlight_rows::<ShellExec>(
|
||||
input_range.clone().unwrap(),
|
||||
cx.theme().status().unreachable_background,
|
||||
false,
|
||||
Default::default(),
|
||||
cx,
|
||||
);
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue