on master help sign in
picker: Factor out file picker, we want to reuse code for other pickers.
25aa45eBlaž Hrastnik
helix-term/src/commands.rs+1−1
@@ -458,7 +458,7 @@ pub fn command_mode(cx: &mut Context) {
458}
458}
459pub fn file_picker(cx: &mut Context) {
459pub fn file_picker(cx: &mut Context) {
460 cx.callback = Some(Box::new(|compositor: &mut Compositor| {
460 cx.callback = Some(Box::new(|compositor: &mut Compositor| {
461− let picker = ui::Picker::new();
461+ let picker = ui::file_picker("./");
462 compositor.push(Box::new(picker));
462 compositor.push(Box::new(picker));
463 }));
463 }));
464}
464}
helix-term/src/ui/mod.rs+32
@@ -14,3 +14,35 @@ pub use tui::style::{Color, Modifier, Style};
14pub fn text_color() -> Style {
14pub fn text_color() -> Style {
15 Style::default().fg(Color::Rgb(219, 191, 239)) // lilac
15 Style::default().fg(Color::Rgb(219, 191, 239)) // lilac
16}
16}
17+
18+use std::path::PathBuf;
19+pub fn file_picker(root: &str) -> Picker<PathBuf> {
20+ use ignore::Walk;
21+ // TODO: determine root based on git root
22+ let files = Walk::new(root).filter_map(|entry| match entry {
23+ Ok(entry) => {
24+ // filter dirs, but we might need special handling for symlinks!
25+ if !entry.file_type().unwrap().is_dir() {
26+ Some(entry.into_path())
27+ } else {
28+ None
29+ }
30+ }
31+ Err(_err) => None,
32+ });
33+
34+ const MAX: usize = 1024;
35+
36+ use helix_view::Editor;
37+ Picker::new(
38+ files.take(MAX).collect(),
39+ |path: &PathBuf| {
40+ // format_fn
41+ path.strip_prefix("./").unwrap().to_str().unwrap() // TODO: render paths without ./
42+ },
43+ |editor: &mut Editor, path: &PathBuf| {
44+ let size = editor.view().unwrap().size;
45+ editor.open(path.into(), size);
46+ },
47+ )
48+}
helix-term/src/ui/picker.rs+38−42
@@ -9,16 +9,13 @@ use tui::{
9
9
10use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
10use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
11use fuzzy_matcher::FuzzyMatcher;
11use fuzzy_matcher::FuzzyMatcher;
12−use ignore::Walk;
13−
14−use std::path::PathBuf;
15
12
16use crate::ui::{Prompt, PromptEvent};
13use crate::ui::{Prompt, PromptEvent};
17use helix_core::Position;
14use helix_core::Position;
18use helix_view::Editor;
15use helix_view::Editor;
19
16
20−pub struct Picker {
17+pub struct Picker<T> {
21− files: Vec<PathBuf>,
18+ options: Vec<T>,
22 // filter: String,
19 // filter: String,
23 matcher: Box<Matcher>,
20 matcher: Box<Matcher>,
24 /// (index, score)
21 /// (index, score)
@@ -27,22 +24,17 @@ pub struct Picker {
27 cursor: usize,
24 cursor: usize,
28 // pattern: String,
25 // pattern: String,
29 prompt: Prompt,
26 prompt: Prompt,
30−}
31
27
32−impl Picker {
28+ format_fn: Box<dyn Fn(&T) -> &str>,
33− pub fn new() -> Self {
29+ callback_fn: Box<dyn Fn(&mut Editor, &T)>,
34− let files = Walk::new("./").filter_map(|entry| match entry {
35− Ok(entry) => {
36− // filter dirs, but we might need special handling for symlinks!
37− if !entry.file_type().unwrap().is_dir() {
38− Some(entry.into_path())
39− } else {
40− None
41− }
42− }
43− Err(_err) => None,
44− });
30+}
45
31
32+impl<T> Picker<T> {
33+ pub fn new(
34+ options: Vec<T>,
35+ format_fn: impl Fn(&T) -> &str + 'static,
36+ callback_fn: impl Fn(&mut Editor, &T) + 'static,
37+ ) -> Self {
46 let prompt = Prompt::new(
38 let prompt = Prompt::new(
47 "".to_string(),
39 "".to_string(),
48 |pattern: &str| Vec::new(),
40 |pattern: &str| Vec::new(),
@@ -51,14 +43,14 @@ impl Picker {
51 },
43 },
52 );
44 );
53
45
54− const MAX: usize = 1024;
55−
56 let mut picker = Self {
46 let mut picker = Self {
57− files: files.take(MAX).collect(),
47+ options,
58 matcher: Box::new(Matcher::default()),
48 matcher: Box::new(Matcher::default()),
59 matches: Vec::new(),
49 matches: Vec::new(),
60 cursor: 0,
50 cursor: 0,
61 prompt,
51 prompt,
52+ format_fn: Box::new(format_fn),
53+ callback_fn: Box::new(callback_fn),
62 };
54 };
63
55
64 // TODO: scoring on empty input should just use a fastpath
56 // TODO: scoring on empty input should just use a fastpath
@@ -70,9 +62,10 @@ impl Picker {
70 pub fn score(&mut self) {
62 pub fn score(&mut self) {
71 // need to borrow via pattern match otherwise it complains about simultaneous borrow
63 // need to borrow via pattern match otherwise it complains about simultaneous borrow
72 let Self {
64 let Self {
73− ref mut files,
65+ ref mut options,
74 ref mut matcher,
66 ref mut matcher,
75 ref mut matches,
67 ref mut matches,
68+ ref format_fn,
76 ..
69 ..
77 } = *self;
70 } = *self;
78
71
@@ -80,15 +73,19 @@ impl Picker {
80
73
81 // reuse the matches allocation
74 // reuse the matches allocation
82 matches.clear();
75 matches.clear();
76+ matches.extend(
77+ self.options
78+ .iter()
79+ .enumerate()
83− matches.extend(self.files.iter().enumerate().filter_map(|(index, path)| {
80+ .filter_map(|(index, option)| {
84− match path.to_str() {
81+ // TODO: maybe using format_fn isn't the best idea here
82+ let text = (format_fn)(option);
85− // TODO: using fuzzy_indices could give us the char idx for match highlighting
83+ // TODO: using fuzzy_indices could give us the char idx for match highlighting
86− Some(path) => matcher
84+ matcher
87− .fuzzy_match(path, pattern)
85+ .fuzzy_match(text, pattern)
88− .map(|score| (index, score)),
86+ .map(|score| (index, score))
89− None => None,
90− }
87+ }),
91− }));
88+ );
92 matches.sort_unstable_by_key(|(_, score)| -score);
89 matches.sort_unstable_by_key(|(_, score)| -score);
93
90
94 // reset cursor position
91 // reset cursor position
@@ -101,15 +98,15 @@ impl Picker {
101
98
102 pub fn move_down(&mut self) {
99 pub fn move_down(&mut self) {
103 // TODO: len - 1
100 // TODO: len - 1
104− if self.cursor < self.files.len() {
101+ if self.cursor < self.options.len() {
105 self.cursor += 1;
102 self.cursor += 1;
106 }
103 }
107 }
104 }
108
105
109− pub fn selection(&self) -> Option<&PathBuf> {
106+ pub fn selection(&self) -> Option<&T> {
110 self.matches
107 self.matches
111 .get(self.cursor)
108 .get(self.cursor)
112− .map(|(index, _score)| &self.files[*index])
109+ .map(|(index, _score)| &self.options[*index])
113 }
110 }
114}
111}
115
112
@@ -118,7 +115,7 @@ impl Picker {
118// - on input change:
115// - on input change:
119// - score all the names in relation to input
116// - score all the names in relation to input
120
117
121−impl Component for Picker {
118+impl<T> Component for Picker<T> {
122 fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
119 fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
123 let key_event = match event {
120 let key_event = match event {
124 Event::Key(event) => event,
121 Event::Key(event) => event,
@@ -163,9 +160,8 @@ impl Component for Picker {
163 code: KeyCode::Enter,
160 code: KeyCode::Enter,
164 ..
161 ..
165 } => {
162 } => {
166− let size = cx.editor.view().unwrap().size;
167− if let Some(path) = self.selection() {
163+ if let Some(option) = self.selection() {
168− cx.editor.open(path.into(), size);
164+ (self.callback_fn)(&mut cx.editor, option);
169 }
165 }
170 return close_fn;
166 return close_fn;
171 }
167 }
@@ -238,10 +234,10 @@ impl Component for Picker {
238 let rows = inner.height - 2; // -1 for search bar
234 let rows = inner.height - 2; // -1 for search bar
239
235
240 let files = self.matches.iter().map(|(index, _score)| {
236 let files = self.matches.iter().map(|(index, _score)| {
241− (index, self.files.get(*index).unwrap()) // get_unchecked
237+ (index, self.options.get(*index).unwrap()) // get_unchecked
242 });
238 });
243
239
244− for (i, (_index, file)) in files.take(rows as usize).enumerate() {
240+ for (i, (_index, option)) in files.take(rows as usize).enumerate() {
245 if i == self.cursor {
241 if i == self.cursor {
246 surface.set_string(inner.x + 1, inner.y + 2 + i as u16, ">", selected);
242 surface.set_string(inner.x + 1, inner.y + 2 + i as u16, ">", selected);
247 }
243 }
@@ -249,7 +245,7 @@ impl Component for Picker {
249 surface.set_stringn(
245 surface.set_stringn(
250 inner.x + 3,
246 inner.x + 3,
251 inner.y + 2 + i as u16,
247 inner.y + 2 + i as u16,
252− file.strip_prefix("./").unwrap().to_str().unwrap(), // TODO: render paths without ./
248+ (self.format_fn)(option),
253 inner.width as usize - 1,
249 inner.width as usize - 1,
254 if i == self.cursor { selected } else { style },
250 if i == self.cursor { selected } else { style },
255 );
251 );