use std::ops::{Bound, Range, RangeInclusive}; use buffer_diff::{BufferDiff, BufferDiffSnapshot};use collections::HashMap;use feature_flags::{FeatureFlag, FeatureFlagAppExt as _};use gpui::{Action, AppContext as _, Entity, EventEmitter, Focusable, Subscription, WeakEntity};use language::{Buffer, Capability};use multi_buffer::{ Anchor, ExcerptId, ExcerptRange, ExpandExcerptDirection, MultiBuffer, MultiBufferPoint, MultiBufferSnapshot, PathKey,};use project::Project;use rope::Point;use text::{OffsetRangeExt as _, Patch, ToPoint as _};use ui::{ App, Context, InteractiveElement as _, IntoElement as _, ParentElement as _, Render, Styled as _, Window, div,}; use crate::{ display_map::CompanionExcerptPatch, split_editor_view::{SplitEditorState, SplitEditorView},};use workspace::{ActivatePaneLeft, ActivatePaneRight, Item, Workspace}; use crate::{ Autoscroll, DisplayMap, Editor, EditorEvent, ToggleCodeActions, ToggleSoftWrap, actions::{DisableBreakpoint, EditLogBreakpoint, EnableBreakpoint, ToggleBreakpoint}, display_map::Companion,};use zed_actions::assistant::InlineAssist; pub(crate) fn convert_lhs_rows_to_rhs( lhs_excerpt_to_rhs_excerpt: &HashMap<ExcerptId, ExcerptId>, rhs_snapshot: &MultiBufferSnapshot, lhs_snapshot: &MultiBufferSnapshot, lhs_bounds: (Bound<MultiBufferPoint>, Bound<MultiBufferPoint>),) -> Vec<CompanionExcerptPatch> { patches_for_range( lhs_excerpt_to_rhs_excerpt, lhs_snapshot, rhs_snapshot, lhs_bounds, |diff, range, buffer| diff.patch_for_base_text_range(range, buffer), )} pub(crate) fn convert_rhs_rows_to_lhs( rhs_excerpt_to_lhs_excerpt: &HashMap<ExcerptId, ExcerptId>, lhs_snapshot: &MultiBufferSnapshot, rhs_snapshot: &MultiBufferSnapshot, rhs_bounds: (Bound<MultiBufferPoint>, Bound<MultiBufferPoint>),) -> Vec<CompanionExcerptPatch> { patches_for_range( rhs_excerpt_to_lhs_excerpt, rhs_snapshot, lhs_snapshot, rhs_bounds, |diff, range, buffer| diff.patch_for_buffer_range(range, buffer), )} fn patches_for_range<F>( excerpt_map: &HashMap<ExcerptId, ExcerptId>, source_snapshot: &MultiBufferSnapshot, target_snapshot: &MultiBufferSnapshot, source_bounds: (Bound<MultiBufferPoint>, Bound<MultiBufferPoint>), translate_fn: F,) -> Vec<CompanionExcerptPatch>where F: Fn(&BufferDiffSnapshot, RangeInclusive<Point>, &text::BufferSnapshot) -> Patch<Point>,{ let mut result = Vec::new(); let mut patches = HashMap::default(); for (source_buffer, buffer_offset_range, source_excerpt_id) in source_snapshot.range_to_buffer_ranges(source_bounds) { let target_excerpt_id = excerpt_map.get(&source_excerpt_id).copied().unwrap(); let target_buffer = target_snapshot .buffer_for_excerpt(target_excerpt_id) .unwrap(); let patch = patches.entry(source_buffer.remote_id()).or_insert_with(|| { let diff = source_snapshot .diff_for_buffer_id(source_buffer.remote_id()) .unwrap(); let rhs_buffer = if source_buffer.remote_id() == diff.base_text().remote_id() { &target_buffer } else { source_buffer }; // TODO(split-diff) pass only the union of the ranges for the affected excerpts translate_fn(diff, Point::zero()..=source_buffer.max_point(), rhs_buffer) }); let buffer_point_range = buffer_offset_range.to_point(source_buffer); // TODO(split-diff) maybe narrow the patch to only the edited part of the excerpt // (less useful for project diff, but important if we want to do singleton side-by-side diff) result.push(patch_for_excerpt( source_snapshot, target_snapshot, source_excerpt_id, target_excerpt_id, source_buffer, target_buffer, patch, buffer_point_range, )); } result} fn patch_for_excerpt( source_snapshot: &MultiBufferSnapshot, target_snapshot: &MultiBufferSnapshot, source_excerpt_id: ExcerptId, target_excerpt_id: ExcerptId, source_buffer: &text::BufferSnapshot, target_buffer: &text::BufferSnapshot, patch: &Patch<Point>, source_edited_range: Range<Point>,) -> CompanionExcerptPatch { let source_multibuffer_range = source_snapshot .range_for_excerpt(source_excerpt_id) .unwrap(); let source_excerpt_start_in_multibuffer = source_multibuffer_range.start; let source_context_range = source_snapshot .context_range_for_excerpt(source_excerpt_id) .unwrap(); let source_excerpt_start_in_buffer = source_context_range.start.to_point(&source_buffer); let source_excerpt_end_in_buffer = source_context_range.end.to_point(&source_buffer); let target_multibuffer_range = target_snapshot .range_for_excerpt(target_excerpt_id) .unwrap(); let target_excerpt_start_in_multibuffer = target_multibuffer_range.start; let target_context_range = target_snapshot .context_range_for_excerpt(target_excerpt_id) .unwrap(); let target_excerpt_start_in_buffer = target_context_range.start.to_point(&target_buffer); let target_excerpt_end_in_buffer = target_context_range.end.to_point(&target_buffer); let edits = patch .edits() .iter() .skip_while(|edit| edit.old.end < source_excerpt_start_in_buffer) .take_while(|edit| edit.old.start <= source_excerpt_end_in_buffer) .map(|edit| { let clamped_source_start = edit .old .start .max(source_excerpt_start_in_buffer) .min(source_excerpt_end_in_buffer); let clamped_source_end = edit .old .end .max(source_excerpt_start_in_buffer) .min(source_excerpt_end_in_buffer); let source_multibuffer_start = source_excerpt_start_in_multibuffer + (clamped_source_start - source_excerpt_start_in_buffer); let source_multibuffer_end = source_excerpt_start_in_multibuffer + (clamped_source_end - source_excerpt_start_in_buffer); let clamped_target_start = edit .new .start .max(target_excerpt_start_in_buffer) .min(target_excerpt_end_in_buffer); let clamped_target_end = edit .new .end .max(target_excerpt_start_in_buffer) .min(target_excerpt_end_in_buffer); let target_multibuffer_start = target_excerpt_start_in_multibuffer + (clamped_target_start - target_excerpt_start_in_buffer); let target_multibuffer_end = target_excerpt_start_in_multibuffer + (clamped_target_end - target_excerpt_start_in_buffer); text::Edit { old: source_multibuffer_start..source_multibuffer_end, new: target_multibuffer_start..target_multibuffer_end, } }); let edits = [text::Edit { old: source_excerpt_start_in_multibuffer..source_excerpt_start_in_multibuffer, new: target_excerpt_start_in_multibuffer..target_excerpt_start_in_multibuffer, }] .into_iter() .chain(edits); let mut merged_edits: Vec<text::Edit<Point>> = Vec::new(); for edit in edits { if let Some(last) = merged_edits.last_mut() { if edit.new.start <= last.new.end { last.old.end = last.old.end.max(edit.old.end); last.new.end = last.new.end.max(edit.new.end); continue; } } merged_edits.push(edit); } let edited_range = source_excerpt_start_in_multibuffer + (source_edited_range.start - source_excerpt_start_in_buffer) ..source_excerpt_start_in_multibuffer + (source_edited_range.end - source_excerpt_start_in_buffer); let source_excerpt_end = source_excerpt_start_in_multibuffer + (source_excerpt_end_in_buffer - source_excerpt_start_in_buffer); let target_excerpt_end = target_excerpt_start_in_multibuffer + (target_excerpt_end_in_buffer - target_excerpt_start_in_buffer); CompanionExcerptPatch { patch: Patch::new(merged_edits), edited_range, source_excerpt_range: source_excerpt_start_in_multibuffer..source_excerpt_end, target_excerpt_range: target_excerpt_start_in_multibuffer..target_excerpt_end, }} pub struct SplitDiffFeatureFlag; impl FeatureFlag for SplitDiffFeatureFlag { const NAME: &'static str = "split-diff"; fn enabled_for_staff() -> bool { true }} #[derive(Clone, Copy, PartialEq, Eq, Action, Default)]#[action(namespace = editor)]struct SplitDiff; #[derive(Clone, Copy, PartialEq, Eq, Action, Default)]#[action(namespace = editor)]struct UnsplitDiff; #[derive(Clone, Copy, PartialEq, Eq, Action, Default)]#[action(namespace = editor)]pub struct ToggleSplitDiff; #[derive(Clone, Copy, PartialEq, Eq, Action, Default)]#[action(namespace = editor)]struct JumpToCorrespondingRow; /// When locked cursors mode is enabled, cursor movements in one editor will/// update the cursor position in the other editor to the corresponding row.#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]#[action(namespace = editor)]pub struct ToggleLockedCursors; pub struct SplittableEditor { rhs_multibuffer: Entity<MultiBuffer>, rhs_editor: Entity<Editor>, lhs: Option<LhsEditor>, workspace: WeakEntity<Workspace>, split_state: Entity<SplitEditorState>, locked_cursors: bool, _subscriptions: Vec<Subscription>,} struct LhsEditor { multibuffer: Entity<MultiBuffer>, editor: Entity<Editor>, has_latest_selection: bool, _subscriptions: Vec<Subscription>,} impl SplittableEditor { pub fn rhs_editor(&self) -> &Entity<Editor> { &self.rhs_editor } pub fn lhs_editor(&self) -> Option<&Entity<Editor>> { self.lhs.as_ref().map(|s| &s.editor) } pub fn is_split(&self) -> bool { self.lhs.is_some() } pub fn last_selected_editor(&self) -> &Entity<Editor> { if let Some(lhs) = &self.lhs && lhs.has_latest_selection { &lhs.editor } else { &self.rhs_editor } } pub fn new_unsplit( rhs_multibuffer: Entity<MultiBuffer>, project: Entity<Project>, workspace: Entity<Workspace>, window: &mut Window, cx: &mut Context<Self>, ) -> Self { let rhs_editor = cx.new(|cx| { let mut editor = Editor::for_multibuffer(rhs_multibuffer.clone(), Some(project.clone()), window, cx); editor.set_expand_all_diff_hunks(cx); editor }); // TODO(split-diff) we might want to tag editor events with whether they came from rhs/lhs let subscriptions = vec![cx.subscribe( &rhs_editor, |this, _, event: &EditorEvent, cx| match event { EditorEvent::ExpandExcerptsRequested { excerpt_ids, lines, direction, } => { this.expand_excerpts(excerpt_ids.iter().copied(), *lines, *direction, cx); } EditorEvent::SelectionsChanged { .. } => { if let Some(lhs) = &mut this.lhs { lhs.has_latest_selection = false; } cx.emit(event.clone()); } _ => cx.emit(event.clone()), }, )]; window.defer(cx, { let workspace = workspace.downgrade(); let rhs_editor = rhs_editor.downgrade(); move |window, cx| { workspace .update(cx, |workspace, cx| { rhs_editor.update(cx, |editor, cx| { editor.added_to_workspace(workspace, window, cx); }) }) .ok(); } }); let split_state = cx.new(|cx| SplitEditorState::new(cx)); Self { rhs_editor, rhs_multibuffer, lhs: None, workspace: workspace.downgrade(), split_state, locked_cursors: false, _subscriptions: subscriptions, } } fn split(&mut self, _: &SplitDiff, window: &mut Window, cx: &mut Context<Self>) { if !cx.has_flag::<SplitDiffFeatureFlag>() { return; } if self.lhs.is_some() { return; } let Some(workspace) = self.workspace.upgrade() else { return; }; let project = workspace.read(cx).project().clone(); let lhs_multibuffer = cx.new(|cx| { let mut multibuffer = MultiBuffer::new(Capability::ReadOnly); multibuffer.set_all_diff_hunks_expanded(cx); multibuffer }); let lhs_editor = cx.new(|cx| { let mut editor = Editor::for_multibuffer(lhs_multibuffer.clone(), Some(project.clone()), window, cx); editor.set_number_deleted_lines(true, cx); editor.set_delegate_expand_excerpts(true); editor.set_show_vertical_scrollbar(false, cx); editor.set_minimap_visibility(crate::MinimapVisibility::Disabled, window, cx); editor }); let subscriptions = vec![cx.subscribe( &lhs_editor, |this, _, event: &EditorEvent, cx| match event { EditorEvent::ExpandExcerptsRequested { excerpt_ids, lines, direction, } => { if this.lhs.is_some() { let rhs_display_map = this.rhs_editor.read(cx).display_map.read(cx); let rhs_ids: Vec<_> = excerpt_ids .iter() .filter_map(|id| { rhs_display_map.companion_excerpt_to_my_excerpt(*id, cx) }) .collect(); this.expand_excerpts(rhs_ids.into_iter(), *lines, *direction, cx); } } EditorEvent::SelectionsChanged { .. } => { if let Some(lhs) = &mut this.lhs { lhs.has_latest_selection = true; } cx.emit(event.clone()); } _ => cx.emit(event.clone()), }, )]; let mut lhs = LhsEditor { editor: lhs_editor, multibuffer: lhs_multibuffer, has_latest_selection: false, _subscriptions: subscriptions, }; let rhs_display_map = self.rhs_editor.read(cx).display_map.clone(); let lhs_display_map = lhs.editor.read(cx).display_map.clone(); let rhs_display_map_id = rhs_display_map.entity_id(); self.rhs_editor.update(cx, |editor, cx| { editor.set_delegate_expand_excerpts(true); editor.buffer().update(cx, |rhs_multibuffer, cx| { rhs_multibuffer.set_show_deleted_hunks(false, cx); rhs_multibuffer.set_use_extended_diff_range(true, cx); }) }); let path_diffs: Vec<_> = { let rhs_multibuffer = self.rhs_multibuffer.read(cx); rhs_multibuffer .paths() .filter_map(|path| { let excerpt_id = rhs_multibuffer.excerpts_for_path(path).next()?; let snapshot = rhs_multibuffer.snapshot(cx); let buffer = snapshot.buffer_for_excerpt(excerpt_id)?; let diff = rhs_multibuffer.diff_for(buffer.remote_id())?; Some((path.clone(), diff)) }) .collect() }; let rhs_folded_buffers = rhs_display_map.read(cx).folded_buffers().clone(); let mut companion = Companion::new( rhs_display_map_id, rhs_folded_buffers, convert_rhs_rows_to_lhs, convert_lhs_rows_to_rhs, ); for (path, diff) in path_diffs { for (lhs, rhs) in lhs.update_path_excerpts_from_rhs(path, &self.rhs_multibuffer, diff.clone(), cx) { companion.add_excerpt_mapping(lhs, rhs); } companion.add_buffer_mapping( diff.read(cx).base_text(cx).remote_id(), diff.read(cx).buffer_id, ); } let companion = cx.new(|_| companion); rhs_display_map.update(cx, |dm, cx| { dm.set_companion(Some((lhs_display_map.downgrade(), companion.clone())), cx); }); lhs_display_map.update(cx, |dm, cx| { dm.set_companion(Some((rhs_display_map.downgrade(), companion)), cx); }); let shared_scroll_anchor = self .rhs_editor .read(cx) .scroll_manager .scroll_anchor_entity(); lhs.editor.update(cx, |editor, _cx| { editor .scroll_manager .set_shared_scroll_anchor(shared_scroll_anchor); }); let this = cx.entity().downgrade(); self.rhs_editor.update(cx, |editor, _cx| { let this = this.clone(); editor.set_on_local_selections_changed(Some(Box::new( move |cursor_position, window, cx| { let this = this.clone(); window.defer(cx, move |window, cx| { this.update(cx, |this, cx| { if this.locked_cursors { this.sync_cursor_to_other_side(true, cursor_position, window, cx); } }) .ok(); }) }, ))); }); lhs.editor.update(cx, |editor, _cx| { let this = this.clone(); editor.set_on_local_selections_changed(Some(Box::new(Lines 1–500 of 3519. Load more
use std::ops::{Bound, Range, RangeInclusive}; use buffer_diff::{BufferDiff, BufferDiffSnapshot}; use collections::HashMap; use feature_flags::{FeatureFlag, FeatureFlagAppExt as _}; use gpui::{Action, AppContext as _, Entity, EventEmitter, Focusable, Subscription, WeakEntity}; use language::{Buffer, Capability}; use multi_buffer::{ Anchor, ExcerptId, ExcerptRange, ExpandExcerptDirection, MultiBuffer, MultiBufferPoint, MultiBufferSnapshot, PathKey, }; use project::Project; use rope::Point; use text::{OffsetRangeExt as _, Patch, ToPoint as _}; use ui::{ App, Context, InteractiveElement as _, IntoElement as _, ParentElement as _, Render, Styled as _, Window, div, }; use crate::{ display_map::CompanionExcerptPatch, split_editor_view::{SplitEditorState, SplitEditorView}, }; use workspace::{ActivatePaneLeft, ActivatePaneRight, Item, Workspace}; use crate::{ Autoscroll, DisplayMap, Editor, EditorEvent, ToggleCodeActions, ToggleSoftWrap, actions::{DisableBreakpoint, EditLogBreakpoint, EnableBreakpoint, ToggleBreakpoint}, display_map::Companion, }; use zed_actions::assistant::InlineAssist; pub(crate) fn convert_lhs_rows_to_rhs( lhs_excerpt_to_rhs_excerpt: &HashMap<ExcerptId, ExcerptId>, rhs_snapshot: &MultiBufferSnapshot, lhs_snapshot: &MultiBufferSnapshot, lhs_bounds: (Bound<MultiBufferPoint>, Bound<MultiBufferPoint>), ) -> Vec<CompanionExcerptPatch> { patches_for_range( lhs_excerpt_to_rhs_excerpt, lhs_snapshot, rhs_snapshot, lhs_bounds, |diff, range, buffer| diff.patch_for_base_text_range(range, buffer), ) } pub(crate) fn convert_rhs_rows_to_lhs( rhs_excerpt_to_lhs_excerpt: &HashMap<ExcerptId, ExcerptId>, lhs_snapshot: &MultiBufferSnapshot, rhs_snapshot: &MultiBufferSnapshot, rhs_bounds: (Bound<MultiBufferPoint>, Bound<MultiBufferPoint>), ) -> Vec<CompanionExcerptPatch> { patches_for_range( rhs_excerpt_to_lhs_excerpt, rhs_snapshot, lhs_snapshot, rhs_bounds, |diff, range, buffer| diff.patch_for_buffer_range(range, buffer), ) } fn patches_for_range<F>( excerpt_map: &HashMap<ExcerptId, ExcerptId>, source_snapshot: &MultiBufferSnapshot, target_snapshot: &MultiBufferSnapshot, source_bounds: (Bound<MultiBufferPoint>, Bound<MultiBufferPoint>), translate_fn: F, ) -> Vec<CompanionExcerptPatch> where F: Fn(&BufferDiffSnapshot, RangeInclusive<Point>, &text::BufferSnapshot) -> Patch<Point>, { let mut result = Vec::new(); let mut patches = HashMap::default(); for (source_buffer, buffer_offset_range, source_excerpt_id) in source_snapshot.range_to_buffer_ranges(source_bounds) { let target_excerpt_id = excerpt_map.get(&source_excerpt_id).copied().unwrap(); let target_buffer = target_snapshot .buffer_for_excerpt(target_excerpt_id) .unwrap(); let patch = patches.entry(source_buffer.remote_id()).or_insert_with(|| { let diff = source_snapshot .diff_for_buffer_id(source_buffer.remote_id()) .unwrap(); let rhs_buffer = if source_buffer.remote_id() == diff.base_text().remote_id() { &target_buffer } else { source_buffer }; // TODO(split-diff) pass only the union of the ranges for the affected excerpts translate_fn(diff, Point::zero()..=source_buffer.max_point(), rhs_buffer) }); let buffer_point_range = buffer_offset_range.to_point(source_buffer); // TODO(split-diff) maybe narrow the patch to only the edited part of the excerpt // (less useful for project diff, but important if we want to do singleton side-by-side diff) result.push(patch_for_excerpt( source_snapshot, target_snapshot, source_excerpt_id, target_excerpt_id, source_buffer, target_buffer, patch, buffer_point_range, )); } result } fn patch_for_excerpt( source_snapshot: &MultiBufferSnapshot, target_snapshot: &MultiBufferSnapshot, source_excerpt_id: ExcerptId, target_excerpt_id: ExcerptId, source_buffer: &text::BufferSnapshot, target_buffer: &text::BufferSnapshot, patch: &Patch<Point>, source_edited_range: Range<Point>, ) -> CompanionExcerptPatch { let source_multibuffer_range = source_snapshot .range_for_excerpt(source_excerpt_id) .unwrap(); let source_excerpt_start_in_multibuffer = source_multibuffer_range.start; let source_context_range = source_snapshot .context_range_for_excerpt(source_excerpt_id) .unwrap(); let source_excerpt_start_in_buffer = source_context_range.start.to_point(&source_buffer); let source_excerpt_end_in_buffer = source_context_range.end.to_point(&source_buffer); let target_multibuffer_range = target_snapshot .range_for_excerpt(target_excerpt_id) .unwrap(); let target_excerpt_start_in_multibuffer = target_multibuffer_range.start; let target_context_range = target_snapshot .context_range_for_excerpt(target_excerpt_id) .unwrap(); let target_excerpt_start_in_buffer = target_context_range.start.to_point(&target_buffer); let target_excerpt_end_in_buffer = target_context_range.end.to_point(&target_buffer); let edits = patch .edits() .iter() .skip_while(|edit| edit.old.end < source_excerpt_start_in_buffer) .take_while(|edit| edit.old.start <= source_excerpt_end_in_buffer) .map(|edit| { let clamped_source_start = edit .old .start .max(source_excerpt_start_in_buffer) .min(source_excerpt_end_in_buffer); let clamped_source_end = edit .old .end .max(source_excerpt_start_in_buffer) .min(source_excerpt_end_in_buffer); let source_multibuffer_start = source_excerpt_start_in_multibuffer + (clamped_source_start - source_excerpt_start_in_buffer); let source_multibuffer_end = source_excerpt_start_in_multibuffer + (clamped_source_end - source_excerpt_start_in_buffer); let clamped_target_start = edit .new .start .max(target_excerpt_start_in_buffer) .min(target_excerpt_end_in_buffer); let clamped_target_end = edit .new .end .max(target_excerpt_start_in_buffer) .min(target_excerpt_end_in_buffer); let target_multibuffer_start = target_excerpt_start_in_multibuffer + (clamped_target_start - target_excerpt_start_in_buffer); let target_multibuffer_end = target_excerpt_start_in_multibuffer + (clamped_target_end - target_excerpt_start_in_buffer); text::Edit { old: source_multibuffer_start..source_multibuffer_end, new: target_multibuffer_start..target_multibuffer_end, } }); let edits = [text::Edit { old: source_excerpt_start_in_multibuffer..source_excerpt_start_in_multibuffer, new: target_excerpt_start_in_multibuffer..target_excerpt_start_in_multibuffer, }] .into_iter() .chain(edits); let mut merged_edits: Vec<text::Edit<Point>> = Vec::new(); for edit in edits { if let Some(last) = merged_edits.last_mut() { if edit.new.start <= last.new.end { last.old.end = last.old.end.max(edit.old.end); last.new.end = last.new.end.max(edit.new.end); continue; } } merged_edits.push(edit); } let edited_range = source_excerpt_start_in_multibuffer + (source_edited_range.start - source_excerpt_start_in_buffer) ..source_excerpt_start_in_multibuffer + (source_edited_range.end - source_excerpt_start_in_buffer); let source_excerpt_end = source_excerpt_start_in_multibuffer + (source_excerpt_end_in_buffer - source_excerpt_start_in_buffer); let target_excerpt_end = target_excerpt_start_in_multibuffer + (target_excerpt_end_in_buffer - target_excerpt_start_in_buffer); CompanionExcerptPatch { patch: Patch::new(merged_edits), edited_range, source_excerpt_range: source_excerpt_start_in_multibuffer..source_excerpt_end, target_excerpt_range: target_excerpt_start_in_multibuffer..target_excerpt_end, } } pub struct SplitDiffFeatureFlag; impl FeatureFlag for SplitDiffFeatureFlag { const NAME: &'static str = "split-diff"; fn enabled_for_staff() -> bool { true } } #[derive(Clone, Copy, PartialEq, Eq, Action, Default)] #[action(namespace = editor)] struct SplitDiff; #[derive(Clone, Copy, PartialEq, Eq, Action, Default)] #[action(namespace = editor)] struct UnsplitDiff; #[derive(Clone, Copy, PartialEq, Eq, Action, Default)] #[action(namespace = editor)] pub struct ToggleSplitDiff; #[derive(Clone, Copy, PartialEq, Eq, Action, Default)] #[action(namespace = editor)] struct JumpToCorrespondingRow; /// When locked cursors mode is enabled, cursor movements in one editor will /// update the cursor position in the other editor to the corresponding row. #[derive(Clone, Copy, PartialEq, Eq, Action, Default)] #[action(namespace = editor)] pub struct ToggleLockedCursors; pub struct SplittableEditor { rhs_multibuffer: Entity<MultiBuffer>, rhs_editor: Entity<Editor>, lhs: Option<LhsEditor>, workspace: WeakEntity<Workspace>, split_state: Entity<SplitEditorState>, locked_cursors: bool, _subscriptions: Vec<Subscription>, } struct LhsEditor { multibuffer: Entity<MultiBuffer>, editor: Entity<Editor>, has_latest_selection: bool, _subscriptions: Vec<Subscription>, } impl SplittableEditor { pub fn rhs_editor(&self) -> &Entity<Editor> { &self.rhs_editor } pub fn lhs_editor(&self) -> Option<&Entity<Editor>> { self.lhs.as_ref().map(|s| &s.editor) } pub fn is_split(&self) -> bool { self.lhs.is_some() } pub fn last_selected_editor(&self) -> &Entity<Editor> { if let Some(lhs) = &self.lhs && lhs.has_latest_selection { &lhs.editor } else { &self.rhs_editor } } pub fn new_unsplit( rhs_multibuffer: Entity<MultiBuffer>, project: Entity<Project>, workspace: Entity<Workspace>, window: &mut Window, cx: &mut Context<Self>, ) -> Self { let rhs_editor = cx.new(|cx| { let mut editor = Editor::for_multibuffer(rhs_multibuffer.clone(), Some(project.clone()), window, cx); editor.set_expand_all_diff_hunks(cx); editor }); // TODO(split-diff) we might want to tag editor events with whether they came from rhs/lhs let subscriptions = vec![cx.subscribe( &rhs_editor, |this, _, event: &EditorEvent, cx| match event { EditorEvent::ExpandExcerptsRequested { excerpt_ids, lines, direction, } => { this.expand_excerpts(excerpt_ids.iter().copied(), *lines, *direction, cx); } EditorEvent::SelectionsChanged { .. } => { if let Some(lhs) = &mut this.lhs { lhs.has_latest_selection = false; } cx.emit(event.clone()); } _ => cx.emit(event.clone()), }, )]; window.defer(cx, { let workspace = workspace.downgrade(); let rhs_editor = rhs_editor.downgrade(); move |window, cx| { workspace .update(cx, |workspace, cx| { rhs_editor.update(cx, |editor, cx| { editor.added_to_workspace(workspace, window, cx); }) }) .ok(); } }); let split_state = cx.new(|cx| SplitEditorState::new(cx)); Self { rhs_editor, rhs_multibuffer, lhs: None, workspace: workspace.downgrade(), split_state, locked_cursors: false, _subscriptions: subscriptions, } } fn split(&mut self, _: &SplitDiff, window: &mut Window, cx: &mut Context<Self>) { if !cx.has_flag::<SplitDiffFeatureFlag>() { return; } if self.lhs.is_some() { return; } let Some(workspace) = self.workspace.upgrade() else { return; }; let project = workspace.read(cx).project().clone(); let lhs_multibuffer = cx.new(|cx| { let mut multibuffer = MultiBuffer::new(Capability::ReadOnly); multibuffer.set_all_diff_hunks_expanded(cx); multibuffer }); let lhs_editor = cx.new(|cx| { let mut editor = Editor::for_multibuffer(lhs_multibuffer.clone(), Some(project.clone()), window, cx); editor.set_number_deleted_lines(true, cx); editor.set_delegate_expand_excerpts(true); editor.set_show_vertical_scrollbar(false, cx); editor.set_minimap_visibility(crate::MinimapVisibility::Disabled, window, cx); editor }); let subscriptions = vec![cx.subscribe( &lhs_editor, |this, _, event: &EditorEvent, cx| match event { EditorEvent::ExpandExcerptsRequested { excerpt_ids, lines, direction, } => { if this.lhs.is_some() { let rhs_display_map = this.rhs_editor.read(cx).display_map.read(cx); let rhs_ids: Vec<_> = excerpt_ids .iter() .filter_map(|id| { rhs_display_map.companion_excerpt_to_my_excerpt(*id, cx) }) .collect(); this.expand_excerpts(rhs_ids.into_iter(), *lines, *direction, cx); } } EditorEvent::SelectionsChanged { .. } => { if let Some(lhs) = &mut this.lhs { lhs.has_latest_selection = true; } cx.emit(event.clone()); } _ => cx.emit(event.clone()), }, )]; let mut lhs = LhsEditor { editor: lhs_editor, multibuffer: lhs_multibuffer, has_latest_selection: false, _subscriptions: subscriptions, }; let rhs_display_map = self.rhs_editor.read(cx).display_map.clone(); let lhs_display_map = lhs.editor.read(cx).display_map.clone(); let rhs_display_map_id = rhs_display_map.entity_id(); self.rhs_editor.update(cx, |editor, cx| { editor.set_delegate_expand_excerpts(true); editor.buffer().update(cx, |rhs_multibuffer, cx| { rhs_multibuffer.set_show_deleted_hunks(false, cx); rhs_multibuffer.set_use_extended_diff_range(true, cx); }) }); let path_diffs: Vec<_> = { let rhs_multibuffer = self.rhs_multibuffer.read(cx); rhs_multibuffer .paths() .filter_map(|path| { let excerpt_id = rhs_multibuffer.excerpts_for_path(path).next()?; let snapshot = rhs_multibuffer.snapshot(cx); let buffer = snapshot.buffer_for_excerpt(excerpt_id)?; let diff = rhs_multibuffer.diff_for(buffer.remote_id())?; Some((path.clone(), diff)) }) .collect() }; let rhs_folded_buffers = rhs_display_map.read(cx).folded_buffers().clone(); let mut companion = Companion::new( rhs_display_map_id, rhs_folded_buffers, convert_rhs_rows_to_lhs, convert_lhs_rows_to_rhs, ); for (path, diff) in path_diffs { for (lhs, rhs) in lhs.update_path_excerpts_from_rhs(path, &self.rhs_multibuffer, diff.clone(), cx) { companion.add_excerpt_mapping(lhs, rhs); } companion.add_buffer_mapping( diff.read(cx).base_text(cx).remote_id(), diff.read(cx).buffer_id, ); } let companion = cx.new(|_| companion); rhs_display_map.update(cx, |dm, cx| { dm.set_companion(Some((lhs_display_map.downgrade(), companion.clone())), cx); }); lhs_display_map.update(cx, |dm, cx| { dm.set_companion(Some((rhs_display_map.downgrade(), companion)), cx); }); let shared_scroll_anchor = self .rhs_editor .read(cx) .scroll_manager .scroll_anchor_entity(); lhs.editor.update(cx, |editor, _cx| { editor .scroll_manager .set_shared_scroll_anchor(shared_scroll_anchor); }); let this = cx.entity().downgrade(); self.rhs_editor.update(cx, |editor, _cx| { let this = this.clone(); editor.set_on_local_selections_changed(Some(Box::new( move |cursor_position, window, cx| { let this = this.clone(); window.defer(cx, move |window, cx| { this.update(cx, |this, cx| { if this.locked_cursors { this.sync_cursor_to_other_side(true, cursor_position, window, cx); } }) .ok(); }) }, ))); }); lhs.editor.update(cx, |editor, _cx| { let this = this.clone(); editor.set_on_local_selections_changed(Some(Box::new(