From b9c0f756c44578aa30712e8c98e9be681946ce32 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Fri, 30 Jan 2026 09:42:28 +0100 Subject: [PATCH] fs: Cache filesystem case sensitivity (#47995) We query this on the main thread on every file system access otherwise Release Notes: - N/A *or* Added/Fixed/Improved ... --- assets/keymaps/default-linux.json | 12 ++-- assets/keymaps/default-windows.json | 8 +-- crates/fs/src/fs.rs | 85 +++++++++++++++++++---------- crates/worktree/src/worktree.rs | 12 +--- 4 files changed, 69 insertions(+), 48 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index a327dfa55f35482ef2bea8d504ed003dfd2b82c5..1ba2968558d912cafbfc0f300fa0357f420c0f74 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -737,8 +737,8 @@ "alt-tab": "editor::AcceptEditPrediction", "alt-l": "editor::AcceptEditPrediction", "tab": "editor::AcceptEditPrediction", - "alt-right": "editor::AcceptNextWordEditPrediction", - "alt-down": "editor::AcceptNextLineEditPrediction", + "ctrl-shift-right": "editor::AcceptNextWordEditPrediction", + "ctrl-shift-down": "editor::AcceptNextLineEditPrediction", }, }, { @@ -746,8 +746,8 @@ "bindings": { "alt-tab": "editor::AcceptEditPrediction", "alt-l": "editor::AcceptEditPrediction", - "alt-right": "editor::AcceptNextWordEditPrediction", - "alt-down": "editor::AcceptNextLineEditPrediction", + "ctrl-shift-right": "editor::AcceptNextWordEditPrediction", + "ctrl-shift-down": "editor::AcceptNextLineEditPrediction", }, }, { @@ -1350,8 +1350,8 @@ "ctrl-m": "notebook::AddCodeBlock", "ctrl-shift-m": "notebook::AddMarkdownBlock", "ctrl-shift-r": "notebook::RestartKernel", - "ctrl-c": "notebook::InterruptKernel" - } + "ctrl-c": "notebook::InterruptKernel", + }, }, { "context": "GitBranchSelector || (GitBranchSelector > Picker > Editor)", diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json index b53fa4e7200617dfa43d438c3c04152e4eb0e559..fbd634751d89e880135751e823ab7d64112fc9eb 100644 --- a/assets/keymaps/default-windows.json +++ b/assets/keymaps/default-windows.json @@ -733,8 +733,8 @@ "alt-tab": "editor::AcceptEditPrediction", "alt-l": "editor::AcceptEditPrediction", "tab": "editor::AcceptEditPrediction", - "alt-right": "editor::AcceptNextWordEditPrediction", - "alt-down": "editor::AcceptNextLineEditPrediction", + "ctrl-shift-right": "editor::AcceptNextWordEditPrediction", + "ctrl-shift-down": "editor::AcceptNextLineEditPrediction", }, }, { @@ -743,8 +743,8 @@ "bindings": { "alt-tab": "editor::AcceptEditPrediction", "alt-l": "editor::AcceptEditPrediction", - "alt-right": "editor::AcceptNextWordEditPrediction", - "alt-down": "editor::AcceptNextLineEditPrediction", + "ctrl-shift-right": "editor::AcceptNextWordEditPrediction", + "ctrl-shift-down": "editor::AcceptNextLineEditPrediction", }, }, { diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 312cf28c21161c60289c8f41d0db0904e869455f..d540dd7e3ec7d4413ef52a2c7f48d7b82578db79 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -1,8 +1,9 @@ pub mod fs_watcher; use parking_lot::Mutex; -use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::atomic::{AtomicU8, AtomicUsize, Ordering}; use std::time::Instant; +use util::maybe; use anyhow::{Context as _, Result, anyhow}; #[cfg(any(target_os = "linux", target_os = "freebsd"))] @@ -145,7 +146,7 @@ pub trait Fs: Send + Sync { -> Result<()>; async fn git_clone(&self, repo_url: &str, abs_work_directory: &Path) -> Result<()>; fn is_fake(&self) -> bool; - async fn is_case_sensitive(&self) -> Result; + async fn is_case_sensitive(&self) -> bool; fn subscribe_to_jobs(&self) -> JobEventReceiver; #[cfg(feature = "test-support")] @@ -310,6 +311,7 @@ pub struct RealFs { executor: BackgroundExecutor, next_job_id: Arc, job_event_subscribers: Arc>>, + is_case_sensitive: AtomicU8, } pub trait FileHandle: Send + Sync + std::fmt::Debug { @@ -417,6 +419,7 @@ impl RealFs { executor, next_job_id: Arc::new(AtomicUsize::new(0)), job_event_subscribers: Arc::new(Mutex::new(Vec::new())), + is_case_sensitive: Default::default(), } } @@ -1118,37 +1121,63 @@ impl Fs for RealFs { /// that have the same name except for the casing. /// /// It creates both files in a temporary directory it removes at the end. - async fn is_case_sensitive(&self) -> Result { - let temp_dir = TempDir::new()?; - let test_file_1 = temp_dir.path().join("case_sensitivity_test.tmp"); - let test_file_2 = temp_dir.path().join("CASE_SENSITIVITY_TEST.TMP"); - - let create_opts = CreateOptions { - overwrite: false, - ignore_if_exists: false, - }; + async fn is_case_sensitive(&self) -> bool { + const UNINITIALIZED: u8 = 0; + const CASE_SENSITIVE: u8 = 1; + const NOT_CASE_SENSITIVE: u8 = 2; + + // Note we could CAS here, but really, if we race we do this work twice at worst which isn't a big deal. + let load = self.is_case_sensitive.load(Ordering::Acquire); + if load != UNINITIALIZED { + return load == CASE_SENSITIVE; + } + let temp_dir = self.executor.spawn(async { TempDir::new() }); + let res = maybe!(async { + let temp_dir = temp_dir.await?; + let test_file_1 = temp_dir.path().join("case_sensitivity_test.tmp"); + let test_file_2 = temp_dir.path().join("CASE_SENSITIVITY_TEST.TMP"); + + let create_opts = CreateOptions { + overwrite: false, + ignore_if_exists: false, + }; - // Create file1 - self.create_file(&test_file_1, create_opts).await?; + // Create file1 + self.create_file(&test_file_1, create_opts).await?; - // Now check whether it's possible to create file2 - let case_sensitive = match self.create_file(&test_file_2, create_opts).await { - Ok(_) => Ok(true), - Err(e) => { - if let Some(io_error) = e.downcast_ref::() { - if io_error.kind() == io::ErrorKind::AlreadyExists { - Ok(false) + // Now check whether it's possible to create file2 + let case_sensitive = match self.create_file(&test_file_2, create_opts).await { + Ok(_) => Ok(true), + Err(e) => { + if let Some(io_error) = e.downcast_ref::() { + if io_error.kind() == io::ErrorKind::AlreadyExists { + Ok(false) + } else { + Err(e) + } } else { Err(e) } - } else { - Err(e) } - } - }; + }; - temp_dir.close()?; - case_sensitive + temp_dir.close()?; + case_sensitive + }).await.unwrap_or_else(|e| { + log::error!( + "Failed to determine whether filesystem is case sensitive (falling back to true) due to error: {e:#}" + ); + true + }); + self.is_case_sensitive.store( + if res { + CASE_SENSITIVE + } else { + NOT_CASE_SENSITIVE + }, + Ordering::Release, + ); + res } } @@ -2760,8 +2789,8 @@ impl Fs for FakeFs { true } - async fn is_case_sensitive(&self) -> Result { - Ok(true) + async fn is_case_sensitive(&self) -> bool { + true } fn subscribe_to_jobs(&self) -> JobEventReceiver { diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 10fc6e1e9092c52fbee718e367d58f52a16e2e55..4f51019dd7443413aa01e6cfa44e6e62b02f9b8c 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -377,12 +377,7 @@ impl Worktree { .await .context("failed to stat worktree path")?; - let fs_case_sensitive = fs.is_case_sensitive().await.unwrap_or_else(|e| { - log::error!( - "Failed to determine whether filesystem is case sensitive (falling back to true) due to error: {e:#}" - ); - true - }); + let fs_case_sensitive = fs.is_case_sensitive().await; let root_file_handle = if metadata.as_ref().is_some() { fs.open_handle(&abs_path) @@ -1087,10 +1082,7 @@ impl LocalWorktree { } else { (Box::pin(stream::pending()) as _, Arc::new(NullWatcher) as _) }; - let fs_case_sensitive = fs.is_case_sensitive().await.unwrap_or_else(|e| { - log::error!("Failed to determine whether filesystem is case sensitive: {e:#}"); - true - }); + let fs_case_sensitive = fs.is_case_sensitive().await; let mut scanner = BackgroundScanner { fs,