use rust_i18n::t;
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum WarningLevel {
#[default]
None,
Warning,
Error,
}
#[derive(Debug, Clone, Default)]
pub struct WarningPopupContent {
pub title: String,
pub message: String,
pub actions: Vec<WarningAction>,
}
#[derive(Debug, Clone)]
pub struct WarningAction {
pub label: String,
pub action_id: WarningActionId,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WarningActionId {
ViewLog,
Dismiss,
DisableLsp(String),
CopyToClipboard(String),
Custom(String),
}
pub trait WarningDomain: Send + Sync {
fn id(&self) -> &str;
fn label(&self) -> String;
fn level(&self) -> WarningLevel;
fn popup_content(&self) -> WarningPopupContent;
fn has_warnings(&self) -> bool {
self.level() != WarningLevel::None
}
}
#[derive(Debug, Default)]
pub struct GeneralWarningDomain {
pub count: usize,
pub level: WarningLevel,
pub log_path: Option<PathBuf>,
pub last_update: Option<std::time::Instant>,
}
impl GeneralWarningDomain {
pub fn new() -> Self {
Self::default()
}
pub fn add_warnings(&mut self, count: usize) {
self.count = self.count.saturating_add(count);
if self.level == WarningLevel::None {
self.level = WarningLevel::Warning;
}
self.last_update = Some(std::time::Instant::now());
}
pub fn clear(&mut self) {
self.count = 0;
self.level = WarningLevel::None;
self.last_update = None;
}
pub fn set_log_path(&mut self, path: PathBuf) {
self.log_path = Some(path);
}
}
impl WarningDomain for GeneralWarningDomain {
fn id(&self) -> &str {
"general"
}
fn label(&self) -> String {
if self.count > 0 {
format!("[⚠ {}]", self.count)
} else {
String::new()
}
}
fn level(&self) -> WarningLevel {
self.level
}
fn popup_content(&self) -> WarningPopupContent {
let message = if self.count == 1 {
t!("warning.one_logged").to_string()
} else {
t!("warning.many_logged", count = self.count).to_string()
};
let mut actions = vec![WarningAction {
label: t!("warning.dismiss").to_string(),
action_id: WarningActionId::Dismiss,
}];
if self.log_path.is_some() {
actions.insert(
0,
WarningAction {
label: t!("warning.view_log").to_string(),
action_id: WarningActionId::ViewLog,
},
);
}
WarningPopupContent {
title: t!("warning.title").to_string(),
message,
actions,
}
}
fn has_warnings(&self) -> bool {
self.count > 0
}
}
#[derive(Debug, Default)]
pub struct LspWarningDomain {
pub language: Option<String>,
pub server_command: Option<String>,
pub error_message: Option<String>,
pub level: WarningLevel,
}
impl LspWarningDomain {
pub fn new() -> Self {
Self::default()
}
pub fn set_error(&mut self, language: String, server_command: String, message: String) {
self.language = Some(language);
self.server_command = Some(server_command);
self.error_message = Some(message);
self.level = WarningLevel::Error;
}
pub fn clear(&mut self) {
self.language = None;
self.server_command = None;
self.error_message = None;
self.level = WarningLevel::None;
}
pub fn update_from_statuses(
&mut self,
statuses: &std::collections::HashMap<
String,
crate::services::async_bridge::LspServerStatus,
>,
) {
use crate::services::async_bridge::LspServerStatus;
let error_lang = statuses
.iter()
.find(|(_, status)| matches!(status, LspServerStatus::Error))
.map(|(lang, _)| lang.clone());
if let Some(lang) = error_lang {
self.language = Some(lang);
self.level = WarningLevel::Error;
} else {
self.clear();
}
}
}
impl WarningDomain for LspWarningDomain {
fn id(&self) -> &str {
"lsp"
}
fn label(&self) -> String {
String::new()
}
fn level(&self) -> WarningLevel {
self.level
}
fn popup_content(&self) -> WarningPopupContent {
let title = if let Some(lang) = &self.language {
t!("warning.lsp_title", language = lang).to_string()
} else {
t!("warning.lsp_title_default").to_string()
};
let message = if let Some(cmd) = &self.server_command {
t!(
"warning.lsp_server_not_found",
command = cmd,
hint = self.get_install_hint()
)
.to_string()
} else if let Some(err) = &self.error_message {
err.clone()
} else {
t!("warning.lsp_server_error").to_string()
};
let mut actions = vec![WarningAction {
label: t!("warning.dismiss").to_string(),
action_id: WarningActionId::Dismiss,
}];
if let Some(lang) = &self.language {
actions.insert(
0,
WarningAction {
label: t!("warning.disable_lsp", language = lang).to_string(),
action_id: WarningActionId::DisableLsp(lang.clone()),
},
);
}
if let Some(cmd) = self.get_install_command() {
actions.insert(
0,
WarningAction {
label: t!("warning.copy_install_command").to_string(),
action_id: WarningActionId::CopyToClipboard(cmd),
},
);
}
WarningPopupContent {
title,
message,
actions,
}
}
}
impl LspWarningDomain {
fn get_install_hint(&self) -> String {
let cmd = self.server_command.as_deref().unwrap_or("");
match cmd {
"pylsp" => t!("lsp.install_hint.pylsp").to_string(),
"rust-analyzer" => t!("lsp.install_hint.rust_analyzer").to_string(),
"typescript-language-server" => t!("lsp.install_hint.typescript").to_string(),
"gopls" => t!("lsp.install_hint.gopls").to_string(),
"clangd" => t!("lsp.install_hint.clangd").to_string(),
"bash-language-server" => t!("lsp.install_hint.bash").to_string(),
"vscode-html-language-server"
| "vscode-css-language-server"
| "vscode-json-language-server" => t!("lsp.install_hint.vscode").to_string(),
"csharp-ls" => t!("lsp.install_hint.csharp").to_string(),
_ => t!("lsp.install_hint.generic", command = cmd).to_string(),
}
}
fn get_install_command(&self) -> Option<String> {
let cmd = self.server_command.as_deref()?;
match cmd {
"pylsp" => Some("pip install python-lsp-server".to_string()),
"rust-analyzer" => Some("rustup component add rust-analyzer".to_string()),
"typescript-language-server" => {
Some("npm install -g typescript-language-server typescript".to_string())
}
"gopls" => Some("go install golang.org/x/tools/gopls@latest".to_string()),
"bash-language-server" => Some("npm install -g bash-language-server".to_string()),
"clangd" => Some("sudo apt install clangd".to_string()),
"vscode-html-language-server"
| "vscode-css-language-server"
| "vscode-json-language-server" => {
Some("npm install -g vscode-langservers-extracted".to_string())
}
"csharp-ls" => Some("dotnet tool install --global csharp-ls".to_string()),
_ => None,
}
}
}
#[derive(Default)]
pub struct WarningDomainRegistry {
pub general: GeneralWarningDomain,
pub lsp: LspWarningDomain,
}
impl WarningDomainRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn active_domains(&self) -> Vec<&dyn WarningDomain> {
let mut domains: Vec<&dyn WarningDomain> = Vec::new();
if self.lsp.has_warnings() {
domains.push(&self.lsp);
}
if self.general.has_warnings() {
domains.push(&self.general);
}
domains
}
pub fn highest_level(&self) -> WarningLevel {
if self.lsp.level() == WarningLevel::Error || self.general.level() == WarningLevel::Error {
WarningLevel::Error
} else if self.lsp.level() == WarningLevel::Warning
|| self.general.level() == WarningLevel::Warning
{
WarningLevel::Warning
} else {
WarningLevel::None
}
}
pub fn has_any_warnings(&self) -> bool {
self.lsp.has_warnings() || self.general.has_warnings()
}
}