Unnamed repository; edit this file 'description' to name the repository.
-rw-r--r--helix-core/src/diagnostic.rs70
-rw-r--r--helix-core/src/lib.rs1
-rw-r--r--helix-lsp-types/src/lib.rs4
-rw-r--r--helix-lsp/src/lib.rs60
-rw-r--r--helix-term/src/application.rs25
-rw-r--r--helix-term/src/commands/lsp.rs247
-rw-r--r--helix-term/src/commands/typed.rs2
-rw-r--r--helix-term/src/ui/editor.rs35
-rw-r--r--helix-term/src/ui/statusline.rs16
-rw-r--r--helix-term/src/ui/text_decorations/diagnostics.rs12
-rw-r--r--helix-view/src/action.rs10
-rw-r--r--helix-view/src/annotations/diagnostics.rs6
-rw-r--r--helix-view/src/diagnostic.rs218
-rw-r--r--helix-view/src/document.rs242
-rw-r--r--helix-view/src/editor.rs149
-rw-r--r--helix-view/src/gutter.rs22
-rw-r--r--helix-view/src/handlers/lsp.rs91
-rw-r--r--helix-view/src/lib.rs11
18 files changed, 646 insertions, 575 deletions
diff --git a/helix-core/src/diagnostic.rs b/helix-core/src/diagnostic.rs
index b9360b52..d4bc9469 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<Severity>,
- pub code: Option<NumberOrString>,
- pub provider: DiagnosticProvider,
- pub tags: Vec<DiagnosticTag>,
- pub source: Option<String>,
- pub data: Option<serde_json::Value>,
-}
-
-/// 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,
- }
- }
-}
-
// 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 09865ca4..f21b008a 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 fd668de5..2877c459 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 567e8a70..41075f2c 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 2b2ff855..40a03e2d 100644
--- a/helix-term/src/application.rs
+++ b/helix-term/src/application.rs
@@ -729,16 +729,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 {
@@ -844,8 +851,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 d6ce4c1c..773957c6 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::config::LanguageServerFeature,
- text_annotations::InlineAnnotation, Selection, Uri,
+ syntax::config::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<Location> {
- 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<Self> {
+ 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<FileLocation<'a>> {
+ 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<FileLocation> {
- 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<PickerDiagnostic, DiagnosticStyles>;
fn diag_picker(
cx: &Context,
- diagnostics: impl IntoIterator<Item = (Uri, Vec<(lsp::Diagnostic, DiagnosticProvider)>)>,
+ diagnostics: impl IntoIterator<Item = (Uri, Vec<Diagnostic>)>,
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,11 +238,12 @@ 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()
@@ -253,11 +253,12 @@ fn diag_picker(
item.diag.source.as_deref().unwrap_or("").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()
@@ -295,7 +296,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)
}
@@ -319,8 +320,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() {
@@ -353,8 +356,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,
})
@@ -421,7 +426,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)))
@@ -478,8 +483,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,
})
@@ -547,7 +554,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);
@@ -609,20 +616,26 @@ fn goto_impl(editor: &mut Editor, compositor: &mut Compositor, locations: Vec<Lo
let columns = [ui::PickerColumn::new(
"location",
|item: &Location, cwdir: &std::path::PathBuf| {
- let path = if let Some(path) = item.uri.as_path() {
- path.strip_prefix(cwdir).unwrap_or(path).to_string_lossy()
+ use std::fmt::Write;
+ let mut path = if let Some(path) = item.uri.as_path() {
+ path.strip_prefix(cwdir)
+ .unwrap_or(path)
+ .to_string_lossy()
+ .to_string()
} else {
- item.uri.to_string().into()
+ item.uri.to_string()
};
-
- format!("{path}:{}", item.range.start.line + 1).into()
+ if let helix_view::Range::Lsp { range, .. } = item.range {
+ write!(path, ":{}", range.start.line + 1).unwrap();
+ }
+ path.into()
},
)];
let picker = Picker::new(columns, 0, locations, cwdir, |cx, location, action| {
jump_to_location(cx.editor, location, action)
})
- .with_preview(|_editor, location| location_to_file_location(location));
+ .with_preview(|editor, location| location.file_location(editor));
compositor.push(Box::new(overlaid(picker)));
}
}
@@ -650,12 +663,14 @@ where
match response {
Ok((response, offset_encoding)) => 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(
@@ -667,9 +682,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 => (),
@@ -749,7 +762,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 2013a9d8..f53a9c9e 100644
--- a/helix-term/src/commands/typed.rs
+++ b/helix-term/src/commands/typed.rs
@@ -2497,7 +2497,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 9343d55d..6fca4b1b 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, OverlayHighlights},
@@ -310,7 +309,9 @@ impl EditorView {
theme: &Theme,
overlay_highlights: &mut Vec<OverlayHighlights>,
) {
- 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_highlight_exact(scope)
@@ -353,7 +354,7 @@ impl EditorView {
for diagnostic in doc.diagnostics() {
// Separate diagnostics into different Vecs by severity.
- let vec = match diagnostic.severity {
+ let vec = match diagnostic.inner.severity {
Some(Severity::Info) => &mut info_vec,
Some(Severity::Hint) => &mut hint_vec,
Some(Severity::Warning) => &mut warning_vec,
@@ -365,16 +366,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, diagnostic.range);
}
- for tag in &diagnostic.tags {
+ for tag in &diagnostic.inner.tags {
match tag {
DiagnosticTag::Unnecessary => {
if unnecessary.is_some() {
@@ -684,6 +685,7 @@ impl EditorView {
theme: &Theme,
) {
use helix_core::diagnostic::Severity;
+ use helix_view::diagnostic::NumberOrString;
use tui::{
layout::Alignment,
text::Text,
@@ -707,17 +709,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 ea3d27bd..a5b7f95e 100644
--- a/helix-term/src/ui/statusline.rs
+++ b/helix-term/src/ui/statusline.rs
@@ -1,8 +1,7 @@
use std::borrow::Cow;
use helix_core::indent::IndentStyle;
-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},
@@ -218,14 +217,13 @@ fn render_diagnostics<'a, F>(context: &mut RenderContext<'a>, write: F)
where
F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy,
{
- use helix_core::diagnostic::Severity;
let (hints, info, warnings, errors) =
context
.doc
.diagnostics()
.iter()
.fold((0, 0, 0, 0), |mut counts, diag| {
- match diag.severity {
+ match diag.inner.severity {
Some(Severity::Hint) | None => counts.0 += 1,
Some(Severity::Info) => counts.1 += 1,
Some(Severity::Warning) => counts.2 += 1,
@@ -270,16 +268,16 @@ where
use helix_core::diagnostic::Severity;
let (hints, info, warnings, errors) = context.editor.diagnostics.values().flatten().fold(
(0u32, 0u32, 0u32, 0u32),
- |mut counts, (diag, _)| {
+ |mut counts, diag| {
match diag.severity {
// PERF: For large workspace diagnostics, this loop can be very tight.
//
// Most often the diagnostics will be for warnings and errors.
// Errors should tend to be fixed fast, leaving warnings as the most common.
- Some(DiagnosticSeverity::WARNING) => counts.2 += 1,
- Some(DiagnosticSeverity::ERROR) => counts.3 += 1,
- Some(DiagnosticSeverity::HINT) => counts.0 += 1,
- Some(DiagnosticSeverity::INFORMATION) => counts.1 += 1,
+ Some(Severity::Warning) => counts.2 += 1,
+ Some(Severity::Error) => counts.3 += 1,
+ Some(Severity::Hint) => counts.0 += 1,
+ Some(Severity::Info) => counts.1 += 1,
// Fallback to `hint`.
_ => counts.0 += 1,
}
diff --git a/helix-term/src/ui/text_decorations/diagnostics.rs b/helix-term/src/ui/text_decorations/diagnostics.rs
index fb82bcf5..c560be93 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 64ab75e0..b9659dad 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::config::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 7802ca63..2f698c4e 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 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(),
+ ))
+ }
+}
diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs
index e7d83ab0..aaf9aab5 100644
--- a/helix-view/src/document.rs
+++ b/helix-view/src/document.rs
@@ -4,16 +4,15 @@ 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::command_line::Token;
-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::config::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;
@@ -40,10 +39,11 @@ use helix_core::{
indent::{auto_detect_indent_style, IndentStyle},
line_ending::auto_detect_line_ending,
syntax::{self, config::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},
expansion,
@@ -1452,45 +1452,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<InlineAnnotation>| {
@@ -2022,94 +1984,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<Diagnostic> {
- 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
@@ -2124,17 +1998,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
@@ -2142,19 +2016,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
@@ -2318,6 +2186,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<Self>, 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<Diagnostic> {
+ 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 194285a8..d6efd25f 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;
@@ -45,7 +47,6 @@ use anyhow::{anyhow, bail, Error};
pub use helix_core::diagnostic::Severity;
use helix_core::{
auto_pairs::AutoPairs,
- diagnostic::DiagnosticProvider,
syntax::{
self,
config::{AutoPairConfig, IndentationHeuristic, LanguageServerFeature, SoftWrap},
@@ -53,7 +54,6 @@ use helix_core::{
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};
@@ -1059,7 +1059,7 @@ pub struct Breakpoint {
use futures_util::stream::{Flatten, Once};
-type Diagnostics = BTreeMap<Uri, Vec<(lsp::Diagnostic, DiagnosticProvider)>>;
+type Diagnostics = BTreeMap<Uri, Vec<Diagnostic>>;
pub struct Editor {
/// Current editing mode.
@@ -1756,7 +1756,7 @@ impl Editor {
}
pub fn new_file_from_stdin(&mut self, action: Action) -> Result<DocumentId, Error> {
- 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)),
@@ -2045,8 +2045,8 @@ impl Editor {
language_servers: &'a helix_lsp::Registry,
diagnostics: &'a Diagnostics,
document: &Document,
- ) -> impl Iterator<Item = helix_core::Diagnostic> + 'a {
- Editor::doc_diagnostics_with_filter(language_servers, diagnostics, document, |_, _| true)
+ ) -> impl Iterator<Item = document::Diagnostic> + 'a {
+ Editor::doc_diagnostics_with_filter(language_servers, diagnostics, document, |_| true)
}
/// Returns all supported diagnostics for the document
@@ -2055,37 +2055,38 @@ impl Editor {
language_servers: &'a helix_lsp::Registry,
diagnostics: &'a Diagnostics,
document: &Document,
- filter: impl Fn(&lsp::Diagnostic, &DiagnosticProvider) -> bool + 'a,
- ) -> impl Iterator<Item = helix_core::Diagnostic> + 'a {
+ filter: impl Fn(&Diagnostic) -> bool + 'a,
+ ) -> impl Iterator<Item = document::Diagnostic> + '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()
@@ -2266,6 +2267,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<i32>,
+ mut diagnostics: Vec<Diagnostic>,
+ ) {
+ 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 c2cbc0da..244e0565 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 e9a9c654..c27a4198 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<i32>,
- mut diagnostics: Vec<lsp::Diagnostic>,
- ) {
- 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 46bcf7e6..1c0d7e08 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;