From 7b4949bd06b4f85d8116b34b24c3e1936b1fc62c Mon Sep 17 00:00:00 2001 From: David3u <3udavid@gmail.com> Date: Tue, 19 May 2026 19:08:56 -0400 Subject: [PATCH] Open non-writeable files in Capability::Read mode --- crates/fs/src/fs.rs | 4 ++ crates/project/src/buffer_store.rs | 9 ++++- .../tests/integration/project_tests.rs | 40 ++++++++++++++++++- crates/worktree/src/worktree.rs | 14 ++++--- 4 files changed, 58 insertions(+), 9 deletions(-) diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 7809e65eeb32d4..5b883549cfde0c 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -295,6 +295,7 @@ pub struct Metadata { pub len: u64, pub is_fifo: bool, pub is_executable: bool, + pub is_writable: bool, } /// Filesystem modification time. The purpose of this newtype is to discourage use of operations @@ -1032,6 +1033,7 @@ impl Fs for RealFs { is_dir: metadata.file_type().is_dir(), is_fifo, is_executable, + is_writable: !metadata.permissions().readonly(), })) } @@ -3000,6 +3002,7 @@ impl Fs for FakeFs { is_symlink, is_fifo: false, is_executable: false, + is_writable: true, }, FakeFsEntry::Dir { inode, mtime, len, .. @@ -3011,6 +3014,7 @@ impl Fs for FakeFs { is_symlink, is_fifo: false, is_executable: false, + is_writable: true, }, FakeFsEntry::Symlink { .. } => unreachable!(), })) diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index f9076753998e93..816cf97c8651c8 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -647,6 +647,12 @@ impl LocalBufferStore { let path = path.clone(); let buffer = match load_file.await { Ok(loaded) => { + let is_writable = loaded.is_writable; + let capability = if is_writable { + Capability::ReadWrite + } else { + Capability::Read + }; let reservation = cx.reserve_entity::(); let buffer_id = BufferId::from(reservation.entity_id().as_non_zero_u64()); let text_buffer = cx @@ -655,8 +661,7 @@ impl LocalBufferStore { }) .await; cx.insert_entity(reservation, |_| { - let mut buffer = - Buffer::build(text_buffer, Some(loaded.file), Capability::ReadWrite); + let mut buffer = Buffer::build(text_buffer, Some(loaded.file), capability); buffer.set_encoding(loaded.encoding); buffer.set_has_bom(loaded.has_bom); buffer diff --git a/crates/project/tests/integration/project_tests.rs b/crates/project/tests/integration/project_tests.rs index b93dd8a7274b6d..b75a6429603f00 100644 --- a/crates/project/tests/integration/project_tests.rs +++ b/crates/project/tests/integration/project_tests.rs @@ -28,7 +28,7 @@ use buffer_diff::{ }; use collections::{BTreeSet, HashMap, HashSet}; use encoding_rs; -use fs::{FakeFs, PathEventKind}; +use fs::{FakeFs, PathEventKind, RealFs}; use futures::{StreamExt, future}; use git::{ GitHostingProviderRegistry, @@ -13249,6 +13249,44 @@ async fn test_read_only_files_empty_setting(cx: &mut gpui::TestAppContext) { }); } +#[gpui::test] +#[cfg(not(windows))] +async fn test_os_read_only_files_open_as_read_only(cx: &mut gpui::TestAppContext) { + init_test(cx); + cx.executor().allow_parking(); + + let root = TempTree::new(json!({ + "project": { + "test.txt": "hello", + }, + })); + let file_path = root.path().join("project/test.txt"); + let mut permissions = std::fs::metadata(&file_path).unwrap().permissions(); + permissions.set_readonly(true); + std::fs::set_permissions(&file_path, permissions).unwrap(); + + let project = Project::test( + Arc::new(RealFs::new(None, cx.executor())), + [root.path()], + cx, + ) + .await; + + let buffer = project + .update(cx, |project, cx| { + project.open_local_buffer(file_path.as_path(), cx) + }) + .await + .unwrap(); + + buffer.read_with(cx, |buffer, _| { + assert!( + buffer.read_only(), + "OS read-only files should open as read-only" + ); + }); +} + #[gpui::test] async fn test_read_only_files_with_lock_files(cx: &mut gpui::TestAppContext) { init_test(cx); diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index f1dfaec5c856a8..ac4133ccedb9c7 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -110,6 +110,7 @@ pub struct LoadedFile { pub text: String, pub encoding: &'static Encoding, pub has_bom: bool, + pub is_writable: bool, } pub struct LoadedBinaryFile { @@ -1491,15 +1492,15 @@ impl LocalWorktree { // if it is too large // 5GB seems to be more reasonable, peaking at ~16GB, while 6GB jumps up to >24GB which seems like a // reasonable limit + const FILE_SIZE_MAX: u64 = 6 * 1024 * 1024 * 1024; // 6GB + let metadata = fs.metadata(&abs_path).await?; + if let Some(metadata) = metadata.as_ref() + && metadata.len >= FILE_SIZE_MAX { - const FILE_SIZE_MAX: u64 = 6 * 1024 * 1024 * 1024; // 6GB - if let Ok(Some(metadata)) = fs.metadata(&abs_path).await - && metadata.len >= FILE_SIZE_MAX - { - anyhow::bail!("File is too large to load"); - } + anyhow::bail!("File is too large to load"); } let (text, encoding, has_bom) = decode_file_text(fs.as_ref(), &abs_path).await?; + let is_writable = metadata.is_some_and(|metadata| metadata.is_writable); let worktree = this.upgrade().context("worktree was dropped")?; let file = match entry.await? { @@ -1533,6 +1534,7 @@ impl LocalWorktree { text, encoding, has_bom, + is_writable, }) }) }