use std::collections::HashMap;
use std::path::PathBuf;
use std::thread;
use crossbeam_channel::{Receiver, Sender};
use hjkl_buffer::Sign;
use crate::syntax::BufferId;
pub struct GitJob {
pub buffer_id: BufferId,
pub path: PathBuf,
pub bytes: Vec<u8>,
pub dirty_gen: u64,
}
pub struct GitResult {
pub buffer_id: BufferId,
pub dirty_gen: u64,
pub signs: Vec<Sign>,
pub is_untracked: bool,
}
pub struct GitSignsWorker {
tx: Sender<GitJob>,
rx: Receiver<GitResult>,
_join: thread::JoinHandle<()>,
}
impl GitSignsWorker {
pub fn new() -> Self {
let (job_tx, job_rx) = crossbeam_channel::unbounded::<GitJob>();
let (res_tx, res_rx) = crossbeam_channel::unbounded::<GitResult>();
let handle = thread::Builder::new()
.name("hjkl-git-signs".into())
.spawn(move || worker_loop(job_rx, res_tx))
.expect("spawn git-signs worker");
Self {
tx: job_tx,
rx: res_rx,
_join: handle,
}
}
pub fn submit(&self, job: GitJob) {
let _ = self.tx.send(job);
}
pub fn try_recv(&self) -> Option<GitResult> {
self.rx.try_recv().ok()
}
}
fn worker_loop(job_rx: Receiver<GitJob>, res_tx: Sender<GitResult>) {
let mut pending: HashMap<BufferId, GitJob> = HashMap::new();
let mut queue: Vec<BufferId> = Vec::new();
loop {
let first = match job_rx.recv() {
Ok(j) => j,
Err(_) => return, };
let mut batch = vec![first];
while let Ok(j) = job_rx.try_recv() {
batch.push(j);
}
for job in batch {
let id = job.buffer_id;
let is_new = !pending.contains_key(&id);
pending.insert(id, job);
if is_new {
queue.push(id);
}
}
let ids: Vec<BufferId> = std::mem::take(&mut queue);
for id in ids {
let job = match pending.remove(&id) {
Some(j) => j,
None => continue, };
let signs = crate::git::signs_for_bytes(&job.path, &job.bytes);
let is_untracked = crate::git::is_untracked(&job.path);
let result = GitResult {
buffer_id: job.buffer_id,
dirty_gen: job.dirty_gen,
signs,
is_untracked,
};
if res_tx.send(result).is_err() {
return;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::{Duration, Instant};
#[test]
fn worker_returns_result_for_nonexistent_path() {
let worker = GitSignsWorker::new();
let job = GitJob {
buffer_id: 42,
path: PathBuf::from("/tmp/nonexistent_hjkl_git_test_path_12345/file.txt"),
bytes: Vec::new(),
dirty_gen: 7,
};
worker.submit(job);
let deadline = Instant::now() + Duration::from_millis(500);
let result = loop {
if let Some(r) = worker.try_recv() {
break Some(r);
}
if Instant::now() >= deadline {
break None;
}
std::thread::sleep(Duration::from_millis(10));
};
let result = result.expect("worker should return a result within 500ms");
assert_eq!(result.buffer_id, 42);
assert_eq!(result.dirty_gen, 7);
assert!(
result.signs.is_empty(),
"expected empty signs for nonexistent path; got {:?}",
result.signs
);
assert!(
!result.is_untracked || result.signs.is_empty(),
"unexpected signs for nonexistent path: {:?}",
result.signs
);
}
#[test]
fn worker_coalesces_jobs_for_same_buffer() {
let worker = GitSignsWorker::new();
for dg in 0u64..5 {
worker.submit(GitJob {
buffer_id: 1,
path: PathBuf::from("/tmp/nonexistent_hjkl_coalesce_test/f.txt"),
bytes: Vec::new(),
dirty_gen: dg,
});
}
let deadline = Instant::now() + Duration::from_millis(500);
let mut results: Vec<GitResult> = Vec::new();
loop {
while let Some(r) = worker.try_recv() {
results.push(r);
}
if Instant::now() >= deadline {
break;
}
std::thread::sleep(Duration::from_millis(10));
}
assert!(
!results.is_empty(),
"expected at least one result from the worker"
);
for r in &results {
assert_eq!(r.buffer_id, 1);
}
let last_gen = results.iter().map(|r| r.dirty_gen).max().unwrap();
assert_eq!(last_gen, 4, "expected latest dirty_gen=4 to be delivered");
}
}