mod rope; mod term; use rope::Rope; use std::error::Error; use std::ffi::{OsStr, OsString}; use std::io::{stdin, stdout, Read, Write}; use std::path::Path; use term::Key; #[derive(Debug, Clone, Copy)] enum Mode { Insert, Backspace, Normal, } #[derive(Debug, Clone)] struct State { buf: Rope, mode: Mode, row_start: usize, cursor_row: usize, cursor_col: usize, line_offset: usize, line_cols: usize, prev: Option>, } fn parse_status_report(buf: &[u8]) -> Result<(usize, usize), Box> { let mut semicolon = 0; for (i, &c) in buf.iter().enumerate() { if c == b';' { semicolon = i; break; } } if semicolon < 2 || semicolon == buf.len() - 1 { return Err(Box::from("invalid response")); } let row_str = std::str::from_utf8(&buf[2..semicolon])?; let col_str = std::str::from_utf8(&buf[semicolon + 1..buf.len() - 1])?; let row = row_str.parse::()?; let col = col_str.parse::()?; // Yuck... 1 indexing return Ok((row - 1, col - 1)); } impl State { fn cursor_pos(&mut self) -> Result<(usize, usize), Box> { write!(stdout(), "\x1b[6n") .map_err(|err| format!("cursor position: status report: {}", err))?; stdout() .flush() .map_err(|err| format!("cursor position: status report: {}", err))?; let mut buf = vec![0; 20]; let n = stdin() .read(&mut buf) .map_err(|err| format!("cursor position: read status report: {}", err))?; buf.truncate(n); let pos = parse_status_report(&buf).map_err(|_| { format!( "cursor position: invalid response: {}", String::from_utf8_lossy(&buf) ) })?; return Ok(pos); } fn repaint_all(&mut self) -> Result<(), Box> { let size = term::size()?; let mut stdout = stdout().lock(); write!(stdout, "\x1b[H\x1b[J")?; for i in self.row_start..self.row_start + usize::from(size.ws_row) { if i > self.row_start { write!(stdout, "\r\n")?; } if i > self.buf.lines() { break; } self.buf.line(i).print(&mut stdout)?; let (_, col) = self.cursor_pos()?; if col == usize::from(size.ws_col - 1) { write!(stdout, "\x1b[{}G\x1b[30m\x1b[47m>\x1b[m ", size.ws_col - 1)?; } } return Ok(()); } fn repaint_line_full(&mut self) -> Result<(), Box> { let term_size = term::size()?; let mut stdout = stdout().lock(); write!(stdout, "\x1b[{}H\x1b[K", self.cursor_row + 1)?; let line = self.buf.line(self.row_start + self.cursor_row); line.print(&mut stdout)?; let (_, col) = self.cursor_pos()?; self.line_cols = col; let mut truncated = false; if col == usize::from(term_size.ws_col - 1) { truncated = true; self.line_cols = col - 1; } if self.cursor_col >= self.line_cols { self.line_offset = line.len(); if truncated { write!( stdout, "\x1b[{}G\x1b[30m\x1b[47m>\x1b[m \x1b[{}G", term_size.ws_col - 1, self.line_cols + 1, )?; } return Ok(()); } let mut lo = 0; let mut hi = line.len(); // Binary search :D while hi > lo { let x = line.floor_char_boundary(lo + (hi - lo) / 2); write!(stdout, "\x1b[G")?; line.slice(0, x).print(&mut stdout)?; let (_, col) = self.cursor_pos()?; if col <= self.cursor_col { lo = line.ceil_char_boundary(x + 1); } else { hi = x; } } self.line_offset = line.floor_char_boundary(lo - 1); write!(stdout, "\x1b[G")?; line.print(&mut stdout)?; if truncated { write!( stdout, "\x1b[{}G\x1b[30m\x1b[47m>\x1b[m ", term_size.ws_col - 1 )?; } write!(stdout, "\x1b[{}G", self.cursor_col + 1)?; return Ok(()); } fn up(&mut self) -> Result<(), Box> { if self.cursor_row == 0 && self.row_start == 0 { return Ok(()); } if self.cursor_row == 0 { self.row_start -= 1; self.repaint_all()?; } else { self.cursor_row -= 1; } self.repaint_line_full()?; return Ok(()); } fn down(&mut self) -> Result<(), Box> { if self.row_start + self.cursor_row == self.buf.lines() { return Ok(()); } let size = term::size()?; if self.cursor_row == usize::from(size.ws_row - 1) { self.row_start += 1; self.repaint_all()?; } else { self.cursor_row += 1; } self.repaint_line_full()?; return Ok(()); } fn left(&mut self) -> Result<(), Box> { if self.cursor_col > self.line_cols { self.cursor_col = self.line_cols; } if self.cursor_col == 0 && self.cursor_row == 0 && self.row_start == 0 { return Ok(()); } if self.cursor_col == 0 { self.up()?; self.cursor_col = self.line_cols; } else { self.cursor_col -= 1; } self.repaint_line_full()?; return Ok(()); } fn right(&mut self) -> Result<(), Box> { if self.cursor_col >= self.line_cols && self.row_start + self.cursor_row == self.buf.lines() { return Ok(()); } if self.cursor_col >= self.line_cols { self.down()?; self.cursor_col = 0; } else { self.cursor_col += 1; } self.repaint_line_full()?; return Ok(()); } fn set_mode(&mut self, mode: Mode) { match (self.mode, mode) { (Mode::Insert, Mode::Insert) => (), (Mode::Insert, _) => { if let Ok((_, col)) = self.cursor_pos() { self.cursor_col = col; } let _ = self.repaint_line_full(); } (Mode::Normal, Mode::Normal) => (), (Mode::Normal, _) => { let prev = self.prev.take(); let mut curr = Box::new(self.clone()); curr.prev = prev; self.prev = Some(curr); } _ => (), } self.mode = mode; } } fn edit(file: &OsStr) -> Result<(), Box> { let _raw_handle = term::raw().map_err(|err| format!("cannot put terminal in raw mode: {}", err))?; let r = Rope::open(Path::new(file))?; let size = term::size()?; let mut state = State { buf: r, mode: Mode::Normal, row_start: 0, cursor_row: 0, cursor_col: 0, line_offset: 0, line_cols: 0, prev: None, }; let _ = state.repaint_all(); let _ = write!(stdout(), "\x1b[H"); let _ = stdout().flush(); loop { let c = term::read_key().map_err(|err| format!("read key: {}", err))?; match c { Key::CtrlQ => { break; } Key::CtrlS => { state.set_mode(Mode::Normal); let _ = state.buf.save(Path::new(file)); } Key::CtrlZ => { state.set_mode(Mode::Normal); let Some(prev) = state.prev else { continue; }; state = *prev; let _ = state.repaint_all(); let _ = state.repaint_line_full(); let _ = stdout().flush(); } Key::Up => { state.set_mode(Mode::Normal); let _ = state.up(); let _ = stdout().flush(); } Key::Down => { state.set_mode(Mode::Normal); let _ = state.down(); let _ = stdout().flush(); } Key::Left => { state.set_mode(Mode::Normal); let _ = state.left(); let _ = stdout().flush(); } Key::Right => { state.set_mode(Mode::Normal); let _ = state.right(); let _ = stdout().flush(); } Key::Backspace => { state.set_mode(Mode::Backspace); if state.row_start == 0 && state.cursor_row == 0 && state.line_offset == 0 { continue; } let line_start = state.buf.line_idx(state.row_start + state.cursor_row); let gap_end = line_start + state.line_offset; let gap_start = state.buf.floor_char_boundary(gap_end - 1); if state.line_offset == 0 { let _ = state.left(); } else { let _ = write!(stdout(), "\x1b[G"); let _ = state.buf.slice(line_start, gap_start).print(&mut stdout()); let (_, col) = state.cursor_pos()?; state.cursor_col = col; } state.buf = state .buf .slice(0, gap_start) .concat(&state.buf.slice(gap_end, state.buf.len())); let _ = state.repaint_all(); let _ = state.repaint_line_full(); let _ = stdout().flush(); } Key::Char(b'\r') => { state.set_mode(Mode::Insert); state.buf = state.buf.insert( state.buf.line_idx(state.row_start + state.cursor_row) + state.line_offset, b'\n', ); if state.cursor_row == usize::from(size.ws_row - 1) { state.row_start += 1; } else { state.cursor_row += 1; } state.cursor_col = 0; let _ = state.repaint_all(); let _ = state.repaint_line_full(); let _ = stdout().flush(); } Key::Char(c) => { state.set_mode(Mode::Insert); state.cursor_col = std::cmp::min(state.cursor_col, state.line_cols); state.buf = state.buf.insert( state.buf.line_idx(state.row_start + state.cursor_row) + state.line_offset, c, ); state.line_offset += 1; let _ = stdout().write_all(&vec![c]); let _ = stdout().flush(); } } } return Ok(()); } fn main() { let args: Vec = std::env::args_os().collect(); if args.len() != 2 { println!("Usage: edit "); std::process::exit(1); } if let Err(err) = edit(&args[1]) { println!("FAIL: {}", err); std::process::exit(1); } }