fs: Replace GlobalWatcher with an OsWatcher that contains one backend (#63898)

Before, `GlobalWatcher` held a native backend and a polling backend, and
its methods dispatched to one or the other.

After this PR, we have `OsWatcher` which wraps a single watch backend,
and `RealFs` and `FakeFs` hold a native `OsWatcher` and a polling
`OsWatcher`.

This moves the split closer to the place where we decide which backend
should be used, and lets us clean up some indirection. A nice bonus is
that the "is this backend recursive" data exists in exactly one place.

Release Notes:
- N/A
This commit is contained in:
Cole Miller authored and GitHub committed 2026-09-07 20:42:19 +00:00
1 parent 102a6c5d8a
commit 7f03a472b9
4 files changed
+280 -351

No files matched your search

+25 -10
View File
@@ -446,7 +446,8 @@ pub struct RealFs {
this: std::sync::Weak<Self>,
bundled_git_binary_path: Option<PathBuf>,
executor: BackgroundExecutor,
global_watcher: Arc<fs_watcher::GlobalWatcher>,
native_watcher: Arc<fs_watcher::OsWatcher>,
poll_watcher: Arc<fs_watcher::OsWatcher>,
next_job_id: Arc<AtomicUsize>,
job_event_subscribers: Arc<Mutex<Vec<JobEventSender>>>,
trash: Arc<Mutex<SlotMap<TrashId, TrashedEntry>>>,
@@ -556,7 +557,14 @@ impl RealFs {
Arc::new_cyclic(|this| Self {
this: this.clone(),
bundled_git_binary_path: git_binary_path,
global_watcher: fs_watcher::GlobalWatcher::new(executor.clone()),
native_watcher: fs_watcher::OsWatcher::new(
fs_watcher::OsWatcherKind::Native,
executor.clone(),
),
poll_watcher: fs_watcher::OsWatcher::new(
fs_watcher::OsWatcherKind::Poll,
executor.clone(),
),
executor,
next_job_id: Arc::new(AtomicUsize::new(0)),
job_event_subscribers: Arc::new(Mutex::new(Vec::new())),
@@ -1164,7 +1172,7 @@ impl Fs for RealFs {
}
fn start_native_watcher(&self) -> Result<()> {
self.global_watcher.ensure_native_watcher()
self.native_watcher.ensure_backend()
}
fn path_exists(&self, path: &Path) -> bool {
@@ -1193,7 +1201,8 @@ impl Fs for RealFs {
.expect("RealFs is only constructed inside an Arc");
fs_watcher::watch(
this,
self.global_watcher.clone(),
self.native_watcher.clone(),
self.poll_watcher.clone(),
self.executor.clone(),
path,
latency,
@@ -1419,7 +1428,8 @@ pub struct FakeFs {
// Use an unfair lock to ensure tests are deterministic.
state: Arc<Mutex<FakeFsState>>,
executor: gpui::BackgroundExecutor,
global_watcher: Arc<fs_watcher::GlobalWatcher>,
native_watcher: Arc<fs_watcher::OsWatcher>,
poll_watcher: Arc<fs_watcher::OsWatcher>,
}
#[cfg(feature = "test-support")]
@@ -1443,7 +1453,7 @@ struct FakeFsState {
/// The kernel's side of file watching, as far as the real watcher code above
/// notify can tell: the paths the backend has registered and the callback that
/// delivers events for them into the `GlobalWatcher`.
/// delivers events for them into the native `OsWatcher`.
#[cfg(feature = "test-support")]
#[derive(Default)]
struct FakeWatches {
@@ -1810,19 +1820,23 @@ impl FakeFs {
remove_dir_errors: Default::default(),
case_sensitive: true,
}));
let global_watcher = fs_watcher::GlobalWatcher::with_native_backend(
let native_watcher = fs_watcher::OsWatcher::with_backend(
fs_watcher::OsWatcherKind::Native,
executor.clone(),
Some(Box::new(FakeWatchBackend {
state: state.clone(),
})),
);
state.lock().watches.event_sink = Some(Box::new(global_watcher.native_event_sink()));
state.lock().watches.event_sink = Some(Box::new(native_watcher.event_sink()));
let poll_watcher =
fs_watcher::OsWatcher::new(fs_watcher::OsWatcherKind::Poll, executor.clone());
let this = Arc::new_cyclic(|this| Self {
this: this.clone(),
executor: executor.clone(),
state,
global_watcher,
native_watcher,
poll_watcher,
});
executor.spawn({
@@ -3331,7 +3345,8 @@ impl Fs for FakeFs {
// own, so a real debounce would stall every test until `advance_clock`.
let (events, watcher) = fs_watcher::watch(
self.this.upgrade().unwrap(),
self.global_watcher.clone(),
self.native_watcher.clone(),
self.poll_watcher.clone(),
self.executor.clone(),
path,
Duration::ZERO,
File diff suppressed because it is too large. Load diff
@@ -77,13 +77,16 @@ pub fn suggest_on_worktree_updated(
window: &mut Window,
cx: &mut Context<Workspace>,
) {
let cli_auto_open = workspace.open_in_dev_container();
if workspace.open_in_dev_container() {
open_dev_container_from_cli(workspace, window, cx);
return;
}
let devcontainer_updated = updated_entries.iter().any(|(path, _, _)| {
path.as_ref() == devcontainer_dir_path() || path.as_ref() == devcontainer_json_path()
});
if !devcontainer_updated && !cli_auto_open {
if !devcontainer_updated {
return;
}
@@ -97,35 +100,7 @@ pub fn suggest_on_worktree_updated(
return;
}
let has_configs = !find_configs_in_snapshot(worktree).is_empty();
if cli_auto_open {
workspace.set_open_in_dev_container(false);
let task = cx.spawn_in(window, async move |workspace, cx| {
let scans_complete =
workspace.update(cx, |workspace, cx| workspace.worktree_scans_complete(cx))?;
scans_complete.await;
workspace.update_in(cx, |workspace, window, cx| {
let has_configs = workspace
.project()
.read(cx)
.worktrees(cx)
.any(|wt| !find_configs_in_snapshot(wt.read(cx)).is_empty());
if has_configs {
cx.on_next_frame(window, move |_workspace, window, cx| {
window.dispatch_action(Box::new(zed_actions::OpenDevContainer), cx);
});
} else {
log::warn!("--dev-container: no devcontainer configuration found in project");
}
})
});
workspace.set_dev_container_task(task);
return;
}
if !has_configs {
if find_configs_in_snapshot(worktree).is_empty() {
return;
}
@@ -193,3 +168,32 @@ pub fn suggest_on_worktree_updated(
});
});
}
pub fn open_dev_container_from_cli(
workspace: &mut Workspace,
window: &mut Window,
cx: &mut Context<Workspace>,
) {
workspace.set_open_in_dev_container(false);
let task = cx.spawn_in(window, async move |workspace, cx| {
let scans_complete =
workspace.update(cx, |workspace, cx| workspace.worktree_scans_complete(cx))?;
scans_complete.await;
workspace.update_in(cx, |workspace, window, cx| {
let has_configs = workspace
.project()
.read(cx)
.worktrees(cx)
.any(|worktree| !find_configs_in_snapshot(worktree.read(cx)).is_empty());
if has_configs {
cx.on_next_frame(window, move |_workspace, window, cx| {
window.dispatch_action(Box::new(zed_actions::OpenDevContainer), cx);
});
} else {
log::warn!("--dev-container: no devcontainer configuration found in project");
}
})
});
workspace.set_dev_container_task(task);
}
@@ -545,6 +545,12 @@ pub fn init(cx: &mut App) {
let Some(window) = window else {
return;
};
// A workspace opened with `--dev-container` has its worktrees scanning
// before this observer runs, so their update events can't be relied on
// to trigger the auto-open.
if workspace.open_in_dev_container() {
dev_container_suggest::open_dev_container_from_cli(workspace, window, cx);
}
cx.subscribe_in(
workspace.project(),
window,