From 78143bae4d4a0e352fb3bb58dbf6683bbb1a171f Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 6 Feb 2026 09:55:54 -0600 Subject: [PATCH] Add path suppression for VFS events in JobThreadContext - Implemented a new `suppress_path` method to prevent re-processing of file-watcher events triggered by the ChangeProcessor, enhancing event handling during renames and writes. - Updated various sections of the `change_processor.rs` file to call `suppress_path` for relevant paths during rename operations, ensuring accurate metadata synchronization and preventing unnecessary processing. - Enhanced the logic for updating instance metadata to maintain consistency after filesystem path changes, improving the robustness of the change processing mechanism. --- src/change_processor.rs | 143 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 142 insertions(+), 1 deletion(-) diff --git a/src/change_processor.rs b/src/change_processor.rs index 9e109271..f1726859 100644 --- a/src/change_processor.rs +++ b/src/change_processor.rs @@ -160,6 +160,29 @@ impl JobThreadContext { None } + /// Suppress a single VFS event for the given path. Prevents the + /// ChangeProcessor from re-processing a file-watcher event that we + /// triggered ourselves (renames, writes). + fn suppress_path(&self, path: &Path) { + let mut suppressed = self.suppressed_paths.lock().unwrap(); + let key = if let Ok(canonical) = std::fs::canonicalize(path) { + canonical + } else if let Some(parent) = path.parent() { + if let Ok(canonical_parent) = std::fs::canonicalize(parent) { + if let Some(file_name) = path.file_name() { + canonical_parent.join(file_name) + } else { + path.to_path_buf() + } + } else { + path.to_path_buf() + } + } else { + path.to_path_buf() + }; + *suppressed.entry(key).or_insert(0) += 1; + } + /// Computes and applies patches to the DOM for a given file path. /// /// This function finds the nearest ancestor to the given path that has associated instances @@ -503,6 +526,11 @@ impl JobThreadContext { } } + // Collect (Ref, new_instigating_source) for instances whose + // filesystem path changed (rename / ClassName transition). + // Applied after the PatchSet to keep metadata in sync. + let mut metadata_updates: Vec<(Ref, PathBuf)> = Vec::new(); + for update in &patch_set.updated_instances { let id = update.id; @@ -548,6 +576,8 @@ impl JobThreadContext { dir_path.display(), new_dir_path.display() ); + self.suppress_path(dir_path); + self.suppress_path(&new_dir_path); if let Err(err) = fs::rename(dir_path, &new_dir_path) { @@ -571,6 +601,8 @@ impl JobThreadContext { "{}.meta.json5", encoded_new_name )); + self.suppress_path(&old_meta); + self.suppress_path(&new_meta); let _ = fs::rename(&old_meta, &new_meta); } @@ -622,6 +654,8 @@ impl JobThreadContext { path.display(), new_path.display() ); + self.suppress_path(path); + self.suppress_path(&new_path); if let Err(err) = fs::rename(path, &new_path) { log::error!( "Failed to rename {:?} to {:?}: {}", @@ -638,6 +672,8 @@ impl JobThreadContext { "{}.meta.json5", encoded_new_name )); + self.suppress_path(&old_meta); + self.suppress_path(&new_meta); let _ = fs::rename(&old_meta, &new_meta); } } @@ -751,6 +787,8 @@ impl JobThreadContext { actual_file.display(), new_path.display() ); + self.suppress_path(&actual_file); + self.suppress_path(&new_path); if let Err(err) = fs::rename(&actual_file, &new_path) { @@ -829,6 +867,7 @@ impl JobThreadContext { "Two-way sync: Writing Source to {}", write_path.display() ); + self.suppress_path(write_path); if let Err(err) = fs::write(write_path, value) { log::error!( "Failed to write Source to {:?} for instance {:?}: {}", @@ -845,12 +884,35 @@ impl JobThreadContext { log::trace!("Skipping non-Source property change: {}", key); } } + + // Record metadata update so instigating_source and + // relevant_paths stay in sync after rename / class change. + if let Some(ref new_path) = overridden_source_path { + metadata_updates.push((id, new_path.clone())); + } } else { log::warn!("Cannot update instance {:?}, it does not exist.", id); } } - apply_patch_set(&mut tree, patch_set) + let applied = apply_patch_set(&mut tree, patch_set); + + // Update metadata for instances whose filesystem path changed. + // This keeps instigating_source and path_to_ids in sync so that + // subsequent VFS events (and future renames) target the correct + // instance and path. + for (id, new_instigating_source) in metadata_updates { + if let Some(old_metadata) = tree.get_metadata(id).cloned() { + let mut new_metadata = old_metadata; + new_metadata.instigating_source = + Some(InstigatingSource::Path(new_instigating_source.clone())); + new_metadata.relevant_paths = + rebuild_relevant_paths(&new_instigating_source); + tree.update_metadata(id, new_metadata); + } + } + + applied }; if !applied_patch.is_empty() { @@ -971,6 +1033,37 @@ fn compute_and_apply_changes(tree: &mut RojoTree, vfs: &Vfs, id: Ref) -> Option< } }; + let patch_set = compute_patch_set(snapshot, tree, id); + let applied = apply_patch_set(tree, patch_set); + Some(ComputeResult { + applied, + removed_path: None, + }) + } else if is_init_file && std::fs::metadata(snapshot_path).is_ok() { + // Init file was deleted but the parent directory still + // exists. Re-snapshot the directory — it will become a + // Folder (or whatever the directory middleware produces + // without an init file). + log::info!( + "compute_and_apply_changes: init file {} deleted but parent \ + directory {} still exists. Re-snapshotting directory.", + path.display(), + snapshot_path.display() + ); + + let snapshot = + match snapshot_from_vfs(&metadata.context, vfs, snapshot_path) { + Ok(snapshot) => snapshot, + Err(err) => { + log::error!( + "Directory re-snapshot error for {}: {:?}", + snapshot_path.display(), + err + ); + return None; + } + }; + let patch_set = compute_patch_set(snapshot, tree, id); let applied = apply_patch_set(tree, patch_set); Some(ComputeResult { @@ -1051,3 +1144,51 @@ fn compute_and_apply_changes(tree: &mut RojoTree, vfs: &Vfs, id: Ref) -> Option< } } } + +/// Rebuild the `relevant_paths` list for an instance given its new +/// `instigating_source` path. This mirrors the logic in the snapshot +/// middleware so that `path_to_ids` stays correct after a rename. +fn rebuild_relevant_paths(new_path: &Path) -> Vec { + let file_name = new_path + .file_name() + .and_then(|f| f.to_str()) + .unwrap_or(""); + let is_init = file_name.starts_with("init."); + + if is_init { + // Directory-format script: relevant paths are the directory itself + // plus every possible init file variant inside it. + let dir_path = new_path.parent().unwrap().to_path_buf(); + vec![ + dir_path.clone(), + dir_path.join("init.luau"), + dir_path.join("init.server.luau"), + dir_path.join("init.client.luau"), + dir_path.join("init.local.luau"), + dir_path.join("init.legacy.luau"), + dir_path.join("init.csv"), + dir_path.join("init.lua"), + dir_path.join("init.server.lua"), + dir_path.join("init.client.lua"), + dir_path.join("init.meta.json5"), + ] + } else { + // Standalone file: relevant paths are the file itself and its + // adjacent meta file. + let stem = new_path + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or(""); + let base_name = stem + .strip_suffix(".server") + .or_else(|| stem.strip_suffix(".client")) + .or_else(|| stem.strip_suffix(".plugin")) + .or_else(|| stem.strip_suffix(".local")) + .or_else(|| stem.strip_suffix(".legacy")) + .unwrap_or(stem); + vec![ + new_path.to_path_buf(), + new_path.with_file_name(format!("{base_name}.meta.json5")), + ] + } +}