Skip to content

Commit 7cba26e

Browse files
committed
Implement multi-select for open mode
This introduces the ability to select multiple entries in open mode. Entries can be toggled on/off using the space key when navigating using the normal sub-mode.
1 parent e7f9c83 commit 7cba26e

6 files changed

Lines changed: 254 additions & 23 deletions

File tree

‎src/commands/open.rs‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,12 @@ pub fn pin_query(app: &mut Application) -> Result {
99

1010
Ok(())
1111
}
12+
13+
pub fn toggle_selection(app: &mut Application) -> Result {
14+
match app.mode {
15+
Mode::Open(ref mut mode) => mode.toggle_selection(),
16+
_ => bail!("Can't mark selections outside of open mode."),
17+
}
18+
19+
Ok(())
20+
}

‎src/commands/search_select.rs‎

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,29 +14,27 @@ pub fn accept(app: &mut Application) -> Result {
1414
(selection.command)(app)?;
1515
}
1616
Mode::Open(ref mut mode) => {
17-
let DisplayablePath(path) = mode
18-
.selection()
19-
.ok_or("Couldn't find a selected path to open")?;
20-
21-
let syntax_definition = app
22-
.preferences
23-
.borrow()
24-
.syntax_definition_name(path)
25-
.and_then(|name| app.workspace.syntax_set.find_syntax_by_name(&name).cloned());
26-
27-
app.workspace
28-
.open_buffer(path)
29-
.chain_err(|| "Couldn't open a buffer for the specified path.")?;
30-
31-
let buffer = app.workspace.current_buffer.as_mut().unwrap();
32-
33-
// Only override the default syntax definition if the user provided
34-
// a valid one in their preferences.
35-
if syntax_definition.is_some() {
36-
buffer.syntax_definition = syntax_definition;
17+
for DisplayablePath(path) in mode.selections() {
18+
let syntax_definition = app
19+
.preferences
20+
.borrow()
21+
.syntax_definition_name(path)
22+
.and_then(|name| app.workspace.syntax_set.find_syntax_by_name(&name).cloned());
23+
24+
app.workspace
25+
.open_buffer(path)
26+
.chain_err(|| "Couldn't open a buffer for the specified path.")?;
27+
28+
let buffer = app.workspace.current_buffer.as_mut().unwrap();
29+
30+
// Only override the default syntax definition if the user provided
31+
// a valid one in their preferences.
32+
if syntax_definition.is_some() {
33+
buffer.syntax_definition = syntax_definition;
34+
}
35+
36+
app.view.initialize_buffer(buffer)?;
3737
}
38-
39-
app.view.initialize_buffer(buffer)?;
4038
}
4139
Mode::Theme(ref mut mode) => {
4240
let theme_key = mode.selection().ok_or("No theme selected")?;

‎src/input/key_map/default.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ path:
162162

163163
search_select:
164164
enter: search_select::accept
165-
space: search_select::accept
165+
space: open::toggle_selection
166166
backspace: search_select::pop_search_token
167167
escape: application::switch_to_normal_mode
168168
up: search_select::select_previous

‎src/models/application/modes/open/displayable_path.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::path::PathBuf;
33

44
// Newtype to make a standard path buffer presentable (via the Display
55
// trait), which is required for any type used in search/select mode.
6+
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
67
pub struct DisplayablePath(pub PathBuf);
78

89
impl fmt::Display for DisplayablePath {

‎src/models/application/modes/open/mod.rs‎

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use crate::models::application::Event;
77
use crate::util::SelectableVec;
88
use bloodhound::ExclusionPattern;
99
pub use bloodhound::Index;
10+
use std::collections::HashSet;
1011
use std::fmt;
1112
use std::path::PathBuf;
1213
use std::slice::Iter;
@@ -25,6 +26,7 @@ pub struct OpenMode {
2526
pinned_input: String,
2627
index: OpenModeIndex,
2728
pub results: SelectableVec<DisplayablePath>,
29+
marked_results: HashSet<usize>,
2830
config: SearchSelectConfig,
2931
}
3032

@@ -36,6 +38,7 @@ impl OpenMode {
3638
pinned_input: String::new(),
3739
index: OpenModeIndex::Indexing(path),
3840
results: SelectableVec::new(Vec::new()),
41+
marked_results: HashSet::new(),
3942
config,
4043
}
4144
}
@@ -56,6 +59,7 @@ impl OpenMode {
5659
self.config = config;
5760
self.index = OpenModeIndex::Indexing(path.clone());
5861
self.results = SelectableVec::new(Vec::new());
62+
self.marked_results = HashSet::new();
5963

6064
// Build and populate the index in a separate thread.
6165
thread::spawn(move || {
@@ -107,6 +111,30 @@ impl OpenMode {
107111
PopSearchToken::pop_search_token(self);
108112
}
109113
}
114+
115+
pub fn toggle_selection(&mut self) {
116+
if let None = self.marked_results.take(&self.selected_index()) {
117+
self.marked_results.insert(self.selected_index());
118+
}
119+
}
120+
121+
pub fn selections(&self) -> Vec<&DisplayablePath> {
122+
let mut selections: Vec<&DisplayablePath> = self
123+
.marked_results
124+
.iter()
125+
.map(|i| self.results.get(*i).unwrap())
126+
.collect();
127+
selections.push(self.selection().unwrap());
128+
129+
selections
130+
}
131+
132+
pub fn selected_indices(&self) -> Vec<usize> {
133+
let mut selected_indices: Vec<usize> = self.marked_results.iter().copied().collect();
134+
selected_indices.push(self.selected_index());
135+
136+
selected_indices
137+
}
110138
}
111139

112140
impl fmt::Display for OpenMode {
@@ -137,6 +165,7 @@ impl SearchSelectMode for OpenMode {
137165
};
138166

139167
self.results = SelectableVec::new(results);
168+
self.marked_results = HashSet::new();
140169
}
141170

142171
fn query(&mut self) -> &mut String {
@@ -191,6 +220,7 @@ impl SearchSelectMode for OpenMode {
191220
#[cfg(test)]
192221
mod tests {
193222
use super::OpenMode;
223+
use crate::models::application::modes::open::DisplayablePath;
194224
use crate::models::application::modes::{SearchSelectConfig, SearchSelectMode};
195225
use crate::models::application::Event;
196226
use std::env;
@@ -306,4 +336,193 @@ mod tests {
306336
mode.pop_search_token();
307337
assert_eq!(mode.pinned_query(), "");
308338
}
339+
340+
#[test]
341+
fn selections_returns_current_selection() {
342+
let path = env::current_dir().expect("can't get current directory/path");
343+
let config = SearchSelectConfig::default();
344+
let mut mode = OpenMode::new(path.clone(), config.clone());
345+
let (sender, receiver) = channel();
346+
347+
// Populate the index
348+
mode.reset(path, None, sender, config);
349+
if let Ok(Event::OpenModeIndexComplete(index)) = receiver.recv() {
350+
mode.set_index(index);
351+
}
352+
353+
mode.query().push_str("Cargo.toml");
354+
mode.search();
355+
356+
let selections: Vec<&DisplayablePath> = mode.selections().iter().copied().collect();
357+
assert_eq!(selections, vec![mode.results().next().unwrap()]);
358+
}
359+
360+
#[test]
361+
fn selections_includes_marked_selections() {
362+
let path = env::current_dir().expect("can't get current directory/path");
363+
let config = SearchSelectConfig::default();
364+
let mut mode = OpenMode::new(path.clone(), config.clone());
365+
let (sender, receiver) = channel();
366+
367+
// Populate the index
368+
mode.reset(path, None, sender, config);
369+
if let Ok(Event::OpenModeIndexComplete(index)) = receiver.recv() {
370+
mode.set_index(index);
371+
}
372+
373+
mode.query().push_str("Cargo");
374+
mode.search();
375+
mode.toggle_selection();
376+
mode.select_next();
377+
378+
let selections: Vec<&DisplayablePath> = mode.selections().iter().copied().collect();
379+
assert_eq!(selections, mode.results().take(2).collect::<Vec<_>>());
380+
}
381+
382+
#[test]
383+
fn selections_does_not_include_unmarked_indices() {
384+
let path = env::current_dir().expect("can't get current directory/path");
385+
let config = SearchSelectConfig::default();
386+
let mut mode = OpenMode::new(path.clone(), config.clone());
387+
let (sender, receiver) = channel();
388+
389+
// Populate the index
390+
mode.reset(path, None, sender, config);
391+
if let Ok(Event::OpenModeIndexComplete(index)) = receiver.recv() {
392+
mode.set_index(index);
393+
}
394+
395+
mode.query().push_str("Cargo");
396+
mode.search();
397+
mode.toggle_selection();
398+
mode.toggle_selection();
399+
mode.select_next();
400+
401+
let selections: Vec<&DisplayablePath> = mode.selections().iter().copied().collect();
402+
assert_eq!(selections, vec![mode.results().nth(1).unwrap()]);
403+
}
404+
405+
#[test]
406+
fn selected_indices_returns_current_index() {
407+
let path = env::current_dir().expect("can't get current directory/path");
408+
let config = SearchSelectConfig::default();
409+
let mut mode = OpenMode::new(path.clone(), config.clone());
410+
let (sender, receiver) = channel();
411+
412+
// Populate the index
413+
mode.reset(path, None, sender, config);
414+
if let Ok(Event::OpenModeIndexComplete(index)) = receiver.recv() {
415+
mode.set_index(index);
416+
}
417+
418+
mode.query().push_str("Cargo.toml");
419+
mode.search();
420+
421+
assert_eq!(mode.selected_indices(), vec![0]);
422+
}
423+
424+
#[test]
425+
fn selected_indices_includes_marked_indices() {
426+
let path = env::current_dir().expect("can't get current directory/path");
427+
let config = SearchSelectConfig::default();
428+
let mut mode = OpenMode::new(path.clone(), config.clone());
429+
let (sender, receiver) = channel();
430+
431+
// Populate the index
432+
mode.reset(path, None, sender, config);
433+
if let Ok(Event::OpenModeIndexComplete(index)) = receiver.recv() {
434+
mode.set_index(index);
435+
}
436+
437+
mode.query().push_str("Cargo");
438+
mode.search();
439+
mode.toggle_selection();
440+
mode.select_next();
441+
442+
assert_eq!(mode.selected_indices(), vec![0, 1]);
443+
}
444+
445+
#[test]
446+
fn selected_indices_does_not_include_unmarked_indices() {
447+
let path = env::current_dir().expect("can't get current directory/path");
448+
let config = SearchSelectConfig::default();
449+
let mut mode = OpenMode::new(path.clone(), config.clone());
450+
let (sender, receiver) = channel();
451+
452+
// Populate the index
453+
mode.reset(path, None, sender, config);
454+
if let Ok(Event::OpenModeIndexComplete(index)) = receiver.recv() {
455+
mode.set_index(index);
456+
}
457+
458+
mode.query().push_str("Cargo");
459+
mode.search();
460+
mode.toggle_selection();
461+
mode.toggle_selection();
462+
mode.select_next();
463+
464+
assert_eq!(mode.selected_indices(), vec![1]);
465+
}
466+
467+
#[test]
468+
fn search_clears_marked_indices() {
469+
let path = env::current_dir().expect("can't get current directory/path");
470+
let config = SearchSelectConfig::default();
471+
let mut mode = OpenMode::new(path.clone(), config.clone());
472+
let (sender, receiver) = channel();
473+
474+
// Populate the index
475+
mode.reset(path.clone(), None, sender.clone(), config.clone());
476+
if let Ok(Event::OpenModeIndexComplete(index)) = receiver.recv() {
477+
mode.set_index(index);
478+
}
479+
480+
// Produce results and mark one of them
481+
mode.query().push_str("Cargo");
482+
mode.search();
483+
mode.toggle_selection();
484+
485+
// Change the search results
486+
mode.query().push_str(".");
487+
mode.search();
488+
489+
// Ensure the previously-marked result isn't currently selected
490+
mode.select_next();
491+
492+
// Verify that the marked result isn't included
493+
assert_eq!(mode.selected_indices(), vec![1]);
494+
}
495+
496+
#[test]
497+
fn reset_clears_marked_indices() {
498+
let path = env::current_dir().expect("can't get current directory/path");
499+
let config = SearchSelectConfig::default();
500+
let mut mode = OpenMode::new(path.clone(), config.clone());
501+
let (sender, receiver) = channel();
502+
503+
// Populate the index
504+
mode.reset(path.clone(), None, sender.clone(), config.clone());
505+
if let Ok(Event::OpenModeIndexComplete(index)) = receiver.recv() {
506+
mode.set_index(index);
507+
}
508+
509+
// Produce results and mark one of them
510+
mode.query().push_str("Cargo");
511+
mode.search();
512+
mode.toggle_selection();
513+
514+
// Reset the mode and repopulate the index
515+
mode.reset(path, None, sender, config);
516+
if let Ok(Event::OpenModeIndexComplete(index)) = receiver.recv() {
517+
mode.set_index(index);
518+
}
519+
mode.query().push_str("Cargo");
520+
mode.search();
521+
522+
// Ensure the previously-marked result isn't currently selected
523+
mode.select_next();
524+
525+
// Verify that the marked result isn't included
526+
assert_eq!(mode.selected_indices(), vec![1]);
527+
}
309528
}

‎src/presenters/modes/open.rs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,13 @@ pub fn display(workspace: &mut Workspace, mode: &mut OpenMode, view: &mut View)
4141
);
4242
} else {
4343
// Draw the list of search results.
44+
let selected_indices = mode.selected_indices();
45+
4446
for (line, result) in mode.results().enumerate() {
4547
let (content, colors, style) = if line == mode.selected_index() {
4648
(format!("> {}", result), Colors::Focused, Style::Bold)
49+
} else if selected_indices.contains(&line) {
50+
(format!(" {}", result), Colors::Focused, Style::Bold)
4751
} else {
4852
(format!(" {}", result), Colors::Default, Style::Default)
4953
};

0 commit comments

Comments
 (0)