diff --git a/assets/icons/circle.svg b/assets/icons/circle.svg
new file mode 100644
index 0000000000..67306cb12a
--- /dev/null
+++ b/assets/icons/circle.svg
@@ -0,0 +1 @@
+
diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs
index 49b12c0a3f..19028168dd 100644
--- a/crates/git_ui/src/git_panel.rs
+++ b/crates/git_ui/src/git_panel.rs
@@ -75,15 +75,15 @@ struct SerializedGitPanel {
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
enum Section {
- Changed,
- Created,
+ Tracked,
+ New,
}
impl Section {
pub fn contains(&self, status: FileStatus) -> bool {
match self {
- Section::Changed => !status.is_created(),
- Section::Created => status.is_created(),
+ Section::Tracked => !status.is_created(),
+ Section::New => status.is_created(),
}
}
}
@@ -99,8 +99,8 @@ impl GitHeaderEntry {
}
pub fn title(&self) -> &'static str {
match self.header {
- Section::Changed => "Changed",
- Section::Created => "New",
+ Section::Tracked => "Changed",
+ Section::New => "New",
}
}
}
@@ -112,9 +112,9 @@ enum GitListEntry {
}
impl GitListEntry {
- fn status_entry(&self) -> Option {
+ fn status_entry(&self) -> Option<&GitStatusEntry> {
match self {
- GitListEntry::GitStatusEntry(entry) => Some(entry.clone()),
+ GitListEntry::GitStatusEntry(entry) => Some(entry),
_ => None,
}
}
@@ -129,7 +129,7 @@ pub struct GitStatusEntry {
pub(crate) is_staged: Option,
}
-pub struct PendingOperation {
+struct PendingOperation {
finished: bool,
will_become_staged: bool,
repo_paths: HashSet,
@@ -158,8 +158,11 @@ pub struct GitPanel {
pending: Vec,
commit_task: Task>,
commit_pending: bool,
- can_commit: bool,
- can_commit_all: bool,
+
+ tracked_staged_count: usize,
+ tracked_count: usize,
+ new_staged_count: usize,
+ new_count: usize,
}
fn commit_message_editor(
@@ -272,8 +275,10 @@ impl GitPanel {
commit_editor,
project,
workspace,
- can_commit: false,
- can_commit_all: false,
+ tracked_staged_count: 0,
+ tracked_count: 0,
+ new_staged_count: 0,
+ new_count: 0,
};
git_panel.schedule_update(false, window, cx);
git_panel.show_scrollbar = git_panel.should_show_scrollbar(cx);
@@ -579,7 +584,7 @@ impl GitPanel {
section.contains(&status_entry)
&& status_entry.is_staged != Some(goal_staged_state)
})
- .map(|status_entry| status_entry.repo_path)
+ .map(|status_entry| status_entry.repo_path.clone())
.collect::>();
(goal_staged_state, entries)
@@ -593,10 +598,12 @@ impl GitPanel {
repo_paths: repo_paths.iter().cloned().collect(),
finished: false,
});
+ let repo_paths = repo_paths.clone();
+ let active_repository = active_repository.clone();
+ self.update_counts();
+ cx.notify();
cx.spawn({
- let repo_paths = repo_paths.clone();
- let active_repository = active_repository.clone();
|this, mut cx| async move {
let result = cx
.update(|cx| {
@@ -673,7 +680,8 @@ impl GitPanel {
let Some(active_repository) = self.active_repository.clone() else {
return;
};
- if !self.can_commit {
+ if !self.has_staged_changes() {
+ self.commit_tracked_changes(&Default::default(), name_and_email, window, cx);
return;
}
let message = self.commit_editor.read(cx).text(cx);
@@ -716,7 +724,7 @@ impl GitPanel {
let Some(active_repository) = self.active_repository.clone() else {
return;
};
- if !self.can_commit_all {
+ if !self.has_staged_changes() || !self.has_tracked_changes() {
return;
}
@@ -731,10 +739,10 @@ impl GitPanel {
.iter()
.filter_map(|entry| entry.status_entry())
.filter(|status_entry| {
- Section::Changed.contains(status_entry.status)
+ Section::Tracked.contains(status_entry.status)
&& !status_entry.is_staged.unwrap_or(false)
})
- .map(|status_entry| status_entry.repo_path)
+ .map(|status_entry| status_entry.repo_path.clone())
.collect::>();
self.commit_task = cx.spawn_in(window, |git_panel, mut cx| async move {
@@ -911,10 +919,6 @@ impl GitPanel {
let repo = repo.read(cx);
let path_set = HashSet::from_iter(repo.status().map(|entry| entry.repo_path));
- let mut has_changed_checked_boxes = false;
- let mut has_changed = false;
- let mut has_added_checked_boxes = false;
-
// Second pass - create entries with proper depth calculation
for entry in repo.status() {
let (depth, difference) =
@@ -951,15 +955,8 @@ impl GitPanel {
};
if is_new {
- if entry.is_staged != Some(false) {
- has_added_checked_boxes = true
- }
new_entries.push(entry);
} else {
- has_changed = true;
- if entry.is_staged != Some(false) {
- has_changed_checked_boxes = true
- }
changed_entries.push(entry);
}
}
@@ -970,7 +967,7 @@ impl GitPanel {
if changed_entries.len() > 0 {
self.entries.push(GitListEntry::Header(GitHeaderEntry {
- header: Section::Changed,
+ header: Section::Tracked,
}));
self.entries.extend(
changed_entries
@@ -980,7 +977,7 @@ impl GitPanel {
}
if new_entries.len() > 0 {
self.entries.push(GitListEntry::Header(GitHeaderEntry {
- header: Section::Created,
+ header: Section::New,
}));
self.entries
.extend(new_entries.into_iter().map(GitListEntry::GitStatusEntry));
@@ -988,39 +985,62 @@ impl GitPanel {
for (ix, entry) in self.entries.iter().enumerate() {
if let Some(status_entry) = entry.status_entry() {
- self.entries_by_path.insert(status_entry.repo_path, ix);
+ self.entries_by_path
+ .insert(status_entry.repo_path.clone(), ix);
}
}
- self.can_commit = has_changed_checked_boxes || has_added_checked_boxes;
- self.can_commit_all = has_changed || has_added_checked_boxes;
+ self.update_counts();
self.select_first_entry_if_none(cx);
cx.notify();
}
- fn header_state(&self, header_type: Section) -> ToggleState {
- let mut count = 0;
- let mut staged_count = 0;
- 'outer: for entry in &self.entries {
- let Some(entry) = entry.status_entry() else {
+ fn update_counts(&mut self) {
+ self.new_count = 0;
+ self.tracked_count = 0;
+ self.new_staged_count = 0;
+ self.tracked_staged_count = 0;
+ for entry in &self.entries {
+ let Some(status_entry) = entry.status_entry() else {
continue;
};
- if entry.status.is_created() != (header_type == Section::Created) {
- continue;
- }
- count += 1;
- for pending in self.pending.iter().rev() {
- if pending.repo_paths.contains(&entry.repo_path) {
- if pending.will_become_staged {
- staged_count += 1;
- }
- continue 'outer;
+ if status_entry.status.is_created() {
+ self.new_count += 1;
+ if self.entry_appears_staged(status_entry) != Some(false) {
+ self.new_staged_count += 1;
+ }
+ } else {
+ self.tracked_count += 1;
+ if self.entry_appears_staged(status_entry) != Some(false) {
+ self.tracked_staged_count += 1;
}
}
- staged_count += entry.status.is_staged().unwrap_or(false) as usize;
}
+ }
+ fn entry_appears_staged(&self, entry: &GitStatusEntry) -> Option {
+ for pending in self.pending.iter().rev() {
+ if pending.repo_paths.contains(&entry.repo_path) {
+ return Some(pending.will_become_staged);
+ }
+ }
+ entry.is_staged
+ }
+
+ fn has_staged_changes(&self) -> bool {
+ self.tracked_staged_count > 0 || self.new_staged_count > 0
+ }
+
+ fn has_tracked_changes(&self) -> bool {
+ self.tracked_count > 0
+ }
+
+ fn header_state(&self, header_type: Section) -> ToggleState {
+ let (staged_count, count) = match header_type {
+ Section::New => (self.new_staged_count, self.new_count),
+ Section::Tracked => (self.tracked_staged_count, self.tracked_count),
+ };
if staged_count == 0 {
ToggleState::Unselected
} else if count == staged_count {
@@ -1157,25 +1177,27 @@ impl GitPanel {
cx: &Context,
) -> impl IntoElement {
let editor = self.commit_editor.clone();
- let can_commit = !self.commit_pending && self.can_commit && !editor.read(cx).is_empty(cx);
- let can_commit_all =
- !self.commit_pending && self.can_commit_all && !editor.read(cx).is_empty(cx);
+ let can_commit =
+ (self.has_staged_changes() || self.has_tracked_changes()) && !self.commit_pending;
let editor_focus_handle = editor.read(cx).focus_handle(cx).clone();
let focus_handle_1 = self.focus_handle(cx).clone();
- let focus_handle_2 = self.focus_handle(cx).clone();
+ let tooltip = if self.has_staged_changes() {
+ "Commit staged changes"
+ } else {
+ "Commit changes to tracked files"
+ };
+ let title = if self.has_staged_changes() {
+ "Commit"
+ } else {
+ "Commit All"
+ };
- let commit_staged_button = self
- .panel_button("commit-staged-changes", "Commit")
+ let commit_button = self
+ .panel_button("commit-changes", title)
.tooltip(move |window, cx| {
let focus_handle = focus_handle_1.clone();
- Tooltip::for_action_in(
- "Commit all staged changes",
- &CommitChanges,
- &focus_handle,
- window,
- cx,
- )
+ Tooltip::for_action_in(tooltip, &CommitChanges, &focus_handle, window, cx)
})
.disabled(!can_commit)
.on_click({
@@ -1185,31 +1207,6 @@ impl GitPanel {
})
});
- let commit_all_button = self
- .panel_button("commit-all-changes", "Commit All")
- .tooltip(move |window, cx| {
- let focus_handle = focus_handle_2.clone();
- Tooltip::for_action_in(
- "Commit all changes, including unstaged changes",
- &CommitAllChanges,
- &focus_handle,
- window,
- cx,
- )
- })
- .disabled(!can_commit_all)
- .on_click({
- let name_and_email = name_and_email.clone();
- cx.listener(move |this, _: &ClickEvent, window, cx| {
- this.commit_tracked_changes(
- &CommitAllChanges,
- name_and_email.clone(),
- window,
- cx,
- )
- })
- });
-
div().w_full().h(px(140.)).px_2().pt_1().pb_2().child(
v_flex()
.id("commit-editor-container")
@@ -1229,8 +1226,7 @@ impl GitPanel {
.right_3()
.gap_1p5()
.child(div().gap_1().flex_grow())
- .child(commit_all_button)
- .child(commit_staged_button),
+ .child(commit_button),
),
)
}
@@ -1423,8 +1419,17 @@ impl GitPanel {
_window: &Window,
cx: &Context,
) -> AnyElement {
- let checkbox = Checkbox::new(header.title(), self.header_state(header.header))
+ let header_state = if self.has_staged_changes() {
+ self.header_state(header.header)
+ } else {
+ match header.header {
+ Section::Tracked => ToggleState::Selected,
+ Section::New => ToggleState::Unselected,
+ }
+ };
+ let checkbox = Checkbox::new(header.title(), header_state)
.disabled(!has_write_access)
+ .placeholder(!self.has_staged_changes())
.fill()
.elevation(ElevationIndex::Surface);
let selected = self.selected_entry == Some(ix);
@@ -1507,14 +1512,19 @@ impl GitPanel {
let id: ElementId = ElementId::Name(format!("entry_{}", display_name).into());
- let is_staged = pending
+ let mut is_staged = pending
.or_else(|| entry.is_staged)
.map(ToggleState::from)
.unwrap_or(ToggleState::Indeterminate);
+ if !self.has_staged_changes() && !entry.status.is_created() {
+ is_staged = ToggleState::Selected;
+ }
+
let checkbox = Checkbox::new(id, is_staged)
.disabled(!has_write_access)
.fill()
+ .placeholder(!self.has_staged_changes())
.elevation(ElevationIndex::Surface)
.on_click({
let entry = entry.clone();
@@ -1532,6 +1542,7 @@ impl GitPanel {
.id(("start-slot", ix))
.gap(DynamicSpacing::Base04.rems(cx))
.child(checkbox)
+ .tooltip(|window, cx| Tooltip::for_action("Stage File", &ToggleStaged, window, cx))
.child(git_status_icon(status, cx))
.on_mouse_down(MouseButton::Left, |_, _, cx| {
// prevent the list item active state triggering when toggling checkbox
diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs
index a3e2c1897a..ba4878898b 100644
--- a/crates/ui/src/components/icon.rs
+++ b/crates/ui/src/components/icon.rs
@@ -164,6 +164,7 @@ pub enum IconName {
ChevronRight,
ChevronUp,
ChevronUpDown,
+ Circle,
Close,
Code,
Command,
diff --git a/crates/ui/src/components/toggle.rs b/crates/ui/src/components/toggle.rs
index 008edd1f07..0413891811 100644
--- a/crates/ui/src/components/toggle.rs
+++ b/crates/ui/src/components/toggle.rs
@@ -43,6 +43,7 @@ pub struct Checkbox {
id: ElementId,
toggle_state: ToggleState,
disabled: bool,
+ placeholder: bool,
on_click: Option>,
filled: bool,
style: ToggleStyle,
@@ -62,6 +63,7 @@ impl Checkbox {
style: ToggleStyle::default(),
tooltip: None,
label: None,
+ placeholder: false,
}
}
@@ -71,6 +73,12 @@ impl Checkbox {
self
}
+ /// Sets the disabled state of the [`Checkbox`].
+ pub fn placeholder(mut self, placeholder: bool) -> Self {
+ self.placeholder = placeholder;
+ self
+ }
+
/// Binds a handler to the [`Checkbox`] that will be called when clicked.
pub fn on_click(
mut self,
@@ -145,23 +153,26 @@ impl Checkbox {
impl RenderOnce for Checkbox {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let group_id = format!("checkbox_group_{:?}", self.id);
+ let color = if self.disabled {
+ Color::Disabled
+ } else if self.placeholder {
+ Color::Placeholder
+ } else {
+ Color::Selected
+ };
let icon = match self.toggle_state {
- ToggleState::Selected => Some(Icon::new(IconName::Check).size(IconSize::Small).color(
- if self.disabled {
- Color::Disabled
- } else {
- Color::Selected
- },
- )),
- ToggleState::Indeterminate => Some(
- Icon::new(IconName::Dash)
+ ToggleState::Selected => Some(if self.placeholder {
+ Icon::new(IconName::Circle)
+ .size(IconSize::XSmall)
+ .color(color)
+ } else {
+ Icon::new(IconName::Check)
.size(IconSize::Small)
- .color(if self.disabled {
- Color::Disabled
- } else {
- Color::Selected
- }),
- ),
+ .color(color)
+ }),
+ ToggleState::Indeterminate => {
+ Some(Icon::new(IconName::Dash).size(IconSize::Small).color(color))
+ }
ToggleState::Unselected => None,
};
diff --git a/crates/ui/src/traits/toggleable.rs b/crates/ui/src/traits/toggleable.rs
index e73a6cd658..8771cedaa7 100644
--- a/crates/ui/src/traits/toggleable.rs
+++ b/crates/ui/src/traits/toggleable.rs
@@ -58,12 +58,12 @@ impl From for ToggleState {
}
}
-impl From