Skip to content

Commit

Permalink
Implement multi-select for open mode
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
jmacdonald committed Oct 24, 2024
1 parent e7f9c83 commit fca85f9
Show file tree
Hide file tree
Showing 6 changed files with 254 additions and 23 deletions.
9 changes: 9 additions & 0 deletions src/commands/open.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}
42 changes: 20 additions & 22 deletions src/commands/search_select.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")?;
Expand Down
2 changes: 1 addition & 1 deletion src/input/key_map/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/models/application/modes/open/displayable_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
219 changes: 219 additions & 0 deletions src/models/application/modes/open/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -25,6 +26,7 @@ pub struct OpenMode {
pinned_input: String,
index: OpenModeIndex,
pub results: SelectableVec<DisplayablePath>,
marked_results: HashSet<usize>,
config: SearchSelectConfig,
}

Expand All @@ -36,6 +38,7 @@ impl OpenMode {
pinned_input: String::new(),
index: OpenModeIndex::Indexing(path),
results: SelectableVec::new(Vec::new()),
marked_results: HashSet::new(),
config,
}
}
Expand All @@ -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 || {
Expand Down Expand Up @@ -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<usize> {
let mut selected_indices: Vec<usize> = self.marked_results.iter().copied().collect();
selected_indices.push(self.selected_index());

selected_indices
}
}

impl fmt::Display for OpenMode {
Expand Down Expand Up @@ -137,6 +165,7 @@ impl SearchSelectMode for OpenMode {
};

self.results = SelectableVec::new(results);
self.marked_results = HashSet::new();
}

fn query(&mut self) -> &mut String {
Expand Down Expand Up @@ -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<String> = 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<String> = 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<String> = 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]);
}
}
4 changes: 4 additions & 0 deletions src/presenters/modes/open.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
};
Expand Down

0 comments on commit fca85f9

Please sign in to comment.