use std::{ ops::{Bound, Range, RangeInclusive}, sync::Arc,}; use buffer_diff::{BufferDiff, BufferDiffSnapshot};use collections::HashMap; use gpui::{ Action, AppContext as _, Entity, EventEmitter, Focusable, Font, Subscription, WeakEntity,};use itertools::Itertools;use language::{Buffer, Capability, HighlightedText};use multi_buffer::{ Anchor, BufferOffset, ExcerptId, ExcerptRange, ExpandExcerptDirection, MultiBuffer, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferSnapshot, PathKey,};use project::Project;use rope::Point;use settings::DiffViewStyle;use text::{Bias, BufferId, 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, element::SplitSide, split_editor_view::{SplitEditorState, SplitEditorView},};use workspace::{ ActivatePaneLeft, ActivatePaneRight, Item, ToolbarItemLocation, Workspace, item::{ItemBufferKind, ItemEvent, SaveOptions, TabContentParams}, searchable::{SearchEvent, SearchToken, SearchableItem, SearchableItemHandle},}; use crate::{ Autoscroll, Editor, EditorEvent, RenderDiffHunkControlsFn, 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 translate_lhs_selections_to_rhs( selections_by_buffer: &HashMap<BufferId, (Vec<Range<BufferOffset>>, Option<u32>)>, splittable: &SplittableEditor, cx: &App,) -> HashMap<Entity<Buffer>, (Vec<Range<BufferOffset>>, Option<u32>)> { let rhs_display_map = splittable.rhs_editor.read(cx).display_map.read(cx); let Some(companion) = rhs_display_map.companion() else { return HashMap::default(); }; let companion = companion.read(cx); let mut translated: HashMap<Entity<Buffer>, (Vec<Range<BufferOffset>>, Option<u32>)> = HashMap::default(); for (lhs_buffer_id, (ranges, scroll_offset)) in selections_by_buffer { let Some(rhs_buffer_id) = companion.lhs_to_rhs_buffer(*lhs_buffer_id) else { continue; }; let Some(rhs_buffer) = splittable .rhs_editor .read(cx) .buffer() .read(cx) .buffer(rhs_buffer_id) else { continue; }; let Some(diff) = splittable .rhs_editor .read(cx) .buffer() .read(cx) .diff_for(rhs_buffer_id) else { continue; }; let diff_snapshot = diff.read(cx).snapshot(cx); let rhs_buffer_snapshot = rhs_buffer.read(cx).snapshot(); let base_text_buffer = diff.read(cx).base_text_buffer(); let base_text_snapshot = base_text_buffer.read(cx).snapshot(); let translated_ranges: Vec<Range<BufferOffset>> = ranges .iter() .map(|range| { let start_point = base_text_snapshot.offset_to_point(range.start.0); let end_point = base_text_snapshot.offset_to_point(range.end.0); let rhs_start = diff_snapshot .base_text_point_to_buffer_point(start_point, &rhs_buffer_snapshot); let rhs_end = diff_snapshot.base_text_point_to_buffer_point(end_point, &rhs_buffer_snapshot); BufferOffset(rhs_buffer_snapshot.point_to_offset(rhs_start)) ..BufferOffset(rhs_buffer_snapshot.point_to_offset(rhs_end)) }) .collect(); translated.insert(rhs_buffer, (translated_ranges, *scroll_offset)); } translated} fn translate_lhs_hunks_to_rhs( lhs_hunks: &[MultiBufferDiffHunk], splittable: &SplittableEditor, cx: &App,) -> Vec<MultiBufferDiffHunk> { let rhs_display_map = splittable.rhs_editor.read(cx).display_map.read(cx); let Some(companion) = rhs_display_map.companion() else { return vec![]; }; let companion = companion.read(cx); let rhs_snapshot = splittable.rhs_multibuffer.read(cx).snapshot(cx); let rhs_hunks: Vec<MultiBufferDiffHunk> = rhs_snapshot.diff_hunks().collect(); let mut translated = Vec::new(); for lhs_hunk in lhs_hunks { let Some(rhs_buffer_id) = companion.lhs_to_rhs_buffer(lhs_hunk.buffer_id) else { continue; }; if let Some(rhs_hunk) = rhs_hunks.iter().find(|rhs_hunk| { rhs_hunk.buffer_id == rhs_buffer_id && rhs_hunk.diff_base_byte_range == lhs_hunk.diff_base_byte_range }) { translated.push(rhs_hunk.clone()); } } translated} 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>,{ struct PendingExcerpt<'a> { source_excerpt_id: ExcerptId, target_excerpt_id: ExcerptId, source_buffer: &'a text::BufferSnapshot, target_buffer: &'a text::BufferSnapshot, buffer_point_range: Range<Point>, source_context_range: Range<Point>, } let mut result = Vec::new(); let mut current_buffer_id: Option<BufferId> = None; let mut pending_excerpts: Vec<PendingExcerpt> = Vec::new(); let mut union_context_start: Option<Point> = None; let mut union_context_end: Option<Point> = None; let flush_buffer = |pending: &mut Vec<PendingExcerpt>, union_start: Point, union_end: Point, result: &mut Vec<CompanionExcerptPatch>| { let Some(first) = pending.first() else { return; }; let diff = source_snapshot .diff_for_buffer_id(first.source_buffer.remote_id()) .expect("buffer with no diff when creating patches"); let rhs_buffer = if first.source_buffer.remote_id() == diff.base_text().remote_id() { first.target_buffer } else { first.source_buffer }; let patch = translate_fn(diff, union_start..=union_end, rhs_buffer); for excerpt in pending.drain(..) { result.push(patch_for_excerpt( source_snapshot, target_snapshot, excerpt.source_excerpt_id, excerpt.target_excerpt_id, excerpt.target_buffer, excerpt.source_context_range, &patch, excerpt.buffer_point_range, )); } }; for (source_buffer, buffer_offset_range, source_excerpt_id, source_context_range) in source_snapshot.range_to_buffer_ranges_with_context(source_bounds) { let Some(target_excerpt_id) = excerpt_map.get(&source_excerpt_id).copied() else { continue; }; let Some(target_buffer) = target_snapshot.buffer_for_excerpt(target_excerpt_id) else { continue; }; let buffer_id = source_buffer.remote_id(); if current_buffer_id != Some(buffer_id) { if let (Some(start), Some(end)) = (union_context_start.take(), union_context_end.take()) { flush_buffer(&mut pending_excerpts, start, end, &mut result); } current_buffer_id = Some(buffer_id); } let buffer_point_range = buffer_offset_range.to_point(source_buffer); let source_context_range = source_context_range.to_point(source_buffer); union_context_start = Some(union_context_start.map_or(source_context_range.start, |s| { s.min(source_context_range.start) })); union_context_end = Some(union_context_end.map_or(source_context_range.end, |e| { e.max(source_context_range.end) })); pending_excerpts.push(PendingExcerpt { source_excerpt_id, target_excerpt_id, source_buffer, target_buffer, buffer_point_range, source_context_range, }); } if let (Some(start), Some(end)) = (union_context_start, union_context_end) { flush_buffer(&mut pending_excerpts, start, end, &mut result); } result} fn patch_for_excerpt( source_snapshot: &MultiBufferSnapshot, target_snapshot: &MultiBufferSnapshot, source_excerpt_id: ExcerptId, target_excerpt_id: ExcerptId, target_buffer: &text::BufferSnapshot, source_context_range: Range<Point>, patch: &Patch<Point>, source_edited_range: Range<Point>,) -> CompanionExcerptPatch { let source_multibuffer_range = source_snapshot .range_for_excerpt(source_excerpt_id) .expect("no excerpt for source id when creating patch"); let source_excerpt_start_in_multibuffer = source_multibuffer_range.start; let source_excerpt_start_in_buffer = source_context_range.start; let source_excerpt_end_in_buffer = source_context_range.end; let target_multibuffer_range = target_snapshot .range_for_excerpt(target_excerpt_id) .expect("no excerpt for target id when creating patch"); let target_excerpt_start_in_multibuffer = target_multibuffer_range.start; let target_context_range = target_snapshot .context_range_for_excerpt(target_excerpt_id) .expect("no range for target id when creating patch"); 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); let clamped_source_end = edit.old.end.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 || edit.old.start <= last.old.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, }} #[derive(Clone, Copy, PartialEq, Eq, Action, Default)]#[action(namespace = editor)]pub struct ToggleSplitDiff; pub struct SplittableEditor { rhs_multibuffer: Entity<MultiBuffer>, rhs_editor: Entity<Editor>, lhs: Option<LhsEditor>, workspace: WeakEntity<Workspace>, split_state: Entity<SplitEditorState>, searched_side: Option<SplitSide>, _subscriptions: Vec<Subscription>,} struct LhsEditor { multibuffer: Entity<MultiBuffer>, editor: Entity<Editor>, was_last_focused: 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 set_render_diff_hunk_controls( &self, render_diff_hunk_controls: RenderDiffHunkControlsFn, cx: &mut Context<Self>, ) { self.rhs_editor.update(cx, |editor, cx| { editor.set_render_diff_hunk_controls(render_diff_hunk_controls.clone(), cx); }); if let Some(lhs) = &self.lhs { lhs.editor.update(cx, |editor, cx| { editor.set_render_diff_hunk_controls(render_diff_hunk_controls.clone(), cx); }); } } fn focused_side(&self) -> SplitSide { if let Some(lhs) = &self.lhs && lhs.was_last_focused { SplitSide::Left } else { SplitSide::Right } } pub fn focused_editor(&self) -> &Entity<Editor> { if let Some(lhs) = &self.lhs && lhs.was_last_focused { &lhs.editor } else { &self.rhs_editor } } pub fn new( style: DiffViewStyle, 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.disable_runnables(); editor.disable_diagnostics(cx); editor.set_minimap_visibility(crate::MinimapVisibility::Disabled, window, 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); } _ => cx.emit(event.clone()), }, ), cx.subscribe(&rhs_editor, |this, _, event: &SearchEvent, cx| { if this.searched_side.is_none() || this.searched_side == Some(SplitSide::Right) { cx.emit(event.clone()); } }), ]; let this = cx.weak_entity(); 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(); }) .ok(); if style == DiffViewStyle::Split { this.update(cx, |this, cx| { this.split(window, cx); }) .ok(); } } }); let split_state = cx.new(|cx| SplitEditorState::new(cx));Lines 1–500 of 6061. Load more
use std::{ ops::{Bound, Range, RangeInclusive}, sync::Arc, }; use buffer_diff::{BufferDiff, BufferDiffSnapshot}; use collections::HashMap; use gpui::{ Action, AppContext as _, Entity, EventEmitter, Focusable, Font, Subscription, WeakEntity, }; use itertools::Itertools; use language::{Buffer, Capability, HighlightedText}; use multi_buffer::{ Anchor, BufferOffset, ExcerptId, ExcerptRange, ExpandExcerptDirection, MultiBuffer, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferSnapshot, PathKey, }; use project::Project; use rope::Point; use settings::DiffViewStyle; use text::{Bias, BufferId, 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, element::SplitSide, split_editor_view::{SplitEditorState, SplitEditorView}, }; use workspace::{ ActivatePaneLeft, ActivatePaneRight, Item, ToolbarItemLocation, Workspace, item::{ItemBufferKind, ItemEvent, SaveOptions, TabContentParams}, searchable::{SearchEvent, SearchToken, SearchableItem, SearchableItemHandle}, }; use crate::{ Autoscroll, Editor, EditorEvent, RenderDiffHunkControlsFn, 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 translate_lhs_selections_to_rhs( selections_by_buffer: &HashMap<BufferId, (Vec<Range<BufferOffset>>, Option<u32>)>, splittable: &SplittableEditor, cx: &App, ) -> HashMap<Entity<Buffer>, (Vec<Range<BufferOffset>>, Option<u32>)> { let rhs_display_map = splittable.rhs_editor.read(cx).display_map.read(cx); let Some(companion) = rhs_display_map.companion() else { return HashMap::default(); }; let companion = companion.read(cx); let mut translated: HashMap<Entity<Buffer>, (Vec<Range<BufferOffset>>, Option<u32>)> = HashMap::default(); for (lhs_buffer_id, (ranges, scroll_offset)) in selections_by_buffer { let Some(rhs_buffer_id) = companion.lhs_to_rhs_buffer(*lhs_buffer_id) else { continue; }; let Some(rhs_buffer) = splittable .rhs_editor .read(cx) .buffer() .read(cx) .buffer(rhs_buffer_id) else { continue; }; let Some(diff) = splittable .rhs_editor .read(cx) .buffer() .read(cx) .diff_for(rhs_buffer_id) else { continue; }; let diff_snapshot = diff.read(cx).snapshot(cx); let rhs_buffer_snapshot = rhs_buffer.read(cx).snapshot(); let base_text_buffer = diff.read(cx).base_text_buffer(); let base_text_snapshot = base_text_buffer.read(cx).snapshot(); let translated_ranges: Vec<Range<BufferOffset>> = ranges .iter() .map(|range| { let start_point = base_text_snapshot.offset_to_point(range.start.0); let end_point = base_text_snapshot.offset_to_point(range.end.0); let rhs_start = diff_snapshot .base_text_point_to_buffer_point(start_point, &rhs_buffer_snapshot); let rhs_end = diff_snapshot.base_text_point_to_buffer_point(end_point, &rhs_buffer_snapshot); BufferOffset(rhs_buffer_snapshot.point_to_offset(rhs_start)) ..BufferOffset(rhs_buffer_snapshot.point_to_offset(rhs_end)) }) .collect(); translated.insert(rhs_buffer, (translated_ranges, *scroll_offset)); } translated } fn translate_lhs_hunks_to_rhs( lhs_hunks: &[MultiBufferDiffHunk], splittable: &SplittableEditor, cx: &App, ) -> Vec<MultiBufferDiffHunk> { let rhs_display_map = splittable.rhs_editor.read(cx).display_map.read(cx); let Some(companion) = rhs_display_map.companion() else { return vec![]; }; let companion = companion.read(cx); let rhs_snapshot = splittable.rhs_multibuffer.read(cx).snapshot(cx); let rhs_hunks: Vec<MultiBufferDiffHunk> = rhs_snapshot.diff_hunks().collect(); let mut translated = Vec::new(); for lhs_hunk in lhs_hunks { let Some(rhs_buffer_id) = companion.lhs_to_rhs_buffer(lhs_hunk.buffer_id) else { continue; }; if let Some(rhs_hunk) = rhs_hunks.iter().find(|rhs_hunk| { rhs_hunk.buffer_id == rhs_buffer_id && rhs_hunk.diff_base_byte_range == lhs_hunk.diff_base_byte_range }) { translated.push(rhs_hunk.clone()); } } translated } 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>, { struct PendingExcerpt<'a> { source_excerpt_id: ExcerptId, target_excerpt_id: ExcerptId, source_buffer: &'a text::BufferSnapshot, target_buffer: &'a text::BufferSnapshot, buffer_point_range: Range<Point>, source_context_range: Range<Point>, } let mut result = Vec::new(); let mut current_buffer_id: Option<BufferId> = None; let mut pending_excerpts: Vec<PendingExcerpt> = Vec::new(); let mut union_context_start: Option<Point> = None; let mut union_context_end: Option<Point> = None; let flush_buffer = |pending: &mut Vec<PendingExcerpt>, union_start: Point, union_end: Point, result: &mut Vec<CompanionExcerptPatch>| { let Some(first) = pending.first() else { return; }; let diff = source_snapshot .diff_for_buffer_id(first.source_buffer.remote_id()) .expect("buffer with no diff when creating patches"); let rhs_buffer = if first.source_buffer.remote_id() == diff.base_text().remote_id() { first.target_buffer } else { first.source_buffer }; let patch = translate_fn(diff, union_start..=union_end, rhs_buffer); for excerpt in pending.drain(..) { result.push(patch_for_excerpt( source_snapshot, target_snapshot, excerpt.source_excerpt_id, excerpt.target_excerpt_id, excerpt.target_buffer, excerpt.source_context_range, &patch, excerpt.buffer_point_range, )); } }; for (source_buffer, buffer_offset_range, source_excerpt_id, source_context_range) in source_snapshot.range_to_buffer_ranges_with_context(source_bounds) { let Some(target_excerpt_id) = excerpt_map.get(&source_excerpt_id).copied() else { continue; }; let Some(target_buffer) = target_snapshot.buffer_for_excerpt(target_excerpt_id) else { continue; }; let buffer_id = source_buffer.remote_id(); if current_buffer_id != Some(buffer_id) { if let (Some(start), Some(end)) = (union_context_start.take(), union_context_end.take()) { flush_buffer(&mut pending_excerpts, start, end, &mut result); } current_buffer_id = Some(buffer_id); } let buffer_point_range = buffer_offset_range.to_point(source_buffer); let source_context_range = source_context_range.to_point(source_buffer); union_context_start = Some(union_context_start.map_or(source_context_range.start, |s| { s.min(source_context_range.start) })); union_context_end = Some(union_context_end.map_or(source_context_range.end, |e| { e.max(source_context_range.end) })); pending_excerpts.push(PendingExcerpt { source_excerpt_id, target_excerpt_id, source_buffer, target_buffer, buffer_point_range, source_context_range, }); } if let (Some(start), Some(end)) = (union_context_start, union_context_end) { flush_buffer(&mut pending_excerpts, start, end, &mut result); } result } fn patch_for_excerpt( source_snapshot: &MultiBufferSnapshot, target_snapshot: &MultiBufferSnapshot, source_excerpt_id: ExcerptId, target_excerpt_id: ExcerptId, target_buffer: &text::BufferSnapshot, source_context_range: Range<Point>, patch: &Patch<Point>, source_edited_range: Range<Point>, ) -> CompanionExcerptPatch { let source_multibuffer_range = source_snapshot .range_for_excerpt(source_excerpt_id) .expect("no excerpt for source id when creating patch"); let source_excerpt_start_in_multibuffer = source_multibuffer_range.start; let source_excerpt_start_in_buffer = source_context_range.start; let source_excerpt_end_in_buffer = source_context_range.end; let target_multibuffer_range = target_snapshot .range_for_excerpt(target_excerpt_id) .expect("no excerpt for target id when creating patch"); let target_excerpt_start_in_multibuffer = target_multibuffer_range.start; let target_context_range = target_snapshot .context_range_for_excerpt(target_excerpt_id) .expect("no range for target id when creating patch"); 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); let clamped_source_end = edit.old.end.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 || edit.old.start <= last.old.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, } } #[derive(Clone, Copy, PartialEq, Eq, Action, Default)] #[action(namespace = editor)] pub struct ToggleSplitDiff; pub struct SplittableEditor { rhs_multibuffer: Entity<MultiBuffer>, rhs_editor: Entity<Editor>, lhs: Option<LhsEditor>, workspace: WeakEntity<Workspace>, split_state: Entity<SplitEditorState>, searched_side: Option<SplitSide>, _subscriptions: Vec<Subscription>, } struct LhsEditor { multibuffer: Entity<MultiBuffer>, editor: Entity<Editor>, was_last_focused: 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 set_render_diff_hunk_controls( &self, render_diff_hunk_controls: RenderDiffHunkControlsFn, cx: &mut Context<Self>, ) { self.rhs_editor.update(cx, |editor, cx| { editor.set_render_diff_hunk_controls(render_diff_hunk_controls.clone(), cx); }); if let Some(lhs) = &self.lhs { lhs.editor.update(cx, |editor, cx| { editor.set_render_diff_hunk_controls(render_diff_hunk_controls.clone(), cx); }); } } fn focused_side(&self) -> SplitSide { if let Some(lhs) = &self.lhs && lhs.was_last_focused { SplitSide::Left } else { SplitSide::Right } } pub fn focused_editor(&self) -> &Entity<Editor> { if let Some(lhs) = &self.lhs && lhs.was_last_focused { &lhs.editor } else { &self.rhs_editor } } pub fn new( style: DiffViewStyle, 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.disable_runnables(); editor.disable_diagnostics(cx); editor.set_minimap_visibility(crate::MinimapVisibility::Disabled, window, 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); } _ => cx.emit(event.clone()), }, ), cx.subscribe(&rhs_editor, |this, _, event: &SearchEvent, cx| { if this.searched_side.is_none() || this.searched_side == Some(SplitSide::Right) { cx.emit(event.clone()); } }), ]; let this = cx.weak_entity(); 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(); }) .ok(); if style == DiffViewStyle::Split { this.update(cx, |this, cx| { this.split(window, cx); }) .ok(); } } }); let split_state = cx.new(|cx| SplitEditorState::new(cx));