on master help sign in
Reuse menu::Item trait in picker (#2814)
6e2aaedGokul Soumya
helix-dap/src/client.rs+1−1
@@ -34,7 +34,7 @@ pub struct Client {
34 pub caps: Option<DebuggerCapabilities>,
34 pub caps: Option<DebuggerCapabilities>,
35 // thread_id -> frames
35 // thread_id -> frames
36 pub stack_frames: HashMap<ThreadId, Vec<StackFrame>>,
36 pub stack_frames: HashMap<ThreadId, Vec<StackFrame>>,
37− pub thread_states: HashMap<ThreadId, String>,
37+ pub thread_states: ThreadStates,
38 pub thread_id: Option<ThreadId>,
38 pub thread_id: Option<ThreadId>,
39 /// Currently active frame for the current thread.
39 /// Currently active frame for the current thread.
40 pub active_frame: Option<usize>,
40 pub active_frame: Option<usize>,
helix-dap/src/types.rs+2
@@ -14,6 +14,8 @@ impl std::fmt::Display for ThreadId {
14 }
14 }
15}
15}
16
16
17+pub type ThreadStates = HashMap<ThreadId, String>;
18+
17pub trait Request {
19pub trait Request {
18 type Arguments: serde::de::DeserializeOwned + serde::Serialize;
20 type Arguments: serde::de::DeserializeOwned + serde::Serialize;
19 type Result: serde::de::DeserializeOwned + serde::Serialize;
21 type Result: serde::de::DeserializeOwned + serde::Serialize;
helix-term/src/commands.rs+92−57
@@ -45,6 +45,7 @@ use movement::Movement;
45use crate::{
45use crate::{
46 args,
46 args,
47 compositor::{self, Component, Compositor},
47 compositor::{self, Component, Compositor},
48+ keymap::ReverseKeymap,
48 ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent},
49 ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent},
49};
50};
50
51
@@ -1744,8 +1745,42 @@ fn search_selection(cx: &mut Context) {
1744}
1745}
1745
1746
1746fn global_search(cx: &mut Context) {
1747fn global_search(cx: &mut Context) {
1748+ #[derive(Debug)]
1749+ struct FileResult {
1750+ path: PathBuf,
1751+ /// 0 indexed lines
1752+ line_num: usize,
1753+ }
1754+
1755+ impl FileResult {
1756+ fn new(path: &Path, line_num: usize) -> Self {
1757+ Self {
1758+ path: path.to_path_buf(),
1759+ line_num,
1760+ }
1761+ }
1762+ }
1763+
1764+ impl ui::menu::Item for FileResult {
1765+ type Data = Option<PathBuf>;
1766+
1767+ fn label(&self, current_path: &Self::Data) -> Spans {
1768+ let relative_path = helix_core::path::get_relative_path(&self.path)
1769+ .to_string_lossy()
1770+ .into_owned();
1771+ if current_path
1772+ .as_ref()
1773+ .map(|p| p == &self.path)
1774+ .unwrap_or(false)
1775+ {
1776+ format!("{} (*)", relative_path).into()
1777+ } else {
1778+ relative_path.into()
1779+ }
1780+ }
1781+ }
1782+
1747− let (all_matches_sx, all_matches_rx) =
1783+ let (all_matches_sx, all_matches_rx) = tokio::sync::mpsc::unbounded_channel::<FileResult>();
1748− tokio::sync::mpsc::unbounded_channel::<(usize, PathBuf)>();
1749 let config = cx.editor.config();
1784 let config = cx.editor.config();
1750 let smart_case = config.search.smart_case;
1785 let smart_case = config.search.smart_case;
1751 let file_picker_config = config.file_picker.clone();
1786 let file_picker_config = config.file_picker.clone();
@@ -1809,7 +1844,7 @@ fn global_search(cx: &mut Context) {
1809 entry.path(),
1844 entry.path(),
1810 sinks::UTF8(|line_num, _| {
1845 sinks::UTF8(|line_num, _| {
1811 all_matches_sx
1846 all_matches_sx
1812− .send((line_num as usize - 1, entry.path().to_path_buf()))
1847+ .send(FileResult::new(entry.path(), line_num as usize - 1))
1813 .unwrap();
1848 .unwrap();
1814
1849
1815 Ok(true)
1850 Ok(true)
@@ -1836,7 +1871,7 @@ fn global_search(cx: &mut Context) {
1836 let current_path = doc_mut!(cx.editor).path().cloned();
1871 let current_path = doc_mut!(cx.editor).path().cloned();
1837
1872
1838 let show_picker = async move {
1873 let show_picker = async move {
1839− let all_matches: Vec<(usize, PathBuf)> =
1874+ let all_matches: Vec<FileResult> =
1840 UnboundedReceiverStream::new(all_matches_rx).collect().await;
1875 UnboundedReceiverStream::new(all_matches_rx).collect().await;
1841 let call: job::Callback =
1876 let call: job::Callback =
1842 Box::new(move |editor: &mut Editor, compositor: &mut Compositor| {
1877 Box::new(move |editor: &mut Editor, compositor: &mut Compositor| {
@@ -1847,17 +1882,8 @@ fn global_search(cx: &mut Context) {
1847
1882
1848 let picker = FilePicker::new(
1883 let picker = FilePicker::new(
1849 all_matches,
1884 all_matches,
1850− move |(_line_num, path)| {
1851− let relative_path = helix_core::path::get_relative_path(path)
1852− .to_string_lossy()
1853− .into_owned();
1854− if current_path.as_ref().map(|p| p == path).unwrap_or(false) {
1885+ current_path,
1855− format!("{} (*)", relative_path).into()
1856− } else {
1857− relative_path.into()
1858− }
1859− },
1860− move |cx, (line_num, path), action| {
1886+ move |cx, FileResult { path, line_num }, action| {
1861 match cx.editor.open(path, action) {
1887 match cx.editor.open(path, action) {
1862 Ok(_) => {}
1888 Ok(_) => {}
1863 Err(e) => {
1889 Err(e) => {
@@ -1879,7 +1905,9 @@ fn global_search(cx: &mut Context) {
1879 doc.set_selection(view.id, Selection::single(start, end));
1905 doc.set_selection(view.id, Selection::single(start, end));
1880 align_view(doc, view, Align::Center);
1906 align_view(doc, view, Align::Center);
1881 },
1907 },
1908+ |_editor, FileResult { path, line_num }| {
1882− |_editor, (line_num, path)| Some((path.clone(), Some((*line_num, *line_num)))),
1909+ Some((path.clone(), Some((*line_num, *line_num))))
1910+ },
1883 );
1911 );
1884 compositor.push(Box::new(overlayed(picker)));
1912 compositor.push(Box::new(overlayed(picker)));
1885 });
1913 });
@@ -2172,8 +2200,10 @@ fn buffer_picker(cx: &mut Context) {
2172 is_current: bool,
2200 is_current: bool,
2173 }
2201 }
2174
2202
2175− impl BufferMeta {
2203+ impl ui::menu::Item for BufferMeta {
2204+ type Data = ();
2205+
2176− fn format(&self) -> Spans {
2206+ fn label(&self, _data: &Self::Data) -> Spans {
2177 let path = self
2207 let path = self
2178 .path
2208 .path
2179 .as_deref()
2209 .as_deref()
@@ -2213,7 +2243,7 @@ fn buffer_picker(cx: &mut Context) {
2213 .iter()
2243 .iter()
2214 .map(|(_, doc)| new_meta(doc))
2244 .map(|(_, doc)| new_meta(doc))
2215 .collect(),
2245 .collect(),
2216− BufferMeta::format,
2246+ (),
2217 |cx, meta, action| {
2247 |cx, meta, action| {
2218 cx.editor.switch(meta.id, action);
2248 cx.editor.switch(meta.id, action);
2219 },
2249 },
@@ -2230,6 +2260,38 @@ fn buffer_picker(cx: &mut Context) {
2230 cx.push_layer(Box::new(overlayed(picker)));
2260 cx.push_layer(Box::new(overlayed(picker)));
2231}
2261}
2232
2262
2263+impl ui::menu::Item for MappableCommand {
2264+ type Data = ReverseKeymap;
2265+
2266+ fn label(&self, keymap: &Self::Data) -> Spans {
2267+ // formats key bindings, multiple bindings are comma separated,
2268+ // individual key presses are joined with `+`
2269+ let fmt_binding = |bindings: &Vec<Vec<KeyEvent>>| -> String {
2270+ bindings
2271+ .iter()
2272+ .map(|bind| {
2273+ bind.iter()
2274+ .map(|key| key.to_string())
2275+ .collect::<Vec<String>>()
2276+ .join("+")
2277+ })
2278+ .collect::<Vec<String>>()
2279+ .join(", ")
2280+ };
2281+
2282+ match self {
2283+ MappableCommand::Typable { doc, name, .. } => match keymap.get(name as &String) {
2284+ Some(bindings) => format!("{} ({})", doc, fmt_binding(bindings)).into(),
2285+ None => doc.as_str().into(),
2286+ },
2287+ MappableCommand::Static { doc, name, .. } => match keymap.get(*name) {
2288+ Some(bindings) => format!("{} ({})", doc, fmt_binding(bindings)).into(),
2289+ None => (*doc).into(),
2290+ },
2291+ }
2292+ }
2293+}
2294+
2233pub fn command_palette(cx: &mut Context) {
2295pub fn command_palette(cx: &mut Context) {
2234 cx.callback = Some(Box::new(
2296 cx.callback = Some(Box::new(
2235 move |compositor: &mut Compositor, cx: &mut compositor::Context| {
2297 move |compositor: &mut Compositor, cx: &mut compositor::Context| {
@@ -2246,44 +2308,17 @@ pub fn command_palette(cx: &mut Context) {
2246 }
2308 }
2247 }));
2309 }));
2248
2310
2249− // formats key bindings, multiple bindings are comma separated,
2250− // individual key presses are joined with `+`
2251− let fmt_binding = |bindings: &Vec<Vec<KeyEvent>>| -> String {
2252− bindings
2253− .iter()
2254− .map(|bind| {
2255− bind.iter()
2256− .map(|key| key.key_sequence_format())
2257− .collect::<String>()
2258− })
2259− .collect::<Vec<String>>()
2260− .join(", ")
2261− };
2262−
2263− let picker = Picker::new(
2264− commands,
2265− move |command| match command {
2266− MappableCommand::Typable { doc, name, .. } => match keymap.get(name) {
2267− Some(bindings) => format!("{} ({})", doc, fmt_binding(bindings)).into(),
2268− None => doc.as_str().into(),
2269− },
2270− MappableCommand::Static { doc, name, .. } => match keymap.get(*name) {
2271− Some(bindings) => format!("{} ({})", doc, fmt_binding(bindings)).into(),
2272− None => (*doc).into(),
2273− },
2274− },
2275− move |cx, command, _action| {
2311+ let picker = Picker::new(commands, keymap, move |cx, command, _action| {
2276− let mut ctx = Context {
2312+ let mut ctx = Context {
2277− register: None,
2313+ register: None,
2278− count: std::num::NonZeroUsize::new(1),
2314+ count: std::num::NonZeroUsize::new(1),
2279− editor: cx.editor,
2315+ editor: cx.editor,
2280− callback: None,
2316+ callback: None,
2281− on_next_key_callback: None,
2317+ on_next_key_callback: None,
2282− jobs: cx.jobs,
2318+ jobs: cx.jobs,
2283− };
2319+ };
2284− command.execute(&mut ctx);
2320+ command.execute(&mut ctx);
2285− },
2286− );
2321+ });
2287 compositor.push(Box::new(overlayed(picker)));
2322 compositor.push(Box::new(overlayed(picker)));
2288 },
2323 },
2289 ));
2324 ));
helix-term/src/commands/dap.rs+39−15
@@ -4,13 +4,15 @@ use crate::{
4 job::{Callback, Jobs},
4 job::{Callback, Jobs},
5 ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent, Text},
5 ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent, Text},
6};
6};
7+use dap::{StackFrame, Thread, ThreadStates};
7−use helix_core::syntax::{DebugArgumentValue, DebugConfigCompletion};
8+use helix_core::syntax::{DebugArgumentValue, DebugConfigCompletion, DebugTemplate};
8use helix_dap::{self as dap, Client};
9use helix_dap::{self as dap, Client};
9use helix_lsp::block_on;
10use helix_lsp::block_on;
10use helix_view::editor::Breakpoint;
11use helix_view::editor::Breakpoint;
11
12
12use serde_json::{to_value, Value};
13use serde_json::{to_value, Value};
13use tokio_stream::wrappers::UnboundedReceiverStream;
14use tokio_stream::wrappers::UnboundedReceiverStream;
15+use tui::text::Spans;
14
16
15use std::collections::HashMap;
17use std::collections::HashMap;
16use std::future::Future;
18use std::future::Future;
@@ -20,6 +22,38 @@ use anyhow::{anyhow, bail};
20
22
21use helix_view::handlers::dap::{breakpoints_changed, jump_to_stack_frame, select_thread_id};
23use helix_view::handlers::dap::{breakpoints_changed, jump_to_stack_frame, select_thread_id};
22
24
25+impl ui::menu::Item for StackFrame {
26+ type Data = ();
27+
28+ fn label(&self, _data: &Self::Data) -> Spans {
29+ self.name.as_str().into() // TODO: include thread_states in the label
30+ }
31+}
32+
33+impl ui::menu::Item for DebugTemplate {
34+ type Data = ();
35+
36+ fn label(&self, _data: &Self::Data) -> Spans {
37+ self.name.as_str().into()
38+ }
39+}
40+
41+impl ui::menu::Item for Thread {
42+ type Data = ThreadStates;
43+
44+ fn label(&self, thread_states: &Self::Data) -> Spans {
45+ format!(
46+ "{} ({})",
47+ self.name,
48+ thread_states
49+ .get(&self.id)
50+ .map(|state| state.as_str())
51+ .unwrap_or("unknown")
52+ )
53+ .into()
54+ }
55+}
56+
23fn thread_picker(
57fn thread_picker(
24 cx: &mut Context,
58 cx: &mut Context,
25 callback_fn: impl Fn(&mut Editor, &dap::Thread) + Send + 'static,
59 callback_fn: impl Fn(&mut Editor, &dap::Thread) + Send + 'static,
@@ -41,17 +75,7 @@ fn thread_picker(
41 let thread_states = debugger.thread_states.clone();
75 let thread_states = debugger.thread_states.clone();
42 let picker = FilePicker::new(
76 let picker = FilePicker::new(
43 threads,
77 threads,
44− move |thread| {
45− format!(
46− "{} ({})",
47− thread.name,
48− thread_states
78+ thread_states,
49− .get(&thread.id)
50− .map(|state| state.as_str())
51− .unwrap_or("unknown")
52− )
53− .into()
54− },
55 move |cx, thread, _action| callback_fn(cx.editor, thread),
79 move |cx, thread, _action| callback_fn(cx.editor, thread),
56 move |editor, thread| {
80 move |editor, thread| {
57 let frames = editor.debugger.as_ref()?.stack_frames.get(&thread.id)?;
81 let frames = editor.debugger.as_ref()?.stack_frames.get(&thread.id)?;
@@ -243,7 +267,7 @@ pub fn dap_launch(cx: &mut Context) {
243
267
244 cx.push_layer(Box::new(overlayed(Picker::new(
268 cx.push_layer(Box::new(overlayed(Picker::new(
245 templates,
269 templates,
246− |template| template.name.as_str().into(),
270+ (),
247 |cx, template, _action| {
271 |cx, template, _action| {
248 let completions = template.completion.clone();
272 let completions = template.completion.clone();
249 let name = template.name.clone();
273 let name = template.name.clone();
@@ -475,7 +499,7 @@ pub fn dap_variables(cx: &mut Context) {
475
499
476 for scope in scopes.iter() {
500 for scope in scopes.iter() {
477 // use helix_view::graphics::Style;
501 // use helix_view::graphics::Style;
478− use tui::text::{Span, Spans};
502+ use tui::text::Span;
479 let response = block_on(debugger.variables(scope.variables_reference));
503 let response = block_on(debugger.variables(scope.variables_reference));
480
504
481 variables.push(Spans::from(Span::styled(
505 variables.push(Spans::from(Span::styled(
@@ -652,7 +676,7 @@ pub fn dap_switch_stack_frame(cx: &mut Context) {
652
676
653 let picker = FilePicker::new(
677 let picker = FilePicker::new(
654 frames,
678 frames,
655− |frame| frame.name.as_str().into(), // TODO: include thread_states in the label
679+ (),
656 move |cx, frame, _action| {
680 move |cx, frame, _action| {
657 let debugger = debugger!(cx.editor);
681 let debugger = debugger!(cx.editor);
658 // TODO: this should be simpler to find
682 // TODO: this should be simpler to find
helix-term/src/commands/lsp.rs+131−91
@@ -19,7 +19,8 @@ use crate::{
19 ui::{self, overlay::overlayed, FileLocation, FilePicker, Popup, PromptEvent},
19 ui::{self, overlay::overlayed, FileLocation, FilePicker, Popup, PromptEvent},
20};
20};
21
21
22−use std::{borrow::Cow, collections::BTreeMap};
22+use std::collections::BTreeMap;
23+use std::{borrow::Cow, path::PathBuf};
23
24
24/// Gets the language server that is attached to a document, and
25/// Gets the language server that is attached to a document, and
25/// if it's not active displays a status message. Using this macro
26/// if it's not active displays a status message. Using this macro
@@ -39,6 +40,112 @@ macro_rules! language_server {
39 };
40 };
40}
41}
41
42
43+impl ui::menu::Item for lsp::Location {
44+ /// Current working directory.
45+ type Data = PathBuf;
46+
47+ fn label(&self, cwdir: &Self::Data) -> Spans {
48+ let file: Cow<'_, str> = (self.uri.scheme() == "file")
49+ .then(|| {
50+ self.uri
51+ .to_file_path()
52+ .map(|path| {
53+ // strip root prefix
54+ path.strip_prefix(&cwdir)
55+ .map(|path| path.to_path_buf())
56+ .unwrap_or(path)
57+ })
58+ .map(|path| Cow::from(path.to_string_lossy().into_owned()))
59+ .ok()
60+ })
61+ .flatten()
62+ .unwrap_or_else(|| self.uri.as_str().into());
63+ let line = self.range.start.line;
64+ format!("{}:{}", file, line).into()
65+ }
66+}
67+
68+impl ui::menu::Item for lsp::SymbolInformation {
69+ /// Path to currently focussed document
70+ type Data = Option<lsp::Url>;
71+
72+ fn label(&self, current_doc_path: &Self::Data) -> Spans {
73+ if current_doc_path.as_ref() == Some(&self.location.uri) {
74+ self.name.as_str().into()
75+ } else {
76+ match self.location.uri.to_file_path() {
77+ Ok(path) => {
78+ let relative_path = helix_core::path::get_relative_path(path.as_path())
79+ .to_string_lossy()
80+ .into_owned();
81+ format!("{} ({})", &self.name, relative_path).into()
82+ }
83+ Err(_) => format!("{} ({})", &self.name, &self.location.uri).into(),
84+ }
85+ }
86+ }
87+}
88+
89+struct DiagnosticStyles {
90+ hint: Style,
91+ info: Style,
92+ warning: Style,
93+ error: Style,
94+}
95+
96+struct PickerDiagnostic {
97+ url: lsp::Url,
98+ diag: lsp::Diagnostic,
99+}
100+
101+impl ui::menu::Item for PickerDiagnostic {
102+ type Data = DiagnosticStyles;
103+
104+ fn label(&self, styles: &Self::Data) -> Spans {
105+ let mut style = self
106+ .diag
107+ .severity
108+ .map(|s| match s {
109+ DiagnosticSeverity::HINT => styles.hint,
110+ DiagnosticSeverity::INFORMATION => styles.info,
111+ DiagnosticSeverity::WARNING => styles.warning,
112+ DiagnosticSeverity::ERROR => styles.error,
113+ _ => Style::default(),
114+ })
115+ .unwrap_or_default();
116+
117+ // remove background as it is distracting in the picker list
118+ style.bg = None;
119+
120+ let code = self
121+ .diag
122+ .code
123+ .as_ref()
124+ .map(|c| match c {
125+ NumberOrString::Number(n) => n.to_string(),
126+ NumberOrString::String(s) => s.to_string(),
127+ })
128+ .unwrap_or_default();
129+
130+ let truncated_path = path::get_truncated_path(self.url.path())
131+ .to_string_lossy()
132+ .into_owned();
133+
134+ Spans::from(vec![
135+ Span::styled(
136+ self.diag.source.clone().unwrap_or_default(),
137+ style.add_modifier(Modifier::BOLD),
138+ ),
139+ Span::raw(": "),
140+ Span::styled(truncated_path, style),
141+ Span::raw(" - "),
142+ Span::styled(code, style.add_modifier(Modifier::BOLD)),
143+ Span::raw(": "),
144+ Span::styled(&self.diag.message, style),
145+ ])
146+ }
147+}
148+
42fn location_to_file_location(location: &lsp::Location) -> FileLocation {
149fn location_to_file_location(location: &lsp::Location) -> FileLocation {
43 let path = location.uri.to_file_path().unwrap();
150 let path = location.uri.to_file_path().unwrap();
44 let line = Some((
151 let line = Some((
@@ -93,29 +200,14 @@ fn sym_picker(
93 offset_encoding: OffsetEncoding,
200 offset_encoding: OffsetEncoding,
94) -> FilePicker<lsp::SymbolInformation> {
201) -> FilePicker<lsp::SymbolInformation> {
95 // TODO: drop current_path comparison and instead use workspace: bool flag?
202 // TODO: drop current_path comparison and instead use workspace: bool flag?
96− let current_path2 = current_path.clone();
97 FilePicker::new(
203 FilePicker::new(
98 symbols,
204 symbols,
99− move |symbol| {
100− if current_path.as_ref() == Some(&symbol.location.uri) {
205+ current_path.clone(),
101− symbol.name.as_str().into()
102− } else {
103− match symbol.location.uri.to_file_path() {
104− Ok(path) => {
105− let relative_path = helix_core::path::get_relative_path(path.as_path())
106− .to_string_lossy()
107− .into_owned();
108− format!("{} ({})", &symbol.name, relative_path).into()
109− }
110− Err(_) => format!("{} ({})", &symbol.name, &symbol.location.uri).into(),
111− }
112− }
113− },
114 move |cx, symbol, action| {
206 move |cx, symbol, action| {
115 let (view, doc) = current!(cx.editor);
207 let (view, doc) = current!(cx.editor);
116 push_jump(view, doc);
208 push_jump(view, doc);
117
209
118− if current_path2.as_ref() != Some(&symbol.location.uri) {
210+ if current_path.as_ref() != Some(&symbol.location.uri) {
119 let uri = &symbol.location.uri;
211 let uri = &symbol.location.uri;
120 let path = match uri.to_file_path() {
212 let path = match uri.to_file_path() {
121 Ok(path) => path,
213 Ok(path) => path,
@@ -155,7 +247,7 @@ fn diag_picker(
155 diagnostics: BTreeMap<lsp::Url, Vec<lsp::Diagnostic>>,
247 diagnostics: BTreeMap<lsp::Url, Vec<lsp::Diagnostic>>,
156 current_path: Option<lsp::Url>,
248 current_path: Option<lsp::Url>,
157 offset_encoding: OffsetEncoding,
249 offset_encoding: OffsetEncoding,
158−) -> FilePicker<(lsp::Url, lsp::Diagnostic)> {
250+) -> FilePicker<PickerDiagnostic> {
159 // TODO: drop current_path comparison and instead use workspace: bool flag?
251 // TODO: drop current_path comparison and instead use workspace: bool flag?
160
252
161 // flatten the map to a vec of (url, diag) pairs
253 // flatten the map to a vec of (url, diag) pairs
@@ -163,59 +255,24 @@ fn diag_picker(
163 for (url, diags) in diagnostics {
255 for (url, diags) in diagnostics {
164 flat_diag.reserve(diags.len());
256 flat_diag.reserve(diags.len());
165 for diag in diags {
257 for diag in diags {
166− flat_diag.push((url.clone(), diag));
258+ flat_diag.push(PickerDiagnostic {
259+ url: url.clone(),
260+ diag,
261+ });
167 }
262 }
168 }
263 }
169
264
265+ let styles = DiagnosticStyles {
170− let hint = cx.editor.theme.get("hint");
266+ hint: cx.editor.theme.get("hint"),
171− let info = cx.editor.theme.get("info");
267+ info: cx.editor.theme.get("info"),
172− let warning = cx.editor.theme.get("warning");
268+ warning: cx.editor.theme.get("warning"),
173− let error = cx.editor.theme.get("error");
269+ error: cx.editor.theme.get("error"),
270+ };
174
271
175 FilePicker::new(
272 FilePicker::new(
176 flat_diag,
273 flat_diag,
177− move |(url, diag)| {
274+ styles,
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| {
275+ move |cx, PickerDiagnostic { url, diag }, action| {
219 if current_path.as_ref() == Some(url) {
276 if current_path.as_ref() == Some(url) {
220 let (view, doc) = current!(cx.editor);
277 let (view, doc) = current!(cx.editor);
221 push_jump(view, doc);
278 push_jump(view, doc);
@@ -233,7 +290,7 @@ fn diag_picker(
233 align_view(doc, view, Align::Center);
290 align_view(doc, view, Align::Center);
234 }
291 }
235 },
292 },
236− move |_editor, (url, diag)| {
293+ move |_editor, PickerDiagnostic { url, diag }| {
237 let location = lsp::Location::new(url.clone(), diag.range);
294 let location = lsp::Location::new(url.clone(), diag.range);
238 Some(location_to_file_location(&location))
295 Some(location_to_file_location(&location))
239 },
296 },
@@ -343,10 +400,11 @@ pub fn workspace_diagnostics_picker(cx: &mut Context) {
343}
400}
344
401
345impl ui::menu::Item for lsp::CodeActionOrCommand {
402impl ui::menu::Item for lsp::CodeActionOrCommand {
403+ type Data = ();
346− fn label(&self) -> &str {
404+ fn label(&self, _data: &Self::Data) -> Spans {
347 match self {
405 match self {
348− lsp::CodeActionOrCommand::CodeAction(action) => action.title.as_str(),
406+ lsp::CodeActionOrCommand::CodeAction(action) => action.title.as_str().into(),
349− lsp::CodeActionOrCommand::Command(command) => command.title.as_str(),
407+ lsp::CodeActionOrCommand::Command(command) => command.title.as_str().into(),
350 }
408 }
351 }
409 }
352}
410}
@@ -391,7 +449,7 @@ pub fn code_action(cx: &mut Context) {
391 return;
449 return;
392 }
450 }
393
451
394− let mut picker = ui::Menu::new(actions, move |editor, code_action, event| {
452+ let mut picker = ui::Menu::new(actions, (), move |editor, code_action, event| {
395 if event != PromptEvent::Validate {
453 if event != PromptEvent::Validate {
396 return;
454 return;
397 }
455 }
@@ -619,6 +677,7 @@ pub fn apply_workspace_edit(
619 }
677 }
620 }
678 }
621}
679}
680+
622fn goto_impl(
681fn goto_impl(
623 editor: &mut Editor,
682 editor: &mut Editor,
624 compositor: &mut Compositor,
683 compositor: &mut Compositor,
@@ -637,26 +696,7 @@ fn goto_impl(
637 _locations => {
696 _locations => {
638 let picker = FilePicker::new(
697 let picker = FilePicker::new(
639 locations,
698 locations,
640− move |location| {
699+ cwdir,
641− let file: Cow<'_, str> = (location.uri.scheme() == "file")
642− .then(|| {
643− location
644− .uri
645− .to_file_path()
646− .map(|path| {
647− // strip root prefix
648− path.strip_prefix(&cwdir)
649− .map(|path| path.to_path_buf())
650− .unwrap_or(path)
651− })
652− .map(|path| Cow::from(path.to_string_lossy().into_owned()))
653− .ok()
654− })
655− .flatten()
656− .unwrap_or_else(|| location.uri.as_str().into());
657− let line = location.range.start.line;
658− format!("{}:{}", file, line).into()
659− },
660 move |cx, location, action| {
700 move |cx, location, action| {
661 jump_to_location(cx.editor, location, offset_encoding, action)
701 jump_to_location(cx.editor, location, offset_encoding, action)
662 },
702 },
helix-term/src/keymap.rs+5−6
@@ -208,18 +208,17 @@ pub struct Keymap {
208 root: KeyTrie,
208 root: KeyTrie,
209}
209}
210
210
211+/// A map of command names to keybinds that will execute the command.
212+pub type ReverseKeymap = HashMap<String, Vec<Vec<KeyEvent>>>;
213+
211impl Keymap {
214impl Keymap {
212 pub fn new(root: KeyTrie) -> Self {
215 pub fn new(root: KeyTrie) -> Self {
213 Keymap { root }
216 Keymap { root }
214 }
217 }
215
218
216− pub fn reverse_map(&self) -> HashMap<String, Vec<Vec<KeyEvent>>> {
219+ pub fn reverse_map(&self) -> ReverseKeymap {
217 // recursively visit all nodes in keymap
220 // recursively visit all nodes in keymap
218− fn map_node(
219− cmd_map: &mut HashMap<String, Vec<Vec<KeyEvent>>>,
220− node: &KeyTrie,
221− keys: &mut Vec<KeyEvent>,
221+ fn map_node(cmd_map: &mut ReverseKeymap, node: &KeyTrie, keys: &mut Vec<KeyEvent>) {
222− ) {
223 match node {
222 match node {
224 KeyTrie::Leaf(cmd) => match cmd {
223 KeyTrie::Leaf(cmd) => match cmd {
225 MappableCommand::Typable { name, .. } => {
224 MappableCommand::Typable { name, .. } => {
helix-term/src/ui/completion.rs+15−8
@@ -2,6 +2,7 @@ use crate::compositor::{Component, Context, EventResult};
2use crossterm::event::{Event, KeyCode, KeyEvent};
2use crossterm::event::{Event, KeyCode, KeyEvent};
3use helix_view::editor::CompleteAction;
3use helix_view::editor::CompleteAction;
4use tui::buffer::Buffer as Surface;
4use tui::buffer::Buffer as Surface;
5+use tui::text::Spans;
5
6
6use std::borrow::Cow;
7use std::borrow::Cow;
7
8
@@ -15,19 +16,25 @@ use helix_lsp::{lsp, util};
15use lsp::CompletionItem;
16use lsp::CompletionItem;
16
17
17impl menu::Item for CompletionItem {
18impl menu::Item for CompletionItem {
19+ type Data = ();
18− fn sort_text(&self) -> &str {
20+ fn sort_text(&self, data: &Self::Data) -> Cow<str> {
19− self.filter_text.as_ref().unwrap_or(&self.label).as_str()
21+ self.filter_text(data)
20 }
22 }
21
23
24+ #[inline]
22− fn filter_text(&self) -> &str {
25+ fn filter_text(&self, _data: &Self::Data) -> Cow<str> {
26+ self.filter_text
27+ .as_ref()
23− self.filter_text.as_ref().unwrap_or(&self.label).as_str()
28+ .unwrap_or(&self.label)
29+ .as_str()
30+ .into()
24 }
31 }
25
32
26− fn label(&self) -> &str {
33+ fn label(&self, _data: &Self::Data) -> Spans {
27− self.label.as_str()
34+ self.label.as_str().into()
28 }
35 }
29
36
30− fn row(&self) -> menu::Row {
37+ fn row(&self, _data: &Self::Data) -> menu::Row {
31 menu::Row::new(vec![
38 menu::Row::new(vec![
32 menu::Cell::from(self.label.as_str()),
39 menu::Cell::from(self.label.as_str()),
33 menu::Cell::from(match self.kind {
40 menu::Cell::from(match self.kind {
@@ -85,7 +92,7 @@ impl Completion {
85 start_offset: usize,
92 start_offset: usize,
86 trigger_offset: usize,
93 trigger_offset: usize,
87 ) -> Self {
94 ) -> Self {
88− let menu = Menu::new(items, move |editor: &mut Editor, item, event| {
95+ let menu = Menu::new(items, (), move |editor: &mut Editor, item, event| {
89 fn item_to_transaction(
96 fn item_to_transaction(
90 doc: &Document,
97 doc: &Document,
91 item: &CompletionItem,
98 item: &CompletionItem,
helix-term/src/ui/menu.rs+39−15
@@ -1,9 +1,11 @@
1+use std::{borrow::Cow, path::PathBuf};
2+
1use crate::{
3use crate::{
2 compositor::{Callback, Component, Compositor, Context, EventResult},
4 compositor::{Callback, Component, Compositor, Context, EventResult},
3 ctrl, key, shift,
5 ctrl, key, shift,
4};
6};
5use crossterm::event::Event;
7use crossterm::event::Event;
6−use tui::{buffer::Buffer as Surface, widgets::Table};
8+use tui::{buffer::Buffer as Surface, text::Spans, widgets::Table};
7
9
8pub use tui::widgets::{Cell, Row};
10pub use tui::widgets::{Cell, Row};
9
11
@@ -14,22 +16,41 @@ use helix_view::{graphics::Rect, Editor};
14use tui::layout::Constraint;
16use tui::layout::Constraint;
15
17
16pub trait Item {
18pub trait Item {
19+ /// Additional editor state that is used for label calculation.
20+ type Data;
21+
17− fn label(&self) -> &str;
22+ fn label(&self, data: &Self::Data) -> Spans;
23+
24+ fn sort_text(&self, data: &Self::Data) -> Cow<str> {
25+ let label: String = self.label(data).into();
26+ label.into()
27+ }
18
28
19− fn sort_text(&self) -> &str {
29+ fn filter_text(&self, data: &Self::Data) -> Cow<str> {
20− self.label()
30+ let label: String = self.label(data).into();
31+ label.into()
21 }
32 }
22− fn filter_text(&self) -> &str {
33+
34+ fn row(&self, data: &Self::Data) -> Row {
23− self.label()
35+ Row::new(vec![Cell::from(self.label(data))])
24 }
36 }
37+}
25
38
26− fn row(&self) -> Row {
39+impl Item for PathBuf {
27− Row::new(vec![Cell::from(self.label())])
40+ /// Root prefix to strip.
41+ type Data = PathBuf;
42+
43+ fn label(&self, root_path: &Self::Data) -> Spans {
44+ self.strip_prefix(&root_path)
45+ .unwrap_or(self)
46+ .to_string_lossy()
47+ .into()
28 }
48 }
29}
49}
30
50
31pub struct Menu<T: Item> {
51pub struct Menu<T: Item> {
32 options: Vec<T>,
52 options: Vec<T>,
53+ editor_data: T::Data,
33
54
34 cursor: Option<usize>,
55 cursor: Option<usize>,
35
56
@@ -54,10 +75,12 @@ impl<T: Item> Menu<T> {
54 // rendering)
75 // rendering)
55 pub fn new(
76 pub fn new(
56 options: Vec<T>,
77 options: Vec<T>,
78+ editor_data: <T as Item>::Data,
57 callback_fn: impl Fn(&mut Editor, Option<&T>, MenuEvent) + 'static,
79 callback_fn: impl Fn(&mut Editor, Option<&T>, MenuEvent) + 'static,
58 ) -> Self {
80 ) -> Self {
59 let mut menu = Self {
81 let mut menu = Self {
60 options,
82 options,
83+ editor_data,
61 matcher: Box::new(Matcher::default()),
84 matcher: Box::new(Matcher::default()),
62 matches: Vec::new(),
85 matches: Vec::new(),
63 cursor: None,
86 cursor: None,
@@ -83,16 +106,17 @@ impl<T: Item> Menu<T> {
83 .iter()
106 .iter()
84 .enumerate()
107 .enumerate()
85 .filter_map(|(index, option)| {
108 .filter_map(|(index, option)| {
86− let text = option.filter_text();
109+ let text: String = option.filter_text(&self.editor_data).into();
87 // TODO: using fuzzy_indices could give us the char idx for match highlighting
110 // TODO: using fuzzy_indices could give us the char idx for match highlighting
88 self.matcher
111 self.matcher
89− .fuzzy_match(text, pattern)
112+ .fuzzy_match(&text, pattern)
90 .map(|score| (index, score))
113 .map(|score| (index, score))
91 }),
114 }),
92 );
115 );
93 // matches.sort_unstable_by_key(|(_, score)| -score);
116 // matches.sort_unstable_by_key(|(_, score)| -score);
94− self.matches
95− .sort_unstable_by_key(|(index, _score)| self.options[*index].sort_text());
117+ self.matches.sort_unstable_by_key(|(index, _score)| {
118+ self.options[*index].sort_text(&self.editor_data)
119+ });
96
120
97 // reset cursor position
121 // reset cursor position
98 self.cursor = None;
122 self.cursor = None;
@@ -127,10 +151,10 @@ impl<T: Item> Menu<T> {
127 let n = self
151 let n = self
128 .options
152 .options
129 .first()
153 .first()
130− .map(|option| option.row().cells.len())
154+ .map(|option| option.row(&self.editor_data).cells.len())
131 .unwrap_or_default();
155 .unwrap_or_default();
132 let max_lens = self.options.iter().fold(vec![0; n], |mut acc, option| {
156 let max_lens = self.options.iter().fold(vec![0; n], |mut acc, option| {
133− let row = option.row();
157+ let row = option.row(&self.editor_data);
134 // maintain max for each column
158 // maintain max for each column
135 for (acc, cell) in acc.iter_mut().zip(row.cells.iter()) {
159 for (acc, cell) in acc.iter_mut().zip(row.cells.iter()) {
136 let width = cell.content.width();
160 let width = cell.content.width();
@@ -300,7 +324,7 @@ impl<T: Item + 'static> Component for Menu<T> {
300 let scroll_line = (win_height - scroll_height) * scroll
324 let scroll_line = (win_height - scroll_height) * scroll
301 / std::cmp::max(1, len.saturating_sub(win_height));
325 / std::cmp::max(1, len.saturating_sub(win_height));
302
326
303− let rows = options.iter().map(|option| option.row());
327+ let rows = options.iter().map(|option| option.row(&self.editor_data));
304 let table = Table::new(rows)
328 let table = Table::new(rows)
305 .style(style)
329 .style(style)
306 .highlight_style(selected)
330 .highlight_style(selected)
helix-term/src/ui/mod.rs+1−6
@@ -23,8 +23,6 @@ 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;
28
26
29use std::path::PathBuf;
27use std::path::PathBuf;
30
28
@@ -172,10 +170,7 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi
172
170
173 FilePicker::new(
171 FilePicker::new(
174 files,
172 files,
175− move |path: &PathBuf| {
173+ root,
176− // format_fn
177− Spans::from(path.strip_prefix(&root).unwrap_or(path).to_string_lossy())
178− },
179 move |cx, path: &PathBuf, action| {
174 move |cx, path: &PathBuf, action| {
180 if let Err(e) = cx.editor.open(path, action) {
175 if let Err(e) = cx.editor.open(path, action) {
181 let err = if let Some(err) = e.source() {
176 let err = if let Some(err) = e.source() {
helix-term/src/ui/picker.rs+19−19
@@ -6,7 +6,6 @@ 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,
10 widgets::{Block, BorderType, Borders},
9 widgets::{Block, BorderType, Borders},
11};
10};
12
11
@@ -30,6 +29,8 @@ use helix_view::{
30 Document, Editor,
29 Document, Editor,
31};
30};
32
31
32+use super::menu::Item;
33+
33pub const MIN_AREA_WIDTH_FOR_PREVIEW: u16 = 72;
34pub const MIN_AREA_WIDTH_FOR_PREVIEW: u16 = 72;
34/// Biggest file size to preview in bytes
35/// Biggest file size to preview in bytes
35pub const MAX_FILE_SIZE_FOR_PREVIEW: u64 = 10 * 1024 * 1024;
36pub const MAX_FILE_SIZE_FOR_PREVIEW: u64 = 10 * 1024 * 1024;
@@ -37,7 +38,7 @@ pub const MAX_FILE_SIZE_FOR_PREVIEW: u64 = 10 * 1024 * 1024;
37/// File path and range of lines (used to align and highlight lines)
38/// File path and range of lines (used to align and highlight lines)
38pub type FileLocation = (PathBuf, Option<(usize, usize)>);
39pub type FileLocation = (PathBuf, Option<(usize, usize)>);
39
40
40−pub struct FilePicker<T> {
41+pub struct FilePicker<T: Item> {
41 picker: Picker<T>,
42 picker: Picker<T>,
42 pub truncate_start: bool,
43 pub truncate_start: bool,
43 /// Caches paths to documents
44 /// Caches paths to documents
@@ -84,15 +85,15 @@ impl Preview<'_, '_> {
84 }
85 }
85}
86}
86
87
87−impl<T> FilePicker<T> {
88+impl<T: Item> FilePicker<T> {
88 pub fn new(
89 pub fn new(
89 options: Vec<T>,
90 options: Vec<T>,
90− format_fn: impl Fn(&T) -> Spans + 'static,
91+ editor_data: T::Data,
91 callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
92 callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
92 preview_fn: impl Fn(&Editor, &T) -> Option<FileLocation> + 'static,
93 preview_fn: impl Fn(&Editor, &T) -> Option<FileLocation> + 'static,
93 ) -> Self {
94 ) -> Self {
94 let truncate_start = true;
95 let truncate_start = true;
95− let mut picker = Picker::new(options, format_fn, callback_fn);
96+ let mut picker = Picker::new(options, editor_data, callback_fn);
96 picker.truncate_start = truncate_start;
97 picker.truncate_start = truncate_start;
97
98
98 Self {
99 Self {
@@ -163,7 +164,7 @@ impl<T> FilePicker<T> {
163 }
164 }
164}
165}
165
166
166−impl<T: 'static> Component for FilePicker<T> {
167+impl<T: Item + 'static> Component for FilePicker<T> {
167 fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
168 fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
168 // +---------+ +---------+
169 // +---------+ +---------+
169 // |prompt | |preview |
170 // |prompt | |preview |
@@ -280,8 +281,9 @@ impl<T: 'static> Component for FilePicker<T> {
280 }
281 }
281}
282}
282
283
283−pub struct Picker<T> {
284+pub struct Picker<T: Item> {
284 options: Vec<T>,
285 options: Vec<T>,
286+ editor_data: T::Data,
285 // filter: String,
287 // filter: String,
286 matcher: Box<Matcher>,
288 matcher: Box<Matcher>,
287 /// (index, score)
289 /// (index, score)
@@ -299,14 +301,13 @@ pub struct Picker<T> {
299 /// Whether to truncate the start (default true)
301 /// Whether to truncate the start (default true)
300 pub truncate_start: bool,
302 pub truncate_start: bool,
301
303
302− format_fn: Box<dyn Fn(&T) -> Spans>,
303 callback_fn: Box<dyn Fn(&mut Context, &T, Action)>,
304 callback_fn: Box<dyn Fn(&mut Context, &T, Action)>,
304}
305}
305
306
306−impl<T> Picker<T> {
307+impl<T: Item> Picker<T> {
307 pub fn new(
308 pub fn new(
308 options: Vec<T>,
309 options: Vec<T>,
309− format_fn: impl Fn(&T) -> Spans + 'static,
310+ editor_data: T::Data,
310 callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
311 callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
311 ) -> Self {
312 ) -> Self {
312 let prompt = Prompt::new(
313 let prompt = Prompt::new(
@@ -318,6 +319,7 @@ impl<T> Picker<T> {
318
319
319 let mut picker = Self {
320 let mut picker = Self {
320 options,
321 options,
322+ editor_data,
321 matcher: Box::new(Matcher::default()),
323 matcher: Box::new(Matcher::default()),
322 matches: Vec::new(),
324 matches: Vec::new(),
323 filters: Vec::new(),
325 filters: Vec::new(),
@@ -325,7 +327,6 @@ impl<T> Picker<T> {
325 prompt,
327 prompt,
326 previous_pattern: String::new(),
328 previous_pattern: String::new(),
327 truncate_start: true,
329 truncate_start: true,
328− format_fn: Box::new(format_fn),
329 callback_fn: Box::new(callback_fn),
330 callback_fn: Box::new(callback_fn),
330 completion_height: 0,
331 completion_height: 0,
331 };
332 };
@@ -371,9 +372,9 @@ impl<T> Picker<T> {
371 #[allow(unstable_name_collisions)]
372 #[allow(unstable_name_collisions)]
372 self.matches.retain_mut(|(index, score)| {
373 self.matches.retain_mut(|(index, score)| {
373 let option = &self.options[*index];
374 let option = &self.options[*index];
374− // TODO: maybe using format_fn isn't the best idea here
375+ let text = option.sort_text(&self.editor_data);
375− let line: String = (self.format_fn)(option).into();
376+
376− match self.matcher.fuzzy_match(&line, pattern) {
377+ match self.matcher.fuzzy_match(&text, pattern) {
377 Some(s) => {
378 Some(s) => {
378 // Update the score
379 // Update the score
379 *score = s;
380 *score = s;
@@ -399,11 +400,10 @@ impl<T> Picker<T> {
399 self.filters.binary_search(&index).ok()?;
400 self.filters.binary_search(&index).ok()?;
400 }
401 }
401
402
402− // TODO: maybe using format_fn isn't the best idea here
403+ let text = option.filter_text(&self.editor_data);
403− let line: String = (self.format_fn)(option).into();
404
404
405 self.matcher
405 self.matcher
406− .fuzzy_match(&line, pattern)
406+ .fuzzy_match(&text, pattern)
407 .map(|score| (index, score))
407 .map(|score| (index, score))
408 }),
408 }),
409 );
409 );
@@ -477,7 +477,7 @@ impl<T> Picker<T> {
477// - on input change:
477// - on input change:
478// - score all the names in relation to input
478// - score all the names in relation to input
479
479
480−impl<T: 'static> Component for Picker<T> {
480+impl<T: Item + 'static> Component for Picker<T> {
481 fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
481 fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
482 self.completion_height = viewport.1.saturating_sub(4);
482 self.completion_height = viewport.1.saturating_sub(4);
483 Some(viewport)
483 Some(viewport)
@@ -610,7 +610,7 @@ impl<T: 'static> Component for Picker<T> {
610 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);
611 }
611 }
612
612
613− let spans = (self.format_fn)(option);
613+ let spans = option.label(&self.editor_data);
614 let (_score, highlights) = self
614 let (_score, highlights) = self
615 .matcher
615 .matcher
616 .fuzzy_indices(&String::from(&spans), self.prompt.line())
616 .fuzzy_indices(&String::from(&spans), self.prompt.line())
helix-tui/src/text.rs+6
@@ -402,6 +402,12 @@ impl<'a> From<&'a str> for Text<'a> {
402 }
402 }
403}
403}
404
404
405+impl<'a> From<Cow<'a, str>> for Text<'a> {
406+ fn from(s: Cow<'a, str>) -> Text<'a> {
407+ Text::raw(s)
408+ }
409+}
410+
405impl<'a> From<Span<'a>> for Text<'a> {
411impl<'a> From<Span<'a>> for Text<'a> {
406 fn from(span: Span<'a>) -> Text<'a> {
412 fn from(span: Span<'a>) -> Text<'a> {
407 Text {
413 Text {