pub mod helpers;
pub mod memoization;
#[cfg(test)]
mod tests;
use helpers::*;
use memoization::MemoizedWrap;
use crate::{cursor, viewport, Component};
use bubbletea_rs::{Cmd, Model as BubbleTeaModel};
use lipgloss_extras::lipgloss;
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
const MIN_HEIGHT: usize = 1;
const DEFAULT_HEIGHT: usize = 6;
const DEFAULT_WIDTH: usize = 40;
const DEFAULT_CHAR_LIMIT: usize = 0; const DEFAULT_MAX_HEIGHT: usize = 99;
const DEFAULT_MAX_WIDTH: usize = 500;
const MAX_LINES: usize = 10000;
#[derive(Debug, Clone)]
pub struct PasteMsg(pub String);
#[derive(Debug, Clone)]
pub struct PasteErrMsg(pub String);
#[derive(Debug, Clone, Default)]
pub struct LineInfo {
pub width: usize,
pub char_width: usize,
pub height: usize,
pub start_column: usize,
pub column_offset: usize,
pub row_offset: usize,
pub char_offset: usize,
}
#[derive(Debug)]
pub struct Model {
pub err: Option<String>,
cache: MemoizedWrap,
pub prompt: String,
pub placeholder: String,
pub show_line_numbers: bool,
pub end_of_buffer_character: char,
pub key_map: TextareaKeyMap,
pub focused_style: TextareaStyle,
pub blurred_style: TextareaStyle,
current_style: TextareaStyle,
pub cursor: cursor::Model,
pub char_limit: usize,
pub max_height: usize,
pub max_width: usize,
prompt_func: Option<fn(usize) -> String>,
prompt_width: usize,
width: usize,
height: usize,
value: Vec<Vec<char>>,
focus: bool,
col: usize,
row: usize,
last_char_offset: usize,
viewport: viewport::Model,
}
impl Model {
pub fn new() -> Self {
let vp = viewport::Model::new(0, 0);
let cur = cursor::Model::new();
let (focused_style, blurred_style) = default_styles();
let mut model = Self {
err: None,
cache: MemoizedWrap::new(),
prompt: format!("{} ", lipgloss::thick_border().left),
placeholder: String::new(),
show_line_numbers: true,
end_of_buffer_character: ' ',
key_map: TextareaKeyMap::default(),
focused_style: focused_style.clone(),
blurred_style: blurred_style.clone(),
current_style: blurred_style, cursor: cur,
char_limit: DEFAULT_CHAR_LIMIT,
max_height: DEFAULT_MAX_HEIGHT,
max_width: DEFAULT_MAX_WIDTH,
prompt_func: None,
prompt_width: 0,
width: DEFAULT_WIDTH,
height: DEFAULT_HEIGHT,
value: vec![vec![]; MIN_HEIGHT],
focus: false,
col: 0,
row: 0,
last_char_offset: 0,
viewport: vp,
};
model.value.reserve(MAX_LINES);
model.set_height(DEFAULT_HEIGHT);
model.set_width(DEFAULT_WIDTH);
model
}
pub fn set_value(&mut self, s: impl Into<String>) {
self.reset();
self.insert_string(s.into());
self.row = self.value.len().saturating_sub(1);
if let Some(line) = self.value.get(self.row) {
self.set_cursor(line.len());
}
}
pub fn insert_string(&mut self, s: impl Into<String>) {
let s = s.into();
let runes: Vec<char> = s.chars().collect();
self.insert_runes_from_user_input(runes);
}
pub fn insert_rune(&mut self, r: char) {
self.insert_runes_from_user_input(vec![r]);
}
pub fn value(&self) -> String {
if self.value.is_empty() {
return String::new();
}
let mut result = String::new();
for (i, line) in self.value.iter().enumerate() {
if i > 0 {
result.push('\n');
}
result.extend(line.iter());
}
result
}
pub fn length(&self) -> usize {
let mut l = 0;
for row in &self.value {
l += row
.iter()
.map(|&ch| UnicodeWidthChar::width(ch).unwrap_or(0))
.sum::<usize>();
}
l + self.value.len().saturating_sub(1)
}
pub fn line_count(&self) -> usize {
self.value.len()
}
pub fn line(&self) -> usize {
self.row
}
pub fn focused(&self) -> bool {
self.focus
}
pub fn reset(&mut self) {
self.value = vec![vec![]; MIN_HEIGHT];
self.value.reserve(MAX_LINES);
self.col = 0;
self.row = 0;
self.viewport.goto_top();
self.set_cursor(0);
}
pub fn width(&self) -> usize {
self.width
}
pub fn height(&self) -> usize {
self.height
}
pub fn set_width(&mut self, w: usize) {
if self.prompt_func.is_none() {
self.prompt_width = self.prompt.width();
}
let reserved_outer = 0;
let mut reserved_inner = self.prompt_width;
if self.show_line_numbers {
let ln_width = 4; reserved_inner += ln_width;
}
let min_width = reserved_inner + reserved_outer + 1;
let mut input_width = w.max(min_width);
if self.max_width > 0 {
input_width = input_width.min(self.max_width);
}
self.viewport.width = input_width.saturating_sub(reserved_outer);
self.width = input_width
.saturating_sub(reserved_outer)
.saturating_sub(reserved_inner);
}
pub fn set_height(&mut self, h: usize) {
if self.max_height > 0 {
self.height = clamp(h, MIN_HEIGHT, self.max_height);
self.viewport.height = clamp(h, MIN_HEIGHT, self.max_height);
} else {
self.height = h.max(MIN_HEIGHT);
self.viewport.height = h.max(MIN_HEIGHT);
}
}
pub fn set_prompt_func(&mut self, prompt_width: usize, func: fn(usize) -> String) {
self.prompt_func = Some(func);
self.prompt_width = prompt_width;
}
pub fn set_cursor(&mut self, col: usize) {
self.col = clamp(
col,
0,
self.value.get(self.row).map_or(0, |line| line.len()),
);
self.last_char_offset = 0;
}
pub fn cursor_start(&mut self) {
self.set_cursor(0);
}
pub fn cursor_end(&mut self) {
if let Some(line) = self.value.get(self.row) {
self.set_cursor(line.len());
}
}
pub fn cursor_down(&mut self) {
let li = self.line_info();
let char_offset = self.last_char_offset.max(li.char_offset);
self.last_char_offset = char_offset;
if li.row_offset + 1 >= li.height && self.row < self.value.len().saturating_sub(1) {
self.row += 1;
self.col = 0;
} else {
const TRAILING_SPACE: usize = 2;
if let Some(line) = self.value.get(self.row) {
self.col =
(li.start_column + li.width + TRAILING_SPACE).min(line.len().saturating_sub(1));
}
}
let nli = self.line_info();
self.col = nli.start_column;
if nli.width == 0 {
return;
}
let mut offset = 0;
while offset < char_offset {
if self.row >= self.value.len()
|| self.col >= self.value.get(self.row).map_or(0, |line| line.len())
|| offset >= nli.char_width.saturating_sub(1)
{
break;
}
if let Some(line) = self.value.get(self.row) {
if let Some(&ch) = line.get(self.col) {
offset += UnicodeWidthChar::width(ch).unwrap_or(0);
}
}
self.col += 1;
}
}
pub fn cursor_up(&mut self) {
let li = self.line_info();
let char_offset = self.last_char_offset.max(li.char_offset);
self.last_char_offset = char_offset;
if li.row_offset == 0 && self.row > 0 {
self.row -= 1;
if let Some(line) = self.value.get(self.row) {
self.col = line.len();
}
} else {
const TRAILING_SPACE: usize = 2;
self.col = li.start_column.saturating_sub(TRAILING_SPACE);
}
let nli = self.line_info();
self.col = nli.start_column;
if nli.width == 0 {
return;
}
let mut offset = 0;
while offset < char_offset {
if let Some(line) = self.value.get(self.row) {
if self.col >= line.len() || offset >= nli.char_width.saturating_sub(1) {
break;
}
if let Some(&ch) = line.get(self.col) {
offset += UnicodeWidthChar::width(ch).unwrap_or(0);
}
self.col += 1;
} else {
break;
}
}
}
pub fn move_to_begin(&mut self) {
self.row = 0;
self.set_cursor(0);
}
pub fn move_to_end(&mut self) {
self.row = self.value.len().saturating_sub(1);
if let Some(line) = self.value.get(self.row) {
self.set_cursor(line.len());
}
}
fn insert_runes_from_user_input(&mut self, mut runes: Vec<char>) {
runes = self.sanitize_runes(runes);
if self.char_limit > 0 {
let avail_space = self.char_limit.saturating_sub(self.length());
if avail_space == 0 {
return;
}
if avail_space < runes.len() {
runes.truncate(avail_space);
}
}
let mut lines = Vec::new();
let mut lstart = 0;
for (i, &r) in runes.iter().enumerate() {
if r == '\n' {
lines.push(runes[lstart..i].to_vec());
lstart = i + 1;
}
}
if lstart <= runes.len() {
lines.push(runes[lstart..].to_vec());
}
if MAX_LINES > 0 && self.value.len() + lines.len() - 1 > MAX_LINES {
let allowed_height = (MAX_LINES - self.value.len() + 1).max(0);
lines.truncate(allowed_height);
}
if lines.is_empty() {
return;
}
while self.row >= self.value.len() {
self.value.push(Vec::new());
}
let tail = if self.col < self.value[self.row].len() {
self.value[self.row][self.col..].to_vec()
} else {
Vec::new()
};
if self.col <= self.value[self.row].len() {
self.value[self.row].truncate(self.col);
}
self.value[self.row].extend_from_slice(&lines[0]);
self.col += lines[0].len();
if lines.len() > 1 {
for (i, line) in lines[1..].iter().enumerate() {
self.value.insert(self.row + 1 + i, line.clone());
}
self.row += lines.len() - 1;
self.col = lines.last().map(|l| l.len()).unwrap_or(0);
self.value[self.row].extend_from_slice(&tail);
} else {
self.value[self.row].extend_from_slice(&tail);
}
self.set_cursor(self.col);
}
fn sanitize_runes(&self, runes: Vec<char>) -> Vec<char> {
runes
}
pub fn line_info(&mut self) -> LineInfo {
if self.row >= self.value.len() {
return LineInfo::default();
}
let current_line = self.value[self.row].clone();
let width = self.width;
let grid = self.cache.wrap(¤t_line, width);
let mut counter = 0;
for (i, line) in grid.iter().enumerate() {
if counter + line.len() == self.col && i + 1 < grid.len() {
let next_line = &grid[i + 1];
return LineInfo {
char_offset: 0,
column_offset: 0,
height: grid.len(),
row_offset: i + 1,
start_column: self.col,
width: next_line.len(),
char_width: next_line
.iter()
.map(|&ch| UnicodeWidthChar::width(ch).unwrap_or(0))
.sum(),
};
}
if counter + line.len() >= self.col {
let col_in_line = self.col.saturating_sub(counter);
let char_off: usize = line
.iter()
.take(col_in_line.min(line.len()))
.map(|&ch| UnicodeWidthChar::width(ch).unwrap_or(0))
.sum();
return LineInfo {
char_offset: char_off,
column_offset: col_in_line,
height: grid.len(),
row_offset: i,
start_column: counter,
width: line.len(),
char_width: line
.iter()
.map(|&ch| UnicodeWidthChar::width(ch).unwrap_or(0))
.sum(),
};
}
counter += line.len();
}
if let Some(last_line) = grid.last() {
let last_counter = counter - last_line.len();
return LineInfo {
char_offset: last_line
.iter()
.map(|&ch| UnicodeWidthChar::width(ch).unwrap_or(0))
.sum(),
column_offset: last_line.len(),
height: grid.len(),
row_offset: grid.len().saturating_sub(1),
start_column: last_counter,
width: last_line.len(),
char_width: last_line
.iter()
.map(|&ch| UnicodeWidthChar::width(ch).unwrap_or(0))
.sum(),
};
}
LineInfo::default()
}
pub fn delete_before_cursor(&mut self) {
if let Some(line) = self.value.get_mut(self.row) {
let tail = if self.col <= line.len() {
line[self.col..].to_vec()
} else {
Vec::new()
};
*line = tail;
}
self.set_cursor(0);
}
pub fn delete_after_cursor(&mut self) {
if let Some(line) = self.value.get_mut(self.row) {
line.truncate(self.col);
let line_len = line.len();
self.set_cursor(line_len);
}
}
pub fn delete_character_backward(&mut self) {
self.col = clamp(
self.col,
0,
self.value.get(self.row).map_or(0, |line| line.len()),
);
if self.col == 0 {
self.merge_line_above(self.row);
return;
}
if let Some(line) = self.value.get_mut(self.row) {
if !line.is_empty() && self.col > 0 {
line.remove(self.col - 1);
self.set_cursor(self.col - 1);
}
}
}
pub fn delete_character_forward(&mut self) {
if let Some(line) = self.value.get_mut(self.row) {
if !line.is_empty() && self.col < line.len() {
line.remove(self.col);
}
}
if self.col >= self.value.get(self.row).map_or(0, |line| line.len()) {
self.merge_line_below(self.row);
}
}
pub fn delete_word_backward(&mut self) {
if self.col == 0 {
self.merge_line_above(self.row);
return;
}
let line = if let Some(line) = self.value.get(self.row) {
line.clone()
} else {
return;
};
if line.is_empty() {
return;
}
let mut start = self.col;
let mut end = self.col;
while end < line.len() && line.get(end).is_some_and(|&c| !c.is_whitespace()) {
end += 1;
}
while start > 0 && line.get(start - 1).is_some_and(|&c| !c.is_whitespace()) {
start -= 1;
}
if self.col < line.len() && line.get(self.col).is_some_and(|&c| !c.is_whitespace()) {
if start > 0 && line.get(start - 1).is_some_and(|&c| c.is_whitespace()) {
start -= 1;
}
}
if let Some(line_mut) = self.value.get_mut(self.row) {
let end_clamped = end.min(line_mut.len());
let start_clamped = start.min(end_clamped);
line_mut.drain(start_clamped..end_clamped);
}
self.set_cursor(start);
}
pub fn delete_word_forward(&mut self) {
let line = if let Some(line) = self.value.get(self.row) {
line.clone()
} else {
return;
};
if self.col >= line.len() || line.is_empty() {
self.merge_line_below(self.row);
return;
}
let old_col = self.col;
let mut new_col = self.col;
while new_col < line.len() {
if let Some(&ch) = line.get(new_col) {
if ch.is_whitespace() {
new_col += 1;
} else {
break;
}
} else {
break;
}
}
while new_col < line.len() {
if let Some(&ch) = line.get(new_col) {
if !ch.is_whitespace() {
new_col += 1;
} else {
break;
}
} else {
break;
}
}
if let Some(line) = self.value.get_mut(self.row) {
if new_col > line.len() {
line.truncate(old_col);
} else {
line.drain(old_col..new_col);
}
}
self.set_cursor(old_col);
}
fn merge_line_below(&mut self, row: usize) {
if row >= self.value.len().saturating_sub(1) {
return;
}
if let Some(next_line) = self.value.get(row + 1).cloned() {
if let Some(current_line) = self.value.get_mut(row) {
current_line.extend_from_slice(&next_line);
}
}
self.value.remove(row + 1);
}
fn merge_line_above(&mut self, row: usize) {
if row == 0 {
return;
}
if let Some(prev_line) = self.value.get(row - 1) {
self.col = prev_line.len();
}
self.row = row - 1;
if let Some(current_line) = self.value.get(row).cloned() {
if let Some(prev_line) = self.value.get_mut(row - 1) {
prev_line.extend_from_slice(¤t_line);
}
}
self.value.remove(row);
}
fn split_line(&mut self, row: usize, col: usize) {
if let Some(line) = self.value.get(row) {
let head = line[..col].to_vec();
let tail = line[col..].to_vec();
self.value[row] = head;
self.value.insert(row + 1, tail);
self.col = 0;
self.row += 1;
}
}
pub fn insert_newline(&mut self) {
if self.max_height > 0 && self.value.len() >= self.max_height {
return;
}
self.col = clamp(
self.col,
0,
self.value.get(self.row).map_or(0, |line| line.len()),
);
self.split_line(self.row, self.col);
}
pub fn character_left(&mut self, inside_line: bool) {
if self.col == 0 && self.row != 0 {
self.row -= 1;
if let Some(line) = self.value.get(self.row) {
self.col = line.len();
if !inside_line {
return;
}
}
}
if self.col > 0 {
self.set_cursor(self.col - 1);
}
}
pub fn character_right(&mut self) {
if let Some(line) = self.value.get(self.row) {
if self.col < line.len() {
self.set_cursor(self.col + 1);
} else if self.row < self.value.len() - 1 {
self.row += 1;
self.cursor_start();
}
}
}
pub fn word_left(&mut self) {
while self.col > 0 {
if let Some(line) = self.value.get(self.row) {
if line.get(self.col - 1).is_some_and(|c| c.is_whitespace()) {
self.set_cursor(self.col - 1);
} else {
break;
}
} else {
break;
}
}
while self.col > 0 {
if let Some(line) = self.value.get(self.row) {
if line.get(self.col - 1).is_some_and(|c| !c.is_whitespace()) {
self.set_cursor(self.col - 1);
} else {
break;
}
} else {
break;
}
}
}
pub fn word_right(&mut self) {
self.do_word_right(|_, _| {});
}
fn do_word_right<F>(&mut self, mut func: F)
where
F: FnMut(usize, usize),
{
if self.row >= self.value.len() {
return;
}
let line = match self.value.get(self.row) {
Some(line) => line.clone(),
None => return,
};
if self.col >= line.len() {
return;
}
let mut pos = self.col;
let mut char_idx = 0;
while pos < line.len() && line[pos].is_whitespace() {
pos += 1;
}
while pos < line.len() && !line[pos].is_whitespace() {
func(char_idx, pos);
pos += 1;
char_idx += 1;
}
self.set_cursor(pos);
}
pub fn uppercase_right(&mut self) {
let start_col = self.col;
let start_row = self.row;
self.word_right(); let end_col = self.col;
if let Some(line) = self.value.get_mut(start_row) {
let end_idx = end_col.min(line.len());
if let Some(slice) = line.get_mut(start_col..end_idx) {
for ch in slice.iter_mut() {
*ch = ch.to_uppercase().next().unwrap_or(*ch);
}
}
}
}
pub fn lowercase_right(&mut self) {
let start_col = self.col;
let start_row = self.row;
self.word_right(); let end_col = self.col;
if let Some(line) = self.value.get_mut(start_row) {
let end_idx = end_col.min(line.len());
if let Some(slice) = line.get_mut(start_col..end_idx) {
for ch in slice.iter_mut() {
*ch = ch.to_lowercase().next().unwrap_or(*ch);
}
}
}
}
pub fn capitalize_right(&mut self) {
let start_col = self.col;
let start_row = self.row;
self.word_right(); let end_col = self.col;
if let Some(line) = self.value.get_mut(start_row) {
let end_idx = end_col.min(line.len());
if let Some(slice) = line.get_mut(start_col..end_idx) {
for (i, ch) in slice.iter_mut().enumerate() {
if i == 0 {
*ch = ch.to_uppercase().next().unwrap_or(*ch);
}
}
}
}
}
pub fn transpose_left(&mut self) {
let row = self.row;
let mut col = self.col;
if let Some(line) = self.value.get_mut(row) {
if col == 0 || line.len() < 2 {
return;
}
if col >= line.len() {
col -= 1;
self.col = col;
}
if col > 0 && col < line.len() {
line.swap(col - 1, col);
if col < line.len() {
self.col = col + 1;
}
}
}
}
pub fn view(&mut self) -> String {
if self.value.is_empty() || (self.value.len() == 1 && self.value[0].is_empty()) {
return self.placeholder_view();
}
self.cursor.text_style = self.current_style.computed_cursor_line();
let mut s = String::new();
let line_info = self.line_info();
let style = &self.current_style;
let mut display_line = 0;
let mut widest_line_number = 0;
for (doc_line_idx, line) in self.value.iter().enumerate() {
let wrapped_lines = self.cache.wrap(line, self.width);
let is_current_doc_line = doc_line_idx == self.row;
for (wrap_idx, wrapped_line) in wrapped_lines.iter().enumerate() {
let prompt = self.get_prompt_string(display_line);
s.push_str(&style.computed_prompt().render(&prompt));
display_line += 1;
let mut ln = String::new();
if self.show_line_numbers {
if wrap_idx == 0 {
if is_current_doc_line {
ln = style
.computed_cursor_line_number()
.render(&self.format_line_number(doc_line_idx + 1));
} else {
ln = style
.computed_line_number()
.render(&self.format_line_number(doc_line_idx + 1));
}
} else if is_current_doc_line {
ln = style
.computed_cursor_line_number()
.render(&self.format_line_number(""));
} else {
ln = style
.computed_line_number()
.render(&self.format_line_number(""));
}
s.push_str(&ln);
}
let lnw = lipgloss::width(&ln);
if lnw > widest_line_number {
widest_line_number = lnw;
}
let strwidth = wrapped_line
.iter()
.map(|&ch| unicode_width::UnicodeWidthChar::width(ch).unwrap_or(0))
.sum::<usize>();
let mut padding = self.width.saturating_sub(strwidth);
if strwidth > self.width {
let content: String = wrapped_line
.iter()
.collect::<String>()
.trim_end()
.to_string();
let new_wrapped_line: Vec<char> = content.chars().collect();
let new_strwidth = new_wrapped_line
.iter()
.map(|&ch| unicode_width::UnicodeWidthChar::width(ch).unwrap_or(0))
.sum::<usize>();
padding = self.width.saturating_sub(new_strwidth);
}
if is_current_doc_line && line_info.row_offset == wrap_idx {
let col_offset = line_info.column_offset;
let before: String = wrapped_line.iter().take(col_offset).collect();
s.push_str(&style.computed_cursor_line().render(&before));
if self.col >= line.len() && line_info.char_offset >= self.width {
self.cursor.set_char(" ");
s.push_str(&self.cursor.view());
} else {
let cursor_char = wrapped_line.get(col_offset).unwrap_or(&' ');
self.cursor.set_char(&cursor_char.to_string());
s.push_str(&self.cursor.view());
let after: String = wrapped_line.iter().skip(col_offset + 1).collect();
s.push_str(&style.computed_cursor_line().render(&after));
}
} else {
let content: String = wrapped_line.iter().collect();
let line_style = if is_current_doc_line {
style.computed_cursor_line()
} else {
style.computed_text()
};
s.push_str(&line_style.render(&content));
}
s.push_str(&style.computed_text().render(&" ".repeat(padding.max(0))));
s.push('\n');
}
}
for _ in 0..(self.height.saturating_sub(display_line)) {
let prompt = self.get_prompt_string(display_line);
s.push_str(&style.computed_prompt().render(&prompt));
display_line += 1;
let left_gutter = self.end_of_buffer_character.to_string();
let right_gap_width =
self.width().saturating_sub(lipgloss::width(&left_gutter)) + widest_line_number;
let right_gap = " ".repeat(right_gap_width.max(0));
s.push_str(
&style
.computed_end_of_buffer()
.render(&(left_gutter + &right_gap)),
);
s.push('\n');
}
s
}
fn get_prompt_string(&self, display_line: usize) -> String {
if let Some(prompt_func) = self.prompt_func {
let prompt = prompt_func(display_line);
let pl = prompt.len();
if pl < self.prompt_width {
format!("{}{}", " ".repeat(self.prompt_width - pl), prompt)
} else {
prompt
}
} else {
self.prompt.clone()
}
}
fn format_line_number(&self, x: impl std::fmt::Display) -> String {
let digits = if self.max_height > 0 {
self.max_height.to_string().len()
} else {
3 };
format!(" {:width$} ", x, width = digits)
}
fn placeholder_view(&mut self) -> String {
if self.placeholder.is_empty() {
return String::new();
}
let mut s = String::new();
let placeholder_lines: Vec<&str> = self.placeholder.lines().collect();
for i in 0..self.height {
let prompt = self.get_prompt_string(i);
s.push_str(&prompt);
if self.show_line_numbers {
let ln = if i == 0 {
self.format_line_number(1)
} else {
self.format_line_number("")
};
s.push_str(&ln);
}
if i < placeholder_lines.len() {
s.push_str(placeholder_lines[i]);
} else {
if self.end_of_buffer_character != ' ' {
s.push(self.end_of_buffer_character);
}
}
s.push('\n');
}
s.trim_end_matches('\n').to_string()
}
pub fn scroll_down(&mut self, lines: usize) {
self.viewport.set_y_offset(self.viewport.y_offset + lines);
}
pub fn scroll_up(&mut self, lines: usize) {
self.viewport
.set_y_offset(self.viewport.y_offset.saturating_sub(lines));
}
pub fn cursor_line_number(&mut self) -> usize {
if self.row >= self.value.len() {
return 0;
}
let mut line_count = 0;
for i in 0..self.row {
if let Some(line) = self.value.get(i).cloned() {
let wrapped_lines = self.cache.wrap(&line, self.width);
line_count += wrapped_lines.len();
}
}
line_count += self.line_info().row_offset;
line_count
}
fn reposition_view(&mut self) {
let cursor_line = self.cursor_line_number();
let minimum = self.viewport.y_offset;
let maximum = minimum + self.viewport.height.saturating_sub(1);
if cursor_line < minimum {
self.viewport.set_y_offset(cursor_line);
} else if cursor_line > maximum {
let new_offset = cursor_line.saturating_sub(self.viewport.height.saturating_sub(1));
self.viewport.set_y_offset(new_offset);
}
}
pub fn update(&mut self, msg: Option<bubbletea_rs::Msg>) -> Option<bubbletea_rs::Cmd> {
if !self.focus {
return None;
}
if let Some(msg) = msg {
if let Some(paste_msg) = msg.downcast_ref::<PasteMsg>() {
self.insert_string(paste_msg.0.clone());
return None;
}
if let Some(_paste_err) = msg.downcast_ref::<PasteErrMsg>() {
return None;
}
if let Some(key_msg) = msg.downcast_ref::<bubbletea_rs::KeyMsg>() {
return self.handle_key_msg(key_msg);
}
let cursor_cmd = self.cursor.update(&msg);
let viewport_cmd = self.viewport.update(msg);
cursor_cmd.or(viewport_cmd)
} else {
None
}
}
fn handle_key_msg(&mut self, key_msg: &bubbletea_rs::KeyMsg) -> Option<bubbletea_rs::Cmd> {
let old_row = self.row;
let old_col = self.col;
if let Some(cmd) = self.handle_clipboard_keys(key_msg) {
return Some(cmd);
}
self.handle_movement_keys(key_msg);
self.handle_deletion_keys(key_msg);
self.handle_text_operations(key_msg);
self.handle_text_insertion(key_msg);
self.handle_character_input(key_msg);
if self.row != old_row || self.col != old_col {
self.reposition_view();
}
None
}
fn handle_clipboard_keys(
&mut self,
key_msg: &bubbletea_rs::KeyMsg,
) -> Option<bubbletea_rs::Cmd> {
use crate::key::matches_binding;
if matches_binding(key_msg, &self.key_map.paste) {
return Some(self.paste_command());
}
None
}
fn handle_movement_keys(&mut self, key_msg: &bubbletea_rs::KeyMsg) {
use crate::key::matches_binding;
if matches_binding(key_msg, &self.key_map.character_forward) {
self.character_right();
} else if matches_binding(key_msg, &self.key_map.character_backward) {
self.character_left(false);
} else if matches_binding(key_msg, &self.key_map.word_forward) {
self.word_right();
} else if matches_binding(key_msg, &self.key_map.word_backward) {
self.word_left();
} else if matches_binding(key_msg, &self.key_map.line_next) {
self.cursor_down();
} else if matches_binding(key_msg, &self.key_map.line_previous) {
self.cursor_up();
} else if matches_binding(key_msg, &self.key_map.line_start) {
self.cursor_start();
} else if matches_binding(key_msg, &self.key_map.line_end) {
self.cursor_end();
} else if matches_binding(key_msg, &self.key_map.input_begin) {
self.move_to_begin();
} else if matches_binding(key_msg, &self.key_map.input_end) {
self.move_to_end();
}
}
fn handle_deletion_keys(&mut self, key_msg: &bubbletea_rs::KeyMsg) {
use crate::key::matches_binding;
if matches_binding(key_msg, &self.key_map.delete_character_backward) {
self.delete_character_backward();
} else if matches_binding(key_msg, &self.key_map.delete_character_forward) {
self.delete_character_forward();
} else if matches_binding(key_msg, &self.key_map.delete_word_backward) {
self.delete_word_backward();
} else if matches_binding(key_msg, &self.key_map.delete_word_forward) {
self.delete_word_forward();
} else if matches_binding(key_msg, &self.key_map.delete_after_cursor) {
self.delete_after_cursor();
} else if matches_binding(key_msg, &self.key_map.delete_before_cursor) {
self.delete_before_cursor();
}
}
fn handle_text_operations(&mut self, key_msg: &bubbletea_rs::KeyMsg) {
use crate::key::matches_binding;
if matches_binding(key_msg, &self.key_map.uppercase_word_forward) {
self.uppercase_right();
} else if matches_binding(key_msg, &self.key_map.lowercase_word_forward) {
self.lowercase_right();
} else if matches_binding(key_msg, &self.key_map.capitalize_word_forward) {
self.capitalize_right();
} else if matches_binding(key_msg, &self.key_map.transpose_character_backward) {
self.transpose_left();
}
}
fn handle_text_insertion(&mut self, key_msg: &bubbletea_rs::KeyMsg) {
use crate::key::matches_binding;
if matches_binding(key_msg, &self.key_map.insert_newline) {
self.insert_newline();
}
}
fn handle_character_input(&mut self, key_msg: &bubbletea_rs::KeyMsg) {
if let Some(ch) = self.extract_character_from_key_msg(key_msg) {
if ch.is_control() {
return;
}
self.insert_rune(ch);
}
}
fn extract_character_from_key_msg(&self, key_msg: &bubbletea_rs::KeyMsg) -> Option<char> {
use crossterm::event::{KeyCode, KeyModifiers};
if key_msg.modifiers.contains(KeyModifiers::CONTROL)
|| key_msg.modifiers.contains(KeyModifiers::ALT)
{
return None;
}
match key_msg.key {
KeyCode::Char(c) => {
if c.is_control() {
None
} else {
Some(c)
}
}
KeyCode::Tab => Some('\t'),
_ => None,
}
}
fn paste_command(&self) -> bubbletea_rs::Cmd {
bubbletea_rs::tick(
std::time::Duration::from_millis(1),
|_| match Self::read_clipboard() {
Ok(content) => Box::new(PasteMsg(content)) as bubbletea_rs::Msg,
Err(err) => Box::new(PasteErrMsg(err)) as bubbletea_rs::Msg,
},
)
}
fn read_clipboard() -> Result<String, String> {
#[cfg(feature = "clipboard-support")]
{
use clipboard::{ClipboardContext, ClipboardProvider};
let res: Result<String, String> = (|| {
let mut ctx: ClipboardContext = ClipboardProvider::new()
.map_err(|e| format!("Failed to create clipboard context: {}", e))?;
ctx.get_contents()
.map_err(|e| format!("Failed to read clipboard: {}", e))
})();
res
}
#[cfg(not(feature = "clipboard-support"))]
{
Ok(String::new())
}
}
pub fn copy_to_clipboard(&self, text: &str) -> Result<(), String> {
#[cfg(feature = "clipboard-support")]
{
use clipboard::{ClipboardContext, ClipboardProvider};
let mut ctx: ClipboardContext = ClipboardProvider::new()
.map_err(|e| format!("Failed to create clipboard context: {}", e))?;
ctx.set_contents(text.to_string())
.map_err(|e| format!("Failed to write to clipboard: {}", e))
}
#[cfg(not(feature = "clipboard-support"))]
{
let _ = text; Err("Clipboard support not enabled".to_string())
}
}
pub fn copy_selection(&self) -> Result<(), String> {
let content = self.value();
self.copy_to_clipboard(&content)
}
pub fn cut_selection(&mut self) -> Result<(), String> {
let content = self.value();
self.copy_to_clipboard(&content)?;
self.reset();
Ok(())
}
}
impl Default for Model {
fn default() -> Self {
Self::new()
}
}
impl Component for Model {
fn focus(&mut self) -> Option<Cmd> {
self.focus = true;
self.current_style = self.focused_style.clone();
self.cursor.focus()
}
fn blur(&mut self) {
self.focus = false;
self.current_style = self.blurred_style.clone();
self.cursor.blur();
}
fn focused(&self) -> bool {
self.focus
}
}
pub fn default_styles() -> (TextareaStyle, TextareaStyle) {
let focused = default_focused_style();
let blurred = default_blurred_style();
(focused, blurred)
}
pub fn new() -> Model {
Model::new()
}