Skip to content

Commit 262ab16

Browse files
committed
Jump to next/prev diagnostic in workspace (#3116)
Implements functions and keybindings to move to next/previous/first/last diagnostic in the workspace
1 parent 0813276 commit 262ab16

File tree

3 files changed

+206
-28
lines changed

3 files changed

+206
-28
lines changed

helix-term/src/commands.rs

Lines changed: 84 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,12 @@ use crate::{
5353

5454
use crate::job::{self, Job, Jobs};
5555
use futures_util::{FutureExt, StreamExt};
56-
use std::{collections::HashMap, fmt, future::Future};
57-
use std::{collections::HashSet, num::NonZeroUsize};
56+
use std::{
57+
collections::{BTreeMap, HashMap, HashSet},
58+
fmt,
59+
future::Future,
60+
num::NonZeroUsize,
61+
};
5862

5963
use std::{
6064
borrow::Cow,
@@ -304,6 +308,10 @@ impl MappableCommand {
304308
goto_last_diag, "Goto last diagnostic",
305309
goto_next_diag, "Goto next diagnostic",
306310
goto_prev_diag, "Goto previous diagnostic",
311+
goto_first_diag_workspace, "Goto first diagnostic in workspace",
312+
goto_last_diag_workspace, "Goto last diagnostic in workspace",
313+
goto_next_diag_workspace, "Goto next diagnostic in workspace",
314+
goto_prev_diag_workspace, "Goto previous diagnostic in workspace",
307315
goto_line_start, "Goto line start",
308316
goto_line_end, "Goto line end",
309317
goto_next_buffer, "Goto next buffer",
@@ -2800,7 +2808,7 @@ fn exit_select_mode(cx: &mut Context) {
28002808
}
28012809
}
28022810

2803-
fn goto_pos(editor: &mut Editor, pos: usize) {
2811+
pub fn goto_pos(editor: &mut Editor, pos: usize) {
28042812
let (view, doc) = current!(editor);
28052813

28062814
push_jump(view, doc);
@@ -2826,23 +2834,81 @@ fn goto_last_diag(cx: &mut Context) {
28262834
goto_pos(cx.editor, pos);
28272835
}
28282836

2829-
fn goto_next_diag(cx: &mut Context) {
2830-
let editor = &mut cx.editor;
2831-
let (view, doc) = current!(editor);
2832-
2837+
/// Finds the position of the next/previous diagnostic.
2838+
pub fn get_next_diag_pos(
2839+
view: &mut View,
2840+
doc: &mut Document,
2841+
direction: Direction,
2842+
) -> Option<usize> {
28332843
let cursor_pos = doc
28342844
.selection(view.id)
28352845
.primary()
28362846
.cursor(doc.text().slice(..));
28372847

2838-
let diag = doc
2839-
.diagnostics()
2840-
.iter()
2841-
.find(|diag| diag.range.start > cursor_pos)
2842-
.or_else(|| doc.diagnostics().first());
2848+
let diag = match direction {
2849+
Direction::Forward => doc
2850+
.diagnostics()
2851+
.iter()
2852+
.find(|diag| diag.range.start > cursor_pos),
2853+
Direction::Backward => doc
2854+
.diagnostics()
2855+
.iter()
2856+
.find(|diag| diag.range.start < cursor_pos),
2857+
};
28432858

2844-
let pos = match diag {
2845-
Some(diag) => diag.range.start,
2859+
return diag.map(|d| d.range.start);
2860+
}
2861+
2862+
/// Finds the next/previous document with diagnostics in it.
2863+
pub fn get_next_diag_doc(
2864+
doc: &mut Document,
2865+
editor_diagnostics: BTreeMap<helix_lsp::lsp::Url, Vec<helix_lsp::lsp::Diagnostic>>,
2866+
direction: Direction,
2867+
) -> Option<PathBuf> {
2868+
let current_url = doc.url();
2869+
let diagnostics = editor_diagnostics.iter();
2870+
let next_diags = match direction {
2871+
Direction::Forward => {
2872+
let mut iter = diagnostics
2873+
.filter(|(_, diags)| !diags.is_empty())
2874+
.skip_while(|(url, _)| Some(*url) != current_url.as_ref());
2875+
iter.next();
2876+
iter.next().or_else(|| {
2877+
editor_diagnostics
2878+
.iter()
2879+
.filter(|(_, diags)| !diags.is_empty())
2880+
.next()
2881+
})
2882+
}
2883+
Direction::Backward => {
2884+
let mut iter = diagnostics
2885+
.rev()
2886+
.filter(|(_, diags)| !diags.is_empty())
2887+
.skip_while(|(url, _)| Some(*url) != current_url.as_ref());
2888+
iter.next(); // skip current
2889+
iter.next().or_else(|| {
2890+
editor_diagnostics
2891+
.iter()
2892+
.filter(|(_, diags)| !diags.is_empty())
2893+
.last()
2894+
})
2895+
}
2896+
};
2897+
match next_diags {
2898+
Some((url, _)) => url.to_file_path().ok(),
2899+
None => None,
2900+
}
2901+
}
2902+
2903+
fn goto_next_diag(cx: &mut Context) {
2904+
let editor = &mut cx.editor;
2905+
let (view, doc) = current!(editor);
2906+
2907+
let diag_pos = get_next_diag_pos(view, doc, Direction::Forward)
2908+
.or_else(|| doc.diagnostics().first().map(|d| d.range.start));
2909+
2910+
let pos = match diag_pos {
2911+
Some(pos) => pos,
28462912
None => return,
28472913
};
28482914

@@ -2853,20 +2919,11 @@ fn goto_prev_diag(cx: &mut Context) {
28532919
let editor = &mut cx.editor;
28542920
let (view, doc) = current!(editor);
28552921

2856-
let cursor_pos = doc
2857-
.selection(view.id)
2858-
.primary()
2859-
.cursor(doc.text().slice(..));
2860-
2861-
let diag = doc
2862-
.diagnostics()
2863-
.iter()
2864-
.rev()
2865-
.find(|diag| diag.range.start < cursor_pos)
2866-
.or_else(|| doc.diagnostics().last());
2922+
let diag_pos = get_next_diag_pos(view, doc, Direction::Backward)
2923+
.or_else(|| doc.diagnostics().last().map(|d| d.range.start));
28672924

2868-
let pos = match diag {
2869-
Some(diag) => diag.range.start,
2925+
let pos = match diag_pos {
2926+
Some(pos) => pos,
28702927
None => return,
28712928
};
28722929

helix-term/src/commands/lsp.rs

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@ use tui::text::{Span, Spans};
88

99
use super::{align_view, push_jump, Align, Context, Editor, Open};
1010

11-
use helix_core::{path, Selection};
11+
use helix_core::{movement::Direction, path, Selection};
1212
use helix_view::{apply_transaction, editor::Action, theme::Style};
1313

1414
use crate::{
15+
commands::{get_next_diag_doc, get_next_diag_pos, goto_pos},
1516
compositor::{self, Compositor},
1617
ui::{
1718
self, lsp::SignatureHelp, overlay::overlayed, FileLocation, FilePicker, Popup, PromptEvent,
@@ -411,6 +412,122 @@ pub fn workspace_diagnostics_picker(cx: &mut Context) {
411412
cx.push_layer(Box::new(overlayed(picker)));
412413
}
413414

415+
pub fn goto_first_diag_workspace(cx: &mut Context) {
416+
let editor = &mut cx.editor;
417+
418+
let mut diagnostics = editor
419+
.diagnostics
420+
.iter()
421+
.filter(|(_, diags)| !diags.is_empty())
422+
.map(|(url, _)| url);
423+
424+
let diag = diagnostics.next();
425+
match diag {
426+
Some(url) => {
427+
let path = url.to_file_path().unwrap();
428+
editor
429+
.open(&path, Action::Replace)
430+
.expect("editor.open failed");
431+
let doc = doc!(editor);
432+
let pos = match doc.diagnostics().first() {
433+
Some(diag) => diag.range.start,
434+
None => return,
435+
};
436+
goto_pos(editor, pos);
437+
}
438+
None => return,
439+
}
440+
}
441+
442+
pub fn goto_last_diag_workspace(cx: &mut Context) {
443+
let editor = &mut cx.editor;
444+
445+
let diagnostics = editor
446+
.diagnostics
447+
.iter()
448+
.filter(|(_, diags)| !diags.is_empty())
449+
.map(|(url, _)| url);
450+
451+
let diag = diagnostics.last();
452+
match diag {
453+
Some(url) => {
454+
let path = url.to_file_path().unwrap();
455+
editor
456+
.open(&path, Action::Replace)
457+
.expect("editor.open failed");
458+
let doc = doc!(editor);
459+
let pos = match doc.diagnostics().last() {
460+
Some(diag) => diag.range.start,
461+
None => return,
462+
};
463+
goto_pos(editor, pos);
464+
}
465+
None => return,
466+
}
467+
}
468+
469+
pub fn goto_next_diag_workspace(cx: &mut Context) {
470+
let editor = &mut cx.editor;
471+
let (view, doc) = current!(editor);
472+
473+
let doc_next_diag_pos = get_next_diag_pos(view, doc, Direction::Forward);
474+
475+
match doc_next_diag_pos {
476+
Some(pos) => goto_pos(editor, pos),
477+
None => {
478+
let diagnostics = editor.diagnostics.clone();
479+
let next_doc = get_next_diag_doc(doc, diagnostics, Direction::Forward);
480+
match next_doc {
481+
Some(path) => {
482+
editor
483+
.open(&path, Action::Replace)
484+
.expect("editor.open failed");
485+
let doc = doc!(editor);
486+
match doc.diagnostics().get(0) {
487+
Some(diag) => {
488+
let pos = diag.range.start;
489+
goto_pos(editor, pos)
490+
}
491+
None => return,
492+
}
493+
}
494+
None => return,
495+
}
496+
}
497+
}
498+
}
499+
500+
pub fn goto_prev_diag_workspace(cx: &mut Context) {
501+
let editor = &mut cx.editor;
502+
let (view, doc) = current!(editor);
503+
504+
let doc_prev_diag_pos = get_next_diag_pos(view, doc, Direction::Backward);
505+
506+
match doc_prev_diag_pos {
507+
Some(pos) => goto_pos(editor, pos),
508+
None => {
509+
let diagnostics = editor.diagnostics.clone();
510+
let next_doc = get_next_diag_doc(doc, diagnostics, Direction::Backward);
511+
match next_doc {
512+
Some(path) => {
513+
editor
514+
.open(&path, Action::Replace)
515+
.expect("editor.open failed");
516+
let doc = doc!(editor);
517+
match doc.diagnostics().last() {
518+
Some(diag) => {
519+
let pos = diag.range.start;
520+
goto_pos(editor, pos)
521+
}
522+
None => return,
523+
}
524+
}
525+
None => return,
526+
}
527+
}
528+
}
529+
}
530+
414531
impl ui::menu::Item for lsp::CodeActionOrCommand {
415532
type Data = ();
416533
fn label(&self, _data: &Self::Data) -> Spans {

helix-term/src/keymap/default.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ pub fn default() -> HashMap<Mode, Keymap> {
100100
"[" => { "Left bracket"
101101
"d" => goto_prev_diag,
102102
"D" => goto_first_diag,
103+
"w" => goto_prev_diag_workspace,
104+
"W" => goto_first_diag_workspace,
103105
"f" => goto_prev_function,
104106
"c" => goto_prev_class,
105107
"a" => goto_prev_parameter,
@@ -111,6 +113,8 @@ pub fn default() -> HashMap<Mode, Keymap> {
111113
"]" => { "Right bracket"
112114
"d" => goto_next_diag,
113115
"D" => goto_last_diag,
116+
"w" => goto_next_diag_workspace,
117+
"W" => goto_last_diag_workspace,
114118
"f" => goto_next_function,
115119
"c" => goto_next_class,
116120
"a" => goto_next_parameter,

0 commit comments

Comments
 (0)