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}