summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/encoding.rs43
-rw-r--r--src/main.rs177
-rw-r--r--src/terminal.rs30
3 files changed, 199 insertions, 51 deletions
diff --git a/src/encoding.rs b/src/encoding.rs
index 08ffb39..ccd2031 100644
--- a/src/encoding.rs
+++ b/src/encoding.rs
@@ -2,7 +2,13 @@
 use crate::types::*;
 use smol::prelude::*;
 
-use smol::io::BoxedReader;
+
+#[derive(Debug)]
+pub struct Decode {
+  pub c: char,
+  pub skipped_bytes: usize,
+  pub found_bytes: usize,
+}
 
 
 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -31,9 +37,12 @@ pub fn get_utf8_byte_type(b: u8) -> UTF8ByteType {
 }
 
 
-pub async fn read_utf8_char(input: &mut BoxedReader) -> Result<char> {
+pub async fn read_utf8_char(input: &mut (impl AsyncRead + Unpin))
+    -> Result<Decode>
+{
   let mut buf = vec![0; 4];
   let mut unread_byte: Option<u8> = None;
+  let mut skipped_bytes = 0;
 
   loop {
     if let Some(byte) = unread_byte {
@@ -43,56 +52,76 @@ pub async fn read_utf8_char(input: &mut BoxedReader) -> Result<char> {
       input.read_exact(&mut buf[0 .. 1]).await?;
     }
 
-    match get_utf8_byte_type(buf[0]) {
-      UTF8ByteType::Single => { },
+    let found_bytes = match get_utf8_byte_type(buf[0]) {
+      UTF8ByteType::Single => {
+        1
+      },
+
       UTF8ByteType::Introducer(2) => {
         input.read_exact(&mut buf[1 .. 2]).await?;
         if get_utf8_byte_type(buf[1]) != UTF8ByteType::Continuation {
           unread_byte = Some(buf[1]);
+          skipped_bytes += 1;
           continue;
         }
+
+        2
       },
+
       UTF8ByteType::Introducer(3) => {
         input.read_exact(&mut buf[1 .. 2]).await?;
         if get_utf8_byte_type(buf[1]) != UTF8ByteType::Continuation {
           unread_byte = Some(buf[1]);
+          skipped_bytes += 1;
           continue;
         }
 
         input.read_exact(&mut buf[2 .. 3]).await?;
         if get_utf8_byte_type(buf[2]) != UTF8ByteType::Continuation {
           unread_byte = Some(buf[2]);
+          skipped_bytes += 2;
           continue;
         }
+
+        3
       },
+
       UTF8ByteType::Introducer(4) => {
         input.read_exact(&mut buf[1 .. 2]).await?;
         if get_utf8_byte_type(buf[1]) != UTF8ByteType::Continuation {
           unread_byte = Some(buf[1]);
+          skipped_bytes += 1;
           continue;
         }
 
         input.read_exact(&mut buf[2 .. 3]).await?;
         if get_utf8_byte_type(buf[2]) != UTF8ByteType::Continuation {
           unread_byte = Some(buf[2]);
+          skipped_bytes += 2;
           continue;
         }
 
         input.read_exact(&mut buf[3 .. 4]).await?;
         if get_utf8_byte_type(buf[3]) != UTF8ByteType::Continuation {
           unread_byte = Some(buf[3]);
+          skipped_bytes += 3;
           continue;
         }
+
+        4
       },
 
       /* If it's not the start of a valid character, ignore it. */
-      _ => continue,
-    }
+      _ => {
+        skipped_bytes += 1;
+        continue;
+      }
+    };
 
     if let Ok(string) = std::str::from_utf8(&buf)
        && let Some(c) = string.chars().next()
     {
-      return Ok(c);
+      return Ok(Decode { c, skipped_bytes, found_bytes });
     }
   }
 }
diff --git a/src/main.rs b/src/main.rs
index 9b2a2e7..59d3baa 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -3,6 +3,7 @@ use crate::types::*;
 use smol::prelude::*;
 
 use smol::fs::File;
+use smol::io::Cursor;
 use smol::lock::RwLock;
 use std::path::PathBuf;
 use std::process::ExitCode;
@@ -40,10 +41,16 @@ struct Window {
   /* The neutral column is the one that vertical movement will attempt to
    * land in, if it exists in the target row.
    */
-  cursor_neutral_column: RwLock<usize>,
+  neutral_column: RwLock<usize>,
   scroll_top: RwLock<usize>,
 }
 
+enum MovementColumnBehavior {
+  CurrentFromNeutral,
+  NeutralFromCurrent,
+  FirstNonBlank,
+}
+
 
 fn main() -> ExitCode {
   smol::block_on(async {
@@ -151,7 +158,7 @@ impl Ivy {
     let buffer = self.buffer.read().await;
     let scroll_top = *window.scroll_top.read().await;
 
-    terminal.do_cursor_position(0, range.start).await?;
+    terminal.set_cursor_position(0, range.start).await?;
 
     let mut screen_y = range.start;
     for i in range.clone() {
@@ -179,7 +186,7 @@ impl Ivy {
     let mut terminal = self.terminal.write().await;
     let height = *terminal.height.read().await;
 
-    terminal.do_cursor_position(0, height).await?;
+    terminal.set_cursor_position(0, height).await?;
     terminal.stdout.write_all("        status goes here".as_bytes()).await?;
 
     Ok(())
@@ -188,7 +195,7 @@ impl Ivy {
   async fn fix_cursor_position(&mut self) -> Result<()> {
     let mut terminal = self.terminal.write().await;
     let window = self.window.read().await;
-    terminal.do_cursor_position(*window.cursor_column.read().await,
+    terminal.set_cursor_position(*window.cursor_column.read().await,
                                 *window.cursor_row.read().await
                                 - *window.scroll_top.read().await).await?;
 
@@ -214,10 +221,10 @@ impl Ivy {
                * the movement actually moves. Tentatively, it doesn't look
                * like POSIX has an opinion on this, but it matches Vim.
                */
-              *window.cursor_neutral_column.write().await = *column;
+              Ok(MovementColumnBehavior::NeutralFromCurrent)
+            } else {
+              Ok(MovementColumnBehavior::CurrentFromNeutral)
             }
-
-            Ok(())
           }).await?;
         },
 
@@ -231,7 +238,7 @@ impl Ivy {
               *row += 1;
             };
 
-            Ok(())
+            Ok(MovementColumnBehavior::CurrentFromNeutral)
           }).await?;
         },
 
@@ -244,7 +251,7 @@ impl Ivy {
               *row -= 1;
             }
 
-            Ok(())
+            Ok(MovementColumnBehavior::CurrentFromNeutral)
           }).await?;
         },
 
@@ -265,11 +272,13 @@ impl Ivy {
                  * the movement actually moves. Tentatively, it doesn't look
                  * like POSIX has an opinion on this, but it matches Vim.
                  */
-                *window.cursor_neutral_column.write().await = *column;
+                Ok(MovementColumnBehavior::NeutralFromCurrent)
+              } else {
+                Ok(MovementColumnBehavior::CurrentFromNeutral)
               }
+            } else {
+              Ok(MovementColumnBehavior::CurrentFromNeutral)
             }
-
-            Ok(())
           }).await?;
         },
 
