Rename 'project_core' crate to 'worktree', make it just about worktrees (#9189)

Max Brunsfeld created

This is just a refactor. I noticed that we now have a `project_core`
crate, which mainly contains the `Worktree` type and its private
helpers, plus the project's settings.

In this PR, I've renamed that crate to `worktree` and did some minor
simplification to its module structure. I also extracted a new
`WorktreeSettings` settings type from the `ProjectSettings`, so that the
worktree settings could live in the worktree crate. This way, the crate
is now exclusively about worktree logic.

Release Notes:

- N/A

Change summary

Cargo.lock                                        | 71 ++++++------
Cargo.toml                                        |  4 
crates/collab/src/tests/following_tests.rs        |  6 
crates/editor/src/editor.rs                       |  2 
crates/extension/Cargo.toml                       |  1 
crates/extension/src/build_extension.rs           | 14 +-
crates/file_finder/src/file_finder_tests.rs       |  2 
crates/project/Cargo.toml                         |  7 
crates/project/src/project.rs                     | 14 +
crates/project/src/project_settings.rs            | 19 ---
crates/project/src/project_tests.rs               |  1 
crates/project/src/task_inventory.rs              |  4 
crates/project_core/src/lib.rs                    | 81 ---------------
crates/project_panel/src/project_panel.rs         | 18 +-
crates/semantic_index/src/semantic_index_tests.rs |  4 
crates/worktree/Cargo.toml                        |  7 
crates/worktree/LICENSE-GPL                       |  0 
crates/worktree/src/ignore.rs                     |  0 
crates/worktree/src/worktree.rs                   | 91 +++++++++++++++-
crates/worktree/src/worktree_settings.rs          | 40 +++++++
crates/worktree/src/worktree_tests.rs             | 15 +-
crates/zed/src/zed.rs                             |  4 
22 files changed, 215 insertions(+), 190 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -7225,11 +7225,11 @@ dependencies = [
  "postage",
  "prettier",
  "pretty_assertions",
- "project_core",
  "rand 0.8.5",
  "regex",
  "release_channel",
  "rpc",
+ "schemars",
  "serde",
  "serde_json",
  "settings",
@@ -7242,40 +7242,7 @@ dependencies = [
  "unindent",
  "util",
  "which 6.0.0",
-]
-
-[[package]]
-name = "project_core"
-version = "0.1.0"
-dependencies = [
- "anyhow",
- "client",
- "clock",
- "collections",
- "fs",
- "futures 0.3.28",
- "fuzzy",
- "git",
- "git2",
- "gpui",
- "ignore",
- "itertools 0.11.0",
- "language",
- "log",
- "lsp",
- "parking_lot 0.11.2",
- "postage",
- "pretty_assertions",
- "rand 0.8.5",
- "rpc",
- "schemars",
- "serde",
- "serde_json",
- "settings",
- "smol",
- "sum_tree",
- "text",
- "util",
+ "worktree",
 ]
 
 [[package]]
@@ -12497,6 +12464,40 @@ dependencies = [
  "uuid",
 ]
 
+[[package]]
+name = "worktree"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "client",
+ "clock",
+ "collections",
+ "fs",
+ "futures 0.3.28",
+ "fuzzy",
+ "git",
+ "git2",
+ "gpui",
+ "ignore",
+ "itertools 0.11.0",
+ "language",
+ "log",
+ "lsp",
+ "parking_lot 0.11.2",
+ "postage",
+ "pretty_assertions",
+ "rand 0.8.5",
+ "rpc",
+ "schemars",
+ "serde",
+ "serde_json",
+ "settings",
+ "smol",
+ "sum_tree",
+ "text",
+ "util",
+]
+
 [[package]]
 name = "ws2_32-sys"
 version = "0.2.1"

Cargo.toml 🔗

@@ -54,7 +54,6 @@ members = [
     "crates/picker",
     "crates/prettier",
     "crates/project",
-    "crates/project_core",
     "crates/project_panel",
     "crates/project_symbols",
     "crates/quick_action_bar",
@@ -90,6 +89,7 @@ members = [
     "crates/vim",
     "crates/welcome",
     "crates/workspace",
+    "crates/worktree",
     "crates/zed",
     "crates/zed_actions",
 
@@ -159,7 +159,7 @@ plugin = { path = "crates/plugin" }
 plugin_macros = { path = "crates/plugin_macros" }
 prettier = { path = "crates/prettier" }
 project = { path = "crates/project" }
-project_core = { path = "crates/project_core" }
+worktree = { path = "crates/worktree" }
 project_panel = { path = "crates/project_panel" }
 project_symbols = { path = "crates/project_symbols" }
 quick_action_bar = { path = "crates/quick_action_bar" }

crates/collab/src/tests/following_tests.rs 🔗

@@ -12,7 +12,7 @@ use gpui::{
 };
 use language::Capability;
 use live_kit_client::MacOSDisplay;
-use project::project_settings::ProjectSettings;
+use project::WorktreeSettings;
 use rpc::proto::PeerId;
 use serde_json::json;
 use settings::SettingsStore;
@@ -1646,8 +1646,8 @@ async fn test_following_into_excluded_file(
     for cx in [&mut cx_a, &mut cx_b] {
         cx.update(|cx| {
             cx.update_global::<SettingsStore, _>(|store, cx| {
-                store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
-                    project_settings.file_scan_exclusions = Some(vec!["**/.git".to_string()]);
+                store.update_user_settings::<WorktreeSettings>(cx, |settings| {
+                    settings.file_scan_exclusions = Some(vec!["**/.git".to_string()]);
                 });
             });
         });

crates/editor/src/editor.rs 🔗

@@ -3245,7 +3245,7 @@ impl Editor {
             .filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty())
             .filter_map(|(buffer_handle, excerpt_visible_range, excerpt_id)| {
                 let buffer = buffer_handle.read(cx);
-                let buffer_file = project::worktree::File::from_dyn(buffer.file())?;
+                let buffer_file = project::File::from_dyn(buffer.file())?;
                 let buffer_worktree = project.worktree_for_id(buffer_file.worktree_id(cx), cx)?;
                 let worktree_entry = buffer_worktree
                     .read(cx)

crates/extension/Cargo.toml 🔗

@@ -10,6 +10,7 @@ workspace = true
 
 [lib]
 path = "src/extension_store.rs"
+doctest = false
 
 [[bin]]
 name = "extension_json_schemas"

crates/extension/src/build_extension.rs 🔗

@@ -260,13 +260,6 @@ impl ExtensionBuilder {
             .args(["fetch", "--depth", "1", "origin", &rev])
             .output()
             .context("failed to execute `git fetch`")?;
-        if !fetch_output.status.success() {
-            bail!(
-                "failed to fetch revision {} in directory '{}'",
-                rev,
-                directory.display()
-            );
-        }
 
         let checkout_output = Command::new("git")
             .arg("--git-dir")
@@ -276,6 +269,13 @@ impl ExtensionBuilder {
             .output()
             .context("failed to execute `git checkout`")?;
         if !checkout_output.status.success() {
+            if !fetch_output.status.success() {
+                bail!(
+                    "failed to fetch revision {} in directory '{}'",
+                    rev,
+                    directory.display()
+                );
+            }
             bail!(
                 "failed to checkout revision {} in directory '{}'",
                 rev,

crates/file_finder/src/file_finder_tests.rs 🔗

@@ -4,7 +4,7 @@ use super::*;
 use editor::Editor;
 use gpui::{Entity, TestAppContext, VisualTestContext};
 use menu::{Confirm, SelectNext};
-use project::worktree::FS_WATCH_LATENCY;
+use project::FS_WATCH_LATENCY;
 use serde_json::json;
 use workspace::{AppState, Workspace};
 

crates/project/Cargo.toml 🔗

@@ -19,7 +19,7 @@ test-support = [
     "settings/test-support",
     "text/test-support",
     "prettier/test-support",
-    "project_core/test-support",
+    "worktree/test-support",
     "gpui/test-support",
 ]
 
@@ -44,10 +44,11 @@ node_runtime.workspace = true
 parking_lot.workspace = true
 postage.workspace = true
 prettier.workspace = true
-project_core.workspace = true
+worktree.workspace = true
 rand.workspace = true
 regex.workspace = true
 rpc.workspace = true
+schemars.workspace = true
 task.workspace = true
 serde.workspace = true
 serde_json.workspace = true
@@ -72,7 +73,7 @@ release_channel.workspace = true
 lsp = { workspace = true, features = ["test-support"] }
 prettier = { workspace = true, features = ["test-support"] }
 pretty_assertions.workspace = true
-project_core = { workspace = true, features = ["test-support"] }
+worktree = { workspace = true, features = ["test-support"] }
 rpc = { workspace = true, features = ["test-support"] }
 settings = { workspace = true, features = ["test-support"] }
 unindent.workspace = true

crates/project/src/project.rs 🔗

@@ -2,6 +2,7 @@ pub mod debounced_delay;
 pub mod lsp_command;
 pub mod lsp_ext_command;
 mod prettier_support;
+pub mod project_settings;
 pub mod search;
 mod task_inventory;
 pub mod terminals;
@@ -56,9 +57,9 @@ use node_runtime::NodeRuntime;
 use parking_lot::{Mutex, RwLock};
 use postage::watch;
 use prettier_support::{DefaultPrettier, PrettierInstance};
-use project_core::project_settings::{LspSettings, ProjectSettings};
-pub use project_core::{DiagnosticSummary, ProjectEntryId};
+use project_settings::{LspSettings, ProjectSettings};
 use rand::prelude::*;
+use worktree::LocalSnapshot;
 
 use rpc::{ErrorCode, ErrorExt as _};
 use search::SearchQuery;
@@ -96,16 +97,20 @@ use util::{
     paths::{LOCAL_SETTINGS_RELATIVE_PATH, LOCAL_TASKS_RELATIVE_PATH},
     post_inc, ResultExt, TryFutureExt as _,
 };
+use worktree::{Snapshot, Traversal};
 
 pub use fs::*;
 pub use language::Location;
 #[cfg(any(test, feature = "test-support"))]
 pub use prettier::FORMAT_SUFFIX as TEST_PRETTIER_FORMAT_SUFFIX;
-pub use project_core::project_settings;
-pub use project_core::worktree::{self, *};
 #[cfg(feature = "test-support")]
 pub use task_inventory::test_inventory::*;
 pub use task_inventory::{Inventory, TaskSourceKind};
+pub use worktree::{
+    DiagnosticSummary, Entry, EntryKind, File, LocalWorktree, PathChange, ProjectEntryId,
+    RepositoryEntry, UpdatedEntriesSet, UpdatedGitRepositoriesSet, Worktree, WorktreeId,
+    WorktreeSettings, FS_WATCH_LATENCY,
+};
 
 const MAX_SERVER_REINSTALL_ATTEMPT_COUNT: u64 = 4;
 const SERVER_REINSTALL_DEBOUNCE_TIMEOUT: Duration = Duration::from_secs(1);
@@ -492,6 +497,7 @@ impl SearchMatchCandidate {
 
 impl Project {
     pub fn init_settings(cx: &mut AppContext) {
+        WorktreeSettings::register(cx);
         ProjectSettings::register(cx);
     }
 

crates/project_core/src/project_settings.rs → crates/project/src/project_settings.rs 🔗

@@ -20,25 +20,6 @@ pub struct ProjectSettings {
     /// Configuration for Git-related features
     #[serde(default)]
     pub git: GitSettings,
-
-    /// Completely ignore files matching globs from `file_scan_exclusions`
-    ///
-    /// Default: [
-    ///   "**/.git",
-    ///   "**/.svn",
-    ///   "**/.hg",
-    ///   "**/CVS",
-    ///   "**/.DS_Store",
-    ///   "**/Thumbs.db",
-    ///   "**/.classpath",
-    ///   "**/.settings"
-    /// ]
-    #[serde(default)]
-    pub file_scan_exclusions: Option<Vec<String>>,
-
-    /// Treat the files matching these globs as `.env` files.
-    /// Default: [ "**/.env*" ]
-    pub private_files: Option<Vec<String>>,
 }
 
 #[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]

crates/project/src/project_tests.rs 🔗

@@ -14,6 +14,7 @@ use serde_json::json;
 use std::{os, task::Poll};
 use unindent::Unindent as _;
 use util::{assert_set_eq, paths::PathMatcher, test::temp_tree};
+use worktree::WorktreeModelHandle as _;
 
 #[gpui::test]
 async fn test_block_via_channel(cx: &mut gpui::TestAppContext) {

crates/project/src/task_inventory.rs 🔗

@@ -9,9 +9,9 @@ use std::{
 use collections::{HashMap, VecDeque};
 use gpui::{AppContext, Context, Model, ModelContext, Subscription};
 use itertools::Itertools;
-use project_core::worktree::WorktreeId;
 use task::{Task, TaskContext, TaskId, TaskSource};
 use util::{post_inc, NumericPrefixWithSuffix};
+use worktree::WorktreeId;
 
 /// Inventory tracks available tasks for a given project.
 pub struct Inventory {
@@ -230,8 +230,8 @@ pub mod test_inventory {
     use std::{path::Path, sync::Arc};
 
     use gpui::{AppContext, Context as _, Model, ModelContext, TestAppContext};
-    use project_core::worktree::WorktreeId;
     use task::{Task, TaskContext, TaskId, TaskSource};
+    use worktree::WorktreeId;
 
     use crate::Inventory;
 

crates/project_core/src/lib.rs 🔗

@@ -1,81 +0,0 @@
-use std::path::Path;
-use std::sync::atomic::AtomicUsize;
-use std::sync::atomic::Ordering::SeqCst;
-
-use language::DiagnosticEntry;
-use lsp::{DiagnosticSeverity, LanguageServerId};
-use rpc::proto;
-use serde::Serialize;
-
-mod ignore;
-pub mod project_settings;
-pub mod worktree;
-#[cfg(test)]
-mod worktree_tests;
-
-#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord)]
-pub struct ProjectEntryId(usize);
-
-impl ProjectEntryId {
-    pub const MAX: Self = Self(usize::MAX);
-
-    pub fn new(counter: &AtomicUsize) -> Self {
-        Self(counter.fetch_add(1, SeqCst))
-    }
-
-    pub fn from_proto(id: u64) -> Self {
-        Self(id as usize)
-    }
-
-    pub fn to_proto(&self) -> u64 {
-        self.0 as u64
-    }
-
-    pub fn to_usize(&self) -> usize {
-        self.0
-    }
-}
-
-#[derive(Copy, Clone, Debug, Default, PartialEq, Serialize)]
-pub struct DiagnosticSummary {
-    pub error_count: usize,
-    pub warning_count: usize,
-}
-
-impl DiagnosticSummary {
-    fn new<'a, T: 'a>(diagnostics: impl IntoIterator<Item = &'a DiagnosticEntry<T>>) -> Self {
-        let mut this = Self {
-            error_count: 0,
-            warning_count: 0,
-        };
-
-        for entry in diagnostics {
-            if entry.diagnostic.is_primary {
-                match entry.diagnostic.severity {
-                    DiagnosticSeverity::ERROR => this.error_count += 1,
-                    DiagnosticSeverity::WARNING => this.warning_count += 1,
-                    _ => {}
-                }
-            }
-        }
-
-        this
-    }
-
-    pub fn is_empty(&self) -> bool {
-        self.error_count == 0 && self.warning_count == 0
-    }
-
-    pub fn to_proto(
-        &self,
-        language_server_id: LanguageServerId,
-        path: &Path,
-    ) -> proto::DiagnosticSummary {
-        proto::DiagnosticSummary {
-            path: path.to_string_lossy().to_string(),
-            language_server_id: language_server_id.0 as u64,
-            error_count: self.error_count as u32,
-            warning_count: self.warning_count as u32,
-        }
-    }
-}

crates/project_panel/src/project_panel.rs 🔗

@@ -1717,7 +1717,7 @@ mod tests {
     use collections::HashSet;
     use gpui::{TestAppContext, View, VisualTestContext, WindowHandle};
     use pretty_assertions::assert_eq;
-    use project::{project_settings::ProjectSettings, FakeFs};
+    use project::{FakeFs, WorktreeSettings};
     use serde_json::json;
     use settings::SettingsStore;
     use std::path::{Path, PathBuf};
@@ -1819,8 +1819,8 @@ mod tests {
         init_test(cx);
         cx.update(|cx| {
             cx.update_global::<SettingsStore, _>(|store, cx| {
-                store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
-                    project_settings.file_scan_exclusions =
+                store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
+                    worktree_settings.file_scan_exclusions =
                         Some(vec!["**/.git".to_string(), "**/4/**".to_string()]);
                 });
             });
@@ -3026,8 +3026,8 @@ mod tests {
         init_test_with_editor(cx);
         cx.update(|cx| {
             cx.update_global::<SettingsStore, _>(|store, cx| {
-                store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
-                    project_settings.file_scan_exclusions = Some(Vec::new());
+                store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
+                    worktree_settings.file_scan_exclusions = Some(Vec::new());
                 });
                 store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
                     project_panel_settings.auto_reveal_entries = Some(false)
@@ -3264,8 +3264,8 @@ mod tests {
         init_test_with_editor(cx);
         cx.update(|cx| {
             cx.update_global::<SettingsStore, _>(|store, cx| {
-                store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
-                    project_settings.file_scan_exclusions = Some(Vec::new());
+                store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
+                    worktree_settings.file_scan_exclusions = Some(Vec::new());
                 });
                 store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
                     project_panel_settings.auto_reveal_entries = Some(false)
@@ -3582,8 +3582,8 @@ mod tests {
             Project::init_settings(cx);
 
             cx.update_global::<SettingsStore, _>(|store, cx| {
-                store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
-                    project_settings.file_scan_exclusions = Some(Vec::new());
+                store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
+                    worktree_settings.file_scan_exclusions = Some(Vec::new());
                 });
             });
         });

crates/semantic_index/src/semantic_index_tests.rs 🔗

@@ -10,7 +10,7 @@ use gpui::{Task, TestAppContext};
 use language::{Language, LanguageConfig, LanguageMatcher, LanguageRegistry, ToOffset};
 use parking_lot::Mutex;
 use pretty_assertions::assert_eq;
-use project::{project_settings::ProjectSettings, FakeFs, Fs, Project};
+use project::{FakeFs, Fs, Project};
 use rand::{rngs::StdRng, Rng};
 use serde_json::json;
 use settings::{Settings, SettingsStore};
@@ -1720,6 +1720,6 @@ fn init_test(cx: &mut TestAppContext) {
         let settings_store = SettingsStore::test(cx);
         cx.set_global(settings_store);
         SemanticIndexSettings::register(cx);
-        ProjectSettings::register(cx);
+        Project::init_settings(cx);
     });
 }

crates/project_core/Cargo.toml → crates/worktree/Cargo.toml 🔗

@@ -1,14 +1,17 @@
 [package]
-name = "project_core"
+name = "worktree"
 version = "0.1.0"
 edition = "2021"
 publish = false
 license = "GPL-3.0-or-later"
 
+[lib]
+path = "src/worktree.rs"
+doctest = false
+
 [lints]
 workspace = true
 
-# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 [features]
 test-support = [
     "client/test-support",

crates/project_core/src/worktree.rs → crates/worktree/src/worktree.rs 🔗

@@ -1,6 +1,8 @@
-use crate::{
-    ignore::IgnoreStack, project_settings::ProjectSettings, DiagnosticSummary, ProjectEntryId,
-};
+mod ignore;
+mod worktree_settings;
+#[cfg(test)]
+mod worktree_tests;
+
 use ::ignore::gitignore::{Gitignore, GitignoreBuilder};
 use anyhow::{anyhow, Context as _, Result};
 use client::{proto, Client};
@@ -26,6 +28,7 @@ use gpui::{
     AppContext, AsyncAppContext, BackgroundExecutor, Context, EventEmitter, Model, ModelContext,
     Task,
 };
+use ignore::IgnoreStack;
 use itertools::Itertools;
 use language::{
     proto::{
@@ -35,13 +38,14 @@ use language::{
     Buffer, Capability, DiagnosticEntry, File as _, LineEnding, PointUtf16, Rope, RopeFingerprint,
     Unclipped,
 };
-use lsp::LanguageServerId;
+use lsp::{DiagnosticSeverity, LanguageServerId};
 use parking_lot::Mutex;
 use postage::{
     barrier,
     prelude::{Sink as _, Stream as _},
     watch,
 };
+use serde::Serialize;
 use settings::{Settings, SettingsStore};
 use smol::channel::{self, Sender};
 use std::{
@@ -68,10 +72,12 @@ use util::{
     ResultExt,
 };
 
+pub use worktree_settings::WorktreeSettings;
+
 #[cfg(feature = "test-support")]
 pub const FS_WATCH_LATENCY: Duration = Duration::from_millis(100);
 #[cfg(not(feature = "test-support"))]
-const FS_WATCH_LATENCY: Duration = Duration::from_millis(100);
+pub const FS_WATCH_LATENCY: Duration = Duration::from_millis(100);
 
 #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
 pub struct WorktreeId(usize);
@@ -340,13 +346,13 @@ impl Worktree {
             cx.observe_global::<SettingsStore>(move |this, cx| {
                 if let Self::Local(this) = this {
                     let new_file_scan_exclusions = path_matchers(
-                        ProjectSettings::get_global(cx)
+                        WorktreeSettings::get_global(cx)
                             .file_scan_exclusions
                             .as_deref(),
                         "file_scan_exclusions",
                     );
                     let new_private_files = path_matchers(
-                        ProjectSettings::get(Some((cx.handle().entity_id().as_u64() as usize, &Path::new(""))), cx).private_files.as_deref(),
+                        WorktreeSettings::get(Some((cx.handle().entity_id().as_u64() as usize, &Path::new(""))), cx).private_files.as_deref(),
                         "private_files",
                     );
 
@@ -396,13 +402,13 @@ impl Worktree {
 
             let mut snapshot = LocalSnapshot {
                 file_scan_exclusions: path_matchers(
-                    ProjectSettings::get_global(cx)
+                    WorktreeSettings::get_global(cx)
                         .file_scan_exclusions
                         .as_deref(),
                     "file_scan_exclusions",
                 ),
                 private_files: path_matchers(
-                    ProjectSettings::get(Some((cx.handle().entity_id().as_u64() as usize, &Path::new(""))), cx).private_files.as_deref(),
+                    WorktreeSettings::get(Some((cx.handle().entity_id().as_u64() as usize, &Path::new(""))), cx).private_files.as_deref(),
                     "private_files",
                 ),
                 ignores_by_parent_abs_path: Default::default(),
@@ -4737,3 +4743,70 @@ fn git_status_to_proto(status: GitFileStatus) -> i32 {
         GitFileStatus::Conflict => proto::GitStatus::Conflict as i32,
     }
 }
+
+#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord)]
+pub struct ProjectEntryId(usize);
+
+impl ProjectEntryId {
+    pub const MAX: Self = Self(usize::MAX);
+
+    pub fn new(counter: &AtomicUsize) -> Self {
+        Self(counter.fetch_add(1, SeqCst))
+    }
+
+    pub fn from_proto(id: u64) -> Self {
+        Self(id as usize)
+    }
+
+    pub fn to_proto(&self) -> u64 {
+        self.0 as u64
+    }
+
+    pub fn to_usize(&self) -> usize {
+        self.0
+    }
+}
+
+#[derive(Copy, Clone, Debug, Default, PartialEq, Serialize)]
+pub struct DiagnosticSummary {
+    pub error_count: usize,
+    pub warning_count: usize,
+}
+
+impl DiagnosticSummary {
+    fn new<'a, T: 'a>(diagnostics: impl IntoIterator<Item = &'a DiagnosticEntry<T>>) -> Self {
+        let mut this = Self {
+            error_count: 0,
+            warning_count: 0,
+        };
+
+        for entry in diagnostics {
+            if entry.diagnostic.is_primary {
+                match entry.diagnostic.severity {
+                    DiagnosticSeverity::ERROR => this.error_count += 1,
+                    DiagnosticSeverity::WARNING => this.warning_count += 1,
+                    _ => {}
+                }
+            }
+        }
+
+        this
+    }
+
+    pub fn is_empty(&self) -> bool {
+        self.error_count == 0 && self.warning_count == 0
+    }
+
+    pub fn to_proto(
+        &self,
+        language_server_id: LanguageServerId,
+        path: &Path,
+    ) -> proto::DiagnosticSummary {
+        proto::DiagnosticSummary {
+            path: path.to_string_lossy().to_string(),
+            language_server_id: language_server_id.0 as u64,
+            error_count: self.error_count as u32,
+            warning_count: self.warning_count as u32,
+        }
+    }
+}

crates/worktree/src/worktree_settings.rs 🔗

@@ -0,0 +1,40 @@
+use gpui::AppContext;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use settings::Settings;
+
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
+pub struct WorktreeSettings {
+    /// Completely ignore files matching globs from `file_scan_exclusions`
+    ///
+    /// Default: [
+    ///   "**/.git",
+    ///   "**/.svn",
+    ///   "**/.hg",
+    ///   "**/CVS",
+    ///   "**/.DS_Store",
+    ///   "**/Thumbs.db",
+    ///   "**/.classpath",
+    ///   "**/.settings"
+    /// ]
+    #[serde(default)]
+    pub file_scan_exclusions: Option<Vec<String>>,
+
+    /// Treat the files matching these globs as `.env` files.
+    /// Default: [ "**/.env*" ]
+    pub private_files: Option<Vec<String>>,
+}
+
+impl Settings for WorktreeSettings {
+    const KEY: Option<&'static str> = None;
+
+    type FileContent = Self;
+
+    fn load(
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+        _: &mut AppContext,
+    ) -> anyhow::Result<Self> {
+        Self::load_via_json_merge(default_value, user_values)
+    }
+}

crates/project_core/src/worktree_tests.rs → crates/worktree/src/worktree_tests.rs 🔗

@@ -1,7 +1,6 @@
 use crate::{
-    project_settings::ProjectSettings,
-    worktree::{Entry, EntryKind, PathChange, Worktree},
-    worktree::{Event, Snapshot, WorktreeModelHandle},
+    worktree_settings::WorktreeSettings, Entry, EntryKind, Event, PathChange, Snapshot, Worktree,
+    WorktreeModelHandle,
 };
 use anyhow::Result;
 use client::Client;
@@ -804,7 +803,7 @@ async fn test_rescan_with_gitignore(cx: &mut TestAppContext) {
     init_test(cx);
     cx.update(|cx| {
         cx.update_global::<SettingsStore, _>(|store, cx| {
-            store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
+            store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
                 project_settings.file_scan_exclusions = Some(Vec::new());
             });
         });
@@ -996,7 +995,7 @@ async fn test_file_scan_exclusions(cx: &mut TestAppContext) {
     }));
     cx.update(|cx| {
         cx.update_global::<SettingsStore, _>(|store, cx| {
-            store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
+            store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
                 project_settings.file_scan_exclusions =
                     Some(vec!["**/foo/**".to_string(), "**/.DS_Store".to_string()]);
             });
@@ -1033,7 +1032,7 @@ async fn test_file_scan_exclusions(cx: &mut TestAppContext) {
 
     cx.update(|cx| {
         cx.update_global::<SettingsStore, _>(|store, cx| {
-            store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
+            store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
                 project_settings.file_scan_exclusions =
                     Some(vec!["**/node_modules/**".to_string()]);
             });
@@ -1097,7 +1096,7 @@ async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) {
     }));
     cx.update(|cx| {
         cx.update_global::<SettingsStore, _>(|store, cx| {
-            store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
+            store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
                 project_settings.file_scan_exclusions = Some(vec![
                     "**/.git".to_string(),
                     "node_modules/".to_string(),
@@ -2541,6 +2540,6 @@ fn init_test(cx: &mut gpui::TestAppContext) {
     cx.update(|cx| {
         let settings_store = SettingsStore::test(cx);
         cx.set_global(settings_store);
-        ProjectSettings::register(cx);
+        WorktreeSettings::register(cx);
     });
 }

crates/zed/src/zed.rs 🔗

@@ -866,7 +866,7 @@ mod tests {
         VisualTestContext, WindowHandle,
     };
     use language::{LanguageMatcher, LanguageRegistry};
-    use project::{project_settings::ProjectSettings, Project, ProjectPath};
+    use project::{Project, ProjectPath, WorktreeSettings};
     use serde_json::json;
     use settings::{handle_settings_file_changes, watch_config_file, SettingsStore};
     use std::path::{Path, PathBuf};
@@ -1480,7 +1480,7 @@ mod tests {
         let app_state = init_test(cx);
         cx.update(|cx| {
             cx.update_global::<SettingsStore, _>(|store, cx| {
-                store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
+                store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
                     project_settings.file_scan_exclusions =
                         Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]);
                 });