mirror of
https://github.com/zed-industries/zed.git
synced 2026-07-09 16:00:35 +00:00
git_panel: Add better view options (#59043)
Self-Review Checklist: - [x] I have reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content adheres to Zed UI standards ([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) and [icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md) guidelines) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable ## Summary This updates the Git Panel view controls so the panel can independently model: - whether entries are shown as a list or tree - whether flat entries are sorted by path or name - whether entries are grouped by status or shown as one combined set It also keeps the Project Diff order consistent with the Git Panel, including tree ordering, status grouping, and flat name/path sorting. ## Why This Is Useful The previous sort_by_path boolean overloaded two separate ideas: sorting and grouping. That made it hard to represent the view users actually wanted, especially a single tree where tracked and untracked files are not split into separate sections. This change replaces that boolean with explicit enum settings: - git_panel.sort_by: path or name - git_panel.group_by: none or status That keeps the settings mutually exclusive, easier to extend, and closer to the UI model. ## Implementation Notes - Adds a dedicated Git Panel View Options menu using the sliders icon. - Moves view, sort, and group controls out of the overflow actions menu. - Disables sort options in tree view because tree order is folder-first rather than pure path/name sorting. - Removes the Tracked heading when grouping is disabled. - Keeps Git Panel tree expansion state when switching view options. - Recomputes Project Diff sort prefixes from tree_view, sort_by, and group_by so diff cards follow the same top-to-bottom order as the Git Panel. - Preserves Project Diff open/closed file state across view option changes by carrying fold state by repo path instead of by synthetic sort key. - Updates settings UI renderers and docs for the new enum settings. ## Testing - [x] cargo fmt --package git_ui --package settings_ui - [x] cargo check -p git_ui - [x] Verified settings UI enum dropdown rendering for Git Panel sort/group settings Closes https://github.com/zed-industries/zed/issues/53555 Closes https://github.com/zed-industries/zed/issues/56039 Closes https://github.com/zed-industries/zed/issues/45438 Release Notes: - Improved Git Panel view options and Project Diff ordering. --------- Co-authored-by: Christopher Biscardi <chris@christopherbiscardi.com>
This commit is contained in:
parent
ca2d7fd9e5
commit
076fd14c88
9 changed files with 653 additions and 128 deletions
|
|
@ -974,10 +974,14 @@
|
|||
//
|
||||
// Default: main
|
||||
"fallback_branch_name": "main",
|
||||
// Whether to sort entries in the panel by path or by status (the default).
|
||||
// How to sort entries in the git panel.
|
||||
//
|
||||
// Default: false
|
||||
"sort_by_path": false,
|
||||
// Default: path
|
||||
"sort_by": "path",
|
||||
// How to group entries in the git panel.
|
||||
//
|
||||
// Default: status
|
||||
"group_by": "status",
|
||||
// Whether to collapse untracked files in the diff panel.
|
||||
//
|
||||
// Default: false
|
||||
|
|
|
|||
|
|
@ -62,7 +62,9 @@ use project::{
|
|||
use prompt_store::RULES_FILE_NAMES;
|
||||
use proto::RpcError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsStore, StatusStyle, update_settings_file};
|
||||
use settings::{
|
||||
GitPanelGroupBy, GitPanelSortBy, Settings, SettingsStore, StatusStyle, update_settings_file,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use std::future::Future;
|
||||
use std::ops::Range;
|
||||
|
|
@ -72,9 +74,9 @@ use strum::{IntoEnumIterator, VariantNames};
|
|||
use theme_settings::ThemeSettings;
|
||||
use time::OffsetDateTime;
|
||||
use ui::{
|
||||
ButtonLike, Checkbox, ContextMenu, Divider, ElevationIndex, IndentGuideColors, KeyBinding,
|
||||
PopoverMenu, ProjectEmptyState, RenderedIndentGuide, ScrollAxes, Scrollbars, SplitButton, Tab,
|
||||
TintColor, Tooltip, WithScrollbar, prelude::*,
|
||||
ButtonLike, Checkbox, ContextMenu, ContextMenuEntry, Divider, ElevationIndex,
|
||||
IndentGuideColors, KeyBinding, PopoverMenu, ProjectEmptyState, RenderedIndentGuide, ScrollAxes,
|
||||
Scrollbars, SplitButton, Tab, TintColor, Tooltip, WithScrollbar, prelude::*,
|
||||
};
|
||||
use util::paths::PathStyle;
|
||||
use util::{ResultExt, TryFutureExt, markdown::MarkdownInlineCode, maybe, rel_path::RelPath};
|
||||
|
|
@ -111,8 +113,14 @@ actions!(
|
|||
LastEntry,
|
||||
/// Toggles automatic co-author suggestions.
|
||||
ToggleFillCoAuthors,
|
||||
/// Toggles sorting entries by path vs status.
|
||||
ToggleSortByPath,
|
||||
/// Sorts entries by path.
|
||||
SetSortByPath,
|
||||
/// Sorts entries by name.
|
||||
SetSortByName,
|
||||
/// Disables grouping entries by status.
|
||||
SetGroupByNone,
|
||||
/// Groups entries by status.
|
||||
SetGroupByStatus,
|
||||
/// Toggles showing entries in tree vs flat view.
|
||||
ToggleTreeView,
|
||||
/// Expands the selected entry to show its children.
|
||||
|
|
@ -159,7 +167,8 @@ struct GitMenuState {
|
|||
has_staged_changes: bool,
|
||||
has_unstaged_changes: bool,
|
||||
has_new_changes: bool,
|
||||
sort_by_path: bool,
|
||||
sort_by: GitPanelSortBy,
|
||||
group_by: GitPanelGroupBy,
|
||||
has_stash_items: bool,
|
||||
tree_view: bool,
|
||||
}
|
||||
|
|
@ -204,27 +213,79 @@ fn git_panel_context_menu(
|
|||
"Trash Untracked Files",
|
||||
TrashUntrackedFiles.boxed_clone(),
|
||||
)
|
||||
.separator()
|
||||
.entry(
|
||||
if state.tree_view {
|
||||
"Flat View"
|
||||
} else {
|
||||
"Tree View"
|
||||
},
|
||||
Some(Box::new(ToggleTreeView)),
|
||||
move |window, cx| window.dispatch_action(Box::new(ToggleTreeView), cx),
|
||||
})
|
||||
}
|
||||
|
||||
fn git_panel_view_options_menu(
|
||||
focus_handle: FocusHandle,
|
||||
state: GitMenuState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Entity<ContextMenu> {
|
||||
ContextMenu::build(window, cx, move |context_menu, _, _| {
|
||||
context_menu
|
||||
.context(focus_handle)
|
||||
.header("View")
|
||||
.item(
|
||||
ContextMenuEntry::new("List")
|
||||
.toggle(IconPosition::End, !state.tree_view)
|
||||
.handler(move |window, cx| {
|
||||
if state.tree_view {
|
||||
window.dispatch_action(Box::new(ToggleTreeView), cx);
|
||||
}
|
||||
}),
|
||||
)
|
||||
.item(
|
||||
ContextMenuEntry::new("Tree")
|
||||
.toggle(IconPosition::End, state.tree_view)
|
||||
.handler(move |window, cx| {
|
||||
if !state.tree_view {
|
||||
window.dispatch_action(Box::new(ToggleTreeView), cx);
|
||||
}
|
||||
}),
|
||||
)
|
||||
.separator()
|
||||
.header("Sort By")
|
||||
.item(
|
||||
ContextMenuEntry::new("Path")
|
||||
.toggle(IconPosition::End, state.sort_by == GitPanelSortBy::Path)
|
||||
.disabled(state.tree_view)
|
||||
.handler(move |window, cx| {
|
||||
if !state.tree_view {
|
||||
window.dispatch_action(Box::new(SetSortByPath), cx);
|
||||
}
|
||||
}),
|
||||
)
|
||||
.item(
|
||||
ContextMenuEntry::new("Name")
|
||||
.toggle(IconPosition::End, state.sort_by == GitPanelSortBy::Name)
|
||||
.disabled(state.tree_view)
|
||||
.handler(move |window, cx| {
|
||||
if !state.tree_view {
|
||||
window.dispatch_action(Box::new(SetSortByName), cx);
|
||||
}
|
||||
}),
|
||||
)
|
||||
.separator()
|
||||
.header("Group By")
|
||||
.item(
|
||||
ContextMenuEntry::new("None")
|
||||
.toggle(IconPosition::End, state.group_by == GitPanelGroupBy::None)
|
||||
.handler(move |window, cx| {
|
||||
if state.group_by != GitPanelGroupBy::None {
|
||||
window.dispatch_action(Box::new(SetGroupByNone), cx);
|
||||
}
|
||||
}),
|
||||
)
|
||||
.item(
|
||||
ContextMenuEntry::new("Status")
|
||||
.toggle(IconPosition::End, state.group_by == GitPanelGroupBy::Status)
|
||||
.handler(move |window, cx| {
|
||||
if state.group_by != GitPanelGroupBy::Status {
|
||||
window.dispatch_action(Box::new(SetGroupByStatus), cx);
|
||||
}
|
||||
}),
|
||||
)
|
||||
.when(!state.tree_view, |this| {
|
||||
this.entry(
|
||||
if state.sort_by_path {
|
||||
"Sort by Status"
|
||||
} else {
|
||||
"Sort by Path"
|
||||
},
|
||||
Some(Box::new(ToggleSortByPath)),
|
||||
move |window, cx| window.dispatch_action(Box::new(ToggleSortByPath), cx),
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -388,7 +449,7 @@ struct TreeViewState {
|
|||
// Length equals the number of visible entries.
|
||||
// This is needed because some entries (like collapsed directories) may be hidden.
|
||||
logical_indices: Vec<usize>,
|
||||
expanded_dirs: HashMap<TreeKey, bool>,
|
||||
expanded_dirs: HashMap<RepoPath, bool>,
|
||||
directory_descendants: HashMap<TreeKey, Vec<GitStatusEntry>>,
|
||||
}
|
||||
|
||||
|
|
@ -463,8 +524,8 @@ impl TreeViewState {
|
|||
let (child_flattened, mut child_statuses) =
|
||||
self.flatten_tree(terminal, section, depth + 1, seen_directories);
|
||||
let key = TreeKey { section, path };
|
||||
let expanded = *self.expanded_dirs.get(&key).unwrap_or(&true);
|
||||
self.expanded_dirs.entry(key.clone()).or_insert(true);
|
||||
let expanded = *self.expanded_dirs.get(&key.path).unwrap_or(&true);
|
||||
self.expanded_dirs.entry(key.path.clone()).or_insert(true);
|
||||
seen_directories.insert(key.clone());
|
||||
|
||||
self.directory_descendants
|
||||
|
|
@ -646,6 +707,7 @@ pub struct GitPanel {
|
|||
generate_commit_message_task: Option<Task<Option<()>>>,
|
||||
entries: Vec<GitListEntry>,
|
||||
view_mode: GitPanelViewMode,
|
||||
tree_expanded_dirs: HashMap<RepoPath, bool>,
|
||||
entries_indices: HashMap<RepoPath, usize>,
|
||||
single_staged_entry: Option<GitStatusEntry>,
|
||||
single_tracked_entry: Option<GitStatusEntry>,
|
||||
|
|
@ -746,24 +808,39 @@ impl GitPanel {
|
|||
let focus_handle = cx.focus_handle();
|
||||
cx.on_focus(&focus_handle, window, Self::focus_in).detach();
|
||||
|
||||
let mut was_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path;
|
||||
let mut was_sort_by = GitPanelSettings::get_global(cx).sort_by;
|
||||
let mut was_group_by = GitPanelSettings::get_global(cx).group_by;
|
||||
let mut was_tree_view = GitPanelSettings::get_global(cx).tree_view;
|
||||
let mut was_file_icons = GitPanelSettings::get_global(cx).file_icons;
|
||||
let mut was_folder_icons = GitPanelSettings::get_global(cx).folder_icons;
|
||||
let mut was_diff_stats = GitPanelSettings::get_global(cx).diff_stats;
|
||||
cx.observe_global_in::<SettingsStore>(window, move |this, window, cx| {
|
||||
let settings = GitPanelSettings::get_global(cx);
|
||||
let sort_by_path = settings.sort_by_path;
|
||||
let sort_by = settings.sort_by;
|
||||
let group_by = settings.group_by;
|
||||
let tree_view = settings.tree_view;
|
||||
let file_icons = settings.file_icons;
|
||||
let folder_icons = settings.folder_icons;
|
||||
let diff_stats = settings.diff_stats;
|
||||
if tree_view != was_tree_view {
|
||||
this.view_mode = GitPanelViewMode::from_settings(cx);
|
||||
match (&mut this.view_mode, tree_view) {
|
||||
(GitPanelViewMode::Tree(state), false) => {
|
||||
this.tree_expanded_dirs = state.expanded_dirs.clone();
|
||||
this.view_mode = GitPanelViewMode::Flat;
|
||||
}
|
||||
(GitPanelViewMode::Flat, true) => {
|
||||
this.view_mode = GitPanelViewMode::Tree(TreeViewState {
|
||||
expanded_dirs: this.tree_expanded_dirs.clone(),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let mut update_entries = false;
|
||||
if sort_by_path != was_sort_by_path || tree_view != was_tree_view {
|
||||
if sort_by != was_sort_by || group_by != was_group_by || tree_view != was_tree_view
|
||||
{
|
||||
this.bulk_staging.take();
|
||||
update_entries = true;
|
||||
}
|
||||
|
|
@ -773,7 +850,8 @@ impl GitPanel {
|
|||
if file_icons != was_file_icons || folder_icons != was_folder_icons {
|
||||
cx.notify();
|
||||
}
|
||||
was_sort_by_path = sort_by_path;
|
||||
was_sort_by = sort_by;
|
||||
was_group_by = group_by;
|
||||
was_tree_view = tree_view;
|
||||
was_file_icons = file_icons;
|
||||
was_folder_icons = folder_icons;
|
||||
|
|
@ -846,6 +924,7 @@ impl GitPanel {
|
|||
generate_commit_message_task: None,
|
||||
entries: Vec::new(),
|
||||
view_mode: GitPanelViewMode::from_settings(cx),
|
||||
tree_expanded_dirs: HashMap::default(),
|
||||
entries_indices: HashMap::default(),
|
||||
focus_handle: cx.focus_handle(),
|
||||
fs,
|
||||
|
|
@ -938,8 +1017,8 @@ impl GitPanel {
|
|||
path: RepoPath::from_rel_path(dir),
|
||||
};
|
||||
|
||||
if tree_state.expanded_dirs.get(&key) == Some(&false) {
|
||||
tree_state.expanded_dirs.insert(key, true);
|
||||
if tree_state.expanded_dirs.get(&key.path) == Some(&false) {
|
||||
tree_state.expanded_dirs.insert(key.path.clone(), true);
|
||||
needs_rebuild = true;
|
||||
}
|
||||
|
||||
|
|
@ -3477,20 +3556,56 @@ impl GitPanel {
|
|||
cx.notify();
|
||||
}
|
||||
|
||||
fn toggle_sort_by_path(
|
||||
&mut self,
|
||||
_: &ToggleSortByPath,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let current_setting = GitPanelSettings::get_global(cx).sort_by_path;
|
||||
fn set_sort_by_path(&mut self, _: &SetSortByPath, _: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(workspace) = self.workspace.upgrade() {
|
||||
let workspace = workspace.read(cx);
|
||||
let fs = workspace.app_state().fs.clone();
|
||||
cx.update_global::<SettingsStore, _>(|store, _cx| {
|
||||
store.update_settings_file(fs, move |settings, _cx| {
|
||||
settings.git_panel.get_or_insert_default().sort_by_path =
|
||||
Some(!current_setting);
|
||||
settings.git_panel.get_or_insert_default().sort_by = Some(GitPanelSortBy::Path);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn set_sort_by_name(&mut self, _: &SetSortByName, _: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(workspace) = self.workspace.upgrade() {
|
||||
let workspace = workspace.read(cx);
|
||||
let fs = workspace.app_state().fs.clone();
|
||||
cx.update_global::<SettingsStore, _>(|store, _cx| {
|
||||
store.update_settings_file(fs, move |settings, _cx| {
|
||||
settings.git_panel.get_or_insert_default().sort_by = Some(GitPanelSortBy::Name);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn set_group_by_none(&mut self, _: &SetGroupByNone, _: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(workspace) = self.workspace.upgrade() {
|
||||
let workspace = workspace.read(cx);
|
||||
let fs = workspace.app_state().fs.clone();
|
||||
cx.update_global::<SettingsStore, _>(|store, _cx| {
|
||||
store.update_settings_file(fs, move |settings, _cx| {
|
||||
settings.git_panel.get_or_insert_default().group_by =
|
||||
Some(GitPanelGroupBy::None);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn set_group_by_status(
|
||||
&mut self,
|
||||
_: &SetGroupByStatus,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(workspace) = self.workspace.upgrade() {
|
||||
let workspace = workspace.read(cx);
|
||||
let fs = workspace.app_state().fs.clone();
|
||||
cx.update_global::<SettingsStore, _>(|store, _cx| {
|
||||
store.update_settings_file(fs, move |settings, _cx| {
|
||||
settings.git_panel.get_or_insert_default().group_by =
|
||||
Some(GitPanelGroupBy::Status);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -3559,8 +3674,9 @@ impl GitPanel {
|
|||
|
||||
fn toggle_directory(&mut self, key: &TreeKey, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(state) = self.view_mode.tree_state_mut() {
|
||||
let expanded = state.expanded_dirs.entry(key.clone()).or_insert(true);
|
||||
let expanded = state.expanded_dirs.entry(key.path.clone()).or_insert(true);
|
||||
*expanded = !*expanded;
|
||||
self.tree_expanded_dirs = state.expanded_dirs.clone();
|
||||
self.update_visible_entries(window, cx);
|
||||
} else {
|
||||
util::debug_panic!("Attempted to toggle directory in flat Git Panel state");
|
||||
|
|
@ -3718,9 +3834,10 @@ impl GitPanel {
|
|||
self.max_width_item_index = None;
|
||||
self.git_access = GitAccess::Yes;
|
||||
|
||||
let sort_by_path = GitPanelSettings::get_global(cx).sort_by_path;
|
||||
let settings = GitPanelSettings::get_global(cx);
|
||||
let sort_by = settings.sort_by;
|
||||
let group_by_status = settings.group_by == GitPanelGroupBy::Status;
|
||||
let is_tree_view = matches!(self.view_mode, GitPanelViewMode::Tree(_));
|
||||
let group_by_status = is_tree_view || !sort_by_path;
|
||||
|
||||
if let Some(active_repo) = self.active_repository.as_ref() {
|
||||
let access = active_repo.update(cx, |active_repo, cx| active_repo.access(cx));
|
||||
|
|
@ -3831,6 +3948,22 @@ impl GitPanel {
|
|||
self.single_tracked_entry = changed_entries.first().cloned();
|
||||
}
|
||||
|
||||
if !is_tree_view {
|
||||
let sort_entries = |entries: &mut Vec<GitStatusEntry>| match sort_by {
|
||||
GitPanelSortBy::Path => entries.sort_by(|a, b| a.repo_path.cmp(&b.repo_path)),
|
||||
GitPanelSortBy::Name => entries.sort_by(|a, b| {
|
||||
a.repo_path
|
||||
.file_name()
|
||||
.cmp(&b.repo_path.file_name())
|
||||
.then_with(|| a.repo_path.cmp(&b.repo_path))
|
||||
}),
|
||||
};
|
||||
|
||||
sort_entries(&mut conflict_entries);
|
||||
sort_entries(&mut changed_entries);
|
||||
sort_entries(&mut new_entries);
|
||||
}
|
||||
|
||||
let mut push_entry =
|
||||
|this: &mut Self,
|
||||
entry: GitListEntry,
|
||||
|
|
@ -3881,12 +4014,14 @@ impl GitPanel {
|
|||
continue;
|
||||
}
|
||||
|
||||
push_entry(
|
||||
self,
|
||||
GitListEntry::Header(GitHeaderEntry { header: section }),
|
||||
true,
|
||||
Some(&mut tree_state.logical_indices),
|
||||
);
|
||||
if section != Section::Tracked || group_by_status {
|
||||
push_entry(
|
||||
self,
|
||||
GitListEntry::Header(GitHeaderEntry { header: section }),
|
||||
true,
|
||||
Some(&mut tree_state.logical_indices),
|
||||
);
|
||||
}
|
||||
|
||||
for (entry, is_visible) in
|
||||
tree_state.build_tree_entries(section, entries, &mut seen_directories)
|
||||
|
|
@ -3900,9 +4035,14 @@ impl GitPanel {
|
|||
}
|
||||
}
|
||||
|
||||
let seen_directory_paths = seen_directories
|
||||
.iter()
|
||||
.map(|directory| directory.path.clone())
|
||||
.collect::<HashSet<_>>();
|
||||
tree_state
|
||||
.expanded_dirs
|
||||
.retain(|key, _| seen_directories.contains(key));
|
||||
.retain(|path, _| seen_directory_paths.contains(path));
|
||||
self.tree_expanded_dirs = tree_state.expanded_dirs.clone();
|
||||
self.view_mode = GitPanelViewMode::Tree(tree_state);
|
||||
}
|
||||
GitPanelViewMode::Flat => {
|
||||
|
|
@ -3911,7 +4051,7 @@ impl GitPanel {
|
|||
continue;
|
||||
}
|
||||
|
||||
if section != Section::Tracked || !sort_by_path {
|
||||
if section != Section::Tracked || group_by_status {
|
||||
push_entry(
|
||||
self,
|
||||
GitListEntry::Header(GitHeaderEntry { header: section }),
|
||||
|
|
@ -4291,7 +4431,42 @@ impl GitPanel {
|
|||
has_staged_changes,
|
||||
has_unstaged_changes,
|
||||
has_new_changes,
|
||||
sort_by_path: GitPanelSettings::get_global(cx).sort_by_path,
|
||||
sort_by: GitPanelSettings::get_global(cx).sort_by,
|
||||
group_by: GitPanelSettings::get_global(cx).group_by,
|
||||
has_stash_items,
|
||||
tree_view: GitPanelSettings::get_global(cx).tree_view,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
})
|
||||
.anchor(Anchor::TopRight)
|
||||
}
|
||||
|
||||
fn render_view_options_menu(&self, id: impl Into<ElementId>) -> impl IntoElement {
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
let has_tracked_changes = self.has_tracked_changes();
|
||||
let has_staged_changes = self.has_staged_changes();
|
||||
let has_unstaged_changes = self.has_unstaged_changes();
|
||||
let has_new_changes = self.new_count > 0;
|
||||
let has_stash_items = self.stash_entries.entries.len() > 0;
|
||||
|
||||
PopoverMenu::new(id.into())
|
||||
.trigger(
|
||||
IconButton::new("view-options-menu-trigger", IconName::Sliders)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(Tooltip::text("View Options")),
|
||||
)
|
||||
.menu(move |window, cx| {
|
||||
Some(git_panel_view_options_menu(
|
||||
focus_handle.clone(),
|
||||
GitMenuState {
|
||||
has_tracked_changes,
|
||||
has_staged_changes,
|
||||
has_unstaged_changes,
|
||||
has_new_changes,
|
||||
sort_by: GitPanelSettings::get_global(cx).sort_by,
|
||||
group_by: GitPanelSettings::get_global(cx).group_by,
|
||||
has_stash_items,
|
||||
tree_view: GitPanelSettings::get_global(cx).tree_view,
|
||||
},
|
||||
|
|
@ -4620,6 +4795,7 @@ impl GitPanel {
|
|||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(self.render_view_options_menu("view_options_menu"))
|
||||
.child(self.render_ellipsis_menu("overflow_menu"))
|
||||
.child(
|
||||
Button::new("stage_unstage_all", text)
|
||||
|
|
@ -6063,7 +6239,8 @@ impl GitPanel {
|
|||
has_staged_changes: self.has_staged_changes(),
|
||||
has_unstaged_changes: self.has_unstaged_changes(),
|
||||
has_new_changes: self.new_count > 0,
|
||||
sort_by_path: GitPanelSettings::get_global(cx).sort_by_path,
|
||||
sort_by: GitPanelSettings::get_global(cx).sort_by,
|
||||
group_by: GitPanelSettings::get_global(cx).group_by,
|
||||
has_stash_items: self.stash_entries.entries.len() > 0,
|
||||
tree_view: GitPanelSettings::get_global(cx).tree_view,
|
||||
},
|
||||
|
|
@ -6782,7 +6959,10 @@ impl Render for GitPanel {
|
|||
.when(has_write_access && has_co_authors, |git_panel| {
|
||||
git_panel.on_action(cx.listener(Self::toggle_fill_co_authors))
|
||||
})
|
||||
.on_action(cx.listener(Self::toggle_sort_by_path))
|
||||
.on_action(cx.listener(Self::set_sort_by_path))
|
||||
.on_action(cx.listener(Self::set_sort_by_name))
|
||||
.on_action(cx.listener(Self::set_group_by_none))
|
||||
.on_action(cx.listener(Self::set_group_by_status))
|
||||
.on_action(cx.listener(Self::toggle_tree_view))
|
||||
.on_action(cx.listener(Self::increase_font_size))
|
||||
.on_action(cx.listener(Self::decrease_font_size))
|
||||
|
|
@ -7766,7 +7946,7 @@ mod tests {
|
|||
cx.update(|_window, cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings(cx, |settings| {
|
||||
settings.git_panel.get_or_insert_default().sort_by_path = Some(true);
|
||||
settings.git_panel.get_or_insert_default().sort_by = Some(GitPanelSortBy::Path);
|
||||
})
|
||||
});
|
||||
});
|
||||
|
|
@ -8377,7 +8557,8 @@ mod tests {
|
|||
cx.update(|_window, cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings(cx, |settings| {
|
||||
settings.git_panel.get_or_insert_default().sort_by_path = Some(true);
|
||||
settings.git_panel.get_or_insert_default().group_by =
|
||||
Some(GitPanelGroupBy::None);
|
||||
})
|
||||
});
|
||||
});
|
||||
|
|
@ -8688,13 +8869,14 @@ mod tests {
|
|||
let cx = &mut VisualTestContext::from_window(window_handle.into(), cx);
|
||||
let panel = workspace.update_in(cx, GitPanel::new);
|
||||
|
||||
// Enable the `sort_by_path` setting and wait for entries to be updated,
|
||||
// Disable status grouping and wait for entries to be updated,
|
||||
// as there should no longer be separators between Tracked and Untracked
|
||||
// files.
|
||||
cx.update(|_window, cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings(cx, |settings| {
|
||||
settings.git_panel.get_or_insert_default().sort_by_path = Some(true);
|
||||
settings.git_panel.get_or_insert_default().group_by =
|
||||
Some(GitPanelGroupBy::None);
|
||||
})
|
||||
});
|
||||
});
|
||||
|
|
@ -8724,6 +8906,116 @@ mod tests {
|
|||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_tree_view_without_status_grouping_combines_statuses(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
json!({
|
||||
".git": {},
|
||||
"src": {
|
||||
"main.rs": "fn main() {}",
|
||||
"utils.rs": "pub fn util() {}",
|
||||
},
|
||||
"tests": {
|
||||
"main_test.rs": "#[test] fn test_main() {}",
|
||||
},
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
fs.set_status_for_repo(
|
||||
path!("/project/.git").as_ref(),
|
||||
&[
|
||||
("src/main.rs", StatusCode::Modified.worktree()),
|
||||
("src/utils.rs", FileStatus::Untracked),
|
||||
("tests/main_test.rs", StatusCode::Modified.worktree()),
|
||||
],
|
||||
);
|
||||
|
||||
let project = Project::test(fs.clone(), [Path::new(path!("/project"))], cx).await;
|
||||
let window_handle =
|
||||
cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
|
||||
let workspace = window_handle
|
||||
.read_with(cx, |mw, _| mw.workspace().clone())
|
||||
.unwrap();
|
||||
let cx = &mut VisualTestContext::from_window(window_handle.into(), cx);
|
||||
|
||||
cx.read(|cx| {
|
||||
project
|
||||
.read(cx)
|
||||
.worktrees(cx)
|
||||
.next()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.as_local()
|
||||
.unwrap()
|
||||
.scan_complete()
|
||||
})
|
||||
.await;
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
cx.update(|_window, cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings(cx, |settings| {
|
||||
let git_panel = settings.git_panel.get_or_insert_default();
|
||||
git_panel.tree_view = Some(true);
|
||||
git_panel.group_by = Some(GitPanelGroupBy::None);
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
let panel = workspace.update_in(cx, GitPanel::new);
|
||||
let handle = cx.update_window_entity(&panel, |panel, _, _| {
|
||||
std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(()))
|
||||
});
|
||||
|
||||
cx.executor().advance_clock(2 * UPDATE_DEBOUNCE);
|
||||
handle.await;
|
||||
|
||||
panel.read_with(cx, |panel, _| {
|
||||
assert!(
|
||||
panel
|
||||
.entries
|
||||
.iter()
|
||||
.all(|entry| !matches!(entry, GitListEntry::Header(_))),
|
||||
"status headers should not be shown when grouping is disabled",
|
||||
);
|
||||
|
||||
let tree_state = panel
|
||||
.view_mode
|
||||
.tree_state()
|
||||
.expect("tree view state should exist");
|
||||
let src_key = panel
|
||||
.entries
|
||||
.iter()
|
||||
.find_map(|entry| match entry {
|
||||
GitListEntry::Directory(dir) if dir.key.path == repo_path("src") => {
|
||||
Some(&dir.key)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.expect("src directory should exist in tree view");
|
||||
let src_descendants = tree_state
|
||||
.directory_descendants
|
||||
.get(src_key)
|
||||
.expect("src descendants should be tracked");
|
||||
|
||||
assert!(
|
||||
src_descendants
|
||||
.iter()
|
||||
.any(|entry| entry.repo_path == repo_path("src/main.rs"))
|
||||
);
|
||||
assert!(
|
||||
src_descendants
|
||||
.iter()
|
||||
.any(|entry| entry.repo_path == repo_path("src/utils.rs"))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_tree_view_reveals_collapsed_parent_on_select_entry_by_path(
|
||||
cx: &mut TestAppContext,
|
||||
|
|
@ -8816,7 +9108,7 @@ mod tests {
|
|||
.view_mode
|
||||
.tree_state()
|
||||
.expect("tree view state should exist");
|
||||
assert_eq!(state.expanded_dirs.get(&src_key).copied(), Some(false));
|
||||
assert_eq!(state.expanded_dirs.get(&src_key.path).copied(), Some(false));
|
||||
});
|
||||
|
||||
let worktree_id =
|
||||
|
|
@ -8835,7 +9127,7 @@ mod tests {
|
|||
.view_mode
|
||||
.tree_state()
|
||||
.expect("tree view state should exist");
|
||||
assert_eq!(state.expanded_dirs.get(&src_key).copied(), Some(true));
|
||||
assert_eq!(state.expanded_dirs.get(&src_key.path).copied(), Some(true));
|
||||
|
||||
let selected_ix = panel.selected_entry.expect("selection should be set");
|
||||
assert!(state.logical_indices.contains(&selected_ix));
|
||||
|
|
@ -8944,7 +9236,7 @@ mod tests {
|
|||
.view_mode
|
||||
.tree_state()
|
||||
.expect("tree view state should exist");
|
||||
assert_eq!(state.expanded_dirs.get(&foo_key).copied(), Some(false));
|
||||
assert_eq!(state.expanded_dirs.get(&foo_key.path).copied(), Some(false));
|
||||
|
||||
let foo_idx = panel
|
||||
.entries
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use editor::{EditorSettings, ui_scrollbar_settings_from_raw};
|
|||
use gpui::Pixels;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{RegisterSetting, Settings, StatusStyle};
|
||||
use settings::{GitPanelGroupBy, GitPanelSortBy, RegisterSetting, Settings, StatusStyle};
|
||||
use ui::{
|
||||
px,
|
||||
scrollbars::{ScrollbarVisibility, ShowScrollbar},
|
||||
|
|
@ -24,7 +24,8 @@ pub struct GitPanelSettings {
|
|||
pub folder_icons: bool,
|
||||
pub scrollbar: ScrollbarSettings,
|
||||
pub fallback_branch_name: String,
|
||||
pub sort_by_path: bool,
|
||||
pub sort_by: GitPanelSortBy,
|
||||
pub group_by: GitPanelGroupBy,
|
||||
pub collapse_untracked_diff: bool,
|
||||
pub tree_view: bool,
|
||||
pub diff_stats: bool,
|
||||
|
|
@ -71,7 +72,8 @@ impl Settings for GitPanelSettings {
|
|||
.map(ui_scrollbar_settings_from_raw),
|
||||
},
|
||||
fallback_branch_name: git_panel.fallback_branch_name.unwrap(),
|
||||
sort_by_path: git_panel.sort_by_path.unwrap(),
|
||||
sort_by: git_panel.sort_by.unwrap(),
|
||||
group_by: git_panel.group_by.unwrap(),
|
||||
collapse_untracked_diff: git_panel.collapse_untracked_diff.unwrap(),
|
||||
tree_view: git_panel.tree_view.unwrap(),
|
||||
diff_stats: git_panel.diff_stats.unwrap(),
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ use project::{
|
|||
branch_diff::{self, BranchDiffEvent, DiffBase},
|
||||
},
|
||||
};
|
||||
use settings::{Settings, SettingsStore};
|
||||
use settings::{GitPanelGroupBy, GitPanelSortBy, Settings, SettingsStore};
|
||||
use std::any::{Any, TypeId};
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
|
|
@ -92,10 +92,6 @@ pub struct ProjectDiff {
|
|||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
const CONFLICT_SORT_PREFIX: u64 = 1;
|
||||
const TRACKED_SORT_PREFIX: u64 = 2;
|
||||
const NEW_SORT_PREFIX: u64 = 3;
|
||||
|
||||
impl ProjectDiff {
|
||||
pub(crate) fn register(workspace: &mut Workspace, cx: &mut Context<Workspace>) {
|
||||
workspace.register_action(Self::deploy);
|
||||
|
|
@ -575,14 +571,20 @@ impl ProjectDiff {
|
|||
},
|
||||
);
|
||||
|
||||
let mut was_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path;
|
||||
let mut was_sort_by = GitPanelSettings::get_global(cx).sort_by;
|
||||
let mut was_group_by = GitPanelSettings::get_global(cx).group_by;
|
||||
let mut was_tree_view = GitPanelSettings::get_global(cx).tree_view;
|
||||
let mut was_collapse_untracked_diff =
|
||||
GitPanelSettings::get_global(cx).collapse_untracked_diff;
|
||||
cx.observe_global_in::<SettingsStore>(window, move |this, window, cx| {
|
||||
let is_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path;
|
||||
let is_collapse_untracked_diff =
|
||||
GitPanelSettings::get_global(cx).collapse_untracked_diff;
|
||||
if is_sort_by_path != was_sort_by_path
|
||||
let settings = GitPanelSettings::get_global(cx);
|
||||
let sort_by = settings.sort_by;
|
||||
let group_by = settings.group_by;
|
||||
let tree_view = settings.tree_view;
|
||||
let is_collapse_untracked_diff = settings.collapse_untracked_diff;
|
||||
if sort_by != was_sort_by
|
||||
|| group_by != was_group_by
|
||||
|| tree_view != was_tree_view
|
||||
|| is_collapse_untracked_diff != was_collapse_untracked_diff
|
||||
{
|
||||
this._task = {
|
||||
|
|
@ -592,7 +594,9 @@ impl ProjectDiff {
|
|||
})
|
||||
}
|
||||
}
|
||||
was_sort_by_path = is_sort_by_path;
|
||||
was_sort_by = sort_by;
|
||||
was_group_by = group_by;
|
||||
was_tree_view = tree_view;
|
||||
was_collapse_untracked_diff = is_collapse_untracked_diff;
|
||||
})
|
||||
.detach();
|
||||
|
|
@ -634,7 +638,7 @@ impl ProjectDiff {
|
|||
return;
|
||||
};
|
||||
let repo = git_repo.read(cx);
|
||||
let sort_prefix = sort_prefix(repo, &entry.repo_path, entry.status, cx);
|
||||
let sort_prefix = project_diff_sort_prefix(repo, &entry.repo_path, entry.status, cx);
|
||||
let path_key = PathKey::with_sort_prefix(sort_prefix, entry.repo_path.as_ref().clone());
|
||||
|
||||
self.move_to_path(path_key, window, cx)
|
||||
|
|
@ -660,7 +664,7 @@ impl ProjectDiff {
|
|||
.status_for_path(&repo_path)
|
||||
.map(|entry| entry.status)
|
||||
.unwrap_or(FileStatus::Untracked);
|
||||
let sort_prefix = sort_prefix(&git_repo.read(cx), &repo_path, status, cx);
|
||||
let sort_prefix = project_diff_sort_prefix(&git_repo.read(cx), &repo_path, status, cx);
|
||||
let path_key = PathKey::with_sort_prefix(sort_prefix, repo_path.as_ref().clone());
|
||||
self.move_to_path(path_key, window, cx)
|
||||
}
|
||||
|
|
@ -817,7 +821,7 @@ impl ProjectDiff {
|
|||
conflict_set: Entity<ConflictSet>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<BufferId> {
|
||||
) -> (BufferId, bool) {
|
||||
let diff_subscription = cx.subscribe_in(&diff, window, {
|
||||
let path_key = path_key.clone();
|
||||
let buffer = buffer.clone();
|
||||
|
|
@ -890,7 +894,8 @@ impl ProjectDiff {
|
|||
}
|
||||
};
|
||||
|
||||
let mut needs_fold = None;
|
||||
let buffer_id = snapshot.text.remote_id();
|
||||
let mut needs_fold = false;
|
||||
|
||||
let (was_empty, is_excerpt_newly_added) = self.editor.update(cx, |editor, cx| {
|
||||
let was_empty = editor.rhs_editor().read(cx).buffer().read(cx).is_empty();
|
||||
|
|
@ -927,7 +932,7 @@ impl ProjectDiff {
|
|||
|| (file_status.is_untracked()
|
||||
&& GitPanelSettings::get_global(cx).collapse_untracked_diff))
|
||||
{
|
||||
needs_fold = Some(snapshot.text.remote_id());
|
||||
needs_fold = true;
|
||||
}
|
||||
})
|
||||
});
|
||||
|
|
@ -949,7 +954,7 @@ impl ProjectDiff {
|
|||
self.move_to_path(path_key, window, cx);
|
||||
}
|
||||
|
||||
needs_fold
|
||||
(buffer_id, needs_fold)
|
||||
}
|
||||
|
||||
fn buffer_ranges_changed(
|
||||
|
|
@ -990,14 +995,38 @@ impl ProjectDiff {
|
|||
.buffers_with_paths()
|
||||
.map(|(buffer_snapshot, path_key)| (path_key.clone(), buffer_snapshot.remote_id()))
|
||||
.collect::<HashMap<_, _>>();
|
||||
let previous_folded_paths = {
|
||||
let snapshot = this.multibuffer.read(cx).snapshot(cx);
|
||||
let rhs_editor = this.editor.read(cx).rhs_editor().clone();
|
||||
snapshot
|
||||
.buffers_with_paths()
|
||||
.map(|(buffer_snapshot, path_key)| {
|
||||
(
|
||||
path_key.path.clone(),
|
||||
rhs_editor
|
||||
.read(cx)
|
||||
.is_buffer_folded(buffer_snapshot.remote_id(), cx),
|
||||
)
|
||||
})
|
||||
.collect::<HashMap<_, _>>()
|
||||
};
|
||||
|
||||
let mut entries = BTreeMap::new();
|
||||
if let Some(repo) = repo {
|
||||
let repo = repo.read(cx);
|
||||
let sort_prefixes = project_diff_sort_prefixes(
|
||||
&repo,
|
||||
buffers_to_load
|
||||
.iter()
|
||||
.map(|diff_buffer| (&diff_buffer.repo_path, diff_buffer.file_status)),
|
||||
cx,
|
||||
);
|
||||
|
||||
for diff_buffer in buffers_to_load {
|
||||
let sort_prefix =
|
||||
sort_prefix(&repo, &diff_buffer.repo_path, diff_buffer.file_status, cx);
|
||||
let sort_prefix = sort_prefixes
|
||||
.get(&diff_buffer.repo_path)
|
||||
.copied()
|
||||
.unwrap_or(u64::MAX);
|
||||
let path_key = PathKey::with_sort_prefix(
|
||||
sort_prefix,
|
||||
diff_buffer.repo_path.as_ref().clone(),
|
||||
|
|
@ -1019,10 +1048,12 @@ impl ProjectDiff {
|
|||
}
|
||||
});
|
||||
|
||||
entries
|
||||
(entries, previous_folded_paths)
|
||||
})?;
|
||||
|
||||
let (entries, previous_folded_paths) = entries;
|
||||
let mut buffers_to_fold = Vec::new();
|
||||
let mut buffers_to_unfold = Vec::new();
|
||||
|
||||
for (path_key, entry) in entries {
|
||||
if let Some((buffer, diff, conflict_set)) = entry.load.await.log_err() {
|
||||
|
|
@ -1031,7 +1062,8 @@ impl ProjectDiff {
|
|||
yield_now().await;
|
||||
cx.update(|window, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
if let Some(buffer_id) = this.register_buffer(
|
||||
let path = path_key.path.clone();
|
||||
let (buffer_id, needs_fold) = this.register_buffer(
|
||||
path_key,
|
||||
entry.file_status,
|
||||
buffer,
|
||||
|
|
@ -1039,7 +1071,14 @@ impl ProjectDiff {
|
|||
conflict_set,
|
||||
window,
|
||||
cx,
|
||||
) {
|
||||
);
|
||||
if let Some(was_folded) = previous_folded_paths.get(&path) {
|
||||
if *was_folded {
|
||||
buffers_to_fold.push(buffer_id);
|
||||
} else {
|
||||
buffers_to_unfold.push(buffer_id);
|
||||
}
|
||||
} else if needs_fold {
|
||||
buffers_to_fold.push(buffer_id);
|
||||
}
|
||||
})
|
||||
|
|
@ -1055,6 +1094,15 @@ impl ProjectDiff {
|
|||
.update(cx, |editor, cx| editor.fold_buffers(buffers_to_fold, cx));
|
||||
});
|
||||
}
|
||||
if !buffers_to_unfold.is_empty() {
|
||||
this.editor.update(cx, |editor, cx| {
|
||||
editor.rhs_editor().update(cx, |editor, cx| {
|
||||
for buffer_id in buffers_to_unfold {
|
||||
editor.unfold_buffer(buffer_id, cx);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
this.pending_scroll.take();
|
||||
cx.notify();
|
||||
})?;
|
||||
|
|
@ -1085,17 +1133,125 @@ impl ProjectDiff {
|
|||
}
|
||||
}
|
||||
|
||||
fn sort_prefix(repo: &Repository, repo_path: &RepoPath, status: FileStatus, cx: &App) -> u64 {
|
||||
let settings = GitPanelSettings::get_global(cx);
|
||||
fn project_diff_sort_prefix(
|
||||
repo: &Repository,
|
||||
repo_path: &RepoPath,
|
||||
status: FileStatus,
|
||||
cx: &App,
|
||||
) -> u64 {
|
||||
let mut entries = repo
|
||||
.cached_status()
|
||||
.map(|entry| (entry.repo_path.clone(), entry.status))
|
||||
.collect::<Vec<_>>();
|
||||
if !entries
|
||||
.iter()
|
||||
.any(|(entry_path, _status)| entry_path == repo_path)
|
||||
{
|
||||
entries.push((repo_path.clone(), status));
|
||||
}
|
||||
|
||||
if settings.sort_by_path && !settings.tree_view {
|
||||
TRACKED_SORT_PREFIX
|
||||
} else if repo.had_conflict_on_last_merge_head_change(repo_path) {
|
||||
CONFLICT_SORT_PREFIX
|
||||
} else if status.is_created() {
|
||||
NEW_SORT_PREFIX
|
||||
} else {
|
||||
TRACKED_SORT_PREFIX
|
||||
project_diff_sort_prefixes(
|
||||
repo,
|
||||
entries.iter().map(|(path, status)| (path, *status)),
|
||||
cx,
|
||||
)
|
||||
.get(repo_path)
|
||||
.copied()
|
||||
.unwrap_or(u64::MAX)
|
||||
}
|
||||
|
||||
fn project_diff_sort_prefixes<'a>(
|
||||
repo: &Repository,
|
||||
entries: impl IntoIterator<Item = (&'a RepoPath, FileStatus)>,
|
||||
cx: &App,
|
||||
) -> HashMap<RepoPath, u64> {
|
||||
let settings = GitPanelSettings::get_global(cx);
|
||||
let group_by_status = settings.group_by == GitPanelGroupBy::Status;
|
||||
|
||||
let mut conflict_entries = Vec::new();
|
||||
let mut tracked_entries = Vec::new();
|
||||
let mut new_entries = Vec::new();
|
||||
|
||||
for (repo_path, status) in entries {
|
||||
let entry = (repo_path.clone(), status);
|
||||
if group_by_status && repo.had_conflict_on_last_merge_head_change(repo_path) {
|
||||
conflict_entries.push(entry);
|
||||
} else if group_by_status && status.is_created() {
|
||||
new_entries.push(entry);
|
||||
} else {
|
||||
tracked_entries.push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
let mut ordered_paths = Vec::new();
|
||||
for mut section_entries in [conflict_entries, tracked_entries, new_entries] {
|
||||
if settings.tree_view {
|
||||
append_tree_order(&mut ordered_paths, section_entries);
|
||||
} else {
|
||||
sort_flat_entries(&mut section_entries, settings.sort_by);
|
||||
ordered_paths.extend(
|
||||
section_entries
|
||||
.into_iter()
|
||||
.map(|(repo_path, _status)| repo_path),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ordered_paths
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, repo_path)| (repo_path, index as u64 + 1))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn sort_flat_entries(entries: &mut [(RepoPath, FileStatus)], sort_by: GitPanelSortBy) {
|
||||
match sort_by {
|
||||
GitPanelSortBy::Path => entries.sort_by(|(a_path, _), (b_path, _)| a_path.cmp(b_path)),
|
||||
GitPanelSortBy::Name => entries.sort_by(|(a_path, _), (b_path, _)| {
|
||||
a_path
|
||||
.file_name()
|
||||
.cmp(&b_path.file_name())
|
||||
.then_with(|| a_path.cmp(b_path))
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn append_tree_order(ordered_paths: &mut Vec<RepoPath>, mut entries: Vec<(RepoPath, FileStatus)>) {
|
||||
entries.sort_by(|(a_path, _), (b_path, _)| a_path.cmp(b_path));
|
||||
|
||||
let mut root = ProjectDiffTreeNode::default();
|
||||
for (repo_path, _status) in entries {
|
||||
let components: Vec<&str> = repo_path.components().collect();
|
||||
if components.is_empty() {
|
||||
root.files.push(repo_path);
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut current = &mut root;
|
||||
for (index, component) in components.iter().enumerate() {
|
||||
if index == components.len() - 1 {
|
||||
current.files.push(repo_path.clone());
|
||||
} else {
|
||||
current = current.children.entry((*component).into()).or_default();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
root.append_ordered_paths(ordered_paths);
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ProjectDiffTreeNode {
|
||||
children: BTreeMap<String, ProjectDiffTreeNode>,
|
||||
files: Vec<RepoPath>,
|
||||
}
|
||||
|
||||
impl ProjectDiffTreeNode {
|
||||
fn append_ordered_paths(self, ordered_paths: &mut Vec<RepoPath>) {
|
||||
for child in self.children.into_values() {
|
||||
child.append_ordered_paths(ordered_paths);
|
||||
}
|
||||
ordered_paths.extend(self.files);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2119,7 +2275,7 @@ mod tests {
|
|||
|
||||
let editor = cx.update_window_entity(&diff, |diff, window, cx| {
|
||||
diff.move_to_path(
|
||||
PathKey::with_sort_prefix(TRACKED_SORT_PREFIX, rel_path("foo").into_arc()),
|
||||
PathKey::with_sort_prefix(2, rel_path("foo").into_arc()),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
|
@ -2140,7 +2296,7 @@ mod tests {
|
|||
|
||||
let editor = cx.update_window_entity(&diff, |diff, window, cx| {
|
||||
diff.move_to_path(
|
||||
PathKey::with_sort_prefix(TRACKED_SORT_PREFIX, rel_path("bar").into_arc()),
|
||||
PathKey::with_sort_prefix(1, rel_path("bar").into_arc()),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
|
@ -2667,7 +2823,15 @@ mod tests {
|
|||
&editor,
|
||||
cx,
|
||||
&"
|
||||
ˇnine
|
||||
ˇone
|
||||
two
|
||||
three
|
||||
four
|
||||
five
|
||||
six
|
||||
seven
|
||||
eight
|
||||
nine
|
||||
ten
|
||||
- eleven
|
||||
+ ELEVEN
|
||||
|
|
@ -2714,13 +2878,16 @@ mod tests {
|
|||
&editor,
|
||||
cx,
|
||||
&"
|
||||
one
|
||||
ˇone
|
||||
- two
|
||||
+ TWO
|
||||
three
|
||||
four
|
||||
five
|
||||
ˇnine
|
||||
six
|
||||
seven
|
||||
eight
|
||||
nine
|
||||
ten
|
||||
- eleven
|
||||
+ ELEVEN
|
||||
|
|
|
|||
|
|
@ -678,11 +678,15 @@ pub struct GitPanelSettingsContent {
|
|||
/// Default: main
|
||||
pub fallback_branch_name: Option<String>,
|
||||
|
||||
/// Whether to sort entries in the panel by path
|
||||
/// or by status (the default).
|
||||
/// How to sort entries in the git panel.
|
||||
///
|
||||
/// Default: false
|
||||
pub sort_by_path: Option<bool>,
|
||||
/// Default: path
|
||||
pub sort_by: Option<GitPanelSortBy>,
|
||||
|
||||
/// How to group entries in the git panel.
|
||||
///
|
||||
/// Default: status
|
||||
pub group_by: Option<GitPanelGroupBy>,
|
||||
|
||||
/// Whether to collapse untracked files in the diff panel.
|
||||
///
|
||||
|
|
@ -716,6 +720,48 @@ pub struct GitPanelSettingsContent {
|
|||
pub commit_title_max_length: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Copy,
|
||||
Clone,
|
||||
Debug,
|
||||
Default,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
JsonSchema,
|
||||
MergeFrom,
|
||||
PartialEq,
|
||||
Eq,
|
||||
strum::VariantArray,
|
||||
strum::VariantNames,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GitPanelSortBy {
|
||||
#[default]
|
||||
Path,
|
||||
Name,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Copy,
|
||||
Clone,
|
||||
Debug,
|
||||
Default,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
JsonSchema,
|
||||
MergeFrom,
|
||||
PartialEq,
|
||||
Eq,
|
||||
strum::VariantArray,
|
||||
strum::VariantNames,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GitPanelGroupBy {
|
||||
None,
|
||||
#[default]
|
||||
Status,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Default,
|
||||
Copy,
|
||||
|
|
|
|||
|
|
@ -5776,7 +5776,7 @@ fn panels_page() -> SettingsPage {
|
|||
]
|
||||
}
|
||||
|
||||
fn git_panel_section() -> [SettingsPageItem; 15] {
|
||||
fn git_panel_section() -> [SettingsPageItem; 16] {
|
||||
[
|
||||
SettingsPageItem::SectionHeader("Git Panel"),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
|
|
@ -5869,19 +5869,28 @@ fn panels_page() -> SettingsPage {
|
|||
files: USER,
|
||||
}),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Sort By Path",
|
||||
description: "Enable to sort entries in the panel by path, disable to sort by status.",
|
||||
title: "Sort By",
|
||||
description: "How to sort entries in the git panel.",
|
||||
field: Box::new(SettingField {
|
||||
organization_override: None,
|
||||
json_path: Some("git_panel.sort_by_path"),
|
||||
pick: |settings_content| {
|
||||
settings_content.git_panel.as_ref()?.sort_by_path.as_ref()
|
||||
},
|
||||
json_path: Some("git_panel.sort_by"),
|
||||
pick: |settings_content| settings_content.git_panel.as_ref()?.sort_by.as_ref(),
|
||||
write: |settings_content, value, _| {
|
||||
settings_content
|
||||
.git_panel
|
||||
.get_or_insert_default()
|
||||
.sort_by_path = value;
|
||||
settings_content.git_panel.get_or_insert_default().sort_by = value;
|
||||
},
|
||||
}),
|
||||
metadata: None,
|
||||
files: USER,
|
||||
}),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Group By",
|
||||
description: "How to group entries in the git panel.",
|
||||
field: Box::new(SettingField {
|
||||
organization_override: None,
|
||||
json_path: Some("git_panel.group_by"),
|
||||
pick: |settings_content| settings_content.git_panel.as_ref()?.group_by.as_ref(),
|
||||
write: |settings_content, value, _| {
|
||||
settings_content.git_panel.get_or_insert_default().group_by = value;
|
||||
},
|
||||
}),
|
||||
metadata: None,
|
||||
|
|
|
|||
|
|
@ -600,6 +600,8 @@ fn init_renderers(cx: &mut App) {
|
|||
.add_basic_renderer::<settings::ThinkingBlockDisplay>(render_dropdown)
|
||||
.add_basic_renderer::<settings::ImageFileSizeUnit>(render_dropdown)
|
||||
.add_basic_renderer::<settings::StatusStyle>(render_dropdown)
|
||||
.add_basic_renderer::<settings::GitPanelSortBy>(render_dropdown)
|
||||
.add_basic_renderer::<settings::GitPanelGroupBy>(render_dropdown)
|
||||
.add_basic_renderer::<settings::EncodingDisplayOptions>(render_dropdown)
|
||||
.add_basic_renderer::<settings::PaneSplitDirectionHorizontal>(render_dropdown)
|
||||
.add_basic_renderer::<settings::PaneSplitDirectionVertical>(render_dropdown)
|
||||
|
|
|
|||
|
|
@ -5386,7 +5386,8 @@ See the [debugger page](../debugger.md) for more information about debugging sup
|
|||
"default_width": 360,
|
||||
"status_style": "icon",
|
||||
"fallback_branch_name": "main",
|
||||
"sort_by_path": false,
|
||||
"sort_by": "path",
|
||||
"group_by": "status",
|
||||
"collapse_untracked_diff": false,
|
||||
"scrollbar": {
|
||||
"show": null
|
||||
|
|
@ -5403,7 +5404,8 @@ See the [debugger page](../debugger.md) for more information about debugging sup
|
|||
- `default_width`: Default width of the git panel
|
||||
- `status_style`: How to display git status. Can be `label_color` or `icon`
|
||||
- `fallback_branch_name`: What branch name to use if `init.defaultBranch` is not set
|
||||
- `sort_by_path`: Whether to sort entries in the panel by path or by status (the default)
|
||||
- `sort_by`: How to sort entries in the git panel. Can be `path` or `name`
|
||||
- `group_by`: How to group entries in the git panel. Can be `none` or `status`
|
||||
- `collapse_untracked_diff`: Whether to collapse untracked files in the diff panel
|
||||
- `scrollbar`: When to show the scrollbar in the git panel
|
||||
- `starts_open`: Whether the git panel should open on startup
|
||||
|
|
|
|||
|
|
@ -559,7 +559,8 @@ See [Terminal settings](./reference/all-settings.md#terminal) for additional non
|
|||
"dock": "left", // Where to dock: left, right
|
||||
"default_width": 360, // Default width of the git panel.
|
||||
"status_style": "icon", // label_color, icon
|
||||
"sort_by_path": false, // Sort by path (false) or status (true)
|
||||
"sort_by": "path", // path, name
|
||||
"group_by": "status", // none, status
|
||||
"scrollbar": {
|
||||
"show": null // Show/hide: (auto, system, always, never)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue