diff --git a/Cargo.lock b/Cargo.lock index fd49633..d1697d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1107,6 +1107,7 @@ dependencies = [ "notify", "open", "patch", + "regex", "rust-embed", "serde", "smol_str", diff --git a/Cargo.toml b/Cargo.toml index 1657638..edc6d09 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,9 +16,10 @@ grep = "0.3.1" ignore = "0.4.21" lexical-sort = "0.3.1" log = "0.4.20" +notify = "6.1.1" open = "5.0.2" patch = "0.7.0" -notify = "6.1.1" +regex = "1.10" serde = { version = "1", features = ["serde_derive"] } tokio = { version = "1", features = ["process", "time"] } # Extra syntax highlighting @@ -58,4 +59,4 @@ wgpu = ["libcosmic/wgpu", "cosmic-files/wgpu"] [profile.release-with-debug] inherits = "release" -debug = true \ No newline at end of file +debug = true diff --git a/i18n/en/cosmic_edit.ftl b/i18n/en/cosmic_edit.ftl index e935fa8..f2c20d4 100644 --- a/i18n/en/cosmic_edit.ftl +++ b/i18n/en/cosmic_edit.ftl @@ -55,6 +55,8 @@ find-next = Find next replace-placeholder = Replace... replace = Replace replace-all = Replace all +case-sensitive = Case sensitive +use-regex = Use regex # Menu diff --git a/src/config.rs b/src/config.rs index 8756395..d89db57 100644 --- a/src/config.rs +++ b/src/config.rs @@ -31,6 +31,8 @@ impl AppTheme { pub struct Config { pub app_theme: AppTheme, pub auto_indent: bool, + pub find_case_sensitive: bool, + pub find_use_regex: bool, pub font_name: String, pub font_size: u16, pub highlight_current_line: bool, @@ -47,6 +49,8 @@ impl Default for Config { Self { app_theme: AppTheme::System, auto_indent: true, + find_case_sensitive: false, + find_use_regex: false, font_name: "Fira Mono".to_string(), font_size: 14, highlight_current_line: true, @@ -61,6 +65,16 @@ impl Default for Config { } impl Config { + pub fn find_regex(&self, pattern: &str) -> Result { + let mut builder = if self.find_use_regex { + regex::RegexBuilder::new(pattern) + } else { + regex::RegexBuilder::new(®ex::escape(pattern)) + }; + builder.case_insensitive(!self.find_case_sensitive); + builder.build() + } + // Calculate metrics from font size pub fn metrics(&self) -> Metrics { let font_size = self.font_size.max(1) as f32; diff --git a/src/main.rs b/src/main.rs index 5c4ecc0..f354c4a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -317,12 +317,14 @@ pub enum Message { DefaultFontSize(usize), DialogMessage(DialogMessage), Find(Option), + FindCaseSensitive(bool), FindNext, FindPrevious, FindReplace, FindReplaceAll, FindReplaceValueChanged(String), FindSearchValueChanged(String), + FindUseRegex(bool), GitProjectStatus(Vec<(String, PathBuf, Vec)>), Key(Modifiers, keyboard::Key), LaunchUrl(String), @@ -1556,10 +1558,27 @@ impl Application for App { // Focus correct input return self.update_focus(); } + Message::FindCaseSensitive(find_case_sensitive) => { + self.config.find_case_sensitive = find_case_sensitive; + return self.save_config(); + } Message::FindNext => { if !self.find_search_value.is_empty() { if let Some(Tab::Editor(tab)) = self.active_tab() { - tab.search(&self.find_search_value, true); + //TODO: do not compile find regex on every search? + match self.config.find_regex(&self.find_search_value) { + Ok(regex) => { + tab.search(®ex, true); + } + Err(err) => { + //TODO: put regex error in find box + log::warn!( + "failed to compile regex {:?}: {}", + self.find_search_value, + err + ); + } + } } } @@ -1569,7 +1588,20 @@ impl Application for App { Message::FindPrevious => { if !self.find_search_value.is_empty() { if let Some(Tab::Editor(tab)) = self.active_tab() { - tab.search(&self.find_search_value, false); + //TODO: do not compile find regex on every search? + match self.config.find_regex(&self.find_search_value) { + Ok(regex) => { + tab.search(®ex, false); + } + Err(err) => { + //TODO: put regex error in find box + log::warn!( + "failed to compile regex {:?}: {}", + self.find_search_value, + err + ); + } + } } } @@ -1579,7 +1611,21 @@ impl Application for App { Message::FindReplace => { if !self.find_search_value.is_empty() { if let Some(Tab::Editor(tab)) = self.active_tab() { - tab.replace(&self.find_search_value, &self.find_replace_value); + //TODO: do not compile find regex on every search? + match self.config.find_regex(&self.find_search_value) { + Ok(regex) => { + //TODO: support captures + tab.replace(®ex, &self.find_replace_value); + } + Err(err) => { + //TODO: put regex error in find box + log::warn!( + "failed to compile regex {:?}: {}", + self.find_search_value, + err + ); + } + } } } @@ -1589,11 +1635,25 @@ impl Application for App { Message::FindReplaceAll => { if !self.find_search_value.is_empty() { if let Some(Tab::Editor(tab)) = self.active_tab() { - { - let mut editor = tab.editor.lock().unwrap(); - editor.set_cursor(cosmic_text::Cursor::new(0, 0)); + //TODO: do not compile find regex on every search? + match self.config.find_regex(&self.find_search_value) { + Ok(regex) => { + //TODO: support captures + { + let mut editor = tab.editor.lock().unwrap(); + editor.set_cursor(cosmic_text::Cursor::new(0, 0)); + } + while tab.replace(®ex, &self.find_replace_value) {} + } + Err(err) => { + //TODO: put regex error in find box + log::warn!( + "failed to compile regex {:?}: {}", + self.find_search_value, + err + ); + } } - while tab.replace(&self.find_search_value, &self.find_replace_value) {} } } @@ -1606,6 +1666,10 @@ impl Application for App { Message::FindSearchValueChanged(value) => { self.find_search_value = value; } + Message::FindUseRegex(find_use_regex) => { + self.config.find_use_regex = find_use_regex; + return self.save_config(); + } Message::GitProjectStatus(project_status) => { self.git_project_status = Some(project_status); } @@ -2480,7 +2544,7 @@ impl Application for App { .padding(space_xxs) .spacing(space_xxs); - let mut column = widget::column::with_capacity(2).push(find_widget); + let mut column = widget::column::with_capacity(3).push(find_widget); if *replace { let replace_input = widget::text_input::text_input( fl!("replace-placeholder"), @@ -2524,6 +2588,26 @@ impl Application for App { column = column.push(replace_widget); } + column = column.push( + widget::row::with_children(vec![ + widget::checkbox( + fl!("case-sensitive"), + self.config.find_case_sensitive, + Message::FindCaseSensitive, + ) + .into(), + widget::checkbox( + fl!("use-regex"), + self.config.find_use_regex, + Message::FindUseRegex, + ) + .into(), + ]) + .align_items(Alignment::Center) + .padding(space_xxs) + .spacing(space_xxs), + ); + tab_column = tab_column .push(widget::layer_container(column).layer(cosmic_theme::Layer::Primary)); } diff --git a/src/tab.rs b/src/tab.rs index c410b71..ec155e5 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -7,6 +7,7 @@ use cosmic::{ use cosmic_files::mime_icon::{mime_for_path, mime_icon, FALLBACK_MIME_ICON}; use cosmic_text::{Attrs, Buffer, Edit, Shaping, SyntaxEditor, ViEditor, Wrap}; use notify::Watcher; +use regex::Regex; use std::{ fs, path::PathBuf, @@ -208,18 +209,17 @@ impl EditorTab { } } - pub fn replace(&self, value: &str, replace: &str) -> bool { + pub fn replace(&self, regex: &Regex, replace: &str) -> bool { let mut editor = self.editor.lock().unwrap(); let mut cursor = editor.cursor(); let start_line = cursor.line; while cursor.line < editor.with_buffer(|buffer| buffer.lines.len()) { - if let Some(index) = editor.with_buffer(|buffer| { - buffer.lines[cursor.line] - .text() - .match_indices(value) - .filter_map(|(i, _)| { - if cursor.line != start_line || i >= cursor.index { - Some(i) + if let Some((index, len)) = editor.with_buffer(|buffer| { + regex + .find_iter(buffer.lines[cursor.line].text()) + .filter_map(|m| { + if cursor.line != start_line || m.start() >= cursor.index { + Some((m.start(), m.len())) } else { None } @@ -228,7 +228,7 @@ impl EditorTab { }) { cursor.index = index; let mut end = cursor; - end.index = index + value.len(); + end.index = index + len; editor.start_change(); editor.delete_range(cursor, end); @@ -245,19 +245,18 @@ impl EditorTab { } // Code adapted from cosmic-text ViEditor search - pub fn search(&self, value: &str, forwards: bool) -> bool { + pub fn search(&self, regex: &Regex, forwards: bool) -> bool { let mut editor = self.editor.lock().unwrap(); let mut cursor = editor.cursor(); let start_line = cursor.line; if forwards { while cursor.line < editor.with_buffer(|buffer| buffer.lines.len()) { if let Some(index) = editor.with_buffer(|buffer| { - buffer.lines[cursor.line] - .text() - .match_indices(value) - .filter_map(|(i, _)| { - if cursor.line != start_line || i > cursor.index { - Some(i) + regex + .find_iter(buffer.lines[cursor.line].text()) + .filter_map(|m| { + if cursor.line != start_line || m.start() > cursor.index { + Some(m.start()) } else { None } @@ -277,17 +276,16 @@ impl EditorTab { cursor.line -= 1; if let Some(index) = editor.with_buffer(|buffer| { - buffer.lines[cursor.line] - .text() - .rmatch_indices(value) - .filter_map(|(i, _)| { - if cursor.line != start_line || i < cursor.index { - Some(i) + regex + .find_iter(buffer.lines[cursor.line].text()) + .filter_map(|m| { + if cursor.line != start_line || m.start() < cursor.index { + Some(m.start()) } else { None } }) - .next() + .last() }) { cursor.index = index; editor.set_cursor(cursor);