chiark / gitweb /
Editor: implement Up and Down keystrokes.
authorSimon Tatham <anakin@pobox.com>
Tue, 2 Jan 2024 14:11:47 +0000 (14:11 +0000)
committerSimon Tatham <anakin@pobox.com>
Tue, 2 Jan 2024 17:30:18 +0000 (17:30 +0000)
src/editor.rs

index 5ed64cb8cfb26228711023a53be57bfa7b1976d4..a178af3e3f17af6dbcad41e921e70d5304a15261 100644 (file)
@@ -723,6 +723,7 @@ struct Composer {
     conf: InstanceStatusConfig,
     last_size: Option<(usize, usize)>,
     keystate: ComposerKeyState,
+    goal_column: Option<usize>,
     header: FileHeader,
     headersep: EditorHeaderSeparator,
 }
@@ -744,6 +745,7 @@ impl Composer {
             conf,
             last_size: None,
             keystate: ComposerKeyState::Start,
+            goal_column: None,
             header,
             headersep: EditorHeaderSeparator::new(),
         }
@@ -985,6 +987,49 @@ impl Composer {
         }
         self.cursor_pos = self.determine_cursor_pos();
     }
+
+    fn goto_xy(&mut self, x: usize, y: usize) -> bool {
+        // If we can't hit the desired x,y position exactly because
+        // it's in the middle of a wide character, I think it's more
+        // desirable to land on the wide character than to its right.
+        //
+        // This means we can't just search for the first layout cell
+        // that is >= (x,y), because that _is_ the one to its right.
+        // Instead we must search for the last one that is <= (x,y).
+
+        let cell_index_after = self.layout
+            .partition_point(|cell| (cell.y, cell.x) <= (y, x));
+        let cell_index = cell_index_after.checked_sub(1)
+            .expect("Cell 0 should be at (0,0) and always count as <= (y,x)");
+        if let Some(cell) = self.layout.get(cell_index) {
+            self.core.point = cell.pos;
+            // Report failure if we didn't even get the right _line_
+            cell.y == y
+        } else {
+            // overshoot to end of text
+            self.core.point = self.core.text.len();
+            false
+        }
+    }
+
+    fn next_line(&mut self) {
+        let (x, y) = self.cursor_pos.expect("post_update should have run");
+        let x = self.goal_column.unwrap_or(x);
+        if !self.goto_xy(x, y+1) {
+            self.goal_column = None;
+        }
+    }
+
+    fn prev_line(&mut self) {
+        let (x, y) = self.cursor_pos.expect("post_update should have run");
+        let x = self.goal_column.unwrap_or(x);
+        if let Some(newy) = y.checked_sub(1) {
+            self.goto_xy(x, newy);
+        } else {
+            self.core.beginning_of_buffer();
+            self.goal_column = None;
+        }
+    }
 }
 
 #[test]
@@ -1215,22 +1260,44 @@ impl ActivityState for Composer {
     {
         use ComposerKeyState::*;
 
+        // Start by identifying whether a keystroke is an up/down one,
+        // so that we can consistently clear the goal column for all
+        // keystrokes that are not
         match (self.keystate, key) {
+            (Start, Ctrl('N')) | (Start, Down) |
+            (Start, Ctrl('P')) | (Start, Up) => {
+                if !self.goal_column.is_some() {
+                    self.goal_column = self.cursor_pos.map(|(x, _y)| x);
+                }
+            }
+            _ => self.goal_column = None,
+        }
+
+        match (self.keystate, key) {
+            (Start, Ctrl('N')) | (Start, Down) => self.next_line(),
+            (Start, Ctrl('P')) | (Start, Up) => self.prev_line(),
+
+            (Start, Return) => {
+                self.core.insert("\n");
+                self.post_update();
+            }
+
+            // ^O is a prefix key that is followed by various less
+            // common keystrokes
             (Start, Ctrl('O')) => {
                 self.keystate = CtrlO;
+                return LogicalAction::Nothing; // no need to post_update
             }
-            (CtrlO, Pr('q')) | (CtrlO, Pr('Q')) => {
-                return LogicalAction::Pop;
-            }
+            (CtrlO, Pr('q')) | (CtrlO, Pr('Q')) => return LogicalAction::Pop,
             (CtrlO, _) => {
                 self.keystate = Start;
                 return LogicalAction::Beep;
             }
-            (Start, _) => {
-                self.core.handle_keypress(key);
-                self.post_update();
-            }
+
+            // Anything else goes to the EditorCore
+            (Start, _) => self.core.handle_keypress(key),
         }
+        self.post_update();
         LogicalAction::Nothing
     }
 }