use std::collections::HashMap;
use std::panic::AssertUnwindSafe;
use std::path::PathBuf;
use std::thread::JoinHandle;
use crossbeam_channel::{Receiver, Sender, select};
use lsp_types::Uri;
use salsa::Database as _;
use crate::incremental::IncrementalDatabase;
use crate::text::PositionEncoding;
use super::format::parse_diagnostics_to_lsp;
use super::read_jobs::{ReadJob, run_read};
use super::state::Outbound;
use super::task_pool::Spawner;
pub(crate) struct AnalysisRequest {
pub(crate) uri: Uri,
pub(crate) path: PathBuf,
pub(crate) text: String,
pub(crate) version: i32,
}
pub(crate) fn spawn_analysis_thread(
analysis_rx: Receiver<AnalysisRequest>,
read_rx: Receiver<ReadJob>,
out_tx: Sender<Outbound>,
read_spawner: Spawner,
encoding: PositionEncoding,
) -> JoinHandle<()> {
let (done_tx, done_rx) = crossbeam_channel::unbounded::<AnalyzeDone>();
std::thread::Builder::new()
.name("fatou-analysis".to_string())
.spawn(move || {
let mut worker = AnalysisWorker {
db: IncrementalDatabase::default(),
out_tx,
done_tx,
inflight: None,
pending: HashMap::new(),
read_spawner,
encoding,
};
worker.run(&analysis_rx, &read_rx, &done_rx);
})
.expect("spawn analysis thread")
}
struct AnalyzeDone {
uri: Uri,
version: i32,
}
struct InflightAnalyze {
uri: Uri,
version: i32,
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum DispatchAction {
Wait,
Start(Uri),
SupersedeAndStart(Uri),
}
pub(crate) fn decide(inflight: Option<(&Uri, i32)>, pending: &HashMap<Uri, i32>) -> DispatchAction {
match inflight {
None => match pending.keys().next() {
Some(uri) => DispatchAction::Start(uri.clone()),
None => DispatchAction::Wait,
},
Some((uri, version)) => {
if pending.get(uri).is_some_and(|&v| v > version) {
DispatchAction::SupersedeAndStart(uri.clone())
} else {
DispatchAction::Wait
}
}
}
}
struct AnalysisWorker {
db: IncrementalDatabase,
out_tx: Sender<Outbound>,
done_tx: Sender<AnalyzeDone>,
inflight: Option<InflightAnalyze>,
pending: HashMap<Uri, AnalysisRequest>,
read_spawner: Spawner,
encoding: PositionEncoding,
}
impl AnalysisWorker {
fn run(
&mut self,
analysis_rx: &Receiver<AnalysisRequest>,
read_rx: &Receiver<ReadJob>,
done_rx: &Receiver<AnalyzeDone>,
) {
loop {
select! {
recv(analysis_rx) -> msg => {
let Ok(req) = msg else { break };
self.enqueue(req);
while let Ok(more) = analysis_rx.try_recv() {
self.enqueue(more);
}
self.try_dispatch();
}
recv(done_rx) -> done => {
let Ok(done) = done else { continue };
if matches!(&self.inflight, Some(f) if f.uri == done.uri && f.version == done.version) {
self.inflight = None;
}
self.try_dispatch();
}
recv(read_rx) -> job => {
let Ok(job) = job else { continue };
let snapshot = self.db.snapshot();
let encoding = self.encoding;
self.read_spawner.spawn(move || run_read(snapshot, job, encoding));
}
}
}
}
fn enqueue(&mut self, req: AnalysisRequest) {
match self.pending.get(&req.uri) {
Some(existing) if existing.version >= req.version => {}
_ => {
self.pending.insert(req.uri.clone(), req);
}
}
}
fn try_dispatch(&mut self) {
let versions: HashMap<Uri, i32> = self
.pending
.iter()
.map(|(uri, req)| (uri.clone(), req.version))
.collect();
let inflight = self.inflight.as_ref().map(|f| (&f.uri, f.version));
let uri = match decide(inflight, &versions) {
DispatchAction::Wait => return,
DispatchAction::Start(uri) => uri,
DispatchAction::SupersedeAndStart(uri) => {
self.db.trigger_cancellation();
self.inflight = None;
uri
}
};
if let Some(req) = self.pending.remove(&uri) {
self.start(req);
}
}
fn start(&mut self, req: AnalysisRequest) {
let file = self.db.upsert_file(&req.path, req.text.clone());
let snapshot = self.db.snapshot();
let out_tx = self.out_tx.clone();
let done_tx = self.done_tx.clone();
let encoding = self.encoding;
let AnalysisRequest {
uri, text, version, ..
} = req;
self.inflight = Some(InflightAnalyze {
uri: uri.clone(),
version,
});
self.read_spawner.spawn(move || {
let result = salsa::Cancelled::catch(AssertUnwindSafe(|| {
parse_diagnostics_to_lsp(snapshot.parse_diagnostics(file), &text, encoding)
}));
if let Ok(diags) = result {
let _ = out_tx.send(Outbound::Diagnostics {
uri: uri.clone(),
version,
diags,
});
}
drop(snapshot);
let _ = done_tx.send(AnalyzeDone { uri, version });
});
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
fn uri_named(name: &str) -> Uri {
Uri::from_str(&format!("file:///work/{name}")).unwrap()
}
#[test]
fn decide_idle_starts_a_pending_uri() {
let a = uri_named("a.jl");
let pending = HashMap::from([(a.clone(), 1)]);
assert_eq!(decide(None, &pending), DispatchAction::Start(a));
}
#[test]
fn decide_idle_empty_queue_waits() {
let pending: HashMap<Uri, i32> = HashMap::new();
assert_eq!(decide(None, &pending), DispatchAction::Wait);
}
#[test]
fn decide_supersedes_same_uri_newer_version() {
let a = uri_named("a.jl");
let pending = HashMap::from([(a.clone(), 2)]);
assert_eq!(
decide(Some((&a, 1)), &pending),
DispatchAction::SupersedeAndStart(a)
);
}
#[test]
fn decide_waits_when_pending_same_uri_not_newer() {
let a = uri_named("a.jl");
let pending = HashMap::from([(a.clone(), 1)]);
assert_eq!(decide(Some((&a, 1)), &pending), DispatchAction::Wait);
}
#[test]
fn decide_never_cancels_a_different_uri() {
let a = uri_named("a.jl");
let pending = HashMap::from([(uri_named("b.jl"), 5), (uri_named("c.jl"), 9)]);
assert_eq!(decide(Some((&a, 1)), &pending), DispatchAction::Wait);
}
#[test]
fn decide_drains_multiple_uris_one_at_a_time() {
let (a, b, c) = (uri_named("a.jl"), uri_named("b.jl"), uri_named("c.jl"));
let mut pending = HashMap::from([(a.clone(), 1), (b.clone(), 1), (c.clone(), 1)]);
let DispatchAction::Start(first) = decide(None, &pending) else {
panic!("expected Start");
};
assert!(pending.contains_key(&first));
pending.remove(&first);
assert_eq!(decide(Some((&first, 1)), &pending), DispatchAction::Wait);
let mut started = vec![first];
while !pending.is_empty() {
let DispatchAction::Start(next) = decide(None, &pending) else {
panic!("expected Start");
};
pending.remove(&next);
started.push(next);
}
started.sort_by_key(|u| u.as_str().to_string());
assert_eq!(started, {
let mut all = vec![a, b, c];
all.sort_by_key(|u| u.as_str().to_string());
all
});
}
}