use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use unicode_segmentation::UnicodeSegmentation;
use rdom_core::{NodeId, Position, Selection};
use crate::TuiDom;
use crate::node::{first_text_descendant, last_text_descendant, text_len};
pub(crate) fn try_handle_key(dom: &mut TuiDom, key: KeyEvent) -> bool {
if let Some(focused) = dom.focused()
&& crate::runtime::selection::user_select::has_none_ancestor(dom, focused)
{
return false;
}
if let Some(sel) = dom.selection()
&& crate::runtime::selection::user_select::ancestor_with(
dom,
sel.focus.node,
crate::layout::UserSelect::All,
)
.is_some()
{
return false;
}
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL)
|| key.modifiers.contains(KeyModifiers::SUPER);
let shift = key.modifiers.contains(KeyModifiers::SHIFT);
match key.code {
KeyCode::Char('a') | KeyCode::Char('A') if ctrl => select_all_under_focus(dom),
KeyCode::Left if shift && ctrl => extend_by_word(dom, Dir::Backward),
KeyCode::Right if shift && ctrl => extend_by_word(dom, Dir::Forward),
KeyCode::Left if shift => extend_by_grapheme(dom, Dir::Backward),
KeyCode::Right if shift => extend_by_grapheme(dom, Dir::Forward),
KeyCode::Up if shift => extend_vertical(dom, -1),
KeyCode::Down if shift => extend_vertical(dom, 1),
KeyCode::Home if shift => extend_to_line_edge(dom, Dir::Backward),
KeyCode::End if shift => extend_to_line_edge(dom, Dir::Forward),
_ => false,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Dir {
Forward,
Backward,
}
fn select_all_under_focus(dom: &mut TuiDom) -> bool {
let root = dom.focused().unwrap_or_else(|| dom.root());
let Some(first) = first_text_descendant(dom, root) else {
return false;
};
let last = last_text_descendant(dom, root).unwrap_or(first);
let end_offset = text_len(dom, last);
let next = Selection::new(Position::new(first, 0), Position::new(last, end_offset));
if dom.selection() == Some(&next) {
return false;
}
dom.set_selection(Some(next));
true
}
fn extend_by_grapheme(dom: &mut TuiDom, dir: Dir) -> bool {
let Some(sel) = dom.selection().copied() else {
return false;
};
let text = match text_of(dom, sel.focus.node) {
Some(t) => t,
None => return false,
};
let offset = sel.focus.offset.min(text.len());
let new_offset = match dir {
Dir::Forward => next_grapheme_byte(&text, offset),
Dir::Backward => prev_grapheme_byte(&text, offset),
};
let Some(new_offset) = new_offset else {
return false;
};
if new_offset == sel.focus.offset {
return false;
}
dom.set_selection(Some(Selection::new(
sel.anchor,
Position::new(sel.focus.node, new_offset),
)));
true
}
fn extend_vertical(dom: &mut TuiDom, delta_y: i32) -> bool {
let Some(sel) = dom.selection().copied() else {
return false;
};
let from = sel.focus;
let new_pos = crate::runtime::editing::movement::vertical_motion(dom, from, delta_y);
let Some(to) = new_pos else {
return false;
};
if to == from {
return false;
}
dom.set_selection(Some(Selection::new(sel.anchor, to)));
true
}
fn extend_to_line_edge(dom: &mut TuiDom, dir: Dir) -> bool {
let Some(sel) = dom.selection().copied() else {
return false;
};
let from = sel.focus;
let to = crate::runtime::editing::movement::line_edge_position(
dom,
from,
matches!(dir, Dir::Forward),
);
let Some(to) = to else {
return false;
};
if to == from {
return false;
}
dom.set_selection(Some(Selection::new(sel.anchor, to)));
true
}
fn extend_by_word(dom: &mut TuiDom, dir: Dir) -> bool {
let Some(sel) = dom.selection().copied() else {
return false;
};
let text = match text_of(dom, sel.focus.node) {
Some(t) => t,
None => return false,
};
let offset = sel.focus.offset.min(text.len());
let new_offset = match dir {
Dir::Forward => next_word_byte(&text, offset),
Dir::Backward => prev_word_byte(&text, offset),
};
let Some(new_offset) = new_offset else {
return false;
};
if new_offset == sel.focus.offset {
return false;
}
dom.set_selection(Some(Selection::new(
sel.anchor,
Position::new(sel.focus.node, new_offset),
)));
true
}
pub(crate) fn next_grapheme_byte(text: &str, offset: usize) -> Option<usize> {
text.grapheme_indices(true)
.map(|(i, _)| i)
.chain(std::iter::once(text.len()))
.find(|&i| i > offset)
}
pub(crate) fn prev_grapheme_byte(text: &str, offset: usize) -> Option<usize> {
text.grapheme_indices(true)
.map(|(i, _)| i)
.rev()
.find(|&i| i < offset)
}
pub(crate) fn next_word_byte(text: &str, offset: usize) -> Option<usize> {
text.split_word_bound_indices()
.map(|(i, _)| i)
.chain(std::iter::once(text.len()))
.find(|&i| i > offset)
}
pub(crate) fn prev_word_byte(text: &str, offset: usize) -> Option<usize> {
text.split_word_bound_indices()
.map(|(i, _)| i)
.rfind(|&i| i < offset)
}
fn text_of(dom: &TuiDom, id: NodeId) -> Option<String> {
dom.node(id).node_value().map(|s| s.to_string())
}
#[cfg(test)]
mod tests;