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 ui::{
 12    App, Context, InteractiveElement as _, IntoElement as _, ParentElement as _, Render,
 13    Styled as _, Window, div,
 14};
 15use workspace::{
 16    ActivePaneDecorator, Item, ItemHandle, Pane, PaneGroup, SplitDirection, Workspace,
 17};
 18
 19use crate::{Editor, EditorEvent};
 20
 21struct SplitDiffFeatureFlag;
 22
 23impl FeatureFlag for SplitDiffFeatureFlag {
 24    const NAME: &'static str = "split-diff";
 25
 26    fn enabled_for_staff() -> bool {
 27        true
 28    }
 29}
 30
 31#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
 32#[action(namespace = editor)]
 33struct SplitDiff;
 34
 35#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
 36#[action(namespace = editor)]
 37struct UnsplitDiff;
 38
 39pub struct SplittableEditor {
 40    primary_editor: Entity<Editor>,
 41    secondary: Option<SecondaryEditor>,
 42    panes: PaneGroup,
 43    workspace: WeakEntity<Workspace>,
 44    _subscriptions: Vec<Subscription>,
 45}
 46
 47struct SecondaryEditor {
 48    editor: Entity<Editor>,
 49    pane: Entity<Pane>,
 50    has_latest_selection: bool,
 51    _subscriptions: Vec<Subscription>,
 52}
 53
 54impl SplittableEditor {
 55    pub fn primary_editor(&self) -> &Entity<Editor> {
 56        &self.primary_editor
 57    }
 58
 59    pub fn last_selected_editor(&self) -> &Entity<Editor> {
 60        if let Some(secondary) = &self.secondary
 61            && secondary.has_latest_selection
 62        {
 63            &secondary.editor
 64        } else {
 65            &self.primary_editor
 66        }
 67    }
 68
 69    pub fn new_unsplit(
 70        buffer: Entity<MultiBuffer>,
 71        project: Entity<Project>,
 72        workspace: Entity<Workspace>,
 73        window: &mut Window,
 74        cx: &mut Context<Self>,
 75    ) -> Self {
 76        let primary_editor =
 77            cx.new(|cx| Editor::for_multibuffer(buffer, Some(project.clone()), window, cx));
 78        let pane = cx.new(|cx| {
 79            let mut pane = Pane::new(
 80                workspace.downgrade(),
 81                project,
 82                Default::default(),
 83                None,
 84                NoAction.boxed_clone(),
 85                true,
 86                window,
 87                cx,
 88            );
 89            pane.set_should_display_tab_bar(|_, _| false);
 90            pane.add_item(primary_editor.boxed_clone(), true, true, None, window, cx);
 91            pane
 92        });
 93        let panes = PaneGroup::new(pane);
 94        // TODO(split-diff) we might want to tag editor events with whether they came from primary/secondary
 95        let subscriptions =
 96            vec![
 97                cx.subscribe(&primary_editor, |this, _, event: &EditorEvent, cx| {
 98                    if let EditorEvent::SelectionsChanged { .. } = event
 99                        && let Some(secondary) = &mut this.secondary
100                    {
101                        secondary.has_latest_selection = false;
102                    }
103                    cx.emit(event.clone())
104                }),
105            ];
106
107        window.defer(cx, {
108            let workspace = workspace.downgrade();
109            let primary_editor = primary_editor.downgrade();
110            move |window, cx| {
111                workspace
112                    .update(cx, |workspace, cx| {
113                        primary_editor.update(cx, |editor, cx| {
114                            editor.added_to_workspace(workspace, window, cx);
115                        })
116                    })
117                    .ok();
118            }
119        });
120        Self {
121            primary_editor,
122            secondary: None,
123            panes,
124            workspace: workspace.downgrade(),
125            _subscriptions: subscriptions,
126        }
127    }
128
129    fn split(&mut self, _: &SplitDiff, window: &mut Window, cx: &mut Context<Self>) {
130        if !cx.has_flag::<SplitDiffFeatureFlag>() {
131            return;
132        }
133        if self.secondary.is_some() {
134            return;
135        }
136        let Some(workspace) = self.workspace.upgrade() else {
137            return;
138        };
139        let project = workspace.read(cx).project().clone();
140
141        // FIXME
142        // - have to subscribe to the diffs to update the base text buffers (and handle language changed I think?)
143
144        let secondary_editor = cx.new(|cx| {
145            let multibuffer = cx.new(|cx| {
146                let mut multibuffer = MultiBuffer::new(Capability::ReadOnly);
147                multibuffer.set_all_diff_hunks_expanded(cx);
148                multibuffer
149            });
150            Editor::for_multibuffer(multibuffer, Some(project.clone()), window, cx)
151        });
152        let secondary_pane = cx.new(|cx| {
153            let mut pane = Pane::new(
154                workspace.downgrade(),
155                workspace.read(cx).project().clone(),
156                Default::default(),
157                None,
158                NoAction.boxed_clone(),
159                true,
160                window,
161                cx,
162            );
163            pane.set_should_display_tab_bar(|_, _| false);
164            pane.add_item(
165                ItemHandle::boxed_clone(&secondary_editor),
166                false,
167                false,
168                None,
169                window,
170                cx,
171            );
172            pane
173        });
174
175        let subscriptions =
176            vec![
177                cx.subscribe(&secondary_editor, |this, _, event: &EditorEvent, cx| {
178                    if let EditorEvent::SelectionsChanged { .. } = event
179                        && let Some(secondary) = &mut this.secondary
180                    {
181                        secondary.has_latest_selection = true;
182                    }
183                    cx.emit(event.clone())
184                }),
185            ];
186        let mut secondary = SecondaryEditor {
187            editor: secondary_editor,
188            pane: secondary_pane.clone(),
189            has_latest_selection: false,
190            _subscriptions: subscriptions,
191        };
192        self.primary_editor.update(cx, |editor, cx| {
193            editor.buffer().update(cx, |primary_multibuffer, cx| {
194                primary_multibuffer.set_show_deleted_hunks(false, cx);
195                let paths = primary_multibuffer.paths().collect::<Vec<_>>();
196                for path in paths {
197                    secondary.sync_path_excerpts(
198                        path,
199                        primary_multibuffer,
200                        project.read(cx).languages().clone(),
201                        cx,
202                    );
203                }
204            })
205        });
206        self.secondary = Some(secondary);
207
208        let primary_pane = self.panes.first_pane();
209        self.panes
210            .split(&primary_pane, &secondary_pane, SplitDirection::Left)
211            .unwrap();
212        cx.notify();
213    }
214
215    fn unsplit(&mut self, _: &UnsplitDiff, _: &mut Window, cx: &mut Context<Self>) {
216        let Some(secondary) = self.secondary.take() else {
217            return;
218        };
219        self.panes.remove(&secondary.pane).unwrap();
220        self.primary_editor.update(cx, |primary, cx| {
221            primary.buffer().update(cx, |buffer, cx| {
222                buffer.set_show_deleted_hunks(true, cx);
223            });
224        });
225        cx.notify();
226    }
227
228    pub fn added_to_workspace(
229        &mut self,
230        workspace: &mut Workspace,
231        window: &mut Window,
232        cx: &mut Context<Self>,
233    ) {
234        self.workspace = workspace.weak_handle();
235        self.primary_editor.update(cx, |primary_editor, cx| {
236            primary_editor.added_to_workspace(workspace, window, cx);
237        });
238        if let Some(secondary) = &self.secondary {
239            secondary.editor.update(cx, |secondary_editor, cx| {
240                secondary_editor.added_to_workspace(workspace, window, cx);
241            });
242        }
243    }
244
245    pub fn set_excerpts_for_path(
246        &mut self,
247        path: PathKey,
248        buffer: Entity<Buffer>,
249        ranges: impl IntoIterator<Item = Range<Point>>,
250        context_line_count: u32,
251        cx: &mut Context<Self>,
252    ) -> (Vec<Range<Anchor>>, bool) {
253        self.primary_editor.update(cx, |editor, cx| {
254            editor.buffer().update(cx, |primary_multibuffer, cx| {
255                let (anchors, added_a_new_excerpt) = primary_multibuffer.set_excerpts_for_path(
256                    path.clone(),
257                    buffer,
258                    ranges,
259                    context_line_count,
260                    cx,
261                );
262                if let Some(secondary) = &mut self.secondary
263                    && let Some(languages) = self
264                        .workspace
265                        .update(cx, |workspace, cx| {
266                            workspace.project().read(cx).languages().clone()
267                        })
268                        .ok()
269                {
270                    secondary.sync_path_excerpts(path, primary_multibuffer, languages, cx);
271                }
272                (anchors, added_a_new_excerpt)
273            })
274        })
275    }
276}
277
278impl EventEmitter<EditorEvent> for SplittableEditor {}
279impl Focusable for SplittableEditor {
280    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
281        self.primary_editor.read(cx).focus_handle(cx)
282    }
283}
284
285impl Render for SplittableEditor {
286    fn render(
287        &mut self,
288        window: &mut ui::Window,
289        cx: &mut ui::Context<Self>,
290    ) -> impl ui::IntoElement {
291        let inner = if self.secondary.is_none() {
292            self.primary_editor.clone().into_any_element()
293        } else if let Some(active) = self.panes.panes().into_iter().next() {
294            self.panes
295                .render(
296                    None,
297                    &ActivePaneDecorator::new(active, &self.workspace),
298                    window,
299                    cx,
300                )
301                .into_any_element()
302        } else {
303            div().into_any_element()
304        };
305        div()
306            .id("splittable-editor")
307            .on_action(cx.listener(Self::split))
308            .on_action(cx.listener(Self::unsplit))
309            .size_full()
310            .child(inner)
311    }
312}
313
314impl SecondaryEditor {
315    fn sync_path_excerpts(
316        &mut self,
317        path_key: PathKey,
318        primary_multibuffer: &mut MultiBuffer,
319        languages: Arc<LanguageRegistry>,
320        cx: &mut App,
321    ) {
322        let excerpt_id = primary_multibuffer
323            .excerpts_for_path(&path_key)
324            .next()
325            .unwrap();
326        let primary_multibuffer_snapshot = primary_multibuffer.snapshot(cx);
327        let main_buffer = primary_multibuffer_snapshot
328            .buffer_for_excerpt(excerpt_id)
329            .unwrap();
330        let diff = primary_multibuffer
331            .diff_for(main_buffer.remote_id())
332            .unwrap();
333        let diff = diff.read(cx).snapshot(cx);
334        let base_text_buffer = self
335            .editor
336            .update(cx, |editor, cx| {
337                editor.buffer().update(cx, |secondary_multibuffer, cx| {
338                    let excerpt_id = secondary_multibuffer.excerpts_for_path(&path_key).next()?;
339                    let secondary_buffer_snapshot = secondary_multibuffer.snapshot(cx);
340                    let buffer = secondary_buffer_snapshot
341                        .buffer_for_excerpt(excerpt_id)
342                        .unwrap();
343                    Some(secondary_multibuffer.buffer(buffer.remote_id()).unwrap())
344                })
345            })
346            .unwrap_or_else(|| {
347                cx.new(|cx| {
348                    let base_text = diff.base_text();
349                    let mut buffer = Buffer::local_normalized(
350                        base_text.as_rope().clone(),
351                        base_text.line_ending(),
352                        cx,
353                    );
354                    buffer.set_language(base_text.language().cloned(), cx);
355                    buffer.set_language_registry(languages);
356                    buffer
357                })
358            });
359        let base_text_buffer_snapshot = base_text_buffer.read(cx).snapshot();
360        let new = primary_multibuffer
361            .excerpts_for_buffer(main_buffer.remote_id(), cx)
362            .into_iter()
363            .map(|(_, excerpt_range)| {
364                let point_to_base_text_point = |point: Point| {
365                    let row = diff.row_to_base_text_row(point.row, main_buffer);
366                    let column = diff.base_text().line_len(row);
367                    Point::new(row, column)
368                };
369                let primary = excerpt_range.primary.to_point(main_buffer);
370                let context = excerpt_range.context.to_point(main_buffer);
371                ExcerptRange {
372                    primary: point_to_base_text_point(primary.start)
373                        ..point_to_base_text_point(primary.end),
374                    context: point_to_base_text_point(context.start)
375                        ..point_to_base_text_point(context.end),
376                }
377            })
378            .collect();
379
380        self.editor.update(cx, |editor, cx| {
381            editor.buffer().update(cx, |buffer, cx| {
382                buffer.update_path_excerpts(
383                    path_key,
384                    base_text_buffer,
385                    &base_text_buffer_snapshot,
386                    new,
387                    cx,
388                )
389            })
390        });
391    }
392}