use unicode_segmentation::UnicodeSegmentation;
use rdom_core::{NodeId, NodeType, Position, Selection};
use crate::TuiDom;
use crate::render::inline::{InlineLayout, inline_flow_for_text, inline_flow_layout};
pub(crate) fn expand_to_word(dom: &mut TuiDom) -> bool {
let Some(sel) = dom.selection().copied() else {
return false;
};
let Some(text) = text_of(dom, sel.anchor.node) else {
return false;
};
let (start, end) = word_bounds(&text, sel.anchor.offset);
if start == end {
return false;
}
let next = Selection::new(
Position::new(sel.anchor.node, start),
Position::new(sel.anchor.node, end),
);
if dom.selection() == Some(&next) {
return false;
}
dom.set_selection(Some(next));
true
}
pub(crate) fn expand_to_line(dom: &mut TuiDom) -> bool {
let Some(sel) = dom.selection().copied() else {
return false;
};
let Some(flow) = inline_flow_for_text(dom, sel.anchor.node) else {
return false;
};
let Some((layout, _content)) = inline_flow_layout(dom, flow) else {
return false;
};
let Some(line_idx) = line_containing(layout, sel.anchor.node, sel.anchor.offset) else {
return false;
};
let line = &layout.lines[line_idx];
let Some(first) = line.fragments.first() else {
return false;
};
let Some(last) = line.fragments.last() else {
return false;
};
let next = Selection::new(
Position::new(first.text_node, first.source_byte_offset),
Position::new(last.text_node, last.source_byte_offset + last.text.len()),
);
if dom.selection() == Some(&next) {
return false;
}
dom.set_selection(Some(next));
true
}
fn word_bounds(text: &str, offset: usize) -> (usize, usize) {
let offset = offset.min(text.len());
let words: Vec<(usize, &str)> = text.split_word_bound_indices().collect();
for (start, word) in &words {
let end = start + word.len();
if *start <= offset && offset < end {
return (*start, end);
}
}
if let Some((start, word)) = words.last() {
return (*start, start + word.len());
}
(0, 0)
}
fn line_containing(layout: &InlineLayout, text_node: NodeId, offset: usize) -> Option<usize> {
for (idx, line) in layout.lines.iter().enumerate() {
for frag in &line.fragments {
if frag.text_node != text_node {
continue;
}
let frag_end = frag.source_byte_offset + frag.text.len();
if frag.source_byte_offset <= offset && offset <= frag_end {
return Some(idx);
}
}
}
None
}
fn text_of(dom: &TuiDom, id: NodeId) -> Option<String> {
if dom.node(id).node_type() != NodeType::Text {
return None;
}
dom.node(id).node_value().map(|s| s.to_string())
}
#[cfg(test)]
mod tests;