use super::*;
#[derive(Clone, Copy)]
pub(super) enum BoundaryBias {
Backward,
Forward,
}
#[derive(Clone, Copy)]
pub(super) enum DeleteOperation {
BackwardChar,
ForwardChar,
BackwardWord,
ForwardWord,
}
impl DeleteOperation {
pub(super) fn atomic_direction(self) -> tui_textarea::AtomicDeleteDirection {
match self {
Self::BackwardChar | Self::BackwardWord => {
tui_textarea::AtomicDeleteDirection::Backward
}
Self::ForwardChar | Self::ForwardWord => tui_textarea::AtomicDeleteDirection::Forward,
}
}
pub(super) fn apply(self, textarea: &mut TextArea<'_>) -> bool {
match self {
Self::BackwardChar => textarea.delete_char(),
Self::ForwardChar => textarea.delete_next_char(),
Self::BackwardWord => textarea.delete_word(),
Self::ForwardWord => textarea.delete_next_word(),
}
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub(super) enum WordCharKind {
Space,
Punct,
Other,
}
impl WordCharKind {
pub(super) fn new(ch: char) -> Self {
if ch.is_whitespace() {
Self::Space
} else if ch == '_' {
Self::Other
} else if ch.is_ascii_punctuation() {
Self::Punct
} else {
Self::Other
}
}
}
fn ordinary_deletion_range(
visible: &str,
cursor: usize,
operation: DeleteOperation,
) -> Option<(usize, usize)> {
let cursor = clamp_char_boundary(visible, cursor);
match operation {
DeleteOperation::BackwardChar | DeleteOperation::ForwardChar => {
if matches!(operation, DeleteOperation::BackwardChar) {
visible[..cursor]
.char_indices()
.next_back()
.map(|(start, _)| (start, cursor))
} else {
visible[cursor..]
.chars()
.next()
.map(|ch| (cursor, cursor.saturating_add(ch.len_utf8())))
}
}
DeleteOperation::BackwardWord | DeleteOperation::ForwardWord => {
let line_start = visible[..cursor]
.rfind('\n')
.map_or(0, |newline| newline.saturating_add(1));
let line_end = visible[cursor..]
.find('\n')
.map_or(visible.len(), |newline| cursor.saturating_add(newline));
let line = &visible[line_start..line_end];
let column = visible[line_start..cursor].chars().count();
match operation {
DeleteOperation::BackwardWord => {
if column > 0 {
let start_column = find_word_start_backward(line, column).unwrap_or(0);
Some((
line_start
.saturating_add(byte_offset_for_char_column(line, start_column)),
cursor,
))
} else if line_start > 0 {
Some((line_start - 1, cursor))
} else {
None
}
}
DeleteOperation::ForwardWord => {
let line_char_count = line.chars().count();
if column < line_char_count {
let end_column = find_word_exclusive_end_forward(line, column)
.unwrap_or(line_char_count);
Some((
cursor,
line_start
.saturating_add(byte_offset_for_char_column(line, end_column)),
))
} else if line_end < visible.len() {
Some((cursor, line_end + 1))
} else {
None
}
}
DeleteOperation::BackwardChar | DeleteOperation::ForwardChar => unreachable!(),
}
}
}
}
fn find_word_start_backward(line: &str, start_column: usize) -> Option<usize> {
let byte = byte_offset_for_char_column(line, start_column);
let mut chars = line[..byte].chars().rev().enumerate();
let mut current = WordCharKind::new(chars.next()?.1);
for (index, ch) in chars {
let next = WordCharKind::new(ch);
if current != WordCharKind::Space && next != current {
return Some(start_column - index);
}
current = next;
}
(current != WordCharKind::Space).then_some(0)
}
fn find_word_exclusive_end_forward(line: &str, start_column: usize) -> Option<usize> {
let mut chars = line.chars().enumerate().skip(start_column);
let mut previous = WordCharKind::new(chars.next()?.1);
for (column, ch) in chars {
let current = WordCharKind::new(ch);
if previous != WordCharKind::Space && previous != current {
return Some(column);
}
previous = current;
}
None
}
impl PromptEditor {
pub(super) fn replace_active_selection(
&mut self,
visible_replacement: &str,
expanded_replacement: &str,
atom: Option<PasteAtom>,
) -> PromptEditResult {
let (start, end) = self.selection_visible_bounds().unwrap_or_else(|| {
let cursor = self.visible_cursor();
(cursor, cursor)
});
let selected_len = self.expanded_visible_range(start, end).len();
if visible_replacement.is_empty() && selected_len == 0 {
return PromptEditResult::Unchanged;
}
let Some(current_len) = self.checked_expanded_text_len() else {
return PromptEditResult::RejectedTooLarge;
};
if !replacement_fits(current_len, selected_len, expanded_replacement.len()) {
return PromptEditResult::RejectedTooLarge;
}
let semantic_history_before = (!self.semantic_history_active && atom.is_some())
.then(|| self.semantic_history.clone());
if atom.is_some() {
self.activate_semantic_history();
}
self.prepare_semantic_edit();
let modified = self.textarea.borrow_mut().insert_str(visible_replacement);
if !modified {
if let Some(semantic_history) = semantic_history_before {
self.semantic_history = semantic_history;
self.semantic_history_active = false;
}
return PromptEditResult::Unchanged;
}
let new_end = start.saturating_add(visible_replacement.len());
self.update_atoms_for_visible_replacement(start, end, start, new_end, atom);
self.finish_content_edit();
PromptEditResult::Changed
}
pub(super) fn edit(&mut self, operation: DeleteOperation) -> PromptEditResult {
self.prepare_semantic_edit();
let Some((old_start, old_end)) = self.deletion_range(operation) else {
return PromptEditResult::Unchanged;
};
let modified = operation.apply(&mut self.textarea.borrow_mut());
if modified {
self.update_atoms_for_visible_replacement(
old_start, old_end, old_start, old_start, None,
);
self.finish_content_edit();
PromptEditResult::Changed
} else {
PromptEditResult::Unchanged
}
}
pub(super) fn deletion_range(&self, operation: DeleteOperation) -> Option<(usize, usize)> {
if let Some((start, end)) = self.selection_byte_bounds()
&& start < end
{
return Some(self.expanded_visible_bounds(start, end));
}
let atomic_range = {
let textarea = self.textarea.borrow();
textarea
.atomic_range_at_cursor(operation.atomic_direction())
.map(|range| {
(
byte_offset_for_cursor(textarea.lines(), (range.row, range.start_col)),
byte_offset_for_cursor(textarea.lines(), (range.row, range.end_col)),
)
})
};
if let Some(range) = atomic_range {
return Some(range);
}
let visible = self.visible_text();
let cursor = self.visible_cursor();
let (start, end) = ordinary_deletion_range(&visible, cursor, operation)?;
Some(self.expanded_visible_bounds(start, end))
}
pub(super) fn edit_without_atom_tracking<F>(&mut self, operation: F) -> PromptEditResult
where
F: FnOnce(&mut TextArea<'static>) -> bool,
{
let modified = operation(&mut self.textarea.borrow_mut());
if modified {
self.bump_content_revision();
self.pending_submission = None;
self.cursor_visible = true;
PromptEditResult::Changed
} else {
PromptEditResult::Unchanged
}
}
}