use feature_flags::{FeatureFlag, FeatureFlagAppExt as _}; use gpui::{ Action, AppContext as _, Entity, EventEmitter, Focusable, NoAction, Subscription, WeakEntity, }; use multi_buffer::{MultiBuffer, MultiBufferFilterMode}; use project::Project; 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_editor: Entity, secondary: Option, panes: PaneGroup, workspace: WeakEntity, _subscriptions: Vec, } struct SecondaryEditor { editor: Entity, pane: Entity, has_latest_selection: bool, _subscriptions: Vec, } impl SplittableEditor { pub fn primary_editor(&self) -> &Entity { &self.primary_editor } pub fn last_selected_editor(&self) -> &Entity { if let Some(secondary) = &self.secondary && secondary.has_latest_selection { &secondary.editor } else { &self.primary_editor } } pub fn new_unsplit( buffer: Entity, project: Entity, workspace: Entity, window: &mut Window, cx: &mut Context, ) -> Self { let primary_editor = cx.new(|cx| Editor::for_multibuffer(buffer, Some(project.clone()), window, cx)); 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| { if let EditorEvent::SelectionsChanged { .. } = event && let Some(secondary) = &mut this.secondary { secondary.has_latest_selection = false; } 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, secondary: None, panes, workspace: workspace.downgrade(), _subscriptions: subscriptions, } } fn split(&mut self, _: &SplitDiff, window: &mut Window, cx: &mut Context) { if !cx.has_flag::() { return; } if self.secondary.is_some() { return; } let Some(workspace) = self.workspace.upgrade() else { return; }; let project = workspace.read(cx).project().clone(); let follower = self.primary_editor.update(cx, |primary, cx| { primary.buffer().update(cx, |buffer, cx| { let follower = buffer.get_or_create_follower(cx); buffer.set_filter_mode(Some(MultiBufferFilterMode::KeepInsertions)); follower }) }); follower.update(cx, |follower, _| { follower.set_filter_mode(Some(MultiBufferFilterMode::KeepDeletions)); }); let secondary_editor = workspace.update(cx, |workspace, cx| { cx.new(|cx| { let mut editor = Editor::for_multibuffer(follower, Some(project), window, cx); // TODO(split-diff) this should be at the multibuffer level editor.set_use_base_text_line_numbers(true, cx); editor.added_to_workspace(workspace, window, cx); 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| { if let EditorEvent::SelectionsChanged { .. } = event && let Some(secondary) = &mut this.secondary { secondary.has_latest_selection = true; } cx.emit(event.clone()) }), ]; self.secondary = Some(SecondaryEditor { editor: secondary_editor, pane: secondary_pane.clone(), has_latest_selection: false, _subscriptions: subscriptions, }); let primary_pane = self.panes.first_pane(); self.panes .split(&primary_pane, &secondary_pane, SplitDirection::Left) .unwrap(); cx.notify(); } fn unsplit(&mut self, _: &UnsplitDiff, _: &mut Window, cx: &mut Context) { let Some(secondary) = self.secondary.take() else { return; }; self.panes.remove(&secondary.pane).unwrap(); self.primary_editor.update(cx, |primary, cx| { primary.buffer().update(cx, |buffer, _| { buffer.set_filter_mode(None); }); }); cx.notify(); } pub fn added_to_workspace( &mut self, workspace: &mut Workspace, window: &mut Window, cx: &mut Context, ) { 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); }); } } } impl EventEmitter for SplittableEditor {} impl Focusable for SplittableEditor { fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { self.primary_editor.read(cx).focus_handle(cx) } } impl Render for SplittableEditor { fn render( &mut self, window: &mut ui::Window, cx: &mut ui::Context, ) -> impl ui::IntoElement { let inner = if self.secondary.is_none() { self.primary_editor.clone().into_any_element() } else if let Some(active) = self.panes.panes().into_iter().next() { self.panes .render( None, &ActivePaneDecorator::new(active, &self.workspace), window, cx, ) .into_any_element() } else { div().into_any_element() }; div() .id("splittable-editor") .on_action(cx.listener(Self::split)) .on_action(cx.listener(Self::unsplit)) .size_full() .child(inner) } }