856 lines · 31.7 KB · Rust
RawDownload
use std::ops::Range; use buffer_diff::BufferDiff;use collections::HashMap;use feature_flags::{FeatureFlag, FeatureFlagAppExt as _};use gpui::{    Action, AppContext as _, Entity, EventEmitter, Focusable, NoAction, Subscription, WeakEntity,};use language::{Buffer, Capability};use multi_buffer::{Anchor, ExcerptId, ExcerptRange, ExpandExcerptDirection, MultiBuffer, PathKey};use project::Project;use rope::Point;use text::{Bias, OffsetRangeExt as _};use ui::{    App, Context, InteractiveElement as _, IntoElement as _, ParentElement as _, Render,    Styled as _, Window, div,};use workspace::{    ActivePaneDecorator, Item, ItemHandle, Pane, PaneGroup, SplitDirection, Workspace,}; use crate::{Editor, EditorEvent}; 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; pub struct SplittableEditor {    primary_multibuffer: Entity<MultiBuffer>,    primary_editor: Entity<Editor>,    secondary: Option<SecondaryEditor>,    panes: PaneGroup,    workspace: WeakEntity<Workspace>,    _subscriptions: Vec<Subscription>,} struct SecondaryEditor {    multibuffer: Entity<MultiBuffer>,    editor: Entity<Editor>,    pane: Entity<Pane>,    has_latest_selection: bool,    primary_to_secondary: HashMap<ExcerptId, ExcerptId>,    secondary_to_primary: HashMap<ExcerptId, ExcerptId>,    _subscriptions: Vec<Subscription>,} impl SplittableEditor {    pub fn primary_editor(&self) -> &Entity<Editor> {        &self.primary_editor    }     pub fn last_selected_editor(&self) -> &Entity<Editor> {        if let Some(secondary) = &self.secondary            && secondary.has_latest_selection        {            &secondary.editor        } else {            &self.primary_editor        }    }     pub fn new_unsplit(        primary_multibuffer: Entity<MultiBuffer>,        project: Entity<Project>,        workspace: Entity<Workspace>,        window: &mut Window,        cx: &mut Context<Self>,    ) -> Self {        let primary_editor = cx.new(|cx| {            let mut editor = Editor::for_multibuffer(                primary_multibuffer.clone(),                Some(project.clone()),                window,                cx,            );            editor.set_expand_all_diff_hunks(cx);            editor        });        let pane = cx.new(|cx| {            let mut pane = Pane::new(                workspace.downgrade(),                project,                Default::default(),                None,                NoAction.boxed_clone(),                true,                window,                cx,            );            pane.set_should_display_tab_bar(|_, _| false);            pane.add_item(primary_editor.boxed_clone(), true, true, None, window, cx);            pane        });        let panes = PaneGroup::new(pane);        // TODO(split-diff) we might want to tag editor events with whether they came from primary/secondary        let subscriptions = vec![cx.subscribe(            &primary_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(secondary) = &mut this.secondary {                        secondary.has_latest_selection = false;                    }                    cx.emit(event.clone());                }                _ => cx.emit(event.clone()),            },        )];         window.defer(cx, {            let workspace = workspace.downgrade();            let primary_editor = primary_editor.downgrade();            move |window, cx| {                workspace                    .update(cx, |workspace, cx| {                        primary_editor.update(cx, |editor, cx| {                            editor.added_to_workspace(workspace, window, cx);                        })                    })                    .ok();            }        });        Self {            primary_editor,            primary_multibuffer,            secondary: None,            panes,            workspace: workspace.downgrade(),            _subscriptions: subscriptions,        }    }     fn split(&mut self, _: &SplitDiff, window: &mut Window, cx: &mut Context<Self>) {        if !cx.has_flag::<SplitDiffFeatureFlag>() {            return;        }        if self.secondary.is_some() {            return;        }        let Some(workspace) = self.workspace.upgrade() else {            return;        };        let project = workspace.read(cx).project().clone();         let secondary_multibuffer = cx.new(|cx| {            let mut multibuffer = MultiBuffer::new(Capability::ReadOnly);            multibuffer.set_all_diff_hunks_expanded(cx);            multibuffer        });        let secondary_editor = cx.new(|cx| {            let mut editor = Editor::for_multibuffer(                secondary_multibuffer.clone(),                Some(project.clone()),                window,                cx,            );            editor.number_deleted_lines = true;            editor.set_delegate_expand_excerpts(true);            editor        });        let secondary_pane = cx.new(|cx| {            let mut pane = Pane::new(                workspace.downgrade(),                workspace.read(cx).project().clone(),                Default::default(),                None,                NoAction.boxed_clone(),                true,                window,                cx,            );            pane.set_should_display_tab_bar(|_, _| false);            pane.add_item(                ItemHandle::boxed_clone(&secondary_editor),                false,                false,                None,                window,                cx,            );            pane        });         let subscriptions = vec![cx.subscribe(            &secondary_editor,            |this, _, event: &EditorEvent, cx| match event {                EditorEvent::ExpandExcerptsRequested {                    excerpt_ids,                    lines,                    direction,                } => {                    if let Some(secondary) = &this.secondary {                        let primary_ids: Vec<_> = excerpt_ids                            .iter()                            .filter_map(|id| secondary.secondary_to_primary.get(id).copied())                            .collect();                        this.expand_excerpts(primary_ids.into_iter(), *lines, *direction, cx);                    }                }                EditorEvent::SelectionsChanged { .. } => {                    if let Some(secondary) = &mut this.secondary {                        secondary.has_latest_selection = true;                    }                    cx.emit(event.clone());                }                _ => cx.emit(event.clone()),            },        )];        let mut secondary = SecondaryEditor {            editor: secondary_editor,            multibuffer: secondary_multibuffer,            pane: secondary_pane.clone(),            has_latest_selection: false,            primary_to_secondary: HashMap::default(),            secondary_to_primary: HashMap::default(),            _subscriptions: subscriptions,        };        self.primary_editor.update(cx, |editor, cx| {            editor.set_delegate_expand_excerpts(true);            editor.buffer().update(cx, |primary_multibuffer, cx| {                primary_multibuffer.set_show_deleted_hunks(false, cx);                let paths = primary_multibuffer.paths().cloned().collect::<Vec<_>>();                for path in paths {                    let Some(excerpt_id) = primary_multibuffer.excerpts_for_path(&path).next()                    else {                        continue;                    };                    let snapshot = primary_multibuffer.snapshot(cx);                    let buffer = snapshot.buffer_for_excerpt(excerpt_id).unwrap();                    let diff = primary_multibuffer.diff_for(buffer.remote_id()).unwrap();                    secondary.sync_path_excerpts(path.clone(), primary_multibuffer, diff, cx);                }            })        });        self.secondary = Some(secondary);         let primary_pane = self.panes.first_pane();        self.panes            .split(&primary_pane, &secondary_pane, SplitDirection::Left, cx)            .unwrap();        cx.notify();    }     fn unsplit(&mut self, _: &UnsplitDiff, _: &mut Window, cx: &mut Context<Self>) {        let Some(secondary) = self.secondary.take() else {            return;        };        self.panes.remove(&secondary.pane, cx).unwrap();        self.primary_editor.update(cx, |primary, cx| {            primary.set_delegate_expand_excerpts(false);            primary.buffer().update(cx, |buffer, cx| {                buffer.set_show_deleted_hunks(true, cx);            });        });        cx.notify();    }     pub fn added_to_workspace(        &mut self,        workspace: &mut Workspace,        window: &mut Window,        cx: &mut Context<Self>,    ) {        self.workspace = workspace.weak_handle();        self.primary_editor.update(cx, |primary_editor, cx| {            primary_editor.added_to_workspace(workspace, window, cx);        });        if let Some(secondary) = &self.secondary {            secondary.editor.update(cx, |secondary_editor, cx| {                secondary_editor.added_to_workspace(workspace, window, cx);            });        }    }     pub fn set_excerpts_for_path(        &mut self,        path: PathKey,        buffer: Entity<Buffer>,        ranges: impl IntoIterator<Item = Range<Point>> + Clone,        context_line_count: u32,        diff: Entity<BufferDiff>,        cx: &mut Context<Self>,    ) -> (Vec<Range<Anchor>>, bool) {        self.primary_multibuffer            .update(cx, |primary_multibuffer, cx| {                let (anchors, added_a_new_excerpt) = primary_multibuffer.set_excerpts_for_path(                    path.clone(),                    buffer.clone(),                    ranges,                    context_line_count,                    cx,                );                if !anchors.is_empty()                    && primary_multibuffer                        .diff_for(buffer.read(cx).remote_id())                        .is_none_or(|old_diff| old_diff.entity_id() != diff.entity_id())                {                    primary_multibuffer.add_diff(diff.clone(), cx);                }                if let Some(secondary) = &mut self.secondary {                    secondary.sync_path_excerpts(path, primary_multibuffer, diff, cx);                }                (anchors, added_a_new_excerpt)            })    }     fn expand_excerpts(        &mut self,        excerpt_ids: impl Iterator<Item = ExcerptId> + Clone,        lines: u32,        direction: ExpandExcerptDirection,        cx: &mut Context<Self>,    ) {        let mut corresponding_paths = HashMap::default();        self.primary_multibuffer.update(cx, |multibuffer, cx| {            let snapshot = multibuffer.snapshot(cx);            if self.secondary.is_some() {                corresponding_paths = excerpt_ids                    .clone()                    .map(|excerpt_id| {                        let path = multibuffer.path_for_excerpt(excerpt_id).unwrap();                        let buffer = snapshot.buffer_for_excerpt(excerpt_id).unwrap();                        let diff = multibuffer.diff_for(buffer.remote_id()).unwrap();                        (path, diff)                    })                    .collect::<HashMap<_, _>>();            }            multibuffer.expand_excerpts(excerpt_ids.clone(), lines, direction, cx);        });         if let Some(secondary) = &mut self.secondary {            self.primary_multibuffer.update(cx, |multibuffer, cx| {                for (path, diff) in corresponding_paths {                    secondary.sync_path_excerpts(path, multibuffer, diff, cx);                }            })        }    }     pub fn remove_excerpts_for_path(&mut self, path: PathKey, cx: &mut Context<Self>) {        self.primary_multibuffer.update(cx, |buffer, cx| {            buffer.remove_excerpts_for_path(path.clone(), cx)        });        if let Some(secondary) = &mut self.secondary {            secondary.remove_mappings_for_path(&path, cx);            secondary                .multibuffer                .update(cx, |buffer, cx| buffer.remove_excerpts_for_path(path, cx))        }    }} #[cfg(test)]impl SplittableEditor {    fn check_invariants(&self, quiesced: bool, cx: &App) {        use buffer_diff::DiffHunkStatusKind;        use collections::HashSet;        use multi_buffer::MultiBufferOffset;        use multi_buffer::MultiBufferRow;        use multi_buffer::MultiBufferSnapshot;         fn format_diff(snapshot: &MultiBufferSnapshot) -> String {            let text = snapshot.text();            let row_infos = snapshot.row_infos(MultiBufferRow(0)).collect::<Vec<_>>();            let boundary_rows = snapshot                .excerpt_boundaries_in_range(MultiBufferOffset(0)..)                .map(|b| b.row)                .collect::<HashSet<_>>();             text.split('\n')                .enumerate()                .zip(row_infos)                .map(|((ix, line), info)| {                    let marker = match info.diff_status.map(|status| status.kind) {                        Some(DiffHunkStatusKind::Added) => "+ ",                        Some(DiffHunkStatusKind::Deleted) => "- ",                        Some(DiffHunkStatusKind::Modified) => unreachable!(),                        None => {                            if !line.is_empty() {                                "  "                            } else {                                ""                            }                        }                    };                    let boundary_row = if boundary_rows.contains(&MultiBufferRow(ix as u32)) {                        "  ----------\n"                    } else {                        ""                    };                    let expand = info                        .expand_info                        .map(|expand_info| match expand_info.direction {                            ExpandExcerptDirection::Up => " [↑]",                            ExpandExcerptDirection::Down => " [↓]",                            ExpandExcerptDirection::UpAndDown => " [↕]",                        })                        .unwrap_or_default();                     format!("{boundary_row}{marker}{line}{expand}")                })                .collect::<Vec<_>>()                .join("\n")        }         let Some(secondary) = &self.secondary else {            return;        };         log::info!(            "primary:\n\n{}",            format_diff(&self.primary_multibuffer.read(cx).snapshot(cx))        );         log::info!(            "secondary:\n\n{}",            format_diff(&secondary.multibuffer.read(cx).snapshot(cx))        );         let primary_excerpts = self.primary_multibuffer.read(cx).excerpt_ids();        let secondary_excerpts = secondary.multibuffer.read(cx).excerpt_ids();        assert_eq!(primary_excerpts.len(), secondary_excerpts.len());         assert_eq!(            secondary.primary_to_secondary.len(),            primary_excerpts.len(),            "primary_to_secondary mapping count should match excerpt count"        );        assert_eq!(            secondary.secondary_to_primary.len(),            secondary_excerpts.len(),            "secondary_to_primary mapping count should match excerpt count"        );         for primary_id in &primary_excerpts {            assert!(                secondary.primary_to_secondary.contains_key(primary_id),                "primary excerpt {:?} should have a mapping to secondary",                primary_id            );        }        for secondary_id in &secondary_excerpts {            assert!(                secondary.secondary_to_primary.contains_key(secondary_id),                "secondary excerpt {:?} should have a mapping to primary",                secondary_id            );        }         for (primary_id, secondary_id) in &secondary.primary_to_secondary {            assert_eq!(                secondary.secondary_to_primary.get(secondary_id),                Some(primary_id),                "mappings should be bijective"            );        }         if quiesced {            let primary_snapshot = self.primary_multibuffer.read(cx).snapshot(cx);            let secondary_snapshot = secondary.multibuffer.read(cx).snapshot(cx);            let primary_diff_hunks = primary_snapshot                .diff_hunks()                .map(|hunk| hunk.diff_base_byte_range)                .collect::<Vec<_>>();            let secondary_diff_hunks = secondary_snapshot                .diff_hunks()                .map(|hunk| hunk.diff_base_byte_range)                .collect::<Vec<_>>();            pretty_assertions::assert_eq!(primary_diff_hunks, secondary_diff_hunks);             // Filtering out empty lines is a bit of a hack, to work around a case where            // the base text has a trailing newline but the current text doesn't, or vice versa.            // In this case, we get the additional newline on one side, but that line is not            // marked as added/deleted by rowinfos.            let primary_unmodified_rows = primary_snapshot                .text()                .split("\n")                .zip(primary_snapshot.row_infos(MultiBufferRow(0)))                .filter(|(line, row_info)| !line.is_empty() && row_info.diff_status.is_none())                .map(|(line, _)| line.to_owned())

Lines 1–500 of 856.