Skip to repository content1099 lines · 35.3 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T02:30:38.242Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
staged_diff.rs
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