use crate::DataProvider;
use crate::editor::EditorCore;
impl<D: DataProvider> EditorCore<D> {
#[cfg(feature = "computed")]
fn resolved_navigable_field(
&self,
requested_field: usize,
prev_field: usize,
field_count: usize,
) -> usize {
let target_field = requested_field.min(field_count - 1);
let Some(computed_state) = &self.ui_state.computed else {
return target_field;
};
if !computed_state.is_computed_field(target_field) {
return target_field;
}
let search_forward_first = target_field >= prev_field;
let mut search_forward =
|| ((target_field + 1)..field_count).find(|&i| !computed_state.is_computed_field(i));
let mut search_backward = || {
(0..target_field)
.rev()
.find(|&i| !computed_state.is_computed_field(i))
};
if search_forward_first {
search_forward()
.or_else(&mut search_backward)
.unwrap_or(prev_field)
} else {
search_backward()
.or_else(&mut search_forward)
.unwrap_or(prev_field)
}
}
pub fn transition_to_field(&mut self, new_field: usize) -> anyhow::Result<()> {
self.break_undo_coalescing();
let field_count = self.data_provider.field_count();
if field_count == 0 {
self.clamp_current_field_to_count(field_count);
return Ok(());
}
let prev_field = self.clamp_current_field_to_count(field_count).unwrap_or(0);
#[cfg(feature = "computed")]
let target_field = self.resolved_navigable_field(new_field, prev_field, field_count);
#[cfg(not(feature = "computed"))]
let target_field = new_field.min(field_count - 1);
if target_field == prev_field {
return Ok(());
}
#[cfg(feature = "validation")]
self.ui_state.validation.clear_last_switch_block();
#[cfg(feature = "validation")]
{
let current_text = self.current_text();
if !self
.ui_state
.validation
.allows_field_switch(prev_field, current_text)
{
if let Some(reason) = self
.ui_state
.validation
.field_switch_block_reason(prev_field, current_text)
{
self.ui_state
.validation
.set_last_switch_block(reason.clone());
tracing::debug!("Field switch blocked: {}", reason);
return Err(anyhow::anyhow!("Cannot switch fields: {}", reason));
}
}
}
#[cfg(feature = "validation")]
{
let text = self.data_provider.field_value(prev_field).to_string();
let _ = self
.ui_state
.validation
.validate_field_content(prev_field, &text);
if let Some(cfg) = self.ui_state.validation.get_field_config(prev_field) {
if cfg.external_validation_enabled && !text.is_empty() {
self.set_external_validation(
prev_field,
crate::validation::ExternalValidationState::Validating,
);
if let Some(cb) = self.external_validation_callback.as_mut() {
let final_state = cb(prev_field, &text);
self.set_external_validation(prev_field, final_state);
}
}
}
}
let ideal_cursor_column = self.ui_state.ideal_cursor_column;
self.ui_state.move_to_field(target_field, field_count);
let current_text = self.current_text();
let max_pos = current_text.chars().count();
self.set_cursor_for_mode(ideal_cursor_column, max_pos);
self.ui_state.ideal_cursor_column = ideal_cursor_column;
#[cfg(feature = "suggestions")]
{
self.dismiss_suggestions();
}
Ok(())
}
pub fn move_first_line(&mut self) -> anyhow::Result<()> {
self.transition_to_field(0)
}
pub fn move_last_line(&mut self) -> anyhow::Result<()> {
let last_field = self.data_provider.field_count().saturating_sub(1);
self.transition_to_field(last_field)
}
pub fn move_up(&mut self) -> bool {
if self.ui_state.current_field == 0 {
return false;
}
let before = self.ui_state.current_field;
let new_field = self.ui_state.current_field - 1;
self.transition_to_field(new_field).is_ok() && self.ui_state.current_field != before
}
pub fn move_down(&mut self) -> bool {
let last = self.data_provider.field_count().saturating_sub(1);
if self.ui_state.current_field >= last {
return false;
}
let before = self.ui_state.current_field;
let new_field = self.ui_state.current_field + 1;
self.transition_to_field(new_field).is_ok() && self.ui_state.current_field != before
}
pub fn move_to_next_field(&mut self) -> anyhow::Result<()> {
let field_count = self.data_provider.field_count();
if field_count == 0 {
return Ok(());
}
let new_field = (self.ui_state.current_field + 1) % field_count;
self.transition_to_field(new_field)
}
pub fn prev_field(&mut self) -> bool {
self.move_up()
}
pub fn next_field(&mut self) -> bool {
self.move_down()
}
}
#[cfg(all(test, feature = "computed"))]
mod tests {
use super::*;
use crate::computed::{ComputedContext, ComputedProvider};
#[derive(Clone, Default)]
struct TestProvider {
fields: Vec<(&'static str, String)>,
}
impl TestProvider {
fn new(names: &[&'static str]) -> Self {
Self {
fields: names.iter().map(|name| (*name, String::new())).collect(),
}
}
}
impl DataProvider for TestProvider {
fn field_count(&self) -> usize {
self.fields.len()
}
fn field_name(&self, index: usize) -> &str {
self.fields[index].0
}
fn field_value(&self, index: usize) -> &str {
&self.fields[index].1
}
fn set_field_value(&mut self, index: usize, value: String) {
self.fields[index].1 = value;
}
}
struct TestComputedProvider;
impl ComputedProvider for TestComputedProvider {
fn handles_field(&self, field_index: usize) -> bool {
matches!(field_index, 2 | 3 | 4)
}
fn field_dependencies(&self, _field_index: usize) -> Vec<usize> {
vec![0]
}
fn compute_field(&mut self, _context: ComputedContext) -> String {
String::new()
}
}
#[test]
fn move_down_skips_trailing_computed_fields() {
let provider = TestProvider::new(&["a", "b", "c", "d", "e"]);
let mut editor = EditorCore::new(provider);
editor.register_computed_provider(&TestComputedProvider);
assert!(editor.transition_to_field(1).is_ok());
assert_eq!(editor.current_field(), 1);
assert!(!editor.move_down());
assert_eq!(editor.current_field(), 1);
}
#[test]
fn move_last_line_lands_on_last_editable_field() {
let provider = TestProvider::new(&["a", "b", "c", "d", "e"]);
let mut editor = EditorCore::new(provider);
editor.register_computed_provider(&TestComputedProvider);
assert!(editor.move_last_line().is_ok());
assert_eq!(editor.current_field(), 1);
}
}