Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
8ff3d53
fs: apply FSEvents watch changes on a watcher thread
soyboyscout Sep 26, 2026
1856e49
fs, worktree: wait for new watches the scanner depends on
soyboyscout Sep 26, 2026
679edc3
workspace, project_panel: embedder hooks for window title, name valid…
soyboyscout Oct 10, 2026
2a352a7
project, project_panel, search, git_ui: read-only local projects, emb…
soyboyscout Oct 10, 2026
7a5666b
project, editor, project_panel, git_ui: forced read-only buffers, sav…
soyboyscout Oct 10, 2026
8797bad
fs, project, editor, project_panel, git_ui, workspace: review fixes f…
soyboyscout Oct 12, 2026
d98dd03
fs, project: wait for covering watches across watchers; refuse buffer…
soyboyscout Oct 12, 2026
730de03
fs, project, project_panel: covered watches follow their ancestor, ex…
soyboyscout Oct 12, 2026
a7d46d8
fs, project, editor: applied_waiter distinguishes a vanished registra…
soyboyscout Oct 12, 2026
5013d7c
fs, editor: covered registrations wait on their ancestor, failed-watc…
soyboyscout Oct 12, 2026
8b0db55
fs: removing an ancestor watch gives its covered registrations a watc…
soyboyscout Oct 12, 2026
e4dd8aa
fs, project: promoted watches stay pending until applied and go in be…
soyboyscout Oct 12, 2026
fd41ef3
fs: add_and_wait rechecks its registration after a cancelled wait
soyboyscout Oct 12, 2026
d05a869
worktree, search: queue deferred watches together; embedded replace r…
soyboyscout Oct 12, 2026
10d0167
fs: retry native backend creation instead of caching its first failure
soyboyscout Oct 12, 2026
950e7c9
fs: a pending-path poll task re-asks its registration after a cancell…
soyboyscout Oct 12, 2026
db76ffe
fs, worktree, project: native availability retried, vanished registra…
soyboyscout Oct 12, 2026
2208787
fs: registration changes publish their commands in order; add_and_wai…
soyboyscout Oct 12, 2026
92bb248
fs: a covered path waits on its nearest covering watch; an unclaimed …
soyboyscout Oct 12, 2026
97113e1
fix(fs): saturate the failed-op index when a watch batch partially ap…
soyboyscout Oct 12, 2026
c5a1d69
chore(project): drop the comment that restates local_read_only
soyboyscout Oct 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions crates/editor/src/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5738,11 +5738,17 @@ impl Editor {
_: &mut Window,
cx: &mut Context<Self>,
) {
// A project an embedder made read-only cannot be unlocked per file.
let project_read_only = self
.project
.as_ref()
.is_some_and(|project| project.read(cx).is_read_only(cx));
if let Some(buffer) = self.buffer.read(cx).as_singleton() {
buffer.update(cx, |buffer, cx| {
buffer.set_capability(
match buffer.capability() {
Capability::ReadWrite => Capability::Read,
Capability::Read if project_read_only => Capability::Read,
Capability::Read => Capability::ReadWrite,
Capability::ReadOnly => Capability::ReadOnly,
},
Expand Down
27 changes: 27 additions & 0 deletions crates/editor/src/items.rs
Original file line number Diff line number Diff line change
Expand Up @@ -888,11 +888,17 @@ impl Item for Editor {
// Note: this mirrors the logic in `Editor::toggle_read_only`, but is reachable
// without relying on focus-based action dispatch.
fn toggle_read_only(&mut self, window: &mut Window, cx: &mut Context<Self>) {
// A project an embedder made read-only cannot be unlocked per file.
let project_read_only = self
.project
.as_ref()
.is_some_and(|project| project.read(cx).is_read_only(cx));
if let Some(buffer) = self.buffer.read(cx).as_singleton() {
buffer.update(cx, |buffer, cx| {
buffer.set_capability(
match buffer.capability() {
Capability::ReadWrite => Capability::Read,
Capability::Read if project_read_only => Capability::Read,
Capability::Read => Capability::ReadWrite,
Capability::ReadOnly => Capability::ReadOnly,
},
Expand Down Expand Up @@ -928,6 +934,27 @@ impl Item for Editor {
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
// A read-only project (a snapshot, a remote tree) has nothing to write:
// skip formatting and the save prompt entirely. Edits made before the
// project became read-only cannot be written either, and saying so keeps
// the item from closing as if they had been saved.
if project.read(cx).is_read_only(cx) {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Resolved
let has_unsaved_edits = self
.buffer()
.read(cx)
.all_buffers()
.iter()
.any(|buffer| buffer.read(cx).is_dirty());
// An autosave tick cannot do anything about it and must not fail
// on every tick; an explicit save says so.
return Task::ready(if has_unsaved_edits && !options.autosave {
Err(anyhow!(
"This project is read-only, so these edits cannot be saved"
))
} else {
Ok(())
});
}
// Add meta data tracking # of auto saves
if options.autosave {
self.report_editor_event(ReportEditorEvent::Saved { auto_saved: true }, None, cx);
Expand Down
19 changes: 15 additions & 4 deletions crates/fs/src/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,14 @@ use std::ffi::OsStr;
pub trait Watcher: Send + Sync {
fn add(&self, path: &Path) -> Result<()>;
fn remove(&self, path: &Path) -> Result<()>;

/// Add `path` and resolve once its OS watch is in place, so no change made
/// after it resolves can be missed. `add` may return before then (a new
/// FSEvents watch is applied on a watcher thread). Waiting never blocks a
/// thread.
fn add_and_wait<'a>(&'a self, path: &'a Path) -> BoxFuture<'a, Result<()>> {
Box::pin(async move { self.add(path) })
}
}

#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
Expand Down Expand Up @@ -1075,13 +1083,15 @@ impl Fs for RealFs {
let (tx, rx) = async_channel::unbounded();
let pending_paths: Arc<Mutex<Vec<PathEvent>>> = Default::default();

let watcher: Arc<dyn Watcher> = Arc::new(fs_watcher::FsWatcher::new(
let watcher = Arc::new(fs_watcher::FsWatcher::new(
executor.clone(),
tx.clone(),
pending_paths.clone(),
));

if let Err(e) = watcher.add(path) {
// Awaited rather than queued, so a change made after `watch` returns is
// never missed; the OS work runs on the watcher thread either way.
if let Err(e) = watcher.add_and_wait(path).await {
log::warn!("Failed to watch {}:\n{e}", path.display());
}

Expand All @@ -1097,11 +1107,12 @@ impl Fs for RealFs {
target = SanitizedPath::new(&canonical).as_path().to_path_buf();
}
}
watcher.add(&target).ok();
watcher.add_and_wait(&target).await.ok();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Handle failure to watch the symlink target

If asynchronous registration of a symlink target fails, such as after an FSEvents restart or watcher-limit error, .ok() silently discards the failure while watch returns normally. The target-parent watch is non-recursive on Linux and may not cover directory descendants, so callers can receive no events from the target and have no diagnostic explaining why; at minimum log this result as is already done for the parent registration.

AGENTS.md reference: AGENTS.md:L8-L11

Useful? React with 👍 / 👎.

if let Some(parent) = target.parent() {
watcher.add(parent).log_err();
watcher.add_and_wait(parent).await.log_err();
}
}
let watcher: Arc<dyn Watcher> = watcher;

(
Box::pin(rx.filter_map({
Expand Down
Loading
Loading