Skip to content

Commit c316f98

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 a63a2ad commit c316f98

File tree

5 files changed

+247
-21
lines changed

5 files changed

+247
-21
lines changed

helix-core/src/diagnostic.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ use std::fmt;
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")]
@@ -50,6 +52,18 @@ pub struct Diagnostic {
5052
pub data: Option<serde_json::Value>,
5153
}
5254

55+
impl Diagnostic {
56+
/// Returns a single selection spanning the range of the diagnostic.
57+
pub fn single_selection(&self) -> Selection {
58+
Selection::single(self.range.start, self.range.end)
59+
}
60+
61+
/// Returns a single reversed selection spanning the range of the diagnostic.
62+
pub fn single_selection_rev(&self) -> Selection {
63+
Selection::single(self.range.end, self.range.start)
64+
}
65+
}
66+
5367
// TODO turn this into an enum + feature flag when lsp becomes optional
5468
pub type DiagnosticProvider = LanguageServerId;
5569

helix-term/src/commands.rs

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,12 @@ impl MappableCommand {
437437
goto_last_diag, "Goto last diagnostic",
438438
goto_next_diag, "Goto next diagnostic",
439439
goto_prev_diag, "Goto previous diagnostic",
440+
goto_first_diag_workspace, "Goto first diagnostic in workspace",
441+
goto_first_error_workspace, "Goto first Error diagnostic in workspace",
442+
goto_first_warning_workspace, "Goto first Warning diagnostic in workspace",
443+
goto_next_diag_workspace, "Goto next diagnostic in workspace",
444+
goto_next_error_workspace, "Goto next Error diagnostic in workspace",
445+
goto_next_warning_workspace, "Goto next Warning diagnostic in workspace",
440446
goto_next_change, "Goto next change",
441447
goto_prev_change, "Goto previous change",
442448
goto_first_change, "Goto first change",
@@ -2893,13 +2899,7 @@ fn flip_selections(cx: &mut Context) {
28932899

28942900
fn ensure_selections_forward(cx: &mut Context) {
28952901
let (view, doc) = current!(cx.editor);
2896-
2897-
let selection = doc
2898-
.selection(view.id)
2899-
.clone()
2900-
.transform(|r| r.with_direction(Direction::Forward));
2901-
2902-
doc.set_selection(view.id, selection);
2902+
helix_view::ensure_selections_forward(view, doc);
29032903
}
29042904

29052905
fn enter_insert_mode(cx: &mut Context) {
@@ -3865,6 +3865,54 @@ fn goto_prev_diag(cx: &mut Context) {
38653865
cx.editor.apply_motion(motion)
38663866
}
38673867

3868+
fn goto_next_diag_workspace(cx: &mut Context) {
3869+
goto_next_diag_workspace_impl(cx, None)
3870+
}
3871+
3872+
fn goto_next_error_workspace(cx: &mut Context) {
3873+
goto_next_diag_workspace_impl(cx, Some(helix_core::diagnostic::Severity::Error))
3874+
}
3875+
3876+
fn goto_next_warning_workspace(cx: &mut Context) {
3877+
goto_next_diag_workspace_impl(cx, Some(helix_core::diagnostic::Severity::Warning))
3878+
}
3879+
3880+
fn goto_next_diag_workspace_impl(
3881+
cx: &mut Context,
3882+
severity_filter: Option<helix_core::diagnostic::Severity>,
3883+
) {
3884+
let diag = helix_view::next_diagnostic_in_workspace(&cx.editor, severity_filter);
3885+
3886+
// wrap around
3887+
let diag =
3888+
diag.or_else(|| helix_view::first_diagnostic_in_workspace(&cx.editor, severity_filter));
3889+
3890+
if let Some(diag) = diag {
3891+
lsp::jump_to_diagnostic(cx, diag.into_owned());
3892+
}
3893+
}
3894+
3895+
fn goto_first_diag_workspace(cx: &mut Context) {
3896+
goto_first_diag_workspace_impl(cx, None)
3897+
}
3898+
3899+
fn goto_first_error_workspace(cx: &mut Context) {
3900+
goto_first_diag_workspace_impl(cx, Some(helix_core::diagnostic::Severity::Error))
3901+
}
3902+
3903+
fn goto_first_warning_workspace(cx: &mut Context) {
3904+
goto_first_diag_workspace_impl(cx, Some(helix_core::diagnostic::Severity::Warning))
3905+
}
3906+
3907+
fn goto_first_diag_workspace_impl(
3908+
cx: &mut Context,
3909+
severity_filter: Option<helix_core::diagnostic::Severity>,
3910+
) {
3911+
if let Some(diag) = helix_view::first_diagnostic_in_workspace(&cx.editor, severity_filter) {
3912+
lsp::jump_to_diagnostic(cx, diag.into_owned());
3913+
}
3914+
}
3915+
38683916
fn goto_first_change(cx: &mut Context) {
38693917
goto_first_change_impl(cx, false);
38703918
}

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(
127127
jump_to_position(editor, path, location.range, offset_encoding, action);
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
@@ -1932,14 +1932,30 @@ impl Document {
19321932
)
19331933
}
19341934

1935+
pub fn lsp_severity_to_severity(
1936+
severity: lsp::DiagnosticSeverity,
1937+
) -> Option<helix_core::diagnostic::Severity> {
1938+
use helix_core::diagnostic::Severity::*;
1939+
match severity {
1940+
lsp::DiagnosticSeverity::ERROR => Some(Error),
1941+
lsp::DiagnosticSeverity::WARNING => Some(Warning),
1942+
lsp::DiagnosticSeverity::INFORMATION => Some(Info),
1943+
lsp::DiagnosticSeverity::HINT => Some(Hint),
1944+
severity => {
1945+
log::error!("unrecognized diagnostic severity: {:?}", severity);
1946+
None
1947+
}
1948+
}
1949+
}
1950+
19351951
pub fn lsp_diagnostic_to_diagnostic(
19361952
text: &Rope,
19371953
language_config: Option<&LanguageConfiguration>,
19381954
diagnostic: &helix_lsp::lsp::Diagnostic,
19391955
language_server_id: LanguageServerId,
19401956
offset_encoding: helix_lsp::OffsetEncoding,
19411957
) -> Option<Diagnostic> {
1942-
use helix_core::diagnostic::{Range, Severity::*};
1958+
use helix_core::diagnostic::Range;
19431959

19441960
// TODO: convert inside server
19451961
let start =
@@ -1957,16 +1973,7 @@ impl Document {
19571973
return None;
19581974
};
19591975

1960-
let severity = diagnostic.severity.and_then(|severity| match severity {
1961-
lsp::DiagnosticSeverity::ERROR => Some(Error),
1962-
lsp::DiagnosticSeverity::WARNING => Some(Warning),
1963-
lsp::DiagnosticSeverity::INFORMATION => Some(Info),
1964-
lsp::DiagnosticSeverity::HINT => Some(Hint),
1965-
severity => {
1966-
log::error!("unrecognized diagnostic severity: {:?}", severity);
1967-
None
1968-
}
1969-
});
1976+
let severity = diagnostic.severity.and_then(Self::lsp_severity_to_severity);
19701977

19711978
if let Some(lang_conf) = language_config {
19721979
if let Some(severity) = severity {

helix-view/src/lib.rs

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

21-
use std::num::NonZeroUsize;
21+
use std::{borrow::Cow, num::NonZeroUsize, path::Path};
2222

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

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

0 commit comments

Comments
 (0)