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
This commit is contained in:
Asger Juul Brunshøj 2024-06-04 13:33:03 +02:00
parent a7651f5bf0
commit aab0239fbe
5 changed files with 247 additions and 21 deletions

View File

@ -3,6 +3,8 @@
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")]
@ -62,6 +64,18 @@ pub struct Diagnostic {
pub data: Option<serde_json::Value>, pub data: Option<serde_json::Value>,
} }
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 // TODO turn this into an enum + feature flag when lsp becomes optional
pub type DiagnosticProvider = LanguageServerId; pub type DiagnosticProvider = LanguageServerId;

View File

@ -416,6 +416,12 @@ pub fn doc(&self) -> &str {
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",
@ -2846,13 +2852,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) {
@ -3714,6 +3714,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(
jump_to_position(editor, path, location.range, offset_encoding, action); jump_to_position(editor, path, location.range, offset_encoding, 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

@ -1895,6 +1895,22 @@ pub fn position(
) )
} }
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>,
@ -1902,7 +1918,7 @@ pub fn lsp_diagnostic_to_diagnostic(
language_server_id: LanguageServerId, language_server_id: LanguageServerId,
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 =
@ -1920,16 +1936,7 @@ pub fn lsp_diagnostic_to_diagnostic(
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

@ -18,7 +18,7 @@
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)]
@ -72,8 +72,152 @@ 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<'e>(
editor: &'e Editor,
severity_filter: Option<helix_core::diagnostic::Severity>,
) -> impl Iterator<Item = WorkspaceDiagnostic<'e>> {
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<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);
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, movement::Direction, Diagnostic};
use helix_lsp::OffsetEncoding;
pub use theme::Theme; pub use theme::Theme;
pub use view::View; pub use view::View;