chiark / gitweb /
Start of text editing: the EditorCore type.
authorSimon Tatham <anakin@pobox.com>
Sun, 31 Dec 2023 19:25:44 +0000 (19:25 +0000)
committerSimon Tatham <anakin@pobox.com>
Sun, 31 Dec 2023 19:25:44 +0000 (19:25 +0000)
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 [new file with mode: 0644]
src/lib.rs

diff --git a/src/editor.rs b/src/editor.rs
new file mode 100644 (file)
index 0000000..4be69e1
--- /dev/null
@@ -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);
+}
index 9acfb73d6fefa0443014c9ff5385093fd9cf0731..2e7d73d690c9b1370082b3bc7cf3e2e0c6a9cc83 100644 (file)
@@ -10,3 +10,4 @@ pub mod activity_stack;
 pub mod tui;
 pub mod menu;
 pub mod file;
+pub mod editor;