cleared codebase
This commit is contained in:
@@ -88,42 +88,27 @@ pub fn render_canvas_with_highlight<T: CanvasTheme, D: DataProvider>(
|
|||||||
highlight_state,
|
highlight_state,
|
||||||
editor.display_cursor_position(), // Use display cursor position for masks
|
editor.display_cursor_position(), // Use display cursor position for masks
|
||||||
false, // TODO: track unsaved changes in editor
|
false, // TODO: track unsaved changes in editor
|
||||||
|i| {
|
// Closures for getting display values and overrides
|
||||||
// Get display value for field i using editor logic (Feature 4 + masks)
|
#[cfg(feature = "validation")]
|
||||||
#[cfg(feature = "validation")]
|
|field_idx| editor.display_text_for_field(field_idx),
|
||||||
{
|
#[cfg(not(feature = "validation"))]
|
||||||
editor.display_text_for_field(i)
|
|field_idx| data_provider.field_value(field_idx).to_string(),
|
||||||
}
|
// Closure for checking display overrides
|
||||||
#[cfg(not(feature = "validation"))]
|
#[cfg(feature = "validation")]
|
||||||
{
|
|field_idx| {
|
||||||
data_provider.field_value(i).to_string()
|
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| {
|
#[cfg(not(feature = "validation"))]
|
||||||
// Check if field has display override (custom formatter or mask)
|
|_field_idx| false,
|
||||||
#[cfg(feature = "validation")]
|
// Closure for providing completion
|
||||||
{
|
|field_idx| {
|
||||||
editor.ui_state().validation_state().get_field_config(i)
|
if field_idx == current_field_idx {
|
||||||
.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 {
|
|
||||||
active_completion.clone()
|
active_completion.clone()
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@@ -269,7 +254,8 @@ where
|
|||||||
{
|
{
|
||||||
let mut active_field_input_rect = None;
|
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 is_active = i == *current_field_idx;
|
||||||
let typed_text = get_display_value(i);
|
let typed_text = get_display_value(i);
|
||||||
|
|
||||||
@@ -353,7 +339,7 @@ fn apply_characterwise_highlighting<'a, T: CanvasTheme>(
|
|||||||
current_cursor_pos: usize,
|
current_cursor_pos: usize,
|
||||||
anchor: &(usize, usize),
|
anchor: &(usize, usize),
|
||||||
theme: &T,
|
theme: &T,
|
||||||
is_active: bool,
|
_is_active: bool,
|
||||||
) -> Line<'a> {
|
) -> Line<'a> {
|
||||||
let (anchor_field, anchor_char) = *anchor;
|
let (anchor_field, anchor_char) = *anchor;
|
||||||
let start_field = min(anchor_field, *current_field_idx);
|
let start_field = min(anchor_field, *current_field_idx);
|
||||||
@@ -456,7 +442,7 @@ fn apply_linewise_highlighting<'a, T: CanvasTheme>(
|
|||||||
current_field_idx: &usize,
|
current_field_idx: &usize,
|
||||||
anchor_line: &usize,
|
anchor_line: &usize,
|
||||||
theme: &T,
|
theme: &T,
|
||||||
is_active: bool,
|
_is_active: bool,
|
||||||
) -> Line<'a> {
|
) -> Line<'a> {
|
||||||
let start_field = min(*anchor_line, *current_field_idx);
|
let start_field = min(*anchor_line, *current_field_idx);
|
||||||
let end_field = max(*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,
|
field_rect: Rect,
|
||||||
text: &str,
|
text: &str,
|
||||||
current_cursor_pos: usize,
|
current_cursor_pos: usize,
|
||||||
has_display_override: bool,
|
_has_display_override: bool,
|
||||||
) {
|
) {
|
||||||
// Sum display widths of the first current_cursor_pos characters
|
// Sum display widths of the first current_cursor_pos characters
|
||||||
let mut cols: u16 = 0;
|
let mut cols: u16 = 0;
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ pub trait ComputedProvider {
|
|||||||
/// Get list of field dependencies for optimization.
|
/// Get list of field dependencies for optimization.
|
||||||
/// If field A depends on fields [1, 3], only recompute A when fields 1 or 3 change.
|
/// 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.
|
/// Default: depend on all fields (always recompute) with a reasonable upper bound.
|
||||||
fn field_dependencies(&self, field_index: usize) -> Vec<usize> {
|
fn field_dependencies(&self, _field_index: usize) -> Vec<usize> {
|
||||||
(0..100).collect()
|
(0..100).collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,33 +19,33 @@ pub trait DataProvider {
|
|||||||
fn set_field_value(&mut self, index: usize, value: String);
|
fn set_field_value(&mut self, index: usize, value: String);
|
||||||
|
|
||||||
/// Check if field supports suggestions (optional)
|
/// Check if field supports suggestions (optional)
|
||||||
fn supports_suggestions(&self, field_index: usize) -> bool {
|
fn supports_suggestions(&self, _field_index: usize) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get display value (for password masking, etc.) - optional
|
/// 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
|
None // Default: use actual value
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get validation configuration for a field (optional)
|
/// Get validation configuration for a field (optional)
|
||||||
/// Only available when the 'validation' feature is enabled
|
/// Only available when the 'validation' feature is enabled
|
||||||
#[cfg(feature = "validation")]
|
#[cfg(feature = "validation")]
|
||||||
fn validation_config(&self, field_index: usize) -> Option<crate::validation::ValidationConfig> {
|
fn validation_config(&self, _field_index: usize) -> Option<crate::validation::ValidationConfig> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if field is computed (display-only, skip in navigation)
|
/// Check if field is computed (display-only, skip in navigation)
|
||||||
/// Default: not computed
|
/// Default: not computed
|
||||||
#[cfg(feature = "computed")]
|
#[cfg(feature = "computed")]
|
||||||
fn is_computed_field(&self, field_index: usize) -> bool {
|
fn is_computed_field(&self, _field_index: usize) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get computed field value if this is a computed field.
|
/// Get computed field value if this is a computed field.
|
||||||
/// Returns None for regular fields. Default: not computed.
|
/// Returns None for regular fields. Default: not computed.
|
||||||
#[cfg(feature = "computed")]
|
#[cfg(feature = "computed")]
|
||||||
fn computed_field_value(&self, field_index: usize) -> Option<String> {
|
fn computed_field_value(&self, _field_index: usize) -> Option<String> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,9 @@
|
|||||||
#[cfg(feature = "cursor-style")]
|
#[cfg(feature = "cursor-style")]
|
||||||
use crate::canvas::CursorManager;
|
use crate::canvas::CursorManager;
|
||||||
|
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use crate::canvas::state::EditorState;
|
use crate::canvas::state::EditorState;
|
||||||
use crate::{DataProvider, SuggestionItem};
|
use crate::{DataProvider, SuggestionItem};
|
||||||
#[cfg(feature = "suggestions")]
|
|
||||||
use crate::SuggestionsProvider;
|
|
||||||
|
|
||||||
use crate::canvas::modes::AppMode;
|
use crate::canvas::modes::AppMode;
|
||||||
use crate::canvas::state::SelectionState;
|
use crate::canvas::state::SelectionState;
|
||||||
|
|
||||||
@@ -49,8 +45,9 @@ impl<D: DataProvider> FormEditor<D> {
|
|||||||
fn byte_to_char_index(s: &str, byte_idx: usize) -> usize {
|
fn byte_to_char_index(s: &str, byte_idx: usize) -> usize {
|
||||||
s[..byte_idx].chars().count()
|
s[..byte_idx].chars().count()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(data_provider: D) -> Self {
|
pub fn new(data_provider: D) -> Self {
|
||||||
let mut editor = Self {
|
let editor = Self {
|
||||||
ui_state: EditorState::new(),
|
ui_state: EditorState::new(),
|
||||||
data_provider,
|
data_provider,
|
||||||
suggestions: Vec::new(),
|
suggestions: Vec::new(),
|
||||||
@@ -61,10 +58,14 @@ impl<D: DataProvider> FormEditor<D> {
|
|||||||
// Initialize validation configurations if validation feature is enabled
|
// Initialize validation configurations if validation feature is enabled
|
||||||
#[cfg(feature = "validation")]
|
#[cfg(feature = "validation")]
|
||||||
{
|
{
|
||||||
|
let mut editor = editor;
|
||||||
editor.initialize_validation();
|
editor.initialize_validation();
|
||||||
|
editor
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "validation"))]
|
||||||
|
{
|
||||||
|
editor
|
||||||
}
|
}
|
||||||
|
|
||||||
editor
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get current field text (convenience method)
|
/// Get current field text (convenience method)
|
||||||
@@ -160,7 +161,7 @@ impl<D: DataProvider> FormEditor<D> {
|
|||||||
if matches!(self.ui_state.current_mode, AppMode::Edit) {
|
if matches!(self.ui_state.current_mode, AppMode::Edit) {
|
||||||
return raw.to_string();
|
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;
|
return formatted;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -179,11 +180,6 @@ impl<D: DataProvider> FormEditor<D> {
|
|||||||
&self.ui_state
|
&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).
|
/// Open the suggestions UI for `field_index` (UI-only; does not fetch).
|
||||||
pub fn open_suggestions(&mut self, field_index: usize) {
|
pub fn open_suggestions(&mut self, field_index: usize) {
|
||||||
self.ui_state.open_suggestions(field_index);
|
self.ui_state.open_suggestions(field_index);
|
||||||
@@ -257,7 +253,7 @@ impl<D: DataProvider> FormEditor<D> {
|
|||||||
return raw.to_string();
|
return raw.to_string();
|
||||||
}
|
}
|
||||||
// Not editing -> formatted
|
// 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;
|
return formatted;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -305,7 +301,6 @@ impl<D: DataProvider> FormEditor<D> {
|
|||||||
// ===================================================================
|
// ===================================================================
|
||||||
|
|
||||||
/// Centralized field transition logic
|
/// Centralized field transition logic
|
||||||
#[cfg_attr(not(feature = "validation"), allow(unused_variables))]
|
|
||||||
pub fn transition_to_field(&mut self, new_field: usize) -> Result<()> {
|
pub fn transition_to_field(&mut self, new_field: usize) -> Result<()> {
|
||||||
let field_count = self.data_provider.field_count();
|
let field_count = self.data_provider.field_count();
|
||||||
if field_count == 0 {
|
if field_count == 0 {
|
||||||
@@ -314,8 +309,11 @@ impl<D: DataProvider> FormEditor<D> {
|
|||||||
|
|
||||||
let prev_field = self.ui_state.current_field;
|
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);
|
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
|
// 2. Computed field skipping
|
||||||
#[cfg(feature = "computed")]
|
#[cfg(feature = "computed")]
|
||||||
@@ -432,8 +430,20 @@ impl<D: DataProvider> FormEditor<D> {
|
|||||||
return Ok(()); // Ignore in non-edit modes
|
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;
|
let field_index = self.ui_state.current_field;
|
||||||
|
#[cfg(feature = "validation")]
|
||||||
let raw_cursor_pos = self.ui_state.cursor_pos;
|
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);
|
let current_raw_text = self.data_provider.field_value(field_index);
|
||||||
|
|
||||||
// Mask gate: reject input that doesn't fit the mask at current position
|
// Mask gate: reject input that doesn't fit the mask at current position
|
||||||
@@ -566,7 +576,12 @@ impl<D: DataProvider> FormEditor<D> {
|
|||||||
/// Restore left and right movement within the current field
|
/// Restore left and right movement within the current field
|
||||||
/// Move cursor left within current field
|
/// Move cursor left within current field
|
||||||
pub fn move_left(&mut self) -> Result<()> {
|
pub fn move_left(&mut self) -> Result<()> {
|
||||||
|
// FIX 3: Only mut when validation feature modifies it
|
||||||
|
#[cfg(feature = "validation")]
|
||||||
let mut moved = false;
|
let mut moved = false;
|
||||||
|
#[cfg(not(feature = "validation"))]
|
||||||
|
let moved = false;
|
||||||
|
|
||||||
// Try mask-aware movement if validation/mask config exists
|
// Try mask-aware movement if validation/mask config exists
|
||||||
#[cfg(feature = "validation")]
|
#[cfg(feature = "validation")]
|
||||||
{
|
{
|
||||||
@@ -600,11 +615,16 @@ impl<D: DataProvider> FormEditor<D> {
|
|||||||
|
|
||||||
/// Move cursor right within current field
|
/// Move cursor right within current field
|
||||||
pub fn move_right(&mut self) -> Result<()> {
|
pub fn move_right(&mut self) -> Result<()> {
|
||||||
|
// FIX 4: Only mut when validation feature modifies it
|
||||||
|
#[cfg(feature = "validation")]
|
||||||
let mut moved = false;
|
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
|
// Try mask-aware movement if mask is configured for this field
|
||||||
#[cfg(feature = "validation")]
|
#[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(cfg) = self.ui_state.validation.get_field_config(field_index) {
|
||||||
if let Some(mask) = &cfg.display_mask {
|
if let Some(mask) = &cfg.display_mask {
|
||||||
let display_pos = mask.raw_pos_to_display_pos(self.ui_state.cursor_pos);
|
let display_pos = mask.raw_pos_to_display_pos(self.ui_state.cursor_pos);
|
||||||
@@ -771,7 +791,7 @@ impl<D: DataProvider> FormEditor<D> {
|
|||||||
pub fn current_formatter_warning(&self) -> Option<String> {
|
pub fn current_formatter_warning(&self) -> Option<String> {
|
||||||
let idx = self.ui_state.current_field;
|
let idx = self.ui_state.current_field;
|
||||||
if let Some(cfg) = self.ui_state.validation.get_field_config(idx) {
|
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;
|
return warn;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -811,38 +831,45 @@ impl<D: DataProvider> FormEditor<D> {
|
|||||||
|
|
||||||
/// Begin suggestions loading for a field (UI updates immediately, no fetch)
|
/// Begin suggestions loading for a field (UI updates immediately, no fetch)
|
||||||
/// This opens the dropdown with "Loading..." state instantly
|
/// 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.
|
/// `apply_suggestions_result()` when ready.
|
||||||
|
#[cfg(feature = "suggestions")]
|
||||||
pub fn start_suggestions(&mut self, field_index: usize) -> Option<String> {
|
pub fn start_suggestions(&mut self, field_index: usize) -> Option<String> {
|
||||||
if !self.data_provider.supports_suggestions(field_index) {
|
if !self.data_provider.supports_suggestions(field_index) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let query = self.current_text().to_string();
|
let query = self.current_text().to_string();
|
||||||
|
|
||||||
// Open suggestions UI immediately - user sees dropdown right away
|
// Open suggestions UI immediately - user sees dropdown right away
|
||||||
self.ui_state.open_suggestions(field_index);
|
self.ui_state.open_suggestions(field_index);
|
||||||
|
|
||||||
// ADD THIS LINE - mark as loading so UI shows "Loading..."
|
// ADD THIS LINE - mark as loading so UI shows "Loading..."
|
||||||
self.ui_state.suggestions.is_loading = true;
|
self.ui_state.suggestions.is_loading = true;
|
||||||
|
|
||||||
// Store the query we're loading for (prevents stale results)
|
// Store the query we're loading for (prevents stale results)
|
||||||
self.ui_state.suggestions.active_query = Some(query.clone());
|
self.ui_state.suggestions.active_query = Some(query.clone());
|
||||||
|
|
||||||
// Clear any old suggestions
|
// Clear any old suggestions
|
||||||
self.suggestions.clear();
|
self.suggestions.clear();
|
||||||
|
|
||||||
// Return the query so caller knows what to fetch
|
// Return the query so caller knows what to fetch
|
||||||
Some(query)
|
Some(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "suggestions"))]
|
||||||
|
pub fn start_suggestions(&mut self, _field_index: usize) -> Option<String> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
/// Apply fetched suggestions results
|
/// Apply fetched suggestions results
|
||||||
///
|
///
|
||||||
/// This will ignore stale results if the field or query has changed since
|
/// This will ignore stale results if the field or query has changed since
|
||||||
/// `start_suggestions()` was called.
|
/// `start_suggestions()` was called.
|
||||||
///
|
///
|
||||||
/// Returns `true` if results were applied, `false` if they were stale/ignored.
|
/// Returns `true` if results were applied, `false` if they were stale/ignored.
|
||||||
|
#[cfg(feature = "suggestions")]
|
||||||
pub fn apply_suggestions_result(
|
pub fn apply_suggestions_result(
|
||||||
&mut self,
|
&mut self,
|
||||||
field_index: usize,
|
field_index: usize,
|
||||||
@@ -874,9 +901,20 @@ impl<D: DataProvider> FormEditor<D> {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "suggestions"))]
|
||||||
|
pub fn apply_suggestions_result(
|
||||||
|
&mut self,
|
||||||
|
_field_index: usize,
|
||||||
|
_query: &str,
|
||||||
|
_results: Vec<SuggestionItem>,
|
||||||
|
) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
/// Check if there's an active suggestions query waiting for results
|
/// Check if there's an active suggestions query waiting for results
|
||||||
///
|
///
|
||||||
/// Returns (field_index, query) if suggestions are loading, None otherwise.
|
/// Returns (field_index, query) if suggestions are loading, None otherwise.
|
||||||
|
#[cfg(feature = "suggestions")]
|
||||||
pub fn pending_suggestions_query(&self) -> Option<(usize, String)> {
|
pub fn pending_suggestions_query(&self) -> Option<(usize, String)> {
|
||||||
if self.ui_state.suggestions.is_loading {
|
if self.ui_state.suggestions.is_loading {
|
||||||
if let (Some(field), Some(query)) = (
|
if let (Some(field), Some(query)) = (
|
||||||
@@ -889,6 +927,11 @@ impl<D: DataProvider> FormEditor<D> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "suggestions"))]
|
||||||
|
pub fn pending_suggestions_query(&self) -> Option<(usize, String)> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
/// Cancel any pending suggestions (useful for cleanup)
|
/// Cancel any pending suggestions (useful for cleanup)
|
||||||
pub fn cancel_suggestions(&mut self) {
|
pub fn cancel_suggestions(&mut self) {
|
||||||
self.close_suggestions();
|
self.close_suggestions();
|
||||||
@@ -932,7 +975,7 @@ impl<D: DataProvider> FormEditor<D> {
|
|||||||
// Validate the new content if validation is enabled
|
// Validate the new content if validation is enabled
|
||||||
#[cfg(feature = "validation")]
|
#[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,
|
field_index,
|
||||||
&suggestion.value_to_store,
|
&suggestion.value_to_store,
|
||||||
);
|
);
|
||||||
@@ -1197,8 +1240,12 @@ impl<D: DataProvider> FormEditor<D> {
|
|||||||
current_text.replace_range(start..end, "");
|
current_text.replace_range(start..end, "");
|
||||||
self.data_provider.set_field_value(field_index, current_text.clone());
|
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;
|
let mut target_cursor = new_cursor;
|
||||||
|
#[cfg(not(feature = "validation"))]
|
||||||
|
let target_cursor = new_cursor;
|
||||||
|
|
||||||
#[cfg(feature = "validation")]
|
#[cfg(feature = "validation")]
|
||||||
{
|
{
|
||||||
if let Some(cfg) = self.ui_state.validation.get_field_config(field_index) {
|
if let Some(cfg) = self.ui_state.validation.get_field_config(field_index) {
|
||||||
@@ -1240,7 +1287,12 @@ impl<D: DataProvider> FormEditor<D> {
|
|||||||
current_text.replace_range(start..end, "");
|
current_text.replace_range(start..end, "");
|
||||||
self.data_provider.set_field_value(field_index, current_text.clone());
|
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;
|
let mut target_cursor = self.ui_state.cursor_pos;
|
||||||
|
#[cfg(not(feature = "validation"))]
|
||||||
|
let target_cursor = self.ui_state.cursor_pos;
|
||||||
|
|
||||||
#[cfg(feature = "validation")]
|
#[cfg(feature = "validation")]
|
||||||
{
|
{
|
||||||
if let Some(cfg) = self.ui_state.validation.get_field_config(field_index) {
|
if let Some(cfg) = self.ui_state.validation.get_field_config(field_index) {
|
||||||
@@ -1349,22 +1401,6 @@ impl<D: DataProvider> FormEditor<D> {
|
|||||||
// HELPER METHODS
|
// 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
|
/// Set the value of the current field
|
||||||
pub fn set_current_field_value(&mut self, value: String) {
|
pub fn set_current_field_value(&mut self, value: String) {
|
||||||
let field_index = self.ui_state.current_field;
|
let field_index = self.ui_state.current_field;
|
||||||
@@ -1376,7 +1412,7 @@ impl<D: DataProvider> FormEditor<D> {
|
|||||||
// Validate the new content if validation is enabled
|
// Validate the new content if validation is enabled
|
||||||
#[cfg(feature = "validation")]
|
#[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,
|
field_index,
|
||||||
&value,
|
&value,
|
||||||
);
|
);
|
||||||
@@ -1396,7 +1432,7 @@ impl<D: DataProvider> FormEditor<D> {
|
|||||||
// Validate the new content if validation is enabled
|
// Validate the new content if validation is enabled
|
||||||
#[cfg(feature = "validation")]
|
#[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,
|
field_index,
|
||||||
&value,
|
&value,
|
||||||
);
|
);
|
||||||
@@ -1474,15 +1510,14 @@ impl<D: DataProvider> FormEditor<D> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Cleanup cursor style (call this when shutting down)
|
/// Cleanup cursor style (call this when shutting down)
|
||||||
|
#[cfg(feature = "cursor-style")]
|
||||||
pub fn cleanup_cursor(&self) -> std::io::Result<()> {
|
pub fn cleanup_cursor(&self) -> std::io::Result<()> {
|
||||||
#[cfg(feature = "cursor-style")]
|
crate::canvas::CursorManager::reset()
|
||||||
{
|
}
|
||||||
crate::canvas::CursorManager::reset()
|
|
||||||
}
|
#[cfg(not(feature = "cursor-style"))]
|
||||||
#[cfg(not(feature = "cursor-style"))]
|
pub fn cleanup_cursor(&self) -> std::io::Result<()> {
|
||||||
{
|
Ok(())
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -125,39 +125,56 @@ impl CharacterLimits {
|
|||||||
position: usize,
|
position: usize,
|
||||||
character: char,
|
character: char,
|
||||||
) -> Option<ValidationResult> {
|
) -> Option<ValidationResult> {
|
||||||
let current_count = self.count(current_text);
|
// FIX: Actually simulate the insertion at the specified position
|
||||||
let char_count = match self.count_mode {
|
// This makes the `position` parameter essential to the logic
|
||||||
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;
|
|
||||||
|
|
||||||
|
// 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
|
// Check max length
|
||||||
if let Some(max) = self.max_length {
|
if let Some(max) = self.max_length {
|
||||||
if new_count > max {
|
if new_count > max {
|
||||||
return Some(ValidationResult::error(format!(
|
return Some(ValidationResult::error(format!(
|
||||||
"Character limit exceeded: {}/{}",
|
"Character limit exceeded: {}/{}",
|
||||||
new_count,
|
new_count,
|
||||||
max
|
max
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check warning threshold
|
// Check warning threshold
|
||||||
if let Some(warning_threshold) = self.warning_threshold {
|
if let Some(warning_threshold) = self.warning_threshold {
|
||||||
if new_count >= warning_threshold && current_count < warning_threshold {
|
if new_count >= warning_threshold && current_count < warning_threshold {
|
||||||
return Some(ValidationResult::warning(format!(
|
return Some(ValidationResult::warning(format!(
|
||||||
"Approaching character limit: {}/{}",
|
"Approaching character limit: {}/{}",
|
||||||
new_count,
|
new_count,
|
||||||
max
|
max
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
None // No validation issues
|
None // No validation issues
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user