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