use super::*;
#[derive(Clone, PartialEq, Eq)]
pub(super) struct PasteAtom {
id: u64,
pub(super) start: usize,
pub(super) end: usize,
line_count: usize,
pub(super) payload: Arc<str>,
}
impl PasteAtom {
pub(super) fn new(id: u64, payload: String) -> Self {
let line_count = logical_line_count(&payload);
Self {
id,
start: 0,
end: 0,
line_count,
payload: Arc::from(payload),
}
}
pub(super) fn label(&self) -> String {
format!("[Pasted #{} ยท {} lines]", self.id, self.line_count)
}
}
pub(super) fn expand_visible_range_with_atoms(
visible: &str,
atoms: &[PasteAtom],
start: usize,
end: usize,
) -> String {
let start = clamp_char_boundary(visible, start);
let end = clamp_char_boundary(visible, end)
.max(start)
.min(visible.len());
if start == end {
return String::new();
}
let mut expanded = String::new();
let mut cursor = start;
for atom in atoms {
if atom.end <= start {
continue;
}
if atom.start >= end {
break;
}
let ordinary_end = atom.start.min(end);
if cursor < ordinary_end {
expanded.push_str(&visible[cursor..ordinary_end]);
}
if atom.start < end && atom.end > start {
expanded.push_str(&atom.payload);
}
cursor = cursor.max(atom.end.min(end));
if cursor >= end {
break;
}
}
if cursor < end {
expanded.push_str(&visible[cursor..end]);
}
expanded
}
impl PromptEditor {
pub(crate) fn insert_paste(
&mut self,
text: &str,
_visible_rows: u16,
_wrap_width: u16,
) -> PromptEditResult {
let replacement = match normalize_prompt_text_bounded(text) {
Ok(replacement) => replacement,
Err(result) => return result,
};
if replacement.is_empty() {
return PromptEditResult::Unchanged;
}
let (start, end) = self.selection_visible_bounds().unwrap_or_else(|| {
let cursor = self.visible_cursor();
(cursor, cursor)
});
let removed_len = self.expanded_visible_range(start, end).len();
let Some(current_len) = self.checked_expanded_text_len() else {
return PromptEditResult::RejectedTooLarge;
};
if !replacement_fits(current_len, removed_len, replacement.len()) {
return PromptEditResult::RejectedTooLarge;
}
let atom = self
.should_collapse_paste(start, end, &replacement)
.then(|| PasteAtom::new(self.next_paste_id, replacement.clone()));
let visible_replacement = atom
.as_ref()
.map(PasteAtom::label)
.unwrap_or_else(|| replacement.clone());
self.replace_active_selection(&visible_replacement, &replacement, atom)
}
pub(super) fn checked_expanded_text_len(&self) -> Option<usize> {
let visible = self.visible_text();
let mut expanded_len: usize = 0;
let mut visible_cursor: usize = 0;
for atom in &self.paste_atoms {
if atom.start < visible_cursor || atom.start >= atom.end || atom.end > visible.len() {
return None;
}
expanded_len = expanded_len.checked_add(atom.start.checked_sub(visible_cursor)?)?;
expanded_len = expanded_len.checked_add(atom.payload.len())?;
visible_cursor = atom.end;
}
expanded_len.checked_add(visible.len().checked_sub(visible_cursor)?)
}
pub(super) fn expanded_visible_range(&self, start: usize, end: usize) -> String {
let visible = self.visible_text();
expand_visible_range_with_atoms(&visible, &self.paste_atoms, start, end)
}
pub(super) fn expanded_visible_bounds(&self, start: usize, end: usize) -> (usize, usize) {
let visible = self.visible_text();
let mut start = clamp_char_boundary(&visible, start);
let mut end = clamp_char_boundary(&visible, end);
for atom in &self.paste_atoms {
if atom.end <= start || atom.start >= end {
continue;
}
start = start.min(atom.start);
end = end.max(atom.end);
}
(start, end)
}
pub(super) fn expanded_offset_for_visible_offset(
&self,
offset: usize,
bias: BoundaryBias,
) -> usize {
let visible = self.visible_text();
let offset = clamp_char_boundary(&visible, offset);
let mut visible_cursor = 0;
let mut expanded_cursor = 0;
for atom in &self.paste_atoms {
if offset <= atom.start {
return expanded_cursor + (offset - visible_cursor);
}
expanded_cursor += atom.start - visible_cursor;
if offset < atom.end {
return match bias {
BoundaryBias::Backward => expanded_cursor,
BoundaryBias::Forward => expanded_cursor + atom.payload.len(),
};
}
expanded_cursor += atom.payload.len();
visible_cursor = atom.end;
}
expanded_cursor + (offset - visible_cursor)
}
pub(super) fn visible_offset_for_expanded_offset(
&self,
offset: usize,
bias: BoundaryBias,
) -> usize {
let visible = self.visible_text();
let Some(expanded_len) = self.checked_expanded_text_len() else {
return visible.len();
};
let offset = offset.min(expanded_len);
let mut visible_cursor = 0;
let mut expanded_cursor = 0;
for atom in &self.paste_atoms {
let ordinary_len = atom.start - visible_cursor;
let ordinary_end = expanded_cursor + ordinary_len;
if offset <= ordinary_end {
return clamp_char_boundary(&visible, visible_cursor + (offset - expanded_cursor));
}
expanded_cursor = ordinary_end;
let payload_end = expanded_cursor + atom.payload.len();
if offset < payload_end {
return match bias {
BoundaryBias::Backward => atom.start,
BoundaryBias::Forward => atom.end,
};
}
if offset == payload_end {
return atom.end;
}
expanded_cursor = payload_end;
visible_cursor = atom.end;
}
clamp_char_boundary(&visible, visible_cursor + (offset - expanded_cursor))
}
pub(super) fn update_atoms_for_visible_replacement(
&mut self,
old_start: usize,
old_end: usize,
new_start: usize,
new_end: usize,
inserted: Option<PasteAtom>,
) {
let mut atoms = self
.paste_atoms
.drain(..)
.filter_map(|mut atom| {
if atom.end <= old_start {
Some(atom)
} else if atom.start >= old_end {
let shift = new_end
.saturating_sub(new_start)
.saturating_sub(old_end.saturating_sub(old_start));
if new_end.saturating_sub(new_start) >= old_end.saturating_sub(old_start) {
atom.start = atom.start.saturating_add(shift);
atom.end = atom.end.saturating_add(shift);
} else {
let shrink = old_end
.saturating_sub(old_start)
.saturating_sub(new_end.saturating_sub(new_start));
atom.start = atom.start.saturating_sub(shrink);
atom.end = atom.end.saturating_sub(shrink);
}
Some(atom)
} else {
None
}
})
.collect::<Vec<_>>();
if let Some(mut atom) = inserted {
atom.start = new_start;
atom.end = new_end;
atoms.push(atom);
self.next_paste_id = self.next_paste_id.saturating_add(1);
}
atoms.sort_by_key(|atom| atom.start);
self.paste_atoms = atoms;
self.refresh_atomic_ranges();
}
pub(super) fn refresh_atomic_ranges(&mut self) {
let visible = self.visible_text();
let ranges = self
.paste_atoms
.iter()
.filter_map(|atom| {
let start = cursor_for_byte_offset(&visible, atom.start);
let end = cursor_for_byte_offset(&visible, atom.end);
(start.0 == end.0 && start.1 < end.1).then_some(tui_textarea::AtomicRange {
row: start.0,
start_col: start.1,
end_col: end.1,
})
})
.collect::<Vec<_>>();
self.textarea.borrow_mut().set_atomic_ranges(ranges);
}
pub(super) fn should_collapse_paste(&self, start: usize, end: usize, text: &str) -> bool {
let (start, end) = self.expanded_visible_bounds(start, end);
let current = self.text();
let expanded_start = self.expanded_offset_for_visible_offset(start, BoundaryBias::Backward);
let expanded_end = self.expanded_offset_for_visible_offset(end, BoundaryBias::Forward);
if expanded_start > expanded_end {
return false;
}
let (Some(prefix), Some(suffix)) =
(current.get(..expanded_start), current.get(expanded_end..))
else {
return false;
};
let mut prospective = String::with_capacity(
prefix
.len()
.checked_add(text.len())
.and_then(|length| suffix.len().checked_add(length))
.unwrap_or(0),
);
prospective.push_str(prefix);
prospective.push_str(text);
prospective.push_str(suffix);
if prospective.trim_start().starts_with('/') || prospective.starts_with('!') {
return false;
}
logical_line_count(text) >= COLLAPSED_PASTE_MIN_LINES
|| text.len() > COLLAPSED_PASTE_BYTE_THRESHOLD
}
}