From 5dc0b6d638915a7ae09da384da66bd7f18c98787 Mon Sep 17 00:00:00 2001 From: candle Date: Wed, 23 Jul 2025 13:14:04 -0400 Subject: [PATCH] file diffs are kept separate --- Cargo.toml | 3 +- README.md | 5 +- src/app/state/config.rs | 1 - src/app/state/find.rs | 2 - src/app/state/state_cache.rs | 102 ++++++++++++++++++++++++++++----- src/app/state/tabs.rs | 3 - src/app/state/ui.rs | 7 --- src/app/theme.rs | 6 -- src/ui/central_panel/editor.rs | 1 - src/ui/preferences_window.rs | 16 +++--- 10 files changed, 102 insertions(+), 44 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 87258b6..7ef5dff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ced" -version = "0.0.9" +version = "0.1.3" edition = "2024" [dependencies] @@ -16,3 +16,4 @@ libc = "0.2.174" syntect = "5.2.0" plist = "1.7.4" diffy = "0.4.2" +uuid = { version = "1.0", features = ["v4"] } diff --git a/README.md b/README.md index 50117b3..e1aa170 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ There is a disturbing lack of simple GUI text editors available on Linux nativel ## Features * Sane text editing with standard keybindings (`Ctrl+A`, `Ctrl+C`, etc.). -* Opens with a blank slate for quick typing, remember Notepad? +* Choose between opening fresh every time, like Notepad, or maintaining a consistent state like Notepad++. * Separate UI zoom that doesn't affect font size (`Ctrl+Shift` + `+`/`-`). * Ricers rejoice, your `pywal` colors will be used! * Weirdly smooth typing experience. @@ -39,6 +39,7 @@ sudo install -Dm644 ced.desktop /usr/share/applications/ced.desktop Here is an example `config.toml`: ```toml +state_cache = true auto_hide_toolbar = false show_line_numbers = false word_wrap = false @@ -53,6 +54,7 @@ syntax_highlighting = true | Option | Default | Description | |--------|---------|-------------| +| `state_cache` | `false` | If `true`, open files will have their unsaved changes cached and will be automatically opened when starting a new session. | | `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`. | @@ -67,7 +69,6 @@ syntax_highlighting = true In order of importance. | Feature | Info | | ------- | ---- | -| **State/Cache:** | A toggleable option to keep an application state and prevent "Quit without saving" warnings. | | **LSP:** | Looking at allowing you to use/attach your own tools for this. | | **Choose Font** | More than just Monospace/Proportional. | | **Vim Mode:** | It's in-escapable. | diff --git a/src/app/state/config.rs b/src/app/state/config.rs index adb36fd..6e6ca66 100644 --- a/src/app/state/config.rs +++ b/src/app/state/config.rs @@ -22,7 +22,6 @@ impl TextEditor { pub fn from_config_with_context(config: Config, cc: &eframe::CreationContext<'_>) -> Self { let mut editor = Self::from_config(config); - // Load state cache if enabled if let Err(e) = editor.load_state_cache() { eprintln!("Failed to load state cache: {e}"); } diff --git a/src/app/state/find.rs b/src/app/state/find.rs index bae0f3f..0e30610 100644 --- a/src/app/state/find.rs +++ b/src/app/state/find.rs @@ -30,7 +30,6 @@ impl TextEditor { 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; } @@ -45,7 +44,6 @@ impl TextEditor { 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; diff --git a/src/app/state/state_cache.rs b/src/app/state/state_cache.rs index f19e4f1..afc92fa 100644 --- a/src/app/state/state_cache.rs +++ b/src/app/state/state_cache.rs @@ -2,10 +2,11 @@ use super::editor::TextEditor; use crate::app::tab::{Tab, compute_content_hash}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; +use uuid::Uuid; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CachedTab { - pub diff: Option, + pub diff_file: Option, // Path to diff file for modified tabs pub full_content: Option, // This is used for 'new files' that don't have a path pub file_path: Option, pub is_modified: bool, @@ -20,18 +21,40 @@ pub struct StateCache { pub tab_counter: usize, } +fn create_diff_file(diff_content: &str) -> Result> { + let diffs_dir = TextEditor::diffs_cache_dir().ok_or("Cannot determine cache directory")?; + std::fs::create_dir_all(&diffs_dir)?; + + let diff_filename = format!("{}.diff", Uuid::new_v4()); + let diff_path = diffs_dir.join(diff_filename); + + std::fs::write(&diff_path, diff_content)?; + Ok(diff_path) +} + +fn load_diff_file(diff_path: &PathBuf) -> Result> { + Ok(std::fs::read_to_string(diff_path)?) +} + impl From<&Tab> for CachedTab { fn from(tab: &Tab) -> Self { if let Some(file_path) = &tab.file_path { let original_content = std::fs::read_to_string(file_path).unwrap_or_default(); - let diff = if tab.is_modified { - Some(diffy::create_patch(&original_content, &tab.content).to_string()) + let diff_file = if tab.is_modified { + let diff_content = diffy::create_patch(&original_content, &tab.content); + match create_diff_file(&diff_content.to_string()) { + Ok(path) => Some(path), + Err(e) => { + eprintln!("Warning: Failed to create diff file: {}", e); + None + } + } } else { None }; Self { - diff, + diff_file, full_content: None, file_path: tab.file_path.clone(), is_modified: tab.is_modified, @@ -40,7 +63,7 @@ impl From<&Tab> for CachedTab { } } else { Self { - diff: None, + diff_file: None, full_content: Some(tab.content.clone()), file_path: None, is_modified: tab.is_modified, @@ -55,21 +78,30 @@ impl From for Tab { fn from(cached: CachedTab) -> Self { if let Some(file_path) = cached.file_path { let original_content = std::fs::read_to_string(&file_path).unwrap_or_default(); - let current_content = if let Some(diff_str) = cached.diff { - match diffy::Patch::from_str(&diff_str) { - Ok(patch) => { - match diffy::apply(&original_content, &patch) { - Ok(content) => content, + let current_content = if let Some(diff_path) = cached.diff_file { + match load_diff_file(&diff_path) { + Ok(diff_content) => { + match diffy::Patch::from_str(&diff_content) { + Ok(patch) => { + match diffy::apply(&original_content, &patch) { + Ok(content) => content, + Err(_) => { + eprintln!("Warning: Failed to apply diff for {}, using original content", + file_path.display()); + original_content + } + } + } Err(_) => { - eprintln!("Warning: Failed to apply diff for {}, using original content", + eprintln!("Warning: Failed to parse diff for {}, using original content", file_path.display()); original_content } } } - Err(_) => { - eprintln!("Warning: Failed to parse diff for {}, using original content", - file_path.display()); + Err(e) => { + eprintln!("Warning: Failed to load diff file {:?}: {}, using original content", + diff_path, e); original_content } } @@ -116,6 +148,35 @@ impl TextEditor { Some(cache_dir.join("state.json")) } + pub fn diffs_cache_dir() -> Option { + let cache_dir = if let Some(cache_dir) = dirs::cache_dir() { + cache_dir.join(env!("CARGO_PKG_NAME")) + } else if let Some(home_dir) = dirs::home_dir() { + home_dir.join(".cache").join(env!("CARGO_PKG_NAME")) + } else { + return None; + }; + + Some(cache_dir.join("diffs")) + } + + fn cleanup_orphaned_diffs(active_diff_files: &[PathBuf]) -> Result<(), Box> { + if let Some(diffs_dir) = Self::diffs_cache_dir() { + if diffs_dir.exists() { + for entry in std::fs::read_dir(diffs_dir)? { + let entry = entry?; + let path = entry.path(); + if path.extension().and_then(|s| s.to_str()) == Some("diff") { + if !active_diff_files.contains(&path) { + let _ = std::fs::remove_file(path); + } + } + } + } + } + Ok(()) + } + pub fn load_state_cache(&mut self) -> Result<(), Box> { if !self.state_cache { return Ok(()); @@ -157,6 +218,12 @@ impl TextEditor { tab_counter: self.tab_counter, }; + let active_diff_files: Vec = state_cache.tabs + .iter() + .filter_map(|tab| tab.diff_file.clone()) + .collect(); + let _ = Self::cleanup_orphaned_diffs(&active_diff_files); + let content = serde_json::to_string_pretty(&state_cache)?; std::fs::write(&cache_path, content)?; @@ -169,6 +236,13 @@ impl TextEditor { std::fs::remove_file(cache_path)?; } } + + if let Some(diffs_dir) = Self::diffs_cache_dir() { + if diffs_dir.exists() { + let _ = std::fs::remove_dir_all(diffs_dir); + } + } + Ok(()) } } \ No newline at end of file diff --git a/src/app/state/tabs.rs b/src/app/state/tabs.rs index bfaba5e..b491955 100644 --- a/src/app/state/tabs.rs +++ b/src/app/state/tabs.rs @@ -19,7 +19,6 @@ impl TextEditor { } self.text_needs_processing = true; - // Save state cache after adding new tab if let Err(e) = self.save_state_cache() { eprintln!("Failed to save state cache: {e}"); } @@ -38,7 +37,6 @@ impl TextEditor { } self.text_needs_processing = true; - // Save state cache after closing tab if let Err(e) = self.save_state_cache() { eprintln!("Failed to save state cache: {e}"); } @@ -53,7 +51,6 @@ impl TextEditor { } self.text_needs_processing = true; - // Save state cache after switching tabs if let Err(e) = self.save_state_cache() { eprintln!("Failed to save state cache: {e}"); } diff --git a/src/app/state/ui.rs b/src/app/state/ui.rs index 1c003ac..0571615 100644 --- a/src/app/state/ui.rs +++ b/src/app/state/ui.rs @@ -23,7 +23,6 @@ impl TextEditor { } } - /// Get the configured font ID based on the editor's font settings pub fn get_font_id(&self) -> egui::FontId { let font_family = match self.font_family.as_str() { "Monospace" => egui::FontFamily::Monospace, @@ -32,13 +31,11 @@ impl TextEditor { egui::FontId::new(self.font_size, font_family) } - /// Immediately apply theme and save to configuration pub fn set_theme(&mut self, ctx: &egui::Context) { theme::apply(self.theme, ctx); self.save_config(); } - /// Apply font settings with immediate text reprocessing pub fn apply_font_settings(&mut self, ctx: &egui::Context) { let font_family = match self.font_family.as_str() { "Monospace" => egui::FontFamily::Monospace, @@ -56,21 +53,18 @@ impl TextEditor { self.save_config(); } - /// Apply font settings with immediate text reprocessing pub fn apply_font_settings_with_ui(&mut self, ctx: &egui::Context, ui: &egui::Ui) { self.apply_font_settings(ctx); self.reprocess_text_for_font_change(ui); self.font_settings_changed = false; } - /// 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() { self.process_text_for_rendering(&active_tab.content.to_string(), ui); } } - /// Calculates the available width for the text editor, accounting for line numbers and separator pub fn calculate_editor_dimensions(&self, ui: &egui::Ui) -> EditorDimensions { let total_available_width = ui.available_width(); @@ -114,7 +108,6 @@ impl TextEditor { } } - /// Calculate the available width for non-word-wrapped content based on processed text data pub fn calculate_content_based_width(&self, ui: &egui::Ui) -> f32 { let processing_result = self.get_text_processing_result(); diff --git a/src/app/theme.rs b/src/app/theme.rs index 0a6dac4..afce1c2 100644 --- a/src/app/theme.rs +++ b/src/app/theme.rs @@ -261,7 +261,6 @@ fn build_custom_theme_plist( let mut settings_array = Vec::new(); - // Global settings let mut global_settings_dict = Dictionary::new(); let mut inner_global_settings = Dictionary::new(); inner_global_settings.insert("background".to_string(), Value::String(background_color.to_string())); @@ -269,7 +268,6 @@ fn build_custom_theme_plist( global_settings_dict.insert("settings".to_string(), Value::Dictionary(inner_global_settings)); settings_array.push(Value::Dictionary(global_settings_dict)); - // Comment scope let mut comment_scope_dict = Dictionary::new(); comment_scope_dict.insert("name".to_string(), Value::String("Comment".to_string())); comment_scope_dict.insert("scope".to_string(), Value::String("comment".to_string())); @@ -279,7 +277,6 @@ fn build_custom_theme_plist( comment_scope_dict.insert("settings".to_string(), Value::Dictionary(comment_settings)); settings_array.push(Value::Dictionary(comment_scope_dict)); - // String scope let mut string_scope_dict = Dictionary::new(); string_scope_dict.insert("name".to_string(), Value::String("String".to_string())); string_scope_dict.insert("scope".to_string(), Value::String("string".to_string())); @@ -288,7 +285,6 @@ fn build_custom_theme_plist( string_scope_dict.insert("settings".to_string(), Value::Dictionary(string_settings)); settings_array.push(Value::Dictionary(string_scope_dict)); - // Keyword scope let mut keyword_scope_dict = Dictionary::new(); keyword_scope_dict.insert("name".to_string(), Value::String("Keyword".to_string())); keyword_scope_dict.insert("scope".to_string(), Value::String("keyword".to_string())); @@ -297,8 +293,6 @@ fn build_custom_theme_plist( keyword_scope_dict.insert("settings".to_string(), Value::Dictionary(keyword_settings)); settings_array.push(Value::Dictionary(keyword_scope_dict)); - // Add more scopes as needed... - root_dict.insert("settings".to_string(), Value::Array(settings_array)); Value::Dictionary(root_dict) diff --git a/src/ui/central_panel/editor.rs b/src/ui/central_panel/editor.rs index 15ba191..1c0311e 100644 --- a/src/ui/central_panel/editor.rs +++ b/src/ui/central_panel/editor.rs @@ -158,7 +158,6 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R None }; - // Save state cache when content changes (after releasing the borrow) if content_changed { if let Err(e) = app.save_state_cache() { eprintln!("Failed to save state cache: {e}"); diff --git a/src/ui/preferences_window.rs b/src/ui/preferences_window.rs index 9576d82..b861684 100644 --- a/src/ui/preferences_window.rs +++ b/src/ui/preferences_window.rs @@ -32,9 +32,9 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) { ui.horizontal(|ui| { if ui - .checkbox(&mut app.state_cache, "State Cache") + .checkbox(&mut app.state_cache, "Cache State") .on_hover_text( - "Save and restore open tabs and unsaved changes between sessions" + "Unsaved changes will be cached between sessions" ) .changed() { @@ -56,6 +56,12 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) { { app.save_config(); } + if ui + .checkbox(&mut app.syntax_highlighting, "Syntax Highlighting") + .changed() + { + app.save_config(); + } }); ui.add_space(4.0); @@ -79,11 +85,6 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) { { app.save_config(); } - }); - - ui.add_space(4.0); - - ui.horizontal(|ui| { if ui .checkbox(&mut app.hide_tab_bar, "Hide Tab Bar") .on_hover_text("Hide the tab bar and show tab title in menu bar instead") @@ -91,6 +92,7 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) { { app.save_config(); } + }); ui.add_space(12.0);