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.clone(),
309                    ranges,
310                    context_line_count,
311                    cx,
312                );
313                if !anchors.is_empty()
314                    && primary_multibuffer
315                        .diff_for(buffer.read(cx).remote_id())
316                        .is_none_or(|old_diff| old_diff.entity_id() != diff.entity_id())
317                {
318                    primary_multibuffer.add_diff(diff.clone(), cx);
319                }
320                if let Some(secondary) = &mut self.secondary {
321                    secondary.sync_path_excerpts(path, primary_multibuffer, diff, cx);
322                }
323                (anchors, added_a_new_excerpt)
324            })
325    }
326
327    fn expand_excerpts(
328        &mut self,
329        excerpt_ids: impl Iterator<Item = ExcerptId> + Clone,
330        lines: u32,
331        direction: ExpandExcerptDirection,
332        cx: &mut Context<Self>,
333    ) {
334        let mut corresponding_paths = HashMap::default();
335        self.primary_multibuffer.update(cx, |multibuffer, cx| {
336            let snapshot = multibuffer.snapshot(cx);
337            if self.secondary.is_some() {
338                corresponding_paths = excerpt_ids
339                    .clone()
340                    .map(|excerpt_id| {
341                        let path = multibuffer.path_for_excerpt(excerpt_id).unwrap();
342                        let buffer = snapshot.buffer_for_excerpt(excerpt_id).unwrap();
343                        let diff = multibuffer.diff_for(buffer.remote_id()).unwrap();
344                        (path, diff)
345                    })
346                    .collect::<HashMap<_, _>>();
347            }
348            multibuffer.expand_excerpts(excerpt_ids.clone(), lines, direction, cx);
349        });
350
351        if let Some(secondary) = &mut self.secondary {
352            self.primary_multibuffer.update(cx, |multibuffer, cx| {
353                for (path, diff) in corresponding_paths {
354                    secondary.sync_path_excerpts(path, multibuffer, diff, cx);
355                }
356            })
357        }
358    }
359
360    pub fn remove_excerpts_for_path(&mut self, path: PathKey, cx: &mut Context<Self>) {
361        self.primary_multibuffer.update(cx, |buffer, cx| {
362            buffer.remove_excerpts_for_path(path.clone(), cx)
363        });
364        if let Some(secondary) = &mut self.secondary {
365            secondary.remove_mappings_for_path(&path, cx);
366            secondary
367                .multibuffer
368                .update(cx, |buffer, cx| buffer.remove_excerpts_for_path(path, cx))
369        }
370    }
371}
372
373#[cfg(test)]
374impl SplittableEditor {
375    fn check_invariants(&self, quiesced: bool, cx: &App) {
376        use buffer_diff::DiffHunkStatusKind;
377        use collections::HashSet;
378        use multi_buffer::MultiBufferOffset;
379        use multi_buffer::MultiBufferRow;
380        use multi_buffer::MultiBufferSnapshot;
381
382        fn format_diff(snapshot: &MultiBufferSnapshot) -> String {
383            let text = snapshot.text();
384            let row_infos = snapshot.row_infos(MultiBufferRow(0)).collect::<Vec<_>>();
385            let boundary_rows = snapshot
386                .excerpt_boundaries_in_range(MultiBufferOffset(0)..)
387                .map(|b| b.row)
388                .collect::<HashSet<_>>();
389
390            text.split('\n')
391                .enumerate()
392                .zip(row_infos)
393                .map(|((ix, line), info)| {
394                    let marker = match info.diff_status.map(|status| status.kind) {
395                        Some(DiffHunkStatusKind::Added) => "+ ",
396                        Some(DiffHunkStatusKind::Deleted) => "- ",
397                        Some(DiffHunkStatusKind::Modified) => unreachable!(),
398                        None => {
399                            if !line.is_empty() {
400                                "  "
401                            } else {
402                                ""
403                            }
404                        }
405                    };
406                    let boundary_row = if boundary_rows.contains(&MultiBufferRow(ix as u32)) {
407                        "  ----------\n"
408                    } else {
409                        ""
410                    };
411                    let expand = info
412                        .expand_info
413                        .map(|expand_info| match expand_info.direction {
414                            ExpandExcerptDirection::Up => " [↑]",
415                            ExpandExcerptDirection::Down => " [↓]",
416                            ExpandExcerptDirection::UpAndDown => " [↕]",
417                        })
418                        .unwrap_or_default();
419
420                    format!("{boundary_row}{marker}{line}{expand}")
421                })
422                .collect::<Vec<_>>()
423                .join("\n")
424        }
425
426        let Some(secondary) = &self.secondary else {
427            return;
428        };
429
430        log::info!(
431            "primary:\n\n{}",
432            format_diff(&self.primary_multibuffer.read(cx).snapshot(cx))
433        );
434
435        log::info!(
436            "secondary:\n\n{}",
437            format_diff(&secondary.multibuffer.read(cx).snapshot(cx))
438        );
439
440        let primary_excerpts = self.primary_multibuffer.read(cx).excerpt_ids();
441        let secondary_excerpts = secondary.multibuffer.read(cx).excerpt_ids();
442        assert_eq!(primary_excerpts.len(), secondary_excerpts.len());
443
444        assert_eq!(
445            secondary.primary_to_secondary.len(),
446            primary_excerpts.len(),
447            "primary_to_secondary mapping count should match excerpt count"
448        );
449        assert_eq!(
450            secondary.secondary_to_primary.len(),
451            secondary_excerpts.len(),
452            "secondary_to_primary mapping count should match excerpt count"
453        );
454
455        for primary_id in &primary_excerpts {
456            assert!(
457                secondary.primary_to_secondary.contains_key(primary_id),
458                "primary excerpt {:?} should have a mapping to secondary",
459                primary_id
460            );
461        }
462        for secondary_id in &secondary_excerpts {
463            assert!(
464                secondary.secondary_to_primary.contains_key(secondary_id),
465                "secondary excerpt {:?} should have a mapping to primary",
466                secondary_id
467            );
468        }
469
470        for (primary_id, secondary_id) in &secondary.primary_to_secondary {
471            assert_eq!(
472                secondary.secondary_to_primary.get(secondary_id),
473                Some(primary_id),
474                "mappings should be bijective"
475            );
476        }
477
478        if quiesced {
479            let primary_snapshot = self.primary_multibuffer.read(cx).snapshot(cx);
480            let secondary_snapshot = secondary.multibuffer.read(cx).snapshot(cx);
481            let primary_diff_hunks = primary_snapshot
482                .diff_hunks()
483                .map(|hunk| hunk.diff_base_byte_range)
484                .collect::<Vec<_>>();
485            let secondary_diff_hunks = secondary_snapshot
486                .diff_hunks()
487                .map(|hunk| hunk.diff_base_byte_range)
488                .collect::<Vec<_>>();
489            pretty_assertions::assert_eq!(primary_diff_hunks, secondary_diff_hunks);
490
491            // Filtering out empty lines is a bit of a hack, to work around a case where
492            // the base text has a trailing newline but the current text doesn't, or vice versa.
493            // In this case, we get the additional newline on one side, but that line is not
494            // marked as added/deleted by rowinfos.
495            let primary_unmodified_rows = primary_snapshot
496                .text()
497                .split("\n")
498                .zip(primary_snapshot.row_infos(MultiBufferRow(0)))
499                .filter(|(line, row_info)| !line.is_empty() && row_info.diff_status.is_none())
500                .map(|(line, _)| line.to_owned())
501                .collect::<Vec<_>>();
502            let secondary_unmodified_rows = secondary_snapshot
503                .text()
504                .split("\n")
505                .zip(secondary_snapshot.row_infos(MultiBufferRow(0)))
506                .filter(|(line, row_info)| !line.is_empty() && row_info.diff_status.is_none())
507                .map(|(line, _)| line.to_owned())
508                .collect::<Vec<_>>();
509            pretty_assertions::assert_eq!(primary_unmodified_rows, secondary_unmodified_rows);
510        }
511    }
512
513    fn randomly_edit_excerpts(
514        &mut self,
515        rng: &mut impl rand::Rng,
516        mutation_count: usize,
517        cx: &mut Context<Self>,
518    ) {
519        use collections::HashSet;
520        use rand::prelude::*;
521        use std::env;
522        use util::RandomCharIter;
523
524        let max_excerpts = env::var("MAX_EXCERPTS")
525            .map(|i| i.parse().expect("invalid `MAX_EXCERPTS` variable"))
526            .unwrap_or(5);
527
528        for _ in 0..mutation_count {
529            let paths = self
530                .primary_multibuffer
531                .read(cx)
532                .paths()
533                .cloned()
534                .collect::<Vec<_>>();
535            let excerpt_ids = self.primary_multibuffer.read(cx).excerpt_ids();
536
537            if rng.random_bool(0.1) && !excerpt_ids.is_empty() {
538                let mut excerpts = HashSet::default();
539                for _ in 0..rng.random_range(0..excerpt_ids.len()) {
540                    excerpts.extend(excerpt_ids.choose(rng).copied());
541                }
542
543                let line_count = rng.random_range(0..5);
544
545                log::info!("Expanding excerpts {excerpts:?} by {line_count} lines");
546
547                self.expand_excerpts(
548                    excerpts.iter().cloned(),
549                    line_count,
550                    ExpandExcerptDirection::UpAndDown,
551                    cx,
552                );
553                continue;
554            }
555
556            if excerpt_ids.is_empty() || (rng.random() && excerpt_ids.len() < max_excerpts) {
557                let len = rng.random_range(100..500);
558                let text = RandomCharIter::new(&mut *rng).take(len).collect::<String>();
559                let buffer = cx.new(|cx| Buffer::local(text, cx));
560                log::info!(
561                    "Creating new buffer {} with text: {:?}",
562                    buffer.read(cx).remote_id(),
563                    buffer.read(cx).text()
564                );
565                let buffer_snapshot = buffer.read(cx).snapshot();
566                let diff = cx.new(|cx| BufferDiff::new_unchanged(&buffer_snapshot, cx));
567                // Create some initial diff hunks.
568                buffer.update(cx, |buffer, cx| {
569                    buffer.randomly_edit(rng, 1, cx);
570                });
571                let buffer_snapshot = buffer.read(cx).text_snapshot();
572                let ranges = diff.update(cx, |diff, cx| {
573                    diff.recalculate_diff_sync(&buffer_snapshot, cx);
574                    diff.snapshot(cx)
575                        .hunks(&buffer_snapshot)
576                        .map(|hunk| hunk.buffer_range.to_point(&buffer_snapshot))
577                        .collect::<Vec<_>>()
578                });
579                let path = PathKey::for_buffer(&buffer, cx);
580                self.set_excerpts_for_path(path, buffer, ranges, 2, diff, cx);
581            } else {
582                let remove_count = rng.random_range(1..=paths.len());
583                let paths_to_remove = paths
584                    .choose_multiple(rng, remove_count)
585                    .cloned()
586                    .collect::<Vec<_>>();
587                for path in paths_to_remove {
588                    self.remove_excerpts_for_path(path.clone(), cx);
589                }
590            }
591        }
592    }
593}
594
595impl EventEmitter<EditorEvent> for SplittableEditor {}
596impl Focusable for SplittableEditor {
597    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
598        self.primary_editor.read(cx).focus_handle(cx)
599    }
600}
601
602impl Render for SplittableEditor {
603    fn render(
604        &mut self,
605        window: &mut ui::Window,
606        cx: &mut ui::Context<Self>,
607    ) -> impl ui::IntoElement {
608        let inner = if self.secondary.is_none() {
609            self.primary_editor.clone().into_any_element()
610        } else if let Some(active) = self.panes.panes().into_iter().next() {
611            self.panes
612                .render(
613                    None,
614                    &ActivePaneDecorator::new(active, &self.workspace),
615                    window,
616                    cx,
617                )
618                .into_any_element()
619        } else {
620            div().into_any_element()
621        };
622        div()
623            .id("splittable-editor")
624            .on_action(cx.listener(Self::split))
625            .on_action(cx.listener(Self::unsplit))
626            .size_full()
627            .child(inner)
628    }
629}
630
631impl SecondaryEditor {
632    fn sync_path_excerpts(
633        &mut self,
634        path_key: PathKey,
635        primary_multibuffer: &mut MultiBuffer,
636        diff: Entity<BufferDiff>,
637        cx: &mut App,
638    ) {
639        let Some(excerpt_id) = primary_multibuffer.excerpts_for_path(&path_key).next() else {
640            self.remove_mappings_for_path(&path_key, cx);
641            self.multibuffer.update(cx, |multibuffer, cx| {
642                multibuffer.remove_excerpts_for_path(path_key, cx);
643            });
644            return;
645        };
646
647        let primary_excerpt_ids: Vec<ExcerptId> =
648            primary_multibuffer.excerpts_for_path(&path_key).collect();
649
650        let primary_multibuffer_snapshot = primary_multibuffer.snapshot(cx);
651        let main_buffer = primary_multibuffer_snapshot
652            .buffer_for_excerpt(excerpt_id)
653            .unwrap();
654        let base_text_buffer = diff.read(cx).base_text_buffer();
655        let diff_snapshot = diff.read(cx).snapshot(cx);
656        let base_text_buffer_snapshot = base_text_buffer.read(cx).snapshot();
657        let new = primary_multibuffer
658            .excerpts_for_buffer(main_buffer.remote_id(), cx)
659            .into_iter()
660            .map(|(_, excerpt_range)| {
661                let point_range_to_base_text_point_range = |range: Range<Point>| {
662                    let start_row = diff_snapshot.row_to_base_text_row(
663                        range.start.row,
664                        Bias::Left,
665                        main_buffer,
666                    );
667                    let end_row =
668                        diff_snapshot.row_to_base_text_row(range.end.row, Bias::Right, main_buffer);
669                    let end_column = diff_snapshot.base_text().line_len(end_row);
670                    Point::new(start_row, 0)..Point::new(end_row, end_column)
671                };
672                let primary = excerpt_range.primary.to_point(main_buffer);
673                let context = excerpt_range.context.to_point(main_buffer);
674                ExcerptRange {
675                    primary: point_range_to_base_text_point_range(primary),
676                    context: point_range_to_base_text_point_range(context),
677                }
678            })
679            .collect();
680
681        let main_buffer = primary_multibuffer.buffer(main_buffer.remote_id()).unwrap();
682
683        self.remove_mappings_for_path(&path_key, cx);
684
685        self.editor.update(cx, |editor, cx| {
686            editor.buffer().update(cx, |buffer, cx| {
687                let (ids, _) = buffer.update_path_excerpts(
688                    path_key.clone(),
689                    base_text_buffer.clone(),
690                    &base_text_buffer_snapshot,
691                    new,
692                    cx,
693                );
694                if !ids.is_empty()
695                    && buffer
696                        .diff_for(base_text_buffer.read(cx).remote_id())
697                        .is_none_or(|old_diff| old_diff.entity_id() != diff.entity_id())
698                {
699                    buffer.add_inverted_diff(diff, main_buffer, cx);
700                }
701            })
702        });
703
704        let secondary_excerpt_ids: Vec<ExcerptId> = self
705            .multibuffer
706            .read(cx)
707            .excerpts_for_path(&path_key)
708            .collect();
709
710        for (primary_id, secondary_id) in primary_excerpt_ids.into_iter().zip(secondary_excerpt_ids)
711        {
712            self.primary_to_secondary.insert(primary_id, secondary_id);
713            self.secondary_to_primary.insert(secondary_id, primary_id);
714        }
715    }
716
717    fn remove_mappings_for_path(&mut self, path_key: &PathKey, cx: &App) {
718        let secondary_excerpt_ids: Vec<ExcerptId> = self
719            .multibuffer
720            .read(cx)
721            .excerpts_for_path(path_key)
722            .collect();
723
724        for secondary_id in secondary_excerpt_ids {
725            if let Some(primary_id) = self.secondary_to_primary.remove(&secondary_id) {
726                self.primary_to_secondary.remove(&primary_id);
727            }
728        }
729    }
730}
731
732#[cfg(test)]
733mod tests {
734    use fs::FakeFs;
735    use gpui::AppContext as _;
736    use language::Capability;
737    use multi_buffer::{MultiBuffer, PathKey};
738    use project::Project;
739    use rand::rngs::StdRng;
740    use settings::SettingsStore;
741    use ui::VisualContext as _;
742    use workspace::Workspace;
743
744    use crate::SplittableEditor;
745
746    fn init_test(cx: &mut gpui::TestAppContext) {
747        cx.update(|cx| {
748            let store = SettingsStore::test(cx);
749            cx.set_global(store);
750            theme::init(theme::LoadThemes::JustBase, cx);
751            crate::init(cx);
752        });
753    }
754
755    #[ignore]
756    #[gpui::test(iterations = 100)]
757    async fn test_random_split_editor(mut rng: StdRng, cx: &mut gpui::TestAppContext) {
758        use rand::prelude::*;
759
760        init_test(cx);
761        let project = Project::test(FakeFs::new(cx.executor()), [], cx).await;
762        let (workspace, cx) =
763            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
764        let primary_multibuffer = cx.new(|cx| {
765            let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
766            multibuffer.set_all_diff_hunks_expanded(cx);
767            multibuffer
768        });
769        let editor = cx.new_window_entity(|window, cx| {
770            let mut editor =
771                SplittableEditor::new_unsplit(primary_multibuffer, project, workspace, window, cx);
772            editor.split(&Default::default(), window, cx);
773            editor
774        });
775
776        let operations = std::env::var("OPERATIONS")
777            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
778            .unwrap_or(20);
779        let rng = &mut rng;
780        for _ in 0..operations {
781            editor.update(cx, |editor, cx| {
782                let buffers = editor
783                    .primary_editor
784                    .read(cx)
785                    .buffer()
786                    .read(cx)
787                    .all_buffers();
788
789                if buffers.is_empty() {
790                    editor.randomly_edit_excerpts(rng, 2, cx);
791                    editor.check_invariants(true, cx);
792                    return;
793                }
794
795                let quiesced = match rng.random_range(0..100) {
796                    0..=69 if !buffers.is_empty() => {
797                        let buffer = buffers.iter().choose(rng).unwrap();
798                        buffer.update(cx, |buffer, cx| {
799                            if rng.random() {
800                                log::info!("randomly editing single buffer");
801                                buffer.randomly_edit(rng, 5, cx);
802                            } else {
803                                log::info!("randomly undoing/redoing in single buffer");
804                                buffer.randomly_undo_redo(rng, cx);
805                            }
806                        });
807                        false
808                    }
809                    70..=79 => {
810                        log::info!("mutating excerpts");
811                        editor.randomly_edit_excerpts(rng, 2, cx);
812                        false
813                    }
814                    80..=89 if !buffers.is_empty() => {
815                        log::info!("recalculating buffer diff");
816                        let buffer = buffers.iter().choose(rng).unwrap();
817                        let diff = editor
818                            .primary_multibuffer
819                            .read(cx)
820                            .diff_for(buffer.read(cx).remote_id())
821                            .unwrap();
822                        let buffer_snapshot = buffer.read(cx).text_snapshot();
823                        diff.update(cx, |diff, cx| {
824                            diff.recalculate_diff_sync(&buffer_snapshot, cx);
825                        });
826                        false
827                    }
828                    _ => {
829                        log::info!("quiescing");
830                        for buffer in buffers {
831                            let buffer_snapshot = buffer.read(cx).text_snapshot();
832                            let diff = editor
833                                .primary_multibuffer
834                                .read(cx)
835                                .diff_for(buffer.read(cx).remote_id())
836                                .unwrap();
837                            diff.update(cx, |diff, cx| {
838                                diff.recalculate_diff_sync(&buffer_snapshot, cx);
839                            });
840                            let diff_snapshot = diff.read(cx).snapshot(cx);
841                            let ranges = diff_snapshot
842                                .hunks(&buffer_snapshot)
843                                .map(|hunk| hunk.range)
844                                .collect::<Vec<_>>();
845                            let path = PathKey::for_buffer(&buffer, cx);
846                            editor.set_excerpts_for_path(path, buffer, ranges, 2, diff, cx);
847                        }
848                        true
849                    }
850                };
851
852                editor.check_invariants(quiesced, cx);
853            });
854        }
855    }
856}