use promkit_core::grapheme::StyledGraphemes;
#[derive(Clone, Default)]
pub struct Text {
lines: Vec<StyledGraphemes>,
current_line: Option<usize>,
}
impl<T: AsRef<str>> From<T> for Text {
fn from(text: T) -> Self {
let value = text.as_ref();
let lines: Vec<StyledGraphemes> = if value.is_empty() {
Vec::new()
} else {
value
.split('\n')
.map(|line| if line.is_empty() { "\0" } else { line })
.map(StyledGraphemes::from)
.collect()
};
let current_line = (!lines.is_empty()).then_some(0);
Self {
lines,
current_line,
}
}
}
impl Text {
pub fn from_styled_graphemes(lines: Vec<StyledGraphemes>) -> Self {
let current_line = (!lines.is_empty()).then_some(0);
Self {
lines,
current_line,
}
}
pub fn replace_contents(&mut self, lines: Vec<StyledGraphemes>) {
self.current_line = if lines.is_empty() {
None
} else {
Some(self.current_line.unwrap_or_default().min(lines.len() - 1))
};
self.lines = lines;
}
pub fn items(&self) -> &Vec<StyledGraphemes> {
&self.lines
}
pub fn position(&self) -> usize {
self.current_line.unwrap_or_default()
}
pub fn current_line(&self) -> Option<usize> {
self.current_line
}
pub fn backward(&mut self) -> bool {
let Some(position) = self.current_line.filter(|position| *position > 0) else {
return false;
};
self.current_line = Some(position - 1);
true
}
pub fn forward(&mut self) -> bool {
let Some(position) = self
.current_line
.filter(|position| position.saturating_add(1) < self.lines.len())
else {
return false;
};
self.current_line = Some(position + 1);
true
}
pub fn move_to(&mut self, position: usize) -> bool {
if position < self.lines.len() {
self.current_line = Some(position);
true
} else {
false
}
}
}
#[cfg(test)]
mod tests {
use promkit_core::grapheme::StyledGraphemes;
use super::Text;
mod from {
use super::*;
#[test]
fn empty_input_creates_no_lines() {
let text = Text::from("");
assert!(text.items().is_empty());
}
#[test]
fn explicit_empty_lines_are_preserved() {
let text = Text::from("a\n\nb");
assert_eq!(text.items().len(), 3);
assert_eq!(text.items()[1].chars(), vec!['\0']);
}
}
mod replace_contents {
use super::*;
#[test]
fn restores_a_current_line_for_empty_text() {
let mut text = Text::default();
text.replace_contents(vec![StyledGraphemes::from("first")]);
assert_eq!(text.current_line(), Some(0));
}
}
mod backward {
use super::*;
#[test]
fn stops_at_the_first_line() {
let mut text = Text::from("first\nsecond");
assert!(!text.backward());
assert_eq!(text.current_line(), Some(0));
}
}
mod forward {
use super::*;
#[test]
fn stops_at_the_last_line() {
let mut text = Text::from("first\nsecond");
assert!(text.forward());
assert_eq!(text.current_line(), Some(1));
assert!(!text.forward());
assert_eq!(text.current_line(), Some(1));
}
}
mod move_to {
use super::*;
#[test]
fn rejects_an_out_of_bounds_line() {
let mut text = Text::from("first\nsecond");
assert!(!text.move_to(2));
assert_eq!(text.current_line(), Some(0));
}
}
}