mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
project_panel: Add sort mode (#40160)
Closes #4533 (partly at least) Release Notes: - Added `project_panel.sort_mode` option to control explorer file sort (directories first, mixed, files first) ## Summary Adds three sorting modes for the project panel to give users more control over how files and directories are displayed: - **`directories_first`** (default): Current behaviour - directories grouped before files - **`mixed`**: Files and directories sorted together alphabetically - **`files_first`**: filed grouped before directories ## Motivation Users coming from different editors and file managers have different expectations for file sorting. Some prefer directories grouped at the top (traditional), while others prefer the macOS Finder-style mixed sorting where "Apple1/", "apple2.tsx" and "Apple3/" appear alphabetically mixed together. ### Screenshots New sort options in settings: <img width="515" height="160" alt="image" src="https://github.com/user-attachments/assets/8f4e6668-6989-4881-a9bd-ed1f4f0beb40" /> Directories first | Mixed | Files first -------------|-----|----- <img width="328" height="888" alt="image" src="https://github.com/user-attachments/assets/308e5c7a-6e6a-46ba-813d-6e268222925c" /> | <img width="327" height="891" alt="image" src="https://github.com/user-attachments/assets/8274d8ca-b60f-456e-be36-e35a3259483c" /> | <img width="328" height="890" alt="image" src="https://github.com/user-attachments/assets/3c3b1332-cf08-4eaf-9bed-527c00b41529" /> ### Agent usage Copilot-cli/claude-code/codex-cli helped out a lot. I'm not from a rust background, but really wanted this solved, and it gave me a chance to play with some of the coding agents I'm not permitted to use for work stuff --------- Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
This commit is contained in:
parent
175162af4f
commit
a2d3e3baf9
12 changed files with 849 additions and 44 deletions
|
|
@ -742,6 +742,16 @@
|
|||
// "never"
|
||||
"show": "always"
|
||||
},
|
||||
// Sort order for entries in the project panel.
|
||||
// This setting can take three values:
|
||||
//
|
||||
// 1. Show directories first, then files:
|
||||
// "directories_first"
|
||||
// 2. Mix directories and files together:
|
||||
// "mixed"
|
||||
// 3. Show files first, then directories:
|
||||
// "files_first"
|
||||
"sort_mode": "directories_first",
|
||||
// Whether to enable drag-and-drop operations in the project panel.
|
||||
"drag_and_drop": true,
|
||||
// Whether to hide the root entry when only one folder is open in the window.
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
use criterion::{Criterion, criterion_group, criterion_main};
|
||||
use project::{Entry, EntryKind, GitEntry, ProjectEntryId};
|
||||
use project_panel::par_sort_worktree_entries;
|
||||
use project_panel::par_sort_worktree_entries_with_mode;
|
||||
use settings::ProjectPanelSortMode;
|
||||
use std::sync::Arc;
|
||||
use util::rel_path::RelPath;
|
||||
|
||||
fn load_linux_repo_snapshot() -> Vec<GitEntry> {
|
||||
let file = std::fs::read_to_string(
|
||||
"/Users/hiro/Projects/zed/crates/project_panel/benches/linux_repo_snapshot.txt",
|
||||
)
|
||||
let file = std::fs::read_to_string(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/benches/linux_repo_snapshot.txt"
|
||||
))
|
||||
.expect("Failed to read file");
|
||||
file.lines()
|
||||
.filter_map(|line| {
|
||||
|
|
@ -42,10 +44,36 @@ fn load_linux_repo_snapshot() -> Vec<GitEntry> {
|
|||
}
|
||||
fn criterion_benchmark(c: &mut Criterion) {
|
||||
let snapshot = load_linux_repo_snapshot();
|
||||
|
||||
c.bench_function("Sort linux worktree snapshot", |b| {
|
||||
b.iter_batched(
|
||||
|| snapshot.clone(),
|
||||
|mut snapshot| par_sort_worktree_entries(&mut snapshot),
|
||||
|mut snapshot| {
|
||||
par_sort_worktree_entries_with_mode(
|
||||
&mut snapshot,
|
||||
ProjectPanelSortMode::DirectoriesFirst,
|
||||
)
|
||||
},
|
||||
criterion::BatchSize::LargeInput,
|
||||
);
|
||||
});
|
||||
|
||||
c.bench_function("Sort linux worktree snapshot (Mixed)", |b| {
|
||||
b.iter_batched(
|
||||
|| snapshot.clone(),
|
||||
|mut snapshot| {
|
||||
par_sort_worktree_entries_with_mode(&mut snapshot, ProjectPanelSortMode::Mixed)
|
||||
},
|
||||
criterion::BatchSize::LargeInput,
|
||||
);
|
||||
});
|
||||
|
||||
c.bench_function("Sort linux worktree snapshot (FilesFirst)", |b| {
|
||||
b.iter_batched(
|
||||
|| snapshot.clone(),
|
||||
|mut snapshot| {
|
||||
par_sort_worktree_entries_with_mode(&mut snapshot, ProjectPanelSortMode::FilesFirst)
|
||||
},
|
||||
criterion::BatchSize::LargeInput,
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -703,6 +703,9 @@ impl ProjectPanel {
|
|||
if project_panel_settings.hide_hidden != new_settings.hide_hidden {
|
||||
this.update_visible_entries(None, false, false, window, cx);
|
||||
}
|
||||
if project_panel_settings.sort_mode != new_settings.sort_mode {
|
||||
this.update_visible_entries(None, false, false, window, cx);
|
||||
}
|
||||
if project_panel_settings.sticky_scroll && !new_settings.sticky_scroll {
|
||||
this.sticky_items_count = 0;
|
||||
}
|
||||
|
|
@ -2102,7 +2105,8 @@ impl ProjectPanel {
|
|||
.map(|entry| entry.to_owned())
|
||||
.collect();
|
||||
|
||||
sort_worktree_entries(&mut siblings);
|
||||
let mode = ProjectPanelSettings::get_global(cx).sort_mode;
|
||||
sort_worktree_entries_with_mode(&mut siblings, mode);
|
||||
let sibling_entry_index = siblings
|
||||
.iter()
|
||||
.position(|sibling| sibling.id == latest_entry.id)?;
|
||||
|
|
@ -3229,6 +3233,7 @@ impl ProjectPanel {
|
|||
let settings = ProjectPanelSettings::get_global(cx);
|
||||
let auto_collapse_dirs = settings.auto_fold_dirs;
|
||||
let hide_gitignore = settings.hide_gitignore;
|
||||
let sort_mode = settings.sort_mode;
|
||||
let project = self.project.read(cx);
|
||||
let repo_snapshots = project.git_store().read(cx).repo_snapshots(cx);
|
||||
|
||||
|
|
@ -3440,7 +3445,10 @@ impl ProjectPanel {
|
|||
entry_iter.advance();
|
||||
}
|
||||
|
||||
par_sort_worktree_entries(&mut visible_worktree_entries);
|
||||
par_sort_worktree_entries_with_mode(
|
||||
&mut visible_worktree_entries,
|
||||
sort_mode,
|
||||
);
|
||||
new_state.visible_entries.push(VisibleEntriesForWorktree {
|
||||
worktree_id,
|
||||
entries: visible_worktree_entries,
|
||||
|
|
@ -6101,21 +6109,42 @@ impl ClipboardEntry {
|
|||
}
|
||||
}
|
||||
|
||||
fn cmp<T: AsRef<Entry>>(lhs: T, rhs: T) -> cmp::Ordering {
|
||||
let entry_a = lhs.as_ref();
|
||||
let entry_b = rhs.as_ref();
|
||||
util::paths::compare_rel_paths(
|
||||
(&entry_a.path, entry_a.is_file()),
|
||||
(&entry_b.path, entry_b.is_file()),
|
||||
)
|
||||
#[inline]
|
||||
fn cmp_directories_first(a: &Entry, b: &Entry) -> cmp::Ordering {
|
||||
util::paths::compare_rel_paths((&a.path, a.is_file()), (&b.path, b.is_file()))
|
||||
}
|
||||
|
||||
pub fn sort_worktree_entries(entries: &mut [impl AsRef<Entry>]) {
|
||||
entries.sort_by(|lhs, rhs| cmp(lhs, rhs));
|
||||
#[inline]
|
||||
fn cmp_mixed(a: &Entry, b: &Entry) -> cmp::Ordering {
|
||||
util::paths::compare_rel_paths_mixed((&a.path, a.is_file()), (&b.path, b.is_file()))
|
||||
}
|
||||
|
||||
pub fn par_sort_worktree_entries(entries: &mut Vec<GitEntry>) {
|
||||
entries.par_sort_by(|lhs, rhs| cmp(lhs, rhs));
|
||||
#[inline]
|
||||
fn cmp_files_first(a: &Entry, b: &Entry) -> cmp::Ordering {
|
||||
util::paths::compare_rel_paths_files_first((&a.path, a.is_file()), (&b.path, b.is_file()))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn cmp_with_mode(a: &Entry, b: &Entry, mode: &settings::ProjectPanelSortMode) -> cmp::Ordering {
|
||||
match mode {
|
||||
settings::ProjectPanelSortMode::DirectoriesFirst => cmp_directories_first(a, b),
|
||||
settings::ProjectPanelSortMode::Mixed => cmp_mixed(a, b),
|
||||
settings::ProjectPanelSortMode::FilesFirst => cmp_files_first(a, b),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sort_worktree_entries_with_mode(
|
||||
entries: &mut [impl AsRef<Entry>],
|
||||
mode: settings::ProjectPanelSortMode,
|
||||
) {
|
||||
entries.sort_by(|lhs, rhs| cmp_with_mode(lhs.as_ref(), rhs.as_ref(), &mode));
|
||||
}
|
||||
|
||||
pub fn par_sort_worktree_entries_with_mode(
|
||||
entries: &mut Vec<GitEntry>,
|
||||
mode: settings::ProjectPanelSortMode,
|
||||
) {
|
||||
entries.par_sort_by(|lhs, rhs| cmp_with_mode(lhs, rhs, &mode));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ use gpui::Pixels;
|
|||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{
|
||||
DockSide, ProjectPanelEntrySpacing, RegisterSetting, Settings, ShowDiagnostics,
|
||||
ShowIndentGuides,
|
||||
DockSide, ProjectPanelEntrySpacing, ProjectPanelSortMode, RegisterSetting, Settings,
|
||||
ShowDiagnostics, ShowIndentGuides,
|
||||
};
|
||||
use ui::{
|
||||
px,
|
||||
|
|
@ -33,6 +33,7 @@ pub struct ProjectPanelSettings {
|
|||
pub hide_hidden: bool,
|
||||
pub drag_and_drop: bool,
|
||||
pub auto_open: AutoOpenSettings,
|
||||
pub sort_mode: ProjectPanelSortMode,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
|
|
@ -115,6 +116,9 @@ impl Settings for ProjectPanelSettings {
|
|||
on_drop: auto_open.on_drop.unwrap(),
|
||||
}
|
||||
},
|
||||
sort_mode: project_panel
|
||||
.sort_mode
|
||||
.unwrap_or(ProjectPanelSortMode::DirectoriesFirst),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -326,6 +326,7 @@ async fn test_auto_collapse_dir_paths(cx: &mut gpui::TestAppContext) {
|
|||
ProjectPanelSettings::override_global(
|
||||
ProjectPanelSettings {
|
||||
auto_fold_dirs: true,
|
||||
sort_mode: settings::ProjectPanelSortMode::DirectoriesFirst,
|
||||
..settings
|
||||
},
|
||||
cx,
|
||||
|
|
@ -7704,6 +7705,215 @@ fn visible_entries_as_strings(
|
|||
result
|
||||
}
|
||||
|
||||
/// Test that missing sort_mode field defaults to DirectoriesFirst
|
||||
#[gpui::test]
|
||||
async fn test_sort_mode_default_fallback(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
// Verify that when sort_mode is not specified, it defaults to DirectoriesFirst
|
||||
let default_settings = cx.read(|cx| *ProjectPanelSettings::get_global(cx));
|
||||
assert_eq!(
|
||||
default_settings.sort_mode,
|
||||
settings::ProjectPanelSortMode::DirectoriesFirst,
|
||||
"sort_mode should default to DirectoriesFirst"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test sort modes: DirectoriesFirst (default) vs Mixed
|
||||
#[gpui::test]
|
||||
async fn test_sort_mode_directories_first(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"zebra.txt": "",
|
||||
"Apple": {},
|
||||
"banana.rs": "",
|
||||
"Carrot": {},
|
||||
"aardvark.txt": "",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
|
||||
let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
let panel = workspace.update(cx, ProjectPanel::new).unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
// Default sort mode should be DirectoriesFirst
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..50, cx),
|
||||
&[
|
||||
"v root",
|
||||
" > Apple",
|
||||
" > Carrot",
|
||||
" aardvark.txt",
|
||||
" banana.rs",
|
||||
" zebra.txt",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_sort_mode_mixed(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"Zebra.txt": "",
|
||||
"apple": {},
|
||||
"Banana.rs": "",
|
||||
"carrot": {},
|
||||
"Aardvark.txt": "",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
|
||||
let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
|
||||
// Switch to Mixed mode
|
||||
cx.update(|_, cx| {
|
||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||
store.update_user_settings(cx, |settings| {
|
||||
settings.project_panel.get_or_insert_default().sort_mode =
|
||||
Some(settings::ProjectPanelSortMode::Mixed);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
let panel = workspace.update(cx, ProjectPanel::new).unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
// Mixed mode: case-insensitive sorting
|
||||
// Aardvark < apple < Banana < carrot < Zebra (all case-insensitive)
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..50, cx),
|
||||
&[
|
||||
"v root",
|
||||
" Aardvark.txt",
|
||||
" > apple",
|
||||
" Banana.rs",
|
||||
" > carrot",
|
||||
" Zebra.txt",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_sort_mode_files_first(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"Zebra.txt": "",
|
||||
"apple": {},
|
||||
"Banana.rs": "",
|
||||
"carrot": {},
|
||||
"Aardvark.txt": "",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
|
||||
let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
|
||||
// Switch to FilesFirst mode
|
||||
cx.update(|_, cx| {
|
||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||
store.update_user_settings(cx, |settings| {
|
||||
settings.project_panel.get_or_insert_default().sort_mode =
|
||||
Some(settings::ProjectPanelSortMode::FilesFirst);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
let panel = workspace.update(cx, ProjectPanel::new).unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
// FilesFirst mode: files first, then directories (both case-insensitive)
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..50, cx),
|
||||
&[
|
||||
"v root",
|
||||
" Aardvark.txt",
|
||||
" Banana.rs",
|
||||
" Zebra.txt",
|
||||
" > apple",
|
||||
" > carrot",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_sort_mode_toggle(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"file2.txt": "",
|
||||
"dir1": {},
|
||||
"file1.txt": "",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
|
||||
let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
let panel = workspace.update(cx, ProjectPanel::new).unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
// Initially DirectoriesFirst
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..50, cx),
|
||||
&["v root", " > dir1", " file1.txt", " file2.txt",]
|
||||
);
|
||||
|
||||
// Toggle to Mixed
|
||||
cx.update(|_, cx| {
|
||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||
store.update_user_settings(cx, |settings| {
|
||||
settings.project_panel.get_or_insert_default().sort_mode =
|
||||
Some(settings::ProjectPanelSortMode::Mixed);
|
||||
});
|
||||
});
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..50, cx),
|
||||
&["v root", " > dir1", " file1.txt", " file2.txt",]
|
||||
);
|
||||
|
||||
// Toggle back to DirectoriesFirst
|
||||
cx.update(|_, cx| {
|
||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||
store.update_user_settings(cx, |settings| {
|
||||
settings.project_panel.get_or_insert_default().sort_mode =
|
||||
Some(settings::ProjectPanelSortMode::DirectoriesFirst);
|
||||
});
|
||||
});
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..50, cx),
|
||||
&["v root", " > dir1", " file1.txt", " file2.txt",]
|
||||
);
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
|
|
|
|||
|
|
@ -609,6 +609,10 @@ pub struct ProjectPanelSettingsContent {
|
|||
pub drag_and_drop: Option<bool>,
|
||||
/// Settings for automatically opening files.
|
||||
pub auto_open: Option<ProjectPanelAutoOpenSettings>,
|
||||
/// How to order sibling entries in the project panel.
|
||||
///
|
||||
/// Default: directories_first
|
||||
pub sort_mode: Option<ProjectPanelSortMode>,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
|
|
@ -634,6 +638,31 @@ pub enum ProjectPanelEntrySpacing {
|
|||
Standard,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Copy,
|
||||
Clone,
|
||||
Debug,
|
||||
Default,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
JsonSchema,
|
||||
MergeFrom,
|
||||
PartialEq,
|
||||
Eq,
|
||||
strum::VariantArray,
|
||||
strum::VariantNames,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ProjectPanelSortMode {
|
||||
/// Show directories first, then files
|
||||
#[default]
|
||||
DirectoriesFirst,
|
||||
/// Mix directories and files together
|
||||
Mixed,
|
||||
/// Show files first, then directories
|
||||
FilesFirst,
|
||||
}
|
||||
|
||||
#[skip_serializing_none]
|
||||
#[derive(
|
||||
Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq, Default,
|
||||
|
|
|
|||
|
|
@ -668,6 +668,7 @@ impl VsCodeSettings {
|
|||
show_diagnostics: self
|
||||
.read_bool("problems.decorations.enabled")
|
||||
.and_then(|b| if b { Some(ShowDiagnostics::Off) } else { None }),
|
||||
sort_mode: None,
|
||||
starts_open: None,
|
||||
sticky_scroll: None,
|
||||
auto_open: None,
|
||||
|
|
|
|||
|
|
@ -3822,6 +3822,24 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
|
|||
metadata: None,
|
||||
files: USER,
|
||||
}),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Sort Mode",
|
||||
description: "Sort order for entries in the project panel.",
|
||||
field: Box::new(SettingField {
|
||||
pick: |settings_content| {
|
||||
settings_content.project_panel.as_ref()?.sort_mode.as_ref()
|
||||
},
|
||||
write: |settings_content, value| {
|
||||
settings_content
|
||||
.project_panel
|
||||
.get_or_insert_default()
|
||||
.sort_mode = value;
|
||||
},
|
||||
json_path: Some("project_panel.sort_mode"),
|
||||
}),
|
||||
metadata: None,
|
||||
files: USER,
|
||||
}),
|
||||
SettingsPageItem::SectionHeader("Terminal Panel"),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Terminal Dock",
|
||||
|
|
|
|||
|
|
@ -451,6 +451,7 @@ fn init_renderers(cx: &mut App) {
|
|||
.add_basic_renderer::<settings::ShowDiagnostics>(render_dropdown)
|
||||
.add_basic_renderer::<settings::ShowCloseButton>(render_dropdown)
|
||||
.add_basic_renderer::<settings::ProjectPanelEntrySpacing>(render_dropdown)
|
||||
.add_basic_renderer::<settings::ProjectPanelSortMode>(render_dropdown)
|
||||
.add_basic_renderer::<settings::RewrapBehavior>(render_dropdown)
|
||||
.add_basic_renderer::<settings::FormatOnSave>(render_dropdown)
|
||||
.add_basic_renderer::<settings::IndentGuideColoring>(render_dropdown)
|
||||
|
|
|
|||
|
|
@ -944,36 +944,47 @@ pub fn natural_sort(a: &str, b: &str) -> Ordering {
|
|||
}
|
||||
}
|
||||
|
||||
/// Case-insensitive natural sort without applying the final lowercase/uppercase tie-breaker.
|
||||
/// This is useful when comparing individual path components where we want to keep walking
|
||||
/// deeper components before deciding on casing.
|
||||
fn natural_sort_no_tiebreak(a: &str, b: &str) -> Ordering {
|
||||
if a.eq_ignore_ascii_case(b) {
|
||||
Ordering::Equal
|
||||
} else {
|
||||
natural_sort(a, b)
|
||||
}
|
||||
}
|
||||
|
||||
fn stem_and_extension(filename: &str) -> (Option<&str>, Option<&str>) {
|
||||
if filename.is_empty() {
|
||||
return (None, None);
|
||||
}
|
||||
|
||||
match filename.rsplit_once('.') {
|
||||
// Case 1: No dot was found. The entire name is the stem.
|
||||
None => (Some(filename), None),
|
||||
|
||||
// Case 2: A dot was found.
|
||||
Some((before, after)) => {
|
||||
// This is the crucial check for dotfiles like ".bashrc".
|
||||
// If `before` is empty, the dot was the first character.
|
||||
// In that case, we revert to the "whole name is the stem" logic.
|
||||
if before.is_empty() {
|
||||
(Some(filename), None)
|
||||
} else {
|
||||
// Otherwise, we have a standard stem and extension.
|
||||
(Some(before), Some(after))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compare_rel_paths(
|
||||
(path_a, a_is_file): (&RelPath, bool),
|
||||
(path_b, b_is_file): (&RelPath, bool),
|
||||
) -> Ordering {
|
||||
let mut components_a = path_a.components();
|
||||
let mut components_b = path_b.components();
|
||||
|
||||
fn stem_and_extension(filename: &str) -> (Option<&str>, Option<&str>) {
|
||||
if filename.is_empty() {
|
||||
return (None, None);
|
||||
}
|
||||
|
||||
match filename.rsplit_once('.') {
|
||||
// Case 1: No dot was found. The entire name is the stem.
|
||||
None => (Some(filename), None),
|
||||
|
||||
// Case 2: A dot was found.
|
||||
Some((before, after)) => {
|
||||
// This is the crucial check for dotfiles like ".bashrc".
|
||||
// If `before` is empty, the dot was the first character.
|
||||
// In that case, we revert to the "whole name is the stem" logic.
|
||||
if before.is_empty() {
|
||||
(Some(filename), None)
|
||||
} else {
|
||||
// Otherwise, we have a standard stem and extension.
|
||||
(Some(before), Some(after))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
loop {
|
||||
match (components_a.next(), components_b.next()) {
|
||||
(Some(component_a), Some(component_b)) => {
|
||||
|
|
@ -1020,6 +1031,156 @@ pub fn compare_rel_paths(
|
|||
}
|
||||
}
|
||||
|
||||
/// Compare two relative paths with mixed files and directories using
|
||||
/// case-insensitive natural sorting. For example, "Apple", "aardvark.txt",
|
||||
/// and "Zebra" would be sorted as: aardvark.txt, Apple, Zebra
|
||||
/// (case-insensitive alphabetical).
|
||||
pub fn compare_rel_paths_mixed(
|
||||
(path_a, a_is_file): (&RelPath, bool),
|
||||
(path_b, b_is_file): (&RelPath, bool),
|
||||
) -> Ordering {
|
||||
let original_paths_equal = std::ptr::eq(path_a, path_b) || path_a == path_b;
|
||||
let mut components_a = path_a.components();
|
||||
let mut components_b = path_b.components();
|
||||
|
||||
loop {
|
||||
match (components_a.next(), components_b.next()) {
|
||||
(Some(component_a), Some(component_b)) => {
|
||||
let a_leaf_file = a_is_file && components_a.rest().is_empty();
|
||||
let b_leaf_file = b_is_file && components_b.rest().is_empty();
|
||||
|
||||
let (a_stem, a_ext) = a_leaf_file
|
||||
.then(|| stem_and_extension(component_a))
|
||||
.unwrap_or_default();
|
||||
let (b_stem, b_ext) = b_leaf_file
|
||||
.then(|| stem_and_extension(component_b))
|
||||
.unwrap_or_default();
|
||||
let a_key = if a_leaf_file {
|
||||
a_stem
|
||||
} else {
|
||||
Some(component_a)
|
||||
};
|
||||
let b_key = if b_leaf_file {
|
||||
b_stem
|
||||
} else {
|
||||
Some(component_b)
|
||||
};
|
||||
|
||||
let ordering = match (a_key, b_key) {
|
||||
(Some(a), Some(b)) => natural_sort_no_tiebreak(a, b)
|
||||
.then_with(|| match (a_leaf_file, b_leaf_file) {
|
||||
(true, false) if a == b => Ordering::Greater,
|
||||
(false, true) if a == b => Ordering::Less,
|
||||
_ => Ordering::Equal,
|
||||
})
|
||||
.then_with(|| {
|
||||
if a_leaf_file && b_leaf_file {
|
||||
let a_ext_str = a_ext.unwrap_or_default().to_lowercase();
|
||||
let b_ext_str = b_ext.unwrap_or_default().to_lowercase();
|
||||
b_ext_str.cmp(&a_ext_str)
|
||||
} else {
|
||||
Ordering::Equal
|
||||
}
|
||||
}),
|
||||
(Some(_), None) => Ordering::Greater,
|
||||
(None, Some(_)) => Ordering::Less,
|
||||
(None, None) => Ordering::Equal,
|
||||
};
|
||||
|
||||
if !ordering.is_eq() {
|
||||
return ordering;
|
||||
}
|
||||
}
|
||||
(Some(_), None) => return Ordering::Greater,
|
||||
(None, Some(_)) => return Ordering::Less,
|
||||
(None, None) => {
|
||||
// Deterministic tie-break: use natural sort to prefer lowercase when paths
|
||||
// are otherwise equal but still differ in casing.
|
||||
if !original_paths_equal {
|
||||
return natural_sort(path_a.as_unix_str(), path_b.as_unix_str());
|
||||
}
|
||||
return Ordering::Equal;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Compare two relative paths with files before directories using
|
||||
/// case-insensitive natural sorting. At each directory level, all files
|
||||
/// are sorted before all directories, with case-insensitive alphabetical
|
||||
/// ordering within each group.
|
||||
pub fn compare_rel_paths_files_first(
|
||||
(path_a, a_is_file): (&RelPath, bool),
|
||||
(path_b, b_is_file): (&RelPath, bool),
|
||||
) -> Ordering {
|
||||
let original_paths_equal = std::ptr::eq(path_a, path_b) || path_a == path_b;
|
||||
let mut components_a = path_a.components();
|
||||
let mut components_b = path_b.components();
|
||||
|
||||
loop {
|
||||
match (components_a.next(), components_b.next()) {
|
||||
(Some(component_a), Some(component_b)) => {
|
||||
let a_leaf_file = a_is_file && components_a.rest().is_empty();
|
||||
let b_leaf_file = b_is_file && components_b.rest().is_empty();
|
||||
|
||||
let (a_stem, a_ext) = a_leaf_file
|
||||
.then(|| stem_and_extension(component_a))
|
||||
.unwrap_or_default();
|
||||
let (b_stem, b_ext) = b_leaf_file
|
||||
.then(|| stem_and_extension(component_b))
|
||||
.unwrap_or_default();
|
||||
let a_key = if a_leaf_file {
|
||||
a_stem
|
||||
} else {
|
||||
Some(component_a)
|
||||
};
|
||||
let b_key = if b_leaf_file {
|
||||
b_stem
|
||||
} else {
|
||||
Some(component_b)
|
||||
};
|
||||
|
||||
let ordering = match (a_key, b_key) {
|
||||
(Some(a), Some(b)) => {
|
||||
if a_leaf_file && !b_leaf_file {
|
||||
Ordering::Less
|
||||
} else if !a_leaf_file && b_leaf_file {
|
||||
Ordering::Greater
|
||||
} else {
|
||||
natural_sort_no_tiebreak(a, b).then_with(|| {
|
||||
if a_leaf_file && b_leaf_file {
|
||||
let a_ext_str = a_ext.unwrap_or_default().to_lowercase();
|
||||
let b_ext_str = b_ext.unwrap_or_default().to_lowercase();
|
||||
a_ext_str.cmp(&b_ext_str)
|
||||
} else {
|
||||
Ordering::Equal
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
(Some(_), None) => Ordering::Greater,
|
||||
(None, Some(_)) => Ordering::Less,
|
||||
(None, None) => Ordering::Equal,
|
||||
};
|
||||
|
||||
if !ordering.is_eq() {
|
||||
return ordering;
|
||||
}
|
||||
}
|
||||
(Some(_), None) => return Ordering::Greater,
|
||||
(None, Some(_)) => return Ordering::Less,
|
||||
(None, None) => {
|
||||
// Deterministic tie-break: use natural sort to prefer lowercase when paths
|
||||
// are otherwise equal but still differ in casing.
|
||||
if !original_paths_equal {
|
||||
return natural_sort(path_a.as_unix_str(), path_b.as_unix_str());
|
||||
}
|
||||
return Ordering::Equal;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compare_paths(
|
||||
(path_a, a_is_file): (&Path, bool),
|
||||
(path_b, b_is_file): (&Path, bool),
|
||||
|
|
@ -1265,6 +1426,285 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[perf]
|
||||
fn compare_rel_paths_mixed_case_insensitive() {
|
||||
// Test that mixed mode is case-insensitive
|
||||
let mut paths = vec![
|
||||
(RelPath::unix("zebra.txt").unwrap(), true),
|
||||
(RelPath::unix("Apple").unwrap(), false),
|
||||
(RelPath::unix("banana.rs").unwrap(), true),
|
||||
(RelPath::unix("Carrot").unwrap(), false),
|
||||
(RelPath::unix("aardvark.txt").unwrap(), true),
|
||||
];
|
||||
paths.sort_by(|&a, &b| compare_rel_paths_mixed(a, b));
|
||||
// Case-insensitive: aardvark < Apple < banana < Carrot < zebra
|
||||
assert_eq!(
|
||||
paths,
|
||||
vec![
|
||||
(RelPath::unix("aardvark.txt").unwrap(), true),
|
||||
(RelPath::unix("Apple").unwrap(), false),
|
||||
(RelPath::unix("banana.rs").unwrap(), true),
|
||||
(RelPath::unix("Carrot").unwrap(), false),
|
||||
(RelPath::unix("zebra.txt").unwrap(), true),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[perf]
|
||||
fn compare_rel_paths_files_first_basic() {
|
||||
// Test that files come before directories
|
||||
let mut paths = vec![
|
||||
(RelPath::unix("zebra.txt").unwrap(), true),
|
||||
(RelPath::unix("Apple").unwrap(), false),
|
||||
(RelPath::unix("banana.rs").unwrap(), true),
|
||||
(RelPath::unix("Carrot").unwrap(), false),
|
||||
(RelPath::unix("aardvark.txt").unwrap(), true),
|
||||
];
|
||||
paths.sort_by(|&a, &b| compare_rel_paths_files_first(a, b));
|
||||
// Files first (case-insensitive), then directories (case-insensitive)
|
||||
assert_eq!(
|
||||
paths,
|
||||
vec![
|
||||
(RelPath::unix("aardvark.txt").unwrap(), true),
|
||||
(RelPath::unix("banana.rs").unwrap(), true),
|
||||
(RelPath::unix("zebra.txt").unwrap(), true),
|
||||
(RelPath::unix("Apple").unwrap(), false),
|
||||
(RelPath::unix("Carrot").unwrap(), false),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[perf]
|
||||
fn compare_rel_paths_files_first_case_insensitive() {
|
||||
// Test case-insensitive sorting within files and directories
|
||||
let mut paths = vec![
|
||||
(RelPath::unix("Zebra.txt").unwrap(), true),
|
||||
(RelPath::unix("apple").unwrap(), false),
|
||||
(RelPath::unix("Banana.rs").unwrap(), true),
|
||||
(RelPath::unix("carrot").unwrap(), false),
|
||||
(RelPath::unix("Aardvark.txt").unwrap(), true),
|
||||
];
|
||||
paths.sort_by(|&a, &b| compare_rel_paths_files_first(a, b));
|
||||
assert_eq!(
|
||||
paths,
|
||||
vec![
|
||||
(RelPath::unix("Aardvark.txt").unwrap(), true),
|
||||
(RelPath::unix("Banana.rs").unwrap(), true),
|
||||
(RelPath::unix("Zebra.txt").unwrap(), true),
|
||||
(RelPath::unix("apple").unwrap(), false),
|
||||
(RelPath::unix("carrot").unwrap(), false),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[perf]
|
||||
fn compare_rel_paths_files_first_numeric() {
|
||||
// Test natural number sorting with files first
|
||||
let mut paths = vec![
|
||||
(RelPath::unix("file10.txt").unwrap(), true),
|
||||
(RelPath::unix("dir2").unwrap(), false),
|
||||
(RelPath::unix("file2.txt").unwrap(), true),
|
||||
(RelPath::unix("dir10").unwrap(), false),
|
||||
(RelPath::unix("file1.txt").unwrap(), true),
|
||||
];
|
||||
paths.sort_by(|&a, &b| compare_rel_paths_files_first(a, b));
|
||||
assert_eq!(
|
||||
paths,
|
||||
vec![
|
||||
(RelPath::unix("file1.txt").unwrap(), true),
|
||||
(RelPath::unix("file2.txt").unwrap(), true),
|
||||
(RelPath::unix("file10.txt").unwrap(), true),
|
||||
(RelPath::unix("dir2").unwrap(), false),
|
||||
(RelPath::unix("dir10").unwrap(), false),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[perf]
|
||||
fn compare_rel_paths_mixed_case() {
|
||||
// Test case-insensitive sorting with varied capitalization
|
||||
let mut paths = vec![
|
||||
(RelPath::unix("README.md").unwrap(), true),
|
||||
(RelPath::unix("readme.txt").unwrap(), true),
|
||||
(RelPath::unix("ReadMe.rs").unwrap(), true),
|
||||
];
|
||||
paths.sort_by(|&a, &b| compare_rel_paths_mixed(a, b));
|
||||
// All "readme" variants should group together, sorted by extension
|
||||
assert_eq!(
|
||||
paths,
|
||||
vec![
|
||||
(RelPath::unix("readme.txt").unwrap(), true),
|
||||
(RelPath::unix("ReadMe.rs").unwrap(), true),
|
||||
(RelPath::unix("README.md").unwrap(), true),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[perf]
|
||||
fn compare_rel_paths_mixed_files_and_dirs() {
|
||||
// Verify directories and files are still mixed
|
||||
let mut paths = vec![
|
||||
(RelPath::unix("file2.txt").unwrap(), true),
|
||||
(RelPath::unix("Dir1").unwrap(), false),
|
||||
(RelPath::unix("file1.txt").unwrap(), true),
|
||||
(RelPath::unix("dir2").unwrap(), false),
|
||||
];
|
||||
paths.sort_by(|&a, &b| compare_rel_paths_mixed(a, b));
|
||||
// Case-insensitive: dir1, dir2, file1, file2 (all mixed)
|
||||
assert_eq!(
|
||||
paths,
|
||||
vec![
|
||||
(RelPath::unix("Dir1").unwrap(), false),
|
||||
(RelPath::unix("dir2").unwrap(), false),
|
||||
(RelPath::unix("file1.txt").unwrap(), true),
|
||||
(RelPath::unix("file2.txt").unwrap(), true),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[perf]
|
||||
fn compare_rel_paths_mixed_with_nested_paths() {
|
||||
// Test that nested paths still work correctly
|
||||
let mut paths = vec![
|
||||
(RelPath::unix("src/main.rs").unwrap(), true),
|
||||
(RelPath::unix("Cargo.toml").unwrap(), true),
|
||||
(RelPath::unix("src").unwrap(), false),
|
||||
(RelPath::unix("target").unwrap(), false),
|
||||
];
|
||||
paths.sort_by(|&a, &b| compare_rel_paths_mixed(a, b));
|
||||
assert_eq!(
|
||||
paths,
|
||||
vec![
|
||||
(RelPath::unix("Cargo.toml").unwrap(), true),
|
||||
(RelPath::unix("src").unwrap(), false),
|
||||
(RelPath::unix("src/main.rs").unwrap(), true),
|
||||
(RelPath::unix("target").unwrap(), false),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[perf]
|
||||
fn compare_rel_paths_files_first_with_nested() {
|
||||
// Files come before directories, even with nested paths
|
||||
let mut paths = vec![
|
||||
(RelPath::unix("src/lib.rs").unwrap(), true),
|
||||
(RelPath::unix("README.md").unwrap(), true),
|
||||
(RelPath::unix("src").unwrap(), false),
|
||||
(RelPath::unix("tests").unwrap(), false),
|
||||
];
|
||||
paths.sort_by(|&a, &b| compare_rel_paths_files_first(a, b));
|
||||
assert_eq!(
|
||||
paths,
|
||||
vec![
|
||||
(RelPath::unix("README.md").unwrap(), true),
|
||||
(RelPath::unix("src").unwrap(), false),
|
||||
(RelPath::unix("src/lib.rs").unwrap(), true),
|
||||
(RelPath::unix("tests").unwrap(), false),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[perf]
|
||||
fn compare_rel_paths_mixed_dotfiles() {
|
||||
// Test that dotfiles are handled correctly in mixed mode
|
||||
let mut paths = vec![
|
||||
(RelPath::unix(".gitignore").unwrap(), true),
|
||||
(RelPath::unix("README.md").unwrap(), true),
|
||||
(RelPath::unix(".github").unwrap(), false),
|
||||
(RelPath::unix("src").unwrap(), false),
|
||||
];
|
||||
paths.sort_by(|&a, &b| compare_rel_paths_mixed(a, b));
|
||||
assert_eq!(
|
||||
paths,
|
||||
vec![
|
||||
(RelPath::unix(".github").unwrap(), false),
|
||||
(RelPath::unix(".gitignore").unwrap(), true),
|
||||
(RelPath::unix("README.md").unwrap(), true),
|
||||
(RelPath::unix("src").unwrap(), false),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[perf]
|
||||
fn compare_rel_paths_files_first_dotfiles() {
|
||||
// Test that dotfiles come first when they're files
|
||||
let mut paths = vec![
|
||||
(RelPath::unix(".gitignore").unwrap(), true),
|
||||
(RelPath::unix("README.md").unwrap(), true),
|
||||
(RelPath::unix(".github").unwrap(), false),
|
||||
(RelPath::unix("src").unwrap(), false),
|
||||
];
|
||||
paths.sort_by(|&a, &b| compare_rel_paths_files_first(a, b));
|
||||
assert_eq!(
|
||||
paths,
|
||||
vec![
|
||||
(RelPath::unix(".gitignore").unwrap(), true),
|
||||
(RelPath::unix("README.md").unwrap(), true),
|
||||
(RelPath::unix(".github").unwrap(), false),
|
||||
(RelPath::unix("src").unwrap(), false),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[perf]
|
||||
fn compare_rel_paths_mixed_same_stem_different_extension() {
|
||||
// Files with same stem but different extensions should sort by extension
|
||||
let mut paths = vec![
|
||||
(RelPath::unix("file.rs").unwrap(), true),
|
||||
(RelPath::unix("file.md").unwrap(), true),
|
||||
(RelPath::unix("file.txt").unwrap(), true),
|
||||
];
|
||||
paths.sort_by(|&a, &b| compare_rel_paths_mixed(a, b));
|
||||
assert_eq!(
|
||||
paths,
|
||||
vec![
|
||||
(RelPath::unix("file.txt").unwrap(), true),
|
||||
(RelPath::unix("file.rs").unwrap(), true),
|
||||
(RelPath::unix("file.md").unwrap(), true),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[perf]
|
||||
fn compare_rel_paths_files_first_same_stem() {
|
||||
// Same stem files should still sort by extension with files_first
|
||||
let mut paths = vec![
|
||||
(RelPath::unix("main.rs").unwrap(), true),
|
||||
(RelPath::unix("main.c").unwrap(), true),
|
||||
(RelPath::unix("main").unwrap(), false),
|
||||
];
|
||||
paths.sort_by(|&a, &b| compare_rel_paths_files_first(a, b));
|
||||
assert_eq!(
|
||||
paths,
|
||||
vec![
|
||||
(RelPath::unix("main.c").unwrap(), true),
|
||||
(RelPath::unix("main.rs").unwrap(), true),
|
||||
(RelPath::unix("main").unwrap(), false),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[perf]
|
||||
fn compare_rel_paths_mixed_deep_nesting() {
|
||||
// Test sorting with deeply nested paths
|
||||
let mut paths = vec![
|
||||
(RelPath::unix("a/b/c.txt").unwrap(), true),
|
||||
(RelPath::unix("A/B.txt").unwrap(), true),
|
||||
(RelPath::unix("a.txt").unwrap(), true),
|
||||
(RelPath::unix("A.txt").unwrap(), true),
|
||||
];
|
||||
paths.sort_by(|&a, &b| compare_rel_paths_mixed(a, b));
|
||||
assert_eq!(
|
||||
paths,
|
||||
vec![
|
||||
(RelPath::unix("A/B.txt").unwrap(), true),
|
||||
(RelPath::unix("a/b/c.txt").unwrap(), true),
|
||||
(RelPath::unix("a.txt").unwrap(), true),
|
||||
(RelPath::unix("A.txt").unwrap(), true),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[perf]
|
||||
fn path_with_position_parse_posix_path() {
|
||||
// Test POSIX filename edge cases
|
||||
|
|
|
|||
|
|
@ -4298,6 +4298,7 @@ Run the {#action theme_selector::Toggle} action in the command palette to see a
|
|||
"indent_guides": {
|
||||
"show": "always"
|
||||
},
|
||||
"sort_mode": "directories_first",
|
||||
"hide_root": false,
|
||||
"hide_hidden": false,
|
||||
"starts_open": true,
|
||||
|
|
@ -4514,6 +4515,38 @@ Run the {#action theme_selector::Toggle} action in the command palette to see a
|
|||
}
|
||||
```
|
||||
|
||||
### Sort Mode
|
||||
|
||||
- Description: Sort order for entries in the project panel
|
||||
- Setting: `sort_mode`
|
||||
- Default: `directories_first`
|
||||
|
||||
**Options**
|
||||
|
||||
1. Show directories first, then files
|
||||
|
||||
```json [settings]
|
||||
{
|
||||
"sort_mode": "directories_first"
|
||||
}
|
||||
```
|
||||
|
||||
2. Mix directories and files together
|
||||
|
||||
```json [settings]
|
||||
{
|
||||
"sort_mode": "mixed"
|
||||
}
|
||||
```
|
||||
|
||||
3. Show files first, then directories
|
||||
|
||||
```json [settings]
|
||||
{
|
||||
"sort_mode": "files_first"
|
||||
}
|
||||
```
|
||||
|
||||
### Auto Open
|
||||
|
||||
- Description: Control whether files are opened automatically after different creation flows in the project panel.
|
||||
|
|
|
|||
|
|
@ -457,6 +457,8 @@ Project panel can be shown/hidden with {#action project_panel::ToggleFocus} ({#k
|
|||
// When to show indent guides in the project panel. (always, never)
|
||||
"show": "always"
|
||||
},
|
||||
// Sort order for entries (directories_first, mixed, files_first)
|
||||
"sort_mode": "directories_first",
|
||||
// Whether to hide the root entry when only one folder is open in the window.
|
||||
"hide_root": false,
|
||||
// Whether to hide the hidden entries in the project panel.
|
||||
|
|
|
|||
Loading…
Reference in a new issue