windows: Fix fs watch when file doesn't exist or is a symlink (#22660)
Closes #22659 More context can be found in attached issue. This is specific to Windows: 1. Add parent directory watching for fs watch when the file doesn't exist. For example, when Zed is first launched and `settings.json` isn't there. 2. Add proper symlink handling for fs watch. For example, when `settings.json` is a symlink. This is exactly same as how we handle it on Linux. Release Notes: - Fixed an issue where items on the Welcome page could not be toggled on Windows, either on first launch or when `settings.json` is a symlink.
This commit is contained in:
parent
d58f006498
commit
d3fc00d5a0
4 changed files with 56 additions and 72 deletions
|
@ -1,8 +1,8 @@
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
mod mac_watcher;
|
mod mac_watcher;
|
||||||
|
|
||||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
pub mod linux_watcher;
|
pub mod fs_watcher;
|
||||||
|
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use git::GitHostingProviderRegistry;
|
use git::GitHostingProviderRegistry;
|
||||||
|
@ -700,7 +700,7 @@ impl Fs for RealFs {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
async fn watch(
|
async fn watch(
|
||||||
&self,
|
&self,
|
||||||
path: &Path,
|
path: &Path,
|
||||||
|
@ -710,10 +710,11 @@ impl Fs for RealFs {
|
||||||
Arc<dyn Watcher>,
|
Arc<dyn Watcher>,
|
||||||
) {
|
) {
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
use util::paths::SanitizedPath;
|
||||||
|
|
||||||
let (tx, rx) = smol::channel::unbounded();
|
let (tx, rx) = smol::channel::unbounded();
|
||||||
let pending_paths: Arc<Mutex<Vec<PathEvent>>> = Default::default();
|
let pending_paths: Arc<Mutex<Vec<PathEvent>>> = Default::default();
|
||||||
let watcher = Arc::new(linux_watcher::LinuxWatcher::new(tx, pending_paths.clone()));
|
let watcher = Arc::new(fs_watcher::FsWatcher::new(tx, pending_paths.clone()));
|
||||||
|
|
||||||
if watcher.add(path).is_err() {
|
if watcher.add(path).is_err() {
|
||||||
// If the path doesn't exist yet (e.g. settings.json), watch the parent dir to learn when it's created.
|
// If the path doesn't exist yet (e.g. settings.json), watch the parent dir to learn when it's created.
|
||||||
|
@ -731,7 +732,7 @@ impl Fs for RealFs {
|
||||||
if let Some(parent) = path.parent() {
|
if let Some(parent) = path.parent() {
|
||||||
target = parent.join(target);
|
target = parent.join(target);
|
||||||
if let Ok(canonical) = self.canonicalize(&target).await {
|
if let Ok(canonical) = self.canonicalize(&target).await {
|
||||||
target = canonical;
|
target = SanitizedPath::from(canonical).as_path().to_path_buf();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -758,56 +759,6 @@ impl Fs for RealFs {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
async fn watch(
|
|
||||||
&self,
|
|
||||||
path: &Path,
|
|
||||||
_latency: Duration,
|
|
||||||
) -> (
|
|
||||||
Pin<Box<dyn Send + Stream<Item = Vec<PathEvent>>>>,
|
|
||||||
Arc<dyn Watcher>,
|
|
||||||
) {
|
|
||||||
use notify::{EventKind, Watcher};
|
|
||||||
|
|
||||||
let (tx, rx) = smol::channel::unbounded();
|
|
||||||
|
|
||||||
let mut file_watcher = notify::recommended_watcher({
|
|
||||||
let tx = tx.clone();
|
|
||||||
move |event: Result<notify::Event, _>| {
|
|
||||||
if let Some(event) = event.log_err() {
|
|
||||||
let kind = match event.kind {
|
|
||||||
EventKind::Create(_) => Some(PathEventKind::Created),
|
|
||||||
EventKind::Modify(_) => Some(PathEventKind::Changed),
|
|
||||||
EventKind::Remove(_) => Some(PathEventKind::Removed),
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
tx.try_send(
|
|
||||||
event
|
|
||||||
.paths
|
|
||||||
.into_iter()
|
|
||||||
.map(|path| PathEvent { path, kind })
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
)
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.expect("Could not start file watcher");
|
|
||||||
|
|
||||||
file_watcher
|
|
||||||
.watch(path, notify::RecursiveMode::Recursive)
|
|
||||||
.log_err();
|
|
||||||
|
|
||||||
(
|
|
||||||
Box::pin(rx.chain(futures::stream::once(async move {
|
|
||||||
drop(file_watcher);
|
|
||||||
vec![]
|
|
||||||
}))),
|
|
||||||
Arc::new(RealWatcher {}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn open_repo(&self, dotgit_path: &Path) -> Option<Arc<dyn GitRepository>> {
|
fn open_repo(&self, dotgit_path: &Path) -> Option<Arc<dyn GitRepository>> {
|
||||||
// with libgit2, we can open git repo from an existing work dir
|
// with libgit2, we can open git repo from an existing work dir
|
||||||
// https://libgit2.org/docs/reference/main/repository/git_repository_open.html
|
// https://libgit2.org/docs/reference/main/repository/git_repository_open.html
|
||||||
|
|
|
@ -5,12 +5,12 @@ use util::ResultExt;
|
||||||
|
|
||||||
use crate::{PathEvent, PathEventKind, Watcher};
|
use crate::{PathEvent, PathEventKind, Watcher};
|
||||||
|
|
||||||
pub struct LinuxWatcher {
|
pub struct FsWatcher {
|
||||||
tx: smol::channel::Sender<()>,
|
tx: smol::channel::Sender<()>,
|
||||||
pending_path_events: Arc<Mutex<Vec<PathEvent>>>,
|
pending_path_events: Arc<Mutex<Vec<PathEvent>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LinuxWatcher {
|
impl FsWatcher {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
tx: smol::channel::Sender<()>,
|
tx: smol::channel::Sender<()>,
|
||||||
pending_path_events: Arc<Mutex<Vec<PathEvent>>>,
|
pending_path_events: Arc<Mutex<Vec<PathEvent>>>,
|
||||||
|
@ -22,7 +22,7 @@ impl LinuxWatcher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Watcher for LinuxWatcher {
|
impl Watcher for FsWatcher {
|
||||||
fn add(&self, path: &std::path::Path) -> gpui::Result<()> {
|
fn add(&self, path: &std::path::Path) -> gpui::Result<()> {
|
||||||
let root_path = path.to_path_buf();
|
let root_path = path.to_path_buf();
|
||||||
|
|
||||||
|
@ -69,7 +69,7 @@ impl Watcher for LinuxWatcher {
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
global(|g| {
|
global(|g| {
|
||||||
g.inotify
|
g.watcher
|
||||||
.lock()
|
.lock()
|
||||||
.watch(path, notify::RecursiveMode::NonRecursive)
|
.watch(path, notify::RecursiveMode::NonRecursive)
|
||||||
})??;
|
})??;
|
||||||
|
@ -79,16 +79,18 @@ impl Watcher for LinuxWatcher {
|
||||||
|
|
||||||
fn remove(&self, path: &std::path::Path) -> gpui::Result<()> {
|
fn remove(&self, path: &std::path::Path) -> gpui::Result<()> {
|
||||||
use notify::Watcher;
|
use notify::Watcher;
|
||||||
Ok(global(|w| w.inotify.lock().unwatch(path))??)
|
Ok(global(|w| w.watcher.lock().unwatch(path))??)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct GlobalWatcher {
|
pub struct GlobalWatcher {
|
||||||
// two mutexes because calling inotify.add triggers an inotify.event, which needs watchers.
|
// two mutexes because calling watcher.add triggers an watcher.event, which needs watchers.
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
pub(super) inotify: Mutex<notify::INotifyWatcher>,
|
pub(super) watcher: Mutex<notify::INotifyWatcher>,
|
||||||
#[cfg(target_os = "freebsd")]
|
#[cfg(target_os = "freebsd")]
|
||||||
pub(super) inotify: Mutex<notify::KqueueWatcher>,
|
pub(super) watcher: Mutex<notify::KqueueWatcher>,
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub(super) watcher: Mutex<notify::ReadDirectoryChangesWatcher>,
|
||||||
pub(super) watchers: Mutex<Vec<Box<dyn Fn(¬ify::Event) + Send + Sync>>>,
|
pub(super) watchers: Mutex<Vec<Box<dyn Fn(¬ify::Event) + Send + Sync>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,7 +100,8 @@ impl GlobalWatcher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static INOTIFY_INSTANCE: OnceLock<anyhow::Result<GlobalWatcher, notify::Error>> = OnceLock::new();
|
static FS_WATCHER_INSTANCE: OnceLock<anyhow::Result<GlobalWatcher, notify::Error>> =
|
||||||
|
OnceLock::new();
|
||||||
|
|
||||||
fn handle_event(event: Result<notify::Event, notify::Error>) {
|
fn handle_event(event: Result<notify::Event, notify::Error>) {
|
||||||
let Some(event) = event.log_err() else { return };
|
let Some(event) = event.log_err() else { return };
|
||||||
|
@ -111,9 +114,9 @@ fn handle_event(event: Result<notify::Event, notify::Error>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn global<T>(f: impl FnOnce(&GlobalWatcher) -> T) -> anyhow::Result<T> {
|
pub fn global<T>(f: impl FnOnce(&GlobalWatcher) -> T) -> anyhow::Result<T> {
|
||||||
let result = INOTIFY_INSTANCE.get_or_init(|| {
|
let result = FS_WATCHER_INSTANCE.get_or_init(|| {
|
||||||
notify::recommended_watcher(handle_event).map(|file_watcher| GlobalWatcher {
|
notify::recommended_watcher(handle_event).map(|file_watcher| GlobalWatcher {
|
||||||
inotify: Mutex::new(file_watcher),
|
watcher: Mutex::new(file_watcher),
|
||||||
watchers: Default::default(),
|
watchers: Default::default(),
|
||||||
})
|
})
|
||||||
});
|
});
|
|
@ -854,8 +854,8 @@ async fn test_write_file(cx: &mut TestAppContext) {
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
fs::linux_watcher::global(|_| {}).unwrap();
|
fs::fs_watcher::global(|_| {}).unwrap();
|
||||||
|
|
||||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||||
.await;
|
.await;
|
||||||
|
|
|
@ -153,8 +153,8 @@ pub fn initialize_workspace(
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
initialize_linux_file_watcher(cx);
|
initialize_file_watcher(cx);
|
||||||
|
|
||||||
if let Some(specs) = cx.gpu_specs() {
|
if let Some(specs) = cx.gpu_specs() {
|
||||||
log::info!("Using GPU: {:?}", specs);
|
log::info!("Using GPU: {:?}", specs);
|
||||||
|
@ -235,8 +235,8 @@ fn feature_gate_zed_pro_actions(cx: &mut AppContext) {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||||
fn initialize_linux_file_watcher(cx: &mut ViewContext<Workspace>) {
|
fn initialize_file_watcher(cx: &mut ViewContext<Workspace>) {
|
||||||
if let Err(e) = fs::linux_watcher::global(|_| {}) {
|
if let Err(e) = fs::fs_watcher::global(|_| {}) {
|
||||||
let message = format!(
|
let message = format!(
|
||||||
db::indoc! {r#"
|
db::indoc! {r#"
|
||||||
inotify_init returned {}
|
inotify_init returned {}
|
||||||
|
@ -264,6 +264,36 @@ fn initialize_linux_file_watcher(cx: &mut ViewContext<Workspace>) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
fn initialize_file_watcher(cx: &mut ViewContext<Workspace>) {
|
||||||
|
if let Err(e) = fs::fs_watcher::global(|_| {}) {
|
||||||
|
let message = format!(
|
||||||
|
db::indoc! {r#"
|
||||||
|
ReadDirectoryChangesW initialization failed: {}
|
||||||
|
|
||||||
|
This may occur on network filesystems and WSL paths. For troubleshooting see: https://zed.dev/docs/windows
|
||||||
|
"#},
|
||||||
|
e
|
||||||
|
);
|
||||||
|
let prompt = cx.prompt(
|
||||||
|
PromptLevel::Critical,
|
||||||
|
"Could not start ReadDirectoryChangesW",
|
||||||
|
Some(&message),
|
||||||
|
&["Troubleshoot and Quit"],
|
||||||
|
);
|
||||||
|
cx.spawn(|_, mut cx| async move {
|
||||||
|
if prompt.await == Ok(0) {
|
||||||
|
cx.update(|cx| {
|
||||||
|
cx.open_url("https://zed.dev/docs/windows");
|
||||||
|
cx.quit()
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn show_software_emulation_warning_if_needed(
|
fn show_software_emulation_warning_if_needed(
|
||||||
specs: gpui::GpuSpecs,
|
specs: gpui::GpuSpecs,
|
||||||
cx: &mut ViewContext<Workspace>,
|
cx: &mut ViewContext<Workspace>,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue