Problem
Project Explorer doesn't update when files are created/deleted/modified externally.
Related issues: #7900, #7954, #8295
Root Cause Analysis
Components Identified (via stack traces and issues)
FileTreeEntryState -> Maintains file tree in memory
RepositoryMetadataModel -> Processes events via handle_watcher_event()
File Watcher (notify crate) -> Watches filesystem changes
Problems Identified
| Problem |
Evidence |
Impact |
| UI not receiving events |
Deleted files remain visible |
Main bug |
| No debouncing |
Memory spikes during rapid changes |
Performance |
| Clone on main thread |
72GB clone of FileTreeEntryState |
Freeze/crash |
| Unbounded growth |
FileTreeEntryState grows infinitely |
Memory leak |
Proposed Solution
Corrected Flow
Filesystem Change
↓
DebouncedFileWatcher (aggregates events 100-300ms)
↓
handle_watcher_event() [BACKGROUND THREAD]
↓
FileTreeEntryState.apply_event() [in-place, no clone]
↓
broadcast::send(FileTreeUpdate)
↓
ProjectExplorerPane.invalidate_paths() [UI THREAD]
↓
UI updates automatically
Key Changes
1. Implement Event Debouncing (repo_metadata/watcher.rs)
pub struct DebouncedFileWatcher {
watcher: RecommendedWatcher,
pending_events: HashMap<PathBuf, FileChangeEvent>,
debounce_duration: Duration,
last_flush: Instant,
}
impl DebouncedFileWatcher {
/// Coalesce events: DELETE + CREATE = MODIFY
fn coalesce_events(&self, events: Vec<FileChangeEvent>) -> Vec<FileChangeEvent> {
let mut by_path: HashMap<PathBuf, FileChangeEvent> = HashMap::new();
for event in events {
by_path.entry(event.path.clone())
.and_modify(|existing| {
if existing.kind == EventKind::Remove && event.kind == EventKind::Create {
existing.kind = EventKind::Modify;
} else {
*existing = event.clone();
}
})
.or_insert(event);
}
by_path.into_values().collect()
}
}
2. Background Processing with UI Notification (repo_metadata/model.rs)
pub struct RepositoryMetadataModel {
file_tree: Arc<RwLock<FileTreeEntryState>>,
ui_notifier: broadcast::Sender<FileTreeUpdate>,
}
impl RepositoryMetadataModel {
pub fn handle_watcher_event(&self, event: notify::Event) {
let file_tree = Arc::clone(&self.file_tree);
let ui_notifier = self.ui_notifier.clone();
// Process in background thread (NOT on main thread)
tokio::spawn(async move {
{
let mut tree = file_tree.write().await;
tree.apply_event(&event);
}
// CRITICAL: Notify UI of changes
let _ = ui_notifier.send(FileTreeUpdate {
paths: event.paths.clone(),
kind: event.kind,
});
});
}
}
3. Bounded LRU Cache (repo_metadata/file_tree_store.rs)
pub struct FileTreeEntryState {
entries: LruCache<PathBuf, FileEntry>, // Bounded cache
expanded_dirs: HashSet<PathBuf>,
max_entries: usize,
}
impl FileTreeEntryState {
/// In-place update without cloning
pub fn apply_event(&mut self, event: ¬ify::Event) {
match event.kind {
EventKind::Create(_) => { /* add entries */ }
EventKind::Remove(_) => { /* remove entries */ }
EventKind::Modify(_) => { /* refresh metadata */ }
_ => {}
}
// Enforce capacity limit
while self.entries.len() > self.max_entries {
self.entries.pop_lru();
}
}
}
4. Hybrid Watcher with Polling Fallback
pub struct HybridFileWatcher {
native_watcher: Option<RecommendedWatcher>,
poll_watcher: PollWatcher,
use_polling_for: HashSet<PathBuf>,
}
impl HybridFileWatcher {
pub fn watch(&mut self, path: &Path) -> Result<(), Error> {
// Try native watcher first, fallback to polling for NFS/WSL
if let Some(ref mut native) = self.native_watcher {
if native.watch(path, RecursiveMode::Recursive).is_ok() {
return Ok(());
}
}
self.poll_watcher.watch(path, RecursiveMode::Recursive)
}
}
5. UI Listener (ui/project_explorer.rs)
impl ProjectExplorerPane {
pub fn setup_file_tree_listener(&mut self, rx: broadcast::Receiver<FileTreeUpdate>) {
let tree_view = self.tree_view.clone();
tokio::spawn(async move {
let mut rx = rx;
while let Ok(update) = rx.recv().await {
tree_view.invalidate_paths(&update.paths);
tree_view.request_redraw();
}
});
}
}
Reference Implementation
A complete working implementation is available at:
https://github.com/DenisCDev/renata-geral/tree/main/warp-file-watcher-fix
Testing Checklist
Files to Modify
| File |
Change |
repo_metadata/watcher.rs |
Add DebouncedFileWatcher, HybridFileWatcher |
repo_metadata/model.rs |
Process events in background, notify UI |
repo_metadata/file_tree_store.rs |
Use LRU cache, apply_event in-place |
ui/project_explorer.rs |
Listen to broadcast channel, invalidate paths |
Problem
Project Explorer doesn't update when files are created/deleted/modified externally.
Related issues: #7900, #7954, #8295
Root Cause Analysis
Components Identified (via stack traces and issues)
Problems Identified
Proposed Solution
Corrected Flow
Key Changes
1. Implement Event Debouncing (
repo_metadata/watcher.rs)2. Background Processing with UI Notification (
repo_metadata/model.rs)3. Bounded LRU Cache (
repo_metadata/file_tree_store.rs)4. Hybrid Watcher with Polling Fallback
5. UI Listener (
ui/project_explorer.rs)Reference Implementation
A complete working implementation is available at:
https://github.com/DenisCDev/renata-geral/tree/main/warp-file-watcher-fix
Testing Checklist
Files to Modify
repo_metadata/watcher.rsrepo_metadata/model.rsrepo_metadata/file_tree_store.rsui/project_explorer.rs