From c1d0c7af1e083a445c35ee954b4f4c52b5c26cd3 Mon Sep 17 00:00:00 2001 From: candle Date: Tue, 15 Jul 2025 11:20:52 -0400 Subject: [PATCH 1/8] config checking/merging, slightly more generous scroll width for giga fast typing --- src/app/config.rs | 58 +++++++++++++++++++++++++++++++++++---------- src/app/state/ui.rs | 2 +- 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/src/app/config.rs b/src/app/config.rs index 3f3655c..8fa2eab 100644 --- a/src/app/config.rs +++ b/src/app/config.rs @@ -5,22 +5,38 @@ use super::theme::Theme; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Config { + #[serde(default = "default_auto_hide_toolbar")] pub auto_hide_toolbar: bool, + #[serde(default = "default_auto_hide_tab_bar")] pub auto_hide_tab_bar: bool, + #[serde(default = "default_show_line_numbers")] pub show_line_numbers: bool, + #[serde(default = "default_word_wrap")] pub word_wrap: bool, + #[serde(default = "Theme::default")] pub theme: Theme, + #[serde(default = "default_line_side")] pub line_side: bool, + #[serde(default = "default_font_family")] pub font_family: String, + #[serde(default = "default_font_size")] pub font_size: f32, // pub vim_mode: bool, } +fn default_auto_hide_toolbar() -> bool { false } +fn default_auto_hide_tab_bar() -> bool { true } +fn default_show_line_numbers() -> bool { false } +fn default_word_wrap() -> bool { true } +fn default_line_side() -> bool { false } +fn default_font_family() -> String { "Proportional".to_string() } +fn default_font_size() -> f32 { 14.0 } + impl Default for Config { fn default() -> Self { Self { auto_hide_toolbar: false, - auto_hide_tab_bar: false, + auto_hide_tab_bar: true, show_line_numbers: false, word_wrap: true, theme: Theme::default(), @@ -35,9 +51,9 @@ impl Default for Config { impl Config { pub fn config_path() -> Option { let config_dir = if let Some(config_dir) = dirs::config_dir() { - config_dir.join("ced") + config_dir.join(format!("{}", env!("CARGO_PKG_NAME"))) } else if let Some(home_dir) = dirs::home_dir() { - home_dir.join(".config").join("ced") + home_dir.join(".config").join(format!("{}", env!("CARGO_PKG_NAME"))) } else { return None; }; @@ -60,16 +76,22 @@ impl Config { } match std::fs::read_to_string(&config_path) { - Ok(content) => match toml::from_str::(&content) { - Ok(config) => config, - Err(e) => { - eprintln!( - "Failed to parse config file {}: {}", - config_path.display(), - e - ); - Self::default() - } + Ok(content) => { + let mut config = match toml::from_str::(&content) { + Ok(config) => config, + Err(e) => { + eprintln!( + "Failed to parse config file {}: {}", + config_path.display(), + e + ); + return Self::default(); + } + }; + + let default_config = Self::default(); + config.merge_with_default(default_config); + config }, Err(e) => { eprintln!( @@ -82,6 +104,16 @@ impl Config { } } + fn merge_with_default(&mut self, default: Config) { + if self.font_family.is_empty() { + self.font_family = default.font_family; + } + + if self.font_size <= 0.0 { + self.font_size = default.font_size; + } + } + pub fn save(&self) -> Result<(), Box> { let config_path = Self::config_path().ok_or("Cannot determine config directory")?; diff --git a/src/app/state/ui.rs b/src/app/state/ui.rs index cec69b0..f989864 100644 --- a/src/app/state/ui.rs +++ b/src/app/state/ui.rs @@ -125,7 +125,7 @@ impl TextEditor { return self.calculate_editor_dimensions(ui).text_width; } - let longest_line_width = processing_result.longest_line_pixel_width + self.font_size; + let longest_line_width = processing_result.longest_line_pixel_width + (self.font_size * 2.0); let dimensions = self.calculate_editor_dimensions(ui); longest_line_width.max(dimensions.text_width) -- 2.47.1 From b3130823745bd093fde3f9c5dd41c90373eec886 Mon Sep 17 00:00:00 2001 From: candle Date: Tue, 15 Jul 2025 20:08:38 -0400 Subject: [PATCH 2/8] better tab config name --- README.md | 1 + src/app/config.rs | 42 +++++++++++++++++++++++++++------------ src/app/state/app_impl.rs | 2 +- src/app/state/config.rs | 4 ++-- src/app/state/default.rs | 2 +- src/app/state/editor.rs | 2 +- src/ui/menu_bar.rs | 7 ++----- 7 files changed, 37 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 046e00e..15c8e81 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ font_size = 16.0 | Option | Default | Description | |--------|---------|-------------| | `auto_hide_toolbar` | `false` | If `true`, the menu bar at the top will be hidden. Move your mouse to the top of the window to reveal it. | +| `hide_tab_bar` | 'true' | If `false`, a separate tab bar will be drawn below the toolbar. | | `show_line_numbers` | `false` | If `true`, line numbers will be displayed on the side specified by `line_side`. | | `line_side` | `false` | If `false`, line numbers are on the left. If `true`, they are on the right. | | `word_wrap` | `false` | If `true`, lines will wrap when they reach the edge of the window. | diff --git a/src/app/config.rs b/src/app/config.rs index 8fa2eab..7f14af5 100644 --- a/src/app/config.rs +++ b/src/app/config.rs @@ -7,8 +7,8 @@ use super::theme::Theme; pub struct Config { #[serde(default = "default_auto_hide_toolbar")] pub auto_hide_toolbar: bool, - #[serde(default = "default_auto_hide_tab_bar")] - pub auto_hide_tab_bar: bool, + #[serde(default = "default_hide_tab_bar")] + pub hide_tab_bar: bool, #[serde(default = "default_show_line_numbers")] pub show_line_numbers: bool, #[serde(default = "default_word_wrap")] @@ -24,19 +24,33 @@ pub struct Config { // pub vim_mode: bool, } -fn default_auto_hide_toolbar() -> bool { false } -fn default_auto_hide_tab_bar() -> bool { true } -fn default_show_line_numbers() -> bool { false } -fn default_word_wrap() -> bool { true } -fn default_line_side() -> bool { false } -fn default_font_family() -> String { "Proportional".to_string() } -fn default_font_size() -> f32 { 14.0 } +fn default_auto_hide_toolbar() -> bool { + false +} +fn default_hide_tab_bar() -> bool { + true +} +fn default_show_line_numbers() -> bool { + false +} +fn default_word_wrap() -> bool { + true +} +fn default_line_side() -> bool { + false +} +fn default_font_family() -> String { + "Proportional".to_string() +} +fn default_font_size() -> f32 { + 14.0 +} impl Default for Config { fn default() -> Self { Self { auto_hide_toolbar: false, - auto_hide_tab_bar: true, + hide_tab_bar: true, show_line_numbers: false, word_wrap: true, theme: Theme::default(), @@ -53,7 +67,9 @@ impl Config { let config_dir = if let Some(config_dir) = dirs::config_dir() { config_dir.join(format!("{}", env!("CARGO_PKG_NAME"))) } else if let Some(home_dir) = dirs::home_dir() { - home_dir.join(".config").join(format!("{}", env!("CARGO_PKG_NAME"))) + home_dir + .join(".config") + .join(format!("{}", env!("CARGO_PKG_NAME"))) } else { return None; }; @@ -92,7 +108,7 @@ impl Config { let default_config = Self::default(); config.merge_with_default(default_config); config - }, + } Err(e) => { eprintln!( "Failed to read config file {}: {}", @@ -108,7 +124,7 @@ impl Config { if self.font_family.is_empty() { self.font_family = default.font_family; } - + if self.font_size <= 0.0 { self.font_size = default.font_size; } diff --git a/src/app/state/app_impl.rs b/src/app/state/app_impl.rs index bb39ec5..c0021e6 100644 --- a/src/app/state/app_impl.rs +++ b/src/app/state/app_impl.rs @@ -24,7 +24,7 @@ impl eframe::App for TextEditor { menu_bar(self, ctx); - if !self.auto_hide_tab_bar { + if !self.hide_tab_bar { tab_bar(self, ctx); } diff --git a/src/app/state/config.rs b/src/app/state/config.rs index 644dd09..815ff13 100644 --- a/src/app/state/config.rs +++ b/src/app/state/config.rs @@ -19,7 +19,7 @@ impl TextEditor { show_line_numbers: config.show_line_numbers, word_wrap: config.word_wrap, auto_hide_toolbar: config.auto_hide_toolbar, - auto_hide_tab_bar: config.auto_hide_tab_bar, + hide_tab_bar: config.hide_tab_bar, theme: config.theme, line_side: config.line_side, font_family: config.font_family, @@ -80,7 +80,7 @@ impl TextEditor { Config { auto_hide_toolbar: self.auto_hide_toolbar, show_line_numbers: self.show_line_numbers, - auto_hide_tab_bar: self.auto_hide_tab_bar, + hide_tab_bar: self.hide_tab_bar, word_wrap: self.word_wrap, theme: self.theme, line_side: self.line_side, diff --git a/src/app/state/default.rs b/src/app/state/default.rs index 54afebc..4370523 100644 --- a/src/app/state/default.rs +++ b/src/app/state/default.rs @@ -19,7 +19,7 @@ impl Default for TextEditor { show_line_numbers: false, word_wrap: true, auto_hide_toolbar: false, - auto_hide_tab_bar: true, + hide_tab_bar: true, theme: Theme::default(), line_side: false, font_family: "Proportional".to_string(), diff --git a/src/app/state/editor.rs b/src/app/state/editor.rs index 0211e2d..a0511aa 100644 --- a/src/app/state/editor.rs +++ b/src/app/state/editor.rs @@ -44,7 +44,7 @@ pub struct TextEditor { pub(crate) show_line_numbers: bool, pub(crate) word_wrap: bool, pub(crate) auto_hide_toolbar: bool, - pub(crate) auto_hide_tab_bar: bool, + pub(crate) hide_tab_bar: bool, pub(crate) theme: Theme, pub(crate) line_side: bool, pub(crate) font_family: String, diff --git a/src/ui/menu_bar.rs b/src/ui/menu_bar.rs index 2ffcd80..ea61237 100644 --- a/src/ui/menu_bar.rs +++ b/src/ui/menu_bar.rs @@ -182,10 +182,7 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) { app.save_config(); ui.close_menu(); } - if ui - .checkbox(&mut app.auto_hide_tab_bar, "Hide Tab Bar") - .clicked() - { + if ui.checkbox(&mut app.hide_tab_bar, "Hide Tab Bar").clicked() { app.save_config(); ui.close_menu(); } @@ -270,7 +267,7 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) { } }); - if app.auto_hide_tab_bar { + if app.hide_tab_bar { let tab_title = if let Some(tab) = app.get_active_tab() { tab.title.clone() } else { -- 2.47.1 From 77eba47f9d186975259f578eb0b7ec2d613e3e02 Mon Sep 17 00:00:00 2001 From: candle Date: Wed, 16 Jul 2025 13:27:31 -0400 Subject: [PATCH 3/8] Updated dependencies, even better and smarter scrolling, find and replace functionality --- README.md | 2 +- src/app/shortcuts.rs | 28 +++++- src/app/state/app_impl.rs | 1 - src/app/state/config.rs | 9 +- src/app/state/default.rs | 10 +- src/app/state/editor.rs | 29 +++--- src/app/state/find.rs | 130 ++++++++++++++++++++++++- src/app/state/lifecycle.rs | 2 +- src/app/state/processing.rs | 92 +++++++++++------ src/app/state/tabs.rs | 9 ++ src/app/state/ui.rs | 3 +- src/app/tab.rs | 3 - src/io.rs | 10 +- src/ui/about_window.rs | 2 + src/ui/central_panel.rs | 48 +++++---- src/ui/central_panel/editor.rs | 79 ++++++++++++--- src/ui/central_panel/find_highlight.rs | 96 +++++++++++------- src/ui/find_window.rs | 103 ++++++++++++++++---- src/ui/menu_bar.rs | 59 ++++++----- src/ui/preferences_window.rs | 2 + src/ui/shortcuts_window.rs | 12 +-- 21 files changed, 547 insertions(+), 182 deletions(-) diff --git a/README.md b/README.md index 15c8e81..10272ef 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ font_size = 16.0 In order of importance. | Feature | Info | | ------- | ---- | -| **Find/Replace:** | In progress. | +| **Find/Replace:** | Functioning. | | **State/Cache:** | A toggleable option to keep an application state and prevent "Quit without saving" warnings. | | **Syntax Highlighting/LSP:** | Looking at allowing you to use/attach your own tools for this. | | **Choose Font** | More than just Monospace/Proportional. | diff --git a/src/app/shortcuts.rs b/src/app/shortcuts.rs index 4319e72..f59d685 100644 --- a/src/app/shortcuts.rs +++ b/src/app/shortcuts.rs @@ -15,6 +15,7 @@ enum ShortcutAction { ToggleWordWrap, ToggleAutoHideToolbar, ToggleFind, + FocusFind, NextTab, PrevTab, PageUp, @@ -55,6 +56,11 @@ fn get_shortcuts() -> Vec { egui::Key::W, ShortcutAction::CloseTab, ), + ( + egui::Modifiers::CTRL | egui::Modifiers::SHIFT, + egui::Key::F, + ShortcutAction::FocusFind, + ), ( egui::Modifiers::CTRL, egui::Key::F, @@ -167,15 +173,12 @@ fn execute_action(action: ShortcutAction, editor: &mut TextEditor, _ctx: &egui:: } ShortcutAction::CloseTab => { if editor.tabs.len() > 1 { - // Check if the current tab has unsaved changes if let Some(current_tab) = editor.get_active_tab() { if current_tab.is_modified { - // Show dialog for unsaved changes editor.pending_unsaved_action = Some( super::state::UnsavedAction::CloseTab(editor.active_tab_index), ); } else { - // Close tab directly if no unsaved changes editor.close_tab(editor.active_tab_index); } } @@ -251,13 +254,25 @@ fn execute_action(action: ShortcutAction, editor: &mut TextEditor, _ctx: &egui:: ShortcutAction::Escape => { editor.show_about = false; editor.show_shortcuts = false; + if editor.show_find { + editor.should_select_current_match = true; + } editor.show_find = false; editor.show_preferences = false; editor.pending_unsaved_action = None; false } ShortcutAction::ToggleFind => { - //editor.show_find = !editor.show_find; + editor.show_find = !editor.show_find; + if editor.show_find && !editor.find_query.is_empty() { + editor.update_find_matches(); + } + false + } + ShortcutAction::FocusFind => { + if editor.show_find { + editor.focus_find = true; + } false } ShortcutAction::Preferences => { @@ -300,4 +315,9 @@ pub fn handle(editor: &mut TextEditor, ctx: &egui::Context) { if global_zoom_occurred { ctx.set_zoom_factor(editor.zoom_factor); } + + if editor.should_select_current_match { + editor.select_current_match(ctx); + editor.should_select_current_match = false; + } } diff --git a/src/app/state/app_impl.rs b/src/app/state/app_impl.rs index c0021e6..441992d 100644 --- a/src/app/state/app_impl.rs +++ b/src/app/state/app_impl.rs @@ -46,7 +46,6 @@ impl eframe::App for TextEditor { self.show_unsaved_changes_dialog(ctx); } - // Update the previous find state for next frame self.prev_show_find = self.show_find; } } diff --git a/src/app/state/config.rs b/src/app/state/config.rs index 815ff13..49bed39 100644 --- a/src/app/state/config.rs +++ b/src/app/state/config.rs @@ -32,21 +32,22 @@ impl TextEditor { text_processing_result: std::sync::Arc::new(std::sync::Mutex::new(Default::default())), _processing_thread_handle: None, find_query: String::new(), + replace_query: String::new(), find_matches: Vec::new(), current_match_index: None, case_sensitive_search: false, + show_replace_section: false, prev_show_find: false, + focus_find: false, // vim_mode: config.vim_mode, - - // Cursor tracking for smart scrolling previous_cursor_position: None, - - // Track previous content for incremental processing previous_content: String::new(), previous_cursor_char_index: None, current_cursor_line: 0, previous_cursor_line: 0, font_settings_changed: false, + text_needs_processing: false, + should_select_current_match: false, } } diff --git a/src/app/state/default.rs b/src/app/state/default.rs index 4370523..63960fa 100644 --- a/src/app/state/default.rs +++ b/src/app/state/default.rs @@ -31,22 +31,22 @@ impl Default for TextEditor { menu_bar_stable_until: None, text_processing_result: Arc::new(Mutex::new(TextProcessingResult::default())), _processing_thread_handle: None, - // Find functionality find_query: String::new(), + replace_query: String::new(), find_matches: Vec::new(), current_match_index: None, case_sensitive_search: false, + show_replace_section: false, prev_show_find: false, - - // Cursor tracking for smart scrolling + focus_find: false, previous_cursor_position: None, - - // Track previous content for incremental processing previous_content: String::new(), previous_cursor_char_index: None, current_cursor_line: 0, previous_cursor_line: 0, font_settings_changed: false, + text_needs_processing: false, + should_select_current_match: false, } } } diff --git a/src/app/state/editor.rs b/src/app/state/editor.rs index a0511aa..ca330f0 100644 --- a/src/app/state/editor.rs +++ b/src/app/state/editor.rs @@ -13,9 +13,9 @@ pub enum UnsavedAction { #[derive(Clone)] pub struct TextProcessingResult { pub line_count: usize, - pub longest_line_index: usize, // Which line is the longest (0-based) - pub longest_line_length: usize, // Character count of the longest line - pub longest_line_pixel_width: f32, // Actual pixel width of the longest line + pub longest_line_index: usize, + pub longest_line_length: usize, + pub longest_line_pixel_width: f32, } impl Default for TextProcessingResult { @@ -33,7 +33,7 @@ impl Default for TextProcessingResult { pub struct TextEditor { pub(crate) tabs: Vec, pub(crate) active_tab_index: usize, - pub(crate) tab_counter: usize, // Counter for numbering new tabs + pub(crate) tab_counter: usize, pub(crate) show_about: bool, pub(crate) show_shortcuts: bool, pub(crate) show_find: bool, @@ -57,18 +57,19 @@ pub struct TextEditor { pub(crate) text_processing_result: Arc>, pub(crate) _processing_thread_handle: Option>, pub(crate) find_query: String, - pub(crate) find_matches: Vec<(usize, usize)>, // (start_pos, end_pos) byte positions + pub(crate) replace_query: String, + pub(crate) find_matches: Vec<(usize, usize)>, pub(crate) current_match_index: Option, pub(crate) case_sensitive_search: bool, - pub(crate) prev_show_find: bool, // Track previous state to detect transitions - - // Cursor tracking for smart scrolling - pub(crate) previous_cursor_position: Option, - - // Track previous content for incremental processing + pub(crate) show_replace_section: bool, + pub(crate) prev_show_find: bool, + pub(crate) focus_find: bool, pub(crate) previous_content: String, pub(crate) previous_cursor_char_index: Option, - pub(crate) current_cursor_line: usize, // Track current line number incrementally - pub(crate) previous_cursor_line: usize, // Track previous line for comparison - pub(crate) font_settings_changed: bool, // Flag to trigger text reprocessing when font changes + pub(crate) current_cursor_line: usize, + pub(crate) previous_cursor_line: usize, + pub(crate) font_settings_changed: bool, + pub(crate) text_needs_processing: bool, + pub(crate) should_select_current_match: bool, + pub(crate) previous_cursor_position: Option, } diff --git a/src/app/state/find.rs b/src/app/state/find.rs index cd80b23..b057577 100644 --- a/src/app/state/find.rs +++ b/src/app/state/find.rs @@ -1,7 +1,9 @@ use super::editor::TextEditor; +use eframe::egui; impl TextEditor { pub fn update_find_matches(&mut self) { + let previous_match_index = self.current_match_index; self.find_matches.clear(); self.current_match_index = None; @@ -32,12 +34,20 @@ impl TextEditor { } if !self.find_matches.is_empty() { - self.current_match_index = Some(0); + if let Some(prev_index) = previous_match_index { + if prev_index < self.find_matches.len() { + self.current_match_index = Some(prev_index); + } else { + self.current_match_index = Some(0); + } + } else { + self.current_match_index = Some(0); + } } } } - pub fn find_next(&mut self) { + pub fn find_next(&mut self, ctx: &egui::Context) { if self.find_matches.is_empty() { return; } @@ -47,9 +57,12 @@ impl TextEditor { } else { self.current_match_index = Some(0); } + + self.select_current_match(ctx); + self.should_select_current_match = true; } - pub fn find_previous(&mut self) { + pub fn find_previous(&mut self, ctx: &egui::Context) { if self.find_matches.is_empty() { return; } @@ -63,6 +76,9 @@ impl TextEditor { } else { self.current_match_index = Some(0); } + + self.select_current_match(ctx); + self.should_select_current_match = true; } pub fn get_current_match_position(&self) -> Option<(usize, usize)> { @@ -72,4 +88,112 @@ impl TextEditor { None } } + + pub fn select_current_match(&self, ctx: &egui::Context) { + if let Some((start_byte, end_byte)) = self.get_current_match_position() { + if let Some(active_tab) = self.get_active_tab() { + let content = &active_tab.content; + + let start_char = content[..start_byte.min(content.len())].chars().count(); + let end_char = content[..end_byte.min(content.len())].chars().count(); + + let text_edit_id = egui::Id::new("main_text_editor"); + if let Some(mut state) = egui::TextEdit::load_state(ctx, text_edit_id) { + let selection_range = egui::text::CCursorRange::two( + egui::text::CCursor::new(start_char), + egui::text::CCursor::new(end_char), + ); + state.cursor.set_char_range(Some(selection_range)); + egui::TextEdit::store_state(ctx, text_edit_id, state); + } + } + } + } + + pub fn replace_current_match(&mut self, ctx: &egui::Context) { + if self.find_query.is_empty() || self.find_matches.is_empty() { + return; + } + + if let Some((start_byte, end_byte)) = self.get_current_match_position() { + let replace_query = self.replace_query.clone(); + let replacement_end = start_byte + replace_query.len(); + + if let Some(active_tab) = self.get_active_tab_mut() { + let content = &active_tab.content; + + let mut new_content = content.clone(); + new_content.replace_range(start_byte..end_byte, &replace_query); + + active_tab.content = new_content; + active_tab.is_modified = true; + } + + self.update_find_matches(); + + if let Some(active_tab) = self.get_active_tab() { + let replacement_end_char = active_tab.content + [..replacement_end.min(active_tab.content.len())] + .chars() + .count(); + + let text_edit_id = egui::Id::new("main_text_editor"); + if let Some(mut state) = egui::TextEdit::load_state(ctx, text_edit_id) { + state + .cursor + .set_char_range(Some(egui::text::CCursorRange::one( + egui::text::CCursor::new(replacement_end_char), + ))); + egui::TextEdit::store_state(ctx, text_edit_id, state); + } + } + } + } + + pub fn replace_all(&mut self, ctx: &egui::Context) { + if self.find_query.is_empty() || self.find_matches.is_empty() { + return; + } + + let find_query = self.find_query.clone(); + let replace_query = self.replace_query.clone(); + let case_sensitive = self.case_sensitive_search; + let find_matches = self.find_matches.clone(); + + if let Some(active_tab) = self.get_active_tab_mut() { + let content = &active_tab.content; + + let new_content = if case_sensitive { + content.replace(&find_query, &replace_query) + } else { + let mut result = String::new(); + let mut last_end = 0; + + for (start_byte, end_byte) in &find_matches { + result.push_str(&content[last_end..*start_byte]); + result.push_str(&replace_query); + last_end = *end_byte; + } + result.push_str(&content[last_end..]); + result + }; + + active_tab.content = new_content; + active_tab.is_modified = true; + } + + self.update_find_matches(); + + self.current_match_index = None; + + let text_edit_id = egui::Id::new("main_text_editor"); + if let Some(mut state) = egui::TextEdit::load_state(ctx, text_edit_id) { + state + .cursor + .set_char_range(Some(egui::text::CCursorRange::one( + egui::text::CCursor::new(0), + ))); + egui::TextEdit::store_state(ctx, text_edit_id, state); + } + } } diff --git a/src/app/state/lifecycle.rs b/src/app/state/lifecycle.rs index 95f597f..a366228 100644 --- a/src/app/state/lifecycle.rs +++ b/src/app/state/lifecycle.rs @@ -57,7 +57,7 @@ impl TextEditor { } } } else { - return; // Should not happen if called correctly + return; }; let visuals = &ctx.style().visuals; diff --git a/src/app/state/processing.rs b/src/app/state/processing.rs index 3f6e54f..b716553 100644 --- a/src/app/state/processing.rs +++ b/src/app/state/processing.rs @@ -2,12 +2,12 @@ use super::editor::{TextEditor, TextProcessingResult}; use eframe::egui; impl TextEditor { - /// Process text content and find the longest line (only used for initial scan) pub fn process_text_for_rendering(&mut self, content: &str, ui: &egui::Ui) { - let lines: Vec<&str> = content.lines().collect(); - let line_count = lines.len().max(1); + let line_count = content.bytes().filter(|&b| b == b'\n').count() + 1; - if lines.is_empty() { + let lines: Vec<&str> = content.lines().collect(); + + if content.is_empty() { self.update_processing_result(TextProcessingResult { line_count: 1, longest_line_index: 0, @@ -20,6 +20,16 @@ impl TextEditor { let mut longest_line_index = 0; let mut longest_line_length = 0; + if lines.is_empty() { + self.update_processing_result(TextProcessingResult { + line_count, + longest_line_index: 0, + longest_line_length: 0, + longest_line_pixel_width: 0.0, + }); + return; + } + for (index, line) in lines.iter().enumerate() { let char_count = line.chars().count(); if char_count > longest_line_length { @@ -56,7 +66,6 @@ impl TextEditor { self.update_processing_result(result); } - /// Efficiently detect and process line changes without full content iteration pub fn process_incremental_change( &mut self, old_content: &str, @@ -103,7 +112,6 @@ impl TextEditor { self.previous_cursor_line = self.current_cursor_line; } - /// Calculate the change in cursor line without full iteration fn calculate_cursor_line_change( &self, old_content: &str, @@ -124,7 +132,6 @@ impl TextEditor { new_newlines as isize - old_newlines as isize } - /// Handle character replacement (same length change) fn handle_character_replacement( &mut self, _old_content: &str, @@ -144,7 +151,6 @@ impl TextEditor { ); } - /// Handle content addition fn handle_content_addition( &mut self, old_content: &str, @@ -183,21 +189,39 @@ impl TextEditor { if newlines_added > 0 { let mut current_result = self.get_text_processing_result(); current_result.line_count += newlines_added; - self.update_processing_result(current_result); + + let addition_start_line = old_content[..added_start] + .bytes() + .filter(|&b| b == b'\n') + .count(); + let addition_end_line = old_content[..added_end.min(old_content.len())] + .bytes() + .filter(|&b| b == b'\n') + .count(); + + if current_result.longest_line_index >= addition_start_line + && current_result.longest_line_index <= addition_end_line + { + self.process_text_for_rendering(new_content, ui); + } else { + if addition_end_line < current_result.longest_line_index { + current_result.longest_line_index += newlines_added; + } + self.update_processing_result(current_result); + } + } else { + let current_line = self.extract_current_line(new_content, new_cursor_pos); + let current_line_length = current_line.chars().count(); + + self.update_line_if_longer( + self.current_cursor_line, + ¤t_line, + current_line_length, + ui, + ); } - - let current_line = self.extract_current_line(new_content, new_cursor_pos); - let current_line_length = current_line.chars().count(); - - self.update_line_if_longer( - self.current_cursor_line, - ¤t_line, - current_line_length, - ui, - ); } - /// Handle content removal fn handle_content_removal( &mut self, old_content: &str, @@ -237,11 +261,27 @@ impl TextEditor { let mut current_result = self.get_text_processing_result(); current_result.line_count = current_result.line_count.saturating_sub(newlines_removed); - if self.current_cursor_line <= current_result.longest_line_index { - self.process_text_for_rendering(new_content, ui); - } + let removal_start_line = old_content[..removed_start] + .bytes() + .filter(|&b| b == b'\n') + .count(); + let removal_end_line = old_content[..removed_end] + .bytes() + .filter(|&b| b == b'\n') + .count(); - self.update_processing_result(current_result); + if current_result.longest_line_index >= removal_start_line + && current_result.longest_line_index <= removal_end_line + { + self.process_text_for_rendering(new_content, ui); + } else { + if removal_end_line < current_result.longest_line_index { + current_result.longest_line_index = current_result + .longest_line_index + .saturating_sub(newlines_removed); + } + self.update_processing_result(current_result); + } } let current_line = self.extract_current_line(new_content, new_cursor_pos); @@ -262,7 +302,6 @@ impl TextEditor { } } - /// Extract the current line efficiently without full content scan fn extract_current_line(&self, content: &str, cursor_pos: usize) -> String { let bytes = content.as_bytes(); @@ -279,7 +318,6 @@ impl TextEditor { content[line_start..line_end].to_string() } - /// Update longest line info if the current line is longer fn update_line_if_longer( &mut self, line_index: usize, @@ -314,7 +352,6 @@ impl TextEditor { } } - /// Get the current text processing result pub fn get_text_processing_result(&self) -> TextProcessingResult { self.text_processing_result .lock() @@ -322,7 +359,6 @@ impl TextEditor { .unwrap_or_default() } - /// Update the processing result atomically fn update_processing_result(&self, result: TextProcessingResult) { if let Ok(mut processing_result) = self.text_processing_result.lock() { *processing_result = result; diff --git a/src/app/state/tabs.rs b/src/app/state/tabs.rs index 8e1ee75..f87efb6 100644 --- a/src/app/state/tabs.rs +++ b/src/app/state/tabs.rs @@ -14,6 +14,9 @@ impl TextEditor { self.tab_counter += 1; self.tabs.push(Tab::new_empty(self.tab_counter)); self.active_tab_index = self.tabs.len() - 1; + if self.show_find && !self.find_query.is_empty() { + self.update_find_matches(); + } } pub fn close_tab(&mut self, tab_index: usize) { @@ -24,12 +27,18 @@ impl TextEditor { } else if self.active_tab_index > tab_index { self.active_tab_index -= 1; } + if self.show_find && !self.find_query.is_empty() { + self.update_find_matches(); + } } } pub fn switch_to_tab(&mut self, tab_index: usize) { if tab_index < self.tabs.len() { self.active_tab_index = tab_index; + if self.show_find && !self.find_query.is_empty() { + self.update_find_matches(); + } } } } diff --git a/src/app/state/ui.rs b/src/app/state/ui.rs index f989864..c436e1f 100644 --- a/src/app/state/ui.rs +++ b/src/app/state/ui.rs @@ -125,7 +125,8 @@ impl TextEditor { return self.calculate_editor_dimensions(ui).text_width; } - let longest_line_width = processing_result.longest_line_pixel_width + (self.font_size * 2.0); + let longest_line_width = + processing_result.longest_line_pixel_width + (self.font_size * 2.0); let dimensions = self.calculate_editor_dimensions(ui); longest_line_width.max(dimensions.text_width) diff --git a/src/app/tab.rs b/src/app/tab.rs index 4e88e83..2e34888 100644 --- a/src/app/tab.rs +++ b/src/app/tab.rs @@ -60,8 +60,6 @@ impl Tab { } pub fn update_modified_state(&mut self) { - // Compare current content hash with original content hash to determine if modified - // Special case: new_X tabs are only considered modified if they have content if self.title.starts_with("new_") { self.is_modified = !self.content.is_empty(); } else { @@ -72,7 +70,6 @@ impl Tab { } pub fn mark_as_saved(&mut self) { - // Update the original content hash to match current content after saving self.original_content_hash = compute_content_hash(&self.content, &mut self.hasher); self.last_content_hash = self.original_content_hash; self.is_modified = false; diff --git a/src/io.rs b/src/io.rs index 94a5b25..6f490a3 100644 --- a/src/io.rs +++ b/src/io.rs @@ -14,7 +14,6 @@ pub(crate) fn open_file(app: &mut TextEditor) { { match fs::read_to_string(&path) { Ok(content) => { - // Check if the current active tab is empty/clean and can be replaced let should_replace_current_tab = if let Some(active_tab) = app.get_active_tab() { active_tab.file_path.is_none() && active_tab.content.is_empty() @@ -24,7 +23,6 @@ pub(crate) fn open_file(app: &mut TextEditor) { }; if should_replace_current_tab { - // Replace the current empty tab if let Some(active_tab) = app.get_active_tab_mut() { active_tab.content = content; active_tab.file_path = Some(path.clone()); @@ -33,13 +31,17 @@ pub(crate) fn open_file(app: &mut TextEditor) { .and_then(|n| n.to_str()) .unwrap_or("Untitled") .to_string(); - active_tab.mark_as_saved(); // This will set the hash and mark as not modified + active_tab.mark_as_saved(); } + app.text_needs_processing = true; } else { - // Create a new tab as before let new_tab = Tab::new_with_file(content, path); app.tabs.push(new_tab); app.active_tab_index = app.tabs.len() - 1; + app.text_needs_processing = true; + } + if app.show_find && !app.find_query.is_empty() { + app.update_find_matches(); } } Err(err) => { diff --git a/src/ui/about_window.rs b/src/ui/about_window.rs index 1e573ca..18f2008 100644 --- a/src/ui/about_window.rs +++ b/src/ui/about_window.rs @@ -11,6 +11,8 @@ pub(crate) fn about_window(app: &mut TextEditor, ctx: &egui::Context) { .collapsible(false) .resizable(false) .anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO) + .fade_in(true) + .fade_out(true) .frame(egui::Frame { fill: visuals.window_fill, stroke: visuals.window_stroke, diff --git a/src/ui/central_panel.rs b/src/ui/central_panel.rs index 6233551..5ebab4e 100644 --- a/src/ui/central_panel.rs +++ b/src/ui/central_panel.rs @@ -4,6 +4,7 @@ mod line_numbers; use crate::app::TextEditor; use eframe::egui; +use egui::UiKind; use self::editor::editor_view_ui; use self::line_numbers::{get_visual_line_mapping, render_line_numbers}; @@ -35,7 +36,7 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) { editor_view_ui(ui, app); }); - show_context_menu(ui, app, &context_response); + handle_empty(ui, app, &context_response); }); return; } @@ -75,7 +76,7 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) { ui.add_space(3.0); let separator_x = ui.cursor().left(); let mut y_range = ui.available_rect_before_wrap().y_range(); - y_range.max += 2.0 * font_size; // Extend separator to cover more vertical space + y_range.max += 2.0 * font_size; ui.painter() .vline(separator_x, y_range, ui.visuals().window_stroke); ui.add_space(4.0); @@ -85,26 +86,22 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) { .auto_shrink([false; 2]) .show(ui, |ui| { if line_side { - // Line numbers on the right let text_editor_width = editor_dimensions.text_width + editor_dimensions.total_reserved_width; ui.allocate_ui_with_layout( egui::vec2(text_editor_width, editor_height), egui::Layout::left_to_right(egui::Align::TOP), |ui| { - // Constrain editor to specific width to leave space for line numbers ui.allocate_ui_with_layout( egui::vec2(editor_dimensions.text_width, editor_height), egui::Layout::left_to_right(egui::Align::TOP), |ui| { - // Create an invisible interaction area for context menu let full_rect = ui.available_rect_before_wrap(); let context_response = ui.allocate_response( full_rect.size(), egui::Sense::click(), ); - // Reset cursor to render editor at the top ui.scope_builder( egui::UiBuilder::new().max_rect(full_rect), |ui| { @@ -112,7 +109,7 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) { }, ); - show_context_menu(ui, app, &context_response); + handle_empty(ui, app, &context_response); }, ); separator_widget(ui); @@ -120,7 +117,6 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) { }, ); } else { - // Line numbers on the left let text_editor_width = editor_dimensions.text_width + editor_dimensions.total_reserved_width; ui.allocate_ui_with_layout( @@ -130,12 +126,10 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) { line_numbers_widget(ui); separator_widget(ui); - // Create an invisible interaction area for context menu let editor_area = ui.available_rect_before_wrap(); let context_response = ui.allocate_response(editor_area.size(), egui::Sense::click()); - // Reset cursor to render editor at the current position ui.scope_builder( egui::UiBuilder::new().max_rect(editor_area), |ui| { @@ -143,7 +137,7 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) { }, ); - show_context_menu(ui, app, &context_response); + handle_empty(ui, app, &context_response); }, ); } @@ -151,7 +145,25 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) { }); } -fn show_context_menu(_ui: &mut egui::Ui, app: &mut TextEditor, context_response: &egui::Response) { +fn handle_empty(_ui: &mut egui::Ui, app: &mut TextEditor, context_response: &egui::Response) { + if context_response.clicked() { + let text_edit_id = egui::Id::new("main_text_editor"); + if let Some(mut state) = egui::TextEdit::load_state(_ui.ctx(), text_edit_id) { + if let Some(active_tab) = app.get_active_tab() { + let text_len = active_tab.content.len(); + let cursor_pos = egui::text::CCursor::new(text_len); + state + .cursor + .set_char_range(Some(egui::text::CCursorRange::one(cursor_pos))); + egui::TextEdit::store_state(_ui.ctx(), text_edit_id, state); + + _ui.ctx().memory_mut(|mem| { + mem.request_focus(text_edit_id); + }); + } + } + } + context_response.context_menu(|ui| { let text_len = app.get_active_tab().unwrap().content.len(); let reset_zoom_key = egui::Id::new("editor_reset_zoom"); @@ -159,17 +171,17 @@ fn show_context_menu(_ui: &mut egui::Ui, app: &mut TextEditor, context_response: if ui.button("Cut").clicked() { ui.ctx() .send_viewport_cmd(egui::ViewportCommand::RequestCut); - ui.close_menu(); + ui.close_kind(UiKind::Menu); } if ui.button("Copy").clicked() { ui.ctx() .send_viewport_cmd(egui::ViewportCommand::RequestCopy); - ui.close_menu(); + ui.close_kind(UiKind::Menu); } if ui.button("Paste").clicked() { ui.ctx() .send_viewport_cmd(egui::ViewportCommand::RequestPaste); - ui.close_menu(); + ui.close_kind(UiKind::Menu); } if ui.button("Delete").clicked() { ui.ctx().input_mut(|i| { @@ -181,7 +193,7 @@ fn show_context_menu(_ui: &mut egui::Ui, app: &mut TextEditor, context_response: modifiers: egui::Modifiers::NONE, }) }); - ui.close_menu(); + ui.close_kind(UiKind::Menu); } if ui.button("Select All").clicked() { let text_edit_id = egui::Id::new("main_text_editor"); @@ -193,14 +205,14 @@ fn show_context_menu(_ui: &mut egui::Ui, app: &mut TextEditor, context_response: state.cursor.set_char_range(Some(select_all_range)); egui::TextEdit::store_state(ui.ctx(), text_edit_id, state); } - ui.close_menu(); + ui.close_kind(UiKind::Menu); } ui.separator(); if ui.button("Reset Zoom").clicked() { ui.ctx().memory_mut(|mem| { mem.data.insert_temp(reset_zoom_key, true); }); - ui.close_menu(); + ui.close_kind(UiKind::Menu); } }); } diff --git a/src/ui/central_panel/editor.rs b/src/ui/central_panel/editor.rs index 9a493c6..64069a8 100644 --- a/src/ui/central_panel/editor.rs +++ b/src/ui/central_panel/editor.rs @@ -1,6 +1,8 @@ use crate::app::TextEditor; use eframe::egui; +use super::find_highlight; + pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::Response { let _current_match_position = app.get_current_match_position(); let show_find = app.show_find; @@ -30,6 +32,18 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R 0.0 }; + let find_data = if show_find && !app.find_matches.is_empty() { + app.get_active_tab().map(|tab| { + ( + tab.content.clone(), + app.find_matches.clone(), + app.current_match_index, + ) + }) + } else { + None + }; + let Some(active_tab) = app.get_active_tab_mut() else { return ui.label("No file open, how did you get here?"); }; @@ -38,6 +52,44 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R let editor_rect = ui.available_rect_before_wrap(); ui.painter().rect_filled(editor_rect, 0.0, bg_color); + if let Some((content, matches, current_match_index)) = &find_data { + let font_id = ui + .style() + .text_styles + .get(&egui::TextStyle::Monospace) + .unwrap_or(&egui::FontId::monospace(font_size)) + .clone(); + + let desired_width = if word_wrap { + ui.available_width() + } else { + f32::INFINITY + }; + + let temp_galley = ui.fonts(|fonts| { + fonts.layout( + content.clone(), + font_id.clone(), + ui.visuals().text_color(), + desired_width, + ) + }); + + let text_area_left = editor_rect.left() + 4.0; + let text_area_top = editor_rect.top() + 2.0; + + find_highlight::draw_find_highlights( + ui, + content, + matches, + *current_match_index, + &temp_galley, + text_area_left, + text_area_top, + font_size, + ); + } + let desired_width = if word_wrap { ui.available_width() } else { @@ -50,7 +102,7 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R .code_editor() .desired_width(desired_width) .desired_rows(0) - .lock_focus(true) + .lock_focus(!show_find) .cursor_at_end(false) .id(egui::Id::new("main_text_editor")); @@ -78,6 +130,10 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R None }; + if content_changed && app.show_find && !app.find_query.is_empty() { + app.update_find_matches(); + } + let current_cursor_pos = output .state .cursor @@ -99,16 +155,9 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R curr_cursor_pos, ui, ); - } else { - app.process_text_for_rendering(&content, ui); - - if let Some(cursor_pos) = current_cursor_pos { - app.current_cursor_line = content[..cursor_pos] - .bytes() - .filter(|&b| b == b'\n') - .count(); - } } + } else { + app.process_text_for_rendering(&content, ui); } app.previous_content = content.clone(); @@ -120,7 +169,6 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R } } - // Check if font settings changed and trigger reprocessing if app.font_settings_changed { if let Some(active_tab) = app.get_active_tab() { let content = active_tab.content.clone(); @@ -131,6 +179,14 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R app.font_settings_changed = false; } + if app.text_needs_processing { + if let Some(active_tab) = app.get_active_tab() { + let content = active_tab.content.clone(); + app.process_text_for_rendering(&content, ui); + } + app.text_needs_processing = false; + } + if !word_wrap { if let Some(cursor_pos) = current_cursor_pos { let cursor_moved = Some(cursor_pos) != app.previous_cursor_position; @@ -170,7 +226,6 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R } } - // Request focus if no dialogs are open if !output.response.has_focus() && !show_preferences && !show_about diff --git a/src/ui/central_panel/find_highlight.rs b/src/ui/central_panel/find_highlight.rs index 836e140..f0082ea 100644 --- a/src/ui/central_panel/find_highlight.rs +++ b/src/ui/central_panel/find_highlight.rs @@ -1,11 +1,13 @@ use eframe::egui; -pub(super) fn _draw_find_highlight( +pub(super) fn draw_find_highlights( ui: &mut egui::Ui, content: &str, - start_pos: usize, - end_pos: usize, - editor_rect: egui::Rect, + matches: &[(usize, usize)], + current_match_index: Option, + galley: &std::sync::Arc, + text_area_left: f32, + text_area_top: f32, font_size: f32, ) { let font_id = ui @@ -15,10 +17,40 @@ pub(super) fn _draw_find_highlight( .unwrap_or(&egui::FontId::monospace(font_size)) .clone(); - let text_up_to_start = &content[..start_pos.min(content.len())]; + for (match_index, &(start_pos, end_pos)) in matches.iter().enumerate() { + let is_current_match = current_match_index == Some(match_index); + draw_single_highlight( + ui, + content, + start_pos, + end_pos, + text_area_left, + text_area_top, + galley, + &font_id, + is_current_match, + ); + } +} +fn draw_single_highlight( + ui: &mut egui::Ui, + content: &str, + start_pos: usize, + end_pos: usize, + text_area_left: f32, + text_area_top: f32, + galley: &std::sync::Arc, + font_id: &egui::FontId, + is_current_match: bool, +) { + let text_up_to_start = &content[..start_pos.min(content.len())]; let start_line = text_up_to_start.chars().filter(|&c| c == '\n').count(); + if start_line >= galley.rows.len() { + return; + } + let line_start_byte_pos = text_up_to_start.rfind('\n').map(|pos| pos + 1).unwrap_or(0); let line_start_char_pos = content[..line_start_byte_pos].chars().count(); let start_char_pos = content[..start_pos].chars().count(); @@ -32,13 +64,6 @@ pub(super) fn _draw_find_highlight( let line_text = lines[start_line]; let text_before_match: String = line_text.chars().take(start_col).collect(); - let line_height = ui.fonts(|fonts| fonts.row_height(&font_id)); - - let horizontal_margin = ui.spacing().button_padding.x - 4.0; - let vertical_margin = ui.spacing().button_padding.y - 1.0; - let text_area_left = editor_rect.left() + horizontal_margin; - let text_area_top = editor_rect.top() + vertical_margin; - let text_before_width = ui.fonts(|fonts| { fonts .layout( @@ -51,30 +76,35 @@ pub(super) fn _draw_find_highlight( .x }); - let start_y = text_area_top + (start_line as f32 * line_height); + let galley_row = &galley.rows[start_line]; + let start_y = text_area_top + galley_row.min_y(); + let line_height = galley_row.height(); let start_x = text_area_left + text_before_width; - { - let match_text = &content[start_pos..end_pos.min(content.len())]; + let match_text = &content[start_pos..end_pos.min(content.len())]; + let match_width = ui.fonts(|fonts| { + fonts + .layout( + match_text.to_string(), + font_id.clone(), + ui.visuals().text_color(), + f32::INFINITY, + ) + .size() + .x + }); - let match_width = ui.fonts(|fonts| { - fonts - .layout( - match_text.to_string(), - font_id.clone(), - ui.visuals().text_color(), - f32::INFINITY, - ) - .size() - .x - }); + let highlight_rect = egui::Rect::from_min_size( + egui::pos2(start_x, start_y), + egui::vec2(match_width, line_height), + ); - let highlight_rect = egui::Rect::from_min_size( - egui::pos2(start_x, start_y), - egui::vec2(match_width, line_height), - ); + let highlight_color = if is_current_match { + ui.visuals().selection.bg_fill + } else { + ui.visuals().selection.bg_fill.gamma_multiply(0.6) + }; - ui.painter() - .rect_filled(highlight_rect, 0.0, ui.visuals().selection.bg_fill); - } + let painter = ui.painter(); + painter.rect_filled(highlight_rect, 0.0, highlight_color); } diff --git a/src/ui/find_window.rs b/src/ui/find_window.rs index 5dfdec7..7fed489 100644 --- a/src/ui/find_window.rs +++ b/src/ui/find_window.rs @@ -6,12 +6,34 @@ pub(crate) fn find_window(app: &mut TextEditor, ctx: &egui::Context) { let mut should_close = false; let mut query_changed = false; + let mut should_focus_editor = false; - egui::Window::new("Find") + let just_opened = app.show_find && !app.prev_show_find; + + if just_opened && !app.find_query.is_empty() { + app.update_find_matches(); + if app.current_match_index.is_some() { + app.select_current_match(ctx); + app.should_select_current_match = true; + } + } + + let focus_requested = ctx.memory(|mem| { + mem.data + .get_temp::(egui::Id::new("focus_find_input")) + .unwrap_or(false) + }); + + let top_right_pos = egui::Pos2::new(ctx.available_rect().right(), 22.0); + + egui::Window::new("") .collapsible(false) .resizable(false) .movable(true) - .anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO) + .title_bar(false) + .default_pos(top_right_pos) + .fade_in(true) + .fade_out(true) .frame(egui::Frame { fill: visuals.window_fill, stroke: visuals.window_stroke, @@ -22,13 +44,20 @@ pub(crate) fn find_window(app: &mut TextEditor, ctx: &egui::Context) { }) .show(ctx, |ui| { ui.vertical(|ui| { - ui.set_min_width(300.0); - ui.horizontal(|ui| { + let arrow_text = if app.show_replace_section { + "⏷" + } else { + "⏵" + }; + if ui.button(arrow_text).clicked() { + app.show_replace_section = !app.show_replace_section; + } + ui.label("Find:"); let response = ui.add( egui::TextEdit::singleline(&mut app.find_query) - .desired_width(200.0) + .desired_width(250.0) .hint_text("Enter search text..."), ); @@ -36,16 +65,29 @@ pub(crate) fn find_window(app: &mut TextEditor, ctx: &egui::Context) { query_changed = true; } - if !response.has_focus() { + if just_opened || focus_requested || app.focus_find { response.request_focus(); + app.focus_find = false; } if response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) { - app.find_next(); + app.find_next(ctx); response.request_focus(); } }); + if app.show_replace_section { + ui.horizontal(|ui| { + ui.add_space(4.0); + ui.label("Replace:"); + let _replace_response = ui.add( + egui::TextEdit::singleline(&mut app.replace_query) + .desired_width(250.0) + .hint_text("Enter replacement text..."), + ); + }); + } + ui.add_space(8.0); ui.horizontal(|ui| { @@ -55,6 +97,24 @@ pub(crate) fn find_window(app: &mut TextEditor, ctx: &egui::Context) { if case_sensitive_changed { query_changed = true; } + if app.show_replace_section { + ui.add_space(8.0); + + let replace_current_enabled = + !app.find_matches.is_empty() && app.current_match_index.is_some(); + ui.add_enabled_ui(replace_current_enabled, |ui| { + if ui.button("Replace").clicked() { + app.replace_current_match(ctx); + } + }); + + let replace_all_enabled = !app.find_matches.is_empty(); + ui.add_enabled_ui(replace_all_enabled, |ui| { + if ui.button("Replace All").clicked() { + app.replace_all(ctx); + } + }); + } }); ui.add_space(8.0); @@ -75,7 +135,7 @@ pub(crate) fn find_window(app: &mut TextEditor, ctx: &egui::Context) { ui.label(egui::RichText::new(match_text).weak()); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - if ui.button("✕").clicked() { + if ui.button("❌").clicked() { should_close = true; } @@ -84,14 +144,14 @@ pub(crate) fn find_window(app: &mut TextEditor, ctx: &egui::Context) { let next_enabled = !app.find_matches.is_empty(); ui.add_enabled_ui(next_enabled, |ui| { if ui.button("Next").clicked() { - app.find_next(); + app.find_next(ctx); } }); let prev_enabled = !app.find_matches.is_empty(); ui.add_enabled_ui(prev_enabled, |ui| { if ui.button("Previous").clicked() { - app.find_previous(); + app.find_previous(ctx); } }); }); @@ -101,21 +161,28 @@ pub(crate) fn find_window(app: &mut TextEditor, ctx: &egui::Context) { if query_changed { app.update_find_matches(); + if app.current_match_index.is_some() { + app.select_current_match(ctx); + app.should_select_current_match = true; + } } if should_close { + app.select_current_match(ctx); + app.should_select_current_match = true; app.show_find = false; } ctx.input(|i| { - if i.key_pressed(egui::Key::Escape) { - app.show_find = false; - } else if i.key_pressed(egui::Key::F3) { - if i.modifiers.shift { - app.find_previous(); - } else { - app.find_next(); - } + if i.key_pressed(egui::Key::Enter) && i.modifiers.ctrl && app.show_find { + should_focus_editor = true; + app.should_select_current_match = true; } }); + + if should_focus_editor { + ctx.memory_mut(|mem| { + mem.request_focus(egui::Id::new("main_text_editor")); + }); + } } diff --git a/src/ui/menu_bar.rs b/src/ui/menu_bar.rs index ea61237..552c673 100644 --- a/src/ui/menu_bar.rs +++ b/src/ui/menu_bar.rs @@ -1,5 +1,6 @@ use crate::{app::TextEditor, io}; use eframe::egui::{self, Frame}; +use egui::UiKind; pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) { let now = std::time::Instant::now(); @@ -43,34 +44,34 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) { } } - egui::menu::bar(ui, |ui| { + egui::MenuBar::new().ui(ui, |ui| { ui.menu_button("File", |ui| { app.menu_interaction_active = true; if ui.button("New").clicked() { io::new_file(app); - ui.close_menu(); + ui.close_kind(UiKind::Menu); } if ui.button("Open...").clicked() { io::open_file(app); - ui.close_menu(); + ui.close_kind(UiKind::Menu); } ui.separator(); if ui.button("Save").clicked() { io::save_file(app); - ui.close_menu(); + ui.close_kind(UiKind::Menu); } if ui.button("Save As...").clicked() { io::save_as_file(app); - ui.close_menu(); + ui.close_kind(UiKind::Menu); } ui.separator(); if ui.button("Preferences").clicked() { app.show_preferences = true; - ui.close_menu(); + ui.close_kind(UiKind::Menu); } if ui.button("Exit").clicked() { app.request_quit(ctx); - ui.close_menu(); + ui.close_kind(UiKind::Menu); } }); @@ -78,16 +79,16 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) { app.menu_interaction_active = true; if ui.button("Cut").clicked() { ui.ctx().input_mut(|i| i.events.push(egui::Event::Cut)); - ui.close_menu(); + ui.close_kind(UiKind::Menu); } if ui.button("Copy").clicked() { ui.ctx().input_mut(|i| i.events.push(egui::Event::Copy)); - ui.close_menu(); + ui.close_kind(UiKind::Menu); } if ui.button("Paste").clicked() { ui.ctx() .send_viewport_cmd(egui::ViewportCommand::RequestPaste); - ui.close_menu(); + ui.close_kind(UiKind::Menu); } if ui.button("Delete").clicked() { ui.ctx().input_mut(|i| { @@ -99,7 +100,7 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) { modifiers: egui::Modifiers::NONE, }) }); - ui.close_menu(); + ui.close_kind(UiKind::Menu); } if ui.button("Select All").clicked() { let text_edit_id = egui::Id::new("main_text_editor"); @@ -116,7 +117,7 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) { egui::TextEdit::store_state(ui.ctx(), text_edit_id, state); } } - ui.close_menu(); + ui.close_kind(UiKind::Menu); } ui.separator(); if ui.button("Undo").clicked() { @@ -138,10 +139,13 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) { state.set_undoer(undoer); egui::TextEdit::store_state(ui.ctx(), text_edit_id, state); active_tab.update_modified_state(); + if app.show_find && !app.find_query.is_empty() { + app.update_find_matches(); + } } } } - ui.close_menu(); + ui.close_kind(UiKind::Menu); } if ui.button("Redo").clicked() { let text_edit_id = egui::Id::new("main_text_editor"); @@ -162,10 +166,13 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) { state.set_undoer(undoer); egui::TextEdit::store_state(ui.ctx(), text_edit_id, state); active_tab.update_modified_state(); + if app.show_find && !app.find_query.is_empty() { + app.update_find_matches(); + } } } } - ui.close_menu(); + ui.close_kind(UiKind::Menu); } }); @@ -176,22 +183,22 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) { .clicked() { app.save_config(); - ui.close_menu(); + ui.close_kind(UiKind::Menu); } if ui.checkbox(&mut app.word_wrap, "Word Wrap").clicked() { app.save_config(); - ui.close_menu(); + ui.close_kind(UiKind::Menu); } if ui.checkbox(&mut app.hide_tab_bar, "Hide Tab Bar").clicked() { app.save_config(); - ui.close_menu(); + ui.close_kind(UiKind::Menu); } if ui .checkbox(&mut app.auto_hide_toolbar, "Auto Hide Toolbar") .clicked() { app.save_config(); - ui.close_menu(); + ui.close_kind(UiKind::Menu); } ui.separator(); @@ -199,7 +206,7 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) { if ui.button("Reset Zoom").clicked() { app.zoom_factor = 1.0; ctx.set_zoom_factor(1.0); - ui.close_menu(); + ui.close_kind(UiKind::Menu); } ui.separator(); @@ -219,7 +226,7 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) { if current_theme != crate::app::theme::Theme::System { app.set_theme(ctx); } - ui.close_menu(); + ui.close_kind(UiKind::Menu); } if ui .radio_value( @@ -232,7 +239,7 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) { if current_theme != crate::app::theme::Theme::Light { app.set_theme(ctx); } - ui.close_menu(); + ui.close_kind(UiKind::Menu); } if ui .radio_value(&mut app.theme, crate::app::theme::Theme::Dark, "Dark") @@ -241,16 +248,16 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) { if current_theme != crate::app::theme::Theme::Dark { app.set_theme(ctx); } - ui.close_menu(); + ui.close_kind(UiKind::Menu); } ui.separator(); if ui.radio_value(&mut app.line_side, false, "Left").clicked() { app.save_config(); - ui.close_menu(); + ui.close_kind(UiKind::Menu); } if ui.radio_value(&mut app.line_side, true, "Right").clicked() { app.save_config(); - ui.close_menu(); + ui.close_kind(UiKind::Menu); } }); }); @@ -259,11 +266,11 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) { app.menu_interaction_active = true; if ui.button("Shortcuts").clicked() { app.show_shortcuts = true; - ui.close_menu(); + ui.close_kind(UiKind::Menu); } if ui.button("About").clicked() { app.show_about = true; - ui.close_menu(); + ui.close_kind(UiKind::Menu); } }); diff --git a/src/ui/preferences_window.rs b/src/ui/preferences_window.rs index ccad264..993ff6a 100644 --- a/src/ui/preferences_window.rs +++ b/src/ui/preferences_window.rs @@ -14,6 +14,8 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) { .anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO) .default_open(true) .max_size(max_size) + .fade_in(true) + .fade_out(true) .frame(egui::Frame { fill: visuals.window_fill, stroke: visuals.window_stroke, diff --git a/src/ui/shortcuts_window.rs b/src/ui/shortcuts_window.rs index 74d2b85..a3ca874 100644 --- a/src/ui/shortcuts_window.rs +++ b/src/ui/shortcuts_window.rs @@ -22,6 +22,7 @@ fn render_shortcuts_content(ui: &mut egui::Ui) { ui.label(egui::RichText::new("Ctrl + V: Paste").size(14.0)); ui.label(egui::RichText::new("Ctrl + A: Select All").size(14.0)); ui.label(egui::RichText::new("Ctrl + D: Delete Line").size(14.0)); + ui.label(egui::RichText::new("Ctrl + F: Find").size(14.0)); ui.add_space(16.0); ui.separator(); @@ -33,7 +34,6 @@ fn render_shortcuts_content(ui: &mut egui::Ui) { ui.label(egui::RichText::new("Ctrl + P: Preferences").size(14.0)); ui.label(egui::RichText::new("Ctrl + =/-: Increase/Decrease Font Size").size(14.0)); ui.label(egui::RichText::new("Ctrl + Shift + =/-: Zoom In/Out").size(14.0)); - // ui.label( // egui::RichText::new("Ctrl + Shift + .: Toggle Vim Mode") // .size(14.0) @@ -42,7 +42,8 @@ fn render_shortcuts_content(ui: &mut egui::Ui) { // egui::RichText::new("Ctrl + .: Toggle Vim Mode") // .size(14.0) // ); - ui.add_space(12.0); + ui.add_space(16.0); + ui.separator(); }); } @@ -50,7 +51,6 @@ pub(crate) fn shortcuts_window(app: &mut TextEditor, ctx: &egui::Context) { let visuals = &ctx.style().visuals; let screen_rect = ctx.screen_rect(); - // Calculate appropriate window size that always fits nicely in the main window let window_width = (screen_rect.width() * 0.6).clamp(300.0, 400.0); let window_height = (screen_rect.height() * 0.7).clamp(250.0, 500.0); @@ -59,6 +59,8 @@ pub(crate) fn shortcuts_window(app: &mut TextEditor, ctx: &egui::Context) { .resizable(false) .anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO) .fixed_size([window_width, window_height]) + .fade_in(true) + .fade_out(true) .frame(egui::Frame { fill: visuals.window_fill, stroke: visuals.window_stroke, @@ -69,8 +71,7 @@ pub(crate) fn shortcuts_window(app: &mut TextEditor, ctx: &egui::Context) { }) .show(ctx, |ui| { ui.vertical(|ui| { - // Scrollable content area - let available_height = ui.available_height() - 40.0; // Reserve space for close button + let available_height = ui.available_height() - 40.0; ui.allocate_ui_with_layout( [ui.available_width(), available_height].into(), egui::Layout::top_down(egui::Align::Center), @@ -83,7 +84,6 @@ pub(crate) fn shortcuts_window(app: &mut TextEditor, ctx: &egui::Context) { }, ); - // Fixed close button at bottom ui.vertical_centered(|ui| { ui.add_space(8.0); let visuals = ui.visuals(); -- 2.47.1 From 325252e96b9f08d257083a74b6a381d31cae03f1 Mon Sep 17 00:00:00 2001 From: candle Date: Wed, 16 Jul 2025 13:29:31 -0400 Subject: [PATCH 4/8] updated cargo.toml --- Cargo.toml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a3242f3..8ee159f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,13 +1,13 @@ [package] name = "ced" -version = "0.0.4" +version = "0.0.9" edition = "2024" [dependencies] -eframe = "0.31" -egui = "0.31" -serde = { version = "1.0", features = ["derive"] } -rfd = "0.15" -toml = "0.8" -dirs = "5.0" -libc = "0.2" +eframe = "0.32" +egui = "0.32" +serde = { version = "1.0.219", features = ["derive"] } +rfd = "0.15.4" +toml = "0.9.2" +dirs = "6.0" +libc = "0.2.174" -- 2.47.1 From 48cfcb99973ef81a998aaf6b9eddd9f978a8c151 Mon Sep 17 00:00:00 2001 From: candle Date: Wed, 16 Jul 2025 15:28:55 -0400 Subject: [PATCH 5/8] slicing more safely now --- src/app/state/processing.rs | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/src/app/state/processing.rs b/src/app/state/processing.rs index b716553..09cfff7 100644 --- a/src/app/state/processing.rs +++ b/src/app/state/processing.rs @@ -2,6 +2,15 @@ use super::editor::{TextEditor, TextProcessingResult}; use eframe::egui; impl TextEditor { + fn safe_slice_to_pos(content: &str, pos: usize) -> &str { + let pos = pos.min(content.len()); + let mut boundary_pos = pos; + while boundary_pos > 0 && !content.is_char_boundary(boundary_pos) { + boundary_pos -= 1; + } + &content[..boundary_pos] + } + pub fn process_text_for_rendering(&mut self, content: &str, ui: &egui::Ui) { let line_count = content.bytes().filter(|&b| b == b'\n').count() + 1; @@ -119,12 +128,12 @@ impl TextEditor { old_cursor_pos: usize, new_cursor_pos: usize, ) -> isize { - let old_newlines = old_content[..old_cursor_pos.min(old_content.len())] + let old_newlines = Self::safe_slice_to_pos(old_content, old_cursor_pos) .bytes() .filter(|&b| b == b'\n') .count(); - let new_newlines = new_content[..new_cursor_pos.min(new_content.len())] + let new_newlines = Self::safe_slice_to_pos(new_content, new_cursor_pos) .bytes() .filter(|&b| b == b'\n') .count(); @@ -190,11 +199,11 @@ impl TextEditor { let mut current_result = self.get_text_processing_result(); current_result.line_count += newlines_added; - let addition_start_line = old_content[..added_start] + let addition_start_line = Self::safe_slice_to_pos(old_content, added_start) .bytes() .filter(|&b| b == b'\n') .count(); - let addition_end_line = old_content[..added_end.min(old_content.len())] + let addition_end_line = Self::safe_slice_to_pos(old_content, added_end) .bytes() .filter(|&b| b == b'\n') .count(); @@ -261,11 +270,11 @@ impl TextEditor { let mut current_result = self.get_text_processing_result(); current_result.line_count = current_result.line_count.saturating_sub(newlines_removed); - let removal_start_line = old_content[..removed_start] + let removal_start_line = Self::safe_slice_to_pos(old_content, removed_start) .bytes() .filter(|&b| b == b'\n') .count(); - let removal_end_line = old_content[..removed_end] + let removal_end_line = Self::safe_slice_to_pos(old_content, removed_end) .bytes() .filter(|&b| b == b'\n') .count(); @@ -304,18 +313,26 @@ impl TextEditor { fn extract_current_line(&self, content: &str, cursor_pos: usize) -> String { let bytes = content.as_bytes(); + let safe_cursor_pos = cursor_pos.min(bytes.len()); - let mut line_start = cursor_pos; + let mut line_start = safe_cursor_pos; while line_start > 0 && bytes[line_start - 1] != b'\n' { line_start -= 1; } - let mut line_end = cursor_pos; + let mut line_end = safe_cursor_pos; while line_end < bytes.len() && bytes[line_end] != b'\n' { line_end += 1; } - content[line_start..line_end].to_string() + let line_start_boundary = line_start; + let line_end_boundary = line_end; + + if content.is_char_boundary(line_start_boundary) && content.is_char_boundary(line_end_boundary) { + content[line_start_boundary..line_end_boundary].to_string() + } else { + Self::safe_slice_to_pos(content, line_end_boundary)[line_start_boundary..].to_string() + } } fn update_line_if_longer( -- 2.47.1 From 6fa0aa0b617a2725b4ec9fe38b901af704a29ccf Mon Sep 17 00:00:00 2001 From: candle Date: Wed, 16 Jul 2025 16:04:14 -0400 Subject: [PATCH 6/8] same thing for find --- src/app/state/find.rs | 39 ++++++++++++++++++++------ src/app/state/processing.rs | 2 +- src/ui/central_panel/find_highlight.rs | 16 +++++++++-- 3 files changed, 44 insertions(+), 13 deletions(-) diff --git a/src/app/state/find.rs b/src/app/state/find.rs index b057577..3d9cd1c 100644 --- a/src/app/state/find.rs +++ b/src/app/state/find.rs @@ -26,11 +26,33 @@ impl TextEditor { }; let mut start = 0; - while let Some(pos) = search_content[start..].find(&query) { - let absolute_pos = start + pos; - self.find_matches - .push((absolute_pos, absolute_pos + query.len())); - start = absolute_pos + 1; + while start < search_content.len() { + let search_slice = if search_content.is_char_boundary(start) { + &search_content[start..] + } else { + // Find next valid boundary + while start < search_content.len() && !search_content.is_char_boundary(start) { + start += 1; + } + if start >= search_content.len() { + break; + } + &search_content[start..] + }; + + if let Some(pos) = search_slice.find(&query) { + let absolute_pos = start + pos; + self.find_matches + .push((absolute_pos, absolute_pos + query.len())); + + // Advance to next valid character boundary instead of just +1 + start = absolute_pos + 1; + while start < search_content.len() && !search_content.is_char_boundary(start) { + start += 1; + } + } else { + break; + } } if !self.find_matches.is_empty() { @@ -94,8 +116,8 @@ impl TextEditor { if let Some(active_tab) = self.get_active_tab() { let content = &active_tab.content; - let start_char = content[..start_byte.min(content.len())].chars().count(); - let end_char = content[..end_byte.min(content.len())].chars().count(); + let start_char = Self::safe_slice_to_pos(content, start_byte).chars().count(); + let end_char = Self::safe_slice_to_pos(content, end_byte).chars().count(); let text_edit_id = egui::Id::new("main_text_editor"); if let Some(mut state) = egui::TextEdit::load_state(ctx, text_edit_id) { @@ -132,8 +154,7 @@ impl TextEditor { self.update_find_matches(); if let Some(active_tab) = self.get_active_tab() { - let replacement_end_char = active_tab.content - [..replacement_end.min(active_tab.content.len())] + let replacement_end_char = Self::safe_slice_to_pos(&active_tab.content, replacement_end) .chars() .count(); diff --git a/src/app/state/processing.rs b/src/app/state/processing.rs index 09cfff7..7d27afd 100644 --- a/src/app/state/processing.rs +++ b/src/app/state/processing.rs @@ -2,7 +2,7 @@ use super::editor::{TextEditor, TextProcessingResult}; use eframe::egui; impl TextEditor { - fn safe_slice_to_pos(content: &str, pos: usize) -> &str { + pub(crate) fn safe_slice_to_pos(content: &str, pos: usize) -> &str { let pos = pos.min(content.len()); let mut boundary_pos = pos; while boundary_pos > 0 && !content.is_char_boundary(boundary_pos) { diff --git a/src/ui/central_panel/find_highlight.rs b/src/ui/central_panel/find_highlight.rs index f0082ea..f8f04f8 100644 --- a/src/ui/central_panel/find_highlight.rs +++ b/src/ui/central_panel/find_highlight.rs @@ -1,5 +1,15 @@ use eframe::egui; +/// Safely get a string slice up to a byte position, ensuring UTF-8 boundaries +fn safe_slice_to_pos(content: &str, pos: usize) -> &str { + let pos = pos.min(content.len()); + let mut boundary_pos = pos; + while boundary_pos > 0 && !content.is_char_boundary(boundary_pos) { + boundary_pos -= 1; + } + &content[..boundary_pos] +} + pub(super) fn draw_find_highlights( ui: &mut egui::Ui, content: &str, @@ -44,7 +54,7 @@ fn draw_single_highlight( font_id: &egui::FontId, is_current_match: bool, ) { - let text_up_to_start = &content[..start_pos.min(content.len())]; + let text_up_to_start = safe_slice_to_pos(content, start_pos); let start_line = text_up_to_start.chars().filter(|&c| c == '\n').count(); if start_line >= galley.rows.len() { @@ -52,8 +62,8 @@ fn draw_single_highlight( } let line_start_byte_pos = text_up_to_start.rfind('\n').map(|pos| pos + 1).unwrap_or(0); - let line_start_char_pos = content[..line_start_byte_pos].chars().count(); - let start_char_pos = content[..start_pos].chars().count(); + let line_start_char_pos = safe_slice_to_pos(content, line_start_byte_pos).chars().count(); + let start_char_pos = safe_slice_to_pos(content, start_pos).chars().count(); let start_col = start_char_pos - line_start_char_pos; let lines: Vec<&str> = content.lines().collect(); -- 2.47.1 From 1edf0995c00dc3bb7efca4295e096fa93ce4e25f Mon Sep 17 00:00:00 2001 From: candle Date: Wed, 16 Jul 2025 17:20:09 -0400 Subject: [PATCH 7/8] no more clones --- src/app/shortcuts.rs | 2 +- src/app/state/config.rs | 4 +-- src/app/state/find.rs | 14 +++++------ src/app/state/lifecycle.rs | 8 +++--- src/app/state/processing.rs | 4 +-- src/app/state/tabs.rs | 3 +++ src/app/state/ui.rs | 7 ++---- src/io.rs | 20 ++++++--------- src/ui/central_panel/editor.rs | 34 +++++++++----------------- src/ui/central_panel/find_highlight.rs | 6 ++--- src/ui/central_panel/line_numbers.rs | 8 +++--- src/ui/menu_bar.rs | 14 +++++------ src/ui/preferences_window.rs | 12 ++++----- 13 files changed, 59 insertions(+), 77 deletions(-) diff --git a/src/app/shortcuts.rs b/src/app/shortcuts.rs index f59d685..bba8e66 100644 --- a/src/app/shortcuts.rs +++ b/src/app/shortcuts.rs @@ -149,7 +149,7 @@ fn get_shortcuts() -> Vec { ] } -fn execute_action(action: ShortcutAction, editor: &mut TextEditor, _ctx: &egui::Context) -> bool { +fn execute_action(action: ShortcutAction, editor: &mut TextEditor, ctx: &egui::Context) -> bool { match action { ShortcutAction::NewFile => { io::new_file(editor); diff --git a/src/app/state/config.rs b/src/app/state/config.rs index 49bed39..5e00e5f 100644 --- a/src/app/state/config.rs +++ b/src/app/state/config.rs @@ -57,7 +57,7 @@ impl TextEditor { cc.egui_ctx.options_mut(|o| o.zoom_with_keyboard = false); - let mut style = (*cc.egui_ctx.style()).clone(); + let mut style = (*cc.egui_ctx.style()).to_owned(); style .text_styles .insert(egui::TextStyle::Body, egui::FontId::proportional(16.0)); @@ -85,7 +85,7 @@ impl TextEditor { word_wrap: self.word_wrap, theme: self.theme, line_side: self.line_side, - font_family: self.font_family.clone(), + font_family: self.font_family.to_string(), font_size: self.font_size, // vim_mode: self.vim_mode, } diff --git a/src/app/state/find.rs b/src/app/state/find.rs index 3d9cd1c..f2e32c7 100644 --- a/src/app/state/find.rs +++ b/src/app/state/find.rs @@ -14,13 +14,13 @@ impl TextEditor { if let Some(tab) = self.get_active_tab() { let content = &tab.content; let query = if self.case_sensitive_search { - self.find_query.clone() + self.find_query.to_owned() } else { self.find_query.to_lowercase() }; let search_content = if self.case_sensitive_search { - content.clone() + content.to_string() } else { content.to_lowercase() }; @@ -138,13 +138,13 @@ impl TextEditor { } if let Some((start_byte, end_byte)) = self.get_current_match_position() { - let replace_query = self.replace_query.clone(); + let replace_query = self.replace_query.to_owned(); let replacement_end = start_byte + replace_query.len(); if let Some(active_tab) = self.get_active_tab_mut() { let content = &active_tab.content; - let mut new_content = content.clone(); + let mut new_content = content.to_string(); new_content.replace_range(start_byte..end_byte, &replace_query); active_tab.content = new_content; @@ -176,10 +176,10 @@ impl TextEditor { return; } - let find_query = self.find_query.clone(); - let replace_query = self.replace_query.clone(); + let find_query = self.find_query.to_owned(); + let replace_query = self.replace_query.to_owned(); let case_sensitive = self.case_sensitive_search; - let find_matches = self.find_matches.clone(); + let find_matches = self.find_matches.to_owned(); if let Some(active_tab) = self.get_active_tab_mut() { let content = &active_tab.content; diff --git a/src/app/state/lifecycle.rs b/src/app/state/lifecycle.rs index a366228..f9a8a59 100644 --- a/src/app/state/lifecycle.rs +++ b/src/app/state/lifecycle.rs @@ -10,7 +10,7 @@ impl TextEditor { self.tabs .iter() .filter(|tab| tab.is_modified) - .map(|tab| tab.title.clone()) + .map(|tab| tab.title.to_owned()) .collect() } @@ -40,19 +40,19 @@ impl TextEditor { "Unsaved Changes".to_string(), "You have unsaved changes.".to_string(), "Quit Without Saving".to_string(), - action.clone(), + action.to_owned(), ), UnsavedAction::CloseTab(tab_index) => { let file_name = self .tabs .get(*tab_index) - .map_or_else(|| "unknown file".to_string(), |tab| tab.title.clone()); + .map_or_else(|| "unknown file".to_string(), |tab| tab.title.to_owned()); ( vec![file_name], "Unsaved Changes".to_string(), "The file has unsaved changes.".to_string(), "Close Without Saving".to_string(), - action.clone(), + action.to_owned(), ) } } diff --git a/src/app/state/processing.rs b/src/app/state/processing.rs index 7d27afd..813f5a1 100644 --- a/src/app/state/processing.rs +++ b/src/app/state/processing.rs @@ -171,7 +171,6 @@ impl TextEditor { let min_len = old_content.len().min(new_content.len()); let mut common_prefix = 0; let mut common_suffix = 0; - for i in 0..min_len { if old_content.as_bytes()[i] == new_content.as_bytes()[i] { common_prefix += 1; @@ -221,7 +220,6 @@ impl TextEditor { } else { let current_line = self.extract_current_line(new_content, new_cursor_pos); let current_line_length = current_line.chars().count(); - self.update_line_if_longer( self.current_cursor_line, ¤t_line, @@ -372,7 +370,7 @@ impl TextEditor { pub fn get_text_processing_result(&self) -> TextProcessingResult { self.text_processing_result .lock() - .map(|result| result.clone()) + .map(|result| result.to_owned()) .unwrap_or_default() } diff --git a/src/app/state/tabs.rs b/src/app/state/tabs.rs index f87efb6..7df0611 100644 --- a/src/app/state/tabs.rs +++ b/src/app/state/tabs.rs @@ -17,6 +17,7 @@ impl TextEditor { if self.show_find && !self.find_query.is_empty() { self.update_find_matches(); } + self.text_needs_processing = true; } pub fn close_tab(&mut self, tab_index: usize) { @@ -30,6 +31,7 @@ impl TextEditor { if self.show_find && !self.find_query.is_empty() { self.update_find_matches(); } + self.text_needs_processing = true; } } @@ -39,6 +41,7 @@ impl TextEditor { if self.show_find && !self.find_query.is_empty() { self.update_find_matches(); } + self.text_needs_processing = true; } } } diff --git a/src/app/state/ui.rs b/src/app/state/ui.rs index c436e1f..1c003ac 100644 --- a/src/app/state/ui.rs +++ b/src/app/state/ui.rs @@ -45,7 +45,7 @@ impl TextEditor { _ => egui::FontFamily::Proportional, }; - let mut style = (*ctx.style()).clone(); + let mut style = (*ctx.style()).to_owned(); style.text_styles.insert( egui::TextStyle::Monospace, egui::FontId::new(self.font_size, font_family), @@ -66,10 +66,7 @@ impl TextEditor { /// Trigger immediate text reprocessing when font settings change pub fn reprocess_text_for_font_change(&mut self, ui: &egui::Ui) { if let Some(active_tab) = self.get_active_tab() { - let content = active_tab.content.clone(); - if !content.is_empty() { - self.process_text_for_rendering(&content, ui); - } + self.process_text_for_rendering(&active_tab.content.to_string(), ui); } } diff --git a/src/io.rs b/src/io.rs index 6f490a3..4d33f3f 100644 --- a/src/io.rs +++ b/src/io.rs @@ -24,13 +24,10 @@ pub(crate) fn open_file(app: &mut TextEditor) { if should_replace_current_tab { if let Some(active_tab) = app.get_active_tab_mut() { + let title = path.file_name().and_then(|n| n.to_str()).unwrap_or("Untitled"); active_tab.content = content; - active_tab.file_path = Some(path.clone()); - active_tab.title = path - .file_name() - .and_then(|n| n.to_str()) - .unwrap_or("Untitled") - .to_string(); + active_tab.file_path = Some(path.to_path_buf()); + active_tab.title = title.to_string(); active_tab.mark_as_saved(); } app.text_needs_processing = true; @@ -54,7 +51,7 @@ pub(crate) fn open_file(app: &mut TextEditor) { pub(crate) fn save_file(app: &mut TextEditor) { if let Some(active_tab) = app.get_active_tab() { if let Some(path) = &active_tab.file_path { - save_to_path(app, path.clone()); + save_to_path(app, path.to_path_buf()); } else { save_as_file(app); } @@ -74,12 +71,9 @@ pub(crate) fn save_to_path(app: &mut TextEditor, path: PathBuf) { if let Some(active_tab) = app.get_active_tab_mut() { match fs::write(&path, &active_tab.content) { Ok(()) => { - active_tab.file_path = Some(path.clone()); - active_tab.title = path - .file_name() - .and_then(|n| n.to_str()) - .unwrap_or("Untitled") - .to_string(); + let title = path.file_name().and_then(|n| n.to_str()).unwrap_or("Untitled"); + active_tab.file_path = Some(path.to_path_buf()); + active_tab.title = title.to_string(); active_tab.mark_as_saved(); } Err(err) => { diff --git a/src/ui/central_panel/editor.rs b/src/ui/central_panel/editor.rs index 64069a8..acd273a 100644 --- a/src/ui/central_panel/editor.rs +++ b/src/ui/central_panel/editor.rs @@ -35,8 +35,8 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R let find_data = if show_find && !app.find_matches.is_empty() { app.get_active_tab().map(|tab| { ( - tab.content.clone(), - app.find_matches.clone(), + tab.content.to_owned(), + app.find_matches.to_owned(), app.current_match_index, ) }) @@ -58,7 +58,7 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R .text_styles .get(&egui::TextStyle::Monospace) .unwrap_or(&egui::FontId::monospace(font_size)) - .clone(); + .to_owned(); let desired_width = if word_wrap { ui.available_width() @@ -68,8 +68,8 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R let temp_galley = ui.fonts(|fonts| { fonts.layout( - content.clone(), - font_id.clone(), + content.to_owned(), + font_id.to_owned(), ui.visuals().text_color(), desired_width, ) @@ -125,7 +125,7 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R let content_changed = output.response.changed(); let content_for_processing = if content_changed { active_tab.update_modified_state(); - Some(active_tab.content.clone()) + Some(active_tab.content.to_owned()) } else { None }; @@ -141,7 +141,7 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R .map(|range| range.primary.index); if let Some(content) = content_for_processing { - let previous_content = app.previous_content.clone(); + let previous_content = app.previous_content.to_owned(); let previous_cursor_pos = app.previous_cursor_char_index; if !previous_content.is_empty() { @@ -160,7 +160,7 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R app.process_text_for_rendering(&content, ui); } - app.previous_content = content.clone(); + app.previous_content = content.to_owned(); app.previous_cursor_char_index = current_cursor_pos; if let Some(active_tab) = app.get_active_tab_mut() { @@ -169,21 +169,12 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R } } - if app.font_settings_changed { + if app.font_settings_changed || app.text_needs_processing { if let Some(active_tab) = app.get_active_tab() { - let content = active_tab.content.clone(); - if !content.is_empty() { - app.process_text_for_rendering(&content, ui); - } - } - app.font_settings_changed = false; - } - - if app.text_needs_processing { - if let Some(active_tab) = app.get_active_tab() { - let content = active_tab.content.clone(); + let content = active_tab.content.to_owned(); app.process_text_for_rendering(&content, ui); } + app.font_settings_changed = false; app.text_needs_processing = false; } @@ -206,7 +197,7 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R .text_styles .get(&egui::TextStyle::Monospace) .unwrap_or(&egui::FontId::monospace(font_size)) - .clone(); + .to_owned(); let line_height = ui.fonts(|fonts| fonts.row_height(&font_id)); let y_pos = output.response.rect.top() + (cursor_line as f32 * line_height); @@ -221,7 +212,6 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R } } } - app.previous_cursor_position = Some(cursor_pos); } } diff --git a/src/ui/central_panel/find_highlight.rs b/src/ui/central_panel/find_highlight.rs index f8f04f8..ca4e488 100644 --- a/src/ui/central_panel/find_highlight.rs +++ b/src/ui/central_panel/find_highlight.rs @@ -25,7 +25,7 @@ pub(super) fn draw_find_highlights( .text_styles .get(&egui::TextStyle::Monospace) .unwrap_or(&egui::FontId::monospace(font_size)) - .clone(); + .to_owned(); for (match_index, &(start_pos, end_pos)) in matches.iter().enumerate() { let is_current_match = current_match_index == Some(match_index); @@ -78,7 +78,7 @@ fn draw_single_highlight( fonts .layout( text_before_match, - font_id.clone(), + font_id.to_owned(), egui::Color32::WHITE, f32::INFINITY, ) @@ -96,7 +96,7 @@ fn draw_single_highlight( fonts .layout( match_text.to_string(), - font_id.clone(), + font_id.to_owned(), ui.visuals().text_color(), f32::INFINITY, ) diff --git a/src/ui/central_panel/line_numbers.rs b/src/ui/central_panel/line_numbers.rs index 52c3916..daa4ed0 100644 --- a/src/ui/central_panel/line_numbers.rs +++ b/src/ui/central_panel/line_numbers.rs @@ -29,7 +29,7 @@ pub(super) fn get_visual_line_mapping( cache .borrow() .as_ref() - .map(|(_, _, mapping)| mapping.clone()) + .map(|(_, _, mapping)| mapping.to_owned()) .unwrap_or_default() }) } @@ -52,7 +52,7 @@ fn calculate_visual_line_mapping( let galley = ui.fonts(|fonts| { fonts.layout( line.to_string(), - font_id.clone(), + font_id.to_owned(), egui::Color32::WHITE, available_width, ) @@ -100,7 +100,7 @@ pub(super) fn render_line_numbers( }; ui.label( egui::RichText::new(text) - .font(font_id.clone()) + .font(font_id.to_owned()) .color(text_color), ); } @@ -109,7 +109,7 @@ pub(super) fn render_line_numbers( let text = format!("{:>width$}", i, width = line_count_width); ui.label( egui::RichText::new(text) - .font(font_id.clone()) + .font(font_id.to_owned()) .color(text_color), ); } diff --git a/src/ui/menu_bar.rs b/src/ui/menu_bar.rs index 552c673..268840e 100644 --- a/src/ui/menu_bar.rs +++ b/src/ui/menu_bar.rs @@ -128,13 +128,13 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) { if let Some(active_tab) = app.get_active_tab_mut() { let current_state = ( state.cursor.char_range().unwrap_or_default(), - active_tab.content.clone(), + active_tab.content.to_string(), ); let mut undoer = state.undoer(); if let Some((cursor_range, content)) = undoer.undo(¤t_state) { - active_tab.content = content.clone(); + active_tab.content = content.to_string(); state.cursor.set_char_range(Some(*cursor_range)); state.set_undoer(undoer); egui::TextEdit::store_state(ui.ctx(), text_edit_id, state); @@ -155,13 +155,13 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) { if let Some(active_tab) = app.get_active_tab_mut() { let current_state = ( state.cursor.char_range().unwrap_or_default(), - active_tab.content.clone(), + active_tab.content.to_string(), ); let mut undoer = state.undoer(); if let Some((cursor_range, content)) = undoer.redo(¤t_state) { - active_tab.content = content.clone(); + active_tab.content = content.to_string(); state.cursor.set_char_range(Some(*cursor_range)); state.set_undoer(undoer); egui::TextEdit::store_state(ui.ctx(), text_edit_id, state); @@ -276,14 +276,14 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) { if app.hide_tab_bar { let tab_title = if let Some(tab) = app.get_active_tab() { - tab.title.clone() + tab.title.to_owned() } else { let empty_tab = crate::app::tab::Tab::new_empty(1); - empty_tab.title.clone() + empty_tab.title.to_owned() }; let window_width = ctx.screen_rect().width(); - let font_id = ui.style().text_styles[&egui::TextStyle::Body].clone(); + let font_id = ui.style().text_styles[&egui::TextStyle::Body].to_owned(); let tab_title = if app.get_active_tab().is_some_and(|tab| tab.is_modified) { format!("{tab_title}*") diff --git a/src/ui/preferences_window.rs b/src/ui/preferences_window.rs index 993ff6a..5d09498 100644 --- a/src/ui/preferences_window.rs +++ b/src/ui/preferences_window.rs @@ -78,7 +78,7 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) { .font_size_input .as_ref() .unwrap_or(&"14".to_string()) - .clone(); + .to_owned(); let response = ui.add( egui::TextEdit::singleline(&mut font_size_text) .desired_width(50.0) @@ -86,7 +86,7 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) { .id(egui::Id::new("font_size_input")), ); - app.font_size_input = Some(font_size_text.clone()); + app.font_size_input = Some(font_size_text.to_owned()); if response.clicked() { response.request_focus(); @@ -132,18 +132,18 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) { egui::RichText::new( "The quick brown fox jumps over the lazy dog.", ) - .font(preview_font.clone()), + .font(preview_font.to_owned()), ); ui.label( egui::RichText::new("ABCDEFGHIJKLMNOPQRSTUVWXYZ") - .font(preview_font.clone()), + .font(preview_font.to_owned()), ); ui.label( egui::RichText::new("abcdefghijklmnopqrstuvwxyz") - .font(preview_font.clone()), + .font(preview_font.to_owned()), ); ui.label( - egui::RichText::new("1234567890 !@#$%^&*()").font(preview_font), + egui::RichText::new("1234567890 !@#$%^&*()").font(preview_font.to_owned()), ); }); }); -- 2.47.1 From f56ad6c7c5a363aa37ab6b660cf6541d4400d11f Mon Sep 17 00:00:00 2001 From: candle Date: Wed, 16 Jul 2025 17:34:11 -0400 Subject: [PATCH 8/8] formatting/some sanity fixes --- src/app/shortcuts.rs | 8 ++++---- src/app/state/find.rs | 11 ++++++----- src/app/state/lifecycle.rs | 20 ++++++++++++-------- src/app/state/processing.rs | 6 ++++-- src/io.rs | 10 ++++++++-- src/ui/central_panel/find_highlight.rs | 4 +++- src/ui/preferences_window.rs | 3 ++- 7 files changed, 39 insertions(+), 23 deletions(-) diff --git a/src/app/shortcuts.rs b/src/app/shortcuts.rs index bba8e66..660f1ed 100644 --- a/src/app/shortcuts.rs +++ b/src/app/shortcuts.rs @@ -149,7 +149,7 @@ fn get_shortcuts() -> Vec { ] } -fn execute_action(action: ShortcutAction, editor: &mut TextEditor, ctx: &egui::Context) -> bool { +fn execute_action(action: ShortcutAction, editor: &mut TextEditor) -> bool { match action { ShortcutAction::NewFile => { io::new_file(editor); @@ -291,16 +291,16 @@ pub fn handle(editor: &mut TextEditor, ctx: &egui::Context) { if i.consume_key(modifiers, key) { match action { ShortcutAction::ZoomIn | ShortcutAction::ZoomOut => { - font_zoom_occurred = execute_action(action, editor, ctx); + font_zoom_occurred = execute_action(action, editor); } ShortcutAction::GlobalZoomIn | ShortcutAction::GlobalZoomOut | ShortcutAction::ResetZoom => { - execute_action(action, editor, ctx); + execute_action(action, editor); global_zoom_occurred = true; } _ => { - execute_action(action, editor, ctx); + execute_action(action, editor); } } break; diff --git a/src/app/state/find.rs b/src/app/state/find.rs index f2e32c7..bae0f3f 100644 --- a/src/app/state/find.rs +++ b/src/app/state/find.rs @@ -39,12 +39,12 @@ impl TextEditor { } &search_content[start..] }; - + if let Some(pos) = search_slice.find(&query) { let absolute_pos = start + pos; self.find_matches .push((absolute_pos, absolute_pos + query.len())); - + // Advance to next valid character boundary instead of just +1 start = absolute_pos + 1; while start < search_content.len() && !search_content.is_char_boundary(start) { @@ -154,9 +154,10 @@ impl TextEditor { self.update_find_matches(); if let Some(active_tab) = self.get_active_tab() { - let replacement_end_char = Self::safe_slice_to_pos(&active_tab.content, replacement_end) - .chars() - .count(); + let replacement_end_char = + Self::safe_slice_to_pos(&active_tab.content, replacement_end) + .chars() + .count(); let text_edit_id = egui::Id::new("main_text_editor"); if let Some(mut state) = egui::TextEdit::load_state(ctx, text_edit_id) { diff --git a/src/app/state/lifecycle.rs b/src/app/state/lifecycle.rs index f9a8a59..028923a 100644 --- a/src/app/state/lifecycle.rs +++ b/src/app/state/lifecycle.rs @@ -35,13 +35,17 @@ impl TextEditor { let (files_to_list, title, confirmation_text, button_text, action) = if let Some(action) = &self.pending_unsaved_action { match action { - UnsavedAction::Quit => ( - self.get_unsaved_files(), - "Unsaved Changes".to_string(), - "You have unsaved changes.".to_string(), - "Quit Without Saving".to_string(), - action.to_owned(), - ), + UnsavedAction::Quit => { + let files = self.get_unsaved_files(); + let file_plural = if files.len() > 1 { "s" } else { "" }; + ( + files, + "Unsaved Changes".to_string(), + format!("File{file_plural} with unsaved changes:"), + "Quit Without Saving".to_string(), + action.to_owned(), + ) + } UnsavedAction::CloseTab(tab_index) => { let file_name = self .tabs @@ -50,7 +54,7 @@ impl TextEditor { ( vec![file_name], "Unsaved Changes".to_string(), - "The file has unsaved changes.".to_string(), + "This file has unsaved changes:".to_string(), "Close Without Saving".to_string(), action.to_owned(), ) diff --git a/src/app/state/processing.rs b/src/app/state/processing.rs index 813f5a1..78e3e24 100644 --- a/src/app/state/processing.rs +++ b/src/app/state/processing.rs @@ -325,8 +325,10 @@ impl TextEditor { let line_start_boundary = line_start; let line_end_boundary = line_end; - - if content.is_char_boundary(line_start_boundary) && content.is_char_boundary(line_end_boundary) { + + if content.is_char_boundary(line_start_boundary) + && content.is_char_boundary(line_end_boundary) + { content[line_start_boundary..line_end_boundary].to_string() } else { Self::safe_slice_to_pos(content, line_end_boundary)[line_start_boundary..].to_string() diff --git a/src/io.rs b/src/io.rs index 4d33f3f..e78e1bd 100644 --- a/src/io.rs +++ b/src/io.rs @@ -24,7 +24,10 @@ pub(crate) fn open_file(app: &mut TextEditor) { if should_replace_current_tab { if let Some(active_tab) = app.get_active_tab_mut() { - let title = path.file_name().and_then(|n| n.to_str()).unwrap_or("Untitled"); + let title = path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("Untitled"); active_tab.content = content; active_tab.file_path = Some(path.to_path_buf()); active_tab.title = title.to_string(); @@ -71,7 +74,10 @@ pub(crate) fn save_to_path(app: &mut TextEditor, path: PathBuf) { if let Some(active_tab) = app.get_active_tab_mut() { match fs::write(&path, &active_tab.content) { Ok(()) => { - let title = path.file_name().and_then(|n| n.to_str()).unwrap_or("Untitled"); + let title = path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("Untitled"); active_tab.file_path = Some(path.to_path_buf()); active_tab.title = title.to_string(); active_tab.mark_as_saved(); diff --git a/src/ui/central_panel/find_highlight.rs b/src/ui/central_panel/find_highlight.rs index ca4e488..38b813b 100644 --- a/src/ui/central_panel/find_highlight.rs +++ b/src/ui/central_panel/find_highlight.rs @@ -62,7 +62,9 @@ fn draw_single_highlight( } let line_start_byte_pos = text_up_to_start.rfind('\n').map(|pos| pos + 1).unwrap_or(0); - let line_start_char_pos = safe_slice_to_pos(content, line_start_byte_pos).chars().count(); + let line_start_char_pos = safe_slice_to_pos(content, line_start_byte_pos) + .chars() + .count(); let start_char_pos = safe_slice_to_pos(content, start_pos).chars().count(); let start_col = start_char_pos - line_start_char_pos; diff --git a/src/ui/preferences_window.rs b/src/ui/preferences_window.rs index 5d09498..3ecd285 100644 --- a/src/ui/preferences_window.rs +++ b/src/ui/preferences_window.rs @@ -143,7 +143,8 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) { .font(preview_font.to_owned()), ); ui.label( - egui::RichText::new("1234567890 !@#$%^&*()").font(preview_font.to_owned()), + egui::RichText::new("1234567890 !@#$%^&*()") + .font(preview_font.to_owned()), ); }); }); -- 2.47.1