split.rs

  1use std::{ops::Range, sync::Arc};
  2
  3use feature_flags::{FeatureFlag, FeatureFlagAppExt as _};
  4use gpui::{
  5    Action, AppContext as _, Entity, EventEmitter, Focusable, NoAction, Subscription, WeakEntity,
  6};
  7use language::{Buffer, Capability, LanguageRegistry};
  8use multi_buffer::{Anchor, ExcerptRange, MultiBuffer, PathKey};
  9use project::Project;
 10use rope::Point;
 11use text::OffsetRangeExt as _;
 12use ui::{
 13    App, Context, InteractiveElement as _, IntoElement as _, ParentElement as _, Render,
 14    Styled as _, Window, div,
 15};
 16use workspace::{
 17    ActivePaneDecorator, Item, ItemHandle, Pane, PaneGroup, SplitDirection, Workspace,
 18};
 19
 20use crate::{Editor, EditorEvent};
 21
 22struct SplitDiffFeatureFlag;
 23
 24impl FeatureFlag for SplitDiffFeatureFlag {
 25    const NAME: &'static str = "split-diff";
 26
 27    fn enabled_for_staff() -> bool {
 28        true
 29    }
 30}
 31
 32#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
 33#[action(namespace = editor)]
 34struct SplitDiff;
 35
 36#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
 37#[action(namespace = editor)]
 38struct UnsplitDiff;
 39
 40pub struct SplittableEditor {
 41    primary_editor: Entity<Editor>,
 42    secondary: Option<SecondaryEditor>,
 43    panes: PaneGroup,
 44    workspace: WeakEntity<Workspace>,
 45    _subscriptions: Vec<Subscription>,
 46}
 47
 48struct SecondaryEditor {
 49    editor: Entity<Editor>,
 50    pane: Entity<Pane>,
 51    has_latest_selection: bool,
 52    _subscriptions: Vec<Subscription>,
 53}
 54
 55impl SplittableEditor {
 56    pub fn primary_editor(&self) -> &Entity<Editor> {
 57        &self.primary_editor
 58    }
 59
 60    pub fn last_selected_editor(&self) -> &Entity<Editor> {
 61        if let Some(secondary) = &self.secondary
 62            && secondary.has_latest_selection
 63        {
 64            &secondary.editor
 65        } else {
 66            &self.primary_editor
 67        }
 68    }
 69
 70    pub fn new_unsplit(
 71        buffer: Entity<MultiBuffer>,
 72        project: Entity<Project>,
 73        workspace: Entity<Workspace>,
 74        window: &mut Window,
 75        cx: &mut Context<Self>,
 76    ) -> Self {
 77        let primary_editor =
 78            cx.new(|cx| Editor::for_multibuffer(buffer, Some(project.clone()), window, cx));
 79        let pane = cx.new(|cx| {
 80            let mut pane = Pane::new(
 81                workspace.downgrade(),
 82                project,
 83                Default::default(),
 84                None,
 85                NoAction.boxed_clone(),
 86                true,
 87                window,
 88                cx,
 89            );
 90            pane.set_should_display_tab_bar(|_, _| false);
 91            pane.add_item(primary_editor.boxed_clone(), true, true, None, window, cx);
 92            pane
 93        });
 94        let panes = PaneGroup::new(pane);
 95        // TODO(split-diff) we might want to tag editor events with whether they came from primary/secondary
 96        let subscriptions =
 97            vec![
 98                cx.subscribe(&primary_editor, |this, _, event: &EditorEvent, cx| {
 99                    if let EditorEvent::SelectionsChanged { .. } = event
100                        && let Some(secondary) = &mut this.secondary
101                    {
102                        secondary.has_latest_selection = false;
103                    }
104                    cx.emit(event.clone())
105                }),
106            ];
107
108        window.defer(cx, {
109            let workspace = workspace.downgrade();
110            let primary_editor = primary_editor.downgrade();
111            move |window, cx| {
112                workspace
113                    .update(cx, |workspace, cx| {
114                        primary_editor.update(cx, |editor, cx| {
115                            editor.added_to_workspace(workspace, window, cx);
116                        })
117                    })
118                    .ok();
119            }
120        });
121        Self {
122            primary_editor,
123            secondary: None,
124            panes,
125            workspace: workspace.downgrade(),
126            _subscriptions: subscriptions,
127        }
128    }
129
130    fn split(&mut self, _: &SplitDiff, window: &mut Window, cx: &mut Context<Self>) {
131        if !cx.has_flag::<SplitDiffFeatureFlag>() {
132            return;
133        }
134        if self.secondary.is_some() {
135            return;
136        }
137        let Some(workspace) = self.workspace.upgrade() else {
138            return;
139        };
140        let project = workspace.read(cx).project().clone();
141
142        // FIXME
143        // - have to subscribe to the diffs to update the base text buffers (and handle language changed I think?)
144
145        let secondary_editor = cx.new(|cx| {
146            let multibuffer = cx.new(|cx| {
147                let mut multibuffer = MultiBuffer::new(Capability::ReadOnly);
148                multibuffer.set_all_diff_hunks_expanded(cx);
149                multibuffer
150            });
151            Editor::for_multibuffer(multibuffer, Some(project.clone()), window, cx)
152        });
153        let secondary_pane = cx.new(|cx| {
154            let mut pane = Pane::new(
155                workspace.downgrade(),
156                workspace.read(cx).project().clone(),
157                Default::default(),
158                None,
159                NoAction.boxed_clone(),
160                true,
161                window,
162                cx,
163            );
164            pane.set_should_display_tab_bar(|_, _| false);
165            pane.add_item(
166                ItemHandle::boxed_clone(&secondary_editor),
167                false,
168                false,
169                None,
170                window,
171                cx,
172            );
173            pane
174        });
175
176        let subscriptions =
177            vec![
178                cx.subscribe(&secondary_editor, |this, _, event: &EditorEvent, cx| {
179                    if let EditorEvent::SelectionsChanged { .. } = event
180                        && let Some(secondary) = &mut this.secondary
181                    {
182                        secondary.has_latest_selection = true;
183                    }
184                    cx.emit(event.clone())
185                }),
186            ];
187        let mut secondary = SecondaryEditor {
188            editor: secondary_editor,
189            pane: secondary_pane.clone(),
190            has_latest_selection: false,
191            _subscriptions: subscriptions,
192        };
193        self.primary_editor.update(cx, |editor, cx| {
194            editor.buffer().update(cx, |primary_multibuffer, cx| {
195                primary_multibuffer.set_show_deleted_hunks(false, cx);
196                let paths = primary_multibuffer.paths().collect::<Vec<_>>();
197                for path in paths {
198                    secondary.sync_path_excerpts(
199                        path,
200                        primary_multibuffer,
201                        project.read(cx).languages().clone(),
202                        cx,
203                    );
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    // FIXME need add_diff management in here too
247
248    pub fn set_excerpts_for_path(
249        &mut self,
250        path: PathKey,
251        buffer: Entity<Buffer>,
252        ranges: impl IntoIterator<Item = Range<Point>>,
253        context_line_count: u32,
254        cx: &mut Context<Self>,
255    ) -> (Vec<Range<Anchor>>, bool) {
256        self.primary_editor.update(cx, |editor, cx| {
257            editor.buffer().update(cx, |primary_multibuffer, cx| {
258                let (anchors, added_a_new_excerpt) = primary_multibuffer.set_excerpts_for_path(
259                    path.clone(),
260                    buffer,
261                    ranges,
262                    context_line_count,
263                    cx,
264                );
265                // - we just added some excerpts for a specific buffer to the primary (RHS)
266                // - but the diff for that buffer doesn't get attached to the primary multibuffer until slightly later
267                // - however, for sync_path_excerpts we require that we have a diff for the buffer
268                if let Some(secondary) = &mut self.secondary
269                    && let Some(languages) = self
270                        .workspace
271                        .update(cx, |workspace, cx| {
272                            workspace.project().read(cx).languages().clone()
273                        })
274                        .ok()
275                {
276                    secondary.sync_path_excerpts(path, primary_multibuffer, languages, cx);
277                }
278                (anchors, added_a_new_excerpt)
279            })
280        })
281    }
282}
283
284impl EventEmitter<EditorEvent> for SplittableEditor {}
285impl Focusable for SplittableEditor {
286    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
287        self.primary_editor.read(cx).focus_handle(cx)
288    }
289}
290
291impl Render for SplittableEditor {
292    fn render(
293        &mut self,
294        window: &mut ui::Window,
295        cx: &mut ui::Context<Self>,
296    ) -> impl ui::IntoElement {
297        let inner = if self.secondary.is_none() {
298            self.primary_editor.clone().into_any_element()
299        } else if let Some(active) = self.panes.panes().into_iter().next() {
300            self.panes
301                .render(
302                    None,
303                    &ActivePaneDecorator::new(active, &self.workspace),
304                    window,
305                    cx,
306                )
307                .into_any_element()
308        } else {
309            div().into_any_element()
310        };
311        div()
312            .id("splittable-editor")
313            .on_action(cx.listener(Self::split))
314            .on_action(cx.listener(Self::unsplit))
315            .size_full()
316            .child(inner)
317    }
318}
319
320impl SecondaryEditor {
321    fn sync_path_excerpts(
322        &mut self,
323        path_key: PathKey,
324        primary_multibuffer: &mut MultiBuffer,
325        languages: Arc<LanguageRegistry>,
326        cx: &mut App,
327    ) {
328        let excerpt_id = primary_multibuffer
329            .excerpts_for_path(&path_key)
330            .next()
331            .unwrap();
332        let primary_multibuffer_snapshot = primary_multibuffer.snapshot(cx);
333        let main_buffer = primary_multibuffer_snapshot
334            .buffer_for_excerpt(excerpt_id)
335            .unwrap();
336        let diff = primary_multibuffer
337            .diff_for(main_buffer.remote_id())
338            .unwrap();
339        let diff = diff.read(cx).snapshot(cx);
340        let base_text_buffer = self
341            .editor
342            .update(cx, |editor, cx| {
343                editor.buffer().update(cx, |secondary_multibuffer, cx| {
344                    let excerpt_id = secondary_multibuffer.excerpts_for_path(&path_key).next()?;
345                    let secondary_buffer_snapshot = secondary_multibuffer.snapshot(cx);
346                    let buffer = secondary_buffer_snapshot
347                        .buffer_for_excerpt(excerpt_id)
348                        .unwrap();
349                    Some(secondary_multibuffer.buffer(buffer.remote_id()).unwrap())
350                })
351            })
352            .unwrap_or_else(|| {
353                cx.new(|cx| {
354                    // FIXME we might not have a language at this point for the base text;
355                    // need to handle the case where the language comes in afterward
356                    let base_text = diff.base_text();
357                    let mut buffer = Buffer::local_normalized(
358                        base_text.as_rope().clone(),
359                        base_text.line_ending(),
360                        cx,
361                    );
362                    buffer.set_language(base_text.language().cloned(), cx);
363                    buffer.set_language_registry(languages);
364                    buffer
365                })
366            });
367        let base_text_buffer_snapshot = base_text_buffer.read(cx).snapshot();
368        let new = primary_multibuffer
369            .excerpts_for_buffer(main_buffer.remote_id(), cx)
370            .into_iter()
371            .map(|(_, excerpt_range)| {
372                let point_range_to_base_text_point_range = |range: Range<Point>| {
373                    let start_row = diff.row_to_base_text_row(range.start.row, main_buffer);
374                    let start_column = 0;
375                    let end_row = diff.row_to_base_text_row(range.end.row, main_buffer);
376                    let end_column = diff.base_text().line_len(end_row);
377                    Point::new(start_row, start_column)..Point::new(end_row, end_column)
378                };
379                let primary = excerpt_range.primary.to_point(main_buffer);
380                let context = excerpt_range.context.to_point(main_buffer);
381                ExcerptRange {
382                    primary: point_range_to_base_text_point_range(primary),
383                    context: point_range_to_base_text_point_range(context),
384                }
385            })
386            .collect();
387
388        let diff = primary_multibuffer
389            .diff_for(main_buffer.remote_id())
390            .unwrap();
391        let main_buffer = primary_multibuffer.buffer(main_buffer.remote_id()).unwrap();
392
393        self.editor.update(cx, |editor, cx| {
394            editor.buffer().update(cx, |buffer, cx| {
395                buffer.update_path_excerpts(
396                    path_key,
397                    base_text_buffer,
398                    &base_text_buffer_snapshot,
399                    new,
400                    cx,
401                );
402                buffer.add_inverted_diff(
403                    base_text_buffer_snapshot.remote_id(),
404                    diff,
405                    main_buffer,
406                    cx,
407                );
408            })
409        });
410    }
411}
412
413#[cfg(test)]
414mod tests {
415    use buffer_diff::BufferDiff;
416    use db::indoc;
417    use fs::FakeFs;
418    use gpui::AppContext as _;
419    use language::{Buffer, Capability};
420    use multi_buffer::MultiBuffer;
421    use project::Project;
422    use settings::SettingsStore;
423    use ui::VisualContext as _;
424    use workspace::Workspace;
425
426    use crate::SplittableEditor;
427
428    #[gpui::test]
429    async fn test_basic_excerpts(cx: &mut gpui::TestAppContext) {
430        cx.update(|cx| {
431            let store = SettingsStore::test(cx);
432            cx.set_global(store);
433            theme::init(theme::LoadThemes::JustBase, cx);
434            crate::init(cx);
435        });
436        let base_text = indoc! {"
437            hello
438        "};
439        let buffer_text = indoc! {"
440            HELLO!
441        "};
442        let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
443        let diff = cx.new(|cx| {
444            BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx)
445        });
446        let project = Project::test(FakeFs::new(cx.executor()), [], cx).await;
447        let (workspace, cx) =
448            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
449        let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
450        let editor = cx.new_window_entity(|window, cx| {
451            SplittableEditor::new_unsplit(multibuffer, project, workspace, window, cx)
452        });
453    }
454}