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
214 // Whether to reveal it in the project panel automatically,214 // Whether to reveal it in the project panel automatically,
215 // when a corresponding project entry becomes active.215 // when a corresponding project entry becomes active.
216 // Gitignored entries are never auto revealed.216 // Gitignored entries are never auto revealed.
217 "auto_reveal_entries": true217 "auto_reveal_entries": true,
218 /// Whether to fold directories automatically
219 /// when a directory has only one directory inside.
220 "auto_fold_dirs": false
218 },221 },
219 "collaboration_panel": {222 "collaboration_panel": {
220 // Whether to show the collaboration panel button in the status bar.223 // Whether to show the collaboration panel button in the status bar.
crates/project_panel/src/project_panel.rs+310−8
@@ -1,6 +1,6 @@
1mod project_panel_settings;1mod project_panel_settings;
2use client::{ErrorCode, ErrorExt};2use client::{ErrorCode, ErrorExt};
3use settings::Settings;3use settings::{Settings, SettingsStore};
4 4
5use db::kvp::KEY_VALUE_STORE;5use db::kvp::KEY_VALUE_STORE;
6use editor::{actions::Cancel, items::entry_git_aware_label_color, scroll::Autoscroll, Editor};6use 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
24use serde::{Deserialize, Serialize};24use serde::{Deserialize, Serialize};
25use std::{25use std::{
26 cmp::Ordering,26 cmp::Ordering,
27 collections::HashSet,
27 ffi::OsStr,28 ffi::OsStr,
28 ops::Range,29 ops::Range,
29 path::{Path, PathBuf},30 path::{Path, PathBuf},
@@ -50,6 +51,7 @@ pub struct ProjectPanel { · 20 unmodified lines
50 visible_entries: Vec<(WorktreeId, Vec<Entry>)>,51 visible_entries: Vec<(WorktreeId, Vec<Entry>)>,
51 last_worktree_root_id: Option<ProjectEntryId>,52 last_worktree_root_id: Option<ProjectEntryId>,
52 expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>,53 expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>,
54 unfolded_dir_ids: HashSet<ProjectEntryId>,
53 selection: Option<Selection>,55 selection: Option<Selection>,
54 context_menu: Option<(View<ContextMenu>, Point<Pixels>, Subscription)>,56 context_menu: Option<(View<ContextMenu>, Point<Pixels>, Subscription)>,
55 edit_state: Option<EditState>,57 edit_state: Option<EditState>,
@@ -133,6 +135,8 @@ actions!( · 77 unmodified lines
133 OpenPermanent,135 OpenPermanent,
134 ToggleFocus,136 ToggleFocus,
135 NewSearchInDirectory,137 NewSearchInDirectory,
138 UnfoldDirectory,
139 FoldDirectory,
136 ]140 ]
137);141);
138 142
@@ -235,6 +239,16 @@ impl ProjectPanel { · 96 unmodified lines
235 })239 })
236 .detach();240 .detach();
237 241
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
238 let mut this = Self {252 let mut this = Self {
239 project: project.clone(),253 project: project.clone(),
240 fs: workspace.app_state().fs.clone(),254 fs: workspace.app_state().fs.clone(),
@@ -243,6 +257,7 @@ impl ProjectPanel { · 2 unmodified lines
243 visible_entries: Default::default(),257 visible_entries: Default::default(),
244 last_worktree_root_id: Default::default(),258 last_worktree_root_id: Default::default(),
245 expanded_dir_ids: Default::default(),259 expanded_dir_ids: Default::default(),
260 unfolded_dir_ids: Default::default(),
246 selection: None,261 selection: None,
247 edit_state: None,262 edit_state: None,
248 context_menu: None,263 context_menu: None,
@@ -403,8 +418,11 @@ impl ProjectPanel { · 154 unmodified lines
403 });418 });
404 419
405 if let Some((worktree, entry)) = self.selected_entry(cx) {420 if let Some((worktree, entry)) = self.selected_entry(cx) {
421 let auto_fold_dirs = ProjectPanelSettings::get_global(cx).auto_fold_dirs;
406 let is_root = Some(entry) == worktree.root_entry();422 let is_root = Some(entry) == worktree.root_entry();
407 let is_dir = entry.is_dir();423 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);
408 let worktree_id = worktree.id();426 let worktree_id = worktree.id();
409 let is_local = project.is_local();427 let is_local = project.is_local();
410 let is_read_only = project.is_read_only();428 let is_read_only = project.is_read_only();
@@ -430,6 +448,12 @@ impl ProjectPanel { · 19 unmodified lines
430 menu.separator()448 menu.separator()
431 .action("Find in Folder…", Box::new(NewSearchInDirectory))449 .action("Find in Folder…", Box::new(NewSearchInDirectory))
432 })450 })
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 })
433 .separator()457 .separator()
434 .action("Cut", Box::new(Cut))458 .action("Cut", Box::new(Cut))
435 .action("Copy", Box::new(Copy))459 .action("Copy", Box::new(Copy))
@@ -482,6 +506,37 @@ impl ProjectPanel { · 46 unmodified lines
482 cx.notify();506 cx.notify();
483 }507 }
484 508
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
485 fn expand_selected_entry(&mut self, _: &ExpandSelectedEntry, cx: &mut ViewContext<Self>) {540 fn expand_selected_entry(&mut self, _: &ExpandSelectedEntry, cx: &mut ViewContext<Self>) {
486 if let Some((worktree, entry)) = self.selected_entry(cx) {541 if let Some((worktree, entry)) = self.selected_entry(cx) {
487 if entry.is_dir() {542 if entry.is_dir() {
@@ -859,6 +914,59 @@ impl ProjectPanel { · 371 unmodified lines
859 });914 });
860 }915 }
861 916
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
862 fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {970 fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
863 if let Some(selection) = self.selection {971 if let Some(selection) = self.selection {
864 let (mut worktree_ix, mut entry_ix, _) =972 let (mut worktree_ix, mut entry_ix, _) =
@@ -1153,6 +1261,7 @@ impl ProjectPanel { · 288 unmodified lines
1153 new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,1261 new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
1154 cx: &mut ViewContext<Self>,1262 cx: &mut ViewContext<Self>,
1155 ) {1263 ) {
1264 let auto_collapse_dirs = ProjectPanelSettings::get_global(cx).auto_fold_dirs;
1156 let project = self.project.read(cx);1265 let project = self.project.read(cx);
1157 self.last_worktree_root_id = project1266 self.last_worktree_root_id = project
1158 .visible_worktrees(cx)1267 .visible_worktrees(cx)
@@ -1194,8 +1303,25 @@ impl ProjectPanel { · 35 unmodified lines
1194 1303
1195 let mut visible_worktree_entries = Vec::new();1304 let mut visible_worktree_entries = Vec::new();
1196 let mut entry_iter = snapshot.entries(true);1305 let mut entry_iter = snapshot.entries(true);
1197
1198 while let Some(entry) = entry_iter.entry() {1306 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
1199 visible_worktree_entries.push(entry.clone());1325 visible_worktree_entries.push(entry.clone());
1200 if Some(entry.id) == new_entry_parent_id {1326 if Some(entry.id) == new_entry_parent_id {
1201 visible_worktree_entries.push(Entry {1327 visible_worktree_entries.push(Entry {
@@ -1367,16 +1493,32 @@ impl ProjectPanel { · 165 unmodified lines
1367 }1493 }
1368 };1494 };
1369 1495
1370 let mut details = EntryDetails {1496 let (depth, difference) = ProjectPanel::calculate_depth_and_difference(
1371 filename: entry1497 entry,
1498 visible_worktree_entries,
1499 );
1500
1501 let filename = match difference {
1502 diff if diff > 1 => entry
1372 .path1503 .path
1373 .file_name()1504 .iter()
1374 .unwrap_or(root_name)1505 .skip(entry.path.components().count() - diff)
1375 .to_string_lossy()1506 .collect::<PathBuf>()
1507 .to_str()
1508 .unwrap_or_default()
1376 .to_string(),1509 .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,
1377 icon,1519 icon,
1378 path: entry.path.clone(),1520 path: entry.path.clone(),
1379 depth: entry.path.components().count(),1521 depth,
1380 kind: entry.kind,1522 kind: entry.kind,
1381 is_ignored: entry.is_ignored,1523 is_ignored: entry.is_ignored,
1382 is_expanded,1524 is_expanded,
@@ -1420,6 +1562,45 @@ impl ProjectPanel { · 37 unmodified lines
1420 }1562 }
1421 }1563 }
1422 1564
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
1423 fn render_entry(1604 fn render_entry(
1424 &self,1605 &self,
1425 entry_id: ProjectEntryId,1606 entry_id: ProjectEntryId,
@@ -1572,6 +1753,8 @@ impl Render for ProjectPanel { · 146 unmodified lines
1572 .on_action(cx.listener(Self::copy_path))1753 .on_action(cx.listener(Self::copy_path))
1573 .on_action(cx.listener(Self::copy_relative_path))1754 .on_action(cx.listener(Self::copy_relative_path))
1574 .on_action(cx.listener(Self::new_search_in_directory))1755 .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))
1575 .when(!project.is_read_only(), |el| {1758 .when(!project.is_read_only(), |el| {
1576 el.on_action(cx.listener(Self::new_file))1759 el.on_action(cx.listener(Self::new_file))
1577 .on_action(cx.listener(Self::new_directory))1760 .on_action(cx.listener(Self::new_directory))
@@ -1983,6 +2166,125 @@ mod tests { · 405 unmodified lines
1983 );2166 );
1984 }2167 }
1985 2168
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
1986 #[gpui::test(iterations = 30)]2288 #[gpui::test(iterations = 30)]
1987 async fn test_editing_files(cx: &mut gpui::TestAppContext) {2289 async fn test_editing_files(cx: &mut gpui::TestAppContext) {
1988 init_test(cx);2290 init_test(cx);
crates/project_panel/src/project_panel_settings.rs+8−2
@@ -4,14 +4,14 @@ use schemars::JsonSchema; · 3 unmodified lines
4use serde_derive::{Deserialize, Serialize};4use serde_derive::{Deserialize, Serialize};
5use settings::{Settings, SettingsSources};5use settings::{Settings, SettingsSources};
6 6
7#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]7#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Copy, PartialEq)]
8#[serde(rename_all = "snake_case")]8#[serde(rename_all = "snake_case")]
9pub enum ProjectPanelDockPosition {9pub enum ProjectPanelDockPosition {
10 Left,10 Left,
11 Right,11 Right,
12}12}
13 13
14#[derive(Deserialize, Debug)]14#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
15pub struct ProjectPanelSettings {15pub struct ProjectPanelSettings {
16 pub default_width: Pixels,16 pub default_width: Pixels,
17 pub dock: ProjectPanelDockPosition,17 pub dock: ProjectPanelDockPosition,
@@ -20,6 +20,7 @@ pub struct ProjectPanelSettings { · 2 unmodified lines
20 pub git_status: bool,20 pub git_status: bool,
21 pub indent_size: f32,21 pub indent_size: f32,
22 pub auto_reveal_entries: bool,22 pub auto_reveal_entries: bool,
23 pub auto_fold_dirs: bool,
23}24}
24 25
25#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]26#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
@@ -54,6 +55,11 @@ pub struct ProjectPanelSettingsContent { · 28 unmodified lines
54 ///55 ///
55 /// Default: true56 /// Default: true
56 pub auto_reveal_entries: Option<bool>,57 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>,
57}63}
58 64
59impl Settings for ProjectPanelSettings {65impl Settings for ProjectPanelSettings {
crates/worktree/src/worktree.rs+2−2
@@ -2079,7 +2079,7 @@ impl Snapshot { · 2078 unmodified lines
2079 .map(|entry| &entry.path)2079 .map(|entry| &entry.path)
2080 }2080 }
2081 2081
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> {
2083 let mut cursor = self.entries_by_path.cursor();2083 let mut cursor = self.entries_by_path.cursor();
2084 cursor.seek(&TraversalTarget::Path(parent_path), Bias::Right, &());2084 cursor.seek(&TraversalTarget::Path(parent_path), Bias::Right, &());
2085 let traversal = Traversal {2085 let traversal = Traversal {
@@ -4706,7 +4706,7 @@ impl<'a, 'b> SeekTarget<'a, EntrySummary, (TraversalProgress<'a>, GitStatuses)> · 2620 unmodified lines
4706 }4706 }
4707}4707}
4708 4708
4709struct ChildEntriesIter<'a> {4709pub struct ChildEntriesIter<'a> {
4710 parent_path: &'a Path,4710 parent_path: &'a Path,
4711 traversal: Traversal<'a>,4711 traversal: Traversal<'a>,
4712}4712}