From 5b07984c6b36ccacae196ab772b6e04cafd3586a Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Sun, 31 Dec 2023 19:25:44 +0000 Subject: [PATCH] Start of text editing: the EditorCore type. This is the common piece between bottom-line text entry and the full-screen post composer, handling all the editing keys that work the same in both. --- src/editor.rs | 228 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 2 files changed, 229 insertions(+) create mode 100644 src/editor.rs diff --git a/src/editor.rs b/src/editor.rs new file mode 100644 index 0000000..4be69e1 --- /dev/null +++ b/src/editor.rs @@ -0,0 +1,228 @@ +use unicode_width::UnicodeWidthChar; + +use super::tui::{OurKey, OurKey::*}; + +struct EditorCore { + text: String, + paste_buffer: String, + point: usize, +} + +impl EditorCore { + fn is_char_boundary(&self, pos: usize) -> bool { + if !self.text.is_char_boundary(pos) { + false + } else { + match self.text[pos..].chars().next() { + None => true, // end of string + + // not just before a combining character + Some(c) => UnicodeWidthChar::width(c).unwrap_or(0) > 0, + } + } + } + + fn is_word_boundary(&self, pos: usize) -> bool { + if !self.is_char_boundary(pos) { + false + } else { + match self.text[pos..].chars().next() { + None => true, // end of string + + // not just before a space + Some(c) => c != ' ' + } + } + } + + fn forward(&mut self) -> bool { + let len = self.text.len(); + if self.point >= len { + false + } else { + self.point += 1; + while !self.is_char_boundary(self.point) { + self.point += 1; + } + true + } + } + + fn backward(&mut self) -> bool { + if self.point == 0 { + false + } else { + self.point -= 1; + while !self.is_char_boundary(self.point) { + self.point -= 1; + } + true + } + } + + fn delete_backward(&mut self) { + let prev_point = self.point; + if self.backward() { + self.text = self.text[..self.point].to_owned() + + &self.text[prev_point..]; + } + } + + fn delete_forward(&mut self) { + let prev_point = self.point; + if self.forward() { + self.text = self.text[..prev_point].to_owned() + + &self.text[self.point..]; + self.point = prev_point; + } + } + + fn forward_word(&mut self) -> bool { + let len = self.text.len(); + if self.point >= len { + false + } else { + self.point += 1; + while !self.is_word_boundary(self.point) { + self.point += 1; + } + true + } + } + + fn backward_word(&mut self) -> bool { + if self.point == 0 { + false + } else { + self.point -= 1; + while !self.is_word_boundary(self.point) { + self.point -= 1; + } + true + } + } + + fn insert(&mut self, text: &str) { + self.text = self.text[..self.point].to_owned() + text + + &self.text[self.point..]; + self.point += text.len(); + } + + fn paste(&mut self) { + self.text = self.text[..self.point].to_owned() + &self.paste_buffer + + &self.text[self.point..]; + self.point += self.paste_buffer.len(); + } + + fn handle_keypress(&mut self, key: OurKey) { + match key { + Left | Ctrl('B') => { self.backward(); }, + Right | Ctrl('F') => { self.forward(); }, + Backspace => { self.delete_backward(); }, + Del | Ctrl('D') => { self.delete_forward(); }, + Ctrl('W') => { self.backward_word(); }, + Ctrl('T') => { self.forward_word(); }, + Ctrl('Y') => { self.paste(); }, + Pr(c) => { self.insert(&c.to_string()); }, + _ => (), + } + } +} + +#[test] +fn test_forward_backward() { + let mut ec = EditorCore { + text: "héllo, wørld".to_owned(), + point: 0, + paste_buffer: "".to_owned(), + }; + + assert_eq!(ec.forward(), true); + assert_eq!(ec.point, 1); + assert_eq!(ec.forward(), true); + assert_eq!(ec.point, 3); + assert_eq!(ec.forward(), true); + assert_eq!(ec.point, 4); + assert_eq!(ec.backward(), true); + assert_eq!(ec.point, 3); + assert_eq!(ec.backward(), true); + assert_eq!(ec.point, 1); + assert_eq!(ec.backward(), true); + assert_eq!(ec.point, 0); + assert_eq!(ec.backward(), false); + + ec.point = ec.text.len() - 2; + assert_eq!(ec.forward(), true); + assert_eq!(ec.point, ec.text.len() - 1); + assert_eq!(ec.forward(), true); + assert_eq!(ec.point, ec.text.len()); + assert_eq!(ec.forward(), false); + assert_eq!(ec.point, ec.text.len()); + assert_eq!(ec.backward(), true); + assert_eq!(ec.point, ec.text.len() - 1); + assert_eq!(ec.backward(), true); + assert_eq!(ec.point, ec.text.len() - 2); + assert_eq!(ec.backward(), true); + assert_eq!(ec.point, ec.text.len() - 3); + assert_eq!(ec.backward(), true); + assert_eq!(ec.point, ec.text.len() - 5); + assert_eq!(ec.backward(), true); +} + +#[test] +fn test_delete() { + let mut ec = EditorCore { + text: "hélło".to_owned(), + point: 3, + paste_buffer: "".to_owned(), + }; + + ec.delete_forward(); + assert_eq!(&ec.text, "héło"); + assert_eq!(ec.point, 3); + ec.delete_forward(); + assert_eq!(&ec.text, "héo"); + assert_eq!(ec.point, 3); + ec.delete_backward(); + assert_eq!(&ec.text, "ho"); + assert_eq!(ec.point, 1); + ec.delete_backward(); + assert_eq!(&ec.text, "o"); + assert_eq!(ec.point, 0); + ec.delete_backward(); + assert_eq!(&ec.text, "o"); + assert_eq!(ec.point, 0); + ec.delete_forward(); + assert_eq!(&ec.text, ""); + assert_eq!(ec.point, 0); + ec.delete_forward(); + assert_eq!(&ec.text, ""); + assert_eq!(ec.point, 0); +} + +#[test] +fn test_insert() { + let mut ec = EditorCore { + text: "hélło".to_owned(), + point: 3, + paste_buffer: "".to_owned(), + }; + + ec.insert("PÏNG"); + assert_eq!(&ec.text, "héPÏNGlło"); + assert_eq!(ec.point, 8); + + // We don't let you move _to_ a position before a combining char, + // but you can insert one in a way that makes your previous position + // no longer valid + ec.text = "Beszel".to_string(); + ec.point = 4; + ec.insert("\u{301}"); + assert_eq!(&ec.text, "Besźel"); + assert_eq!(ec.point, 6); + + ec.paste_buffer = "PASTE".to_owned(); + ec.paste(); + assert_eq!(&ec.text, "BesźPASTEel"); + assert_eq!(ec.point, 11); +} diff --git a/src/lib.rs b/src/lib.rs index 9acfb73..2e7d73d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,3 +10,4 @@ pub mod activity_stack; pub mod tui; pub mod menu; pub mod file; +pub mod editor; -- 2.30.2