Rescan worktree on scan exclusions settings change

Kirill Bulatov created

Change summary

crates/project/src/project_settings.rs |   1 
crates/project/src/worktree.rs         | 193 ++++++++++++++++++---------
crates/util/src/paths.rs               |   8 +
3 files changed, 136 insertions(+), 66 deletions(-)

Detailed changes

crates/project/src/project_settings.rs 🔗

@@ -11,7 +11,6 @@ pub struct ProjectSettings {
     #[serde(default)]
     pub git: GitSettings,
     // TODO kb better names and docs and tests
-    // TODO kb how to react on their changes?
     #[serde(default)]
     pub scan_exclude_files: Vec<String>,
 }

crates/project/src/worktree.rs 🔗

@@ -22,7 +22,10 @@ use futures::{
 };
 use fuzzy::CharBag;
 use git::{DOT_GIT, GITIGNORE};
-use gpui::{executor, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task};
+use gpui::{
+    executor, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Subscription, Task,
+};
+use itertools::Itertools;
 use language::{
     proto::{
         deserialize_fingerprint, deserialize_version, serialize_fingerprint, serialize_line_ending,
@@ -37,6 +40,7 @@ use postage::{
     prelude::{Sink as _, Stream as _},
     watch,
 };
+use settings::SettingsStore;
 use smol::channel::{self, Sender};
 use std::{
     any::Any,
@@ -74,7 +78,8 @@ pub struct LocalWorktree {
     scan_requests_tx: channel::Sender<ScanRequest>,
     path_prefixes_to_scan_tx: channel::Sender<Arc<Path>>,
     is_scanning: (watch::Sender<bool>, watch::Receiver<bool>),
-    _background_scanner_task: Task<()>,
+    _settings_subscription: Subscription,
+    _background_scanner_tasks: Vec<Task<()>>,
     share: Option<ShareState>,
     diagnostics: HashMap<
         Arc<Path>,
@@ -304,30 +309,55 @@ impl Worktree {
             .await
             .context("failed to stat worktree path")?;
 
+        let closure_fs = Arc::clone(&fs);
+        let closure_next_entry_id = Arc::clone(&next_entry_id);
+        let closure_abs_path = abs_path.to_path_buf();
         Ok(cx.add_model(move |cx: &mut ModelContext<Worktree>| {
+            let settings_subscription = cx.observe_global::<SettingsStore, _>(move |this, cx| {
+                if let Self::Local(this) = this {
+                    let new_scan_exclude_files =
+                        scan_exclude_files(settings::get::<ProjectSettings>(cx));
+                    if new_scan_exclude_files != this.snapshot.scan_exclude_files {
+                        this.snapshot.scan_exclude_files = new_scan_exclude_files;
+                        log::info!(
+                            "Re-scanning due to new scan exclude files: {:?}",
+                            this.snapshot
+                                .scan_exclude_files
+                                .iter()
+                                .map(ToString::to_string)
+                                .collect::<Vec<_>>()
+                        );
+
+                        let (scan_requests_tx, scan_requests_rx) = channel::unbounded();
+                        let (path_prefixes_to_scan_tx, path_prefixes_to_scan_rx) =
+                            channel::unbounded();
+                        this.scan_requests_tx = scan_requests_tx;
+                        this.path_prefixes_to_scan_tx = path_prefixes_to_scan_tx;
+                        this._background_scanner_tasks = start_background_scan_tasks(
+                            &closure_abs_path,
+                            this.snapshot(),
+                            scan_requests_rx,
+                            path_prefixes_to_scan_rx,
+                            Arc::clone(&closure_next_entry_id),
+                            Arc::clone(&closure_fs),
+                            cx,
+                        );
+                        this.is_scanning = watch::channel_with(true);
+                        // TODO kb change more state? will this even work now?
+                    }
+                }
+            });
+
             let root_name = abs_path
                 .file_name()
                 .map_or(String::new(), |f| f.to_string_lossy().to_string());
-            let project_settings = settings::get::<ProjectSettings>(cx);
-            let scan_exclude_files = project_settings.scan_exclude_files.iter()
-            .filter_map(|pattern| {
-                PathMatcher::new(pattern)
-                    .map(Some)
-                    .unwrap_or_else(|e| {
-                        log::error!(
-                            "Skipping pattern {pattern} in `scan_exclude_files` project settings due to parsing error: {e:#}"
-                        );
-                        None
-                    })
-            })
-            .collect::<Vec<_>>();
             let mut snapshot = LocalSnapshot {
-                scan_exclude_files,
+                scan_exclude_files: scan_exclude_files(settings::get::<ProjectSettings>(cx)),
                 ignores_by_parent_abs_path: Default::default(),
                 git_repositories: Default::default(),
                 snapshot: Snapshot {
                     id: WorktreeId::from_usize(cx.model_id()),
-                    abs_path: abs_path.clone(),
+                    abs_path: abs_path.to_path_buf().into(),
                     root_name: root_name.clone(),
                     root_char_bag: root_name.chars().map(|c| c.to_ascii_lowercase()).collect(),
                     entries_by_path: Default::default(),
@@ -352,60 +382,23 @@ impl Worktree {
 
             let (scan_requests_tx, scan_requests_rx) = channel::unbounded();
             let (path_prefixes_to_scan_tx, path_prefixes_to_scan_rx) = channel::unbounded();
-            let (scan_states_tx, mut scan_states_rx) = mpsc::unbounded();
-
-            cx.spawn_weak(|this, mut cx| async move {
-                while let Some((state, this)) = scan_states_rx.next().await.zip(this.upgrade(&cx)) {
-                    this.update(&mut cx, |this, cx| {
-                        let this = this.as_local_mut().unwrap();
-                        match state {
-                            ScanState::Started => {
-                                *this.is_scanning.0.borrow_mut() = true;
-                            }
-                            ScanState::Updated {
-                                snapshot,
-                                changes,
-                                barrier,
-                                scanning,
-                            } => {
-                                *this.is_scanning.0.borrow_mut() = scanning;
-                                this.set_snapshot(snapshot, changes, cx);
-                                drop(barrier);
-                            }
-                        }
-                        cx.notify();
-                    });
-                }
-            })
-            .detach();
-
-            let background_scanner_task = cx.background().spawn({
-                let fs = fs.clone();
-                let snapshot = snapshot.clone();
-                let background = cx.background().clone();
-                async move {
-                    let events = fs.watch(&abs_path, Duration::from_millis(100)).await;
-                    BackgroundScanner::new(
-                        snapshot,
-                        next_entry_id,
-                        fs,
-                        scan_states_tx,
-                        background,
-                        scan_requests_rx,
-                        path_prefixes_to_scan_rx,
-                    )
-                    .run(events)
-                    .await;
-                }
-            });
-
+            let task_snapshot = snapshot.clone();
             Worktree::Local(LocalWorktree {
                 snapshot,
                 is_scanning: watch::channel_with(true),
                 share: None,
                 scan_requests_tx,
                 path_prefixes_to_scan_tx,
-                _background_scanner_task: background_scanner_task,
+                _settings_subscription: settings_subscription,
+                _background_scanner_tasks: start_background_scan_tasks(
+                    &abs_path,
+                    task_snapshot,
+                    scan_requests_rx,
+                    path_prefixes_to_scan_rx,
+                    Arc::clone(&next_entry_id),
+                    Arc::clone(&fs),
+                    cx,
+                ),
                 diagnostics: Default::default(),
                 diagnostic_summaries: Default::default(),
                 client,
@@ -602,6 +595,76 @@ impl Worktree {
     }
 }
 
+fn start_background_scan_tasks(
+    abs_path: &Path,
+    snapshot: LocalSnapshot,
+    scan_requests_rx: channel::Receiver<ScanRequest>,
+    path_prefixes_to_scan_rx: channel::Receiver<Arc<Path>>,
+    next_entry_id: Arc<AtomicUsize>,
+    fs: Arc<dyn Fs>,
+    cx: &mut ModelContext<'_, Worktree>,
+) -> Vec<Task<()>> {
+    let (scan_states_tx, mut scan_states_rx) = mpsc::unbounded();
+    let background_scanner = cx.background().spawn({
+        let abs_path = abs_path.to_path_buf();
+        let background = cx.background().clone();
+        async move {
+            let events = fs.watch(&abs_path, Duration::from_millis(100)).await;
+            BackgroundScanner::new(
+                snapshot,
+                next_entry_id,
+                fs,
+                scan_states_tx,
+                background,
+                scan_requests_rx,
+                path_prefixes_to_scan_rx,
+            )
+            .run(events)
+            .await;
+        }
+    });
+    let scan_state_updater = cx.spawn_weak(|this, mut cx| async move {
+        while let Some((state, this)) = scan_states_rx.next().await.zip(this.upgrade(&cx)) {
+            this.update(&mut cx, |this, cx| {
+                let this = this.as_local_mut().unwrap();
+                match state {
+                    ScanState::Started => {
+                        *this.is_scanning.0.borrow_mut() = true;
+                    }
+                    ScanState::Updated {
+                        snapshot,
+                        changes,
+                        barrier,
+                        scanning,
+                    } => {
+                        *this.is_scanning.0.borrow_mut() = scanning;
+                        this.set_snapshot(snapshot, changes, cx);
+                        drop(barrier);
+                    }
+                }
+                cx.notify();
+            });
+        }
+    });
+    vec![background_scanner, scan_state_updater]
+}
+
+fn scan_exclude_files(project_settings: &ProjectSettings) -> Vec<PathMatcher> {
+    project_settings.scan_exclude_files.iter()
+    .sorted()
+    .filter_map(|pattern| {
+        PathMatcher::new(pattern)
+            .map(Some)
+            .unwrap_or_else(|e| {
+                log::error!(
+                    "Skipping pattern {pattern} in `scan_exclude_files` project settings due to parsing error: {e:#}"
+                );
+                None
+            })
+    })
+    .collect()
+}
+
 impl LocalWorktree {
     pub fn contains_abs_path(&self, path: &Path) -> bool {
         path.starts_with(&self.abs_path)

crates/util/src/paths.rs 🔗

@@ -202,6 +202,14 @@ impl std::fmt::Display for PathMatcher {
     }
 }
 
+impl PartialEq for PathMatcher {
+    fn eq(&self, other: &Self) -> bool {
+        self.maybe_path.eq(&other.maybe_path)
+    }
+}
+
+impl Eq for PathMatcher {}
+
 impl PathMatcher {
     pub fn new(maybe_glob: &str) -> Result<Self, globset::Error> {
         Ok(PathMatcher {