| @@ -1,6 +1,6 @@ |
| 1 | 1 | mod project_panel_settings; |
| 2 | 2 | use client::{ErrorCode, ErrorExt}; |
| 3 | | −use settings::Settings; |
| 3 | +use settings::{Settings, SettingsStore}; |
| 4 | 4 | |
| 5 | 5 | use db::kvp::KEY_VALUE_STORE; |
| 6 | 6 | 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 |
| 24 | 24 | use serde::{Deserialize, Serialize}; |
| 25 | 25 | use std::{ |
| 26 | 26 | cmp::Ordering, |
| 27 | + collections::HashSet, |
| 27 | 28 | ffi::OsStr, |
| 28 | 29 | ops::Range, |
| 29 | 30 | path::{Path, PathBuf}, |
| @@ -50,6 +51,7 @@ pub struct ProjectPanel { · 20 unmodified lines |
| 50 | 51 | visible_entries: Vec<(WorktreeId, Vec<Entry>)>, |
| 51 | 52 | last_worktree_root_id: Option<ProjectEntryId>, |
| 52 | 53 | expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>, |
| 54 | + unfolded_dir_ids: HashSet<ProjectEntryId>, |
| 53 | 55 | selection: Option<Selection>, |
| 54 | 56 | context_menu: Option<(View<ContextMenu>, Point<Pixels>, Subscription)>, |
| 55 | 57 | edit_state: Option<EditState>, |
| @@ -133,6 +135,8 @@ actions!( · 77 unmodified lines |
| 133 | 135 | OpenPermanent, |
| 134 | 136 | ToggleFocus, |
| 135 | 137 | NewSearchInDirectory, |
| 138 | + UnfoldDirectory, |
| 139 | + FoldDirectory, |
| 136 | 140 | ] |
| 137 | 141 | ); |
| 138 | 142 | |
| @@ -235,6 +239,16 @@ impl ProjectPanel { · 96 unmodified lines |
| 235 | 239 | }) |
| 236 | 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 | 252 | let mut this = Self { |
| 239 | 253 | project: project.clone(), |
| 240 | 254 | fs: workspace.app_state().fs.clone(), |
| @@ -243,6 +257,7 @@ impl ProjectPanel { · 2 unmodified lines |
| 243 | 257 | visible_entries: Default::default(), |
| 244 | 258 | last_worktree_root_id: Default::default(), |
| 245 | 259 | expanded_dir_ids: Default::default(), |
| 260 | + unfolded_dir_ids: Default::default(), |
| 246 | 261 | selection: None, |
| 247 | 262 | edit_state: None, |
| 248 | 263 | context_menu: None, |
| @@ -403,8 +418,11 @@ impl ProjectPanel { · 154 unmodified lines |
| 403 | 418 | }); |
| 404 | 419 | |
| 405 | 420 | if let Some((worktree, entry)) = self.selected_entry(cx) { |
| 421 | + let auto_fold_dirs = ProjectPanelSettings::get_global(cx).auto_fold_dirs; |
| 406 | 422 | let is_root = Some(entry) == worktree.root_entry(); |
| 407 | 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 | 426 | let worktree_id = worktree.id(); |
| 409 | 427 | let is_local = project.is_local(); |
| 410 | 428 | let is_read_only = project.is_read_only(); |
| @@ -430,6 +448,12 @@ impl ProjectPanel { · 19 unmodified lines |
| 430 | 448 | menu.separator() |
| 431 | 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 | 457 | .separator() |
| 434 | 458 | .action("Cut", Box::new(Cut)) |
| 435 | 459 | .action("Copy", Box::new(Copy)) |
| @@ -482,6 +506,37 @@ impl ProjectPanel { · 46 unmodified lines |
| 482 | 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 | 540 | fn expand_selected_entry(&mut self, _: &ExpandSelectedEntry, cx: &mut ViewContext<Self>) { |
| 486 | 541 | if let Some((worktree, entry)) = self.selected_entry(cx) { |
| 487 | 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 | 970 | fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) { |
| 863 | 971 | if let Some(selection) = self.selection { |
| 864 | 972 | let (mut worktree_ix, mut entry_ix, _) = |
| @@ -1153,6 +1261,7 @@ impl ProjectPanel { · 288 unmodified lines |
| 1153 | 1261 | new_selected_entry: Option<(WorktreeId, ProjectEntryId)>, |
| 1154 | 1262 | cx: &mut ViewContext<Self>, |
| 1155 | 1263 | ) { |
| 1264 | + let auto_collapse_dirs = ProjectPanelSettings::get_global(cx).auto_fold_dirs; |
| 1156 | 1265 | let project = self.project.read(cx); |
| 1157 | 1266 | self.last_worktree_root_id = project |
| 1158 | 1267 | .visible_worktrees(cx) |
| @@ -1194,8 +1303,25 @@ impl ProjectPanel { · 35 unmodified lines |
| 1194 | 1303 | |
| 1195 | 1304 | let mut visible_worktree_entries = Vec::new(); |
| 1196 | 1305 | let mut entry_iter = snapshot.entries(true); |
| 1197 | | − |
| 1198 | 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 | 1325 | visible_worktree_entries.push(entry.clone()); |
| 1200 | 1326 | if Some(entry.id) == new_entry_parent_id { |
| 1201 | 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 { |
| 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 |
| 1372 | 1503 | .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() |
| 1376 | 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 | 1519 | icon, |
| 1378 | 1520 | path: entry.path.clone(), |
| 1379 | | − depth: entry.path.components().count(), |
| 1521 | + depth, |
| 1380 | 1522 | kind: entry.kind, |
| 1381 | 1523 | is_ignored: entry.is_ignored, |
| 1382 | 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 | 1604 | fn render_entry( |
| 1424 | 1605 | &self, |
| 1425 | 1606 | entry_id: ProjectEntryId, |
| @@ -1572,6 +1753,8 @@ impl Render for ProjectPanel { · 146 unmodified lines |
| 1572 | 1753 | .on_action(cx.listener(Self::copy_path)) |
| 1573 | 1754 | .on_action(cx.listener(Self::copy_relative_path)) |
| 1574 | 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 | 1758 | .when(!project.is_read_only(), |el| { |
| 1576 | 1759 | el.on_action(cx.listener(Self::new_file)) |
| 1577 | 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 | 2288 | #[gpui::test(iterations = 30)] |
| 1987 | 2289 | async fn test_editing_files(cx: &mut gpui::TestAppContext) { |
| 1988 | 2290 | init_test(cx); |