Skip to content

Commit 192d9a8

Browse files
committed
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
1 parent a05c151 commit 192d9a8

File tree

6 files changed

+263
-21
lines changed

6 files changed

+263
-21
lines changed

book/src/generated/static-cmd.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,12 @@
147147
| `goto_last_diag` | Goto last diagnostic | normal: `` ]D ``, select: `` ]D `` |
148148
| `goto_next_diag` | Goto next diagnostic | normal: `` ]d ``, select: `` ]d `` |
149149
| `goto_prev_diag` | Goto previous diagnostic | normal: `` [d ``, select: `` [d `` |
150+
| `goto_first_diag_workspace` | Goto first diagnostic in workspace | |
151+
| `goto_first_error_workspace` | Goto first Error diagnostic in workspace | |
152+
| `goto_first_warning_workspace` | Goto first Warning diagnostic in workspace | |
153+
| `goto_next_diag_workspace` | Goto next diagnostic in workspace | |
154+
| `goto_next_error_workspace` | Goto next Error diagnostic in workspace | |
155+
| `goto_next_warning_workspace` | Goto next Warning diagnostic in workspace | |
150156
| `goto_next_change` | Goto next change | normal: `` ]g ``, select: `` ]g `` |
151157
| `goto_prev_change` | Goto previous change | normal: `` [g ``, select: `` [g `` |
152158
| `goto_first_change` | Goto first change | normal: `` [G ``, select: `` [G `` |

helix-core/src/diagnostic.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ use std::{fmt, sync::Arc};
44
pub use helix_stdx::range::Range;
55
use serde::{Deserialize, Serialize};
66

7+
use crate::Selection;
8+
79
/// Describes the severity level of a [`Diagnostic`].
810
#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)]
911
#[serde(rename_all = "lowercase")]
@@ -99,4 +101,14 @@ impl Diagnostic {
99101
pub fn severity(&self) -> Severity {
100102
self.severity.unwrap_or(Severity::Warning)
101103
}
104+
105+
/// Returns a single selection spanning the range of the diagnostic.
106+
pub fn single_selection(&self) -> Selection {
107+
Selection::single(self.range.start, self.range.end)
108+
}
109+
110+
/// Returns a single reversed selection spanning the range of the diagnostic.
111+
pub fn single_selection_rev(&self) -> Selection {
112+
Selection::single(self.range.end, self.range.start)
113+
}
102114
}

helix-term/src/commands.rs

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,12 @@ impl MappableCommand {
445445
goto_last_diag, "Goto last diagnostic",
446446
goto_next_diag, "Goto next diagnostic",
447447
goto_prev_diag, "Goto previous diagnostic",
448+
goto_first_diag_workspace, "Goto first diagnostic in workspace",
449+
goto_first_error_workspace, "Goto first Error diagnostic in workspace",
450+
goto_first_warning_workspace, "Goto first Warning diagnostic in workspace",
451+
goto_next_diag_workspace, "Goto next diagnostic in workspace",
452+
goto_next_error_workspace, "Goto next Error diagnostic in workspace",
453+
goto_next_warning_workspace, "Goto next Warning diagnostic in workspace",
448454
goto_next_change, "Goto next change",
449455
goto_prev_change, "Goto previous change",
450456
goto_first_change, "Goto first change",
@@ -2982,13 +2988,7 @@ fn flip_selections(cx: &mut Context) {
29822988

29832989
fn ensure_selections_forward(cx: &mut Context) {
29842990
let (view, doc) = current!(cx.editor);
2985-
2986-
let selection = doc
2987-
.selection(view.id)
2988-
.clone()
2989-
.transform(|r| r.with_direction(Direction::Forward));
2990-
2991-
doc.set_selection(view.id, selection);
2991+
helix_view::ensure_selections_forward(view, doc);
29922992
}
29932993

29942994
fn enter_insert_mode(cx: &mut Context) {
@@ -3998,6 +3998,54 @@ fn goto_prev_diag(cx: &mut Context) {
39983998
cx.editor.apply_motion(motion)
39993999
}
40004000

4001+
fn goto_next_diag_workspace(cx: &mut Context) {
4002+
goto_next_diag_workspace_impl(cx, None)
4003+
}
4004+
4005+
fn goto_next_error_workspace(cx: &mut Context) {
4006+
goto_next_diag_workspace_impl(cx, Some(helix_core::diagnostic::Severity::Error))
4007+
}
4008+
4009+
fn goto_next_warning_workspace(cx: &mut Context) {
4010+
goto_next_diag_workspace_impl(cx, Some(helix_core::diagnostic::Severity::Warning))
4011+
}
4012+
4013+
fn goto_next_diag_workspace_impl(
4014+
cx: &mut Context,
4015+
severity_filter: Option<helix_core::diagnostic::Severity>,
4016+
) {
4017+
let diag = helix_view::next_diagnostic_in_workspace(cx.editor, severity_filter);
4018+
4019+
// wrap around
4020+
let diag =
4021+
diag.or_else(|| helix_view::first_diagnostic_in_workspace(cx.editor, severity_filter));
4022+
4023+
if let Some(diag) = diag {
4024+
lsp::jump_to_diagnostic(cx, diag.into_owned());
4025+
}
4026+
}
4027+
4028+
fn goto_first_diag_workspace(cx: &mut Context) {
4029+
goto_first_diag_workspace_impl(cx, None)
4030+
}
4031+
4032+
fn goto_first_error_workspace(cx: &mut Context) {
4033+
goto_first_diag_workspace_impl(cx, Some(helix_core::diagnostic::Severity::Error))
4034+
}
4035+
4036+
fn goto_first_warning_workspace(cx: &mut Context) {
4037+
goto_first_diag_workspace_impl(cx, Some(helix_core::diagnostic::Severity::Warning))
4038+
}
4039+
4040+
fn goto_first_diag_workspace_impl(
4041+
cx: &mut Context,
4042+
severity_filter: Option<helix_core::diagnostic::Severity>,
4043+
) {
4044+
if let Some(diag) = helix_view::first_diagnostic_in_workspace(cx.editor, severity_filter) {
4045+
lsp::jump_to_diagnostic(cx, diag.into_owned());
4046+
}
4047+
}
4048+
40014049
fn goto_first_change(cx: &mut Context) {
40024050
goto_first_change_impl(cx, false);
40034051
}

helix-term/src/commands/lsp.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ fn jump_to_location(editor: &mut Editor, location: &Location, action: Action) {
127127
);
128128
}
129129

130-
fn jump_to_position(
130+
pub fn jump_to_position(
131131
editor: &mut Editor,
132132
path: &Path,
133133
range: lsp::Range,
@@ -159,6 +159,19 @@ fn jump_to_position(
159159
}
160160
}
161161

162+
pub fn jump_to_diagnostic(cx: &mut Context, diagnostic: helix_view::WorkspaceDiagnostic<'static>) {
163+
let path = diagnostic.path;
164+
let range = diagnostic.diagnostic.range;
165+
let offset_encoding = diagnostic.offset_encoding;
166+
167+
let motion = move |editor: &mut Editor| {
168+
jump_to_position(editor, &path, range, offset_encoding, Action::Replace);
169+
let (view, doc) = current!(editor);
170+
helix_view::ensure_selections_forward(view, doc);
171+
};
172+
cx.editor.apply_motion(motion);
173+
}
174+
162175
fn display_symbol_kind(kind: lsp::SymbolKind) -> &'static str {
163176
match kind {
164177
lsp::SymbolKind::FILE => "file",

helix-view/src/document.rs

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2020,14 +2020,30 @@ impl Document {
20202020
)
20212021
}
20222022

2023+
pub fn lsp_severity_to_severity(
2024+
severity: lsp::DiagnosticSeverity,
2025+
) -> Option<helix_core::diagnostic::Severity> {
2026+
use helix_core::diagnostic::Severity::*;
2027+
match severity {
2028+
lsp::DiagnosticSeverity::ERROR => Some(Error),
2029+
lsp::DiagnosticSeverity::WARNING => Some(Warning),
2030+
lsp::DiagnosticSeverity::INFORMATION => Some(Info),
2031+
lsp::DiagnosticSeverity::HINT => Some(Hint),
2032+
severity => {
2033+
log::error!("unrecognized diagnostic severity: {:?}", severity);
2034+
None
2035+
}
2036+
}
2037+
}
2038+
20232039
pub fn lsp_diagnostic_to_diagnostic(
20242040
text: &Rope,
20252041
language_config: Option<&LanguageConfiguration>,
20262042
diagnostic: &helix_lsp::lsp::Diagnostic,
20272043
provider: DiagnosticProvider,
20282044
offset_encoding: helix_lsp::OffsetEncoding,
20292045
) -> Option<Diagnostic> {
2030-
use helix_core::diagnostic::{Range, Severity::*};
2046+
use helix_core::diagnostic::Range;
20312047

20322048
// TODO: convert inside server
20332049
let start =
@@ -2045,16 +2061,7 @@ impl Document {
20452061
return None;
20462062
};
20472063

2048-
let severity = diagnostic.severity.and_then(|severity| match severity {
2049-
lsp::DiagnosticSeverity::ERROR => Some(Error),
2050-
lsp::DiagnosticSeverity::WARNING => Some(Warning),
2051-
lsp::DiagnosticSeverity::INFORMATION => Some(Info),
2052-
lsp::DiagnosticSeverity::HINT => Some(Hint),
2053-
severity => {
2054-
log::error!("unrecognized diagnostic severity: {:?}", severity);
2055-
None
2056-
}
2057-
});
2064+
let severity = diagnostic.severity.and_then(Self::lsp_severity_to_severity);
20582065

20592066
if let Some(lang_conf) = language_config {
20602067
if let Some(severity) = severity {

helix-view/src/lib.rs

Lines changed: 158 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ pub mod theme;
1919
pub mod tree;
2020
pub mod view;
2121

22-
use std::num::NonZeroUsize;
22+
use std::{borrow::Cow, num::NonZeroUsize, path::Path};
2323

2424
// uses NonZeroUsize so Option<DocumentId> use a byte rather than two
2525
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
@@ -73,8 +73,164 @@ pub fn align_view(doc: &mut Document, view: &View, align: Align) {
7373
doc.set_view_offset(view.id, view_offset);
7474
}
7575

76+
/// Returns the left-side position of the primary selection.
77+
pub fn primary_cursor(view: &View, doc: &Document) -> usize {
78+
doc.selection(view.id)
79+
.primary()
80+
.cursor(doc.text().slice(..))
81+
}
82+
83+
/// Returns the next diagnostic in the document if any.
84+
///
85+
/// This does not wrap-around.
86+
pub fn next_diagnostic_in_doc<'d>(
87+
view: &View,
88+
doc: &'d Document,
89+
severity_filter: Option<helix_core::diagnostic::Severity>,
90+
) -> Option<&'d Diagnostic> {
91+
let cursor = primary_cursor(view, doc);
92+
doc.diagnostics()
93+
.iter()
94+
.filter(|diagnostic| diagnostic.severity >= severity_filter)
95+
.find(|diag| diag.range.start > cursor)
96+
}
97+
98+
/// Returns the previous diagnostic in the document if any.
99+
///
100+
/// This does not wrap-around.
101+
pub fn prev_diagnostic_in_doc<'d>(
102+
view: &View,
103+
doc: &'d Document,
104+
severity_filter: Option<helix_core::diagnostic::Severity>,
105+
) -> Option<&'d Diagnostic> {
106+
let cursor = primary_cursor(view, doc);
107+
doc.diagnostics()
108+
.iter()
109+
.rev()
110+
.filter(|diagnostic| diagnostic.severity >= severity_filter)
111+
.find(|diag| diag.range.start < cursor)
112+
}
113+
114+
pub struct WorkspaceDiagnostic<'e> {
115+
pub path: Cow<'e, Path>,
116+
pub diagnostic: Cow<'e, helix_lsp::lsp::Diagnostic>,
117+
pub offset_encoding: OffsetEncoding,
118+
}
119+
impl<'e> WorkspaceDiagnostic<'e> {
120+
pub fn into_owned(self) -> WorkspaceDiagnostic<'static> {
121+
WorkspaceDiagnostic {
122+
path: Cow::Owned(self.path.into_owned()),
123+
diagnostic: Cow::Owned(self.diagnostic.into_owned()),
124+
offset_encoding: self.offset_encoding,
125+
}
126+
}
127+
}
128+
129+
fn workspace_diagnostics(
130+
editor: &Editor,
131+
severity_filter: Option<helix_core::diagnostic::Severity>,
132+
) -> impl Iterator<Item = WorkspaceDiagnostic<'_>> {
133+
editor
134+
.diagnostics
135+
.iter()
136+
.filter_map(|(uri, diagnostics)| {
137+
// Extract Path from diagnostic Uri, skipping diagnostics that don't have a path.
138+
uri.as_path().map(|p| (p, diagnostics))
139+
})
140+
.flat_map(|(path, diagnostics)| {
141+
let mut diagnostics = diagnostics.iter().collect::<Vec<_>>();
142+
diagnostics.sort_by_key(|(diagnostic, _)| diagnostic.range.start);
143+
144+
diagnostics
145+
.into_iter()
146+
.map(move |(diagnostic, diagnostic_provider)| {
147+
(path, diagnostic, diagnostic_provider)
148+
})
149+
})
150+
.filter(move |(_, diagnostic, _)| {
151+
// Filter by severity
152+
let severity = diagnostic
153+
.severity
154+
.and_then(Document::lsp_severity_to_severity);
155+
severity >= severity_filter
156+
})
157+
.map(|(path, diag, diagnostic_provider)| {
158+
match diagnostic_provider {
159+
DiagnosticProvider::Lsp { server_id, .. } => {
160+
// Map language server ID to offset encoding
161+
let offset_encoding = editor
162+
.language_server_by_id(*server_id)
163+
.map(|client| client.offset_encoding())
164+
.unwrap_or_default();
165+
(path, diag, offset_encoding)
166+
}
167+
}
168+
})
169+
.map(|(path, diagnostic, offset_encoding)| WorkspaceDiagnostic {
170+
path: Cow::Borrowed(path),
171+
diagnostic: Cow::Borrowed(diagnostic),
172+
offset_encoding,
173+
})
174+
}
175+
176+
pub fn first_diagnostic_in_workspace(
177+
editor: &Editor,
178+
severity_filter: Option<helix_core::diagnostic::Severity>,
179+
) -> Option<WorkspaceDiagnostic> {
180+
workspace_diagnostics(editor, severity_filter).next()
181+
}
182+
183+
pub fn next_diagnostic_in_workspace(
184+
editor: &Editor,
185+
severity_filter: Option<helix_core::diagnostic::Severity>,
186+
) -> Option<WorkspaceDiagnostic> {
187+
let (view, doc) = current_ref!(editor);
188+
189+
let Some(current_doc_path) = doc.path() else {
190+
return first_diagnostic_in_workspace(editor, severity_filter);
191+
};
192+
193+
let cursor = primary_cursor(view, doc);
194+
195+
#[allow(clippy::filter_next)]
196+
workspace_diagnostics(editor, severity_filter)
197+
.filter(|d| {
198+
// Skip diagnostics before the current document
199+
d.path >= current_doc_path.as_path()
200+
})
201+
.filter(|d| {
202+
// Skip diagnostics before the primary cursor in the current document
203+
if d.path == current_doc_path.as_path() {
204+
let Some(start) = helix_lsp::util::lsp_pos_to_pos(
205+
doc.text(),
206+
d.diagnostic.range.start,
207+
d.offset_encoding,
208+
) else {
209+
return false;
210+
};
211+
if start <= cursor {
212+
return false;
213+
}
214+
}
215+
true
216+
})
217+
.next()
218+
}
219+
220+
pub fn ensure_selections_forward(view: &View, doc: &mut Document) {
221+
let selection = doc
222+
.selection(view.id)
223+
.clone()
224+
.transform(|r| r.with_direction(Direction::Forward));
225+
226+
doc.set_selection(view.id, selection);
227+
}
228+
76229
pub use document::Document;
77230
pub use editor::Editor;
78-
use helix_core::char_idx_at_visual_offset;
231+
use helix_core::{
232+
char_idx_at_visual_offset, diagnostic::DiagnosticProvider, movement::Direction, Diagnostic,
233+
};
234+
use helix_lsp::OffsetEncoding;
79235
pub use theme::Theme;
80236
pub use view::View;

0 commit comments

Comments
 (0)