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_multibuffer: Entity<MultiBuffer>,
43 primary_editor: Entity<Editor>,
44 secondary: Option<SecondaryEditor>,
45 panes: PaneGroup,
46 workspace: WeakEntity<Workspace>,
47 _subscriptions: Vec<Subscription>,
48}
49
50struct SecondaryEditor {
51 multibuffer: Entity<MultiBuffer>,
52 editor: Entity<Editor>,
53 pane: Entity<Pane>,
54 has_latest_selection: bool,
55 _subscriptions: Vec<Subscription>,
56}
57
58impl SplittableEditor {
59 pub fn primary_editor(&self) -> &Entity<Editor> {
60 &self.primary_editor
61 }
62
63 pub fn last_selected_editor(&self) -> &Entity<Editor> {
64 if let Some(secondary) = &self.secondary
65 && secondary.has_latest_selection
66 {
67 &secondary.editor
68 } else {
69 &self.primary_editor
70 }
71 }
72
73 pub fn new_unsplit(
74 primary_multibuffer: Entity<MultiBuffer>,
75 project: Entity<Project>,
76 workspace: Entity<Workspace>,
77 window: &mut Window,
78 cx: &mut Context<Self>,
79 ) -> Self {
80 let primary_editor = cx.new(|cx| {
81 Editor::for_multibuffer(
82 primary_multibuffer.clone(),
83 Some(project.clone()),
84 window,
85 cx,
86 )
87 });
88 let pane = cx.new(|cx| {
89 let mut pane = Pane::new(
90 workspace.downgrade(),
91 project,
92 Default::default(),
93 None,
94 NoAction.boxed_clone(),
95 true,
96 window,
97 cx,
98 );
99 pane.set_should_display_tab_bar(|_, _| false);
100 pane.add_item(primary_editor.boxed_clone(), true, true, None, window, cx);
101 pane
102 });
103 let panes = PaneGroup::new(pane);
104 // TODO(split-diff) we might want to tag editor events with whether they came from primary/secondary
105 let subscriptions =
106 vec![
107 cx.subscribe(&primary_editor, |this, _, event: &EditorEvent, cx| {
108 if let EditorEvent::SelectionsChanged { .. } = event
109 && let Some(secondary) = &mut this.secondary
110 {
111 secondary.has_latest_selection = false;
112 }
113 cx.emit(event.clone())
114 }),
115 ];
116
117 window.defer(cx, {
118 let workspace = workspace.downgrade();
119 let primary_editor = primary_editor.downgrade();
120 move |window, cx| {
121 workspace
122 .update(cx, |workspace, cx| {
123 primary_editor.update(cx, |editor, cx| {
124 editor.added_to_workspace(workspace, window, cx);
125 })
126 })
127 .ok();
128 }
129 });
130 Self {
131 primary_editor,
132 primary_multibuffer,
133 secondary: None,
134 panes,
135 workspace: workspace.downgrade(),
136 _subscriptions: subscriptions,
137 }
138 }
139
140 fn split(&mut self, _: &SplitDiff, window: &mut Window, cx: &mut Context<Self>) {
141 if !cx.has_flag::<SplitDiffFeatureFlag>() {
142 return;
143 }
144 if self.secondary.is_some() {
145 return;
146 }
147 let Some(workspace) = self.workspace.upgrade() else {
148 return;
149 };
150 let project = workspace.read(cx).project().clone();
151
152 let secondary_multibuffer = cx.new(|cx| {
153 let mut multibuffer = MultiBuffer::new(Capability::ReadOnly);
154 multibuffer.set_all_diff_hunks_expanded(cx);
155 multibuffer
156 });
157 let secondary_editor = cx.new(|cx| {
158 let mut editor = Editor::for_multibuffer(
159 secondary_multibuffer.clone(),
160 Some(project.clone()),
161 window,
162 cx,
163 );
164 editor.number_deleted_lines = true;
165 editor
166 });
167 let secondary_pane = cx.new(|cx| {
168 let mut pane = Pane::new(
169 workspace.downgrade(),
170 workspace.read(cx).project().clone(),
171 Default::default(),
172 None,
173 NoAction.boxed_clone(),
174 true,
175 window,
176 cx,
177 );
178 pane.set_should_display_tab_bar(|_, _| false);
179 pane.add_item(
180 ItemHandle::boxed_clone(&secondary_editor),
181 false,
182 false,
183 None,
184 window,
185 cx,
186 );
187 pane
188 });
189
190 let subscriptions =
191 vec![
192 cx.subscribe(&secondary_editor, |this, _, event: &EditorEvent, cx| {
193 if let EditorEvent::SelectionsChanged { .. } = event
194 && let Some(secondary) = &mut this.secondary
195 {
196 secondary.has_latest_selection = true;
197 }
198 cx.emit(event.clone())
199 }),
200 ];
201 let mut secondary = SecondaryEditor {
202 editor: secondary_editor,
203 multibuffer: secondary_multibuffer,
204 pane: secondary_pane.clone(),
205 has_latest_selection: false,
206 _subscriptions: subscriptions,
207 };
208 self.primary_editor.update(cx, |editor, cx| {
209 editor.buffer().update(cx, |primary_multibuffer, cx| {
210 primary_multibuffer.set_show_deleted_hunks(false, cx);
211 let paths = primary_multibuffer.paths().collect::<Vec<_>>();
212 for path in paths {
213 let Some(excerpt_id) = primary_multibuffer.excerpts_for_path(&path).next()
214 else {
215 continue;
216 };
217 let snapshot = primary_multibuffer.snapshot(cx);
218 let buffer = snapshot.buffer_for_excerpt(excerpt_id).unwrap();
219 let diff = primary_multibuffer.diff_for(buffer.remote_id()).unwrap();
220 secondary.sync_path_excerpts(path, primary_multibuffer, diff, cx);
221 }
222 })
223 });
224 self.secondary = Some(secondary);
225
226 let primary_pane = self.panes.first_pane();
227 self.panes
228 .split(&primary_pane, &secondary_pane, SplitDirection::Left)
229 .unwrap();
230 cx.notify();
231 }
232
233 fn unsplit(&mut self, _: &UnsplitDiff, _: &mut Window, cx: &mut Context<Self>) {
234 let Some(secondary) = self.secondary.take() else {
235 return;
236 };
237 self.panes.remove(&secondary.pane).unwrap();
238 self.primary_editor.update(cx, |primary, cx| {
239 primary.buffer().update(cx, |buffer, cx| {
240 buffer.set_show_deleted_hunks(true, cx);
241 });
242 });
243 cx.notify();
244 }
245
246 pub fn added_to_workspace(
247 &mut self,
248 workspace: &mut Workspace,
249 window: &mut Window,
250 cx: &mut Context<Self>,
251 ) {
252 self.workspace = workspace.weak_handle();
253 self.primary_editor.update(cx, |primary_editor, cx| {
254 primary_editor.added_to_workspace(workspace, window, cx);
255 });
256 if let Some(secondary) = &self.secondary {
257 secondary.editor.update(cx, |secondary_editor, cx| {
258 secondary_editor.added_to_workspace(workspace, window, cx);
259 });
260 }
261 }
262
263 pub fn set_excerpts_for_path(
264 &mut self,
265 path: PathKey,
266 buffer: Entity<Buffer>,
267 ranges: impl IntoIterator<Item = Range<Point>>,
268 context_line_count: u32,
269 diff: Entity<BufferDiff>,
270 cx: &mut Context<Self>,
271 ) -> (Vec<Range<Anchor>>, bool) {
272 self.primary_editor.update(cx, |editor, cx| {
273 editor.buffer().update(cx, |primary_multibuffer, cx| {
274 let (anchors, added_a_new_excerpt) = primary_multibuffer.set_excerpts_for_path(
275 path.clone(),
276 buffer,
277 ranges,
278 context_line_count,
279 cx,
280 );
281 primary_multibuffer.add_diff(diff.clone(), cx);
282 if let Some(secondary) = &mut self.secondary {
283 secondary.sync_path_excerpts(path, primary_multibuffer, diff, cx);
284 }
285 (anchors, added_a_new_excerpt)
286 })
287 })
288 }
289
290 /// Expands excerpts in both sides.
291 ///
292 /// While the left multibuffer does have separate excerpts with separate
293 /// IDs, this is an implementation detail. We do not expose the left excerpt
294 /// IDs in the public API of [`SplittableEditor`].
295 pub fn expand_excerpts(
296 &mut self,
297 excerpt_ids: impl Iterator<Item = ExcerptId> + Clone,
298 lines: u32,
299 direction: ExpandExcerptDirection,
300 cx: &mut Context<Self>,
301 ) {
302 self.primary_multibuffer.update(cx, |multibuffer, cx| {
303 multibuffer.expand_excerpts(excerpt_ids.clone(), lines, direction, cx);
304 });
305 let paths: Vec<(ExcerptId, PathKey)> = excerpt_ids
306 .flat_map(|excerpt_id| {
307 let path = self
308 .primary_multibuffer
309 .read(cx)
310 .path_for_excerpt(excerpt_id)
311 .cloned()?;
312 Some((excerpt_id, path))
313 })
314 .collect();
315
316 if let Some(secondary) = &mut self.secondary {
317 self.primary_editor.update(cx, |editor, cx| {
318 editor.buffer().update(cx, |multibuffer, cx| {
319 let snapshot = multibuffer.snapshot(cx);
320 for (excerpt_id, path) in paths {
321 let buffer = snapshot.buffer_for_excerpt(excerpt_id).unwrap();
322 let diff = multibuffer.diff_for(buffer.remote_id()).unwrap();
323 secondary.sync_path_excerpts(path, multibuffer, diff, cx);
324 }
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, cx: &App) {
345 let Some(secondary) = &self.secondary else {
346 return;
347 };
348
349 let primary_excerpts = self.primary_multibuffer.read(cx).excerpt_ids();
350 let secondary_excerpts = secondary.multibuffer.read(cx).excerpt_ids();
351 assert_eq!(primary_excerpts.len(), secondary_excerpts.len());
352
353 // self.primary_multibuffer.read(cx).check_invariants(cx);
354 // secondary.multibuffer.read(cx).check_invariants(cx);
355 // Assertions:...
356 //
357 // left.display_lines().filter(is_unmodified) == right.display_lines().filter(is_unmodified)
358 //
359 // left excerpts and right excerpts bijectivity
360 //
361 //
362
363 // let primary_buffer_text = self
364 // .primary_multibuffer
365 // .read(cx)
366 // .text_summary_for_range(Anchor::min()..Anchor::max());
367 // let secondary_buffer_text = secondary
368 // .multibuffer
369 // .read(cx)
370 // .text_summary_for_range(Anchor::min()..Anchor::max());
371 // let primary_buffer_base_text = self
372 // .primary_multibuffer
373 // .read(cx)
374 // .base_text_summary_for_range(Anchor::min()..Anchor::max());
375 // let secondary_buffer_base_text = secondary
376 // .multibuffer
377 // .read(cx)
378 // .base_text_summary_for_range(Anchor::min()..Anchor::max());
379 }
380
381 fn randomly_edit_excerpts(
382 &mut self,
383 rng: &mut impl rand::Rng,
384 mutation_count: usize,
385 cx: &mut Context<Self>,
386 ) {
387 use collections::HashSet;
388 use rand::prelude::*;
389 use std::env;
390 use util::RandomCharIter;
391
392 let max_excerpts = env::var("MAX_EXCERPTS")
393 .map(|i| i.parse().expect("invalid `MAX_EXCERPTS` variable"))
394 .unwrap_or(5);
395
396 let paths = self
397 .primary_multibuffer
398 .read(cx)
399 .paths()
400 .collect::<Vec<_>>();
401 let excerpt_ids = self.primary_multibuffer.read(cx).excerpt_ids();
402
403 for _ in 0..mutation_count {
404 if rng.random_bool(0.05) {
405 log::info!("Clearing multi-buffer");
406 self.primary_multibuffer.update(cx, |multibuffer, cx| {
407 multibuffer.clear(cx);
408 });
409 continue;
410 } else if rng.random_bool(0.1) && !excerpt_ids.is_empty() {
411 let mut excerpts = HashSet::default();
412 for _ in 0..rng.random_range(0..excerpt_ids.len()) {
413 excerpts.extend(excerpt_ids.choose(rng).copied());
414 }
415
416 let line_count = rng.random_range(0..5);
417
418 log::info!("Expanding excerpts {excerpts:?} by {line_count} lines");
419
420 self.expand_excerpts(
421 excerpts.iter().cloned(),
422 line_count,
423 ExpandExcerptDirection::UpAndDown,
424 cx,
425 );
426 continue;
427 }
428
429 if excerpt_ids.is_empty() || (rng.random() && excerpt_ids.len() < max_excerpts) {
430 let existing_buffers = self.primary_multibuffer.read(cx).all_buffers();
431 let buffer = if rng.random() || existing_buffers.is_empty() {
432 let len = rng.random_range(0..500);
433 let text = RandomCharIter::new(&mut *rng).take(len).collect::<String>();
434 let buffer = cx.new(|cx| Buffer::local(text, cx));
435 log::info!(
436 "Creating new buffer {} with text: {:?}",
437 buffer.read(cx).remote_id(),
438 buffer.read(cx).text()
439 );
440 buffer
441 } else {
442 existing_buffers.iter().choose(rng).unwrap().clone()
443 };
444
445 let buffer_snapshot = buffer.read(cx).snapshot();
446 let diff = cx.new(|cx| BufferDiff::new_unchanged(&buffer_snapshot, cx));
447 // Create some initial diff hunks.
448 buffer.update(cx, |buffer, cx| {
449 buffer.randomly_edit(rng, 2, cx);
450 });
451 let buffer_snapshot = buffer.read(cx).text_snapshot();
452 let ranges = diff.update(cx, |diff, cx| {
453 diff.recalculate_diff_sync(&buffer_snapshot, cx);
454 diff.snapshot(cx)
455 .hunks(&buffer_snapshot)
456 .map(|hunk| hunk.range.clone())
457 .collect::<Vec<_>>()
458 });
459 let path = PathKey::for_buffer(&buffer, cx);
460 self.set_excerpts_for_path(path, buffer, ranges, 2, diff, cx);
461 } else {
462 let remove_count = rng.random_range(1..=paths.len());
463 let paths_to_remove = paths
464 .choose_multiple(rng, remove_count)
465 .cloned()
466 .collect::<Vec<_>>();
467 for path in paths_to_remove {
468 self.remove_excerpts_for_path(path, cx);
469 }
470 }
471 }
472 }
473
474 fn randomly_mutate(
475 &mut self,
476 rng: &mut impl rand::Rng,
477 mutation_count: usize,
478 cx: &mut Context<Self>,
479 ) {
480 use rand::prelude::*;
481
482 if rng.random_bool(0.7) {
483 let buffers = self.primary_editor.read(cx).buffer().read(cx).all_buffers();
484 let buffer = buffers.iter().choose(rng);
485
486 if let Some(buffer) = buffer {
487 buffer.update(cx, |buffer, cx| {
488 if rng.random() {
489 buffer.randomly_edit(rng, mutation_count, cx);
490 } else {
491 buffer.randomly_undo_redo(rng, cx);
492 }
493 });
494 } else {
495 self.primary_multibuffer.update(cx, |multibuffer, cx| {
496 multibuffer.randomly_edit(rng, mutation_count, cx);
497 });
498 }
499 } else if rng.random() {
500 self.randomly_edit_excerpts(rng, mutation_count, cx);
501 } else {
502 for buffer in self.primary_multibuffer.read(cx).all_buffers() {
503 let diff = self
504 .primary_multibuffer
505 .read(cx)
506 .diff_for(buffer.read(cx).remote_id())
507 .unwrap();
508 let buffer_snapshot = buffer.read(cx).text_snapshot();
509 diff.update(cx, |diff, cx| {
510 diff.recalculate_diff_sync(&buffer_snapshot, cx);
511 });
512 // TODO(split-diff) might be a good idea to try to separate the diff recalculation from the excerpt recalculation
513 let diff_snapshot = diff.read(cx).snapshot(cx);
514 let ranges = diff_snapshot
515 .hunks(&buffer_snapshot)
516 .map(|hunk| hunk.range.clone());
517 let path = PathKey::for_buffer(&buffer, cx);
518 self.set_excerpts_for_path(path, buffer, ranges, 2, diff, cx);
519 }
520 }
521
522 self.check_invariants(cx);
523 }
524}
525
526impl EventEmitter<EditorEvent> for SplittableEditor {}
527impl Focusable for SplittableEditor {
528 fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
529 self.primary_editor.read(cx).focus_handle(cx)
530 }
531}
532
533impl Render for SplittableEditor {
534 fn render(
535 &mut self,
536 window: &mut ui::Window,
537 cx: &mut ui::Context<Self>,
538 ) -> impl ui::IntoElement {
539 let inner = if self.secondary.is_none() {
540 self.primary_editor.clone().into_any_element()
541 } else if let Some(active) = self.panes.panes().into_iter().next() {
542 self.panes
543 .render(
544 None,
545 &ActivePaneDecorator::new(active, &self.workspace),
546 window,
547 cx,
548 )
549 .into_any_element()
550 } else {
551 div().into_any_element()
552 };
553 div()
554 .id("splittable-editor")
555 .on_action(cx.listener(Self::split))
556 .on_action(cx.listener(Self::unsplit))
557 .size_full()
558 .child(inner)
559 }
560}
561
562impl SecondaryEditor {
563 fn sync_path_excerpts(
564 &mut self,
565 path_key: PathKey,
566 primary_multibuffer: &mut MultiBuffer,
567 diff: Entity<BufferDiff>,
568 cx: &mut App,
569 ) {
570 let excerpt_id = primary_multibuffer
571 .excerpts_for_path(&path_key)
572 .next()
573 .unwrap();
574 let primary_multibuffer_snapshot = primary_multibuffer.snapshot(cx);
575 let main_buffer = primary_multibuffer_snapshot
576 .buffer_for_excerpt(excerpt_id)
577 .unwrap();
578 let base_text_buffer = diff.read(cx).base_text_buffer();
579 let diff_snapshot = diff.read(cx).snapshot(cx);
580 let base_text_buffer_snapshot = base_text_buffer.read(cx).snapshot();
581 let new = primary_multibuffer
582 .excerpts_for_buffer(main_buffer.remote_id(), cx)
583 .into_iter()
584 .map(|(_, excerpt_range)| {
585 let point_range_to_base_text_point_range = |range: Range<Point>| {
586 let start_row =
587 diff_snapshot.row_to_base_text_row(range.start.row, main_buffer);
588 let end_row = diff_snapshot.row_to_base_text_row(range.end.row, main_buffer);
589 let end_column = diff_snapshot.base_text().line_len(end_row);
590 Point::new(start_row, 0)..Point::new(end_row, end_column)
591 };
592 let primary = excerpt_range.primary.to_point(main_buffer);
593 let context = excerpt_range.context.to_point(main_buffer);
594 ExcerptRange {
595 primary: point_range_to_base_text_point_range(dbg!(primary)),
596 context: point_range_to_base_text_point_range(dbg!(context)),
597 }
598 })
599 .collect();
600
601 let main_buffer = primary_multibuffer.buffer(main_buffer.remote_id()).unwrap();
602
603 self.editor.update(cx, |editor, cx| {
604 editor.buffer().update(cx, |buffer, cx| {
605 buffer.update_path_excerpts(
606 path_key,
607 base_text_buffer,
608 &base_text_buffer_snapshot,
609 new,
610 cx,
611 );
612 buffer.add_inverted_diff(diff, main_buffer, cx);
613 })
614 });
615 }
616}
617
618#[cfg(test)]
619mod tests {
620 use buffer_diff::BufferDiff;
621 use db::indoc;
622 use fs::FakeFs;
623 use gpui::AppContext as _;
624 use language::{Buffer, Capability};
625 use multi_buffer::MultiBuffer;
626 use project::Project;
627 use rand::{Rng, rngs::StdRng};
628 use settings::SettingsStore;
629 use ui::VisualContext as _;
630 use workspace::Workspace;
631
632 use crate::SplittableEditor;
633
634 fn init_test(cx: &mut gpui::TestAppContext) {
635 cx.update(|cx| {
636 let store = SettingsStore::test(cx);
637 cx.set_global(store);
638 theme::init(theme::LoadThemes::JustBase, cx);
639 crate::init(cx);
640 });
641 }
642
643 #[gpui::test]
644 async fn test_basic_excerpts(mut rng: StdRng, cx: &mut gpui::TestAppContext) {
645 init_test(cx);
646 let base_text = indoc! {"
647 hello
648 "};
649 let buffer_text = indoc! {"
650 HELLO!
651 "};
652 let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
653 let diff = cx.new(|cx| {
654 BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx)
655 });
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 multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
660 let editor = cx.new_window_entity(|window, cx| {
661 SplittableEditor::new_unsplit(multibuffer, project, workspace, window, cx)
662 });
663
664 let mutation_count = rng.random_range(0..100);
665 editor.update(cx, |editor, cx| {
666 editor.randomly_mutate(&mut rng, mutation_count, cx);
667 })
668
669 // for _ in 0..random() {
670 // editor.update(cx, |editor, cx| {
671 // randomly_mutate(primary_multibuffer);
672 // editor.primary_editor().update(cx, |editor, cx| {
673 // editor.edit(vec![(random()..random(), "...")], cx);
674 // })
675 // });
676 // }
677
678 // editor.read(cx).primary_editor().read(cx).display_map.read(cx)
679 }
680}