on master help sign in
Add workspace and document diagnostics picker (#2013)
ed89f88Falco Hirschenberger
book/src/keymap.md+2
@@ -241,6 +241,8 @@ This layer is a kludge of mappings, mostly pickers.
241| `k` | Show documentation for item under cursor in a [popup](#popup) (**LSP**) | `hover` |
241| `k` | Show documentation for item under cursor in a [popup](#popup) (**LSP**) | `hover` |
242| `s` | Open document symbol picker (**LSP**) | `symbol_picker` |
242| `s` | Open document symbol picker (**LSP**) | `symbol_picker` |
243| `S` | Open workspace symbol picker (**LSP**) | `workspace_symbol_picker` |
243| `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`
244| `r` | Rename symbol (**LSP**) | `rename_symbol` |
246| `r` | Rename symbol (**LSP**) | `rename_symbol` |
245| `a` | Apply code action (**LSP**) | `code_action` |
247| `a` | Apply code action (**LSP**) | `code_action` |
246| `'` | Open last fuzzy picker | `last_picker` |
248| `'` | Open last fuzzy picker | `last_picker` |
helix-core/src/path.rs+51
@@ -90,3 +90,54 @@ pub fn get_relative_path(path: &Path) -> PathBuf {
90 };
90 };
91 fold_home_dir(path)
91 fold_home_dir(path)
92}
92}
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 {
495 ));
495 ));
496 }
496 }
497 }
497 }
498− Notification::PublishDiagnostics(params) => {
498+ Notification::PublishDiagnostics(mut params) => {
499 let path = params.uri.to_file_path().unwrap();
499 let path = params.uri.to_file_path().unwrap();
500 let doc = self.editor.document_by_path_mut(&path);
500 let doc = self.editor.document_by_path_mut(&path);
501
501
@@ -505,12 +505,9 @@ impl Application {
505
505
506 let diagnostics = params
506 let diagnostics = params
507 .diagnostics
507 .diagnostics
508− .into_iter()
508+ .iter()
509 .filter_map(|diagnostic| {
509 .filter_map(|diagnostic| {
510− use helix_core::{
511− diagnostic::{Range, Severity::*},
510+ use helix_core::diagnostic::{Diagnostic, Range, Severity::*};
512− Diagnostic,
513− };
514 use lsp::DiagnosticSeverity;
511 use lsp::DiagnosticSeverity;
515
512
516 let language_server = doc.language_server().unwrap();
513 let language_server = doc.language_server().unwrap();
@@ -561,7 +558,7 @@ impl Application {
561 Some(Diagnostic {
558 Some(Diagnostic {
562 range: Range { start, end },
559 range: Range { start, end },
563 line: diagnostic.range.start.line as usize,
560 line: diagnostic.range.start.line as usize,
564− message: diagnostic.message,
561+ message: diagnostic.message.clone(),
565 severity,
562 severity,
566 // code
563 // code
567 // source
564 // source
@@ -571,6 +568,23 @@ impl Application {
571
568
572 doc.set_diagnostics(diagnostics);
569 doc.set_diagnostics(diagnostics);
573 }
570 }
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);
574 }
588 }
575 Notification::ShowMessage(params) => {
589 Notification::ShowMessage(params) => {
576 log::warn!("unhandled window/showMessage: {:?}", params);
590 log::warn!("unhandled window/showMessage: {:?}", params);
helix-term/src/commands.rs+7−5
@@ -4,6 +4,7 @@ pub(crate) mod typed;
4
4
5pub use dap::*;
5pub use dap::*;
6pub use lsp::*;
6pub use lsp::*;
7+use tui::text::Spans;
7pub use typed::*;
8pub use typed::*;
8
9
9use helix_core::{
10use helix_core::{
@@ -265,6 +266,8 @@ impl MappableCommand {
265 symbol_picker, "Open symbol picker",
266 symbol_picker, "Open symbol picker",
266 select_references_to_symbol_under_cursor, "Select symbol references",
267 select_references_to_symbol_under_cursor, "Select symbol references",
267 workspace_symbol_picker, "Open workspace symbol picker",
268 workspace_symbol_picker, "Open workspace symbol picker",
269+ diagnostics_picker, "Open diagnostic picker",
270+ workspace_diagnostics_picker, "Open workspace diagnostic picker",
268 last_picker, "Open last picker",
271 last_picker, "Open last picker",
269 prepend_to_line, "Insert at start of line",
272 prepend_to_line, "Insert at start of line",
270 append_to_line, "Insert at end of line",
273 append_to_line, "Insert at end of line",
@@ -2170,7 +2173,7 @@ fn buffer_picker(cx: &mut Context) {
2170 }
2173 }
2171
2174
2172 impl BufferMeta {
2175 impl BufferMeta {
2173− fn format(&self) -> Cow<str> {
2176+ fn format(&self) -> Spans {
2174 let path = self
2177 let path = self
2175 .path
2178 .path
2176 .as_deref()
2179 .as_deref()
@@ -2193,7 +2196,7 @@ fn buffer_picker(cx: &mut Context) {
2193 } else {
2196 } else {
2194 format!(" ({})", flags.join(""))
2197 format!(" ({})", flags.join(""))
2195 };
2198 };
2196− Cow::Owned(format!("{} {}{}", self.id, path, flag))
2199+ format!("{} {}{}", self.id, path, flag).into()
2197 }
2200 }
2198 }
2201 }
2199
2202
@@ -2260,10 +2263,9 @@ pub fn command_palette(cx: &mut Context) {
2260 let picker = Picker::new(
2263 let picker = Picker::new(
2261 commands,
2264 commands,
2262 move |command| match command {
2265 move |command| match command {
2263− MappableCommand::Typable { doc, name, .. } => match keymap.get(name as &String)
2266+ MappableCommand::Typable { doc, name, .. } => match keymap.get(name) {
2264− {
2265 Some(bindings) => format!("{} ({})", doc, fmt_binding(bindings)).into(),
2267 Some(bindings) => format!("{} ({})", doc, fmt_binding(bindings)).into(),
2266− None => doc.into(),
2268+ None => doc.as_str().into(),
2267 },
2269 },
2268 MappableCommand::Static { doc, name, .. } => match keymap.get(*name) {
2270 MappableCommand::Static { doc, name, .. } => match keymap.get(*name) {
2269 Some(bindings) => format!("{} ({})", doc, fmt_binding(bindings)).into(),
2271 Some(bindings) => format!("{} ({})", doc, fmt_binding(bindings)).into(),
helix-term/src/commands/lsp.rs+131−4
@@ -1,20 +1,25 @@
1use helix_lsp::{
1use helix_lsp::{
2− block_on, lsp,
2+ block_on,
3+ lsp::{self, DiagnosticSeverity, NumberOrString},
3 util::{diagnostic_to_lsp_diagnostic, lsp_pos_to_pos, lsp_range_to_range, range_to_lsp_range},
4 util::{diagnostic_to_lsp_diagnostic, lsp_pos_to_pos, lsp_range_to_range, range_to_lsp_range},
4 OffsetEncoding,
5 OffsetEncoding,
5};
6};
7+use tui::text::{Span, Spans};
6
8
7use super::{align_view, push_jump, Align, Context, Editor};
9use super::{align_view, push_jump, Align, Context, Editor};
8
10
9−use helix_core::Selection;
11+use helix_core::{path, Selection};
10−use helix_view::editor::Action;
12+use helix_view::{
13+ editor::Action,
14+ theme::{Modifier, Style},
15+};
11
16
12use crate::{
17use crate::{
13 compositor::{self, Compositor},
18 compositor::{self, Compositor},
14 ui::{self, overlay::overlayed, FileLocation, FilePicker, Popup, PromptEvent},
19 ui::{self, overlay::overlayed, FileLocation, FilePicker, Popup, PromptEvent},
15};
20};
16
21
17−use std::borrow::Cow;
22+use std::{borrow::Cow, collections::BTreeMap};
18
23
19/// Gets the language server that is attached to a document, and
24/// Gets the language server that is attached to a document, and
20/// if it's not active displays a status message. Using this macro
25/// if it's not active displays a status message. Using this macro
@@ -145,6 +150,97 @@ fn sym_picker(
145 .truncate_start(false)
150 .truncate_start(false)
146}
151}
147
152
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+
148pub fn symbol_picker(cx: &mut Context) {
244pub fn symbol_picker(cx: &mut Context) {
149 fn nested_to_flat(
245 fn nested_to_flat(
150 list: &mut Vec<lsp::SymbolInformation>,
246 list: &mut Vec<lsp::SymbolInformation>,
@@ -215,6 +311,37 @@ pub fn workspace_symbol_picker(cx: &mut Context) {
215 )
311 )
216}
312}
217
313
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+
218impl ui::menu::Item for lsp::CodeActionOrCommand {
345impl ui::menu::Item for lsp::CodeActionOrCommand {
219 fn label(&self) -> &str {
346 fn label(&self) -> &str {
220 match self {
347 match self {
helix-term/src/keymap/default.rs+2
@@ -207,6 +207,8 @@ pub fn default() -> HashMap<Mode, Keymap> {
207 "b" => buffer_picker,
207 "b" => buffer_picker,
208 "s" => symbol_picker,
208 "s" => symbol_picker,
209 "S" => workspace_symbol_picker,
209 "S" => workspace_symbol_picker,
210+ "g" => diagnostics_picker,
211+ "G" => workspace_diagnostics_picker,
210 "a" => code_action,
212 "a" => code_action,
211 "'" => last_picker,
213 "'" => last_picker,
212 "d" => { "Debug (experimental)" sticky=true
214 "d" => { "Debug (experimental)" sticky=true
helix-term/src/ui/mod.rs+3−1
@@ -23,6 +23,8 @@ pub use text::Text;
23use helix_core::regex::Regex;
23use helix_core::regex::Regex;
24use helix_core::regex::RegexBuilder;
24use helix_core::regex::RegexBuilder;
25use helix_view::{Document, Editor, View};
25use helix_view::{Document, Editor, View};
26+use tui;
27+use tui::text::Spans;
26
28
27use std::path::PathBuf;
29use std::path::PathBuf;
28
30
@@ -172,7 +174,7 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi
172 files,
174 files,
173 move |path: &PathBuf| {
175 move |path: &PathBuf| {
174 // format_fn
176 // 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())
176 },
178 },
177 move |cx, path: &PathBuf, action| {
179 move |cx, path: &PathBuf, action| {
178 if let Err(e) = cx.editor.open(path, action) {
180 if let Err(e) = cx.editor.open(path, action) {
helix-term/src/ui/picker.rs+32−29
@@ -6,6 +6,7 @@ use crate::{
6use crossterm::event::Event;
6use crossterm::event::Event;
7use tui::{
7use tui::{
8 buffer::Buffer as Surface,
8 buffer::Buffer as Surface,
9+ text::Spans,
9 widgets::{Block, BorderType, Borders},
10 widgets::{Block, BorderType, Borders},
10};
11};
11
12
@@ -15,7 +16,6 @@ use tui::widgets::Widget;
15
16
16use std::time::Instant;
17use std::time::Instant;
17use std::{
18use std::{
18− borrow::Cow,
19 cmp::Reverse,
19 cmp::Reverse,
20 collections::HashMap,
20 collections::HashMap,
21 io::Read,
21 io::Read,
@@ -87,7 +87,7 @@ impl Preview<'_, '_> {
87impl<T> FilePicker<T> {
87impl<T> FilePicker<T> {
88 pub fn new(
88 pub fn new(
89 options: Vec<T>,
89 options: Vec<T>,
90− format_fn: impl Fn(&T) -> Cow<str> + 'static,
90+ format_fn: impl Fn(&T) -> Spans + 'static,
91 callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
91 callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
92 preview_fn: impl Fn(&Editor, &T) -> Option<FileLocation> + 'static,
92 preview_fn: impl Fn(&Editor, &T) -> Option<FileLocation> + 'static,
93 ) -> Self {
93 ) -> Self {
@@ -299,14 +299,14 @@ pub struct Picker<T> {
299 /// Whether to truncate the start (default true)
299 /// Whether to truncate the start (default true)
300 pub truncate_start: bool,
300 pub truncate_start: bool,
301
301
302− format_fn: Box<dyn Fn(&T) -> Cow<str>>,
302+ format_fn: Box<dyn Fn(&T) -> Spans>,
303 callback_fn: Box<dyn Fn(&mut Context, &T, Action)>,
303 callback_fn: Box<dyn Fn(&mut Context, &T, Action)>,
304}
304}
305
305
306impl<T> Picker<T> {
306impl<T> Picker<T> {
307 pub fn new(
307 pub fn new(
308 options: Vec<T>,
308 options: Vec<T>,
309− format_fn: impl Fn(&T) -> Cow<str> + 'static,
309+ format_fn: impl Fn(&T) -> Spans + 'static,
310 callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
310 callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
311 ) -> Self {
311 ) -> Self {
312 let prompt = Prompt::new(
312 let prompt = Prompt::new(
@@ -372,9 +372,8 @@ impl<T> Picker<T> {
372 self.matches.retain_mut(|(index, score)| {
372 self.matches.retain_mut(|(index, score)| {
373 let option = &self.options[*index];
373 let option = &self.options[*index];
374 // TODO: maybe using format_fn isn't the best idea here
374 // TODO: maybe using format_fn isn't the best idea here
375− let text = (self.format_fn)(option);
375+ let line: String = (self.format_fn)(option).into();
376−
377− match self.matcher.fuzzy_match(&text, pattern) {
376+ match self.matcher.fuzzy_match(&line, pattern) {
378 Some(s) => {
377 Some(s) => {
379 // Update the score
378 // Update the score
380 *score = s;
379 *score = s;
@@ -401,10 +400,10 @@ impl<T> Picker<T> {
401 }
400 }
402
401
403 // TODO: maybe using format_fn isn't the best idea here
402 // 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();
405
404
406 self.matcher
405 self.matcher
407− .fuzzy_match(&text, pattern)
406+ .fuzzy_match(&line, pattern)
408 .map(|score| (index, score))
407 .map(|score| (index, score))
409 }),
408 }),
410 );
409 );
@@ -611,30 +610,34 @@ impl<T: 'static> Component for Picker<T> {
611 surface.set_string(inner.x.saturating_sub(2), inner.y + i as u16, ">", selected);
610 surface.set_string(inner.x.saturating_sub(2), inner.y + i as u16, ">", selected);
612 }
611 }
613
612
614− let formatted = (self.format_fn)(option);
613+ let spans = (self.format_fn)(option);
615−
616 let (_score, highlights) = self
614 let (_score, highlights) = self
617 .matcher
615 .matcher
618− .fuzzy_indices(&formatted, self.prompt.line())
616+ .fuzzy_indices(&String::from(&spans), self.prompt.line())
619 .unwrap_or_default();
617 .unwrap_or_default();
620
618
619+ spans.0.into_iter().fold(inner, |pos, span| {
620+ let new_x = surface
621− surface.set_string_truncated(
621+ .set_string_truncated(
622− inner.x,
622+ pos.x,
623− inner.y + i as u16,
623+ pos.y + i as u16,
624− &formatted,
624+ &span.content,
625− inner.width as usize,
625+ pos.width as usize,
626− |idx| {
626+ |idx| {
627− if highlights.contains(&idx) {
627+ if highlights.contains(&idx) {
628− highlighted
628+ highlighted.patch(span.style)
629− } else if is_active {
629+ } else if is_active {
630− selected
630+ selected.patch(span.style)
631− } else {
631+ } else {
632− text_style
632+ text_style.patch(span.style)
633− }
633+ }
634− },
634+ },
635− true,
635+ true,
636− self.truncate_start,
636+ self.truncate_start,
637+ )
638+ .0;
639+ pos.clip_left(new_x - pos.x)
637− );
640+ });
638 }
641 }
639 }
642 }
640
643
helix-tui/src/text.rs+19−4
@@ -194,6 +194,12 @@ impl<'a> From<&'a str> for Span<'a> {
194 }
194 }
195}
195}
196
196
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+
197/// A string composed of clusters of graphemes, each with their own style.
203/// A string composed of clusters of graphemes, each with their own style.
198#[derive(Debug, Default, Clone, PartialEq)]
204#[derive(Debug, Default, Clone, PartialEq)]
199pub struct Spans<'a>(pub Vec<Span<'a>>);
205pub struct Spans<'a>(pub Vec<Span<'a>>);
@@ -229,6 +235,12 @@ impl<'a> From<&'a str> for Spans<'a> {
229 }
235 }
230}
236}
231
237
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+
232impl<'a> From<Vec<Span<'a>>> for Spans<'a> {
244impl<'a> From<Vec<Span<'a>>> for Spans<'a> {
233 fn from(spans: Vec<Span<'a>>) -> Spans<'a> {
245 fn from(spans: Vec<Span<'a>>) -> Spans<'a> {
234 Spans(spans)
246 Spans(spans)
@@ -243,10 +255,13 @@ impl<'a> From<Span<'a>> for Spans<'a> {
243
255
244impl<'a> From<Spans<'a>> for String {
256impl<'a> From<Spans<'a>> for String {
245 fn from(line: Spans<'a>) -> String {
257 fn from(line: Spans<'a>) -> String {
246− line.0.iter().fold(String::new(), |mut acc, s| {
258+ line.0.iter().map(|s| &*s.content).collect()
247− acc.push_str(s.content.as_ref());
259+ }
248− acc
260+}
249− })
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()
250 }
265 }
251}
266}
252
267
helix-tui/src/widgets/block.rs+1−1
@@ -77,7 +77,7 @@ impl<'a> Block<'a> {
77 )]
77 )]
78 pub fn title_style(mut self, style: Style) -> Block<'a> {
78 pub fn title_style(mut self, style: Style) -> Block<'a> {
79 if let Some(t) = self.title {
79 if let Some(t) = self.title {
80− let title = String::from(t);
80+ let title = String::from(&t);
81 self.title = Some(Spans::from(Span::styled(title, style)));
81 self.title = Some(Spans::from(Span::styled(title, style)));
82 }
82 }
83 self
83 self
helix-view/src/editor.rs+3
@@ -39,6 +39,7 @@ use helix_core::{
39 Change,
39 Change,
40};
40};
41use helix_dap as dap;
41use helix_dap as dap;
42+use helix_lsp::lsp;
42
43
43use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer};
44use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer};
44
45
@@ -462,6 +463,7 @@ pub struct Editor {
462 pub macro_replaying: Vec<char>,
463 pub macro_replaying: Vec<char>,
463 pub theme: Theme,
464 pub theme: Theme,
464 pub language_servers: helix_lsp::Registry,
465 pub language_servers: helix_lsp::Registry,
466+ pub diagnostics: BTreeMap<lsp::Url, Vec<lsp::Diagnostic>>,
465
467
466 pub debugger: Option<dap::Client>,
468 pub debugger: Option<dap::Client>,
467 pub debugger_events: SelectAll<UnboundedReceiverStream<dap::Payload>>,
469 pub debugger_events: SelectAll<UnboundedReceiverStream<dap::Payload>>,
@@ -533,6 +535,7 @@ impl Editor {
533 macro_replaying: Vec::new(),
535 macro_replaying: Vec::new(),
534 theme: theme_loader.default(),
536 theme: theme_loader.default(),
535 language_servers,
537 language_servers,
538+ diagnostics: BTreeMap::new(),
536 debugger: None,
539 debugger: None,
537 debugger_events: SelectAll::new(),
540 debugger_events: SelectAll::new(),
538 breakpoints: HashMap::new(),
541 breakpoints: HashMap::new(),
xtask/src/main.rs+1−1
@@ -106,7 +106,7 @@ pub mod md_gen {
106 .collect::<Vec<_>>()
106 .collect::<Vec<_>>()
107 .join(", ");
107 .join(", ");
108
108
109− let doc = cmd.doc.replace("\n", "<br>");
109+ let doc = cmd.doc.replace('\n', "<br>");
110
110
111 md.push_str(&md_table_row(&[names.to_owned(), doc.to_owned()]));
111 md.push_str(&md_table_row(&[names.to_owned(), doc.to_owned()]));
112 }
112 }