diff --git a/canvas/src/canvas/gui.rs b/canvas/src/canvas/gui.rs index 37ba8d3..ff57daa 100644 --- a/canvas/src/canvas/gui.rs +++ b/canvas/src/canvas/gui.rs @@ -88,42 +88,27 @@ pub fn render_canvas_with_highlight( highlight_state, editor.display_cursor_position(), // Use display cursor position for masks false, // TODO: track unsaved changes in editor - |i| { - // Get display value for field i using editor logic (Feature 4 + masks) - #[cfg(feature = "validation")] - { - editor.display_text_for_field(i) - } - #[cfg(not(feature = "validation"))] - { - data_provider.field_value(i).to_string() - } + // Closures for getting display values and overrides + #[cfg(feature = "validation")] + |field_idx| editor.display_text_for_field(field_idx), + #[cfg(not(feature = "validation"))] + |field_idx| data_provider.field_value(field_idx).to_string(), + // Closure for checking display overrides + #[cfg(feature = "validation")] + |field_idx| { + editor.ui_state().validation_state().get_field_config(field_idx) + .map(|cfg| { + let has_formatter = cfg.custom_formatter.is_some(); + let has_mask = cfg.display_mask.is_some(); + has_formatter || has_mask + }) + .unwrap_or(false) }, - |i| { - // Check if field has display override (custom formatter or mask) - #[cfg(feature = "validation")] - { - editor.ui_state().validation_state().get_field_config(i) - .map(|cfg| { - // Formatter takes precedence; if present, it's a display override - #[allow(unused_mut)] - let mut has_override = false; - #[cfg(feature = "validation")] - { - has_override = cfg.custom_formatter.is_some(); - } - has_override || cfg.display_mask.is_some() - }) - .unwrap_or(false) - } - #[cfg(not(feature = "validation"))] - { - false - } - }, - // NEW: provide completion for the active field - |i| { - if i == current_field_idx { + #[cfg(not(feature = "validation"))] + |_field_idx| false, + // Closure for providing completion + |field_idx| { + if field_idx == current_field_idx { active_completion.clone() } else { None @@ -269,7 +254,8 @@ where { let mut active_field_input_rect = None; - for (i, input) in inputs.iter().enumerate() { + // FIX: Iterate over indices only since we never use the input values directly + for i in 0..inputs.len() { let is_active = i == *current_field_idx; let typed_text = get_display_value(i); @@ -353,7 +339,7 @@ fn apply_characterwise_highlighting<'a, T: CanvasTheme>( current_cursor_pos: usize, anchor: &(usize, usize), theme: &T, - is_active: bool, + _is_active: bool, ) -> Line<'a> { let (anchor_field, anchor_char) = *anchor; let start_field = min(anchor_field, *current_field_idx); @@ -456,7 +442,7 @@ fn apply_linewise_highlighting<'a, T: CanvasTheme>( current_field_idx: &usize, anchor_line: &usize, theme: &T, - is_active: bool, + _is_active: bool, ) -> Line<'a> { let start_field = min(*anchor_line, *current_field_idx); let end_field = max(*anchor_line, *current_field_idx); @@ -487,7 +473,7 @@ fn set_cursor_position( field_rect: Rect, text: &str, current_cursor_pos: usize, - has_display_override: bool, + _has_display_override: bool, ) { // Sum display widths of the first current_cursor_pos characters let mut cols: u16 = 0; diff --git a/canvas/src/computed/provider.rs b/canvas/src/computed/provider.rs index 33f255d..f8dd241 100644 --- a/canvas/src/computed/provider.rs +++ b/canvas/src/computed/provider.rs @@ -25,7 +25,7 @@ pub trait ComputedProvider { /// Get list of field dependencies for optimization. /// If field A depends on fields [1, 3], only recompute A when fields 1 or 3 change. /// Default: depend on all fields (always recompute) with a reasonable upper bound. - fn field_dependencies(&self, field_index: usize) -> Vec { + fn field_dependencies(&self, _field_index: usize) -> Vec { (0..100).collect() } } diff --git a/canvas/src/data_provider.rs b/canvas/src/data_provider.rs index a5eb6f7..0de2106 100644 --- a/canvas/src/data_provider.rs +++ b/canvas/src/data_provider.rs @@ -19,33 +19,33 @@ pub trait DataProvider { fn set_field_value(&mut self, index: usize, value: String); /// Check if field supports suggestions (optional) - fn supports_suggestions(&self, field_index: usize) -> bool { + fn supports_suggestions(&self, _field_index: usize) -> bool { false } /// Get display value (for password masking, etc.) - optional - fn display_value(&self, index: usize) -> Option<&str> { + fn display_value(&self, _index: usize) -> Option<&str> { None // Default: use actual value } - + /// Get validation configuration for a field (optional) /// Only available when the 'validation' feature is enabled #[cfg(feature = "validation")] - fn validation_config(&self, field_index: usize) -> Option { + fn validation_config(&self, _field_index: usize) -> Option { None } /// Check if field is computed (display-only, skip in navigation) /// Default: not computed #[cfg(feature = "computed")] - fn is_computed_field(&self, field_index: usize) -> bool { + fn is_computed_field(&self, _field_index: usize) -> bool { false } /// Get computed field value if this is a computed field. /// Returns None for regular fields. Default: not computed. #[cfg(feature = "computed")] - fn computed_field_value(&self, field_index: usize) -> Option { + fn computed_field_value(&self, _field_index: usize) -> Option { None } } diff --git a/canvas/src/editor.rs b/canvas/src/editor.rs index 0698d18..170b4c9 100644 --- a/canvas/src/editor.rs +++ b/canvas/src/editor.rs @@ -4,13 +4,9 @@ #[cfg(feature = "cursor-style")] use crate::canvas::CursorManager; - use anyhow::Result; use crate::canvas::state::EditorState; use crate::{DataProvider, SuggestionItem}; -#[cfg(feature = "suggestions")] -use crate::SuggestionsProvider; - use crate::canvas::modes::AppMode; use crate::canvas::state::SelectionState; @@ -49,8 +45,9 @@ impl FormEditor { fn byte_to_char_index(s: &str, byte_idx: usize) -> usize { s[..byte_idx].chars().count() } + pub fn new(data_provider: D) -> Self { - let mut editor = Self { + let editor = Self { ui_state: EditorState::new(), data_provider, suggestions: Vec::new(), @@ -61,10 +58,14 @@ impl FormEditor { // Initialize validation configurations if validation feature is enabled #[cfg(feature = "validation")] { + let mut editor = editor; editor.initialize_validation(); + editor + } + #[cfg(not(feature = "validation"))] + { + editor } - - editor } /// Get current field text (convenience method) @@ -160,7 +161,7 @@ impl FormEditor { if matches!(self.ui_state.current_mode, AppMode::Edit) { return raw.to_string(); } - if let Some((formatted, mapper, warning)) = cfg.run_custom_formatter(raw) { + if let Some((formatted, _mapper, _warning)) = cfg.run_custom_formatter(raw) { return formatted; } } @@ -179,11 +180,6 @@ impl FormEditor { &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); @@ -257,7 +253,7 @@ impl FormEditor { return raw.to_string(); } // Not editing -> formatted - if let Some((formatted, mapper, warning)) = cfg.run_custom_formatter(raw) { + if let Some((formatted, _mapper, _warning)) = cfg.run_custom_formatter(raw) { return formatted; } } @@ -305,7 +301,6 @@ impl FormEditor { // =================================================================== /// Centralized field transition logic - #[cfg_attr(not(feature = "validation"), allow(unused_variables))] pub fn transition_to_field(&mut self, new_field: usize) -> Result<()> { let field_count = self.data_provider.field_count(); if field_count == 0 { @@ -314,8 +309,11 @@ impl FormEditor { let prev_field = self.ui_state.current_field; - // 1. Bounds check + // FIX 2: Only mut when computed feature actually modifies it + #[cfg(feature = "computed")] let mut target_field = new_field.min(field_count - 1); + #[cfg(not(feature = "computed"))] + let target_field = new_field.min(field_count - 1); // 2. Computed field skipping #[cfg(feature = "computed")] @@ -432,8 +430,20 @@ impl FormEditor { return Ok(()); // Ignore in non-edit modes } + // Variables are only declared when the features that use them are enabled + #[cfg(feature = "validation")] let field_index = self.ui_state.current_field; + #[cfg(feature = "validation")] let raw_cursor_pos = self.ui_state.cursor_pos; + #[cfg(feature = "validation")] + let current_raw_text = self.data_provider.field_value(field_index); + + // When validation is disabled, we declare these variables differently + #[cfg(not(feature = "validation"))] + let field_index = self.ui_state.current_field; + #[cfg(not(feature = "validation"))] + let raw_cursor_pos = self.ui_state.cursor_pos; + #[cfg(not(feature = "validation"))] let current_raw_text = self.data_provider.field_value(field_index); // Mask gate: reject input that doesn't fit the mask at current position @@ -566,7 +576,12 @@ impl FormEditor { /// Restore left and right movement within the current field /// Move cursor left within current field pub fn move_left(&mut self) -> Result<()> { + // FIX 3: Only mut when validation feature modifies it + #[cfg(feature = "validation")] let mut moved = false; + #[cfg(not(feature = "validation"))] + let moved = false; + // Try mask-aware movement if validation/mask config exists #[cfg(feature = "validation")] { @@ -600,11 +615,16 @@ impl FormEditor { /// Move cursor right within current field pub fn move_right(&mut self) -> Result<()> { + // FIX 4: Only mut when validation feature modifies it + #[cfg(feature = "validation")] let mut moved = false; - let field_index = self.ui_state.current_field; + #[cfg(not(feature = "validation"))] + let moved = false; + // Try mask-aware movement if mask is configured for this field #[cfg(feature = "validation")] { + let field_index = self.ui_state.current_field; if let Some(cfg) = self.ui_state.validation.get_field_config(field_index) { if let Some(mask) = &cfg.display_mask { let display_pos = mask.raw_pos_to_display_pos(self.ui_state.cursor_pos); @@ -771,7 +791,7 @@ impl FormEditor { pub fn current_formatter_warning(&self) -> Option { let idx = self.ui_state.current_field; if let Some(cfg) = self.ui_state.validation.get_field_config(idx) { - if let Some((fmt, mapper, warn)) = cfg.run_custom_formatter(self.current_text()) { + if let Some((_fmt, _mapper, warn)) = cfg.run_custom_formatter(self.current_text()) { return warn; } } @@ -811,38 +831,45 @@ impl FormEditor { /// Begin suggestions loading for a field (UI updates immediately, no fetch) /// This opens the dropdown with "Loading..." state instantly - /// - /// The caller is responsible for fetching suggestions and calling + /// + /// The caller is responsible for fetching suggestions and calling /// `apply_suggestions_result()` when ready. + #[cfg(feature = "suggestions")] pub fn start_suggestions(&mut self, field_index: usize) -> Option { if !self.data_provider.supports_suggestions(field_index) { return None; } let query = self.current_text().to_string(); - + // Open suggestions UI immediately - user sees dropdown right away self.ui_state.open_suggestions(field_index); - + // ADD THIS LINE - mark as loading so UI shows "Loading..." self.ui_state.suggestions.is_loading = true; - + // Store the query we're loading for (prevents stale results) self.ui_state.suggestions.active_query = Some(query.clone()); - + // Clear any old suggestions self.suggestions.clear(); - + // Return the query so caller knows what to fetch Some(query) } + #[cfg(not(feature = "suggestions"))] + pub fn start_suggestions(&mut self, _field_index: usize) -> Option { + None + } + /// Apply fetched suggestions results - /// + /// /// This will ignore stale results if the field or query has changed since /// `start_suggestions()` was called. - /// + /// /// Returns `true` if results were applied, `false` if they were stale/ignored. + #[cfg(feature = "suggestions")] pub fn apply_suggestions_result( &mut self, field_index: usize, @@ -874,9 +901,20 @@ impl FormEditor { true } + #[cfg(not(feature = "suggestions"))] + pub fn apply_suggestions_result( + &mut self, + _field_index: usize, + _query: &str, + _results: Vec, + ) -> bool { + false + } + /// Check if there's an active suggestions query waiting for results - /// + /// /// Returns (field_index, query) if suggestions are loading, None otherwise. + #[cfg(feature = "suggestions")] pub fn pending_suggestions_query(&self) -> Option<(usize, String)> { if self.ui_state.suggestions.is_loading { if let (Some(field), Some(query)) = ( @@ -889,6 +927,11 @@ impl FormEditor { None } + #[cfg(not(feature = "suggestions"))] + pub fn pending_suggestions_query(&self) -> Option<(usize, String)> { + None + } + /// Cancel any pending suggestions (useful for cleanup) pub fn cancel_suggestions(&mut self) { self.close_suggestions(); @@ -932,7 +975,7 @@ impl FormEditor { // Validate the new content if validation is enabled #[cfg(feature = "validation")] { - let validation_result = self.ui_state.validation.validate_field_content( + let _validation_result = self.ui_state.validation.validate_field_content( field_index, &suggestion.value_to_store, ); @@ -1197,8 +1240,12 @@ impl FormEditor { current_text.replace_range(start..end, ""); self.data_provider.set_field_value(field_index, current_text.clone()); - // Always run reposition logic + // FIX 5: Only mut when validation feature might modify it + #[cfg(feature = "validation")] let mut target_cursor = new_cursor; + #[cfg(not(feature = "validation"))] + let target_cursor = new_cursor; + #[cfg(feature = "validation")] { if let Some(cfg) = self.ui_state.validation.get_field_config(field_index) { @@ -1240,7 +1287,12 @@ impl FormEditor { current_text.replace_range(start..end, ""); self.data_provider.set_field_value(field_index, current_text.clone()); + // FIX 6: Only mut when validation feature might modify it + #[cfg(feature = "validation")] let mut target_cursor = self.ui_state.cursor_pos; + #[cfg(not(feature = "validation"))] + let target_cursor = self.ui_state.cursor_pos; + #[cfg(feature = "validation")] { if let Some(cfg) = self.ui_state.validation.get_field_config(field_index) { @@ -1349,22 +1401,6 @@ impl FormEditor { // 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; @@ -1376,7 +1412,7 @@ impl FormEditor { // Validate the new content if validation is enabled #[cfg(feature = "validation")] { - let validation_result = self.ui_state.validation.validate_field_content( + let _validation_result = self.ui_state.validation.validate_field_content( field_index, &value, ); @@ -1396,7 +1432,7 @@ impl FormEditor { // Validate the new content if validation is enabled #[cfg(feature = "validation")] { - let validation_result = self.ui_state.validation.validate_field_content( + let _validation_result = self.ui_state.validation.validate_field_content( field_index, &value, ); @@ -1474,15 +1510,14 @@ impl FormEditor { } /// Cleanup cursor style (call this when shutting down) + #[cfg(feature = "cursor-style")] pub fn cleanup_cursor(&self) -> std::io::Result<()> { - #[cfg(feature = "cursor-style")] - { - crate::canvas::CursorManager::reset() - } - #[cfg(not(feature = "cursor-style"))] - { - Ok(()) - } + crate::canvas::CursorManager::reset() + } + + #[cfg(not(feature = "cursor-style"))] + pub fn cleanup_cursor(&self) -> std::io::Result<()> { + Ok(()) } diff --git a/canvas/src/validation/limits.rs b/canvas/src/validation/limits.rs index 002c229..e1f75b9 100644 --- a/canvas/src/validation/limits.rs +++ b/canvas/src/validation/limits.rs @@ -125,39 +125,56 @@ impl CharacterLimits { position: usize, character: char, ) -> Option { - let current_count = self.count(current_text); - let char_count = match self.count_mode { - CountMode::Characters => 1, - CountMode::DisplayWidth => { - let char_str = character.to_string(); - char_str.width() - }, - CountMode::Bytes => character.len_utf8(), - }; - let new_count = current_count + char_count; + // FIX: Actually simulate the insertion at the specified position + // This makes the `position` parameter essential to the logic + // 1. Create the new string by inserting the character at the correct position + let mut new_text = String::with_capacity(current_text.len() + character.len_utf8()); + let mut chars = current_text.chars(); + + // Append characters from the original string that come before the insertion point + // We clamp the position to be safe + let clamped_pos = position.min(current_text.chars().count()); + for _ in 0..clamped_pos { + if let Some(ch) = chars.next() { + new_text.push(ch); + } + } + + // Insert the new character + new_text.push(character); + + // Append the rest of the original string + for ch in chars { + new_text.push(ch); + } + + // 2. Now perform all validation on the *actual* resulting text + let new_count = self.count(&new_text); + let current_count = self.count(current_text); + // Check max length if let Some(max) = self.max_length { if new_count > max { return Some(ValidationResult::error(format!( - "Character limit exceeded: {}/{}", - new_count, + "Character limit exceeded: {}/{}", + new_count, max ))); } - + // Check warning threshold if let Some(warning_threshold) = self.warning_threshold { if new_count >= warning_threshold && current_count < warning_threshold { return Some(ValidationResult::warning(format!( - "Approaching character limit: {}/{}", - new_count, + "Approaching character limit: {}/{}", + new_count, max ))); } } } - + None // No validation issues }