use std::sync::Arc;
use std::time::Instant;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LspInitStatus {
NotStarted,
Spawning,
Initializing,
Ready,
Failed,
}
impl LspInitStatus {
pub fn label(&self) -> &'static str {
match self {
Self::NotStarted => "Not Started",
Self::Spawning => "Starting Process",
Self::Initializing => "Initializing",
Self::Ready => "Ready",
Self::Failed => "Failed",
}
}
pub fn is_complete(&self) -> bool {
matches!(self, Self::Ready | Self::Failed)
}
pub fn is_ready(&self) -> bool {
matches!(self, Self::Ready)
}
pub fn is_in_progress(&self) -> bool {
matches!(self, Self::Spawning | Self::Initializing)
}
}
pub struct LspProgressTracker {
language: String,
server_name: String,
status: LspInitStatus,
progress: f32,
message: String,
started_at: Option<Instant>,
}
impl LspProgressTracker {
pub fn new(language: impl Into<String>, server_name: impl Into<String>) -> Self {
Self {
language: language.into(),
server_name: server_name.into(),
status: LspInitStatus::NotStarted,
progress: 0.0,
message: "Waiting to start...".to_string(),
started_at: None,
}
}
pub fn language(&self) -> &str {
&self.language
}
pub fn server_name(&self) -> &str {
&self.server_name
}
pub fn status(&self) -> LspInitStatus {
self.status
}
pub fn progress(&self) -> f32 {
self.progress
}
pub fn message(&self) -> &str {
&self.message
}
pub fn elapsed_secs(&self) -> Option<f64> {
self.started_at.map(|t| t.elapsed().as_secs_f64())
}
pub fn is_ready(&self) -> bool {
self.status.is_ready()
}
pub fn start(&mut self) {
self.status = LspInitStatus::Spawning;
self.progress = 0.0;
self.message = "Starting process...".to_string();
self.started_at = Some(Instant::now());
}
pub fn update(&mut self, progress: f32, message: impl Into<String>) {
if progress > 0.0 && progress <= 1.0 {
self.progress = progress;
self.message = message.into();
if progress < 0.3 {
self.status = LspInitStatus::Spawning;
} else if progress < 1.0 {
self.status = LspInitStatus::Initializing;
}
}
}
pub fn complete(&mut self) {
self.status = LspInitStatus::Ready;
self.progress = 1.0;
self.message = "Ready".to_string();
}
pub fn fail(&mut self, error: impl Into<String>) {
self.status = LspInitStatus::Failed;
self.progress = 0.0;
self.message = error.into();
}
}
pub trait LspProgressCallback: Send + Sync {
fn on_progress(&self, progress: f32, message: &str);
fn on_complete(&self);
fn on_error(&self, error: &str);
}
pub struct NoOpProgressCallback;
impl LspProgressCallback for NoOpProgressCallback {
fn on_progress(&self, _progress: f32, _message: &str) {}
fn on_complete(&self) {}
fn on_error(&self, _error: &str) {}
}
pub struct LoggingProgressCallback;
impl LspProgressCallback for LoggingProgressCallback {
fn on_progress(&self, progress: f32, message: &str) {
log::info!("LSP Progress: {:.0}% - {}", progress * 100.0, message);
}
fn on_complete(&self) {
log::info!("LSP Initialization complete");
}
fn on_error(&self, error: &str) {
log::error!("LSP Initialization failed: {}", error);
}
}
pub struct MultiProgressCallback {
callbacks: Vec<Arc<dyn LspProgressCallback>>,
}
impl MultiProgressCallback {
pub fn new(callbacks: Vec<Arc<dyn LspProgressCallback>>) -> Self {
Self { callbacks }
}
pub fn add(&mut self, callback: Arc<dyn LspProgressCallback>) {
self.callbacks.push(callback);
}
}
impl LspProgressCallback for MultiProgressCallback {
fn on_progress(&self, progress: f32, message: &str) {
for callback in &self.callbacks {
callback.on_progress(progress, message);
}
}
fn on_complete(&self) {
for callback in &self.callbacks {
callback.on_complete();
}
}
fn on_error(&self, error: &str) {
for callback in &self.callbacks {
callback.on_error(error);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_status_labels() {
assert_eq!(LspInitStatus::NotStarted.label(), "Not Started");
assert_eq!(LspInitStatus::Spawning.label(), "Starting Process");
assert_eq!(LspInitStatus::Initializing.label(), "Initializing");
assert_eq!(LspInitStatus::Ready.label(), "Ready");
assert_eq!(LspInitStatus::Failed.label(), "Failed");
}
#[test]
fn test_status_checks() {
assert!(LspInitStatus::Ready.is_complete());
assert!(LspInitStatus::Failed.is_complete());
assert!(!LspInitStatus::Initializing.is_complete());
assert!(LspInitStatus::Ready.is_ready());
assert!(!LspInitStatus::Failed.is_ready());
assert!(LspInitStatus::Spawning.is_in_progress());
assert!(LspInitStatus::Initializing.is_in_progress());
assert!(!LspInitStatus::Ready.is_in_progress());
}
#[test]
fn test_progress_tracker_lifecycle() {
let mut tracker = LspProgressTracker::new("rust", "rust-analyzer");
assert_eq!(tracker.status(), LspInitStatus::NotStarted);
assert_eq!(tracker.progress(), 0.0);
assert_eq!(tracker.message(), "Waiting to start...");
assert!(tracker.elapsed_secs().is_none());
tracker.start();
assert_eq!(tracker.status(), LspInitStatus::Spawning);
assert_eq!(tracker.progress(), 0.0);
assert!(tracker.elapsed_secs().is_some());
tracker.update(0.5, "Loading workspace...");
assert_eq!(tracker.status(), LspInitStatus::Initializing);
assert_eq!(tracker.progress(), 0.5);
assert_eq!(tracker.message(), "Loading workspace...");
tracker.complete();
assert_eq!(tracker.status(), LspInitStatus::Ready);
assert_eq!(tracker.progress(), 1.0);
assert_eq!(tracker.message(), "Ready");
}
#[test]
fn test_progress_tracker_failure() {
let mut tracker = LspProgressTracker::new("typescript", "typescript-language-server");
tracker.start();
tracker.update(0.3, "Initializing...");
tracker.fail("Process spawn failed");
assert_eq!(tracker.status(), LspInitStatus::Failed);
assert_eq!(tracker.message(), "Process spawn failed");
assert!(!tracker.is_ready());
}
#[test]
fn test_no_op_callback() {
let callback = NoOpProgressCallback;
callback.on_progress(0.5, "test");
callback.on_complete();
callback.on_error("test error");
}
#[test]
fn test_multi_callback() {
use std::sync::Mutex;
struct TestCallback {
progress_calls: Mutex<Vec<(f32, String)>>,
}
impl LspProgressCallback for TestCallback {
fn on_progress(&self, progress: f32, message: &str) {
self.progress_calls.lock().unwrap().push((progress, message.to_string()));
}
fn on_complete(&self) {}
fn on_error(&self, _error: &str) {}
}
let cb1 = Arc::new(TestCallback {
progress_calls: Mutex::new(Vec::new()),
});
let cb2 = Arc::new(NoOpProgressCallback);
let multi = MultiProgressCallback::new(vec![cb1.clone(), cb2]);
multi.on_progress(0.5, "test");
let calls = cb1.progress_calls.lock().unwrap();
assert_eq!(calls.len(), 1);
assert_eq!(calls[0], (0.5, "test".to_string()));
}
}