use std::collections::{HashMap, HashSet, VecDeque};
use std::path::PathBuf;
use std::sync::{Arc, Mutex, Weak};
use serde_json::Value;
use tokio::{
sync::RwLock,
task::{self, JoinHandle},
};
use tower_lsp::lsp_types::{ConfigurationItem, Position, Range, TextEdit, WorkspaceEdit};
use tower_lsp::{
Client,
lsp_types::{MessageType, Url},
};
use crate::{CoverageReport, Settings};
#[derive(Debug)]
pub struct CoverageLanguageServer {
pub context: Arc<CoverageLanguageServerContext>,
}
impl CoverageLanguageServer {
pub fn new(client: Client) -> Self {
Self {
context: CoverageLanguageServerContext::new(client),
}
}
}
impl std::ops::Deref for CoverageLanguageServer {
type Target = CoverageLanguageServerContext;
fn deref(&self) -> &Self::Target {
&self.context
}
}
#[derive(Debug)]
pub struct CoverageLanguageServerContext {
pub client: Client,
pub root_uri: RwLock<Url>,
pub report: RwLock<Option<CoverageReport>>,
pub open_docs: RwLock<HashSet<Url>>,
join_handle: Mutex<Option<JoinHandle<()>>>,
}
impl CoverageLanguageServerContext {
pub fn new(client: Client) -> Arc<Self> {
Arc::new(Self {
client,
root_uri: RwLock::new(
Url::from_directory_path(std::env::current_dir().unwrap()).unwrap(),
),
report: Default::default(),
open_docs: Default::default(),
join_handle: Default::default(),
})
}
pub fn start(self: &Arc<Self>) {
self.join_handle.lock().unwrap().get_or_insert_with(|| {
let weak = Arc::downgrade(self);
task::spawn(CoverageLanguageServerContext::run(weak))
});
}
pub async fn stop(self: &Arc<Self>) {
let join_handle = self.join_handle.lock().unwrap().take();
if let Some(join_handle) = join_handle {
join_handle.abort();
if let Err(err) = join_handle.await {
tracing::error!(?err, "parsing thread join error");
}
}
}
async fn run(weak: Weak<Self>) {
while let Some(ctx) = weak.upgrade() {
ctx.update().await;
drop(ctx);
let interval = Settings::get().interval;
tokio::time::sleep(interval).await;
}
}
async fn get_update_file(&self) -> Option<PathBuf> {
let setting_file_guard = Settings::get().lcov_file.as_ref().map(Arc::clone);
let setting_file = setting_file_guard.as_ref().map(|v| v.as_ref());
match self.report.read().await.as_ref() {
Some(report)
if setting_file.is_some_and(|setting_file| setting_file != &report.path) =>
{
Some(setting_file.unwrap().clone())
}
Some(report) if report.is_outdated() => Some(report.path.clone()),
Some(_) => None,
None => match setting_file {
Some(file) => Some(file.clone()),
None => self.find_lcov_file().await,
},
}
}
pub async fn update(&self) {
if let Some(file) = self.get_update_file().await {
self.client
.log_message(MessageType::INFO, format!("(re)loading file: {file:?}"))
.await;
let mut report = match CoverageReport::try_from(file) {
Ok(report) => report,
Err(err) => {
self.client
.log_message(
MessageType::ERROR,
format!("failed to load report: {err:?}"),
)
.await;
self.report.write().await.take();
#[cfg(feature = "notifications")]
self.send_update_notification(true).await;
return;
}
};
let root_uri = self.root_uri.read().await.clone();
if let Err(err) = report.load(&root_uri) {
self.client
.log_message(
MessageType::ERROR,
format!("failed to parse report: {err:?}"),
)
.await
} else {
self.report.write().await.replace(report);
#[cfg(feature = "notifications")]
self.send_update_notification(false).await;
}
}
}
#[cfg(feature = "notifications")]
pub async fn send_update_notification(&self, forced: bool) {
let opened = self.open_docs.read().await.clone();
let mut changes = HashMap::with_capacity(1);
let edit = Vec::from([TextEdit {
range: Range::new(Position::new(0, 0), Position::new(0, 0)),
new_text: " ".into(),
}]);
for doc in opened.into_iter() {
if !forced
&& self
.report
.read()
.await
.as_ref()
.is_none_or(|report| !report.db.contains_key(&doc))
{
continue;
}
changes.clear();
changes.insert(doc.clone(), edit.clone());
if let Err(err) = self
.client
.apply_edit(WorkspaceEdit {
changes: Some(changes.clone()),
..Default::default()
})
.await
{
tracing::error!(?err, "WorkspaceEdit error");
return;
}
for (_, change) in changes.iter_mut() {
let text_edit = change.first_mut().unwrap();
text_edit.range.end.character = 1;
text_edit.new_text = String::default();
}
if let Err(err) = self
.client
.apply_edit(WorkspaceEdit {
changes: Some(changes.clone()),
..Default::default()
})
.await
{
tracing::error!(?err, "WorkspaceEdit error");
return;
}
}
}
pub async fn find_lcov_file(&self) -> Option<PathBuf> {
self.client
.log_message(MessageType::INFO, "crawling for coverage file")
.await;
let mut dir_stack = VecDeque::with_capacity(64);
let root_path = self.root_uri.read().await.path().to_string();
dir_stack.push_back(PathBuf::from(&root_path));
while let Some(path) = dir_stack.pop_front() {
let mut reader = match tokio::fs::read_dir(path).await.ok() {
Some(reader) => reader,
None => {
self.client
.log_message(MessageType::WARNING, "failed to read_dir: {path:?}")
.await;
continue;
}
};
while let Ok(Some(entry)) = reader.next_entry().await {
let path = entry.path();
if path.is_dir() {
dir_stack.push_back(path);
} else if path.extension().is_some_and(|ext| ext == "info") {
self.client
.show_message(
MessageType::INFO,
format!(
"coverage file found: {:?}",
path.strip_prefix(&root_path).unwrap_or(&path)
),
)
.await;
return Some(path);
}
}
}
None
}
pub async fn get_configuration(&self) -> Option<Value> {
self.client
.configuration(vec![ConfigurationItem::default()])
.await
.ok()
.and_then(|mut v| v.pop())
}
}
impl Drop for CoverageLanguageServerContext {
fn drop(&mut self) {
if let Some(join_handle) = self.join_handle.lock().unwrap().take() {
join_handle.abort();
}
}
}