1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
use std::path::{Path, PathBuf};
use std::sync::mpsc::{self, Receiver, Sender};
use std::thread;
use std::time::Duration;
use notify::{Event, RecommendedWatcher, RecursiveMode, Watcher};
use anyhow::{Result, Context};
use super::{FileEvent, FileEventKind, filter::FileFilter};
use super::events::AppEvent;
pub struct FileWatcher {
_watcher: RecommendedWatcher,
event_rx: Receiver<AppEvent>,
filter: FileFilter,
}
impl FileWatcher {
pub fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
let path = path.as_ref();
let filter = FileFilter::new(path)?;
let (tx, rx) = mpsc::channel::<notify::Result<Event>>();
let (event_tx, event_rx) = mpsc::channel::<AppEvent>();
// Create the notify watcher
let mut watcher = notify::recommended_watcher(tx)
.context("Failed to create file system watcher")?;
watcher
.watch(path, RecursiveMode::Recursive)
.context("Failed to start watching directory")?;
let filter_clone = FileFilter::new(path)?;
// Spawn background thread to process notify events
thread::spawn(move || {
let mut previous_contents = std::collections::HashMap::<PathBuf, String>::new();
let mut last_event_time = std::collections::HashMap::<PathBuf, std::time::Instant>::new();
while let Ok(result) = rx.recv() {
match result {
Ok(event) => {
// Debounce rapid events on the same path
let now = std::time::Instant::now();
for path in event.paths {
// Filter out ignored files
if !filter_clone.should_watch(&path) {
continue;
}
// Debounce: ignore events that happen too quickly after the previous one
if let Some(last_time) = last_event_time.get(&path) {
if now.duration_since(*last_time) < Duration::from_millis(100) {
continue; // Skip this event as it's too soon
}
}
last_event_time.insert(path.clone(), now);
let file_event = match event.kind {
notify::EventKind::Create(_) => {
let mut fe = FileEvent::new(path.clone(), FileEventKind::Created);
// For new files, read content for preview
if filter_clone.is_text_file(&path) {
if let Ok(content) = std::fs::read_to_string(&path) {
let preview = if content.len() > 200 {
format!("{}...", &content[..200])
} else {
content.clone()
};
fe = fe.with_preview(preview);
previous_contents.insert(path.clone(), content);
}
}
Some(fe)
}
notify::EventKind::Modify(_) => {
let mut fe = FileEvent::new(path.clone(), FileEventKind::Modified);
// Generate diff for modified files
if filter_clone.is_text_file(&path) {
if let Ok(new_content) = std::fs::read_to_string(&path) {
if let Some(old_content) = previous_contents.get(&path) {
// Skip if content hasn't actually changed
if *old_content == new_content {
continue;
}
let diff = crate::diff::generate_unified_diff(old_content, &new_content, &path, &path);
fe = fe.with_diff(diff);
} else {
// First time seeing this file - show a preview instead of empty diff
let preview = if new_content.len() > 200 {
format!("{}...", &new_content[..200])
} else {
new_content.clone()
};
fe = fe.with_preview(preview);
}
previous_contents.insert(path.clone(), new_content);
}
}
Some(fe)
}
notify::EventKind::Remove(_) => {
previous_contents.remove(&path);
Some(FileEvent::new(path.clone(), FileEventKind::Deleted))
}
_ => None,
};
if let Some(fe) = file_event {
if event_tx.send(AppEvent::FileChanged(fe)).is_err() {
break; // Receiver dropped, exit thread
}
}
}
}
Err(err) => {
tracing::error!("File watcher error: {}", err);
}
}
}
});
Ok(Self {
_watcher: watcher,
event_rx,
filter,
})
}
pub fn try_recv(&self) -> Result<AppEvent, std::sync::mpsc::TryRecvError> {
self.event_rx.try_recv()
}
pub fn recv(&self) -> Result<AppEvent, std::sync::mpsc::RecvError> {
self.event_rx.recv()
}
pub fn recv_timeout(&self, timeout: Duration) -> Result<AppEvent, std::sync::mpsc::RecvTimeoutError> {
self.event_rx.recv_timeout(timeout)
}
pub fn get_initial_files(&self) -> Result<Vec<PathBuf>> {
self.filter.get_watchable_files()
}
}
pub fn start_ticker(sender: Sender<AppEvent>) {
thread::spawn(move || {
loop {
thread::sleep(Duration::from_millis(100));
if sender.send(AppEvent::Tick).is_err() {
break;
}
}
});
}