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                if let Some(secondary) = &mut self.secondary
266                    && let Some(languages) = self
267                        .workspace
268                        .update(cx, |workspace, cx| {
269                            workspace.project().read(cx).languages().clone()
270                        })
271                        .ok()
272                {
273                    secondary.sync_path_excerpts(path, primary_multibuffer, languages, cx);
274                }
275                (anchors, added_a_new_excerpt)
276            })
277        })
278    }
279}
280
281impl EventEmitter<EditorEvent> for SplittableEditor {}
282impl Focusable for SplittableEditor {
283    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
284        self.primary_editor.read(cx).focus_handle(cx)
285    }
286}
287
288impl Render for SplittableEditor {
289    fn render(
290        &mut self,
291        window: &mut ui::Window,
292        cx: &mut ui::Context<Self>,
293    ) -> impl ui::IntoElement {
294        let inner = if self.secondary.is_none() {
295            self.primary_editor.clone().into_any_element()
296        } else if let Some(active) = self.panes.panes().into_iter().next() {
297            self.panes
298                .render(
299                    None,
300                    &ActivePaneDecorator::new(active, &self.workspace),
301                    window,
302                    cx,
303                )
304                .into_any_element()
305        } else {
306            div().into_any_element()
307        };
308        div()
309            .id("splittable-editor")
310            .on_action(cx.listener(Self::split))
311            .on_action(cx.listener(Self::unsplit))
312            .size_full()
313            .child(inner)
314    }
315}
316
317impl SecondaryEditor {
318    fn sync_path_excerpts(
319        &mut self,
320        path_key: PathKey,
321        primary_multibuffer: &mut MultiBuffer,
322        languages: Arc<LanguageRegistry>,
323        cx: &mut App,
324    ) {
325        let excerpt_id = primary_multibuffer
326            .excerpts_for_path(&path_key)
327            .next()
328            .unwrap();
329        let primary_multibuffer_snapshot = primary_multibuffer.snapshot(cx);
330        let main_buffer = primary_multibuffer_snapshot
331            .buffer_for_excerpt(excerpt_id)
332            .unwrap();
333        let diff = primary_multibuffer
334            .diff_for(main_buffer.remote_id())
335            .unwrap();
336        let diff = diff.read(cx).snapshot(cx);
337        let base_text_buffer = self
338            .editor
339            .update(cx, |editor, cx| {
340                editor.buffer().update(cx, |secondary_multibuffer, cx| {
341                    let excerpt_id = secondary_multibuffer.excerpts_for_path(&path_key).next()?;
342                    let secondary_buffer_snapshot = secondary_multibuffer.snapshot(cx);
343                    let buffer = secondary_buffer_snapshot
344                        .buffer_for_excerpt(excerpt_id)
345                        .unwrap();
346                    Some(secondary_multibuffer.buffer(buffer.remote_id()).unwrap())
347                })
348            })
349            .unwrap_or_else(|| {
350                cx.new(|cx| {
351                    let base_text = diff.base_text();
352                    let mut buffer = Buffer::local_normalized(
353                        base_text.as_rope().clone(),
354                        base_text.line_ending(),
355                        cx,
356                    );
357                    buffer.set_language(base_text.language().cloned(), cx);
358                    buffer.set_language_registry(languages);
359                    buffer
360                })
361            });
362        let base_text_buffer_snapshot = base_text_buffer.read(cx).snapshot();
363        let new = primary_multibuffer
364            .excerpts_for_buffer(main_buffer.remote_id(), cx)
365            .into_iter()
366            .map(|(_, excerpt_range)| {
367                let point_to_base_text_point = |point: Point| {
368                    let row = diff.row_to_base_text_row(point.row, main_buffer);
369                    let column = diff.base_text().line_len(row);
370                    Point::new(row, column)
371                };
372                let primary = excerpt_range.primary.to_point(main_buffer);
373                let context = excerpt_range.context.to_point(main_buffer);
374                ExcerptRange {
375                    primary: point_to_base_text_point(primary.start)
376                        ..point_to_base_text_point(primary.end),
377                    context: point_to_base_text_point(context.start)
378                        ..point_to_base_text_point(context.end),
379                }
380            })
381            .collect();
382
383        self.editor.update(cx, |editor, cx| {
384            editor.buffer().update(cx, |buffer, cx| {
385                buffer.update_path_excerpts(
386                    path_key,
387                    base_text_buffer,
388                    &base_text_buffer_snapshot,
389                    new,
390                    cx,
391                )
392            })
393        });
394    }
395}
396
397#[cfg(test)]
398mod tests {
399    use buffer_diff::BufferDiff;
400    use db::indoc;
401    use fs::FakeFs;
402    use gpui::AppContext as _;
403    use language::{Buffer, Capability};
404    use multi_buffer::MultiBuffer;
405    use project::Project;
406    use ui::VisualContext as _;
407    use workspace::Workspace;
408
409    use crate::SplittableEditor;
410
411    #[gpui::test]
412    async fn test_basic_excerpts(cx: &mut gpui::TestAppContext) {
413        let base_text = indoc! {"
414            hello
415        "};
416        let buffer_text = indoc! {"
417            HELLO!
418        "};
419        let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
420        let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
421        let project = Project::test(FakeFs::new(cx.executor()), [], cx).await;
422        let (workspace, cx) =
423            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
424        let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
425        let editor = cx.new_window_entity(|window, cx| {
426            SplittableEditor::new_unsplit(multibuffer, project, workspace, window, cx)
427        });
428    }
429}