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