use edit::rope::Rope; use edit::term; use edit::term::Key; use edit::term::Reader; use std::error::Error; use std::ffi::{OsStr, OsString}; use std::fs::File; use std::io::{stderr, stdout, Write}; use std::path::{Path, PathBuf}; fn open(file: &Path) -> Result> { let mut f = File::open(file)?; let r = Rope::read(&mut f)?; Ok(r) } fn line_offset(stdin: &mut Reader, line: &Rope, col: u16) -> Result> { let mut n = 0; for i in 0.. { if i == line.len() { return Ok(i); } let c = line.byte(i); if !(c == b'\t' || (b' '..=b'~').contains(&c)) { break; } if n == col { return Ok(i); } if c == b'\t' { n += 8 - n % 8; } else { n += 1; } } // Binary search :D let mut lo = 0; let mut hi = line.len() + 1; while lo < hi { let h = line.floor_char_boundary(lo + (hi - lo) / 2); write!(stdout(), "\x1b[G")?; line.slice(0, h).print(&mut stdout())?; let (_, c) = stdin.cursor_pos()?; if c <= col { lo = h + 1; if lo < line.len() { lo = line.ceil_char_boundary(lo); } } else { hi = h; } } if lo == line.len() + 1 { return Ok(line.len()); } if lo == 0 { // You might think this case is impossible (I did), but terminals can do a lot of // weird things and I'm trying to make this robust. return Err(Box::from("incorrect cursor_pos")); } Ok(line.floor_char_boundary(lo - 1)) } fn floor_grapheme_cluster( stdin: &mut Reader, line: &Rope, offset: usize, ) -> Result> { if offset == 0 || offset == line.len() || line.byte(offset) < 128 { return Ok(offset); } let mut offset = line.floor_char_boundary(offset); write!(stdout(), "\x1b[G")?; line.slice(0, line.ceil_char_boundary(offset + 1)) .print(&mut stdout())?; let (_, target_col) = stdin.cursor_pos()?; while offset > 0 { write!(stdout(), "\x1b[G")?; line.slice(0, offset).print(&mut stdout())?; let (_, col) = stdin.cursor_pos()?; if col < target_col { return Ok(offset); } offset = line.floor_char_boundary(offset - 1); } Ok(0) } fn ceil_grapheme_cluster( stdin: &mut Reader, line: &Rope, offset: usize, ) -> Result> { let mut offset = line.ceil_char_boundary(offset); if offset == line.len() || line.byte(offset) < 128 { return Ok(offset); } write!(stdout(), "\x1b[G")?; line.slice(0, offset).print(&mut stdout())?; let (_, target_col) = stdin.cursor_pos()?; loop { if offset >= line.len() { return Ok(line.len()); } let old_offset = offset; offset = line.ceil_char_boundary(old_offset + 1); line.slice(old_offset, offset).print(&mut stdout())?; let (_, col) = stdin.cursor_pos()?; if col > target_col { return Ok(line.floor_char_boundary(offset - 1)); } } } struct Snapshot { buf: Rope, line_offset: usize, row_start: usize, cursor_row: u16, cursor_col: u16, } struct State { history: Vec, n: Option, buf: Rope, line_offset: usize, row_start: usize, cursor_row: u16, cursor_col: u16, stdin: Reader, path: PathBuf, inserting: bool, need_repaint_screen: bool, need_repaint_line: bool, } impl State { fn line(&self) -> Rope { self.buf.line(self.row_start + usize::from(self.cursor_row)) } fn update_cursor_col(&mut self) -> Result<(), Box> { let size = term::size()?; let line = self.line(); write!(stdout(), "\x1b[{}H", self.cursor_row + 1)?; line.slice(0, self.line_offset).print(&mut stdout())?; let (_, col) = self.stdin.cursor_pos()?; if col == size.ws_col - 1 { self.cursor_col = col - 1; write!( stdout(), "\x1b[{};{}H\x1b[K", self.cursor_row + 1, self.cursor_col + 1 )?; return Ok(()); } if self.line_offset == line.len() { if col > self.cursor_col { self.cursor_col = col; } write!(stdout(), "\x1b[K")?; return Ok(()); } self.cursor_col = col; line.slice(self.line_offset, line.len()) .print(&mut stdout())?; write!( stdout(), "\x1b[C\x1b[D\x1b[K\x1b[{};{}H", self.cursor_row + 1, self.cursor_col + 1 )?; Ok(()) } fn repaint_screen(&self) -> Result<(), Box> { let size = term::size()?; 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; } let line = self.buf.line(i); line.print(&mut stdout())?; write!(stdout(), "\x1b[C\x1b[D\x1b[K")?; } Ok(()) } fn snapshot(&self) -> Snapshot { Snapshot { buf: self.buf.clone(), line_offset: self.line_offset, row_start: self.row_start, cursor_row: self.cursor_row, cursor_col: self.cursor_col, } } fn load(&mut self) { let Some(n) = self.n else { return; }; let snapshot = &self.history[n]; self.buf = snapshot.buf.clone(); self.line_offset = snapshot.line_offset; self.row_start = snapshot.row_start; self.cursor_row = snapshot.cursor_row; self.cursor_col = snapshot.cursor_col; } fn insert(&mut self, inserting: bool) { self.inserting = true; if inserting { return; } if let Some(n) = self.n { self.history.truncate(n); self.n = None; } self.history.push(self.snapshot()); } fn keypress(&mut self, key: Key) -> Result> { const CTRL_Q: char = (b'Q' ^ b'@') as char; const CTRL_S: char = (b'S' ^ b'@') as char; const CTRL_Y: char = (b'Y' ^ b'@') as char; const CTRL_Z: char = (b'Z' ^ b'@') as char; const BACKSPACE: char = (b'H' ^ b'@') as char; const OTHER_BACKSPACE: char = (b'?' ^ b'@') as char; let inserting = self.inserting; self.inserting = false; match key { Key::Timeout => { if inserting { self.inserting = true; } if !self.need_repaint_screen && !self.need_repaint_line { return Ok(true); } if self.need_repaint_screen { self.repaint_screen()?; self.need_repaint_screen = false; } self.update_cursor_col()?; self.need_repaint_line = false; stdout().flush()?; } Key::Up => { if self.cursor_row == 0 && self.row_start == 0 { return Ok(true); } let prev_line = self .buf .line(self.row_start + usize::from(self.cursor_row) - 1); if self.cursor_row == 0 { self.line_offset = line_offset(&mut self.stdin, &prev_line, self.cursor_col)?; self.row_start -= 1; self.repaint_screen()?; self.update_cursor_col()?; stdout().flush()?; return Ok(true); } write!(stdout(), "\x1b[{}H", self.cursor_row)?; self.line_offset = line_offset(&mut self.stdin, &prev_line, self.cursor_col)?; self.cursor_row -= 1; self.update_cursor_col()?; stdout().flush()?; } Key::Down => { if self.row_start + usize::from(self.cursor_row) >= self.buf.lines() { return Ok(true); } let size = term::size()?; let next_line = self .buf .line(self.row_start + usize::from(self.cursor_row) + 1); if self.cursor_row < size.ws_row - 1 { write!(stdout(), "\x1b[{}H", self.cursor_row + 2)?; self.line_offset = line_offset(&mut self.stdin, &next_line, self.cursor_col)?; self.cursor_row += 1; self.update_cursor_col()?; stdout().flush()?; return Ok(true); } self.line_offset = line_offset(&mut self.stdin, &next_line, self.cursor_col)?; self.row_start += 1; self.repaint_screen()?; self.update_cursor_col()?; stdout().flush()?; } Key::Left => { if self.line_offset > 0 { let line = self.line(); self.line_offset = floor_grapheme_cluster(&mut self.stdin, &line, self.line_offset - 1)?; self.update_cursor_col()?; stdout().flush()?; return Ok(true); } if self.cursor_row > 0 { let size = term::size()?; write!(stdout(), "\x1b[{}H", self.cursor_row)?; self.line_offset = line_offset( &mut self.stdin, &self .buf .line(self.row_start + usize::from(self.cursor_row) - 1), size.ws_col - 2, )?; self.cursor_row -= 1; self.update_cursor_col()?; stdout().flush()?; return Ok(true); } if self.row_start == 0 { return Ok(true); } let size = term::size()?; self.line_offset = line_offset( &mut self.stdin, &self .buf .line(self.row_start + usize::from(self.cursor_row) - 1), size.ws_col - 2, )?; self.row_start -= 1; self.repaint_screen()?; self.update_cursor_col()?; stdout().flush()?; } Key::Right => { let size = term::size()?; let line = self.line(); if self.cursor_col < size.ws_col - 2 && self.line_offset < line.len() { self.line_offset = ceil_grapheme_cluster(&mut self.stdin, &line, self.line_offset + 1)?; self.update_cursor_col()?; stdout().flush()?; return Ok(true); } if self.row_start + usize::from(self.cursor_row) >= self.buf.lines() { return Ok(true); } if self.cursor_row < size.ws_row - 1 { self.cursor_row += 1; self.line_offset = 0; self.cursor_col = 0; write!(stdout(), "\x1b[{}H", self.cursor_row + 1)?; stdout().flush()?; return Ok(true); } self.row_start += 1; self.line_offset = 0; self.cursor_col = 0; self.repaint_screen()?; write!(stdout(), "\x1b[{}H", self.cursor_row + 1)?; stdout().flush()?; } Key::Home => { self.line_offset = 0; self.cursor_col = 0; write!(stdout(), "\x1b[{}H", self.cursor_row + 1)?; stdout().flush()?; } Key::End => { let size = term::size()?; let line = self.line(); self.line_offset = line_offset(&mut self.stdin, &line, size.ws_col - 2)?; self.update_cursor_col()?; stdout().flush()?; } Key::PgUp => { if self.row_start == 0 { self.cursor_row = 0; self.cursor_col = 0; self.line_offset = 0; write!(stdout(), "\x1b[H")?; stdout().flush()?; return Ok(true); } let size = term::size()?; let mut start = 0; if usize::from(size.ws_row) < self.row_start + 2 { start = self.row_start + 2 - usize::from(size.ws_row); }; self.line_offset = line_offset( &mut self.stdin, &self.buf.line(start + usize::from(self.cursor_row)), self.cursor_col, )?; self.row_start = start; self.repaint_screen()?; self.update_cursor_col()?; stdout().flush()?; } Key::PgDn => { let size = term::size()?; if self.buf.lines() - self.row_start < usize::from(size.ws_row) { self.cursor_row = u16::try_from(self.buf.lines() - self.row_start) .expect("self.buf.lines() - self.row_start < size.ws_row"); self.line_offset = self.line().len(); self.update_cursor_col()?; stdout().flush()?; return Ok(true); } let mut start = self.row_start + 1; if size.ws_row >= 2 { start = self.row_start + usize::from(size.ws_row) - 2; } if start + usize::from(self.cursor_row) <= self.buf.lines() { self.line_offset = line_offset( &mut self.stdin, &self.buf.line(start + usize::from(self.cursor_row)), self.cursor_col, )?; self.row_start = start; self.repaint_screen()?; self.update_cursor_col()?; stdout().flush()?; return Ok(true); } self.line_offset = line_offset( &mut self.stdin, &self.buf.line(self.buf.lines()), self.cursor_col, )?; self.row_start = start; self.cursor_row = u16::try_from(self.buf.lines() - start) .expect("start + self.cursor_row > self.buf.lines()"); self.repaint_screen()?; self.update_cursor_col()?; stdout().flush()?; } Key::Char(CTRL_Q) => { return Ok(false); } Key::Char(CTRL_S) => { let mut f = File::create(&self.path)?; self.buf.write(&mut f)?; f.sync_all()?; } Key::Char(CTRL_Z) => { if let Some(n) = self.n { if n > 0 { self.n = Some(n - 1); } } else { self.n = Some(self.history.len() - 1); self.history.push(self.snapshot()); } self.load(); self.repaint_screen()?; self.update_cursor_col()?; stdout().flush()?; } Key::Char(CTRL_Y) => { let Some(n) = self.n else { return Ok(true); }; if n == self.history.len() - 1 { return Ok(true); } self.n = Some(n + 1); self.load(); self.repaint_screen()?; self.update_cursor_col()?; stdout().flush()?; } Key::Char(BACKSPACE) | Key::Char(OTHER_BACKSPACE) => { self.insert(inserting); if self.line_offset == 0 && self.cursor_row == 0 && self.row_start == 0 { return Ok(true); } let line_start = self .buf .line_start(self.row_start + usize::from(self.cursor_row)); let offset = line_start + self.line_offset; if self.line_offset == 0 { if self.cursor_row == 0 { self.row_start -= 1; } else { self.cursor_row -= 1; } self.line_offset = self.line().len(); self.buf = self .buf .slice(0, offset - 1) .concat(&self.buf.slice(offset, self.buf.len())); self.repaint_screen()?; self.update_cursor_col()?; stdout().flush()?; return Ok(true); } let line = self.line(); self.line_offset = match floor_grapheme_cluster(&mut self.stdin, &line, self.line_offset - 1) { Ok(x) => x, Err(_) => line.floor_char_boundary(self.line_offset - 1), }; self.buf = self .buf .slice(0, line_start + self.line_offset) .concat(&self.buf.slice(offset, self.buf.len())); self.update_cursor_col()?; stdout().flush()?; } Key::Char('\r') | Key::Char('\n') => { self.insert(inserting); let size = term::size()?; let offset = self .buf .line_start(self.row_start + usize::from(self.cursor_row)) + self.line_offset; self.buf = self .buf .slice(0, offset) .concat(&Rope::new("\n")) .concat(&self.buf.slice(offset, self.buf.len())); if self.cursor_row < size.ws_row - 1 { self.cursor_row += 1; } else { self.row_start += 1; } self.line_offset = 0; self.cursor_col = 0; self.need_repaint_screen = true; write!(stdout(), "\r\n")?; stdout().flush()?; } Key::Char(c) => { self.insert(inserting); let offset = self .buf .line_start(self.row_start + usize::from(self.cursor_row)) + self.line_offset; let mut buf = [0; 4]; let r = Rope::new(c.encode_utf8(&mut buf)); self.buf = self .buf .slice(0, offset) .concat(&r) .concat(&self.buf.slice(offset, self.buf.len())); self.line_offset += r.len(); self.need_repaint_line = true; r.print(&mut stdout())?; stdout().flush()?; } } Ok(true) } } fn edit(file: &OsStr) -> Result<(), String> { let file = PathBuf::from(file); let r = match open(&file) { Ok(r) => r, Err(err) => { return Err(format!("open file {}: {}", file.display(), err)); } }; let _raw_mode = match term::raw_mode() { Ok(x) => x, Err(err) => { return Err(format!("enter raw mode: {}", err)); } }; let mut state = State { history: vec![Snapshot { buf: r.clone(), row_start: 0, cursor_row: 0, cursor_col: 0, line_offset: 0, }], n: None, buf: r, row_start: 0, cursor_row: 0, cursor_col: 0, line_offset: 0, stdin: Reader::new(std::io::stdin()), path: file, inserting: false, need_repaint_line: false, need_repaint_screen: false, }; if let Err(err) = state.repaint_screen() { return Err(format!("cannot write to screen: {}", err)); } if let Err(err) = write!(stdout(), "\x1b[H") { return Err(format!("cannot write to screen: {}", err)); } if let Err(err) = stdout().flush() { return Err(format!("cannot write to screen: {}", err)); } loop { let Ok(key) = state.stdin.read_key() else { return Err(String::from("no input")); }; let Ok(ok) = state.keypress(key) else { state.need_repaint_screen = true; let _ = stdout().flush(); continue; }; // I miss "do while" so much... if !ok { break; } } Ok(()) } fn main() { let args: Vec = std::env::args_os().collect(); if args.len() != 2 { let _ = writeln!(stderr(), "Usage: edit "); std::process::exit(1); } if let Err(err) = edit(&args[1]) { let _ = writeln!(stderr(), "FAIL: {}", err); std::process::exit(1); } }