Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
64 changes: 47 additions & 17 deletions src-tauri/src/commands/tokens.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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]
Expand All @@ -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");
}

Expand All @@ -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;
}
Expand All @@ -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]
Expand All @@ -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}";
}
Expand All @@ -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]
Expand Down
131 changes: 104 additions & 27 deletions src-tauri/src/file_watcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ 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,
time::{Duration, SystemTime},
};
use tauri::{AppHandle, Emitter};

Expand All @@ -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,
}

Expand All @@ -28,7 +28,7 @@ pub struct FileWatcher {
debouncer: Arc<Mutex<Option<Debouncer<notify::RecommendedWatcher>>>>,
watched_paths: Arc<Mutex<HashSet<PathBuf>>>,
watched_directories: Arc<Mutex<HashSet<PathBuf>>>,
known_files: Arc<Mutex<HashSet<PathBuf>>>,
known_files: Arc<Mutex<HashMap<PathBuf, SystemTime>>>,
}

impl FileWatcher {
Expand All @@ -38,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(HashSet::new())),
known_files: Arc::new(Mutex::new(HashMap::new())),
}
}

Expand All @@ -57,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(())
}

Expand Down Expand Up @@ -98,7 +109,7 @@ impl FileWatcher {
app_handle: &AppHandle,
watched_paths: &Arc<Mutex<HashSet<PathBuf>>>,
watched_directories: &Arc<Mutex<HashSet<PathBuf>>>,
known_files: &Arc<Mutex<HashSet<PathBuf>>>,
known_files: &Arc<Mutex<HashMap<PathBuf, SystemTime>>>,
) {
let watched_paths = watched_paths.lock().unwrap();
let watched_dirs = watched_directories.lock().unwrap();
Expand All @@ -109,17 +120,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);
}
}
}

Copilot AI Aug 5, 2025

Copy link

Choose a reason for hiding this comment

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

This closing brace appears to be orphaned and would cause a compilation error. The function structure seems incorrect - there should be a closing brace for the function at line 103, not an additional one here.

Copilot uses AI. Check for mistakes.

Expand All @@ -133,17 +148,44 @@ impl FileWatcher {

fn determine_event_type(
path: &PathBuf,
known_files: &Arc<Mutex<HashSet<PathBuf>>>,
) -> FileChangeType {
let mut known_files = known_files.lock().unwrap();
known_files: &Arc<Mutex<HashMap<PathBuf, SystemTime>>>,
) -> Option<FileChangeType> {
let mut files = known_files.lock().unwrap();

if !path.exists() {
known_files.remove(path);
FileChangeType::Deleted
} else if known_files.insert(path.clone()) {
FileChangeType::Created
files.remove(path);
Some(FileChangeType::Deleted)
} else if let Ok(metadata) = std::fs::metadata(path) {
// 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) = files.get(path) {
if stored_mtime == current_mtime {
None
} else {
files.insert(path.clone(), current_mtime);
Some(FileChangeType::Reloaded)
}
} else {
files.insert(path.clone(), current_mtime);
Some(FileChangeType::Opened)
}
} else {
FileChangeType::Modified
log::warn!(
"[FileWatcher] Could not read metadata for {:?}, treating as reload",
path
);
Some(FileChangeType::Reloaded)
}
}

Expand All @@ -168,7 +210,25 @@ 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, handle errors explicitly
if let Ok(metadata) = std::fs::metadata(path_buf) {
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()
.unwrap()
.insert(path_buf.clone(), mtime);
}
}

watched_paths.insert(path_buf.clone());
Expand All @@ -190,7 +250,20 @@ 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 = 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);
}
});

Ok(())
Expand All @@ -210,6 +283,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 {
Expand All @@ -233,6 +309,7 @@ impl FileWatcher {
}

watched_paths.clear();
self.known_files.lock().unwrap().clear();
*debouncer_guard = None;
}
}