mirror of
https://github.com/alexpasmantier/television.git
synced 2025-06-07 03:55:23 +00:00
feat: add substring matching with exact flag
This commit is contained in:
parent
ce02824f3c
commit
0abeb904de
@ -512,8 +512,9 @@ pub fn draw(c: &mut Criterion) {
|
||||
]));
|
||||
channel.find("television");
|
||||
// Wait for the channel to finish loading
|
||||
let mut tv =
|
||||
Television::new(tx, channel, config, None, false, false);
|
||||
let mut tv = Television::new(
|
||||
tx, channel, config, None, false, false, false,
|
||||
);
|
||||
for _ in 0..5 {
|
||||
// tick the matcher
|
||||
let _ = tv.channel.results(10, 0);
|
||||
|
@ -16,7 +16,11 @@ use crate::{
|
||||
render::{render, RenderingTask},
|
||||
};
|
||||
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
pub struct AppOptions {
|
||||
/// Whether the application should use subsring matching instead of fuzzy
|
||||
/// matching.
|
||||
pub exact: bool,
|
||||
/// Whether the application should automatically select the first entry if there is only one
|
||||
/// entry available.
|
||||
pub select_1: bool,
|
||||
@ -30,6 +34,7 @@ pub struct AppOptions {
|
||||
impl Default for AppOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
exact: false,
|
||||
select_1: false,
|
||||
no_remote: false,
|
||||
no_help: false,
|
||||
@ -39,13 +44,16 @@ impl Default for AppOptions {
|
||||
}
|
||||
|
||||
impl AppOptions {
|
||||
#[allow(clippy::fn_params_excessive_bools)]
|
||||
pub fn new(
|
||||
exact: bool,
|
||||
select_1: bool,
|
||||
no_remote: bool,
|
||||
no_help: bool,
|
||||
tick_rate: f64,
|
||||
) -> Self {
|
||||
Self {
|
||||
exact,
|
||||
select_1,
|
||||
no_remote,
|
||||
no_help,
|
||||
@ -149,6 +157,7 @@ impl App {
|
||||
input,
|
||||
options.no_remote,
|
||||
options.no_help,
|
||||
options.exact,
|
||||
);
|
||||
|
||||
Self {
|
||||
|
@ -99,6 +99,14 @@ pub struct Cli {
|
||||
#[arg(long, value_name = "STRING", verbatim_doc_comment)]
|
||||
pub autocomplete_prompt: Option<String>,
|
||||
|
||||
/// Use substring matching instead of fuzzy matching.
|
||||
///
|
||||
/// This will use substring matching instead of fuzzy matching when
|
||||
/// searching for entries. This is useful when the user wants to search for
|
||||
/// an exact match instead of a fuzzy match e.g. to improve performance.
|
||||
#[arg(long, default_value = "false", verbatim_doc_comment)]
|
||||
pub exact: bool,
|
||||
|
||||
/// Automatically select and output the first entry if there is only one
|
||||
/// entry.
|
||||
///
|
||||
|
@ -31,6 +31,7 @@ pub struct PostProcessedCli {
|
||||
pub working_directory: Option<String>,
|
||||
pub autocomplete_prompt: Option<String>,
|
||||
pub keybindings: Option<KeyBindings>,
|
||||
pub exact: bool,
|
||||
pub select_1: bool,
|
||||
pub no_remote: bool,
|
||||
pub no_help: bool,
|
||||
@ -50,6 +51,7 @@ impl Default for PostProcessedCli {
|
||||
working_directory: None,
|
||||
autocomplete_prompt: None,
|
||||
keybindings: None,
|
||||
exact: false,
|
||||
select_1: false,
|
||||
no_remote: false,
|
||||
no_help: false,
|
||||
@ -118,6 +120,7 @@ impl From<Cli> for PostProcessedCli {
|
||||
working_directory,
|
||||
autocomplete_prompt: cli.autocomplete_prompt,
|
||||
keybindings,
|
||||
exact: cli.exact,
|
||||
select_1: cli.select_1,
|
||||
no_remote: cli.no_remote,
|
||||
no_help: cli.no_help,
|
||||
|
@ -66,6 +66,7 @@ async fn main() -> Result<()> {
|
||||
|
||||
debug!("Creating application...");
|
||||
let options = AppOptions::new(
|
||||
args.exact,
|
||||
args.select_1,
|
||||
args.no_remote,
|
||||
args.no_help,
|
||||
|
@ -9,6 +9,7 @@
|
||||
/// default number of threads (which corresponds to the number of available logical
|
||||
/// cores on the current machine).
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
pub struct Config {
|
||||
/// The number of threads to use for the fuzzy matcher.
|
||||
pub n_threads: Option<usize>,
|
||||
|
@ -31,6 +31,12 @@ pub enum Mode {
|
||||
SendToChannel,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Copy, Clone, Hash, Eq, Debug, Serialize, Deserialize)]
|
||||
pub enum MatchingMode {
|
||||
Substring,
|
||||
Fuzzy,
|
||||
}
|
||||
|
||||
pub struct Television {
|
||||
action_tx: UnboundedSender<Action>,
|
||||
pub config: Config,
|
||||
@ -38,6 +44,7 @@ pub struct Television {
|
||||
pub remote_control: Option<TelevisionChannel>,
|
||||
pub mode: Mode,
|
||||
pub current_pattern: String,
|
||||
pub matching_mode: MatchingMode,
|
||||
pub results_picker: Picker,
|
||||
pub rc_picker: Picker,
|
||||
pub previewer: Previewer,
|
||||
@ -60,6 +67,7 @@ impl Television {
|
||||
input: Option<String>,
|
||||
no_remote: bool,
|
||||
no_help: bool,
|
||||
exact: bool,
|
||||
) -> Self {
|
||||
let mut results_picker = Picker::new(input.clone());
|
||||
if config.ui.input_bar_position == InputPosition::Bottom {
|
||||
@ -103,6 +111,12 @@ impl Television {
|
||||
config.ui.show_help_bar = false;
|
||||
}
|
||||
|
||||
let matching_mode = if exact {
|
||||
MatchingMode::Substring
|
||||
} else {
|
||||
MatchingMode::Fuzzy
|
||||
};
|
||||
|
||||
Self {
|
||||
action_tx,
|
||||
config,
|
||||
@ -111,6 +125,7 @@ impl Television {
|
||||
mode: Mode::Channel,
|
||||
current_pattern: EMPTY_STRING.to_string(),
|
||||
results_picker,
|
||||
matching_mode,
|
||||
rc_picker: Picker::default(),
|
||||
previewer,
|
||||
preview_state,
|
||||
@ -182,7 +197,10 @@ impl Television {
|
||||
fn find(&mut self, pattern: &str) {
|
||||
match self.mode {
|
||||
Mode::Channel => {
|
||||
self.channel.find(pattern);
|
||||
self.channel.find(
|
||||
Self::preprocess_pattern(self.matching_mode, pattern)
|
||||
.as_str(),
|
||||
);
|
||||
}
|
||||
Mode::RemoteControl | Mode::SendToChannel => {
|
||||
self.remote_control.as_mut().unwrap().find(pattern);
|
||||
@ -190,6 +208,21 @@ impl Television {
|
||||
}
|
||||
}
|
||||
|
||||
fn preprocess_pattern(mode: MatchingMode, pattern: &str) -> String {
|
||||
if mode == MatchingMode::Substring {
|
||||
return pattern
|
||||
.split_ascii_whitespace()
|
||||
.map(|x| {
|
||||
let mut new = x.to_string();
|
||||
new.insert(0, '\'');
|
||||
new
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join(" ");
|
||||
}
|
||||
pattern.to_string()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_selected_entry(&self, mode: Option<Mode>) -> Option<Entry> {
|
||||
match mode.unwrap_or(self.mode) {
|
||||
@ -660,3 +693,24 @@ impl Television {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::television::{MatchingMode, Television};
|
||||
|
||||
#[test]
|
||||
fn test_prompt_preprocessing() {
|
||||
let one_word = "test";
|
||||
let mult_word = "this is a specific test";
|
||||
let expect_one = "'test";
|
||||
let expect_mult = "'this 'is 'a 'specific 'test";
|
||||
assert_eq!(
|
||||
Television::preprocess_pattern(MatchingMode::Substring, one_word),
|
||||
expect_one
|
||||
);
|
||||
assert_eq!(
|
||||
Television::preprocess_pattern(MatchingMode::Substring, mult_word),
|
||||
expect_mult
|
||||
);
|
||||
}
|
||||
}
|
||||
|
96
tests/app.rs
96
tests/app.rs
@ -23,6 +23,7 @@ const DEFAULT_TIMEOUT: Duration = Duration::from_millis(100);
|
||||
fn setup_app(
|
||||
channel: Option<TelevisionChannel>,
|
||||
select_1: bool,
|
||||
exact: bool,
|
||||
) -> (
|
||||
JoinHandle<television::app::AppOutput>,
|
||||
tokio::sync::mpsc::UnboundedSender<Action>,
|
||||
@ -41,8 +42,13 @@ fn setup_app(
|
||||
config.application.tick_rate = 100.0;
|
||||
let input = None;
|
||||
|
||||
let options =
|
||||
AppOptions::new(select_1, false, false, config.application.tick_rate);
|
||||
let options = AppOptions::new(
|
||||
exact,
|
||||
select_1,
|
||||
false,
|
||||
false,
|
||||
config.application.tick_rate,
|
||||
);
|
||||
let mut app = App::new(chan, config, input, options);
|
||||
|
||||
// retrieve the app's action channel handle in order to send a quit action
|
||||
@ -59,7 +65,7 @@ fn setup_app(
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 3)]
|
||||
async fn test_app_does_quit() {
|
||||
let (f, tx) = setup_app(None, false);
|
||||
let (f, tx) = setup_app(None, false, false);
|
||||
|
||||
// send a quit action to the app
|
||||
tx.send(Action::Quit).unwrap();
|
||||
@ -71,7 +77,7 @@ async fn test_app_does_quit() {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 3)]
|
||||
async fn test_app_starts_normally() {
|
||||
let (f, _) = setup_app(None, false);
|
||||
let (f, _) = setup_app(None, false, false);
|
||||
|
||||
// assert that the app is still running after the default timeout
|
||||
std::thread::sleep(DEFAULT_TIMEOUT);
|
||||
@ -82,7 +88,7 @@ async fn test_app_starts_normally() {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 3)]
|
||||
async fn test_app_basic_search() {
|
||||
let (f, tx) = setup_app(None, false);
|
||||
let (f, tx) = setup_app(None, false, false);
|
||||
|
||||
// send actions to the app
|
||||
for c in "file1".chars() {
|
||||
@ -111,7 +117,81 @@ async fn test_app_basic_search() {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 3)]
|
||||
async fn test_app_basic_search_multiselect() {
|
||||
let (f, tx) = setup_app(None, false);
|
||||
let (f, tx) = setup_app(None, false, false);
|
||||
|
||||
// send actions to the app
|
||||
for c in "file".chars() {
|
||||
tx.send(Action::AddInputChar(c)).unwrap();
|
||||
}
|
||||
|
||||
// select both files
|
||||
tx.send(Action::ToggleSelectionDown).unwrap();
|
||||
std::thread::sleep(Duration::from_millis(50));
|
||||
tx.send(Action::ToggleSelectionDown).unwrap();
|
||||
std::thread::sleep(Duration::from_millis(50));
|
||||
tx.send(Action::ConfirmSelection).unwrap();
|
||||
|
||||
// check the output with a timeout
|
||||
let output = timeout(DEFAULT_TIMEOUT, f)
|
||||
.await
|
||||
.expect("app did not finish within the default timeout")
|
||||
.unwrap();
|
||||
|
||||
assert!(output.selected_entries.is_some());
|
||||
assert_eq!(
|
||||
output
|
||||
.selected_entries
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|e| &e.name)
|
||||
.collect::<HashSet<_>>(),
|
||||
HashSet::from([&"file1.txt".to_string(), &"file2.txt".to_string()])
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 3)]
|
||||
async fn test_app_exact_search_multiselect() {
|
||||
let (f, tx) = setup_app(None, false, true);
|
||||
|
||||
// send actions to the app
|
||||
for c in "fie".chars() {
|
||||
tx.send(Action::AddInputChar(c)).unwrap();
|
||||
}
|
||||
|
||||
tx.send(Action::ConfirmSelection).unwrap();
|
||||
|
||||
// check the output with a timeout
|
||||
let output = timeout(DEFAULT_TIMEOUT, f)
|
||||
.await
|
||||
.expect("app did not finish within the default timeout")
|
||||
.unwrap();
|
||||
|
||||
assert!(output.selected_entries.is_some());
|
||||
assert!(&output
|
||||
.selected_entries
|
||||
.clone()
|
||||
.unwrap()
|
||||
.drain()
|
||||
.next()
|
||||
.unwrap()
|
||||
.value
|
||||
.is_none());
|
||||
assert_eq!(
|
||||
output
|
||||
.selected_entries
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|e| &e.name)
|
||||
.collect::<HashSet<_>>(),
|
||||
HashSet::from([&"fie".to_string()])
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 3)]
|
||||
async fn test_app_exact_search_positive() {
|
||||
let (f, tx) = setup_app(None, false, true);
|
||||
|
||||
// send actions to the app
|
||||
for c in "file".chars() {
|
||||
@ -150,7 +230,7 @@ async fn test_app_exits_when_select_1_and_only_one_result() {
|
||||
TelevisionChannel::Stdin(television::channels::stdin::Channel::from(
|
||||
vec!["file1.txt".to_string()],
|
||||
));
|
||||
let (f, tx) = setup_app(Some(channel), true);
|
||||
let (f, tx) = setup_app(Some(channel), true, false);
|
||||
|
||||
// tick a few times to get the results
|
||||
for _ in 0..=10 {
|
||||
@ -184,7 +264,7 @@ async fn test_app_does_not_exit_when_select_1_and_more_than_one_result() {
|
||||
TelevisionChannel::Stdin(television::channels::stdin::Channel::from(
|
||||
vec!["file1.txt".to_string(), "file2.txt".to_string()],
|
||||
));
|
||||
let (f, tx) = setup_app(Some(channel), true);
|
||||
let (f, tx) = setup_app(Some(channel), true, false);
|
||||
|
||||
// tick a few times to get the results
|
||||
for _ in 0..=10 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user