refactoring: extract matcher logic into separate crate

This commit is contained in:
Alexandre Pasmantier 2024-11-09 23:32:04 +01:00
parent 4e4ef9761b
commit c1f41bf107
21 changed files with 310 additions and 208 deletions

View File

@ -39,11 +39,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@nightly
uses: actions/checkout@v3
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: Swatinem/rust-cache@v2
- name: Clippy check
run: cargo clippy --all-targets --all-features --workspace -- -D warnings
- name: Clippy Check
run: cargo clippy -- -D warnings

11
Cargo.lock generated
View File

@ -3094,7 +3094,7 @@ dependencies = [
[[package]]
name = "television"
version = "0.2.13"
version = "0.3.13"
dependencies = [
"anyhow",
"bat",
@ -3127,6 +3127,7 @@ dependencies = [
"strum",
"syntect",
"television-derive",
"television-fuzzy",
"termtree",
"tokio",
"toml",
@ -3146,6 +3147,14 @@ dependencies = [
"syn 2.0.87",
]
[[package]]
name = "television-fuzzy"
version = "0.1.1"
dependencies = [
"nucleo",
"parking_lot",
]
[[package]]
name = "tempfile"
version = "3.14.0"

View File

@ -1,6 +1,6 @@
[package]
name = "television"
version = "0.2.13"
version = "0.3.13"
edition = "2021"
description = "The revolution will be televised."
license = "MIT"
@ -22,9 +22,10 @@ path = "crates/television/main.rs"
name = "tv"
[workspace]
members = ["crates/television_derive"]
members = ["crates/television_derive", "crates/television_fuzzy"]
[dependencies]
television-fuzzy = { version = "0.1.1", path = "crates/television_fuzzy" }
television-derive = { version = "0.1.2", path = "crates/television_derive" }
better-panic = "0.3.0"
clap = { version = "4.4.5", features = [

View File

@ -1,4 +1,4 @@
VERSION=0.2.13
VERSION=0.3.13
NAME=television
EXEC=tv
PREFIX=$(HOME)/.local

View File

@ -1,12 +1,11 @@
use crate::fuzzy::matcher::{Config, Injector, Matcher};
use devicons::FileIcon;
use tracing::debug;
use crate::channels::OnAir;
use crate::entry::Entry;
use crate::previewers::PreviewType;
use crate::utils::indices::sep_name_and_value_indices;
use crate::utils::strings::preprocess_line;
use devicons::FileIcon;
use television_fuzzy::matcher::{config::Config, injector::Injector, Matcher};
use tracing::debug;
#[derive(Debug, Clone)]
struct Alias {

View File

@ -2,10 +2,10 @@ use devicons::FileIcon;
use super::OnAir;
use crate::entry::Entry;
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;
use television_fuzzy::matcher::{config::Config, Matcher};
#[derive(Debug, Clone)]
struct EnvVar {

View File

@ -1,12 +1,12 @@
use crate::channels::{OnAir, TelevisionChannel};
use crate::entry::Entry;
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 std::collections::HashSet;
use std::path::PathBuf;
use television_fuzzy::matcher::{config::Config, injector::Injector, Matcher};
pub struct Channel {
matcher: Matcher<String>,

View File

@ -12,8 +12,8 @@ use crate::{
};
use crate::channels::OnAir;
use crate::fuzzy::matcher::{Config, Injector, Matcher};
use crate::utils::strings::preprocess_line;
use television_fuzzy::matcher::{config::Config, injector::Injector, Matcher};
pub struct Channel {
matcher: Matcher<String>,

View File

@ -1,13 +1,12 @@
use clap::ValueEnum;
use devicons::FileIcon;
use crate::channels::{TelevisionChannel, UnitChannel};
use crate::{
channels::{CliTvChannel, OnAir},
entry::Entry,
fuzzy::matcher::{Config, Matcher},
previewers::PreviewType,
};
use clap::ValueEnum;
use devicons::FileIcon;
use television_fuzzy::matcher::{config::Config, Matcher};
pub struct RemoteControl {
matcher: Matcher<String>,

View File

@ -5,9 +5,9 @@ use devicons::FileIcon;
use super::OnAir;
use crate::entry::Entry;
use crate::fuzzy::matcher::{Config, Matcher};
use crate::previewers::PreviewType;
use crate::utils::strings::preprocess_line;
use television_fuzzy::matcher::{config::Config, Matcher};
pub struct Channel {
matcher: Matcher<String>,

View File

@ -1,14 +1,5 @@
use devicons::FileIcon;
use ignore::WalkState;
use std::{
fs::File,
io::{BufRead, Read, Seek},
path::{Path, PathBuf},
sync::{atomic::AtomicUsize, Arc},
};
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},
@ -17,10 +8,16 @@ use crate::utils::{
use crate::{
entry::Entry, utils::strings::proportion_of_printable_ascii_characters,
};
use crate::{
fuzzy::matcher::{Config, Injector, Matcher},
previewers::PreviewType,
use devicons::FileIcon;
use ignore::WalkState;
use std::{
fs::File,
io::{BufRead, Read, Seek},
path::{Path, PathBuf},
sync::{atomic::AtomicUsize, Arc},
};
use television_fuzzy::matcher::{config::Config, injector::Injector, Matcher};
use tracing::{debug, warn};
#[derive(Debug, Clone)]
struct CandidateLine {

View File

@ -1,27 +0,0 @@
use parking_lot::Mutex;
use std::ops::DerefMut;
pub mod matcher;
pub struct LazyMutex<T> {
inner: Mutex<Option<T>>,
init: fn() -> T,
}
impl<T> LazyMutex<T> {
pub const fn new(init: fn() -> T) -> Self {
Self {
inner: Mutex::new(None),
init,
}
}
pub fn lock(&self) -> impl DerefMut<Target = T> + '_ {
parking_lot::MutexGuard::map(self.inner.lock(), |val| {
val.get_or_insert_with(self.init)
})
}
}
pub static MATCHER: LazyMutex<nucleo::Matcher> =
LazyMutex::new(nucleo::Matcher::default);

View File

@ -18,7 +18,6 @@ pub mod config;
pub mod entry;
pub mod errors;
pub mod event;
pub mod fuzzy;
pub mod logging;
pub mod picker;
pub mod previewers;

View File

@ -101,5 +101,5 @@ lazy_static! {
pub fn load_highlighting_assets() -> HighlightingAssets {
HighlightingAssets::from_cache(PROJECT_DIRS.cache_dir())
.unwrap_or_else(|_| bat::assets::HighlightingAssets::from_binary())
.unwrap_or_else(|_| HighlightingAssets::from_binary())
}

View File

@ -0,0 +1,19 @@
[package]
name = "television-fuzzy"
version = "0.1.1"
edition = "2021"
description = "The revolution will be televised."
license = "MIT"
authors = ["Alexandre Pasmantier <alex.pasmant@gmail.com>"]
repository = "https://github.com/alexpasmantier/television"
keywords = ["search", "fuzzy", "preview", "tui", "terminal"]
categories = [
"command-line-utilities",
"command-line-interface",
"concurrency",
"development-tools",
]
[dependencies]
nucleo = "0.5.0"
parking_lot = "0.12.3"

View File

@ -0,0 +1,3 @@
pub mod matcher;
pub use matcher::Matcher;

View File

@ -0,0 +1,70 @@
/// 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, Debug)]
pub struct Config {
/// The number of threads to use for the fuzzy matcher.
pub n_threads: Option<usize>,
/// Whether to ignore case when matching.
pub ignore_case: bool,
/// Whether to prefer prefix matches.
pub prefer_prefix: bool,
/// Whether to optimize for matching paths.
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
}
}

View File

@ -0,0 +1,49 @@
/// 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<I>
where
I: Sync + Send + Clone + 'static,
{
/// The inner `Injector` from the `Nucleo` fuzzy matcher.
inner: nucleo::Injector<I>,
}
impl<I> Injector<I>
where
I: Sync + Send + Clone + 'static,
{
pub fn new(inner: nucleo::Injector<I>) -> 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
/// ```
/// use television_fuzzy::matcher::{Config, Matcher};
///
/// 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<F>(&self, item: I, f: F)
where
F: FnOnce(&I, &mut [nucleo::Utf32String]),
{
self.inner.push(item, f);
}
}

View File

@ -0,0 +1,91 @@
use nucleo::Matcher;
use parking_lot::Mutex;
use std::ops::DerefMut;
/// A lazy-initialized mutex.
///
/// This is used to lazily initialize a nucleo matcher (which pre-allocates
/// quite a bit of memory upfront which can be expensive during initialization).
///
/// # Example
/// ```rust
/// use television_fuzzy::matcher::lazy::LazyMutex;
///
/// struct Thing {
/// // ...
/// }
///
/// impl Thing {
/// fn new() -> Self {
/// // something expensive
/// Thing { }
/// }
/// }
///
/// static THING_TO_LAZY_INIT: LazyMutex<Thing> = LazyMutex::new(|| {
/// Thing::new()
/// });
/// ```
pub struct LazyMutex<T> {
/// The inner value, wrapped in a mutex.
inner: Mutex<Option<T>>,
/// The initialization function.
init: fn() -> T,
}
impl<T> LazyMutex<T> {
pub const fn new(init: fn() -> T) -> Self {
Self {
inner: Mutex::new(None),
init,
}
}
/// Locks the mutex and returns a guard that allows mutable access to the
/// inner value.
pub fn lock(&self) -> impl DerefMut<Target = T> + '_ {
parking_lot::MutexGuard::map(self.inner.lock(), |val| {
val.get_or_insert_with(self.init)
})
}
}
/// A lazy-initialized nucleo matcher used for conveniently computing match indices.
///
/// This is used to lazily initialize a nucleo matcher (which pre-allocates quite a bit of memory
/// upfront which can be expensive at initialization).
///
/// This matcher is used as a convenience for computing match indices on a subset of matched items.
///
/// # Example
/// ```
/// use television_fuzzy::matcher::{lazy::MATCHER, matched_item::MatchedItem};
///
/// let snapshot = channel_matcher.snapshot();
///
/// let mut col_indices = vec![];
/// let mut matcher = MATCHER.lock();
///
/// snapshot
/// .matched_items(..)
/// .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();
/// ```
pub static MATCHER: LazyMutex<Matcher> = LazyMutex::new(Matcher::default);

View File

@ -0,0 +1,20 @@
/// 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.
#[derive(Debug, Clone)]
pub struct MatchedItem<I>
where
I: Sync + Send + Clone + 'static,
{
/// The matched item.
pub inner: I,
/// The dimension against which the item was matched (as a string).
pub matched_string: String,
/// The indices of the matched characters.
pub match_indices: Vec<(u32, u32)>,
}

View File

@ -1,36 +1,24 @@
use injector::Injector;
use std::sync::Arc;
use super::MATCHER;
use crate::matcher::{
config::Config, lazy::MATCHER, matched_item::MatchedItem,
};
pub mod config;
pub mod injector;
pub mod lazy;
pub mod matched_item;
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<I>
where
I: Sync + Send + Clone + 'static,
{
/// The matched item.
pub inner: I,
/// The dimension against which the item was matched (as a string).
pub matched_string: String,
/// The indices of the matched characters.
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)]
#[derive(Default, Debug, Clone, Copy)]
pub struct Status {
/// Whether the matcher is currently running.
pub running: bool,
@ -44,125 +32,6 @@ impl From<nucleo::Status> for Status {
}
}
/// 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 {
/// The number of threads to use for the fuzzy matcher.
pub n_threads: Option<usize>,
/// Whether to ignore case when matching.
pub ignore_case: bool,
/// Whether to prefer prefix matches.
pub prefer_prefix: bool,
/// Whether to optimize for matching paths.
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<I>
where
I: Sync + Send + Clone + 'static,
{
/// The inner `Injector` from the `Nucleo` fuzzy matcher.
inner: nucleo::Injector<I>,
}
impl<I> Injector<I>
where
I: Sync + Send + Clone + 'static,
{
pub fn new(inner: nucleo::Injector<I>) -> 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<F>(&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`.
@ -220,6 +89,8 @@ where
///
/// # Example
/// ```
/// use television_fuzzy::matcher::{Config, Matcher};
///
/// let config = Config::default();
/// let matcher = Matcher::new(config);
/// let injector = matcher.injector();
@ -268,8 +139,10 @@ where
///
/// # Example
/// ```
/// use television_fuzzy::matcher::{Config, Matcher};
///
/// let config = Config::default();
/// let matcher = Matcher::new(config);
/// let mut matcher: Matcher<String> = Matcher::new(config);
/// matcher.find("some pattern");
///
/// let results = matcher.results(10, 0);
@ -322,12 +195,13 @@ where
///
/// # Example
/// ```
/// use television_fuzzy::matcher::{Config, Matcher};
///
/// let config = Config::default();
/// let matcher = Matcher::new(config);
/// let mut matcher: Matcher<String> = Matcher::new(config);
/// matcher.find("some pattern");
///
/// if let Some(item) = matcher.get_result(0) {
/// println!("{:?}", item);
/// // Do something with the matched item
/// // ...
/// }