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            Editor::for_multibuffer(multibuffer, Some(project.clone()), window, cx)
150        });
151        let secondary_pane = cx.new(|cx| {
152            let mut pane = Pane::new(
153                workspace.downgrade(),
154                workspace.read(cx).project().clone(),
155                Default::default(),
156                None,
157                NoAction.boxed_clone(),
158                true,
159                window,
160                cx,
161            );
162            pane.set_should_display_tab_bar(|_, _| false);
163            pane.add_item(
164                ItemHandle::boxed_clone(&secondary_editor),
165                false,
166                false,
167                None,
168                window,
169                cx,
170            );
171            pane
172        });
173
174        let subscriptions =
175            vec![
176                cx.subscribe(&secondary_editor, |this, _, event: &EditorEvent, cx| {
177                    if let EditorEvent::SelectionsChanged { .. } = event
178                        && let Some(secondary) = &mut this.secondary
179                    {
180                        secondary.has_latest_selection = true;
181                    }
182                    cx.emit(event.clone())
183                }),
184            ];
185        let mut secondary = SecondaryEditor {
186            editor: secondary_editor,
187            pane: secondary_pane.clone(),
188            has_latest_selection: false,
189            _subscriptions: subscriptions,
190        };
191        self.primary_editor.update(cx, |editor, cx| {
192            editor.buffer().update(cx, |primary_multibuffer, cx| {
193                primary_multibuffer.set_show_deleted_hunks(false, cx);
194                let paths = primary_multibuffer.paths().collect::<Vec<_>>();
195                for path in paths {
196                    let Some(excerpt_id) = primary_multibuffer.excerpts_for_path(&path).next()
197                    else {
198                        continue;
199                    };
200                    let snapshot = primary_multibuffer.snapshot(cx);
201                    let buffer = snapshot.buffer_for_excerpt(excerpt_id).unwrap();
202                    let diff = primary_multibuffer.diff_for(buffer.remote_id()).unwrap();
203                    secondary.sync_path_excerpts(path, primary_multibuffer, diff, cx);
204                }
205            })
206        });
207        self.secondary = Some(secondary);
208
209        let primary_pane = self.panes.first_pane();
210        self.panes
211            .split(&primary_pane, &secondary_pane, SplitDirection::Left)
212            .unwrap();
213        cx.notify();
214    }
215
216    fn unsplit(&mut self, _: &UnsplitDiff, _: &mut Window, cx: &mut Context<Self>) {
217        let Some(secondary) = self.secondary.take() else {
218            return;
219        };
220        self.panes.remove(&secondary.pane).unwrap();
221        self.primary_editor.update(cx, |primary, cx| {
222            primary.buffer().update(cx, |buffer, cx| {
223                buffer.set_show_deleted_hunks(true, cx);
224            });
225        });
226        cx.notify();
227    }
228
229    pub fn added_to_workspace(
230        &mut self,
231        workspace: &mut Workspace,
232        window: &mut Window,
233        cx: &mut Context<Self>,
234    ) {
235        self.workspace = workspace.weak_handle();
236        self.primary_editor.update(cx, |primary_editor, cx| {
237            primary_editor.added_to_workspace(workspace, window, cx);
238        });
239        if let Some(secondary) = &self.secondary {
240            secondary.editor.update(cx, |secondary_editor, cx| {
241                secondary_editor.added_to_workspace(workspace, window, cx);
242            });
243        }
244    }
245
246    pub fn set_excerpts_for_path(
247        &mut self,
248        path: PathKey,
249        buffer: Entity<Buffer>,
250        ranges: impl IntoIterator<Item = Range<Point>>,
251        context_line_count: u32,
252        diff: Entity<BufferDiff>,
253        cx: &mut Context<Self>,
254    ) -> (Vec<Range<Anchor>>, bool) {
255        self.primary_editor.update(cx, |editor, cx| {
256            editor.buffer().update(cx, |primary_multibuffer, cx| {
257                let (anchors, added_a_new_excerpt) = primary_multibuffer.set_excerpts_for_path(
258                    path.clone(),
259                    buffer,
260                    ranges,
261                    context_line_count,
262                    cx,
263                );
264                primary_multibuffer.add_diff(diff.clone(), cx);
265                if let Some(secondary) = &mut self.secondary {
266                    secondary.sync_path_excerpts(path, primary_multibuffer, diff, cx);
267                }
268                (anchors, added_a_new_excerpt)
269            })
270        })
271    }
272}
273
274impl EventEmitter<EditorEvent> for SplittableEditor {}
275impl Focusable for SplittableEditor {
276    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
277        self.primary_editor.read(cx).focus_handle(cx)
278    }
279}
280
281impl Render for SplittableEditor {
282    fn render(
283        &mut self,
284        window: &mut ui::Window,
285        cx: &mut ui::Context<Self>,
286    ) -> impl ui::IntoElement {
287        let inner = if self.secondary.is_none() {
288            self.primary_editor.clone().into_any_element()
289        } else if let Some(active) = self.panes.panes().into_iter().next() {
290            self.panes
291                .render(
292                    None,
293                    &ActivePaneDecorator::new(active, &self.workspace),
294                    window,
295                    cx,
296                )
297                .into_any_element()
298        } else {
299            div().into_any_element()
300        };
301        div()
302            .id("splittable-editor")
303            .on_action(cx.listener(Self::split))
304            .on_action(cx.listener(Self::unsplit))
305            .size_full()
306            .child(inner)
307    }
308}
309
310impl SecondaryEditor {
311    fn sync_path_excerpts(
312        &mut self,
313        path_key: PathKey,
314        primary_multibuffer: &mut MultiBuffer,
315        diff: Entity<BufferDiff>,
316        cx: &mut App,
317    ) {
318        let excerpt_id = primary_multibuffer
319            .excerpts_for_path(&path_key)
320            .next()
321            .unwrap();
322        let primary_multibuffer_snapshot = primary_multibuffer.snapshot(cx);
323        let main_buffer = primary_multibuffer_snapshot
324            .buffer_for_excerpt(excerpt_id)
325            .unwrap();
326        let base_text_buffer = diff.read(cx).base_text_buffer();
327        let diff_snapshot = diff.read(cx).snapshot(cx);
328        let base_text_buffer_snapshot = base_text_buffer.read(cx).snapshot();
329        let new = primary_multibuffer
330            .excerpts_for_buffer(main_buffer.remote_id(), cx)
331            .into_iter()
332            .map(|(_, excerpt_range)| {
333                let point_range_to_base_text_point_range = |range: Range<Point>| {
334                    let start_row =
335                        diff_snapshot.row_to_base_text_row(range.start.row, main_buffer);
336                    let start_column = 0;
337                    let end_row = diff_snapshot.row_to_base_text_row(range.end.row, main_buffer);
338                    let end_column = diff_snapshot.base_text().line_len(end_row);
339                    Point::new(start_row, start_column)..Point::new(end_row, end_column)
340                };
341                let primary = excerpt_range.primary.to_point(main_buffer);
342                let context = excerpt_range.context.to_point(main_buffer);
343                ExcerptRange {
344                    primary: point_range_to_base_text_point_range(primary),
345                    context: point_range_to_base_text_point_range(context),
346                }
347            })
348            .collect();
349
350        let main_buffer = primary_multibuffer.buffer(main_buffer.remote_id()).unwrap();
351
352        self.editor.update(cx, |editor, cx| {
353            editor.buffer().update(cx, |buffer, cx| {
354                buffer.update_path_excerpts(
355                    path_key,
356                    base_text_buffer,
357                    &base_text_buffer_snapshot,
358                    new,
359                    cx,
360                );
361                buffer.add_inverted_diff(
362                    base_text_buffer_snapshot.remote_id(),
363                    diff,
364                    main_buffer,
365                    cx,
366                );
367            })
368        });
369    }
370}
371
372#[cfg(test)]
373mod tests {
374    use buffer_diff::BufferDiff;
375    use db::indoc;
376    use fs::FakeFs;
377    use gpui::AppContext as _;
378    use language::{Buffer, Capability};
379    use multi_buffer::MultiBuffer;
380    use project::Project;
381    use settings::SettingsStore;
382    use ui::VisualContext as _;
383    use workspace::Workspace;
384
385    use crate::SplittableEditor;
386
387    #[gpui::test]
388    async fn test_basic_excerpts(cx: &mut gpui::TestAppContext) {
389        cx.update(|cx| {
390            let store = SettingsStore::test(cx);
391            cx.set_global(store);
392            theme::init(theme::LoadThemes::JustBase, cx);
393            crate::init(cx);
394        });
395        let base_text = indoc! {"
396            hello
397        "};
398        let buffer_text = indoc! {"
399            HELLO!
400        "};
401        let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
402        let diff = cx.new(|cx| {
403            BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx)
404        });
405        let project = Project::test(FakeFs::new(cx.executor()), [], cx).await;
406        let (workspace, cx) =
407            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
408        let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
409        let editor = cx.new_window_entity(|window, cx| {
410            SplittableEditor::new_unsplit(multibuffer, project, workspace, window, cx)
411        });
412    }
413}