split.rs

  1use std::ops::Range;
  2
  3use buffer_diff::BufferDiff;
  4use collections::HashMap;
  5use feature_flags::{FeatureFlag, FeatureFlagAppExt as _};
  6use gpui::{
  7    Action, AppContext as _, Entity, EventEmitter, Focusable, NoAction, Subscription, WeakEntity,
  8};
  9use language::{Buffer, Capability};
 10use multi_buffer::{Anchor, ExcerptId, ExcerptRange, ExpandExcerptDirection, MultiBuffer, PathKey};
 11use project::Project;
 12use rope::Point;
 13use text::{Bias, OffsetRangeExt as _};
 14use ui::{
 15    App, Context, InteractiveElement as _, IntoElement as _, ParentElement as _, Render,
 16    Styled as _, Window, div,
 17};
 18use workspace::{
 19    ActivePaneDecorator, Item, ItemHandle, Pane, PaneGroup, SplitDirection, Workspace,
 20};
 21
 22use crate::{Editor, EditorEvent};
 23
 24struct SplitDiffFeatureFlag;
 25
 26impl FeatureFlag for SplitDiffFeatureFlag {
 27    const NAME: &'static str = "split-diff";
 28
 29    fn enabled_for_staff() -> bool {
 30        true
 31    }
 32}
 33
 34#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
 35#[action(namespace = editor)]
 36struct SplitDiff;
 37
 38#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
 39#[action(namespace = editor)]
 40struct UnsplitDiff;
 41
 42pub struct SplittableEditor {
 43    primary_multibuffer: Entity<MultiBuffer>,
 44    primary_editor: Entity<Editor>,
 45    secondary: Option<SecondaryEditor>,
 46    panes: PaneGroup,
 47    workspace: WeakEntity<Workspace>,
 48    _subscriptions: Vec<Subscription>,
 49}
 50
 51struct SecondaryEditor {
 52    multibuffer: Entity<MultiBuffer>,
 53    editor: Entity<Editor>,
 54    pane: Entity<Pane>,
 55    has_latest_selection: bool,
 56    primary_to_secondary: HashMap<ExcerptId, ExcerptId>,
 57    secondary_to_primary: HashMap<ExcerptId, ExcerptId>,
 58    _subscriptions: Vec<Subscription>,
 59}
 60
 61impl SplittableEditor {
 62    pub fn primary_editor(&self) -> &Entity<Editor> {
 63        &self.primary_editor
 64    }
 65
 66    pub fn last_selected_editor(&self) -> &Entity<Editor> {
 67        if let Some(secondary) = &self.secondary
 68            && secondary.has_latest_selection
 69        {
 70            &secondary.editor
 71        } else {
 72            &self.primary_editor
 73        }
 74    }
 75
 76    pub fn new_unsplit(
 77        primary_multibuffer: Entity<MultiBuffer>,
 78        project: Entity<Project>,
 79        workspace: Entity<Workspace>,
 80        window: &mut Window,
 81        cx: &mut Context<Self>,
 82    ) -> Self {
 83        let primary_editor = cx.new(|cx| {
 84            let mut editor = Editor::for_multibuffer(
 85                primary_multibuffer.clone(),
 86                Some(project.clone()),
 87                window,
 88                cx,
 89            );
 90            editor.set_expand_all_diff_hunks(cx);
 91            editor
 92        });
 93        let pane = cx.new(|cx| {
 94            let mut pane = Pane::new(
 95                workspace.downgrade(),
 96                project,
 97                Default::default(),
 98                None,
 99                NoAction.boxed_clone(),
100                true,
101                window,
102                cx,
103            );
104            pane.set_should_display_tab_bar(|_, _| false);
105            pane.add_item(primary_editor.boxed_clone(), true, true, None, window, cx);
106            pane
107        });
108        let panes = PaneGroup::new(pane);
109        // TODO(split-diff) we might want to tag editor events with whether they came from primary/secondary
110        let subscriptions = vec![cx.subscribe(
111            &primary_editor,
112            |this, _, event: &EditorEvent, cx| match event {
113                EditorEvent::ExpandExcerptsRequested {
114                    excerpt_ids,
115                    lines,
116                    direction,
117                } => {
118                    this.expand_excerpts(excerpt_ids.iter().copied(), *lines, *direction, cx);
119                }
120                EditorEvent::SelectionsChanged { .. } => {
121                    if let Some(secondary) = &mut this.secondary {
122                        secondary.has_latest_selection = false;
123                    }
124                    cx.emit(event.clone());
125                }
126                _ => cx.emit(event.clone()),
127            },
128        )];
129
130        window.defer(cx, {
131            let workspace = workspace.downgrade();
132            let primary_editor = primary_editor.downgrade();
133            move |window, cx| {
134                workspace
135                    .update(cx, |workspace, cx| {
136                        primary_editor.update(cx, |editor, cx| {
137                            editor.added_to_workspace(workspace, window, cx);
138                        })
139                    })
140                    .ok();
141            }
142        });
143        Self {
144            primary_editor,
145            primary_multibuffer,
146            secondary: None,
147            panes,
148            workspace: workspace.downgrade(),
149            _subscriptions: subscriptions,
150        }
151    }
152
153    fn split(&mut self, _: &SplitDiff, window: &mut Window, cx: &mut Context<Self>) {
154        if !cx.has_flag::<SplitDiffFeatureFlag>() {
155            return;
156        }
157        if self.secondary.is_some() {
158            return;
159        }
160        let Some(workspace) = self.workspace.upgrade() else {
161            return;
162        };
163        let project = workspace.read(cx).project().clone();
164
165        let secondary_multibuffer = cx.new(|cx| {
166            let mut multibuffer = MultiBuffer::new(Capability::ReadOnly);
167            multibuffer.set_all_diff_hunks_expanded(cx);
168            multibuffer
169        });
170        let secondary_editor = cx.new(|cx| {
171            let mut editor = Editor::for_multibuffer(
172                secondary_multibuffer.clone(),
173                Some(project.clone()),
174                window,
175                cx,
176            );
177            editor.number_deleted_lines = true;
178            editor.set_delegate_expand_excerpts(true);
179            editor
180        });
181        let secondary_pane = cx.new(|cx| {
182            let mut pane = Pane::new(
183                workspace.downgrade(),
184                workspace.read(cx).project().clone(),
185                Default::default(),
186                None,
187                NoAction.boxed_clone(),
188                true,
189                window,
190                cx,
191            );
192            pane.set_should_display_tab_bar(|_, _| false);
193            pane.add_item(
194                ItemHandle::boxed_clone(&secondary_editor),
195                false,
196                false,
197                None,
198                window,
199                cx,
200            );
201            pane
202        });
203
204        let subscriptions = vec![cx.subscribe(
205            &secondary_editor,
206            |this, _, event: &EditorEvent, cx| match event {
207                EditorEvent::ExpandExcerptsRequested {
208                    excerpt_ids,
209                    lines,
210                    direction,
211                } => {
212                    if let Some(secondary) = &this.secondary {
213                        let primary_ids: Vec<_> = excerpt_ids
214                            .iter()
215                            .filter_map(|id| secondary.secondary_to_primary.get(id).copied())
216                            .collect();
217                        this.expand_excerpts(primary_ids.into_iter(), *lines, *direction, cx);
218                    }
219                }
220                EditorEvent::SelectionsChanged { .. } => {
221                    if let Some(secondary) = &mut this.secondary {
222                        secondary.has_latest_selection = true;
223                    }
224                    cx.emit(event.clone());
225                }
226                _ => cx.emit(event.clone()),
227            },
228        )];
229        let mut secondary = SecondaryEditor {
230            editor: secondary_editor,
231            multibuffer: secondary_multibuffer,
232            pane: secondary_pane.clone(),
233            has_latest_selection: false,
234            primary_to_secondary: HashMap::default(),
235            secondary_to_primary: HashMap::default(),
236            _subscriptions: subscriptions,
237        };
238        self.primary_editor.update(cx, |editor, cx| {
239            editor.set_delegate_expand_excerpts(true);
240            editor.buffer().update(cx, |primary_multibuffer, cx| {
241                primary_multibuffer.set_show_deleted_hunks(false, cx);
242                let paths = primary_multibuffer.paths().cloned().collect::<Vec<_>>();
243                for path in paths {
244                    let Some(excerpt_id) = primary_multibuffer.excerpts_for_path(&path).next()
245                    else {
246                        continue;
247                    };
248                    let snapshot = primary_multibuffer.snapshot(cx);
249                    let buffer = snapshot.buffer_for_excerpt(excerpt_id).unwrap();
250                    let diff = primary_multibuffer.diff_for(buffer.remote_id()).unwrap();
251                    secondary.sync_path_excerpts(path.clone(), primary_multibuffer, diff, cx);
252                }
253            })
254        });
255        self.secondary = Some(secondary);
256
257        let primary_pane = self.panes.first_pane();
258        self.panes
259            .split(&primary_pane, &secondary_pane, SplitDirection::Left, cx)
260            .unwrap();
261        cx.notify();
262    }
263
264    fn unsplit(&mut self, _: &UnsplitDiff, _: &mut Window, cx: &mut Context<Self>) {
265        let Some(secondary) = self.secondary.take() else {
266            return;
267        };
268        self.panes.remove(&secondary.pane, cx).unwrap();
269        self.primary_editor.update(cx, |primary, cx| {
270            primary.set_delegate_expand_excerpts(false);
271            primary.buffer().update(cx, |buffer, cx| {
272                buffer.set_show_deleted_hunks(true, cx);
273            });
274        });
275        cx.notify();
276    }
277
278    pub fn added_to_workspace(
279        &mut self,
280        workspace: &mut Workspace,
281        window: &mut Window,
282        cx: &mut Context<Self>,
283    ) {
284        self.workspace = workspace.weak_handle();
285        self.primary_editor.update(cx, |primary_editor, cx| {
286            primary_editor.added_to_workspace(workspace, window, cx);
287        });
288        if let Some(secondary) = &self.secondary {
289            secondary.editor.update(cx, |secondary_editor, cx| {
290                secondary_editor.added_to_workspace(workspace, window, cx);
291            });
292        }
293    }
294
295    pub fn set_excerpts_for_path(
296        &mut self,
297        path: PathKey,
298        buffer: Entity<Buffer>,
299        ranges: impl IntoIterator<Item = Range<Point>> + Clone,
300        context_line_count: u32,
301        diff: Entity<BufferDiff>,
302        cx: &mut Context<Self>,
303    ) -> (Vec<Range<Anchor>>, bool) {
304        self.primary_multibuffer
305            .update(cx, |primary_multibuffer, cx| {
306                let (anchors, added_a_new_excerpt) = primary_multibuffer.set_excerpts_for_path(
307                    path.clone(),
308                    buffer,
309                    ranges,
310                    context_line_count,
311                    cx,
312                );
313                primary_multibuffer.add_diff(diff.clone(), cx);
314                if let Some(secondary) = &mut self.secondary {
315                    secondary.sync_path_excerpts(path, primary_multibuffer, diff, cx);
316                }
317                (anchors, added_a_new_excerpt)
318            })
319    }
320
321    fn expand_excerpts(
322        &mut self,
323        excerpt_ids: impl Iterator<Item = ExcerptId> + Clone,
324        lines: u32,
325        direction: ExpandExcerptDirection,
326        cx: &mut Context<Self>,
327    ) {
328        let mut corresponding_paths = HashMap::default();
329        self.primary_multibuffer.update(cx, |multibuffer, cx| {
330            let snapshot = multibuffer.snapshot(cx);
331            if self.secondary.is_some() {
332                corresponding_paths = excerpt_ids
333                    .clone()
334                    .map(|excerpt_id| {
335                        let path = multibuffer.path_for_excerpt(excerpt_id).unwrap();
336                        let buffer = snapshot.buffer_for_excerpt(excerpt_id).unwrap();
337                        let diff = multibuffer.diff_for(buffer.remote_id()).unwrap();
338                        (path, diff)
339                    })
340                    .collect::<HashMap<_, _>>();
341            }
342            multibuffer.expand_excerpts(excerpt_ids.clone(), lines, direction, cx);
343        });
344
345        if let Some(secondary) = &mut self.secondary {
346            self.primary_multibuffer.update(cx, |multibuffer, cx| {
347                for (path, diff) in corresponding_paths {
348                    secondary.sync_path_excerpts(path, multibuffer, diff, cx);
349                }
350            })
351        }
352    }
353
354    pub fn remove_excerpts_for_path(&mut self, path: PathKey, cx: &mut Context<Self>) {
355        self.primary_multibuffer.update(cx, |buffer, cx| {
356            buffer.remove_excerpts_for_path(path.clone(), cx)
357        });
358        if let Some(secondary) = &mut self.secondary {
359            secondary.remove_mappings_for_path(&path, cx);
360            secondary
361                .multibuffer
362                .update(cx, |buffer, cx| buffer.remove_excerpts_for_path(path, cx))
363        }
364    }
365}
366
367#[cfg(test)]
368impl SplittableEditor {
369    fn check_invariants(&self, quiesced: bool, cx: &App) {
370        use buffer_diff::DiffHunkStatusKind;
371        use collections::HashSet;
372        use multi_buffer::MultiBufferOffset;
373        use multi_buffer::MultiBufferRow;
374        use multi_buffer::MultiBufferSnapshot;
375
376        fn format_diff(snapshot: &MultiBufferSnapshot) -> String {
377            let text = snapshot.text();
378            let row_infos = snapshot.row_infos(MultiBufferRow(0)).collect::<Vec<_>>();
379            let boundary_rows = snapshot
380                .excerpt_boundaries_in_range(MultiBufferOffset(0)..)
381                .map(|b| b.row)
382                .collect::<HashSet<_>>();
383
384            text.split('\n')
385                .enumerate()
386                .zip(row_infos)
387                .map(|((ix, line), info)| {
388                    let marker = match info.diff_status.map(|status| status.kind) {
389                        Some(DiffHunkStatusKind::Added) => "+ ",
390                        Some(DiffHunkStatusKind::Deleted) => "- ",
391                        Some(DiffHunkStatusKind::Modified) => unreachable!(),
392                        None => {
393                            if !line.is_empty() {
394                                "  "
395                            } else {
396                                ""
397                            }
398                        }
399                    };
400                    let boundary_row = if boundary_rows.contains(&MultiBufferRow(ix as u32)) {
401                        "  ----------\n"
402                    } else {
403                        ""
404                    };
405                    let expand = info
406                        .expand_info
407                        .map(|expand_info| match expand_info.direction {
408                            ExpandExcerptDirection::Up => " [↑]",
409                            ExpandExcerptDirection::Down => " [↓]",
410                            ExpandExcerptDirection::UpAndDown => " [↕]",
411                        })
412                        .unwrap_or_default();
413
414                    format!("{boundary_row}{marker}{line}{expand}")
415                })
416                .collect::<Vec<_>>()
417                .join("\n")
418        }
419
420        let Some(secondary) = &self.secondary else {
421            return;
422        };
423
424        log::info!(
425            "primary:\n\n{}",
426            format_diff(&self.primary_multibuffer.read(cx).snapshot(cx))
427        );
428
429        log::info!(
430            "secondary:\n\n{}",
431            format_diff(&secondary.multibuffer.read(cx).snapshot(cx))
432        );
433
434        let primary_excerpts = self.primary_multibuffer.read(cx).excerpt_ids();
435        let secondary_excerpts = secondary.multibuffer.read(cx).excerpt_ids();
436        assert_eq!(primary_excerpts.len(), secondary_excerpts.len());
437
438        assert_eq!(
439            secondary.primary_to_secondary.len(),
440            primary_excerpts.len(),
441            "primary_to_secondary mapping count should match excerpt count"
442        );
443        assert_eq!(
444            secondary.secondary_to_primary.len(),
445            secondary_excerpts.len(),
446            "secondary_to_primary mapping count should match excerpt count"
447        );
448
449        for primary_id in &primary_excerpts {
450            assert!(
451                secondary.primary_to_secondary.contains_key(primary_id),
452                "primary excerpt {:?} should have a mapping to secondary",
453                primary_id
454            );
455        }
456        for secondary_id in &secondary_excerpts {
457            assert!(
458                secondary.secondary_to_primary.contains_key(secondary_id),
459                "secondary excerpt {:?} should have a mapping to primary",
460                secondary_id
461            );
462        }
463
464        for (primary_id, secondary_id) in &secondary.primary_to_secondary {
465            assert_eq!(
466                secondary.secondary_to_primary.get(secondary_id),
467                Some(primary_id),
468                "mappings should be bijective"
469            );
470        }
471
472        if quiesced {
473            let primary_snapshot = self.primary_multibuffer.read(cx).snapshot(cx);
474            let secondary_snapshot = secondary.multibuffer.read(cx).snapshot(cx);
475            let primary_diff_hunks = primary_snapshot
476                .diff_hunks()
477                .map(|hunk| hunk.diff_base_byte_range)
478                .collect::<Vec<_>>();
479            let secondary_diff_hunks = secondary_snapshot
480                .diff_hunks()
481                .map(|hunk| hunk.diff_base_byte_range)
482                .collect::<Vec<_>>();
483            pretty_assertions::assert_eq!(primary_diff_hunks, secondary_diff_hunks);
484
485            // Filtering out empty lines is a bit of a hack, to work around a case where
486            // the base text has a trailing newline but the current text doesn't, or vice versa.
487            // In this case, we get the additional newline on one side, but that line is not
488            // marked as added/deleted by rowinfos.
489            let primary_unmodified_rows = primary_snapshot
490                .text()
491                .split("\n")
492                .zip(primary_snapshot.row_infos(MultiBufferRow(0)))
493                .filter(|(line, row_info)| !line.is_empty() && row_info.diff_status.is_none())
494                .map(|(line, _)| line.to_owned())
495                .collect::<Vec<_>>();
496            let secondary_unmodified_rows = secondary_snapshot
497                .text()
498                .split("\n")
499                .zip(secondary_snapshot.row_infos(MultiBufferRow(0)))
500                .filter(|(line, row_info)| !line.is_empty() && row_info.diff_status.is_none())
501                .map(|(line, _)| line.to_owned())
502                .collect::<Vec<_>>();
503            pretty_assertions::assert_eq!(primary_unmodified_rows, secondary_unmodified_rows);
504        }
505    }
506
507    fn randomly_edit_excerpts(
508        &mut self,
509        rng: &mut impl rand::Rng,
510        mutation_count: usize,
511        cx: &mut Context<Self>,
512    ) {
513        use collections::HashSet;
514        use rand::prelude::*;
515        use std::env;
516        use util::RandomCharIter;
517
518        let max_excerpts = env::var("MAX_EXCERPTS")
519            .map(|i| i.parse().expect("invalid `MAX_EXCERPTS` variable"))
520            .unwrap_or(5);
521
522        for _ in 0..mutation_count {
523            let paths = self
524                .primary_multibuffer
525                .read(cx)
526                .paths()
527                .cloned()
528                .collect::<Vec<_>>();
529            let excerpt_ids = self.primary_multibuffer.read(cx).excerpt_ids();
530
531            if rng.random_bool(0.1) && !excerpt_ids.is_empty() {
532                let mut excerpts = HashSet::default();
533                for _ in 0..rng.random_range(0..excerpt_ids.len()) {
534                    excerpts.extend(excerpt_ids.choose(rng).copied());
535                }
536
537                let line_count = rng.random_range(0..5);
538
539                log::info!("Expanding excerpts {excerpts:?} by {line_count} lines");
540
541                self.expand_excerpts(
542                    excerpts.iter().cloned(),
543                    line_count,
544                    ExpandExcerptDirection::UpAndDown,
545                    cx,
546                );
547                continue;
548            }
549
550            if excerpt_ids.is_empty() || (rng.random() && excerpt_ids.len() < max_excerpts) {
551                let len = rng.random_range(100..500);
552                let text = RandomCharIter::new(&mut *rng).take(len).collect::<String>();
553                let buffer = cx.new(|cx| Buffer::local(text, cx));
554                log::info!(
555                    "Creating new buffer {} with text: {:?}",
556                    buffer.read(cx).remote_id(),
557                    buffer.read(cx).text()
558                );
559                let buffer_snapshot = buffer.read(cx).snapshot();
560                let diff = cx.new(|cx| BufferDiff::new_unchanged(&buffer_snapshot, cx));
561                // Create some initial diff hunks.
562                buffer.update(cx, |buffer, cx| {
563                    buffer.randomly_edit(rng, 1, cx);
564                });
565                let buffer_snapshot = buffer.read(cx).text_snapshot();
566                let ranges = diff.update(cx, |diff, cx| {
567                    diff.recalculate_diff_sync(&buffer_snapshot, cx);
568                    diff.snapshot(cx)
569                        .hunks(&buffer_snapshot)
570                        .map(|hunk| hunk.buffer_range.to_point(&buffer_snapshot))
571                        .collect::<Vec<_>>()
572                });
573                let path = PathKey::for_buffer(&buffer, cx);
574                self.set_excerpts_for_path(path, buffer, ranges, 2, diff, cx);
575            } else {
576                let remove_count = rng.random_range(1..=paths.len());
577                let paths_to_remove = paths
578                    .choose_multiple(rng, remove_count)
579                    .cloned()
580                    .collect::<Vec<_>>();
581                for path in paths_to_remove {
582                    self.remove_excerpts_for_path(path.clone(), cx);
583                }
584            }
585        }
586    }
587}
588
589impl EventEmitter<EditorEvent> for SplittableEditor {}
590impl Focusable for SplittableEditor {
591    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
592        self.primary_editor.read(cx).focus_handle(cx)
593    }
594}
595
596impl Render for SplittableEditor {
597    fn render(
598        &mut self,
599        window: &mut ui::Window,
600        cx: &mut ui::Context<Self>,
601    ) -> impl ui::IntoElement {
602        let inner = if self.secondary.is_none() {
603            self.primary_editor.clone().into_any_element()
604        } else if let Some(active) = self.panes.panes().into_iter().next() {
605            self.panes
606                .render(
607                    None,
608                    &ActivePaneDecorator::new(active, &self.workspace),
609                    window,
610                    cx,
611                )
612                .into_any_element()
613        } else {
614            div().into_any_element()
615        };
616        div()
617            .id("splittable-editor")
618            .on_action(cx.listener(Self::split))
619            .on_action(cx.listener(Self::unsplit))
620            .size_full()
621            .child(inner)
622    }
623}
624
625impl SecondaryEditor {
626    fn sync_path_excerpts(
627        &mut self,
628        path_key: PathKey,
629        primary_multibuffer: &mut MultiBuffer,
630        diff: Entity<BufferDiff>,
631        cx: &mut App,
632    ) {
633        let Some(excerpt_id) = primary_multibuffer.excerpts_for_path(&path_key).next() else {
634            self.remove_mappings_for_path(&path_key, cx);
635            self.multibuffer.update(cx, |multibuffer, cx| {
636                multibuffer.remove_excerpts_for_path(path_key, cx);
637            });
638            return;
639        };
640
641        let primary_excerpt_ids: Vec<ExcerptId> =
642            primary_multibuffer.excerpts_for_path(&path_key).collect();
643
644        let primary_multibuffer_snapshot = primary_multibuffer.snapshot(cx);
645        let main_buffer = primary_multibuffer_snapshot
646            .buffer_for_excerpt(excerpt_id)
647            .unwrap();
648        let base_text_buffer = diff.read(cx).base_text_buffer();
649        let diff_snapshot = diff.read(cx).snapshot(cx);
650        let base_text_buffer_snapshot = base_text_buffer.read(cx).snapshot();
651        let new = primary_multibuffer
652            .excerpts_for_buffer(main_buffer.remote_id(), cx)
653            .into_iter()
654            .map(|(_, excerpt_range)| {
655                let point_range_to_base_text_point_range = |range: Range<Point>| {
656                    let start_row = diff_snapshot.row_to_base_text_row(
657                        range.start.row,
658                        Bias::Left,
659                        main_buffer,
660                    );
661                    let end_row =
662                        diff_snapshot.row_to_base_text_row(range.end.row, Bias::Right, main_buffer);
663                    let end_column = diff_snapshot.base_text().line_len(end_row);
664                    Point::new(start_row, 0)..Point::new(end_row, end_column)
665                };
666                let primary = excerpt_range.primary.to_point(main_buffer);
667                let context = excerpt_range.context.to_point(main_buffer);
668                ExcerptRange {
669                    primary: point_range_to_base_text_point_range(primary),
670                    context: point_range_to_base_text_point_range(context),
671                }
672            })
673            .collect();
674
675        let main_buffer = primary_multibuffer.buffer(main_buffer.remote_id()).unwrap();
676
677        self.remove_mappings_for_path(&path_key, cx);
678
679        self.editor.update(cx, |editor, cx| {
680            editor.buffer().update(cx, |buffer, cx| {
681                buffer.update_path_excerpts(
682                    path_key.clone(),
683                    base_text_buffer,
684                    &base_text_buffer_snapshot,
685                    new,
686                    cx,
687                );
688                buffer.add_inverted_diff(diff, main_buffer, cx);
689            })
690        });
691
692        let secondary_excerpt_ids: Vec<ExcerptId> = self
693            .multibuffer
694            .read(cx)
695            .excerpts_for_path(&path_key)
696            .collect();
697
698        for (primary_id, secondary_id) in primary_excerpt_ids.into_iter().zip(secondary_excerpt_ids)
699        {
700            self.primary_to_secondary.insert(primary_id, secondary_id);
701            self.secondary_to_primary.insert(secondary_id, primary_id);
702        }
703    }
704
705    fn remove_mappings_for_path(&mut self, path_key: &PathKey, cx: &App) {
706        let secondary_excerpt_ids: Vec<ExcerptId> = self
707            .multibuffer
708            .read(cx)
709            .excerpts_for_path(path_key)
710            .collect();
711
712        for secondary_id in secondary_excerpt_ids {
713            if let Some(primary_id) = self.secondary_to_primary.remove(&secondary_id) {
714                self.primary_to_secondary.remove(&primary_id);
715            }
716        }
717    }
718}
719
720#[cfg(test)]
721mod tests {
722    use fs::FakeFs;
723    use gpui::AppContext as _;
724    use language::Capability;
725    use multi_buffer::{MultiBuffer, PathKey};
726    use project::Project;
727    use rand::rngs::StdRng;
728    use settings::SettingsStore;
729    use ui::VisualContext as _;
730    use workspace::Workspace;
731
732    use crate::SplittableEditor;
733
734    fn init_test(cx: &mut gpui::TestAppContext) {
735        cx.update(|cx| {
736            let store = SettingsStore::test(cx);
737            cx.set_global(store);
738            theme::init(theme::LoadThemes::JustBase, cx);
739            crate::init(cx);
740        });
741    }
742
743    #[gpui::test(iterations = 100)]
744    async fn test_random_split_editor(mut rng: StdRng, cx: &mut gpui::TestAppContext) {
745        use rand::prelude::*;
746
747        init_test(cx);
748        let project = Project::test(FakeFs::new(cx.executor()), [], cx).await;
749        let (workspace, cx) =
750            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
751        let primary_multibuffer = cx.new(|cx| {
752            let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
753            multibuffer.set_all_diff_hunks_expanded(cx);
754            multibuffer
755        });
756        let editor = cx.new_window_entity(|window, cx| {
757            let mut editor =
758                SplittableEditor::new_unsplit(primary_multibuffer, project, workspace, window, cx);
759            editor.split(&Default::default(), window, cx);
760            editor
761        });
762
763        let operations = std::env::var("OPERATIONS")
764            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
765            .unwrap_or(20);
766        let rng = &mut rng;
767        for _ in 0..operations {
768            editor.update(cx, |editor, cx| {
769                let buffers = editor
770                    .primary_editor
771                    .read(cx)
772                    .buffer()
773                    .read(cx)
774                    .all_buffers();
775
776                if buffers.is_empty() {
777                    editor.randomly_edit_excerpts(rng, 2, cx);
778                    editor.check_invariants(true, cx);
779                    return;
780                }
781
782                let quiesced = match rng.random_range(0..100) {
783                    0..=69 if !buffers.is_empty() => {
784                        let buffer = buffers.iter().choose(rng).unwrap();
785                        buffer.update(cx, |buffer, cx| {
786                            if rng.random() {
787                                log::info!("randomly editing single buffer");
788                                buffer.randomly_edit(rng, 5, cx);
789                            } else {
790                                log::info!("randomly undoing/redoing in single buffer");
791                                buffer.randomly_undo_redo(rng, cx);
792                            }
793                        });
794                        false
795                    }
796                    70..=79 => {
797                        log::info!("mutating excerpts");
798                        editor.randomly_edit_excerpts(rng, 2, cx);
799                        false
800                    }
801                    80..=89 if !buffers.is_empty() => {
802                        log::info!("recalculating buffer diff");
803                        let buffer = buffers.iter().choose(rng).unwrap();
804                        let diff = editor
805                            .primary_multibuffer
806                            .read(cx)
807                            .diff_for(buffer.read(cx).remote_id())
808                            .unwrap();
809                        let buffer_snapshot = buffer.read(cx).text_snapshot();
810                        diff.update(cx, |diff, cx| {
811                            diff.recalculate_diff_sync(&buffer_snapshot, cx);
812                        });
813                        false
814                    }
815                    _ => {
816                        log::info!("quiescing");
817                        for buffer in buffers {
818                            let buffer_snapshot = buffer.read(cx).text_snapshot();
819                            let diff = editor
820                                .primary_multibuffer
821                                .read(cx)
822                                .diff_for(buffer.read(cx).remote_id())
823                                .unwrap();
824                            diff.update(cx, |diff, cx| {
825                                diff.recalculate_diff_sync(&buffer_snapshot, cx);
826                            });
827                            let diff_snapshot = diff.read(cx).snapshot(cx);
828                            let ranges = diff_snapshot
829                                .hunks(&buffer_snapshot)
830                                .map(|hunk| hunk.range)
831                                .collect::<Vec<_>>();
832                            let path = PathKey::for_buffer(&buffer, cx);
833                            editor.set_excerpts_for_path(path, buffer, ranges, 2, diff, cx);
834                        }
835                        true
836                    }
837                };
838
839                editor.check_invariants(quiesced, cx);
840            });
841        }
842    }
843}