diff options
| -rw-r--r-- | src/encoding.rs | 43 | ||||
| -rw-r--r-- | src/main.rs | 226 | ||||
| -rw-r--r-- | src/terminal.rs | 30 |
3 files changed, 238 insertions, 61 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 b94b274..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 { @@ -133,19 +140,30 @@ impl Ivy { Ok(()) } - async fn draw(&mut self) -> Result<()> { - let mut terminal = self.terminal.write().await; - let buffer = self.buffer.read().await; + async fn draw_all(&mut self) -> Result<()> { + let height = *self.terminal.read().await.height.read().await; - let height = *terminal.height.read().await; + self.draw_range(0 .. height - 1).await?; + self.draw_status_line().await?; + self.fix_cursor_position().await?; + + self.terminal.write().await.stdout.flush().await?; + Ok(()) + } + + async fn draw_range(&mut self, range: Range<usize>) -> Result<()> { + let mut terminal = self.terminal.write().await; let window = self.window.read().await; + let buffer = self.buffer.read().await; let scroll_top = *window.scroll_top.read().await; - let mut screen_y = 0; - for i in 0 .. height - 1 { + terminal.set_cursor_position(0, range.start).await?; + + let mut screen_y = range.start; + for i in range.clone() { if let Some(span) = buffer.line_span(i + scroll_top).await { - if i > 0 { + if i > range.start { terminal.stdout.write_all("\n".as_bytes()).await?; } @@ -157,24 +175,35 @@ impl Ivy { } } - for _ in screen_y .. height - 1 { + for _ in screen_y .. range.end { terminal.stdout.write_all("\n~".as_bytes()).await?; } - terminal.do_cursor_position(0, height).await?; + Ok(()) + } + + async fn draw_status_line(&mut self) -> Result<()> { + let mut terminal = self.terminal.write().await; + let height = *terminal.height.read().await; + + terminal.set_cursor_position(0, height).await?; terminal.stdout.write_all(" status goes here".as_bytes()).await?; + Ok(()) + } + + 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?; - terminal.stdout.flush().await?; Ok(()) } async fn interact(&mut self) -> Result<()> { - self.draw().await?; + self.draw_all().await?; loop { let c = self.terminal.write().await.read_char().await?; @@ -192,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?; }, @@ -209,7 +238,7 @@ impl Ivy { *row += 1; }; - Ok(()) + Ok(MovementColumnBehavior::CurrentFromNeutral) }).await?; }, @@ -222,7 +251,7 @@ impl Ivy { *row -= 1; } - Ok(()) + Ok(MovementColumnBehavior::CurrentFromNeutral) }).await?; }, @@ -243,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?; }, @@ -261,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?; }, @@ -282,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?; }, @@ -303,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) = { @@ -315,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?; @@ -336,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?; } } @@ -344,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; @@ -352,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; @@ -366,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; @@ -391,9 +524,16 @@ impl Ivy { let difference = new_scroll_top as isize - old_scroll_top as isize; if (difference.abs() as usize) < height { self.terminal.write().await.scroll(difference).await?; + + if difference > 0 { + self.draw_range((height as isize - difference) as usize + .. height).await?; + } else { + self.draw_range(0 .. difference.abs() as usize).await?; + } } else { self.terminal.write().await.clear().await?; - self.draw().await?; + self.draw_all().await?; } } @@ -489,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?; |