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