use crate::ui::{Key, Mods};
use crate::widgets::input::{word_step, ClusterMap};
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub enum SubmitPolicy {
#[default]
EnterSubmits,
EnterInserts,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub(crate) struct Caret {
pub byte: usize,
pub anchor: Option<usize>,
pub goal: Option<i32>,
pub sticky: bool,
pub top: i32,
pub detached: bool,
}
impl Caret {
pub fn origin() -> Caret {
Caret {
byte: 0,
anchor: None,
goal: None,
sticky: false,
top: 0,
detached: false,
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub(crate) struct Row {
pub start: usize,
pub text_end: usize,
pub end: usize,
}
pub(crate) struct RowMap {
pub rows: Vec<Row>,
}
impl RowMap {
pub fn build(text: &str, width: i32) -> RowMap {
let width = width.max(1);
let mut rows: Vec<Row> = Vec::new();
let mut start = 0usize; let mut col = 0i32; let mut break_at: Option<(usize, i32)> = None;
for seg in crate::text::segments(text) {
let seg_end = seg.offset + seg.cluster.len();
let first = seg.cluster.chars().next();
let is_ws = first.is_some_and(char::is_whitespace);
if crate::text::is_control_cluster(seg.cluster) {
if seg.cluster.contains('\n') {
rows.push(Row {
start,
text_end: seg.offset,
end: seg_end,
});
start = seg_end;
col = 0;
break_at = None;
} else if is_ws {
break_at = Some((seg_end, col));
}
continue;
}
if !is_ws && col + seg.width > width && col > 0 {
match break_at {
Some((b, bcol)) if b > start && b <= seg.offset => {
rows.push(Row {
start,
text_end: b,
end: b,
});
start = b;
col -= bcol;
}
_ => {
rows.push(Row {
start,
text_end: seg.offset,
end: seg.offset,
});
start = seg.offset;
col = 0;
}
}
break_at = None;
}
col += seg.width;
if is_ws {
break_at = Some((seg_end, col));
}
}
rows.push(Row {
start,
text_end: text.len(),
end: text.len(),
});
if col >= width {
rows.push(Row {
start: text.len(),
text_end: text.len(),
end: text.len(),
});
}
RowMap { rows }
}
pub fn len(&self) -> usize {
self.rows.len()
}
pub fn row_of(&self, byte: usize, sticky: bool) -> usize {
let idx = self
.rows
.partition_point(|r| r.end <= byte)
.min(self.rows.len() - 1);
if sticky && idx > 0 {
let prev = self.rows[idx - 1];
if prev.end == byte && prev.text_end == prev.end {
return idx - 1;
}
}
idx
}
pub fn col_of(&self, text: &str, byte: usize, row_idx: usize) -> i32 {
let row = self.rows[row_idx];
let slice = &text[row.start..row.text_end];
let rel = byte.saturating_sub(row.start).min(slice.len());
crate::text::width(&slice[..rel])
}
pub fn byte_at_col(&self, text: &str, row_idx: usize, goal: i32) -> usize {
let row = self.rows[row_idx];
let slice = &text[row.start..row.text_end];
let mut col = 0i32;
for seg in crate::text::segments(slice) {
if seg.width > 0 && col + seg.width > goal {
return row.start + seg.offset;
}
col += seg.width;
}
row.text_end
}
pub fn visual(&self, text: &str, byte: usize, sticky: bool) -> (usize, i32) {
let row = self.row_of(byte, sticky);
(row, self.col_of(text, byte, row))
}
}
pub(crate) fn snap_boundary(text: &str, byte: usize) -> usize {
if byte >= text.len() {
return text.len();
}
for seg in crate::text::segments(text) {
if seg.offset == byte {
return byte;
}
let end = seg.offset + seg.cluster.len();
if end > byte {
return end;
}
}
text.len()
}
pub(crate) fn selection_range(c: &Caret) -> (usize, usize) {
match c.anchor {
Some(a) if a != c.byte => (a.min(c.byte), a.max(c.byte)),
_ => (usize::MAX, usize::MAX),
}
}
pub(crate) fn delete_selection(text: &mut String, c: &mut Caret) -> bool {
let (lo, hi) = selection_range(c);
if lo == usize::MAX {
c.anchor = None;
return false;
}
text.replace_range(lo..hi, "");
c.byte = lo;
c.anchor = None;
true
}
pub(crate) fn insert_at_caret(text: &mut String, c: &mut Caret, s: &str) {
delete_selection(text, c);
let at = snap_boundary(text, c.byte.min(text.len()));
text.insert_str(at, s);
c.byte = snap_boundary(text, at + s.len());
c.anchor = None;
c.goal = None;
c.sticky = false;
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum EditOutcome {
Handled { edited: bool },
Submit,
HistoryBack,
HistoryForward,
Ignored,
}
pub(crate) fn apply_key(
text: &mut String,
c: &mut Caret,
key: Key,
mods: Mods,
width: i32,
policy: SubmitPolicy,
) -> EditOutcome {
let shift = mods.contains(Mods::SHIFT);
let alt = mods.contains(Mods::ALT);
let ctrl = mods.contains(Mods::CTRL);
c.byte = snap_boundary(text, c.byte.min(text.len()));
if let Some(a) = c.anchor {
c.anchor = Some(snap_boundary(text, a.min(text.len())));
}
let move_to = |c: &mut Caret, target: usize, sticky: bool| {
if shift {
if c.anchor.is_none() {
c.anchor = Some(c.byte);
}
} else {
c.anchor = None;
}
c.byte = target;
c.sticky = sticky;
};
if let Some(intent) = crate::widgets::edit_keys::word_intent(key, mods) {
use crate::widgets::edit_keys::WordIntent;
c.goal = None;
return match intent {
WordIntent::Left => {
move_to(c, word_step_bytes(text, c.byte, -1), false);
EditOutcome::Handled { edited: false }
}
WordIntent::Right => {
move_to(c, word_step_bytes(text, c.byte, 1), false);
EditOutcome::Handled { edited: false }
}
WordIntent::DeleteBack => {
let mut edited = delete_selection(text, c);
if !edited && c.byte > 0 {
let cut = word_step_bytes(text, c.byte, -1);
edited = cut < c.byte;
text.replace_range(cut..c.byte, "");
c.byte = cut;
}
c.sticky = false;
EditOutcome::Handled { edited }
}
WordIntent::DeleteForward => {
let mut edited = delete_selection(text, c);
if !edited && c.byte < text.len() {
let end = word_step_bytes(text, c.byte, 1);
edited = end > c.byte;
text.replace_range(c.byte..end, "");
}
c.sticky = false;
EditOutcome::Handled { edited }
}
};
}
match key {
Key::Left => {
move_to(c, crate::text::prev_boundary(text, c.byte), false);
c.goal = None;
EditOutcome::Handled { edited: false }
}
Key::Right => {
move_to(c, crate::text::next_boundary(text, c.byte), false);
c.goal = None;
EditOutcome::Handled { edited: false }
}
Key::Char('a') if ctrl && !alt => {
let rows = RowMap::build(text, width);
let target = rows.rows[rows.row_of(c.byte, c.sticky)].start;
move_to(c, target, false);
c.goal = None;
EditOutcome::Handled { edited: false }
}
Key::Home => {
let target = if ctrl {
0
} else {
let rows = RowMap::build(text, width);
rows.rows[rows.row_of(c.byte, c.sticky)].start
};
move_to(c, target, false);
c.goal = None;
EditOutcome::Handled { edited: false }
}
Key::Char('e') if ctrl && !alt => {
let rows = RowMap::build(text, width);
let row = rows.rows[rows.row_of(c.byte, c.sticky)];
let sticky = row_end_has_room(&rows, text, row, width);
move_to(c, row.text_end, sticky);
c.goal = None;
EditOutcome::Handled { edited: false }
}
Key::End => {
let (target, sticky) = if ctrl {
(text.len(), false)
} else {
let rows = RowMap::build(text, width);
let row = rows.rows[rows.row_of(c.byte, c.sticky)];
(row.text_end, row_end_has_room(&rows, text, row, width))
};
move_to(c, target, sticky);
c.goal = None;
EditOutcome::Handled { edited: false }
}
Key::Up | Key::Down => {
let rows = RowMap::build(text, width);
let (row, col) = rows.visual(text, c.byte, c.sticky);
let goal = c.goal.unwrap_or(col);
if key == Key::Up {
if row > 0 {
let target = rows.byte_at_col(text, row - 1, goal);
let sticky = soft_row_end(&rows, text, row - 1, target, width);
move_to(c, target, sticky);
c.goal = Some(goal);
} else if shift || c.byte > 0 {
move_to(c, 0, false);
c.goal = None;
} else {
return EditOutcome::HistoryBack;
}
} else {
let last = rows.len() - 1;
if row < last {
let target = rows.byte_at_col(text, row + 1, goal);
let sticky = soft_row_end(&rows, text, row + 1, target, width);
move_to(c, target, sticky);
c.goal = Some(goal);
} else if shift || c.byte < text.len() {
move_to(c, text.len(), false);
c.goal = None;
} else {
return EditOutcome::HistoryForward;
}
}
EditOutcome::Handled { edited: false }
}
Key::Char(ch) if !ctrl && !alt => {
let mut buf = [0u8; 4];
insert_at_caret(text, c, ch.encode_utf8(&mut buf));
EditOutcome::Handled { edited: true }
}
Key::Backspace => {
if !delete_selection(text, c) && c.byte > 0 {
let cut = crate::text::prev_boundary(text, c.byte);
text.replace_range(cut..c.byte, "");
c.byte = cut;
}
c.goal = None;
c.sticky = false;
EditOutcome::Handled { edited: true }
}
Key::Delete => {
if !delete_selection(text, c) && c.byte < text.len() {
let end = crate::text::next_boundary(text, c.byte);
text.replace_range(c.byte..end, "");
}
c.goal = None;
c.sticky = false;
EditOutcome::Handled { edited: true }
}
Key::Enter => {
if alt || shift {
insert_at_caret(text, c, "\n");
return EditOutcome::Handled { edited: true };
}
match policy {
SubmitPolicy::EnterSubmits => EditOutcome::Submit,
SubmitPolicy::EnterInserts => {
insert_at_caret(text, c, "\n");
EditOutcome::Handled { edited: true }
}
}
}
Key::Char('j') if ctrl && !alt => {
insert_at_caret(text, c, "\n");
EditOutcome::Handled { edited: true }
}
_ => EditOutcome::Ignored,
}
}
fn soft_row_end(rows: &RowMap, text: &str, row: usize, byte: usize, width: i32) -> bool {
let r = rows.rows[row];
byte == r.end && row_end_has_room(rows, text, r, width)
}
fn row_end_has_room(_rows: &RowMap, text: &str, row: Row, width: i32) -> bool {
row.text_end == row.end
&& row.end > row.start
&& crate::text::width(&text[row.start..row.text_end]) < width
}
fn word_step_bytes(text: &str, from: usize, dir: i32) -> usize {
let map = ClusterMap::of(text);
let idx = map.cluster_after(from);
let target = word_step(text, &map, idx, dir);
map.byte(target)
}
pub(crate) fn adjust_top(c: &mut Caret, caret_row: i32, total_rows: i32, visible: i32) {
let visible = visible.max(1);
if c.detached {
c.top = c.top.clamp(0, (total_rows - visible).max(0));
return;
}
if caret_row < c.top {
c.top = caret_row;
}
if caret_row >= c.top + visible {
c.top = caret_row - visible + 1;
}
c.top = c.top.clamp(0, (total_rows - visible).max(0));
}
pub(crate) struct History {
entries: Vec<String>,
nav: Option<usize>,
draft: Option<String>,
cap: usize,
}
impl History {
pub fn new(cap: usize) -> History {
History {
entries: Vec::new(),
nav: None,
draft: None,
cap: cap.max(1),
}
}
pub fn push(&mut self, entry: &str) {
self.nav = None;
self.draft = None;
if entry.is_empty() || self.entries.last().is_some_and(|e| e == entry) {
return;
}
self.entries.push(entry.to_string());
if self.entries.len() > self.cap {
let overflow = self.entries.len() - self.cap;
self.entries.drain(..overflow);
}
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn back(&mut self, current: &str) -> Option<String> {
if self.entries.is_empty() {
return None;
}
match self.nav {
None => {
self.draft = Some(current.to_string());
self.nav = Some(self.entries.len() - 1);
}
Some(0) => return None,
Some(i) => self.nav = Some(i - 1),
}
self.nav.map(|i| self.entries[i].clone())
}
pub fn forward(&mut self) -> Option<String> {
let i = self.nav?;
if i + 1 < self.entries.len() {
self.nav = Some(i + 1);
Some(self.entries[i + 1].clone())
} else {
self.nav = None;
Some(self.draft.take().unwrap_or_default())
}
}
}