Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add flag to use additional regex features (e.g. negative lookahead) #54

Merged
merged 8 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,5 @@ jobs:
- name: Lint
run: cargo clippy

- name: Run tests (Windows)
if: matrix.os == 'windows-latest'
run: cargo test --verbose -- --test-threads=1

- name: Run tests (Unix)
if: matrix.os != 'windows-latest'
- name: Run tests
run: cargo test --verbose
70 changes: 69 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ content_inspector = "0.2.4"
crossterm = { version = "0.27", features = ["event-stream"] }
dirs = "5.0.1"
etcetera = "0.8.0"
fancy-regex = "0.14.0"
futures = "0.3.31"
ignore = "0.4.23"
itertools = "0.13.0"
Expand All @@ -27,6 +28,7 @@ ratatui = "0.27.0"
regex = "1.11.1"
serde = { version = "1.0.215", features = ["derive"] }
serde_json = "1.0.133"
serial_test = "3.2.0"
similar = "2.6.0"
simple-log = "2.1.1"
tokio = { version = "1.41.1", features = ["full"] }
Expand Down
79 changes: 54 additions & 25 deletions src/app.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use anyhow::Error;
use fancy_regex::Regex as FancyRegex;
use ignore::WalkState;
use itertools::Itertools;
use parking_lot::{
Expand Down Expand Up @@ -117,9 +119,9 @@ impl ReplaceState {
pub struct SearchInProgressState {
pub search_state: SearchState,
pub last_render: Instant,
pub handle: JoinHandle<()>,
pub processing_sender: UnboundedSender<BackgroundProcessingEvent>,
pub processing_receiver: UnboundedReceiver<BackgroundProcessingEvent>,
handle: JoinHandle<()>,
processing_sender: UnboundedSender<BackgroundProcessingEvent>,
processing_receiver: UnboundedReceiver<BackgroundProcessingEvent>,
}

impl SearchInProgressState {
Expand Down Expand Up @@ -151,7 +153,7 @@ pub enum Screen {
}

impl Screen {
pub fn search_results_mut(&mut self) -> &mut SearchState {
fn search_results_mut(&mut self) -> &mut SearchState {
match self {
Screen::SearchProgressing(SearchInProgressState { search_state, .. }) => search_state,
Screen::SearchComplete(search_state) => search_state,
Expand Down Expand Up @@ -182,6 +184,7 @@ pub struct SearchFields {
pub fields: [SearchField; NUM_SEARCH_FIELDS],
pub highlighted: usize,
pub show_error_popup: bool,
advanced_regex: bool,
}

macro_rules! define_field_accessor {
Expand Down Expand Up @@ -264,9 +267,15 @@ impl SearchFields {
],
highlighted: 0,
show_error_popup: false,
advanced_regex: false,
}
}

pub fn with_advanced_regex(mut self, advanced_regex: bool) -> Self {
self.advanced_regex = advanced_regex;
self
}

fn highlighted_field_impl(&self) -> &SearchField {
&self.fields[self.highlighted]
}
Expand Down Expand Up @@ -306,11 +315,29 @@ impl SearchFields {
let search_text = search.text();
let result = if self.fixed_strings().checked {
SearchType::Fixed(search_text)
} else if self.advanced_regex {
SearchType::PatternAdvanced(FancyRegex::new(&search_text)?)
} else {
SearchType::Pattern(Regex::new(&search_text)?)
};
Ok(result)
}

pub fn path_pattern_parsed(&self) -> anyhow::Result<Option<SearchType>> {
let path_patt_text = &self.path_pattern().text;
let result = if path_patt_text.is_empty() {
None
} else {
Some({
if self.advanced_regex {
SearchType::PatternAdvanced(FancyRegex::new(path_patt_text)?)
} else {
SearchType::Pattern(Regex::new(path_patt_text)?)
}
})
};
Ok(result)
}
}

enum ValidatedField<T> {
Expand All @@ -321,11 +348,10 @@ enum ValidatedField<T> {
pub struct App {
pub current_screen: Screen,
pub search_fields: SearchFields,
pub directory: PathBuf,
pub include_hidden: bool,
directory: PathBuf,
include_hidden: bool,

pub running: bool,
pub app_event_sender: UnboundedSender<AppEvent>,
app_event_sender: UnboundedSender<AppEvent>,
}

const BINARY_EXTENSIONS: &[&str] = &["png", "gif", "jpg", "jpeg", "ico", "svg", "pdf"];
Expand All @@ -334,20 +360,22 @@ impl App {
pub fn new(
directory: Option<PathBuf>,
include_hidden: bool,
advanced_regex: bool,
app_event_sender: UnboundedSender<AppEvent>,
) -> Self {
let directory = match directory {
Some(d) => d,
None => std::env::current_dir().unwrap(),
};
let search_fields =
SearchFields::with_values("", "", false, "").with_advanced_regex(advanced_regex);

Self {
current_screen: Screen::SearchFields,
search_fields: SearchFields::with_values("", "", false, ""),
directory, // TODO: add this as a field that can be edited, e.g. allow glob patterns
search_fields,
directory,
include_hidden,

running: true,
app_event_sender,
}
}
Expand All @@ -366,6 +394,7 @@ impl App {
*self = Self::new(
Some(self.directory.clone()),
self.include_hidden,
self.search_fields.advanced_regex,
self.app_event_sender.clone(),
);
}
Expand Down Expand Up @@ -615,13 +644,18 @@ impl App {
})
}

fn is_regex_error(e: &Error) -> bool {
e.downcast_ref::<regex::Error>().is_some()
|| e.downcast_ref::<fancy_regex::Error>().is_some()
}

fn validate_fields(
&mut self,
background_processing_sender: UnboundedSender<BackgroundProcessingEvent>,
) -> anyhow::Result<Option<ParsedFields>> {
let search_pattern = match self.search_fields.search_type() {
Err(e) => {
if e.downcast_ref::<regex::Error>().is_some() {
if Self::is_regex_error(&e) {
self.search_fields
.search_mut()
.set_error("Couldn't parse regex".to_owned(), e.to_string());
Expand All @@ -633,19 +667,14 @@ impl App {
Ok(p) => ValidatedField::Parsed(p),
};

let path_pattern_text = self.search_fields.path_pattern().text();
let path_pattern = if path_pattern_text.is_empty() {
ValidatedField::Parsed(None)
} else {
match Regex::new(path_pattern_text.as_str()) {
Err(e) => {
self.search_fields
.path_pattern_mut()
.set_error("Couldn't parse regex".to_owned(), e.to_string());
ValidatedField::Error
}
Ok(r) => ValidatedField::Parsed(Some(r)),
let path_pattern = match self.search_fields.path_pattern_parsed() {
Err(e) => {
self.search_fields
.path_pattern_mut()
.set_error("Couldn't parse regex".to_owned(), e.to_string());
ValidatedField::Error
}
Ok(r) => ValidatedField::Parsed(r),
};

let (search_pattern, path_pattern) = match (search_pattern, path_pattern) {
Expand Down Expand Up @@ -923,7 +952,7 @@ mod tests {

fn build_test_app(results: Vec<SearchResult>) -> App {
let event_handler = EventHandler::new();
let mut app = App::new(None, false, event_handler.app_event_sender);
let mut app = App::new(None, false, false, event_handler.app_event_sender);
app.current_screen = Screen::SearchComplete(SearchState {
results,
selected: 0,
Expand Down
2 changes: 1 addition & 1 deletion src/fields.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use ratatui::{
Frame,
};

#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct FieldError {
pub short: String,
pub long: String,
Expand Down
15 changes: 11 additions & 4 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ struct Args {
default_value = DEFAULT_LOG_LEVEL
)]
log_level: LevelFilter,

/// Use advanced regex features (including negative look-ahead), at the cost of performance
#[arg(short = 'a', long, default_value = "false")]
advanced_regex: bool,
}

fn parse_log_level(s: &str) -> Result<LevelFilter, String> {
Expand All @@ -52,24 +56,27 @@ async fn main() -> anyhow::Result<()> {

setup_logging(args.log_level)?;

let args = Args::parse();

let directory = match args.directory {
None => None,
Some(d) => Some(validate_directory(&d)?),
};

let app_events_handler = EventHandler::new();
let app_event_sender = app_events_handler.app_event_sender.clone();
let mut app = App::new(directory, args.hidden, app_event_sender);
let mut app = App::new(
directory,
args.hidden,
args.advanced_regex,
app_event_sender,
);

let backend = CrosstermBackend::new(io::stdout());
let terminal = Terminal::new(backend)?;
let mut tui = Tui::new(terminal, app_events_handler);
tui.init()?;
tui.draw(&mut app)?;

while app.running {
loop {
let EventHandlingResult { exit, rerender } = tokio::select! {
Some(event) = tui.events.receiver.recv() => {
match event {
Expand Down
Loading
Loading