diff --git a/helix-core/src/diagnostic.rs b/helix-core/src/diagnostic.rs index b9360b525..d4bc94699 100644 --- a/helix-core/src/diagnostic.rs +++ b/helix-core/src/diagnostic.rs @@ -1,7 +1,6 @@ //! LSP diagnostic utility types. -use std::{fmt, sync::Arc}; +use std::fmt; -pub use helix_stdx::range::Range; use serde::{Deserialize, Serialize}; /// Describes the severity level of a [`Diagnostic`]. @@ -20,66 +19,6 @@ impl Default for Severity { } } -#[derive(Debug, Eq, Hash, PartialEq, Clone, Deserialize, Serialize)] -pub enum NumberOrString { - Number(i32), - String(String), -} - -#[derive(Debug, Clone)] -pub enum DiagnosticTag { - Unnecessary, - Deprecated, -} - -/// Corresponds to [`lsp_types::Diagnostic`](https://docs.rs/lsp-types/0.94.0/lsp_types/struct.Diagnostic.html) -#[derive(Debug, Clone)] -pub struct Diagnostic { - pub range: Range, - // whether this diagnostic ends at the end of(or inside) a word - pub ends_at_word: bool, - pub starts_at_word: bool, - pub zero_width: bool, - pub line: usize, - pub message: String, - pub severity: Option, - pub code: Option, - pub provider: DiagnosticProvider, - pub tags: Vec, - pub source: Option, - pub data: Option, -} - -/// 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>, - }, - // Future internal features can go here... -} - -impl DiagnosticProvider { - pub fn language_server_id(&self) -> Option { - match self { - Self::Lsp { server_id, .. } => Some(*server_id), - // _ => None, - } - } -} - // while I would prefer having this in helix-lsp that necessitates a bunch of // conversions I would rather not add. I think its fine since this just a very // trivial newtype wrapper and we would need something similar once we define @@ -93,10 +32,3 @@ impl fmt::Display for LanguageServerId { write!(f, "{:?}", self.0) } } - -impl Diagnostic { - #[inline] - pub fn severity(&self) -> Severity { - self.severity.unwrap_or(Severity::Warning) - } -} diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index 3fcddfcd1..22ec1d653 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -67,7 +67,6 @@ pub use smallvec::{smallvec, SmallVec}; pub use syntax::Syntax; pub use completion::CompletionItem; -pub use diagnostic::Diagnostic; pub use line_ending::{LineEnding, NATIVE_LINE_ENDING}; pub use transaction::{Assoc, Change, ChangeSet, Deletion, Operation, Transaction}; diff --git a/helix-lsp-types/src/lib.rs b/helix-lsp-types/src/lib.rs index 41c483f42..6674b84b6 100644 --- a/helix-lsp-types/src/lib.rs +++ b/helix-lsp-types/src/lib.rs @@ -260,7 +260,9 @@ impl Position { /// A range in a text document expressed as (zero-based) start and end positions. /// A range is comparable to a selection in an editor. Therefore the end position is exclusive. -#[derive(Debug, Eq, PartialEq, Copy, Clone, Default, Deserialize, Serialize, Hash)] +#[derive( + Debug, Eq, PartialEq, PartialOrd, Ord, Copy, Clone, Default, Deserialize, Serialize, Hash, +)] pub struct Range { /// The range's start position. pub start: Position, diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index ba41cbc5a..921f0386e 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -52,7 +52,7 @@ pub enum Error { Other(#[from] anyhow::Error), } -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] pub enum OffsetEncoding { /// UTF-8 code units aka bytes Utf8, @@ -68,63 +68,7 @@ pub mod util { use helix_core::line_ending::{line_end_byte_index, line_end_char_index}; use helix_core::snippets::{RenderedSnippet, Snippet, SnippetRenderCtx}; use helix_core::{chars, RopeSlice}; - use helix_core::{diagnostic::NumberOrString, Range, Rope, Selection, Tendril, Transaction}; - - /// Converts a diagnostic in the document to [`lsp::Diagnostic`]. - /// - /// Panics when [`pos_to_lsp_pos`] would for an invalid range on the diagnostic. - pub fn diagnostic_to_lsp_diagnostic( - doc: &Rope, - diag: &helix_core::diagnostic::Diagnostic, - offset_encoding: OffsetEncoding, - ) -> lsp::Diagnostic { - use helix_core::diagnostic::Severity::*; - - let range = Range::new(diag.range.start, diag.range.end); - let severity = diag.severity.map(|s| match s { - Hint => lsp::DiagnosticSeverity::HINT, - Info => lsp::DiagnosticSeverity::INFORMATION, - Warning => lsp::DiagnosticSeverity::WARNING, - Error => lsp::DiagnosticSeverity::ERROR, - }); - - let code = match diag.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<_> = diag - .tags - .iter() - .map(|tag| match tag { - helix_core::diagnostic::DiagnosticTag::Unnecessary => { - lsp::DiagnosticTag::UNNECESSARY - } - helix_core::diagnostic::DiagnosticTag::Deprecated => lsp::DiagnosticTag::DEPRECATED, - }) - .collect(); - - let tags = if !new_tags.is_empty() { - Some(new_tags) - } else { - None - }; - - lsp::Diagnostic { - range: range_to_lsp_range(doc, range, offset_encoding), - severity, - code, - source: diag.source.clone(), - message: diag.message.to_owned(), - related_information: None, - tags, - data: diag.data.to_owned(), - ..Default::default() - } - } + use helix_core::{Range, Rope, Selection, Tendril, Transaction}; /// Converts [`lsp::Position`] to a position in the document. /// diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 3bc324395..4eb850861 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -722,16 +722,23 @@ impl Application { log::error!("Discarding publishDiagnostic notification sent by an uninitialized server: {}", language_server.name()); return; } - let provider = helix_core::diagnostic::DiagnosticProvider::Lsp { + let provider = helix_view::diagnostic::DiagnosticProvider::Lsp { server_id, identifier: None, }; - self.editor.handle_lsp_diagnostics( - &provider, - uri, - params.version, - params.diagnostics, - ); + let diagnostics = params + .diagnostics + .into_iter() + .map(|diagnostic| { + helix_view::Diagnostic::lsp( + provider.clone(), + language_server.offset_encoding(), + diagnostic, + ) + }) + .collect(); + self.editor + .handle_diagnostics(&provider, uri, params.version, diagnostics); } Notification::ShowMessage(params) => { if self.config.load().editor.lsp.display_messages { @@ -840,8 +847,8 @@ impl Application { // we need to clear those and remove the entries from the list if this leads to // an empty diagnostic list for said files for diags in self.editor.diagnostics.values_mut() { - diags.retain(|(_, provider)| { - provider.language_server_id() != Some(server_id) + diags.retain(|diag| { + diag.provider.language_server_id() != Some(server_id) }); } diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index be8081854..719387d7f 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -1,9 +1,6 @@ use futures_util::{stream::FuturesOrdered, FutureExt}; use helix_lsp::{ - block_on, - lsp::{self, DiagnosticSeverity, NumberOrString}, - util::lsp_range_to_range, - Client, LanguageServerId, OffsetEncoding, + block_on, lsp, util::lsp_range_to_range, Client, LanguageServerId, OffsetEncoding, }; use tokio_stream::StreamExt; use tui::text::Span; @@ -11,8 +8,7 @@ use tui::text::Span; use super::{align_view, push_jump, Align, Context, Editor}; use helix_core::{ - diagnostic::DiagnosticProvider, syntax::LanguageServerFeature, - text_annotations::InlineAnnotation, Selection, Uri, + syntax::LanguageServerFeature, text_annotations::InlineAnnotation, Selection, Uri, }; use helix_stdx::path; use helix_view::{ @@ -20,7 +16,7 @@ use helix_view::{ editor::Action, handlers::lsp::SignatureHelpInvoked, theme::Style, - Document, View, + Diagnostic, Document, DocumentId, View, }; use crate::{ @@ -29,7 +25,7 @@ use crate::{ ui::{self, overlay::overlaid, FileLocation, Picker, Popup, PromptEvent}, }; -use std::{collections::HashSet, fmt::Display, future::Future, path::Path}; +use std::{collections::HashSet, fmt::Display, future::Future}; /// Gets the first language server that is attached to a document which supports a specific feature. /// If there is no configured language server that supports the feature, this displays a status message. @@ -53,31 +49,48 @@ macro_rules! language_server_with_feature { }}; } -/// A wrapper around `lsp::Location` that swaps out the LSP URI for `helix_core::Uri` and adds -/// the server's offset encoding. +/// A wrapper around `lsp::Location`. #[derive(Debug, Clone, PartialEq, Eq)] -struct Location { +pub struct Location { uri: Uri, - range: lsp::Range, - offset_encoding: OffsetEncoding, + range: helix_view::Range, } -fn lsp_location_to_location( - location: lsp::Location, - offset_encoding: OffsetEncoding, -) -> Option { - let uri = match location.uri.try_into() { - Ok(uri) => uri, - Err(err) => { - log::warn!("discarding invalid or unsupported URI: {err}"); - return None; - } - }; - Some(Location { - uri, - range: location.range, - offset_encoding, - }) +impl Location { + fn lsp(location: lsp::Location, offset_encoding: OffsetEncoding) -> Option { + let uri = match location.uri.try_into() { + Ok(uri) => uri, + Err(err) => { + log::warn!("discarding invalid or unsupported URI: {err}"); + return None; + } + }; + Some(Self { + uri, + range: helix_view::Range::Lsp { + range: location.range, + offset_encoding, + }, + }) + } + + fn file_location<'a>(&'a self, editor: &Editor) -> Option> { + let (path_or_id, doc) = match &self.uri { + Uri::File(path) => ((&**path).into(), None), + Uri::Scratch(doc_id) => ((*doc_id).into(), editor.documents.get(doc_id)), + _ => return None, + }; + let lines = match self.range { + helix_view::Range::Lsp { range, .. } => { + Some((range.start.line as usize, range.end.line as usize)) + } + helix_view::Range::Document(range) => doc.map(|doc| { + let text = doc.text().slice(..); + (text.char_to_line(range.start), text.char_to_line(range.end)) + }), + }; + Some((path_or_id, lines)) + } } struct SymbolInformationItem { @@ -94,63 +107,57 @@ struct DiagnosticStyles { struct PickerDiagnostic { location: Location, - diag: lsp::Diagnostic, -} - -fn location_to_file_location(location: &Location) -> Option { - let path = location.uri.as_path()?; - let line = Some(( - location.range.start.line as usize, - location.range.end.line as usize, - )); - Some((path.into(), line)) + diag: Diagnostic, } fn jump_to_location(editor: &mut Editor, location: &Location, action: Action) { let (view, doc) = current!(editor); push_jump(view, doc); - let Some(path) = location.uri.as_path() else { - let err = format!("unable to convert URI to filepath: {:?}", location.uri); - editor.set_error(err); - return; + let doc_id = match &location.uri { + Uri::Scratch(doc_id) => { + editor.switch(*doc_id, action); + *doc_id + } + Uri::File(path) => match editor.open(path, action) { + Ok(doc_id) => doc_id, + Err(err) => { + editor.set_error(format!("failed to open path: {:?}: {:?}", path, err)); + return; + } + }, + _ => return, }; - jump_to_position( - editor, - path, - location.range, - location.offset_encoding, - action, - ); + + jump_to_position(editor, doc_id, location.range, action); } fn jump_to_position( editor: &mut Editor, - path: &Path, - range: lsp::Range, - offset_encoding: OffsetEncoding, + doc_id: DocumentId, + range: helix_view::Range, action: Action, ) { - let doc = match editor.open(path, action) { - Ok(id) => doc_mut!(editor, &id), - Err(err) => { - let err = format!("failed to open path: {:?}: {:?}", path, err); - editor.set_error(err); - return; - } + let Some(doc) = editor.documents.get_mut(&doc_id) else { + return; }; let view = view_mut!(editor); - // TODO: convert inside server - let new_range = if let Some(new_range) = lsp_range_to_range(doc.text(), range, offset_encoding) - { - new_range - } else { - log::warn!("lsp position out of bounds - {:?}", range); - return; + let selection = match range { + helix_view::Range::Lsp { + range, + offset_encoding, + } => { + let Some(range) = lsp_range_to_range(doc.text(), range, offset_encoding) else { + log::warn!("lsp position out of bounds - {:?}", range); + return; + }; + range.into() + } + helix_view::Range::Document(range) => Selection::single(range.start, range.end), }; // we flip the range so that the cursor sits on the start of the symbol // (for example start of the function). - doc.set_selection(view.id, Selection::single(new_range.head, new_range.anchor)); + doc.set_selection(view.id, selection); if action.align_view(view, doc.id()) { align_view(doc, view, Align::Center); } @@ -201,30 +208,22 @@ type DiagnosticsPicker = Picker; fn diag_picker( cx: &Context, - diagnostics: impl IntoIterator)>, + diagnostics: impl IntoIterator)>, format: DiagnosticsFormat, ) -> DiagnosticsPicker { - // TODO: drop current_path comparison and instead use workspace: bool flag? - // flatten the map to a vec of (url, diag) pairs let mut flat_diag = Vec::new(); for (uri, diags) in diagnostics { flat_diag.reserve(diags.len()); - for (diag, provider) in diags { - if let Some(ls) = provider - .language_server_id() - .and_then(|id| cx.editor.language_server_by_id(id)) - { - flat_diag.push(PickerDiagnostic { - location: Location { - uri: uri.clone(), - range: diag.range, - offset_encoding: ls.offset_encoding(), - }, - diag, - }); - } + for diag in diags { + flat_diag.push(PickerDiagnostic { + location: Location { + uri: uri.clone(), + range: diag.range, + }, + diag, + }); } } @@ -239,22 +238,24 @@ fn diag_picker( ui::PickerColumn::new( "severity", |item: &PickerDiagnostic, styles: &DiagnosticStyles| { + use helix_core::diagnostic::Severity::*; match item.diag.severity { - Some(DiagnosticSeverity::HINT) => Span::styled("HINT", styles.hint), - Some(DiagnosticSeverity::INFORMATION) => Span::styled("INFO", styles.info), - Some(DiagnosticSeverity::WARNING) => Span::styled("WARN", styles.warning), - Some(DiagnosticSeverity::ERROR) => Span::styled("ERROR", styles.error), + Some(Hint) => Span::styled("HINT", styles.hint), + Some(Info) => Span::styled("INFO", styles.info), + Some(Warning) => Span::styled("WARN", styles.warning), + Some(Error) => Span::styled("ERROR", styles.error), _ => Span::raw(""), } .into() }, ), ui::PickerColumn::new("code", |item: &PickerDiagnostic, _| { - match item.diag.code.as_ref() { - Some(NumberOrString::Number(n)) => n.to_string().into(), - Some(NumberOrString::String(s)) => s.as_str().into(), - None => "".into(), - } + item.diag + .code + .as_ref() + .map(|c| c.as_string()) + .unwrap_or_default() + .into() }), ui::PickerColumn::new("message", |item: &PickerDiagnostic, _| { item.diag.message.as_str().into() @@ -292,7 +293,7 @@ fn diag_picker( .immediately_show_diagnostic(doc, view.id); }, ) - .with_preview(move |_editor, diag| location_to_file_location(&diag.location)) + .with_preview(|editor, diag| diag.location.file_location(editor)) .truncate_start(false) } @@ -316,8 +317,10 @@ pub fn symbol_picker(cx: &mut Context) { }, location: Location { uri: uri.clone(), - range: symbol.selection_range, - offset_encoding, + range: helix_view::Range::Lsp { + range: symbol.selection_range, + offset_encoding, + }, }, }); for child in symbol.children.into_iter().flatten() { @@ -350,8 +353,10 @@ pub fn symbol_picker(cx: &mut Context) { .map(|symbol| SymbolInformationItem { location: Location { uri: doc_uri.clone(), - range: symbol.location.range, - offset_encoding, + range: helix_view::Range::Lsp { + range: symbol.location.range, + offset_encoding, + }, }, symbol, }) @@ -418,7 +423,7 @@ pub fn symbol_picker(cx: &mut Context) { jump_to_location(cx.editor, &item.location, action); }, ) - .with_preview(move |_editor, item| location_to_file_location(&item.location)) + .with_preview(|editor, item| item.location.file_location(editor)) .truncate_start(false); compositor.push(Box::new(overlaid(picker))) @@ -475,8 +480,10 @@ pub fn workspace_symbol_picker(cx: &mut Context) { Some(SymbolInformationItem { location: Location { uri, - range: symbol.location.range, - offset_encoding, + range: helix_view::Range::Lsp { + range: symbol.location.range, + offset_encoding, + }, }, symbol, }) @@ -544,7 +551,7 @@ pub fn workspace_symbol_picker(cx: &mut Context) { jump_to_location(cx.editor, &item.location, action); }, ) - .with_preview(|_editor, item| location_to_file_location(&item.location)) + .with_preview(|editor, item| item.location.file_location(editor)) .with_dynamic_query(get_symbols, None) .truncate_start(false); @@ -606,20 +613,26 @@ fn goto_impl(editor: &mut Editor, compositor: &mut Compositor, locations: Vec match response { Some(lsp::GotoDefinitionResponse::Scalar(lsp_location)) => { - locations.extend(lsp_location_to_location(lsp_location, offset_encoding)); + locations.extend(Location::lsp(lsp_location, offset_encoding)); } Some(lsp::GotoDefinitionResponse::Array(lsp_locations)) => { - locations.extend(lsp_locations.into_iter().flat_map(|location| { - lsp_location_to_location(location, offset_encoding) - })); + locations.extend( + lsp_locations + .into_iter() + .flat_map(|location| Location::lsp(location, offset_encoding)), + ); } Some(lsp::GotoDefinitionResponse::Link(lsp_locations)) => { locations.extend( @@ -664,9 +679,7 @@ where location_link.target_range, ) }) - .flat_map(|location| { - lsp_location_to_location(location, offset_encoding) - }), + .flat_map(|location| Location::lsp(location, offset_encoding)), ); } None => (), @@ -746,7 +759,7 @@ pub fn goto_reference(cx: &mut Context) { lsp_locations .into_iter() .flatten() - .flat_map(|location| lsp_location_to_location(location, offset_encoding)), + .flat_map(|location| Location::lsp(location, offset_encoding)), ), Err(err) => log::error!("Error requesting references: {err}"), } diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index e1c09a04d..94896295a 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -2491,7 +2491,7 @@ fn yank_diagnostic( .diagnostics() .iter() .filter(|d| primary.overlaps(&helix_core::Range::new(d.range.start, d.range.end))) - .map(|d| d.message.clone()) + .map(|d| d.inner.message.clone()) .collect(); let n = diag.len(); if n == 0 { diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 6be565747..593195c04 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -14,7 +14,6 @@ use crate::{ }; use helix_core::{ - diagnostic::NumberOrString, graphemes::{next_grapheme_boundary, prev_grapheme_boundary}, movement::Direction, syntax::{self, HighlightEvent}, @@ -360,7 +359,9 @@ impl EditorView { doc: &Document, theme: &Theme, ) -> [Vec<(usize, std::ops::Range)>; 7] { - use helix_core::diagnostic::{DiagnosticTag, Range, Severity}; + use helix_core::diagnostic::Severity; + use helix_stdx::Range; + use helix_view::diagnostic::DiagnosticTag; let get_scope_of = |scope| { theme .find_scope_index_exact(scope) @@ -411,7 +412,7 @@ impl EditorView { for diagnostic in doc.diagnostics() { // Separate diagnostics into different Vecs by severity. - let (vec, scope) = match diagnostic.severity { + let (vec, scope) = match diagnostic.inner.severity { Some(Severity::Info) => (&mut info_vec, info), Some(Severity::Hint) => (&mut hint_vec, hint), Some(Severity::Warning) => (&mut warning_vec, warning), @@ -423,16 +424,16 @@ impl EditorView { // the diagnostic as info/hint/default and only render it as unnecessary/deprecated // instead. For warning/error diagnostics, render both the severity highlight and // the tag highlight. - if diagnostic.tags.is_empty() + if diagnostic.inner.tags.is_empty() || matches!( - diagnostic.severity, + diagnostic.inner.severity, Some(Severity::Warning | Severity::Error) ) { push_diagnostic(vec, scope, diagnostic.range); } - for tag in &diagnostic.tags { + for tag in &diagnostic.inner.tags { match tag { DiagnosticTag::Unnecessary => { if let Some(scope) = unnecessary { @@ -734,6 +735,7 @@ impl EditorView { theme: &Theme, ) { use helix_core::diagnostic::Severity; + use helix_view::diagnostic::NumberOrString; use tui::{ layout::Alignment, text::Text, @@ -757,17 +759,18 @@ impl EditorView { let mut lines = Vec::new(); let background_style = theme.get("ui.background"); for diagnostic in diagnostics { - let style = Style::reset() - .patch(background_style) - .patch(match diagnostic.severity { - Some(Severity::Error) => error, - Some(Severity::Warning) | None => warning, - Some(Severity::Info) => info, - Some(Severity::Hint) => hint, - }); - let text = Text::styled(&diagnostic.message, style); + let style = + Style::reset() + .patch(background_style) + .patch(match diagnostic.inner.severity { + Some(Severity::Error) => error, + Some(Severity::Warning) | None => warning, + Some(Severity::Info) => info, + Some(Severity::Hint) => hint, + }); + let text = Text::styled(&diagnostic.inner.message, style); lines.extend(text.lines); - let code = diagnostic.code.as_ref().map(|x| match x { + let code = diagnostic.inner.code.as_ref().map(|x| match x { NumberOrString::Number(n) => format!("({n})"), NumberOrString::String(s) => format!("({s})"), }); diff --git a/helix-term/src/ui/statusline.rs b/helix-term/src/ui/statusline.rs index 7437cbd07..fa3eac950 100644 --- a/helix-term/src/ui/statusline.rs +++ b/helix-term/src/ui/statusline.rs @@ -1,5 +1,4 @@ -use helix_core::{coords_at_pos, encoding, Position}; -use helix_lsp::lsp::DiagnosticSeverity; +use helix_core::{coords_at_pos, diagnostic::Severity, encoding, Position}; use helix_view::document::DEFAULT_LANGUAGE_NAME; use helix_view::{ document::{Mode, SCRATCH_BUFFER_NAME}, @@ -231,8 +230,7 @@ where .diagnostics() .iter() .fold((0, 0), |mut counts, diag| { - use helix_core::diagnostic::Severity; - match diag.severity { + match diag.inner.severity { Some(Severity::Warning) => counts.0 += 1, Some(Severity::Error) | None => counts.1 += 1, _ => {} @@ -269,10 +267,10 @@ where .diagnostics .values() .flatten() - .fold((0, 0), |mut counts, (diag, _)| { + .fold((0, 0), |mut counts, diag| { match diag.severity { - Some(DiagnosticSeverity::WARNING) => counts.0 += 1, - Some(DiagnosticSeverity::ERROR) | None => counts.1 += 1, + Some(Severity::Warning) => counts.0 += 1, + Some(Severity::Error) | None => counts.1 += 1, _ => {} } counts diff --git a/helix-term/src/ui/text_decorations/diagnostics.rs b/helix-term/src/ui/text_decorations/diagnostics.rs index fb82bcf54..c560be931 100644 --- a/helix-term/src/ui/text_decorations/diagnostics.rs +++ b/helix-term/src/ui/text_decorations/diagnostics.rs @@ -4,13 +4,13 @@ use helix_core::diagnostic::Severity; use helix_core::doc_formatter::{DocumentFormatter, FormattedGrapheme}; use helix_core::graphemes::Grapheme; use helix_core::text_annotations::TextAnnotations; -use helix_core::{Diagnostic, Position}; +use helix_core::Position; use helix_view::annotations::diagnostics::{ DiagnosticFilter, InlineDiagnosticAccumulator, InlineDiagnosticsConfig, }; use helix_view::theme::Style; -use helix_view::{Document, Theme}; +use helix_view::{document::Diagnostic, Document, Theme}; use crate::ui::document::{LinePos, TextRenderer}; use crate::ui::text_decorations::Decoration; @@ -102,7 +102,7 @@ impl Renderer<'_, '_> { let mut end_col = start_col; let mut draw_col = (col + 1) as u16; - for line in diag.message.lines() { + for line in diag.inner.message.lines() { if !self.renderer.column_in_bounds(draw_col as usize, 1) { break; } @@ -139,7 +139,7 @@ impl Renderer<'_, '_> { let text_fmt = self.config.text_fmt(text_col, self.renderer.viewport.width); let annotations = TextAnnotations::default(); let formatter = DocumentFormatter::new_at_prev_checkpoint( - diag.message.as_str().trim().into(), + diag.inner.message.as_str().trim().into(), &text_fmt, &annotations, 0, @@ -262,9 +262,9 @@ impl Decoration for InlineDiagnostics<'_> { match filter { DiagnosticFilter::Enable(filter) => eol_diganogistcs .filter(|(diag, _)| filter > diag.severity()) - .max_by_key(|(diagnostic, _)| diagnostic.severity), + .max_by_key(|(diagnostic, _)| diagnostic.inner.severity), DiagnosticFilter::Disable => { - eol_diganogistcs.max_by_key(|(diagnostic, _)| diagnostic.severity) + eol_diganogistcs.max_by_key(|(diagnostic, _)| diagnostic.inner.severity) } } } diff --git a/helix-view/src/action.rs b/helix-view/src/action.rs index 3c6d99984..5fc57bfc4 100644 --- a/helix-view/src/action.rs +++ b/helix-view/src/action.rs @@ -2,11 +2,7 @@ use std::{borrow::Cow, collections::HashSet, fmt, future::Future}; use futures_util::{stream::FuturesOrdered, FutureExt as _}; use helix_core::syntax::LanguageServerFeature; -use helix_lsp::{ - lsp, - util::{diagnostic_to_lsp_diagnostic, range_to_lsp_range}, - LanguageServerId, -}; +use helix_lsp::{lsp, util::range_to_lsp_range, LanguageServerId}; use tokio_stream::StreamExt as _; use crate::Editor; @@ -179,13 +175,13 @@ impl Editor { .diagnostics() .iter() .filter(|&diag| { - diag.provider.language_server_id() == Some(language_server_id) + diag.inner.provider.language_server_id() == Some(language_server_id) && selection.overlaps(&helix_core::Range::new( diag.range.start, diag.range.end, )) }) - .map(|diag| diagnostic_to_lsp_diagnostic(doc.text(), diag, offset_encoding)) + .map(|diag| diag.inner.to_lsp_diagnostic(doc.text(), offset_encoding)) .collect(), only: None, trigger_kind: Some(lsp::CodeActionTriggerKind::INVOKED), diff --git a/helix-view/src/annotations/diagnostics.rs b/helix-view/src/annotations/diagnostics.rs index 7802ca637..2f698c4e3 100644 --- a/helix-view/src/annotations/diagnostics.rs +++ b/helix-view/src/annotations/diagnostics.rs @@ -1,10 +1,10 @@ use helix_core::diagnostic::Severity; use helix_core::doc_formatter::{FormattedGrapheme, TextFormat}; use helix_core::text_annotations::LineAnnotation; -use helix_core::{softwrapped_dimensions, Diagnostic, Position}; +use helix_core::{softwrapped_dimensions, Position}; use serde::{Deserialize, Serialize}; -use crate::Document; +use crate::{document::Diagnostic, Document}; /// Describes the severity level of a [`Diagnostic`]. #[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord)] @@ -305,7 +305,7 @@ impl LineAnnotation for InlineDiagnostics<'_> { .drain(..) .map(|(diag, anchor)| { let text_fmt = self.state.config.text_fmt(anchor, self.width); - softwrapped_dimensions(diag.message.as_str().trim().into(), &text_fmt).0 + softwrapped_dimensions(diag.inner.message.as_str().trim().into(), &text_fmt).0 }) .sum(); Position::new(multi as usize + diagostic_height, 0) diff --git a/helix-view/src/diagnostic.rs b/helix-view/src/diagnostic.rs new file mode 100644 index 000000000..414eb2023 --- /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>, + }, + // Future internal features can go here... +} + +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(), + )) + } +} diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 00bdc8732..6ea31cd9f 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -4,15 +4,14 @@ use arc_swap::ArcSwap; use futures_util::future::BoxFuture; use futures_util::FutureExt; use helix_core::auto_pairs::AutoPairs; -use helix_core::chars::char_is_word; -use helix_core::diagnostic::DiagnosticProvider; +use helix_core::diagnostic::Severity; use helix_core::doc_formatter::TextFormat; use helix_core::encoding::Encoding; use helix_core::snippets::{ActiveSnippet, SnippetRenderCtx}; use helix_core::syntax::{Highlight, LanguageServerFeature}; use helix_core::text_annotations::{InlineAnnotation, Overlay}; +use helix_core::RopeSlice; use helix_event::TaskController; -use helix_lsp::util::lsp_pos_to_pos; use helix_stdx::faccess::{copy_metadata, readonly}; use helix_vcs::{DiffHandle, DiffProviderRegistry}; use once_cell::sync::OnceCell; @@ -39,10 +38,11 @@ use helix_core::{ indent::{auto_detect_indent_style, IndentStyle}, line_ending::auto_detect_line_ending, syntax::{self, LanguageConfiguration}, - ChangeSet, Diagnostic, LineEnding, Range, Rope, RopeBuilder, Selection, Syntax, Transaction, + ChangeSet, LineEnding, Range, Rope, RopeBuilder, Selection, Syntax, Transaction, }; use crate::{ + diagnostic::DiagnosticProvider, editor::Config, events::{DocumentDidChange, SelectionDidChange}, view::ViewPosition, @@ -1421,45 +1421,7 @@ impl Document { diff_handle.update_document(self.text.clone(), false); } - // map diagnostics over changes too - changes.update_positions(self.diagnostics.iter_mut().map(|diagnostic| { - let assoc = if diagnostic.starts_at_word { - Assoc::BeforeWord - } else { - Assoc::After - }; - (&mut diagnostic.range.start, assoc) - })); - changes.update_positions(self.diagnostics.iter_mut().filter_map(|diagnostic| { - if diagnostic.zero_width { - // for zero width diagnostics treat the diagnostic as a point - // rather than a range - return None; - } - let assoc = if diagnostic.ends_at_word { - Assoc::AfterWord - } else { - Assoc::Before - }; - Some((&mut diagnostic.range.end, assoc)) - })); - self.diagnostics.retain_mut(|diagnostic| { - if diagnostic.zero_width { - diagnostic.range.end = diagnostic.range.start - } else if diagnostic.range.start >= diagnostic.range.end { - return false; - } - diagnostic.line = self.text.char_to_line(diagnostic.range.start); - true - }); - - self.diagnostics.sort_by_key(|diagnostic| { - ( - diagnostic.range, - diagnostic.severity, - diagnostic.provider.clone(), - ) - }); + Diagnostic::apply_changes(&mut self.diagnostics, changes, self.text.slice(..)); // Update the inlay hint annotations' positions, helping ensure they are displayed in the proper place let apply_inlay_hint_changes = |annotations: &mut Vec| { @@ -1991,94 +1953,6 @@ impl Document { ) } - pub fn lsp_diagnostic_to_diagnostic( - text: &Rope, - language_config: Option<&LanguageConfiguration>, - diagnostic: &helix_lsp::lsp::Diagnostic, - provider: DiagnosticProvider, - offset_encoding: helix_lsp::OffsetEncoding, - ) -> Option { - use helix_core::diagnostic::{Range, Severity::*}; - - // TODO: convert inside server - let start = - if let Some(start) = lsp_pos_to_pos(text, diagnostic.range.start, offset_encoding) { - start - } else { - log::warn!("lsp position out of bounds - {:?}", diagnostic); - return None; - }; - - let end = if let Some(end) = lsp_pos_to_pos(text, diagnostic.range.end, offset_encoding) { - end - } else { - log::warn!("lsp position out of bounds - {:?}", diagnostic); - return None; - }; - - let severity = diagnostic.severity.and_then(|severity| match severity { - lsp::DiagnosticSeverity::ERROR => Some(Error), - lsp::DiagnosticSeverity::WARNING => Some(Warning), - lsp::DiagnosticSeverity::INFORMATION => Some(Info), - lsp::DiagnosticSeverity::HINT => Some(Hint), - severity => { - log::error!("unrecognized diagnostic severity: {:?}", severity); - None - } - }); - - if let Some(lang_conf) = language_config { - if let Some(severity) = severity { - if severity < lang_conf.diagnostic_severity { - return None; - } - } - }; - use helix_core::diagnostic::{DiagnosticTag, NumberOrString}; - - let code = match diagnostic.code.clone() { - 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 { - let new_tags = tags - .iter() - .filter_map(|tag| match *tag { - lsp::DiagnosticTag::DEPRECATED => Some(DiagnosticTag::Deprecated), - lsp::DiagnosticTag::UNNECESSARY => Some(DiagnosticTag::Unnecessary), - _ => None, - }) - .collect(); - - new_tags - } else { - Vec::new() - }; - - let ends_at_word = - start != end && end != 0 && text.get_char(end - 1).is_some_and(char_is_word); - let starts_at_word = start != end && text.get_char(start).is_some_and(char_is_word); - - Some(Diagnostic { - range: Range { start, end }, - ends_at_word, - starts_at_word, - zero_width: start == end, - line: diagnostic.range.start.line as usize, - message: diagnostic.message.clone(), - severity, - code, - tags, - source: diagnostic.source.clone(), - data: diagnostic.data.clone(), - provider, - }) - } - #[inline] pub fn diagnostics(&self) -> &[Diagnostic] { &self.diagnostics @@ -2093,17 +1967,17 @@ impl Document { if unchanged_sources.is_empty() { if let Some(provider) = provider { self.diagnostics - .retain(|diagnostic| &diagnostic.provider != provider); + .retain(|diagnostic| &diagnostic.inner.provider != provider); } else { self.diagnostics.clear(); } } else { self.diagnostics.retain(|d| { - if provider.is_some_and(|provider| provider != &d.provider) { + if provider.is_some_and(|provider| provider != &d.inner.provider) { return true; } - if let Some(source) = &d.source { + if let Some(source) = &d.inner.source { unchanged_sources.contains(source) } else { false @@ -2111,19 +1985,13 @@ impl Document { }); } self.diagnostics.extend(diagnostics); - self.diagnostics.sort_by_key(|diagnostic| { - ( - diagnostic.range, - diagnostic.severity, - diagnostic.provider.clone(), - ) - }); + self.diagnostics.sort(); } - /// clears diagnostics for a given language server id if set, otherwise all diagnostics are cleared + /// clears diagnostics for a given language server id pub fn clear_diagnostics_for_language_server(&mut self, id: LanguageServerId) { self.diagnostics - .retain(|d| d.provider.language_server_id() != Some(id)); + .retain(|d| d.inner.provider.language_server_id() != Some(id)); } /// Get the document's auto pairs. If the document has a recognized @@ -2288,6 +2156,94 @@ impl Display for FormatterError { } } +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct Diagnostic { + pub inner: crate::Diagnostic, + pub range: helix_stdx::Range, + pub line: usize, + ends_at_word: bool, + starts_at_word: bool, + zero_width: bool, +} + +impl Diagnostic { + #[inline] + pub fn severity(&self) -> Severity { + self.inner.severity.unwrap_or(Severity::Warning) + } + + fn apply_changes(diagnostics: &mut Vec, changes: &ChangeSet, text: RopeSlice) { + use helix_core::Assoc; + + changes.update_positions(diagnostics.iter_mut().map(|diagnostic| { + let assoc = if diagnostic.starts_at_word { + Assoc::BeforeWord + } else { + Assoc::After + }; + (&mut diagnostic.range.start, assoc) + })); + changes.update_positions(diagnostics.iter_mut().filter_map(|diagnostic| { + if diagnostic.zero_width { + // for zero width diagnostics treat the diagnostic as a point + // rather than a range + return None; + } + let assoc = if diagnostic.ends_at_word { + Assoc::AfterWord + } else { + Assoc::Before + }; + Some((&mut diagnostic.range.end, assoc)) + })); + diagnostics.retain_mut(|diagnostic| { + if diagnostic.zero_width { + diagnostic.range.end = diagnostic.range.start; + } else if diagnostic.range.start >= diagnostic.range.end { + return false; + } + diagnostic.line = text.char_to_line(diagnostic.range.start); + true + }); + + diagnostics.sort(); + } +} + +impl crate::Diagnostic { + pub(crate) fn to_document_diagnostic(&self, text: &Rope) -> Option { + use helix_core::chars::char_is_word; + use helix_lsp::util; + + let (start, end, line) = match self.range { + crate::Range::Lsp { + range, + offset_encoding, + } => { + let start = util::lsp_pos_to_pos(text, range.start, offset_encoding)?; + let end = util::lsp_pos_to_pos(text, range.end, offset_encoding)?; + (start, end, range.start.line as usize) + } + crate::Range::Document(range) => { + (range.start, range.end, text.char_to_line(range.start)) + } + }; + + let ends_at_word = + start != end && end != 0 && text.get_char(end - 1).is_some_and(char_is_word); + let starts_at_word = start != end && text.get_char(start).is_some_and(char_is_word); + + Some(Diagnostic { + inner: self.clone(), + range: helix_stdx::Range { start, end }, + line, + starts_at_word, + ends_at_word, + zero_width: start == end, + }) + } +} + #[cfg(test)] mod test { use arc_swap::ArcSwap; diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index a5d364b15..3822aaf32 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -1,10 +1,12 @@ use crate::{ annotations::diagnostics::{DiagnosticFilter, InlineDiagnosticsConfig}, clipboard::ClipboardProvider, + diagnostic::DiagnosticProvider, document::{ - DocumentOpenError, DocumentSavedEventFuture, DocumentSavedEventResult, Mode, SavePoint, + self, DocumentOpenError, DocumentSavedEventFuture, DocumentSavedEventResult, Mode, + SavePoint, }, - events::{DocumentDidClose, DocumentDidOpen, DocumentFocusLost}, + events::{DiagnosticsDidChange, DocumentDidClose, DocumentDidOpen, DocumentFocusLost}, graphics::{CursorKind, Rect}, handlers::Handlers, info::Info, @@ -12,7 +14,7 @@ use crate::{ register::Registers, theme::{self, Theme}, tree::{self, Tree}, - Document, DocumentId, View, ViewId, + Diagnostic, Document, DocumentId, View, ViewId, }; use dap::StackFrame; use helix_event::dispatch; @@ -44,12 +46,10 @@ use anyhow::{anyhow, bail, Error}; pub use helix_core::diagnostic::Severity; use helix_core::{ auto_pairs::AutoPairs, - diagnostic::DiagnosticProvider, syntax::{self, AutoPairConfig, IndentationHeuristic, LanguageServerFeature, SoftWrap}, Change, LineEnding, Position, Range, Selection, Uri, NATIVE_LINE_ENDING, }; use helix_dap as dap; -use helix_lsp::lsp; use helix_stdx::path::canonicalize; use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer}; @@ -1044,7 +1044,7 @@ pub struct Breakpoint { use futures_util::stream::{Flatten, Once}; -type Diagnostics = BTreeMap>; +type Diagnostics = BTreeMap>; pub struct Editor { /// Current editing mode. @@ -1734,7 +1734,7 @@ impl Editor { } pub fn new_file_from_stdin(&mut self, action: Action) -> Result { - let (stdin, encoding, has_bom) = crate::document::read_to_string(&mut stdin(), None)?; + let (stdin, encoding, has_bom) = document::read_to_string(&mut stdin(), None)?; let doc = Document::from( helix_core::Rope::default(), Some((encoding, has_bom)), @@ -2014,8 +2014,8 @@ impl Editor { language_servers: &'a helix_lsp::Registry, diagnostics: &'a Diagnostics, document: &Document, - ) -> impl Iterator + 'a { - Editor::doc_diagnostics_with_filter(language_servers, diagnostics, document, |_, _| true) + ) -> impl Iterator + 'a { + Editor::doc_diagnostics_with_filter(language_servers, diagnostics, document, |_| true) } /// Returns all supported diagnostics for the document @@ -2024,37 +2024,38 @@ impl Editor { language_servers: &'a helix_lsp::Registry, diagnostics: &'a Diagnostics, document: &Document, - filter: impl Fn(&lsp::Diagnostic, &DiagnosticProvider) -> bool + 'a, - ) -> impl Iterator + 'a { + filter: impl Fn(&Diagnostic) -> bool + 'a, + ) -> impl Iterator + 'a { let text = document.text().clone(); let language_config = document.language.clone(); diagnostics .get(&document.uri()) .map(|diags| { - diags.iter().filter_map(move |(diagnostic, provider)| { - let server_id = provider.language_server_id()?; - let ls = language_servers.get_by_id(server_id)?; - language_config - .as_ref() - .and_then(|c| { - c.language_servers.iter().find(|features| { - features.name == ls.name() - && features.has_feature(LanguageServerFeature::Diagnostics) - }) - }) - .and_then(|_| { - if filter(diagnostic, provider) { - Document::lsp_diagnostic_to_diagnostic( - &text, - language_config.as_deref(), - diagnostic, - provider.clone(), - ls.offset_encoding(), - ) - } else { - None - } - }) + diags.iter().filter_map(move |diagnostic| { + let language_server = diagnostic + .provider + .language_server_id() + .and_then(|id| language_servers.get_by_id(id)); + + if let Some((config, server)) = language_config.as_ref().zip(language_server) { + config.language_servers.iter().find(|features| { + features.name == server.name() + && features.has_feature(LanguageServerFeature::Diagnostics) + })?; + } + if diagnostic.severity.is_some_and(|severity| { + language_config + .as_ref() + .is_some_and(|config| severity < config.diagnostic_severity) + }) { + return None; + } + + if filter(diagnostic) { + diagnostic.to_document_diagnostic(&text) + } else { + None + } }) }) .into_iter() @@ -2235,6 +2236,84 @@ impl Editor { pub fn get_last_cwd(&mut self) -> Option<&Path> { self.last_cwd.as_deref() } + + pub fn handle_diagnostics( + &mut self, + provider: &DiagnosticProvider, + uri: Uri, + version: Option, + mut diagnostics: Vec, + ) { + use std::collections::btree_map::Entry; + + let doc = self.documents.values_mut().find(|doc| doc.uri() == uri); + + if let Some((version, doc)) = version.zip(doc.as_ref()) { + if version != doc.version() { + log::info!("Version ({version}) is out of date for {uri:?} (expected ({})), dropping diagnostics", doc.version()); + return; + } + } + + let mut unchanged_diag_sources = Vec::new(); + if let Some((lang_conf, old_diagnostics)) = doc + .as_ref() + .and_then(|doc| Some((doc.language_config()?, self.diagnostics.get(&uri)?))) + { + if !lang_conf.persistent_diagnostic_sources.is_empty() { + diagnostics.sort(); + } + for source in &lang_conf.persistent_diagnostic_sources { + let new_diagnostics = diagnostics + .iter() + .filter(|d| d.source.as_ref() == Some(source)); + let old_diagnostics = old_diagnostics + .iter() + .filter(|d| &d.provider == provider && d.source.as_ref() == Some(source)); + if new_diagnostics.eq(old_diagnostics) { + unchanged_diag_sources.push(source.clone()) + } + } + } + + // Insert the original lsp::Diagnostics here because we may have no open document + // for diagnostic message and so we can't calculate the exact position. + // When using them later in the diagnostics picker, we calculate them on-demand. + let diagnostics = match self.diagnostics.entry(uri) { + Entry::Occupied(o) => { + let current_diagnostics = o.into_mut(); + // there may entries of other language servers, which is why we can't overwrite the whole entry + current_diagnostics.retain(|diagnostic| &diagnostic.provider != provider); + current_diagnostics.extend(diagnostics); + current_diagnostics + // Sort diagnostics first by severity and then by line numbers. + } + Entry::Vacant(v) => v.insert(diagnostics), + }; + + diagnostics.sort(); + + if let Some(doc) = doc { + let diagnostic_of_language_server_and_not_in_unchanged_sources = + |diagnostic: &crate::Diagnostic| { + &diagnostic.provider == provider + && diagnostic + .source + .as_ref() + .map_or(true, |source| !unchanged_diag_sources.contains(source)) + }; + let diagnostics = Self::doc_diagnostics_with_filter( + &self.language_servers, + &self.diagnostics, + doc, + diagnostic_of_language_server_and_not_in_unchanged_sources, + ); + doc.replace_diagnostics(diagnostics, &unchanged_diag_sources, Some(provider)); + + let doc = doc.id(); + helix_event::dispatch(DiagnosticsDidChange { editor: self, doc }); + } + } } fn try_restore_indent(doc: &mut Document, view: &mut View) { diff --git a/helix-view/src/gutter.rs b/helix-view/src/gutter.rs index 665a78bcc..43314f776 100644 --- a/helix-view/src/gutter.rs +++ b/helix-view/src/gutter.rs @@ -69,20 +69,22 @@ pub fn diagnostic<'doc>( .iter() .take_while(|d| { d.line == line - && d.provider.language_server_id().map_or(true, |id| { + && d.inner.provider.language_server_id().map_or(true, |id| { doc.language_servers_with_feature(LanguageServerFeature::Diagnostics) .any(|ls| ls.id() == id) }) }); - diagnostics_on_line.max_by_key(|d| d.severity).map(|d| { - write!(out, "●").ok(); - match d.severity { - Some(Severity::Error) => error, - Some(Severity::Warning) | None => warning, - Some(Severity::Info) => info, - Some(Severity::Hint) => hint, - } - }) + diagnostics_on_line + .max_by_key(|d| d.inner.severity) + .map(|d| { + write!(out, "●").ok(); + match d.inner.severity { + Some(Severity::Error) => error, + Some(Severity::Warning) | None => warning, + Some(Severity::Info) => info, + Some(Severity::Hint) => hint, + } + }) }, ) } diff --git a/helix-view/src/handlers/lsp.rs b/helix-view/src/handlers/lsp.rs index e9a9c6548..c27a4198b 100644 --- a/helix-view/src/handlers/lsp.rs +++ b/helix-view/src/handlers/lsp.rs @@ -1,12 +1,8 @@ -use std::collections::btree_map::Entry; use std::fmt::Display; use crate::editor::Action; -use crate::events::{ - DiagnosticsDidChange, DocumentDidChange, DocumentDidClose, LanguageServerInitialized, -}; +use crate::events::{DocumentDidChange, DocumentDidClose, LanguageServerInitialized}; use crate::{DocumentId, Editor}; -use helix_core::diagnostic::DiagnosticProvider; use helix_core::Uri; use helix_event::register_hook; use helix_lsp::util::generate_transaction_from_edits; @@ -282,91 +278,6 @@ impl Editor { Ok(()) } - pub fn handle_lsp_diagnostics( - &mut self, - provider: &DiagnosticProvider, - uri: Uri, - version: Option, - mut diagnostics: Vec, - ) { - let doc = self.documents.values_mut().find(|doc| doc.uri() == uri); - - if let Some((version, doc)) = version.zip(doc.as_ref()) { - if version != doc.version() { - log::info!("Version ({version}) is out of date for {uri:?} (expected ({})), dropping PublishDiagnostic notification", doc.version()); - return; - } - } - - let mut unchanged_diag_sources = Vec::new(); - if let Some((lang_conf, old_diagnostics)) = doc - .as_ref() - .and_then(|doc| Some((doc.language_config()?, self.diagnostics.get(&uri)?))) - { - if !lang_conf.persistent_diagnostic_sources.is_empty() { - // Sort diagnostics first by severity and then by line numbers. - // Note: The `lsp::DiagnosticSeverity` enum is already defined in decreasing order - diagnostics.sort_by_key(|d| (d.severity, d.range.start)); - } - for source in &lang_conf.persistent_diagnostic_sources { - let new_diagnostics = diagnostics - .iter() - .filter(|d| d.source.as_ref() == Some(source)); - let old_diagnostics = old_diagnostics - .iter() - .filter(|(d, d_provider)| { - d_provider == provider && d.source.as_ref() == Some(source) - }) - .map(|(d, _)| d); - if new_diagnostics.eq(old_diagnostics) { - unchanged_diag_sources.push(source.clone()) - } - } - } - - let diagnostics = diagnostics.into_iter().map(|d| (d, provider.clone())); - - // Insert the original lsp::Diagnostics here because we may have no open document - // for diagnostic message and so we can't calculate the exact position. - // When using them later in the diagnostics picker, we calculate them on-demand. - let diagnostics = match self.diagnostics.entry(uri) { - Entry::Occupied(o) => { - let current_diagnostics = o.into_mut(); - // there may entries of other language servers, which is why we can't overwrite the whole entry - current_diagnostics.retain(|(_, d_provider)| d_provider != provider); - current_diagnostics.extend(diagnostics); - current_diagnostics - // Sort diagnostics first by severity and then by line numbers. - } - Entry::Vacant(v) => v.insert(diagnostics.collect()), - }; - - // Sort diagnostics first by severity and then by line numbers. - // Note: The `lsp::DiagnosticSeverity` enum is already defined in decreasing order - diagnostics.sort_by_key(|(d, provider)| (d.severity, d.range.start, provider.clone())); - - if let Some(doc) = doc { - let diagnostic_of_language_server_and_not_in_unchanged_sources = - |diagnostic: &lsp::Diagnostic, d_provider: &DiagnosticProvider| { - d_provider == provider - && diagnostic - .source - .as_ref() - .map_or(true, |source| !unchanged_diag_sources.contains(source)) - }; - let diagnostics = Self::doc_diagnostics_with_filter( - &self.language_servers, - &self.diagnostics, - doc, - diagnostic_of_language_server_and_not_in_unchanged_sources, - ); - doc.replace_diagnostics(diagnostics, &unchanged_diag_sources, Some(provider)); - - let doc = doc.id(); - helix_event::dispatch(DiagnosticsDidChange { editor: self, doc }); - } - } - pub fn execute_lsp_command(&mut self, command: lsp::Command, server_id: LanguageServerId) { // the command is executed on the server and communicated back // to the client asynchronously using workspace edits diff --git a/helix-view/src/lib.rs b/helix-view/src/lib.rs index 46bcf7e66..1c0d7e083 100644 --- a/helix-view/src/lib.rs +++ b/helix-view/src/lib.rs @@ -5,6 +5,7 @@ mod action; pub mod annotations; pub mod base64; pub mod clipboard; +pub mod diagnostic; pub mod document; pub mod editor; pub mod events; @@ -55,7 +56,17 @@ pub fn align_view(doc: &mut Document, view: &View, align: Align) { doc.set_view_offset(view.id, view_offset); } +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum Range { + Document(helix_stdx::Range), + Lsp { + range: helix_lsp::lsp::Range, + offset_encoding: helix_lsp::OffsetEncoding, + }, +} + pub use action::Action; +pub use diagnostic::Diagnostic; pub use document::Document; pub use editor::Editor; use helix_core::char_idx_at_visual_offset;