add "goto first/next workspace diagnostic" commands

Adds
- goto_first_diag_workspace
- goto_first_error_workspace
- goto_first_warning_workspace
- goto_next_diag_workspace
- goto_next_error_workspace
- goto_next_warning_workspace
pull/10913/head
Asger Juul Brunshøj 2024-06-04 13:33:03 +02:00
parent ebf96bd469
commit f3b29c77bc
6 changed files with 263 additions and 21 deletions

View File

@ -147,6 +147,12 @@
| `goto_last_diag` | Goto last diagnostic | normal: `` ]D ``, select: `` ]D `` | | `goto_last_diag` | Goto last diagnostic | normal: `` ]D ``, select: `` ]D `` |
| `goto_next_diag` | Goto next 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_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_next_change` | Goto next change | normal: `` ]g ``, select: `` ]g `` |
| `goto_prev_change` | Goto previous 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 `` | | `goto_first_change` | Goto first change | normal: `` [G ``, select: `` [G `` |

View File

@ -4,6 +4,8 @@ use std::{fmt, sync::Arc};
pub use helix_stdx::range::Range; pub use helix_stdx::range::Range;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::Selection;
/// Describes the severity level of a [`Diagnostic`]. /// Describes the severity level of a [`Diagnostic`].
#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
@ -99,4 +101,14 @@ impl Diagnostic {
pub fn severity(&self) -> Severity { pub fn severity(&self) -> Severity {
self.severity.unwrap_or(Severity::Warning) 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)
}
} }

View File

@ -445,6 +445,12 @@ impl MappableCommand {
goto_last_diag, "Goto last diagnostic", goto_last_diag, "Goto last diagnostic",
goto_next_diag, "Goto next diagnostic", goto_next_diag, "Goto next diagnostic",
goto_prev_diag, "Goto previous 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_next_change, "Goto next change",
goto_prev_change, "Goto previous change", goto_prev_change, "Goto previous change",
goto_first_change, "Goto first change", goto_first_change, "Goto first change",
@ -2980,13 +2986,7 @@ fn flip_selections(cx: &mut Context) {
fn ensure_selections_forward(cx: &mut Context) { fn ensure_selections_forward(cx: &mut Context) {
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
helix_view::ensure_selections_forward(view, doc);
let selection = doc
.selection(view.id)
.clone()
.transform(|r| r.with_direction(Direction::Forward));
doc.set_selection(view.id, selection);
} }
fn enter_insert_mode(cx: &mut Context) { fn enter_insert_mode(cx: &mut Context) {
@ -3993,6 +3993,54 @@ fn goto_prev_diag(cx: &mut Context) {
cx.editor.apply_motion(motion) 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<helix_core::diagnostic::Severity>,
) {
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<helix_core::diagnostic::Severity>,
) {
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) { fn goto_first_change(cx: &mut Context) {
goto_first_change_impl(cx, false); goto_first_change_impl(cx, false);
} }

View File

@ -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, editor: &mut Editor,
path: &Path, path: &Path,
range: lsp::Range, 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 { fn display_symbol_kind(kind: lsp::SymbolKind) -> &'static str {
match kind { match kind {
lsp::SymbolKind::FILE => "file", lsp::SymbolKind::FILE => "file",

View File

@ -2019,6 +2019,22 @@ impl Document {
) )
} }
pub fn lsp_severity_to_severity(
severity: lsp::DiagnosticSeverity,
) -> Option<helix_core::diagnostic::Severity> {
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( pub fn lsp_diagnostic_to_diagnostic(
text: &Rope, text: &Rope,
language_config: Option<&LanguageConfiguration>, language_config: Option<&LanguageConfiguration>,
@ -2026,7 +2042,7 @@ impl Document {
provider: DiagnosticProvider, provider: DiagnosticProvider,
offset_encoding: helix_lsp::OffsetEncoding, offset_encoding: helix_lsp::OffsetEncoding,
) -> Option<Diagnostic> { ) -> Option<Diagnostic> {
use helix_core::diagnostic::{Range, Severity::*}; use helix_core::diagnostic::Range;
// TODO: convert inside server // TODO: convert inside server
let start = let start =
@ -2044,16 +2060,7 @@ impl Document {
return None; return None;
}; };
let severity = diagnostic.severity.and_then(|severity| match severity { let severity = diagnostic.severity.and_then(Self::lsp_severity_to_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(lang_conf) = language_config {
if let Some(severity) = severity { if let Some(severity) = severity {

View File

@ -19,7 +19,7 @@ pub mod theme;
pub mod tree; pub mod tree;
pub mod view; pub mod view;
use std::num::NonZeroUsize; use std::{borrow::Cow, num::NonZeroUsize, path::Path};
// uses NonZeroUsize so Option<DocumentId> use a byte rather than two // uses NonZeroUsize so Option<DocumentId> use a byte rather than two
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] #[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); 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<helix_core::diagnostic::Severity>,
) -> 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<helix_core::diagnostic::Severity>,
) -> 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<helix_core::diagnostic::Severity>,
) -> impl Iterator<Item = WorkspaceDiagnostic<'_>> {
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::<Vec<_>>();
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<helix_core::diagnostic::Severity>,
) -> Option<WorkspaceDiagnostic> {
workspace_diagnostics(editor, severity_filter).next()
}
pub fn next_diagnostic_in_workspace(
editor: &Editor,
severity_filter: Option<helix_core::diagnostic::Severity>,
) -> Option<WorkspaceDiagnostic> {
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 document::Document;
pub use editor::Editor; 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 theme::Theme;
pub use view::View; pub use view::View;