on master help sign in
Add workspace and document diagnostics picker (#2013)

* Add workspace and document diagnostics picker fixes #1891 * Fix some of @archseer's annotations * Add From<&Spans> impl for String * More descriptive parameter names. * Adding From<Cow<str>> impls for Span and Spans * Add new keymap entries to docs * Avoid some clones * Fix api change * Update helix-term/src/application.rs Co-authored-by: Bjorn Ove Hay Andersen <bjrnove@gmail.com> * Fix a clippy hint * Sort diagnostics first by URL and then by severity. * Sort diagnostics first by URL and then by severity. * Ignore missing lsp severity entries * Add truncated filepath * Typo * Strip cwd from paths and use url-path without schema * Make tests a doctest * Better variable names Co-authored-by: Falco Hirschenberger <falco.hirschenberger@itwm.fraunhofer.de> Co-authored-by: Bjorn Ove Hay Andersen <bjrnove@gmail.com>

commited89f8897eab84bf7614a718d5d1e3ec5c57086c authorFalco Hirschenberger <falco.hirschenberger@gmail.com> dateThu 30 Jun 2022, 11:16 (4 years ago) parent94fc41a
12 files changed+273−52
book/src/keymap.md+2 helix-core/src/path.rs+51 helix-term/src/application.rs+21−7 helix-term/src/commands.rs+7−5 helix-term/src/commands/lsp.rs+131−4 helix-term/src/keymap/default.rs+2 helix-term/src/ui/mod.rs+3−1 helix-term/src/ui/picker.rs+32−29 helix-tui/src/text.rs+19−4 helix-tui/src/widgets/block.rs+1−1 helix-view/src/editor.rs+3 xtask/src/main.rs+1−1
book/src/keymap.md+2
@@ -241,6 +241,8 @@ This layer is a kludge of mappings, mostly pickers.
241241| `k` | Show documentation for item under cursor in a [popup](#popup) (**LSP**) | `hover` |
242242| `s` | Open document symbol picker (**LSP**) | `symbol_picker` |
243243| `S` | Open workspace symbol picker (**LSP**) | `workspace_symbol_picker` |
244+| `g` | Open document diagnosics picker (**LSP**) | `diagnostics_picker` |
245+| `G` | Open workspace diagnostics picker (**LSP**) | `workspace_diagnosics_picker`
244246| `r` | Rename symbol (**LSP**) | `rename_symbol` |
245247| `a` | Apply code action (**LSP**) | `code_action` |
246248| `'` | Open last fuzzy picker | `last_picker` |
helix-core/src/path.rs+51
@@ -90,3 +90,54 @@ pub fn get_relative_path(path: &Path) -> PathBuf {
9090 };
9191 fold_home_dir(path)
9292}
93+
94+/// Returns a truncated filepath where the basepart of the path is reduced to the first
95+/// char of the folder and the whole filename appended.
96+///
97+/// Also strip the current working directory from the beginning of the path.
98+/// Note that this function does not check if the truncated path is unambiguous.
99+///
100+/// ```
101+/// use helix_core::path::get_truncated_path;
102+/// use std::path::Path;
103+///
104+/// assert_eq!(
105+/// get_truncated_path("/home/cnorris/documents/jokes.txt").as_path(),
106+/// Path::new("/h/c/d/jokes.txt")
107+/// );
108+/// assert_eq!(
109+/// get_truncated_path("jokes.txt").as_path(),
110+/// Path::new("jokes.txt")
111+/// );
112+/// assert_eq!(
113+/// get_truncated_path("/jokes.txt").as_path(),
114+/// Path::new("/jokes.txt")
115+/// );
116+/// assert_eq!(
117+/// get_truncated_path("/h/c/d/jokes.txt").as_path(),
118+/// Path::new("/h/c/d/jokes.txt")
119+/// );
120+/// assert_eq!(get_truncated_path("").as_path(), Path::new(""));
121+/// ```
122+///
123+pub fn get_truncated_path<P: AsRef<Path>>(path: P) -> PathBuf {
124+ let cwd = std::env::current_dir().unwrap_or_default();
125+ let path = path
126+ .as_ref()
127+ .strip_prefix(cwd)
128+ .unwrap_or_else(|_| path.as_ref());
129+ let file = path.file_name().unwrap_or_default();
130+ let base = path.parent().unwrap_or_else(|| Path::new(""));
131+ let mut ret = PathBuf::new();
132+ for d in base {
133+ ret.push(
134+ d.to_string_lossy()
135+ .chars()
136+ .next()
137+ .unwrap_or_default()
138+ .to_string(),
139+ );
140+ }
141+ ret.push(file);
142+ ret
143+}
helix-term/src/application.rs+21−7
@@ -495,7 +495,7 @@ impl Application {
495495 ));
496496 }
497497 }
498− Notification::PublishDiagnostics(params) => {
498+ Notification::PublishDiagnostics(mut params) => {
499499 let path = params.uri.to_file_path().unwrap();
500500 let doc = self.editor.document_by_path_mut(&path);
501501
@@ -505,12 +505,9 @@ impl Application {
505505
506506 let diagnostics = params
507507 .diagnostics
508− .into_iter()
508+ .iter()
509509 .filter_map(|diagnostic| {
510− use helix_core::{
511− diagnostic::{Range, Severity::*},
512− Diagnostic,
513− };
510+ use helix_core::diagnostic::{Diagnostic, Range, Severity::*};
514511 use lsp::DiagnosticSeverity;
515512
516513 let language_server = doc.language_server().unwrap();
@@ -561,7 +558,7 @@ impl Application {
561558 Some(Diagnostic {
562559 range: Range { start, end },
563560 line: diagnostic.range.start.line as usize,
564− message: diagnostic.message,
561+ message: diagnostic.message.clone(),
565562 severity,
566563 // code
567564 // source
@@ -571,6 +568,23 @@ impl Application {
571568
572569 doc.set_diagnostics(diagnostics);
573570 }
571+
572+ // Sort diagnostics first by URL and then by severity.
573+ // Note: The `lsp::DiagnosticSeverity` enum is already defined in decreasing order
574+ params.diagnostics.sort_unstable_by(|a, b| {
575+ if let (Some(a), Some(b)) = (a.severity, b.severity) {
576+ a.partial_cmp(&b).unwrap()
577+ } else {
578+ std::cmp::Ordering::Equal
579+ }
580+ });
581+
582+ // Insert the original lsp::Diagnostics here because we may have no open document
583+ // for diagnosic message and so we can't calculate the exact position.
584+ // When using them later in the diagnostics picker, we calculate them on-demand.
585+ self.editor
586+ .diagnostics
587+ .insert(params.uri, params.diagnostics);
574588 }
575589 Notification::ShowMessage(params) => {
576590 log::warn!("unhandled window/showMessage: {:?}", params);
helix-term/src/commands.rs+7−5
@@ -4,6 +4,7 @@ pub(crate) mod typed;
44
55pub use dap::*;
66pub use lsp::*;
7+use tui::text::Spans;
78pub use typed::*;
89
910use helix_core::{
@@ -265,6 +266,8 @@ impl MappableCommand {
265266 symbol_picker, "Open symbol picker",
266267 select_references_to_symbol_under_cursor, "Select symbol references",
267268 workspace_symbol_picker, "Open workspace symbol picker",
269+ diagnostics_picker, "Open diagnostic picker",
270+ workspace_diagnostics_picker, "Open workspace diagnostic picker",
268271 last_picker, "Open last picker",
269272 prepend_to_line, "Insert at start of line",
270273 append_to_line, "Insert at end of line",
@@ -2170,7 +2173,7 @@ fn buffer_picker(cx: &mut Context) {
21702173 }
21712174
21722175 impl BufferMeta {
2173− fn format(&self) -> Cow<str> {
2176+ fn format(&self) -> Spans {
21742177 let path = self
21752178 .path
21762179 .as_deref()
@@ -2193,7 +2196,7 @@ fn buffer_picker(cx: &mut Context) {
21932196 } else {
21942197 format!(" ({})", flags.join(""))
21952198 };
2196− Cow::Owned(format!("{} {}{}", self.id, path, flag))
2199+ format!("{} {}{}", self.id, path, flag).into()
21972200 }
21982201 }
21992202
@@ -2260,10 +2263,9 @@ pub fn command_palette(cx: &mut Context) {
22602263 let picker = Picker::new(
22612264 commands,
22622265 move |command| match command {
2263− MappableCommand::Typable { doc, name, .. } => match keymap.get(name as &String)
2264− {
2266+ MappableCommand::Typable { doc, name, .. } => match keymap.get(name) {
22652267 Some(bindings) => format!("{} ({})", doc, fmt_binding(bindings)).into(),
2266− None => doc.into(),
2268+ None => doc.as_str().into(),
22672269 },
22682270 MappableCommand::Static { doc, name, .. } => match keymap.get(*name) {
22692271 Some(bindings) => format!("{} ({})", doc, fmt_binding(bindings)).into(),
helix-term/src/commands/lsp.rs+131−4
@@ -1,20 +1,25 @@
11use helix_lsp::{
2− block_on, lsp,
2+ block_on,
3+ lsp::{self, DiagnosticSeverity, NumberOrString},
34 util::{diagnostic_to_lsp_diagnostic, lsp_pos_to_pos, lsp_range_to_range, range_to_lsp_range},
45 OffsetEncoding,
56};
7+use tui::text::{Span, Spans};
68
79use super::{align_view, push_jump, Align, Context, Editor};
810
9−use helix_core::Selection;
10−use helix_view::editor::Action;
11+use helix_core::{path, Selection};
12+use helix_view::{
13+ editor::Action,
14+ theme::{Modifier, Style},
15+};
1116
1217use crate::{
1318 compositor::{self, Compositor},
1419 ui::{self, overlay::overlayed, FileLocation, FilePicker, Popup, PromptEvent},
1520};
1621
17−use std::borrow::Cow;
22+use std::{borrow::Cow, collections::BTreeMap};
1823
1924/// Gets the language server that is attached to a document, and
2025/// if it's not active displays a status message. Using this macro
@@ -145,6 +150,97 @@ fn sym_picker(
145150 .truncate_start(false)
146151}
147152
153+fn diag_picker(
154+ cx: &Context,
155+ diagnostics: BTreeMap<lsp::Url, Vec<lsp::Diagnostic>>,
156+ current_path: Option<lsp::Url>,
157+ offset_encoding: OffsetEncoding,
158+) -> FilePicker<(lsp::Url, lsp::Diagnostic)> {
159+ // TODO: drop current_path comparison and instead use workspace: bool flag?
160+
161+ // flatten the map to a vec of (url, diag) pairs
162+ let mut flat_diag = Vec::new();
163+ for (url, diags) in diagnostics {
164+ flat_diag.reserve(diags.len());
165+ for diag in diags {
166+ flat_diag.push((url.clone(), diag));
167+ }
168+ }
169+
170+ let hint = cx.editor.theme.get("hint");
171+ let info = cx.editor.theme.get("info");
172+ let warning = cx.editor.theme.get("warning");
173+ let error = cx.editor.theme.get("error");
174+
175+ FilePicker::new(
176+ flat_diag,
177+ move |(url, diag)| {
178+ let mut style = diag
179+ .severity
180+ .map(|s| match s {
181+ DiagnosticSeverity::HINT => hint,
182+ DiagnosticSeverity::INFORMATION => info,
183+ DiagnosticSeverity::WARNING => warning,
184+ DiagnosticSeverity::ERROR => error,
185+ _ => Style::default(),
186+ })
187+ .unwrap_or_default();
188+
189+ // remove background as it is distracting in the picker list
190+ style.bg = None;
191+
192+ let code = diag
193+ .code
194+ .as_ref()
195+ .map(|c| match c {
196+ NumberOrString::Number(n) => n.to_string(),
197+ NumberOrString::String(s) => s.to_string(),
198+ })
199+ .unwrap_or_default();
200+
201+ let truncated_path = path::get_truncated_path(url.path())
202+ .to_string_lossy()
203+ .into_owned();
204+
205+ Spans::from(vec![
206+ Span::styled(
207+ diag.source.clone().unwrap_or_default(),
208+ style.add_modifier(Modifier::BOLD),
209+ ),
210+ Span::raw(": "),
211+ Span::styled(truncated_path, style),
212+ Span::raw(" - "),
213+ Span::styled(code, style.add_modifier(Modifier::BOLD)),
214+ Span::raw(": "),
215+ Span::styled(&diag.message, style),
216+ ])
217+ },
218+ move |cx, (url, diag), action| {
219+ if current_path.as_ref() == Some(url) {
220+ let (view, doc) = current!(cx.editor);
221+ push_jump(view, doc);
222+ } else {
223+ let path = url.to_file_path().unwrap();
224+ cx.editor.open(&path, action).expect("editor.open failed");
225+ }
226+
227+ let (view, doc) = current!(cx.editor);
228+
229+ if let Some(range) = lsp_range_to_range(doc.text(), diag.range, offset_encoding) {
230+ // we flip the range so that the cursor sits on the start of the symbol
231+ // (for example start of the function).
232+ doc.set_selection(view.id, Selection::single(range.head, range.anchor));
233+ align_view(doc, view, Align::Center);
234+ }
235+ },
236+ move |_editor, (url, diag)| {
237+ let location = lsp::Location::new(url.clone(), diag.range);
238+ Some(location_to_file_location(&location))
239+ },
240+ )
241+ .truncate_start(false)
242+}
243+
148244pub fn symbol_picker(cx: &mut Context) {
149245 fn nested_to_flat(
150246 list: &mut Vec<lsp::SymbolInformation>,
@@ -215,6 +311,37 @@ pub fn workspace_symbol_picker(cx: &mut Context) {
215311 )
216312}
217313
314+pub fn diagnostics_picker(cx: &mut Context) {
315+ let doc = doc!(cx.editor);
316+ let language_server = language_server!(cx.editor, doc);
317+ if let Some(current_url) = doc.url() {
318+ let offset_encoding = language_server.offset_encoding();
319+ let diagnostics = cx
320+ .editor
321+ .diagnostics
322+ .get(&current_url)
323+ .cloned()
324+ .unwrap_or_default();
325+ let picker = diag_picker(
326+ cx,
327+ [(current_url.clone(), diagnostics)].into(),
328+ Some(current_url),
329+ offset_encoding,
330+ );
331+ cx.push_layer(Box::new(overlayed(picker)));
332+ }
333+}
334+
335+pub fn workspace_diagnostics_picker(cx: &mut Context) {
336+ let doc = doc!(cx.editor);
337+ let language_server = language_server!(cx.editor, doc);
338+ let current_url = doc.url();
339+ let offset_encoding = language_server.offset_encoding();
340+ let diagnostics = cx.editor.diagnostics.clone();
341+ let picker = diag_picker(cx, diagnostics, current_url, offset_encoding);
342+ cx.push_layer(Box::new(overlayed(picker)));
343+}
344+
218345impl ui::menu::Item for lsp::CodeActionOrCommand {
219346 fn label(&self) -> &str {
220347 match self {
helix-term/src/keymap/default.rs+2
@@ -207,6 +207,8 @@ pub fn default() -> HashMap<Mode, Keymap> {
207207 "b" => buffer_picker,
208208 "s" => symbol_picker,
209209 "S" => workspace_symbol_picker,
210+ "g" => diagnostics_picker,
211+ "G" => workspace_diagnostics_picker,
210212 "a" => code_action,
211213 "'" => last_picker,
212214 "d" => { "Debug (experimental)" sticky=true
helix-term/src/ui/mod.rs+3−1
@@ -23,6 +23,8 @@ pub use text::Text;
2323use helix_core::regex::Regex;
2424use helix_core::regex::RegexBuilder;
2525use helix_view::{Document, Editor, View};
26+use tui;
27+use tui::text::Spans;
2628
2729use std::path::PathBuf;
2830
@@ -172,7 +174,7 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi
172174 files,
173175 move |path: &PathBuf| {
174176 // format_fn
175− path.strip_prefix(&root).unwrap_or(path).to_string_lossy()
177+ Spans::from(path.strip_prefix(&root).unwrap_or(path).to_string_lossy())
176178 },
177179 move |cx, path: &PathBuf, action| {
178180 if let Err(e) = cx.editor.open(path, action) {
helix-term/src/ui/picker.rs+32−29
@@ -6,6 +6,7 @@ use crate::{
66use crossterm::event::Event;
77use tui::{
88 buffer::Buffer as Surface,
9+ text::Spans,
910 widgets::{Block, BorderType, Borders},
1011};
1112
@@ -15,7 +16,6 @@ use tui::widgets::Widget;
1516
1617use std::time::Instant;
1718use std::{
18− borrow::Cow,
1919 cmp::Reverse,
2020 collections::HashMap,
2121 io::Read,
@@ -87,7 +87,7 @@ impl Preview<'_, '_> {
8787impl<T> FilePicker<T> {
8888 pub fn new(
8989 options: Vec<T>,
90− format_fn: impl Fn(&T) -> Cow<str> + 'static,
90+ format_fn: impl Fn(&T) -> Spans + 'static,
9191 callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
9292 preview_fn: impl Fn(&Editor, &T) -> Option<FileLocation> + 'static,
9393 ) -> Self {
@@ -299,14 +299,14 @@ pub struct Picker<T> {
299299 /// Whether to truncate the start (default true)
300300 pub truncate_start: bool,
301301
302− format_fn: Box<dyn Fn(&T) -> Cow<str>>,
302+ format_fn: Box<dyn Fn(&T) -> Spans>,
303303 callback_fn: Box<dyn Fn(&mut Context, &T, Action)>,
304304}
305305
306306impl<T> Picker<T> {
307307 pub fn new(
308308 options: Vec<T>,
309− format_fn: impl Fn(&T) -> Cow<str> + 'static,
309+ format_fn: impl Fn(&T) -> Spans + 'static,
310310 callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
311311 ) -> Self {
312312 let prompt = Prompt::new(
@@ -372,9 +372,8 @@ impl<T> Picker<T> {
372372 self.matches.retain_mut(|(index, score)| {
373373 let option = &self.options[*index];
374374 // TODO: maybe using format_fn isn't the best idea here
375− let text = (self.format_fn)(option);
376−
377− match self.matcher.fuzzy_match(&text, pattern) {
375+ let line: String = (self.format_fn)(option).into();
376+ match self.matcher.fuzzy_match(&line, pattern) {
378377 Some(s) => {
379378 // Update the score
380379 *score = s;
@@ -401,10 +400,10 @@ impl<T> Picker<T> {
401400 }
402401
403402 // TODO: maybe using format_fn isn't the best idea here
404− let text = (self.format_fn)(option);
403+ let line: String = (self.format_fn)(option).into();
405404
406405 self.matcher
407− .fuzzy_match(&text, pattern)
406+ .fuzzy_match(&line, pattern)
408407 .map(|score| (index, score))
409408 }),
410409 );
@@ -611,30 +610,34 @@ impl<T: 'static> Component for Picker<T> {
611610 surface.set_string(inner.x.saturating_sub(2), inner.y + i as u16, ">", selected);
612611 }
613612
614− let formatted = (self.format_fn)(option);
615−
613+ let spans = (self.format_fn)(option);
616614 let (_score, highlights) = self
617615 .matcher
618− .fuzzy_indices(&formatted, self.prompt.line())
616+ .fuzzy_indices(&String::from(&spans), self.prompt.line())
619617 .unwrap_or_default();
620618
621− surface.set_string_truncated(
622− inner.x,
623− inner.y + i as u16,
624− &formatted,
625− inner.width as usize,
626− |idx| {
627− if highlights.contains(&idx) {
628− highlighted
629− } else if is_active {
630− selected
631− } else {
632− text_style
633− }
634− },
635− true,
636− self.truncate_start,
637− );
619+ spans.0.into_iter().fold(inner, |pos, span| {
620+ let new_x = surface
621+ .set_string_truncated(
622+ pos.x,
623+ pos.y + i as u16,
624+ &span.content,
625+ pos.width as usize,
626+ |idx| {
627+ if highlights.contains(&idx) {
628+ highlighted.patch(span.style)
629+ } else if is_active {
630+ selected.patch(span.style)
631+ } else {
632+ text_style.patch(span.style)
633+ }
634+ },
635+ true,
636+ self.truncate_start,
637+ )
638+ .0;
639+ pos.clip_left(new_x - pos.x)
640+ });
638641 }
639642 }
640643
helix-tui/src/text.rs+19−4
@@ -194,6 +194,12 @@ impl<'a> From<&'a str> for Span<'a> {
194194 }
195195}
196196
197+impl<'a> From<Cow<'a, str>> for Span<'a> {
198+ fn from(s: Cow<'a, str>) -> Span<'a> {
199+ Span::raw(s)
200+ }
201+}
202+
197203/// A string composed of clusters of graphemes, each with their own style.
198204#[derive(Debug, Default, Clone, PartialEq)]
199205pub struct Spans<'a>(pub Vec<Span<'a>>);
@@ -229,6 +235,12 @@ impl<'a> From<&'a str> for Spans<'a> {
229235 }
230236}
231237
238+impl<'a> From<Cow<'a, str>> for Spans<'a> {
239+ fn from(s: Cow<'a, str>) -> Spans<'a> {
240+ Spans(vec![Span::raw(s)])
241+ }
242+}
243+
232244impl<'a> From<Vec<Span<'a>>> for Spans<'a> {
233245 fn from(spans: Vec<Span<'a>>) -> Spans<'a> {
234246 Spans(spans)
@@ -243,10 +255,13 @@ impl<'a> From<Span<'a>> for Spans<'a> {
243255
244256impl<'a> From<Spans<'a>> for String {
245257 fn from(line: Spans<'a>) -> String {
246− line.0.iter().fold(String::new(), |mut acc, s| {
247− acc.push_str(s.content.as_ref());
248− acc
249− })
258+ line.0.iter().map(|s| &*s.content).collect()
259+ }
260+}
261+
262+impl<'a> From<&Spans<'a>> for String {
263+ fn from(line: &Spans<'a>) -> String {
264+ line.0.iter().map(|s| &*s.content).collect()
250265 }
251266}
252267
helix-tui/src/widgets/block.rs+1−1
@@ -77,7 +77,7 @@ impl<'a> Block<'a> {
7777 )]
7878 pub fn title_style(mut self, style: Style) -> Block<'a> {
7979 if let Some(t) = self.title {
80− let title = String::from(t);
80+ let title = String::from(&t);
8181 self.title = Some(Spans::from(Span::styled(title, style)));
8282 }
8383 self
helix-view/src/editor.rs+3
@@ -39,6 +39,7 @@ use helix_core::{
3939 Change,
4040};
4141use helix_dap as dap;
42+use helix_lsp::lsp;
4243
4344use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer};
4445
@@ -462,6 +463,7 @@ pub struct Editor {
462463 pub macro_replaying: Vec<char>,
463464 pub theme: Theme,
464465 pub language_servers: helix_lsp::Registry,
466+ pub diagnostics: BTreeMap<lsp::Url, Vec<lsp::Diagnostic>>,
465467
466468 pub debugger: Option<dap::Client>,
467469 pub debugger_events: SelectAll<UnboundedReceiverStream<dap::Payload>>,
@@ -533,6 +535,7 @@ impl Editor {
533535 macro_replaying: Vec::new(),
534536 theme: theme_loader.default(),
535537 language_servers,
538+ diagnostics: BTreeMap::new(),
536539 debugger: None,
537540 debugger_events: SelectAll::new(),
538541 breakpoints: HashMap::new(),
xtask/src/main.rs+1−1
@@ -106,7 +106,7 @@ pub mod md_gen {
106106 .collect::<Vec<_>>()
107107 .join(", ");
108108
109− let doc = cmd.doc.replace("\n", "<br>");
109+ let doc = cmd.doc.replace('\n', "<br>");
110110
111111 md.push_str(&md_table_row(&[names.to_owned(), doc.to_owned()]));
112112 }