Skip to content

Commit

Permalink
Add workspace and document diagnostics picker (#2013)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

* 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 <[email protected]>
Co-authored-by: Bjorn Ove Hay Andersen <[email protected]>
  • Loading branch information
3 people authored Jun 30, 2022
1 parent 94fc41a commit ed89f88
Show file tree
Hide file tree
Showing 12 changed files with 273 additions and 52 deletions.
2 changes: 2 additions & 0 deletions book/src/keymap.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,8 @@ This layer is a kludge of mappings, mostly pickers.
| `k` | Show documentation for item under cursor in a [popup](#popup) (**LSP**) | `hover` |
| `s` | Open document symbol picker (**LSP**) | `symbol_picker` |
| `S` | Open workspace symbol picker (**LSP**) | `workspace_symbol_picker` |
| `g` | Open document diagnosics picker (**LSP**) | `diagnostics_picker` |
| `G` | Open workspace diagnostics picker (**LSP**) | `workspace_diagnosics_picker`
| `r` | Rename symbol (**LSP**) | `rename_symbol` |
| `a` | Apply code action (**LSP**) | `code_action` |
| `'` | Open last fuzzy picker | `last_picker` |
Expand Down
51 changes: 51 additions & 0 deletions helix-core/src/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,54 @@ pub fn get_relative_path(path: &Path) -> PathBuf {
};
fold_home_dir(path)
}

/// Returns a truncated filepath where the basepart of the path is reduced to the first
/// char of the folder and the whole filename appended.
///
/// Also strip the current working directory from the beginning of the path.
/// Note that this function does not check if the truncated path is unambiguous.
///
/// ```
/// use helix_core::path::get_truncated_path;
/// use std::path::Path;
///
/// assert_eq!(
/// get_truncated_path("/home/cnorris/documents/jokes.txt").as_path(),
/// Path::new("/h/c/d/jokes.txt")
/// );
/// assert_eq!(
/// get_truncated_path("jokes.txt").as_path(),
/// Path::new("jokes.txt")
/// );
/// assert_eq!(
/// get_truncated_path("/jokes.txt").as_path(),
/// Path::new("/jokes.txt")
/// );
/// assert_eq!(
/// get_truncated_path("/h/c/d/jokes.txt").as_path(),
/// Path::new("/h/c/d/jokes.txt")
/// );
/// assert_eq!(get_truncated_path("").as_path(), Path::new(""));
/// ```
///
pub fn get_truncated_path<P: AsRef<Path>>(path: P) -> PathBuf {
let cwd = std::env::current_dir().unwrap_or_default();
let path = path
.as_ref()
.strip_prefix(cwd)
.unwrap_or_else(|_| path.as_ref());
let file = path.file_name().unwrap_or_default();
let base = path.parent().unwrap_or_else(|| Path::new(""));
let mut ret = PathBuf::new();
for d in base {
ret.push(
d.to_string_lossy()
.chars()
.next()
.unwrap_or_default()
.to_string(),
);
}
ret.push(file);
ret
}
28 changes: 21 additions & 7 deletions helix-term/src/application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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();
Expand Down Expand Up @@ -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
Expand All @@ -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);
Expand Down
12 changes: 7 additions & 5 deletions helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pub(crate) mod typed;

pub use dap::*;
pub use lsp::*;
use tui::text::Spans;
pub use typed::*;

use helix_core::{
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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()
Expand All @@ -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()
}
}

Expand Down Expand Up @@ -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(),
Expand Down
135 changes: 131 additions & 4 deletions helix-term/src/commands/lsp.rs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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>,
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions helix-term/src/keymap/default.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion helix-term/src/ui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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) {
Expand Down
Loading

0 comments on commit ed89f88

Please sign in to comment.