Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-view/src/diagnostic.rs')
| -rw-r--r-- | helix-view/src/diagnostic.rs | 218 |
1 files changed, 218 insertions, 0 deletions
diff --git a/helix-view/src/diagnostic.rs b/helix-view/src/diagnostic.rs new file mode 100644 index 00000000..414eb202 --- /dev/null +++ b/helix-view/src/diagnostic.rs @@ -0,0 +1,218 @@ +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<Arc<str>>, + }, + // Future internal features can go here... +} + +impl DiagnosticProvider { + pub fn language_server_id(&self) -> Option<LanguageServerId> { + match self { + Self::Lsp { server_id, .. } => Some(*server_id), + // _ => None, + } + } +} + +#[derive(Clone)] +pub struct Diagnostic { + pub message: String, + pub severity: Option<Severity>, + pub code: Option<NumberOrString>, + pub tags: Vec<DiagnosticTag>, + pub source: Option<String>, + pub range: Range, + pub provider: DiagnosticProvider, + pub data: Option<serde_json::Value>, +} + +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<std::cmp::Ordering> { + 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(), + )) + } +} |