split.rs

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