diff --git a/assets/settings/default.json b/assets/settings/default.json index 492a482198..4332cfc862 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -226,7 +226,7 @@ // Hide the values of in variables from visual display in private files "redact_private_values": false, // The default number of lines to expand excerpts in the multibuffer by. - "expand_excerpt_lines": 3, + "expand_excerpt_lines": 5, // Globs to match against file paths to determine if a file is private. "private_files": ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/secrets.yml"], // Whether to use additional LSP queries to format (and amend) the code after @@ -601,6 +601,13 @@ // // Default: main "fallback_branch_name": "main", + + // Whether to sort entries in the panel by path + // or by status (the default). + // + // Default: false + "sort_by_path": false, + "scrollbar": { // When to show the scrollbar in the git panel. // diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index d2d2811c08..a5bd4a0a15 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -354,6 +354,7 @@ pub struct GitPanel { workspace: WeakEntity, context_menu: Option<(Entity, Point, Subscription)>, modal_open: bool, + show_placeholders: bool, _settings_subscription: Subscription, } @@ -407,6 +408,16 @@ impl GitPanel { }) .detach(); + let mut was_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path; + cx.observe_global::(move |this, cx| { + let is_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path; + if is_sort_by_path != was_sort_by_path { + this.update_visible_entries(cx); + } + was_sort_by_path = is_sort_by_path + }) + .detach(); + // just to let us render a placeholder editor. // Once the active git repo is set, this buffer will be replaced. let temporary_buffer = cx.new(|cx| Buffer::local("", cx)); @@ -506,6 +517,7 @@ impl GitPanel { tracked_staged_count: 0, update_visible_entries_task: Task::ready(()), width: None, + show_placeholders: false, context_menu: None, workspace, modal_open: false, @@ -598,7 +610,14 @@ impl GitPanel { cx.notify(); } - pub fn entry_by_path(&self, path: &RepoPath) -> Option { + pub fn entry_by_path(&self, path: &RepoPath, cx: &App) -> Option { + if GitPanelSettings::get_global(cx).sort_by_path { + return self + .entries + .binary_search_by(|entry| entry.status_entry().unwrap().repo_path.cmp(&path)) + .ok(); + } + if self.conflicted_count > 0 { let conflicted_start = 1; if let Ok(ix) = self.entries[conflicted_start..conflicted_start + self.conflicted_count] @@ -650,7 +669,7 @@ impl GitPanel { let Some(repo_path) = git_repo.read(cx).project_path_to_repo_path(&path, cx) else { return; }; - let Some(ix) = self.entry_by_path(&repo_path) else { + let Some(ix) = self.entry_by_path(&repo_path, cx) else { return; }; self.selected_entry = Some(ix); @@ -2294,6 +2313,8 @@ impl GitPanel { self.tracked_staged_count = 0; self.entry_count = 0; + let sort_by_path = GitPanelSettings::get_global(cx).sort_by_path; + let mut changed_entries = Vec::new(); let mut new_entries = Vec::new(); let mut conflict_entries = Vec::new(); @@ -2353,7 +2374,9 @@ impl GitPanel { None => max_width_item = Some((entry.repo_path.clone(), width_estimate)), } - if is_conflict { + if sort_by_path { + changed_entries.push(entry); + } else if is_conflict { conflict_entries.push(entry); } else if is_new { new_entries.push(entry); @@ -2408,9 +2431,11 @@ impl GitPanel { } if changed_entries.len() > 0 { - self.entries.push(GitListEntry::Header(GitHeaderEntry { - header: Section::Tracked, - })); + if !sort_by_path { + self.entries.push(GitListEntry::Header(GitHeaderEntry { + header: Section::Tracked, + })); + } self.entries.extend( changed_entries .into_iter() @@ -2464,6 +2489,7 @@ impl GitPanel { } fn update_counts(&mut self, repo: &Repository) { + self.show_placeholders = false; self.conflicted_count = 0; self.conflicted_staged_count = 0; self.new_count = 0; @@ -2525,10 +2551,6 @@ impl GitPanel { || self.conflicted_count > self.conflicted_staged_count } - fn has_conflicts(&self) -> bool { - self.conflicted_count > 0 - } - fn has_tracked_changes(&self) -> bool { self.tracked_count > 0 } @@ -2935,14 +2957,11 @@ impl GitPanel { cx: &mut Context, ) -> Option { let active_repository = self.active_repository.clone()?; - let (can_commit, tooltip) = self.configure_commit_button(cx); let panel_editor_style = panel_editor_style(true, window, cx); let enable_coauthors = self.render_co_authors(cx); - let title = self.commit_button_title(); let editor_focus_handle = self.commit_editor.focus_handle(cx); - let commit_tooltip_focus_handle = editor_focus_handle.clone(); let expand_tooltip_focus_handle = editor_focus_handle.clone(); let branch = active_repository.read(cx).branch.clone(); @@ -3010,183 +3029,7 @@ impl GitPanel { h_flex() .gap_0p5() .children(enable_coauthors) - .when(self.amend_pending, { - |this| { - this.h_flex() - .gap_1() - .child( - panel_filled_button("Cancel") - .tooltip({ - let handle = - commit_tooltip_focus_handle.clone(); - move |window, cx| { - Tooltip::for_action_in( - "Cancel amend", - &git::Cancel, - &handle, - window, - cx, - ) - } - }) - .on_click(move |_, window, cx| { - window.dispatch_action( - Box::new(git::Cancel), - cx, - ); - }), - ) - .child( - panel_filled_button(title) - .tooltip({ - let handle = - commit_tooltip_focus_handle.clone(); - move |window, cx| { - if can_commit { - Tooltip::for_action_in( - tooltip, &Amend, &handle, - window, cx, - ) - } else { - Tooltip::simple(tooltip, cx) - } - } - }) - .disabled(!can_commit || self.modal_open) - .on_click({ - let git_panel = git_panel.downgrade(); - move |_, window, cx| { - telemetry::event!( - "Git Amended", - source = "Git Panel" - ); - git_panel - .update(cx, |git_panel, cx| { - git_panel - .set_amend_pending( - false, cx, - ); - git_panel.commit_changes( - CommitOptions { - amend: true, - }, - window, - cx, - ); - }) - .ok(); - } - }), - ) - } - }) - .when(!self.amend_pending, |this| { - this.when(has_previous_commit, |this| { - this.child(SplitButton::new( - ui::ButtonLike::new_rounded_left(ElementId::Name( - format!("split-button-left-{}", title).into(), - )) - .layer(ui::ElevationIndex::ModalSurface) - .size(ui::ButtonSize::Compact) - .child( - div() - .child( - Label::new(title) - .size(LabelSize::Small), - ) - .mr_0p5(), - ) - .on_click({ - let git_panel = git_panel.downgrade(); - move |_, window, cx| { - telemetry::event!( - "Git Committed", - source = "Git Panel" - ); - git_panel - .update(cx, |git_panel, cx| { - git_panel.commit_changes( - CommitOptions { amend: false }, - window, - cx, - ); - }) - .ok(); - } - }) - .disabled(!can_commit || self.modal_open) - .tooltip({ - let handle = - commit_tooltip_focus_handle.clone(); - move |window, cx| { - if can_commit { - Tooltip::with_meta_in( - tooltip, - Some(&git::Commit), - "git commit", - &handle.clone(), - window, - cx, - ) - } else { - Tooltip::simple(tooltip, cx) - } - } - }), - self.render_git_commit_menu( - ElementId::Name( - format!("split-button-right-{}", title) - .into(), - ), - Some(commit_tooltip_focus_handle.clone()), - ) - .into_any_element(), - )) - }) - .when( - !has_previous_commit, - |this| { - this.child( - panel_filled_button(title) - .tooltip(move |window, cx| { - if can_commit { - Tooltip::with_meta_in( - tooltip, - Some(&git::Commit), - "git commit", - &commit_tooltip_focus_handle, - window, - cx, - ) - } else { - Tooltip::simple(tooltip, cx) - } - }) - .disabled(!can_commit || self.modal_open) - .on_click({ - let git_panel = git_panel.downgrade(); - move |_, window, cx| { - telemetry::event!( - "Git Committed", - source = "Git Panel" - ); - git_panel - .update(cx, |git_panel, cx| { - git_panel.commit_changes( - CommitOptions { - amend: false, - }, - window, - cx, - ); - }) - .ok(); - } - }), - ) - }, - ) - }), + .child(self.render_commit_button(has_previous_commit, cx)), ), ) .child( @@ -3235,6 +3078,168 @@ impl GitPanel { Some(footer) } + fn render_commit_button( + &self, + has_previous_commit: bool, + cx: &mut Context, + ) -> impl IntoElement { + let (can_commit, tooltip) = self.configure_commit_button(cx); + let title = self.commit_button_title(); + let commit_tooltip_focus_handle = self.commit_editor.focus_handle(cx); + div() + .id("commit-wrapper") + .on_hover(cx.listener(move |this, hovered, _, cx| { + this.show_placeholders = + *hovered && !this.has_staged_changes() && !this.has_unstaged_conflicts(); + cx.notify() + })) + .when(self.amend_pending, { + |this| { + this.h_flex() + .gap_1() + .child( + panel_filled_button("Cancel") + .tooltip({ + let handle = commit_tooltip_focus_handle.clone(); + move |window, cx| { + Tooltip::for_action_in( + "Cancel amend", + &git::Cancel, + &handle, + window, + cx, + ) + } + }) + .on_click(move |_, window, cx| { + window.dispatch_action(Box::new(git::Cancel), cx); + }), + ) + .child( + panel_filled_button(title) + .tooltip({ + let handle = commit_tooltip_focus_handle.clone(); + move |window, cx| { + if can_commit { + Tooltip::for_action_in( + tooltip, &Amend, &handle, window, cx, + ) + } else { + Tooltip::simple(tooltip, cx) + } + } + }) + .disabled(!can_commit || self.modal_open) + .on_click({ + let git_panel = cx.weak_entity(); + move |_, window, cx| { + telemetry::event!("Git Amended", source = "Git Panel"); + git_panel + .update(cx, |git_panel, cx| { + git_panel.set_amend_pending(false, cx); + git_panel.commit_changes( + CommitOptions { amend: true }, + window, + cx, + ); + }) + .ok(); + } + }), + ) + } + }) + .when(!self.amend_pending, |this| { + this.when(has_previous_commit, |this| { + this.child(SplitButton::new( + ui::ButtonLike::new_rounded_left(ElementId::Name( + format!("split-button-left-{}", title).into(), + )) + .layer(ui::ElevationIndex::ModalSurface) + .size(ui::ButtonSize::Compact) + .child( + div() + .child(Label::new(title).size(LabelSize::Small)) + .mr_0p5(), + ) + .on_click({ + let git_panel = cx.weak_entity(); + move |_, window, cx| { + telemetry::event!("Git Committed", source = "Git Panel"); + git_panel + .update(cx, |git_panel, cx| { + git_panel.commit_changes( + CommitOptions { amend: false }, + window, + cx, + ); + }) + .ok(); + } + }) + .disabled(!can_commit || self.modal_open) + .tooltip({ + let handle = commit_tooltip_focus_handle.clone(); + move |window, cx| { + if can_commit { + Tooltip::with_meta_in( + tooltip, + Some(&git::Commit), + "git commit", + &handle.clone(), + window, + cx, + ) + } else { + Tooltip::simple(tooltip, cx) + } + } + }), + self.render_git_commit_menu( + ElementId::Name(format!("split-button-right-{}", title).into()), + Some(commit_tooltip_focus_handle.clone()), + ) + .into_any_element(), + )) + }) + .when(!has_previous_commit, |this| { + this.child( + panel_filled_button(title) + .tooltip(move |window, cx| { + if can_commit { + Tooltip::with_meta_in( + tooltip, + Some(&git::Commit), + "git commit", + &commit_tooltip_focus_handle, + window, + cx, + ) + } else { + Tooltip::simple(tooltip, cx) + } + }) + .disabled(!can_commit || self.modal_open) + .on_click({ + let git_panel = cx.weak_entity(); + move |_, window, cx| { + telemetry::event!("Git Committed", source = "Git Panel"); + git_panel + .update(cx, |git_panel, cx| { + git_panel.commit_changes( + CommitOptions { amend: false }, + window, + cx, + ); + }) + .ok(); + } + }), + ) + }) + }) + } + fn render_pending_amend(&self, cx: &mut Context) -> impl IntoElement { div() .py_2() @@ -3482,7 +3487,7 @@ impl GitPanel { let repo = self.active_repository.as_ref()?.read(cx); let project_path = (file.worktree_id(cx), file.path()).into(); let repo_path = repo.project_path_to_repo_path(&project_path, cx)?; - let ix = self.entry_by_path(&repo_path)?; + let ix = self.entry_by_path(&repo_path, cx)?; let entry = self.entries.get(ix)?; let entry_staging = self.entry_staging(entry.status_entry()?); @@ -3852,8 +3857,7 @@ impl GitPanel { let entry_staging = self.entry_staging(entry); let mut is_staged: ToggleState = self.entry_staging(entry).as_bool().into(); - - if !self.has_staged_changes() && !self.has_conflicts() && !entry.status.is_created() { + if self.show_placeholders && !self.has_staged_changes() && !entry.status.is_created() { is_staged = ToggleState::Selected; } @@ -3953,11 +3957,6 @@ impl GitPanel { Checkbox::new(checkbox_id, is_staged) .disabled(!has_write_access) .fill() - .placeholder( - !self.has_staged_changes() - && !self.has_conflicts() - && !entry.status.is_created(), - ) .elevation(ElevationIndex::Surface) .on_click({ let entry = entry.clone(); diff --git a/crates/git_ui/src/git_panel_settings.rs b/crates/git_ui/src/git_panel_settings.rs index 198c4815c7..a8ae31f859 100644 --- a/crates/git_ui/src/git_panel_settings.rs +++ b/crates/git_ui/src/git_panel_settings.rs @@ -64,6 +64,12 @@ pub struct GitPanelSettingsContent { /// /// Default: main pub fallback_branch_name: Option, + + /// Whether to sort entries in the panel by path + /// or by status (the default). + /// + /// Default: false + pub sort_by_path: Option, } #[derive(Deserialize, Debug, Clone, PartialEq)] @@ -74,6 +80,7 @@ pub struct GitPanelSettings { pub status_style: StatusStyle, pub scrollbar: ScrollbarSettings, pub fallback_branch_name: String, + pub sort_by_path: bool, } impl Settings for GitPanelSettings { diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index 36ad7e528c..e88af5c47a 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -1,6 +1,7 @@ use crate::{ conflict_view::ConflictAddon, git_panel::{GitPanel, GitPanelAddon, GitStatusEntry}, + git_panel_settings::GitPanelSettings, remote_button::{render_publish_button, render_push_button}, }; use anyhow::Result; @@ -27,10 +28,9 @@ use project::{ Project, ProjectPath, git_store::{GitStore, GitStoreEvent, RepositoryEvent}, }; -use std::{ - any::{Any, TypeId}, - ops::Range, -}; +use settings::{Settings, SettingsStore}; +use std::any::{Any, TypeId}; +use std::ops::Range; use theme::ActiveTheme; use ui::{KeyBinding, Tooltip, prelude::*, vertical_divider}; use util::ResultExt as _; @@ -165,6 +165,16 @@ impl ProjectDiff { }, ); + let mut was_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path; + cx.observe_global::(move |this, cx| { + let is_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path; + if is_sort_by_path != was_sort_by_path { + *this.update_needed.borrow_mut() = (); + } + was_sort_by_path = is_sort_by_path + }) + .detach(); + let (mut send, recv) = postage::watch::channel::<()>(); let worker = window.spawn(cx, { let this = cx.weak_entity(); @@ -349,7 +359,9 @@ impl ProjectDiff { else { continue; }; - let namespace = if repo.has_conflict(&entry.repo_path) { + let namespace = if GitPanelSettings::get_global(cx).sort_by_path { + TRACKED_NAMESPACE + } else if repo.has_conflict(&entry.repo_path) { CONFLICT_NAMESPACE } else if entry.status.is_created() { NEW_NAMESPACE