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_editor: Entity<Editor>,
 43    secondary: Option<SecondaryEditor>,
 44    panes: PaneGroup,
 45    workspace: WeakEntity<Workspace>,
 46    _subscriptions: Vec<Subscription>,
 47}
 48
 49struct SecondaryEditor {
 50    editor: Entity<Editor>,
 51    pane: Entity<Pane>,
 52    has_latest_selection: bool,
 53    _subscriptions: Vec<Subscription>,
 54}
 55
 56impl SplittableEditor {
 57    pub fn primary_editor(&self) -> &Entity<Editor> {
 58        &self.primary_editor
 59    }
 60
 61    pub fn last_selected_editor(&self) -> &Entity<Editor> {
 62        if let Some(secondary) = &self.secondary
 63            && secondary.has_latest_selection
 64        {
 65            &secondary.editor
 66        } else {
 67            &self.primary_editor
 68        }
 69    }
 70
 71    pub fn new_unsplit(
 72        buffer: Entity<MultiBuffer>,
 73        project: Entity<Project>,
 74        workspace: Entity<Workspace>,
 75        window: &mut Window,
 76        cx: &mut Context<Self>,
 77    ) -> Self {
 78        let primary_editor =
 79            cx.new(|cx| Editor::for_multibuffer(buffer, Some(project.clone()), window, cx));
 80        let pane = cx.new(|cx| {
 81            let mut pane = Pane::new(
 82                workspace.downgrade(),
 83                project,
 84                Default::default(),
 85                None,
 86                NoAction.boxed_clone(),
 87                true,
 88                window,
 89                cx,
 90            );
 91            pane.set_should_display_tab_bar(|_, _| false);
 92            pane.add_item(primary_editor.boxed_clone(), true, true, None, window, cx);
 93            pane
 94        });
 95        let panes = PaneGroup::new(pane);
 96        // TODO(split-diff) we might want to tag editor events with whether they came from primary/secondary
 97        let subscriptions =
 98            vec![
 99                cx.subscribe(&primary_editor, |this, _, event: &EditorEvent, cx| {
100                    if let EditorEvent::SelectionsChanged { .. } = event
101                        && let Some(secondary) = &mut this.secondary
102                    {
103                        secondary.has_latest_selection = false;
104                    }
105                    cx.emit(event.clone())
106                }),
107            ];
108
109        window.defer(cx, {
110            let workspace = workspace.downgrade();
111            let primary_editor = primary_editor.downgrade();
112            move |window, cx| {
113                workspace
114                    .update(cx, |workspace, cx| {
115                        primary_editor.update(cx, |editor, cx| {
116                            editor.added_to_workspace(workspace, window, cx);
117                        })
118                    })
119                    .ok();
120            }
121        });
122        Self {
123            primary_editor,
124            secondary: None,
125            panes,
126            workspace: workspace.downgrade(),
127            _subscriptions: subscriptions,
128        }
129    }
130
131    fn split(&mut self, _: &SplitDiff, window: &mut Window, cx: &mut Context<Self>) {
132        if !cx.has_flag::<SplitDiffFeatureFlag>() {
133            return;
134        }
135        if self.secondary.is_some() {
136            return;
137        }
138        let Some(workspace) = self.workspace.upgrade() else {
139            return;
140        };
141        let project = workspace.read(cx).project().clone();
142
143        let secondary_editor = cx.new(|cx| {
144            let multibuffer = cx.new(|cx| {
145                let mut multibuffer = MultiBuffer::new(Capability::ReadOnly);
146                multibuffer.set_all_diff_hunks_expanded(cx);
147                multibuffer
148            });
149            let mut editor =
150                Editor::for_multibuffer(multibuffer, Some(project.clone()), window, cx);
151            editor.number_deleted_lines = true;
152            editor
153        });
154        let secondary_pane = cx.new(|cx| {
155            let mut pane = Pane::new(
156                workspace.downgrade(),
157                workspace.read(cx).project().clone(),
158                Default::default(),
159                None,
160                NoAction.boxed_clone(),
161                true,
162                window,
163                cx,
164            );
165            pane.set_should_display_tab_bar(|_, _| false);
166            pane.add_item(
167                ItemHandle::boxed_clone(&secondary_editor),
168                false,
169                false,
170                None,
171                window,
172                cx,
173            );
174            pane
175        });
176
177        let subscriptions =
178            vec![
179                cx.subscribe(&secondary_editor, |this, _, event: &EditorEvent, cx| {
180                    if let EditorEvent::SelectionsChanged { .. } = event
181                        && let Some(secondary) = &mut this.secondary
182                    {
183                        secondary.has_latest_selection = true;
184                    }
185                    cx.emit(event.clone())
186                }),
187            ];
188        let mut secondary = SecondaryEditor {
189            editor: secondary_editor,
190            pane: secondary_pane.clone(),
191            has_latest_selection: false,
192            _subscriptions: subscriptions,
193        };
194        self.primary_editor.update(cx, |editor, cx| {
195            editor.buffer().update(cx, |primary_multibuffer, cx| {
196                primary_multibuffer.set_show_deleted_hunks(false, cx);
197                let paths = primary_multibuffer.paths().collect::<Vec<_>>();
198                for path in paths {
199                    let Some(excerpt_id) = primary_multibuffer.excerpts_for_path(&path).next()
200                    else {
201                        continue;
202                    };
203                    let snapshot = primary_multibuffer.snapshot(cx);
204                    let buffer = snapshot.buffer_for_excerpt(excerpt_id).unwrap();
205                    let diff = primary_multibuffer.diff_for(buffer.remote_id()).unwrap();
206                    secondary.sync_path_excerpts(path, primary_multibuffer, diff, cx);
207                }
208            })
209        });
210        self.secondary = Some(secondary);
211
212        let primary_pane = self.panes.first_pane();
213        self.panes
214            .split(&primary_pane, &secondary_pane, SplitDirection::Left)
215            .unwrap();
216        cx.notify();
217    }
218
219    fn unsplit(&mut self, _: &UnsplitDiff, _: &mut Window, cx: &mut Context<Self>) {
220        let Some(secondary) = self.secondary.take() else {
221            return;
222        };
223        self.panes.remove(&secondary.pane).unwrap();
224        self.primary_editor.update(cx, |primary, cx| {
225            primary.buffer().update(cx, |buffer, cx| {
226                buffer.set_show_deleted_hunks(true, cx);
227            });
228        });
229        cx.notify();
230    }
231
232    pub fn added_to_workspace(
233        &mut self,
234        workspace: &mut Workspace,
235        window: &mut Window,
236        cx: &mut Context<Self>,
237    ) {
238        self.workspace = workspace.weak_handle();
239        self.primary_editor.update(cx, |primary_editor, cx| {
240            primary_editor.added_to_workspace(workspace, window, cx);
241        });
242        if let Some(secondary) = &self.secondary {
243            secondary.editor.update(cx, |secondary_editor, cx| {
244                secondary_editor.added_to_workspace(workspace, window, cx);
245            });
246        }
247    }
248
249    pub fn set_excerpts_for_path(
250        &mut self,
251        path: PathKey,
252        buffer: Entity<Buffer>,
253        ranges: impl IntoIterator<Item = Range<Point>>,
254        context_line_count: u32,
255        diff: Entity<BufferDiff>,
256        cx: &mut Context<Self>,
257    ) -> (Vec<Range<Anchor>>, bool) {
258        self.primary_editor.update(cx, |editor, cx| {
259            editor.buffer().update(cx, |primary_multibuffer, cx| {
260                let (anchors, added_a_new_excerpt) = primary_multibuffer.set_excerpts_for_path(
261                    path.clone(),
262                    buffer,
263                    ranges,
264                    context_line_count,
265                    cx,
266                );
267                primary_multibuffer.add_diff(diff.clone(), cx);
268                if let Some(secondary) = &mut self.secondary {
269                    secondary.sync_path_excerpts(path, primary_multibuffer, diff, cx);
270                }
271                (anchors, added_a_new_excerpt)
272            })
273        })
274    }
275
276    fn expand_primary_excerpts(
277        &mut self,
278        excerpt_ids: impl Iterator<Item = ExcerptId> + Clone,
279        lines: u32,
280        direction: ExpandExcerptDirection,
281        cx: &mut Context<Self>,
282    ) {
283        self.update_primary_multibuffer(cx, |multibuffer, cx| {
284            multibuffer.expand_excerpts(excerpt_ids.clone(), lines, direction, cx);
285        });
286        let paths: Vec<PathKey> = excerpt_ids
287            .flat_map(|excerpt_id| {
288                self.primary_multibuffer(cx)
289                    .path_for_excerpt(excerpt_id)
290                    .cloned()
291            })
292            .collect();
293
294        if let Some(secondary) = &self.secondary {
295            self.update_primary_multibuffer(cx, |multibuffer, cx| {
296                let snapshot = primary_multibuffer.snapshot(cx);
297                for path in paths {
298                    let buffer = snapshot.buffer_for_excerpt(excerpt_id).unwrap();
299                    let diff = primary_multibuffer.diff_for(buffer.remote_id()).unwrap();
300                    secondary.sync_path_excerpts(path, multibuffer, diff, cx);
301                }
302            })
303        }
304    }
305
306    fn primary_multibuffer<'a>(&'a self, cx: &'a Context<Self>) -> &'a MultiBuffer {
307        self.primary_editor.read(cx).buffer.read(cx)
308    }
309
310    fn update_primary_multibuffer<R>(
311        &mut self,
312        cx: &mut Context<Self>,
313        f: impl FnOnce(&mut MultiBuffer, &mut Context<MultiBuffer>) -> R,
314    ) -> R {
315        self.primary_editor
316            .update(cx, |editor, cx| editor.buffer().update(cx, f))
317    }
318
319    #[cfg(test)]
320    fn check_invariants(&self, cx: &App) {
321        todo!()
322    }
323
324    #[cfg(test)]
325    fn randomly_edit_excerpts(
326        &mut self,
327        rng: &mut impl rand::Rng,
328        mutation_count: usize,
329        cx: &mut Context<Self>,
330    ) {
331        use rand::prelude::*;
332        use std::env;
333        use util::RandomCharIter;
334
335        let max_excerpts = env::var("MAX_EXCERPTS")
336            .map(|i| i.parse().expect("invalid `MAX_EXCERPTS` variable"))
337            .unwrap_or(5);
338
339        let excerpt_ids = self.primary_multibuffer(cx).excerpt_ids();
340
341        let mut buffers = Vec::new();
342        for _ in 0..mutation_count {
343            if rng.random_bool(0.05) {
344                log::info!("Clearing multi-buffer");
345                self.update_primary_multibuffer(cx, |multibuffer, cx| {
346                    multibuffer.clear(cx);
347                });
348                continue;
349            } else if rng.random_bool(0.1) && !excerpt_ids.is_empty() {
350                use collections::HashSet;
351
352                let mut excerpts = HashSet::default();
353                for _ in 0..rng.random_range(0..excerpt_ids.len()) {
354                    excerpts.extend(excerpt_ids.choose(rng).copied());
355                }
356
357                let line_count = rng.random_range(0..5);
358
359                log::info!("Expanding excerpts {excerpts:?} by {line_count} lines");
360
361                // FIXME we need an expand_excerpts API on the splittable editor that does a sync
362                self.expand_primary_excerpts(
363                    excerpts.iter().cloned(),
364                    line_count,
365                    ExpandExcerptDirection::UpAndDown,
366                    cx,
367                );
368                continue;
369            }
370
371            if excerpt_ids.is_empty() || (rng.random() && excerpt_ids.len() < max_excerpts) {
372                let buffer_handle = if rng.random() || self.buffers.is_empty() {
373                    let text = RandomCharIter::new(&mut *rng).take(10).collect::<String>();
374                    buffers.push(cx.new(|cx| Buffer::local(text, cx)));
375                    let buffer = buffers.last().unwrap().read(cx);
376                    log::info!(
377                        "Creating new buffer {} with text: {:?}",
378                        buffer.remote_id(),
379                        buffer.text()
380                    );
381                    buffers.last().unwrap().clone()
382                } else {
383                    self.buffers.values().choose(rng).unwrap().buffer.clone()
384                };
385
386                let buffer = buffer_handle.read(cx);
387                let buffer_text = buffer.text();
388                let ranges = (0..rng.random_range(0..5))
389                    .map(|_| {
390                        let end_ix =
391                            buffer.clip_offset(rng.random_range(0..=buffer.len()), Bias::Right);
392                        let start_ix = buffer.clip_offset(rng.random_range(0..=end_ix), Bias::Left);
393                        ExcerptRange::new(start_ix..end_ix)
394                    })
395                    .collect::<Vec<_>>();
396                log::info!(
397                    "Inserting excerpts from buffer {} and ranges {:?}: {:?}",
398                    buffer_handle.read(cx).remote_id(),
399                    ranges.iter().map(|r| &r.context).collect::<Vec<_>>(),
400                    ranges
401                        .iter()
402                        .map(|r| &buffer_text[r.context.clone()])
403                        .collect::<Vec<_>>()
404                );
405
406                let excerpt_id = self.push_excerpts(buffer_handle.clone(), ranges, cx);
407                log::info!("Inserted with ids: {:?}", excerpt_id);
408            } else {
409                let remove_count = rng.random_range(1..=excerpt_ids.len());
410                let mut excerpts_to_remove = excerpt_ids
411                    .choose_multiple(rng, remove_count)
412                    .cloned()
413                    .collect::<Vec<_>>();
414                let snapshot = self.snapshot.borrow();
415                excerpts_to_remove.sort_unstable_by(|a, b| a.cmp(b, &snapshot));
416                drop(snapshot);
417                log::info!("Removing excerpts {:?}", excerpts_to_remove);
418                self.remove_excerpts(excerpts_to_remove, cx);
419            }
420        }
421    }
422
423    #[cfg(test)]
424    fn randomly_mutate(
425        &mut self,
426        rng: &mut impl rand::Rng,
427        mutation_count: usize,
428        cx: &mut Context<Self>,
429    ) {
430        use rand::prelude::*;
431
432        if rng.random_bool(0.7) {
433            let buffers = self.primary_editor.read(cx).buffer().read(cx).all_buffers();
434            let buffer = buffers.iter().choose(rng);
435
436            if let Some(buffer) = buffer {
437                buffer.update(cx, |buffer, cx| {
438                    if rng.random() {
439                        buffer.randomly_edit(rng, mutation_count, cx);
440                    } else {
441                        buffer.randomly_undo_redo(rng, cx);
442                    }
443                });
444            } else {
445                self.update_primary_multibuffer(cx, |multibuffer, cx| {
446                    multibuffer.randomly_edit(rng, mutation_count, cx);
447                });
448            }
449        } else {
450            self.randomly_edit_excerpts(rng, mutation_count, cx);
451        }
452
453        self.check_invariants(cx);
454    }
455}
456
457impl EventEmitter<EditorEvent> for SplittableEditor {}
458impl Focusable for SplittableEditor {
459    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
460        self.primary_editor.read(cx).focus_handle(cx)
461    }
462}
463
464impl Render for SplittableEditor {
465    fn render(
466        &mut self,
467        window: &mut ui::Window,
468        cx: &mut ui::Context<Self>,
469    ) -> impl ui::IntoElement {
470        let inner = if self.secondary.is_none() {
471            self.primary_editor.clone().into_any_element()
472        } else if let Some(active) = self.panes.panes().into_iter().next() {
473            self.panes
474                .render(
475                    None,
476                    &ActivePaneDecorator::new(active, &self.workspace),
477                    window,
478                    cx,
479                )
480                .into_any_element()
481        } else {
482            div().into_any_element()
483        };
484        div()
485            .id("splittable-editor")
486            .on_action(cx.listener(Self::split))
487            .on_action(cx.listener(Self::unsplit))
488            .size_full()
489            .child(inner)
490    }
491}
492
493impl SecondaryEditor {
494    fn sync_path_excerpts(
495        &mut self,
496        path_key: PathKey,
497        primary_multibuffer: &mut MultiBuffer,
498        diff: Entity<BufferDiff>,
499        cx: &mut App,
500    ) {
501        let excerpt_id = primary_multibuffer
502            .excerpts_for_path(&path_key)
503            .next()
504            .unwrap();
505        let primary_multibuffer_snapshot = primary_multibuffer.snapshot(cx);
506        let main_buffer = primary_multibuffer_snapshot
507            .buffer_for_excerpt(excerpt_id)
508            .unwrap();
509        let base_text_buffer = diff.read(cx).base_text_buffer();
510        let diff_snapshot = diff.read(cx).snapshot(cx);
511        let base_text_buffer_snapshot = base_text_buffer.read(cx).snapshot();
512        let new = primary_multibuffer
513            .excerpts_for_buffer(main_buffer.remote_id(), cx)
514            .into_iter()
515            .map(|(_, excerpt_range)| {
516                let point_range_to_base_text_point_range = |range: Range<Point>| {
517                    let start_row =
518                        diff_snapshot.row_to_base_text_row(range.start.row, main_buffer);
519                    let start_column = 0;
520                    let end_row = diff_snapshot.row_to_base_text_row(range.end.row, main_buffer);
521                    let end_column = diff_snapshot.base_text().line_len(end_row);
522                    Point::new(start_row, start_column)..Point::new(end_row, end_column)
523                };
524                let primary = excerpt_range.primary.to_point(main_buffer);
525                let context = excerpt_range.context.to_point(main_buffer);
526                ExcerptRange {
527                    primary: point_range_to_base_text_point_range(primary),
528                    context: point_range_to_base_text_point_range(context),
529                }
530            })
531            .collect();
532
533        let main_buffer = primary_multibuffer.buffer(main_buffer.remote_id()).unwrap();
534
535        self.editor.update(cx, |editor, cx| {
536            editor.buffer().update(cx, |buffer, cx| {
537                buffer.update_path_excerpts(
538                    path_key,
539                    base_text_buffer,
540                    &base_text_buffer_snapshot,
541                    new,
542                    cx,
543                );
544                buffer.add_inverted_diff(diff, main_buffer, cx);
545            })
546        });
547    }
548}
549
550#[cfg(test)]
551mod tests {
552    use buffer_diff::BufferDiff;
553    use db::indoc;
554    use fs::FakeFs;
555    use gpui::AppContext as _;
556    use language::{Buffer, Capability};
557    use multi_buffer::MultiBuffer;
558    use project::Project;
559    use rand::rngs::StdRng;
560    use settings::SettingsStore;
561    use ui::VisualContext as _;
562    use workspace::Workspace;
563
564    use crate::SplittableEditor;
565
566    #[gpui::test]
567    async fn test_basic_excerpts(mut rng: StdRng, cx: &mut gpui::TestAppContext) {
568        cx.update(|cx| {
569            let store = SettingsStore::test(cx);
570            cx.set_global(store);
571            theme::init(theme::LoadThemes::JustBase, cx);
572            crate::init(cx);
573        });
574        let base_text = indoc! {"
575            hello
576        "};
577        let buffer_text = indoc! {"
578            HELLO!
579        "};
580        let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
581        let diff = cx.new(|cx| {
582            BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx)
583        });
584        let project = Project::test(FakeFs::new(cx.executor()), [], cx).await;
585        let (workspace, cx) =
586            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
587        let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
588        let editor = cx.new_window_entity(|window, cx| {
589            SplittableEditor::new_unsplit(multibuffer, project, workspace, window, cx)
590        });
591
592        let mutation_count = rng.gen_range(0..100);
593        editor.update(cx, |editor, cx| {
594            editor.randomly_mutate(rng, mutation_count, cx);
595        })
596
597        // for _ in 0..random() {
598        //     editor.update(cx, |editor, cx| {
599        //         randomly_mutate(primary_multibuffer);
600        //         editor.primary_editor().update(cx, |editor, cx| {
601        //             editor.edit(vec![(random()..random(), "...")], cx);
602        //         })
603        //     });
604        // }
605
606        // editor.read(cx).primary_editor().read(cx).display_map.read(cx)
607    }
608
609    // MultiB
610
611    // FIXME restore these tests in some form
612    // #[gpui::test]
613    // async fn test_filtered_editor_pair(cx: &mut gpui::TestAppContext) {
614    //     init_test(cx, |_| {});
615    //     let mut leader_cx = EditorTestContext::new(cx).await;
616
617    //     let diff_base = indoc!(
618    //         r#"
619    //         one
620    //         two
621    //         three
622    //         four
623    //         five
624    //         six
625    //         "#
626    //     );
627
628    //     let initial_state = indoc!(
629    //         r#"
630    //         ˇone
631    //         two
632    //         THREE
633    //         four
634    //         five
635    //         six
636    //         "#
637    //     );
638
639    //     leader_cx.set_state(initial_state);
640
641    //     leader_cx.set_head_text(&diff_base);
642    //     leader_cx.run_until_parked();
643
644    //     let follower = leader_cx.update_multibuffer(|leader, cx| {
645    //         leader.set_filter_mode(Some(MultiBufferFilterMode::KeepInsertions));
646    //         leader.set_all_diff_hunks_expanded(cx);
647    //         leader.get_or_create_follower(cx)
648    //     });
649    //     follower.update(cx, |follower, cx| {
650    //         follower.set_filter_mode(Some(MultiBufferFilterMode::KeepDeletions));
651    //         follower.set_all_diff_hunks_expanded(cx);
652    //     });
653
654    //     let follower_editor =
655    //         leader_cx.new_window_entity(|window, cx| build_editor(follower, window, cx));
656    //     // leader_cx.window.focus(&follower_editor.focus_handle(cx));
657
658    //     let mut follower_cx = EditorTestContext::for_editor_in(follower_editor, &mut leader_cx).await;
659    //     cx.run_until_parked();
660
661    //     leader_cx.assert_editor_state(initial_state);
662    //     follower_cx.assert_editor_state(indoc! {
663    //         r#"
664    //         ˇone
665    //         two
666    //         three
667    //         four
668    //         five
669    //         six
670    //         "#
671    //     });
672
673    //     follower_cx.editor(|editor, _window, cx| {
674    //         assert!(editor.read_only(cx));
675    //     });
676
677    //     leader_cx.update_editor(|editor, _window, cx| {
678    //         editor.edit([(Point::new(4, 0)..Point::new(5, 0), "FIVE\n")], cx);
679    //     });
680    //     cx.run_until_parked();
681
682    //     leader_cx.assert_editor_state(indoc! {
683    //         r#"
684    //         ˇone
685    //         two
686    //         THREE
687    //         four
688    //         FIVE
689    //         six
690    //         "#
691    //     });
692
693    //     follower_cx.assert_editor_state(indoc! {
694    //         r#"
695    //         ˇone
696    //         two
697    //         three
698    //         four
699    //         five
700    //         six
701    //         "#
702    //     });
703
704    //     leader_cx.update_editor(|editor, _window, cx| {
705    //         editor.edit([(Point::new(6, 0)..Point::new(6, 0), "SEVEN")], cx);
706    //     });
707    //     cx.run_until_parked();
708
709    //     leader_cx.assert_editor_state(indoc! {
710    //         r#"
711    //         ˇone
712    //         two
713    //         THREE
714    //         four
715    //         FIVE
716    //         six
717    //         SEVEN"#
718    //     });
719
720    //     follower_cx.assert_editor_state(indoc! {
721    //         r#"
722    //         ˇone
723    //         two
724    //         three
725    //         four
726    //         five
727    //         six
728    //         "#
729    //     });
730
731    //     leader_cx.update_editor(|editor, window, cx| {
732    //         editor.move_down(&MoveDown, window, cx);
733    //         editor.refresh_selected_text_highlights(true, window, cx);
734    //     });
735    //     leader_cx.run_until_parked();
736    // }
737
738    // #[gpui::test]
739    // async fn test_filtered_editor_pair_complex(cx: &mut gpui::TestAppContext) {
740    //     init_test(cx, |_| {});
741    //     let base_text = "base\n";
742    //     let buffer_text = "buffer\n";
743
744    //     let buffer1 = cx.new(|cx| Buffer::local(buffer_text, cx));
745    //     let diff1 = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer1, cx));
746
747    //     let extra_buffer_1 = cx.new(|cx| Buffer::local("dummy text 1\n", cx));
748    //     let extra_diff_1 = cx.new(|cx| BufferDiff::new_with_base_text("", &extra_buffer_1, cx));
749    //     let extra_buffer_2 = cx.new(|cx| Buffer::local("dummy text 2\n", cx));
750    //     let extra_diff_2 = cx.new(|cx| BufferDiff::new_with_base_text("", &extra_buffer_2, cx));
751
752    //     let leader = cx.new(|cx| {
753    //         let mut leader = MultiBuffer::new(Capability::ReadWrite);
754    //         leader.set_all_diff_hunks_expanded(cx);
755    //         leader.set_filter_mode(Some(MultiBufferFilterMode::KeepInsertions));
756    //         leader
757    //     });
758    //     let follower = leader.update(cx, |leader, cx| leader.get_or_create_follower(cx));
759    //     follower.update(cx, |follower, _| {
760    //         follower.set_filter_mode(Some(MultiBufferFilterMode::KeepDeletions));
761    //     });
762
763    //     leader.update(cx, |leader, cx| {
764    //         leader.insert_excerpts_after(
765    //             ExcerptId::min(),
766    //             extra_buffer_2.clone(),
767    //             vec![ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
768    //             cx,
769    //         );
770    //         leader.add_diff(extra_diff_2.clone(), cx);
771
772    //         leader.insert_excerpts_after(
773    //             ExcerptId::min(),
774    //             extra_buffer_1.clone(),
775    //             vec![ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
776    //             cx,
777    //         );
778    //         leader.add_diff(extra_diff_1.clone(), cx);
779
780    //         leader.insert_excerpts_after(
781    //             ExcerptId::min(),
782    //             buffer1.clone(),
783    //             vec![ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
784    //             cx,
785    //         );
786    //         leader.add_diff(diff1.clone(), cx);
787    //     });
788
789    //     cx.run_until_parked();
790    //     let mut cx = cx.add_empty_window();
791
792    //     let leader_editor = cx
793    //         .new_window_entity(|window, cx| Editor::for_multibuffer(leader.clone(), None, window, cx));
794    //     let follower_editor = cx.new_window_entity(|window, cx| {
795    //         Editor::for_multibuffer(follower.clone(), None, window, cx)
796    //     });
797
798    //     let mut leader_cx = EditorTestContext::for_editor_in(leader_editor.clone(), &mut cx).await;
799    //     leader_cx.assert_editor_state(indoc! {"
800    //        ˇbuffer
801
802    //        dummy text 1
803
804    //        dummy text 2
805    //     "});
806    //     let mut follower_cx = EditorTestContext::for_editor_in(follower_editor.clone(), &mut cx).await;
807    //     follower_cx.assert_editor_state(indoc! {"
808    //         ˇbase
809
810    //     "});
811    // }
812}