Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T03:27:56.127Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

diff_multibuffer.rs

1039 lines · 38.3 KB · rust
1use crate::{
2    conflict_view,
3    git_panel::{GitPanel, GitStatusEntry},
4    git_panel_settings::GitPanelSettings,
5};
6use anyhow::Result;
7use buffer_diff::BufferDiff;
8use collections::{HashMap, HashSet};
9use editor::{
10    EditorEvent, EditorSettings, SelectionEffects, SplittableEditor, actions::GoToHunk,
11    multibuffer_context_lines, scroll::Autoscroll,
12};
13use futures_lite::future::yield_now;
14use git::{repository::RepoPath, status::FileStatus};
15use gpui::{
16    App, AppContext as _, AsyncWindowContext, Entity, EventEmitter, FocusHandle, Focusable, Render,
17    SharedString, Subscription, Task, WeakEntity,
18};
19use language::{Anchor, Buffer, BufferId, Capability, OffsetRangeExt};
20use multi_buffer::{MultiBuffer, PathKey};
21use project::{
22    ConflictSet, Project, ProjectPath,
23    git_store::{
24        Repository,
25        diff_buffer_list::{self, BranchDiffEvent, DiffBase},
26    },
27};
28use settings::{GitPanelGroupBy, GitPanelSortBy, Settings, SettingsStore};
29use std::{collections::BTreeMap, sync::Arc};
30use theme::ActiveTheme;
31use ui::{CommonAnimationExt as _, KeyBinding, prelude::*};
32use util::{ResultExt as _, rel_path::RelPath};
33use workspace::{
34    CloseActiveItem, ItemNavHistory, Workspace,
35    item::{Item, SaveOptions},
36};
37use ztracing::instrument;
38
39struct BufferSubscriptions {
40    _diff: Entity<BufferDiff>,
41    display_buffer: Entity<Buffer>,
42    _diff_subscription: Subscription,
43    _conflict_set: Option<Entity<ConflictSet>>,
44    _conflict_set_subscription: Option<Subscription>,
45}
46
47pub struct DiffMultibuffer {
48    multibuffer: Entity<MultiBuffer>,
49    branch_diff: Entity<diff_buffer_list::DiffBufferList>,
50    editor: Entity<SplittableEditor>,
51    buffer_subscriptions: HashMap<RepoPath, BufferSubscriptions>,
52    workspace: WeakEntity<Workspace>,
53    focus_handle: FocusHandle,
54    pending_scroll: Option<PathKey>,
55    review_comment_count: usize,
56    empty_label: SharedString,
57    _task: Task<Result<()>>,
58    _subscription: Subscription,
59}
60
61impl DiffMultibuffer {
62    pub(crate) fn new(
63        branch_diff: Entity<diff_buffer_list::DiffBufferList>,
64        multibuffer_capability: Capability,
65        empty_label: impl Into<SharedString>,
66        configure_editor: impl FnOnce(&mut SplittableEditor, &mut Context<SplittableEditor>) + 'static,
67        project: Entity<Project>,
68        workspace: Entity<Workspace>,
69        window: &mut Window,
70        cx: &mut Context<Self>,
71    ) -> Self {
72        let focus_handle = cx.focus_handle();
73        let multibuffer = cx.new(|cx| {
74            let mut multibuffer = MultiBuffer::new(multibuffer_capability);
75            multibuffer.set_all_diff_hunks_expanded(cx);
76            multibuffer
77        });
78        let editor = cx.new(|cx| {
79            let mut diff_display_editor = SplittableEditor::new(
80                EditorSettings::get_global(cx).diff_view_style,
81                multibuffer.clone(),
82                project.clone(),
83                workspace.clone(),
84                window,
85                cx,
86            );
87            configure_editor(&mut diff_display_editor, cx);
88            diff_display_editor.rhs_editor().update(cx, |editor, cx| {
89                editor.set_show_diff_review_button(true, cx);
90            });
91            diff_display_editor
92        });
93        let editor_subscription = cx.subscribe_in(&editor, window, Self::handle_editor_event);
94
95        let primary_editor = editor.read(cx).rhs_editor().clone();
96        let review_comment_subscription =
97            cx.subscribe(&primary_editor, |this, _editor, event: &EditorEvent, cx| {
98                if let EditorEvent::ReviewCommentsChanged { total_count } = event {
99                    this.review_comment_count = *total_count;
100                    cx.notify();
101                }
102            });
103
104        let branch_diff_subscription = cx.subscribe_in(
105            &branch_diff,
106            window,
107            move |this, _git_store, event, window, cx| match event {
108                BranchDiffEvent::FileListChanged => {
109                    this._task = window.spawn(cx, {
110                        let this = cx.weak_entity();
111                        async |cx| Self::refresh(this, cx).await
112                    })
113                }
114                BranchDiffEvent::DiffBaseChanged => {
115                    this.pending_scroll.take();
116                    this._task = window.spawn(cx, {
117                        let this = cx.weak_entity();
118                        async |cx| Self::refresh(this, cx).await
119                    })
120                }
121            },
122        );
123
124        let mut was_sort_by = GitPanelSettings::get_global(cx).sort_by;
125        let mut was_group_by = GitPanelSettings::get_global(cx).group_by;
126        let mut was_tree_view = GitPanelSettings::get_global(cx).tree_view;
127        let mut was_collapse_untracked_diff =
128            GitPanelSettings::get_global(cx).collapse_untracked_diff;
129        cx.observe_global_in::<SettingsStore>(window, move |this, window, cx| {
130            let settings = GitPanelSettings::get_global(cx);
131            let sort_by = settings.sort_by;
132            let group_by = settings.group_by;
133            let tree_view = settings.tree_view;
134            let is_collapse_untracked_diff = settings.collapse_untracked_diff;
135            if sort_by != was_sort_by
136                || group_by != was_group_by
137                || tree_view != was_tree_view
138                || is_collapse_untracked_diff != was_collapse_untracked_diff
139            {
140                this._task = {
141                    window.spawn(cx, {
142                        let this = cx.weak_entity();
143                        async |cx| Self::refresh(this, cx).await
144                    })
145                }
146            }
147            was_sort_by = sort_by;
148            was_group_by = group_by;
149            was_tree_view = tree_view;
150            was_collapse_untracked_diff = is_collapse_untracked_diff;
151        })
152        .detach();
153
154        let task = window.spawn(cx, {
155            let this = cx.weak_entity();
156            async |cx| Self::refresh(this, cx).await
157        });
158
159        Self {
160            workspace: workspace.downgrade(),
161            branch_diff,
162            focus_handle,
163            editor,
164            multibuffer,
165            buffer_subscriptions: Default::default(),
166            pending_scroll: None,
167            review_comment_count: 0,
168            empty_label: empty_label.into(),
169            _task: task,
170            _subscription: Subscription::join(
171                branch_diff_subscription,
172                Subscription::join(editor_subscription, review_comment_subscription),
173            ),
174        }
175    }
176
177    pub(crate) fn diff_base<'a>(&'a self, cx: &'a App) -> &'a DiffBase {
178        self.branch_diff.read(cx).diff_base()
179    }
180
181    pub(crate) fn branch_diff(&self) -> &Entity<diff_buffer_list::DiffBufferList> {
182        &self.branch_diff
183    }
184
185    pub(crate) fn repo(&self, cx: &App) -> Option<Entity<Repository>> {
186        self.branch_diff.read(cx).repo().cloned()
187    }
188
189    pub(crate) fn set_repo(&mut self, repo: Option<Entity<Repository>>, cx: &mut Context<Self>) {
190        self.branch_diff.update(cx, |branch_diff, cx| {
191            branch_diff.set_repo(repo, cx);
192        });
193    }
194
195    pub(crate) fn is_dirty(&self, cx: &App) -> bool {
196        self.multibuffer.read(cx).is_dirty(cx)
197    }
198
199    pub(crate) fn has_conflict(&self, cx: &App) -> bool {
200        self.multibuffer.read(cx).has_conflict(cx)
201    }
202
203    pub(crate) fn multibuffer(&self) -> &Entity<MultiBuffer> {
204        &self.multibuffer
205    }
206
207    pub(crate) fn move_to_entry(
208        &mut self,
209        entry: GitStatusEntry,
210        window: &mut Window,
211        cx: &mut Context<Self>,
212    ) {
213        let Some(git_repo) = self.branch_diff.read(cx).repo() else {
214            return;
215        };
216        let repo = git_repo.read(cx);
217        let path_key = project_diff_path_key(repo, &entry.repo_path, entry.status, cx);
218
219        self.move_to_path(path_key, window, cx)
220    }
221
222    pub(crate) fn move_to_project_path(
223        &mut self,
224        project_path: &ProjectPath,
225        window: &mut Window,
226        cx: &mut Context<Self>,
227    ) {
228        let Some(git_repo) = self.branch_diff.read(cx).repo() else {
229            return;
230        };
231        let Some(repo_path) = git_repo
232            .read(cx)
233            .project_path_to_repo_path(project_path, cx)
234        else {
235            return;
236        };
237        let status = git_repo
238            .read(cx)
239            .status_for_path(&repo_path)
240            .map(|entry| entry.status)
241            .unwrap_or(FileStatus::Untracked);
242        let path_key = project_diff_path_key(&git_repo.read(cx), &repo_path, status, cx);
243        self.move_to_path(path_key, window, cx)
244    }
245
246    pub(crate) fn move_to_beginning(&mut self, window: &mut Window, cx: &mut Context<Self>) {
247        self.editor.update(cx, |editor, cx| {
248            editor.rhs_editor().update(cx, |editor, cx| {
249                editor.change_selections(Default::default(), window, cx, |s| {
250                    s.select_ranges(vec![multi_buffer::Anchor::Min..multi_buffer::Anchor::Min]);
251                });
252            });
253        });
254    }
255
256    pub(crate) fn move_to_path(
257        &mut self,
258        path_key: PathKey,
259        window: &mut Window,
260        cx: &mut Context<Self>,
261    ) {
262        if let Some(position) = self.multibuffer.read(cx).location_for_path(&path_key, cx) {
263            self.editor.update(cx, |editor, cx| {
264                editor.rhs_editor().update(cx, |editor, cx| {
265                    editor.change_selections(
266                        SelectionEffects::scroll(Autoscroll::focused()),
267                        window,
268                        cx,
269                        |s| {
270                            s.select_ranges([position..position]);
271                        },
272                    )
273                })
274            });
275        } else {
276            self.pending_scroll = Some(path_key);
277        }
278    }
279
280    pub(crate) fn autoscroll(&self, cx: &mut Context<Self>) {
281        self.editor.update(cx, |editor, cx| {
282            editor.rhs_editor().update(cx, |editor, cx| {
283                editor.request_autoscroll(Autoscroll::fit(), cx);
284            })
285        })
286    }
287
288    pub(crate) fn calculate_changed_lines(&self, cx: &App) -> (u32, u32) {
289        self.multibuffer.read(cx).snapshot(cx).total_changed_lines()
290    }
291
292    /// Returns the total count of review comments across all hunks/files.
293    pub(crate) fn total_review_comment_count(&self) -> usize {
294        self.review_comment_count
295    }
296
297    /// Returns a reference to the splittable editor.
298    pub(crate) fn editor(&self) -> &Entity<SplittableEditor> {
299        &self.editor
300    }
301
302    pub(crate) fn selected_ranges(
303        &self,
304        cx: &App,
305    ) -> (bool, Vec<std::ops::Range<multi_buffer::Anchor>>) {
306        let editor = self.editor.read(cx).rhs_editor().read(cx);
307        let snapshot = self.multibuffer.read(cx).snapshot(cx);
308        let mut selection = true;
309        let mut ranges = editor
310            .selections
311            .disjoint_anchor_ranges()
312            .collect::<Vec<_>>();
313        if !ranges.iter().any(|range| range.start != range.end) {
314            selection = false;
315            let anchor = editor.selections.newest_anchor().head();
316            if let Some((_, excerpt_range)) = snapshot.excerpt_containing(anchor..anchor)
317                && let Some(range) = snapshot
318                    .anchor_in_buffer(excerpt_range.context.start)
319                    .zip(snapshot.anchor_in_buffer(excerpt_range.context.end))
320                    .map(|(start, end)| start..end)
321            {
322                ranges = vec![range];
323            } else {
324                ranges = Vec::default();
325            };
326        }
327
328        (selection, ranges)
329    }
330
331    /// Ranges for a toolbar stage/unstage action: the selection, or the cursor
332    /// (a zero-width range that resolves to the single hunk under it) when
333    /// there is no selection. Unlike [`Self::selected_ranges`], this never
334    /// widens to the whole excerpt, so actions affect one hunk at a time.
335    fn hunk_action_ranges(&self, cx: &App) -> Vec<std::ops::Range<multi_buffer::Anchor>> {
336        self.editor
337            .read(cx)
338            .rhs_editor()
339            .read(cx)
340            .selections
341            .disjoint_anchor_ranges()
342            .collect()
343    }
344
345    pub(crate) fn stage_or_unstage_selected_hunks(
346        &mut self,
347        stage: bool,
348        move_to_next: bool,
349        window: &mut Window,
350        cx: &mut Context<Self>,
351    ) {
352        let editor = self.editor.read(cx).rhs_editor().clone();
353        let ranges = self.hunk_action_ranges(cx);
354        // Route through the editor's delegated stage path, the same path taken
355        // by the hunk buttons (on either side of a split) and the keyboard.
356        // For staging, dirty buffers are saved first, exactly as they are when
357        // staging from the uncommitted diff or a normal editor.
358        editor.update(cx, |editor, cx| {
359            editor.stage_or_unstage_diff_hunks(stage, ranges, window, cx);
360        });
361        if move_to_next {
362            editor
363                .focus_handle(cx)
364                .dispatch_action(&GoToHunk, window, cx);
365        }
366    }
367
368    pub(crate) fn restore_selected_hunks(
369        &mut self,
370        move_to_next: bool,
371        window: &mut Window,
372        cx: &mut Context<Self>,
373    ) {
374        let editor = self.editor.read(cx).rhs_editor().clone();
375        let ranges = self.hunk_action_ranges(cx);
376        editor.update(cx, |editor, cx| {
377            let snapshot = editor.buffer().read(cx).snapshot(cx);
378            let hunks: Vec<_> = editor.diff_hunks_in_ranges(&ranges, &snapshot).collect();
379            if !hunks.is_empty() {
380                editor.apply_restore(hunks, window, cx);
381            }
382        });
383        if move_to_next {
384            editor
385                .focus_handle(cx)
386                .dispatch_action(&GoToHunk, window, cx);
387        }
388    }
389
390    fn handle_editor_event(
391        &mut self,
392        editor: &Entity<SplittableEditor>,
393        event: &EditorEvent,
394        window: &mut Window,
395        cx: &mut Context<Self>,
396    ) {
397        match event {
398            EditorEvent::SelectionsChanged { local: true } => {
399                // Only follow the git panel selection from the view the user is
400                // actually interacting with. Background (non-active) diff views
401                // refresh on their own and must not hijack the panel selection.
402                if !editor.focus_handle(cx).contains_focused(window, cx) {
403                    return;
404                }
405                let Some(project_path) = self.active_project_path(cx) else {
406                    return;
407                };
408                self.workspace
409                    .update(cx, |workspace, cx| {
410                        if let Some(git_panel) = workspace.panel::<GitPanel>(cx) {
411                            git_panel.update(cx, |git_panel, cx| {
412                                git_panel.select_entry_by_path(project_path, window, cx)
413                            })
414                        }
415                    })
416                    .ok();
417            }
418            EditorEvent::Saved => {
419                self._task =
420                    cx.spawn_in(window, async move |this, cx| Self::refresh(this, cx).await);
421            }
422
423            _ => {}
424        }
425        if editor.focus_handle(cx).contains_focused(window, cx)
426            && self.multibuffer.read(cx).is_empty()
427        {
428            self.focus_handle.focus(window, cx)
429        }
430    }
431
432    #[instrument(skip_all)]
433    fn register_buffer(
434        &mut self,
435        repo_path: RepoPath,
436        path_key: PathKey,
437        file_status: FileStatus,
438        display_buffer: Entity<Buffer>,
439        main_buffer: Entity<Buffer>,
440        diff: Entity<BufferDiff>,
441        conflict_set: Option<Entity<ConflictSet>>,
442        window: &mut Window,
443        cx: &mut Context<Self>,
444    ) -> Option<BufferId> {
445        let diff_subscription = cx.subscribe_in(&diff, window, {
446            let repo_path = repo_path.clone();
447            let path_key = path_key.clone();
448            let display_buffer = display_buffer.clone();
449            let main_buffer = main_buffer.clone();
450            let diff = diff.clone();
451            let conflict_set = conflict_set.clone();
452            move |this, _, event, window, cx| match event {
453                buffer_diff::BufferDiffEvent::DiffChanged(_) => {
454                    this.buffer_ranges_changed(
455                        repo_path.clone(),
456                        path_key.clone(),
457                        file_status,
458                        display_buffer.clone(),
459                        main_buffer.clone(),
460                        diff.clone(),
461                        conflict_set.clone(),
462                        window,
463                        cx,
464                    );
465                }
466                buffer_diff::BufferDiffEvent::BaseTextChanged => {}
467            }
468        });
469        let conflict_set_subscription = conflict_set.as_ref().map(|conflict_set| {
470            cx.subscribe_in(conflict_set, window, {
471                let repo_path = repo_path.clone();
472                let path_key = path_key.clone();
473                let display_buffer = display_buffer.clone();
474                let main_buffer = main_buffer.clone();
475                let diff = diff.clone();
476                let conflict_set = Some(conflict_set.clone());
477                move |this, _, _, window, cx| {
478                    this.buffer_ranges_changed(
479                        repo_path.clone(),
480                        path_key.clone(),
481                        file_status,
482                        display_buffer.clone(),
483                        main_buffer.clone(),
484                        diff.clone(),
485                        conflict_set.clone(),
486                        window,
487                        cx,
488                    )
489                }
490            })
491        });
492        self.buffer_subscriptions.insert(
493            repo_path,
494            BufferSubscriptions {
495                _diff: diff.clone(),
496                display_buffer: display_buffer.clone(),
497                _diff_subscription: diff_subscription,
498                _conflict_set: conflict_set.clone(),
499                _conflict_set_subscription: conflict_set_subscription,
500            },
501        );
502
503        let snapshot = display_buffer.read(cx).snapshot();
504        let diff_snapshot = diff.read(cx).snapshot(cx);
505
506        let excerpt_ranges = {
507            let diff_hunk_ranges = diff_snapshot
508                .hunks_intersecting_range(
509                    Anchor::min_max_range_for_buffer(snapshot.remote_id()),
510                    &snapshot,
511                )
512                .map(|diff_hunk| diff_hunk.buffer_range.to_point(&snapshot));
513            let conflict_ranges = conflict_set.as_ref().and_then(|conflict_set| {
514                let conflicts = conflict_set.read(cx).snapshot();
515                let conflicts = conflicts
516                    .conflicts
517                    .iter()
518                    .map(|conflict| conflict.range.to_point(&snapshot))
519                    .collect::<Vec<_>>();
520                (!conflicts.is_empty()).then_some(conflicts)
521            });
522
523            conflict_ranges.unwrap_or_else(|| diff_hunk_ranges.collect())
524        };
525
526        let buffer_id = snapshot.text.remote_id();
527        let mut needs_fold = false;
528
529        let (was_empty, is_excerpt_newly_added) = self.editor.update(cx, |editor, cx| {
530            let was_empty = editor.rhs_editor().read(cx).buffer().read(cx).is_empty();
531            let is_newly_added = editor.update_excerpts_for_path(
532                path_key.clone(),
533                display_buffer,
534                excerpt_ranges,
535                multibuffer_context_lines(cx),
536                diff,
537                cx,
538            );
539            if let Some(conflict_set) = conflict_set {
540                editor.rhs_editor().update(cx, |editor, cx| {
541                    conflict_view::buffer_ranges_updated(editor, conflict_set, cx);
542                });
543            }
544            (was_empty, is_newly_added)
545        });
546
547        self.editor.update(cx, |editor, cx| {
548            editor.rhs_editor().update(cx, |editor, cx| {
549                if was_empty {
550                    editor.change_selections(
551                        SelectionEffects::no_scroll(),
552                        window,
553                        cx,
554                        |selections| {
555                            selections.select_ranges([
556                                multi_buffer::Anchor::Min..multi_buffer::Anchor::Min
557                            ])
558                        },
559                    );
560                }
561                if is_excerpt_newly_added
562                    && (file_status.is_deleted()
563                        || (file_status.is_untracked()
564                            && GitPanelSettings::get_global(cx).collapse_untracked_diff))
565                {
566                    needs_fold = true;
567                }
568            })
569        });
570
571        if self.multibuffer.read(cx).is_empty()
572            && self
573                .editor
574                .read(cx)
575                .focus_handle(cx)
576                .contains_focused(window, cx)
577        {
578            self.focus_handle.focus(window, cx);
579        } else if self.focus_handle.is_focused(window) && !self.multibuffer.read(cx).is_empty() {
580            self.editor.update(cx, |editor, cx| {
581                editor.focus_handle(cx).focus(window, cx);
582            });
583        }
584        if self.pending_scroll.as_ref() == Some(&path_key) {
585            self.move_to_path(path_key, window, cx);
586        }
587
588        needs_fold.then_some(buffer_id)
589    }
590
591    fn buffer_ranges_changed(
592        &mut self,
593        repo_path: RepoPath,
594        path_key: PathKey,
595        file_status: FileStatus,
596        display_buffer: Entity<Buffer>,
597        main_buffer: Entity<Buffer>,
598        diff: Entity<BufferDiff>,
599        conflict_set: Option<Entity<ConflictSet>>,
600        window: &mut Window,
601        cx: &mut Context<Self>,
602    ) {
603        if display_buffer.read(cx).is_dirty() {
604            return;
605        }
606        self.register_buffer(
607            repo_path,
608            path_key,
609            file_status,
610            display_buffer,
611            main_buffer,
612            diff,
613            conflict_set,
614            window,
615            cx,
616        );
617    }
618
619    #[instrument(skip(this, cx))]
620    pub(crate) async fn refresh(this: WeakEntity<Self>, cx: &mut AsyncWindowContext) -> Result<()> {
621        let entries = this.update(cx, |this, cx| {
622            let (repo, buffers_to_load) = this.branch_diff.update(cx, |branch_diff, cx| {
623                let load_buffers = branch_diff.load_buffers(cx);
624                (branch_diff.repo().cloned(), load_buffers)
625            });
626            let mut previous_paths = this
627                .multibuffer
628                .read(cx)
629                .snapshot(cx)
630                .buffers_with_paths()
631                .map(|(buffer_snapshot, path_key)| (path_key.clone(), buffer_snapshot.remote_id()))
632                .collect::<HashMap<_, _>>();
633
634            let mut entries = BTreeMap::new();
635            let mut live_repo_paths = HashSet::default();
636            if let Some(repo) = repo {
637                let repo = repo.read(cx);
638                for diff_buffer in buffers_to_load {
639                    live_repo_paths.insert(diff_buffer.repo_path.clone());
640                    let path_key = project_diff_path_key(
641                        &repo,
642                        &diff_buffer.repo_path,
643                        diff_buffer.file_status,
644                        cx,
645                    );
646                    previous_paths.remove(&path_key);
647                    entries.insert(path_key, diff_buffer);
648                }
649            }
650
651            let repo_path_by_display_id = this
652                .buffer_subscriptions
653                .iter()
654                .map(|(repo_path, sub)| {
655                    (sub.display_buffer.read(cx).remote_id(), repo_path.clone())
656                })
657                .collect::<HashMap<_, _>>();
658
659            this.editor.update(cx, |editor, cx| {
660                for (path, buffer_id) in previous_paths {
661                    if let Some(repo_path) = repo_path_by_display_id.get(&buffer_id) {
662                        this.buffer_subscriptions.remove(repo_path);
663                    }
664                    editor.rhs_editor().update(cx, |editor, cx| {
665                        conflict_view::buffers_removed(editor, &[buffer_id], cx);
666                    });
667                    let _span = ztracing::info_span!("remove_excerpts_for_path");
668                    _span.enter();
669                    editor.remove_excerpts_for_path(path, cx);
670                }
671            });
672
673            this.buffer_subscriptions
674                .retain(|repo_path, _| live_repo_paths.contains(repo_path));
675
676            entries
677        })?;
678
679        let mut buffers_to_fold = Vec::new();
680
681        for (path_key, entry) in entries {
682            if let Some(loaded_buffer) = entry.load.await.log_err() {
683                // We might be lagging behind enough that all future entry.load futures are no longer pending.
684                // If that is the case, this task will never yield, starving the foreground thread of execution time.
685                yield_now().await;
686                cx.update(|window, cx| {
687                    this.update(cx, |this, cx| {
688                        if let Some(buffer_id) = this.register_buffer(
689                            entry.repo_path,
690                            path_key,
691                            entry.file_status,
692                            loaded_buffer.display_buffer,
693                            loaded_buffer.main_buffer,
694                            loaded_buffer.diff,
695                            loaded_buffer.conflict_set,
696                            window,
697                            cx,
698                        ) {
699                            buffers_to_fold.push(buffer_id);
700                        }
701                    })
702                    .ok();
703                })?;
704            }
705        }
706        this.update(cx, |this, cx| {
707            if !buffers_to_fold.is_empty() {
708                this.editor.update(cx, |editor, cx| {
709                    editor
710                        .rhs_editor()
711                        .update(cx, |editor, cx| editor.fold_buffers(buffers_to_fold, cx));
712                });
713            }
714            this.pending_scroll.take();
715            cx.notify();
716        })?;
717
718        Ok(())
719    }
720
721    pub(crate) fn active_project_path(&self, cx: &App) -> Option<ProjectPath> {
722        let editor = self.editor.read(cx).focused_editor().read(cx);
723        let multibuffer = editor.buffer().read(cx);
724        let position = editor.selections.newest_anchor().head();
725        let snapshot = multibuffer.snapshot(cx);
726        let (text_anchor, _) = snapshot.anchor_to_buffer_anchor(position)?;
727        let buffer = multibuffer.buffer(text_anchor.buffer_id)?;
728
729        let file = buffer.read(cx).file()?;
730        Some(ProjectPath {
731            worktree_id: file.worktree_id(cx),
732            path: file.path().clone(),
733        })
734    }
735
736    pub(crate) fn added_to_workspace(
737        &mut self,
738        workspace: &mut Workspace,
739        window: &mut Window,
740        cx: &mut Context<Self>,
741    ) {
742        self.editor.update(cx, |editor, cx| {
743            editor.added_to_workspace(workspace, window, cx)
744        });
745    }
746
747    pub(crate) fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
748        self.editor.update(cx, |editor, cx| {
749            editor.rhs_editor().update(cx, |primary_editor, cx| {
750                primary_editor.deactivated(window, cx);
751            })
752        });
753    }
754
755    pub(crate) fn navigate(
756        &mut self,
757        data: Arc<dyn std::any::Any + Send>,
758        window: &mut Window,
759        cx: &mut Context<Self>,
760    ) -> bool {
761        self.editor.update(cx, |editor, cx| {
762            editor.rhs_editor().update(cx, |primary_editor, cx| {
763                primary_editor.navigate(data, window, cx)
764            })
765        })
766    }
767
768    pub(crate) fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut Context<Self>) {
769        self.editor.update(cx, |editor, cx| {
770            editor.rhs_editor().update(cx, |primary_editor, _| {
771                primary_editor.set_nav_history(Some(nav_history));
772            })
773        });
774    }
775
776    pub(crate) fn for_each_project_item(
777        &self,
778        cx: &App,
779        f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
780    ) {
781        self.editor
782            .read(cx)
783            .rhs_editor()
784            .read(cx)
785            .for_each_project_item(cx, f)
786    }
787
788    pub(crate) fn save(
789        &mut self,
790        options: SaveOptions,
791        project: Entity<Project>,
792        window: &mut Window,
793        cx: &mut Context<Self>,
794    ) -> Task<Result<()>> {
795        self.editor.update(cx, |editor, cx| {
796            editor.rhs_editor().update(cx, |primary_editor, cx| {
797                primary_editor.save(options, project, window, cx)
798            })
799        })
800    }
801
802    pub(crate) fn reload(
803        &mut self,
804        project: Entity<Project>,
805        window: &mut Window,
806        cx: &mut Context<Self>,
807    ) -> Task<Result<()>> {
808        self.editor.update(cx, |editor, cx| {
809            editor.rhs_editor().update(cx, |primary_editor, cx| {
810                primary_editor.reload(project, window, cx)
811            })
812        })
813    }
814
815    #[cfg(any(test, feature = "test-support"))]
816    pub fn excerpt_paths(&self, cx: &App) -> Vec<std::sync::Arc<util::rel_path::RelPath>> {
817        let snapshot = self
818            .editor()
819            .read(cx)
820            .rhs_editor()
821            .read(cx)
822            .buffer()
823            .read(cx)
824            .snapshot(cx);
825        snapshot
826            .excerpts()
827            .map(|excerpt| {
828                snapshot
829                    .path_for_buffer(excerpt.context.start.buffer_id)
830                    .unwrap()
831                    .path
832                    .clone()
833            })
834            .collect()
835    }
836
837    /// Returns the real (worktree-relative) path of each excerpted buffer, in
838    /// the order the excerpts appear in the multibuffer. Unlike
839    /// [`Self::excerpt_paths`], this resolves the buffer's actual `File` rather
840    /// than the (possibly synthetic) `PathKey` path used for sorting.
841    #[cfg(any(test, feature = "test-support"))]
842    pub fn excerpt_file_paths(&self, cx: &App) -> Vec<String> {
843        let multibuffer = self
844            .editor()
845            .read(cx)
846            .rhs_editor()
847            .read(cx)
848            .buffer()
849            .clone();
850        let snapshot = multibuffer.read(cx).snapshot(cx);
851        let mut result = Vec::new();
852        let mut last_buffer_id = None;
853        for excerpt in snapshot.excerpts() {
854            let buffer_id = excerpt.context.start.buffer_id;
855            if last_buffer_id == Some(buffer_id) {
856                continue;
857            }
858            last_buffer_id = Some(buffer_id);
859            if let Some(buffer) = multibuffer.read(cx).buffer(buffer_id)
860                && let Some(file) = buffer.read(cx).file()
861            {
862                result.push(file.path().as_unix_str().to_string());
863            }
864        }
865        result
866    }
867}
868
869impl EventEmitter<EditorEvent> for DiffMultibuffer {}
870
871impl Focusable for DiffMultibuffer {
872    fn focus_handle(&self, cx: &App) -> FocusHandle {
873        if self.multibuffer.read(cx).is_empty() {
874            self.focus_handle.clone()
875        } else {
876            self.editor.focus_handle(cx)
877        }
878    }
879}
880
881impl Render for DiffMultibuffer {
882    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
883        let is_empty = self.multibuffer.read(cx).is_empty();
884        let is_loading = self.branch_diff.read(cx).is_tree_base_loading() || !self._task.is_ready();
885        let empty_label = self.empty_label.clone();
886
887        div()
888            .track_focus(&self.focus_handle)
889            .key_context(if is_empty { "EmptyPane" } else { "GitDiff" })
890            .bg(cx.theme().colors().editor_background)
891            .flex()
892            .items_center()
893            .justify_center()
894            .size_full()
895            .when(is_empty && is_loading, |el| {
896                let rems = TextSize::Large.rems(cx);
897                el.child(
898                    Icon::new(IconName::LoadCircle)
899                        .size(IconSize::Custom(rems))
900                        .color(Color::Accent)
901                        .with_rotate_animation(3)
902                        .into_any_element(),
903                )
904            })
905            .when(is_empty && !is_loading, |el| {
906                let remote_button = if let Some(panel) = self
907                    .workspace
908                    .upgrade()
909                    .and_then(|workspace| workspace.read(cx).panel::<GitPanel>(cx))
910                {
911                    panel.update(cx, |panel, cx| panel.render_remote_button(cx))
912                } else {
913                    None
914                };
915                let keybinding_focus_handle = self.focus_handle(cx);
916                el.child(
917                    v_flex()
918                        .gap_1()
919                        .child(h_flex().justify_around().child(Label::new(empty_label)))
920                        .map(|el| match remote_button {
921                            Some(button) => el.child(h_flex().justify_around().child(button)),
922                            None => el.child(
923                                h_flex()
924                                    .justify_around()
925                                    .child(Label::new("Remote up to date")),
926                            ),
927                        })
928                        .child(
929                            h_flex().justify_around().mt_1().child(
930                                Button::new("project-diff-close-button", "Close")
931                                    .key_binding(KeyBinding::for_action_in(
932                                        &CloseActiveItem::default(),
933                                        &keybinding_focus_handle,
934                                        cx,
935                                    ))
936                                    .on_click(move |_, window, cx| {
937                                        window.focus(&keybinding_focus_handle, cx);
938                                        window.dispatch_action(
939                                            Box::new(CloseActiveItem::default()),
940                                            cx,
941                                        );
942                                    }),
943                            ),
944                        ),
945                )
946            })
947            .when(!is_empty, |el| el.child(self.editor.clone()))
948    }
949}
950
951const CONFLICT_SORT_PREFIX: u64 = 1;
952const TRACKED_SORT_PREFIX: u64 = 2;
953const NEW_SORT_PREFIX: u64 = 3;
954
955/// Computes a stable [`PathKey`] for a buffer in the project diff.
956///
957/// The key is an intrinsic function of the file's own repo path and status; it
958/// never depends on which other buffers happen to be present in the
959/// multibuffer. This is required because the multibuffer uses the path key both
960/// to order excerpts and to identify which excerpts belong to a given buffer, so
961/// a key that shifted as files were added or removed would break that identity.
962///
963/// Status grouping is encoded in the `sort_prefix`, and the within-group order
964/// is encoded in the (possibly synthetic) path so that `PathKey`'s natural
965/// ordering reproduces the git panel's order. The path here is only ever used
966/// for sorting and multibuffer identity; the path shown in the UI comes from the
967/// buffer's own `File`.
968pub(crate) fn project_diff_path_key(
969    repo: &Repository,
970    repo_path: &RepoPath,
971    status: FileStatus,
972    cx: &App,
973) -> PathKey {
974    let settings = GitPanelSettings::get_global(cx);
975    let sort_prefix = if settings.group_by != GitPanelGroupBy::Status {
976        TRACKED_SORT_PREFIX
977    } else if repo.had_conflict_on_last_merge_head_change(repo_path) {
978        CONFLICT_SORT_PREFIX
979    } else if status.is_created() {
980        NEW_SORT_PREFIX
981    } else {
982        TRACKED_SORT_PREFIX
983    };
984    let path = project_diff_sort_path(repo_path, settings.tree_view, settings.sort_by);
985    PathKey::with_sort_prefix(sort_prefix, path)
986}
987
988fn project_diff_sort_path(
989    repo_path: &RelPath,
990    tree_view: bool,
991    sort_by: GitPanelSortBy,
992) -> Arc<RelPath> {
993    if tree_view {
994        tree_sort_path(repo_path)
995    } else {
996        match sort_by {
997            GitPanelSortBy::Path => repo_path.into_arc(),
998            GitPanelSortBy::Name => name_sort_path(repo_path),
999        }
1000    }
1001}
1002
1003/// Builds a synthetic path that sorts by file name first, falling back to the
1004/// full path to keep the key unique per file.
1005fn name_sort_path(repo_path: &RelPath) -> Arc<RelPath> {
1006    let Some(file_name) = repo_path.file_name() else {
1007        return repo_path.into_arc();
1008    };
1009    let synthetic = format!("{}/{}", file_name, repo_path.as_unix_str());
1010    RelPath::from_unix_str(&synthetic)
1011        .map(|path| path.into_arc())
1012        .unwrap_or_else(|_| repo_path.into_arc())
1013}
1014
1015/// Builds a synthetic path whose natural component-wise ordering reproduces a
1016/// folder-first tree order. Each directory component is prefixed with a NUL
1017/// byte, which can never appear in a real path component and sorts before every
1018/// printable character, so at each level directories sort before files.
1019fn tree_sort_path(repo_path: &RelPath) -> Arc<RelPath> {
1020    let components: Vec<&str> = repo_path.components().collect();
1021    if components.len() <= 1 {
1022        return repo_path.into_arc();
1023    }
1024    let last = components.len() - 1;
1025    let mut synthetic = String::new();
1026    for (index, component) in components.into_iter().enumerate() {
1027        if index > 0 {
1028            synthetic.push('/');
1029        }
1030        if index < last {
1031            synthetic.push('\0');
1032        }
1033        synthetic.push_str(component);
1034    }
1035    RelPath::from_unix_str(&synthetic)
1036        .map(|path| path.into_arc())
1037        .unwrap_or_else(|_| repo_path.into_arc())
1038}
1039
Served at tenant.openagents/omega Member data and write actions are omitted.