diff --git a/crates/television/channels/alias.rs b/crates/television/channels/alias.rs index e337156..d10153f 100644 --- a/crates/television/channels/alias.rs +++ b/crates/television/channels/alias.rs @@ -1,12 +1,9 @@ -use std::sync::Arc; - +use crate::fuzzy::matcher::{Config, Injector, Matcher}; use devicons::FileIcon; -use nucleo::{Config, Injector, Nucleo}; use tracing::debug; use crate::channels::OnAir; use crate::entry::Entry; -use crate::fuzzy::MATCHER; use crate::previewers::PreviewType; use crate::utils::indices::sep_name_and_value_indices; use crate::utils::strings::preprocess_line; @@ -24,12 +21,8 @@ impl Alias { } pub struct Channel { - matcher: Nucleo, - last_pattern: String, + matcher: Matcher, file_icon: FileIcon, - result_count: u32, - total_count: u32, - running: bool, } const NUM_THREADS: usize = 1; @@ -54,26 +47,15 @@ fn get_raw_aliases(shell: &str) -> Vec { impl Channel { pub fn new() -> Self { - let matcher = Nucleo::new( - Config::DEFAULT, - Arc::new(|| {}), - Some(NUM_THREADS), - 1, - ); + let matcher = Matcher::new(Config::default().n_threads(NUM_THREADS)); let injector = matcher.injector(); tokio::spawn(load_aliases(injector)); Self { matcher, - last_pattern: String::new(), file_icon: FileIcon::from(FILE_ICON_STR), - result_count: 0, - total_count: 0, - running: false, } } - - const MATCHER_TICK_TIMEOUT: u64 = 2; } impl Default for Channel { @@ -84,59 +66,29 @@ impl Default for Channel { impl OnAir for Channel { fn find(&mut self, pattern: &str) { - if pattern != self.last_pattern { - self.matcher.pattern.reparse( - 0, - pattern, - nucleo::pattern::CaseMatching::Smart, - nucleo::pattern::Normalization::Smart, - pattern.starts_with(&self.last_pattern), - ); - self.last_pattern = pattern.to_string(); - } + self.matcher.find(pattern); } fn results(&mut self, num_entries: u32, offset: u32) -> Vec { - let status = self.matcher.tick(Self::MATCHER_TICK_TIMEOUT); - let snapshot = self.matcher.snapshot(); - if status.changed { - self.result_count = snapshot.matched_item_count(); - self.total_count = snapshot.item_count(); - } - self.running = status.running; - let mut col_indices = Vec::new(); - let mut matcher = MATCHER.lock(); - let icon = self.file_icon; - - snapshot - .matched_items( - offset - ..(num_entries + offset) - .min(snapshot.matched_item_count()), - ) - .map(move |item| { - snapshot.pattern().column_pattern(0).indices( - item.matcher_columns[0].slice(..), - &mut matcher, - &mut col_indices, - ); - col_indices.sort_unstable(); - col_indices.dedup(); - + self.matcher.tick(); + self.matcher + .results(num_entries, offset) + .into_iter() + .map(|item| { let ( name_indices, value_indices, should_add_name_indices, should_add_value_indices, ) = sep_name_and_value_indices( - &mut col_indices, - u32::try_from(item.data.name.len()).unwrap(), + &mut item.match_indices.iter().map(|i| i.0).collect(), + u32::try_from(item.inner.name.len()).unwrap(), ); let mut entry = - Entry::new(item.data.name.clone(), PreviewType::EnvVar) - .with_value(item.data.value.clone()) - .with_icon(icon); + Entry::new(item.inner.name.clone(), PreviewType::EnvVar) + .with_value(item.inner.value.clone()) + .with_icon(self.file_icon); if should_add_name_indices { entry = entry.with_name_match_ranges( @@ -159,24 +111,23 @@ impl OnAir for Channel { } fn get_result(&self, index: u32) -> Option { - let snapshot = self.matcher.snapshot(); - snapshot.get_matched_item(index).map(|item| { - Entry::new(item.data.name.clone(), PreviewType::EnvVar) - .with_value(item.data.value.clone()) + self.matcher.get_result(index).map(|item| { + Entry::new(item.inner.name.clone(), PreviewType::EnvVar) + .with_value(item.inner.value.clone()) .with_icon(self.file_icon) }) } fn result_count(&self) -> u32 { - self.result_count + self.matcher.matched_item_count } fn total_count(&self) -> u32 { - self.total_count + self.matcher.total_item_count } fn running(&self) -> bool { - self.running + self.matcher.status.running } fn shutdown(&self) {} @@ -204,7 +155,7 @@ async fn load_aliases(injector: Injector) { None }) .for_each(|alias| { - let _ = injector.push(alias.clone(), |_, cols| { + let () = injector.push(alias.clone(), |_, cols| { cols[0] = (alias.name.clone() + &alias.value).into(); }); }); diff --git a/crates/television/channels/env.rs b/crates/television/channels/env.rs index 28c5715..da8977f 100644 --- a/crates/television/channels/env.rs +++ b/crates/television/channels/env.rs @@ -1,17 +1,13 @@ use devicons::FileIcon; -use nucleo::{ - pattern::{CaseMatching, Normalization}, - Config, Nucleo, -}; -use std::sync::Arc; use super::OnAir; use crate::entry::Entry; -use crate::fuzzy::MATCHER; +use crate::fuzzy::matcher::{Config, Matcher}; use crate::previewers::PreviewType; use crate::utils::indices::sep_name_and_value_indices; use crate::utils::strings::preprocess_line; +#[derive(Debug, Clone)] struct EnvVar { name: String, value: String, @@ -19,12 +15,8 @@ struct EnvVar { #[allow(clippy::module_name_repetitions)] pub struct Channel { - matcher: Nucleo, - last_pattern: String, + matcher: Matcher, file_icon: FileIcon, - result_count: u32, - total_count: u32, - running: bool, } const NUM_THREADS: usize = 1; @@ -32,15 +24,10 @@ const FILE_ICON_STR: &str = "config"; impl Channel { pub fn new() -> Self { - let matcher = Nucleo::new( - Config::DEFAULT, - Arc::new(|| {}), - Some(NUM_THREADS), - 1, - ); + let matcher = Matcher::new(Config::default().n_threads(NUM_THREADS)); let injector = matcher.injector(); for (name, value) in std::env::vars() { - let _ = injector.push( + let () = injector.push( EnvVar { name: preprocess_line(&name), value: preprocess_line(&value), @@ -52,15 +39,9 @@ impl Channel { } Channel { matcher, - last_pattern: String::new(), file_icon: FileIcon::from(FILE_ICON_STR), - result_count: 0, - total_count: 0, - running: false, } } - - const MATCHER_TICK_TIMEOUT: u64 = 2; } impl Default for Channel { @@ -71,59 +52,29 @@ impl Default for Channel { impl OnAir for Channel { fn find(&mut self, pattern: &str) { - if pattern != self.last_pattern { - self.matcher.pattern.reparse( - 0, - pattern, - CaseMatching::Smart, - Normalization::Smart, - pattern.starts_with(&self.last_pattern), - ); - self.last_pattern = pattern.to_string(); - } + self.matcher.find(pattern); } fn results(&mut self, num_entries: u32, offset: u32) -> Vec { - let status = self.matcher.tick(Self::MATCHER_TICK_TIMEOUT); - let snapshot = self.matcher.snapshot(); - if status.changed { - self.result_count = snapshot.matched_item_count(); - self.total_count = snapshot.item_count(); - } - self.running = status.running; - let mut col_indices = Vec::new(); - let mut matcher = MATCHER.lock(); - let icon = self.file_icon; - - snapshot - .matched_items( - offset - ..(num_entries + offset) - .min(snapshot.matched_item_count()), - ) - .map(move |item| { - snapshot.pattern().column_pattern(0).indices( - item.matcher_columns[0].slice(..), - &mut matcher, - &mut col_indices, - ); - col_indices.sort_unstable(); - col_indices.dedup(); - + self.matcher.tick(); + self.matcher + .results(num_entries, offset) + .into_iter() + .map(|item| { let ( name_indices, value_indices, should_add_name_indices, should_add_value_indices, ) = sep_name_and_value_indices( - &mut col_indices, - u32::try_from(item.data.name.len()).unwrap(), + &mut item.match_indices.iter().map(|i| i.0).collect(), + u32::try_from(item.inner.name.len()).unwrap(), ); let mut entry = - Entry::new(item.data.name.clone(), PreviewType::EnvVar) - .with_value(item.data.value.clone()) - .with_icon(icon); + Entry::new(item.inner.name.clone(), PreviewType::EnvVar) + .with_value(item.inner.value.clone()) + .with_icon(self.file_icon); if should_add_name_indices { entry = entry.with_name_match_ranges( @@ -146,26 +97,23 @@ impl OnAir for Channel { } fn get_result(&self, index: u32) -> Option { - let snapshot = self.matcher.snapshot(); - snapshot.get_matched_item(index).map(|item| { - let name = item.data.name.clone(); - let value = item.data.value.clone(); - Entry::new(name, PreviewType::EnvVar) - .with_value(value) + self.matcher.get_result(index).map(|item| { + Entry::new(item.inner.name.clone(), PreviewType::EnvVar) + .with_value(item.inner.value.clone()) .with_icon(self.file_icon) }) } fn result_count(&self) -> u32 { - self.result_count + self.matcher.matched_item_count } fn total_count(&self) -> u32 { - self.total_count + self.matcher.total_item_count } fn running(&self) -> bool { - self.running + self.matcher.status.running } fn shutdown(&self) {} diff --git a/crates/television/channels/files.rs b/crates/television/channels/files.rs index 2110dbc..e7be2d4 100644 --- a/crates/television/channels/files.rs +++ b/crates/television/channels/files.rs @@ -1,23 +1,15 @@ -use super::{OnAir, TelevisionChannel}; +use crate::channels::{OnAir, TelevisionChannel}; use crate::entry::Entry; -use crate::fuzzy::MATCHER; +use crate::fuzzy::matcher::{Config, Injector, Matcher}; use crate::previewers::PreviewType; use crate::utils::files::{walk_builder, DEFAULT_NUM_THREADS}; use crate::utils::strings::preprocess_line; use devicons::FileIcon; -use nucleo::{ - pattern::{CaseMatching, Normalization}, - Config, Injector, Nucleo, -}; use std::collections::HashSet; -use std::{path::PathBuf, sync::Arc}; +use std::path::PathBuf; pub struct Channel { - matcher: Nucleo, - last_pattern: String, - result_count: u32, - total_count: u32, - running: bool, + matcher: Matcher, crawl_handle: tokio::task::JoinHandle<()>, // PERF: cache results (to make deleting characters smoother) with // a shallow stack of sub-patterns as keys (e.g. "a", "ab", "abc") @@ -25,25 +17,14 @@ pub struct Channel { impl Channel { pub fn new(paths: Vec) -> Self { - let matcher = Nucleo::new( - Config::DEFAULT.match_paths(), - Arc::new(|| {}), - None, - 1, - ); + let matcher = Matcher::new(Config::default().match_paths(true)); // start loading files in the background let crawl_handle = tokio::spawn(load_files(paths, matcher.injector())); Channel { matcher, - last_pattern: String::new(), - result_count: 0, - total_count: 0, - running: false, crawl_handle, } } - - const MATCHER_TICK_TIMEOUT: u64 = 2; } impl Default for Channel { @@ -91,74 +72,41 @@ impl From<&mut TelevisionChannel> for Channel { impl OnAir for Channel { fn find(&mut self, pattern: &str) { - if pattern != self.last_pattern { - self.matcher.pattern.reparse( - 0, - pattern, - CaseMatching::Smart, - Normalization::Smart, - pattern.starts_with(&self.last_pattern), - ); - self.last_pattern = pattern.to_string(); - } + self.matcher.find(pattern); } fn results(&mut self, num_entries: u32, offset: u32) -> Vec { - let status = self.matcher.tick(Self::MATCHER_TICK_TIMEOUT); - let snapshot = self.matcher.snapshot(); - if status.changed { - self.result_count = snapshot.matched_item_count(); - self.total_count = snapshot.item_count(); - } - self.running = status.running; - let mut indices = Vec::new(); - let mut matcher = MATCHER.lock(); - - snapshot - .matched_items( - offset - ..(num_entries + offset) - .min(snapshot.matched_item_count()), - ) - .map(move |item| { - snapshot.pattern().column_pattern(0).indices( - item.matcher_columns[0].slice(..), - &mut matcher, - &mut indices, - ); - indices.sort_unstable(); - indices.dedup(); - let indices = indices.drain(..); - - let path = item.matcher_columns[0].to_string(); + self.matcher.tick(); + self.matcher + .results(num_entries, offset) + .into_iter() + .map(|item| { + let path = item.matched_string; Entry::new(path.clone(), PreviewType::Files) - .with_name_match_ranges( - indices.map(|i| (i, i + 1)).collect(), - ) + .with_name_match_ranges(item.match_indices) .with_icon(FileIcon::from(&path)) }) .collect() } fn get_result(&self, index: u32) -> Option { - let snapshot = self.matcher.snapshot(); - snapshot.get_matched_item(index).map(|item| { - let path = item.matcher_columns[0].to_string(); + self.matcher.get_result(index).map(|item| { + let path = item.matched_string; Entry::new(path.clone(), PreviewType::Files) .with_icon(FileIcon::from(&path)) }) } fn result_count(&self) -> u32 { - self.result_count + self.matcher.matched_item_count } fn total_count(&self) -> u32 { - self.total_count + self.matcher.total_item_count } fn running(&self) -> bool { - self.running + self.matcher.status.running } fn shutdown(&self) { @@ -192,7 +140,7 @@ async fn load_files(paths: Vec, injector: Injector) { .unwrap_or(entry.path()) .to_string_lossy(), ); - let _ = injector.push(file_path, |e, cols| { + let () = injector.push(file_path, |e, cols| { cols[0] = e.clone().into(); }); } diff --git a/crates/television/channels/git_repos.rs b/crates/television/channels/git_repos.rs index 4fdcedf..55e3e85 100644 --- a/crates/television/channels/git_repos.rs +++ b/crates/television/channels/git_repos.rs @@ -1,42 +1,29 @@ use devicons::FileIcon; use directories::BaseDirs; use ignore::overrides::OverrideBuilder; -use nucleo::{ - pattern::{CaseMatching, Normalization}, - Config, Nucleo, -}; -use std::{path::PathBuf, sync::Arc}; +use std::path::PathBuf; use tokio::task::JoinHandle; use tracing::debug; use crate::{ entry::Entry, - fuzzy::MATCHER, previewers::PreviewType, utils::files::{walk_builder, DEFAULT_NUM_THREADS}, }; use crate::channels::OnAir; +use crate::fuzzy::matcher::{Config, Injector, Matcher}; use crate::utils::strings::preprocess_line; pub struct Channel { - matcher: Nucleo, - last_pattern: String, - result_count: u32, - total_count: u32, - running: bool, + matcher: Matcher, icon: FileIcon, crawl_handle: JoinHandle<()>, } impl Channel { pub fn new() -> Self { - let matcher = Nucleo::new( - Config::DEFAULT.match_paths(), - Arc::new(|| {}), - None, - 1, - ); + let matcher = Matcher::new(Config::default().match_paths(true)); let base_dirs = BaseDirs::new().unwrap(); let crawl_handle = tokio::spawn(crawl_for_repos( base_dirs.home_dir().to_path_buf(), @@ -44,16 +31,10 @@ impl Channel { )); Channel { matcher, - last_pattern: String::new(), - result_count: 0, - total_count: 0, - running: false, icon: FileIcon::from("git"), crawl_handle, } } - - const MATCHER_TICK_TIMEOUT: u64 = 2; } impl Default for Channel { @@ -64,75 +45,41 @@ impl Default for Channel { impl OnAir for Channel { fn find(&mut self, pattern: &str) { - if pattern != self.last_pattern { - self.matcher.pattern.reparse( - 0, - pattern, - CaseMatching::Smart, - Normalization::Smart, - pattern.starts_with(&self.last_pattern), - ); - self.last_pattern = pattern.to_string(); - } + self.matcher.find(pattern); } fn results(&mut self, num_entries: u32, offset: u32) -> Vec { - let status = self.matcher.tick(Self::MATCHER_TICK_TIMEOUT); - let snapshot = self.matcher.snapshot(); - if status.changed { - self.result_count = snapshot.matched_item_count(); - self.total_count = snapshot.item_count(); - } - self.running = status.running; - let mut indices = Vec::new(); - let mut matcher = MATCHER.lock(); - let icon = self.icon; - - snapshot - .matched_items( - offset - ..(num_entries + offset) - .min(snapshot.matched_item_count()), - ) - .map(move |item| { - snapshot.pattern().column_pattern(0).indices( - item.matcher_columns[0].slice(..), - &mut matcher, - &mut indices, - ); - indices.sort_unstable(); - indices.dedup(); - let indices = indices.drain(..); - - let path = item.matcher_columns[0].to_string(); + self.matcher.tick(); + self.matcher + .results(num_entries, offset) + .into_iter() + .map(|item| { + let path = item.matched_string; Entry::new(path.clone(), PreviewType::Directory) - .with_name_match_ranges( - indices.map(|i| (i, i + 1)).collect(), - ) - .with_icon(icon) + .with_name_match_ranges(item.match_indices) + .with_icon(self.icon) }) .collect() } fn get_result(&self, index: u32) -> Option { - let snapshot = self.matcher.snapshot(); - snapshot.get_matched_item(index).map(|item| { - let path = item.matcher_columns[0].to_string(); + self.matcher.get_result(index).map(|item| { + let path = item.matched_string; Entry::new(path.clone(), PreviewType::Directory) .with_icon(self.icon) }) } fn result_count(&self) -> u32 { - self.result_count + self.matcher.matched_item_count } fn total_count(&self) -> u32 { - self.total_count + self.matcher.total_item_count } fn running(&self) -> bool { - self.running + self.matcher.status.running } fn shutdown(&self) { @@ -189,10 +136,7 @@ fn get_ignored_paths() -> Vec { ignored_paths } #[allow(clippy::unused_async)] -async fn crawl_for_repos( - starting_point: PathBuf, - injector: nucleo::Injector, -) { +async fn crawl_for_repos(starting_point: PathBuf, injector: Injector) { let mut walker_overrides_builder = OverrideBuilder::new(&starting_point); walker_overrides_builder.add(".git").unwrap(); let walker = walk_builder( @@ -214,7 +158,7 @@ async fn crawl_for_repos( &entry.path().parent().unwrap().to_string_lossy(), ); debug!("Found git repo: {:?}", parent_path); - let _ = injector.push(parent_path, |e, cols| { + let () = injector.push(parent_path, |e, cols| { cols[0] = e.clone().into(); }); return ignore::WalkState::Skip; diff --git a/crates/television/channels/remote_control.rs b/crates/television/channels/remote_control.rs index 7416e6a..7d1b584 100644 --- a/crates/television/channels/remote_control.rs +++ b/crates/television/channels/remote_control.rs @@ -1,51 +1,30 @@ -use std::sync::Arc; - use clap::ValueEnum; use devicons::FileIcon; -use nucleo::{ - pattern::{CaseMatching, Normalization}, - Config, Nucleo, -}; use crate::channels::{TelevisionChannel, UnitChannel}; use crate::{ channels::{CliTvChannel, OnAir}, entry::Entry, - fuzzy::MATCHER, + fuzzy::matcher::{Config, Matcher}, previewers::PreviewType, }; pub struct RemoteControl { - matcher: Nucleo, - last_pattern: String, - result_count: u32, - total_count: u32, - running: bool, + matcher: Matcher, } const NUM_THREADS: usize = 1; impl RemoteControl { pub fn new(channels: Vec) -> Self { - let matcher = Nucleo::new( - Config::DEFAULT, - Arc::new(|| {}), - Some(NUM_THREADS), - 1, - ); + let matcher = Matcher::new(Config::default().n_threads(NUM_THREADS)); let injector = matcher.injector(); for channel in channels { - let _ = injector.push(channel.to_string(), |e, cols| { + let () = injector.push(channel.to_string(), |e, cols| { cols[0] = e.clone().into(); }); } - RemoteControl { - matcher, - last_pattern: String::new(), - result_count: 0, - total_count: 0, - running: false, - } + RemoteControl { matcher } } pub fn with_transitions_from( @@ -53,8 +32,6 @@ impl RemoteControl { ) -> Self { Self::new(television_channel.available_transitions()) } - - const MATCHER_TICK_TIMEOUT: u64 = 2; } impl Default for RemoteControl { @@ -70,80 +47,45 @@ impl Default for RemoteControl { const TV_ICON: FileIcon = FileIcon { icon: '📺', - color: "#ffffff", + color: "#000000", }; impl OnAir for RemoteControl { fn find(&mut self, pattern: &str) { - if pattern != self.last_pattern { - self.matcher.pattern.reparse( - 0, - pattern, - CaseMatching::Smart, - Normalization::Smart, - pattern.starts_with(&self.last_pattern), - ); - self.last_pattern = pattern.to_string(); - } + self.matcher.find(pattern); } fn results(&mut self, num_entries: u32, offset: u32) -> Vec { - let status = self.matcher.tick(Self::MATCHER_TICK_TIMEOUT); - let snapshot = self.matcher.snapshot(); - if status.changed { - self.result_count = snapshot.matched_item_count(); - self.total_count = snapshot.item_count(); - } - self.running = status.running; - let mut indices = Vec::new(); - let mut matcher = MATCHER.lock(); - - snapshot - .matched_items( - offset - ..(num_entries + offset) - .min(snapshot.matched_item_count()), - ) - .map(move |item| { - snapshot.pattern().column_pattern(0).indices( - item.matcher_columns[0].slice(..), - &mut matcher, - &mut indices, - ); - indices.sort_unstable(); - indices.dedup(); - let indices = indices.drain(..); - - let name = item.matcher_columns[0].to_string(); - Entry::new(name, PreviewType::Basic) - .with_name_match_ranges( - indices.map(|i| (i, i + 1)).collect(), - ) + self.matcher.tick(); + self.matcher + .results(num_entries, offset) + .into_iter() + .map(|item| { + let path = item.matched_string; + Entry::new(path.clone(), PreviewType::Basic) + .with_name_match_ranges(item.match_indices) .with_icon(TV_ICON) }) .collect() } fn get_result(&self, index: u32) -> Option { - let snapshot = self.matcher.snapshot(); - snapshot.get_matched_item(index).map(|item| { - let name = item.matcher_columns[0].to_string(); - // TODO: Add new Previewer for Channel selection which displays a - // short description of the channel - Entry::new(name.clone(), PreviewType::Basic).with_icon(TV_ICON) + self.matcher.get_result(index).map(|item| { + let path = item.matched_string; + Entry::new(path.clone(), PreviewType::Basic).with_icon(TV_ICON) }) } fn result_count(&self) -> u32 { - self.result_count + self.matcher.matched_item_count } fn total_count(&self) -> u32 { - self.total_count + self.matcher.total_item_count } fn running(&self) -> bool { - self.running + self.matcher.status.running } fn shutdown(&self) {} diff --git a/crates/television/channels/stdin.rs b/crates/television/channels/stdin.rs index 63f3546..5883d58 100644 --- a/crates/television/channels/stdin.rs +++ b/crates/television/channels/stdin.rs @@ -1,22 +1,17 @@ +use std::io::BufRead; use std::path::Path; -use std::{io::BufRead, sync::Arc}; use devicons::FileIcon; -use nucleo::{Config, Nucleo}; use super::OnAir; use crate::entry::Entry; -use crate::fuzzy::MATCHER; +use crate::fuzzy::matcher::{Config, Matcher}; use crate::previewers::PreviewType; use crate::utils::strings::preprocess_line; pub struct Channel { - matcher: Nucleo, - last_pattern: String, - result_count: u32, - total_count: u32, + matcher: Matcher, icon: FileIcon, - running: bool, } const NUM_THREADS: usize = 2; @@ -27,29 +22,18 @@ impl Channel { for line in std::io::stdin().lock().lines().map_while(Result::ok) { lines.push(preprocess_line(&line)); } - let matcher = Nucleo::new( - Config::DEFAULT, - Arc::new(|| {}), - Some(NUM_THREADS), - 1, - ); + let matcher = Matcher::new(Config::default().n_threads(NUM_THREADS)); let injector = matcher.injector(); for line in &lines { - let _ = injector.push(line.clone(), |e, cols| { + let () = injector.push(line.clone(), |e, cols| { cols[0] = e.clone().into(); }); } Self { matcher, - last_pattern: String::new(), - result_count: 0, - total_count: 0, icon: FileIcon::from("nu"), - running: false, } } - - const MATCHER_TICK_TIMEOUT: u64 = 2; } impl Default for Channel { @@ -59,95 +43,57 @@ impl Default for Channel { } impl OnAir for Channel { - // maybe this could be sort of automatic with a blanket impl (making Finder generic over - // its matcher type or something) fn find(&mut self, pattern: &str) { - if pattern != self.last_pattern { - self.matcher.pattern.reparse( - 0, - pattern, - nucleo::pattern::CaseMatching::Smart, - nucleo::pattern::Normalization::Smart, - pattern.starts_with(&self.last_pattern), - ); - self.last_pattern = pattern.to_string(); - } + self.matcher.find(pattern); } fn results(&mut self, num_entries: u32, offset: u32) -> Vec { - let status = self.matcher.tick(Self::MATCHER_TICK_TIMEOUT); - let snapshot = self.matcher.snapshot(); - if status.changed { - self.result_count = snapshot.matched_item_count(); - self.total_count = snapshot.item_count(); - } - self.running = status.running; - let mut indices = Vec::new(); - let mut matcher = MATCHER.lock(); - let icon = self.icon; - - snapshot - .matched_items( - offset - ..(num_entries + offset) - .min(snapshot.matched_item_count()), - ) - .map(move |item| { - snapshot.pattern().column_pattern(0).indices( - item.matcher_columns[0].slice(..), - &mut matcher, - &mut indices, - ); - indices.sort_unstable(); - indices.dedup(); - let indices = indices.drain(..); - - let content = item.matcher_columns[0].to_string(); - let path = Path::new(&content); + self.matcher.tick(); + self.matcher + .results(num_entries, offset) + .into_iter() + .map(|item| { + let path = Path::new(&item.matched_string); let icon = if path.try_exists().unwrap_or(false) { FileIcon::from(path) } else { - icon + self.icon }; - Entry::new(content.clone(), PreviewType::Basic) - .with_name_match_ranges( - indices.map(|i| (i, i + 1)).collect(), - ) + Entry::new(item.matched_string, PreviewType::Basic) + .with_name_match_ranges(item.match_indices) .with_icon(icon) }) .collect() } fn get_result(&self, index: u32) -> Option { - let snapshot = self.matcher.snapshot(); - snapshot.get_matched_item(index).map(|item| { - let content = item.matcher_columns[0].to_string(); + self.matcher.get_result(index).map(|item| { + let path = Path::new(&item.matched_string); // if we recognize a file path, use a file icon // and set the preview type to "Files" - let path = Path::new(&content); if path.is_file() { - Entry::new(content.clone(), PreviewType::Files) + Entry::new(item.matched_string.clone(), PreviewType::Files) .with_icon(FileIcon::from(path)) } else if path.is_dir() { - Entry::new(content.clone(), PreviewType::Directory) + Entry::new(item.matched_string.clone(), PreviewType::Directory) .with_icon(FileIcon::from(path)) } else { - Entry::new(content.clone(), PreviewType::Basic) + Entry::new(item.matched_string.clone(), PreviewType::Basic) .with_icon(self.icon) } }) } fn result_count(&self) -> u32 { - self.result_count + self.matcher.matched_item_count } fn total_count(&self) -> u32 { - self.total_count + self.matcher.total_item_count } fn running(&self) -> bool { - self.running + self.matcher.status.running } fn shutdown(&self) {} diff --git a/crates/television/channels/text.rs b/crates/television/channels/text.rs index 28f4495..bbd3b06 100644 --- a/crates/television/channels/text.rs +++ b/crates/television/channels/text.rs @@ -1,9 +1,5 @@ use devicons::FileIcon; use ignore::WalkState; -use nucleo::{ - pattern::{CaseMatching, Normalization}, - Config, Injector, Nucleo, -}; use std::{ fs::File, io::{BufRead, Read, Seek}, @@ -13,7 +9,7 @@ use std::{ use tracing::{debug, warn}; use super::{OnAir, TelevisionChannel}; -use crate::previewers::PreviewType; +use crate::utils::strings::PRINTABLE_ASCII_THRESHOLD; use crate::utils::{ files::{is_not_text, walk_builder, DEFAULT_NUM_THREADS}, strings::preprocess_line, @@ -21,9 +17,12 @@ use crate::utils::{ use crate::{ entry::Entry, utils::strings::proportion_of_printable_ascii_characters, }; -use crate::{fuzzy::MATCHER, utils::strings::PRINTABLE_ASCII_THRESHOLD}; +use crate::{ + fuzzy::matcher::{Config, Injector, Matcher}, + previewers::PreviewType, +}; -#[derive(Debug)] +#[derive(Debug, Clone)] struct CandidateLine { path: PathBuf, line: String, @@ -42,17 +41,13 @@ impl CandidateLine { #[allow(clippy::module_name_repetitions)] pub struct Channel { - matcher: Nucleo, - last_pattern: String, - result_count: u32, - total_count: u32, - running: bool, + matcher: Matcher, crawl_handle: tokio::task::JoinHandle<()>, } impl Channel { pub fn new(directories: Vec) -> Self { - let matcher = Nucleo::new(Config::DEFAULT, Arc::new(|| {}), None, 1); + let matcher = Matcher::new(Config::default()); // start loading files in the background let crawl_handle = tokio::spawn(crawl_for_candidates( directories, @@ -60,21 +55,12 @@ impl Channel { )); Channel { matcher, - last_pattern: String::new(), - result_count: 0, - total_count: 0, - running: false, crawl_handle, } } fn from_file_paths(file_paths: Vec) -> Self { - let matcher = Nucleo::new( - Config::DEFAULT.match_paths(), - Arc::new(|| {}), - None, - 1, - ); + let matcher = Matcher::new(Config::default()); let injector = matcher.injector(); let current_dir = std::env::current_dir().unwrap(); let crawl_handle = tokio::spawn(async move { @@ -93,21 +79,12 @@ impl Channel { Channel { matcher, - last_pattern: String::new(), - result_count: 0, - total_count: 0, - running: false, crawl_handle, } } fn from_text_entries(entries: Vec) -> Self { - let matcher = Nucleo::new( - Config::DEFAULT.match_paths(), - Arc::new(|| {}), - None, - 1, - ); + let matcher = Matcher::new(Config::default()); let injector = matcher.injector(); let load_handle = tokio::spawn(async move { for entry in entries.into_iter().take(MAX_LINES_IN_MEM) { @@ -126,15 +103,9 @@ impl Channel { Channel { matcher, - last_pattern: String::new(), - result_count: 0, - total_count: 0, - running: false, crawl_handle: load_handle, } } - - const MATCHER_TICK_TIMEOUT: u64 = 2; } impl Default for Channel { @@ -192,86 +163,55 @@ impl From<&mut TelevisionChannel> for Channel { impl OnAir for Channel { fn find(&mut self, pattern: &str) { - if pattern != self.last_pattern { - self.matcher.pattern.reparse( - 0, - pattern, - CaseMatching::Smart, - Normalization::Smart, - pattern.starts_with(&self.last_pattern), - ); - self.last_pattern = pattern.to_string(); - } + self.matcher.find(pattern); } fn results(&mut self, num_entries: u32, offset: u32) -> Vec { - let status = self.matcher.tick(Self::MATCHER_TICK_TIMEOUT); - let snapshot = self.matcher.snapshot(); - if status.changed { - self.result_count = snapshot.matched_item_count(); - self.total_count = snapshot.item_count(); - } - self.running = status.running; - let mut indices = Vec::new(); - let mut matcher = MATCHER.lock(); - - snapshot - .matched_items( - offset - ..(num_entries + offset) - .min(snapshot.matched_item_count()), - ) - .map(move |item| { - snapshot.pattern().column_pattern(0).indices( - item.matcher_columns[0].slice(..), - &mut matcher, - &mut indices, - ); - indices.sort_unstable(); - indices.dedup(); - let indices = indices.drain(..); - - let line = item.matcher_columns[0].to_string(); + self.matcher.tick(); + self.matcher + .results(num_entries, offset) + .into_iter() + .map(|item| { + let line = item.matched_string; let display_path = - item.data.path.to_string_lossy().to_string(); + item.inner.path.to_string_lossy().to_string(); Entry::new( - display_path.clone() + &item.data.line_number.to_string(), + display_path.clone() + &item.inner.line_number.to_string(), PreviewType::Files, ) .with_display_name(display_path) .with_value(line) - .with_value_match_ranges(indices.map(|i| (i, i + 1)).collect()) - .with_icon(FileIcon::from(item.data.path.as_path())) - .with_line_number(item.data.line_number) + .with_value_match_ranges(item.match_indices) + .with_icon(FileIcon::from(item.inner.path.as_path())) + .with_line_number(item.inner.line_number) }) .collect() } fn get_result(&self, index: u32) -> Option { - let snapshot = self.matcher.snapshot(); - snapshot.get_matched_item(index).map(|item| { - let display_path = item.data.path.to_string_lossy().to_string(); + self.matcher.get_result(index).map(|item| { + let display_path = item.inner.path.to_string_lossy().to_string(); Entry::new(display_path.clone(), PreviewType::Files) .with_display_name( display_path.clone() + ":" - + &item.data.line_number.to_string(), + + &item.inner.line_number.to_string(), ) - .with_icon(FileIcon::from(item.data.path.as_path())) - .with_line_number(item.data.line_number) + .with_icon(FileIcon::from(item.inner.path.as_path())) + .with_line_number(item.inner.line_number) }) } fn result_count(&self) -> u32 { - self.result_count + self.matcher.matched_item_count } fn total_count(&self) -> u32 { - self.total_count + self.matcher.total_item_count } fn running(&self) -> bool { - self.running + self.matcher.status.running } fn shutdown(&self) { @@ -310,9 +250,9 @@ async fn crawl_for_candidates( let current_dir = std::env::current_dir().unwrap(); let mut walker = walk_builder(&directories[0], *DEFAULT_NUM_THREADS, None, None); - for path in directories[1..].iter() { + directories[1..].iter().for_each(|path| { walker.add(path); - } + }); let lines_in_mem = Arc::new(AtomicUsize::new(0)); @@ -393,7 +333,7 @@ fn try_inject_lines( line, line_number, ); - let _ = injector.push(candidate, |c, cols| { + let () = injector.push(candidate, |c, cols| { cols[0] = c.line.clone().into(); }); injected_lines += 1; diff --git a/crates/television/fuzzy.rs b/crates/television/fuzzy.rs index 5d83f63..6202394 100644 --- a/crates/television/fuzzy.rs +++ b/crates/television/fuzzy.rs @@ -1,6 +1,8 @@ use parking_lot::Mutex; use std::ops::DerefMut; +pub mod matcher; + pub struct LazyMutex { inner: Mutex>, init: fn() -> T, diff --git a/crates/television/fuzzy/matcher.rs b/crates/television/fuzzy/matcher.rs new file mode 100644 index 0000000..b83d285 --- /dev/null +++ b/crates/television/fuzzy/matcher.rs @@ -0,0 +1,332 @@ +use std::sync::Arc; + +use super::MATCHER; + +const MATCHER_TICK_TIMEOUT: u64 = 2; + +/// A matched item. +/// +/// This contains the matched item, the dimension against which it was matched, +/// represented as a string, and the indices of the matched characters. +/// +/// The indices are pairs of `(start, end)` where `start` is the index of the +/// first character in the match, and `end` is the index of the character after +/// the last character in the match. +pub struct MatchedItem +where + I: Sync + Send + Clone + 'static, +{ + pub inner: I, + pub matched_string: String, + pub match_indices: Vec<(u32, u32)>, +} + +/// The status of the fuzzy matcher. +/// +/// This currently only contains a boolean indicating whether the matcher is +/// running in the background. +/// This mostly serves as a way to communicate the status of the matcher to the +/// front-end and display a loading indicator. +#[derive(Default)] +pub struct Status { + pub running: bool, +} + +impl From for Status { + fn from(status: nucleo::Status) -> Self { + Self { + running: status.running, + } + } +} + +/// The configuration of the fuzzy matcher. +/// +/// This contains the number of threads to use, whether to ignore case, whether +/// to prefer prefix matches, and whether to optimize for matching paths. +/// +/// The default configuration uses the default configuration of the `Nucleo` +/// fuzzy matcher, e.g. case-insensitive matching, no preference for prefix +/// matches, and no optimization for matching paths as well as using the +/// default number of threads (which corresponds to the number of available logical +/// cores on the current machine). +#[derive(Copy, Clone)] +pub struct Config { + pub n_threads: Option, + pub ignore_case: bool, + pub prefer_prefix: bool, + pub match_paths: bool, +} + +impl Default for Config { + fn default() -> Self { + Self { + n_threads: None, + ignore_case: true, + prefer_prefix: false, + match_paths: false, + } + } +} + +impl Config { + /// Set the number of threads to use. + pub fn n_threads(mut self, n_threads: usize) -> Self { + self.n_threads = Some(n_threads); + self + } + + /// Set whether to ignore case. + pub fn ignore_case(mut self, ignore_case: bool) -> Self { + self.ignore_case = ignore_case; + self + } + + /// Set whether to prefer prefix matches. + pub fn prefer_prefix(mut self, prefer_prefix: bool) -> Self { + self.prefer_prefix = prefer_prefix; + self + } + + /// Set whether to optimize for matching paths. + pub fn match_paths(mut self, match_paths: bool) -> Self { + self.match_paths = match_paths; + self + } +} + +impl From<&Config> for nucleo::Config { + fn from(config: &Config) -> Self { + let mut matcher_config = nucleo::Config::DEFAULT; + matcher_config.ignore_case = config.ignore_case; + matcher_config.prefer_prefix = config.prefer_prefix; + if config.match_paths { + matcher_config = matcher_config.match_paths(); + } + matcher_config + } +} + +/// An injector that can be used to push items of type `I` into the fuzzy matcher. +/// +/// This is a wrapper around the `Injector` type from the `Nucleo` fuzzy matcher. +/// +/// The `push` method takes an item of type `I` and a closure that produces the +/// string to match against based on the item. +#[derive(Clone)] +pub struct Injector +where + I: Sync + Send + Clone + 'static, +{ + inner: nucleo::Injector, +} + +impl Injector +where + I: Sync + Send + Clone + 'static, +{ + pub fn new(inner: nucleo::Injector) -> Self { + Self { inner } + } + + /// Push an item into the fuzzy matcher. + /// + /// The closure `f` should produce the string to match against based on the + /// item. + /// + /// # Example + /// ``` + /// let config = Config::default(); + /// let matcher = Matcher::new(config); + /// + /// let injector = matcher.injector(); + /// injector.push( + /// ("some string", 3, "some other string"), + /// // Say we want to match against the third element of the tuple + /// |s, cols| cols[0] = s.2.into() + /// ); + /// ``` + pub fn push(&self, item: I, f: F) + where + F: FnOnce(&I, &mut [nucleo::Utf32String]), + { + self.inner.push(item, f); + } +} + +/// A fuzzy matcher that can be used to match items of type `I`. +/// +/// `I` should be `Sync`, `Send`, `Clone`, and `'static`. +/// This is a wrapper around the `Nucleo` fuzzy matcher that only matches +/// on a single dimension. +/// +/// The matcher can be used to find items that match a given pattern and to +/// retrieve the matched items as well as the indices of the matched characters. +pub struct Matcher +where + I: Sync + Send + Clone + 'static, +{ + inner: nucleo::Nucleo, + pub total_item_count: u32, + pub matched_item_count: u32, + pub status: Status, + pub last_pattern: String, +} + +impl Matcher +where + I: Sync + Send + Clone + 'static, +{ + /// Create a new fuzzy matcher with the given configuration. + pub fn new(config: Config) -> Self { + Self { + inner: nucleo::Nucleo::new( + (&config).into(), + Arc::new(|| {}), + config.n_threads, + 1, + ), + total_item_count: 0, + matched_item_count: 0, + status: Status::default(), + last_pattern: String::new(), + } + } + + /// Tick the fuzzy matcher. + /// + /// This should be called periodically to update the state of the matcher. + pub fn tick(&mut self) { + self.status = self.inner.tick(MATCHER_TICK_TIMEOUT).into(); + } + + /// Get an injector that can be used to push items into the fuzzy matcher. + /// + /// This can be used at any time to push items into the fuzzy matcher. + /// + /// # Example + /// ``` + /// let config = Config::default(); + /// let matcher = Matcher::new(config); + /// let injector = matcher.injector(); + /// + /// injector.push( + /// ("some string", 3, "some other string"), + /// // Say we want to match against the third element of the tuple + /// |s, cols| cols[0] = s.2.into() + /// ); + /// ``` + pub fn injector(&self) -> Injector { + Injector::new(self.inner.injector()) + } + + /// Find items that match the given pattern. + /// + /// This should be called whenever the pattern changes. + /// The `Matcher` will keep track of the last pattern and only reparse the + /// pattern if it has changed, allowing for more efficient matching when + /// `self.last_pattern` is a prefix of the new `pattern`. + pub fn find(&mut self, pattern: &str) { + if pattern != self.last_pattern { + self.inner.pattern.reparse( + 0, + pattern, + nucleo::pattern::CaseMatching::Smart, + nucleo::pattern::Normalization::Smart, + pattern.starts_with(&self.last_pattern), + ); + self.last_pattern = pattern.to_string(); + } + } + + /// Get the matched items. + /// + /// This should be called to retrieve the matched items after calling + /// `find`. + /// + /// The `num_entries` parameter specifies the number of entries to return, + /// and the `offset` parameter specifies the offset of the first entry to + /// return. + /// + /// The returned items are `MatchedItem`s that contain the matched item, the + /// dimension against which it was matched, represented as a string, and the + /// indices of the matched characters. + /// + /// # Example + /// ``` + /// let config = Config::default(); + /// let matcher = Matcher::new(config); + /// matcher.find("some pattern"); + /// + /// let results = matcher.results(10, 0); + /// for item in results { + /// println!("{:?}", item); + /// // Do something with the matched item + /// // ... + /// // Do something with the matched indices + /// // ... + /// } + /// ``` + pub fn results( + &mut self, + num_entries: u32, + offset: u32, + ) -> Vec> { + let snapshot = self.inner.snapshot(); + self.total_item_count = snapshot.item_count(); + self.matched_item_count = snapshot.matched_item_count(); + + let mut col_indices = Vec::new(); + let mut matcher = MATCHER.lock(); + + snapshot + .matched_items( + offset..(num_entries + offset).min(self.matched_item_count), + ) + .map(move |item| { + snapshot.pattern().column_pattern(0).indices( + item.matcher_columns[0].slice(..), + &mut matcher, + &mut col_indices, + ); + col_indices.sort_unstable(); + col_indices.dedup(); + + let indices = col_indices.drain(..); + + let matched_string = item.matcher_columns[0].to_string(); + MatchedItem { + inner: item.data.clone(), + matched_string, + match_indices: indices.map(|i| (i, i + 1)).collect(), + } + }) + .collect() + } + + /// Get a single matched item. + /// + /// # Example + /// ``` + /// let config = Config::default(); + /// let matcher = Matcher::new(config); + /// matcher.find("some pattern"); + /// + /// if let Some(item) = matcher.get_result(0) { + /// println!("{:?}", item); + /// // Do something with the matched item + /// // ... + /// } + /// ``` + pub fn get_result(&self, index: u32) -> Option> { + let snapshot = self.inner.snapshot(); + snapshot.get_matched_item(index).map(|item| { + let matched_string = item.matcher_columns[0].to_string(); + MatchedItem { + inner: item.data.clone(), + matched_string, + match_indices: Vec::new(), + } + }) + } +}