From a3d230b8dfd7451b54e4d4278593390fe9004cc9 Mon Sep 17 00:00:00 2001 From: Nicolas Meylan Date: Fri, 12 Jul 2024 15:48:27 +0200 Subject: [PATCH] wip replace --- Cargo.lock | 1 - Cargo.toml | 2 +- src/array_table.rs | 17 ++++- src/main.rs | 52 +++++++++---- src/object_table.rs | 1 + src/panels.rs | 167 +++++++++++++++++++++++++++++++++++++++-- src/parser/mod.rs | 37 ++++++++- src/replace_panel.rs | 0 src/subtable_window.rs | 2 + 9 files changed, 253 insertions(+), 26 deletions(-) create mode 100644 src/replace_panel.rs diff --git a/Cargo.lock b/Cargo.lock index 9733e38..6d8d1a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1432,7 +1432,6 @@ dependencies = [ [[package]] name = "json-flat-parser" version = "0.1.0" -source = "git+https://github.com/nmeylan/json-parser-flat-format.git?rev=2a953c7#2a953c708fccb55e8f942e8d7d8f5af62f25da12" dependencies = [ "indexmap", "simdutf8", diff --git a/Cargo.toml b/Cargo.toml index ba1c448..4d12eaa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ eframe = { version = "0.28.1", default-features = false, features = [ "x11"] } egui = { version = "0.28.1", default-features = false, features = [] } egui_extras = { version = "0.28.1", default-features = false, features = ["svg"] } -json-flat-parser = {git = "https://github.com/nmeylan/json-parser-flat-format.git", rev = "2a953c7", features = ["indexmap", "simdutf8"]} +json-flat-parser = {git = "https://github.com/nmeylan/json-parser-flat-format.git", rev = "d6d52bf", features = ["indexmap", "simdutf8"]} rayon = {version = "1.10.0"} rfd = {version = "0.14.1"} indexmap = "2.2.6" diff --git a/src/array_table.rs b/src/array_table.rs index c1df6b5..957f75d 100644 --- a/src/array_table.rs +++ b/src/array_table.rs @@ -21,7 +21,8 @@ use crate::components::icon::ButtonWithIcon; use crate::components::popover::PopupMenu; use crate::components::table::{CellLocation, TableBody, TableRow}; use crate::fonts::{COPY, FILTER, PENCIL, PLUS, TABLE, TABLE_CELLS, THUMBTACK}; -use crate::parser::{row_number_entry, search_occurrences}; +use crate::panels::SearchReplaceResponse; +use crate::parser::{replace_occurrences, row_number_entry, search_occurrences}; use crate::subtable_window::SubTable; #[derive(Clone, Debug)] @@ -31,6 +32,7 @@ pub struct Column { pub value_type: ValueType, pub seen_count: usize, pub order: usize, + pub id: usize, } impl Hash for Column { @@ -47,6 +49,7 @@ impl Column { value_type, seen_count: 0, order: 0, + id: 0, } } } @@ -348,6 +351,7 @@ impl ArrayTable { value_type: Default::default(), seen_count: 0, order: 0, + id: 0, }) } None @@ -611,6 +615,7 @@ impl ArrayTable { value_type: columns[col_index].value_type, depth: columns[col_index].depth, position: 0, + column_id: columns[col_index].id, }; updated_value = Some((pointer, mem::take(ref_mut))) } else { @@ -824,6 +829,7 @@ impl ArrayTable { value_type: ValueType::Array(self.nodes.len()), depth: 0, position: 0, + column_id: 0, }; entries.push(FlatJsonValue { pointer: parent_pointer.clone(), value: None }); let updated_array = serialize_to_json_with_option::(&mut entries, updated_pointer.depth - 1).to_json(); @@ -873,6 +879,7 @@ impl ArrayTable { value_type: ValueType::Object(true), depth, position: 0, + column_id: 0, }, value: Some("{}".to_string()) } ], index: new_index @@ -1057,7 +1064,7 @@ impl ArrayTable { let columns = self.columns(cell_location.is_pinned_column_table); let pointer = Self::pointer_key(&self.parent_pointer, row_index, &columns.get(cell_location.column_index).as_ref().unwrap().name); let flat_json_value = FlatJsonValue:: { - pointer: PointerKey { pointer, value_type: columns[cell_location.column_index].value_type, depth: columns[cell_location.column_index].depth, position: 0, }, + pointer: PointerKey { pointer, value_type: columns[cell_location.column_index].value_type, depth: columns[cell_location.column_index].depth, position: 0, column_id: columns[cell_location.column_index].id }, value: None, }; self.update_value(flat_json_value, row_index, !self.is_sub_table); @@ -1071,6 +1078,7 @@ impl ArrayTable { value_type: columns[cell_location.column_index].value_type, depth: columns[cell_location.column_index].depth, position: 0, + column_id: columns[cell_location.column_index].id, }, value: Some(v.clone()), }; @@ -1096,4 +1104,9 @@ impl ArrayTable { ui.ctx().copy_text(value.clone()); } } + + pub fn replace_columns(&mut self, search_replace_response: SearchReplaceResponse) { + replace_occurrences(&mut self.nodes, search_replace_response); + self.cache.borrow_mut().evict(); + } } diff --git a/src/main.rs b/src/main.rs index 19413d5..baab986 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,9 +9,10 @@ pub mod fonts; mod web; mod compatibility; mod panels; +mod replace_panel; use std::{env, mem}; - +use std::any::Any; use std::collections::{BTreeSet}; use std::fs::File; use std::io::Read; @@ -25,13 +26,13 @@ use eframe::{CreationContext, Renderer}; use eframe::egui::Context; use eframe::Theme::Light; use eframe::egui::{Align, Align2, Button, Color32, ComboBox, CursorIcon, Id, Key, KeyboardShortcut, Label, LayerId, Layout, Modifiers, Order, RichText, Sense, Separator, TextEdit, TextStyle, Vec2, Widget}; - +use eframe::epaint::text::TextWrapMode; use json_flat_parser::{FlatJsonValue, JSONParser, ParseOptions, ValueType}; use crate::array_table::{ArrayTable, ScrollToRowMode}; use crate::components::icon; use crate::components::table::HoverData; use crate::fonts::{CHEVRON_DOWN, CHEVRON_UP}; -use crate::panels::AboutPanel; +use crate::panels::{AboutPanel, SearchReplacePanel}; use crate::parser::save_to_file; pub const ACTIVE_COLOR: Color32 = Color32::from_rgb(63, 142, 252); @@ -48,7 +49,7 @@ pub trait View { } /// Something to view -pub trait Window { +pub trait Window { /// Is the demo enabled for this integration? fn is_enabled(&self, _ctx: &eframe::egui::Context) -> bool { true @@ -58,7 +59,9 @@ pub trait Window { fn name(&self) -> &'static str; /// Show windows, etc - fn show(&mut self, ctx: &eframe::egui::Context, open: &mut bool); + fn show(&mut self, ctx: &eframe::egui::Context, open: &mut bool) -> R; + fn as_any(&self) -> &dyn Any; + fn as_any_mut(&mut self) -> &mut dyn Any; } #[derive(Default, Clone)] @@ -117,8 +120,9 @@ fn main() { struct MyApp { frame_history: FrameHistory, table: Option, - windows: Vec>, open: BTreeSet, + about_panel: AboutPanel, + search_replace_panel: SearchReplacePanel, max_depth: u8, depth: u8, selected_file: Option, @@ -150,9 +154,10 @@ impl MyApp { Self { frame_history: FrameHistory::default(), table: None, - windows: vec![Box::::default()], - max_depth: 0, open: Default::default(), + about_panel: Default::default(), + search_replace_panel: Default::default(), + max_depth: 0, depth: 0, selected_file: None, parsing_invalid: false, @@ -166,11 +171,16 @@ impl MyApp { } } pub fn windows(&mut self, ctx: &Context) { - let Self { windows, open, .. } = self; - for window in windows { - let mut is_open = open.contains(window.name()); - window.show(ctx, &mut is_open); - set_open(open, window.name(), is_open); + let Self { open, .. } = self; + let mut is_open = open.contains(self.about_panel.name()); + self.about_panel.show(ctx, &mut is_open); + set_open(open, self.about_panel.name(), is_open); + let mut is_open = open.contains(self.search_replace_panel.name()); + let response = self.search_replace_panel.show(ctx, &mut is_open); + set_open(open, self.search_replace_panel.name(), is_open); + if let Some(search_replace_response) = response { + let table = self.table.as_mut().unwrap(); + table.replace_columns(search_replace_response); } } @@ -321,6 +331,11 @@ impl MyApp { self.unsaved_changes = false; } } + + fn open_replace_panel(&mut self) { + set_open(&mut self.open, "Replace", true); + self.search_replace_panel.set_columns(self.table.as_ref().unwrap().all_columns().clone()); + } } fn set_open(open: &mut BTreeSet, key: &'static str, is_open: bool) { @@ -356,7 +371,7 @@ impl eframe::App for MyApp { #[cfg(not(target_arch = "wasm32"))] { ui.menu_button("File", |ui| { ui.set_min_width(220.0); - ui.style_mut().wrap = Some(false); + ui.style_mut().wrap_mode = Some(TextWrapMode::Extend); if ui.button("Open json file").clicked() { ui.close_menu(); self.file_picker(); @@ -375,6 +390,15 @@ impl eframe::App for MyApp { } }); } + + ui.separator(); + ui.menu_button("Edit", |ui| { + ui.set_min_width(220.0); + if ui.button("Replace").clicked() { + ui.close_menu(); + self.open_replace_panel(); + } + }); } if let Some(ref mut table) = self.table { ui.separator(); diff --git a/src/object_table.rs b/src/object_table.rs index a683e7e..3e06234 100644 --- a/src/object_table.rs +++ b/src/object_table.rs @@ -173,6 +173,7 @@ impl ObjectTable { value_type: ValueType::Array(array_entries.len()), depth: 0, position: 0, + column_id: 0, }; array_entries.push(FlatJsonValue { pointer: parent_pointer, value: None }); let updated_array = serialize_to_json_with_option::(&mut array_entries, depth + 1).to_json(); diff --git a/src/panels.rs b/src/panels.rs index 076ac8f..789e85f 100644 --- a/src/panels.rs +++ b/src/panels.rs @@ -1,15 +1,46 @@ +use std::any::Any; +use std::cell::RefCell; use eframe::egui::Context; use eframe::egui::{Ui}; +use eframe::emath::Align; +use egui::{Button, Grid, Layout, RichText, Sense, TextEdit}; +use json_flat_parser::ValueType; +use crate::ACTIVE_COLOR; +use crate::array_table::Column; +use crate::components::popover::PopupMenu; +use crate::panels::ReplaceMode::Simple; #[derive(Default)] -pub struct AboutPanel { - enabled: bool, - visible: bool, +pub struct AboutPanel {} + +#[derive(Default)] +pub struct SearchReplacePanel { + search_criteria: String, + replace_value: String, + selected_columns: RefCell>, + columns: Vec, + replace_mode: ReplaceMode, +} +#[derive(Clone)] +pub enum ReplaceMode { + Simple, + Regex, +} +impl Default for ReplaceMode { + fn default() -> Self { + Simple + } } +pub struct SearchReplaceResponse { + pub search_criteria: String, + pub replace_value: String, + pub selected_column: Option>, + pub replace_mode: ReplaceMode, +} -impl super::Window for AboutPanel { +impl super::Window<()> for AboutPanel { fn name(&self) -> &'static str { "About" } @@ -25,6 +56,14 @@ impl super::Window for AboutPanel { self.ui(ui); }); } + + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } } impl super::View<()> for AboutPanel { @@ -37,4 +76,122 @@ impl super::View<()> for AboutPanel { ui.hyperlink_to("egui project and its community", "https://github.com/emilk/egui"); ui.hyperlink_to("Maintainers of dependencies used by this project", "https://github.com/nmeylan/json-table-editor/blob/master/Cargo.lock"); } -} \ No newline at end of file +} + +impl SearchReplacePanel { + pub fn set_columns(&mut self, columns: Vec) { + self.columns = columns; + } +} +impl super::Window> for SearchReplacePanel { + fn name(&self) -> &'static str { + "Replace" + } + + fn show(&mut self, ctx: &Context, open: &mut bool) -> Option { + let maybe_inner_response = egui::Window::new(self.name()) + .collapsible(true) + .open(open) + .resizable([true, true]) + .default_width(280.0) + .show(ctx, |ui| { + use super::View as _; + self.ui(ui) + }); + + if let Some(inner_response) = maybe_inner_response { + if let Some(inner_response2) = inner_response.inner { + return inner_response2; + } + } + None + } + + fn as_any(&self) -> &dyn Any { + self + } + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } +} + +impl super::View> for SearchReplacePanel { + fn ui(&mut self, ui: &mut Ui) -> Option { + let search = TextEdit::singleline(&mut self.search_criteria); + let replace = TextEdit::singleline(&mut self.replace_value); + let mut button = Button::new("Replace"); + let grid_response = Grid::new("replace_panel:grid") + .num_columns(2) + .spacing([12.0, 8.0]) + .striped(false) + .show(ui, |ui| { + ui.label("Search: "); + ui.add(search); + ui.end_row(); + ui.label("Replace: "); + ui.add(replace); + ui.end_row(); + ui.label("Column: "); + PopupMenu::new("select_column_to_replace") + .show_ui(ui, |ui| ui.add(Button::new(format!("{} column(s) selected", self.selected_columns.borrow().len()))), |ui| { + for col in self.columns.iter().filter(|c| !(matches!(c.value_type, ValueType::Array(_)) || matches!(c.value_type, ValueType::Object(_)))) { + if col.name.is_empty() { + continue; + } + let mut chcked = false; + + for selected in self.selected_columns.borrow().iter() { + if selected.eq(col) { + chcked = true; + break; + } + } + if ui.checkbox(&mut chcked, col.name.as_str()).clicked() { + if self.selected_columns.borrow().contains(col) { + self.selected_columns.borrow_mut().retain(|c| !c.eq(col)); + } else { + self.selected_columns.borrow_mut().push(col.clone()); + } + } + } + }); + + ui.end_row(); + ui.label(""); + let mut text = RichText::new(".*"); + if matches!(self.replace_mode, ReplaceMode::Regex) { + text = text.color(ACTIVE_COLOR); + } + let enable_regex = Button::new(text); + let mut replace_response = ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + if self.selected_columns.borrow().len() == 0 { + button = button.sense(Sense::hover()); + } + let response_button_replace = ui.add(button); + let mut response = ui.add(enable_regex); + response = response.on_hover_ui(|ui| { ui.label("Regex"); }); + if response.clicked() { + if matches!(self.replace_mode, ReplaceMode::Regex) { + self.replace_mode = ReplaceMode::Simple; + } else { + self.replace_mode = ReplaceMode::Regex; + } + } + response_button_replace + }).inner; + ui.end_row(); + + replace_response + }); + if grid_response.inner.clicked() { + return Some(SearchReplaceResponse { + search_criteria: self.search_criteria.clone(), + replace_value: self.replace_value.clone(), + replace_mode: self.replace_mode.clone(), + selected_column: Some(self.selected_columns.borrow().clone()), + }) + } + None + // return grid_response.inner + } +} diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 80be470..a861d66 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; use std::{fs, mem}; - +use std::hash::{DefaultHasher, Hasher}; use std::io::{BufWriter, Write}; use std::path::Path; use std::sync::{Arc, Mutex}; @@ -12,6 +12,8 @@ use rayon::iter::ParallelIterator; use rayon::iter::IntoParallelIterator; use rayon::prelude::{ParallelSliceMut}; use crate::array_table::{Column, NON_NULL_FILTER_VALUE}; +use crate::panels::{ReplaceMode, SearchReplaceResponse}; + #[macro_export] macro_rules! concat_string { () => { String::with_capacity(0) }; @@ -72,6 +74,7 @@ pub fn change_depth_array(previous_parse_result: ParseResult, mut json_a value_type: entry.pointer.value_type, seen_count: 0, order: unique_keys.len(), + id: unique_keys.len(), }; if let Some(column) = unique_keys.iter_mut().find(|c| c.eq(&&column)) { column.seen_count += 1; @@ -119,7 +122,7 @@ pub fn as_array(mut previous_parse_result: ParseResult) -> Result<(Vec) -> Result<(Vec], te } res } + +pub fn replace_occurrences(previous_parse_result: &mut Vec>, search_replace_response: SearchReplaceResponse) { + let column_ids = if let Some(selected_columns) = search_replace_response.selected_column { + selected_columns.iter().map(|c| c.id).collect::>() + } else { + vec![] + }; + for json_array_entry in previous_parse_result.iter_mut() { + for entry in json_array_entry.entries.iter_mut() { + if column_ids.contains(&entry.pointer.column_id) { + if let Some(ref mut value) = entry.value { + match search_replace_response.replace_mode { + ReplaceMode::Simple => { + *value = value.replace(search_replace_response.search_criteria.as_str(), search_replace_response.replace_value.as_str()); + } + ReplaceMode::Regex => {} + } + } + } + } + } +} diff --git a/src/replace_panel.rs b/src/replace_panel.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/subtable_window.rs b/src/subtable_window.rs index 7f24adb..4128bef 100644 --- a/src/subtable_window.rs +++ b/src/subtable_window.rs @@ -1,4 +1,5 @@ use eframe::egui::{Context, Ui}; +use egui::Order; use json_flat_parser::{FlatJsonValue, ParseOptions, ParseResult, PointerKey, ValueType}; use json_flat_parser::lexer::Lexer; use json_flat_parser::parser::Parser; @@ -88,6 +89,7 @@ impl SubTable { }; r.default_height(40.0 + nodes as f32 * ArrayTable::row_height(&ctx.style(), &ctx.style().spacing)).default_width( 480.0) }) + .order(Order::Middle) .resizable([true, true]) .show(ctx, |ui| { let id = self.name().to_string();