on master help sign in
Improve completion: src/<tab> will now correctly complete to src/main.rs
a32806bBlaž Hrastnik
helix-term/src/commands.rs+11−5
@@ -723,20 +723,26 @@ pub fn command_mode(cx: &mut Context) {
723 // simple heuristic: if there's no space, complete command.
723 // simple heuristic: if there's no space, complete command.
724 // if there's a space, file completion kicks in. We should specialize by command later.
724 // if there's a space, file completion kicks in. We should specialize by command later.
725 if parts.len() <= 1 {
725 if parts.len() <= 1 {
726+ use std::{borrow::Cow, ops::Range};
727+ let end = 0..;
726 COMMAND_LIST
728 COMMAND_LIST
727 .iter()
729 .iter()
728 .filter(|command| command.contains(input))
730 .filter(|command| command.contains(input))
729− .map(|command| std::borrow::Cow::Borrowed(*command))
731+ .map(|command| (end.clone(), Cow::Borrowed(*command)))
730 .collect()
732 .collect()
731 } else {
733 } else {
732 let part = parts.last().unwrap();
734 let part = parts.last().unwrap();
733 ui::completers::filename(part)
735 ui::completers::filename(part)
736+ .into_iter()
737+ .map(|(range, file)| {
738+ // offset ranges to input
739+ let offset = input.len() - part.len();
740+ let range = (range.start + offset)..;
741+ (range, file)
742+ })
743+ .collect()
734
744
735 // TODO
745 // TODO
736− // completion needs to be more advanced: need to return starting index for replace
737− // for example, "src/" completion application.rs needs to insert after /, but "hx"
738− // completion helix-core needs to replace the text.
739− //
740 // additionally, completion items could have a info section that would get
746 // additionally, completion items could have a info section that would get
741 // displayed in a popup above the prompt when items are tabbed over
747 // displayed in a popup above the prompt when items are tabbed over
742 }
748 }
helix-term/src/ui/mod.rs+14−6
@@ -137,9 +137,9 @@ pub fn buffer_picker(views: &[View], current: usize) -> Picker<(Option<PathBuf>,
137}
137}
138
138
139pub mod completers {
139pub mod completers {
140− use std::borrow::Cow;
140+ use std::{borrow::Cow, ops::RangeFrom};
141 // TODO: we could return an iter/lazy thing so it can fetch as many as it needs.
141 // TODO: we could return an iter/lazy thing so it can fetch as many as it needs.
142− pub fn filename(input: &str) -> Vec<Cow<'static, str>> {
142+ pub fn filename(input: &str) -> Vec<(RangeFrom<usize>, Cow<'static, str>)> {
143 // Rust's filename handling is really annoying.
143 // Rust's filename handling is really annoying.
144
144
145 use ignore::WalkBuilder;
145 use ignore::WalkBuilder;
@@ -163,6 +163,8 @@ pub mod completers {
163 (path, file_name)
163 (path, file_name)
164 };
164 };
165
165
166+ let end = (input.len()..);
167+
166 let mut files: Vec<_> = WalkBuilder::new(dir.clone())
168 let mut files: Vec<_> = WalkBuilder::new(dir.clone())
167 .max_depth(Some(1))
169 .max_depth(Some(1))
168 .build()
170 .build()
@@ -178,10 +180,11 @@ pub mod completers {
178 if is_dir {
180 if is_dir {
179 path.push("");
181 path.push("");
180 }
182 }
181− Cow::from(path.to_str().unwrap().to_string())
183+ let path = path.to_str().unwrap().to_string();
184+ (end.clone(), Cow::from(path))
182 })
185 })
183 }) // TODO: unwrap or skip
186 }) // TODO: unwrap or skip
184− .filter(|path| !path.is_empty()) // TODO
187+ .filter(|(_, path)| !path.is_empty()) // TODO
185 .collect();
188 .collect();
186
189
187 // if empty, return a list of dirs and files in current dir
190 // if empty, return a list of dirs and files in current dir
@@ -195,15 +198,20 @@ pub mod completers {
195 // inefficient, but we need to calculate the scores, filter out None, then sort.
198 // inefficient, but we need to calculate the scores, filter out None, then sort.
196 let mut matches: Vec<_> = files
199 let mut matches: Vec<_> = files
197 .into_iter()
200 .into_iter()
198− .filter_map(|file| {
201+ .filter_map(|(range, file)| {
199 matcher
202 matcher
200 .fuzzy_match(&file, &file_name)
203 .fuzzy_match(&file, &file_name)
201 .map(|score| (file, score))
204 .map(|score| (file, score))
202 })
205 })
203 .collect();
206 .collect();
204
207
208+ let range = ((input.len() - file_name.len())..);
209+
205 matches.sort_unstable_by_key(|(_file, score)| Reverse(*score));
210 matches.sort_unstable_by_key(|(_file, score)| Reverse(*score));
206− files = matches.into_iter().map(|(file, _)| file).collect();
211+ files = matches
212+ .into_iter()
213+ .map(|(file, _)| (range.clone(), file))
214+ .collect();
207
215
208 // TODO: complete to longest common match
216 // TODO: complete to longest common match
209 }
217 }
helix-term/src/ui/prompt.rs+8−15
@@ -3,16 +3,15 @@ use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
3use helix_core::Position;
3use helix_core::Position;
4use helix_view::Editor;
4use helix_view::Editor;
5use helix_view::Theme;
5use helix_view::Theme;
6−use std::borrow::Cow;
6+use std::{borrow::Cow, ops::RangeFrom};
7−use std::string::String;
8
7
9pub struct Prompt {
8pub struct Prompt {
10 prompt: String,
9 prompt: String,
11 pub line: String,
10 pub line: String,
12 cursor: usize,
11 cursor: usize,
13− completion: Vec<Cow<'static, str>>,
12+ completion: Vec<(RangeFrom<usize>, Cow<'static, str>)>,
14 completion_selection_index: Option<usize>,
13 completion_selection_index: Option<usize>,
15− completion_fn: Box<dyn FnMut(&str) -> Vec<Cow<'static, str>>>,
14+ completion_fn: Box<dyn FnMut(&str) -> Vec<(RangeFrom<usize>, Cow<'static, str>)>>,
16 callback_fn: Box<dyn FnMut(&mut Editor, &str, PromptEvent)>,
15 callback_fn: Box<dyn FnMut(&mut Editor, &str, PromptEvent)>,
17}
16}
18
17
@@ -29,7 +28,7 @@ pub enum PromptEvent {
29impl Prompt {
28impl Prompt {
30 pub fn new(
29 pub fn new(
31 prompt: String,
30 prompt: String,
32− mut completion_fn: impl FnMut(&str) -> Vec<Cow<'static, str>> + 'static,
31+ mut completion_fn: impl FnMut(&str) -> Vec<(RangeFrom<usize>, Cow<'static, str>)> + 'static,
33 callback_fn: impl FnMut(&mut Editor, &str, PromptEvent) + 'static,
32 callback_fn: impl FnMut(&mut Editor, &str, PromptEvent) + 'static,
34 ) -> Prompt {
33 ) -> Prompt {
35 Prompt {
34 Prompt {
@@ -85,15 +84,9 @@ impl Prompt {
85 self.completion_selection_index.map(|i| i + 1).unwrap_or(0) % self.completion.len();
84 self.completion_selection_index.map(|i| i + 1).unwrap_or(0) % self.completion.len();
86 self.completion_selection_index = Some(index);
85 self.completion_selection_index = Some(index);
87
86
88− let item = &self.completion[index];
87+ let (range, item) = &self.completion[index];
89
88
90− // replace the last arg
91− if let Some(pos) = self.line.rfind(' ') {
92− self.line.replace_range(pos + 1.., item);
89+ self.line.replace_range(range.clone(), item);
93− } else {
94− // need toowned_clone_into nightly feature to reuse allocation
95− self.line = item.to_string();
96− }
97
90
98 self.move_end();
91 self.move_end();
99 // TODO: recalculate completion when completion item is accepted, (Enter)
92 // TODO: recalculate completion when completion item is accepted, (Enter)
@@ -135,7 +128,7 @@ impl Prompt {
135 Rect::new(0, area.height - col_height - 2, area.width, col_height),
128 Rect::new(0, area.height - col_height - 2, area.width, col_height),
136 theme.get("ui.statusline"),
129 theme.get("ui.statusline"),
137 );
130 );
138− for (i, command) in self.completion.iter().enumerate() {
131+ for (i, (_range, completion)) in self.completion.iter().enumerate() {
139 let color = if self.completion_selection_index.is_some()
132 let color = if self.completion_selection_index.is_some()
140 && i == self.completion_selection_index.unwrap()
133 && i == self.completion_selection_index.unwrap()
141 {
134 {
@@ -146,7 +139,7 @@ impl Prompt {
146 surface.set_stringn(
139 surface.set_stringn(
147 1 + col * BASE_WIDTH,
140 1 + col * BASE_WIDTH,
148 area.height - col_height - 2 + row,
141 area.height - col_height - 2 + row,
149− &command,
142+ &completion,
150 BASE_WIDTH as usize - 1,
143 BASE_WIDTH as usize - 1,
151 color,
144 color,
152 );
145 );