cleared codebase
This commit is contained in:
@@ -88,42 +88,27 @@ pub fn render_canvas_with_highlight<T: CanvasTheme, D: DataProvider>(
|
||||
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;
|
||||
|
||||
@@ -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<usize> {
|
||||
fn field_dependencies(&self, _field_index: usize) -> Vec<usize> {
|
||||
(0..100).collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<crate::validation::ValidationConfig> {
|
||||
fn validation_config(&self, _field_index: usize) -> Option<crate::validation::ValidationConfig> {
|
||||
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<String> {
|
||||
fn computed_field_value(&self, _field_index: usize) -> Option<String> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<D: DataProvider> FormEditor<D> {
|
||||
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<D: DataProvider> FormEditor<D> {
|
||||
// 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<D: DataProvider> FormEditor<D> {
|
||||
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<D: DataProvider> FormEditor<D> {
|
||||
&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<D: DataProvider> FormEditor<D> {
|
||||
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<D: DataProvider> FormEditor<D> {
|
||||
// ===================================================================
|
||||
|
||||
/// 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<D: DataProvider> FormEditor<D> {
|
||||
|
||||
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<D: DataProvider> FormEditor<D> {
|
||||
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<D: DataProvider> FormEditor<D> {
|
||||
/// 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<D: DataProvider> FormEditor<D> {
|
||||
|
||||
/// 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<D: DataProvider> FormEditor<D> {
|
||||
pub fn current_formatter_warning(&self) -> Option<String> {
|
||||
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<D: DataProvider> FormEditor<D> {
|
||||
|
||||
/// 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<String> {
|
||||
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<String> {
|
||||
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<D: DataProvider> FormEditor<D> {
|
||||
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
|
||||
///
|
||||
///
|
||||
/// 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<D: DataProvider> FormEditor<D> {
|
||||
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<D: DataProvider> FormEditor<D> {
|
||||
// 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<D: DataProvider> FormEditor<D> {
|
||||
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<D: DataProvider> FormEditor<D> {
|
||||
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<D: DataProvider> FormEditor<D> {
|
||||
// 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<D: DataProvider> FormEditor<D> {
|
||||
// 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<D: DataProvider> FormEditor<D> {
|
||||
// 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<D: DataProvider> FormEditor<D> {
|
||||
}
|
||||
|
||||
/// 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(())
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -125,39 +125,56 @@ impl CharacterLimits {
|
||||
position: usize,
|
||||
character: char,
|
||||
) -> Option<ValidationResult> {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user