split.rs

  1use std::ops::Range;
  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};
  9use multi_buffer::{Anchor, ExcerptId, ExcerptRange, ExpandExcerptDirection, 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_multibuffer: Entity<MultiBuffer>,
 43    primary_editor: Entity<Editor>,
 44    secondary: Option<SecondaryEditor>,
 45    panes: PaneGroup,
 46    workspace: WeakEntity<Workspace>,
 47    _subscriptions: Vec<Subscription>,
 48}
 49
 50struct SecondaryEditor {
 51    multibuffer: Entity<MultiBuffer>,
 52    editor: Entity<Editor>,
 53    pane: Entity<Pane>,
 54    has_latest_selection: bool,
 55    _subscriptions: Vec<Subscription>,
 56}
 57
 58impl SplittableEditor {
 59    pub fn primary_editor(&self) -> &Entity<Editor> {
 60        &self.primary_editor
 61    }
 62
 63    pub fn last_selected_editor(&self) -> &Entity<Editor> {
 64        if let Some(secondary) = &self.secondary
 65            && secondary.has_latest_selection
 66        {
 67            &secondary.editor
 68        } else {
 69            &self.primary_editor
 70        }
 71    }
 72
 73    pub fn new_unsplit(
 74        primary_multibuffer: Entity<MultiBuffer>,
 75        project: Entity<Project>,
 76        workspace: Entity<Workspace>,
 77        window: &mut Window,
 78        cx: &mut Context<Self>,
 79    ) -> Self {
 80        let primary_editor = cx.new(|cx| {
 81            Editor::for_multibuffer(
 82                primary_multibuffer.clone(),
 83                Some(project.clone()),
 84                window,
 85                cx,
 86            )
 87        });
 88        let pane = cx.new(|cx| {
 89            let mut pane = Pane::new(
 90                workspace.downgrade(),
 91                project,
 92                Default::default(),
 93                None,
 94                NoAction.boxed_clone(),
 95                true,
 96                window,
 97                cx,
 98            );
 99            pane.set_should_display_tab_bar(|_, _| false);
100            pane.add_item(primary_editor.boxed_clone(), true, true, None, window, cx);
101            pane
102        });
103        let panes = PaneGroup::new(pane);
104        // TODO(split-diff) we might want to tag editor events with whether they came from primary/secondary
105        let subscriptions =
106            vec![
107                cx.subscribe(&primary_editor, |this, _, event: &EditorEvent, cx| {
108                    if let EditorEvent::SelectionsChanged { .. } = event
109                        && let Some(secondary) = &mut this.secondary
110                    {
111                        secondary.has_latest_selection = false;
112                    }
113                    cx.emit(event.clone())
114                }),
115            ];
116
117        window.defer(cx, {
118            let workspace = workspace.downgrade();
119            let primary_editor = primary_editor.downgrade();
120            move |window, cx| {
121                workspace
122                    .update(cx, |workspace, cx| {
123                        primary_editor.update(cx, |editor, cx| {
124                            editor.added_to_workspace(workspace, window, cx);
125                        })
126                    })
127                    .ok();
128            }
129        });
130        Self {
131            primary_editor,
132            primary_multibuffer,
133            secondary: None,
134            panes,
135            workspace: workspace.downgrade(),
136            _subscriptions: subscriptions,
137        }
138    }
139
140    fn split(&mut self, _: &SplitDiff, window: &mut Window, cx: &mut Context<Self>) {
141        if !cx.has_flag::<SplitDiffFeatureFlag>() {
142            return;
143        }
144        if self.secondary.is_some() {
145            return;
146        }
147        let Some(workspace) = self.workspace.upgrade() else {
148            return;
149        };
150        let project = workspace.read(cx).project().clone();
151
152        let secondary_multibuffer = cx.new(|cx| {
153            let mut multibuffer = MultiBuffer::new(Capability::ReadOnly);
154            multibuffer.set_all_diff_hunks_expanded(cx);
155            multibuffer
156        });
157        let secondary_editor = cx.new(|cx| {
158            let mut editor = Editor::for_multibuffer(
159                secondary_multibuffer.clone(),
160                Some(project.clone()),
161                window,
162                cx,
163            );
164            editor.number_deleted_lines = true;
165            editor
166        });
167        let secondary_pane = cx.new(|cx| {
168            let mut pane = Pane::new(
169                workspace.downgrade(),
170                workspace.read(cx).project().clone(),
171                Default::default(),
172                None,
173                NoAction.boxed_clone(),
174                true,
175                window,
176                cx,
177            );
178            pane.set_should_display_tab_bar(|_, _| false);
179            pane.add_item(
180                ItemHandle::boxed_clone(&secondary_editor),
181                false,
182                false,
183                None,
184                window,
185                cx,
186            );
187            pane
188        });
189
190        let subscriptions =
191            vec![
192                cx.subscribe(&secondary_editor, |this, _, event: &EditorEvent, cx| {
193                    if let EditorEvent::SelectionsChanged { .. } = event
194                        && let Some(secondary) = &mut this.secondary
195                    {
196                        secondary.has_latest_selection = true;
197                    }
198                    cx.emit(event.clone())
199                }),
200            ];
201        let mut secondary = SecondaryEditor {
202            editor: secondary_editor,
203            multibuffer: secondary_multibuffer,
204            pane: secondary_pane.clone(),
205            has_latest_selection: false,
206            _subscriptions: subscriptions,
207        };
208        self.primary_editor.update(cx, |editor, cx| {
209            editor.buffer().update(cx, |primary_multibuffer, cx| {
210                primary_multibuffer.set_show_deleted_hunks(false, cx);
211                let paths = primary_multibuffer.paths().collect::<Vec<_>>();
212                for path in paths {
213                    let Some(excerpt_id) = primary_multibuffer.excerpts_for_path(&path).next()
214                    else {
215                        continue;
216                    };
217                    let snapshot = primary_multibuffer.snapshot(cx);
218                    let buffer = snapshot.buffer_for_excerpt(excerpt_id).unwrap();
219                    let diff = primary_multibuffer.diff_for(buffer.remote_id()).unwrap();
220                    secondary.sync_path_excerpts(path, primary_multibuffer, diff, cx);
221                }
222            })
223        });
224        self.secondary = Some(secondary);
225
226        let primary_pane = self.panes.first_pane();
227        self.panes
228            .split(&primary_pane, &secondary_pane, SplitDirection::Left)
229            .unwrap();
230        cx.notify();
231    }
232
233    fn unsplit(&mut self, _: &UnsplitDiff, _: &mut Window, cx: &mut Context<Self>) {
234        let Some(secondary) = self.secondary.take() else {
235            return;
236        };
237        self.panes.remove(&secondary.pane).unwrap();
238        self.primary_editor.update(cx, |primary, cx| {
239            primary.buffer().update(cx, |buffer, cx| {
240                buffer.set_show_deleted_hunks(true, cx);
241            });
242        });
243        cx.notify();
244    }
245
246    pub fn added_to_workspace(
247        &mut self,
248        workspace: &mut Workspace,
249        window: &mut Window,
250        cx: &mut Context<Self>,
251    ) {
252        self.workspace = workspace.weak_handle();
253        self.primary_editor.update(cx, |primary_editor, cx| {
254            primary_editor.added_to_workspace(workspace, window, cx);
255        });
256        if let Some(secondary) = &self.secondary {
257            secondary.editor.update(cx, |secondary_editor, cx| {
258                secondary_editor.added_to_workspace(workspace, window, cx);
259            });
260        }
261    }
262
263    pub fn set_excerpts_for_path(
264        &mut self,
265        path: PathKey,
266        buffer: Entity<Buffer>,
267        ranges: impl IntoIterator<Item = Range<Point>>,
268        context_line_count: u32,
269        diff: Entity<BufferDiff>,
270        cx: &mut Context<Self>,
271    ) -> (Vec<Range<Anchor>>, bool) {
272        self.primary_editor.update(cx, |editor, cx| {
273            editor.buffer().update(cx, |primary_multibuffer, cx| {
274                let (anchors, added_a_new_excerpt) = primary_multibuffer.set_excerpts_for_path(
275                    path.clone(),
276                    buffer,
277                    ranges,
278                    context_line_count,
279                    cx,
280                );
281                primary_multibuffer.add_diff(diff.clone(), cx);
282                if let Some(secondary) = &mut self.secondary {
283                    secondary.sync_path_excerpts(path, primary_multibuffer, diff, cx);
284                }
285                (anchors, added_a_new_excerpt)
286            })
287        })
288    }
289
290    /// Expands excerpts in both sides.
291    ///
292    /// While the left multibuffer does have separate excerpts with separate
293    /// IDs, this is an implementation detail. We do not expose the left excerpt
294    /// IDs in the public API of [`SplittableEditor`].
295    pub fn expand_excerpts(
296        &mut self,
297        excerpt_ids: impl Iterator<Item = ExcerptId> + Clone,
298        lines: u32,
299        direction: ExpandExcerptDirection,
300        cx: &mut Context<Self>,
301    ) {
302        self.primary_multibuffer.update(cx, |multibuffer, cx| {
303            multibuffer.expand_excerpts(excerpt_ids.clone(), lines, direction, cx);
304        });
305        let paths: Vec<(ExcerptId, PathKey)> = excerpt_ids
306            .flat_map(|excerpt_id| {
307                let path = self
308                    .primary_multibuffer
309                    .read(cx)
310                    .path_for_excerpt(excerpt_id)
311                    .cloned()?;
312                Some((excerpt_id, path))
313            })
314            .collect();
315
316        if let Some(secondary) = &mut self.secondary {
317            self.primary_editor.update(cx, |editor, cx| {
318                editor.buffer().update(cx, |multibuffer, cx| {
319                    let snapshot = multibuffer.snapshot(cx);
320                    for (excerpt_id, path) in paths {
321                        let buffer = snapshot.buffer_for_excerpt(excerpt_id).unwrap();
322                        let diff = multibuffer.diff_for(buffer.remote_id()).unwrap();
323                        secondary.sync_path_excerpts(path, multibuffer, diff, cx);
324                    }
325                })
326            })
327        }
328    }
329
330    pub fn remove_excerpts_for_path(&mut self, path: PathKey, cx: &mut Context<Self>) {
331        self.primary_multibuffer.update(cx, |buffer, cx| {
332            buffer.remove_excerpts_for_path(path.clone(), cx)
333        });
334        if let Some(secondary) = &self.secondary {
335            secondary
336                .multibuffer
337                .update(cx, |buffer, cx| buffer.remove_excerpts_for_path(path, cx))
338        }
339    }
340}
341
342#[cfg(test)]
343impl SplittableEditor {
344    fn check_invariants(&self, cx: &App) {
345        let Some(secondary) = &self.secondary else {
346            return;
347        };
348
349        let primary_excerpts = self.primary_multibuffer.read(cx).excerpt_ids();
350        let secondary_excerpts = secondary.multibuffer.read(cx).excerpt_ids();
351        assert_eq!(primary_excerpts.len(), secondary_excerpts.len());
352
353        // self.primary_multibuffer.read(cx).check_invariants(cx);
354        // secondary.multibuffer.read(cx).check_invariants(cx);
355        // Assertions:...
356        //
357        // left.display_lines().filter(is_unmodified) == right.display_lines().filter(is_unmodified)
358        //
359        // left excerpts and right excerpts bijectivity
360        //
361        //
362
363        // let primary_buffer_text = self
364        //     .primary_multibuffer
365        //     .read(cx)
366        //     .text_summary_for_range(Anchor::min()..Anchor::max());
367        // let secondary_buffer_text = secondary
368        //     .multibuffer
369        //     .read(cx)
370        //     .text_summary_for_range(Anchor::min()..Anchor::max());
371        // let primary_buffer_base_text = self
372        //     .primary_multibuffer
373        //     .read(cx)
374        //     .base_text_summary_for_range(Anchor::min()..Anchor::max());
375        // let secondary_buffer_base_text = secondary
376        //     .multibuffer
377        //     .read(cx)
378        //     .base_text_summary_for_range(Anchor::min()..Anchor::max());
379    }
380
381    fn randomly_edit_excerpts(
382        &mut self,
383        rng: &mut impl rand::Rng,
384        mutation_count: usize,
385        cx: &mut Context<Self>,
386    ) {
387        use collections::HashSet;
388        use rand::prelude::*;
389        use std::env;
390        use util::RandomCharIter;
391
392        let max_excerpts = env::var("MAX_EXCERPTS")
393            .map(|i| i.parse().expect("invalid `MAX_EXCERPTS` variable"))
394            .unwrap_or(5);
395
396        let paths = self
397            .primary_multibuffer
398            .read(cx)
399            .paths()
400            .collect::<Vec<_>>();
401        let excerpt_ids = self.primary_multibuffer.read(cx).excerpt_ids();
402
403        for _ in 0..mutation_count {
404            if rng.random_bool(0.05) {
405                log::info!("Clearing multi-buffer");
406                self.primary_multibuffer.update(cx, |multibuffer, cx| {
407                    multibuffer.clear(cx);
408                });
409                continue;
410            } else if rng.random_bool(0.1) && !excerpt_ids.is_empty() {
411                let mut excerpts = HashSet::default();
412                for _ in 0..rng.random_range(0..excerpt_ids.len()) {
413                    excerpts.extend(excerpt_ids.choose(rng).copied());
414                }
415
416                let line_count = rng.random_range(0..5);
417
418                log::info!("Expanding excerpts {excerpts:?} by {line_count} lines");
419
420                self.expand_excerpts(
421                    excerpts.iter().cloned(),
422                    line_count,
423                    ExpandExcerptDirection::UpAndDown,
424                    cx,
425                );
426                continue;
427            }
428
429            if excerpt_ids.is_empty() || (rng.random() && excerpt_ids.len() < max_excerpts) {
430                let existing_buffers = self.primary_multibuffer.read(cx).all_buffers();
431                let buffer = if rng.random() || existing_buffers.is_empty() {
432                    let len = rng.random_range(0..500);
433                    let text = RandomCharIter::new(&mut *rng).take(len).collect::<String>();
434                    let buffer = cx.new(|cx| Buffer::local(text, cx));
435                    log::info!(
436                        "Creating new buffer {} with text: {:?}",
437                        buffer.read(cx).remote_id(),
438                        buffer.read(cx).text()
439                    );
440                    buffer
441                } else {
442                    existing_buffers.iter().choose(rng).unwrap().clone()
443                };
444
445                let buffer_snapshot = buffer.read(cx).snapshot();
446                let diff = cx.new(|cx| BufferDiff::new_unchanged(&buffer_snapshot, cx));
447                // Create some initial diff hunks.
448                buffer.update(cx, |buffer, cx| {
449                    buffer.randomly_edit(rng, 2, cx);
450                });
451                let buffer_snapshot = buffer.read(cx).text_snapshot();
452                let ranges = diff.update(cx, |diff, cx| {
453                    diff.recalculate_diff_sync(&buffer_snapshot, cx);
454                    diff.snapshot(cx)
455                        .hunks(&buffer_snapshot)
456                        .map(|hunk| hunk.range.clone())
457                        .collect::<Vec<_>>()
458                });
459                let path = PathKey::for_buffer(&buffer, cx);
460                self.set_excerpts_for_path(path, buffer, ranges, 2, diff, cx);
461            } else {
462                let remove_count = rng.random_range(1..=paths.len());
463                let paths_to_remove = paths
464                    .choose_multiple(rng, remove_count)
465                    .cloned()
466                    .collect::<Vec<_>>();
467                for path in paths_to_remove {
468                    self.remove_excerpts_for_path(path, cx);
469                }
470            }
471        }
472    }
473
474    fn randomly_mutate(
475        &mut self,
476        rng: &mut impl rand::Rng,
477        mutation_count: usize,
478        cx: &mut Context<Self>,
479    ) {
480        use rand::prelude::*;
481
482        if rng.random_bool(0.7) {
483            let buffers = self.primary_editor.read(cx).buffer().read(cx).all_buffers();
484            let buffer = buffers.iter().choose(rng);
485
486            if let Some(buffer) = buffer {
487                buffer.update(cx, |buffer, cx| {
488                    if rng.random() {
489                        buffer.randomly_edit(rng, mutation_count, cx);
490                    } else {
491                        buffer.randomly_undo_redo(rng, cx);
492                    }
493                });
494            } else {
495                self.primary_multibuffer.update(cx, |multibuffer, cx| {
496                    multibuffer.randomly_edit(rng, mutation_count, cx);
497                });
498            }
499        } else if rng.random() {
500            self.randomly_edit_excerpts(rng, mutation_count, cx);
501        } else {
502            for buffer in self.primary_multibuffer.read(cx).all_buffers() {
503                let diff = self
504                    .primary_multibuffer
505                    .read(cx)
506                    .diff_for(buffer.read(cx).remote_id())
507                    .unwrap();
508                let buffer_snapshot = buffer.read(cx).text_snapshot();
509                diff.update(cx, |diff, cx| {
510                    diff.recalculate_diff_sync(&buffer_snapshot, cx);
511                });
512                // TODO(split-diff) might be a good idea to try to separate the diff recalculation from the excerpt recalculation
513                let diff_snapshot = diff.read(cx).snapshot(cx);
514                let ranges = diff_snapshot
515                    .hunks(&buffer_snapshot)
516                    .map(|hunk| hunk.range.clone());
517                let path = PathKey::for_buffer(&buffer, cx);
518                self.set_excerpts_for_path(path, buffer, ranges, 2, diff, cx);
519            }
520        }
521
522        self.check_invariants(cx);
523    }
524}
525
526impl EventEmitter<EditorEvent> for SplittableEditor {}
527impl Focusable for SplittableEditor {
528    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
529        self.primary_editor.read(cx).focus_handle(cx)
530    }
531}
532
533impl Render for SplittableEditor {
534    fn render(
535        &mut self,
536        window: &mut ui::Window,
537        cx: &mut ui::Context<Self>,
538    ) -> impl ui::IntoElement {
539        let inner = if self.secondary.is_none() {
540            self.primary_editor.clone().into_any_element()
541        } else if let Some(active) = self.panes.panes().into_iter().next() {
542            self.panes
543                .render(
544                    None,
545                    &ActivePaneDecorator::new(active, &self.workspace),
546                    window,
547                    cx,
548                )
549                .into_any_element()
550        } else {
551            div().into_any_element()
552        };
553        div()
554            .id("splittable-editor")
555            .on_action(cx.listener(Self::split))
556            .on_action(cx.listener(Self::unsplit))
557            .size_full()
558            .child(inner)
559    }
560}
561
562impl SecondaryEditor {
563    fn sync_path_excerpts(
564        &mut self,
565        path_key: PathKey,
566        primary_multibuffer: &mut MultiBuffer,
567        diff: Entity<BufferDiff>,
568        cx: &mut App,
569    ) {
570        let excerpt_id = primary_multibuffer
571            .excerpts_for_path(&path_key)
572            .next()
573            .unwrap();
574        let primary_multibuffer_snapshot = primary_multibuffer.snapshot(cx);
575        let main_buffer = primary_multibuffer_snapshot
576            .buffer_for_excerpt(excerpt_id)
577            .unwrap();
578        let base_text_buffer = diff.read(cx).base_text_buffer();
579        let diff_snapshot = diff.read(cx).snapshot(cx);
580        let base_text_buffer_snapshot = base_text_buffer.read(cx).snapshot();
581        let new = primary_multibuffer
582            .excerpts_for_buffer(main_buffer.remote_id(), cx)
583            .into_iter()
584            .map(|(_, excerpt_range)| {
585                let point_range_to_base_text_point_range = |range: Range<Point>| {
586                    let start_row =
587                        diff_snapshot.row_to_base_text_row(range.start.row, main_buffer);
588                    let end_row = diff_snapshot.row_to_base_text_row(range.end.row, main_buffer);
589                    let end_column = diff_snapshot.base_text().line_len(end_row);
590                    Point::new(start_row, 0)..Point::new(end_row, end_column)
591                };
592                let primary = excerpt_range.primary.to_point(main_buffer);
593                let context = excerpt_range.context.to_point(main_buffer);
594                ExcerptRange {
595                    primary: point_range_to_base_text_point_range(dbg!(primary)),
596                    context: point_range_to_base_text_point_range(dbg!(context)),
597                }
598            })
599            .collect();
600
601        let main_buffer = primary_multibuffer.buffer(main_buffer.remote_id()).unwrap();
602
603        self.editor.update(cx, |editor, cx| {
604            editor.buffer().update(cx, |buffer, cx| {
605                buffer.update_path_excerpts(
606                    path_key,
607                    base_text_buffer,
608                    &base_text_buffer_snapshot,
609                    new,
610                    cx,
611                );
612                buffer.add_inverted_diff(diff, main_buffer, cx);
613            })
614        });
615    }
616}
617
618#[cfg(test)]
619mod tests {
620    use buffer_diff::BufferDiff;
621    use db::indoc;
622    use fs::FakeFs;
623    use gpui::AppContext as _;
624    use language::{Buffer, Capability};
625    use multi_buffer::MultiBuffer;
626    use project::Project;
627    use rand::{Rng, rngs::StdRng};
628    use settings::SettingsStore;
629    use ui::VisualContext as _;
630    use workspace::Workspace;
631
632    use crate::SplittableEditor;
633
634    fn init_test(cx: &mut gpui::TestAppContext) {
635        cx.update(|cx| {
636            let store = SettingsStore::test(cx);
637            cx.set_global(store);
638            theme::init(theme::LoadThemes::JustBase, cx);
639            crate::init(cx);
640        });
641    }
642
643    #[gpui::test]
644    async fn test_basic_excerpts(mut rng: StdRng, cx: &mut gpui::TestAppContext) {
645        init_test(cx);
646        let base_text = indoc! {"
647            hello
648        "};
649        let buffer_text = indoc! {"
650            HELLO!
651        "};
652        let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
653        let diff = cx.new(|cx| {
654            BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx)
655        });
656        let project = Project::test(FakeFs::new(cx.executor()), [], cx).await;
657        let (workspace, cx) =
658            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
659        let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
660        let editor = cx.new_window_entity(|window, cx| {
661            SplittableEditor::new_unsplit(multibuffer, project, workspace, window, cx)
662        });
663
664        let mutation_count = rng.random_range(0..100);
665        editor.update(cx, |editor, cx| {
666            editor.randomly_mutate(&mut rng, mutation_count, cx);
667        })
668
669        // for _ in 0..random() {
670        //     editor.update(cx, |editor, cx| {
671        //         randomly_mutate(primary_multibuffer);
672        //         editor.primary_editor().update(cx, |editor, cx| {
673        //             editor.edit(vec![(random()..random(), "...")], cx);
674        //         })
675        //     });
676        // }
677
678        // editor.read(cx).primary_editor().read(cx).display_map.read(cx)
679    }
680}