// src/editor.rs //! Main API for the canvas library - FormEditor with library-owned state #[cfg(feature = "cursor-style")] use crate::canvas::CursorManager; use anyhow::Result; use crate::canvas::state::EditorState; use crate::data_provider::{DataProvider, SuggestionsProvider, SuggestionItem}; use crate::canvas::modes::AppMode; use crate::canvas::state::SelectionState; /// Main editor that manages UI state internally and delegates data to user pub struct FormEditor { // Library owns all UI state ui_state: EditorState, // User owns business data data_provider: D, // Autocomplete suggestions (library manages UI, user provides data) pub(crate) suggestions: Vec, } impl FormEditor { pub fn new(data_provider: D) -> Self { let mut editor = Self { ui_state: EditorState::new(), data_provider, suggestions: Vec::new(), }; // Initialize validation configurations if validation feature is enabled #[cfg(feature = "validation")] { editor.initialize_validation(); } editor } /// Compute inline completion for current selection and current text. fn compute_current_completion(&self) -> Option { let typed = self.current_text(); let idx = self.ui_state.suggestions.selected_index?; let sugg = self.suggestions.get(idx)?; if let Some(rest) = sugg.value_to_store.strip_prefix(typed) { if !rest.is_empty() { return Some(rest.to_string()); } } None } /// Update UI state's completion text from current selection pub fn update_inline_completion(&mut self) { self.ui_state.suggestions.completion_text = self.compute_current_completion(); } /// Initialize validation configurations from data provider #[cfg(feature = "validation")] fn initialize_validation(&mut self) { let field_count = self.data_provider.field_count(); for field_index in 0..field_count { if let Some(config) = self.data_provider.validation_config(field_index) { self.ui_state.validation.set_field_config(field_index, config); } } } // =================================================================== // READ-ONLY ACCESS: User can fetch UI state // =================================================================== /// Get current field index (for user's compatibility) pub fn current_field(&self) -> usize { self.ui_state.current_field() } /// Get current cursor position (for user's compatibility) pub fn cursor_position(&self) -> usize { self.ui_state.cursor_position() } /// Get current mode (for user's mode-dependent logic) pub fn mode(&self) -> AppMode { self.ui_state.mode() } /// Check if suggestions dropdown is active (for user's logic) pub fn is_suggestions_active(&self) -> bool { self.ui_state.is_suggestions_active() } /// Get current field text (convenience method) pub fn current_text(&self) -> &str { let field_index = self.ui_state.current_field; if field_index < self.data_provider.field_count() { self.data_provider.field_value(field_index) } else { "" } } /// Get current field text for display. /// /// Policies: /// - Feature 4 (custom formatter): /// - While editing the focused field: ALWAYS show raw (no custom formatting). /// - When not editing the field: show formatted (fallback to raw on error). /// - Mask-only fields: mask applies even in Edit mode (preserve legacy behavior). /// - Otherwise: raw. #[cfg(feature = "validation")] pub fn current_display_text(&self) -> String { let field_index = self.ui_state.current_field; let raw = if field_index < self.data_provider.field_count() { self.data_provider.field_value(field_index) } else { "" }; if let Some(cfg) = self.ui_state.validation.get_field_config(field_index) { // 1) Mask-only fields: mask applies even in Edit (legacy behavior) if cfg.custom_formatter.is_none() { if let Some(mask) = &cfg.display_mask { return mask.apply_to_display(raw); } } // 2) Feature 4 fields: raw while editing, formatted otherwise if cfg.custom_formatter.is_some() { if matches!(self.ui_state.current_mode, AppMode::Edit) { return raw.to_string(); } if let Some((formatted, _mapper, _warning)) = cfg.run_custom_formatter(raw) { return formatted; } } // 3) Fallback to mask if present (when formatter didn't produce output) if let Some(mask) = &cfg.display_mask { return mask.apply_to_display(raw); } } raw.to_string() } /// Get reference to UI state for rendering pub fn ui_state(&self) -> &EditorState { &self.ui_state } /// Mutable access to UI state for internal crate use only. pub(crate) fn ui_state_mut(&mut self) -> &mut EditorState { &mut self.ui_state } /// Open the suggestions UI for `field_index` (UI-only; does not fetch). pub fn open_suggestions(&mut self, field_index: usize) { self.ui_state.open_suggestions(field_index); } /// Close suggestions UI and clear the current suggestion results. pub fn close_suggestions(&mut self) { self.ui_state.close_suggestions(); self.suggestions.clear(); } /// Set external validation state for a field (Feature 5) #[cfg(feature = "validation")] pub fn set_external_validation( &mut self, field_index: usize, state: crate::validation::ExternalValidationState, ) { self.ui_state .validation .set_external_validation(field_index, state); } /// Clear external validation state for a field (Feature 5) #[cfg(feature = "validation")] pub fn clear_external_validation(&mut self, field_index: usize) { self.ui_state.validation.clear_external_validation(field_index); } /// Get effective display text for any field index. /// /// Policies: /// - Feature 4 fields (with custom formatter): /// - If the field is currently focused AND in Edit mode: return raw (no formatting). /// - Otherwise: return formatted (fallback to raw on error). /// - Mask-only fields: mask applies regardless of mode (legacy behavior). /// - Otherwise: raw. #[cfg(feature = "validation")] pub fn display_text_for_field(&self, field_index: usize) -> String { let raw = if field_index < self.data_provider.field_count() { self.data_provider.field_value(field_index) } else { "" }; if let Some(cfg) = self.ui_state.validation.get_field_config(field_index) { // Mask-only fields: mask applies even in Edit mode if cfg.custom_formatter.is_none() { if let Some(mask) = &cfg.display_mask { return mask.apply_to_display(raw); } } // Feature 4 fields: if cfg.custom_formatter.is_some() { // Focused + Edit -> raw if field_index == self.ui_state.current_field && matches!(self.ui_state.current_mode, AppMode::Edit) { return raw.to_string(); } // Not editing -> formatted if let Some((formatted, _mapper, _warning)) = cfg.run_custom_formatter(raw) { return formatted; } } // Fallback to mask if present (in case formatter didn't return output) if let Some(mask) = &cfg.display_mask { return mask.apply_to_display(raw); } } raw.to_string() } /// Get reference to data provider for rendering pub fn data_provider(&self) -> &D { &self.data_provider } /// Get autocomplete suggestions for rendering (read-only) pub fn suggestions(&self) -> &[SuggestionItem] { &self.suggestions } /// Get validation state (for user's business logic) /// Only available when the 'validation' feature is enabled #[cfg(feature = "validation")] pub fn validation_state(&self) -> &crate::validation::ValidationState { self.ui_state.validation_state() } /// Get validation result for current field #[cfg(feature = "validation")] pub fn current_field_validation(&self) -> Option<&crate::validation::ValidationResult> { self.ui_state.validation.get_field_result(self.ui_state.current_field) } /// Get validation result for specific field #[cfg(feature = "validation")] pub fn field_validation(&self, field_index: usize) -> Option<&crate::validation::ValidationResult> { self.ui_state.validation.get_field_result(field_index) } // =================================================================== // SYNC OPERATIONS: No async needed for basic editing // =================================================================== /// Handle character insertion with proper mask/limit coordination pub fn insert_char(&mut self, ch: char) -> Result<()> { if self.ui_state.current_mode != AppMode::Edit { return Ok(()); // Ignore in non-edit modes } let field_index = self.ui_state.current_field; let raw_cursor_pos = self.ui_state.cursor_pos; let current_raw_text = self.data_provider.field_value(field_index); // 🔥 CRITICAL FIX 1: Check mask constraints FIRST #[cfg(feature = "validation")] { if let Some(cfg) = self.ui_state.validation.get_field_config(field_index) { if let Some(mask) = &cfg.display_mask { // Get display cursor position let display_cursor_pos = mask.raw_pos_to_display_pos(raw_cursor_pos); // ❌ PREVENT BUG: Reject input if cursor is beyond mask pattern if display_cursor_pos >= mask.pattern().len() { tracing::debug!( "Character insertion rejected: cursor beyond mask pattern length" ); return Ok(()); // Silently reject - user can't type beyond mask } // ❌ PREVENT BUG: Reject input if cursor is on a separator position if !mask.is_input_position(display_cursor_pos) { tracing::debug!( "Character insertion rejected: cursor on separator position {}", display_cursor_pos ); return Ok(()); // Silently reject - can't type on separators } // ❌ PREVENT BUG: Check if we're at max input positions for this mask let input_char_count = (0..mask.pattern().len()) .filter(|&pos| mask.is_input_position(pos)) .count(); if current_raw_text.len() >= input_char_count { tracing::debug!( "Character insertion rejected: mask pattern full ({} input positions)", input_char_count ); return Ok(()); // Silently reject - mask is full } } } } // 🔥 CRITICAL FIX 2: Validate character insertion with mask awareness #[cfg(feature = "validation")] { let validation_result = self.ui_state.validation.validate_char_insertion( field_index, current_raw_text, raw_cursor_pos, ch, ); // Reject input if validation failed with error if !validation_result.is_acceptable() { tracing::debug!( "Character insertion rejected for field {}: {:?}", field_index, validation_result ); return Ok(()); // Silently reject invalid input } } // 🔥 CRITICAL FIX 3: Validate the insertion won't break display/limit coordination let new_raw_text = { let mut temp = current_raw_text.to_string(); temp.insert(raw_cursor_pos, ch); temp }; #[cfg(feature = "validation")] { if let Some(cfg) = self.ui_state.validation.get_field_config(field_index) { // Check character limits on the new raw text if let Some(limits) = &cfg.character_limits { if let Some(result) = limits.validate_content(&new_raw_text) { if !result.is_acceptable() { tracing::debug!( "Character insertion rejected: would exceed character limits" ); return Ok(()); // Silently reject - would exceed limits } } } // Check that mask can handle the new raw text length if let Some(mask) = &cfg.display_mask { let input_positions = (0..mask.pattern().len()) .filter(|&pos| mask.is_input_position(pos)) .count(); if new_raw_text.len() > input_positions { tracing::debug!( "Character insertion rejected: raw text length {} exceeds mask input positions {}", new_raw_text.len(), input_positions ); return Ok(()); // Silently reject - mask can't handle this length } } } } // ✅ ALL CHECKS PASSED: Safe to insert character self.data_provider.set_field_value(field_index, new_raw_text); // 🔥 CRITICAL FIX 4: Update cursor position correctly for mask context #[cfg(feature = "validation")] { if let Some(cfg) = self.ui_state.validation.get_field_config(field_index) { if let Some(mask) = &cfg.display_mask { // Move to next input position, skipping separators let new_raw_pos = raw_cursor_pos + 1; let display_pos = mask.raw_pos_to_display_pos(new_raw_pos); let next_input_pos = mask.next_input_position(display_pos); let next_raw_pos = mask.display_pos_to_raw_pos(next_input_pos); self.ui_state.cursor_pos = next_raw_pos; self.ui_state.ideal_cursor_column = next_raw_pos; return Ok(()); } } } // No mask: simple increment self.ui_state.cursor_pos = raw_cursor_pos + 1; self.ui_state.ideal_cursor_column = self.ui_state.cursor_pos; Ok(()) } /// Handle cursor movement left - skips mask separator positions pub fn move_left(&mut self) { if self.ui_state.cursor_pos == 0 { return; } let field_index = self.ui_state.current_field; let mut new_pos = self.ui_state.cursor_pos - 1; // Skip mask separator positions if configured #[cfg(feature = "validation")] if let Some(cfg) = self.ui_state.validation.get_field_config(field_index) { if let Some(mask) = &cfg.display_mask { // Convert to display position, find previous input position, convert back let display_pos = mask.raw_pos_to_display_pos(new_pos); if let Some(prev_input_display_pos) = mask.prev_input_position(display_pos) { new_pos = mask.display_pos_to_raw_pos(prev_input_display_pos); } } } self.ui_state.cursor_pos = new_pos; self.ui_state.ideal_cursor_column = self.ui_state.cursor_pos; } /// Handle cursor movement right - skips mask separator positions pub fn move_right(&mut self) { let current_text = self.current_text(); let is_edit_mode = self.ui_state.current_mode == AppMode::Edit; let max_pos = if is_edit_mode { current_text.len() } else { current_text.len().saturating_sub(1) }; if self.ui_state.cursor_pos >= max_pos { return; } let field_index = self.ui_state.current_field; let mut new_pos = self.ui_state.cursor_pos + 1; // Skip mask separator positions if configured #[cfg(feature = "validation")] if let Some(cfg) = self.ui_state.validation.get_field_config(field_index) { if let Some(mask) = &cfg.display_mask { // Convert to display position, find next input position, convert back let display_pos = mask.raw_pos_to_display_pos(new_pos); let next_input_display_pos = mask.next_input_position(display_pos); new_pos = mask.display_pos_to_raw_pos(next_input_display_pos); new_pos = new_pos.min(max_pos); } } if new_pos <= max_pos { self.ui_state.cursor_pos = new_pos; self.ui_state.ideal_cursor_column = self.ui_state.cursor_pos; } } /// Handle field navigation pub fn move_to_next_field(&mut self) { let field_count = self.data_provider.field_count(); let next_field = (self.ui_state.current_field + 1) % field_count; // Validate current field content before moving if validation is enabled #[cfg(feature = "validation")] { let current_text = self.current_text().to_string(); // Convert to String to avoid borrow conflicts let _validation_result = self.ui_state.validation.validate_field_content( self.ui_state.current_field, ¤t_text, ); // Note: We don't prevent field switching on validation failure, // just record the validation state } self.ui_state.move_to_field(next_field, field_count); // Clamp cursor to new field let current_text = self.current_text(); let max_pos = current_text.len(); self.ui_state.set_cursor( self.ui_state.ideal_cursor_column, max_pos, self.ui_state.current_mode == AppMode::Edit ); } /// Change mode (for vim compatibility) pub fn set_mode(&mut self, mode: AppMode) { match (self.ui_state.current_mode, mode) { // Entering highlight mode from read-only (AppMode::ReadOnly, AppMode::Highlight) => { self.enter_highlight_mode(); } // Exiting highlight mode (AppMode::Highlight, AppMode::ReadOnly) => { self.exit_highlight_mode(); } // Other transitions (_, new_mode) => { self.ui_state.current_mode = new_mode; if new_mode != AppMode::Highlight { self.ui_state.selection = SelectionState::None; } #[cfg(feature = "cursor-style")] { let _ = CursorManager::update_for_mode(new_mode); } } } } /// Enter edit mode with cursor positioned for append (vim 'a' command) pub fn enter_append_mode(&mut self) { let current_text = self.current_text(); // Calculate append position: always move right, even at line end let append_pos = if current_text.is_empty() { 0 } else { (self.ui_state.cursor_pos + 1).min(current_text.len()) }; // Set cursor position for append self.ui_state.cursor_pos = append_pos; self.ui_state.ideal_cursor_column = append_pos; // Enter edit mode (which will update cursor style) self.set_mode(AppMode::Edit); } // =================================================================== // VALIDATION METHODS (only available with validation feature) // =================================================================== /// Enable or disable validation #[cfg(feature = "validation")] pub fn set_validation_enabled(&mut self, enabled: bool) { self.ui_state.validation.set_enabled(enabled); } /// Check if validation is enabled #[cfg(feature = "validation")] pub fn is_validation_enabled(&self) -> bool { self.ui_state.validation.is_enabled() } /// Set validation configuration for a specific field #[cfg(feature = "validation")] pub fn set_field_validation(&mut self, field_index: usize, config: crate::validation::ValidationConfig) { self.ui_state.validation.set_field_config(field_index, config); } /// Remove validation configuration for a specific field #[cfg(feature = "validation")] pub fn remove_field_validation(&mut self, field_index: usize) { self.ui_state.validation.remove_field_config(field_index); } /// Manually validate current field content #[cfg(feature = "validation")] pub fn validate_current_field(&mut self) -> crate::validation::ValidationResult { let field_index = self.ui_state.current_field; let current_text = self.current_text().to_string(); self.ui_state.validation.validate_field_content(field_index, ¤t_text) } /// Manually validate specific field content #[cfg(feature = "validation")] pub fn validate_field(&mut self, field_index: usize) -> Option { if field_index < self.data_provider.field_count() { let text = self.data_provider.field_value(field_index).to_string(); Some(self.ui_state.validation.validate_field_content(field_index, &text)) } else { None } } /// Clear validation results for all fields #[cfg(feature = "validation")] pub fn clear_validation_results(&mut self) { self.ui_state.validation.clear_all_results(); } /// Get validation summary for all fields #[cfg(feature = "validation")] pub fn validation_summary(&self) -> crate::validation::ValidationSummary { self.ui_state.validation.summary() } /// Check if field switching is allowed from current field #[cfg(feature = "validation")] pub fn can_switch_fields(&self) -> bool { let current_text = self.current_text(); self.ui_state.validation.allows_field_switch(self.ui_state.current_field, current_text) } /// Get reason why field switching is blocked (if any) #[cfg(feature = "validation")] pub fn field_switch_block_reason(&self) -> Option { let current_text = self.current_text(); self.ui_state.validation.field_switch_block_reason(self.ui_state.current_field, current_text) } // =================================================================== // ASYNC OPERATIONS: Only suggestions need async // =================================================================== /// Trigger suggestions (async because it fetches data) pub async fn trigger_suggestions(&mut self, provider: &mut A) -> Result<()> where A: SuggestionsProvider, { let field_index = self.ui_state.current_field; if !self.data_provider.supports_suggestions(field_index) { return Ok(()); } // Activate suggestions UI self.ui_state.activate_suggestions(field_index); // Fetch suggestions from user (no conversion needed!) let query = self.current_text(); self.suggestions = provider.fetch_suggestions(field_index, query).await?; // Update UI state self.ui_state.suggestions.is_loading = false; if !self.suggestions.is_empty() { self.ui_state.suggestions.selected_index = Some(0); // Compute initial inline completion from first suggestion self.update_inline_completion(); } Ok(()) } /// Navigate suggestions pub fn suggestions_next(&mut self) { if !self.ui_state.suggestions.is_active || self.suggestions.is_empty() { return; } let current = self.ui_state.suggestions.selected_index.unwrap_or(0); let next = (current + 1) % self.suggestions.len(); self.ui_state.suggestions.selected_index = Some(next); // Update inline completion to reflect new highlighted item self.update_inline_completion(); } /// Apply selected suggestion /// Apply selected suggestion pub fn apply_suggestion(&mut self) -> Option { if let Some(selected_index) = self.ui_state.suggestions.selected_index { if let Some(suggestion) = self.suggestions.get(selected_index).cloned() { let field_index = self.ui_state.current_field; // Apply to user's data self.data_provider.set_field_value( field_index, suggestion.value_to_store.clone() ); // Update cursor position self.ui_state.cursor_pos = suggestion.value_to_store.len(); self.ui_state.ideal_cursor_column = self.ui_state.cursor_pos; // Close suggestions self.ui_state.deactivate_suggestions(); self.suggestions.clear(); // Validate the new content if validation is enabled #[cfg(feature = "validation")] { let _validation_result = self.ui_state.validation.validate_field_content( field_index, &suggestion.value_to_store, ); } return Some(suggestion.display_text); } } None } // =================================================================== // MOVEMENT METHODS (keeping existing implementations) // =================================================================== /// Move to previous field (vim k / up arrow) pub fn move_up(&mut self) -> Result<()> { let field_count = self.data_provider.field_count(); if field_count == 0 { return Ok(()); } // Skip computed fields during navigation when feature enabled #[cfg(feature = "computed")] { if let Some(computed_state) = &self.ui_state.computed { // Find previous non-computed field let mut candidate = self.ui_state.current_field; for _ in 0..field_count { candidate = candidate.saturating_sub(1); if !computed_state.is_computed_field(candidate) { // Validate and move as usual #[cfg(feature = "validation")] { let current_text = self.current_text(); if !self.ui_state.validation.allows_field_switch(self.ui_state.current_field, current_text) { if let Some(reason) = self.ui_state.validation.field_switch_block_reason(self.ui_state.current_field, current_text) { tracing::debug!("Field switch blocked: {}", reason); return Err(anyhow::anyhow!("Cannot switch fields: {}", reason)); } } } #[cfg(feature = "validation")] { let current_text = self.current_text().to_string(); let _validation_result = self.ui_state.validation.validate_field_content( self.ui_state.current_field, ¤t_text, ); } self.ui_state.move_to_field(candidate, field_count); self.clamp_cursor_to_current_field(); return Ok(()); } if candidate == 0 { break; } } } } // Check if field switching is allowed (minimum character enforcement) #[cfg(feature = "validation")] { let current_text = self.current_text(); if !self.ui_state.validation.allows_field_switch(self.ui_state.current_field, current_text) { if let Some(reason) = self.ui_state.validation.field_switch_block_reason(self.ui_state.current_field, current_text) { tracing::debug!("Field switch blocked: {}", reason); return Err(anyhow::anyhow!("Cannot switch fields: {}", reason)); } } } // Validate current field before moving #[cfg(feature = "validation")] { let current_text = self.current_text().to_string(); // Convert to String to avoid borrow conflicts let _validation_result = self.ui_state.validation.validate_field_content( self.ui_state.current_field, ¤t_text, ); } let current_field = self.ui_state.current_field; let new_field = current_field.saturating_sub(1); self.ui_state.move_to_field(new_field, field_count); self.clamp_cursor_to_current_field(); Ok(()) } /// Move to next field (vim j / down arrow) pub fn move_down(&mut self) -> Result<()> { let field_count = self.data_provider.field_count(); if field_count == 0 { return Ok(()); } // Skip computed fields during navigation when feature enabled #[cfg(feature = "computed")] { if let Some(computed_state) = &self.ui_state.computed { // Find next non-computed field let mut candidate = self.ui_state.current_field; for _ in 0..field_count { candidate = (candidate + 1).min(field_count - 1); if !computed_state.is_computed_field(candidate) { // Validate and move as usual #[cfg(feature = "validation")] { let current_text = self.current_text(); if !self.ui_state.validation.allows_field_switch(self.ui_state.current_field, current_text) { if let Some(reason) = self.ui_state.validation.field_switch_block_reason(self.ui_state.current_field, current_text) { tracing::debug!("Field switch blocked: {}", reason); return Err(anyhow::anyhow!("Cannot switch fields: {}", reason)); } } } #[cfg(feature = "validation")] { let current_text = self.current_text().to_string(); let _validation_result = self.ui_state.validation.validate_field_content( self.ui_state.current_field, ¤t_text, ); } self.ui_state.move_to_field(candidate, field_count); self.clamp_cursor_to_current_field(); return Ok(()); } if candidate == field_count - 1 { break; } } } } // Check if field switching is allowed (minimum character enforcement) #[cfg(feature = "validation")] { let current_text = self.current_text(); if !self.ui_state.validation.allows_field_switch(self.ui_state.current_field, current_text) { if let Some(reason) = self.ui_state.validation.field_switch_block_reason(self.ui_state.current_field, current_text) { tracing::debug!("Field switch blocked: {}", reason); return Err(anyhow::anyhow!("Cannot switch fields: {}", reason)); } } } // Validate current field before moving #[cfg(feature = "validation")] { let current_text = self.current_text().to_string(); // Convert to String to avoid borrow conflicts let _validation_result = self.ui_state.validation.validate_field_content( self.ui_state.current_field, ¤t_text, ); } let current_field = self.ui_state.current_field; let new_field = (current_field + 1).min(field_count - 1); self.ui_state.move_to_field(new_field, field_count); self.clamp_cursor_to_current_field(); Ok(()) } /// Move to first field (vim gg) pub fn move_first_line(&mut self) { let field_count = self.data_provider.field_count(); if field_count == 0 { return; } self.ui_state.move_to_field(0, field_count); self.clamp_cursor_to_current_field(); } /// Move to last field (vim G) pub fn move_last_line(&mut self) { let field_count = self.data_provider.field_count(); if field_count == 0 { return; } let last_field = field_count - 1; self.ui_state.move_to_field(last_field, field_count); self.clamp_cursor_to_current_field(); } /// Move to previous field (alternative to move_up) pub fn prev_field(&mut self) -> Result<()> { self.move_up() } // ================================================================================================ // COMPUTED FIELDS (behind 'computed' feature) // ================================================================================================ /// Initialize computed fields from provider and computed provider #[cfg(feature = "computed")] pub fn set_computed_provider(&mut self, mut provider: C) where C: crate::computed::ComputedProvider, { // Initialize computed state self.ui_state.computed = Some(crate::computed::ComputedState::new()); // Register computed fields and their dependencies let field_count = self.data_provider.field_count(); for field_index in 0..field_count { if provider.handles_field(field_index) { let deps = provider.field_dependencies(field_index); if let Some(computed_state) = &mut self.ui_state.computed { computed_state.register_computed_field(field_index, deps); } } } // Initial computation of all computed fields self.recompute_all_fields(&mut provider); } /// Recompute specific computed fields #[cfg(feature = "computed")] pub fn recompute_fields(&mut self, provider: &mut C, field_indices: &[usize]) where C: crate::computed::ComputedProvider, { if let Some(computed_state) = &mut self.ui_state.computed { // Collect all field values for context let field_values: Vec = (0..self.data_provider.field_count()) .map(|i| { if computed_state.is_computed_field(i) { // Use cached computed value computed_state .get_computed_value(i) .cloned() .unwrap_or_default() } else { // Use regular field value self.data_provider.field_value(i).to_string() } }) .collect(); let field_refs: Vec<&str> = field_values.iter().map(|s| s.as_str()).collect(); // Recompute specified fields for &field_index in field_indices { if provider.handles_field(field_index) { let context = crate::computed::ComputedContext { field_values: &field_refs, target_field: field_index, current_field: Some(self.ui_state.current_field), }; let computed_value = provider.compute_field(context); computed_state.set_computed_value(field_index, computed_value); } } } } /// Recompute all computed fields #[cfg(feature = "computed")] pub fn recompute_all_fields(&mut self, provider: &mut C) where C: crate::computed::ComputedProvider, { if let Some(computed_state) = &self.ui_state.computed { let computed_fields: Vec = computed_state.computed_fields().collect(); self.recompute_fields(provider, &computed_fields); } } /// Trigger recomputation when field changes (call this after set_field_value) #[cfg(feature = "computed")] pub fn on_field_changed(&mut self, provider: &mut C, changed_field: usize) where C: crate::computed::ComputedProvider, { if let Some(computed_state) = &self.ui_state.computed { let fields_to_update = computed_state.fields_to_recompute(changed_field); if !fields_to_update.is_empty() { self.recompute_fields(provider, &fields_to_update); } } } /// Enhanced getter that returns computed values for computed fields when available #[cfg(feature = "computed")] pub fn effective_field_value(&self, field_index: usize) -> String { if let Some(computed_state) = &self.ui_state.computed { if let Some(computed_value) = computed_state.get_computed_value(field_index) { return computed_value.clone(); } } self.data_provider.field_value(field_index).to_string() } /// Move to next field (alternative to move_down) pub fn next_field(&mut self) -> Result<()> { self.move_down() } /// Move to start of current field (vim 0) pub fn move_line_start(&mut self) { use crate::canvas::actions::movement::line::line_start_position; let new_pos = line_start_position(); self.ui_state.cursor_pos = new_pos; self.ui_state.ideal_cursor_column = new_pos; } /// Move to end of current field (vim $) pub fn move_line_end(&mut self) { use crate::canvas::actions::movement::line::line_end_position; let current_text = self.current_text(); let is_edit_mode = self.ui_state.current_mode == AppMode::Edit; let new_pos = line_end_position(current_text, is_edit_mode); self.ui_state.cursor_pos = new_pos; self.ui_state.ideal_cursor_column = new_pos; } /// Move to start of next word (vim w) pub fn move_word_next(&mut self) { use crate::canvas::actions::movement::word::find_next_word_start; let current_text = self.current_text(); if current_text.is_empty() { return; } let new_pos = find_next_word_start(current_text, self.ui_state.cursor_pos); let is_edit_mode = self.ui_state.current_mode == AppMode::Edit; // Clamp to valid bounds for current mode let final_pos = if is_edit_mode { new_pos.min(current_text.len()) } else { new_pos.min(current_text.len().saturating_sub(1)) }; self.ui_state.cursor_pos = final_pos; self.ui_state.ideal_cursor_column = final_pos; } /// Move to start of previous word (vim b) pub fn move_word_prev(&mut self) { use crate::canvas::actions::movement::word::find_prev_word_start; let current_text = self.current_text(); if current_text.is_empty() { return; } let new_pos = find_prev_word_start(current_text, self.ui_state.cursor_pos); self.ui_state.cursor_pos = new_pos; self.ui_state.ideal_cursor_column = new_pos; } /// Move to end of current/next word (vim e) pub fn move_word_end(&mut self) { use crate::canvas::actions::movement::word::find_word_end; let current_text = self.current_text(); if current_text.is_empty() { return; } let current_pos = self.ui_state.cursor_pos; let new_pos = find_word_end(current_text, current_pos); // If we didn't move, try next word let final_pos = if new_pos == current_pos && current_pos + 1 < current_text.len() { find_word_end(current_text, current_pos + 1) } else { new_pos }; // Clamp for read-only mode let is_edit_mode = self.ui_state.current_mode == AppMode::Edit; let clamped_pos = if is_edit_mode { final_pos.min(current_text.len()) } else { final_pos.min(current_text.len().saturating_sub(1)) }; self.ui_state.cursor_pos = clamped_pos; self.ui_state.ideal_cursor_column = clamped_pos; } /// Move to end of previous word (vim ge) pub fn move_word_end_prev(&mut self) { use crate::canvas::actions::movement::word::find_prev_word_end; let current_text = self.current_text(); if current_text.is_empty() { return; } let new_pos = find_prev_word_end(current_text, self.ui_state.cursor_pos); self.ui_state.cursor_pos = new_pos; self.ui_state.ideal_cursor_column = new_pos; } /// Delete character before cursor (vim x in insert mode / backspace) pub fn delete_backward(&mut self) -> Result<()> { if self.ui_state.current_mode != AppMode::Edit { return Ok(()); // Silently ignore in non-edit modes } if self.ui_state.cursor_pos == 0 { return Ok(()); // Nothing to delete } let field_index = self.ui_state.current_field; let mut current_text = self.data_provider.field_value(field_index).to_string(); if self.ui_state.cursor_pos <= current_text.len() { current_text.remove(self.ui_state.cursor_pos - 1); self.data_provider.set_field_value(field_index, current_text.clone()); self.ui_state.cursor_pos -= 1; self.ui_state.ideal_cursor_column = self.ui_state.cursor_pos; // Validate the new content if validation is enabled #[cfg(feature = "validation")] { let _validation_result = self.ui_state.validation.validate_field_content( field_index, ¤t_text, ); } } Ok(()) } /// Delete character under cursor (vim x / delete key) pub fn delete_forward(&mut self) -> Result<()> { if self.ui_state.current_mode != AppMode::Edit { return Ok(()); // Silently ignore in non-edit modes } let field_index = self.ui_state.current_field; let mut current_text = self.data_provider.field_value(field_index).to_string(); if self.ui_state.cursor_pos < current_text.len() { current_text.remove(self.ui_state.cursor_pos); self.data_provider.set_field_value(field_index, current_text.clone()); // Validate the new content if validation is enabled #[cfg(feature = "validation")] { let _validation_result = self.ui_state.validation.validate_field_content( field_index, ¤t_text, ); } } Ok(()) } /// Exit edit mode to read-only mode (vim Escape) pub fn exit_edit_mode(&mut self) -> Result<()> { // Validate current field content when exiting edit mode #[cfg(feature = "validation")] { let current_text = self.current_text(); if !self.ui_state.validation.allows_field_switch(self.ui_state.current_field, current_text) { if let Some(reason) = self.ui_state.validation.field_switch_block_reason(self.ui_state.current_field, current_text) { return Err(anyhow::anyhow!("Cannot exit edit mode: {}", reason)); } } } // Adjust cursor position when transitioning from edit to normal mode let current_text = self.current_text(); if !current_text.is_empty() { // In normal mode, cursor must be ON a character, not after the last one let max_normal_pos = current_text.len().saturating_sub(1); if self.ui_state.cursor_pos > max_normal_pos { self.ui_state.cursor_pos = max_normal_pos; self.ui_state.ideal_cursor_column = self.ui_state.cursor_pos; } } self.set_mode(AppMode::ReadOnly); // Deactivate suggestions when exiting edit mode self.ui_state.deactivate_suggestions(); Ok(()) } /// Enter edit mode from read-only mode (vim i/a/o) pub fn enter_edit_mode(&mut self) { #[cfg(feature = "computed")] { if let Some(computed_state) = &self.ui_state.computed { if computed_state.is_computed_field(self.ui_state.current_field) { // Can't edit computed fields - silently ignore return; } } } self.set_mode(AppMode::Edit); } // =================================================================== // HELPER METHODS // =================================================================== /// Clamp cursor position to valid bounds for current field and mode fn clamp_cursor_to_current_field(&mut self) { let current_text = self.current_text(); let is_edit_mode = self.ui_state.current_mode == AppMode::Edit; use crate::canvas::actions::movement::line::safe_cursor_position; let safe_pos = safe_cursor_position( current_text, self.ui_state.ideal_cursor_column, is_edit_mode ); self.ui_state.cursor_pos = safe_pos; } /// Set the value of the current field pub fn set_current_field_value(&mut self, value: String) { let field_index = self.ui_state.current_field; self.data_provider.set_field_value(field_index, value.clone()); // Reset cursor to start of field self.ui_state.cursor_pos = 0; self.ui_state.ideal_cursor_column = 0; // Validate the new content if validation is enabled #[cfg(feature = "validation")] { let _validation_result = self.ui_state.validation.validate_field_content( field_index, &value, ); } } /// Set the value of a specific field by index pub fn set_field_value(&mut self, field_index: usize, value: String) { if field_index < self.data_provider.field_count() { self.data_provider.set_field_value(field_index, value.clone()); // If we're modifying the current field, reset cursor if field_index == self.ui_state.current_field { self.ui_state.cursor_pos = 0; self.ui_state.ideal_cursor_column = 0; } // Validate the new content if validation is enabled #[cfg(feature = "validation")] { let _validation_result = self.ui_state.validation.validate_field_content( field_index, &value, ); } } } /// Clear the current field (set to empty string) pub fn clear_current_field(&mut self) { self.set_current_field_value(String::new()); } /// Get mutable access to data provider (for advanced operations) pub fn data_provider_mut(&mut self) -> &mut D { &mut self.data_provider } /// Set cursor to exact position (for vim-style movements like f, F, t, T) pub fn set_cursor_position(&mut self, position: usize) { let current_text = self.current_text(); let is_edit_mode = self.ui_state.current_mode == AppMode::Edit; // Clamp to valid bounds for current mode let max_pos = if is_edit_mode { current_text.len() // Edit mode: can go past end } else { current_text.len().saturating_sub(1).max(0) // Read-only: stay within text }; let clamped_pos = position.min(max_pos); // Update cursor position directly self.ui_state.cursor_pos = clamped_pos; self.ui_state.ideal_cursor_column = clamped_pos; } /// Get cursor position for display (maps raw cursor to display position with formatter/mask) pub fn display_cursor_position(&self) -> usize { let current_text = self.current_text(); let raw_pos = match self.ui_state.current_mode { AppMode::Edit => self.ui_state.cursor_pos.min(current_text.len()), _ => { if current_text.is_empty() { 0 } else { self.ui_state.cursor_pos.min(current_text.len().saturating_sub(1)) } } }; #[cfg(feature = "validation")] { let field_index = self.ui_state.current_field; if let Some(cfg) = self.ui_state.validation.get_field_config(field_index) { // Only apply custom formatter cursor mapping when NOT editing if !matches!(self.ui_state.current_mode, AppMode::Edit) { if let Some((formatted, mapper, _warning)) = cfg.run_custom_formatter(current_text) { return mapper.raw_to_formatted(current_text, &formatted, raw_pos); } } // Fallback to display mask if let Some(mask) = &cfg.display_mask { return mask.raw_pos_to_display_pos(self.ui_state.cursor_pos); } } } self.ui_state.cursor_pos } /// Cleanup cursor style (call this when shutting down) pub fn cleanup_cursor(&self) -> std::io::Result<()> { #[cfg(feature = "cursor-style")] { crate::canvas::CursorManager::reset() } #[cfg(not(feature = "cursor-style"))] { Ok(()) } } // =================================================================== // HIGHLIGHT MODE // =================================================================== /// Enter highlight mode (visual mode) pub fn enter_highlight_mode(&mut self) { if self.ui_state.current_mode == AppMode::ReadOnly { self.ui_state.current_mode = AppMode::Highlight; self.ui_state.selection = SelectionState::Characterwise { anchor: (self.ui_state.current_field, self.ui_state.cursor_pos), }; #[cfg(feature = "cursor-style")] { let _ = CursorManager::update_for_mode(AppMode::Highlight); } } } /// Enter highlight line mode (visual line mode) pub fn enter_highlight_line_mode(&mut self) { if self.ui_state.current_mode == AppMode::ReadOnly { self.ui_state.current_mode = AppMode::Highlight; self.ui_state.selection = SelectionState::Linewise { anchor_field: self.ui_state.current_field, }; #[cfg(feature = "cursor-style")] { let _ = CursorManager::update_for_mode(AppMode::Highlight); } } } /// Exit highlight mode back to read-only pub fn exit_highlight_mode(&mut self) { if self.ui_state.current_mode == AppMode::Highlight { self.ui_state.current_mode = AppMode::ReadOnly; self.ui_state.selection = SelectionState::None; #[cfg(feature = "cursor-style")] { let _ = CursorManager::update_for_mode(AppMode::ReadOnly); } } } /// Check if currently in highlight mode pub fn is_highlight_mode(&self) -> bool { self.ui_state.current_mode == AppMode::Highlight } /// Get current selection state pub fn selection_state(&self) -> &SelectionState { &self.ui_state.selection } /// Enhanced movement methods that update selection in highlight mode pub fn move_left_with_selection(&mut self) { self.move_left(); // Selection anchor stays in place, cursor position updates automatically } pub fn move_right_with_selection(&mut self) { self.move_right(); // Selection anchor stays in place, cursor position updates automatically } pub fn move_up_with_selection(&mut self) { let _ = self.move_up(); // Selection anchor stays in place, cursor position updates automatically } pub fn move_down_with_selection(&mut self) { let _ = self.move_down(); // Selection anchor stays in place, cursor position updates automatically } // Add similar methods for word movement, line movement, etc. pub fn move_word_next_with_selection(&mut self) { self.move_word_next(); } pub fn move_word_prev_with_selection(&mut self) { self.move_word_prev(); } pub fn move_line_start_with_selection(&mut self) { self.move_line_start(); } pub fn move_line_end_with_selection(&mut self) { self.move_line_end(); } } // Add Drop implementation for automatic cleanup impl Drop for FormEditor { fn drop(&mut self) { // Reset cursor to default when FormEditor is dropped let _ = self.cleanup_cursor(); } }