1use std::{ops::Range, sync::Arc};
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, LanguageRegistry};
9use multi_buffer::{Anchor, ExcerptRange, 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_editor: Entity<Editor>,
43 secondary: Option<SecondaryEditor>,
44 panes: PaneGroup,
45 workspace: WeakEntity<Workspace>,
46 _subscriptions: Vec<Subscription>,
47}
48
49struct SecondaryEditor {
50 editor: Entity<Editor>,
51 pane: Entity<Pane>,
52 has_latest_selection: bool,
53 _subscriptions: Vec<Subscription>,
54}
55
56impl SplittableEditor {
57 pub fn primary_editor(&self) -> &Entity<Editor> {
58 &self.primary_editor
59 }
60
61 pub fn last_selected_editor(&self) -> &Entity<Editor> {
62 if let Some(secondary) = &self.secondary
63 && secondary.has_latest_selection
64 {
65 &secondary.editor
66 } else {
67 &self.primary_editor
68 }
69 }
70
71 pub fn new_unsplit(
72 buffer: Entity<MultiBuffer>,
73 project: Entity<Project>,
74 workspace: Entity<Workspace>,
75 window: &mut Window,
76 cx: &mut Context<Self>,
77 ) -> Self {
78 let primary_editor =
79 cx.new(|cx| Editor::for_multibuffer(buffer, Some(project.clone()), window, cx));
80 let pane = cx.new(|cx| {
81 let mut pane = Pane::new(
82 workspace.downgrade(),
83 project,
84 Default::default(),
85 None,
86 NoAction.boxed_clone(),
87 true,
88 window,
89 cx,
90 );
91 pane.set_should_display_tab_bar(|_, _| false);
92 pane.add_item(primary_editor.boxed_clone(), true, true, None, window, cx);
93 pane
94 });
95 let panes = PaneGroup::new(pane);
96 // TODO(split-diff) we might want to tag editor events with whether they came from primary/secondary
97 let subscriptions =
98 vec![
99 cx.subscribe(&primary_editor, |this, _, event: &EditorEvent, cx| {
100 if let EditorEvent::SelectionsChanged { .. } = event
101 && let Some(secondary) = &mut this.secondary
102 {
103 secondary.has_latest_selection = false;
104 }
105 cx.emit(event.clone())
106 }),
107 ];
108
109 window.defer(cx, {
110 let workspace = workspace.downgrade();
111 let primary_editor = primary_editor.downgrade();
112 move |window, cx| {
113 workspace
114 .update(cx, |workspace, cx| {
115 primary_editor.update(cx, |editor, cx| {
116 editor.added_to_workspace(workspace, window, cx);
117 })
118 })
119 .ok();
120 }
121 });
122 Self {
123 primary_editor,
124 secondary: None,
125 panes,
126 workspace: workspace.downgrade(),
127 _subscriptions: subscriptions,
128 }
129 }
130
131 fn split(&mut self, _: &SplitDiff, window: &mut Window, cx: &mut Context<Self>) {
132 if !cx.has_flag::<SplitDiffFeatureFlag>() {
133 return;
134 }
135 if self.secondary.is_some() {
136 return;
137 }
138 let Some(workspace) = self.workspace.upgrade() else {
139 return;
140 };
141 let project = workspace.read(cx).project().clone();
142
143 let secondary_editor = cx.new(|cx| {
144 let multibuffer = cx.new(|cx| {
145 let mut multibuffer = MultiBuffer::new(Capability::ReadOnly);
146 multibuffer.set_all_diff_hunks_expanded(cx);
147 multibuffer
148 });
149 let mut editor =
150 Editor::for_multibuffer(multibuffer, Some(project.clone()), window, cx);
151 editor.number_deleted_lines = true;
152 editor
153 });
154 let secondary_pane = cx.new(|cx| {
155 let mut pane = Pane::new(
156 workspace.downgrade(),
157 workspace.read(cx).project().clone(),
158 Default::default(),
159 None,
160 NoAction.boxed_clone(),
161 true,
162 window,
163 cx,
164 );
165 pane.set_should_display_tab_bar(|_, _| false);
166 pane.add_item(
167 ItemHandle::boxed_clone(&secondary_editor),
168 false,
169 false,
170 None,
171 window,
172 cx,
173 );
174 pane
175 });
176
177 let subscriptions =
178 vec![
179 cx.subscribe(&secondary_editor, |this, _, event: &EditorEvent, cx| {
180 if let EditorEvent::SelectionsChanged { .. } = event
181 && let Some(secondary) = &mut this.secondary
182 {
183 secondary.has_latest_selection = true;
184 }
185 cx.emit(event.clone())
186 }),
187 ];
188 let mut secondary = SecondaryEditor {
189 editor: secondary_editor,
190 pane: secondary_pane.clone(),
191 has_latest_selection: false,
192 _subscriptions: subscriptions,
193 };
194 self.primary_editor.update(cx, |editor, cx| {
195 editor.buffer().update(cx, |primary_multibuffer, cx| {
196 primary_multibuffer.set_show_deleted_hunks(false, cx);
197 let paths = primary_multibuffer.paths().collect::<Vec<_>>();
198 for path in paths {
199 let Some(excerpt_id) = primary_multibuffer.excerpts_for_path(&path).next()
200 else {
201 continue;
202 };
203 let snapshot = primary_multibuffer.snapshot(cx);
204 let buffer = snapshot.buffer_for_excerpt(excerpt_id).unwrap();
205 let diff = primary_multibuffer.diff_for(buffer.remote_id()).unwrap();
206 secondary.sync_path_excerpts(path, primary_multibuffer, diff, cx);
207 }
208 })
209 });
210 self.secondary = Some(secondary);
211
212 let primary_pane = self.panes.first_pane();
213 self.panes
214 .split(&primary_pane, &secondary_pane, SplitDirection::Left)
215 .unwrap();
216 cx.notify();
217 }
218
219 fn unsplit(&mut self, _: &UnsplitDiff, _: &mut Window, cx: &mut Context<Self>) {
220 let Some(secondary) = self.secondary.take() else {
221 return;
222 };
223 self.panes.remove(&secondary.pane).unwrap();
224 self.primary_editor.update(cx, |primary, cx| {
225 primary.buffer().update(cx, |buffer, cx| {
226 buffer.set_show_deleted_hunks(true, cx);
227 });
228 });
229 cx.notify();
230 }
231
232 pub fn added_to_workspace(
233 &mut self,
234 workspace: &mut Workspace,
235 window: &mut Window,
236 cx: &mut Context<Self>,
237 ) {
238 self.workspace = workspace.weak_handle();
239 self.primary_editor.update(cx, |primary_editor, cx| {
240 primary_editor.added_to_workspace(workspace, window, cx);
241 });
242 if let Some(secondary) = &self.secondary {
243 secondary.editor.update(cx, |secondary_editor, cx| {
244 secondary_editor.added_to_workspace(workspace, window, cx);
245 });
246 }
247 }
248
249 pub fn set_excerpts_for_path(
250 &mut self,
251 path: PathKey,
252 buffer: Entity<Buffer>,
253 ranges: impl IntoIterator<Item = Range<Point>>,
254 context_line_count: u32,
255 diff: Entity<BufferDiff>,
256 cx: &mut Context<Self>,
257 ) -> (Vec<Range<Anchor>>, bool) {
258 self.primary_editor.update(cx, |editor, cx| {
259 editor.buffer().update(cx, |primary_multibuffer, cx| {
260 let (anchors, added_a_new_excerpt) = primary_multibuffer.set_excerpts_for_path(
261 path.clone(),
262 buffer,
263 ranges,
264 context_line_count,
265 cx,
266 );
267 primary_multibuffer.add_diff(diff.clone(), cx);
268 if let Some(secondary) = &mut self.secondary {
269 secondary.sync_path_excerpts(path, primary_multibuffer, diff, cx);
270 }
271 (anchors, added_a_new_excerpt)
272 })
273 })
274 }
275}
276
277impl EventEmitter<EditorEvent> for SplittableEditor {}
278impl Focusable for SplittableEditor {
279 fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
280 self.primary_editor.read(cx).focus_handle(cx)
281 }
282}
283
284impl Render for SplittableEditor {
285 fn render(
286 &mut self,
287 window: &mut ui::Window,
288 cx: &mut ui::Context<Self>,
289 ) -> impl ui::IntoElement {
290 let inner = if self.secondary.is_none() {
291 self.primary_editor.clone().into_any_element()
292 } else if let Some(active) = self.panes.panes().into_iter().next() {
293 self.panes
294 .render(
295 None,
296 &ActivePaneDecorator::new(active, &self.workspace),
297 window,
298 cx,
299 )
300 .into_any_element()
301 } else {
302 div().into_any_element()
303 };
304 div()
305 .id("splittable-editor")
306 .on_action(cx.listener(Self::split))
307 .on_action(cx.listener(Self::unsplit))
308 .size_full()
309 .child(inner)
310 }
311}
312
313impl SecondaryEditor {
314 fn sync_path_excerpts(
315 &mut self,
316 path_key: PathKey,
317 primary_multibuffer: &mut MultiBuffer,
318 diff: Entity<BufferDiff>,
319 cx: &mut App,
320 ) {
321 let excerpt_id = primary_multibuffer
322 .excerpts_for_path(&path_key)
323 .next()
324 .unwrap();
325 let primary_multibuffer_snapshot = primary_multibuffer.snapshot(cx);
326 let main_buffer = primary_multibuffer_snapshot
327 .buffer_for_excerpt(excerpt_id)
328 .unwrap();
329 let base_text_buffer = diff.read(cx).base_text_buffer();
330 let diff_snapshot = diff.read(cx).snapshot(cx);
331 let base_text_buffer_snapshot = base_text_buffer.read(cx).snapshot();
332 let new = primary_multibuffer
333 .excerpts_for_buffer(main_buffer.remote_id(), cx)
334 .into_iter()
335 .map(|(_, excerpt_range)| {
336 let point_range_to_base_text_point_range = |range: Range<Point>| {
337 let start_row =
338 diff_snapshot.row_to_base_text_row(range.start.row, main_buffer);
339 let start_column = 0;
340 let end_row = diff_snapshot.row_to_base_text_row(range.end.row, main_buffer);
341 let end_column = diff_snapshot.base_text().line_len(end_row);
342 Point::new(start_row, start_column)..Point::new(end_row, end_column)
343 };
344 let primary = excerpt_range.primary.to_point(main_buffer);
345 let context = excerpt_range.context.to_point(main_buffer);
346 ExcerptRange {
347 primary: point_range_to_base_text_point_range(primary),
348 context: point_range_to_base_text_point_range(context),
349 }
350 })
351 .collect();
352
353 let main_buffer = primary_multibuffer.buffer(main_buffer.remote_id()).unwrap();
354
355 self.editor.update(cx, |editor, cx| {
356 editor.buffer().update(cx, |buffer, cx| {
357 buffer.update_path_excerpts(
358 path_key,
359 base_text_buffer,
360 &base_text_buffer_snapshot,
361 new,
362 cx,
363 );
364 buffer.add_inverted_diff(diff, main_buffer, cx);
365 })
366 });
367 }
368}
369
370#[cfg(test)]
371mod tests {
372 use buffer_diff::BufferDiff;
373 use db::indoc;
374 use fs::FakeFs;
375 use gpui::AppContext as _;
376 use language::{Buffer, Capability};
377 use multi_buffer::MultiBuffer;
378 use project::Project;
379 use settings::SettingsStore;
380 use ui::VisualContext as _;
381 use workspace::Workspace;
382
383 use crate::SplittableEditor;
384
385 #[gpui::test]
386 async fn test_basic_excerpts(cx: &mut gpui::TestAppContext) {
387 cx.update(|cx| {
388 let store = SettingsStore::test(cx);
389 cx.set_global(store);
390 theme::init(theme::LoadThemes::JustBase, cx);
391 crate::init(cx);
392 });
393 let base_text = indoc! {"
394 hello
395 "};
396 let buffer_text = indoc! {"
397 HELLO!
398 "};
399 let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
400 let diff = cx.new(|cx| {
401 BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx)
402 });
403 let project = Project::test(FakeFs::new(cx.executor()), [], cx).await;
404 let (workspace, cx) =
405 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
406 let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
407 let editor = cx.new_window_entity(|window, cx| {
408 SplittableEditor::new_unsplit(multibuffer, project, workspace, window, cx)
409 });
410
411 // for _ in 0..random() {
412 // editor.update(cx, |editor, cx| {
413 // randomly_mutate(primary_multibuffer);
414 // editor.primary_editor().update(cx, |editor, cx| {
415 // editor.edit(vec![(random()..random(), "...")], cx);
416 // })
417 // });
418 // }
419
420 // editor.read(cx).primary_editor().read(cx).display_map.read(cx)
421 }
422
423 // MultiB
424
425 // FIXME restore these tests in some form
426 // #[gpui::test]
427 // async fn test_filtered_editor_pair(cx: &mut gpui::TestAppContext) {
428 // init_test(cx, |_| {});
429 // let mut leader_cx = EditorTestContext::new(cx).await;
430
431 // let diff_base = indoc!(
432 // r#"
433 // one
434 // two
435 // three
436 // four
437 // five
438 // six
439 // "#
440 // );
441
442 // let initial_state = indoc!(
443 // r#"
444 // ˇone
445 // two
446 // THREE
447 // four
448 // five
449 // six
450 // "#
451 // );
452
453 // leader_cx.set_state(initial_state);
454
455 // leader_cx.set_head_text(&diff_base);
456 // leader_cx.run_until_parked();
457
458 // let follower = leader_cx.update_multibuffer(|leader, cx| {
459 // leader.set_filter_mode(Some(MultiBufferFilterMode::KeepInsertions));
460 // leader.set_all_diff_hunks_expanded(cx);
461 // leader.get_or_create_follower(cx)
462 // });
463 // follower.update(cx, |follower, cx| {
464 // follower.set_filter_mode(Some(MultiBufferFilterMode::KeepDeletions));
465 // follower.set_all_diff_hunks_expanded(cx);
466 // });
467
468 // let follower_editor =
469 // leader_cx.new_window_entity(|window, cx| build_editor(follower, window, cx));
470 // // leader_cx.window.focus(&follower_editor.focus_handle(cx));
471
472 // let mut follower_cx = EditorTestContext::for_editor_in(follower_editor, &mut leader_cx).await;
473 // cx.run_until_parked();
474
475 // leader_cx.assert_editor_state(initial_state);
476 // follower_cx.assert_editor_state(indoc! {
477 // r#"
478 // ˇone
479 // two
480 // three
481 // four
482 // five
483 // six
484 // "#
485 // });
486
487 // follower_cx.editor(|editor, _window, cx| {
488 // assert!(editor.read_only(cx));
489 // });
490
491 // leader_cx.update_editor(|editor, _window, cx| {
492 // editor.edit([(Point::new(4, 0)..Point::new(5, 0), "FIVE\n")], cx);
493 // });
494 // cx.run_until_parked();
495
496 // leader_cx.assert_editor_state(indoc! {
497 // r#"
498 // ˇone
499 // two
500 // THREE
501 // four
502 // FIVE
503 // six
504 // "#
505 // });
506
507 // follower_cx.assert_editor_state(indoc! {
508 // r#"
509 // ˇone
510 // two
511 // three
512 // four
513 // five
514 // six
515 // "#
516 // });
517
518 // leader_cx.update_editor(|editor, _window, cx| {
519 // editor.edit([(Point::new(6, 0)..Point::new(6, 0), "SEVEN")], cx);
520 // });
521 // cx.run_until_parked();
522
523 // leader_cx.assert_editor_state(indoc! {
524 // r#"
525 // ˇone
526 // two
527 // THREE
528 // four
529 // FIVE
530 // six
531 // SEVEN"#
532 // });
533
534 // follower_cx.assert_editor_state(indoc! {
535 // r#"
536 // ˇone
537 // two
538 // three
539 // four
540 // five
541 // six
542 // "#
543 // });
544
545 // leader_cx.update_editor(|editor, window, cx| {
546 // editor.move_down(&MoveDown, window, cx);
547 // editor.refresh_selected_text_highlights(true, window, cx);
548 // });
549 // leader_cx.run_until_parked();
550 // }
551
552 // #[gpui::test]
553 // async fn test_filtered_editor_pair_complex(cx: &mut gpui::TestAppContext) {
554 // init_test(cx, |_| {});
555 // let base_text = "base\n";
556 // let buffer_text = "buffer\n";
557
558 // let buffer1 = cx.new(|cx| Buffer::local(buffer_text, cx));
559 // let diff1 = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer1, cx));
560
561 // let extra_buffer_1 = cx.new(|cx| Buffer::local("dummy text 1\n", cx));
562 // let extra_diff_1 = cx.new(|cx| BufferDiff::new_with_base_text("", &extra_buffer_1, cx));
563 // let extra_buffer_2 = cx.new(|cx| Buffer::local("dummy text 2\n", cx));
564 // let extra_diff_2 = cx.new(|cx| BufferDiff::new_with_base_text("", &extra_buffer_2, cx));
565
566 // let leader = cx.new(|cx| {
567 // let mut leader = MultiBuffer::new(Capability::ReadWrite);
568 // leader.set_all_diff_hunks_expanded(cx);
569 // leader.set_filter_mode(Some(MultiBufferFilterMode::KeepInsertions));
570 // leader
571 // });
572 // let follower = leader.update(cx, |leader, cx| leader.get_or_create_follower(cx));
573 // follower.update(cx, |follower, _| {
574 // follower.set_filter_mode(Some(MultiBufferFilterMode::KeepDeletions));
575 // });
576
577 // leader.update(cx, |leader, cx| {
578 // leader.insert_excerpts_after(
579 // ExcerptId::min(),
580 // extra_buffer_2.clone(),
581 // vec![ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
582 // cx,
583 // );
584 // leader.add_diff(extra_diff_2.clone(), cx);
585
586 // leader.insert_excerpts_after(
587 // ExcerptId::min(),
588 // extra_buffer_1.clone(),
589 // vec![ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
590 // cx,
591 // );
592 // leader.add_diff(extra_diff_1.clone(), cx);
593
594 // leader.insert_excerpts_after(
595 // ExcerptId::min(),
596 // buffer1.clone(),
597 // vec![ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
598 // cx,
599 // );
600 // leader.add_diff(diff1.clone(), cx);
601 // });
602
603 // cx.run_until_parked();
604 // let mut cx = cx.add_empty_window();
605
606 // let leader_editor = cx
607 // .new_window_entity(|window, cx| Editor::for_multibuffer(leader.clone(), None, window, cx));
608 // let follower_editor = cx.new_window_entity(|window, cx| {
609 // Editor::for_multibuffer(follower.clone(), None, window, cx)
610 // });
611
612 // let mut leader_cx = EditorTestContext::for_editor_in(leader_editor.clone(), &mut cx).await;
613 // leader_cx.assert_editor_state(indoc! {"
614 // ˇbuffer
615
616 // dummy text 1
617
618 // dummy text 2
619 // "});
620 // let mut follower_cx = EditorTestContext::for_editor_in(follower_editor.clone(), &mut cx).await;
621 // follower_cx.assert_editor_state(indoc! {"
622 // ˇbase
623
624 // "});
625 // }
626}