Skip to content

Commit

Permalink
Merge pull request #52 from thomasschafer/tschafer-show-regex-error
Browse files Browse the repository at this point in the history
Show regex error in popup
  • Loading branch information
thomasschafer authored Dec 2, 2024
2 parents 0d146ff + a7651af commit 3ea504b
Show file tree
Hide file tree
Showing 7 changed files with 171 additions and 59 deletions.
104 changes: 70 additions & 34 deletions src/app.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use ignore::WalkState;
use itertools::Itertools;
use log::info;
use parking_lot::{
MappedRwLockReadGuard, MappedRwLockWriteGuard, RwLock, RwLockReadGuard, RwLockWriteGuard,
};
Expand All @@ -22,7 +21,7 @@ use tokio::{

use crate::{
event::{AppEvent, BackgroundProcessingEvent, ReplaceResult, SearchResult},
fields::{CheckboxField, Field, TextField},
fields::{CheckboxField, Field, FieldError, TextField},
parsed_fields::{ParsedFields, SearchType},
utils::relative_path_from,
EventHandlingResult,
Expand Down Expand Up @@ -175,6 +174,7 @@ pub const NUM_SEARCH_FIELDS: usize = 4;
pub struct SearchFields {
pub fields: [SearchField; NUM_SEARCH_FIELDS],
pub highlighted: usize,
pub show_error_popup: bool,
}

macro_rules! define_field_accessor {
Expand Down Expand Up @@ -256,11 +256,20 @@ impl SearchFields {
},
],
highlighted: 0,
show_error_popup: false,
}
}

fn highlighted_field_impl(&self) -> &SearchField {
&self.fields[self.highlighted]
}

pub fn highlighted_field(&self) -> &Arc<RwLock<Field>> {
&self.fields[self.highlighted].field
&self.highlighted_field_impl().field
}

pub fn highlighted_field_name(&self) -> &FieldName {
&self.highlighted_field_impl().name
}

