cdafffaa70
Côté JS : décompose main.js en modules ES6 (state, services, ui, router) pour séparer les responsabilités et faciliter la maintenance. Côté Rust : ajoute 7 tests sur is_relevant_path (notify_watcher) pour couvrir les cas cachés/non-md ; couverture totale 77 % > seuil 60 %. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
108 lines
2.7 KiB
Rust
108 lines
2.7 KiB
Rust
use notify::EventKind;
|
|
use serde::Serialize;
|
|
use std::collections::HashMap;
|
|
use std::path::{Path, PathBuf};
|
|
use std::sync::mpsc::RecvTimeoutError;
|
|
use std::time::{Duration, Instant};
|
|
use tauri::{AppHandle, Emitter};
|
|
|
|
#[derive(Serialize, Clone)]
|
|
struct FileChangedPayload {
|
|
path: String,
|
|
}
|
|
|
|
pub fn run_debounce_loop(
|
|
rx: std::sync::mpsc::Receiver<notify::Result<notify::Event>>,
|
|
app: AppHandle,
|
|
) {
|
|
let mut pending: HashMap<PathBuf, Instant> = HashMap::new();
|
|
|
|
loop {
|
|
match rx.recv_timeout(Duration::from_millis(10)) {
|
|
Ok(Ok(event)) => {
|
|
if matches!(
|
|
event.kind,
|
|
EventKind::Modify(_) | EventKind::Create(_) | EventKind::Remove(_)
|
|
) {
|
|
for p in event.paths {
|
|
if is_relevant_path(&p) {
|
|
pending.insert(p, Instant::now());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Ok(Err(_)) => {}
|
|
Err(RecvTimeoutError::Timeout) => {}
|
|
Err(RecvTimeoutError::Disconnected) => break,
|
|
}
|
|
|
|
let now = Instant::now();
|
|
let ready: Vec<PathBuf> = pending
|
|
.iter()
|
|
.filter(|(_, t)| now.duration_since(**t) >= Duration::from_millis(80))
|
|
.map(|(p, _)| p.clone())
|
|
.collect();
|
|
|
|
for p in ready {
|
|
pending.remove(&p);
|
|
let _ = app.emit(
|
|
"file-changed",
|
|
FileChangedPayload {
|
|
path: p.to_string_lossy().to_string(),
|
|
},
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn is_relevant_path(path: &Path) -> bool {
|
|
if path
|
|
.components()
|
|
.any(|c| c.as_os_str().to_string_lossy().starts_with('.'))
|
|
{
|
|
return false;
|
|
}
|
|
path.extension().is_some_and(|e| e == "md")
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::path::Path;
|
|
|
|
#[test]
|
|
fn relevant_path_md_file() {
|
|
assert!(is_relevant_path(Path::new("/docs/notes.md")));
|
|
}
|
|
|
|
#[test]
|
|
fn relevant_path_nested_md() {
|
|
assert!(is_relevant_path(Path::new("/wiki/section/page.md")));
|
|
}
|
|
|
|
#[test]
|
|
fn irrelevant_path_txt_extension() {
|
|
assert!(!is_relevant_path(Path::new("/docs/readme.txt")));
|
|
}
|
|
|
|
#[test]
|
|
fn irrelevant_path_no_extension() {
|
|
assert!(!is_relevant_path(Path::new("/docs/makefile")));
|
|
}
|
|
|
|
#[test]
|
|
fn irrelevant_path_hidden_file() {
|
|
assert!(!is_relevant_path(Path::new("/docs/.hidden.md")));
|
|
}
|
|
|
|
#[test]
|
|
fn irrelevant_path_hidden_dir() {
|
|
assert!(!is_relevant_path(Path::new("/docs/.git/file.md")));
|
|
}
|
|
|
|
#[test]
|
|
fn irrelevant_path_hidden_dir_at_root() {
|
|
assert!(!is_relevant_path(Path::new(".hidden/file.md")));
|
|
}
|
|
}
|