diff --git a/book/src/generated/static-cmd.md b/book/src/generated/static-cmd.md index 92a1fb2a1..05edbe510 100644 --- a/book/src/generated/static-cmd.md +++ b/book/src/generated/static-cmd.md @@ -147,6 +147,12 @@ | `goto_last_diag` | Goto last diagnostic | normal: `` ]D ``, select: `` ]D `` | | `goto_next_diag` | Goto next diagnostic | normal: `` ]d ``, select: `` ]d `` | | `goto_prev_diag` | Goto previous diagnostic | normal: `` [d ``, select: `` [d `` | +| `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 | normal: `` ]g ``, select: `` ]g `` | | `goto_prev_change` | Goto previous change | normal: `` [g ``, select: `` [g `` | | `goto_first_change` | Goto first change | normal: `` [G ``, select: `` [G `` | diff --git a/helix-core/src/diagnostic.rs b/helix-core/src/diagnostic.rs index b9360b525..724483049 100644 --- a/helix-core/src/diagnostic.rs +++ b/helix-core/src/diagnostic.rs @@ -4,6 +4,8 @@ use std::{fmt, sync::Arc}; pub use helix_stdx::range::Range; 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")] @@ -99,4 +101,14 @@ impl Diagnostic { pub fn severity(&self) -> Severity { self.severity.unwrap_or(Severity::Warning) } + + /// 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) + } } diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 64768c0dc..7f0114d92 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -445,6 +445,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", @@ -2980,13 +2986,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) { @@ -3993,6 +3993,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 9c55c830c..8afd8944c 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -127,7 +127,7 @@ fn jump_to_location(editor: &mut Editor, location: &Location, action: 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 fb89e2e0c..2b18f1289 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -2019,6 +2019,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>, @@ -2026,7 +2042,7 @@ impl Document { provider: DiagnosticProvider, offset_encoding: helix_lsp::OffsetEncoding, ) -> Option { - use helix_core::diagnostic::{Range, Severity::*}; + use helix_core::diagnostic::Range; // TODO: convert inside server let start = @@ -2044,16 +2060,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 e30a23381..559326e17 100644 --- a/helix-view/src/lib.rs +++ b/helix-view/src/lib.rs @@ -19,7 +19,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)] @@ -73,8 +73,164 @@ 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( + editor: &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)| { + let mut diagnostics = diagnostics.iter().collect::>(); + diagnostics.sort_by_key(|(diagnostic, _)| diagnostic.range.start); + + diagnostics + .into_iter() + .map(move |(diagnostic, diagnostic_provider)| { + (path, diagnostic, diagnostic_provider) + }) + }) + .filter(move |(_, diagnostic, _)| { + // Filter by severity + let severity = diagnostic + .severity + .and_then(Document::lsp_severity_to_severity); + severity >= severity_filter + }) + .map(|(path, diag, diagnostic_provider)| { + match diagnostic_provider { + DiagnosticProvider::Lsp { server_id, .. } => { + // Map language server ID to offset encoding + let offset_encoding = editor + .language_server_by_id(*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); + + #[allow(clippy::filter_next)] + 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, diagnostic::DiagnosticProvider, movement::Direction, Diagnostic, +}; +use helix_lsp::OffsetEncoding; pub use theme::Theme; pub use view::View;