@@ -283,9 +292,7 @@ impl Ivy {
             /* For this one, the neutral column changes regardless of
              * whether the column does.
              */
-            *window.cursor_neutral_column.write().await = *column;
-
-            Ok(())
+            Ok(MovementColumnBehavior::NeutralFromCurrent)
           }).await?;
         },
 
@@ -304,14 +311,43 @@ impl Ivy {
               } else {
                 *column = 0;
               }
+            }
 
-              /* For this one, the neutral column changes regardless of
-               * whether the column does.
-               */
-              *window.cursor_neutral_column.write().await = *column;
+            /* For this one, the neutral column changes regardless of
+             * whether the column does.
+             */
+            Ok(MovementColumnBehavior::NeutralFromCurrent)
+          }).await?;
+        },
+
+        'H' => {
+          self.handle_movement(async |ivy: &mut Ivy| {
+            let window = ivy.window.write().await;
+
+            let mut row = window.cursor_row.write().await;
+            *row = *window.scroll_top.read().await;
+
+            Ok(MovementColumnBehavior::FirstNonBlank)
+          }).await?;
+        },
+
+        'L' => {
+          self.handle_movement(async |ivy: &mut Ivy| {
+            let window = ivy.window.write().await;
+            let terminal = ivy.terminal.read().await;
+            let buffer = ivy.buffer.read().await;
+
+            let scroll_top = *window.scroll_top.read().await;
+            let height = *terminal.height.read().await;
+            let total_lines = buffer.lines.read().await.len();
+
+            let mut row = window.cursor_row.write().await;
+            *row = scroll_top + height - 2;
+            if *row > total_lines - 1 {
+              *row = total_lines - 1;
             }
 
-            Ok(())
+            Ok(MovementColumnBehavior::FirstNonBlank)
           }).await?;
         },
 
