use super::*;
pub(super) fn byte_offset_for_char_column(line: &str, column: usize) -> usize {
line.char_indices()
.nth(column)
.map_or(line.len(), |(offset, _)| offset)
}
pub(super) fn move_textarea_cursor_to(
textarea: &mut TextArea<'_>,
target: (usize, usize),
) -> Result<usize, ()> {
let atomic_ranges = textarea.atomic_ranges().to_vec();
let result = move_textarea_cursor_to_inner(textarea, target);
if atomic_ranges.is_empty() {
textarea.clear_atomic_ranges();
} else {
textarea.set_atomic_ranges(atomic_ranges);
}
result
}
fn move_textarea_cursor_to_inner(
textarea: &mut TextArea<'_>,
target: (usize, usize),
) -> Result<usize, ()> {
if textarea.cursor() == target {
return Ok(0);
}
let max = usize::from(u16::MAX);
if target.0 <= max
&& target.1 <= max
&& textarea.atomic_ranges().is_empty()
&& textarea.search_pattern().is_none()
{
textarea.move_cursor(CursorMove::Jump(target.0 as u16, target.1 as u16));
if textarea.cursor() == target {
return Ok(1);
}
}
let snapshot = textarea.clone();
let end = {
let lines = textarea.lines();
(
lines.len().saturating_sub(1),
lines.last().map_or(0, |line| line.chars().count()),
)
};
if target.0 <= max && target.1 > max && textarea.cursor().0 != target.0 {
textarea.move_cursor(CursorMove::Jump(target.0 as u16, u16::MAX));
if textarea.cursor().0 != target.0 {
*textarea = snapshot.clone();
}
}
if target > (max, max) {
let distance_from_end = {
let lines = textarea.lines();
logical_cursor_offset(lines, end).saturating_sub(logical_cursor_offset(lines, target))
};
if distance_from_end <= 16 {
textarea.move_cursor(CursorMove::Bottom);
textarea.move_cursor(CursorMove::End);
let mut remaining = distance_from_end;
while textarea.cursor() != target && remaining > 0 {
let before = textarea.cursor();
textarea.move_cursor(CursorMove::Back);
if textarea.cursor() >= before {
break;
}
remaining -= 1;
}
if textarea.cursor() == target {
return Ok(distance_from_end.saturating_add(2));
}
*textarea = snapshot.clone();
}
}
let current = textarea.cursor();
let forward = current < target;
if textarea.atomic_ranges().is_empty()
&& current.0 == target.0
&& current.1.abs_diff(target.1) > 1
{
let selection = textarea.selection_range();
let was_selecting = textarea.is_selecting();
let selection_collapsed = selection == Some((current, current));
let anchor = match selection {
Some((start, end)) if start != end => {
if current == start {
end
} else if current == end {
start
} else {
return move_textarea_cursor_with_snapshot(textarea, target, snapshot);
}
}
_ => current,
};
let range_start = current.1.min(target.1);
let range_end = current.1.max(target.1);
let anchor_inside = anchor.0 == current.0 && range_start < anchor.1 && anchor.1 < range_end;
let restore_collapsed = was_selecting && !selection_collapsed && target == anchor;
if anchor_inside {
let split_result: Result<usize, ()> = (|| {
let first = move_textarea_cursor_to(textarea, anchor)?;
let second = move_textarea_cursor_to(textarea, target)?;
Ok(first.saturating_add(second))
})();
textarea.clear_atomic_ranges();
if let Ok(calls) = split_result {
return Ok(calls);
}
*textarea = snapshot;
return Err(());
}
if !anchor_inside {
let distance = current.1.abs_diff(target.1);
let atomic_result = (|| {
if forward {
search_one_boundary(textarea, true)?;
textarea
.try_set_atomic_ranges([tui_textarea::AtomicRange {
row: current.0,
start_col: range_start,
end_col: range_end,
}])
.map_err(|_| ())?;
textarea.normalize_cursor_around_atomic_ranges(
tui_textarea::AtomicCursorBias::Forward,
);
} else if was_selecting && selection_collapsed {
search_one_boundary(textarea, false)?;
if distance == 2 {
search_one_boundary(textarea, false)?;
} else {
textarea
.try_set_atomic_ranges([tui_textarea::AtomicRange {
row: current.0,
start_col: target.1,
end_col: current.1 - 1,
}])
.map_err(|_| ())?;
search_one_boundary(textarea, false)?;
textarea.normalize_cursor_around_atomic_ranges(
tui_textarea::AtomicCursorBias::Backward,
);
}
} else {
textarea
.try_set_atomic_ranges([tui_textarea::AtomicRange {
row: current.0,
start_col: range_start,
end_col: range_end,
}])
.map_err(|_| ())?;
search_one_boundary(textarea, false)?;
textarea.normalize_cursor_around_atomic_ranges(
tui_textarea::AtomicCursorBias::Backward,
);
}
(textarea.cursor() == target).then_some(1).ok_or(())
})();
textarea.clear_atomic_ranges();
if atomic_result.is_ok() {
if restore_collapsed {
textarea.start_selection();
}
return atomic_result;
}
*textarea = snapshot.clone();
}
}
move_textarea_cursor_with_snapshot(textarea, target, snapshot)
}
fn move_textarea_cursor_with_snapshot<'a>(
textarea: &mut TextArea<'a>,
target: (usize, usize),
snapshot: TextArea<'a>,
) -> Result<usize, ()> {
let result = move_textarea_cursor_with_retry(textarea, target);
if result.is_err() {
*textarea = snapshot;
}
result
}
fn search_one_boundary(textarea: &mut TextArea<'_>, forward: bool) -> Result<(), ()> {
let before = logical_cursor_offset(textarea.lines(), textarea.cursor());
textarea.set_search_pattern("(?:)").map_err(|_| ())?;
let moved = if forward {
textarea.search_forward(false)
} else {
textarea.search_back(false)
};
let after = logical_cursor_offset(textarea.lines(), textarea.cursor());
let _ = textarea.set_search_pattern("");
if moved && after == before.saturating_add_signed(if forward { 1 } else { -1 }) {
Ok(())
} else {
Err(())
}
}
fn move_textarea_cursor_with_retry(
textarea: &mut TextArea<'_>,
target: (usize, usize),
) -> Result<usize, ()> {
let mut calls = 0;
if move_textarea_cursor_until(textarea, target, &mut calls) {
return Ok(calls);
}
if textarea.set_search_pattern("(?:)").is_err() {
return Err(());
}
let result = (|| {
let target_offset = logical_cursor_offset(textarea.lines(), target);
let mut remaining = target_offset
.abs_diff(logical_cursor_offset(textarea.lines(), textarea.cursor()))
.saturating_add(1);
while textarea.cursor() != target && remaining > 0 {
let before = textarea.cursor();
let forward = before < target;
let moved = if forward {
textarea.search_forward(false)
} else {
textarea.search_back(false)
};
calls += 1;
let after = textarea.cursor();
if !moved
|| after == before
|| (forward && (after <= before || after > target))
|| (!forward && (after >= before || after < target))
{
return Err(());
}
remaining -= 1;
}
(textarea.cursor() == target).then_some(calls).ok_or(())
})();
let _ = textarea.set_search_pattern("");
result
}
fn move_textarea_cursor_until(
textarea: &mut TextArea<'_>,
target: (usize, usize),
calls: &mut usize,
) -> bool {
let current = textarea.cursor();
let max = usize::from(u16::MAX);
if current.0 > max || current.1 > max || target.0 > max || target.1 > max {
return false;
}
let current_offset = logical_cursor_offset(textarea.lines(), current);
let target_offset = logical_cursor_offset(textarea.lines(), target);
let mut remaining = target_offset.abs_diff(current_offset).saturating_add(1);
loop {
let before = textarea.cursor();
if before == target {
return true;
}
if remaining == 0 {
return false;
}
textarea.move_cursor(if before < target {
CursorMove::Forward
} else {
CursorMove::Back
});
*calls += 1;
let after = textarea.cursor();
if after == target {
return true;
}
if after == before
|| (before < target && (after <= before || after > target))
|| (before > target && (after >= before || after < target))
{
return false;
}
remaining -= 1;
}
}
pub(super) fn byte_offset_for_cursor(lines: &[String], (row, column): (usize, usize)) -> usize {
let row = row.min(lines.len().saturating_sub(1));
let mut offset = lines[..row].iter().map(|line| line.len() + 1).sum();
let line = &lines[row];
offset += line
.char_indices()
.nth(column)
.map(|(index, _)| index)
.unwrap_or(line.len());
offset
}
fn logical_cursor_offset(lines: &[String], (row, column): (usize, usize)) -> usize {
let row = row.min(lines.len().saturating_sub(1));
let preceding = lines[..row]
.iter()
.map(|line| line.chars().count().saturating_add(1))
.sum::<usize>();
preceding.saturating_add(column.min(lines[row].chars().count()))
}
fn cursor_for_byte_offset_lines(lines: &[String], byte: usize) -> (usize, usize) {
let mut line_start: usize = 0;
for (row, line) in lines.iter().enumerate() {
let line_end = line_start.saturating_add(line.len());
if byte <= line_end {
let local = clamp_line_char_boundary(line, byte.saturating_sub(line_start));
return (row, line[..local].chars().count());
}
line_start = line_end.saturating_add(1);
}
let last_row = lines.len().saturating_sub(1);
(last_row, lines[last_row].chars().count())
}
fn clamp_line_char_boundary(line: &str, byte: usize) -> usize {
let mut byte = byte.min(line.len());
while byte > 0 && !line.is_char_boundary(byte) {
byte -= 1;
}
byte
}
pub(super) fn cursor_for_byte_offset(text: &str, byte: usize) -> (usize, usize) {
let byte = clamp_char_boundary(text, byte);
let mut row = 0;
let mut line_start = 0;
for segment in text.split_inclusive('\n') {
let line_end = line_start + segment.trim_end_matches('\n').len();
if byte <= line_end {
return (row, text[line_start..byte].chars().count());
}
line_start += segment.len();
row += 1;
}
(row, text[line_start..byte].chars().count())
}
pub(crate) fn clamp_char_boundary(text: &str, byte: usize) -> usize {
let mut byte = byte.min(text.len());
while byte > 0 && !text.is_char_boundary(byte) {
byte -= 1;
}
byte
}
impl PromptEditor {
pub(super) fn move_character_left(&mut self) -> PromptMovementOutcome {
let collapse_to = self.selection_byte_bounds().map(|(start, _)| start);
let result = if let Some(byte) = collapse_to {
self.move_to_byte_inner(byte, false)
} else {
self.move_one_character(false, false)
};
self.cursor_visible = true;
movement_outcome(result)
}
pub(super) fn extend_character_left(&mut self) -> PromptMovementOutcome {
let mut textarea = self.textarea.borrow_mut();
let snapshot = textarea.clone();
if !textarea.is_selecting() {
textarea.start_selection();
}
drop(textarea);
let result = self.move_one_character(false, true);
if result.is_err() {
*self.textarea.borrow_mut() = snapshot;
}
self.cursor_visible = true;
movement_outcome(result)
}
pub(super) fn extend_cursor(&mut self, movement: CursorMove) -> PromptMovementOutcome {
let before = self.visible_cursor();
let mut textarea = self.textarea.borrow_mut();
let snapshot = textarea.clone();
if !textarea.is_selecting() {
textarea.start_selection();
}
drop(textarea);
let result = if matches!(movement, CursorMove::Forward | CursorMove::Back) {
self.move_one_character(matches!(movement, CursorMove::Forward), true)
} else {
let mut textarea = self.textarea.borrow_mut();
textarea.move_cursor(movement);
Ok(1)
};
if result.is_err() {
*self.textarea.borrow_mut() = snapshot;
}
self.cursor_visible = true;
if result.is_err() {
PromptMovementOutcome::Failed
} else if self.visible_cursor() == before {
PromptMovementOutcome::Unchanged
} else {
PromptMovementOutcome::Moved
}
}
pub(super) fn move_cursor(&mut self, movement: CursorMove) -> PromptMovementOutcome {
let collapse_to = match movement {
CursorMove::Back | CursorMove::WordBack => {
self.selection_byte_bounds().map(|(start, _)| start)
}
CursorMove::Forward | CursorMove::WordForward => {
self.selection_byte_bounds().map(|(_, end)| end)
}
_ => None,
};
if let Some(byte) = collapse_to {
return movement_outcome(self.move_to_byte_inner(byte, false));
}
let before = self.visible_cursor();
let result = if matches!(movement, CursorMove::Forward | CursorMove::Back) {
self.move_one_character(matches!(movement, CursorMove::Forward), false)
} else {
let mut textarea = self.textarea.borrow_mut();
textarea.cancel_selection();
textarea.move_cursor(movement);
Ok(1)
};
self.cursor_visible = true;
if result.is_err() {
PromptMovementOutcome::Failed
} else if self.visible_cursor() == before {
PromptMovementOutcome::Unchanged
} else {
PromptMovementOutcome::Moved
}
}
pub(super) fn move_to_byte_inner(
&mut self,
byte: usize,
extend_selection: bool,
) -> Result<usize, ()> {
let target = {
let textarea = self.textarea.borrow();
cursor_for_byte_offset_lines(textarea.lines(), byte)
};
let mut textarea = self.textarea.borrow_mut();
let snapshot = (!extend_selection && textarea.is_selecting()).then(|| textarea.clone());
if !extend_selection {
textarea.cancel_selection();
}
let result = move_textarea_cursor_to(&mut textarea, target);
if result.is_err()
&& let Some(snapshot) = snapshot
{
*textarea = snapshot;
}
result
}
pub(super) fn move_one_character(
&mut self,
forward: bool,
extend_selection: bool,
) -> Result<usize, ()> {
let visible = self.visible_text();
let current = self.visible_cursor().min(visible.len());
let target = if forward {
let next = visible[current..]
.chars()
.next()
.map_or(current, |ch| current.saturating_add(ch.len_utf8()));
self.paste_atoms
.iter()
.find(|atom| atom.start <= current && current < atom.end)
.map_or_else(
|| {
self.paste_atoms
.iter()
.find(|atom| atom.start < next && next < atom.end)
.map_or(next, |atom| atom.end)
},
|atom| atom.end,
)
} else {
self.paste_atoms
.iter()
.find(|atom| atom.start < current && current <= atom.end)
.map_or_else(
|| {
visible[..current]
.chars()
.next_back()
.map_or(current, |ch| current.saturating_sub(ch.len_utf8()))
},
|atom| atom.start,
)
};
self.move_to_byte_inner(target, extend_selection)
}
}