pub fn focus_next(&mut self) {
Expand All @@ -272,10 +281,17 @@ impl SearchFields {
(self.highlighted + self.fields.len().saturating_sub(1)) % self.fields.len();
}

pub fn clear_errors(&mut self) {
pub fn errors(&self) -> Vec<(&str, FieldError)> {
self.fields
.iter_mut()
.for_each(|field| field.field.write().clear_error())
.iter()
.filter_map(|field| {
field
.field
.read()
.error()
.map(|err| (field.name.title(), err))
})
.collect::<Vec<_>>()
}

pub fn search_type(&self) -> anyhow::Result<SearchType> {
Expand All @@ -290,6 +306,11 @@ impl SearchFields {
}
}

enum ValidatedField<T> {
Parsed(T),
Error,
}

pub struct App {
pub current_screen: Screen,
pub search_fields: SearchFields,
Expand Down Expand Up @@ -473,22 +494,29 @@ impl App {
}

fn handle_key_searching(&mut self, key: &KeyEvent) -> bool {
self.search_fields.clear_errors();
match (key.code, key.modifiers) {
(KeyCode::Enter, _) => {
self.app_event_sender.send(AppEvent::PerformSearch).unwrap();
}
(KeyCode::BackTab, _) | (KeyCode::Tab, KeyModifiers::ALT) => {
self.search_fields.focus_prev();
}
(KeyCode::Tab, _) => {
self.search_fields.focus_next();
}
(code, modifiers) => {
self.search_fields
.highlighted_field()
.write()
.handle_keys(code, modifiers);
if self.search_fields.show_error_popup {
self.search_fields.show_error_popup = false;
} else {
match (key.code, key.modifiers) {
(KeyCode::Enter, _) => {
self.app_event_sender.send(AppEvent::PerformSearch).unwrap();
}
(KeyCode::BackTab, _) | (KeyCode::Tab, KeyModifiers::ALT) => {
self.search_fields.focus_prev();
}
(KeyCode::Tab, _) => {
self.search_fields.focus_next();
}
(code, modifiers) => {
if let FieldName::FixedStrings = self.search_fields.highlighted_field_name() {
// TODO: ideally this should only happen when the field is checked, but for now this will do
self.search_fields.search_mut().clear_error();
};
self.search_fields
.highlighted_field()
.write()
.handle_keys(code, modifiers);
}
}
};
false
Expand Down Expand Up @@ -543,11 +571,13 @@ impl App {
}

match (key.code, key.modifiers) {
(KeyCode::Esc, _) | (KeyCode::Char('c'), KeyModifiers::CONTROL) => {
(KeyCode::Esc, _) | (KeyCode::Char('c'), KeyModifiers::CONTROL)
if !self.search_fields.show_error_popup =>
{
return Ok(EventHandlingResult {
exit: true,
rerender: true,
})
});
}
(KeyCode::Char('r'), KeyModifiers::CONTROL) => {
self.reset();
Expand All @@ -574,37 +604,43 @@ impl App {
}

fn validate_fields(
&self,
&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() {
info!("Error when parsing search regex {}", e);
self.search_fields
.search_mut()
.set_error("Couldn't parse regex".to_owned());
return Ok(None);
.set_error("Couldn't parse regex".to_owned(), e.to_string());
ValidatedField::Error
} else {
return Err(e);
}
}
Ok(p) => p,
Ok(p) => ValidatedField::Parsed(p),
};

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

let (search_pattern, path_pattern) = match (search_pattern, path_pattern) {
(ValidatedField::Parsed(s), ValidatedField::Parsed(p)) => (s, p),
_ => {
self.search_fields.show_error_popup = true;
return Ok(None);
}
};

Expand Down
1 change: 1 addition & 0 deletions src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ pub struct EventHandler {
pub app_event_sender: mpsc::UnboundedSender<AppEvent>,
}

#[derive(Debug)]
pub struct EventHandlingResult {
pub exit: bool,
pub rerender: bool,
Expand Down
29 changes: 14 additions & 15 deletions src/fields.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,17 @@ use ratatui::{
Frame,
};

#[derive(Clone)]
pub struct FieldError {
pub short: String,
pub long: String,
}

#[derive(Default)]
pub struct TextField {
pub text: String,
pub cursor_idx: usize,
pub error: Option<String>,
pub error: Option<FieldError>,
}

impl TextField {
Expand Down Expand Up @@ -151,8 +157,8 @@ impl TextField {
self.cursor_idx = 0;
}

pub fn set_error(&mut self, error: String) {
self.error = Some(error);
pub fn set_error(&mut self, short: String, long: String) {
self.error = Some(FieldError { short, long });
}

pub fn clear_error(&mut self) {
Expand Down Expand Up @@ -213,7 +219,7 @@ impl TextField {

pub struct CheckboxField {
pub checked: bool,
pub error: Option<String>, // TODO: render this
pub error: Option<FieldError>, // Not used currently so not rendered
}

impl CheckboxField {
Expand Down Expand Up @@ -246,6 +252,7 @@ impl Field {
}

pub fn handle_keys(&mut self, code: KeyCode, modifiers: KeyModifiers) {
self.clear_error();
match self {
Field::Text(f) => f.handle_keys(code, modifiers),
Field::Checkbox(f) => f.handle_keys(code, modifiers),
Expand All @@ -259,22 +266,14 @@ impl Field {
}
}

#[allow(dead_code)]
pub fn set_error(&mut self, error: String) {
match self {
Field::Text(f) => f.set_error(error),
Field::Checkbox(_) => todo!(),
}
}

pub fn clear_error(&mut self) {
match self {
Field::Text(f) => f.clear_error(),
Field::Checkbox(_) => {} // TODO
}
}

fn error(&self) -> Option<String> {
pub fn error(&self) -> Option<FieldError> {
match self {
Field::Text(f) => f.error.clone(),
Field::Checkbox(f) => f.error.clone(),
Expand Down Expand Up @@ -320,9 +319,9 @@ impl Field {
}
}

if let Some(error_string) = self.error() {
if let Some(error) = self.error() {
frame.render_widget(
Paragraph::new(Text::styled(format!("Error: {error_string}"), Color::Red)),
Paragraph::new(Text::styled(format!("Error: {}", error.short), Color::Red)),
outer_chunks[1],
);
};
Expand Down
1 change: 0 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ fn parse_log_level(s: &str) -> Result<LevelFilter, String> {
LevelFilter::from_str(s).map_err(|_| format!("Invalid log level: {}", s))
}

// In main(), update the logging setup:
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let args = Args::parse();
Expand Down
55 changes: 49 additions & 6 deletions src/ui.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
use itertools::Itertools;
use ratatui::{
layout::Constraint,
layout::{Alignment, Direction, Flex, Layout, Rect},
style::{Color, Style},
layout::{Alignment, Constraint, Direction, Flex, Layout, Rect},
style::{Color, Style, Stylize},
text::{Line, Span, Text},
widgets::{Block, List, ListItem, Paragraph},
widgets::{Block, Clear, List, ListItem, Paragraph, Wrap},
Frame,
};
use similar::{Change, ChangeTag, TextDiff};
Expand Down Expand Up @@ -52,8 +51,52 @@ fn render_search_view(frame: &mut Frame<'_>, app: &App, rect: Rect) {
)
});

let highlighted_area = areas[app.search_fields.highlighted];
if let Some(cursor_idx) = app.search_fields.highlighted_field().read().cursor_idx() {
if app.search_fields.show_error_popup {
let error_lines: Vec<Line<'_>> = app
.search_fields
.errors()
.iter()
.flat_map(|(name, error)| {
let name_line = Line::from(vec![Span::styled(*name, Style::default().bold())]);

let error_lines: Vec<Line<'_>> = error
.long
.lines()
.map(|line| {
Line::from(vec![Span::styled(
format!(" {line}"),
Style::default().fg(Color::Red),
)])
})
.collect();

std::iter::once(name_line)
.chain(error_lines)
.chain(std::iter::once(Line::from("")))
.collect::<Vec<_>>()
})
.collect();

let content_height = error_lines.len() as u16 + 1;

let popup_area = center(
area,
Constraint::Percentage(80),
Constraint::Length(content_height),
);

let popup = Paragraph::new(error_lines)
.block(
Block::bordered()
.title("Errors")
.title_alignment(Alignment::Center),
)
.wrap(Wrap { trim: true });
frame.render_widget(Clear, popup_area);
frame.render_widget(popup, popup_area);
} else if let Some(cursor_idx) = app.search_fields.highlighted_field().read().cursor_idx() {
let highlighted_area = areas[app.search_fields.highlighted];

frame.set_cursor(
highlighted_area.x + cursor_idx as u16 + 1,
highlighted_area.y + 1,
Expand Down
Loading

0 comments on commit 3ea504b

Please sign in to comment.