aboutsummaryrefslogtreecommitdiff
path: root/helix-term
diff options
context:
space:
mode:
authorFalco Hirschenberger2022-06-30 09:16:18 +0000
committerGitHub2022-06-30 09:16:18 +0000
commited89f8897eab84bf7614a718d5d1e3ec5c57086c (patch)
tree347bd0db6509b1c4a6e88f6ca7e461dc8a923215 /helix-term
parent94fc41a41920fc705f01637e7902f06a1c32d998 (diff)
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>
Diffstat (limited to 'helix-term')
-rw-r--r--helix-term/src/application.rs28
-rw-r--r--helix-term/src/commands.rs12
-rw-r--r--helix-term/src/commands/lsp.rs135
-rw-r--r--helix-term/src/keymap/default.rs2
-rw-r--r--helix-term/src/ui/mod.rs4
-rw-r--r--helix-term/src/ui/picker.rs61
6 files changed, 196 insertions, 46 deletions
diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs
index 48e9c275..23f4610f 100644
--- a/helix-term/src/application.rs
+++ b/helix-term/src/application.rs
@@ -495,7 +495,7 @@ impl Application {
));
}
}
- Notification::PublishDiagnostics(params) => {
+ Notification::PublishDiagnostics(mut params) => {
let path = params.uri.to_file_path().unwrap();
let doc = self.editor.document_by_path_mut(&path);
@@ -505,12 +505,9 @@ impl Application {
let diagnostics = params
.diagnostics
- .into_iter()
+ .iter()
.filter_map(|diagnostic| {
- use helix_core::{
- diagnostic::{Range, Severity::*},
- Diagnostic,
- };
+ use helix_core::diagnostic::{Diagnostic, Range, Severity::*};
use lsp::DiagnosticSeverity;
let language_server = doc.language_server().unwrap();
@@ -561,7 +558,7 @@ impl Application {
Some(Diagnostic {
range: Range { start, end },
line: diagnostic.range.start.line as usize,
- message: diagnostic.message,
+ message: diagnostic.message.clone(),
severity,
// code
// source
@@ -571,6 +568,23 @@ impl Application {
doc.set_diagnostics(diagnostics);
}
+
+ // Sort diagnostics first by URL and then by severity.
+ // Note: The `lsp::DiagnosticSeverity` enum is already defined in decreasing order
+ params.diagnostics.sort_unstable_by(|a, b| {
+ if let (Some(a), Some(b)) = (a.severity, b.severity) {
+ a.partial_cmp(&b).unwrap()
+ } else {
+ std::cmp::Ordering::Equal
+ }
+ });
+
+ // Insert the original lsp::Diagnostics here because we may have no open document
+ // for diagnosic message and so we can't calculate the exact position.
+ // When using them later in the diagnostics picker, we calculate them on-demand.
+ self.editor
+ .diagnostics
+ .insert(params.uri, params.diagnostics);
}
Notification::ShowMessage(params) => {
log::warn!("unhandled window/showMessage: {:?}", params);
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index d1bec0ce..bc8e6530 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -4,6 +4,7 @@ pub(crate) mod typed;
pub use dap::*;
pub use lsp::*;
+use tui::text::Spans;
pub use typed::*;
use helix_core::{
@@ -265,6 +266,8 @@ impl MappableCommand {
symbol_picker, "Open symbol picker",
select_references_to_symbol_under_cursor, "Select symbol references",
workspace_symbol_picker, "Open workspace symbol picker",
+ diagnostics_picker, "Open diagnostic picker",
+ workspace_diagnostics_picker, "Open workspace diagnostic picker",
last_picker, "Open last picker",
prepend_to_line, "Insert at start of line",
append_to_line, "Insert at end of line",
@@ -2170,7 +2173,7 @@ fn buffer_picker(cx: &mut Context) {
}
impl BufferMeta {
- fn format(&self) -> Cow<str> {
+ fn format(&self) -> Spans {
let path = self
.path
.as_deref()
@@ -2193,7 +2196,7 @@ fn buffer_picker(cx: &mut Context) {
} else {
format!(" ({})", flags.join(""))
};
- Cow::Owned(format!("{} {}{}", self.id, path, flag))
+ format!("{} {}{}", self.id, path, flag).into()
}
}
@@ -2260,10 +2263,9 @@ pub fn command_palette(cx: &mut Context) {
let picker = Picker::new(
commands,
move |command| match command {
- MappableCommand::Typable { doc, name, .. } => match keymap.get(name as &String)
- {
+ MappableCommand::Typable { doc, name, .. } => match keymap.get(name) {
Some(bindings) => format!("{} ({})", doc, fmt_binding(bindings)).into(),
- None => doc.into(),
+ None => doc.as_str().into(),
},
MappableCommand::Static { doc, name, .. } => match keymap.get(*name) {
Some(bindings) => format!("{} ({})", doc, fmt_binding(bindings)).into(),
diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs
index c1cdb164..d11c44cd 100644
--- a/helix-term/src/commands/lsp.rs
+++ b/helix-term/src/commands/lsp.rs
@@ -1,20 +1,25 @@
use helix_lsp::{
- block_on, lsp,
+ block_on,
+ lsp::{self, DiagnosticSeverity, NumberOrString},
util::{diagnostic_to_lsp_diagnostic, lsp_pos_to_pos, lsp_range_to_range, range_to_lsp_range},
OffsetEncoding,
};
+use tui::text::{Span, Spans};
use super::{align_view, push_jump, Align, Context, Editor};
-use helix_core::Selection;
-use helix_view::editor::Action;
+use helix_core::{path, Selection};
+use helix_view::{
+ editor::Action,
+ theme::{Modifier, Style},
+};
use crate::{
compositor::{self, Compositor},
ui::{self, overlay::overlayed, FileLocation, FilePicker, Popup, PromptEvent},
};
-use std::borrow::Cow;
+use std::{borrow::Cow, collections::BTreeMap};
/// Gets the language server that is attached to a document, and
/// if it's not active displays a status message. Using this macro
@@ -145,6 +150,97 @@ fn sym_picker(
.truncate_start(false)
}
+fn diag_picker(
+ cx: &Context,
+ diagnostics: BTreeMap<lsp::Url, Vec<lsp::Diagnostic>>,
+ current_path: Option<lsp::Url>,
+ offset_encoding: OffsetEncoding,
+) -> FilePicker<(lsp::Url, lsp::Diagnostic)> {
+ // TODO: drop current_path comparison and instead use workspace: bool flag?
+
+ // flatten the map to a vec of (url, diag) pairs
+ let mut flat_diag = Vec::new();
+ for (url, diags) in diagnostics {
+ flat_diag.reserve(diags.len());
+ for diag in diags {
+ flat_diag.push((url.clone(), diag));
+ }
+ }
+
+ let hint = cx.editor.theme.get("hint");
+ let info = cx.editor.theme.get("info");
+ let warning = cx.editor.theme.get("warning");
+ let error = cx.editor.theme.get("error");
+
+ FilePicker::new(
+ flat_diag,
+ move |(url, diag)| {
+ let mut style = diag
+ .severity
+ .map(|s| match s {
+ DiagnosticSeverity::HINT => hint,
+ DiagnosticSeverity::INFORMATION => info,
+ DiagnosticSeverity::WARNING => warning,
+ DiagnosticSeverity::ERROR => error,
+ _ => Style::default(),
+ })
+ .unwrap_or_default();
+
+ // remove background as it is distracting in the picker list
+ style.bg = None;
+
+ let code = diag
+ .code
+ .as_ref()
+ .map(|c| match c {
+ NumberOrString::Number(n) => n.to_string(),
+ NumberOrString::String(s) => s.to_string(),
+ })
+ .unwrap_or_default();
+
+ let truncated_path = path::get_truncated_path(url.path())
+ .to_string_lossy()
+ .into_owned();
+
+ Spans::from(vec![
+ Span::styled(
+ diag.source.clone().unwrap_or_default(),
+ style.add_modifier(Modifier::BOLD),
+ ),
+ Span::raw(": "),
+ Span::styled(truncated_path, style),
+ Span::raw(" - "),
+ Span::styled(code, style.add_modifier(Modifier::BOLD)),
+ Span::raw(": "),
+ Span::styled(&diag.message, style),
+ ])
+ },
+ move |cx, (url, diag), action| {
+ if current_path.as_ref() == Some(url) {
+ let (view, doc) = current!(cx.editor);
+ push_jump(view, doc);
+ } else {
+ let path = url.to_file_path().unwrap();
+ cx.editor.open(&path, action).expect("editor.open failed");
+ }
+
+ let (view, doc) = current!(cx.editor);
+
+ if let Some(range) = lsp_range_to_range(doc.text(), diag.range, offset_encoding) {
+ // we flip the range so that the cursor sits on the start of the symbol
+ // (for example start of the function).
+ doc.set_selection(view.id, Selection::single(range.head, range.anchor));
+ align_view(doc, view, Align::Center);
+ }
+ },
+ move |_editor, (url, diag)| {
+ let location = lsp::Location::new(url.clone(), diag.range);
+ Some(location_to_file_location(&location))
+ },
+ )
+ .truncate_start(false)
+}
+
pub fn symbol_picker(cx: &mut Context) {
fn nested_to_flat(
list: &mut Vec<lsp::SymbolInformation>,
@@ -215,6 +311,37 @@ pub fn workspace_symbol_picker(cx: &mut Context) {
)
}
+pub fn diagnostics_picker(cx: &mut Context) {
+ let doc = doc!(cx.editor);
+ let language_server = language_server!(cx.editor, doc);
+ if let Some(current_url) = doc.url() {
+ let offset_encoding = language_server.offset_encoding();
+ let diagnostics = cx
+ .editor
+ .diagnostics
+ .get(&current_url)
+ .cloned()
+ .unwrap_or_default();
+ let picker = diag_picker(
+ cx,
+ [(current_url.clone(), diagnostics)].into(),
+ Some(current_url),
+ offset_encoding,
+ );
+ cx.push_layer(Box::new(overlayed(picker)));
+ }
+}
+
+pub fn workspace_diagnostics_picker(cx: &mut Context) {
+ let doc = doc!(cx.editor);
+ let language_server = language_server!(cx.editor, doc);
+ let current_url = doc.url();
+ let offset_encoding = language_server.offset_encoding();
+ let diagnostics = cx.editor.diagnostics.clone();
+ let picker = diag_picker(cx, diagnostics, current_url, offset_encoding);
+ cx.push_layer(Box::new(overlayed(picker)));
+}
+
impl ui::menu::Item for lsp::CodeActionOrCommand {
fn label(&self) -> &str {
match self {
diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs
index e5bb0482..29b0b670 100644
--- a/helix-term/src/keymap/default.rs
+++ b/helix-term/src/keymap/default.rs
@@ -207,6 +207,8 @@ pub fn default() -> HashMap<Mode, Keymap> {
"b" => buffer_picker,
"s" => symbol_picker,
"S" => workspace_symbol_picker,
+ "g" => diagnostics_picker,
+ "G" => workspace_diagnostics_picker,
"a" => code_action,
"'" => last_picker,
"d" => { "Debug (experimental)" sticky=true
diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs
index 47a68a18..c1e5c988 100644
--- a/helix-term/src/ui/mod.rs
+++ b/helix-term/src/ui/mod.rs
@@ -23,6 +23,8 @@ pub use text::Text;
use helix_core::regex::Regex;
use helix_core::regex::RegexBuilder;
use helix_view::{Document, Editor, View};
+use tui;
+use tui::text::Spans;
use std::path::PathBuf;
@@ -172,7 +174,7 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi
files,
move |path: &PathBuf| {
// format_fn
- path.strip_prefix(&root).unwrap_or(path).to_string_lossy()
+ Spans::from(path.strip_prefix(&root).unwrap_or(path).to_string_lossy())
},
move |cx, path: &PathBuf, action| {
if let Err(e) = cx.editor.open(path, action) {
diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs
index ebff9827..1581b0a1 100644
--- a/helix-term/src/ui/picker.rs
+++ b/helix-term/src/ui/picker.rs
@@ -6,6 +6,7 @@ use crate::{
use crossterm::event::Event;
use tui::{
buffer::Buffer as Surface,
+ text::Spans,
widgets::{Block, BorderType, Borders},
};
@@ -15,7 +16,6 @@ use tui::widgets::Widget;
use std::time::Instant;
use std::{
- borrow::Cow,
cmp::Reverse,
collections::HashMap,
io::Read,
@@ -87,7 +87,7 @@ impl Preview<'_, '_> {
impl<T> FilePicker<T> {
pub fn new(
options: Vec<T>,
- format_fn: impl Fn(&T) -> Cow<str> + 'static,
+ format_fn: impl Fn(&T) -> Spans + 'static,
callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
preview_fn: impl Fn(&Editor, &T) -> Option<FileLocation> + 'static,
) -> Self {
@@ -299,14 +299,14 @@ pub struct Picker<T> {
/// Whether to truncate the start (default true)
pub truncate_start: bool,
- format_fn: Box<dyn Fn(&T) -> Cow<str>>,
+ format_fn: Box<dyn Fn(&T) -> Spans>,
callback_fn: Box<dyn Fn(&mut Context, &T, Action)>,
}
impl<T> Picker<T> {
pub fn new(
options: Vec<T>,
- format_fn: impl Fn(&T) -> Cow<str> + 'static,
+ format_fn: impl Fn(&T) -> Spans + 'static,
callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
) -> Self {
let prompt = Prompt::new(
@@ -372,9 +372,8 @@ impl<T> Picker<T> {
self.matches.retain_mut(|(index, score)| {
let option = &self.options[*index];
// TODO: maybe using format_fn isn't the best idea here
- let text = (self.format_fn)(option);
-
- match self.matcher.fuzzy_match(&text, pattern) {
+ let line: String = (self.format_fn)(option).into();
+ match self.matcher.fuzzy_match(&line, pattern) {
Some(s) => {
// Update the score
*score = s;
@@ -401,10 +400,10 @@ impl<T> Picker<T> {
}
// TODO: maybe using format_fn isn't the best idea here
- let text = (self.format_fn)(option);
+ let line: String = (self.format_fn)(option).into();
self.matcher
- .fuzzy_match(&text, pattern)
+ .fuzzy_match(&line, pattern)
.map(|score| (index, score))
}),
);
@@ -611,30 +610,34 @@ impl<T: 'static> Component for Picker<T> {
surface.set_string(inner.x.saturating_sub(2), inner.y + i as u16, ">", selected);
}
- let formatted = (self.format_fn)(option);
-
+ let spans = (self.format_fn)(option);
let (_score, highlights) = self
.matcher
- .fuzzy_indices(&formatted, self.prompt.line())
+ .fuzzy_indices(&String::from(&spans), self.prompt.line())
.unwrap_or_default();
- surface.set_string_truncated(
- inner.x,
- inner.y + i as u16,
- &formatted,
- inner.width as usize,
- |idx| {
- if highlights.contains(&idx) {
- highlighted
- } else if is_active {
- selected
- } else {
- text_style
- }
- },
- true,
- self.truncate_start,
- );
+ spans.0.into_iter().fold(inner, |pos, span| {
+ let new_x = surface
+ .set_string_truncated(
+ pos.x,
+ pos.y + i as u16,
+ &span.content,
+ pos.width as usize,
+ |idx| {
+ if highlights.contains(&idx) {
+ highlighted.patch(span.style)
+ } else if is_active {
+ selected.patch(span.style)
+ } else {
+ text_style.patch(span.style)
+ }
+ },
+ true,
+ self.truncate_start,
+ )
+ .0;
+ pos.clip_left(new_x - pos.x)
+ });
}
}