diff --git a/src/commands/open.rs b/src/commands/open.rs index 3ec228cf..fe309838 100644 --- a/src/commands/open.rs +++ b/src/commands/open.rs @@ -9,3 +9,12 @@ pub fn pin_query(app: &mut Application) -> Result { Ok(()) } + +pub fn toggle_selection(app: &mut Application) -> Result { + match app.mode { + Mode::Open(ref mut mode) => mode.toggle_selection(), + _ => bail!("Can't mark selections outside of open mode."), + } + + Ok(()) +} diff --git a/src/commands/search_select.rs b/src/commands/search_select.rs index be9fad33..e8c4a056 100644 --- a/src/commands/search_select.rs +++ b/src/commands/search_select.rs @@ -14,29 +14,27 @@ pub fn accept(app: &mut Application) -> Result { (selection.command)(app)?; } Mode::Open(ref mut mode) => { - let DisplayablePath(path) = mode - .selection() - .ok_or("Couldn't find a selected path to open")?; - - let syntax_definition = app - .preferences - .borrow() - .syntax_definition_name(path) - .and_then(|name| app.workspace.syntax_set.find_syntax_by_name(&name).cloned()); - - app.workspace - .open_buffer(path) - .chain_err(|| "Couldn't open a buffer for the specified path.")?; - - let buffer = app.workspace.current_buffer.as_mut().unwrap(); - - // Only override the default syntax definition if the user provided - // a valid one in their preferences. - if syntax_definition.is_some() { - buffer.syntax_definition = syntax_definition; + for DisplayablePath(path) in mode.selections() { + let syntax_definition = app + .preferences + .borrow() + .syntax_definition_name(path) + .and_then(|name| app.workspace.syntax_set.find_syntax_by_name(&name).cloned()); + + app.workspace + .open_buffer(path) + .chain_err(|| "Couldn't open a buffer for the specified path.")?; + + let buffer = app.workspace.current_buffer.as_mut().unwrap(); + + // Only override the default syntax definition if the user provided + // a valid one in their preferences. + if syntax_definition.is_some() { + buffer.syntax_definition = syntax_definition; + } + + app.view.initialize_buffer(buffer)?; } - - app.view.initialize_buffer(buffer)?; } Mode::Theme(ref mut mode) => { let theme_key = mode.selection().ok_or("No theme selected")?; diff --git a/src/input/key_map/default.yml b/src/input/key_map/default.yml index 8b4ae1ff..fe30bd15 100644 --- a/src/input/key_map/default.yml +++ b/src/input/key_map/default.yml @@ -162,7 +162,7 @@ path: search_select: enter: search_select::accept - space: search_select::accept + space: open::toggle_selection backspace: search_select::pop_search_token escape: application::switch_to_normal_mode up: search_select::select_previous diff --git a/src/models/application/modes/open/displayable_path.rs b/src/models/application/modes/open/displayable_path.rs index ee4fa46c..81d15d43 100644 --- a/src/models/application/modes/open/displayable_path.rs +++ b/src/models/application/modes/open/displayable_path.rs @@ -3,6 +3,7 @@ use std::path::PathBuf; // Newtype to make a standard path buffer presentable (via the Display // trait), which is required for any type used in search/select mode. +#[derive(Clone, Eq, Hash, PartialEq)] pub struct DisplayablePath(pub PathBuf); impl fmt::Display for DisplayablePath { diff --git a/src/models/application/modes/open/mod.rs b/src/models/application/modes/open/mod.rs index 46bd397c..91d06898 100644 --- a/src/models/application/modes/open/mod.rs +++ b/src/models/application/modes/open/mod.rs @@ -7,6 +7,7 @@ use crate::models::application::Event; use crate::util::SelectableVec; use bloodhound::ExclusionPattern; pub use bloodhound::Index; +use std::collections::HashSet; use std::fmt; use std::path::PathBuf; use std::slice::Iter; @@ -25,6 +26,7 @@ pub struct OpenMode { pinned_input: String, index: OpenModeIndex, pub results: SelectableVec, + marked_results: HashSet, config: SearchSelectConfig, } @@ -36,6 +38,7 @@ impl OpenMode { pinned_input: String::new(), index: OpenModeIndex::Indexing(path), results: SelectableVec::new(Vec::new()), + marked_results: HashSet::new(), config, } } @@ -56,6 +59,7 @@ impl OpenMode { self.config = config; self.index = OpenModeIndex::Indexing(path.clone()); self.results = SelectableVec::new(Vec::new()); + self.marked_results = HashSet::new(); // Build and populate the index in a separate thread. thread::spawn(move || { @@ -107,6 +111,30 @@ impl OpenMode { PopSearchToken::pop_search_token(self); } } + + pub fn toggle_selection(&mut self) { + if let None = self.marked_results.take(&self.selected_index()) { + self.marked_results.insert(self.selected_index()); + } + } + + pub fn selections(&self) -> Vec<&DisplayablePath> { + let mut selections: Vec<&DisplayablePath> = self + .marked_results + .iter() + .map(|i| self.results.get(*i).unwrap()) + .collect(); + selections.push(self.selection().unwrap()); + + selections + } + + pub fn selected_indices(&self) -> Vec { + let mut selected_indices: Vec = self.marked_results.iter().copied().collect(); + selected_indices.push(self.selected_index()); + + selected_indices + } } impl fmt::Display for OpenMode { @@ -137,6 +165,7 @@ impl SearchSelectMode for OpenMode { }; self.results = SelectableVec::new(results); + self.marked_results = HashSet::new(); } fn query(&mut self) -> &mut String { @@ -306,4 +335,194 @@ mod tests { mode.pop_search_token(); assert_eq!(mode.pinned_query(), ""); } + + #[test] + fn selections_returns_current_selection() { + let path = env::current_dir().expect("can't get current directory/path"); + let config = SearchSelectConfig::default(); + let mut mode = OpenMode::new(path.clone(), config.clone()); + let (sender, receiver) = channel(); + + // Populate the index + mode.reset(path, None, sender, config); + if let Ok(Event::OpenModeIndexComplete(index)) = receiver.recv() { + mode.set_index(index); + } + + mode.query().push_str("Cargo.toml"); + mode.search(); + + let selections: Vec = mode.selections().iter().map(|r| r.to_string()).collect(); + assert_eq!(selections, vec!["Cargo.toml"]); + } + + #[test] + fn selections_includes_marked_selections() { + let path = env::current_dir().expect("can't get current directory/path"); + let config = SearchSelectConfig::default(); + let mut mode = OpenMode::new(path.clone(), config.clone()); + let (sender, receiver) = channel(); + + // Populate the index + mode.reset(path, None, sender, config); + if let Ok(Event::OpenModeIndexComplete(index)) = receiver.recv() { + mode.set_index(index); + } + + mode.query().push_str("Cargo"); + mode.search(); + mode.toggle_selection(); + mode.select_next(); + + let mut selections: Vec = mode.selections().iter().map(|r| r.to_string()).collect(); + selections.sort(); + assert_eq!(selections, vec!["Cargo.lock", "Cargo.toml"]); + } + + #[test] + fn selections_does_not_include_unmarked_indices() { + let path = env::current_dir().expect("can't get current directory/path"); + let config = SearchSelectConfig::default(); + let mut mode = OpenMode::new(path.clone(), config.clone()); + let (sender, receiver) = channel(); + + // Populate the index + mode.reset(path, None, sender, config); + if let Ok(Event::OpenModeIndexComplete(index)) = receiver.recv() { + mode.set_index(index); + } + + mode.query().push_str("Cargo"); + mode.search(); + mode.toggle_selection(); + mode.toggle_selection(); + mode.select_next(); + + let selections: Vec = mode.selections().iter().map(|r| r.to_string()).collect(); + assert_eq!(selections, vec!["Cargo.lock"]); + } + + #[test] + fn selected_indices_returns_current_index() { + let path = env::current_dir().expect("can't get current directory/path"); + let config = SearchSelectConfig::default(); + let mut mode = OpenMode::new(path.clone(), config.clone()); + let (sender, receiver) = channel(); + + // Populate the index + mode.reset(path, None, sender, config); + if let Ok(Event::OpenModeIndexComplete(index)) = receiver.recv() { + mode.set_index(index); + } + + mode.query().push_str("Cargo.toml"); + mode.search(); + + assert_eq!(mode.selected_indices(), vec![0]); + } + + #[test] + fn selected_indices_includes_marked_indices() { + let path = env::current_dir().expect("can't get current directory/path"); + let config = SearchSelectConfig::default(); + let mut mode = OpenMode::new(path.clone(), config.clone()); + let (sender, receiver) = channel(); + + // Populate the index + mode.reset(path, None, sender, config); + if let Ok(Event::OpenModeIndexComplete(index)) = receiver.recv() { + mode.set_index(index); + } + + mode.query().push_str("Cargo"); + mode.search(); + mode.toggle_selection(); + mode.select_next(); + + assert_eq!(mode.selected_indices(), vec![0, 1]); + } + + #[test] + fn selected_indices_does_not_include_unmarked_indices() { + let path = env::current_dir().expect("can't get current directory/path"); + let config = SearchSelectConfig::default(); + let mut mode = OpenMode::new(path.clone(), config.clone()); + let (sender, receiver) = channel(); + + // Populate the index + mode.reset(path, None, sender, config); + if let Ok(Event::OpenModeIndexComplete(index)) = receiver.recv() { + mode.set_index(index); + } + + mode.query().push_str("Cargo"); + mode.search(); + mode.toggle_selection(); + mode.toggle_selection(); + mode.select_next(); + + assert_eq!(mode.selected_indices(), vec![1]); + } + + #[test] + fn search_clears_marked_indices() { + let path = env::current_dir().expect("can't get current directory/path"); + let config = SearchSelectConfig::default(); + let mut mode = OpenMode::new(path.clone(), config.clone()); + let (sender, receiver) = channel(); + + // Populate the index + mode.reset(path.clone(), None, sender.clone(), config.clone()); + if let Ok(Event::OpenModeIndexComplete(index)) = receiver.recv() { + mode.set_index(index); + } + + // Produce results and mark one of them + mode.query().push_str("Cargo"); + mode.search(); + mode.toggle_selection(); + + // Change the search results + mode.query().push_str("."); + mode.search(); + + // Ensure the previously-marked result isn't currently selected + mode.select_next(); + + // Verify that the marked result isn't included + assert_eq!(mode.selected_indices(), vec![1]); + } + + #[test] + fn reset_clears_marked_indices() { + let path = env::current_dir().expect("can't get current directory/path"); + let config = SearchSelectConfig::default(); + let mut mode = OpenMode::new(path.clone(), config.clone()); + let (sender, receiver) = channel(); + + // Populate the index + mode.reset(path.clone(), None, sender.clone(), config.clone()); + if let Ok(Event::OpenModeIndexComplete(index)) = receiver.recv() { + mode.set_index(index); + } + + // Produce results and mark one of them + mode.query().push_str("Cargo"); + mode.search(); + mode.toggle_selection(); + + // Reset the mode and repopulate the index + mode.reset(path, None, sender, config); + if let Ok(Event::OpenModeIndexComplete(index)) = receiver.recv() { + mode.set_index(index); + } + mode.query().push_str("Cargo"); + mode.search(); + + // Ensure the previously-marked result isn't currently selected + mode.select_next(); + + // Verify that the marked result isn't included + assert_eq!(mode.selected_indices(), vec![1]); + } } diff --git a/src/presenters/modes/open.rs b/src/presenters/modes/open.rs index 3a3469a9..8cbdca9e 100644 --- a/src/presenters/modes/open.rs +++ b/src/presenters/modes/open.rs @@ -41,9 +41,13 @@ pub fn display(workspace: &mut Workspace, mode: &mut OpenMode, view: &mut View) ); } else { // Draw the list of search results. + let selected_indices = mode.selected_indices(); + for (line, result) in mode.results().enumerate() { let (content, colors, style) = if line == mode.selected_index() { (format!("> {}", result), Colors::Focused, Style::Bold) + } else if selected_indices.contains(&line) { + (format!(" {}", result), Colors::Focused, Style::Bold) } else { (format!(" {}", result), Colors::Default, Style::Default) };