#![forbid(unsafe_code)] use crate::types::*; use smol::prelude::*; use smol::fs::File; use smol::lock::RwLock; use std::path::PathBuf; use std::process::ExitCode; use std::ops::Range; mod encoding; mod terminal; mod types; struct Ivy { terminal: RwLock, buffer: RwLock, window: RwLock, argument_files: Vec, } struct Buffer { path: Option, contents: RwLock>, lines: RwLock>, has_end_newline: RwLock, } struct Window { /* The traditional order of writing "row, column" is the opposite of the * traditional order of writing "x, y"; be mindful. An alternate approach * would use "x" and "y" for all the names, which might arguably reduce * confusion, but personally we find the ordering concern to be * aesthetically pleasing, a quiet reminder of how much humans care about * everything. */ cursor_row: RwLock, cursor_column: RwLock, /* 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, scroll_top: RwLock, } fn main() -> ExitCode { smol::block_on(async { if let Err(e) = Ivy::new().await.run().await { println!("{:?}", e); ExitCode::FAILURE } else { ExitCode::SUCCESS } }) } impl Ivy { async fn new() -> Self { Ivy { terminal: RwLock::new(Terminal::new().await), buffer: RwLock::new(Buffer::new().await), window: RwLock::new(Window::new().await), argument_files: Vec::new(), } } async fn run(mut self) -> Result<()> { /* The awkward structure here is because it's important that cleanup still * happen even if something fails in the middle. */ let mut error = None; error = error.or(self.init().await.err()); error = error.or(self.interact().await.err()); error = error.or(self.zap().await.err()); if let Some(error) = error { Err(error) } else { Ok(()) } } async fn init(&mut self) -> Result<()> { self.init_arguments().await?; self.init_editor().await?; self.terminal.write().await.init_termios().await?; self.terminal.write().await.init_full_screen().await?; Ok(()) } async fn zap(&mut self) -> Result<()> { let mut error = None; error = error.or( self.terminal.write().await.zap_full_screen().await.err()); error = error.or( self.terminal.write().await.zap_termios().await.err()); if let Some(error) = error { Err(error) } else { Ok(()) } } async fn init_arguments(&mut self) -> Result<()> { self.argument_files.clear(); let mut args = std::env::args(); let _ = args.next(); for argument in args { self.argument_files.push(argument.into()); } Ok(()) } async fn init_editor(&mut self) -> Result<()> { if !self.argument_files.is_empty() { let path = self.argument_files.remove(0); let new_buffer = Buffer::from_file(path).await?; *self.buffer.write().await = new_buffer; } Ok(()) } async fn draw(&mut self) -> Result<()> { let mut terminal = self.terminal.write().await; let buffer = self.buffer.read().await; let height = *terminal.height.read().await; let window = self.window.read().await; let scroll_top = *window.scroll_top.read().await; let mut screen_y = 0; for i in 0 .. height - 1 { if let Some(span) = buffer.line_span(i + scroll_top).await { if i > 0 { terminal.stdout.write_all("\n".as_bytes()).await?; } terminal.stdout.write_all(&buffer.contents.read().await[span]).await?; screen_y += 1; } else { break; } } for _ in screen_y .. height - 1 { terminal.stdout.write_all("\n~".as_bytes()).await?; } terminal.stdout.write_all("\n status goes here".as_bytes()).await?; let window = self.window.read().await; terminal.do_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?; loop { let c = self.terminal.write().await.read_char().await?; match c { 'h' => { self.handle_movement(async |ivy: &mut Ivy| { let window = ivy.window.write().await; let mut column = window.cursor_column.write().await; if *column > 0 { *column -= 1; /* The neutral column only changes if the horizontal part of * 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(()) }).await?; }, 'j' => { self.handle_movement(async |ivy: &mut Ivy| { let buffer = ivy.buffer.read().await; let window = ivy.window.write().await; let mut row = window.cursor_row.write().await; if *row + 1 < buffer.lines.read().await.len() { *row += 1; }; Ok(()) }).await?; }, 'k' => { self.handle_movement(async |ivy: &mut Ivy| { let window = ivy.window.write().await; let mut row = window.cursor_row.write().await; if *row > 0 { *row -= 1; } Ok(()) }).await?; }, 'l' => { self.handle_movement(async |ivy: &mut Ivy| { let buffer = ivy.buffer.read().await; let window = ivy.window.write().await; let row = *window.cursor_row.read().await; if let Some(span) = buffer.line_span(row).await { let width = span.end - span.start; let mut column = window.cursor_column.write().await; if *column + 1 < width { *column += 1; /* The neutral column only changes if the horizontal part of * 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(()) }).await?; }, '0' => { self.handle_movement(async |ivy: &mut Ivy| { let window = ivy.window.write().await; let mut column = window.cursor_column.write().await; *column = 0; /* For this one, the neutral column changes regardless of * whether the column does. */ *window.cursor_neutral_column.write().await = *column; Ok(()) }).await?; }, '$' => { self.handle_movement(async |ivy: &mut Ivy| { let buffer = ivy.buffer.read().await; let window = ivy.window.write().await; let row = *window.cursor_row.read().await; if let Some(span) = buffer.line_span(row).await { let width = span.end - span.start; let mut column = window.cursor_column.write().await; if width > 0 { *column = width - 1; } else { *column = 0; } /* For this one, the neutral column changes regardless of * whether the column does. */ *window.cursor_neutral_column.write().await = *column; } Ok(()) }).await?; }, _ => { return Ok(()); }, } } } /* 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. */ async fn handle_movement(&mut self, movement: impl AsyncFn(&mut Ivy) -> Result<()>) -> Result<()> { let (old_row, old_column) = { let window = self.window.read().await; (*window.cursor_row.read().await, *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?; self.scroll_to_cursor().await?; { let window = self.window.read().await; let row = *window.cursor_row.read().await; let column = *window.cursor_column.read().await; if old_row != row || old_column != column { 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.stdout.flush().await?; } } Ok(()) } async fn clamp_cursor_column(&mut self) -> Result<()> { let window = self.window.write().await; let row = window.cursor_row.read().await; let mut column = window.cursor_column.write().await; let buffer = self.buffer.read().await; if let Some(span) = buffer.line_span(*row).await { let width = span.end - span.start; let neutral_column = *window.cursor_neutral_column.read().await; if neutral_column < width { *column = neutral_column; } else if width > 0 { *column = width - 1; } else { *column = 0; } } Ok(()) } async fn scroll_to_cursor(&mut self) -> Result<()> { let old_scroll_top = *self.window.read().await.scroll_top.read().await; { let terminal = self.terminal.read().await; let window = self.window.write().await; let mut scroll_top = window.scroll_top.write().await; let row = *window.cursor_row.read().await; let height = *terminal.height.read().await; let last_visible = *scroll_top + height - 2; if row < *scroll_top { *scroll_top -= *scroll_top - row; } else if row > last_visible { *scroll_top += row - last_visible; } } let new_scroll_top = *self.window.read().await.scroll_top.read().await; if old_scroll_top != new_scroll_top { self.terminal.write().await.clear().await?; self.draw().await?; } Ok(()) } } impl Buffer { pub async fn new() -> Self { Buffer { path: None, contents: RwLock::new("\n".to_string().into_bytes()), lines: RwLock::new(vec![0]), has_end_newline: RwLock::new(true), } } pub async fn from_file(path: PathBuf) -> Result { let mut file = File::open(&path).await?; let mut contents = Vec::new(); let _ = file.read_to_end(&mut contents).await?; let mut result = Buffer { path: Some(path), contents: RwLock::new(contents), lines: RwLock::new(vec![0]), has_end_newline: RwLock::new(true), }; result.count_lines().await; Ok(result) } pub async fn count_lines(&mut self) { let contents = self.contents.read().await; *self.lines.write().await = vec![0]; if contents.len() == 0 { *self.has_end_newline.write().await = false; } else { let mut offset = 0; while offset < contents.len() { let c = contents[offset]; offset += 1; if c == b'\n' { if offset < contents.len() { self.lines.write().await.push(offset); } else { *self.has_end_newline.write().await = true; } } else if offset == contents.len() { *self.has_end_newline.write().await = false; } } } } pub async fn line_span(&self, index: usize) -> Option> { let lines = self.lines.read().await; if index < lines.len() { let start = lines[index]; if index + 1 < lines.len() { let end = lines[index + 1] - 1; Some(start .. end) } else { let end = if *self.has_end_newline.read().await { self.contents.read().await.len() - 1 } else { self.contents.read().await.len() }; Some(start .. end) } } else { None } } } impl Window { pub async fn new() -> Self { Window { cursor_row: RwLock::new(0), cursor_column: RwLock::new(0), cursor_neutral_column: RwLock::new(0), scroll_top: RwLock::new(0), } } }