diff --git a/client/src/components/common.rs b/client/src/components/common.rs index c3b3cc0..57a242a 100644 --- a/client/src/components/common.rs +++ b/client/src/components/common.rs @@ -5,7 +5,6 @@ pub mod text_editor; pub mod background; pub mod dialog; pub mod autocomplete; -pub mod search_palette; pub mod find_file_palette; pub use command_line::*; @@ -14,5 +13,4 @@ pub use text_editor::*; pub use background::*; pub use dialog::*; pub use autocomplete::*; -pub use search_palette::*; pub use find_file_palette::*; diff --git a/client/src/lib.rs b/client/src/lib.rs index cebe294..684343d 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -10,6 +10,7 @@ pub mod services; pub mod utils; pub mod buffer; pub mod sidebar; +pub mod search; pub use ui::run_ui; diff --git a/client/src/modes/handlers/event.rs b/client/src/modes/handlers/event.rs index fd85d42..1bd7c4e 100644 --- a/client/src/modes/handlers/event.rs +++ b/client/src/modes/handlers/event.rs @@ -3,6 +3,7 @@ use crate::config::binds::config::Config; use crate::config::binds::key_sequences::KeySequenceTracker; use crate::buffer::{AppView, BufferState, switch_buffer, functions, toggle_buffer_list}; use crate::sidebar::toggle_sidebar; +use crate::search::event::handle_search_palette_event; use crate::functions::modes::navigation::add_logic_nav; use crate::functions::modes::navigation::add_logic_nav::SaveLogicResultSender; use crate::functions::modes::navigation::add_table_nav::SaveTableResultSender; @@ -20,7 +21,6 @@ use crate::services::grpc_client::GrpcClient; use canvas::{FormEditor, AppMode as CanvasMode}; use crate::state::{ app::{ - search::SearchState, state::AppState, }, pages::{ @@ -30,6 +30,7 @@ use crate::state::{ intro::IntroState, }, }; +use crate::search::state::SearchState; use crate::tui::functions::common::login::LoginResult; use crate::tui::functions::common::register::RegisterResult; use crate::tui::{ @@ -215,132 +216,6 @@ impl EventHandler { } } - // This function handles state changes. - async fn handle_search_palette_event( - &mut self, - key_event: KeyEvent, - app_state: &mut AppState, - ) -> Result { - let mut should_close = false; - let mut outcome_message = String::new(); - let mut trigger_search = false; - - // Step 1: Handle search_state logic in a short scope - let (maybe_data, maybe_id) = { - if let Some(search_state) = app_state.search_state.as_mut() { - match key_event.code { - KeyCode::Esc => { - should_close = true; - outcome_message = "Search cancelled".to_string(); - (None, None) - } - KeyCode::Enter => { - if let Some(selected_hit) = - search_state.results.get(search_state.selected_index) - { - if let Ok(data) = serde_json::from_str::< - std::collections::HashMap, - >(&selected_hit.content_json) - { - (Some(data), Some(selected_hit.id)) - } else { - (None, None) - } - } else { - (None, None) - } - } - KeyCode::Up => { - search_state.previous_result(); - (None, None) - } - KeyCode::Down => { - search_state.next_result(); - (None, None) - } - KeyCode::Char(c) => { - search_state.input.insert(search_state.cursor_position, c); - search_state.cursor_position += 1; - trigger_search = true; - (None, None) - } - KeyCode::Backspace => { - if search_state.cursor_position > 0 { - search_state.cursor_position -= 1; - search_state.input.remove(search_state.cursor_position); - trigger_search = true; - } - (None, None) - } - KeyCode::Left => { - search_state.cursor_position = - search_state.cursor_position.saturating_sub(1); - (None, None) - } - KeyCode::Right => { - if search_state.cursor_position < search_state.input.len() { - search_state.cursor_position += 1; - } - (None, None) - } - _ => (None, None), - } - } else { - (None, None) - } - }; - - // Step 2: Now safe to borrow form_state - if let (Some(data), Some(id)) = (maybe_data, maybe_id) { - if let Some(fs) = app_state.form_state_mut() { - let detached_pos = fs.total_count + 2; - fs.update_from_response(&data, detached_pos); - } - should_close = true; - outcome_message = format!("Loaded record ID {}", id); - } - - // Step 3: Trigger async search if needed - if trigger_search { - if let Some(search_state) = app_state.search_state.as_mut() { - search_state.is_loading = true; - search_state.results.clear(); - search_state.selected_index = 0; - - let query = search_state.input.clone(); - let table_name = search_state.table_name.clone(); - let sender = self.search_result_sender.clone(); - let mut grpc_client = self.grpc_client.clone(); - - info!("--- 1. Spawning search task for query: '{}' ---", query); - tokio::spawn(async move { - info!("--- 2. Background task started. ---"); - match grpc_client.search_table(table_name, query).await { - Ok(response) => { - info!( - "--- 3a. gRPC call successful. Found {} hits. ---", - response.hits.len() - ); - let _ = sender.send(response.hits); - } - Err(e) => { - error!("--- 3b. gRPC call failed: {:?} ---", e); - let _ = sender.send(vec![]); - } - } - }); - } - } - - if should_close { - app_state.search_state = None; - app_state.ui.show_search_palette = false; - app_state.ui.focus_outside_canvas = false; - } - - Ok(EventOutcome::Ok(outcome_message)) - } - #[allow(clippy::too_many_arguments)] pub async fn handle_event( &mut self, @@ -358,9 +233,16 @@ impl EventHandler { ) -> Result { if app_state.ui.show_search_palette { if let Event::Key(key_event) = event { - return self.handle_search_palette_event(key_event, app_state).await; + if let Some(message) = handle_search_palette_event( + key_event, + app_state, + &mut self.grpc_client, + self.search_result_sender.clone(), + ).await? { + return Ok(EventOutcome::Ok(message)); + } + return Ok(EventOutcome::Ok(String::new())); } - return Ok(EventOutcome::Ok(String::new())); } let mut current_mode = diff --git a/client/src/modes/mod.rs b/client/src/modes/mod.rs index 1f60132..971fc8a 100644 --- a/client/src/modes/mod.rs +++ b/client/src/modes/mod.rs @@ -7,4 +7,3 @@ pub mod canvas; pub use handlers::*; pub use general::*; pub use common::*; -pub use canvas::*; diff --git a/client/src/search/event.rs b/client/src/search/event.rs new file mode 100644 index 0000000..33439f1 --- /dev/null +++ b/client/src/search/event.rs @@ -0,0 +1,107 @@ +// src/search/event.rs +use crate::search::state::SearchState; +use crate::state::app::state::AppState; +use crate::services::grpc_client::GrpcClient; +use common::proto::komp_ac::search::search_response::Hit; +use crossterm::event::KeyCode; +use tokio::sync::mpsc; +use tracing::{error, info}; +use std::collections::HashMap; +use anyhow::Result; + +pub async fn handle_search_palette_event( + key_event: crossterm::event::KeyEvent, + app_state: &mut AppState, + grpc_client: &mut GrpcClient, + search_result_sender: mpsc::UnboundedSender>, +) -> Result> { + let mut should_close = false; + let mut outcome_message = None; + let mut trigger_search = false; + + if let Some(search_state) = app_state.search_state.as_mut() { + match key_event.code { + KeyCode::Esc => { + should_close = true; + outcome_message = Some("Search cancelled".to_string()); + } + KeyCode::Enter => { + // Step 1: Extract the data we need while holding the borrow + let maybe_data = search_state + .results + .get(search_state.selected_index) + .map(|hit| (hit.id, hit.content_json.clone())); + + // Step 2: Process outside the borrow + if let Some((id, content_json)) = maybe_data { + if let Ok(data) = serde_json::from_str::>(&content_json) { + if let Some(fs) = app_state.form_state_mut() { + let detached_pos = fs.total_count + 2; + fs.update_from_response(&data, detached_pos); + } + should_close = true; + outcome_message = Some(format!("Loaded record ID {}", id)); + } + } + } + KeyCode::Up => search_state.previous_result(), + KeyCode::Down => search_state.next_result(), + KeyCode::Char(c) => { + search_state.input.insert(search_state.cursor_position, c); + search_state.cursor_position += 1; + trigger_search = true; + } + KeyCode::Backspace => { + if search_state.cursor_position > 0 { + search_state.cursor_position -= 1; + search_state.input.remove(search_state.cursor_position); + trigger_search = true; + } + } + KeyCode::Left => { + search_state.cursor_position = + search_state.cursor_position.saturating_sub(1); + } + KeyCode::Right => { + if search_state.cursor_position < search_state.input.len() { + search_state.cursor_position += 1; + } + } + _ => {} + } + } + + if trigger_search { + if let Some(search_state) = app_state.search_state.as_mut() { + search_state.is_loading = true; + search_state.results.clear(); + search_state.selected_index = 0; + + let query = search_state.input.clone(); + let table_name = search_state.table_name.clone(); + let sender = search_result_sender.clone(); + let mut grpc_client = grpc_client.clone(); + + info!("Spawning search task for query: '{}'", query); + tokio::spawn(async move { + match grpc_client.search_table(table_name, query).await { + Ok(response) => { + let _ = sender.send(response.hits); + } + Err(e) => { + error!("Search failed: {:?}", e); + let _ = sender.send(vec![]); + } + } + }); + } + } + + if should_close { + app_state.search_state = None; + app_state.ui.show_search_palette = false; + app_state.ui.focus_outside_canvas = false; + } + + Ok(outcome_message) +} diff --git a/client/src/search/mod.rs b/client/src/search/mod.rs new file mode 100644 index 0000000..b542c53 --- /dev/null +++ b/client/src/search/mod.rs @@ -0,0 +1,7 @@ +// src/search/mod.rs + +pub mod state; +pub mod ui; +pub mod event; + +pub use ui::*; diff --git a/client/src/state/app/search.rs b/client/src/search/state.rs similarity index 98% rename from client/src/state/app/search.rs rename to client/src/search/state.rs index 0ee31bd..eb243f6 100644 --- a/client/src/state/app/search.rs +++ b/client/src/search/state.rs @@ -1,4 +1,4 @@ -// src/state/app/search.rs +// src/search/state.rs use common::proto::komp_ac::search::search_response::Hit; diff --git a/client/src/components/common/search_palette.rs b/client/src/search/ui.rs similarity index 97% rename from client/src/components/common/search_palette.rs rename to client/src/search/ui.rs index feba96f..77861ab 100644 --- a/client/src/components/common/search_palette.rs +++ b/client/src/search/ui.rs @@ -1,7 +1,7 @@ -// src/components/common/search_palette.rs +// src/search/ui.rs use crate::config::colors::themes::Theme; -use crate::state::app::search::SearchState; +use crate::search::state::SearchState; use ratatui::{ layout::{Constraint, Direction, Layout, Rect}, style::{Modifier, Style}, diff --git a/client/src/state/app.rs b/client/src/state/app.rs index 4635f0c..2551055 100644 --- a/client/src/state/app.rs +++ b/client/src/state/app.rs @@ -1,4 +1,3 @@ // src/state/app.rs pub mod state; -pub mod search; diff --git a/client/src/state/app/state.rs b/client/src/state/app/state.rs index 1987d77..d26018a 100644 --- a/client/src/state/app/state.rs +++ b/client/src/state/app/state.rs @@ -5,7 +5,7 @@ use common::proto::komp_ac::table_definition::ProfileTreeResponse; // NEW: Import the types we need for the cache use common::proto::komp_ac::table_structure::TableStructureResponse; use crate::modes::handlers::mode_manager::AppMode; -use crate::state::app::search::SearchState; +use crate::search::state::SearchState; use crate::ui::handlers::context::DialogPurpose; use crate::state::pages::form::FormState; use crate::config::binds::Config; diff --git a/client/src/ui/handlers/render.rs b/client/src/ui/handlers/render.rs index 632ecfe..c4033ae 100644 --- a/client/src/ui/handlers/render.rs +++ b/client/src/ui/handlers/render.rs @@ -6,7 +6,6 @@ use crate::components::{ auth::{login::render_login, register::render_register}, common::dialog::render_dialog, common::find_file_palette, - common::search_palette::render_search_palette, intro::intro::render_intro, render_background, render_command_line, @@ -14,6 +13,7 @@ use crate::components::{ }; use crate::sidebar::{calculate_sidebar_layout, render_sidebar}; use crate::buffer::render_buffer_list; +use crate::search::render_search_palette; use crate::config::colors::themes::Theme; use crate::modes::general::command_navigation::NavigationState; use crate::buffer::state::BufferState;