split.rs

  1use feature_flags::{FeatureFlag, FeatureFlagAppExt as _};
  2use gpui::{
  3    Action, AppContext as _, Entity, EventEmitter, Focusable, NoAction, Subscription, WeakEntity,
  4};
  5use multi_buffer::{MultiBuffer, MultiBufferFilterMode};
  6use project::Project;
  7use ui::{
  8    App, Context, InteractiveElement as _, IntoElement as _, ParentElement as _, Render,
  9    Styled as _, Window, div,
 10};
 11use workspace::{
 12    ActivePaneDecorator, Item, ItemHandle, Pane, PaneGroup, SplitDirection, Workspace,
 13};
 14
 15use crate::{Editor, EditorEvent};
 16
 17struct SplitDiffFeatureFlag;
 18
 19impl FeatureFlag for SplitDiffFeatureFlag {
 20    const NAME: &'static str = "split-diff";
 21
 22    fn enabled_for_staff() -> bool {
 23        true
 24    }
 25}
 26
 27#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
 28#[action(namespace = editor)]
 29struct SplitDiff;
 30
 31#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
 32#[action(namespace = editor)]
 33struct UnsplitDiff;
 34
 35pub struct SplittableEditor {
 36    primary_editor: Entity<Editor>,
 37    secondary: Option<SecondaryEditor>,
 38    panes: PaneGroup,
 39    workspace: WeakEntity<Workspace>,
 40    _subscriptions: Vec<Subscription>,
 41}
 42
 43struct SecondaryEditor {
 44    editor: Entity<Editor>,
 45    pane: Entity<Pane>,
 46    has_latest_selection: bool,
 47    _subscriptions: Vec<Subscription>,
 48}
 49
 50impl SplittableEditor {
 51    pub fn primary_editor(&self) -> &Entity<Editor> {
 52        &self.primary_editor
 53    }
 54
 55    pub fn last_selected_editor(&self) -> &Entity<Editor> {
 56        if let Some(secondary) = &self.secondary
 57            && secondary.has_latest_selection
 58        {
 59            &secondary.editor
 60        } else {
 61            &self.primary_editor
 62        }
 63    }
 64
 65    pub fn new_unsplit(
 66        buffer: Entity<MultiBuffer>,
 67        project: Entity<Project>,
 68        workspace: Entity<Workspace>,
 69        window: &mut Window,
 70        cx: &mut Context<Self>,
 71    ) -> Self {
 72        let primary_editor =
 73            cx.new(|cx| Editor::for_multibuffer(buffer, Some(project.clone()), window, cx));
 74        let pane = cx.new(|cx| {
 75            let mut pane = Pane::new(
 76                workspace.downgrade(),
 77                project,
 78                Default::default(),
 79                None,
 80                NoAction.boxed_clone(),
 81                true,
 82                window,
 83                cx,
 84            );
 85            pane.set_should_display_tab_bar(|_, _| false);
 86            pane.add_item(primary_editor.boxed_clone(), true, true, None, window, cx);
 87            pane
 88        });
 89        let panes = PaneGroup::new(pane);
 90        // TODO(split-diff) we might want to tag editor events with whether they came from primary/secondary
 91        let subscriptions =
 92            vec![
 93                cx.subscribe(&primary_editor, |this, _, event: &EditorEvent, cx| {
 94                    if let EditorEvent::SelectionsChanged { .. } = event
 95                        && let Some(secondary) = &mut this.secondary
 96                    {
 97                        secondary.has_latest_selection = false;
 98                    }
 99                    cx.emit(event.clone())
100                }),
101            ];
102
103        window.defer(cx, {
104            let workspace = workspace.downgrade();
105            let primary_editor = primary_editor.downgrade();
106            move |window, cx| {
107                workspace
108                    .update(cx, |workspace, cx| {
109                        primary_editor.update(cx, |editor, cx| {
110                            editor.added_to_workspace(workspace, window, cx);
111                        })
112                    })
113                    .ok();
114            }
115        });
116        Self {
117            primary_editor,
118            secondary: None,
119            panes,
120            workspace: workspace.downgrade(),
121            _subscriptions: subscriptions,
122        }
123    }
124
125    fn split(&mut self, _: &SplitDiff, window: &mut Window, cx: &mut Context<Self>) {
126        if !cx.has_flag::<SplitDiffFeatureFlag>() {
127            return;
128        }
129        if self.secondary.is_some() {
130            return;
131        }
132        let Some(workspace) = self.workspace.upgrade() else {
133            return;
134        };
135        let project = workspace.read(cx).project().clone();
136        let follower = self.primary_editor.update(cx, |primary, cx| {
137            primary.buffer().update(cx, |buffer, cx| {
138                let follower = buffer.get_or_create_follower(cx);
139                buffer.set_filter_mode(Some(MultiBufferFilterMode::KeepInsertions));
140                follower
141            })
142        });
143        follower.update(cx, |follower, _| {
144            follower.set_filter_mode(Some(MultiBufferFilterMode::KeepDeletions));
145        });
146        let secondary_editor = workspace.update(cx, |workspace, cx| {
147            cx.new(|cx| {
148                let mut editor = Editor::for_multibuffer(follower, Some(project), window, cx);
149                // TODO(split-diff) this should be at the multibuffer level
150                editor.set_use_base_text_line_numbers(true, cx);
151                editor.added_to_workspace(workspace, window, cx);
152                editor
153            })
154        });
155        let secondary_pane = cx.new(|cx| {
156            let mut pane = Pane::new(
157                workspace.downgrade(),
158                workspace.read(cx).project().clone(),
159                Default::default(),
160                None,
161                NoAction.boxed_clone(),
162                true,
163                window,
164                cx,
165            );
166            pane.set_should_display_tab_bar(|_, _| false);
167            pane.add_item(
168                ItemHandle::boxed_clone(&secondary_editor),
169                false,
170                false,
171                None,
172                window,
173                cx,
174            );
175            pane
176        });
177
178        let subscriptions =
179            vec![
180                cx.subscribe(&secondary_editor, |this, _, event: &EditorEvent, cx| {
181                    if let EditorEvent::SelectionsChanged { .. } = event
182                        && let Some(secondary) = &mut this.secondary
183                    {
184                        secondary.has_latest_selection = true;
185                    }
186                    cx.emit(event.clone())
187                }),
188            ];
189        self.secondary = Some(SecondaryEditor {
190            editor: secondary_editor,
191            pane: secondary_pane.clone(),
192            has_latest_selection: false,
193            _subscriptions: subscriptions,
194        });
195        let primary_pane = self.panes.first_pane();
196        self.panes
197            .split(&primary_pane, &secondary_pane, SplitDirection::Left)
198            .unwrap();
199        cx.notify();
200    }
201
202    fn unsplit(&mut self, _: &UnsplitDiff, _: &mut Window, cx: &mut Context<Self>) {
203        let Some(secondary) = self.secondary.take() else {
204            return;
205        };
206        self.panes.remove(&secondary.pane).unwrap();
207        self.primary_editor.update(cx, |primary, cx| {
208            primary.buffer().update(cx, |buffer, _| {
209                buffer.set_filter_mode(None);
210            });
211        });
212        cx.notify();
213    }
214
215    pub fn added_to_workspace(
216        &mut self,
217        workspace: &mut Workspace,
218        window: &mut Window,
219        cx: &mut Context<Self>,
220    ) {
221        self.workspace = workspace.weak_handle();
222        self.primary_editor.update(cx, |primary_editor, cx| {
223            primary_editor.added_to_workspace(workspace, window, cx);
224        });
225        if let Some(secondary) = &self.secondary {
226            secondary.editor.update(cx, |secondary_editor, cx| {
227                secondary_editor.added_to_workspace(workspace, window, cx);
228            });
229        }
230    }
231}
232
233impl EventEmitter<EditorEvent> for SplittableEditor {}
234impl Focusable for SplittableEditor {
235    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
236        self.primary_editor.read(cx).focus_handle(cx)
237    }
238}
239
240impl Render for SplittableEditor {
241    fn render(
242        &mut self,
243        window: &mut ui::Window,
244        cx: &mut ui::Context<Self>,
245    ) -> impl ui::IntoElement {
246        let Some(active) = self.panes.panes().into_iter().next() else {
247            return div().into_any_element();
248        };
249        div()
250            .id("splittable-editor")
251            .on_action(cx.listener(Self::split))
252            .on_action(cx.listener(Self::unsplit))
253            .size_full()
254            .child(self.panes.render(
255                None,
256                &ActivePaneDecorator::new(active, &self.workspace),
257                window,
258                cx,
259            ))
260            .into_any_element()
261    }
262}