cove/src/ui/widgets/editor.rs

542 lines
16 KiB
Rust

use std::iter;
use std::sync::Arc;
use async_trait::async_trait;
use crossterm::style::{ContentStyle, Stylize};
use parking_lot::{FairMutex, Mutex};
use toss::frame::{Frame, Pos, Size};
use toss::styled::Styled;
use toss::terminal::Terminal;
use unicode_segmentation::UnicodeSegmentation;
use crate::ui::util;
use super::text::Text;
use super::Widget;
/// Like [`Frame::wrap`] but includes a final break index if the text ends with
/// a newline.
fn wrap(frame: &mut Frame, text: &str, width: usize) -> Vec<usize> {
let mut breaks = frame.wrap(text, width);
if text.ends_with('\n') {
breaks.push(text.len())
}
breaks
}
///////////
// State //
///////////
struct InnerEditorState {
text: String,
/// Index of the cursor in the text.
///
/// Must point to a valid grapheme boundary.
idx: usize,
/// Column of the cursor on the screen just after it was last moved
/// horizontally.
col: usize,
/// Width of the text when the editor was last rendered.
///
/// Does not include additional column for cursor.
last_width: u16,
}
impl InnerEditorState {
fn new(text: String) -> Self {
Self {
idx: text.len(),
col: 0,
last_width: u16::MAX,
text,
}
}
///////////////////////////////
// Grapheme helper functions //
///////////////////////////////
fn grapheme_boundaries(&self) -> Vec<usize> {
self.text
.grapheme_indices(true)
.map(|(i, _)| i)
.chain(iter::once(self.text.len()))
.collect()
}
/// Ensure the cursor index lies on a grapheme boundary. If it doesn't, it
/// is moved to the next grapheme boundary.
///
/// Can handle arbitrary cursor index.
fn move_cursor_to_grapheme_boundary(&mut self) {
for i in self.grapheme_boundaries() {
#[allow(clippy::comparison_chain)]
if i == self.idx {
// We're at a valid grapheme boundary already
return;
} else if i > self.idx {
// There was no valid grapheme boundary at our cursor index, so
// we'll take the next one we can get.
self.idx = i;
return;
}
}
// The cursor was out of bounds, so move it to the last valid index.
self.idx = self.text.len();
}
///////////////////////////////
// Line/col helper functions //
///////////////////////////////
/// Like [`Self::grapheme_boundaries`] but for lines.
///
/// Note that the last line can have a length of 0 if the text ends with a
/// newline.
fn line_boundaries(&self) -> Vec<usize> {
let newlines = self
.text
.char_indices()
.filter(|(_, c)| *c == '\n')
.map(|(i, _)| i + 1); // utf-8 encodes '\n' as a single byte
iter::once(0)
.chain(newlines)
.chain(iter::once(self.text.len()))
.collect()
}
/// Find the cursor's current line.
///
/// Returns `(line_nr, start_idx, end_idx)`.
fn cursor_line(&self, boundaries: &[usize]) -> (usize, usize, usize) {
let mut result = (0, 0, 0);
for (i, (start, end)) in boundaries.iter().zip(boundaries.iter().skip(1)).enumerate() {
if self.idx >= *start {
result = (i, *start, *end);
} else {
break;
}
}
result
}
fn cursor_col(&self, frame: &mut Frame, line_start: usize) -> usize {
frame.width(&self.text[line_start..self.idx])
}
fn line(&self, line: usize) -> (usize, usize) {
let boundaries = self.line_boundaries();
boundaries
.iter()
.copied()
.zip(boundaries.iter().copied().skip(1))
.nth(line)
.expect("line exists")
}
fn move_cursor_to_line_col(&mut self, frame: &mut Frame, line: usize, col: usize) {
let (start, end) = self.line(line);
let line = &self.text[start..end];
let mut width = 0;
for (gi, g) in line.grapheme_indices(true) {
self.idx = start + gi;
if col > width {
width += frame.grapheme_width(g, width) as usize;
} else {
return;
}
}
if !line.ends_with('\n') {
self.idx = end;
}
}
fn record_cursor_col(&mut self, frame: &mut Frame) {
let boundaries = self.line_boundaries();
let (_, start, _) = self.cursor_line(&boundaries);
self.col = self.cursor_col(frame, start);
}
/////////////
// Editing //
/////////////
fn clear(&mut self) {
self.text = String::new();
self.idx = 0;
self.col = 0;
}
fn set_text(&mut self, frame: &mut Frame, text: String) {
self.text = text;
self.move_cursor_to_grapheme_boundary();
self.record_cursor_col(frame);
}
/// Insert a character at the current cursor position and move the cursor
/// accordingly.
fn insert_char(&mut self, frame: &mut Frame, ch: char) {
self.text.insert(self.idx, ch);
self.idx += ch.len_utf8();
self.record_cursor_col(frame);
}
/// Insert a string at the current cursor position and move the cursor
/// accordingly.
fn insert_str(&mut self, frame: &mut Frame, str: &str) {
self.text.insert_str(self.idx, str);
self.idx += str.len();
self.record_cursor_col(frame);
}
/// Delete the grapheme before the cursor position.
fn backspace(&mut self, frame: &mut Frame) {
let boundaries = self.grapheme_boundaries();
for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)) {
if *end == self.idx {
self.text.replace_range(start..end, "");
self.idx = *start;
self.record_cursor_col(frame);
break;
}
}
}
/// Delete the grapheme after the cursor position.
fn delete(&mut self) {
let boundaries = self.grapheme_boundaries();
for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)) {
if *start == self.idx {
self.text.replace_range(start..end, "");
break;
}
}
}
/////////////////////
// Cursor movement //
/////////////////////
fn move_cursor_left(&mut self, frame: &mut Frame) {
let boundaries = self.grapheme_boundaries();
for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)) {
if *end == self.idx {
self.idx = *start;
self.record_cursor_col(frame);
break;
}
}
}
fn move_cursor_right(&mut self, frame: &mut Frame) {
let boundaries = self.grapheme_boundaries();
for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)) {
if *start == self.idx {
self.idx = *end;
self.record_cursor_col(frame);
break;
}
}
}
fn move_cursor_left_a_word(&mut self, frame: &mut Frame) {
let boundaries = self.grapheme_boundaries();
let mut encountered_word = false;
for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)).rev() {
if *end == self.idx {
let g = &self.text[*start..*end];
let whitespace = g.chars().all(|c| c.is_whitespace());
if encountered_word && whitespace {
break;
} else if !whitespace {
encountered_word = true;
}
self.idx = *start;
}
}
self.record_cursor_col(frame);
}
fn move_cursor_right_a_word(&mut self, frame: &mut Frame) {
let boundaries = self.grapheme_boundaries();
let mut encountered_word = false;
for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)) {
if *start == self.idx {
let g = &self.text[*start..*end];
let whitespace = g.chars().all(|c| c.is_whitespace());
if encountered_word && whitespace {
break;
} else if !whitespace {
encountered_word = true;
}
self.idx = *end;
}
}
self.record_cursor_col(frame);
}
fn move_cursor_to_start_of_line(&mut self, frame: &mut Frame) {
let boundaries = self.line_boundaries();
let (line, _, _) = self.cursor_line(&boundaries);
self.move_cursor_to_line_col(frame, line, 0);
self.record_cursor_col(frame);
}
fn move_cursor_to_end_of_line(&mut self, frame: &mut Frame) {
let boundaries = self.line_boundaries();
let (line, _, _) = self.cursor_line(&boundaries);
self.move_cursor_to_line_col(frame, line, usize::MAX);
self.record_cursor_col(frame);
}
fn move_cursor_up(&mut self, frame: &mut Frame) {
let boundaries = self.line_boundaries();
let (line, _, _) = self.cursor_line(&boundaries);
if line > 0 {
self.move_cursor_to_line_col(frame, line - 1, self.col);
}
}
fn move_cursor_down(&mut self, frame: &mut Frame) {
let boundaries = self.line_boundaries();
// There's always at least one line, and always at least two line
// boundaries at 0 and self.text.len().
let amount_of_lines = boundaries.len() - 1;
let (line, _, _) = self.cursor_line(&boundaries);
if line + 1 < amount_of_lines {
self.move_cursor_to_line_col(frame, line + 1, self.col);
}
}
}
pub struct EditorState(Arc<Mutex<InnerEditorState>>);
impl EditorState {
pub fn new() -> Self {
Self(Arc::new(Mutex::new(InnerEditorState::new(String::new()))))
}
pub fn with_initial_text(text: String) -> Self {
Self(Arc::new(Mutex::new(InnerEditorState::new(text))))
}
pub fn widget(&self) -> Editor {
let guard = self.0.lock();
let text = Styled::new_plain(guard.text.clone());
let idx = guard.idx;
Editor {
state: self.0.clone(),
text,
idx,
focus: true,
hidden: None,
}
}
pub fn text(&self) -> String {
self.0.lock().text.clone()
}
pub fn clear(&self) {
self.0.lock().clear();
}
pub fn set_text(&self, frame: &mut Frame, text: String) {
self.0.lock().set_text(frame, text);
}
pub fn insert_char(&self, frame: &mut Frame, ch: char) {
self.0.lock().insert_char(frame, ch);
}
pub fn insert_str(&self, frame: &mut Frame, str: &str) {
self.0.lock().insert_str(frame, str);
}
/// Delete the grapheme before the cursor position.
pub fn backspace(&self, frame: &mut Frame) {
self.0.lock().backspace(frame);
}
/// Delete the grapheme after the cursor position.
pub fn delete(&self) {
self.0.lock().delete();
}
pub fn move_cursor_left(&self, frame: &mut Frame) {
self.0.lock().move_cursor_left(frame);
}
pub fn move_cursor_right(&self, frame: &mut Frame) {
self.0.lock().move_cursor_right(frame);
}
pub fn move_cursor_left_a_word(&self, frame: &mut Frame) {
self.0.lock().move_cursor_left_a_word(frame);
}
pub fn move_cursor_right_a_word(&self, frame: &mut Frame) {
self.0.lock().move_cursor_right_a_word(frame);
}
pub fn move_cursor_to_start_of_line(&self, frame: &mut Frame) {
self.0.lock().move_cursor_to_start_of_line(frame);
}
pub fn move_cursor_to_end_of_line(&self, frame: &mut Frame) {
self.0.lock().move_cursor_to_end_of_line(frame);
}
pub fn move_cursor_up(&self, frame: &mut Frame) {
self.0.lock().move_cursor_up(frame);
}
pub fn move_cursor_down(&self, frame: &mut Frame) {
self.0.lock().move_cursor_down(frame);
}
pub fn edit_externally(&self, terminal: &mut Terminal, crossterm_lock: &Arc<FairMutex<()>>) {
let mut guard = self.0.lock();
if let Some(text) = util::prompt(terminal, crossterm_lock, &guard.text) {
if let Some(text) = text.strip_suffix('\n') {
guard.set_text(terminal.frame(), text.to_string());
} else {
guard.set_text(terminal.frame(), text);
}
}
}
}
////////////
// Widget //
////////////
pub struct Editor {
state: Arc<Mutex<InnerEditorState>>,
text: Styled,
idx: usize,
focus: bool,
hidden: Option<Box<Text>>,
}
impl Editor {
pub fn highlight<F>(mut self, f: F) -> Self
where
F: FnOnce(&str) -> Styled,
{
let new_text = f(self.text.text());
assert_eq!(self.text.text(), new_text.text());
self.text = new_text;
self
}
pub fn focus(mut self, active: bool) -> Self {
self.focus = active;
self
}
pub fn hidden(self) -> Self {
self.hidden_with_placeholder(("<hidden>", ContentStyle::default().grey().italic()))
}
pub fn hidden_with_placeholder<S: Into<Styled>>(mut self, placeholder: S) -> Self {
self.hidden = Some(Box::new(Text::new(placeholder)));
self
}
fn wrapped_cursor(cursor_idx: usize, break_indices: &[usize]) -> (usize, usize) {
let mut row = 0;
let mut line_idx = cursor_idx;
for break_idx in break_indices {
if cursor_idx < *break_idx {
break;
} else {
row += 1;
line_idx = cursor_idx - break_idx;
}
}
(row, line_idx)
}
pub fn cursor_row(&self, frame: &mut Frame) -> usize {
let width = self.state.lock().last_width;
let text_width = (width - 1) as usize;
let indices = wrap(frame, self.text.text(), text_width);
let (row, _) = Self::wrapped_cursor(self.idx, &indices);
row
}
}
#[async_trait]
impl Widget for Editor {
fn size(&self, frame: &mut Frame, max_width: Option<u16>, max_height: Option<u16>) -> Size {
if let Some(placeholder) = &self.hidden {
let mut size = if self.text.text().is_empty() {
Size::new(1, 1)
} else {
placeholder.size(frame, max_width, max_height)
};
// Cursor needs to fit regardless of focus
size.width = size.width.max(1);
size.height = size.height.max(1);
return size;
}
let max_width = max_width.map(|w| w as usize).unwrap_or(usize::MAX).max(1);
let max_text_width = max_width - 1;
let indices = wrap(frame, self.text.text(), max_text_width);
let lines = self.text.clone().split_at_indices(&indices);
let min_width = lines
.iter()
.map(|l| frame.width(l.text().trim_end()))
.max()
.unwrap_or(0)
+ 1;
let min_height = lines.len();
Size::new(min_width as u16, min_height as u16)
}
async fn render(self: Box<Self>, frame: &mut Frame) {
if let Some(placeholder) = self.hidden {
if !self.text.text().is_empty() {
placeholder.render(frame).await;
}
if self.focus {
frame.set_cursor(Some(Pos::ZERO));
}
return;
}
let width = frame.size().width.max(1);
let text_width = (width - 1) as usize;
let indices = wrap(frame, self.text.text(), text_width);
let lines = self.text.split_at_indices(&indices);
if self.focus {
let (cursor_row, cursor_line_idx) = Self::wrapped_cursor(self.idx, &indices);
let cursor_col = frame.width(lines[cursor_row].text().split_at(cursor_line_idx).0);
let cursor_col = cursor_col.min(text_width);
frame.set_cursor(Some(Pos::new(cursor_col as i32, cursor_row as i32)));
}
for (i, line) in lines.into_iter().enumerate() {
frame.write(Pos::new(0, i as i32), line);
}
self.state.lock().last_width = width;
}
}