split.rs

  1use std::{ops::Range, sync::Arc};
  2
  3use buffer_diff::BufferDiff;
  4use feature_flags::{FeatureFlag, FeatureFlagAppExt as _};
  5use gpui::{
  6    Action, AppContext as _, Entity, EventEmitter, Focusable, NoAction, Subscription, WeakEntity,
  7};
  8use language::{Buffer, Capability, LanguageRegistry};
  9use multi_buffer::{Anchor, ExcerptRange, MultiBuffer, PathKey};
 10use project::Project;
 11use rope::Point;
 12use text::OffsetRangeExt as _;
 13use ui::{
 14    App, Context, InteractiveElement as _, IntoElement as _, ParentElement as _, Render,
 15    Styled as _, Window, div,
 16};
 17use workspace::{
 18    ActivePaneDecorator, Item, ItemHandle, Pane, PaneGroup, SplitDirection, Workspace,
 19};
 20
 21use crate::{Editor, EditorEvent};
 22
 23struct SplitDiffFeatureFlag;
 24
 25impl FeatureFlag for SplitDiffFeatureFlag {
 26    const NAME: &'static str = "split-diff";
 27
 28    fn enabled_for_staff() -> bool {
 29        true
 30    }
 31}
 32
 33#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
 34#[action(namespace = editor)]
 35struct SplitDiff;
 36
 37#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
 38#[action(namespace = editor)]
 39struct UnsplitDiff;
 40
 41pub struct SplittableEditor {
 42    primary_editor: Entity<Editor>,
 43    secondary: Option<SecondaryEditor>,
 44    panes: PaneGroup,
 45    workspace: WeakEntity<Workspace>,
 46    _subscriptions: Vec<Subscription>,
 47}
 48
 49struct SecondaryEditor {
 50    editor: Entity<Editor>,
 51    pane: Entity<Pane>,
 52    has_latest_selection: bool,
 53    _subscriptions: Vec<Subscription>,
 54}
 55
 56impl SplittableEditor {
 57    pub fn primary_editor(&self) -> &Entity<Editor> {
 58        &self.primary_editor
 59    }
 60
 61    pub fn last_selected_editor(&self) -> &Entity<Editor> {
 62        if let Some(secondary) = &self.secondary
 63            && secondary.has_latest_selection
 64        {
 65            &secondary.editor
 66        } else {
 67            &self.primary_editor
 68        }
 69    }
 70
 71    pub fn new_unsplit(
 72        buffer: Entity<MultiBuffer>,
 73        project: Entity<Project>,
 74        workspace: Entity<Workspace>,
 75        window: &mut Window,
 76        cx: &mut Context<Self>,
 77    ) -> Self {
 78        let primary_editor =
 79            cx.new(|cx| Editor::for_multibuffer(buffer, Some(project.clone()), window, cx));
 80        let pane = cx.new(|cx| {
 81            let mut pane = Pane::new(
 82                workspace.downgrade(),
 83                project,
 84                Default::default(),
 85                None,
 86                NoAction.boxed_clone(),
 87                true,
 88                window,
 89                cx,
 90            );
 91            pane.set_should_display_tab_bar(|_, _| false);
 92            pane.add_item(primary_editor.boxed_clone(), true, true, None, window, cx);
 93            pane
 94        });
 95        let panes = PaneGroup::new(pane);
 96        // TODO(split-diff) we might want to tag editor events with whether they came from primary/secondary
 97        let subscriptions =
 98            vec![
 99                cx.subscribe(&primary_editor, |this, _, event: &EditorEvent, cx| {
100                    if let EditorEvent::SelectionsChanged { .. } = event
101                        && let Some(secondary) = &mut this.secondary
102                    {
103                        secondary.has_latest_selection = false;
104                    }
105                    cx.emit(event.clone())
106                }),
107            ];
108
109        window.defer(cx, {
110            let workspace = workspace.downgrade();
111            let primary_editor = primary_editor.downgrade();
112            move |window, cx| {
113                workspace
114                    .update(cx, |workspace, cx| {
115                        primary_editor.update(cx, |editor, cx| {
116                            editor.added_to_workspace(workspace, window, cx);
117                        })
118                    })
119                    .ok();
120            }
121        });
122        Self {
123            primary_editor,
124            secondary: None,
125            panes,
126            workspace: workspace.downgrade(),
127            _subscriptions: subscriptions,
128        }
129    }
130
131    fn split(&mut self, _: &SplitDiff, window: &mut Window, cx: &mut Context<Self>) {
132        if !cx.has_flag::<SplitDiffFeatureFlag>() {
133            return;
134        }
135        if self.secondary.is_some() {
136            return;
137        }
138        let Some(workspace) = self.workspace.upgrade() else {
139            return;
140        };
141        let project = workspace.read(cx).project().clone();
142
143        // FIXME
144        // - have to subscribe to the diffs to update the base text buffers (and handle language changed I think?)
145
146        let secondary_editor = cx.new(|cx| {
147            let multibuffer = cx.new(|cx| {
148                let mut multibuffer = MultiBuffer::new(Capability::ReadOnly);
149                multibuffer.set_all_diff_hunks_expanded(cx);
150                multibuffer
151            });
152            Editor::for_multibuffer(multibuffer, Some(project.clone()), window, cx)
153        });
154        let secondary_pane = cx.new(|cx| {
155            let mut pane = Pane::new(
156                workspace.downgrade(),
157                workspace.read(cx).project().clone(),
158                Default::default(),
159                None,
160                NoAction.boxed_clone(),
161                true,
162                window,
163                cx,
164            );
165            pane.set_should_display_tab_bar(|_, _| false);
166            pane.add_item(
167                ItemHandle::boxed_clone(&secondary_editor),
168                false,
169                false,
170                None,
171                window,
172                cx,
173            );
174            pane
175        });
176
177        let subscriptions =
178            vec![
179                cx.subscribe(&secondary_editor, |this, _, event: &EditorEvent, cx| {
180                    if let EditorEvent::SelectionsChanged { .. } = event
181                        && let Some(secondary) = &mut this.secondary
182                    {
183                        secondary.has_latest_selection = true;
184                    }
185                    cx.emit(event.clone())
186                }),
187            ];
188        let mut secondary = SecondaryEditor {
189            editor: secondary_editor,
190            pane: secondary_pane.clone(),
191            has_latest_selection: false,
192            _subscriptions: subscriptions,
193        };
194        self.primary_editor.update(cx, |editor, cx| {
195            editor.buffer().update(cx, |primary_multibuffer, cx| {
196                primary_multibuffer.set_show_deleted_hunks(false, cx);
197                let paths = primary_multibuffer.paths().collect::<Vec<_>>();
198                for path in paths {
199                    secondary.sync_path_excerpts(path, primary_multibuffer, diff, cx);
200                }
201            })
202        });
203        self.secondary = Some(secondary);
204
205        let primary_pane = self.panes.first_pane();
206        self.panes
207            .split(&primary_pane, &secondary_pane, SplitDirection::Left)
208            .unwrap();
209        cx.notify();
210    }
211
212    fn unsplit(&mut self, _: &UnsplitDiff, _: &mut Window, cx: &mut Context<Self>) {
213        let Some(secondary) = self.secondary.take() else {
214            return;
215        };
216        self.panes.remove(&secondary.pane).unwrap();
217        self.primary_editor.update(cx, |primary, cx| {
218            primary.buffer().update(cx, |buffer, cx| {
219                buffer.set_show_deleted_hunks(true, cx);
220            });
221        });
222        cx.notify();
223    }
224
225    pub fn added_to_workspace(
226        &mut self,
227        workspace: &mut Workspace,
228        window: &mut Window,
229        cx: &mut Context<Self>,
230    ) {
231        self.workspace = workspace.weak_handle();
232        self.primary_editor.update(cx, |primary_editor, cx| {
233            primary_editor.added_to_workspace(workspace, window, cx);
234        });
235        if let Some(secondary) = &self.secondary {
236            secondary.editor.update(cx, |secondary_editor, cx| {
237                secondary_editor.added_to_workspace(workspace, window, cx);
238            });
239        }
240    }
241
242    pub fn set_excerpts_for_path(
243        &mut self,
244        path: PathKey,
245        buffer: Entity<Buffer>,
246        ranges: impl IntoIterator<Item = Range<Point>>,
247        context_line_count: u32,
248        diff: Entity<BufferDiff>,
249        cx: &mut Context<Self>,
250    ) -> (Vec<Range<Anchor>>, bool) {
251        self.primary_editor.update(cx, |editor, cx| {
252            editor.buffer().update(cx, |primary_multibuffer, cx| {
253                let (anchors, added_a_new_excerpt) = primary_multibuffer.set_excerpts_for_path(
254                    path.clone(),
255                    buffer,
256                    ranges,
257                    context_line_count,
258                    cx,
259                );
260                // - we just added some excerpts for a specific buffer to the primary (RHS)
261                // - but the diff for that buffer doesn't get attached to the primary multibuffer until slightly later
262                // - however, for sync_path_excerpts we require that we have a diff for the buffer
263                if let Some(secondary) = &mut self.secondary
264                    && let Some(languages) = self
265                        .workspace
266                        .update(cx, |workspace, cx| {
267                            workspace.project().read(cx).languages().clone()
268                        })
269                        .ok()
270                {
271                    secondary.sync_path_excerpts(path, primary_multibuffer, diff, cx);
272                }
273                (anchors, added_a_new_excerpt)
274            })
275        })
276    }
277}
278
279impl EventEmitter<EditorEvent> for SplittableEditor {}
280impl Focusable for SplittableEditor {
281    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
282        self.primary_editor.read(cx).focus_handle(cx)
283    }
284}
285
286impl Render for SplittableEditor {
287    fn render(
288        &mut self,
289        window: &mut ui::Window,
290        cx: &mut ui::Context<Self>,
291    ) -> impl ui::IntoElement {
292        let inner = if self.secondary.is_none() {
293            self.primary_editor.clone().into_any_element()
294        } else if let Some(active) = self.panes.panes().into_iter().next() {
295            self.panes
296                .render(
297                    None,
298                    &ActivePaneDecorator::new(active, &self.workspace),
299                    window,
300                    cx,
301                )
302                .into_any_element()
303        } else {
304            div().into_any_element()
305        };
306        div()
307            .id("splittable-editor")
308            .on_action(cx.listener(Self::split))
309            .on_action(cx.listener(Self::unsplit))
310            .size_full()
311            .child(inner)
312    }
313}
314
315impl SecondaryEditor {
316    fn sync_path_excerpts(
317        &mut self,
318        path_key: PathKey,
319        primary_multibuffer: &mut MultiBuffer,
320        diff: Entity<BufferDiff>,
321        cx: &mut App,
322    ) {
323        let excerpt_id = primary_multibuffer
324            .excerpts_for_path(&path_key)
325            .next()
326            .unwrap();
327        let primary_multibuffer_snapshot = primary_multibuffer.snapshot(cx);
328        let main_buffer = primary_multibuffer_snapshot
329            .buffer_for_excerpt(excerpt_id)
330            .unwrap();
331        let base_text_buffer = diff.read(cx).base_text_buffer();
332        let diff = diff.read(cx).snapshot(cx);
333        let base_text_buffer_snapshot = base_text_buffer.read(cx).snapshot();
334        let new = primary_multibuffer
335            .excerpts_for_buffer(main_buffer.remote_id(), cx)
336            .into_iter()
337            .map(|(_, excerpt_range)| {
338                let point_range_to_base_text_point_range = |range: Range<Point>| {
339                    let start_row = diff.row_to_base_text_row(range.start.row, main_buffer);
340                    let start_column = 0;
341                    let end_row = diff.row_to_base_text_row(range.end.row, main_buffer);
342                    let end_column = diff.base_text().line_len(end_row);
343                    Point::new(start_row, start_column)..Point::new(end_row, end_column)
344                };
345                let primary = excerpt_range.primary.to_point(main_buffer);
346                let context = excerpt_range.context.to_point(main_buffer);
347                ExcerptRange {
348                    primary: point_range_to_base_text_point_range(primary),
349                    context: point_range_to_base_text_point_range(context),
350                }
351            })
352            .collect();
353
354        let main_buffer = primary_multibuffer.buffer(main_buffer.remote_id()).unwrap();
355
356        self.editor.update(cx, |editor, cx| {
357            editor.buffer().update(cx, |buffer, cx| {
358                buffer.update_path_excerpts(
359                    path_key,
360                    base_text_buffer,
361                    &base_text_buffer_snapshot,
362                    new,
363                    cx,
364                );
365                buffer.add_inverted_diff(
366                    base_text_buffer_snapshot.remote_id(),
367                    diff,
368                    main_buffer,
369                    cx,
370                );
371            })
372        });
373    }
374}
375
376#[cfg(test)]
377mod tests {
378    use buffer_diff::BufferDiff;
379    use db::indoc;
380    use fs::FakeFs;
381    use gpui::AppContext as _;
382    use language::{Buffer, Capability};
383    use multi_buffer::MultiBuffer;
384    use project::Project;
385    use settings::SettingsStore;
386    use ui::VisualContext as _;
387    use workspace::Workspace;
388
389    use crate::SplittableEditor;
390
391    #[gpui::test]
392    async fn test_basic_excerpts(cx: &mut gpui::TestAppContext) {
393        cx.update(|cx| {
394            let store = SettingsStore::test(cx);
395            cx.set_global(store);
396            theme::init(theme::LoadThemes::JustBase, cx);
397            crate::init(cx);
398        });
399        let base_text = indoc! {"
400            hello
401        "};
402        let buffer_text = indoc! {"
403            HELLO!
404        "};
405        let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
406        let diff = cx.new(|cx| {
407            BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx)
408        });
409        let project = Project::test(FakeFs::new(cx.executor()), [], cx).await;
410        let (workspace, cx) =
411            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
412        let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
413        let editor = cx.new_window_entity(|window, cx| {
414            SplittableEditor::new_unsplit(multibuffer, project, workspace, window, cx)
415        });
416    }
417}