Merge pull request #1050 from zed-industries/ignored-files

Antonio Scandurra created

Show ignored entries in project panel

Change summary

crates/collab/src/rpc.rs                  |  37 +++++--
crates/project/src/worktree.rs            | 125 +++++++++++++++++++++++-
crates/project_panel/src/project_panel.rs |  42 ++++++--
crates/theme/src/theme.rs                 |   3 
styles/src/styleTree/projectPanel.ts      |   1 
5 files changed, 178 insertions(+), 30 deletions(-)

Detailed changes

crates/collab/src/rpc.rs 🔗

@@ -1744,8 +1744,13 @@ mod tests {
         fs.insert_tree(
             "/a",
             json!({
+                ".gitignore": "ignored-dir",
                 "a.txt": "a-contents",
                 "b.txt": "b-contents",
+                "ignored-dir": {
+                    "c.txt": "",
+                    "d.txt": "",
+                }
             }),
         )
         .await;
@@ -1775,7 +1780,6 @@ mod tests {
         // Join that project as client B
         let client_b_peer_id = client_b.peer_id;
         let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
-
         let replica_id_b = project_b.read_with(cx_b, |project, _| {
             assert_eq!(
                 project
@@ -1788,16 +1792,27 @@ mod tests {
             );
             project.replica_id()
         });
-        project_a
-            .condition(&cx_a, |tree, _| {
-                tree.collaborators()
-                    .get(&client_b_peer_id)
-                    .map_or(false, |collaborator| {
-                        collaborator.replica_id == replica_id_b
-                            && collaborator.user.github_login == "user_b"
-                    })
-            })
-            .await;
+
+        deterministic.run_until_parked();
+        project_a.read_with(cx_a, |project, _| {
+            let client_b_collaborator = project.collaborators().get(&client_b_peer_id).unwrap();
+            assert_eq!(client_b_collaborator.replica_id, replica_id_b);
+            assert_eq!(client_b_collaborator.user.github_login, "user_b");
+        });
+        project_b.read_with(cx_b, |project, cx| {
+            let worktree = project.worktrees(cx).next().unwrap().read(cx);
+            assert_eq!(
+                worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
+                [
+                    Path::new(".gitignore"),
+                    Path::new("a.txt"),
+                    Path::new("b.txt"),
+                    Path::new("ignored-dir"),
+                    Path::new("ignored-dir/c.txt"),
+                    Path::new("ignored-dir/d.txt"),
+                ]
+            );
+        });
 
         // Open the same file as client B and client A.
         let buffer_b = project_b

crates/project/src/worktree.rs 🔗

@@ -40,6 +40,7 @@ use std::{
     ffi::{OsStr, OsString},
     fmt,
     future::Future,
+    mem,
     ops::{Deref, DerefMut},
     os::unix::prelude::{OsStrExt, OsStringExt},
     path::{Path, PathBuf},
@@ -827,8 +828,8 @@ impl LocalWorktree {
             next_entry_id = snapshot.next_entry_id.clone();
         }
         cx.spawn_weak(|this, mut cx| async move {
-            let entry = Entry::new(
-                path,
+            let mut entry = Entry::new(
+                path.clone(),
                 &fs.metadata(&abs_path)
                     .await?
                     .ok_or_else(|| anyhow!("could not read saved file metadata"))?,
@@ -842,6 +843,9 @@ impl LocalWorktree {
             let (entry, snapshot, snapshots_tx) = this.read_with(&cx, |this, _| {
                 let this = this.as_local().unwrap();
                 let mut snapshot = this.background_snapshot.lock();
+                entry.is_ignored = snapshot
+                    .ignore_stack_for_path(&path, entry.is_dir())
+                    .is_path_ignored(&path, entry.is_dir());
                 if let Some(old_path) = old_path {
                     snapshot.remove_path(&old_path);
                 }
@@ -951,9 +955,43 @@ impl LocalWorktree {
                         })?;
                     }
 
+                    // Stream ignored entries in chunks.
+                    {
+                        let mut ignored_entries = prev_snapshot
+                            .entries_by_path
+                            .iter()
+                            .filter(|e| e.is_ignored);
+                        let mut ignored_entries_to_send = Vec::new();
+                        loop {
+                            #[cfg(any(test, feature = "test-support"))]
+                            const CHUNK_SIZE: usize = 2;
+                            #[cfg(not(any(test, feature = "test-support")))]
+                            const CHUNK_SIZE: usize = 256;
+
+                            let entry = ignored_entries.next();
+                            if ignored_entries_to_send.len() >= CHUNK_SIZE || entry.is_none() {
+                                rpc.request(proto::UpdateWorktree {
+                                    project_id,
+                                    worktree_id,
+                                    root_name: prev_snapshot.root_name().to_string(),
+                                    updated_entries: mem::take(&mut ignored_entries_to_send),
+                                    removed_entries: Default::default(),
+                                    scan_id: prev_snapshot.scan_id as u64,
+                                })
+                                .await?;
+                            }
+
+                            if let Some(entry) = entry {
+                                ignored_entries_to_send.push(entry.into());
+                            } else {
+                                break;
+                            }
+                        }
+                    }
+
                     while let Ok(snapshot) = snapshots_to_send_rx.recv().await {
                         let message =
-                            snapshot.build_update(&prev_snapshot, project_id, worktree_id, false);
+                            snapshot.build_update(&prev_snapshot, project_id, worktree_id, true);
                         rpc.request(message).await?;
                         prev_snapshot = snapshot;
                     }
@@ -1905,6 +1943,7 @@ impl sum_tree::Summary for EntrySummary {
 
     fn add_summary(&mut self, rhs: &Self, _: &()) {
         self.max_path = rhs.max_path.clone();
+        self.count += rhs.count;
         self.visible_count += rhs.visible_count;
         self.file_count += rhs.file_count;
         self.visible_file_count += rhs.visible_file_count;
@@ -2675,6 +2714,7 @@ mod tests {
     use anyhow::Result;
     use client::test::FakeHttpClient;
     use fs::RealFs;
+    use gpui::TestAppContext;
     use rand::prelude::*;
     use serde_json::json;
     use std::{
@@ -2685,7 +2725,7 @@ mod tests {
     use util::test::temp_tree;
 
     #[gpui::test]
-    async fn test_traversal(cx: &mut gpui::TestAppContext) {
+    async fn test_traversal(cx: &mut TestAppContext) {
         let fs = FakeFs::new(cx.background());
         fs.insert_tree(
             "/root",
@@ -2727,11 +2767,23 @@ mod tests {
                     Path::new("a/c"),
                 ]
             );
+            assert_eq!(
+                tree.entries(true)
+                    .map(|entry| entry.path.as_ref())
+                    .collect::<Vec<_>>(),
+                vec![
+                    Path::new(""),
+                    Path::new(".gitignore"),
+                    Path::new("a"),
+                    Path::new("a/b"),
+                    Path::new("a/c"),
+                ]
+            );
         })
     }
 
     #[gpui::test]
-    async fn test_rescan_with_gitignore(cx: &mut gpui::TestAppContext) {
+    async fn test_rescan_with_gitignore(cx: &mut TestAppContext) {
         let dir = temp_tree(json!({
             ".git": {},
             ".gitignore": "ignored-dir\n",
@@ -2781,6 +2833,59 @@ mod tests {
         });
     }
 
+    #[gpui::test]
+    async fn test_write_file(cx: &mut TestAppContext) {
+        let dir = temp_tree(json!({
+            ".git": {},
+            ".gitignore": "ignored-dir\n",
+            "tracked-dir": {},
+            "ignored-dir": {}
+        }));
+
+        let http_client = FakeHttpClient::with_404_response();
+        let client = Client::new(http_client.clone());
+
+        let tree = Worktree::local(
+            client,
+            dir.path(),
+            true,
+            Arc::new(RealFs),
+            Default::default(),
+            &mut cx.to_async(),
+        )
+        .await
+        .unwrap();
+        cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+            .await;
+        tree.flush_fs_events(&cx).await;
+
+        tree.update(cx, |tree, cx| {
+            tree.as_local().unwrap().write_file(
+                Path::new("tracked-dir/file.txt"),
+                "hello".into(),
+                cx,
+            )
+        })
+        .await
+        .unwrap();
+        tree.update(cx, |tree, cx| {
+            tree.as_local().unwrap().write_file(
+                Path::new("ignored-dir/file.txt"),
+                "world".into(),
+                cx,
+            )
+        })
+        .await
+        .unwrap();
+
+        tree.read_with(cx, |tree, _| {
+            let tracked = tree.entry_for_path("tracked-dir/file.txt").unwrap();
+            let ignored = tree.entry_for_path("ignored-dir/file.txt").unwrap();
+            assert_eq!(tracked.is_ignored, false);
+            assert_eq!(ignored.is_ignored, true);
+        });
+    }
+
     #[gpui::test(iterations = 100)]
     fn test_random(mut rng: StdRng) {
         let operations = env::var("OPERATIONS")
@@ -3072,12 +3177,18 @@ mod tests {
                 }
             }
 
-            let dfs_paths = self
+            let dfs_paths_via_iter = self
                 .entries_by_path
                 .cursor::<()>()
                 .map(|e| e.path.as_ref())
                 .collect::<Vec<_>>();
-            assert_eq!(bfs_paths, dfs_paths);
+            assert_eq!(bfs_paths, dfs_paths_via_iter);
+
+            let dfs_paths_via_traversal = self
+                .entries(true)
+                .map(|e| e.path.as_ref())
+                .collect::<Vec<_>>();
+            assert_eq!(dfs_paths_via_traversal, dfs_paths_via_iter);
 
             for (ignore_parent_path, _) in &self.ignores {
                 assert!(self.entry_for_path(ignore_parent_path).is_some());

crates/project_panel/src/project_panel.rs 🔗

@@ -59,6 +59,7 @@ struct EntryDetails {
     filename: String,
     depth: usize,
     kind: EntryKind,
+    is_ignored: bool,
     is_expanded: bool,
     is_selected: bool,
     is_editing: bool,
@@ -613,7 +614,7 @@ impl ProjectPanel {
             }
 
             let mut visible_worktree_entries = Vec::new();
-            let mut entry_iter = snapshot.entries(false);
+            let mut entry_iter = snapshot.entries(true);
             while let Some(entry) = entry_iter.entry() {
                 visible_worktree_entries.push(entry.clone());
                 if Some(entry.id) == new_entry_parent_id {
@@ -739,6 +740,7 @@ impl ProjectPanel {
                             .to_string(),
                         depth: entry.path.components().count(),
                         kind: entry.kind,
+                        is_ignored: entry.is_ignored,
                         is_expanded: expanded_entry_ids.binary_search(&entry.id).is_ok(),
                         is_selected: self.selection.map_or(false, |e| {
                             e.worktree_id == snapshot.id() && e.entry_id == entry.id
@@ -784,7 +786,11 @@ impl ProjectPanel {
         let show_editor = details.is_editing && !details.is_processing;
         MouseEventHandler::new::<Self, _, _>(entry_id.to_usize(), cx, |state, _| {
             let padding = theme.container.padding.left + details.depth as f32 * theme.indent_width;
-            let style = theme.entry.style_for(state, details.is_selected);
+            let mut style = theme.entry.style_for(state, details.is_selected).clone();
+            if details.is_ignored {
+                style.text.color.fade_out(theme.ignored_entry_fade);
+                style.icon_color.fade_out(theme.ignored_entry_fade);
+            }
             let row_container_style = if show_editor {
                 theme.filename_editor.container
             } else {
@@ -966,6 +972,7 @@ mod tests {
             visible_entries_as_strings(&panel, 0..50, cx),
             &[
                 "v root1",
+                "    > .git",
                 "    > a",
                 "    > b",
                 "    > C",
@@ -981,6 +988,7 @@ mod tests {
             visible_entries_as_strings(&panel, 0..50, cx),
             &[
                 "v root1",
+                "    > .git",
                 "    > a",
                 "    v b  <== selected",
                 "        > 3",
@@ -994,7 +1002,7 @@ mod tests {
         );
 
         assert_eq!(
-            visible_entries_as_strings(&panel, 5..8, cx),
+            visible_entries_as_strings(&panel, 6..9, cx),
             &[
                 //
                 "    > C",
@@ -1058,6 +1066,7 @@ mod tests {
             visible_entries_as_strings(&panel, 0..10, cx),
             &[
                 "v root1  <== selected",
+                "    > .git",
                 "    > a",
                 "    > b",
                 "    > C",
@@ -1076,6 +1085,7 @@ mod tests {
             visible_entries_as_strings(&panel, 0..10, cx),
             &[
                 "v root1",
+                "    > .git",
                 "    > a",
                 "    > b",
                 "    > C",
@@ -1097,6 +1107,7 @@ mod tests {
             visible_entries_as_strings(&panel, 0..10, cx),
             &[
                 "v root1",
+                "    > .git",
                 "    > a",
                 "    > b",
                 "    > C",
@@ -1113,6 +1124,7 @@ mod tests {
             visible_entries_as_strings(&panel, 0..10, cx),
             &[
                 "v root1",
+                "    > .git",
                 "    > a",
                 "    > b",
                 "    > C",
@@ -1127,9 +1139,10 @@ mod tests {
         select_path(&panel, "root1/b", cx);
         panel.update(cx, |panel, cx| panel.add_file(&AddFile, cx));
         assert_eq!(
-            visible_entries_as_strings(&panel, 0..9, cx),
+            visible_entries_as_strings(&panel, 0..10, cx),
             &[
                 "v root1",
+                "    > .git",
                 "    > a",
                 "    v b",
                 "        > 3",
@@ -1151,9 +1164,10 @@ mod tests {
             .await
             .unwrap();
         assert_eq!(
-            visible_entries_as_strings(&panel, 0..9, cx),
+            visible_entries_as_strings(&panel, 0..10, cx),
             &[
                 "v root1",
+                "    > .git",
                 "    > a",
                 "    v b",
                 "        > 3",
@@ -1168,9 +1182,10 @@ mod tests {
         select_path(&panel, "root1/b/another-filename", cx);
         panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
         assert_eq!(
-            visible_entries_as_strings(&panel, 0..9, cx),
+            visible_entries_as_strings(&panel, 0..10, cx),
             &[
                 "v root1",
+                "    > .git",
                 "    > a",
                 "    v b",
                 "        > 3",
@@ -1189,9 +1204,10 @@ mod tests {
             panel.confirm(&Confirm, cx).unwrap()
         });
         assert_eq!(
-            visible_entries_as_strings(&panel, 0..9, cx),
+            visible_entries_as_strings(&panel, 0..10, cx),
             &[
                 "v root1",
+                "    > .git",
                 "    > a",
                 "    v b",
                 "        > 3",
@@ -1205,9 +1221,10 @@ mod tests {
 
         confirm.await.unwrap();
         assert_eq!(
-            visible_entries_as_strings(&panel, 0..9, cx),
+            visible_entries_as_strings(&panel, 0..10, cx),
             &[
                 "v root1",
+                "    > .git",
                 "    > a",
                 "    v b",
                 "        > 3",
@@ -1221,9 +1238,10 @@ mod tests {
 
         panel.update(cx, |panel, cx| panel.add_directory(&AddDirectory, cx));
         assert_eq!(
-            visible_entries_as_strings(&panel, 0..9, cx),
+            visible_entries_as_strings(&panel, 0..10, cx),
             &[
                 "v root1",
+                "    > .git",
                 "    > a",
                 "    v b",
                 "        > [EDITOR: '']  <== selected",
@@ -1243,9 +1261,10 @@ mod tests {
         });
         panel.update(cx, |panel, cx| panel.select_next(&Default::default(), cx));
         assert_eq!(
-            visible_entries_as_strings(&panel, 0..9, cx),
+            visible_entries_as_strings(&panel, 0..10, cx),
             &[
                 "v root1",
+                "    > .git",
                 "    > a",
                 "    v b",
                 "        > [PROCESSING: 'new-dir']",
@@ -1259,9 +1278,10 @@ mod tests {
 
         confirm.await.unwrap();
         assert_eq!(
-            visible_entries_as_strings(&panel, 0..9, cx),
+            visible_entries_as_strings(&panel, 0..10, cx),
             &[
                 "v root1",
+                "    > .git",
                 "    > a",
                 "    v b",
                 "        > 3  <== selected",

crates/theme/src/theme.rs 🔗

@@ -223,11 +223,12 @@ pub struct ProjectPanel {
     #[serde(flatten)]
     pub container: ContainerStyle,
     pub entry: Interactive<ProjectPanelEntry>,
+    pub ignored_entry_fade: f32,
     pub filename_editor: FieldEditor,
     pub indent_width: f32,
 }
 
-#[derive(Debug, Deserialize, Default)]
+#[derive(Clone, Debug, Deserialize, Default)]
 pub struct ProjectPanelEntry {
     pub height: f32,
     #[serde(flatten)]

styles/src/styleTree/projectPanel.ts 🔗

@@ -26,6 +26,7 @@ export default function projectPanel(theme: Theme) {
         text: text(theme, "mono", "active", { size: "sm" }),
       }
     },
+    ignoredEntryFade: 0.6,
     filenameEditor: {
       background: backgroundColor(theme, 500, "active"),
       text: text(theme, "mono", "primary", { size: "sm" }),