use super::rope_buffer::{CursorMove, RopeBuffer};
use ratatui::layout::Rect;
use unicode_segmentation::UnicodeSegmentation;
use crate::components::autocomplete::AcceptAction;
pub fn apply_accept_to_textarea(ta: &mut RopeBuffer, action: &AcceptAction) {
let before = ta.text().clone();
let (Some(start), Some(end)) = (
before.position_at_byte(action.range.start),
before.position_at_byte(action.range.end),
) else {
return;
};
ta.cancel_selection();
ta.move_cursor(CursorMove::Jump(start.row(), start.column().get()));
if action.range.end > action.range.start {
let preserved_yank = ta.yank_text();
let Some(clusters) = before
.span(start, end)
.and_then(|span| before.slice(span))
.map(|removed| removed.graphemes(true).count())
else {
return;
};
ta.delete_str(clusters);
ta.set_yank_text(preserved_yank);
}
ta.insert_str(&action.new_text);
let after = ta.text().clone();
if let Some(position) = after.position_at_byte(action.new_cursor_byte) {
ta.move_cursor(CursorMove::Jump(position.row(), position.column().get()));
}
}
pub fn cursor_screen_pos(
rendered_col: usize,
cursor_vrow: usize,
visual_scroll_offset: usize,
rect: Rect,
) -> Option<(u16, u16)> {
if cursor_vrow < visual_scroll_offset {
return None;
}
let vrow_in_view = cursor_vrow - visual_scroll_offset;
if vrow_in_view as u16 >= rect.height {
return None;
}
Some((rect.x + rendered_col as u16, rect.y + vrow_in_view as u16))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cursor_screen_pos_scrolled_off_top() {
let rect = Rect::new(2, 5, 80, 24);
assert!(cursor_screen_pos(0, 0, 5, rect).is_none());
}
#[test]
fn cursor_screen_pos_scrolled_off_bottom() {
let rect = Rect::new(0, 0, 80, 10);
assert!(cursor_screen_pos(0, 100, 0, rect).is_none());
}
#[test]
fn cursor_screen_pos_in_view() {
let rect = Rect::new(2, 5, 80, 24);
assert_eq!(cursor_screen_pos(7, 12, 10, rect), Some((9, 7)));
}
#[test]
fn apply_accept_replaces_and_positions_cursor() {
let mut ta = RopeBuffer::new(crate::ropetext::Text::from("see [[me"));
ta.move_cursor(CursorMove::End);
let action = AcceptAction {
range: 6..8,
new_text: "meeting]]".to_string(),
new_cursor_byte: 15,
saved_search_name: None,
};
apply_accept_to_textarea(&mut ta, &action);
let result: String = ta.text().to_string();
assert_eq!(result, "see [[meeting]]");
let (row, col) = ta.cursor();
assert_eq!((row, col), (0, 15));
}
#[test]
fn apply_accept_over_a_multi_scalar_cluster_stops_at_the_range() {
let mut ta = RopeBuffer::new(crate::ropetext::Text::from("see [[\u{1F1EA}\u{1F1F8} rest"));
ta.move_cursor(CursorMove::End);
let action = AcceptAction {
range: 4..14,
new_text: "[[Spain]]".to_string(),
new_cursor_byte: 13,
saved_search_name: None,
};
apply_accept_to_textarea(&mut ta, &action);
assert_eq!(
ta.text().to_string(),
"see [[Spain]] rest",
"the space after the flag is outside the range and must survive"
);
}
#[test]
fn apply_accept_preserves_textarea_yank_buffer() {
let mut ta = RopeBuffer::new(crate::ropetext::Text::from("see [[me"));
ta.set_yank_text("previously yanked text");
ta.move_cursor(CursorMove::End);
let action = AcceptAction {
range: 6..8,
new_text: "meeting]]".to_string(),
new_cursor_byte: 15,
saved_search_name: None,
};
apply_accept_to_textarea(&mut ta, &action);
assert_eq!(ta.yank_text(), "previously yanked text");
}
#[test]
fn apply_accept_replaces_across_multiple_lines_unaffected() {
let mut ta = RopeBuffer::new(crate::ropetext::Text::from("alpha\nsee [[me"));
let action = AcceptAction {
range: 12..14, new_text: "meeting]]".to_string(),
new_cursor_byte: 21,
saved_search_name: None,
};
apply_accept_to_textarea(&mut ta, &action);
let result: String = ta.text().to_string();
assert_eq!(result, "alpha\nsee [[meeting]]");
}
}