use lsp_types::{NumberOrString, ProgressParams, ProgressParamsValue, WorkDoneProgress};
use serde::Serialize;
use std::collections::HashMap;
use std::time::Instant;
pub type ProgressToken = NumberOrString;
#[derive(Debug, Clone)]
pub struct ProgressState {
pub title: String,
pub message: Option<String>,
pub percentage: Option<u32>,
pub started: Instant,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum ServerState {
Initializing,
Indexing,
Ready,
Dead,
}
impl ServerState {
pub fn from_u8(value: u8) -> Self {
match value {
0 => ServerState::Initializing,
1 => ServerState::Indexing,
2 => ServerState::Ready,
_ => ServerState::Dead,
}
}
pub fn as_u8(self) -> u8 {
match self {
ServerState::Initializing => 0,
ServerState::Indexing => 1,
ServerState::Ready => 2,
ServerState::Dead => 3,
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct ServerStatus {
pub language: String,
pub state: ServerState,
#[serde(skip_serializing_if = "Option::is_none")]
pub progress_title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub progress_message: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub progress_percentage: Option<u32>,
pub uptime_secs: u64,
}
#[derive(Debug, Default)]
pub struct ProgressTracker {
active_progress: HashMap<ProgressToken, ProgressState>,
}
impl ProgressTracker {
pub fn new() -> Self {
Self::default()
}
pub fn update(&mut self, params: &ProgressParams) {
match ¶ms.value {
ProgressParamsValue::WorkDone(progress) => match progress {
WorkDoneProgress::Begin(begin) => {
self.active_progress.insert(
params.token.clone(),
ProgressState {
title: begin.title.clone(),
message: begin.message.clone(),
percentage: begin.percentage,
started: Instant::now(),
},
);
}
WorkDoneProgress::Report(report) => {
if let Some(state) = self.active_progress.get_mut(¶ms.token) {
if report.message.is_some() {
state.message.clone_from(&report.message);
}
if report.percentage.is_some() {
state.percentage = report.percentage;
}
}
}
WorkDoneProgress::End(_) => {
self.active_progress.remove(¶ms.token);
}
},
}
}
pub fn is_busy(&self) -> bool {
!self.active_progress.is_empty()
}
pub fn primary_progress(&self) -> Option<&ProgressState> {
self.active_progress
.values()
.min_by_key(|p| p.percentage.unwrap_or(0))
}
pub fn clear(&mut self) {
self.active_progress.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_progress_params(token: &str, progress: WorkDoneProgress) -> ProgressParams {
ProgressParams {
token: NumberOrString::String(token.to_string()),
value: ProgressParamsValue::WorkDone(progress),
}
}
#[test]
fn test_progress_begin_end() {
let mut tracker = ProgressTracker::new();
assert!(!tracker.is_busy());
let begin = make_progress_params(
"indexing",
WorkDoneProgress::Begin(lsp_types::WorkDoneProgressBegin {
title: "Indexing".to_string(),
cancellable: None,
message: Some("src/main.rs".to_string()),
percentage: Some(0),
}),
);
tracker.update(&begin);
assert!(tracker.is_busy());
let primary = tracker.primary_progress().unwrap();
assert_eq!(primary.title, "Indexing");
assert_eq!(primary.message, Some("src/main.rs".to_string()));
assert_eq!(primary.percentage, Some(0));
let end = make_progress_params(
"indexing",
WorkDoneProgress::End(lsp_types::WorkDoneProgressEnd { message: None }),
);
tracker.update(&end);
assert!(!tracker.is_busy());
}
#[test]
fn test_progress_report() {
let mut tracker = ProgressTracker::new();
let begin = make_progress_params(
"indexing",
WorkDoneProgress::Begin(lsp_types::WorkDoneProgressBegin {
title: "Indexing".to_string(),
cancellable: None,
message: None,
percentage: Some(0),
}),
);
tracker.update(&begin);
let report = make_progress_params(
"indexing",
WorkDoneProgress::Report(lsp_types::WorkDoneProgressReport {
cancellable: None,
message: Some("50% done".to_string()),
percentage: Some(50),
}),
);
tracker.update(&report);
let primary = tracker.primary_progress().unwrap();
assert_eq!(primary.percentage, Some(50));
assert_eq!(primary.message, Some("50% done".to_string()));
}
#[test]
fn test_multiple_progress_tokens() {
let mut tracker = ProgressTracker::new();
let begin1 = make_progress_params(
"indexing",
WorkDoneProgress::Begin(lsp_types::WorkDoneProgressBegin {
title: "Indexing".to_string(),
cancellable: None,
message: None,
percentage: Some(50),
}),
);
let begin2 = make_progress_params(
"analyzing",
WorkDoneProgress::Begin(lsp_types::WorkDoneProgressBegin {
title: "Analyzing".to_string(),
cancellable: None,
message: None,
percentage: Some(10),
}),
);
tracker.update(&begin1);
tracker.update(&begin2);
assert!(tracker.is_busy());
let primary = tracker.primary_progress().unwrap();
assert_eq!(primary.title, "Analyzing");
assert_eq!(primary.percentage, Some(10));
let end1 = make_progress_params(
"indexing",
WorkDoneProgress::End(lsp_types::WorkDoneProgressEnd { message: None }),
);
tracker.update(&end1);
assert!(tracker.is_busy());
let primary = tracker.primary_progress().unwrap();
assert_eq!(primary.title, "Analyzing");
}
#[test]
fn test_server_state_conversion() {
assert_eq!(ServerState::from_u8(0), ServerState::Initializing);
assert_eq!(ServerState::from_u8(1), ServerState::Indexing);
assert_eq!(ServerState::from_u8(2), ServerState::Ready);
assert_eq!(ServerState::from_u8(3), ServerState::Dead);
assert_eq!(ServerState::from_u8(99), ServerState::Dead);
assert_eq!(ServerState::Initializing.as_u8(), 0);
assert_eq!(ServerState::Indexing.as_u8(), 1);
assert_eq!(ServerState::Ready.as_u8(), 2);
assert_eq!(ServerState::Dead.as_u8(), 3);
}
}