Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T02:30:38.242Z 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

staged_diff.rs

1099 lines · 35.3 KB · rust
1use crate::{
2    diff_multibuffer::DiffMultibuffer,
3    git_panel::{GitPanel, GitPanelAddon, GitStatusEntry},
4};
5use anyhow::{Context as _, Result};
6use buffer_diff::DiffHunkStatus;
7use editor::{
8    DiffHunkDelegate, Editor, EditorEvent, ResolvedDiffHunks, SplittableEditor,
9    actions::{GoToHunk, GoToPreviousHunk},
10};
11use git::{Commit, UnstageAll, UnstageAndNext};
12use gpui::{
13    Action, AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable, Render,
14    SharedString, Subscription, Task, WeakEntity,
15};
16use language::Capability;
17use project::{
18    Project, ProjectPath,
19    git_store::diff_buffer_list::{DiffBase, DiffBufferList},
20    project_settings::ProjectSettings,
21};
22use settings::Settings;
23use std::{
24    any::{Any, TypeId},
25    ops::Range,
26    sync::Arc,
27};
28use ui::{DiffStat, Divider, Icon, Tooltip, Window, prelude::*};
29use util::ResultExt as _;
30use workspace::{
31    ItemNavHistory, SerializableItem, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
32    Workspace,
33    item::{Item, ItemEvent, ItemHandle, SaveOptions, TabContentParams},
34    searchable::SearchableItemHandle,
35};
36
37pub(crate) struct StagedDiffDelegate;
38
39impl DiffHunkDelegate for StagedDiffDelegate {
40    fn toggle(
41        &self,
42        hunks: Vec<ResolvedDiffHunks>,
43        editor: &mut Editor,
44        window: &mut Window,
45        cx: &mut Context<Editor>,
46    ) {
47        self.stage_or_unstage(false, hunks, editor, window, cx);
48    }
49
50    fn stage_or_unstage(
51        &self,
52        stage: bool,
53        hunks: Vec<ResolvedDiffHunks>,
54        editor: &mut Editor,
55        _window: &mut Window,
56        cx: &mut Context<Editor>,
57    ) {
58        if stage {
59            return;
60        }
61        let Some(project) = editor.project().cloned() else {
62            return;
63        };
64        for hunks in hunks {
65            let index_ranges = hunks
66                .hunks
67                .into_iter()
68                .map(|hunk| hunk.buffer_range)
69                .collect::<Vec<_>>();
70            if index_ranges.is_empty() {
71                continue;
72            }
73            project
74                .update(cx, |project, cx| {
75                    project.unstage_staged_hunks(hunks.diff, index_ranges, cx)
76                })
77                .log_err();
78        }
79    }
80
81    fn render_hunk_controls(
82        &self,
83        row: u32,
84        status: &DiffHunkStatus,
85        hunk_range: Range<editor::Anchor>,
86        _is_created_file: bool,
87        line_height: Pixels,
88        editor: &Entity<Editor>,
89        _window: &mut Window,
90        cx: &mut App,
91    ) -> AnyElement {
92        if !ProjectSettings::get_global(cx)
93            .git
94            .show_stage_restore_buttons
95        {
96            return gpui::Empty.into_any_element();
97        }
98        let hunk_range = hunk_range.start..hunk_range.start;
99        h_flex()
100            .h(line_height)
101            .mr_1()
102            .gap_1()
103            .px_0p5()
104            .pb_1()
105            .border_x_1()
106            .border_b_1()
107            .border_color(cx.theme().colors().border_variant)
108            .rounded_b_lg()
109            .bg(cx.theme().colors().editor_background)
110            .block_mouse_except_scroll()
111            .shadow_md()
112            .child(
113                Button::new(("unstage", row as u64), "Unstage")
114                    .alpha(if status.is_pending() { 0.66 } else { 1.0 })
115                    .tooltip(Tooltip::text("Unstage Hunk"))
116                    .on_click({
117                        let editor = editor.clone();
118                        move |_event, window, cx| {
119                            editor.update(cx, |editor, cx| {
120                                editor.stage_or_unstage_diff_hunks(
121                                    false,
122                                    vec![hunk_range.clone()],
123                                    window,
124                                    cx,
125                                );
126                            });
127                        }
128                    }),
129            )
130            .into_any_element()
131    }
132}
133
134/// The workspace item for the staged diff. It wraps a single read-only
135/// [`DiffMultibuffer`] over [`DiffBase::Staged`] and delegates the [`Item`]
136/// surface to it.
137pub struct StagedDiff {
138    diff: Entity<DiffMultibuffer>,
139    project: Entity<Project>,
140    workspace: WeakEntity<Workspace>,
141    _diff_event_subscription: Subscription,
142}
143
144impl StagedDiff {
145    pub(crate) fn register(workspace: &mut Workspace, cx: &mut Context<Workspace>) {
146        let _ = workspace;
147        workspace::register_serializable_item::<Self>(cx);
148    }
149
150    pub fn deploy_at(
151        workspace: &mut Workspace,
152        entry: Option<GitStatusEntry>,
153        window: &mut Window,
154        cx: &mut Context<Workspace>,
155    ) {
156        telemetry::event!(
157            "Git Staged Diff Opened",
158            source = if entry.is_some() {
159                "Git Panel"
160            } else {
161                "Action"
162            }
163        );
164        let intended_repo = workspace.project().read(cx).active_repository(cx);
165        let existing = workspace.items_of_type::<Self>(cx).next();
166        let staged_diff = if let Some(existing) = existing {
167            workspace.activate_item(&existing, true, true, window, cx);
168            existing
169        } else {
170            let workspace_handle = cx.entity();
171            let staged_diff =
172                cx.new(|cx| Self::new(workspace.project().clone(), workspace_handle, window, cx));
173            workspace.add_item_to_active_pane(
174                Box::new(staged_diff.clone()),
175                None,
176                true,
177                window,
178                cx,
179            );
180            staged_diff
181        };
182
183        if let Some(intended) = &intended_repo {
184            let needs_switch = staged_diff
185                .read(cx)
186                .diff
187                .read(cx)
188                .repo(cx)
189                .map_or(true, |current| current.entity_id() != intended.entity_id());
190            if needs_switch {
191                staged_diff.update(cx, |staged_diff, cx| {
192                    staged_diff.diff.update(cx, |diff, cx| {
193                        diff.set_repo(Some(intended.clone()), cx);
194                    });
195                });
196            }
197        }
198
199        if let Some(entry) = entry {
200            staged_diff.update(cx, |staged_diff, cx| {
201                staged_diff.move_to_entry(entry, window, cx);
202            });
203        }
204    }
205
206    pub(crate) fn move_to_entry(
207        &mut self,
208        entry: GitStatusEntry,
209        window: &mut Window,
210        cx: &mut Context<Self>,
211    ) {
212        self.diff
213            .update(cx, |diff, cx| diff.move_to_entry(entry, window, cx));
214    }
215
216    pub(crate) fn new(
217        project: Entity<Project>,
218        workspace: Entity<Workspace>,
219        window: &mut Window,
220        cx: &mut Context<Self>,
221    ) -> Self {
222        let branch_diff =
223            cx.new(|cx| DiffBufferList::new(DiffBase::Staged, project.clone(), window, cx));
224        let workspace_handle = workspace.downgrade();
225        let diff = cx.new(|cx| {
226            DiffMultibuffer::new(
227                branch_diff,
228                Capability::ReadOnly,
229                "No staged changes",
230                move |editor, cx| {
231                    editor.set_diff_hunk_delegate(Some(Arc::new(StagedDiffDelegate)), cx);
232                    editor.rhs_editor().update(cx, |rhs_editor, _cx| {
233                        rhs_editor.set_read_only(true);
234                        rhs_editor.register_addon(GitPanelAddon {
235                            workspace: workspace_handle,
236                        });
237                    });
238                },
239                project.clone(),
240                workspace.clone(),
241                window,
242                cx,
243            )
244        });
245        Self::from_diff(diff, project, workspace, cx)
246    }
247
248    pub(crate) fn from_diff(
249        diff: Entity<DiffMultibuffer>,
250        project: Entity<Project>,
251        workspace: Entity<Workspace>,
252        cx: &mut Context<Self>,
253    ) -> Self {
254        let diff_event_subscription = cx.subscribe(&diff, |_, _, event: &EditorEvent, cx| {
255            cx.emit(event.clone())
256        });
257
258        Self {
259            diff,
260            project,
261            workspace: workspace.downgrade(),
262            _diff_event_subscription: diff_event_subscription,
263        }
264    }
265
266    fn button_states(&self, cx: &App) -> ButtonStates {
267        let diff = self.diff.read(cx);
268        let editor = diff.editor().read(cx).rhs_editor().clone();
269        let editor = editor.read(cx);
270        let snapshot = diff.multibuffer().read(cx).snapshot(cx);
271        let prev_next = snapshot.diff_hunks().nth(1).is_some();
272        let (selection, ranges) = diff.selected_ranges(cx);
273        let unstage = editor
274            .diff_hunks_in_ranges(&ranges, &snapshot)
275            .next()
276            .is_some();
277        let mut unstage_all = false;
278        self.workspace
279            .read_with(cx, |workspace, cx| {
280                if let Some(git_panel) = workspace.panel::<GitPanel>(cx) {
281                    unstage_all = git_panel.read(cx).can_unstage_all();
282                }
283            })
284            .ok();
285
286        ButtonStates {
287            unstage,
288            prev_next,
289            selection,
290            unstage_all,
291        }
292    }
293
294    fn unstage_selected_staged_hunks(
295        &mut self,
296        move_to_next: bool,
297        window: &mut Window,
298        cx: &mut Context<Self>,
299    ) {
300        self.diff.update(cx, |diff, cx| {
301            diff.stage_or_unstage_selected_hunks(false, move_to_next, window, cx)
302        });
303    }
304}
305
306struct ButtonStates {
307    unstage: bool,
308    prev_next: bool,
309    selection: bool,
310    unstage_all: bool,
311}
312
313impl EventEmitter<EditorEvent> for StagedDiff {}
314
315impl Focusable for StagedDiff {
316    fn focus_handle(&self, cx: &App) -> FocusHandle {
317        self.diff.read(cx).focus_handle(cx)
318    }
319}
320
321impl Item for StagedDiff {
322    type Event = EditorEvent;
323
324    fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
325        Some(Icon::new(IconName::GitBranch).color(Color::Muted))
326    }
327
328    fn to_item_events(event: &EditorEvent, f: &mut dyn FnMut(ItemEvent)) {
329        Editor::to_item_events(event, f)
330    }
331
332    fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
333        self.diff
334            .update(cx, |diff, cx| diff.deactivated(window, cx));
335    }
336
337    fn navigate(
338        &mut self,
339        data: Arc<dyn Any + Send>,
340        window: &mut Window,
341        cx: &mut Context<Self>,
342    ) -> bool {
343        self.diff
344            .update(cx, |diff, cx| diff.navigate(data, window, cx))
345    }
346
347    fn tab_tooltip_text(&self, _: &App) -> Option<SharedString> {
348        Some("Staged Changes".into())
349    }
350
351    fn tab_content(&self, params: TabContentParams, _window: &Window, _cx: &App) -> AnyElement {
352        Label::new(self.tab_content_text(0, _cx))
353            .color(if params.selected {
354                Color::Default
355            } else {
356                Color::Muted
357            })
358            .into_any_element()
359    }
360
361    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
362        "Staged Changes".into()
363    }
364
365    fn telemetry_event_text(&self) -> Option<&'static str> {
366        Some("Git Staged Diff Opened")
367    }
368
369    fn as_searchable(&self, _: &Entity<Self>, cx: &App) -> Option<Box<dyn SearchableItemHandle>> {
370        Some(Box::new(self.diff.read(cx).editor().clone()))
371    }
372
373    fn for_each_project_item(
374        &self,
375        cx: &App,
376        f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
377    ) {
378        self.diff.read(cx).for_each_project_item(cx, f);
379    }
380
381    fn set_nav_history(
382        &mut self,
383        nav_history: ItemNavHistory,
384        _: &mut Window,
385        cx: &mut Context<Self>,
386    ) {
387        self.diff
388            .update(cx, |diff, cx| diff.set_nav_history(nav_history, cx));
389    }
390
391    fn can_split(&self) -> bool {
392        true
393    }
394
395    fn clone_on_split(
396        &self,
397        _workspace_id: Option<workspace::WorkspaceId>,
398        window: &mut Window,
399        cx: &mut Context<Self>,
400    ) -> Task<Option<Entity<Self>>>
401    where
402        Self: Sized,
403    {
404        let Some(workspace) = self.workspace.upgrade() else {
405            return Task::ready(None);
406        };
407        let project = self.project.clone();
408        Task::ready(Some(cx.new(|cx| Self::new(project, workspace, window, cx))))
409    }
410
411    fn is_dirty(&self, cx: &App) -> bool {
412        self.diff.read(cx).is_dirty(cx)
413    }
414
415    fn has_conflict(&self, cx: &App) -> bool {
416        self.diff.read(cx).has_conflict(cx)
417    }
418
419    fn can_save(&self, _: &App) -> bool {
420        false
421    }
422
423    fn save(
424        &mut self,
425        _: SaveOptions,
426        _: Entity<Project>,
427        _: &mut Window,
428        _: &mut Context<Self>,
429    ) -> Task<Result<()>> {
430        Task::ready(Ok(()))
431    }
432
433    fn save_as(
434        &mut self,
435        _: Entity<Project>,
436        _: ProjectPath,
437        _: &mut Window,
438        _: &mut Context<Self>,
439    ) -> Task<Result<()>> {
440        unreachable!()
441    }
442
443    fn reload(
444        &mut self,
445        project: Entity<Project>,
446        window: &mut Window,
447        cx: &mut Context<Self>,
448    ) -> Task<Result<()>> {
449        self.diff
450            .update(cx, |diff, cx| diff.reload(project, window, cx))
451    }
452
453    fn act_as_type<'a>(
454        &'a self,
455        type_id: TypeId,
456        self_handle: &'a Entity<Self>,
457        cx: &'a App,
458    ) -> Option<gpui::AnyEntity> {
459        if type_id == TypeId::of::<Self>() {
460            Some(self_handle.clone().into())
461        } else if type_id == TypeId::of::<DiffMultibuffer>() {
462            Some(self.diff.clone().into())
463        } else if type_id == TypeId::of::<Editor>() {
464            Some(
465                self.diff
466                    .read(cx)
467                    .editor()
468                    .read(cx)
469                    .rhs_editor()
470                    .clone()
471                    .into(),
472            )
473        } else if type_id == TypeId::of::<SplittableEditor>() {
474            Some(self.diff.read(cx).editor().clone().into())
475        } else if type_id == TypeId::of::<DiffBufferList>() {
476            Some(self.diff.read(cx).branch_diff().clone().into())
477        } else {
478            None
479        }
480    }
481
482    fn added_to_workspace(
483        &mut self,
484        workspace: &mut Workspace,
485        window: &mut Window,
486        cx: &mut Context<Self>,
487    ) {
488        self.diff.update(cx, |diff, cx| {
489            diff.added_to_workspace(workspace, window, cx)
490        });
491    }
492}
493
494impl SerializableItem for StagedDiff {
495    fn serialized_item_kind() -> &'static str {
496        "StagedDiff"
497    }
498
499    fn cleanup(
500        _: workspace::WorkspaceId,
501        _: Vec<workspace::ItemId>,
502        _: &mut Window,
503        _: &mut App,
504    ) -> Task<Result<()>> {
505        Task::ready(Ok(()))
506    }
507
508    fn deserialize(
509        project: Entity<Project>,
510        workspace: WeakEntity<Workspace>,
511        _: workspace::WorkspaceId,
512        _: workspace::ItemId,
513        window: &mut Window,
514        cx: &mut App,
515    ) -> Task<Result<Entity<Self>>> {
516        window.spawn(cx, async move |cx| {
517            let workspace = workspace.upgrade().context("workspace gone")?;
518            cx.update(|window, cx| Ok(cx.new(|cx| Self::new(project, workspace, window, cx))))?
519        })
520    }
521
522    fn serialize(
523        &mut self,
524        _: &mut Workspace,
525        _: workspace::ItemId,
526        _: bool,
527        _: &mut Window,
528        _: &mut Context<Self>,
529    ) -> Option<Task<Result<()>>> {
530        Some(Task::ready(Ok(())))
531    }
532
533    fn should_serialize(&self, _: &Self::Event) -> bool {
534        false
535    }
536}
537
538impl Render for StagedDiff {
539    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
540        self.diff.clone()
541    }
542}
543
544pub struct StagedDiffToolbar {
545    staged_diff: Option<WeakEntity<StagedDiff>>,
546    workspace: WeakEntity<Workspace>,
547}
548
549impl StagedDiffToolbar {
550    pub fn new(workspace: &Workspace, _: &mut Context<Self>) -> Self {
551        Self {
552            staged_diff: None,
553            workspace: workspace.weak_handle(),
554        }
555    }
556
557    fn staged_diff(&self, _: &App) -> Option<Entity<StagedDiff>> {
558        self.staged_diff.as_ref()?.upgrade()
559    }
560
561    fn dispatch_action(&self, action: &dyn Action, window: &mut Window, cx: &mut Context<Self>) {
562        if let Some(staged_diff) = self.staged_diff(cx) {
563            staged_diff.focus_handle(cx).focus(window, cx);
564        }
565        let action = action.boxed_clone();
566        cx.defer(move |cx| {
567            cx.dispatch_action(action.as_ref());
568        })
569    }
570
571    fn unstage_selected_staged_hunks(
572        &mut self,
573        move_to_next: bool,
574        window: &mut Window,
575        cx: &mut Context<Self>,
576    ) {
577        let Some(staged_diff) = self.staged_diff(cx) else {
578            return;
579        };
580        staged_diff.update(cx, |staged_diff, cx| {
581            staged_diff.unstage_selected_staged_hunks(move_to_next, window, cx);
582        });
583    }
584
585    fn unstage_all(&mut self, window: &mut Window, cx: &mut Context<Self>) {
586        self.workspace
587            .update(cx, |workspace, cx| {
588                let Some(panel) = workspace.panel::<GitPanel>(cx) else {
589                    return;
590                };
591                panel.update(cx, |panel, cx| {
592                    panel.unstage_all(&Default::default(), window, cx);
593                });
594            })
595            .ok();
596    }
597}
598
599impl EventEmitter<ToolbarItemEvent> for StagedDiffToolbar {}
600
601impl ToolbarItemView for StagedDiffToolbar {
602    fn set_active_pane_item(
603        &mut self,
604        active_pane_item: Option<&dyn ItemHandle>,
605        _: &mut Window,
606        cx: &mut Context<Self>,
607    ) -> ToolbarItemLocation {
608        self.staged_diff = active_pane_item
609            .and_then(|item| item.act_as::<StagedDiff>(cx))
610            .map(|entity| entity.downgrade());
611        if self.staged_diff.is_some() {
612            ToolbarItemLocation::PrimaryRight
613        } else {
614            ToolbarItemLocation::Hidden
615        }
616    }
617
618    fn pane_focus_update(
619        &mut self,
620        _pane_focused: bool,
621        _window: &mut Window,
622        _cx: &mut Context<Self>,
623    ) {
624    }
625}
626
627impl Render for StagedDiffToolbar {
628    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
629        let Some(staged_diff) = self.staged_diff(cx) else {
630            return div();
631        };
632        let focus_handle = staged_diff.focus_handle(cx);
633        let button_states = staged_diff.read(cx).button_states(cx);
634
635        let diff = staged_diff.read(cx).diff.read(cx);
636        let (additions, deletions) = diff.calculate_changed_lines(cx);
637        let is_multibuffer_empty = diff.multibuffer().read(cx).is_empty();
638
639        h_flex()
640            .my_neg_1()
641            .py_1()
642            .gap_1p5()
643            .flex_wrap()
644            .justify_between()
645            .when(!is_multibuffer_empty, |this| {
646                this.child(DiffStat::new(
647                    "staged-diff-stat",
648                    additions as usize,
649                    deletions as usize,
650                ))
651                .child(Divider::vertical().ml_1())
652            })
653            // n.b. the only reason these arrows are here is because we don't
654            // support "undo" for staging so we need a way to go back.
655            .child(
656                h_group_sm()
657                    .child(
658                        IconButton::new("up", IconName::ArrowUp)
659                            .icon_size(IconSize::Small)
660                            .disabled(!button_states.prev_next)
661                            .tooltip(Tooltip::for_action_title_in(
662                                "Go to Previous Hunk",
663                                &GoToPreviousHunk,
664                                &focus_handle,
665                            ))
666                            .on_click(cx.listener(|this, _, window, cx| {
667                                this.dispatch_action(&GoToPreviousHunk, window, cx)
668                            })),
669                    )
670                    .child(
671                        IconButton::new("down", IconName::ArrowDown)
672                            .icon_size(IconSize::Small)
673                            .disabled(!button_states.prev_next)
674                            .tooltip(Tooltip::for_action_title_in(
675                                "Go to Next Hunk",
676                                &GoToHunk,
677                                &focus_handle,
678                            ))
679                            .on_click(cx.listener(|this, _, window, cx| {
680                                this.dispatch_action(&GoToHunk, window, cx)
681                            })),
682                    ),
683            )
684            .child(Divider::vertical())
685            .child(
686                h_group_sm()
687                    .when(button_states.selection, |this| {
688                        this.child(
689                            Button::new("unstage", "Unstage")
690                                .disabled(!button_states.unstage)
691                                .tooltip(Tooltip::text("Unstage Selected Hunks"))
692                                .on_click(cx.listener(|this, _, window, cx| {
693                                    this.unstage_selected_staged_hunks(false, window, cx)
694                                })),
695                        )
696                    })
697                    .when(!button_states.selection, |this| {
698                        this.child(
699                            Button::new("unstage", "Unstage")
700                                .disabled(!button_states.unstage)
701                                .tooltip(Tooltip::for_action_title_in(
702                                    "Unstage and Go to Next Hunk",
703                                    &UnstageAndNext,
704                                    &focus_handle,
705                                ))
706                                .on_click(cx.listener(|this, _, window, cx| {
707                                    this.unstage_selected_staged_hunks(true, window, cx)
708                                })),
709                        )
710                    }),
711            )
712            .child(Divider::vertical())
713            .child(
714                Button::new("unstage-all", "Unstage All")
715                    .width(rems_from_px(80.))
716                    .disabled(!button_states.unstage_all)
717                    .tooltip(Tooltip::for_action_title_in(
718                        "Unstage All Changes",
719                        &UnstageAll,
720                        &focus_handle,
721                    ))
722                    .on_click(cx.listener(|this, _, window, cx| this.unstage_all(window, cx))),
723            )
724            .child(Divider::vertical())
725            .child(
726                Button::new("commit", "Commit")
727                    .tooltip(Tooltip::for_action_title_in(
728                        "Commit",
729                        &Commit,
730                        &focus_handle,
731                    ))
732                    .on_click(cx.listener(|this, _, window, cx| {
733                        this.dispatch_action(&Commit, window, cx);
734                    })),
735            )
736    }
737}
738
739#[cfg(test)]
740mod tests {
741    use crate::project_diff::{self, ProjectDiff};
742    use git::repository::RepoPath;
743    use gpui::{Action as _, TestAppContext};
744    use language::Point;
745    use project::{FakeFs, Fs as _};
746    use serde_json::json;
747    use settings::{DiffViewStyle, SettingsStore};
748    use std::path::Path;
749    use unindent::Unindent as _;
750    use util::{path, rel_path::rel_path};
751    use workspace::MultiWorkspace;
752
753    use super::*;
754
755    fn init_test(cx: &mut TestAppContext) {
756        cx.update(|cx| {
757            let store = SettingsStore::test(cx);
758            cx.set_global(store);
759            cx.update_global::<SettingsStore, _>(|store, cx| {
760                store.update_user_settings(cx, |settings| {
761                    settings.editor.diff_view_style = Some(DiffViewStyle::Unified);
762                });
763            });
764            theme_settings::init(theme::LoadThemes::JustBase, cx);
765            editor::init(cx);
766            crate::init(cx);
767        });
768    }
769
770    #[gpui::test]
771    async fn test_staged_changes_deploy_as_a_separate_staged_diff_item(cx: &mut TestAppContext) {
772        init_test(cx);
773
774        let committed_contents = r#"
775            fn main() {
776                println!("hello world");
777            }
778        "#
779        .unindent();
780        let staged_contents = r#"
781            fn main() {
782                println!("goodbye world");
783            }
784        "#
785        .unindent();
786        let file_contents = r#"
787            // print goodbye
788            fn main() {
789                println!("goodbye world");
790            }
791        "#
792        .unindent();
793
794        let fs = FakeFs::new(cx.executor());
795        fs.insert_tree(
796            path!("/project"),
797            json!({
798                ".git": {},
799                "src": {
800                    "main.rs": file_contents.clone(),
801                }
802            }),
803        )
804        .await;
805
806        fs.set_head_for_repo(
807            Path::new(path!("/project/.git")),
808            &[("src/main.rs", committed_contents)],
809            "deadbeef",
810        );
811        fs.set_index_for_repo(
812            Path::new(path!("/project/.git")),
813            &[("src/main.rs", staged_contents.clone())],
814        );
815
816        let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
817        let (multi_workspace, cx) =
818            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
819        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
820        cx.run_until_parked();
821
822        cx.focus(&workspace);
823        cx.update(|window, cx| {
824            window.dispatch_action(project_diff::Diff.boxed_clone(), cx);
825        });
826        cx.run_until_parked();
827
828        let uncommitted_item = workspace.update(cx, |workspace, cx| {
829            workspace.active_item_as::<ProjectDiff>(cx).unwrap()
830        });
831
832        workspace.update_in(cx, |workspace, window, cx| {
833            StagedDiff::deploy_at(workspace, None, window, cx);
834        });
835        cx.run_until_parked();
836
837        workspace.update(cx, |workspace, cx| {
838            let staged_diff = workspace.active_item_as::<StagedDiff>(cx).unwrap();
839            assert_ne!(staged_diff.entity_id(), uncommitted_item.entity_id());
840            let staged_item = workspace
841                .active_item(cx)
842                .unwrap()
843                .act_as::<DiffMultibuffer>(cx)
844                .unwrap();
845            assert_ne!(staged_item.entity_id(), uncommitted_item.entity_id());
846            assert_eq!(
847                staged_item.read_with(cx, |diff, cx| diff.diff_base(cx).clone()),
848                DiffBase::Staged
849            );
850            assert!(staged_item.read_with(cx, |diff, cx| diff.multibuffer().read(cx).read_only()));
851            assert_eq!(workspace.items_of_type::<ProjectDiff>(cx).count(), 1);
852            assert_eq!(workspace.items_of_type::<StagedDiff>(cx).count(), 1);
853
854            let active_item = workspace.active_item(cx).unwrap();
855            assert!(active_item.act_as::<StagedDiff>(cx).is_some());
856            assert!(active_item.act_as::<DiffMultibuffer>(cx).is_some());
857            assert_eq!(
858                active_item
859                    .to_serializable_item_handle(cx)
860                    .unwrap()
861                    .serialized_item_kind(),
862                "StagedDiff"
863            );
864            assert_eq!(active_item.tab_content_text(0, cx), "Staged Changes");
865            assert!(!active_item.can_save(cx));
866        });
867    }
868
869    #[gpui::test]
870    async fn test_toggle_staged_unstages_from_staged_view(cx: &mut TestAppContext) {
871        init_test(cx);
872
873        let committed_contents = r#"
874            fn main() {
875                println!("hello world");
876            }
877        "#
878        .unindent();
879        let staged_contents = r#"
880            fn main() {
881                println!("goodbye world");
882            }
883        "#
884        .unindent();
885        let file_contents = r#"
886            // print goodbye
887            fn main() {
888                println!("goodbye world");
889            }
890        "#
891        .unindent();
892
893        let fs = FakeFs::new(cx.executor());
894        fs.insert_tree(
895            path!("/project"),
896            json!({
897                ".git": {},
898                "src": {
899                    "main.rs": file_contents,
900                }
901            }),
902        )
903        .await;
904        fs.set_head_for_repo(
905            Path::new(path!("/project/.git")),
906            &[("src/main.rs", committed_contents.clone())],
907            "deadbeef",
908        );
909        fs.set_index_for_repo(
910            Path::new(path!("/project/.git")),
911            &[("src/main.rs", staged_contents)],
912        );
913        let repo = fs
914            .open_repo(path!("/project/.git").as_ref(), Some("git".as_ref()))
915            .unwrap();
916
917        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
918        let (multi_workspace, cx) =
919            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
920        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
921        cx.run_until_parked();
922
923        workspace.update_in(cx, |workspace, window, cx| {
924            StagedDiff::deploy_at(workspace, None, window, cx);
925        });
926        cx.run_until_parked();
927
928        let editor = workspace.update(cx, |workspace, cx| {
929            let staged_diff = workspace.active_item_as::<StagedDiff>(cx).unwrap();
930            let staged_diff = staged_diff.read(cx);
931            staged_diff
932                .diff
933                .read(cx)
934                .editor()
935                .read(cx)
936                .rhs_editor()
937                .clone()
938        });
939        editor.read_with(cx, |editor, cx| {
940            let snapshot = editor.buffer().read(cx).snapshot(cx);
941            assert_eq!(
942                editor
943                    .diff_hunks_in_ranges(&[editor::Anchor::Min..editor::Anchor::Max], &snapshot)
944                    .count(),
945                1
946            );
947        });
948
949        // Hold back FS events so the first assertions observe the optimistic
950        // state rather than a reloaded diff.
951        fs.pause_events();
952
953        editor.update_in(cx, |editor, window, cx| {
954            editor.change_selections(Default::default(), window, cx, |s| {
955                s.select_ranges([Point::new(1, 0)..Point::new(1, 0)]);
956            });
957        });
958        cx.focus(&editor);
959        cx.update(|window, cx| {
960            window.dispatch_action(git::ToggleStaged.boxed_clone(), cx);
961        });
962        cx.run_until_parked();
963
964        // The hunk is optimistically suppressed from the staged view, and the
965        // index write has landed.
966        editor.read_with(cx, |editor, cx| {
967            let snapshot = editor.buffer().read(cx).snapshot(cx);
968            assert_eq!(
969                editor
970                    .diff_hunks_in_ranges(&[editor::Anchor::Min..editor::Anchor::Max], &snapshot)
971                    .count(),
972                0
973            );
974        });
975        assert_eq!(
976            repo.load_index_text(RepoPath::from_rel_path(rel_path("src/main.rs")))
977                .await
978                .unwrap(),
979            committed_contents
980        );
981
982        fs.unpause_events_and_flush();
983        cx.run_until_parked();
984
985        // Once the write is reconciled, the staged view remains empty.
986        editor.read_with(cx, |editor, cx| {
987            let snapshot = editor.buffer().read(cx).snapshot(cx);
988            assert_eq!(
989                editor
990                    .diff_hunks_in_ranges(&[editor::Anchor::Min..editor::Anchor::Max], &snapshot)
991                    .count(),
992                0
993            );
994        });
995    }
996
997    #[gpui::test]
998    async fn test_staged_diff_restores_as_staged_diff(cx: &mut TestAppContext) {
999        init_test(cx);
1000
1001        let committed_contents = r#"
1002            fn main() {
1003                println!("hello world");
1004            }
1005        "#
1006        .unindent();
1007        let staged_contents = r#"
1008            fn main() {
1009                println!("goodbye world");
1010            }
1011        "#
1012        .unindent();
1013        let file_contents = r#"
1014            // print goodbye
1015            fn main() {
1016                println!("goodbye world");
1017            }
1018        "#
1019        .unindent();
1020
1021        let fs = FakeFs::new(cx.executor());
1022        fs.insert_tree(
1023            path!("/project"),
1024            json!({
1025                ".git": {},
1026                "src": {
1027                    "main.rs": file_contents,
1028                }
1029            }),
1030        )
1031        .await;
1032
1033        fs.set_head_for_repo(
1034            Path::new(path!("/project/.git")),
1035            &[("src/main.rs", committed_contents)],
1036            "deadbeef",
1037        );
1038        fs.set_index_for_repo(
1039            Path::new(path!("/project/.git")),
1040            &[("src/main.rs", staged_contents)],
1041        );
1042
1043        let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1044        let (multi_workspace, cx) =
1045            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
1046        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
1047        cx.run_until_parked();
1048
1049        let project = workspace.update(cx, |workspace, _| workspace.project().clone());
1050        let workspace_id = workspace::WorkspaceId::from_i64(1);
1051        let item_id = 42;
1052
1053        let restore_task = workspace.update_in(cx, |_workspace, window, cx| {
1054            <StagedDiff as SerializableItem>::deserialize(
1055                project.clone(),
1056                cx.entity().downgrade(),
1057                workspace_id,
1058                item_id,
1059                window,
1060                cx,
1061            )
1062        });
1063        let restored_staged_diff = restore_task.await.unwrap();
1064
1065        workspace.update_in(cx, |workspace, window, cx| {
1066            workspace.add_item_to_active_pane(
1067                Box::new(restored_staged_diff.clone()),
1068                None,
1069                true,
1070                window,
1071                cx,
1072            );
1073        });
1074        cx.run_until_parked();
1075
1076        workspace.update(cx, |workspace, cx| {
1077            let active_item = workspace.active_item(cx).unwrap();
1078            assert!(active_item.act_as::<StagedDiff>(cx).is_some());
1079            assert!(active_item.act_as::<DiffMultibuffer>(cx).is_some());
1080            assert_eq!(
1081                active_item
1082                    .to_serializable_item_handle(cx)
1083                    .unwrap()
1084                    .serialized_item_kind(),
1085                "StagedDiff"
1086            );
1087            assert_eq!(active_item.tab_content_text(0, cx), "Staged Changes");
1088            assert!(!active_item.can_save(cx));
1089            assert_eq!(workspace.items_of_type::<ProjectDiff>(cx).count(), 0);
1090            assert_eq!(workspace.items_of_type::<StagedDiff>(cx).count(), 1);
1091            let diff = active_item.act_as::<DiffMultibuffer>(cx).unwrap();
1092            assert_eq!(
1093                diff.read_with(cx, |diff, cx| diff.diff_base(cx).clone()),
1094                DiffBase::Staged
1095            );
1096        });
1097    }
1098}
1099
Served at tenant.openagents/omega Member data and write actions are omitted.