use helix_core::{diagnostic::Severity, Rope}; use helix_lsp::{lsp, LanguageServerId, OffsetEncoding}; use std::{borrow::Cow, fmt, sync::Arc}; use crate::Range; #[derive(Debug, Eq, Hash, PartialEq, Clone)] pub enum NumberOrString { Number(i32), String(String), } impl NumberOrString { pub fn as_string(&self) -> Cow<'_, str> { match self { Self::Number(n) => Cow::Owned(n.to_string()), Self::String(s) => Cow::Borrowed(s.as_str()), } } } #[derive(Debug, Clone, PartialEq, Eq)] pub enum DiagnosticTag { Unnecessary, Deprecated, } /// The source of a diagnostic. /// /// This type is cheap to clone: all data is either `Copy` or wrapped in an `Arc`. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub enum DiagnosticProvider { Lsp { /// The ID of the language server which sent the diagnostic. server_id: LanguageServerId, /// An optional identifier under which diagnostics are managed by the client. /// /// `identifier` is a field from the LSP "Pull Diagnostics" feature meant to provide an /// optional "namespace" for diagnostics: a language server can respond to a diagnostics /// pull request with an identifier and these diagnostics should be treated as separate /// from push diagnostics. Rust-analyzer uses this feature for example to provide Cargo /// diagnostics with push and internal diagnostics with pull. The push diagnostics should /// not clear the pull diagnostics and vice-versa. identifier: Option>, }, Spelling, } impl DiagnosticProvider { pub fn language_server_id(&self) -> Option { match self { Self::Lsp { server_id, .. } => Some(*server_id), _ => None, } } } #[derive(Clone)] pub struct Diagnostic { pub message: String, pub severity: Option, pub code: Option, pub tags: Vec, pub source: Option, pub range: Range, pub provider: DiagnosticProvider, pub data: Option, } impl fmt::Debug for Diagnostic { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Diagnostic") .field("message", &self.message) .field("severity", &self.severity) .field("code", &self.code) .field("tags", &self.tags) .field("source", &self.source) .field("range", &self.range) .field("provider", &self.provider) .finish_non_exhaustive() } } impl Diagnostic { pub fn lsp( provider: DiagnosticProvider, offset_encoding: OffsetEncoding, diagnostic: lsp::Diagnostic, ) -> Self { let severity = diagnostic.severity.and_then(|severity| match severity { lsp::DiagnosticSeverity::ERROR => Some(Severity::Error), lsp::DiagnosticSeverity::WARNING => Some(Severity::Warning), lsp::DiagnosticSeverity::INFORMATION => Some(Severity::Info), lsp::DiagnosticSeverity::HINT => Some(Severity::Hint), severity => { log::error!("unrecognized diagnostic severity: {:?}", severity); None } }); let code = match diagnostic.code { Some(x) => match x { lsp::NumberOrString::Number(x) => Some(NumberOrString::Number(x)), lsp::NumberOrString::String(x) => Some(NumberOrString::String(x)), }, None => None, }; let tags = if let Some(tags) = diagnostic.tags { tags.into_iter() .filter_map(|tag| match tag { lsp::DiagnosticTag::DEPRECATED => Some(DiagnosticTag::Deprecated), lsp::DiagnosticTag::UNNECESSARY => Some(DiagnosticTag::Unnecessary), _ => None, }) .collect() } else { Vec::new() }; Self { message: diagnostic.message, severity, code, tags, source: diagnostic.source, range: Range::Lsp { range: diagnostic.range, offset_encoding, }, provider, data: diagnostic.data, } } /// Converts the diagnostic to a [lsp::Diagnostic]. pub fn to_lsp_diagnostic( &self, text: &Rope, offset_encoding: OffsetEncoding, ) -> lsp::Diagnostic { let range = match self.range { Range::Document(range) => helix_lsp::util::range_to_lsp_range( text, helix_core::Range::new(range.start, range.end), offset_encoding, ), Range::Lsp { range, .. } => range, }; let severity = self.severity.map(|severity| match severity { Severity::Hint => lsp::DiagnosticSeverity::HINT, Severity::Info => lsp::DiagnosticSeverity::INFORMATION, Severity::Warning => lsp::DiagnosticSeverity::WARNING, Severity::Error => lsp::DiagnosticSeverity::ERROR, }); let code = match self.code.clone() { Some(x) => match x { NumberOrString::Number(x) => Some(lsp::NumberOrString::Number(x)), NumberOrString::String(x) => Some(lsp::NumberOrString::String(x)), }, None => None, }; let new_tags: Vec<_> = self .tags .iter() .map(|tag| match tag { DiagnosticTag::Unnecessary => lsp::DiagnosticTag::UNNECESSARY, DiagnosticTag::Deprecated => lsp::DiagnosticTag::DEPRECATED, }) .collect(); let tags = if !new_tags.is_empty() { Some(new_tags) } else { None }; lsp::Diagnostic { range, severity, code, source: self.source.clone(), message: self.message.clone(), tags, data: self.data.clone(), ..Default::default() } } } impl PartialEq for Diagnostic { fn eq(&self, other: &Self) -> bool { self.message == other.message && self.severity == other.severity && self.code == other.code && self.tags == other.tags && self.source == other.source && self.range == other.range && self.provider == other.provider && self.data == other.data } } impl Eq for Diagnostic {} impl PartialOrd for Diagnostic { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for Diagnostic { fn cmp(&self, other: &Self) -> std::cmp::Ordering { (self.range, self.severity, self.provider.clone()).cmp(&( other.range, other.severity, other.provider.clone(), )) } }