Codex Handbook
tui/src/bottom_pane/textarea.rs 3837 lines
//! The textarea owns editable composer text, placeholder elements, cursor/wrap state, and a//! single-entry kill buffer.//!//! Whole-buffer replacement APIs intentionally rebuild only the visible draft state. They clear//! element ranges and derived cursor/wrapping caches, but they keep the kill buffer intact so a//! caller can clear or rewrite the draft and still allow `Ctrl+Y` to restore the user's most//! recent `Ctrl+K`. This is the contract higher-level composer flows rely on after submit,//! slash-command dispatch, and other synthetic clears.//!//! This module does not implement an Emacs-style multi-entry kill ring. It keeps only the most//! recent killed span.use crate::key_hint::KeyBindingListExt;use crate::key_hint::is_altgr;use crate::keymap::EditorKeymap;use crate::keymap::RuntimeKeymap;use crate::keymap::VimNormalKeymap;use crate::keymap::VimOperatorKeymap;use crate::keymap::VimTextObjectKeymap;use codex_protocol::user_input::ByteRange;use codex_protocol::user_input::TextElement as UserTextElement;use crossterm::event::KeyCode;use crossterm::event::KeyEvent;use crossterm::event::KeyEventKind;use crossterm::event::KeyModifiers;use ratatui::buffer::Buffer;use ratatui::layout::Rect;use ratatui::style::Color;use ratatui::style::Style;use ratatui::widgets::StatefulWidgetRef;use ratatui::widgets::WidgetRef;use std::cell::Ref;use std::cell::RefCell;use std::ops::Range;use textwrap::Options;use unicode_segmentation::UnicodeSegmentation;use unicode_width::UnicodeWidthStr;mod vim;use self::vim::VimMode;use self::vim::VimMotion;use self::vim::VimOperator;use self::vim::VimPending;use self::vim::VimTextObjectScope;const WORD_SEPARATORS: &str = "`~!@#$%^&*()-=+[{]}\\|;:'\",.<>/?";fn is_word_separator(ch: char) -> bool {    WORD_SEPARATORS.contains(ch)}fn split_word_pieces(run: &str) -> Vec<(usize, &str)> {    let mut pieces = Vec::new();    for (segment_start, segment) in run.split_word_bound_indices() {        let mut piece_start = 0;        let mut chars = segment.char_indices();        let Some((_, first_char)) = chars.next() else {            continue;        };        let mut in_separator = is_word_separator(first_char);        for (idx, ch) in chars {            let is_separator = is_word_separator(ch);            if is_separator == in_separator {                continue;            }            pieces.push((segment_start + piece_start, &segment[piece_start..idx]));            piece_start = idx;            in_separator = is_separator;        }        pieces.push((segment_start + piece_start, &segment[piece_start..]));    }    pieces}#[derive(Debug, Clone)]struct TextElement {    id: u64,    range: Range<usize>,}#[derive(Debug, Clone, PartialEq, Eq)]pub(crate) struct TextElementSnapshot {    pub(crate) id: u64,    pub(crate) range: Range<usize>,    pub(crate) text: String,}/// `TextArea` is the editable buffer behind the TUI composer.////// It owns the raw UTF-8 text, placeholder-like text elements that must move atomically with/// edits, cursor/wrapping state for rendering, and a single-entry kill buffer for `Ctrl+K` //// `Ctrl+Y` style editing. Callers may replace the entire visible buffer through/// [`Self::set_text_clearing_elements`] or [`Self::set_text_with_elements`] without disturbing the/// kill buffer; if they incorrectly assume those methods fully reset editing state, a later yank/// will appear to restore stale text from the user's perspective.#[derive(Debug)]pub(crate) struct TextArea {    text: String,    cursor_pos: usize,    wrap_cache: RefCell<Option<WrapCache>>,    preferred_col: Option<usize>,    elements: Vec<TextElement>,    next_element_id: u64,    kill_buffer: String,    kill_buffer_kind: KillBufferKind,    vim_enabled: bool,    vim_mode: VimMode,    vim_pending: VimPending,    editor_keymap: EditorKeymap,    vim_normal_keymap: VimNormalKeymap,    vim_operator_keymap: VimOperatorKeymap,    vim_text_object_keymap: VimTextObjectKeymap,}#[derive(Debug, Clone)]struct WrapCache {    width: u16,    lines: Vec<Range<usize>>,}#[derive(Debug, Default, Clone, Copy)]pub(crate) struct TextAreaState {    /// Index into wrapped lines of the first visible line.    scroll: u16,}#[derive(Debug, Clone, Copy, PartialEq, Eq)]enum KillBufferKind {    /// Characterwise kills and yanks paste at the cursor.    Characterwise,    /// Linewise kills and yanks paste as whole lines below the cursor line.    Linewise,}impl TextArea {    pub fn new() -> Self {        let defaults = RuntimeKeymap::defaults();        Self {            text: String::new(),            cursor_pos: 0,            wrap_cache: RefCell::new(None),            preferred_col: None,            elements: Vec::new(),            next_element_id: 1,            kill_buffer: String::new(),            kill_buffer_kind: KillBufferKind::Characterwise,            vim_enabled: false,            vim_mode: VimMode::Insert,            vim_pending: VimPending::None,            editor_keymap: defaults.editor,            vim_normal_keymap: defaults.vim_normal,            vim_operator_keymap: defaults.vim_operator,            vim_text_object_keymap: defaults.vim_text_object,        }    }    /// Replace the editor and Vim keymaps used by subsequent text-editing input.    ///    /// This method intentionally swaps only the keymap caches. It does not    /// reinterpret pending input, change Vim mode, move the cursor, or mutate    /// the kill buffer, so callers can safely apply a live config update while    /// preserving the current draft exactly as typed.    pub fn set_keymap_bindings(&mut self, keymap: &RuntimeKeymap) {        self.editor_keymap = keymap.editor.clone();        self.vim_normal_keymap = keymap.vim_normal.clone();        self.vim_operator_keymap = keymap.vim_operator.clone();        self.vim_text_object_keymap = keymap.vim_text_object.clone();    }    /// Replace the visible textarea text and clear any existing text elements.    ///    /// This is the "fresh buffer" path for callers that want plain text with no placeholder    /// ranges. It intentionally preserves the current kill buffer, because higher-level flows such    /// as submit or slash-command dispatch clear the draft through this method and still want    /// `Ctrl+Y` to recover the user's most recent kill.    pub fn set_text_clearing_elements(&mut self, text: &str) {        self.set_text_inner(text, /*elements*/ None);    }    /// Replace the visible textarea text and rebuild the provided text elements.    ///    /// As with [`Self::set_text_clearing_elements`], this resets only state derived from the    /// visible buffer. The kill buffer survives so callers restoring drafts or external edits do    /// not silently discard a pending yank target.    pub fn set_text_with_elements(&mut self, text: &str, elements: &[UserTextElement]) {        self.set_text_inner(text, Some(elements));    }    fn set_text_inner(&mut self, text: &str, elements: Option<&[UserTextElement]>) {        // Stage 1: replace the raw text and keep the cursor in a safe byte range.        self.text = text.to_string();        self.cursor_pos = self.cursor_pos.clamp(0, self.text.len());        // Stage 2: rebuild element ranges from scratch against the new text.        self.elements.clear();        if let Some(elements) = elements {            for elem in elements {                let mut start = elem.byte_range.start.min(self.text.len());                let mut end = elem.byte_range.end.min(self.text.len());                start = self.clamp_pos_to_char_boundary(start);                end = self.clamp_pos_to_char_boundary(end);                if start >= end {                    continue;                }                let id = self.next_element_id();                self.elements.push(TextElement {                    id,                    range: start..end,                });            }            self.elements.sort_by_key(|e| e.range.start);        }        // Stage 3: clamp the cursor and reset derived state tied to the prior content.        // The kill buffer is editing history rather than visible-buffer state, so full-buffer        // replacements intentionally leave it alone.        self.cursor_pos = self.clamp_pos_to_nearest_boundary(self.cursor_pos);        self.wrap_cache.replace(None);        self.preferred_col = None;    }    /// Enable or disable modal Vim editing for the textarea.    ///    /// Enabling always enters normal mode and disabling always returns to    /// insert semantics. Pending operators are cleared in both directions so a    /// toggle cannot leave the next keypress interpreted as the second half of    /// an old `d` or `y` command.    pub(crate) fn set_vim_enabled(&mut self, enabled: bool) {        self.vim_enabled = enabled;        self.vim_pending = VimPending::None;        self.vim_mode = if enabled {            VimMode::Normal        } else {            VimMode::Insert        };    }    /// Return whether modal Vim editing is currently enabled.    pub(crate) fn is_vim_enabled(&self) -> bool {        self.vim_enabled    }    /// Return whether Vim mode is enabled and currently waiting in normal mode.    ///    /// Composer-level handlers use this to decide whether Up/Down should be    /// offered to history navigation only after normal-mode movement reaches a    /// text boundary.    pub(crate) fn is_vim_normal_mode(&self) -> bool {        self.vim_enabled && self.vim_mode == VimMode::Normal    }    /// Return the cursor position that represents the last editable item in Vim normal mode.    pub(crate) fn vim_normal_end_cursor(&self) -> usize {        if self.text.is_empty() {            0        } else {            self.prev_atomic_boundary(self.text.len())        }    }    /// Return whether a Vim operator is waiting for a motion.    ///    /// This is observable so the composer can avoid stealing the second key of    /// `d{motion}` or `y{motion}` for higher-level shortcuts.    pub(crate) fn is_vim_operator_pending(&self) -> bool {        !matches!(self.vim_pending, VimPending::None)    }    /// Enter Vim insert mode if modal editing is enabled.    ///    /// Calling this while Vim is disabled is a no-op, which lets parent    /// workflows reset mode after submissions without first branching on the    /// current keymap state.    pub(crate) fn enter_vim_insert_mode(&mut self) {        if self.vim_enabled {            self.vim_mode = VimMode::Insert;            self.vim_pending = VimPending::None;        }    }    /// Enter Vim normal mode if modal editing is enabled.    ///    /// This clears any pending operator and preferred vertical column. The    /// latter matches normal Vim navigation expectations after leaving insert    /// mode; preserving the old column would make the next `j` or `k` jump to a    /// stale visual target.    pub(crate) fn enter_vim_normal_mode(&mut self) {        if self.vim_enabled {            self.vim_mode = VimMode::Normal;            self.vim_pending = VimPending::None;            self.preferred_col = None;        }    }    /// Return whether rapid plain-key bursts should be treated as paste input.    ///    /// Paste burst detection is disabled in Vim normal mode so a fast sequence    /// like `dd` or `yw` remains command input instead of being converted into    /// literal text.    pub(crate) fn allows_paste_burst(&self) -> bool {        !self.vim_enabled || self.vim_mode == VimMode::Insert    }    /// Return whether rendering should use the insert-mode cursor style.    pub(crate) fn uses_vim_insert_cursor(&self) -> bool {        self.vim_enabled && self.vim_mode == VimMode::Insert    }    /// Return whether Escape should be intercepted before composer-level routing.    ///    /// In Vim insert mode, Escape is an editing transition rather than a popup    /// cancel/backtrack shortcut. Letting the composer handle it first would    /// close UI surfaces while leaving the textarea in insert mode.    pub(crate) fn should_handle_vim_insert_escape(&self, event: KeyEvent) -> bool {        self.vim_enabled            && self.vim_mode == VimMode::Insert            && event.code == KeyCode::Esc            && event.modifiers == KeyModifiers::NONE            && matches!(event.kind, KeyEventKind::Press | KeyEventKind::Repeat)    }    /// Return the footer label for the active Vim mode.    ///    /// `None` means Vim editing is disabled, so callers should omit the mode    /// indicator rather than rendering an insert-mode label for normal    /// non-modal editing.    pub(crate) fn vim_mode_label(&self) -> Option<&'static str> {        if !self.vim_enabled {            return None;        }        Some(match self.vim_mode {            VimMode::Normal => "Normal",            VimMode::Insert => "Insert",        })    }    pub fn text(&self) -> &str {        &self.text    }    pub fn insert_str(&mut self, text: &str) {        self.insert_str_at(self.cursor_pos, text);    }    pub fn insert_str_at(&mut self, pos: usize, text: &str) {        let pos = self.clamp_pos_for_insertion(pos);        self.text.insert_str(pos, text);        self.wrap_cache.replace(None);        if pos <= self.cursor_pos {            self.cursor_pos += text.len();        }        self.shift_elements(pos, /*removed*/ 0, text.len());        self.preferred_col = None;    }    pub fn replace_range(&mut self, range: std::ops::Range<usize>, text: &str) {        let range = self.expand_range_to_element_boundaries(range);        self.replace_range_raw(range, text);    }    fn replace_range_raw(&mut self, range: std::ops::Range<usize>, text: &str) {        assert!(range.start <= range.end);        let start = range.start.clamp(0, self.text.len());        let end = range.end.clamp(0, self.text.len());        let removed_len = end - start;        let inserted_len = text.len();        if removed_len == 0 && inserted_len == 0 {            return;        }        let diff = inserted_len as isize - removed_len as isize;        self.text.replace_range(range, text);        self.wrap_cache.replace(None);        self.preferred_col = None;        self.update_elements_after_replace(start, end, inserted_len);        // Update the cursor position to account for the edit.        self.cursor_pos = if self.cursor_pos < start {            // Cursor was before the edited range – no shift.            self.cursor_pos        } else if self.cursor_pos <= end {            // Cursor was inside the replaced range – move to end of the new text.            start + inserted_len        } else {            // Cursor was after the replaced range – shift by the length diff.            ((self.cursor_pos as isize) + diff) as usize        }        .min(self.text.len());        // Ensure cursor is not inside an element        self.cursor_pos = self.clamp_pos_to_nearest_boundary(self.cursor_pos);    }    pub fn cursor(&self) -> usize {        self.cursor_pos    }    pub fn set_cursor(&mut self, pos: usize) {        self.cursor_pos = pos.clamp(0, self.text.len());        self.cursor_pos = self.clamp_pos_to_nearest_boundary(self.cursor_pos);        self.preferred_col = None;    }    pub fn desired_height(&self, width: u16) -> u16 {        self.wrapped_lines(width).len() as u16    }    #[cfg_attr(not(test), allow(dead_code))]    pub fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> {        self.cursor_pos_with_state(area, TextAreaState::default())    }    /// Compute the on-screen cursor position taking scrolling into account.    pub fn cursor_pos_with_state(&self, area: Rect, state: TextAreaState) -> Option<(u16, u16)> {        let lines = self.wrapped_lines(area.width);        let effective_scroll = self.effective_scroll(area.height, &lines, state.scroll);        let i = Self::wrapped_line_index_by_start(&lines, self.cursor_pos)?;        let ls = &lines[i];        let col = self.text[ls.start..self.cursor_pos].width() as u16;        let screen_row = i            .saturating_sub(effective_scroll as usize)            .try_into()            .unwrap_or(0);        Some((area.x + col, area.y + screen_row))    }    pub fn is_empty(&self) -> bool {        self.text.is_empty()    }    fn current_display_col(&self) -> usize {        let bol = self.beginning_of_current_line();        self.text[bol..self.cursor_pos].width()    }    fn wrapped_line_index_by_start(lines: &[Range<usize>], pos: usize) -> Option<usize> {        // partition_point returns the index of the first element for which        // the predicate is false, i.e. the count of elements with start <= pos.        let idx = lines.partition_point(|r| r.start <= pos);        if idx == 0 { None } else { Some(idx - 1) }    }    fn move_to_display_col_on_line(        &mut self,        line_start: usize,        line_end: usize,        target_col: usize,    ) {        let mut width_so_far = 0usize;        for (i, g) in self.text[line_start..line_end].grapheme_indices(true) {            width_so_far += g.width();            if width_so_far > target_col {                self.cursor_pos = line_start + i;                // Avoid landing inside an element; round to nearest boundary                self.cursor_pos = self.clamp_pos_to_nearest_boundary(self.cursor_pos);                return;            }        }        self.cursor_pos = line_end;        self.cursor_pos = self.clamp_pos_to_nearest_boundary(self.cursor_pos);    }    fn beginning_of_line(&self, pos: usize) -> usize {        self.text[..pos].rfind('\n').map(|i| i + 1).unwrap_or(0)    }    fn beginning_of_current_line(&self) -> usize {        self.beginning_of_line(self.cursor_pos)    }    fn first_non_blank_of_current_line(&self) -> usize {        let bol = self.beginning_of_current_line();        let eol = self.end_of_current_line();        self.text[bol..eol]            .char_indices()            .find_map(|(offset, ch)| (!ch.is_whitespace()).then_some(bol + offset))            .unwrap_or(eol)    }    fn end_of_line(&self, pos: usize) -> usize {        self.text[pos..]            .find('\n')            .map(|i| i + pos)            .unwrap_or(self.text.len())    }    fn end_of_current_line(&self) -> usize {        self.end_of_line(self.cursor_pos)    }    pub fn input(&mut self, event: KeyEvent) {        // Only process key presses or repeats; ignore releases to avoid inserting        // characters on key-up events when modifiers are no longer reported.        if !matches!(event.kind, KeyEventKind::Press | KeyEventKind::Repeat) {            return;        }        if self.vim_enabled {            self.handle_vim_input(event);        } else {            let keymap = self.editor_keymap.clone();            self.input_with_keymap(event, &keymap);        }    }    pub fn input_with_keymap(&mut self, event: KeyEvent, keymap: &EditorKeymap) {        if keymap.insert_newline.is_pressed(event) {            self.insert_str("\n");            return;        }        if keymap.delete_backward_word.is_pressed(event) {            self.delete_backward_word();            return;        }        // Windows AltGr generates ALT|CONTROL. Preserve typed characters for AltGr users        // unless a specific shortcut already matched above.        if let KeyEvent {            code: KeyCode::Char(c),            modifiers,            ..        } = event            && is_altgr(modifiers)        {            self.insert_str(&c.to_string());            return;        }        if keymap.delete_backward.is_pressed(event) {            self.delete_backward(/*n*/ 1);            return;        }        if keymap.delete_forward_word.is_pressed(event) {            self.delete_forward_word();            return;        }        if keymap.delete_forward.is_pressed(event) {            self.delete_forward(/*n*/ 1);            return;        }        if keymap.kill_line_start.is_pressed(event) {            self.kill_to_beginning_of_line();            return;        }        if keymap.kill_whole_line.is_pressed(event) {            self.kill_current_line();            return;        }        if keymap.kill_line_end.is_pressed(event) {            self.kill_to_end_of_line();            return;        }        if keymap.yank.is_pressed(event) {            self.yank();            return;        }        if keymap.move_word_left.is_pressed(event) {            self.set_cursor(self.beginning_of_previous_word());            return;        }        if keymap.move_word_right.is_pressed(event) {            self.set_cursor(self.end_of_next_word());            return;        }        if keymap.move_left.is_pressed(event) {            self.move_cursor_left();            return;        }        if keymap.move_right.is_pressed(event) {            self.move_cursor_right();            return;        }        if keymap.move_up.is_pressed(event) {            self.move_cursor_up();            return;        }        if keymap.move_down.is_pressed(event) {            self.move_cursor_down();            return;        }        if keymap.move_line_start.is_pressed(event) {            let move_up_at_bol = matches!(                event,                KeyEvent {                    code: KeyCode::Char('a'),                    modifiers: KeyModifiers::CONTROL,                    ..                }            );            self.move_cursor_to_beginning_of_line(move_up_at_bol);            return;        }        if keymap.move_line_end.is_pressed(event) {            let move_down_at_eol = matches!(                event,                KeyEvent {                    code: KeyCode::Char('e'),                    modifiers: KeyModifiers::CONTROL,                    ..                }            );            self.move_cursor_to_end_of_line(move_down_at_eol);            return;        }        if let KeyEvent {            code: KeyCode::Char(c),            modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT,            ..        } = event        {            // Insert plain characters (and Shift-modified). Do not insert when ALT is held,            // because many terminals map Option/Meta combos to ALT+<char>.            if c.is_ascii_control() {                return;            }            self.insert_str(&c.to_string());        }        tracing::debug!("Unhandled key event in TextArea: {:?}", event);    }    fn handle_vim_input(&mut self, event: KeyEvent) {        match self.vim_mode {            VimMode::Insert => self.handle_vim_insert(event),            VimMode::Normal => self.handle_vim_normal(event),        }    }    fn handle_vim_insert(&mut self, event: KeyEvent) {        if matches!(event.code, KeyCode::Esc) {            let bol = self.beginning_of_current_line();            if self.cursor_pos > bol {                self.cursor_pos = self.prev_atomic_boundary(self.cursor_pos).max(bol);            }            self.enter_vim_normal_mode();            return;        }        let keymap = self.editor_keymap.clone();        self.input_with_keymap(event, &keymap);    }    fn handle_vim_normal(&mut self, event: KeyEvent) {        let pending = std::mem::replace(&mut self.vim_pending, VimPending::None);        match pending {            VimPending::None => {}            VimPending::Operator(op) => {                self.handle_vim_operator(op, event);                return;            }            VimPending::TextObject { operator, scope } => {                self.handle_vim_text_object(operator, scope, event);                return;            }        }        if self.vim_normal_keymap.enter_insert.is_pressed(event) {            self.vim_mode = VimMode::Insert;            return;        }        if self.vim_normal_keymap.append_after_cursor.is_pressed(event) {            let next = self.next_atomic_boundary(self.cursor_pos);            self.set_cursor(next);            self.vim_mode = VimMode::Insert;            return;        }        if self.vim_normal_keymap.append_line_end.is_pressed(event) {            self.set_cursor(self.end_of_current_line());            self.vim_mode = VimMode::Insert;            return;        }        if self.vim_normal_keymap.insert_line_start.is_pressed(event) {            self.set_cursor(self.first_non_blank_of_current_line());            self.vim_mode = VimMode::Insert;            return;        }        if self.vim_normal_keymap.open_line_below.is_pressed(event) {            let eol = self.end_of_current_line();            let old_len = self.text.len();            let insert_at = if eol < old_len { eol + 1 } else { eol };            self.insert_str_at(insert_at, "\n");            let cursor = if eol < old_len {                insert_at            } else {                insert_at + 1            };            self.set_cursor(cursor);            self.vim_mode = VimMode::Insert;            return;        }        if self.vim_normal_keymap.open_line_above.is_pressed(event) {            let bol = self.beginning_of_current_line();            self.insert_str_at(bol, "\n");            self.set_cursor(bol);            self.vim_mode = VimMode::Insert;            return;        }        if self.vim_normal_keymap.move_left.is_pressed(event) {            self.move_cursor_left();            return;        }        if self.vim_normal_keymap.move_right.is_pressed(event) {            self.move_cursor_right();            return;        }        if self.vim_normal_keymap.move_down.is_pressed(event) {            self.move_cursor_down();            return;        }        if self.vim_normal_keymap.move_up.is_pressed(event) {            self.move_cursor_up();            return;        }        if self.vim_normal_keymap.move_word_forward.is_pressed(event) {            self.set_cursor(self.beginning_of_next_word());            return;        }        if self.vim_normal_keymap.move_word_backward.is_pressed(event) {            self.set_cursor(self.beginning_of_previous_word());            return;        }        if self.vim_normal_keymap.move_word_end.is_pressed(event) {            self.set_cursor(self.vim_word_end_cursor());            return;        }        if self.vim_normal_keymap.move_line_start.is_pressed(event) {            self.set_cursor(self.beginning_of_current_line());            return;        }        if self.vim_normal_keymap.move_line_end.is_pressed(event) {            self.set_cursor(self.vim_line_end_cursor());            return;        }        if self.vim_normal_keymap.delete_char.is_pressed(event) {            self.delete_forward_kill(/*n*/ 1);            return;        }        if self.vim_normal_keymap.substitute_char.is_pressed(event) {            if self.cursor_pos < self.end_of_current_line() {                self.delete_forward_kill(/*n*/ 1);            }            self.vim_mode = VimMode::Insert;            return;        }        if self.vim_normal_keymap.delete_to_line_end.is_pressed(event) {            self.vim_kill_to_end_of_line();            return;        }        if self.vim_normal_keymap.change_to_line_end.is_pressed(event) {            self.vim_kill_to_end_of_line();            self.vim_mode = VimMode::Insert;            return;        }        if self.vim_normal_keymap.yank_line.is_pressed(event) {            self.yank_current_line();            return;        }        if self.vim_normal_keymap.paste_after.is_pressed(event) {            self.paste_after_cursor();            return;        }        if self            .vim_normal_keymap            .start_delete_operator            .is_pressed(event)        {            self.vim_pending = VimPending::Operator(VimOperator::Delete);            return;        }        if self.vim_normal_keymap.start_yank_operator.is_pressed(event) {            self.vim_pending = VimPending::Operator(VimOperator::Yank);            return;        }        if self            .vim_normal_keymap            .start_change_operator            .is_pressed(event)        {            self.vim_pending = VimPending::Operator(VimOperator::Change);            return;        }        if self.vim_normal_keymap.cancel_operator.is_pressed(event) {            self.vim_pending = VimPending::None;        }    }    fn handle_vim_operator(&mut self, op: VimOperator, event: KeyEvent) -> bool {        if op == VimOperator::Delete && self.vim_operator_keymap.delete_line.is_pressed(event) {            self.kill_current_line();            return true;        }        if op == VimOperator::Yank && self.vim_operator_keymap.yank_line.is_pressed(event) {            self.yank_current_line();            return true;        }        if self.vim_operator_keymap.cancel.is_pressed(event) {            return true;        }        if let Some(scope) = self.vim_text_object_scope_for_event(event) {            self.vim_pending = VimPending::TextObject {                operator: op,                scope,            };            return true;        }        if op != VimOperator::Change            && let Some(motion) = self.vim_motion_for_event(event)        {            self.apply_vim_operator(op, motion);            return true;        }        false    }    fn handle_vim_text_object(        &mut self,        op: VimOperator,        scope: VimTextObjectScope,        event: KeyEvent,    ) -> bool {        if self.vim_text_object_keymap.cancel.is_pressed(event) {            return true;        }        let Some(object) = self.vim_text_object_for_event(event) else {            return false;        };        if let Some(range) = self.text_object_range(object, scope) {            self.apply_vim_operator_to_range(op, range);        }        true    }    fn vim_motion_for_event(&self, event: KeyEvent) -> Option<VimMotion> {        if self.vim_operator_keymap.motion_left.is_pressed(event) {            return Some(VimMotion::Left);        }        if self.vim_operator_keymap.motion_right.is_pressed(event) {            return Some(VimMotion::Right);        }        if self.vim_operator_keymap.motion_down.is_pressed(event) {            return Some(VimMotion::Down);        }        if self.vim_operator_keymap.motion_up.is_pressed(event) {            return Some(VimMotion::Up);        }        if self            .vim_operator_keymap            .motion_word_forward            .is_pressed(event)        {            return Some(VimMotion::WordForward);        }        if self            .vim_operator_keymap            .motion_word_backward            .is_pressed(event)        {            return Some(VimMotion::WordBackward);        }        if self.vim_operator_keymap.motion_word_end.is_pressed(event) {            return Some(VimMotion::WordEnd);        }        if self.vim_operator_keymap.motion_line_start.is_pressed(event) {            return Some(VimMotion::LineStart);        }        if self.vim_operator_keymap.motion_line_end.is_pressed(event) {            return Some(VimMotion::LineEnd);        }        None    }    fn apply_vim_operator(&mut self, op: VimOperator, motion: VimMotion) {        let Some(range) = self.range_for_motion(motion) else {            return;        };        match op {            VimOperator::Delete => self.kill_range(range),            VimOperator::Yank => self.yank_range(range),            VimOperator::Change => {}        }    }    fn apply_vim_operator_to_range(&mut self, op: VimOperator, range: Range<usize>) {        match op {            VimOperator::Delete => self.kill_range(range),            VimOperator::Yank => self.yank_range(range),            VimOperator::Change => {                self.kill_range(range);                self.vim_mode = VimMode::Insert;            }        }    }    fn range_for_motion(&mut self, motion: VimMotion) -> Option<Range<usize>> {        if matches!(motion, VimMotion::Up | VimMotion::Down) {            return self.linewise_range_for_vertical_motion(motion);        }        let start = self.cursor_pos;        let target = self.target_for_motion(motion);        if start == target {            return None;        }        let (range_start, range_end) = if target < start {            (target, start)        } else {            (start, target)        };        Some(range_start..range_end)    }    fn linewise_range_for_vertical_motion(&self, motion: VimMotion) -> Option<Range<usize>> {        let current = self.current_line_range_with_newline();        let range = match motion {            VimMotion::Up => {                let start = if current.start == 0 {                    current.start                } else {                    self.beginning_of_line(current.start.saturating_sub(1))                };                start..current.end            }            VimMotion::Down => {                let end = if current.end >= self.text.len() {                    current.end                } else {                    let next_eol = self.end_of_line(current.end);                    if next_eol < self.text.len() {                        next_eol + 1                    } else {                        next_eol                    }                };                current.start..end            }            VimMotion::Left            | VimMotion::Right            | VimMotion::WordForward            | VimMotion::WordBackward            | VimMotion::WordEnd            | VimMotion::LineStart            | VimMotion::LineEnd => return None,        };        (range.start < range.end).then_some(range)    }    fn target_for_motion(&mut self, motion: VimMotion) -> usize {        let original_cursor = self.cursor_pos;        let original_preferred = self.preferred_col;        match motion {            VimMotion::Left => self.move_cursor_left(),            VimMotion::Right => self.move_cursor_right(),            VimMotion::Up => self.move_cursor_up(),            VimMotion::Down => self.move_cursor_down(),            VimMotion::WordForward => self.set_cursor(self.beginning_of_next_word()),            VimMotion::WordBackward => self.set_cursor(self.beginning_of_previous_word()),            VimMotion::WordEnd => self.set_cursor(self.vim_word_end_exclusive()),            VimMotion::LineStart => self.set_cursor(self.beginning_of_current_line()),            VimMotion::LineEnd => self.set_cursor(self.end_of_current_line()),        }        let target = self.cursor_pos;        self.cursor_pos = original_cursor;        self.preferred_col = original_preferred;        target    }    // ####### Input Functions #######    pub fn delete_backward(&mut self, n: usize) {        if n == 0 || self.cursor_pos == 0 {            return;        }        let mut target = self.cursor_pos;        for _ in 0..n {            target = self.prev_atomic_boundary(target);            if target == 0 {                break;            }        }        self.replace_range(target..self.cursor_pos, "");    }    pub fn delete_forward(&mut self, n: usize) {        if n == 0 || self.cursor_pos >= self.text.len() {            return;        }        let mut target = self.cursor_pos;        for _ in 0..n {            target = self.next_atomic_boundary(target);            if target >= self.text.len() {                break;            }        }        self.replace_range(self.cursor_pos..target, "");    }    pub fn delete_forward_kill(&mut self, n: usize) {        if n == 0 || self.cursor_pos >= self.text.len() {            return;        }        let mut target = self.cursor_pos;        for _ in 0..n {            target = self.next_atomic_boundary(target);            if target >= self.text.len() {                break;            }        }        self.kill_range(self.cursor_pos..target);    }    pub fn delete_backward_word(&mut self) {        let start = self.beginning_of_previous_word();        self.kill_range(start..self.cursor_pos);    }    /// Delete text to the right of the cursor using "word" semantics.    ///    /// Deletes from the current cursor position through the end of the next word as determined    /// by `end_of_next_word()`. Any whitespace (including newlines) between the cursor and that    /// word is included in the deletion.    pub fn delete_forward_word(&mut self) {        let end = self.end_of_next_word();        if end > self.cursor_pos {            self.kill_range(self.cursor_pos..end);        }    }    /// Kill from the cursor to the end of the current logical line.    ///    /// If the cursor is already at end-of-line and a trailing newline exists, this kills that    /// newline so repeated invocations continue making progress. The removed text becomes the next    /// yank target and remains available even if a caller later clears or rewrites the visible    /// buffer via `set_text_*`.    pub fn kill_to_end_of_line(&mut self) {        let eol = self.end_of_current_line();        let range = if self.cursor_pos == eol {            if eol < self.text.len() {                Some(self.cursor_pos..eol + 1)            } else {                None            }        } else {            Some(self.cursor_pos..eol)        };        if let Some(range) = range {            self.kill_range(range);        }    }    fn vim_kill_to_end_of_line(&mut self) {        let eol = self.end_of_current_line();        if self.cursor_pos < eol {            self.kill_range(self.cursor_pos..eol);        }    }    pub fn kill_to_beginning_of_line(&mut self) {        let bol = self.beginning_of_current_line();        let range = if self.cursor_pos == bol {            if bol > 0 { Some(bol - 1..bol) } else { None }        } else {            Some(bol..self.cursor_pos)        };        if let Some(range) = range {            self.kill_range(range);        }    }    /// Insert the most recently killed text at the cursor.    ///    /// This uses the textarea's single-entry kill buffer. Because whole-buffer replacement APIs do    /// not clear that buffer, `yank` can restore text after composer-level clears such as submit    /// and slash-command dispatch.    pub fn yank(&mut self) {        if self.kill_buffer.is_empty() {            return;        }        let text = self.kill_buffer.clone();        self.insert_str(&text);    }    fn kill_range(&mut self, range: Range<usize>) {        self.kill_range_with_kind(range, KillBufferKind::Characterwise);    }    fn kill_line_range(&mut self, range: Range<usize>) {        self.kill_range_with_kind(range, KillBufferKind::Linewise);    }    fn kill_range_with_kind(&mut self, range: Range<usize>, kind: KillBufferKind) {        let range = self.expand_range_to_element_boundaries(range);        if range.start >= range.end {            return;        }        let removed = self.text[range.clone()].to_string();        if removed.is_empty() {            return;        }        self.store_kill_buffer(removed, kind);        self.replace_range_raw(range, "");    }    fn yank_range(&mut self, range: Range<usize>) {        self.yank_range_with_kind(range, KillBufferKind::Characterwise);    }    fn yank_line_range(&mut self, range: Range<usize>) {        self.yank_range_with_kind(range, KillBufferKind::Linewise);    }    fn yank_range_with_kind(&mut self, range: Range<usize>, kind: KillBufferKind) {        let range = self.expand_range_to_element_boundaries(range);        if range.start >= range.end {            return;        }        let removed = self.text[range].to_string();        if removed.is_empty() {            return;        }        self.store_kill_buffer(removed, kind);    }    fn store_kill_buffer(&mut self, text: String, kind: KillBufferKind) {        self.kill_buffer = text;        self.kill_buffer_kind = kind;    }    fn paste_after_cursor(&mut self) {        if self.kill_buffer.is_empty() {            return;        }        if self.kill_buffer_kind == KillBufferKind::Linewise {            self.paste_line_after_current_line();            return;        }        let insert_at = self.next_atomic_boundary(self.cursor_pos);        self.set_cursor(insert_at);        let text = self.kill_buffer.clone();        self.insert_str(&text);    }    fn paste_line_after_current_line(&mut self) {        let eol = self.end_of_current_line();        let insert_at = if eol < self.text.len() { eol + 1 } else { eol };        let cursor = if eol < self.text.len() {            insert_at        } else {            insert_at + 1        };        let text = if eol < self.text.len() {            if self.kill_buffer.ends_with('\n') {                self.kill_buffer.clone()            } else {                format!("{}\n", self.kill_buffer)            }        } else {            format!("\n{}", self.kill_buffer.trim_end_matches('\n'))        };        self.insert_str_at(insert_at, &text);        self.set_cursor(cursor.min(self.text.len()));    }    fn yank_current_line(&mut self) {        let range = self.current_line_range_with_newline();        self.yank_line_range(range);    }    fn kill_current_line(&mut self) {        let range = self.current_line_range_with_newline();        self.kill_line_range(range);    }    fn current_line_range_with_newline(&self) -> Range<usize> {        let bol = self.beginning_of_current_line();        let eol = self.end_of_current_line();        let end = if eol < self.text.len() { eol + 1 } else { eol };        bol..end    }    /// Move the cursor left by a single grapheme cluster.    pub fn move_cursor_left(&mut self) {        self.cursor_pos = self.prev_atomic_boundary(self.cursor_pos);        self.preferred_col = None;    }    /// Move the cursor right by a single grapheme cluster.    pub fn move_cursor_right(&mut self) {        self.cursor_pos = self.next_atomic_boundary(self.cursor_pos);        self.preferred_col = None;    }    pub fn move_cursor_up(&mut self) {        // If we have a wrapping cache, prefer navigating across wrapped (visual) lines.        if let Some((target_col, maybe_line)) = {            let cache_ref = self.wrap_cache.borrow();            if let Some(cache) = cache_ref.as_ref() {                let lines = &cache.lines;                if let Some(idx) = Self::wrapped_line_index_by_start(lines, self.cursor_pos) {                    let cur_range = &lines[idx];                    let target_col = self                        .preferred_col                        .unwrap_or_else(|| self.text[cur_range.start..self.cursor_pos].width());                    if idx > 0 {                        let prev = &lines[idx - 1];                        let line_start = prev.start;                        let line_end = prev.end.saturating_sub(1);                        Some((target_col, Some((line_start, line_end))))                    } else {                        Some((target_col, None))                    }                } else {                    None                }            } else {                None            }        } {            // We had wrapping info. Apply movement accordingly.            match maybe_line {                Some((line_start, line_end)) => {                    if self.preferred_col.is_none() {                        self.preferred_col = Some(target_col);                    }                    self.move_to_display_col_on_line(line_start, line_end, target_col);                    return;                }                None => {                    // Already at first visual line -> move to start                    self.cursor_pos = 0;                    self.preferred_col = None;                    return;                }            }        }        // Fallback to logical line navigation if we don't have wrapping info yet.        if let Some(prev_nl) = self.text[..self.cursor_pos].rfind('\n') {            let target_col = match self.preferred_col {                Some(c) => c,                None => {                    let c = self.current_display_col();                    self.preferred_col = Some(c);                    c                }            };            let prev_line_start = self.text[..prev_nl].rfind('\n').map(|i| i + 1).unwrap_or(0);            let prev_line_end = prev_nl;            self.move_to_display_col_on_line(prev_line_start, prev_line_end, target_col);        } else {            self.cursor_pos = 0;            self.preferred_col = None;        }    }    pub fn move_cursor_down(&mut self) {        // If we have a wrapping cache, prefer navigating across wrapped (visual) lines.        if let Some((target_col, move_to_last)) = {            let cache_ref = self.wrap_cache.borrow();            if let Some(cache) = cache_ref.as_ref() {                let lines = &cache.lines;                if let Some(idx) = Self::wrapped_line_index_by_start(lines, self.cursor_pos) {                    let cur_range = &lines[idx];                    let target_col = self                        .preferred_col                        .unwrap_or_else(|| self.text[cur_range.start..self.cursor_pos].width());                    if idx + 1 < lines.len() {                        let next = &lines[idx + 1];                        let line_start = next.start;                        let line_end = next.end.saturating_sub(1);                        Some((target_col, Some((line_start, line_end))))                    } else {                        Some((target_col, None))                    }                } else {                    None                }            } else {                None            }        } {            match move_to_last {                Some((line_start, line_end)) => {                    if self.preferred_col.is_none() {                        self.preferred_col = Some(target_col);                    }                    self.move_to_display_col_on_line(line_start, line_end, target_col);                    return;                }                None => {                    // Already on last visual line -> move to end                    self.cursor_pos = self.text.len();                    self.preferred_col = None;                    return;                }            }        }        // Fallback to logical line navigation if we don't have wrapping info yet.        let target_col = match self.preferred_col {            Some(c) => c,            None => {                let c = self.current_display_col();                self.preferred_col = Some(c);                c            }        };        if let Some(next_nl) = self.text[self.cursor_pos..]            .find('\n')            .map(|i| i + self.cursor_pos)        {            let next_line_start = next_nl + 1;            let next_line_end = self.text[next_line_start..]                .find('\n')                .map(|i| i + next_line_start)                .unwrap_or(self.text.len());            self.move_to_display_col_on_line(next_line_start, next_line_end, target_col);        } else {            self.cursor_pos = self.text.len();            self.preferred_col = None;        }    }    pub fn move_cursor_to_beginning_of_line(&mut self, move_up_at_bol: bool) {        let bol = self.beginning_of_current_line();        if move_up_at_bol && self.cursor_pos == bol {            self.set_cursor(self.beginning_of_line(self.cursor_pos.saturating_sub(1)));        } else {            self.set_cursor(bol);        }        self.preferred_col = None;    }    pub fn move_cursor_to_end_of_line(&mut self, move_down_at_eol: bool) {        let eol = self.end_of_current_line();        if move_down_at_eol && self.cursor_pos == eol {            let next_pos = (self.cursor_pos.saturating_add(1)).min(self.text.len());            self.set_cursor(self.end_of_line(next_pos));        } else {            self.set_cursor(eol);        }    }    // ===== Text elements support =====    pub fn element_payloads(&self) -> Vec<String> {        self.elements            .iter()            .filter_map(|e| self.text.get(e.range.clone()).map(str::to_string))            .collect()    }    pub fn text_elements(&self) -> Vec<UserTextElement> {        self.elements            .iter()            .map(|e| {                let placeholder = self.text.get(e.range.clone()).map(str::to_string);                UserTextElement::new(                    ByteRange {                        start: e.range.start,                        end: e.range.end,                    },                    placeholder,                )            })            .collect()    }    pub(crate) fn text_element_snapshots(&self) -> Vec<TextElementSnapshot> {        self.elements            .iter()            .filter_map(|element| {                self.text                    .get(element.range.clone())                    .map(|text| TextElementSnapshot {                        id: element.id,                        range: element.range.clone(),                        text: text.to_string(),                    })            })            .collect()    }    pub(crate) fn element_id_for_exact_range(&self, range: Range<usize>) -> Option<u64> {        self.elements            .iter()            .find(|element| element.range == range)            .map(|element| element.id)    }    /// Renames a single text element in-place, keeping it atomic.    ///    /// Use this when the element payload is an identifier (e.g. a placeholder) that must be    /// updated without converting the element back into normal text.    pub fn replace_element_payload(&mut self, old: &str, new: &str) -> bool {        let Some(idx) = self            .elements            .iter()            .position(|e| self.text.get(e.range.clone()) == Some(old))        else {            return false;        };        let range = self.elements[idx].range.clone();        let start = range.start;        let end = range.end;        if start > end || end > self.text.len() {            return false;        }        let removed_len = end - start;        let inserted_len = new.len();        let diff = inserted_len as isize - removed_len as isize;        self.text.replace_range(range, new);        self.wrap_cache.replace(None);        self.preferred_col = None;        // Update the modified element's range.        self.elements[idx].range = start..(start + inserted_len);        // Shift element ranges that occur after the replaced element.        if diff != 0 {            for (j, e) in self.elements.iter_mut().enumerate() {                if j == idx {                    continue;                }                if e.range.end <= start {                    continue;                }                if e.range.start >= end {                    e.range.start = ((e.range.start as isize) + diff) as usize;                    e.range.end = ((e.range.end as isize) + diff) as usize;                    continue;                }                // Elements should not partially overlap each other; degrade gracefully by                // snapping anything intersecting the replaced range to the new bounds.                e.range.start = start.min(e.range.start);                e.range.end = (start + inserted_len).max(e.range.end.saturating_add_signed(diff));            }        }        // Update the cursor position to account for the edit.        self.cursor_pos = if self.cursor_pos < start {            self.cursor_pos        } else if self.cursor_pos <= end {            start + inserted_len        } else {            ((self.cursor_pos as isize) + diff) as usize        };        self.cursor_pos = self.clamp_pos_to_nearest_boundary(self.cursor_pos);        // Keep element ordering deterministic.        self.elements.sort_by_key(|e| e.range.start);        true    }    pub fn insert_element(&mut self, text: &str) -> u64 {        let start = self.clamp_pos_for_insertion(self.cursor_pos);        self.insert_str_at(start, text);        let end = start + text.len();        let id = self.add_element(start..end);        // Place cursor at end of inserted element        self.set_cursor(end);        id    }    fn add_element(&mut self, range: Range<usize>) -> u64 {        let id = self.next_element_id();        self.elements.push(TextElement { id, range });        self.elements.sort_by_key(|e| e.range.start);        id    }    /// Mark an existing text range as an atomic element without changing the text.    ///    /// This is used to convert already-typed tokens (like `/plan`) into elements    /// so they render and edit atomically. Overlapping or duplicate ranges are ignored.    pub fn add_element_range(&mut self, range: Range<usize>) -> Option<u64> {        let start = self.clamp_pos_to_char_boundary(range.start.min(self.text.len()));        let end = self.clamp_pos_to_char_boundary(range.end.min(self.text.len()));        if start >= end {            return None;        }        if self            .elements            .iter()            .any(|e| e.range.start == start && e.range.end == end)        {            return None;        }        if self            .elements            .iter()            .any(|e| start < e.range.end && end > e.range.start)        {            return None;        }        let id = self.add_element(start..end);        Some(id)    }    pub fn remove_element_range(&mut self, range: Range<usize>) -> bool {        let start = self.clamp_pos_to_char_boundary(range.start.min(self.text.len()));        let end = self.clamp_pos_to_char_boundary(range.end.min(self.text.len()));        if start >= end {            return false;        }        let len_before = self.elements.len();        self.elements            .retain(|elem| elem.range.start != start || elem.range.end != end);        len_before != self.elements.len()    }    fn next_element_id(&mut self) -> u64 {        let id = self.next_element_id;        self.next_element_id = self.next_element_id.saturating_add(1);        id    }    fn find_element_containing(&self, pos: usize) -> Option<usize> {        self.elements            .iter()            .position(|e| pos > e.range.start && pos < e.range.end)    }    fn clamp_pos_to_char_boundary(&self, pos: usize) -> usize {        let pos = pos.min(self.text.len());        if self.text.is_char_boundary(pos) {            return pos;        }        let mut prev = pos;        while prev > 0 && !self.text.is_char_boundary(prev) {            prev -= 1;        }        let mut next = pos;        while next < self.text.len() && !self.text.is_char_boundary(next) {            next += 1;        }        if pos.saturating_sub(prev) <= next.saturating_sub(pos) {            prev        } else {            next        }    }    fn clamp_pos_to_nearest_boundary(&self, pos: usize) -> usize {        let pos = self.clamp_pos_to_char_boundary(pos);        if let Some(idx) = self.find_element_containing(pos) {            let e = &self.elements[idx];            let dist_start = pos.saturating_sub(e.range.start);            let dist_end = e.range.end.saturating_sub(pos);            if dist_start <= dist_end {                self.clamp_pos_to_char_boundary(e.range.start)            } else {                self.clamp_pos_to_char_boundary(e.range.end)            }        } else {            pos        }    }    fn clamp_pos_for_insertion(&self, pos: usize) -> usize {        let pos = self.clamp_pos_to_char_boundary(pos);        // Do not allow inserting into the middle of an element        if let Some(idx) = self.find_element_containing(pos) {            let e = &self.elements[idx];            // Choose closest edge for insertion            let dist_start = pos.saturating_sub(e.range.start);            let dist_end = e.range.end.saturating_sub(pos);            if dist_start <= dist_end {                self.clamp_pos_to_char_boundary(e.range.start)            } else {                self.clamp_pos_to_char_boundary(e.range.end)            }        } else {            pos        }    }    fn expand_range_to_element_boundaries(&self, mut range: Range<usize>) -> Range<usize> {        // Expand to include any intersecting elements fully        loop {            let mut changed = false;            for e in &self.elements {                if e.range.start < range.end && e.range.end > range.start {                    let new_start = range.start.min(e.range.start);                    let new_end = range.end.max(e.range.end);                    if new_start != range.start || new_end != range.end {                        range.start = new_start;                        range.end = new_end;                        changed = true;                    }                }            }            if !changed {                break;            }        }        range    }    fn shift_elements(&mut self, at: usize, removed: usize, inserted: usize) {        // Generic shift: for pure insert, removed = 0; for delete, inserted = 0.        let end = at + removed;        let diff = inserted as isize - removed as isize;        // Remove elements fully deleted by the operation and shift the rest        self.elements            .retain(|e| !(e.range.start >= at && e.range.end <= end));        for e in &mut self.elements {            if e.range.end <= at {                // before edit            } else if e.range.start >= end {                // after edit                e.range.start = ((e.range.start as isize) + diff) as usize;                e.range.end = ((e.range.end as isize) + diff) as usize;            } else {                // Overlap with element but not fully contained (shouldn't happen when using                // element-aware replace, but degrade gracefully by snapping element to new bounds)                let new_start = at.min(e.range.start);                let new_end = at + inserted.max(e.range.end.saturating_sub(end));                e.range.start = new_start;                e.range.end = new_end;            }        }    }    fn update_elements_after_replace(&mut self, start: usize, end: usize, inserted_len: usize) {        self.shift_elements(start, end.saturating_sub(start), inserted_len);    }    fn prev_atomic_boundary(&self, pos: usize) -> usize {        if pos == 0 {            return 0;        }        // If currently at an element end or inside, jump to start of that element.        if let Some(idx) = self            .elements            .iter()            .position(|e| pos > e.range.start && pos <= e.range.end)        {            return self.elements[idx].range.start;        }        let mut gc = unicode_segmentation::GraphemeCursor::new(pos, self.text.len(), false);        match gc.prev_boundary(&self.text, 0) {            Ok(Some(b)) => {                if let Some(idx) = self.find_element_containing(b) {                    self.elements[idx].range.start                } else {                    b                }            }            Ok(None) => 0,            Err(_) => pos.saturating_sub(1),        }    }    fn next_atomic_boundary(&self, pos: usize) -> usize {        if pos >= self.text.len() {            return self.text.len();        }        // If currently at an element start or inside, jump to end of that element.        if let Some(idx) = self            .elements            .iter()            .position(|e| pos >= e.range.start && pos < e.range.end)        {            return self.elements[idx].range.end;        }        let mut gc = unicode_segmentation::GraphemeCursor::new(pos, self.text.len(), false);        match gc.next_boundary(&self.text, 0) {            Ok(Some(b)) => {                if let Some(idx) = self.find_element_containing(b) {                    self.elements[idx].range.end                } else {                    b                }            }            Ok(None) => self.text.len(),            Err(_) => pos.saturating_add(1),        }    }    pub(crate) fn beginning_of_previous_word(&self) -> usize {        let prefix = &self.text[..self.cursor_pos];        let Some((first_non_ws_idx, ch)) = prefix            .char_indices()            .rev()            .find(|&(_, ch)| !ch.is_whitespace())        else {            return 0;        };        let run_start = prefix[..first_non_ws_idx]            .char_indices()            .rev()            .find(|&(_, ch)| ch.is_whitespace())            .map_or(0, |(idx, ch)| idx + ch.len_utf8());        let run_end = first_non_ws_idx + ch.len_utf8();        let pieces = split_word_pieces(&prefix[run_start..run_end]);        let mut pieces = pieces.into_iter().rev().peekable();        let Some((piece_start, piece)) = pieces.next() else {            return run_start;        };        let mut start = run_start + piece_start;        if piece.chars().all(is_word_separator) {            while let Some((idx, piece)) = pieces.peek() {                if !piece.chars().all(is_word_separator) {                    break;                }                start = run_start + *idx;                pieces.next();            }        }        self.adjust_pos_out_of_elements(start, /*prefer_start*/ true)    }    pub(crate) fn end_of_next_word(&self) -> usize {        self.end_of_next_word_from(self.cursor_pos)    }    fn end_of_next_word_from(&self, cursor_pos: usize) -> usize {        let suffix = &self.text[cursor_pos..];        let Some(first_non_ws) = suffix.find(|ch: char| !ch.is_whitespace()) else {            return self.text.len();        };        let run = &suffix[first_non_ws..];        let run = &run[..run.find(char::is_whitespace).unwrap_or(run.len())];        let mut pieces = split_word_pieces(run).into_iter().peekable();        let Some((start, piece)) = pieces.next() else {            return cursor_pos + first_non_ws;        };        let word_start = cursor_pos + first_non_ws + start;        let mut end = word_start + piece.len();        if piece.chars().all(is_word_separator) {            while let Some((idx, piece)) = pieces.peek() {                if !piece.chars().all(is_word_separator) {                    break;                }                end = cursor_pos + first_non_ws + *idx + piece.len();                pieces.next();            }        }        self.adjust_pos_out_of_elements(end, /*prefer_start*/ false)    }    fn vim_word_end_exclusive(&self) -> usize {        let end = self.end_of_next_word();        let target = if end > self.cursor_pos {            self.prev_atomic_boundary(end)        } else {            end        };        if target == self.cursor_pos && end < self.text.len() {            self.end_of_next_word_from(end)        } else {            end        }    }    fn vim_word_end_cursor(&self) -> usize {        let end = self.vim_word_end_exclusive();        if end > self.cursor_pos {            self.prev_atomic_boundary(end)        } else {            end        }    }    fn vim_line_end_cursor(&self) -> usize {        let bol = self.beginning_of_current_line();        let eol = self.end_of_current_line();        if eol > bol {            self.prev_atomic_boundary(eol).max(bol)        } else {            eol        }    }    pub(crate) fn beginning_of_next_word(&self) -> usize {        let Some(first_non_ws) = self.text[self.cursor_pos..].find(|c: char| !c.is_whitespace())        else {            return self.text.len();        };        let word_start = self.cursor_pos + first_non_ws;        if word_start != self.cursor_pos {            return self.adjust_pos_out_of_elements(word_start, /*prefer_start*/ true);        }        let end = self.end_of_next_word();        if end >= self.text.len() {            return self.text.len();        }        let Some(next_non_ws) = self.text[end..].find(|c: char| !c.is_whitespace()) else {            return self.text.len();        };        self.adjust_pos_out_of_elements(end + next_non_ws, /*prefer_start*/ true)    }    fn adjust_pos_out_of_elements(&self, pos: usize, prefer_start: bool) -> usize {        if let Some(idx) = self.find_element_containing(pos) {            let e = &self.elements[idx];            if prefer_start {                e.range.start            } else {                e.range.end            }        } else {            pos        }    }    #[expect(clippy::unwrap_used)]    fn wrapped_lines(&self, width: u16) -> Ref<'_, Vec<Range<usize>>> {        // Ensure cache is ready (potentially mutably borrow, then drop)        {            let mut cache = self.wrap_cache.borrow_mut();            let needs_recalc = match cache.as_ref() {                Some(c) => c.width != width,                None => true,            };            if needs_recalc {                let lines = crate::wrapping::wrap_ranges(                    &self.text,                    Options::new(width as usize).wrap_algorithm(textwrap::WrapAlgorithm::FirstFit),                );                *cache = Some(WrapCache { width, lines });            }        }        let cache = self.wrap_cache.borrow();        Ref::map(cache, |c| &c.as_ref().unwrap().lines)    }    /// Calculate the scroll offset that should be used to satisfy the    /// invariants given the current area size and wrapped lines.    ///    /// - Cursor is always on screen.    /// - No scrolling if content fits in the area.    fn effective_scroll(        &self,        area_height: u16,        lines: &[Range<usize>],        current_scroll: u16,    ) -> u16 {        let total_lines = lines.len() as u16;        if area_height >= total_lines {            return 0;        }        // Where is the cursor within wrapped lines? Prefer assigning boundary positions        // (where pos equals the start of a wrapped line) to that later line.        let cursor_line_idx =            Self::wrapped_line_index_by_start(lines, self.cursor_pos).unwrap_or(0) as u16;        let max_scroll = total_lines.saturating_sub(area_height);        let mut scroll = current_scroll.min(max_scroll);        // Ensure cursor is visible within [scroll, scroll + area_height)        if cursor_line_idx < scroll {            scroll = cursor_line_idx;        } else if cursor_line_idx >= scroll + area_height {            scroll = cursor_line_idx + 1 - area_height;        }        scroll    }}impl WidgetRef for &TextArea {    fn render_ref(&self, area: Rect, buf: &mut Buffer) {        let lines = self.wrapped_lines(area.width);        self.render_lines(area, buf, &lines, 0..lines.len(), Style::default(), &[]);    }}impl StatefulWidgetRef for &TextArea {    type State = TextAreaState;    fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {        let lines = self.wrapped_lines(area.width);        let scroll = self.effective_scroll(area.height, &lines, state.scroll);        state.scroll = scroll;        let start = scroll as usize;        let end = (scroll + area.height).min(lines.len() as u16) as usize;        self.render_lines(area, buf, &lines, start..end, Style::default(), &[]);    }}impl TextArea {    pub(crate) fn render_ref_masked(        &self,        area: Rect,        buf: &mut Buffer,        state: &mut TextAreaState,        mask_char: char,    ) {        let lines = self.wrapped_lines(area.width);        let scroll = self.effective_scroll(area.height, &lines, state.scroll);        state.scroll = scroll;        let start = scroll as usize;        let end = (scroll + area.height).min(lines.len() as u16) as usize;        self.render_lines_masked(area, buf, &lines, start..end, mask_char);    }    /// Render the textarea with `base_style` plus additional render-only highlight ranges.    ///    /// Highlight ranges are byte ranges in `self.text`. They affect only the buffer rendering and    /// do not mutate the editable text, cursor, element metadata, or wrapping cache.    pub(crate) fn render_ref_styled_with_highlights(        &self,        area: Rect,        buf: &mut Buffer,        state: &mut TextAreaState,        base_style: Style,        highlights: &[(Range<usize>, Style)],    ) {        let lines = self.wrapped_lines(area.width);        let scroll = self.effective_scroll(area.height, &lines, state.scroll);        state.scroll = scroll;        let start = scroll as usize;        let end = (scroll + area.height).min(lines.len() as u16) as usize;        self.render_lines(area, buf, &lines, start..end, base_style, highlights);    }    fn render_lines(        &self,        area: Rect,        buf: &mut Buffer,        lines: &[Range<usize>],        range: std::ops::Range<usize>,        base_style: Style,        highlights: &[(Range<usize>, Style)],    ) {        for (row, idx) in range.enumerate() {            let r = &lines[idx];            let y = area.y + row as u16;            let line_range = r.start..r.end - 1;            buf.set_style(Rect::new(area.x, y, area.width, 1), base_style);            // Draw base line with the provided style.            buf.set_string(area.x, y, &self.text[line_range.clone()], base_style);            // Overlay styled segments for elements that intersect this line.            for elem in &self.elements {                // Compute overlap with displayed slice.                let overlap_start = elem.range.start.max(line_range.start);                let overlap_end = elem.range.end.min(line_range.end);                if overlap_start >= overlap_end {                    continue;                }                let styled = &self.text[overlap_start..overlap_end];                let x_off = self.text[line_range.start..overlap_start].width() as u16;                let style = base_style.fg(Color::Cyan);                buf.set_string(area.x + x_off, y, styled, style);            }            // Overlay render-only highlight ranges last so transient search highlighting remains            // visible even when it intersects attachment placeholders or other styled elements.            for (highlight_range, style) in highlights {                let overlap_start = highlight_range.start.max(line_range.start);                let overlap_end = highlight_range.end.min(line_range.end);                if overlap_start >= overlap_end {                    continue;                }                let highlighted = &self.text[overlap_start..overlap_end];                let x_off = self.text[line_range.start..overlap_start].width() as u16;                buf.set_string(area.x + x_off, y, highlighted, *style);            }        }    }    fn render_lines_masked(        &self,        area: Rect,        buf: &mut Buffer,        lines: &[Range<usize>],        range: std::ops::Range<usize>,        mask_char: char,    ) {        for (row, idx) in range.enumerate() {            let r = &lines[idx];            let y = area.y + row as u16;            let line_range = r.start..r.end - 1;            let masked = self.text[line_range.clone()]                .chars()                .map(|_| mask_char)                .collect::<String>();            buf.set_string(area.x, y, &masked, Style::default());        }    }}#[cfg(test)]mod tests {    use super::*;    use crate::key_hint;    // crossterm types are intentionally not imported here to avoid unused warnings    use pretty_assertions::assert_eq;    use rand::prelude::*;    fn rand_grapheme(rng: &mut rand::rngs::StdRng) -> String {        let r: u8 = rng.random_range(0..100);        match r {            0..=4 => "\n".to_string(),            5..=12 => " ".to_string(),            13..=35 => (rng.random_range(b'a'..=b'z') as char).to_string(),            36..=45 => (rng.random_range(b'A'..=b'Z') as char).to_string(),            46..=52 => (rng.random_range(b'0'..=b'9') as char).to_string(),            53..=65 => {                // Some emoji (wide graphemes)                let choices = ["👍", "😊", "🐍", "🚀", "🧪", "🌟"];                choices[rng.random_range(0..choices.len())].to_string()            }            66..=75 => {                // CJK wide characters                let choices = ["漢", "字", "測", "試", "你", "好", "界", "编", "码"];                choices[rng.random_range(0..choices.len())].to_string()            }            76..=85 => {                // Combining mark sequences                let base = ["e", "a", "o", "n", "u"][rng.random_range(0..5)];                let marks = ["\u{0301}", "\u{0308}", "\u{0302}", "\u{0303}"];                format!("{base}{}", marks[rng.random_range(0..marks.len())])            }            86..=92 => {                // Some non-latin single codepoints (Greek, Cyrillic, Hebrew)                let choices = ["Ω", "β", "Ж", "ю", "ש", "م", "ह"];                choices[rng.random_range(0..choices.len())].to_string()            }            _ => {                // ZWJ sequences (single graphemes but multi-codepoint)                let choices = [                    "👩\u{200D}💻", // woman technologist                    "👨\u{200D}💻", // man technologist                    "🏳️\u{200D}🌈", // rainbow flag                ];                choices[rng.random_range(0..choices.len())].to_string()            }        }    }    fn ta_with(text: &str) -> TextArea {        let mut t = TextArea::new();        t.insert_str(text);        t    }    #[test]    fn insert_and_replace_update_cursor_and_text() {        // insert helpers        let mut t = ta_with("hello");        t.set_cursor(/*pos*/ 5);        t.insert_str("!");        assert_eq!(t.text(), "hello!");        assert_eq!(t.cursor(), 6);        t.insert_str_at(/*pos*/ 0, "X");        assert_eq!(t.text(), "Xhello!");        assert_eq!(t.cursor(), 7);        // Insert after the cursor should not move it        t.set_cursor(/*pos*/ 1);        let end = t.text().len();        t.insert_str_at(end, "Y");        assert_eq!(t.text(), "Xhello!Y");        assert_eq!(t.cursor(), 1);        // replace_range cases        // 1) cursor before range        let mut t = ta_with("abcd");        t.set_cursor(/*pos*/ 1);        t.replace_range(2..3, "Z");        assert_eq!(t.text(), "abZd");        assert_eq!(t.cursor(), 1);        // 2) cursor inside range        let mut t = ta_with("abcd");        t.set_cursor(/*pos*/ 2);        t.replace_range(1..3, "Q");        assert_eq!(t.text(), "aQd");        assert_eq!(t.cursor(), 2);        // 3) cursor after range with shifted by diff        let mut t = ta_with("abcd");        t.set_cursor(/*pos*/ 4);        t.replace_range(0..1, "AA");        assert_eq!(t.text(), "AAbcd");        assert_eq!(t.cursor(), 5);    }    #[test]    fn insert_str_at_clamps_to_char_boundary() {        let mut t = TextArea::new();        t.insert_str("你");        t.set_cursor(/*pos*/ 0);        t.insert_str_at(/*pos*/ 1, "A");        assert_eq!(t.text(), "A你");        assert_eq!(t.cursor(), 1);    }    #[test]    fn set_text_clamps_cursor_to_char_boundary() {        let mut t = TextArea::new();        t.insert_str("abcd");        t.set_cursor(/*pos*/ 1);        t.set_text_clearing_elements("你");        assert_eq!(t.cursor(), 0);        t.insert_str("a");        assert_eq!(t.text(), "a你");    }    #[test]    fn delete_backward_and_forward_edges() {        let mut t = ta_with("abc");        t.set_cursor(/*pos*/ 1);        t.delete_backward(/*n*/ 1);        assert_eq!(t.text(), "bc");        assert_eq!(t.cursor(), 0);        // deleting backward at start is a no-op        t.set_cursor(/*pos*/ 0);        t.delete_backward(/*n*/ 1);        assert_eq!(t.text(), "bc");        assert_eq!(t.cursor(), 0);        // forward delete removes next grapheme        t.set_cursor(/*pos*/ 1);        t.delete_forward(/*n*/ 1);        assert_eq!(t.text(), "b");        assert_eq!(t.cursor(), 1);        // forward delete at end is a no-op        t.set_cursor(t.text().len());        t.delete_forward(/*n*/ 1);        assert_eq!(t.text(), "b");    }    #[test]    fn delete_forward_deletes_element_at_left_edge() {        let mut t = TextArea::new();        t.insert_str("a");        t.insert_element("<element>");        t.insert_str("b");        let elem_start = t.elements[0].range.start;        t.set_cursor(elem_start);        t.delete_forward(/*n*/ 1);        assert_eq!(t.text(), "ab");        assert_eq!(t.cursor(), elem_start);    }    #[test]    fn vim_insert_and_escape() {        let mut t = TextArea::new();        t.set_vim_enabled(/*enabled*/ true);        t.input(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE));        t.input(KeyEvent::new(KeyCode::Char('h'), KeyModifiers::NONE));        t.input(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));        assert_eq!(t.text(), "h");        assert_eq!(t.vim_mode_label(), Some("Normal"));        assert_eq!(t.cursor(), 0);    }    #[test]    fn vim_insert_key_enters_insert_mode() {        let mut t = TextArea::new();        t.set_vim_enabled(/*enabled*/ true);        t.input(KeyEvent::new(KeyCode::Insert, KeyModifiers::NONE));        t.input(KeyEvent::new(KeyCode::Char('h'), KeyModifiers::NONE));        assert_eq!(t.text(), "h");        assert_eq!(t.vim_mode_label(), Some("Insert"));    }    #[test]    fn vim_normal_arrow_keys_move_cursor() {        let mut t = ta_with("ab\ncd");        t.set_cursor(/*pos*/ 1);        t.set_vim_enabled(/*enabled*/ true);        t.input(KeyEvent::new(KeyCode::Right, KeyModifiers::NONE));        assert_eq!(t.cursor(), 2);        t.input(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE));        assert_eq!(t.cursor(), 5);        t.input(KeyEvent::new(KeyCode::Left, KeyModifiers::NONE));        assert_eq!(t.cursor(), 4);        t.input(KeyEvent::new(KeyCode::Up, KeyModifiers::NONE));        assert_eq!(t.cursor(), 1);    }    #[test]    fn vim_escape_from_insert_at_start_does_not_underflow() {        let mut t = TextArea::new();        t.set_vim_enabled(/*enabled*/ true);        t.input(KeyEvent::new(KeyCode::Char('z'), KeyModifiers::NONE));        t.input(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));        assert_eq!(t.vim_mode_label(), Some("Normal"));        assert_eq!(t.cursor(), 0);    }    #[test]    fn vim_escape_from_insert_at_line_start_stays_on_line() {        let mut t = ta_with("one\ntwo");        t.set_cursor(/*pos*/ "one\n".len());        t.set_vim_enabled(/*enabled*/ true);        t.input(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE));        t.input(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));        assert_eq!(t.vim_mode_label(), Some("Normal"));        assert_eq!(t.cursor(), "one\n".len());    }    #[test]    fn vim_escape_moves_by_grapheme_boundary() {        let mut t = ta_with("👍👍");        t.set_cursor(t.text().len());        t.set_vim_enabled(/*enabled*/ true);        t.input(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE));        t.input(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));        assert_eq!(t.vim_mode_label(), Some("Normal"));        assert_eq!(t.cursor(), "👍".len());    }    #[test]    fn vim_escape_respects_atomic_element_boundary() {        let mut t = TextArea::new();        t.insert_str("a");        t.insert_element("<element>");        t.set_vim_enabled(/*enabled*/ true);        t.input(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE));        t.input(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));        assert_eq!(t.vim_mode_label(), Some("Normal"));        assert_eq!(t.cursor(), 1);    }    #[test]    fn vim_shift_i_enters_insert_at_first_non_blank_with_shift_only_binding() {        let mut t = ta_with("hello\n  world");        t.vim_normal_keymap.insert_line_start = vec![key_hint::shift(KeyCode::Char('i'))];        t.set_cursor(/*pos*/ "hello\n  wor".len());        t.set_vim_enabled(/*enabled*/ true);        t.input(KeyEvent::new(KeyCode::Char('I'), KeyModifiers::NONE));        assert_eq!(t.vim_mode_label(), Some("Insert"));        assert_eq!(t.cursor(), "hello\n  ".len());    }    #[test]    fn vim_shift_a_enters_insert_at_line_end_with_shift_only_binding() {        let mut t = ta_with("hello\nworld");        t.vim_normal_keymap.append_line_end = vec![key_hint::shift(KeyCode::Char('a'))];        t.set_cursor(/*pos*/ 8);        t.set_vim_enabled(/*enabled*/ true);        t.input(KeyEvent::new(KeyCode::Char('A'), KeyModifiers::NONE));        assert_eq!(t.vim_mode_label(), Some("Insert"));        assert_eq!(t.cursor(), 11);    }    #[test]    fn vim_shift_c_changes_to_line_end_and_enters_insert_mode() {        let mut t = ta_with("hello world\nnext line");        t.set_cursor(/*pos*/ 6);        t.set_vim_enabled(/*enabled*/ true);        t.input(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::SHIFT));        assert_eq!(t.text(), "hello \nnext line");        assert_eq!(t.vim_mode_label(), Some("Insert"));        assert_eq!(t.cursor(), 6);        assert_eq!(t.kill_buffer, "world");    }    #[test]    fn vim_uppercase_c_changes_to_line_end() {        let mut t = ta_with("hello world\nnext line");        t.set_cursor(/*pos*/ 6);        t.set_vim_enabled(/*enabled*/ true);        t.input(KeyEvent::new(KeyCode::Char('C'), KeyModifiers::NONE));        assert_eq!(t.text(), "hello \nnext line");        assert_eq!(t.vim_mode_label(), Some("Insert"));        assert_eq!(t.cursor(), 6);    }    #[test]    fn vim_s_substitutes_current_character_and_enters_insert_mode() {        let mut t = ta_with("abc");        t.set_cursor(/*pos*/ 1);        t.set_vim_enabled(/*enabled*/ true);        t.input(KeyEvent::new(KeyCode::Char('s'), KeyModifiers::NONE));        assert_eq!(t.text(), "ac");        assert_eq!(t.cursor(), 1);        assert_eq!(t.vim_mode_label(), Some("Insert"));        t.input(KeyEvent::new(KeyCode::Char('X'), KeyModifiers::NONE));        assert_eq!(t.text(), "aXc");        assert_eq!(t.cursor(), 2);        assert_eq!(t.vim_mode_label(), Some("Insert"));    }    #[test]    fn vim_s_on_empty_line_enters_insert_without_deleting_newline() {        let mut t = ta_with("before\n\nnext");        t.set_cursor(/*pos*/ "before\n".len());        t.set_vim_enabled(/*enabled*/ true);        t.input(KeyEvent::new(KeyCode::Char('s'), KeyModifiers::NONE));        assert_eq!(t.text(), "before\n\nnext");        assert_eq!(t.cursor(), "before\n".len());        assert_eq!(t.vim_mode_label(), Some("Insert"));    }    #[test]    fn vim_d_at_line_end_does_not_remove_newline() {        let mut t = ta_with("hello\nworld");        t.set_cursor(/*pos*/ "hello".len());        t.set_vim_enabled(/*enabled*/ true);        t.input(KeyEvent::new(KeyCode::Char('D'), KeyModifiers::NONE));        assert_eq!(t.text(), "hello\nworld");        assert_eq!(t.vim_mode_label(), Some("Normal"));        assert_eq!(t.kill_buffer, "");    }    #[test]    fn vim_c_at_line_end_enters_insert_without_removing_newline() {        let mut t = ta_with("hello\nworld");        t.set_cursor(/*pos*/ "hello".len());        t.set_vim_enabled(/*enabled*/ true);        t.input(KeyEvent::new(KeyCode::Char('C'), KeyModifiers::NONE));        assert_eq!(t.text(), "hello\nworld");        assert_eq!(t.vim_mode_label(), Some("Insert"));        assert_eq!(t.cursor(), "hello".len());        assert_eq!(t.kill_buffer, "");    }    #[test]    fn vim_shift_o_opens_line_above_with_shift_only_binding() {        let mut t = ta_with("hello\nworld");        t.vim_normal_keymap.open_line_above = vec![key_hint::shift(KeyCode::Char('o'))];        t.set_cursor(/*pos*/ 8);        t.set_vim_enabled(/*enabled*/ true);        t.input(KeyEvent::new(KeyCode::Char('O'), KeyModifiers::NONE));        assert_eq!(t.text(), "hello\n\nworld");        assert_eq!(t.vim_mode_label(), Some("Insert"));        assert_eq!(t.cursor(), 6);    }    #[test]    fn vim_o_opens_line_below_on_inserted_line() {        let mut t = ta_with("one\ntwo");        t.set_cursor(/*pos*/ 1);        t.set_vim_enabled(/*enabled*/ true);        t.input(KeyEvent::new(KeyCode::Char('o'), KeyModifiers::NONE));        assert_eq!(t.text(), "one\n\ntwo");        assert_eq!(t.vim_mode_label(), Some("Insert"));        assert_eq!(t.cursor(), "one\n".len());    }    #[test]    fn vim_o_opens_line_below_final_line_and_moves_to_new_line() {        let mut t = ta_with("one");        t.set_cursor(/*pos*/ 1);        t.set_vim_enabled(/*enabled*/ true);        t.input(KeyEvent::new(KeyCode::Char('o'), KeyModifiers::NONE));        assert_eq!(t.text(), "one\n");        assert_eq!(t.vim_mode_label(), Some("Insert"));        assert_eq!(t.cursor(), "one\n".len());    }    #[test]    fn vim_delete_word() {        let mut t = ta_with("hello world");        t.set_cursor(/*pos*/ 0);        t.set_vim_enabled(/*enabled*/ true);        t.input(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE));        t.input(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::NONE));        assert_eq!(t.text(), "world");        assert_eq!(t.kill_buffer, "hello ");    }    #[test]    fn vim_change_inner_word_deletes_word_and_enters_insert() {        let mut t = ta_with("hello world");        t.set_cursor(/*pos*/ "hello ".len());        t.set_vim_enabled(/*enabled*/ true);        t.input(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE));        t.input(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE));        t.input(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::NONE));        assert_eq!(t.text(), "hello ");        assert_eq!(t.kill_buffer, "world");        assert_eq!(t.cursor(), "hello ".len());        assert_eq!(t.vim_mode_label(), Some("Insert"));    }    #[test]    fn vim_word_text_objects_cover_delete_yank_and_big_word() {        let mut t = ta_with("hello world");        t.set_cursor(/*pos*/ 1);        t.set_vim_enabled(/*enabled*/ true);        t.input(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));        t.input(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE));        t.input(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::NONE));        assert_eq!(t.text(), "hello world");        assert_eq!(t.kill_buffer, "hello ");        assert_eq!(t.vim_mode_label(), Some("Normal"));        let mut t = ta_with("foo.bar/baz qux");        t.set_cursor(/*pos*/ "foo.".len());        t.set_vim_enabled(/*enabled*/ true);        t.input(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE));        t.input(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE));        t.input(KeyEvent::new(KeyCode::Char('W'), KeyModifiers::NONE));        assert_eq!(t.text(), " qux");        assert_eq!(t.kill_buffer, "foo.bar/baz");    }    #[test]    fn vim_word_text_objects_accept_cursor_at_word_end() {        let mut t = ta_with("hello world");        t.set_cursor(/*pos*/ "hello".len());        t.set_vim_enabled(/*enabled*/ true);        t.input(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE));        t.input(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE));        t.input(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::NONE));        assert_eq!(t.text(), "world");        assert_eq!(t.kill_buffer, "hello ");        let mut t = ta_with("foo bar");        t.set_cursor(t.text().len());        t.set_vim_enabled(/*enabled*/ true);        t.input(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE));        t.input(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE));        t.input(KeyEvent::new(KeyCode::Char('W'), KeyModifiers::NONE));        assert_eq!(t.text(), "foo ");        assert_eq!(t.kill_buffer, "bar");        assert_eq!(t.cursor(), "foo ".len());        assert_eq!(t.vim_mode_label(), Some("Insert"));    }    #[test]    fn vim_delimiter_text_objects_select_innermost_pair_and_aliases() {        let mut t = ta_with("a(b(c)d)e");        t.set_cursor(/*pos*/ "a(b(".len());        t.set_vim_enabled(/*enabled*/ true);        t.input(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE));        t.input(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE));        t.input(KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE));        assert_eq!(t.text(), "a(b()d)e");        assert_eq!(t.kill_buffer, "c");        assert_eq!(t.vim_mode_label(), Some("Insert"));        let mut t = ta_with("a [b] c");        t.set_cursor(/*pos*/ "a [".len());        t.set_vim_enabled(/*enabled*/ true);        t.input(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE));        t.input(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE));        t.input(KeyEvent::new(KeyCode::Char(']'), KeyModifiers::NONE));        assert_eq!(t.text(), "a  c");        assert_eq!(t.kill_buffer, "[b]");    }    #[test]    fn vim_empty_inner_text_objects_are_valid_targets() {        let mut t = ta_with("call()");        t.set_cursor(/*pos*/ "call(".len());        t.set_vim_enabled(/*enabled*/ true);        t.input(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE));        t.input(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE));        t.input(KeyEvent::new(KeyCode::Char('('), KeyModifiers::NONE));        assert_eq!(t.text(), "call()");        assert_eq!(t.kill_buffer, "");        assert_eq!(t.cursor(), "call(".len());        assert_eq!(t.vim_mode_label(), Some("Insert"));        let mut t = ta_with(r#"say "" now"#);        t.set_cursor(/*pos*/ r#"say ""#.len());        t.set_vim_enabled(/*enabled*/ true);        t.input(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE));        t.input(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE));        t.input(KeyEvent::new(KeyCode::Char('"'), KeyModifiers::NONE));        assert_eq!(t.text(), r#"say "" now"#);        assert_eq!(t.kill_buffer, "");        assert_eq!(t.cursor(), r#"say ""#.len());        assert_eq!(t.vim_mode_label(), Some("Insert"));    }    #[test]    fn vim_quote_text_objects_are_line_local_and_handle_escapes() {        let mut t = ta_with(r#"say "a \"b\" c" now"#);        t.set_cursor(/*pos*/ r#"say "a \"#.len());        t.set_vim_enabled(/*enabled*/ true);        t.input(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE));        t.input(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE));        t.input(KeyEvent::new(KeyCode::Char('"'), KeyModifiers::SHIFT));        assert_eq!(t.text(), r#"say "" now"#);        assert_eq!(t.kill_buffer, r#"a \"b\" c"#);        assert_eq!(t.vim_mode_label(), Some("Insert"));        let mut t = ta_with("one \"two\nthree\" four");        t.set_cursor(/*pos*/ "one \"two\n".len());        t.set_vim_enabled(/*enabled*/ true);        t.input(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE));        t.input(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE));        t.input(KeyEvent::new(KeyCode::Char('"'), KeyModifiers::NONE));        assert_eq!(t.text(), "one \"two\nthree\" four");        assert_eq!(t.kill_buffer, "");    }    #[test]    fn vim_text_object_cancellation_and_unsupported_change_motions_do_not_edit() {        let mut t = ta_with("hello world");        t.set_cursor(/*pos*/ 1);        t.set_vim_enabled(/*enabled*/ true);        t.input(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE));        t.input(KeyEvent::new(KeyCode::Char('$'), KeyModifiers::NONE));        assert_eq!(t.text(), "hello world");        assert_eq!(t.kill_buffer, "");        assert_eq!(t.vim_mode_label(), Some("Normal"));        assert!(!t.is_vim_operator_pending());        t.input(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE));        t.input(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE));        assert!(t.is_vim_operator_pending());        t.input(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));        assert_eq!(t.text(), "hello world");        assert_eq!(t.kill_buffer, "");        assert!(!t.is_vim_operator_pending());    }    #[test]    fn vim_operator_invalid_motion_is_consumed() {        let mut t = ta_with("hello");        t.set_cursor(/*pos*/ 0);        t.set_vim_enabled(/*enabled*/ true);        t.input(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE));        assert!(t.is_vim_operator_pending());        t.input(KeyEvent::new(KeyCode::Char('z'), KeyModifiers::NONE));        assert_eq!(t.text(), "hello");        assert_eq!(t.vim_mode_label(), Some("Normal"));        assert_eq!(t.cursor(), 0);        assert!(!t.is_vim_operator_pending());    }    #[test]    fn vim_e_lands_on_word_end_character() {        let mut t = ta_with("abc");        t.set_cursor(/*pos*/ 0);        t.set_vim_enabled(/*enabled*/ true);        t.input(KeyEvent::new(KeyCode::Char('e'), KeyModifiers::NONE));        assert_eq!(t.cursor(), 2);        t.input(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));        assert_eq!(t.text(), "ab");        assert_eq!(t.kill_buffer, "c");    }    #[test]    fn vim_e_advances_from_each_word_end() {        let mut t = ta_with("alpha beta gamma");        t.set_cursor("alph".len()); // codespell:ignore alph        t.set_vim_enabled(/*enabled*/ true);        let mut states = Vec::new();        for _ in 0..3 {            t.input(KeyEvent::new(KeyCode::Char('e'), KeyModifiers::NONE));            states.push(format!("{}\n{}^", t.text(), " ".repeat(t.cursor())));        }        insta::assert_snapshot!("vim_e_advances_from_each_word_end", states.join("\n\n"));    }    #[test]    fn vim_delete_to_word_end_advances_from_existing_word_end() {        let mut t = ta_with("alpha beta gamma");        t.set_cursor("alph".len()); // codespell:ignore alph        t.set_vim_enabled(/*enabled*/ true);        t.input(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE));        t.input(KeyEvent::new(KeyCode::Char('e'), KeyModifiers::NONE));        assert_eq!(t.text(), "alph gamma"); // codespell:ignore alph        assert_eq!(t.kill_buffer, "a beta");    }    #[test]    fn vim_e_from_word_end_can_land_on_trailing_space() {        let mut t = ta_with("alpha   ");        t.set_cursor("alph".len()); // codespell:ignore alph        t.set_vim_enabled(/*enabled*/ true);        t.input(KeyEvent::new(KeyCode::Char('e'), KeyModifiers::NONE));        assert_eq!(t.cursor(), "alpha  ".len());    }    #[test]    fn vim_e_advances_across_atomic_element_word_ends() {        let mut t = TextArea::new();        t.insert_str("alpha ");        t.insert_element("<element>");        t.insert_str(" gamma");        let element_start = t.elements[0].range.start;        t.set_cursor("alph".len()); // codespell:ignore alph        t.set_vim_enabled(/*enabled*/ true);        t.input(KeyEvent::new(KeyCode::Char('e'), KeyModifiers::NONE));        assert_eq!(t.cursor(), element_start);        t.input(KeyEvent::new(KeyCode::Char('e'), KeyModifiers::NONE));        assert_eq!(t.cursor(), "alpha <element> gamm".len());    }    #[test]    fn vim_dollar_lands_on_line_end_character() {        let mut t = ta_with("abc\n123");        t.set_cursor(/*pos*/ 1);        t.set_vim_enabled(/*enabled*/ true);        t.input(KeyEvent::new(KeyCode::Char('$'), KeyModifiers::NONE));        assert_eq!(t.cursor(), 2);        t.input(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));        assert_eq!(t.text(), "ab\n123");        assert_eq!(t.kill_buffer, "c");    }    #[test]    fn vim_linewise_yank_pastes_below_current_line() {        let mut t = ta_with("abc\n123\nxyz");        t.set_cursor(/*pos*/ 1);        t.set_vim_enabled(/*enabled*/ true);        t.input(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));        t.input(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));        t.input(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::NONE));        assert_eq!(t.text(), "abc\nabc\n123\nxyz");        assert_eq!(t.cursor(), "abc\n".len());        assert_eq!(t.kill_buffer, "abc\n");        assert_eq!(t.kill_buffer_kind, KillBufferKind::Linewise);    }    #[test]    fn delete_backward_word_and_kill_line_variants() {        // delete backward word at end removes the whole previous word        let mut t = ta_with("hello   world  ");        t.set_cursor(t.text().len());        t.delete_backward_word();        assert_eq!(t.text(), "hello   ");        assert_eq!(t.cursor(), 8);        // From inside a word, delete from word start to cursor        let mut t = ta_with("foo bar");        t.set_cursor(/*pos*/ 6); // inside "bar" (after 'a')        t.delete_backward_word();        assert_eq!(t.text(), "foo r");        assert_eq!(t.cursor(), 4);        // From end, delete the last word only        let mut t = ta_with("foo bar");        t.set_cursor(t.text().len());        t.delete_backward_word();        assert_eq!(t.text(), "foo ");        assert_eq!(t.cursor(), 4);        // kill_to_end_of_line when not at EOL        let mut t = ta_with("abc\ndef");        t.set_cursor(/*pos*/ 1); // on first line, middle        t.kill_to_end_of_line();        assert_eq!(t.text(), "a\ndef");        assert_eq!(t.cursor(), 1);        // kill_to_end_of_line when at EOL deletes newline        let mut t = ta_with("abc\ndef");        t.set_cursor(/*pos*/ 3); // EOL of first line        t.kill_to_end_of_line();        assert_eq!(t.text(), "abcdef");        assert_eq!(t.cursor(), 3);        // kill_to_beginning_of_line from middle of line        let mut t = ta_with("abc\ndef");        t.set_cursor(/*pos*/ 5); // on second line, after 'e'        t.kill_to_beginning_of_line();        assert_eq!(t.text(), "abc\nef");        // kill_to_beginning_of_line at beginning of non-first line removes the previous newline        let mut t = ta_with("abc\ndef");        t.set_cursor(/*pos*/ 4); // beginning of second line        t.kill_to_beginning_of_line();        assert_eq!(t.text(), "abcdef");        assert_eq!(t.cursor(), 3);    }    #[test]    fn kill_current_line_removes_current_line_linewise() {        let mut t = ta_with("abc\ndef\nghi");        t.set_cursor(/*pos*/ 5);        t.kill_current_line();        assert_eq!(t.text(), "abc\nghi");        assert_eq!(t.cursor(), 4);        assert_eq!(t.kill_buffer, "def\n");        assert_eq!(t.kill_buffer_kind, KillBufferKind::Linewise);    }    #[test]    fn kill_current_line_keeps_previous_newline_for_final_line() {        let mut t = ta_with("abc\ndef");        t.set_cursor(/*pos*/ 5);        t.kill_current_line();        assert_eq!(t.text(), "abc\n");        assert_eq!(t.cursor(), 4);        assert_eq!(t.kill_buffer, "def");        assert_eq!(t.kill_buffer_kind, KillBufferKind::Linewise);    }    #[test]    fn kill_whole_line_keymap_dispatch_uses_linewise_kill() {        let mut t = ta_with("abc\ndef\nghi");        t.set_cursor(/*pos*/ 5);        let mut keymap = RuntimeKeymap::defaults().editor;        keymap.kill_line_start.clear();        keymap.kill_whole_line = vec![key_hint::ctrl(KeyCode::Char('u'))];        t.input_with_keymap(            KeyEvent::new(KeyCode::Char('u'), KeyModifiers::CONTROL),            &keymap,        );        assert_eq!(t.text(), "abc\nghi");        assert_eq!(t.cursor(), 4);        assert_eq!(t.kill_buffer, "def\n");        assert_eq!(t.kill_buffer_kind, KillBufferKind::Linewise);    }    #[test]    fn delete_forward_word_variants() {        let mut t = ta_with("hello   world ");        t.set_cursor(/*pos*/ 0);        t.delete_forward_word();        assert_eq!(t.text(), "   world ");        assert_eq!(t.cursor(), 0);        let mut t = ta_with("hello   world ");        t.set_cursor(/*pos*/ 1);        t.delete_forward_word();        assert_eq!(t.text(), "h   world ");        assert_eq!(t.cursor(), 1);        let mut t = ta_with("hello   world");        t.set_cursor(t.text().len());        t.delete_forward_word();        assert_eq!(t.text(), "hello   world");        assert_eq!(t.cursor(), t.text().len());        let mut t = ta_with("foo   \nbar");        t.set_cursor(/*pos*/ 3);        t.delete_forward_word();        assert_eq!(t.text(), "foo");        assert_eq!(t.cursor(), 3);        let mut t = ta_with("foo\nbar");        t.set_cursor(/*pos*/ 3);        t.delete_forward_word();        assert_eq!(t.text(), "foo");        assert_eq!(t.cursor(), 3);        let mut t = ta_with("hello   world ");        t.set_cursor(t.text().len() + 10);        t.delete_forward_word();        assert_eq!(t.text(), "hello   world ");        assert_eq!(t.cursor(), t.text().len());    }    #[test]    fn delete_forward_word_handles_atomic_elements() {        let mut t = TextArea::new();        t.insert_element("<element>");        t.insert_str(" tail");        t.set_cursor(/*pos*/ 0);        t.delete_forward_word();        assert_eq!(t.text(), " tail");        assert_eq!(t.cursor(), 0);        let mut t = TextArea::new();        t.insert_str("   ");        t.insert_element("<element>");        t.insert_str(" tail");        t.set_cursor(/*pos*/ 0);        t.delete_forward_word();        assert_eq!(t.text(), " tail");        assert_eq!(t.cursor(), 0);        let mut t = TextArea::new();        t.insert_str("prefix ");        t.insert_element("<element>");        t.insert_str(" tail");        // cursor in the middle of the element, delete_forward_word deletes the element        let elem_range = t.elements[0].range.clone();        t.cursor_pos = elem_range.start + (elem_range.len() / 2);        t.delete_forward_word();        assert_eq!(t.text(), "prefix  tail");        assert_eq!(t.cursor(), elem_range.start);    }    #[test]    fn delete_backward_word_respects_word_separators() {        let mut t = ta_with("path/to/file");        t.set_cursor(t.text().len());        t.delete_backward_word();        assert_eq!(t.text(), "path/to/");        assert_eq!(t.cursor(), t.text().len());        t.delete_backward_word();        assert_eq!(t.text(), "path/to");        assert_eq!(t.cursor(), t.text().len());        let mut t = ta_with("foo/ ");        t.set_cursor(t.text().len());        t.delete_backward_word();        assert_eq!(t.text(), "foo");        assert_eq!(t.cursor(), 3);        let mut t = ta_with("foo /");        t.set_cursor(t.text().len());        t.delete_backward_word();        assert_eq!(t.text(), "foo ");        assert_eq!(t.cursor(), 4);    }    #[test]    fn delete_forward_word_respects_word_separators() {        let mut t = ta_with("path/to/file");        t.set_cursor(/*pos*/ 0);        t.delete_forward_word();        assert_eq!(t.text(), "/to/file");        assert_eq!(t.cursor(), 0);        t.delete_forward_word();        assert_eq!(t.text(), "to/file");        assert_eq!(t.cursor(), 0);        let mut t = ta_with("/ foo");        t.set_cursor(/*pos*/ 0);        t.delete_forward_word();        assert_eq!(t.text(), " foo");        assert_eq!(t.cursor(), 0);        let mut t = ta_with(" /foo");        t.set_cursor(/*pos*/ 0);        t.delete_forward_word();        assert_eq!(t.text(), "foo");        assert_eq!(t.cursor(), 0);    }    #[test]    fn yank_restores_last_kill() {        let mut t = ta_with("hello");        t.set_cursor(/*pos*/ 0);        t.kill_to_end_of_line();        assert_eq!(t.text(), "");        assert_eq!(t.cursor(), 0);        t.yank();        assert_eq!(t.text(), "hello");        assert_eq!(t.cursor(), 5);        let mut t = ta_with("hello world");        t.set_cursor(t.text().len());        t.delete_backward_word();        assert_eq!(t.text(), "hello ");        assert_eq!(t.cursor(), 6);        t.yank();        assert_eq!(t.text(), "hello world");        assert_eq!(t.cursor(), 11);        let mut t = ta_with("hello");        t.set_cursor(/*pos*/ 5);        t.kill_to_beginning_of_line();        assert_eq!(t.text(), "");        assert_eq!(t.cursor(), 0);        t.yank();        assert_eq!(t.text(), "hello");        assert_eq!(t.cursor(), 5);    }    #[test]    fn kill_buffer_persists_across_set_text() {        let mut t = ta_with("restore me");        t.set_cursor(/*pos*/ 0);        t.kill_to_end_of_line();        assert!(t.text().is_empty());        t.set_text_clearing_elements("/diff");        t.set_text_clearing_elements("");        t.yank();        assert_eq!(t.text(), "restore me");        assert_eq!(t.cursor(), "restore me".len());    }    #[test]    fn cursor_left_and_right_handle_graphemes() {        let mut t = ta_with("a👍b");        t.set_cursor(t.text().len());        t.move_cursor_left(); // before 'b'        let after_first_left = t.cursor();        t.move_cursor_left(); // before '👍'        let after_second_left = t.cursor();        t.move_cursor_left(); // before 'a'        let after_third_left = t.cursor();        assert!(after_first_left < t.text().len());        assert!(after_second_left < after_first_left);        assert!(after_third_left < after_second_left);        // Move right back to end safely        t.move_cursor_right();        t.move_cursor_right();        t.move_cursor_right();        assert_eq!(t.cursor(), t.text().len());    }    #[test]    fn control_b_and_f_move_cursor() {        let mut t = ta_with("abcd");        t.set_cursor(/*pos*/ 1);        t.input(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::CONTROL));        assert_eq!(t.cursor(), 2);        t.input(KeyEvent::new(KeyCode::Char('b'), KeyModifiers::CONTROL));        assert_eq!(t.cursor(), 1);    }    #[test]    fn control_b_f_fallback_control_chars_move_cursor() {        let mut t = ta_with("abcd");        t.set_cursor(/*pos*/ 2);        // Simulate terminals that send C0 control chars without CONTROL modifier.        // ^B (U+0002) should move left        t.input(KeyEvent::new(KeyCode::Char('\u{0002}'), KeyModifiers::NONE));        assert_eq!(t.cursor(), 1);        // ^F (U+0006) should move right        t.input(KeyEvent::new(KeyCode::Char('\u{0006}'), KeyModifiers::NONE));        assert_eq!(t.cursor(), 2);    }    #[test]    fn c0_line_feed_inserts_newline_through_insert_newline_keymap() {        let mut t = ta_with("ab");        t.set_cursor(/*pos*/ 1);        t.input(KeyEvent::new(KeyCode::Char('\u{000a}'), KeyModifiers::NONE));        assert_eq!(t.text(), "a\nb");        assert_eq!(t.cursor(), 2);    }    #[test]    fn c0_control_chars_respect_unbound_editor_movement() {        let mut t = ta_with("a\nb");        t.set_cursor(/*pos*/ 2);        let mut keymap = RuntimeKeymap::defaults().editor;        keymap.move_up.clear();        t.input_with_keymap(            KeyEvent::new(KeyCode::Char('\u{0010}'), KeyModifiers::NONE),            &keymap,        );        assert_eq!(t.cursor(), 2);    }    #[test]    fn c0_control_chars_respect_remapped_editor_movement() {        let mut t = ta_with("a\nb");        t.set_cursor(/*pos*/ 0);        let mut keymap = RuntimeKeymap::defaults().editor;        keymap.move_up.clear();        keymap.move_down = vec![crate::key_hint::ctrl(KeyCode::Char('p'))];        t.input_with_keymap(            KeyEvent::new(KeyCode::Char('\u{0010}'), KeyModifiers::NONE),            &keymap,        );        assert_eq!(t.cursor(), 2);    }    #[test]    fn delete_backward_word_alt_keys() {        // Test the custom Alt+Ctrl+h binding        let mut t = ta_with("hello world");        t.set_cursor(t.text().len()); // cursor at the end        t.input(KeyEvent::new(            KeyCode::Char('h'),            KeyModifiers::CONTROL | KeyModifiers::ALT,        ));        assert_eq!(t.text(), "hello ");        assert_eq!(t.cursor(), 6);        // Test the standard Alt+Backspace binding        let mut t = ta_with("hello world");        t.set_cursor(t.text().len()); // cursor at the end        t.input(KeyEvent::new(KeyCode::Backspace, KeyModifiers::ALT));        assert_eq!(t.text(), "hello ");        assert_eq!(t.cursor(), 6);    }    #[test]    fn shift_backspace_and_shift_delete_keep_grapheme_delete_behavior() {        let mut t = ta_with("abc");        t.set_cursor(/*pos*/ 2);        t.input(KeyEvent::new(KeyCode::Backspace, KeyModifiers::SHIFT));        assert_eq!(t.text(), "ac");        assert_eq!(t.cursor(), 1);        let mut t = ta_with("abc");        t.set_cursor(/*pos*/ 1);        t.input(KeyEvent::new(KeyCode::Delete, KeyModifiers::SHIFT));        assert_eq!(t.text(), "ac");        assert_eq!(t.cursor(), 1);    }    #[test]    fn control_backspace_variants_delete_backward_word() {        for modifiers in [            KeyModifiers::CONTROL,            KeyModifiers::CONTROL | KeyModifiers::SHIFT,        ] {            let mut t = ta_with("hello world");            t.set_cursor(t.text().len());            t.input(KeyEvent::new(KeyCode::Backspace, modifiers));            assert_eq!(t.text(), "hello ");            assert_eq!(t.cursor(), 6);        }    }    #[test]    fn control_delete_variants_delete_forward_word() {        for modifiers in [            KeyModifiers::CONTROL,            KeyModifiers::CONTROL | KeyModifiers::SHIFT,        ] {            let mut t = ta_with("hello world");            t.set_cursor(/*pos*/ 0);            t.input(KeyEvent::new(KeyCode::Delete, modifiers));            assert_eq!(t.text(), " world");            assert_eq!(t.cursor(), 0);        }    }    #[test]    fn delete_backward_word_handles_narrow_no_break_space() {        let mut t = ta_with("32\u{202F}AM");        t.set_cursor(t.text().len());        t.input(KeyEvent::new(KeyCode::Backspace, KeyModifiers::ALT));        pretty_assertions::assert_eq!(t.text(), "32\u{202F}");        pretty_assertions::assert_eq!(t.cursor(), t.text().len());    }    #[test]    fn delete_forward_word_with_without_alt_modifier() {        let mut t = ta_with("hello world");        t.set_cursor(/*pos*/ 0);        t.input(KeyEvent::new(KeyCode::Delete, KeyModifiers::ALT));        assert_eq!(t.text(), " world");        assert_eq!(t.cursor(), 0);        let mut t = ta_with("hello");        t.set_cursor(/*pos*/ 0);        t.input(KeyEvent::new(KeyCode::Delete, KeyModifiers::NONE));        assert_eq!(t.text(), "ello");        assert_eq!(t.cursor(), 0);    }    #[test]    fn delete_forward_word_alt_d() {        let mut t = ta_with("hello world");        t.set_cursor(/*pos*/ 6);        t.input(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::ALT));        pretty_assertions::assert_eq!(t.text(), "hello ");        pretty_assertions::assert_eq!(t.cursor(), 6);    }    #[test]    fn control_h_backspace() {        // Test Ctrl+H as backspace        let mut t = ta_with("12345");        t.set_cursor(/*pos*/ 3); // cursor after '3'        t.input(KeyEvent::new(KeyCode::Char('h'), KeyModifiers::CONTROL));        assert_eq!(t.text(), "1245");        assert_eq!(t.cursor(), 2);        // Test Ctrl+H at beginning (should be no-op)        t.set_cursor(/*pos*/ 0);        t.input(KeyEvent::new(KeyCode::Char('h'), KeyModifiers::CONTROL));        assert_eq!(t.text(), "1245");        assert_eq!(t.cursor(), 0);        // Test Ctrl+H at end        t.set_cursor(t.text().len());        t.input(KeyEvent::new(KeyCode::Char('h'), KeyModifiers::CONTROL));        assert_eq!(t.text(), "124");        assert_eq!(t.cursor(), 3);    }    #[cfg_attr(not(windows), ignore = "AltGr modifier only applies on Windows")]    #[test]    fn altgr_ctrl_alt_char_inserts_literal() {        let mut t = ta_with("");        t.input(KeyEvent::new(            KeyCode::Char('c'),            KeyModifiers::CONTROL | KeyModifiers::ALT,        ));        assert_eq!(t.text(), "c");        assert_eq!(t.cursor(), 1);    }    #[test]    fn cursor_vertical_movement_across_lines_and_bounds() {        let mut t = ta_with("short\nloooooooooong\nmid");        // Place cursor on second line, column 5        let second_line_start = 6; // after first '\n'        t.set_cursor(second_line_start + 5);        // Move up: target column preserved, clamped by line length        t.move_cursor_up();        assert_eq!(t.cursor(), 5); // first line has len 5        // Move up again goes to start of text        t.move_cursor_up();        assert_eq!(t.cursor(), 0);        // Move down: from start to target col tracked        t.move_cursor_down();        // On first move down, we should land on second line, at col 0 (target col remembered as 0)        let pos_after_down = t.cursor();        assert!(pos_after_down >= second_line_start);        // Move down again to third line; clamp to its length        t.move_cursor_down();        let third_line_start = t.text().find("mid").unwrap();        let third_line_end = third_line_start + 3;        assert!(t.cursor() >= third_line_start && t.cursor() <= third_line_end);        // Moving down at last line jumps to end        t.move_cursor_down();        assert_eq!(t.cursor(), t.text().len());    }    #[test]    fn home_end_and_emacs_style_home_end() {        let mut t = ta_with("one\ntwo\nthree");        // Position at middle of second line        let second_line_start = t.text().find("two").unwrap();        t.set_cursor(second_line_start + 1);        t.move_cursor_to_beginning_of_line(/*move_up_at_bol*/ false);        assert_eq!(t.cursor(), second_line_start);        // Ctrl-A behavior: if at BOL, go to beginning of previous line        t.move_cursor_to_beginning_of_line(/*move_up_at_bol*/ true);        assert_eq!(t.cursor(), 0); // beginning of first line        // Move to EOL of first line        t.move_cursor_to_end_of_line(/*move_down_at_eol*/ false);        assert_eq!(t.cursor(), 3);        // Ctrl-E: if at EOL, go to end of next line        t.move_cursor_to_end_of_line(/*move_down_at_eol*/ true);        // end of second line ("two") is right before its '\n'        let end_second_nl = t.text().find("\nthree").unwrap();        assert_eq!(t.cursor(), end_second_nl);    }    #[test]    fn end_of_line_or_down_at_end_of_text() {        let mut t = ta_with("one\ntwo");        // Place cursor at absolute end of the text        t.set_cursor(t.text().len());        // Should remain at end without panicking        t.move_cursor_to_end_of_line(/*move_down_at_eol*/ true);        assert_eq!(t.cursor(), t.text().len());        // Also verify behavior when at EOL of a non-final line:        let eol_first_line = 3; // index of '\n' in "one\ntwo"        t.set_cursor(eol_first_line);        t.move_cursor_to_end_of_line(/*move_down_at_eol*/ true);        assert_eq!(t.cursor(), t.text().len()); // moves to end of next (last) line    }    #[test]    fn word_navigation_helpers() {        let t = ta_with("  alpha  beta   gamma");        let mut t = t; // make mutable for set_cursor        // Put cursor after "alpha"        let after_alpha = t.text().find("alpha").unwrap() + "alpha".len();        t.set_cursor(after_alpha);        assert_eq!(t.beginning_of_previous_word(), 2); // skip initial spaces        // Put cursor at start of beta        let beta_start = t.text().find("beta").unwrap();        t.set_cursor(beta_start);        assert_eq!(t.end_of_next_word(), beta_start + "beta".len());        // If at end, end_of_next_word returns len        t.set_cursor(t.text().len());        assert_eq!(t.end_of_next_word(), t.text().len());    }    #[test]    fn word_navigation_cjk_each_char_is_boundary() {        let text = "你好世界";        let mut t = ta_with(text);        t.set_cursor(/*pos*/ text.len());        assert_eq!(t.beginning_of_previous_word(), 9);        t.set_cursor(/*pos*/ 9);        assert_eq!(t.beginning_of_previous_word(), 6);        t.set_cursor(/*pos*/ 6);        assert_eq!(t.beginning_of_previous_word(), 3);        t.set_cursor(/*pos*/ 3);        assert_eq!(t.beginning_of_previous_word(), 0);    }    #[test]    fn word_navigation_cjk_forward() {        let text = "你好世界";        let mut t = ta_with(text);        t.set_cursor(/*pos*/ 0);        assert_eq!(t.end_of_next_word(), 3);        t.set_cursor(/*pos*/ 3);        assert_eq!(t.end_of_next_word(), 6);        t.set_cursor(/*pos*/ 6);        assert_eq!(t.end_of_next_word(), 9);        t.set_cursor(/*pos*/ 9);        assert_eq!(t.end_of_next_word(), 12);    }    #[test]    fn word_navigation_mixed_ascii_cjk() {        let text = "hello你好";        let mut t = ta_with(text);        t.set_cursor(/*pos*/ 0);        assert_eq!(t.end_of_next_word(), 5);        t.set_cursor(/*pos*/ 5);        assert_eq!(t.end_of_next_word(), 8);        t.set_cursor(/*pos*/ text.len());        assert_eq!(t.beginning_of_previous_word(), 8);        t.set_cursor(/*pos*/ 8);        assert_eq!(t.beginning_of_previous_word(), 5);        t.set_cursor(/*pos*/ 5);        assert_eq!(t.beginning_of_previous_word(), 0);    }    #[test]    fn word_navigation_preserves_separator_breaks_within_unicode_segments() {        let mut t = ta_with("can't 32.3 foo.bar");        t.set_cursor(/*pos*/ 5);        assert_eq!(t.beginning_of_previous_word(), 4);        t.set_cursor(/*pos*/ 4);        assert_eq!(t.beginning_of_previous_word(), 3);        t.set_cursor(/*pos*/ 10);        assert_eq!(t.beginning_of_previous_word(), 9);        t.set_cursor(/*pos*/ 18);        assert_eq!(t.beginning_of_previous_word(), 15);    }    #[test]    fn wrapping_and_cursor_positions() {        let mut t = ta_with("hello world here");        let area = Rect::new(0, 0, 6, 10); // width 6 -> wraps words        // desired height counts wrapped lines        assert!(t.desired_height(area.width) >= 3);        // Place cursor in "world"        let world_start = t.text().find("world").unwrap();        t.set_cursor(world_start + 3);        let (_x, y) = t.cursor_pos(area).unwrap();        assert_eq!(y, 1); // world should be on second wrapped line        // With state and small height, cursor is mapped onto visible row        let mut state = TextAreaState::default();        let small_area = Rect::new(0, 0, 6, 1);        // First call: cursor not visible -> effective scroll ensures it is        let (_x, y) = t.cursor_pos_with_state(small_area, state).unwrap();        assert_eq!(y, 0);        // Render with state to update actual scroll value        let mut buf = Buffer::empty(small_area);        ratatui::widgets::StatefulWidgetRef::render_ref(&(&t), small_area, &mut buf, &mut state);        // After render, state.scroll should be adjusted so cursor row fits        let effective_lines = t.desired_height(small_area.width);        assert!(state.scroll < effective_lines);    }    #[test]    fn render_highlights_apply_style_without_mutating_text() {        let t = ta_with("hello world");        let area = Rect::new(0, 0, 20, 1);        let mut state = TextAreaState::default();        let mut buf = Buffer::empty(area);        let highlight_style = Style::default().add_modifier(ratatui::style::Modifier::REVERSED);        t.render_ref_styled_with_highlights(            area,            &mut buf,            &mut state,            Style::default(),            &[(6..11, highlight_style)],        );        assert_eq!(t.text(), "hello world");        assert!(            !buf[(0, 0)]                .style()                .add_modifier                .contains(ratatui::style::Modifier::REVERSED)        );        assert!(            buf[(6, 0)]                .style()                .add_modifier                .contains(ratatui::style::Modifier::REVERSED)        );        assert!(            buf[(10, 0)]                .style()                .add_modifier                .contains(ratatui::style::Modifier::REVERSED)        );    }    #[test]    fn cursor_pos_with_state_basic_and_scroll_behaviors() {        // Case 1: No wrapping needed, height fits — scroll ignored, y maps directly.        let mut t = ta_with("hello world");        t.set_cursor(/*pos*/ 3);        let area = Rect::new(2, 5, 20, 3);        // Even if an absurd scroll is provided, when content fits the area the        // effective scroll is 0 and the cursor position matches cursor_pos.        let bad_state = TextAreaState { scroll: 999 };        let (x1, y1) = t.cursor_pos(area).unwrap();        let (x2, y2) = t.cursor_pos_with_state(area, bad_state).unwrap();        assert_eq!((x2, y2), (x1, y1));        // Case 2: Cursor below the current window — y should be clamped to the        // bottom row (area.height - 1) after adjusting effective scroll.        let mut t = ta_with("one two three four five six");        // Force wrapping to many visual lines.        let wrap_width = 4;        let _ = t.desired_height(wrap_width);        // Put cursor somewhere near the end so it's definitely below the first window.        t.set_cursor(t.text().len().saturating_sub(2));        let small_area = Rect::new(0, 0, wrap_width, 2);        let state = TextAreaState { scroll: 0 };        let (_x, y) = t.cursor_pos_with_state(small_area, state).unwrap();        assert_eq!(y, small_area.y + small_area.height - 1);        // Case 3: Cursor above the current window — y should be top row (0)        // when the provided scroll is too large.        let mut t = ta_with("alpha beta gamma delta epsilon zeta");        let wrap_width = 5;        let lines = t.desired_height(wrap_width);        // Place cursor near start so an excessive scroll moves it to top row.        t.set_cursor(/*pos*/ 1);        let area = Rect::new(0, 0, wrap_width, 3);        let state = TextAreaState {            scroll: lines.saturating_mul(2),        };        let (_x, y) = t.cursor_pos_with_state(area, state).unwrap();        assert_eq!(y, area.y);    }    #[test]    fn wrapped_navigation_across_visual_lines() {        let mut t = ta_with("abcdefghij");        // Force wrapping at width 4: lines -> ["abcd", "efgh", "ij"]        let _ = t.desired_height(/*width*/ 4);        // From the very start, moving down should go to the start of the next wrapped line (index 4)        t.set_cursor(/*pos*/ 0);        t.move_cursor_down();        assert_eq!(t.cursor(), 4);        // Cursor at boundary index 4 should be displayed at start of second wrapped line        t.set_cursor(/*pos*/ 4);        let area = Rect::new(0, 0, 4, 10);        let (x, y) = t.cursor_pos(area).unwrap();        assert_eq!((x, y), (0, 1));        // With state and small height, cursor should be visible at row 0, col 0        let small_area = Rect::new(0, 0, 4, 1);        let state = TextAreaState::default();        let (x, y) = t.cursor_pos_with_state(small_area, state).unwrap();        assert_eq!((x, y), (0, 0));        // Place cursor in the middle of the second wrapped line ("efgh"), at 'g'        t.set_cursor(/*pos*/ 6);        // Move up should go to same column on previous wrapped line -> index 2 ('c')        t.move_cursor_up();        assert_eq!(t.cursor(), 2);        // Move down should return to same position on the next wrapped line -> back to index 6 ('g')        t.move_cursor_down();        assert_eq!(t.cursor(), 6);        // Move down again should go to third wrapped line. Target col is 2, but the line has len 2 -> clamp to end        t.move_cursor_down();        assert_eq!(t.cursor(), t.text().len());    }    #[test]    fn cursor_pos_with_state_after_movements() {        let mut t = ta_with("abcdefghij");        // Wrap width 4 -> visual lines: abcd | efgh | ij        let _ = t.desired_height(/*width*/ 4);        let area = Rect::new(0, 0, 4, 2);        let mut state = TextAreaState::default();        let mut buf = Buffer::empty(area);        // Start at beginning        t.set_cursor(/*pos*/ 0);        ratatui::widgets::StatefulWidgetRef::render_ref(&(&t), area, &mut buf, &mut state);        let (x, y) = t.cursor_pos_with_state(area, state).unwrap();        assert_eq!((x, y), (0, 0));        // Move down to second visual line; should be at bottom row (row 1) within 2-line viewport        t.move_cursor_down();        ratatui::widgets::StatefulWidgetRef::render_ref(&(&t), area, &mut buf, &mut state);        let (x, y) = t.cursor_pos_with_state(area, state).unwrap();        assert_eq!((x, y), (0, 1));        // Move down to third visual line; viewport scrolls and keeps cursor on bottom row        t.move_cursor_down();        ratatui::widgets::StatefulWidgetRef::render_ref(&(&t), area, &mut buf, &mut state);        let (x, y) = t.cursor_pos_with_state(area, state).unwrap();        assert_eq!((x, y), (0, 1));        // Move up to second visual line; with current scroll, it appears on top row        t.move_cursor_up();        ratatui::widgets::StatefulWidgetRef::render_ref(&(&t), area, &mut buf, &mut state);        let (x, y) = t.cursor_pos_with_state(area, state).unwrap();        assert_eq!((x, y), (0, 0));        // Column preservation across moves: set to col 2 on first line, move down        t.set_cursor(/*pos*/ 2);        ratatui::widgets::StatefulWidgetRef::render_ref(&(&t), area, &mut buf, &mut state);        let (x0, y0) = t.cursor_pos_with_state(area, state).unwrap();        assert_eq!((x0, y0), (2, 0));        t.move_cursor_down();        ratatui::widgets::StatefulWidgetRef::render_ref(&(&t), area, &mut buf, &mut state);        let (x1, y1) = t.cursor_pos_with_state(area, state).unwrap();        assert_eq!((x1, y1), (2, 1));    }    #[test]    fn wrapped_navigation_with_newlines_and_spaces() {        // Include spaces and an explicit newline to exercise boundaries        let mut t = ta_with("word1  word2\nword3");        // Width 6 will wrap "word1  " and then "word2" before the newline        let _ = t.desired_height(/*width*/ 6);        // Put cursor on the second wrapped line before the newline, at column 1 of "word2"        let start_word2 = t.text().find("word2").unwrap();        t.set_cursor(start_word2 + 1);        // Up should go to first wrapped line, column 1 -> index 1        t.move_cursor_up();        assert_eq!(t.cursor(), 1);        // Down should return to the same visual column on "word2"        t.move_cursor_down();        assert_eq!(t.cursor(), start_word2 + 1);        // Down again should cross the logical newline to the next visual line ("word3"), clamped to its length if needed        t.move_cursor_down();        let start_word3 = t.text().find("word3").unwrap();        assert!(t.cursor() >= start_word3 && t.cursor() <= start_word3 + "word3".len());    }    #[test]    fn wrapped_navigation_with_wide_graphemes() {        // Four thumbs up, each of display width 2, with width 3 to force wrapping inside grapheme boundaries        let mut t = ta_with("👍👍👍👍");        let _ = t.desired_height(/*width*/ 3);        // Put cursor after the second emoji (which should be on first wrapped line)        t.set_cursor("👍👍".len());        // Move down should go to the start of the next wrapped line (same column preserved but clamped)        t.move_cursor_down();        // We expect to land somewhere within the third emoji or at the start of it        let pos_after_down = t.cursor();        assert!(pos_after_down >= "👍👍".len());        // Moving up should take us back to the original position        t.move_cursor_up();        assert_eq!(t.cursor(), "👍👍".len());    }    #[test]    fn fuzz_textarea_randomized() {        // Deterministic seed for reproducibility        // Seed the RNG based on the current day in Pacific Time (PST/PDT). This        // keeps the fuzz test deterministic within a day while still varying        // day-to-day to improve coverage.        let pst_today_seed: u64 = (chrono::Utc::now() - chrono::Duration::hours(8))            .date_naive()            .and_hms_opt(0, 0, 0)            .unwrap()            .and_utc()            .timestamp() as u64;        let mut rng = rand::rngs::StdRng::seed_from_u64(pst_today_seed);        for _case in 0..500 {            let mut ta = TextArea::new();            let mut state = TextAreaState::default();            // Track element payloads we insert. Payloads use characters '[' and ']' which            // are not produced by rand_grapheme(), avoiding accidental collisions.            let mut elem_texts: Vec<String> = Vec::new();            let mut next_elem_id: usize = 0;            // Start with a random base string            let base_len = rng.random_range(0..30);            let mut base = String::new();            for _ in 0..base_len {                base.push_str(&rand_grapheme(&mut rng));            }            ta.set_text_clearing_elements(&base);            // Choose a valid char boundary for initial cursor            let mut boundaries: Vec<usize> = vec![0];            boundaries.extend(ta.text().char_indices().map(|(i, _)| i).skip(1));            boundaries.push(ta.text().len());            let init = boundaries[rng.random_range(0..boundaries.len())];            ta.set_cursor(init);            let mut width: u16 = rng.random_range(1..=12);            let mut height: u16 = rng.random_range(1..=4);            for _step in 0..60 {                // Mostly stable width/height, occasionally change                if rng.random_bool(0.1) {                    width = rng.random_range(1..=12);                }                if rng.random_bool(0.1) {                    height = rng.random_range(1..=4);                }                // Pick an operation                match rng.random_range(0..18) {                    0 => {                        // insert small random string at cursor                        let len = rng.random_range(0..6);                        let mut s = String::new();                        for _ in 0..len {                            s.push_str(&rand_grapheme(&mut rng));                        }                        ta.insert_str(&s);                    }                    1 => {                        // replace_range with small random slice                        let mut b: Vec<usize> = vec![0];                        b.extend(ta.text().char_indices().map(|(i, _)| i).skip(1));                        b.push(ta.text().len());                        let i1 = rng.random_range(0..b.len());                        let i2 = rng.random_range(0..b.len());                        let (start, end) = if b[i1] <= b[i2] {                            (b[i1], b[i2])                        } else {                            (b[i2], b[i1])                        };                        let insert_len = rng.random_range(0..=4);                        let mut s = String::new();                        for _ in 0..insert_len {                            s.push_str(&rand_grapheme(&mut rng));                        }                        let before = ta.text().len();                        // If the chosen range intersects an element, replace_range will expand to                        // element boundaries, so the naive size delta assertion does not hold.                        let intersects_element = elem_texts.iter().any(|payload| {                            if let Some(pstart) = ta.text().find(payload) {                                let pend = pstart + payload.len();                                pstart < end && pend > start                            } else {                                false                            }                        });                        ta.replace_range(start..end, &s);                        if !intersects_element {                            let after = ta.text().len();                            assert_eq!(                                after as isize,                                before as isize + (s.len() as isize) - ((end - start) as isize)                            );                        }                    }                    2 => ta.delete_backward(rng.random_range(0..=3)),                    3 => ta.delete_forward(rng.random_range(0..=3)),                    4 => ta.delete_backward_word(),                    5 => ta.kill_to_beginning_of_line(),                    6 => ta.kill_to_end_of_line(),                    7 => ta.move_cursor_left(),                    8 => ta.move_cursor_right(),                    9 => ta.move_cursor_up(),                    10 => ta.move_cursor_down(),                    11 => ta.move_cursor_to_beginning_of_line(/*move_up_at_bol*/ true),                    12 => ta.move_cursor_to_end_of_line(/*move_down_at_eol*/ true),                    13 => {                        // Insert an element with a unique sentinel payload                        let payload =                            format!("[[EL#{}:{}]]", next_elem_id, rng.random_range(1000..9999));                        next_elem_id += 1;                        ta.insert_element(&payload);                        elem_texts.push(payload);                    }                    14 => {                        // Try inserting inside an existing element (should clamp to boundary)                        if let Some(payload) = elem_texts.choose(&mut rng).cloned()                            && let Some(start) = ta.text().find(&payload)                        {                            let end = start + payload.len();                            if end - start > 2 {                                let pos = rng.random_range(start + 1..end - 1);                                let ins = rand_grapheme(&mut rng);                                ta.insert_str_at(pos, &ins);                            }                        }                    }                    15 => {                        // Replace a range that intersects an element -> whole element should be replaced                        if let Some(payload) = elem_texts.choose(&mut rng).cloned()                            && let Some(start) = ta.text().find(&payload)                        {                            let end = start + payload.len();                            // Create an intersecting range [start-δ, end-δ2)                            let mut s = start.saturating_sub(rng.random_range(0..=2));                            let mut e = (end + rng.random_range(0..=2)).min(ta.text().len());                            // Align to char boundaries to satisfy String::replace_range contract                            let txt = ta.text();                            while s > 0 && !txt.is_char_boundary(s) {                                s -= 1;                            }                            while e < txt.len() && !txt.is_char_boundary(e) {                                e += 1;                            }                            if s < e {                                // Small replacement text                                let mut srep = String::new();                                for _ in 0..rng.random_range(0..=2) {                                    srep.push_str(&rand_grapheme(&mut rng));                                }                                ta.replace_range(s..e, &srep);                            }                        }                    }                    16 => {                        // Try setting the cursor to a position inside an element; it should clamp out                        if let Some(payload) = elem_texts.choose(&mut rng).cloned()                            && let Some(start) = ta.text().find(&payload)                        {                            let end = start + payload.len();                            if end - start > 2 {                                let pos = rng.random_range(start + 1..end - 1);                                ta.set_cursor(pos);                            }                        }                    }                    _ => {                        // Jump to word boundaries                        if rng.random_bool(0.5) {                            let p = ta.beginning_of_previous_word();                            ta.set_cursor(p);                        } else {                            let p = ta.end_of_next_word();                            ta.set_cursor(p);                        }                    }                }                // Sanity invariants                assert!(ta.cursor() <= ta.text().len());                // Element invariants                for payload in &elem_texts {                    if let Some(start) = ta.text().find(payload) {                        let end = start + payload.len();                        // 1) Text inside elements matches the initially set payload                        assert_eq!(&ta.text()[start..end], payload);                        // 2) Cursor is never strictly inside an element                        let c = ta.cursor();                        assert!(                            c <= start || c >= end,                            "cursor inside element: {start}..{end} at {c}"                        );                    }                }                // Render and compute cursor positions; ensure they are in-bounds and do not panic                let area = Rect::new(0, 0, width, height);                // Stateless render into an area tall enough for all wrapped lines                let total_lines = ta.desired_height(width);                let full_area = Rect::new(0, 0, width, total_lines.max(1));                let mut buf = Buffer::empty(full_area);                ratatui::widgets::WidgetRef::render_ref(&(&ta), full_area, &mut buf);                // cursor_pos: x must be within width when present                let _ = ta.cursor_pos(area);                // cursor_pos_with_state: always within viewport rows                let (_x, _y) = ta                    .cursor_pos_with_state(area, state)                    .unwrap_or((area.x, area.y));                // Stateful render should not panic, and updates scroll                let mut sbuf = Buffer::empty(area);                ratatui::widgets::StatefulWidgetRef::render_ref(                    &(&ta),                    area,                    &mut sbuf,                    &mut state,                );                // After wrapping, desired height equals the number of lines we would render without scroll                let total_lines = total_lines as usize;                // state.scroll must not exceed total_lines when content fits within area height                if (height as usize) >= total_lines {                    assert_eq!(state.scroll, 0);                }            }        }    }}