diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index c6ff3fe495..e52aba0108 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -69,7 +69,7 @@ pub struct GitListEntry { display_name: String, repo_path: RepoPath, status: GitStatusPair, - toggle_state: ToggleState, + is_staged: Option, } pub struct GitPanel { @@ -91,18 +91,11 @@ pub struct GitPanel { /// At this point it doesn't matter what repository the entry belongs to, /// as only one repositories' entries are visible in the list at a time. visible_entries: Vec, + all_staged: Option, width: Option, reveal_in_editor: Task<()>, } -fn status_to_toggle_state(status: &GitStatusPair) -> ToggleState { - match status.is_staged() { - Some(true) => ToggleState::Selected, - Some(false) => ToggleState::Unselected, - None => ToggleState::Indeterminate, - } -} - impl GitPanel { pub fn load( workspace: WeakView, @@ -314,6 +307,7 @@ impl GitPanel { fs, pending_serialization: Task::ready(None), visible_entries: Vec::new(), + all_staged: None, current_modifiers: cx.modifiers(), width: Some(px(360.)), scrollbar_state: ScrollbarState::new(scroll_handle.clone()).parent_view(cx.view()), @@ -602,10 +596,26 @@ impl GitPanel { } fn stage_all(&mut self, _: &StageAll, cx: &mut ViewContext) { - self.git_state.update(cx, |state, _| state.stage_all()); + let to_stage = self + .visible_entries + .iter_mut() + .filter_map(|entry| { + let is_unstaged = !entry.is_staged.unwrap_or(false); + entry.is_staged = Some(true); + is_unstaged.then(|| entry.repo_path.clone()) + }) + .collect(); + self.all_staged = Some(true); + self.git_state + .update(cx, |state, _| state.stage_entries(to_stage)); } fn unstage_all(&mut self, _: &UnstageAll, cx: &mut ViewContext) { + // This should only be called when all entries are staged. + for entry in &mut self.visible_entries { + entry.is_staged = Some(false); + } + self.all_staged = Some(false); self.git_state.update(cx, |state, _| { state.unstage_all(); }); @@ -639,11 +649,6 @@ impl GitPanel { println!("Commit all changes triggered"); } - fn all_staged(&self) -> bool { - // TODO: Implement all_staged - true - } - fn no_entries(&self) -> bool { self.visible_entries.is_empty() } @@ -678,7 +683,7 @@ impl GitPanel { status, depth: 0, display_name: filename, - toggle_state: entry.toggle_state, + is_staged: entry.is_staged, }; callback(ix, details, cx); @@ -705,10 +710,19 @@ impl GitPanel { let path_set = HashSet::from_iter(repo.status().map(|entry| entry.repo_path)); // Second pass - create entries with proper depth calculation - for entry in repo.status() { + let mut all_staged = None; + for (ix, entry) in repo.status().enumerate() { let (depth, difference) = Self::calculate_depth_and_difference(&entry.repo_path, &path_set); - let toggle_state = status_to_toggle_state(&entry.status); + let is_staged = entry.status.is_staged(); + all_staged = if ix == 0 { + is_staged + } else { + match (all_staged, is_staged) { + (None, _) | (_, None) => None, + (Some(a), Some(b)) => (a == b).then_some(a), + } + }; let display_name = if difference > 1 { // Show partial path for deeply nested files @@ -734,11 +748,12 @@ impl GitPanel { display_name, repo_path: entry.repo_path, status: entry.status, - toggle_state, + is_staged, }; self.visible_entries.push(entry); } + self.all_staged = all_staged; // Sort entries by path to maintain consistent order self.visible_entries @@ -805,7 +820,11 @@ impl GitPanel { .child( h_flex() .gap_2() - .child(Checkbox::new("all-changes", true.into()).disabled(true)) + .child(Checkbox::new( + "all-changes", + self.all_staged + .map_or(ToggleState::Indeterminate, ToggleState::from), + )) .child(div().text_buffer(cx).text_ui_sm(cx).child(changes_string)), ) .child(div().flex_grow()) @@ -814,27 +833,50 @@ impl GitPanel { .gap_2() .child( IconButton::new("discard-changes", IconName::Undo) - .tooltip(move |cx| { + .tooltip({ let focus_handle = focus_handle.clone(); - - Tooltip::for_action_in( - "Discard all changes", - &RevertAll, - &focus_handle, - cx, - ) + move |cx| { + Tooltip::for_action_in( + "Discard all changes", + &RevertAll, + &focus_handle, + cx, + ) + } }) .icon_size(IconSize::Small) .disabled(true), ) - .child(if self.all_staged() { - self.panel_button("unstage-all", "Unstage All").on_click( - cx.listener(move |_, _, cx| cx.dispatch_action(Box::new(RevertAll))), - ) + .child(if self.all_staged.unwrap_or(false) { + self.panel_button("unstage-all", "Unstage All") + .tooltip({ + let focus_handle = focus_handle.clone(); + move |cx| { + Tooltip::for_action_in( + "Unstage all changes", + &UnstageAll, + &focus_handle, + cx, + ) + } + }) + .on_click( + cx.listener(move |this, _, cx| this.unstage_all(&UnstageAll, cx)), + ) } else { - self.panel_button("stage-all", "Stage All").on_click( - cx.listener(move |_, _, cx| cx.dispatch_action(Box::new(StageAll))), - ) + self.panel_button("stage-all", "Stage All") + .tooltip({ + let focus_handle = focus_handle.clone(); + move |cx| { + Tooltip::for_action_in( + "Stage all changes", + &StageAll, + &focus_handle, + cx, + ) + } + }) + .on_click(cx.listener(move |this, _, cx| this.stage_all(&StageAll, cx))) }), ) } @@ -1049,30 +1091,39 @@ impl GitPanel { entry = entry .child( - Checkbox::new(checkbox_id, entry_details.toggle_state) - .fill() - .elevation(ElevationIndex::Surface) - .on_click({ - let handle = handle.clone(); - let repo_path = repo_path.clone(); - move |toggle, cx| { - let Some(this) = handle.upgrade() else { - return; - }; - this.update(cx, |this, _| { - this.visible_entries[ix].toggle_state = *toggle; - }); - state.update(cx, { - let repo_path = repo_path.clone(); - move |state, _| match toggle { - ToggleState::Selected | ToggleState::Indeterminate => { - state.stage_entry(repo_path); - } - ToggleState::Unselected => state.unstage_entry(repo_path), + Checkbox::new( + checkbox_id, + entry_details + .is_staged + .map_or(ToggleState::Indeterminate, ToggleState::from), + ) + .fill() + .elevation(ElevationIndex::Surface) + .on_click({ + let handle = handle.clone(); + let repo_path = repo_path.clone(); + move |toggle, cx| { + let Some(this) = handle.upgrade() else { + return; + }; + this.update(cx, |this, _| { + this.visible_entries[ix].is_staged = match *toggle { + ToggleState::Selected => Some(true), + ToggleState::Unselected => Some(false), + ToggleState::Indeterminate => None, + } + }); + state.update(cx, { + let repo_path = repo_path.clone(); + move |state, _| match toggle { + ToggleState::Selected | ToggleState::Indeterminate => { + state.stage_entry(repo_path); } - }); - } - }), + ToggleState::Unselected => state.unstage_entry(repo_path), + } + }); + } + }), ) .child(git_status_icon(status)) .child(