History

Fix Auto folded dirs performance issues (#8556)

Fixed auto folded dirs which caused significant performance issues #8476 (#7674) Moved from iterating over snapshot entries to use `child_entries` function from `worktree.rs` by making it public @maxbrunsfeld Release Notes: - Fixed a bug where project panel settings changes would not be applied immediately. - Added a `project_panel.auto_fold_dirs` setting which collapses the nesting in the project panel when there is a chain of folders containing a single folder. <img width="288" alt="Screenshot 2024-04-12 at 11 10 58 AM" src="https://github.com/zed-industries/zed/assets/2280405/efd61e75-026c-464d-ba4d-90db5f68bad3"> ---------

Yury Abykhodau committed 2 years ago

Commit 8bca9ce1 parent 28586064 files · +324 −13Browse files

Unified · Split

4 files
  1. assets/settings/default.json +4 −1
  2. crates/project_panel/src/project_panel.rs +310 −8
  3. crates/project_panel/src/project_panel_settings.rs +8 −2
  4. crates/worktree/src/worktree.rs +2 −2
assets/settings/default.json+4−1
@@ -214,7 +214,10 @@ · 213 unmodified lines
214214 // Whether to reveal it in the project panel automatically,
215215 // when a corresponding project entry becomes active.
216216 // Gitignored entries are never auto revealed.
217− "auto_reveal_entries": true
217+ "auto_reveal_entries": true,
218+ /// Whether to fold directories automatically
219+ /// when a directory has only one directory inside.
220+ "auto_fold_dirs": false
218221 },
219222 "collaboration_panel": {
220223 // Whether to show the collaboration panel button in the status bar.
crates/project_panel/src/project_panel.rs+310−8
@@ -1,6 +1,6 @@
11 mod project_panel_settings;
22 use client::{ErrorCode, ErrorExt};
3−use settings::Settings;
3+use settings::{Settings, SettingsStore};
44
55 use db::kvp::KEY_VALUE_STORE;
66 use editor::{actions::Cancel, items::entry_git_aware_label_color, scroll::Autoscroll, Editor};
@@ -24,6 +24,7 @@ use project_panel_settings::{ProjectPanelDockPosition, ProjectPanelSettings}; · 17 unmodified lines
2424 use serde::{Deserialize, Serialize};
2525 use std::{
2626 cmp::Ordering,
27+ collections::HashSet,
2728 ffi::OsStr,
2829 ops::Range,
2930 path::{Path, PathBuf},
@@ -50,6 +51,7 @@ pub struct ProjectPanel { · 20 unmodified lines
5051 visible_entries: Vec<(WorktreeId, Vec<Entry>)>,
5152 last_worktree_root_id: Option<ProjectEntryId>,
5253 expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>,
54+ unfolded_dir_ids: HashSet<ProjectEntryId>,
5355 selection: Option<Selection>,
5456 context_menu: Option<(View<ContextMenu>, Point<Pixels>, Subscription)>,
5557 edit_state: Option<EditState>,
@@ -133,6 +135,8 @@ actions!( · 77 unmodified lines
133135 OpenPermanent,
134136 ToggleFocus,
135137 NewSearchInDirectory,
138+ UnfoldDirectory,
139+ FoldDirectory,
136140 ]
137141 );
138142
@@ -235,6 +239,16 @@ impl ProjectPanel { · 96 unmodified lines
235239 })
236240 .detach();
237241
242+ let mut project_panel_settings = *ProjectPanelSettings::get_global(cx);
243+ cx.observe_global::<SettingsStore>(move |_, cx| {
244+ let new_settings = *ProjectPanelSettings::get_global(cx);
245+ if project_panel_settings != new_settings {
246+ project_panel_settings = new_settings;
247+ cx.notify();
248+ }
249+ })
250+ .detach();
251+
238252 let mut this = Self {
239253 project: project.clone(),
240254 fs: workspace.app_state().fs.clone(),
@@ -243,6 +257,7 @@ impl ProjectPanel { · 2 unmodified lines
243257 visible_entries: Default::default(),
244258 last_worktree_root_id: Default::default(),
245259 expanded_dir_ids: Default::default(),
260+ unfolded_dir_ids: Default::default(),
246261 selection: None,
247262 edit_state: None,
248263 context_menu: None,
@@ -403,8 +418,11 @@ impl ProjectPanel { · 154 unmodified lines
403418 });
404419
405420 if let Some((worktree, entry)) = self.selected_entry(cx) {
421+ let auto_fold_dirs = ProjectPanelSettings::get_global(cx).auto_fold_dirs;
406422 let is_root = Some(entry) == worktree.root_entry();
407423 let is_dir = entry.is_dir();
424+ let is_foldable = auto_fold_dirs && self.is_foldable(entry, worktree);
425+ let is_unfoldable = auto_fold_dirs && self.is_unfoldable(entry, worktree);
408426 let worktree_id = worktree.id();
409427 let is_local = project.is_local();
410428 let is_read_only = project.is_read_only();
@@ -430,6 +448,12 @@ impl ProjectPanel { · 19 unmodified lines
430448 menu.separator()
431449 .action("Find in Folder…", Box::new(NewSearchInDirectory))
432450 })
451+ .when(is_unfoldable, |menu| {
452+ menu.action("Unfold Directory", Box::new(UnfoldDirectory))
453+ })
454+ .when(is_foldable, |menu| {
455+ menu.action("Fold Directory", Box::new(FoldDirectory))
456+ })
433457 .separator()
434458 .action("Cut", Box::new(Cut))
435459 .action("Copy", Box::new(Copy))
@@ -482,6 +506,37 @@ impl ProjectPanel { · 46 unmodified lines
482506 cx.notify();
483507 }
484508
509+ fn is_unfoldable(&self, entry: &Entry, worktree: &Worktree) -> bool {
510+ if !entry.is_dir() || self.unfolded_dir_ids.contains(&entry.id) {
511+ return false;
512+ }
513+
514+ if let Some(parent_path) = entry.path.parent() {
515+ let snapshot = worktree.snapshot();
516+ let mut child_entries = snapshot.child_entries(&parent_path);
517+ if let Some(child) = child_entries.next() {
518+ if child_entries.next().is_none() {
519+ return child.kind.is_dir();
520+ }
521+ }
522+ };
523+ false
524+ }
525+
526+ fn is_foldable(&self, entry: &Entry, worktree: &Worktree) -> bool {
527+ if entry.is_dir() {
528+ let snapshot = worktree.snapshot();
529+
530+ let mut child_entries = snapshot.child_entries(&entry.path);
531+ if let Some(child) = child_entries.next() {
532+ if child_entries.next().is_none() {
533+ return child.kind.is_dir();
534+ }
535+ }
536+ }
537+ false
538+ }
539+
485540 fn expand_selected_entry(&mut self, _: &ExpandSelectedEntry, cx: &mut ViewContext<Self>) {
486541 if let Some((worktree, entry)) = self.selected_entry(cx) {
487542 if entry.is_dir() {
@@ -859,6 +914,59 @@ impl ProjectPanel { · 371 unmodified lines
859914 });
860915 }
861916
917+ fn unfold_directory(&mut self, _: &UnfoldDirectory, cx: &mut ViewContext<Self>) {
918+ if let Some((worktree, entry)) = self.selected_entry(cx) {
919+ self.unfolded_dir_ids.insert(entry.id);
920+
921+ let snapshot = worktree.snapshot();
922+ let mut parent_path = entry.path.parent();
923+ while let Some(path) = parent_path {
924+ if let Some(parent_entry) = worktree.entry_for_path(path) {
925+ let mut children_iter = snapshot.child_entries(path);
926+
927+ if children_iter.by_ref().take(2).count() > 1 {
928+ break;
929+ }
930+
931+ self.unfolded_dir_ids.insert(parent_entry.id);
932+ parent_path = path.parent();
933+ } else {
934+ break;
935+ }
936+ }
937+
938+ self.update_visible_entries(None, cx);
939+ self.autoscroll(cx);
940+ cx.notify();
941+ }
942+ }
943+
944+ fn fold_directory(&mut self, _: &FoldDirectory, cx: &mut ViewContext<Self>) {
945+ if let Some((worktree, entry)) = self.selected_entry(cx) {
946+ self.unfolded_dir_ids.remove(&entry.id);
947+
948+ let snapshot = worktree.snapshot();
949+ let mut path = &*entry.path;
950+ loop {
951+ let mut child_entries_iter = snapshot.child_entries(path);
952+ if let Some(child) = child_entries_iter.next() {
953+ if child_entries_iter.next().is_none() && child.is_dir() {
954+ self.unfolded_dir_ids.remove(&child.id);
955+ path = &*child.path;
956+ } else {
957+ break;
958+ }
959+ } else {
960+ break;
961+ }
962+ }
963+
964+ self.update_visible_entries(None, cx);
965+ self.autoscroll(cx);
966+ cx.notify();
967+ }
968+ }
969+
862970 fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
863971 if let Some(selection) = self.selection {
864972 let (mut worktree_ix, mut entry_ix, _) =
@@ -1153,6 +1261,7 @@ impl ProjectPanel { · 288 unmodified lines
11531261 new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
11541262 cx: &mut ViewContext<Self>,
11551263 ) {
1264+ let auto_collapse_dirs = ProjectPanelSettings::get_global(cx).auto_fold_dirs;
11561265 let project = self.project.read(cx);
11571266 self.last_worktree_root_id = project
11581267 .visible_worktrees(cx)
@@ -1194,8 +1303,25 @@ impl ProjectPanel { · 35 unmodified lines
11941303
11951304 let mut visible_worktree_entries = Vec::new();
11961305 let mut entry_iter = snapshot.entries(true);
1197−
11981306 while let Some(entry) = entry_iter.entry() {
1307+ if auto_collapse_dirs
1308+ && entry.kind.is_dir()
1309+ && !self.unfolded_dir_ids.contains(&entry.id)
1310+ {
1311+ if let Some(root_path) = snapshot.root_entry() {
1312+ let mut child_entries = snapshot.child_entries(&entry.path);
1313+ if let Some(child) = child_entries.next() {
1314+ if entry.path != root_path.path
1315+ && child_entries.next().is_none()
1316+ && child.kind.is_dir()
1317+ {
1318+ entry_iter.advance();
1319+ continue;
1320+ }
1321+ }
1322+ }
1323+ }
1324+
11991325 visible_worktree_entries.push(entry.clone());
12001326 if Some(entry.id) == new_entry_parent_id {
12011327 visible_worktree_entries.push(Entry {
@@ -1367,16 +1493,32 @@ impl ProjectPanel { · 165 unmodified lines
13671493 }
13681494 };
13691495
1370− let mut details = EntryDetails {
1371− filename: entry
1496+ let (depth, difference) = ProjectPanel::calculate_depth_and_difference(
1497+ entry,
1498+ visible_worktree_entries,
1499+ );
1500+
1501+ let filename = match difference {
1502+ diff if diff > 1 => entry
13721503 .path
1373− .file_name()
1374− .unwrap_or(root_name)
1375− .to_string_lossy()
1504+ .iter()
1505+ .skip(entry.path.components().count() - diff)
1506+ .collect::<PathBuf>()
1507+ .to_str()
1508+ .unwrap_or_default()
13761509 .to_string(),
1510+ _ => entry
1511+ .path
1512+ .file_name()
1513+ .map(|name| name.to_string_lossy().into_owned())
1514+ .unwrap_or_else(|| root_name.to_string_lossy().to_string()),
1515+ };
1516+
1517+ let mut details = EntryDetails {
1518+ filename,
13771519 icon,
13781520 path: entry.path.clone(),
1379− depth: entry.path.components().count(),
1521+ depth,
13801522 kind: entry.kind,
13811523 is_ignored: entry.is_ignored,
13821524 is_expanded,
@@ -1420,6 +1562,45 @@ impl ProjectPanel { · 37 unmodified lines
14201562 }
14211563 }
14221564
1565+ fn calculate_depth_and_difference(
1566+ entry: &Entry,
1567+ visible_worktree_entries: &Vec<Entry>,
1568+ ) -> (usize, usize) {
1569+ let visible_worktree_paths: HashSet<Arc<Path>> = visible_worktree_entries
1570+ .iter()
1571+ .map(|e| e.path.clone())
1572+ .collect();
1573+
1574+ let (depth, difference) = entry
1575+ .path
1576+ .ancestors()
1577+ .skip(1) // Skip the entry itself
1578+ .find_map(|ancestor| {
1579+ if visible_worktree_paths.contains(ancestor) {
1580+ let parent_entry = visible_worktree_entries
1581+ .iter()
1582+ .find(|&e| &*e.path == ancestor)
1583+ .unwrap();
1584+
1585+ let entry_path_components_count = entry.path.components().count();
1586+ let parent_path_components_count = parent_entry.path.components().count();
1587+ let difference = entry_path_components_count - parent_path_components_count;
1588+ let depth = parent_entry
1589+ .path
1590+ .ancestors()
1591+ .skip(1)
1592+ .filter(|ancestor| visible_worktree_paths.contains(*ancestor))
1593+ .count();
1594+ Some((depth + 1, difference))
1595+ } else {
1596+ None
1597+ }
1598+ })
1599+ .unwrap_or((0, 0));
1600+
1601+ (depth, difference)
1602+ }
1603+
14231604 fn render_entry(
14241605 &self,
14251606 entry_id: ProjectEntryId,
@@ -1572,6 +1753,8 @@ impl Render for ProjectPanel { · 146 unmodified lines
15721753 .on_action(cx.listener(Self::copy_path))
15731754 .on_action(cx.listener(Self::copy_relative_path))
15741755 .on_action(cx.listener(Self::new_search_in_directory))
1756+ .on_action(cx.listener(Self::unfold_directory))
1757+ .on_action(cx.listener(Self::fold_directory))
15751758 .when(!project.is_read_only(), |el| {
15761759 el.on_action(cx.listener(Self::new_file))
15771760 .on_action(cx.listener(Self::new_directory))
@@ -1983,6 +2166,125 @@ mod tests { · 405 unmodified lines
19832166 );
19842167 }
19852168
2169+ #[gpui::test]
2170+ async fn test_auto_collapse_dir_paths(cx: &mut gpui::TestAppContext) {
2171+ init_test(cx);
2172+
2173+ let fs = FakeFs::new(cx.executor().clone());
2174+ fs.insert_tree(
2175+ "/root1",
2176+ json!({
2177+ "dir_1": {
2178+ "nested_dir_1": {
2179+ "nested_dir_2": {
2180+ "nested_dir_3": {
2181+ "file_a.java": "// File contents",
2182+ "file_b.java": "// File contents",
2183+ "file_c.java": "// File contents",
2184+ "nested_dir_4": {
2185+ "nested_dir_5": {
2186+ "file_d.java": "// File contents",
2187+ }
2188+ }
2189+ }
2190+ }
2191+ }
2192+ }
2193+ }),
2194+ )
2195+ .await;
2196+ fs.insert_tree(
2197+ "/root2",
2198+ json!({
2199+ "dir_2": {
2200+ "file_1.java": "// File contents",
2201+ }
2202+ }),
2203+ )
2204+ .await;
2205+
2206+ let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
2207+ let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2208+ let cx = &mut VisualTestContext::from_window(*workspace, cx);
2209+ cx.update(|cx| {
2210+ let settings = *ProjectPanelSettings::get_global(cx);
2211+ ProjectPanelSettings::override_global(
2212+ ProjectPanelSettings {
2213+ auto_fold_dirs: true,
2214+ ..settings
2215+ },
2216+ cx,
2217+ );
2218+ });
2219+ let panel = workspace
2220+ .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx))
2221+ .unwrap();
2222+ assert_eq!(
2223+ visible_entries_as_strings(&panel, 0..10, cx),
2224+ &[
2225+ "v root1",
2226+ " > dir_1/nested_dir_1/nested_dir_2/nested_dir_3",
2227+ "v root2",
2228+ " > dir_2",
2229+ ]
2230+ );
2231+
2232+ toggle_expand_dir(
2233+ &panel,
2234+ "root1/dir_1/nested_dir_1/nested_dir_2/nested_dir_3",
2235+ cx,
2236+ );
2237+ assert_eq!(
2238+ visible_entries_as_strings(&panel, 0..10, cx),
2239+ &[
2240+ "v root1",
2241+ " v dir_1/nested_dir_1/nested_dir_2/nested_dir_3 <== selected",
2242+ " > nested_dir_4/nested_dir_5",
2243+ " file_a.java",
2244+ " file_b.java",
2245+ " file_c.java",
2246+ "v root2",
2247+ " > dir_2",
2248+ ]
2249+ );
2250+
2251+ toggle_expand_dir(
2252+ &panel,
2253+ "root1/dir_1/nested_dir_1/nested_dir_2/nested_dir_3/nested_dir_4/nested_dir_5",
2254+ cx,
2255+ );
2256+ assert_eq!(
2257+ visible_entries_as_strings(&panel, 0..10, cx),
2258+ &[
2259+ "v root1",
2260+ " v dir_1/nested_dir_1/nested_dir_2/nested_dir_3",
2261+ " v nested_dir_4/nested_dir_5 <== selected",
2262+ " file_d.java",
2263+ " file_a.java",
2264+ " file_b.java",
2265+ " file_c.java",
2266+ "v root2",
2267+ " > dir_2",
2268+ ]
2269+ );
2270+ toggle_expand_dir(&panel, "root2/dir_2", cx);
2271+ assert_eq!(
2272+ visible_entries_as_strings(&panel, 0..10, cx),
2273+ &[
2274+ "v root1",
2275+ " v dir_1/nested_dir_1/nested_dir_2/nested_dir_3",
2276+ " v nested_dir_4/nested_dir_5",
2277+ " file_d.java",
2278+ " file_a.java",
2279+ " file_b.java",
2280+ " file_c.java",
2281+ "v root2",
2282+ " v dir_2 <== selected",
2283+ " file_1.java",
2284+ ]
2285+ );
2286+ }
2287+
19862288 #[gpui::test(iterations = 30)]
19872289 async fn test_editing_files(cx: &mut gpui::TestAppContext) {
19882290 init_test(cx);
crates/project_panel/src/project_panel_settings.rs+8−2
@@ -4,14 +4,14 @@ use schemars::JsonSchema; · 3 unmodified lines
44 use serde_derive::{Deserialize, Serialize};
55 use settings::{Settings, SettingsSources};
66
7−#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
7+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Copy, PartialEq)]
88 #[serde(rename_all = "snake_case")]
99 pub enum ProjectPanelDockPosition {
1010 Left,
1111 Right,
1212 }
1313
14−#[derive(Deserialize, Debug)]
14+#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
1515 pub struct ProjectPanelSettings {
1616 pub default_width: Pixels,
1717 pub dock: ProjectPanelDockPosition,
@@ -20,6 +20,7 @@ pub struct ProjectPanelSettings { · 2 unmodified lines
2020 pub git_status: bool,
2121 pub indent_size: f32,
2222 pub auto_reveal_entries: bool,
23+ pub auto_fold_dirs: bool,
2324 }
2425
2526 #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
@@ -54,6 +55,11 @@ pub struct ProjectPanelSettingsContent { · 28 unmodified lines
5455 ///
5556 /// Default: true
5657 pub auto_reveal_entries: Option<bool>,
58+ /// Whether to fold directories automatically
59+ /// when directory has only one directory inside.
60+ ///
61+ /// Default: false
62+ pub auto_fold_dirs: Option<bool>,
5763 }
5864
5965 impl Settings for ProjectPanelSettings {
crates/worktree/src/worktree.rs+2−2
@@ -2079,7 +2079,7 @@ impl Snapshot { · 2078 unmodified lines
20792079 .map(|entry| &entry.path)
20802080 }
20812081
2082− fn child_entries<'a>(&'a self, parent_path: &'a Path) -> ChildEntriesIter<'a> {
2082+ pub fn child_entries<'a>(&'a self, parent_path: &'a Path) -> ChildEntriesIter<'a> {
20832083 let mut cursor = self.entries_by_path.cursor();
20842084 cursor.seek(&TraversalTarget::Path(parent_path), Bias::Right, &());
20852085 let traversal = Traversal {
@@ -4706,7 +4706,7 @@ impl<'a, 'b> SeekTarget<'a, EntrySummary, (TraversalProgress<'a>, GitStatuses)> · 2620 unmodified lines
47064706 }
47074707 }
47084708
4709−struct ChildEntriesIter<'a> {
4709+pub struct ChildEntriesIter<'a> {
47104710 parent_path: &'a Path,
47114711 traversal: Traversal<'a>,
47124712 }