From 35ee7d0643c50dab9dd5800139f6579e243a55d0 Mon Sep 17 00:00:00 2001 From: ninet33n19 Date: Tue, 5 Aug 2025 20:05:25 +0530 Subject: [PATCH 1/3] Track file modification times to ignore metadata-only changes Switch known_files from HashSet to HashMap to track modification times. Only emit file change events when the modification time changes, ignoring metadata-only updates such as access time changes. --- src-tauri/src/file_watcher.rs | 92 +++++++++++++++++++++++++---------- 1 file changed, 67 insertions(+), 25 deletions(-) diff --git a/src-tauri/src/file_watcher.rs b/src-tauri/src/file_watcher.rs index 2b3055f6e..f91051a42 100644 --- a/src-tauri/src/file_watcher.rs +++ b/src-tauri/src/file_watcher.rs @@ -2,7 +2,7 @@ use anyhow::{Context, Result, bail}; use notify::RecursiveMode; use notify_debouncer_mini::{DebounceEventResult, Debouncer, new_debouncer}; use std::{ - collections::HashSet, + collections::{HashMap, HashSet}, path::PathBuf, sync::{Arc, Mutex}, time::Duration, @@ -28,7 +28,8 @@ pub struct FileWatcher { debouncer: Arc>>>, watched_paths: Arc>>, watched_directories: Arc>>, - known_files: Arc>>, + known_files: Arc>>, /* Changed to HashMap with + * mtime */ } impl FileWatcher { @@ -38,7 +39,7 @@ impl FileWatcher { debouncer: Arc::new(Mutex::new(None)), watched_paths: Arc::new(Mutex::new(HashSet::new())), watched_directories: Arc::new(Mutex::new(HashSet::new())), - known_files: Arc::new(Mutex::new(HashSet::new())), + known_files: Arc::new(Mutex::new(HashMap::new())), // Initialize as HashMap } } @@ -98,7 +99,7 @@ impl FileWatcher { app_handle: &AppHandle, watched_paths: &Arc>>, watched_directories: &Arc>>, - known_files: &Arc>>, + known_files: &Arc>>, // Updated type ) { let watched_paths = watched_paths.lock().unwrap(); let watched_dirs = watched_directories.lock().unwrap(); @@ -109,17 +110,21 @@ impl FileWatcher { } let event_type = Self::determine_event_type(&event.path, known_files); - let change_event = FileChangeEvent { - path: event.path.to_string_lossy().to_string(), - event_type, - }; - - log::debug!( - "[FileWatcher] Emitting file-changed event for: {} ({:?})", - change_event.path, - change_event.event_type - ); - let _ = app_handle.emit("file-changed", &change_event); + + // Only emit event if it's not a metadata-only change + if let Some(event_type) = event_type { + let change_event = FileChangeEvent { + path: event.path.to_string_lossy().to_string(), + event_type, + }; + + log::debug!( + "[FileWatcher] Emitting file-changed event for: {} ({:?})", + change_event.path, + change_event.event_type + ); + let _ = app_handle.emit("file-changed", &change_event); + } } } @@ -133,17 +138,35 @@ impl FileWatcher { fn determine_event_type( path: &PathBuf, - known_files: &Arc>>, - ) -> FileChangeType { - let mut known_files = known_files.lock().unwrap(); + known_files: &Arc>>, // Updated type + ) -> Option { + let mut known_files_map = known_files.lock().unwrap(); if !path.exists() { - known_files.remove(path); - FileChangeType::Deleted - } else if known_files.insert(path.clone()) { - FileChangeType::Created + known_files_map.remove(path); + Some(FileChangeType::Deleted) + } else if let Ok(metadata) = std::fs::metadata(path) { + let current_mtime = metadata + .modified() + .unwrap_or(std::time::SystemTime::UNIX_EPOCH); + + if let Some(&stored_mtime) = known_files_map.get(path) { + // If modification time hasn't changed, it's likely just access time - ignore + if stored_mtime == current_mtime { + None // No actual content change + } else { + // Modification time changed - actual content modification + known_files_map.insert(path.clone(), current_mtime); + Some(FileChangeType::Modified) + } + } else { + // New file that wasn't tracked before + known_files_map.insert(path.clone(), current_mtime); + Some(FileChangeType::Created) + } } else { - FileChangeType::Modified + // Can't get metadata, but file exists - treat as modification + Some(FileChangeType::Modified) } } @@ -168,7 +191,17 @@ impl FileWatcher { if path_buf.is_dir() { self.setup_directory_watching(path_buf)?; } else { - self.known_files.lock().unwrap().insert(path_buf.clone()); + // Track initial modification time for files + if let Ok(metadata) = std::fs::metadata(path_buf) { + let mtime = metadata + .modified() + .unwrap_or(std::time::SystemTime::UNIX_EPOCH); + self + .known_files + .lock() + .unwrap() + .insert(path_buf.clone(), mtime); + } } watched_paths.insert(path_buf.clone()); @@ -190,7 +223,12 @@ impl FileWatcher { .map(|entry| entry.path()) .filter(|path| path.is_file()) .for_each(|path| { - known_files.insert(path); + if let Ok(metadata) = std::fs::metadata(&path) { + let mtime = metadata + .modified() + .unwrap_or(std::time::SystemTime::UNIX_EPOCH); + known_files.insert(path, mtime); + } }); Ok(()) @@ -210,6 +248,9 @@ impl FileWatcher { watched_dirs.remove(&path_buf); } + // Remove from known files tracking + self.known_files.lock().unwrap().remove(&path_buf); + // Unwatch the path let mut debouncer_guard = self.debouncer.lock().unwrap(); if let Some(ref mut debouncer) = *debouncer_guard { @@ -233,6 +274,7 @@ impl FileWatcher { } watched_paths.clear(); + self.known_files.lock().unwrap().clear(); *debouncer_guard = None; } } From 3618e2e103fd04a26da1158f10e8bba8789d3fed Mon Sep 17 00:00:00 2001 From: ninet33n19 Date: Tue, 5 Aug 2025 21:01:37 +0530 Subject: [PATCH 2/3] Refactor file watcher event types and improve mtime handling - Replace Created/Modified with Opened/Reloaded event types - Emit Opened event when a file is watched - Improve handling of file modification times and error cases - Add logging for metadata access issues --- src-tauri/src/file_watcher.rs | 97 ++++++++++++++++++++++++----------- 1 file changed, 66 insertions(+), 31 deletions(-) diff --git a/src-tauri/src/file_watcher.rs b/src-tauri/src/file_watcher.rs index f91051a42..cbf885444 100644 --- a/src-tauri/src/file_watcher.rs +++ b/src-tauri/src/file_watcher.rs @@ -5,7 +5,7 @@ use std::{ collections::{HashMap, HashSet}, path::PathBuf, sync::{Arc, Mutex}, - time::Duration, + time::{Duration, SystemTime}, }; use tauri::{AppHandle, Emitter}; @@ -18,8 +18,8 @@ pub struct FileChangeEvent { #[derive(Debug, Clone, serde::Serialize)] #[serde(rename_all = "snake_case")] pub enum FileChangeType { - Created, - Modified, + Opened, + Reloaded, Deleted, } @@ -28,8 +28,7 @@ pub struct FileWatcher { debouncer: Arc>>>, watched_paths: Arc>>, watched_directories: Arc>>, - known_files: Arc>>, /* Changed to HashMap with - * mtime */ + known_files: Arc>>, } impl FileWatcher { @@ -39,7 +38,7 @@ impl FileWatcher { debouncer: Arc::new(Mutex::new(None)), watched_paths: Arc::new(Mutex::new(HashSet::new())), watched_directories: Arc::new(Mutex::new(HashSet::new())), - known_files: Arc::new(Mutex::new(HashMap::new())), // Initialize as HashMap + known_files: Arc::new(Mutex::new(HashMap::new())), } } @@ -58,6 +57,17 @@ impl FileWatcher { self.ensure_debouncer_initialized()?; self.setup_path_watching(&path_buf, &mut watched_paths)?; + // Emit an "Opened" event for clarity in the app UI + let change_event = FileChangeEvent { + path: path_buf.to_string_lossy().to_string(), + event_type: FileChangeType::Opened, + }; + log::debug!( + "[FileWatcher] Emitting opened event for: {}", + change_event.path + ); + let _ = self.app_handle.emit("file-changed", &change_event); + Ok(()) } @@ -99,7 +109,7 @@ impl FileWatcher { app_handle: &AppHandle, watched_paths: &Arc>>, watched_directories: &Arc>>, - known_files: &Arc>>, // Updated type + known_files: &Arc>>, ) { let watched_paths = watched_paths.lock().unwrap(); let watched_dirs = watched_directories.lock().unwrap(); @@ -138,35 +148,44 @@ impl FileWatcher { fn determine_event_type( path: &PathBuf, - known_files: &Arc>>, // Updated type + known_files: &Arc>>, ) -> Option { - let mut known_files_map = known_files.lock().unwrap(); + let mut files = known_files.lock().unwrap(); if !path.exists() { - known_files_map.remove(path); + files.remove(path); Some(FileChangeType::Deleted) } else if let Ok(metadata) = std::fs::metadata(path) { - let current_mtime = metadata - .modified() - .unwrap_or(std::time::SystemTime::UNIX_EPOCH); + // Handle modification time explicitly to avoid misleading UNIX_EPOCH fallback + let current_mtime = match metadata.modified() { + Ok(mtime) => mtime, + Err(err) => { + log::warn!( + "[FileWatcher] Could not get modification time for {:?}: {}", + path, + err + ); + SystemTime::now() + } + }; - if let Some(&stored_mtime) = known_files_map.get(path) { - // If modification time hasn't changed, it's likely just access time - ignore + if let Some(&stored_mtime) = files.get(path) { if stored_mtime == current_mtime { - None // No actual content change + None } else { - // Modification time changed - actual content modification - known_files_map.insert(path.clone(), current_mtime); - Some(FileChangeType::Modified) + files.insert(path.clone(), current_mtime); + Some(FileChangeType::Reloaded) } } else { - // New file that wasn't tracked before - known_files_map.insert(path.clone(), current_mtime); - Some(FileChangeType::Created) + files.insert(path.clone(), current_mtime); + Some(FileChangeType::Opened) } } else { - // Can't get metadata, but file exists - treat as modification - Some(FileChangeType::Modified) + log::warn!( + "[FileWatcher] Could not read metadata for {:?}, treating as reload", + path + ); + Some(FileChangeType::Reloaded) } } @@ -191,11 +210,19 @@ impl FileWatcher { if path_buf.is_dir() { self.setup_directory_watching(path_buf)?; } else { - // Track initial modification time for files + // Track initial modification time for files, handle errors explicitly if let Ok(metadata) = std::fs::metadata(path_buf) { - let mtime = metadata - .modified() - .unwrap_or(std::time::SystemTime::UNIX_EPOCH); + let mtime = match metadata.modified() { + Ok(t) => t, + Err(err) => { + log::warn!( + "[FileWatcher] Could not get initial modification time for {:?}: {}", + path_buf, + err + ); + SystemTime::now() + } + }; self .known_files .lock() @@ -224,9 +251,17 @@ impl FileWatcher { .filter(|path| path.is_file()) .for_each(|path| { if let Ok(metadata) = std::fs::metadata(&path) { - let mtime = metadata - .modified() - .unwrap_or(std::time::SystemTime::UNIX_EPOCH); + let mtime = match metadata.modified() { + Ok(t) => t, + Err(err) => { + log::warn!( + "[FileWatcher] Could not get initial modification time for {:?}: {}", + path, + err + ); + SystemTime::now() + } + }; known_files.insert(path, mtime); } }); From 28138b91618e079e010ae5c10b01c4a5f3e0f047 Mon Sep 17 00:00:00 2001 From: ninet33n19 Date: Tue, 5 Aug 2025 21:49:33 +0530 Subject: [PATCH 3/3] Refactor test assertions for readability in tokens.rs --- src-tauri/src/commands/tokens.rs | 64 +++++++++++++++++++++++--------- 1 file changed, 47 insertions(+), 17 deletions(-) diff --git a/src-tauri/src/commands/tokens.rs b/src-tauri/src/commands/tokens.rs index 2a14ca4c1..3f36c5837 100644 --- a/src-tauri/src/commands/tokens.rs +++ b/src-tauri/src/commands/tokens.rs @@ -476,15 +476,15 @@ std = []"#; fn test_tokenize_java() { let code = r#"public class HelloWorld { private String message; - + public HelloWorld(String message) { this.message = message; } - + public void greet() { System.out.println("Hello, " + message + "!"); } - + public static void main(String[] args) { HelloWorld app = new HelloWorld("World"); app.greet(); @@ -495,10 +495,19 @@ std = []"#; println!("Found {} Java tokens", tokens.len()); let token_types: Vec<&str> = tokens.iter().map(|t| t.token_type.as_str()).collect(); - assert!(token_types.contains(&"keyword"), "Should have keyword tokens"); + assert!( + token_types.contains(&"keyword"), + "Should have keyword tokens" + ); assert!(token_types.contains(&"string"), "Should have string tokens"); - assert!(token_types.contains(&"identifier"), "Should have identifier tokens"); - assert!(token_types.contains(&"function"), "Should have function tokens"); + assert!( + token_types.contains(&"identifier"), + "Should have identifier tokens" + ); + assert!( + token_types.contains(&"function"), + "Should have function tokens" + ); } #[test] @@ -515,9 +524,15 @@ int main() { println!("Found {} C tokens", tokens.len()); let token_types: Vec<&str> = tokens.iter().map(|t| t.token_type.as_str()).collect(); - assert!(token_types.contains(&"keyword"), "Should have keyword tokens"); + assert!( + token_types.contains(&"keyword"), + "Should have keyword tokens" + ); assert!(token_types.contains(&"string"), "Should have string tokens"); - assert!(token_types.contains(&"identifier"), "Should have identifier tokens"); + assert!( + token_types.contains(&"identifier"), + "Should have identifier tokens" + ); assert!(token_types.contains(&"number"), "Should have number tokens"); } @@ -529,10 +544,10 @@ int main() { class Greeter { private: std::string name; - + public: Greeter(const std::string& n) : name(n) {} - + void greet() const { std::cout << "Hello, " << name << "!" << std::endl; } @@ -549,8 +564,14 @@ int main() { let token_types: Vec<&str> = tokens.iter().map(|t| t.token_type.as_str()).collect(); assert!(!tokens.is_empty(), "Should have tokens"); - assert!(token_types.contains(&"keyword"), "Should have keyword tokens"); - assert!(token_types.contains(&"function") || token_types.contains(&"identifier"), "Should have function or identifier tokens"); + assert!( + token_types.contains(&"keyword"), + "Should have keyword tokens" + ); + assert!( + token_types.contains(&"function") || token_types.contains(&"identifier"), + "Should have function or identifier tokens" + ); } #[test] @@ -559,12 +580,12 @@ int main() { class User { private $name; private $email; - + public function __construct($name, $email) { $this->name = $name; $this->email = $email; } - + public function greet() { echo "Hello, {$this->name}! Your email is {$this->email}"; } @@ -578,10 +599,19 @@ $user->greet(); println!("Found {} PHP tokens", tokens.len()); let token_types: Vec<&str> = tokens.iter().map(|t| t.token_type.as_str()).collect(); - assert!(token_types.contains(&"keyword"), "Should have keyword tokens"); + assert!( + token_types.contains(&"keyword"), + "Should have keyword tokens" + ); assert!(token_types.contains(&"string"), "Should have string tokens"); - assert!(token_types.contains(&"identifier"), "Should have identifier tokens"); - assert!(token_types.contains(&"function"), "Should have function tokens"); + assert!( + token_types.contains(&"identifier"), + "Should have identifier tokens" + ); + assert!( + token_types.contains(&"function"), + "Should have function tokens" + ); } #[test]