diff --git a/helix-core/src/diagnostic.rs b/helix-core/src/diagnostic.rs index 4e89361d2..21a6afcce 100644 --- a/helix-core/src/diagnostic.rs +++ b/helix-core/src/diagnostic.rs @@ -3,6 +3,8 @@ use std::fmt; use serde::{Deserialize, Serialize}; +use crate::Selection; + /// Describes the severity level of a [`Diagnostic`]. #[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] @@ -62,6 +64,18 @@ pub struct Diagnostic { pub data: Option, } +impl Diagnostic { + /// Returns a single selection spanning the range of the diagnostic. + pub fn single_selection(&self) -> Selection { + Selection::single(self.range.start, self.range.end) + } + + /// Returns a single reversed selection spanning the range of the diagnostic. + pub fn single_selection_rev(&self) -> Selection { + Selection::single(self.range.end, self.range.start) + } +} + // TODO turn this into an enum + feature flag when lsp becomes optional pub type DiagnosticProvider = LanguageServerId; diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index b1c29378d..08024876e 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -416,6 +416,12 @@ impl MappableCommand { goto_last_diag, "Goto last diagnostic", goto_next_diag, "Goto next diagnostic", goto_prev_diag, "Goto previous diagnostic", + goto_first_diag_workspace, "Goto first diagnostic in workspace", + goto_first_error_workspace, "Goto first Error diagnostic in workspace", + goto_first_warning_workspace, "Goto first Warning diagnostic in workspace", + goto_next_diag_workspace, "Goto next diagnostic in workspace", + goto_next_error_workspace, "Goto next Error diagnostic in workspace", + goto_next_warning_workspace, "Goto next Warning diagnostic in workspace", goto_next_change, "Goto next change", goto_prev_change, "Goto previous change", goto_first_change, "Goto first change", @@ -2846,13 +2852,7 @@ fn flip_selections(cx: &mut Context) { fn ensure_selections_forward(cx: &mut Context) { let (view, doc) = current!(cx.editor); - - let selection = doc - .selection(view.id) - .clone() - .transform(|r| r.with_direction(Direction::Forward)); - - doc.set_selection(view.id, selection); + helix_view::ensure_selections_forward(view, doc); } fn enter_insert_mode(cx: &mut Context) { @@ -3714,6 +3714,54 @@ fn goto_prev_diag(cx: &mut Context) { cx.editor.apply_motion(motion) } +fn goto_next_diag_workspace(cx: &mut Context) { + goto_next_diag_workspace_impl(cx, None) +} + +fn goto_next_error_workspace(cx: &mut Context) { + goto_next_diag_workspace_impl(cx, Some(helix_core::diagnostic::Severity::Error)) +} + +fn goto_next_warning_workspace(cx: &mut Context) { + goto_next_diag_workspace_impl(cx, Some(helix_core::diagnostic::Severity::Warning)) +} + +fn goto_next_diag_workspace_impl( + cx: &mut Context, + severity_filter: Option, +) { + let diag = helix_view::next_diagnostic_in_workspace(&cx.editor, severity_filter); + + // wrap around + let diag = + diag.or_else(|| helix_view::first_diagnostic_in_workspace(&cx.editor, severity_filter)); + + if let Some(diag) = diag { + lsp::jump_to_diagnostic(cx, diag.into_owned()); + } +} + +fn goto_first_diag_workspace(cx: &mut Context) { + goto_first_diag_workspace_impl(cx, None) +} + +fn goto_first_error_workspace(cx: &mut Context) { + goto_first_diag_workspace_impl(cx, Some(helix_core::diagnostic::Severity::Error)) +} + +fn goto_first_warning_workspace(cx: &mut Context) { + goto_first_diag_workspace_impl(cx, Some(helix_core::diagnostic::Severity::Warning)) +} + +fn goto_first_diag_workspace_impl( + cx: &mut Context, + severity_filter: Option, +) { + if let Some(diag) = helix_view::first_diagnostic_in_workspace(&cx.editor, severity_filter) { + lsp::jump_to_diagnostic(cx, diag.into_owned()); + } +} + fn goto_first_change(cx: &mut Context) { goto_first_change_impl(cx, false); } diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index fcc0333e8..ddd3b99c5 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -127,7 +127,7 @@ fn jump_to_location( jump_to_position(editor, path, location.range, offset_encoding, action); } -fn jump_to_position( +pub fn jump_to_position( editor: &mut Editor, path: &Path, range: lsp::Range, @@ -159,6 +159,19 @@ fn jump_to_position( } } +pub fn jump_to_diagnostic(cx: &mut Context, diagnostic: helix_view::WorkspaceDiagnostic<'static>) { + let path = diagnostic.path; + let range = diagnostic.diagnostic.range; + let offset_encoding = diagnostic.offset_encoding; + + let motion = move |editor: &mut Editor| { + jump_to_position(editor, &path, range, offset_encoding, Action::Replace); + let (view, doc) = current!(editor); + helix_view::ensure_selections_forward(view, doc); + }; + cx.editor.apply_motion(motion); +} + fn display_symbol_kind(kind: lsp::SymbolKind) -> &'static str { match kind { lsp::SymbolKind::FILE => "file", diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 91ec27874..c9ad1d4f1 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -1895,6 +1895,22 @@ impl Document { ) } + pub fn lsp_severity_to_severity( + severity: lsp::DiagnosticSeverity, + ) -> Option { + use helix_core::diagnostic::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 + } + } + } + pub fn lsp_diagnostic_to_diagnostic( text: &Rope, language_config: Option<&LanguageConfiguration>, @@ -1902,7 +1918,7 @@ impl Document { language_server_id: LanguageServerId, offset_encoding: helix_lsp::OffsetEncoding, ) -> Option { - use helix_core::diagnostic::{Range, Severity::*}; + use helix_core::diagnostic::Range; // TODO: convert inside server let start = @@ -1920,16 +1936,7 @@ impl Document { 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 - } - }); + let severity = diagnostic.severity.and_then(Self::lsp_severity_to_severity); if let Some(lang_conf) = language_config { if let Some(severity) = severity { diff --git a/helix-view/src/lib.rs b/helix-view/src/lib.rs index d54b49ef5..3d9520df3 100644 --- a/helix-view/src/lib.rs +++ b/helix-view/src/lib.rs @@ -18,7 +18,7 @@ pub mod theme; pub mod tree; pub mod view; -use std::num::NonZeroUsize; +use std::{borrow::Cow, num::NonZeroUsize, path::Path}; // uses NonZeroUsize so Option use a byte rather than two #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] @@ -72,8 +72,152 @@ pub fn align_view(doc: &mut Document, view: &View, align: Align) { doc.set_view_offset(view.id, view_offset); } +/// Returns the left-side position of the primary selection. +pub fn primary_cursor(view: &View, doc: &Document) -> usize { + doc.selection(view.id) + .primary() + .cursor(doc.text().slice(..)) +} + +/// Returns the next diagnostic in the document if any. +/// +/// This does not wrap-around. +pub fn next_diagnostic_in_doc<'d>( + view: &View, + doc: &'d Document, + severity_filter: Option, +) -> Option<&'d Diagnostic> { + let cursor = primary_cursor(view, doc); + doc.diagnostics() + .iter() + .filter(|diagnostic| diagnostic.severity >= severity_filter) + .find(|diag| diag.range.start > cursor) +} + +/// Returns the previous diagnostic in the document if any. +/// +/// This does not wrap-around. +pub fn prev_diagnostic_in_doc<'d>( + view: &View, + doc: &'d Document, + severity_filter: Option, +) -> Option<&'d Diagnostic> { + let cursor = primary_cursor(view, doc); + doc.diagnostics() + .iter() + .rev() + .filter(|diagnostic| diagnostic.severity >= severity_filter) + .find(|diag| diag.range.start < cursor) +} + +pub struct WorkspaceDiagnostic<'e> { + pub path: Cow<'e, Path>, + pub diagnostic: Cow<'e, helix_lsp::lsp::Diagnostic>, + pub offset_encoding: OffsetEncoding, +} +impl<'e> WorkspaceDiagnostic<'e> { + pub fn into_owned(self) -> WorkspaceDiagnostic<'static> { + WorkspaceDiagnostic { + path: Cow::Owned(self.path.into_owned()), + diagnostic: Cow::Owned(self.diagnostic.into_owned()), + offset_encoding: self.offset_encoding, + } + } +} + +fn workspace_diagnostics<'e>( + editor: &'e Editor, + severity_filter: Option, +) -> impl Iterator> { + editor + .diagnostics + .iter() + .filter_map(|(uri, diagnostics)| { + // Extract Path from diagnostic Uri, skipping diagnostics that don't have a path. + uri.as_path().map(|p| (p, diagnostics)) + }) + .flat_map(|(path, diagnostics)| { + diagnostics + .iter() + .map(move |(diagnostic, language_server_id)| (path, diagnostic, language_server_id)) + }) + .filter(move |(_, diagnostic, _)| { + // Filter by severity + let severity = diagnostic + .severity + .and_then(Document::lsp_severity_to_severity); + severity >= severity_filter + }) + .map(|(path, diag, language_server_id)| { + // Map language server ID to offset encoding + let offset_encoding = editor + .language_server_by_id(*language_server_id) + .map(|client| client.offset_encoding()) + .unwrap_or_default(); + (path, diag, offset_encoding) + }) + .map(|(path, diagnostic, offset_encoding)| WorkspaceDiagnostic { + path: Cow::Borrowed(path), + diagnostic: Cow::Borrowed(diagnostic), + offset_encoding, + }) +} + +pub fn first_diagnostic_in_workspace( + editor: &Editor, + severity_filter: Option, +) -> Option { + workspace_diagnostics(editor, severity_filter).next() +} + +pub fn next_diagnostic_in_workspace( + editor: &Editor, + severity_filter: Option, +) -> Option { + let (view, doc) = current_ref!(editor); + + let Some(current_doc_path) = doc.path() else { + return first_diagnostic_in_workspace(editor, severity_filter); + }; + + let cursor = primary_cursor(view, doc); + + workspace_diagnostics(editor, severity_filter) + .filter(|d| { + // Skip diagnostics before the current document + d.path >= current_doc_path.as_path() + }) + .filter(|d| { + // Skip diagnostics before the primary cursor in the current document + if d.path == current_doc_path.as_path() { + let Some(start) = helix_lsp::util::lsp_pos_to_pos( + doc.text(), + d.diagnostic.range.start, + d.offset_encoding, + ) else { + return false; + }; + if start <= cursor { + return false; + } + } + true + }) + .next() +} + +pub fn ensure_selections_forward(view: &View, doc: &mut Document) { + let selection = doc + .selection(view.id) + .clone() + .transform(|r| r.with_direction(Direction::Forward)); + + doc.set_selection(view.id, selection); +} + pub use document::Document; pub use editor::Editor; -use helix_core::char_idx_at_visual_offset; +use helix_core::{char_idx_at_visual_offset, movement::Direction, Diagnostic}; +use helix_lsp::OffsetEncoding; pub use theme::Theme; pub use view::View;