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().cloned().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.clone(), 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, cx)
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, cx).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).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 .cloned()
469 .collect::<Vec<_>>();
470 let excerpt_ids = self.primary_multibuffer.read(cx).excerpt_ids();
471
472 if rng.random_bool(0.1) && !excerpt_ids.is_empty() {
473 let mut excerpts = HashSet::default();
474 for _ in 0..rng.random_range(0..excerpt_ids.len()) {
475 excerpts.extend(excerpt_ids.choose(rng).copied());
476 }
477
478 let line_count = rng.random_range(0..5);
479
480 log::info!("Expanding excerpts {excerpts:?} by {line_count} lines");
481
482 self.expand_excerpts(
483 excerpts.iter().cloned(),
484 line_count,
485 ExpandExcerptDirection::UpAndDown,
486 cx,
487 );
488 continue;
489 }
490
491 if excerpt_ids.is_empty() || (rng.random() && excerpt_ids.len() < max_excerpts) {
492 let len = rng.random_range(100..500);
493 let text = RandomCharIter::new(&mut *rng).take(len).collect::<String>();
494 let buffer = cx.new(|cx| Buffer::local(text, cx));
495 log::info!(
496 "Creating new buffer {} with text: {:?}",
497 buffer.read(cx).remote_id(),
498 buffer.read(cx).text()
499 );
500 let buffer_snapshot = buffer.read(cx).snapshot();
501 let diff = cx.new(|cx| BufferDiff::new_unchanged(&buffer_snapshot, cx));
502 // Create some initial diff hunks.
503 buffer.update(cx, |buffer, cx| {
504 buffer.randomly_edit(rng, 1, cx);
505 });
506 let buffer_snapshot = buffer.read(cx).text_snapshot();
507 let ranges = diff.update(cx, |diff, cx| {
508 diff.recalculate_diff_sync(&buffer_snapshot, cx);
509 diff.snapshot(cx)
510 .hunks(&buffer_snapshot)
511 .map(|hunk| hunk.buffer_range.to_point(&buffer_snapshot))
512 .collect::<Vec<_>>()
513 });
514 let path = PathKey::for_buffer(&buffer, cx);
515 self.set_excerpts_for_path(path, buffer, ranges, 2, diff, cx);
516 } else {
517 let remove_count = rng.random_range(1..=paths.len());
518 let paths_to_remove = paths
519 .choose_multiple(rng, remove_count)
520 .cloned()
521 .collect::<Vec<_>>();
522 for path in paths_to_remove {
523 self.remove_excerpts_for_path(path.clone(), cx);
524 }
525 }
526 }
527 }
528}
529
530impl EventEmitter<EditorEvent> for SplittableEditor {}
531impl Focusable for SplittableEditor {
532 fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
533 self.primary_editor.read(cx).focus_handle(cx)
534 }
535}
536
537impl Render for SplittableEditor {
538 fn render(
539 &mut self,
540 window: &mut ui::Window,
541 cx: &mut ui::Context<Self>,
542 ) -> impl ui::IntoElement {
543 let inner = if self.secondary.is_none() {
544 self.primary_editor.clone().into_any_element()
545 } else if let Some(active) = self.panes.panes().into_iter().next() {
546 self.panes
547 .render(
548 None,
549 &ActivePaneDecorator::new(active, &self.workspace),
550 window,
551 cx,
552 )
553 .into_any_element()
554 } else {
555 div().into_any_element()
556 };
557 div()
558 .id("splittable-editor")
559 .on_action(cx.listener(Self::split))
560 .on_action(cx.listener(Self::unsplit))
561 .size_full()
562 .child(inner)
563 }
564}
565
566impl SecondaryEditor {
567 fn sync_path_excerpts(
568 &mut self,
569 path_key: PathKey,
570 primary_multibuffer: &mut MultiBuffer,
571 diff: Entity<BufferDiff>,
572 cx: &mut App,
573 ) {
574 let Some(excerpt_id) = primary_multibuffer.excerpts_for_path(&path_key).next() else {
575 self.multibuffer.update(cx, |multibuffer, cx| {
576 multibuffer.remove_excerpts_for_path(path_key, cx);
577 });
578 return;
579 };
580 let primary_multibuffer_snapshot = primary_multibuffer.snapshot(cx);
581 let main_buffer = primary_multibuffer_snapshot
582 .buffer_for_excerpt(excerpt_id)
583 .unwrap();
584 let base_text_buffer = diff.read(cx).base_text_buffer();
585 let diff_snapshot = diff.read(cx).snapshot(cx);
586 let base_text_buffer_snapshot = base_text_buffer.read(cx).snapshot();
587 let new = primary_multibuffer
588 .excerpts_for_buffer(main_buffer.remote_id(), cx)
589 .into_iter()
590 .map(|(_, excerpt_range)| {
591 let point_range_to_base_text_point_range = |range: Range<Point>| {
592 let start_row = diff_snapshot.row_to_base_text_row(
593 range.start.row,
594 Bias::Left,
595 main_buffer,
596 );
597 let end_row =
598 diff_snapshot.row_to_base_text_row(range.end.row, Bias::Right, main_buffer);
599 let end_column = diff_snapshot.base_text().line_len(end_row);
600 Point::new(start_row, 0)..Point::new(end_row, end_column)
601 };
602 let primary = excerpt_range.primary.to_point(main_buffer);
603 let context = excerpt_range.context.to_point(main_buffer);
604 ExcerptRange {
605 primary: point_range_to_base_text_point_range(primary),
606 context: point_range_to_base_text_point_range(context),
607 }
608 })
609 .collect();
610
611 let main_buffer = primary_multibuffer.buffer(main_buffer.remote_id()).unwrap();
612
613 self.editor.update(cx, |editor, cx| {
614 editor.buffer().update(cx, |buffer, cx| {
615 buffer.update_path_excerpts(
616 path_key,
617 base_text_buffer,
618 &base_text_buffer_snapshot,
619 new,
620 cx,
621 );
622 buffer.add_inverted_diff(diff, main_buffer, cx);
623 })
624 });
625 }
626}
627
628#[cfg(test)]
629mod tests {
630 use fs::FakeFs;
631 use gpui::AppContext as _;
632 use language::Capability;
633 use multi_buffer::{MultiBuffer, PathKey};
634 use project::Project;
635 use rand::rngs::StdRng;
636 use settings::SettingsStore;
637 use ui::VisualContext as _;
638 use workspace::Workspace;
639
640 use crate::SplittableEditor;
641
642 fn init_test(cx: &mut gpui::TestAppContext) {
643 cx.update(|cx| {
644 let store = SettingsStore::test(cx);
645 cx.set_global(store);
646 theme::init(theme::LoadThemes::JustBase, cx);
647 crate::init(cx);
648 });
649 }
650
651 #[gpui::test(iterations = 100)]
652 async fn test_random_split_editor(mut rng: StdRng, cx: &mut gpui::TestAppContext) {
653 use rand::prelude::*;
654
655 init_test(cx);
656 let project = Project::test(FakeFs::new(cx.executor()), [], cx).await;
657 let (workspace, cx) =
658 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
659 let primary_multibuffer = cx.new(|cx| {
660 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
661 multibuffer.set_all_diff_hunks_expanded(cx);
662 multibuffer
663 });
664 let editor = cx.new_window_entity(|window, cx| {
665 let mut editor =
666 SplittableEditor::new_unsplit(primary_multibuffer, project, workspace, window, cx);
667 editor.split(&Default::default(), window, cx);
668 editor
669 });
670
671 let operations = std::env::var("OPERATIONS")
672 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
673 .unwrap_or(20);
674 let rng = &mut rng;
675 for _ in 0..operations {
676 editor.update(cx, |editor, cx| {
677 let buffers = editor
678 .primary_editor
679 .read(cx)
680 .buffer()
681 .read(cx)
682 .all_buffers();
683
684 if buffers.is_empty() {
685 editor.randomly_edit_excerpts(rng, 2, cx);
686 editor.check_invariants(true, cx);
687 return;
688 }
689
690 let quiesced = match rng.random_range(0..100) {
691 0..=69 if !buffers.is_empty() => {
692 let buffer = buffers.iter().choose(rng).unwrap();
693 buffer.update(cx, |buffer, cx| {
694 if rng.random() {
695 log::info!("randomly editing single buffer");
696 buffer.randomly_edit(rng, 5, cx);
697 } else {
698 log::info!("randomly undoing/redoing in single buffer");
699 buffer.randomly_undo_redo(rng, cx);
700 }
701 });
702 false
703 }
704 70..=79 => {
705 log::info!("mutating excerpts");
706 editor.randomly_edit_excerpts(rng, 2, cx);
707 false
708 }
709 80..=89 if !buffers.is_empty() => {
710 log::info!("recalculating buffer diff");
711 let buffer = buffers.iter().choose(rng).unwrap();
712 let diff = editor
713 .primary_multibuffer
714 .read(cx)
715 .diff_for(buffer.read(cx).remote_id())
716 .unwrap();
717 let buffer_snapshot = buffer.read(cx).text_snapshot();
718 diff.update(cx, |diff, cx| {
719 diff.recalculate_diff_sync(&buffer_snapshot, cx);
720 });
721 false
722 }
723 _ => {
724 log::info!("quiescing");
725 for buffer in buffers {
726 let buffer_snapshot = buffer.read(cx).text_snapshot();
727 let diff = editor
728 .primary_multibuffer
729 .read(cx)
730 .diff_for(buffer.read(cx).remote_id())
731 .unwrap();
732 diff.update(cx, |diff, cx| {
733 diff.recalculate_diff_sync(&buffer_snapshot, cx);
734 });
735 let diff_snapshot = diff.read(cx).snapshot(cx);
736 let ranges = diff_snapshot
737 .hunks(&buffer_snapshot)
738 .map(|hunk| hunk.range)
739 .collect::<Vec<_>>();
740 let path = PathKey::for_buffer(&buffer, cx);
741 editor.set_excerpts_for_path(path, buffer, ranges, 2, diff, cx);
742 }
743 true
744 }
745 };
746
747 editor.check_invariants(quiesced, cx);
748 });
749 }
750 }
751}