@@ -325,9 +361,13 @@ impl Ivy {
   /* A movement is an arbitrarily complicated action that will not change the
    * contents of the buffer, but may change other things, including the cursor
    * position.
+   *
+   * The return value of the action indicates whether to update the neutral
+   * column.
    */
   async fn handle_movement(&mut self,
-                           movement: impl AsyncFn(&mut Ivy) -> Result<()>)
+                           movement: impl AsyncFn(&mut Ivy)
+                                         -> Result<MovementColumnBehavior>)
       -> Result<()>
   {
     let (old_row, old_column) = {
@@ -337,15 +377,28 @@ impl Ivy {
        *window.cursor_column.read().await)
     };
 
-    movement(self).await?;
-
-    /* We clamp the column to the line width unconditionally, regardless of
-     * what kind of movement we did. This is always correct, because no
-     * intended behavior ever places the cursor beyond the end of the line.
-     * It does require taking a couple of locks that we might always need, but
-     * there's no strong reason to avoid that.
-     */
-    self.clamp_cursor_column().await?;
+    let column_behavior = movement(self).await?;
+
+    match column_behavior {
+      MovementColumnBehavior::NeutralFromCurrent => {
+        /* We clamp the destination column to the line width and use that as
+         * the neutral column.
+         */
+        self.update_neutral_column().await?;
+      },
+      MovementColumnBehavior::CurrentFromNeutral => {
+        /* We clamp the neutral column to the line width and use that as the
+         * destination column.
+         */
+        self.clamp_cursor_column().await?;
+      },
+      MovementColumnBehavior::FirstNonBlank => {
+        /* We compute the first non-blank column on the line and use that as
+         * both the destination and neutral columns.
+         */
+        self.first_non_blank_column().await?;
+      },
+    }
 
     self.scroll_to_cursor().await?;
 
@@ -358,7 +411,7 @@ impl Ivy {
         let mut terminal = self.terminal.write().await;
         let scroll_top = *window.scroll_top.read().await;
 
-        terminal.do_cursor_position(column, row - scroll_top).await?;
+        terminal.set_cursor_position(column, row - scroll_top).await?;
         terminal.stdout.flush().await?;
       }
     }
@@ -366,6 +419,28 @@ impl Ivy {
     Ok(())
   }
 
