@@ -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(¤t_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 {