split.rs

  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}