+  async fn update_neutral_column(&mut self) -> Result<()> {
+    let buffer = self.buffer.write().await;
+    let window = self.window.write().await;
+    let mut column = window.cursor_column.write().await;
+    let mut neutral_column = window.neutral_column.write().await;
+    let row = window.cursor_row.read().await;
+
+    if let Some(span) = buffer.line_span(*row).await {
+      let width = span.end - span.start;
+
+      if width == 0 {
+        *column = 0;
+      } else if *column > width - 1 {
+        *column = width - 1;
+      }
+    }
+
+    *neutral_column = *column;
+
+    Ok(())
+  }
+
   async fn clamp_cursor_column(&mut self) -> Result<()> {
     let window = self.window.write().await;
     let row = window.cursor_row.read().await;
@@ -374,7 +449,7 @@ impl Ivy {
 
     if let Some(span) = buffer.line_span(*row).await {
       let width = span.end - span.start;
-      let neutral_column = *window.cursor_neutral_column.read().await;
+      let neutral_column = *window.neutral_column.read().await;
 
       if neutral_column < width {
         *column = neutral_column;
@@ -388,6 +463,42 @@ impl Ivy {
     Ok(())
   }
 
+  async fn first_non_blank_column(&mut self) -> Result<()> {
+    let row = *self.window.read().await.cursor_row.read().await;
+    let buffer = self.buffer.write().await;
+
+    if let Some(row_span) = buffer.line_span(row).await {
+      let mut offset = 0;
+      loop {
+        let sub_span = row_span.start + offset .. row_span.end;
+        let mut contents = buffer.contents.write().await;
+        let mut cursor = Cursor::new(&mut contents[sub_span]);
+
+        if let Ok(decode) = encoding::read_utf8_char(&mut cursor).await {
+          offset += decode.skipped_bytes;
+
+          if decode.c.is_whitespace() {
+            offset += decode.found_bytes;
+          } else {
+            break;
+          }
+        } else {
+          break;
+        }
+      }
+
+      let window = self.window.write().await;
+      *window.cursor_column.write().await = offset;
+      *window.neutral_column.write().await = offset;
+    } else {
+      let window = self.window.write().await;
+      *window.cursor_column.write().await = 0;
+      *window.neutral_column.write().await = 0;
+    }
+
+    Ok(())
+  }
+
   async fn scroll_to_cursor(&mut self) -> Result<()> {
     let old_scroll_top = *self.window.read().await.scroll_top.read().await;
 
@@ -518,7 +629,7 @@ impl Window {
     Window {
       cursor_row: RwLock::new(0),
       cursor_column: RwLock::new(0),
-      cursor_neutral_column: RwLock::new(0),
+      neutral_column: RwLock::new(0),
       scroll_top: RwLock::new(0),
     }
   }
diff --git a/src/terminal.rs b/src/terminal.rs
index 1ba2b01..c46a4a2 100644
--- a/src/terminal.rs
+++ b/src/terminal.rs
@@ -74,10 +74,16 @@ impl Terminal {
   }
 
   pub async fn init_full_screen(&mut self) -> Result<()> {
-    self.do_start_alternate_screen().await?;
-    self.do_cursor_position(0, 0).await?;
-
-    let (width, height) = self.do_report_size().await?;
+    /* Theoretically, xterm clears the alternate screen buffer when switching
+     * into it. Konsole does not necessarily do so, so we add an explicit
+     * clear. This is only noticeable when we fail to exit cleanly and return
+     * to the shell while still in the alternate screen buffer.
+     */
+    self.start_alternate_screen().await?;
+    self.clear().await?;
+    self.set_cursor_position(0, 0).await?;
+
+    let (width, height) = self.report_size().await?;
     *self.width.write().await = width.try_into().unwrap();
     *self.height.write().await = height.try_into().unwrap();
 
@@ -87,20 +93,22 @@ impl Terminal {
   }
 
   pub async fn zap_full_screen(&mut self) -> Result<()> {
-    let (width, height) = self.do_report_size().await?;
+    let (width, height) = self.report_size().await?;
     *self.width.write().await = width.try_into().unwrap();
     *self.height.write().await = height.try_into().unwrap();
 
     self.set_scroll_region(0, height).await?;
 
-    self.do_end_alternate_screen().await?;
+    self.end_alternate_screen().await?;
     self.stdout.flush().await?;
 
     Ok(())
   }
 
   pub async fn read_char(&mut self) -> Result<char> {
-    encoding::read_utf8_char(&mut self.stdin).await
+    let decode = encoding::read_utf8_char(&mut self.stdin).await?;
+
+    Ok(decode.c)
   }
 
   pub async fn do_escape(&mut self, escape_type: EscapeType, code: &str,
@@ -188,24 +196,24 @@ impl Terminal {
   }
 
   // xterm
-  pub async fn do_start_alternate_screen(&mut self) -> Result<()> {
+  pub async fn start_alternate_screen(&mut self) -> Result<()> {
     self.do_escape(EscapeType::CSIPrivate, "h", &[1049]).await
   }
 
   // xterm
-  pub async fn do_end_alternate_screen(&mut self) -> Result<()> {
+  pub async fn end_alternate_screen(&mut self) -> Result<()> {
     self.do_escape(EscapeType::CSIPrivate, "l", &[1049]).await
   }
 
   // vt220? vt100?
-  pub async fn do_cursor_position(&mut self, x: usize, y: usize)
+  pub async fn set_cursor_position(&mut self, x: usize, y: usize)
       -> Result<()>
   {
     self.do_escape(EscapeType::CSI, "H", &[y + 1, x + 1]).await
   }
 
   // dtterm? xterm
-  pub async fn do_report_size(&mut self) -> Result<(usize, usize)> {
+  pub async fn report_size(&mut self) -> Result<(usize, usize)> {
     self.do_escape(EscapeType::CSI, "t", &[18]).await?;
     self.stdout.flush().await?;
     let (code, values) = self.read_report(EscapeType::CSI).await?;