diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a664e5..2cf93ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] - Bugfix: Missing CPR arguments causes panic +- Switched dependency termion to crossterm for setting the raw mode in the std examples for windows compatibility +- Split the modules up into directories for better visibility ## [0.5.0 - 2024-12-12] diff --git a/Cargo.toml b/Cargo.toml index 73b910a..087d0a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,5 @@ [workspace] +resolver = "2" members = [ "noline", diff --git a/examples/std/Cargo.toml b/examples/std/Cargo.toml index e6935dd..75b3281 100644 --- a/examples/std/Cargo.toml +++ b/examples/std/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -tokio = { version = "1.38.0", features = [ +tokio = { version = "1.44.2", features = [ "full", "io-util", "sync", @@ -16,9 +16,9 @@ tokio = { version = "1.38.0", features = [ ] } noline = { path = "../../noline", features = ["std"] } heapless = "0.8.0" -termion = "4.0.0" embedded-io-async = "0.6.1" embedded-io = "0.6.1" +crossterm = "0.29.0" [[bin]] name = "std-sync" diff --git a/examples/std/src/bin/std-async-tokio.rs b/examples/std/src/bin/std-async-tokio.rs index be3ccf9..7fae3e1 100644 --- a/examples/std/src/bin/std-async-tokio.rs +++ b/examples/std/src/bin/std-async-tokio.rs @@ -1,6 +1,5 @@ -use embedded_io_async::Write; +use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; use noline::builder::EditorBuilder; -use termion::raw::IntoRawMode; use tokio::io; use tokio::io::{AsyncReadExt, AsyncWriteExt}; @@ -43,7 +42,7 @@ impl embedded_io_async::Write for IOWrapper { #[tokio::main(flavor = "current_thread")] async fn main() { let term_task = tokio::spawn(async { - let _raw_term = std::io::stdout().into_raw_mode().unwrap(); + enable_raw_mode().unwrap(); let mut io = IOWrapper::new(); let prompt = "> "; @@ -58,6 +57,7 @@ async fn main() { let s = format!("Read: '{}'\n\r", line); io.stdout.write_all(s.as_bytes()).await.unwrap(); } + disable_raw_mode().unwrap(); }); match term_task.await { diff --git a/examples/std/src/bin/std-sync.rs b/examples/std/src/bin/std-sync.rs index 4accf5a..c74624b 100644 --- a/examples/std/src/bin/std-sync.rs +++ b/examples/std/src/bin/std-sync.rs @@ -1,8 +1,6 @@ -use noline::builder::EditorBuilder; -use std::io; -use termion::raw::IntoRawMode; - +use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; use embedded_io::{ErrorType, Read as EmbRead, Write as EmbWrite}; +use noline::builder::EditorBuilder; use std::io::{Read, Stdin, Stdout, Write}; pub struct IOWrapper { @@ -45,7 +43,7 @@ impl EmbWrite for IOWrapper { } fn main() { - let _stdout = io::stdout().into_raw_mode().unwrap(); + enable_raw_mode().unwrap(); let prompt = "> "; let mut io = IOWrapper::new(); @@ -58,4 +56,5 @@ fn main() { while let Ok(line) = editor.readline(prompt, &mut io) { writeln!(io, "Read: '{}'", line).unwrap(); } + disable_raw_mode().unwrap(); } diff --git a/noline/Cargo.toml b/noline/Cargo.toml index 0535828..7be7a67 100644 --- a/noline/Cargo.toml +++ b/noline/Cargo.toml @@ -16,7 +16,7 @@ include = ["**/*.rs", "Cargo.toml"] [dependencies] embedded-io = "0.6.1" embedded-io-async = "0.6.1" -num_enum = { version = "0.7.2", default-features = false } +num_enum = { version = "0.7.3", default-features = false } [features] @@ -25,8 +25,8 @@ std = ["embedded-io/std", "embedded-io-async/std"] alloc = [] [dev-dependencies] -crossbeam = "0.8.1" -termion = "4.0.0" +crossbeam = "0.8.4" +crossterm = "0.29.0" [package.metadata.docs.rs] all-features = true diff --git a/noline/src/core.rs b/noline/src/core.rs deleted file mode 100644 index 11476bd..0000000 --- a/noline/src/core.rs +++ /dev/null @@ -1,958 +0,0 @@ -//! Core library containing the components for building an editor. -//! -//! Use [`Initializer`] to get [`crate::terminal::Terminal`] and then -//! use [`Line`] to read a single line. - -use crate::history::{History, HistoryNavigator}; -use crate::input::{Action, ControlCharacter::*, Parser, CSI}; -use crate::line_buffer::Buffer; -use crate::line_buffer::LineBuffer; -use crate::output::CursorMove; -use crate::output::{Output, OutputAction}; -use crate::terminal::{Cursor, Terminal}; - -use OutputAction::*; - -enum ResetState { - New, - GetSize, - GetPosition, - Done, -} - -pub struct ResetHandle<'line, 'a, B: Buffer, H: History, I> { - line: &'line mut Line<'a, B, H, I>, - state: ResetState, -} - -impl<'line, 'a, 'item, 'output, B, H, I> ResetHandle<'line, 'a, B, H, I> -where - I: Iterator + Clone + 'a, - B: Buffer, - H: History, - 'item: 'output, -{ - fn new(line: &'line mut Line<'a, B, H, I>) -> Self { - Self { - line, - state: ResetState::New, - } - } - - pub fn start(&mut self) -> Output<'_, B, I> { - assert!(matches!(self.state, ResetState::New)); - self.state = ResetState::GetSize; - - self.line.generate_output(ProbeSize) - } - - pub fn advance(&mut self, byte: u8) -> Option> { - let action = self.line.parser.advance(byte); - - match action { - Action::ControlSequenceIntroducer(CSI::CPR(x, y)) => match self.state { - ResetState::New => panic!("Invalid state"), - ResetState::GetSize => { - self.line.terminal.resize(x, y); - self.state = ResetState::GetPosition; - Some(self.line.generate_output(ClearAndPrintPrompt)) - } - ResetState::GetPosition => { - #[cfg(test)] - dbg!(x, y); - self.line.terminal.reset(Cursor::new(x - 1, y - 1)); - self.state = ResetState::Done; - None - } - ResetState::Done => panic!("Invalid state"), - }, - Action::Ignore => Some(self.line.generate_output(Nothing)), - _ => None, - } - } -} - -#[cfg_attr(test, derive(Debug))] -pub struct Prompt { - parts: I, - len: usize, -} - -impl<'a, I> Prompt -where - I: Iterator + Clone, -{ - fn new(parts: I) -> Self { - Self { - len: parts.clone().map(|part| part.len()).sum(), - parts, - } - } - - pub fn len(&self) -> usize { - self.len - } -} - -impl<'a, I> Prompt -where - I: Iterator + Clone, -{ - pub fn iter(&self) -> I { - self.parts.clone() - } -} - -#[derive(Clone)] -pub struct StrIter<'a> { - s: Option<&'a str>, -} - -impl<'a> Iterator for StrIter<'a> { - type Item = &'a str; - - fn next(&mut self) -> Option { - self.s.take() - } -} - -impl<'a> From<&'a str> for Prompt> { - fn from(value: &'a str) -> Self { - Self::new(StrIter { s: Some(value) }) - } -} - -impl<'a, I> From for Prompt -where - I: Iterator + Clone, -{ - fn from(value: I) -> Self { - Self::new(value) - } -} - -// State machine for reading single line. -// -// Provide input by calling [`Line::advance`], returning -// [`crate::output::Output`] object, which -// -// Before reading line, call [`Line::reset`] to truncate buffer, clear -// line, get cursor position and print prompt. Call [`Line::advance`] -// for each byte read from input and print bytes from -// [`crate::output::Output`] to output. -pub struct Line<'a, B: Buffer, H: History, I> { - buffer: &'a mut LineBuffer, - terminal: &'a mut Terminal, - parser: Parser, - prompt: Prompt, - nav: HistoryNavigator<'a, H>, -} - -impl<'a, 'item, 'output, B: Buffer, H: History, I> Line<'a, B, H, I> -where - I: Iterator + Clone + 'a, - 'item: 'output, - 'a: 'output, -{ - pub fn new( - prompt: impl Into>, - buffer: &'a mut LineBuffer, - terminal: &'a mut Terminal, - history: &'a mut H, - ) -> Self { - Self { - buffer, - terminal, - parser: Parser::new(), - prompt: prompt.into(), - nav: HistoryNavigator::new(history), - } - } - - // Truncate buffer, clear line and print prompt - pub fn reset(&mut self) -> ResetHandle<'_, 'a, B, H, I> { - self.buffer.truncate(); - ResetHandle::new(self) - } - - fn generate_output(&mut self, action: OutputAction) -> Output<'_, B, I> { - Output::new(&self.prompt, self.buffer, self.terminal, action) - } - - fn current_position(&self) -> usize { - let pos = self.terminal.current_offset() as usize; - pos - self.prompt.len() - } - - fn history_move_up(&mut self) -> Output<'_, B, I> { - let entry = if self.nav.is_active() { - self.nav.move_up() - } else if self.buffer.len() == 0 { - self.nav.reset(); - self.nav.move_up() - } else { - Err(()) - }; - - if let Ok(entry) = entry { - let (slice1, slice2) = entry.get_slices(); - - self.buffer.truncate(); - unsafe { - self.buffer.insert_bytes(0, slice1).unwrap(); - self.buffer.insert_bytes(slice1.len(), slice2).unwrap(); - } - - self.generate_output(ClearAndPrintBuffer) - } else { - self.generate_output(RingBell) - } - } - - fn history_move_down(&mut self) -> Output<'_, B, I> { - let entry = if self.nav.is_active() { - self.nav.move_down() - } else { - return self.generate_output(RingBell); - }; - - if let Ok(entry) = entry { - let (slice1, slice2) = entry.get_slices(); - - self.buffer.truncate(); - unsafe { - self.buffer.insert_bytes(0, slice1).unwrap(); - self.buffer.insert_bytes(slice1.len(), slice2).unwrap(); - } - } else { - self.nav.reset(); - self.buffer.truncate(); - } - - self.generate_output(ClearAndPrintBuffer) - } - - // Advance state machine by one byte. Returns output iterator over - // 0 or more byte slices. - pub(crate) fn advance(&mut self, byte: u8) -> Output<'_, B, I> { - let action = self.parser.advance(byte); - - #[cfg(test)] - dbg!(action); - - match action { - Action::Print(c) => { - let pos = self.current_position(); - - if self.buffer.insert_utf8_char(pos, c).is_ok() { - self.generate_output(PrintBufferAndMoveCursorForward) - } else { - self.generate_output(RingBell) - } - } - Action::ControlCharacter(c) => match c { - CtrlA => self.generate_output(MoveCursor(CursorMove::Start)), - CtrlB => self.generate_output(MoveCursor(CursorMove::Back)), - CtrlC => self.generate_output(Abort), - CtrlD => { - let len = self.buffer.len(); - - if len > 0 { - let pos = self.current_position(); - - if pos < len { - self.buffer.delete(pos); - - self.generate_output(EraseAndPrintBuffer) - } else { - self.generate_output(RingBell) - } - } else { - self.generate_output(Abort) - } - } - CtrlE => self.generate_output(MoveCursor(CursorMove::End)), - CtrlF => self.generate_output(MoveCursor(CursorMove::Forward)), - CtrlK => { - let pos = self.current_position(); - - self.buffer.delete_after_char(pos); - - self.generate_output(EraseAfterCursor) - } - CtrlL => { - self.buffer.delete_after_char(0); - self.generate_output(ClearScreen) - } - CtrlN => self.history_move_down(), - CtrlP => self.history_move_up(), - CtrlT => { - let pos = self.current_position(); - - if pos > 0 && pos < self.buffer.as_str().chars().count() { - self.buffer.swap_chars(pos); - self.generate_output(MoveCursorBackAndPrintBufferAndMoveForward) - } else { - self.generate_output(RingBell) - } - } - CtrlU => { - self.buffer.delete_after_char(0); - self.generate_output(ClearLine) - } - CtrlW => { - let pos = self.current_position(); - let move_cursor = -(self.buffer.delete_previous_word(pos) as isize); - self.generate_output(MoveCursorAndEraseAndPrintBuffer(move_cursor)) - } - CarriageReturn | LineFeed => { - if self.buffer.len() > 0 { - let _ = self.nav.history.add_entry(self.buffer.as_str()); - } - - self.generate_output(Done) - } - CtrlH | Backspace => { - let pos = self.current_position(); - if pos > 0 { - self.buffer.delete(pos - 1); - self.generate_output(MoveCursorAndEraseAndPrintBuffer(-1)) - } else { - self.generate_output(RingBell) - } - } - _ => self.generate_output(RingBell), - }, - Action::ControlSequenceIntroducer(csi) => match csi { - CSI::CUF(_) => self.generate_output(MoveCursor(CursorMove::Forward)), - CSI::CUB(_) => self.generate_output(MoveCursor(CursorMove::Back)), - CSI::Home => self.generate_output(MoveCursor(CursorMove::Start)), - CSI::Delete => { - let len = self.buffer.len(); - let pos = self.current_position(); - - if pos < len { - self.buffer.delete(pos); - - self.generate_output(EraseAndPrintBuffer) - } else { - self.generate_output(RingBell) - } - } - CSI::End => self.generate_output(MoveCursor(CursorMove::End)), - CSI::CPR(row, column) => { - let cursor = Cursor::new(row - 1, column - 1); - self.terminal.reset(cursor); - self.generate_output(Nothing) - } - CSI::Unknown(_) => self.generate_output(RingBell), - CSI::CUU(_) => self.history_move_up(), - CSI::CUD(_) => self.history_move_down(), - CSI::CUP(_, _) => self.generate_output(RingBell), - CSI::ED(_) => self.generate_output(RingBell), - CSI::DSR => self.generate_output(RingBell), - CSI::SU(_) => self.generate_output(RingBell), - CSI::SD(_) => self.generate_output(RingBell), - CSI::Invalid => self.generate_output(RingBell), - }, - Action::EscapeSequence(_) => self.generate_output(RingBell), - Action::Ignore => self.generate_output(Nothing), - Action::InvalidUtf8 => self.generate_output(RingBell), - } - } -} - -#[cfg(test)] -pub(crate) mod tests { - use std::vec::Vec; - - use std::string::String; - - use crate::history::{NoHistory, SliceHistory, UnboundedHistory}; - use crate::line_buffer::UnboundedBuffer; - use crate::terminal::Cursor; - use crate::testlib::{csi, MockTerminal, ToByteVec}; - - use super::*; - - struct Editor { - buffer: LineBuffer, - terminal: Terminal, - history: H, - } - - impl Editor { - fn new(buffer: LineBuffer, history: H) -> Self { - let terminal = Terminal::default(); - - Self { - buffer, - terminal, - history, - } - } - - fn get_line( - &mut self, - prompt: &'static str, - mockterm: &mut MockTerminal, - ) -> Line<'_, B, H, StrIter> { - let cursor = mockterm.get_cursor(); - let mut line = Line::new( - prompt, - &mut self.buffer, - &mut self.terminal, - &mut self.history, - ); - - let mut reset = line.reset(); - - let mut reset_start: Vec = reset - .start() - .into_iter() - .filter_map(|item| item.get_bytes().map(|bytes| bytes.to_vec())) - .flatten() - .collect(); - - while !reset_start.is_empty() { - let term_response: Vec = reset_start - .into_iter() - .filter_map(|b| mockterm.advance(b)) - .flat_map(|output| output.into_iter()) - .collect(); - - reset_start = term_response - .iter() - .copied() - .filter_map(|b| { - reset.advance(b).map(|output| { - output - .into_iter() - .map(|item| item.get_bytes().map(|bytes| bytes.to_vec())) - .collect::>() - }) - }) - .flatten() - .flatten() - .flatten() - .collect(); - } - - assert_eq!(mockterm.current_line_as_string(), prompt); - assert_eq!(mockterm.get_cursor(), Cursor::new(cursor.row, prompt.len())); - - line - } - } - - fn advance<'a, B: Buffer, H: History>( - terminal: &mut MockTerminal, - noline: &mut Line<'a, B, H, StrIter<'a>>, - input: impl ToByteVec, - ) -> core::result::Result<(), ()> { - terminal.bell = false; - - for input in input.to_byte_vec() { - for item in noline.advance(input) { - if let Some(bytes) = item.get_bytes() { - for &b in bytes { - terminal.advance(b); - } - } - } - } - - assert_eq!(noline.terminal.get_cursor(), terminal.cursor); - - dbg!(terminal.screen_as_string()); - - if terminal.bell { - Err(()) - } else { - Ok(()) - } - } - - fn get_terminal_and_editor( - rows: usize, - columns: usize, - origin: Cursor, - ) -> (MockTerminal, Editor) { - let terminal = MockTerminal::new(rows, columns, origin); - - let editor = Editor::new(LineBuffer::new_unbounded(), NoHistory {}); - - assert_eq!(terminal.get_cursor(), origin); - - (terminal, editor) - } - - #[test] - fn reset() { - let prompt = "> "; - let (terminal, mut editor) = get_terminal_and_editor(4, 10, Cursor::new(1, 0)); - let mut line = Line::new( - prompt, - &mut editor.buffer, - &mut editor.terminal, - &mut editor.history, - ); - - dbg!(terminal.get_cursor()); - - let mut reset = line.reset(); - - let probe = reset - .start() - .into_iter() - .flat_map(|item| item.get_bytes().unwrap().to_vec()) - .collect::>(); - - assert_eq!(probe, b"\x1b7\x1b[999;999H\x1b[6n\x1b8"); - - let output = b"\x1b[91;45R" - .iter() - .copied() - .flat_map(|b| reset.advance(b).unwrap().into_vec()) - .collect::>(); - - dbg!(terminal.get_cursor()); - - assert_eq!(output, b"\r\x1b[J> \x1b[6n"); - - let output = b"\x1b[2;3R" - .iter() - .copied() - .flat_map(|b| { - if let Some(output) = reset.advance(b) { - output.into_vec() - } else { - Vec::new() - } - }) - .collect::>(); - - dbg!(terminal.get_cursor()); - - assert_eq!(output, b""); - - assert_eq!(line.terminal.get_size(), (91, 45)); - } - - #[test] - fn mock_editor() { - let prompt = "> "; - let (mut terminal, mut editor) = get_terminal_and_editor(4, 20, Cursor::new(1, 0)); - - assert_eq!(terminal.get_cursor(), Cursor::new(1, 0)); - - let line = editor.get_line(prompt, &mut terminal); - - dbg!(&line.terminal); - assert_eq!(terminal.get_cursor(), line.terminal.get_cursor()); - } - - #[test] - fn movecursor() { - let prompt = "> "; - let (mut terminal, mut editor) = get_terminal_and_editor(4, 10, Cursor::new(1, 0)); - - let mut line = editor.get_line(prompt, &mut terminal); - - advance(&mut terminal, &mut line, "Hello, World!").unwrap(); - assert_eq!(terminal.get_cursor(), Cursor::new(2, 5)); - - advance(&mut terminal, &mut line, [csi::LEFT; 6]).unwrap(); - assert_eq!(terminal.get_cursor(), Cursor::new(1, 9)); - - advance(&mut terminal, &mut line, CtrlA).unwrap(); - - assert_eq!(terminal.get_cursor(), Cursor::new(1, 2)); - - assert!(advance(&mut terminal, &mut line, csi::LEFT).is_err()); - - assert_eq!(terminal.get_cursor(), Cursor::new(1, 2)); - - advance(&mut terminal, &mut line, CtrlE).unwrap(); - - assert_eq!(terminal.get_cursor(), Cursor::new(2, 5)); - - assert!(advance(&mut terminal, &mut line, csi::RIGHT).is_err()); - - assert_eq!(terminal.get_cursor(), Cursor::new(2, 5)); - - advance(&mut terminal, &mut line, csi::HOME).unwrap(); - - assert_eq!(terminal.get_cursor(), Cursor::new(1, 2)); - - advance(&mut terminal, &mut line, csi::END).unwrap(); - - assert_eq!(terminal.get_cursor(), Cursor::new(2, 5)); - } - - #[test] - fn cursor_scroll() { - let prompt = "> "; - let (mut terminal, mut editor) = get_terminal_and_editor(4, 10, Cursor::new(3, 0)); - - let mut line = editor.get_line(prompt, &mut terminal); - - advance(&mut terminal, &mut line, "23456789").unwrap(); - assert_eq!(terminal.get_cursor(), Cursor::new(3, 0)); - } - - #[test] - fn clear_line() { - let prompt = "> "; - let (mut terminal, mut editor) = get_terminal_and_editor(4, 20, Cursor::new(1, 0)); - - let mut line = editor.get_line(prompt, &mut terminal); - - dbg!(terminal.get_cursor()); - dbg!(&line.terminal); - assert_eq!(terminal.get_cursor(), Cursor::new(1, 2)); - dbg!(terminal.screen_as_string()); - - advance(&mut terminal, &mut line, "Hello, World!").unwrap(); - assert_eq!(terminal.get_cursor(), Cursor::new(1, 15)); - assert_eq!(terminal.screen_as_string(), "> Hello, World!"); - - advance(&mut terminal, &mut line, CtrlU).unwrap(); - assert_eq!(terminal.get_cursor(), Cursor::new(1, 2)); - assert_eq!(terminal.screen_as_string(), "> "); - - advance(&mut terminal, &mut line, "Hello, World!").unwrap(); - assert_eq!(terminal.get_cursor(), Cursor::new(1, 15)); - assert_eq!(terminal.screen_as_string(), "> Hello, World!"); - } - - #[test] - fn clear_screen() { - let prompt = "> "; - let (mut terminal, mut editor) = get_terminal_and_editor(4, 20, Cursor::new(1, 0)); - - let mut line = editor.get_line(prompt, &mut terminal); - - advance(&mut terminal, &mut line, "Hello, World!").unwrap(); - assert_eq!(terminal.get_cursor(), Cursor::new(1, 15)); - assert_eq!(terminal.screen_as_string(), "> Hello, World!"); - - advance(&mut terminal, &mut line, CtrlL).unwrap(); - assert_eq!(terminal.get_cursor(), Cursor::new(0, 2)); - assert_eq!(terminal.screen_as_string(), "> "); - - advance(&mut terminal, &mut line, "Hello, World!").unwrap(); - assert_eq!(terminal.get_cursor(), Cursor::new(0, 15)); - assert_eq!(terminal.screen_as_string(), "> Hello, World!"); - } - - #[test] - fn scroll() { - let prompt = "> "; - let (mut terminal, mut editor) = get_terminal_and_editor(4, 10, Cursor::new(0, 0)); - - let mut line = editor.get_line(prompt, &mut terminal); - - advance(&mut terminal, &mut line, "aaaaaaaa").unwrap(); - advance(&mut terminal, &mut line, "bbbbbbbbbb").unwrap(); - advance(&mut terminal, &mut line, "cccccccccc").unwrap(); - advance(&mut terminal, &mut line, "ddddddddd").unwrap(); - - assert_eq!(terminal.get_cursor(), Cursor::new(3, 9)); - - assert_eq!( - terminal.screen_as_string(), - "> aaaaaaaa\nbbbbbbbbbb\ncccccccccc\nddddddddd" - ); - - advance(&mut terminal, &mut line, "d").unwrap(); - - assert_eq!(terminal.get_cursor(), Cursor::new(3, 0)); - - assert_eq!( - terminal.screen_as_string(), - "bbbbbbbbbb\ncccccccccc\ndddddddddd" - ); - - advance(&mut terminal, &mut line, "eeeeeeeeee").unwrap(); - - assert_eq!( - terminal.screen_as_string(), - "cccccccccc\ndddddddddd\neeeeeeeeee" - ); - - // advance(&mut terminal, &mut noline, CtrlA); - - // assert_eq!(terminal.get_cursor(), Cursor::new(0, 2)); - // assert_eq!( - // terminal.screen_as_string(), - // "> aaaaaaaa\nbbbbbbbbbb\ncccccccccc\ndddddddddd" - // ); - - // advance(&mut terminal, &mut noline, CtrlE); - // assert_eq!(terminal.get_cursor(), Cursor::new(3, 0)); - // assert_eq!( - // terminal.screen_as_string(), - // "cccccccccc\ndddddddddd\neeeeeeeeee" - // ); - } - - #[test] - fn swap() { - let prompt = "> "; - let (mut terminal, mut editor) = get_terminal_and_editor(4, 10, Cursor::new(0, 0)); - - let mut line = editor.get_line(prompt, &mut terminal); - - advance(&mut terminal, &mut line, "æøå").unwrap(); - assert_eq!(terminal.screen_as_string(), "> æøå"); - assert_eq!(terminal.get_cursor(), Cursor::new(0, 5)); - - assert!(advance(&mut terminal, &mut line, CtrlT).is_err()); - - assert_eq!(terminal.screen_as_string(), "> æøå"); - assert_eq!(terminal.get_cursor(), Cursor::new(0, 5)); - - advance(&mut terminal, &mut line, csi::LEFT).unwrap(); - - assert_eq!(terminal.get_cursor(), Cursor::new(0, 4)); - - advance(&mut terminal, &mut line, CtrlT).unwrap(); - - assert_eq!(line.buffer.as_str(), "æåø"); - assert_eq!(terminal.screen_as_string(), "> æåø"); - assert_eq!(terminal.get_cursor(), Cursor::new(0, 4)); - - advance(&mut terminal, &mut line, CtrlA).unwrap(); - - assert_eq!(terminal.get_cursor(), Cursor::new(0, 2)); - - assert!(advance(&mut terminal, &mut line, CtrlT).is_err()); - assert_eq!(terminal.screen_as_string(), "> æåø"); - } - - #[test] - fn erase_after_cursor() { - let prompt = "> "; - let (mut terminal, mut editor) = get_terminal_and_editor(4, 10, Cursor::new(0, 0)); - - let mut line = editor.get_line(prompt, &mut terminal); - - advance(&mut terminal, &mut line, "rm -rf /").unwrap(); - assert_eq!(terminal.get_cursor(), Cursor::new(1, 0)); - assert_eq!(terminal.screen_as_string(), "> rm -rf /"); - - advance(&mut terminal, &mut line, CtrlA).unwrap(); - advance(&mut terminal, &mut line, [CtrlF; 3]).unwrap(); - - assert_eq!(terminal.get_cursor(), Cursor::new(0, 5)); - - advance(&mut terminal, &mut line, CtrlK).unwrap(); - assert_eq!(line.buffer.as_str(), "rm "); - assert_eq!(terminal.get_cursor(), Cursor::new(0, 5)); - assert_eq!(terminal.screen_as_string(), "> rm "); - } - - #[test] - fn delete_previous_word() { - let prompt = "> "; - let (mut terminal, mut editor) = get_terminal_and_editor(1, 40, Cursor::new(0, 0)); - - let mut line = editor.get_line(prompt, &mut terminal); - - advance(&mut terminal, &mut line, "rm file1 file2 file3").unwrap(); - assert_eq!(terminal.screen_as_string(), "> rm file1 file2 file3"); - - advance(&mut terminal, &mut line, [CtrlB; 5]).unwrap(); - - advance(&mut terminal, &mut line, CtrlW).unwrap(); - assert_eq!(terminal.get_cursor(), Cursor::new(0, 11)); - assert_eq!(line.buffer.as_str(), "rm file1 file3"); - assert_eq!(terminal.screen_as_string(), "> rm file1 file3"); - - advance(&mut terminal, &mut line, CtrlW).unwrap(); - assert_eq!(terminal.screen_as_string(), "> rm file3"); - assert_eq!(terminal.get_cursor(), Cursor::new(0, 5)); - } - - #[test] - fn delete() { - let prompt = "> "; - let (mut terminal, mut editor) = get_terminal_and_editor(1, 40, Cursor::new(0, 0)); - - let mut line = editor.get_line(prompt, &mut terminal); - - assert_eq!(terminal.get_cursor(), Cursor::new(0, 2)); - assert_eq!(terminal.screen_as_string(), "> "); - advance(&mut terminal, &mut line, "abcde").unwrap(); - - advance(&mut terminal, &mut line, CtrlD).unwrap_err(); - - advance(&mut terminal, &mut line, CtrlA).unwrap(); - - advance(&mut terminal, &mut line, CtrlD).unwrap(); - assert_eq!(line.buffer.as_str(), "bcde"); - assert_eq!(terminal.screen_as_string(), "> bcde"); - - advance(&mut terminal, &mut line, [csi::RIGHT; 3]).unwrap(); - advance(&mut terminal, &mut line, CtrlD).unwrap(); - assert_eq!(line.buffer.as_str(), "bcd"); - assert_eq!(terminal.screen_as_string(), "> bcd"); - - advance(&mut terminal, &mut line, CtrlD).unwrap_err(); - - advance(&mut terminal, &mut line, CtrlA).unwrap(); - - advance(&mut terminal, &mut line, csi::DELETE).unwrap(); - assert_eq!(line.buffer.as_str(), "cd"); - assert_eq!(terminal.screen_as_string(), "> cd"); - - advance(&mut terminal, &mut line, csi::DELETE).unwrap(); - assert_eq!(line.buffer.as_str(), "d"); - assert_eq!(terminal.screen_as_string(), "> d"); - } - - #[test] - fn backspace() { - let prompt = "> "; - let (mut terminal, mut editor) = get_terminal_and_editor(1, 40, Cursor::new(0, 0)); - - let mut line = editor.get_line(prompt, &mut terminal); - - assert!(advance(&mut terminal, &mut line, Backspace).is_err()); - - assert_eq!(terminal.get_cursor(), Cursor::new(0, 2)); - assert_eq!(terminal.screen_as_string(), "> "); - advance(&mut terminal, &mut line, "hello").unwrap(); - - advance(&mut terminal, &mut line, Backspace).unwrap(); - assert_eq!(line.buffer.as_str(), "hell"); - assert_eq!(terminal.screen_as_string(), "> hell"); - - advance(&mut terminal, &mut line, [csi::LEFT; 2]).unwrap(); - advance(&mut terminal, &mut line, Backspace).unwrap(); - assert_eq!(line.buffer.as_str(), "hll"); - assert_eq!(terminal.screen_as_string(), "> hll"); - - advance(&mut terminal, &mut line, CtrlA).unwrap(); - advance(&mut terminal, &mut line, Backspace).unwrap_err(); - } - - #[test] - fn slice_buffer() { - let mut array = [0; 20]; - let mut terminal = MockTerminal::new(20, 80, Cursor::new(0, 0)); - let mut editor: Editor<_, NoHistory> = - Editor::new(LineBuffer::from_slice(&mut array), NoHistory {}); - - let mut line = editor.get_line("> ", &mut terminal); - - let input: String = (0..20).map(|_| "a").collect(); - - advance(&mut terminal, &mut line, input.as_str()).unwrap(); - - assert_eq!(advance(&mut terminal, &mut line, "a"), Err(())); - - assert_eq!(line.buffer.as_str(), input); - - advance(&mut terminal, &mut line, Backspace).unwrap(); - } - - #[test] - fn history() { - fn test(history: H) { - let mut terminal = MockTerminal::new(20, 80, Cursor::new(0, 0)); - let mut editor: Editor<_, H> = Editor::new(LineBuffer::new_unbounded(), history); - - let mut line = editor.get_line("> ", &mut terminal); - - advance(&mut terminal, &mut line, "this is a line\r").unwrap(); - - let mut line = editor.get_line("> ", &mut terminal); - - assert_eq!(terminal.screen_as_string(), "> this is a line\n> "); - - assert!(advance(&mut terminal, &mut line, csi::UP).is_ok()); - - assert_eq!( - terminal.screen_as_string(), - "> this is a line\n> this is a line" - ); - - assert!(advance(&mut terminal, &mut line, csi::DOWN).is_ok()); - - assert_eq!(terminal.screen_as_string(), "> this is a line\n> "); - - advance(&mut terminal, &mut line, "another line\r").unwrap(); - - let mut line = editor.get_line("> ", &mut terminal); - advance(&mut terminal, &mut line, "yet another line\r").unwrap(); - - let mut line = editor.get_line("> ", &mut terminal); - - assert_eq!( - terminal.screen_as_string(), - "> this is a line\n> another line\n> yet another line\n> " - ); - - assert!(advance(&mut terminal, &mut line, csi::UP).is_ok()); - - assert_eq!( - terminal.screen_as_string(), - "> this is a line\n> another line\n> yet another line\n> yet another line" - ); - - assert!(advance(&mut terminal, &mut line, csi::UP).is_ok()); - - assert_eq!( - terminal.screen_as_string(), - "> this is a line\n> another line\n> yet another line\n> another line" - ); - - assert!(advance(&mut terminal, &mut line, csi::UP).is_ok()); - - assert_eq!( - terminal.screen_as_string(), - "> this is a line\n> another line\n> yet another line\n> this is a line" - ); - - assert!(advance(&mut terminal, &mut line, csi::UP).is_err()); - - assert_eq!( - terminal.screen_as_string(), - "> this is a line\n> another line\n> yet another line\n> this is a line" - ); - - assert!(advance(&mut terminal, &mut line, csi::DOWN).is_ok()); - - assert_eq!( - terminal.screen_as_string(), - "> this is a line\n> another line\n> yet another line\n> another line" - ); - - assert!(advance(&mut terminal, &mut line, csi::DOWN).is_ok()); - - assert_eq!( - terminal.screen_as_string(), - "> this is a line\n> another line\n> yet another line\n> yet another line" - ); - assert!(advance(&mut terminal, &mut line, csi::DOWN).is_ok()); - - assert_eq!( - terminal.screen_as_string(), - "> this is a line\n> another line\n> yet another line\n> " - ); - - assert!(advance(&mut terminal, &mut line, csi::DOWN).is_err()); - - assert_eq!( - terminal.screen_as_string(), - "> this is a line\n> another line\n> yet another line\n> " - ); - } - - test(UnboundedHistory::new()); - let mut buffer = [0; 128]; - test(SliceHistory::new(&mut buffer)); - } -} diff --git a/noline/src/core/line.rs b/noline/src/core/line.rs new file mode 100644 index 0000000..f3a6184 --- /dev/null +++ b/noline/src/core/line.rs @@ -0,0 +1,241 @@ +use crate::history::{History, HistoryNavigator}; +use crate::input::{Action, ControlCharacter::*, Parser, CSI}; +use crate::line_buffer::Buffer; +use crate::line_buffer::LineBuffer; +use crate::output::CursorMove; +use crate::output::{Output, OutputAction}; +use crate::terminal::{Cursor, Terminal}; + +use super::{Prompt, ResetHandle}; +use OutputAction::*; + +// State machine for reading single line. +// +// Provide input by calling [`Line::advance`], returning +// [`crate::output::Output`] object, which +// +// Before reading line, call [`Line::reset`] to truncate buffer, clear +// line, get cursor position and print prompt. Call [`Line::advance`] +// for each byte read from input and print bytes from +// [`crate::output::Output`] to output. +pub struct Line<'a, B: Buffer, H: History, I> { + pub(super) buffer: &'a mut LineBuffer, + pub(super) terminal: &'a mut Terminal, + pub(super) parser: Parser, + prompt: Prompt, + nav: HistoryNavigator<'a, H>, +} + +impl<'a, 'item, 'output, B: Buffer, H: History, I> Line<'a, B, H, I> +where + I: Iterator + Clone + 'a, + 'item: 'output, + 'a: 'output, +{ + pub fn new( + prompt: impl Into>, + buffer: &'a mut LineBuffer, + terminal: &'a mut Terminal, + history: &'a mut H, + ) -> Self { + Self { + buffer, + terminal, + parser: Parser::new(), + prompt: prompt.into(), + nav: HistoryNavigator::new(history), + } + } + + // Truncate buffer, clear line and print prompt + pub fn reset(&mut self) -> ResetHandle<'_, 'a, B, H, I> { + self.buffer.truncate(); + ResetHandle::new(self) + } + + pub(super) fn generate_output(&mut self, action: OutputAction) -> Output<'_, B, I> { + Output::new(&self.prompt, self.buffer, self.terminal, action) + } + + fn current_position(&self) -> usize { + let pos = self.terminal.current_offset() as usize; + pos - self.prompt.len() + } + + fn history_move_up(&mut self) -> Output<'_, B, I> { + let entry = if self.nav.is_active() { + self.nav.move_up() + } else if self.buffer.len() == 0 { + self.nav.reset(); + self.nav.move_up() + } else { + Err(()) + }; + + if let Ok(entry) = entry { + let (slice1, slice2) = entry.get_slices(); + + self.buffer.truncate(); + unsafe { + self.buffer.insert_bytes(0, slice1).unwrap(); + self.buffer.insert_bytes(slice1.len(), slice2).unwrap(); + } + + self.generate_output(ClearAndPrintBuffer) + } else { + self.generate_output(RingBell) + } + } + + fn history_move_down(&mut self) -> Output<'_, B, I> { + let entry = if self.nav.is_active() { + self.nav.move_down() + } else { + return self.generate_output(RingBell); + }; + + if let Ok(entry) = entry { + let (slice1, slice2) = entry.get_slices(); + + self.buffer.truncate(); + unsafe { + self.buffer.insert_bytes(0, slice1).unwrap(); + self.buffer.insert_bytes(slice1.len(), slice2).unwrap(); + } + } else { + self.nav.reset(); + self.buffer.truncate(); + } + + self.generate_output(ClearAndPrintBuffer) + } + + // Advance state machine by one byte. Returns output iterator over + // 0 or more byte slices. + pub(crate) fn advance(&mut self, byte: u8) -> Output<'_, B, I> { + let action = self.parser.advance(byte); + + #[cfg(test)] + dbg!(action); + + match action { + Action::Print(c) => { + let pos = self.current_position(); + + if self.buffer.insert_utf8_char(pos, c).is_ok() { + self.generate_output(PrintBufferAndMoveCursorForward) + } else { + self.generate_output(RingBell) + } + } + Action::ControlCharacter(c) => match c { + CtrlA => self.generate_output(MoveCursor(CursorMove::Start)), + CtrlB => self.generate_output(MoveCursor(CursorMove::Back)), + CtrlC => self.generate_output(Abort), + CtrlD => { + let len = self.buffer.len(); + + if len > 0 { + let pos = self.current_position(); + + if pos < len { + self.buffer.delete(pos); + + self.generate_output(EraseAndPrintBuffer) + } else { + self.generate_output(RingBell) + } + } else { + self.generate_output(Abort) + } + } + CtrlE => self.generate_output(MoveCursor(CursorMove::End)), + CtrlF => self.generate_output(MoveCursor(CursorMove::Forward)), + CtrlK => { + let pos = self.current_position(); + + self.buffer.delete_after_char(pos); + + self.generate_output(EraseAfterCursor) + } + CtrlL => { + self.buffer.delete_after_char(0); + self.generate_output(ClearScreen) + } + CtrlN => self.history_move_down(), + CtrlP => self.history_move_up(), + CtrlT => { + let pos = self.current_position(); + + if pos > 0 && pos < self.buffer.as_str().chars().count() { + self.buffer.swap_chars(pos); + self.generate_output(MoveCursorBackAndPrintBufferAndMoveForward) + } else { + self.generate_output(RingBell) + } + } + CtrlU => { + self.buffer.delete_after_char(0); + self.generate_output(ClearLine) + } + CtrlW => { + let pos = self.current_position(); + let move_cursor = -(self.buffer.delete_previous_word(pos) as isize); + self.generate_output(MoveCursorAndEraseAndPrintBuffer(move_cursor)) + } + CarriageReturn | LineFeed => { + if self.buffer.len() > 0 { + let _ = self.nav.history.add_entry(self.buffer.as_str()); + } + + self.generate_output(Done) + } + CtrlH | Backspace => { + let pos = self.current_position(); + if pos > 0 { + self.buffer.delete(pos - 1); + self.generate_output(MoveCursorAndEraseAndPrintBuffer(-1)) + } else { + self.generate_output(RingBell) + } + } + _ => self.generate_output(RingBell), + }, + Action::ControlSequenceIntroducer(csi) => match csi { + CSI::CUF(_) => self.generate_output(MoveCursor(CursorMove::Forward)), + CSI::CUB(_) => self.generate_output(MoveCursor(CursorMove::Back)), + CSI::Home => self.generate_output(MoveCursor(CursorMove::Start)), + CSI::Delete => { + let len = self.buffer.len(); + let pos = self.current_position(); + + if pos < len { + self.buffer.delete(pos); + + self.generate_output(EraseAndPrintBuffer) + } else { + self.generate_output(RingBell) + } + } + CSI::End => self.generate_output(MoveCursor(CursorMove::End)), + CSI::CPR(row, column) => { + let cursor = Cursor::new(row - 1, column - 1); + self.terminal.reset(cursor); + self.generate_output(Nothing) + } + CSI::Unknown(_) => self.generate_output(RingBell), + CSI::CUU(_) => self.history_move_up(), + CSI::CUD(_) => self.history_move_down(), + CSI::CUP(_, _) => self.generate_output(RingBell), + CSI::ED(_) => self.generate_output(RingBell), + CSI::DSR => self.generate_output(RingBell), + CSI::SU(_) => self.generate_output(RingBell), + CSI::SD(_) => self.generate_output(RingBell), + CSI::Invalid => self.generate_output(RingBell), + }, + Action::EscapeSequence(_) => self.generate_output(RingBell), + Action::Ignore => self.generate_output(Nothing), + Action::InvalidUtf8 => self.generate_output(RingBell), + } + } +} diff --git a/noline/src/core/mod.rs b/noline/src/core/mod.rs new file mode 100644 index 0000000..fd7c104 --- /dev/null +++ b/noline/src/core/mod.rs @@ -0,0 +1,17 @@ +//! Core library containing the components for building an editor. +//! +//! Use [`Initializer`] to get [`crate::terminal::Terminal`] and then +//! use [`Line`] to read a single line. + +mod line; +mod prompt; +mod reset_handle; +mod str_iter; + +pub use line::*; +pub use prompt::*; +pub use reset_handle::*; +pub use str_iter::*; + +#[cfg(test)] +pub(crate) mod tests; diff --git a/noline/src/core/prompt.rs b/noline/src/core/prompt.rs new file mode 100644 index 0000000..557cd9c --- /dev/null +++ b/noline/src/core/prompt.rs @@ -0,0 +1,47 @@ +use super::StrIter; + +#[cfg_attr(test, derive(Debug))] +pub struct Prompt { + parts: I, + len: usize, +} + +impl<'a, I> Prompt +where + I: Iterator + Clone, +{ + fn new(parts: I) -> Self { + Self { + len: parts.clone().map(|part| part.len()).sum(), + parts, + } + } + + pub fn len(&self) -> usize { + self.len + } +} + +impl<'a, I> Prompt +where + I: Iterator + Clone, +{ + pub fn iter(&self) -> I { + self.parts.clone() + } +} + +impl<'a> From<&'a str> for Prompt> { + fn from(value: &'a str) -> Self { + Self::new(StrIter { s: Some(value) }) + } +} + +impl<'a, I> From for Prompt +where + I: Iterator + Clone, +{ + fn from(value: I) -> Self { + Self::new(value) + } +} diff --git a/noline/src/core/reset_handle.rs b/noline/src/core/reset_handle.rs new file mode 100644 index 0000000..b65fb8c --- /dev/null +++ b/noline/src/core/reset_handle.rs @@ -0,0 +1,67 @@ +use crate::history::History; +use crate::input::{Action, CSI}; +use crate::line_buffer::Buffer; +use crate::output::{Output, OutputAction}; +use crate::terminal::Cursor; + +use super::Line; +use OutputAction::*; + +enum ResetState { + New, + GetSize, + GetPosition, + Done, +} + +pub struct ResetHandle<'line, 'a, B: Buffer, H: History, I> { + line: &'line mut Line<'a, B, H, I>, + state: ResetState, +} + +impl<'line, 'a, 'item, 'output, B, H, I> ResetHandle<'line, 'a, B, H, I> +where + I: Iterator + Clone + 'a, + B: Buffer, + H: History, + 'item: 'output, +{ + pub(super) fn new(line: &'line mut Line<'a, B, H, I>) -> Self { + Self { + line, + state: ResetState::New, + } + } + + pub fn start(&mut self) -> Output<'_, B, I> { + assert!(matches!(self.state, ResetState::New)); + self.state = ResetState::GetSize; + + self.line.generate_output(ProbeSize) + } + + pub fn advance(&mut self, byte: u8) -> Option> { + let action = self.line.parser.advance(byte); + + match action { + Action::ControlSequenceIntroducer(CSI::CPR(x, y)) => match self.state { + ResetState::New => panic!("Invalid state"), + ResetState::GetSize => { + self.line.terminal.resize(x, y); + self.state = ResetState::GetPosition; + Some(self.line.generate_output(ClearAndPrintPrompt)) + } + ResetState::GetPosition => { + #[cfg(test)] + dbg!(x, y); + self.line.terminal.reset(Cursor::new(x - 1, y - 1)); + self.state = ResetState::Done; + None + } + ResetState::Done => panic!("Invalid state"), + }, + Action::Ignore => Some(self.line.generate_output(Nothing)), + _ => None, + } + } +} diff --git a/noline/src/core/str_iter.rs b/noline/src/core/str_iter.rs new file mode 100644 index 0000000..8833e31 --- /dev/null +++ b/noline/src/core/str_iter.rs @@ -0,0 +1,12 @@ +#[derive(Clone)] +pub struct StrIter<'a> { + pub(super) s: Option<&'a str>, +} + +impl<'a> Iterator for StrIter<'a> { + type Item = &'a str; + + fn next(&mut self) -> Option { + self.s.take() + } +} diff --git a/noline/src/core/tests.rs b/noline/src/core/tests.rs new file mode 100644 index 0000000..c7d2a0f --- /dev/null +++ b/noline/src/core/tests.rs @@ -0,0 +1,589 @@ +use super::*; +use crate::history::{History, NoHistory, SliceHistory, UnboundedHistory}; +use crate::input::ControlCharacter::*; +use crate::line_buffer::{Buffer, LineBuffer, UnboundedBuffer}; +use crate::terminal::{Cursor, Terminal}; +use crate::testlib::{csi, MockTerminal, ToByteVec}; +use std::string::String; +use std::vec::Vec; + +struct Editor { + buffer: LineBuffer, + terminal: Terminal, + history: H, +} + +impl Editor { + fn new(buffer: LineBuffer, history: H) -> Self { + let terminal = Terminal::default(); + + Self { + buffer, + terminal, + history, + } + } + + fn get_line( + &mut self, + prompt: &'static str, + mockterm: &mut MockTerminal, + ) -> Line<'_, B, H, StrIter> { + let cursor = mockterm.get_cursor(); + let mut line = Line::new( + prompt, + &mut self.buffer, + &mut self.terminal, + &mut self.history, + ); + + let mut reset = line.reset(); + + let mut reset_start: Vec = reset + .start() + .into_iter() + .filter_map(|item| item.get_bytes().map(|bytes| bytes.to_vec())) + .flatten() + .collect(); + + while !reset_start.is_empty() { + let term_response: Vec = reset_start + .into_iter() + .filter_map(|b| mockterm.advance(b)) + .flat_map(|output| output.into_iter()) + .collect(); + + reset_start = term_response + .iter() + .copied() + .filter_map(|b| { + reset.advance(b).map(|output| { + output + .into_iter() + .map(|item| item.get_bytes().map(|bytes| bytes.to_vec())) + .collect::>() + }) + }) + .flatten() + .flatten() + .flatten() + .collect(); + } + + assert_eq!(mockterm.current_line_as_string(), prompt); + assert_eq!(mockterm.get_cursor(), Cursor::new(cursor.row, prompt.len())); + + line + } +} + +fn advance<'a, B: Buffer, H: History>( + terminal: &mut MockTerminal, + noline: &mut Line<'a, B, H, StrIter<'a>>, + input: impl ToByteVec, +) -> core::result::Result<(), ()> { + terminal.bell = false; + + for input in input.to_byte_vec() { + for item in noline.advance(input) { + if let Some(bytes) = item.get_bytes() { + for &b in bytes { + terminal.advance(b); + } + } + } + } + + assert_eq!(noline.terminal.get_cursor(), terminal.cursor); + + dbg!(terminal.screen_as_string()); + + if terminal.bell { + Err(()) + } else { + Ok(()) + } +} + +fn get_terminal_and_editor( + rows: usize, + columns: usize, + origin: Cursor, +) -> (MockTerminal, Editor) { + let terminal = MockTerminal::new(rows, columns, origin); + + let editor = Editor::new(LineBuffer::new_unbounded(), NoHistory {}); + + assert_eq!(terminal.get_cursor(), origin); + + (terminal, editor) +} + +#[test] +fn reset() { + let prompt = "> "; + let (terminal, mut editor) = get_terminal_and_editor(4, 10, Cursor::new(1, 0)); + let mut line = Line::new( + prompt, + &mut editor.buffer, + &mut editor.terminal, + &mut editor.history, + ); + + dbg!(terminal.get_cursor()); + + let mut reset = line.reset(); + + let probe = reset + .start() + .into_iter() + .flat_map(|item| item.get_bytes().unwrap().to_vec()) + .collect::>(); + + assert_eq!(probe, b"\x1b7\x1b[999;999H\x1b[6n\x1b8"); + + let output = b"\x1b[91;45R" + .iter() + .copied() + .flat_map(|b| reset.advance(b).unwrap().into_vec()) + .collect::>(); + + dbg!(terminal.get_cursor()); + + assert_eq!(output, b"\r\x1b[J> \x1b[6n"); + + let output = b"\x1b[2;3R" + .iter() + .copied() + .flat_map(|b| { + if let Some(output) = reset.advance(b) { + output.into_vec() + } else { + Vec::new() + } + }) + .collect::>(); + + dbg!(terminal.get_cursor()); + + assert_eq!(output, b""); + + assert_eq!(line.terminal.get_size(), (91, 45)); +} + +#[test] +fn mock_editor() { + let prompt = "> "; + let (mut terminal, mut editor) = get_terminal_and_editor(4, 20, Cursor::new(1, 0)); + + assert_eq!(terminal.get_cursor(), Cursor::new(1, 0)); + + let line = editor.get_line(prompt, &mut terminal); + + dbg!(&line.terminal); + assert_eq!(terminal.get_cursor(), line.terminal.get_cursor()); +} + +#[test] +fn movecursor() { + let prompt = "> "; + let (mut terminal, mut editor) = get_terminal_and_editor(4, 10, Cursor::new(1, 0)); + + let mut line = editor.get_line(prompt, &mut terminal); + + advance(&mut terminal, &mut line, "Hello, World!").unwrap(); + assert_eq!(terminal.get_cursor(), Cursor::new(2, 5)); + + advance(&mut terminal, &mut line, [csi::LEFT; 6]).unwrap(); + assert_eq!(terminal.get_cursor(), Cursor::new(1, 9)); + + advance(&mut terminal, &mut line, CtrlA).unwrap(); + + assert_eq!(terminal.get_cursor(), Cursor::new(1, 2)); + + assert!(advance(&mut terminal, &mut line, csi::LEFT).is_err()); + + assert_eq!(terminal.get_cursor(), Cursor::new(1, 2)); + + advance(&mut terminal, &mut line, CtrlE).unwrap(); + + assert_eq!(terminal.get_cursor(), Cursor::new(2, 5)); + + assert!(advance(&mut terminal, &mut line, csi::RIGHT).is_err()); + + assert_eq!(terminal.get_cursor(), Cursor::new(2, 5)); + + advance(&mut terminal, &mut line, csi::HOME).unwrap(); + + assert_eq!(terminal.get_cursor(), Cursor::new(1, 2)); + + advance(&mut terminal, &mut line, csi::END).unwrap(); + + assert_eq!(terminal.get_cursor(), Cursor::new(2, 5)); +} + +#[test] +fn cursor_scroll() { + let prompt = "> "; + let (mut terminal, mut editor) = get_terminal_and_editor(4, 10, Cursor::new(3, 0)); + + let mut line = editor.get_line(prompt, &mut terminal); + + advance(&mut terminal, &mut line, "23456789").unwrap(); + assert_eq!(terminal.get_cursor(), Cursor::new(3, 0)); +} + +#[test] +fn clear_line() { + let prompt = "> "; + let (mut terminal, mut editor) = get_terminal_and_editor(4, 20, Cursor::new(1, 0)); + + let mut line = editor.get_line(prompt, &mut terminal); + + dbg!(terminal.get_cursor()); + dbg!(&line.terminal); + assert_eq!(terminal.get_cursor(), Cursor::new(1, 2)); + dbg!(terminal.screen_as_string()); + + advance(&mut terminal, &mut line, "Hello, World!").unwrap(); + assert_eq!(terminal.get_cursor(), Cursor::new(1, 15)); + assert_eq!(terminal.screen_as_string(), "> Hello, World!"); + + advance(&mut terminal, &mut line, CtrlU).unwrap(); + assert_eq!(terminal.get_cursor(), Cursor::new(1, 2)); + assert_eq!(terminal.screen_as_string(), "> "); + + advance(&mut terminal, &mut line, "Hello, World!").unwrap(); + assert_eq!(terminal.get_cursor(), Cursor::new(1, 15)); + assert_eq!(terminal.screen_as_string(), "> Hello, World!"); +} + +#[test] +fn clear_screen() { + let prompt = "> "; + let (mut terminal, mut editor) = get_terminal_and_editor(4, 20, Cursor::new(1, 0)); + + let mut line = editor.get_line(prompt, &mut terminal); + + advance(&mut terminal, &mut line, "Hello, World!").unwrap(); + assert_eq!(terminal.get_cursor(), Cursor::new(1, 15)); + assert_eq!(terminal.screen_as_string(), "> Hello, World!"); + + advance(&mut terminal, &mut line, CtrlL).unwrap(); + assert_eq!(terminal.get_cursor(), Cursor::new(0, 2)); + assert_eq!(terminal.screen_as_string(), "> "); + + advance(&mut terminal, &mut line, "Hello, World!").unwrap(); + assert_eq!(terminal.get_cursor(), Cursor::new(0, 15)); + assert_eq!(terminal.screen_as_string(), "> Hello, World!"); +} + +#[test] +fn scroll() { + let prompt = "> "; + let (mut terminal, mut editor) = get_terminal_and_editor(4, 10, Cursor::new(0, 0)); + + let mut line = editor.get_line(prompt, &mut terminal); + + advance(&mut terminal, &mut line, "aaaaaaaa").unwrap(); + advance(&mut terminal, &mut line, "bbbbbbbbbb").unwrap(); + advance(&mut terminal, &mut line, "cccccccccc").unwrap(); + advance(&mut terminal, &mut line, "ddddddddd").unwrap(); + + assert_eq!(terminal.get_cursor(), Cursor::new(3, 9)); + + assert_eq!( + terminal.screen_as_string(), + "> aaaaaaaa\nbbbbbbbbbb\ncccccccccc\nddddddddd" + ); + + advance(&mut terminal, &mut line, "d").unwrap(); + + assert_eq!(terminal.get_cursor(), Cursor::new(3, 0)); + + assert_eq!( + terminal.screen_as_string(), + "bbbbbbbbbb\ncccccccccc\ndddddddddd" + ); + + advance(&mut terminal, &mut line, "eeeeeeeeee").unwrap(); + + assert_eq!( + terminal.screen_as_string(), + "cccccccccc\ndddddddddd\neeeeeeeeee" + ); + + // advance(&mut terminal, &mut noline, CtrlA); + + // assert_eq!(terminal.get_cursor(), Cursor::new(0, 2)); + // assert_eq!( + // terminal.screen_as_string(), + // "> aaaaaaaa\nbbbbbbbbbb\ncccccccccc\ndddddddddd" + // ); + + // advance(&mut terminal, &mut noline, CtrlE); + // assert_eq!(terminal.get_cursor(), Cursor::new(3, 0)); + // assert_eq!( + // terminal.screen_as_string(), + // "cccccccccc\ndddddddddd\neeeeeeeeee" + // ); +} + +#[test] +fn swap() { + let prompt = "> "; + let (mut terminal, mut editor) = get_terminal_and_editor(4, 10, Cursor::new(0, 0)); + + let mut line = editor.get_line(prompt, &mut terminal); + + advance(&mut terminal, &mut line, "æøå").unwrap(); + assert_eq!(terminal.screen_as_string(), "> æøå"); + assert_eq!(terminal.get_cursor(), Cursor::new(0, 5)); + + assert!(advance(&mut terminal, &mut line, CtrlT).is_err()); + + assert_eq!(terminal.screen_as_string(), "> æøå"); + assert_eq!(terminal.get_cursor(), Cursor::new(0, 5)); + + advance(&mut terminal, &mut line, csi::LEFT).unwrap(); + + assert_eq!(terminal.get_cursor(), Cursor::new(0, 4)); + + advance(&mut terminal, &mut line, CtrlT).unwrap(); + + assert_eq!(line.buffer.as_str(), "æåø"); + assert_eq!(terminal.screen_as_string(), "> æåø"); + assert_eq!(terminal.get_cursor(), Cursor::new(0, 4)); + + advance(&mut terminal, &mut line, CtrlA).unwrap(); + + assert_eq!(terminal.get_cursor(), Cursor::new(0, 2)); + + assert!(advance(&mut terminal, &mut line, CtrlT).is_err()); + assert_eq!(terminal.screen_as_string(), "> æåø"); +} + +#[test] +fn erase_after_cursor() { + let prompt = "> "; + let (mut terminal, mut editor) = get_terminal_and_editor(4, 10, Cursor::new(0, 0)); + + let mut line = editor.get_line(prompt, &mut terminal); + + advance(&mut terminal, &mut line, "rm -rf /").unwrap(); + assert_eq!(terminal.get_cursor(), Cursor::new(1, 0)); + assert_eq!(terminal.screen_as_string(), "> rm -rf /"); + + advance(&mut terminal, &mut line, CtrlA).unwrap(); + advance(&mut terminal, &mut line, [CtrlF; 3]).unwrap(); + + assert_eq!(terminal.get_cursor(), Cursor::new(0, 5)); + + advance(&mut terminal, &mut line, CtrlK).unwrap(); + assert_eq!(line.buffer.as_str(), "rm "); + assert_eq!(terminal.get_cursor(), Cursor::new(0, 5)); + assert_eq!(terminal.screen_as_string(), "> rm "); +} + +#[test] +fn delete_previous_word() { + let prompt = "> "; + let (mut terminal, mut editor) = get_terminal_and_editor(1, 40, Cursor::new(0, 0)); + + let mut line = editor.get_line(prompt, &mut terminal); + + advance(&mut terminal, &mut line, "rm file1 file2 file3").unwrap(); + assert_eq!(terminal.screen_as_string(), "> rm file1 file2 file3"); + + advance(&mut terminal, &mut line, [CtrlB; 5]).unwrap(); + + advance(&mut terminal, &mut line, CtrlW).unwrap(); + assert_eq!(terminal.get_cursor(), Cursor::new(0, 11)); + assert_eq!(line.buffer.as_str(), "rm file1 file3"); + assert_eq!(terminal.screen_as_string(), "> rm file1 file3"); + + advance(&mut terminal, &mut line, CtrlW).unwrap(); + assert_eq!(terminal.screen_as_string(), "> rm file3"); + assert_eq!(terminal.get_cursor(), Cursor::new(0, 5)); +} + +#[test] +fn delete() { + let prompt = "> "; + let (mut terminal, mut editor) = get_terminal_and_editor(1, 40, Cursor::new(0, 0)); + + let mut line = editor.get_line(prompt, &mut terminal); + + assert_eq!(terminal.get_cursor(), Cursor::new(0, 2)); + assert_eq!(terminal.screen_as_string(), "> "); + advance(&mut terminal, &mut line, "abcde").unwrap(); + + advance(&mut terminal, &mut line, CtrlD).unwrap_err(); + + advance(&mut terminal, &mut line, CtrlA).unwrap(); + + advance(&mut terminal, &mut line, CtrlD).unwrap(); + assert_eq!(line.buffer.as_str(), "bcde"); + assert_eq!(terminal.screen_as_string(), "> bcde"); + + advance(&mut terminal, &mut line, [csi::RIGHT; 3]).unwrap(); + advance(&mut terminal, &mut line, CtrlD).unwrap(); + assert_eq!(line.buffer.as_str(), "bcd"); + assert_eq!(terminal.screen_as_string(), "> bcd"); + + advance(&mut terminal, &mut line, CtrlD).unwrap_err(); + + advance(&mut terminal, &mut line, CtrlA).unwrap(); + + advance(&mut terminal, &mut line, csi::DELETE).unwrap(); + assert_eq!(line.buffer.as_str(), "cd"); + assert_eq!(terminal.screen_as_string(), "> cd"); + + advance(&mut terminal, &mut line, csi::DELETE).unwrap(); + assert_eq!(line.buffer.as_str(), "d"); + assert_eq!(terminal.screen_as_string(), "> d"); +} + +#[test] +fn backspace() { + let prompt = "> "; + let (mut terminal, mut editor) = get_terminal_and_editor(1, 40, Cursor::new(0, 0)); + + let mut line = editor.get_line(prompt, &mut terminal); + + assert!(advance(&mut terminal, &mut line, Backspace).is_err()); + + assert_eq!(terminal.get_cursor(), Cursor::new(0, 2)); + assert_eq!(terminal.screen_as_string(), "> "); + advance(&mut terminal, &mut line, "hello").unwrap(); + + advance(&mut terminal, &mut line, Backspace).unwrap(); + assert_eq!(line.buffer.as_str(), "hell"); + assert_eq!(terminal.screen_as_string(), "> hell"); + + advance(&mut terminal, &mut line, [csi::LEFT; 2]).unwrap(); + advance(&mut terminal, &mut line, Backspace).unwrap(); + assert_eq!(line.buffer.as_str(), "hll"); + assert_eq!(terminal.screen_as_string(), "> hll"); + + advance(&mut terminal, &mut line, CtrlA).unwrap(); + advance(&mut terminal, &mut line, Backspace).unwrap_err(); +} + +#[test] +fn slice_buffer() { + let mut array = [0; 20]; + let mut terminal = MockTerminal::new(20, 80, Cursor::new(0, 0)); + let mut editor: Editor<_, NoHistory> = + Editor::new(LineBuffer::from_slice(&mut array), NoHistory {}); + + let mut line = editor.get_line("> ", &mut terminal); + + let input: String = (0..20).map(|_| "a").collect(); + + advance(&mut terminal, &mut line, input.as_str()).unwrap(); + + assert_eq!(advance(&mut terminal, &mut line, "a"), Err(())); + + assert_eq!(line.buffer.as_str(), input); + + advance(&mut terminal, &mut line, Backspace).unwrap(); +} + +#[test] +fn history() { + fn test(history: H) { + let mut terminal = MockTerminal::new(20, 80, Cursor::new(0, 0)); + let mut editor: Editor<_, H> = Editor::new(LineBuffer::new_unbounded(), history); + + let mut line = editor.get_line("> ", &mut terminal); + + advance(&mut terminal, &mut line, "this is a line\r").unwrap(); + + let mut line = editor.get_line("> ", &mut terminal); + + assert_eq!(terminal.screen_as_string(), "> this is a line\n> "); + + assert!(advance(&mut terminal, &mut line, csi::UP).is_ok()); + + assert_eq!( + terminal.screen_as_string(), + "> this is a line\n> this is a line" + ); + + assert!(advance(&mut terminal, &mut line, csi::DOWN).is_ok()); + + assert_eq!(terminal.screen_as_string(), "> this is a line\n> "); + + advance(&mut terminal, &mut line, "another line\r").unwrap(); + + let mut line = editor.get_line("> ", &mut terminal); + advance(&mut terminal, &mut line, "yet another line\r").unwrap(); + + let mut line = editor.get_line("> ", &mut terminal); + + assert_eq!( + terminal.screen_as_string(), + "> this is a line\n> another line\n> yet another line\n> " + ); + + assert!(advance(&mut terminal, &mut line, csi::UP).is_ok()); + + assert_eq!( + terminal.screen_as_string(), + "> this is a line\n> another line\n> yet another line\n> yet another line" + ); + + assert!(advance(&mut terminal, &mut line, csi::UP).is_ok()); + + assert_eq!( + terminal.screen_as_string(), + "> this is a line\n> another line\n> yet another line\n> another line" + ); + + assert!(advance(&mut terminal, &mut line, csi::UP).is_ok()); + + assert_eq!( + terminal.screen_as_string(), + "> this is a line\n> another line\n> yet another line\n> this is a line" + ); + + assert!(advance(&mut terminal, &mut line, csi::UP).is_err()); + + assert_eq!( + terminal.screen_as_string(), + "> this is a line\n> another line\n> yet another line\n> this is a line" + ); + + assert!(advance(&mut terminal, &mut line, csi::DOWN).is_ok()); + + assert_eq!( + terminal.screen_as_string(), + "> this is a line\n> another line\n> yet another line\n> another line" + ); + + assert!(advance(&mut terminal, &mut line, csi::DOWN).is_ok()); + + assert_eq!( + terminal.screen_as_string(), + "> this is a line\n> another line\n> yet another line\n> yet another line" + ); + assert!(advance(&mut terminal, &mut line, csi::DOWN).is_ok()); + + assert_eq!( + terminal.screen_as_string(), + "> this is a line\n> another line\n> yet another line\n> " + ); + + assert!(advance(&mut terminal, &mut line, csi::DOWN).is_err()); + + assert_eq!( + terminal.screen_as_string(), + "> this is a line\n> another line\n> yet another line\n> " + ); + } + + test(UnboundedHistory::new()); + let mut buffer = [0; 128]; + test(SliceHistory::new(&mut buffer)); +} diff --git a/noline/src/async_editor.rs b/noline/src/editor/async_editor.rs similarity index 100% rename from noline/src/async_editor.rs rename to noline/src/editor/async_editor.rs diff --git a/noline/src/builder.rs b/noline/src/editor/builder.rs similarity index 98% rename from noline/src/builder.rs rename to noline/src/editor/builder.rs index a73f9f8..138aee3 100644 --- a/noline/src/builder.rs +++ b/noline/src/editor/builder.rs @@ -2,12 +2,11 @@ use core::marker::PhantomData; +use super::{async_editor, sync_editor}; use crate::{ - async_editor, error::NolineError, history::{History, NoHistory, SliceHistory}, line_buffer::{Buffer, LineBuffer, NoBuffer, SliceBuffer}, - sync_editor, }; #[cfg(any(test, doc, feature = "alloc", feature = "std"))] diff --git a/noline/src/editor/mod.rs b/noline/src/editor/mod.rs new file mode 100644 index 0000000..7ba9df1 --- /dev/null +++ b/noline/src/editor/mod.rs @@ -0,0 +1,5 @@ +mod async_editor; +pub mod builder; +mod sync_editor; + +pub use builder::*; diff --git a/noline/src/sync_editor.rs b/noline/src/editor/sync_editor.rs similarity index 99% rename from noline/src/sync_editor.rs rename to noline/src/editor/sync_editor.rs index 1db66c3..4b08e13 100644 --- a/noline/src/sync_editor.rs +++ b/noline/src/editor/sync_editor.rs @@ -4,16 +4,15 @@ //! traits. //! //! Use the [`crate::builder::EditorBuilder`] to build an editor. -use embedded_io::{Read, ReadExactError, Write}; +#![allow(elided_named_lifetimes)] +use crate::core::{Line, Prompt}; use crate::error::NolineError; - use crate::history::{get_history_entries, CircularSlice, History}; use crate::line_buffer::{Buffer, LineBuffer}; - -use crate::core::{Line, Prompt}; use crate::output::{Output, OutputItem}; use crate::terminal::Terminal; +use embedded_io::{Read, ReadExactError, Write}; /// Line editor for synchronous IO /// diff --git a/noline/src/history.rs b/noline/src/history.rs deleted file mode 100644 index 0efa249..0000000 --- a/noline/src/history.rs +++ /dev/null @@ -1,601 +0,0 @@ -//! Line history - -use core::{ - iter::{Chain, Zip}, - ops::Range, - slice, -}; - -#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] -#[cfg_attr(test, derive(Debug))] -struct CircularIndex { - index: usize, - size: usize, -} - -impl CircularIndex { - fn new(index: usize, size: usize) -> Self { - Self { index, size } - } - - fn set(&mut self, index: usize) { - self.index = index; - } - - fn add(&mut self, value: usize) { - self.set(self.index + value); - } - - fn increment(&mut self) { - self.add(1); - } - - fn index(&self) -> usize { - self.index % self.size - } - - fn diff(&self, other: CircularIndex) -> isize { - self.index as isize - other.index as isize - } -} - -struct Window { - size: usize, - start: CircularIndex, - end: CircularIndex, -} - -impl Window { - fn new(size: usize) -> Self { - let start = CircularIndex::new(0, size); - let end = CircularIndex::new(0, size); - Self { size, start, end } - } - - fn len(&self) -> usize { - self.end.diff(self.start) as usize - } - - fn widen(&mut self) { - self.end.increment(); - - if self.end.diff(self.start) as usize > self.size { - self.start.increment(); - } - } - - fn narrow(&mut self) { - if self.end.diff(self.start) > 0 { - self.start.increment(); - } - } - - fn start(&self) -> usize { - self.start.index() - } - - fn end(&self) -> usize { - self.end.index() - } -} - -#[cfg_attr(test, derive(Debug))] -enum CircularRange { - Consecutive(Range), - Split(Range, Range), -} - -impl CircularRange { - fn new(start: usize, end: usize, len: usize, capacity: usize) -> Self { - assert!(start <= capacity); - assert!(end <= capacity); - - if len > 0 { - if start < end { - Self::Consecutive(start..end) - } else { - Self::Split(start..capacity, 0..end) - } - } else { - Self::Consecutive(start..end) - } - } - - pub fn get_ranges(&self) -> (Range, Range) { - match self { - CircularRange::Consecutive(range) => (range.clone(), 0..0), - CircularRange::Split(range1, range2) => (range1.clone(), range2.clone()), - } - } -} - -impl IntoIterator for CircularRange { - type Item = usize; - - type IntoIter = Chain, Range>; - - fn into_iter(self) -> Self::IntoIter { - let (range1, range2) = self.get_ranges(); - - range1.chain(range2) - } -} - -/// Slice of a circular buffer -/// -/// Consists of two separate consecutive slices if the circular slice -/// wraps around. -pub struct CircularSlice<'a> { - buffer: &'a [u8], - range: CircularRange, -} - -impl<'a> CircularSlice<'a> { - fn new(buffer: &'a [u8], start: usize, end: usize, len: usize) -> Self { - Self::from_range(buffer, CircularRange::new(start, end, len, buffer.len())) - } - - fn from_range(buffer: &'a [u8], range: CircularRange) -> Self { - Self { buffer, range } - } - - pub(crate) fn get_ranges(&self) -> (Range, Range) { - self.range.get_ranges() - } - - pub(crate) fn get_slices(&self) -> (&'a [u8], &'a [u8]) { - let (range1, range2) = self.get_ranges(); - - (&self.buffer[range1], &self.buffer[range2]) - } -} - -impl<'a> IntoIterator for CircularSlice<'a> { - type Item = (usize, &'a u8); - - type IntoIter = - Chain, slice::Iter<'a, u8>>, Zip, slice::Iter<'a, u8>>>; - - fn into_iter(self) -> Self::IntoIter { - let (range1, range2) = self.get_ranges(); - let (slice1, slice2) = self.get_slices(); - - range1.zip(slice1.iter()).chain(range2.zip(slice2.iter())) - } -} - -/// Trait for line history -pub trait History { - /// Return entry at index, or None if out of bounds - fn get_entry(&self, index: usize) -> Option>; - - /// Add new entry at the end - fn add_entry<'a>(&mut self, entry: &'a str) -> Result<(), &'a str>; - - /// Return number of entries in history - fn number_of_entries(&self) -> usize; - - /// Add entries from an iterator - fn load_entries<'a, I: Iterator>(&mut self, entries: I) -> usize { - entries - .take_while(|entry| self.add_entry(entry).is_ok()) - .count() - } -} - -/// Return an iterator over history entries -/// -/// # Note -/// -/// This should ideally be in the [`History`] trait, but is -/// until `type_alias_impl_trait` is stable. -pub(crate) fn get_history_entries( - history: &H, -) -> impl Iterator> { - (0..(history.number_of_entries())).filter_map(|index| history.get_entry(index)) -} - -/// Static history backed by array -pub struct SliceHistory<'a> { - buffer: &'a mut [u8], - window: Window, -} - -impl<'a> SliceHistory<'a> { - /// Create new static history - pub fn new(buffer: &'a mut [u8]) -> Self { - Self { - window: Window::new(buffer.len()), - buffer, - } - } - - fn get_available_range(&self) -> CircularRange { - let len = self.buffer.len(); - CircularRange::new(self.window.end(), self.window.end(), len, len) - } - - fn get_buffer(&self) -> CircularSlice<'_> { - CircularSlice::new( - self.buffer, - self.window.start(), - self.window.end(), - self.window.len(), - ) - } - - fn get_entry_ranges(&self) -> impl Iterator + '_ { - let delimeters = - self.get_buffer() - .into_iter() - .filter_map(|(index, b)| if *b == 0x0 { Some(index) } else { None }); - - [self.window.start()] - .into_iter() - .chain(delimeters.clone().map(|i| i + 1)) - .zip(delimeters.chain([self.window.end()])) - .filter_map(|(start, end)| { - if start != end { - Some(CircularRange::new( - start, - end, - self.window.len(), - self.buffer.len(), - )) - } else { - None - } - }) - } - - fn get_entries(&self) -> impl Iterator> { - self.get_entry_ranges() - .map(|range| CircularSlice::from_range(self.buffer, range)) - } -} - -impl<'a> History for SliceHistory<'a> { - fn add_entry<'b>(&mut self, entry: &'b str) -> Result<(), &'b str> { - if entry.len() + 1 > self.buffer.len() { - return Err(entry); - } - - for (_, b) in self - .get_available_range() - .into_iter() - .zip(entry.as_bytes().iter()) - { - self.buffer[self.window.end()] = *b; - self.window.widen(); - } - - if self.buffer[self.window.end()] != 0x0 { - self.buffer[self.window.end()] = 0x0; - - self.window.widen(); - - while self.buffer[self.window.start()] != 0x0 { - self.window.narrow(); - } - } else { - self.window.widen(); - } - - Ok(()) - } - - fn number_of_entries(&self) -> usize { - self.get_entries().count() - } - - fn get_entry(&self, index: usize) -> Option> { - self.get_entries().nth(index) - } -} - -/// Emtpy implementation for Editors with no history -pub struct NoHistory {} - -impl NoHistory { - pub fn new() -> Self { - Self {} - } -} - -impl Default for NoHistory { - fn default() -> Self { - Self::new() - } -} - -impl History for NoHistory { - fn get_entry(&self, _index: usize) -> Option> { - None - } - - fn add_entry<'a>(&mut self, entry: &'a str) -> Result<(), &'a str> { - Err(entry) - } - - fn number_of_entries(&self) -> usize { - 0 - } -} - -/// Wrapper used for history navigation in [`core::Line`] -pub(crate) struct HistoryNavigator<'a, H: History> { - pub(crate) history: &'a mut H, - position: Option, -} - -impl<'a, H: History> HistoryNavigator<'a, H> { - pub(crate) fn new(history: &'a mut H) -> Self { - Self { - history, - position: None, - } - } - - fn set_position(&mut self, position: usize) -> usize { - *self.position.insert(position) - } - - fn get_position(&mut self) -> usize { - *self - .position - .get_or_insert_with(|| self.history.number_of_entries()) - } - - pub(crate) fn move_up(&mut self) -> Result, ()> { - let position = self.get_position(); - - if position > 0 { - let position = self.set_position(position - 1); - - Ok(self.history.get_entry(position).unwrap()) - } else { - Err(()) - } - } - - pub(crate) fn move_down(&mut self) -> Result, ()> { - let new_position = self.get_position() + 1; - - if new_position < self.history.number_of_entries() { - let position = self.set_position(new_position); - - Ok(self.history.get_entry(position).unwrap()) - } else { - Err(()) - } - } - - pub(crate) fn reset(&mut self) { - self.position = None; - } - - pub(crate) fn is_active(&self) -> bool { - self.position.is_some() - } -} - -#[cfg(any(test, doc, feature = "alloc", feature = "std"))] -mod alloc { - use super::*; - use alloc::{ - string::{String, ToString}, - vec::Vec, - }; - - extern crate alloc; - - /// Unbounded history backed by [`Vec`] - pub struct UnboundedHistory { - buffer: Vec, - } - - impl UnboundedHistory { - pub fn new() -> Self { - Self { buffer: Vec::new() } - } - } - - impl Default for UnboundedHistory { - fn default() -> Self { - Self::new() - } - } - - impl History for UnboundedHistory { - fn get_entry(&self, index: usize) -> Option> { - let s = self.buffer[index].as_str(); - - Some(CircularSlice::new(s.as_bytes(), 0, s.len(), s.len())) - } - - fn add_entry<'a>(&mut self, entry: &'a str) -> Result<(), &'a str> { - self.buffer.push(entry.to_string()); - - #[cfg(test)] - dbg!(entry); - - Ok(()) - } - - fn number_of_entries(&self) -> usize { - self.buffer.len() - } - } -} - -#[cfg(any(test, doc, feature = "alloc", feature = "std"))] -pub use alloc::UnboundedHistory; - -#[cfg(test)] -mod tests { - use std::vec::Vec; - - use std::string::String; - - use super::*; - - impl<'a> FromIterator> for Vec { - fn from_iter>>(iter: T) -> Self { - iter.into_iter() - .map(|circular| { - let bytes = circular.into_iter().map(|(_, b)| *b).collect::>(); - String::from_utf8(bytes).unwrap() - }) - .collect() - } - } - - #[test] - fn circular_range() { - assert_eq!(CircularRange::new(0, 3, 10, 10).get_ranges(), (0..3, 0..0)); - assert_eq!(CircularRange::new(0, 0, 10, 10).get_ranges(), (0..10, 0..0)); - assert_eq!(CircularRange::new(0, 0, 0, 10).get_ranges(), (0..0, 0..0)); - assert_eq!(CircularRange::new(7, 3, 10, 10).get_ranges(), (7..10, 0..3)); - assert_eq!(CircularRange::new(0, 0, 10, 10).get_ranges(), (0..10, 0..0)); - assert_eq!( - CircularRange::new(0, 10, 10, 10).get_ranges(), - (0..10, 0..0) - ); - assert_eq!(CircularRange::new(9, 9, 10, 10).get_ranges(), (9..10, 0..9)); - assert_eq!( - CircularRange::new(10, 10, 10, 10).get_ranges(), - (10..10, 0..10) - ); - - assert_eq!(CircularRange::new(0, 10, 10, 10).into_iter().count(), 10); - assert_eq!(CircularRange::new(10, 10, 10, 10).into_iter().count(), 10); - assert_eq!(CircularRange::new(4, 4, 10, 10).into_iter().count(), 10); - } - - #[test] - fn circular_slice() { - assert_eq!( - CircularSlice::new("abcdef".as_bytes(), 0, 3, 6).get_slices(), - ("abc".as_bytes(), "".as_bytes()) - ); - - assert_eq!( - CircularSlice::new("abcdef".as_bytes(), 3, 0, 6).get_slices(), - ("def".as_bytes(), "".as_bytes()) - ); - - assert_eq!( - CircularSlice::new("abcdef".as_bytes(), 3, 3, 6).get_slices(), - ("def".as_bytes(), "abc".as_bytes()) - ); - - assert_eq!( - CircularSlice::new("abcdef".as_bytes(), 0, 6, 6).get_slices(), - ("abcdef".as_bytes(), "".as_bytes()) - ); - - assert_eq!( - CircularSlice::new("abcdef".as_bytes(), 0, 0, 6).get_slices(), - ("abcdef".as_bytes(), "".as_bytes()) - ); - - assert_eq!( - CircularSlice::new("abcdef".as_bytes(), 0, 0, 0).get_slices(), - ("".as_bytes(), "".as_bytes()) - ); - - assert_eq!( - CircularSlice::new("abcdef".as_bytes(), 6, 6, 6).get_slices(), - ("".as_bytes(), "abcdef".as_bytes()) - ); - } - - #[test] - fn static_history() { - let mut buffer = [0; 10]; - let mut history: SliceHistory = SliceHistory::new(&mut buffer); - - assert_eq!(history.get_available_range().get_ranges(), (0..10, 0..0)); - - assert_eq!( - history.get_entries().collect::>(), - Vec::::new() - ); - - history.add_entry("abc").unwrap(); - - // dbg!(history.start, history.end, history.len); - // dbg!(history.get_entry_ranges().collect::>()); - // dbg!(history.buffer); - - assert_eq!(history.get_entries().collect::>(), vec!["abc"]); - - history.add_entry("def").unwrap(); - - // dbg!(history.buffer); - - assert_eq!( - history.get_entries().collect::>(), - vec!["abc", "def"] - ); - - history.add_entry("ghi").unwrap(); - - dbg!( - history.window.start(), - history.window.end(), - history.window.len() - ); - - assert_eq!( - history.get_entries().collect::>(), - vec!["def", "ghi"] - ); - - history.add_entry("j").unwrap(); - - // dbg!(history.start, history.end, history.len); - - assert_eq!( - history.get_entries().collect::>(), - vec!["def", "ghi", "j"] - ); - - history.add_entry("012345678").unwrap(); - - assert_eq!( - history.get_entries().collect::>(), - vec!["012345678"] - ); - - assert!(history.add_entry("0123456789").is_err()); - - history.add_entry("abc").unwrap(); - - assert_eq!(history.get_entries().collect::>(), vec!["abc"]); - - history.add_entry("defgh").unwrap(); - - assert_eq!( - history.get_entries().collect::>(), - vec!["abc", "defgh"] - ); - } - - #[test] - fn navigator() { - let mut history = UnboundedHistory::new(); - let mut navigator = HistoryNavigator::new(&mut history); - - assert!(navigator.move_up().is_err()); - assert!(navigator.move_down().is_err()); - - navigator.history.add_entry("line 1").unwrap(); - navigator.reset(); - - assert!(navigator.move_up().is_ok()); - assert!(navigator.move_up().is_err()); - - assert!(navigator.move_down().is_err()); - } -} diff --git a/noline/src/history/alloc.rs b/noline/src/history/alloc.rs new file mode 100644 index 0000000..fc214fa --- /dev/null +++ b/noline/src/history/alloc.rs @@ -0,0 +1,45 @@ +use super::*; +use alloc::{ + string::{String, ToString}, + vec::Vec, +}; + +extern crate alloc; + +/// Unbounded history backed by [`Vec`] +pub struct UnboundedHistory { + buffer: Vec, +} + +impl UnboundedHistory { + pub fn new() -> Self { + Self { buffer: Vec::new() } + } +} + +impl Default for UnboundedHistory { + fn default() -> Self { + Self::new() + } +} + +impl History for UnboundedHistory { + fn get_entry(&self, index: usize) -> Option> { + let s = self.buffer[index].as_str(); + + Some(CircularSlice::new(s.as_bytes(), 0, s.len(), s.len())) + } + + fn add_entry<'a>(&mut self, entry: &'a str) -> Result<(), &'a str> { + self.buffer.push(entry.to_string()); + + #[cfg(test)] + dbg!(entry); + + Ok(()) + } + + fn number_of_entries(&self) -> usize { + self.buffer.len() + } +} diff --git a/noline/src/history/circ_index.rs b/noline/src/history/circ_index.rs new file mode 100644 index 0000000..d288830 --- /dev/null +++ b/noline/src/history/circ_index.rs @@ -0,0 +1,32 @@ +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] +#[cfg_attr(test, derive(Debug))] +pub(super) struct CircularIndex { + index: usize, + size: usize, +} + +impl CircularIndex { + pub(super) fn new(index: usize, size: usize) -> Self { + Self { index, size } + } + + fn set(&mut self, index: usize) { + self.index = index; + } + + fn add(&mut self, value: usize) { + self.set(self.index + value); + } + + pub(super) fn increment(&mut self) { + self.add(1); + } + + pub(super) fn index(&self) -> usize { + self.index % self.size + } + + pub(super) fn diff(&self, other: CircularIndex) -> isize { + self.index as isize - other.index as isize + } +} diff --git a/noline/src/history/circ_range.rs b/noline/src/history/circ_range.rs new file mode 100644 index 0000000..94a3843 --- /dev/null +++ b/noline/src/history/circ_range.rs @@ -0,0 +1,43 @@ +use core::{iter::Chain, ops::Range}; + +#[cfg_attr(test, derive(Debug))] +pub(super) enum CircularRange { + Consecutive(Range), + Split(Range, Range), +} + +impl CircularRange { + pub(super) fn new(start: usize, end: usize, len: usize, capacity: usize) -> Self { + assert!(start <= capacity); + assert!(end <= capacity); + + if len > 0 { + if start < end { + Self::Consecutive(start..end) + } else { + Self::Split(start..capacity, 0..end) + } + } else { + Self::Consecutive(start..end) + } + } + + pub fn get_ranges(&self) -> (Range, Range) { + match self { + CircularRange::Consecutive(range) => (range.clone(), 0..0), + CircularRange::Split(range1, range2) => (range1.clone(), range2.clone()), + } + } +} + +impl IntoIterator for CircularRange { + type Item = usize; + + type IntoIter = Chain, Range>; + + fn into_iter(self) -> Self::IntoIter { + let (range1, range2) = self.get_ranges(); + + range1.chain(range2) + } +} diff --git a/noline/src/history/circ_slice.rs b/noline/src/history/circ_slice.rs new file mode 100644 index 0000000..d337d1b --- /dev/null +++ b/noline/src/history/circ_slice.rs @@ -0,0 +1,49 @@ +use super::circ_range::CircularRange; +use core::{ + iter::{Chain, Zip}, + ops::Range, + slice, +}; + +/// Slice of a circular buffer +/// +/// Consists of two separate consecutive slices if the circular slice +/// wraps around. +pub struct CircularSlice<'a> { + buffer: &'a [u8], + range: CircularRange, +} + +impl<'a> CircularSlice<'a> { + pub(super) fn new(buffer: &'a [u8], start: usize, end: usize, len: usize) -> Self { + Self::from_range(buffer, CircularRange::new(start, end, len, buffer.len())) + } + + pub(super) fn from_range(buffer: &'a [u8], range: CircularRange) -> Self { + Self { buffer, range } + } + + pub(crate) fn get_ranges(&self) -> (Range, Range) { + self.range.get_ranges() + } + + pub(crate) fn get_slices(&self) -> (&'a [u8], &'a [u8]) { + let (range1, range2) = self.get_ranges(); + + (&self.buffer[range1], &self.buffer[range2]) + } +} + +impl<'a> IntoIterator for CircularSlice<'a> { + type Item = (usize, &'a u8); + + type IntoIter = + Chain, slice::Iter<'a, u8>>, Zip, slice::Iter<'a, u8>>>; + + fn into_iter(self) -> Self::IntoIter { + let (range1, range2) = self.get_ranges(); + let (slice1, slice2) = self.get_slices(); + + range1.zip(slice1.iter()).chain(range2.zip(slice2.iter())) + } +} diff --git a/noline/src/history/history.rs b/noline/src/history/history.rs new file mode 100644 index 0000000..400058a --- /dev/null +++ b/noline/src/history/history.rs @@ -0,0 +1,32 @@ +use super::CircularSlice; + +/// Trait for line history +pub trait History { + /// Return entry at index, or None if out of bounds + fn get_entry(&self, index: usize) -> Option>; + + /// Add new entry at the end + fn add_entry<'a>(&mut self, entry: &'a str) -> Result<(), &'a str>; + + /// Return number of entries in history + fn number_of_entries(&self) -> usize; + + /// Add entries from an iterator + fn load_entries<'a, I: Iterator>(&mut self, entries: I) -> usize { + entries + .take_while(|entry| self.add_entry(entry).is_ok()) + .count() + } +} + +/// Return an iterator over history entries +/// +/// # Note +/// +/// This should ideally be in the [`History`] trait, but is +/// until `type_alias_impl_trait` is stable. +pub(crate) fn get_history_entries( + history: &H, +) -> impl Iterator> { + (0..(history.number_of_entries())).filter_map(|index| history.get_entry(index)) +} diff --git a/noline/src/history/history_nav.rs b/noline/src/history/history_nav.rs new file mode 100644 index 0000000..d84bf99 --- /dev/null +++ b/noline/src/history/history_nav.rs @@ -0,0 +1,58 @@ +use super::{CircularSlice, History}; + +/// Wrapper used for history navigation in [`core::Line`] +pub(crate) struct HistoryNavigator<'a, H: History> { + pub(crate) history: &'a mut H, + position: Option, +} + +impl<'a, H: History> HistoryNavigator<'a, H> { + pub(crate) fn new(history: &'a mut H) -> Self { + Self { + history, + position: None, + } + } + + fn set_position(&mut self, position: usize) -> usize { + *self.position.insert(position) + } + + fn get_position(&mut self) -> usize { + *self + .position + .get_or_insert_with(|| self.history.number_of_entries()) + } + + pub(crate) fn move_up(&mut self) -> Result, ()> { + let position = self.get_position(); + + if position > 0 { + let position = self.set_position(position - 1); + + Ok(self.history.get_entry(position).unwrap()) + } else { + Err(()) + } + } + + pub(crate) fn move_down(&mut self) -> Result, ()> { + let new_position = self.get_position() + 1; + + if new_position < self.history.number_of_entries() { + let position = self.set_position(new_position); + + Ok(self.history.get_entry(position).unwrap()) + } else { + Err(()) + } + } + + pub(crate) fn reset(&mut self) { + self.position = None; + } + + pub(crate) fn is_active(&self) -> bool { + self.position.is_some() + } +} diff --git a/noline/src/history/mod.rs b/noline/src/history/mod.rs new file mode 100644 index 0000000..c867ea9 --- /dev/null +++ b/noline/src/history/mod.rs @@ -0,0 +1,25 @@ +//! Line history + +mod circ_index; +mod circ_range; +mod circ_slice; +mod history; +mod history_nav; +mod no_history; +mod slice_history; +mod window; + +pub use circ_slice::*; +pub use history::*; +pub(super) use history_nav::HistoryNavigator; +pub use no_history::*; +pub use slice_history::*; + +#[cfg(any(test, doc, feature = "alloc", feature = "std"))] +mod alloc; + +#[cfg(any(test, doc, feature = "alloc", feature = "std"))] +pub use alloc::UnboundedHistory; + +#[cfg(test)] +pub(crate) mod tests; diff --git a/noline/src/history/no_history.rs b/noline/src/history/no_history.rs new file mode 100644 index 0000000..ccc6ea6 --- /dev/null +++ b/noline/src/history/no_history.rs @@ -0,0 +1,30 @@ +use super::{CircularSlice, History}; + +/// Emtpy implementation for Editors with no history +pub struct NoHistory {} + +impl NoHistory { + pub fn new() -> Self { + Self {} + } +} + +impl Default for NoHistory { + fn default() -> Self { + Self::new() + } +} + +impl History for NoHistory { + fn get_entry(&self, _index: usize) -> Option> { + None + } + + fn add_entry<'a>(&mut self, entry: &'a str) -> Result<(), &'a str> { + Err(entry) + } + + fn number_of_entries(&self) -> usize { + 0 + } +} diff --git a/noline/src/history/slice_history.rs b/noline/src/history/slice_history.rs new file mode 100644 index 0000000..72c5f03 --- /dev/null +++ b/noline/src/history/slice_history.rs @@ -0,0 +1,99 @@ +use super::{circ_range::CircularRange, window::Window, CircularSlice, History}; + +/// Static history backed by array +pub struct SliceHistory<'a> { + buffer: &'a mut [u8], + pub(super) window: Window, +} + +impl<'a> SliceHistory<'a> { + /// Create new static history + pub fn new(buffer: &'a mut [u8]) -> Self { + Self { + window: Window::new(buffer.len()), + buffer, + } + } + + pub(super) fn get_available_range(&self) -> CircularRange { + let len = self.buffer.len(); + CircularRange::new(self.window.end(), self.window.end(), len, len) + } + + fn get_buffer(&self) -> CircularSlice<'_> { + CircularSlice::new( + self.buffer, + self.window.start(), + self.window.end(), + self.window.len(), + ) + } + + fn get_entry_ranges(&self) -> impl Iterator + '_ { + let delimeters = + self.get_buffer() + .into_iter() + .filter_map(|(index, b)| if *b == 0x0 { Some(index) } else { None }); + + [self.window.start()] + .into_iter() + .chain(delimeters.clone().map(|i| i + 1)) + .zip(delimeters.chain([self.window.end()])) + .filter_map(|(start, end)| { + if start != end { + Some(CircularRange::new( + start, + end, + self.window.len(), + self.buffer.len(), + )) + } else { + None + } + }) + } + + pub(super) fn get_entries(&self) -> impl Iterator> { + self.get_entry_ranges() + .map(|range| CircularSlice::from_range(self.buffer, range)) + } +} + +impl<'a> History for SliceHistory<'a> { + fn add_entry<'b>(&mut self, entry: &'b str) -> Result<(), &'b str> { + if entry.len() + 1 > self.buffer.len() { + return Err(entry); + } + + for (_, b) in self + .get_available_range() + .into_iter() + .zip(entry.as_bytes().iter()) + { + self.buffer[self.window.end()] = *b; + self.window.widen(); + } + + if self.buffer[self.window.end()] != 0x0 { + self.buffer[self.window.end()] = 0x0; + + self.window.widen(); + + while self.buffer[self.window.start()] != 0x0 { + self.window.narrow(); + } + } else { + self.window.widen(); + } + + Ok(()) + } + + fn number_of_entries(&self) -> usize { + self.get_entries().count() + } + + fn get_entry(&self, index: usize) -> Option> { + self.get_entries().nth(index) + } +} diff --git a/noline/src/history/tests.rs b/noline/src/history/tests.rs new file mode 100644 index 0000000..d9b343c --- /dev/null +++ b/noline/src/history/tests.rs @@ -0,0 +1,165 @@ +use super::{circ_range::CircularRange, history_nav::HistoryNavigator}; +use std::string::String; +use std::vec::Vec; + +use super::*; + +impl<'a> FromIterator> for Vec { + fn from_iter>>(iter: T) -> Self { + iter.into_iter() + .map(|circular| { + let bytes = circular.into_iter().map(|(_, b)| *b).collect::>(); + String::from_utf8(bytes).unwrap() + }) + .collect() + } +} + +#[test] +fn circular_range() { + assert_eq!(CircularRange::new(0, 3, 10, 10).get_ranges(), (0..3, 0..0)); + assert_eq!(CircularRange::new(0, 0, 10, 10).get_ranges(), (0..10, 0..0)); + assert_eq!(CircularRange::new(0, 0, 0, 10).get_ranges(), (0..0, 0..0)); + assert_eq!(CircularRange::new(7, 3, 10, 10).get_ranges(), (7..10, 0..3)); + assert_eq!(CircularRange::new(0, 0, 10, 10).get_ranges(), (0..10, 0..0)); + assert_eq!( + CircularRange::new(0, 10, 10, 10).get_ranges(), + (0..10, 0..0) + ); + assert_eq!(CircularRange::new(9, 9, 10, 10).get_ranges(), (9..10, 0..9)); + assert_eq!( + CircularRange::new(10, 10, 10, 10).get_ranges(), + (10..10, 0..10) + ); + + assert_eq!(CircularRange::new(0, 10, 10, 10).into_iter().count(), 10); + assert_eq!(CircularRange::new(10, 10, 10, 10).into_iter().count(), 10); + assert_eq!(CircularRange::new(4, 4, 10, 10).into_iter().count(), 10); +} + +#[test] +fn circular_slice() { + assert_eq!( + CircularSlice::new("abcdef".as_bytes(), 0, 3, 6).get_slices(), + ("abc".as_bytes(), "".as_bytes()) + ); + + assert_eq!( + CircularSlice::new("abcdef".as_bytes(), 3, 0, 6).get_slices(), + ("def".as_bytes(), "".as_bytes()) + ); + + assert_eq!( + CircularSlice::new("abcdef".as_bytes(), 3, 3, 6).get_slices(), + ("def".as_bytes(), "abc".as_bytes()) + ); + + assert_eq!( + CircularSlice::new("abcdef".as_bytes(), 0, 6, 6).get_slices(), + ("abcdef".as_bytes(), "".as_bytes()) + ); + + assert_eq!( + CircularSlice::new("abcdef".as_bytes(), 0, 0, 6).get_slices(), + ("abcdef".as_bytes(), "".as_bytes()) + ); + + assert_eq!( + CircularSlice::new("abcdef".as_bytes(), 0, 0, 0).get_slices(), + ("".as_bytes(), "".as_bytes()) + ); + + assert_eq!( + CircularSlice::new("abcdef".as_bytes(), 6, 6, 6).get_slices(), + ("".as_bytes(), "abcdef".as_bytes()) + ); +} + +#[test] +fn static_history() { + let mut buffer = [0; 10]; + let mut history: SliceHistory = SliceHistory::new(&mut buffer); + + assert_eq!(history.get_available_range().get_ranges(), (0..10, 0..0)); + + assert_eq!( + history.get_entries().collect::>(), + Vec::::new() + ); + + history.add_entry("abc").unwrap(); + + // dbg!(history.start, history.end, history.len); + // dbg!(history.get_entry_ranges().collect::>()); + // dbg!(history.buffer); + + assert_eq!(history.get_entries().collect::>(), vec!["abc"]); + + history.add_entry("def").unwrap(); + + // dbg!(history.buffer); + + assert_eq!( + history.get_entries().collect::>(), + vec!["abc", "def"] + ); + + history.add_entry("ghi").unwrap(); + + dbg!( + history.window.start(), + history.window.end(), + history.window.len() + ); + + assert_eq!( + history.get_entries().collect::>(), + vec!["def", "ghi"] + ); + + history.add_entry("j").unwrap(); + + // dbg!(history.start, history.end, history.len); + + assert_eq!( + history.get_entries().collect::>(), + vec!["def", "ghi", "j"] + ); + + history.add_entry("012345678").unwrap(); + + assert_eq!( + history.get_entries().collect::>(), + vec!["012345678"] + ); + + assert!(history.add_entry("0123456789").is_err()); + + history.add_entry("abc").unwrap(); + + assert_eq!(history.get_entries().collect::>(), vec!["abc"]); + + history.add_entry("defgh").unwrap(); + + assert_eq!( + history.get_entries().collect::>(), + vec!["abc", "defgh"] + ); +} + +#[test] +fn navigator() { + let mut history = UnboundedHistory::new(); + let mut navigator = HistoryNavigator::new(&mut history); + + assert!(navigator.move_up().is_err()); + assert!(navigator.move_down().is_err()); + + navigator.history.add_entry("line 1").unwrap(); + navigator.reset(); + + assert!(navigator.move_up().is_ok()); + assert!(navigator.move_up().is_err()); + + assert!(navigator.move_down().is_err()); +} diff --git a/noline/src/history/window.rs b/noline/src/history/window.rs new file mode 100644 index 0000000..1e4fbf0 --- /dev/null +++ b/noline/src/history/window.rs @@ -0,0 +1,41 @@ +use super::circ_index::CircularIndex; + +pub(super) struct Window { + size: usize, + start: CircularIndex, + end: CircularIndex, +} + +impl Window { + pub(super) fn new(size: usize) -> Self { + let start = CircularIndex::new(0, size); + let end = CircularIndex::new(0, size); + Self { size, start, end } + } + + pub(super) fn len(&self) -> usize { + self.end.diff(self.start) as usize + } + + pub(super) fn widen(&mut self) { + self.end.increment(); + + if self.end.diff(self.start) as usize > self.size { + self.start.increment(); + } + } + + pub(super) fn narrow(&mut self) { + if self.end.diff(self.start) > 0 { + self.start.increment(); + } + } + + pub(super) fn start(&self) -> usize { + self.start.index() + } + + pub(super) fn end(&self) -> usize { + self.end.index() + } +} diff --git a/noline/src/input.rs b/noline/src/input.rs deleted file mode 100644 index 95664a9..0000000 --- a/noline/src/input.rs +++ /dev/null @@ -1,367 +0,0 @@ -use num_enum::{IntoPrimitive, TryFromPrimitive}; - -use crate::utf8::{Utf8Char, Utf8Decoder, Utf8DecoderStatus}; - -#[allow(clippy::upper_case_acronyms)] -#[derive(Debug, Eq, PartialEq, Copy, Clone, IntoPrimitive, TryFromPrimitive)] -#[repr(u8)] -pub enum ControlCharacter { - NUL = 0x0, - CtrlA = 0x1, - CtrlB = 0x2, - CtrlC = 0x3, - CtrlD = 0x4, - CtrlE = 0x5, - CtrlF = 0x6, - CtrlG = 0x7, - CtrlH = 0x8, - Tab = 0x9, - LineFeed = 0xA, - CtrlK = 0xB, - CtrlL = 0xC, - CarriageReturn = 0xD, - CtrlN = 0xE, - CtrlO = 0xF, - CtrlP = 0x10, - CtrlQ = 0x11, - CtrlR = 0x12, - CtrlS = 0x13, - CtrlT = 0x14, - CtrlU = 0x15, - CtrlV = 0x16, - CtrlW = 0x17, - CtrlX = 0x18, - CtrlY = 0x19, - CtrlZ = 0x1A, - Escape = 0x1B, - FS = 0x1C, - GS = 0x1D, - RS = 0x1E, - US = 0x1F, - Backspace = 0x7F, -} - -impl ControlCharacter { - fn new(byte: u8) -> Result { - match Self::try_from(byte) { - Ok(this) => Ok(this), - Err(_) => Err(()), - } - } -} - -#[allow(clippy::upper_case_acronyms)] -#[derive(Debug, Eq, PartialEq, Copy, Clone)] -pub enum CSI { - CUU(usize), - CUD(usize), - CUF(usize), - CUB(usize), - CPR(usize, usize), - CUP(usize, usize), - ED(usize), - DSR, - SU(usize), - SD(usize), - Home, - Delete, - End, - Unknown(u8), - Invalid, -} - -impl CSI { - fn new(byte: u8, arg1: Option, arg2: Option) -> Self { - let c = byte as char; - - match c { - 'A' => Self::CUU(arg1.unwrap_or(1)), - 'B' => Self::CUD(arg1.unwrap_or(1)), - 'C' => Self::CUF(arg1.unwrap_or(1)), - 'D' => Self::CUB(arg1.unwrap_or(1)), - 'H' => Self::CUP(arg1.unwrap_or(1), arg2.unwrap_or(1)), - 'J' => Self::ED(arg1.unwrap_or(0)), - 'R' => { - if let (Some(arg1), Some(arg2)) = (arg1, arg2) { - Self::CPR(arg1, arg2) - } else { - Self::Invalid - } - } - 'S' => Self::SU(arg1.unwrap_or(1)), - 'T' => Self::SD(arg1.unwrap_or(1)), - 'n' => Self::DSR, - '~' => { - if let Some(arg) = arg1 { - match arg { - 1 => Self::Home, - 3 => Self::Delete, - 4 => Self::End, - _ => Self::Unknown(byte), - } - } else { - Self::Unknown(byte) - } - } - _ => Self::Unknown(byte), - } - } -} - -#[cfg_attr(test, derive(Debug))] -#[derive(Eq, PartialEq, Copy, Clone)] -pub enum Action { - Ignore, - Print(Utf8Char), - InvalidUtf8, - ControlCharacter(ControlCharacter), - EscapeSequence(u8), - ControlSequenceIntroducer(CSI), -} - -impl Action { - fn escape_sequence(byte: u8) -> Self { - Action::EscapeSequence(byte) - } - - fn control_character(byte: u8) -> Self { - Action::ControlCharacter(ControlCharacter::new(byte).unwrap()) - } - - fn csi(byte: u8, arg1: Option, arg2: Option) -> Self { - Action::ControlSequenceIntroducer(CSI::new(byte, arg1, arg2)) - } -} - -#[derive(Debug, Eq, PartialEq)] -enum State { - Ground, - Utf8Sequence(Option), - EscapeSequence, - CSIStart, - CSIArg1(Option), - CSIArg2(Option, Option), -} - -pub struct Parser { - state: State, -} - -impl Parser { - pub fn new() -> Self { - Self { - state: State::Ground, - } - } - - pub fn advance(&mut self, byte: u8) -> Action { - match self.state { - State::Ground => match byte { - 0x1b => { - self.state = State::EscapeSequence; - Action::Ignore - } - 0x0..=0x1a | 0x1c..=0x1f | 0x7f => Action::control_character(byte), - 0x20..=0x7e | 0x80..=0xff => { - let mut decoder = Utf8Decoder::new(); - - match decoder.advance(byte) { - Utf8DecoderStatus::Continuation => { - self.state = State::Utf8Sequence(Some(decoder)); - Action::Ignore - } - Utf8DecoderStatus::Done(c) => Action::Print(c), - Utf8DecoderStatus::Error => Action::InvalidUtf8, - } - } - }, - State::Utf8Sequence(ref mut decoder) => { - let mut decoder = decoder.take().unwrap(); - - match decoder.advance(byte) { - Utf8DecoderStatus::Continuation => { - self.state = State::Utf8Sequence(Some(decoder)); - Action::Ignore - } - Utf8DecoderStatus::Done(c) => { - self.state = State::Ground; - Action::Print(c) - } - Utf8DecoderStatus::Error => { - self.state = State::Ground; - Action::InvalidUtf8 - } - } - } - State::EscapeSequence => { - if byte == 0x5b { - self.state = State::CSIStart; - Action::Ignore - } else { - self.state = State::Ground; - Action::escape_sequence(byte) - } - } - State::CSIStart => match byte { - 0x30..=0x39 => { - let value: usize = (byte - 0x30) as usize; - self.state = State::CSIArg1(Some(value)); - Action::Ignore - } - 0x3b => { - self.state = State::CSIArg2(None, None); - Action::Ignore - } - 0x40..=0x7e => { - self.state = State::Ground; - Action::csi(byte, None, None) - } - _ => Action::Ignore, - }, - State::CSIArg1(value) => match byte { - 0x30..=0x39 => { - let value: usize = value.unwrap_or(0) * 10 + (byte - 0x30) as usize; - self.state = State::CSIArg1(Some(value)); - Action::Ignore - } - 0x3b => { - self.state = State::CSIArg2(value, None); - Action::Ignore - } - 0x40..=0x7e => { - self.state = State::Ground; - Action::csi(byte, value, None) - } - _ => Action::Ignore, - }, - State::CSIArg2(arg1, arg2) => match byte { - 0x30..=0x39 => { - let arg2: usize = arg2.unwrap_or(0) * 10 + (byte - 0x30) as usize; - self.state = State::CSIArg2(arg1, Some(arg2)); - Action::Ignore - } - 0x40..=0x7e => { - self.state = State::Ground; - Action::csi(byte, arg1, arg2) - } - _ => Action::Ignore, - }, - } - } -} - -#[cfg(test)] -pub(crate) mod tests { - use crate::testlib::ToByteVec; - - use super::*; - use std::vec::Vec; - use ControlCharacter::*; - - fn input_sequence(parser: &mut Parser, seq: impl ToByteVec) -> Vec { - seq.to_byte_vec() - .into_iter() - .map(|b| parser.advance(b)) - .collect() - } - - #[test] - fn parser() { - let mut parser = Parser::new(); - - assert_eq!(parser.state, State::Ground); - - assert_eq!(parser.advance(b'a'), Action::Print(Utf8Char::from_str("a"))); - assert_eq!(parser.advance(0x7), Action::ControlCharacter(CtrlG)); - assert_eq!(parser.advance(0x3), Action::ControlCharacter(CtrlC)); - - let actions = input_sequence(&mut parser, "æ"); - assert_eq!( - actions, - [Action::Ignore, Action::Print(Utf8Char::from_str("æ"))] - ); - - let mut actions = input_sequence(&mut parser, "\x1b[312;836R"); - assert_eq!( - actions.pop().unwrap(), - Action::ControlSequenceIntroducer(CSI::CPR(312, 836)) - ); - while let Some(action) = actions.pop() { - assert_eq!(action, Action::Ignore); - } - - let mut actions = input_sequence(&mut parser, "\x1b[R"); - assert_eq!( - actions.pop().unwrap(), - Action::ControlSequenceIntroducer(CSI::Invalid) - ); - - let mut actions = input_sequence(&mut parser, "\x1b[32R"); - assert_eq!( - actions.pop().unwrap(), - Action::ControlSequenceIntroducer(CSI::Invalid) - ); - - let mut actions = input_sequence(&mut parser, "\x1b[32;R"); - assert_eq!( - actions.pop().unwrap(), - Action::ControlSequenceIntroducer(CSI::Invalid) - ); - - let mut actions = input_sequence(&mut parser, "\x1b[A"); - - assert_eq!( - actions.pop().unwrap(), - Action::ControlSequenceIntroducer(CSI::CUU(1)) - ); - - let mut actions = input_sequence(&mut parser, "\x1b[10B"); - - assert_eq!( - actions.pop().unwrap(), - Action::ControlSequenceIntroducer(CSI::CUD(10)) - ); - - let mut actions = input_sequence(&mut parser, "\x1b[H"); - - assert_eq!( - actions.pop().unwrap(), - Action::ControlSequenceIntroducer(CSI::CUP(1, 1)) - ); - - let mut actions = input_sequence(&mut parser, "\x1b[2;5H"); - - assert_eq!( - actions.pop().unwrap(), - Action::ControlSequenceIntroducer(CSI::CUP(2, 5)) - ); - - let mut actions = input_sequence(&mut parser, "\x1b[;5H"); - - assert_eq!( - actions.pop().unwrap(), - Action::ControlSequenceIntroducer(CSI::CUP(1, 5)) - ); - - let mut actions = input_sequence(&mut parser, "\x1b[17;H"); - - assert_eq!( - actions.pop().unwrap(), - Action::ControlSequenceIntroducer(CSI::CUP(17, 1)) - ); - - let mut actions = input_sequence(&mut parser, "\x1b[;H"); - - assert_eq!( - actions.pop().unwrap(), - Action::ControlSequenceIntroducer(CSI::CUP(1, 1)) - ); - - let mut actions = input_sequence(&mut parser, "\x1b[;10H"); - - assert_eq!( - actions.pop().unwrap(), - Action::ControlSequenceIntroducer(CSI::CUP(1, 10)) - ); - } -} diff --git a/noline/src/input/action.rs b/noline/src/input/action.rs new file mode 100644 index 0000000..d085260 --- /dev/null +++ b/noline/src/input/action.rs @@ -0,0 +1,27 @@ +use super::{ControlCharacter, CSI}; +use crate::utf8::Utf8Char; + +#[cfg_attr(test, derive(Debug))] +#[derive(Eq, PartialEq, Copy, Clone)] +pub enum Action { + Ignore, + Print(Utf8Char), + InvalidUtf8, + ControlCharacter(ControlCharacter), + EscapeSequence(u8), + ControlSequenceIntroducer(CSI), +} + +impl Action { + pub(super) fn escape_sequence(byte: u8) -> Self { + Action::EscapeSequence(byte) + } + + pub(super) fn control_character(byte: u8) -> Self { + Action::ControlCharacter(ControlCharacter::new(byte).unwrap()) + } + + pub(super) fn csi(byte: u8, arg1: Option, arg2: Option) -> Self { + Action::ControlSequenceIntroducer(CSI::new(byte, arg1, arg2)) + } +} diff --git a/noline/src/input/control_char.rs b/noline/src/input/control_char.rs new file mode 100644 index 0000000..823ec45 --- /dev/null +++ b/noline/src/input/control_char.rs @@ -0,0 +1,49 @@ +use num_enum::{IntoPrimitive, TryFromPrimitive}; + +#[allow(clippy::upper_case_acronyms)] +#[derive(Debug, Eq, PartialEq, Copy, Clone, IntoPrimitive, TryFromPrimitive)] +#[repr(u8)] +pub enum ControlCharacter { + NUL = 0x0, + CtrlA = 0x1, + CtrlB = 0x2, + CtrlC = 0x3, + CtrlD = 0x4, + CtrlE = 0x5, + CtrlF = 0x6, + CtrlG = 0x7, + CtrlH = 0x8, + Tab = 0x9, + LineFeed = 0xA, + CtrlK = 0xB, + CtrlL = 0xC, + CarriageReturn = 0xD, + CtrlN = 0xE, + CtrlO = 0xF, + CtrlP = 0x10, + CtrlQ = 0x11, + CtrlR = 0x12, + CtrlS = 0x13, + CtrlT = 0x14, + CtrlU = 0x15, + CtrlV = 0x16, + CtrlW = 0x17, + CtrlX = 0x18, + CtrlY = 0x19, + CtrlZ = 0x1A, + Escape = 0x1B, + FS = 0x1C, + GS = 0x1D, + RS = 0x1E, + US = 0x1F, + Backspace = 0x7F, +} + +impl ControlCharacter { + pub(super) fn new(byte: u8) -> Result { + match Self::try_from(byte) { + Ok(this) => Ok(this), + Err(_) => Err(()), + } + } +} diff --git a/noline/src/input/csi.rs b/noline/src/input/csi.rs new file mode 100644 index 0000000..a376f11 --- /dev/null +++ b/noline/src/input/csi.rs @@ -0,0 +1,57 @@ +#[allow(clippy::upper_case_acronyms)] +#[derive(Debug, Eq, PartialEq, Copy, Clone)] +pub enum CSI { + CUU(usize), + CUD(usize), + CUF(usize), + CUB(usize), + CPR(usize, usize), + CUP(usize, usize), + ED(usize), + DSR, + SU(usize), + SD(usize), + Home, + Delete, + End, + Unknown(u8), + Invalid, +} + +impl CSI { + pub(super) fn new(byte: u8, arg1: Option, arg2: Option) -> Self { + let c = byte as char; + + match c { + 'A' => Self::CUU(arg1.unwrap_or(1)), + 'B' => Self::CUD(arg1.unwrap_or(1)), + 'C' => Self::CUF(arg1.unwrap_or(1)), + 'D' => Self::CUB(arg1.unwrap_or(1)), + 'H' => Self::CUP(arg1.unwrap_or(1), arg2.unwrap_or(1)), + 'J' => Self::ED(arg1.unwrap_or(0)), + 'R' => { + if let (Some(arg1), Some(arg2)) = (arg1, arg2) { + Self::CPR(arg1, arg2) + } else { + Self::Invalid + } + } + 'S' => Self::SU(arg1.unwrap_or(1)), + 'T' => Self::SD(arg1.unwrap_or(1)), + 'n' => Self::DSR, + '~' => { + if let Some(arg) = arg1 { + match arg { + 1 => Self::Home, + 3 => Self::Delete, + 4 => Self::End, + _ => Self::Unknown(byte), + } + } else { + Self::Unknown(byte) + } + } + _ => Self::Unknown(byte), + } + } +} diff --git a/noline/src/input/mod.rs b/noline/src/input/mod.rs new file mode 100644 index 0000000..dcda5c9 --- /dev/null +++ b/noline/src/input/mod.rs @@ -0,0 +1,13 @@ +mod action; +mod control_char; +mod csi; +mod parser; +mod state; + +pub use action::*; +pub use control_char::*; +pub use csi::*; +pub use parser::*; + +#[cfg(test)] +pub(crate) mod tests; diff --git a/noline/src/input/parser.rs b/noline/src/input/parser.rs new file mode 100644 index 0000000..94d4c83 --- /dev/null +++ b/noline/src/input/parser.rs @@ -0,0 +1,109 @@ +use super::{state::State, Action}; +use crate::utf8::{Utf8Decoder, Utf8DecoderStatus}; + +pub struct Parser { + pub(super) state: State, +} + +impl Parser { + pub fn new() -> Self { + Self { + state: State::Ground, + } + } + + pub fn advance(&mut self, byte: u8) -> Action { + match self.state { + State::Ground => match byte { + 0x1b => { + self.state = State::EscapeSequence; + Action::Ignore + } + 0x0..=0x1a | 0x1c..=0x1f | 0x7f => Action::control_character(byte), + 0x20..=0x7e | 0x80..=0xff => { + let mut decoder = Utf8Decoder::new(); + + match decoder.advance(byte) { + Utf8DecoderStatus::Continuation => { + self.state = State::Utf8Sequence(Some(decoder)); + Action::Ignore + } + Utf8DecoderStatus::Done(c) => Action::Print(c), + Utf8DecoderStatus::Error => Action::InvalidUtf8, + } + } + }, + State::Utf8Sequence(ref mut decoder) => { + let mut decoder = decoder.take().unwrap(); + + match decoder.advance(byte) { + Utf8DecoderStatus::Continuation => { + self.state = State::Utf8Sequence(Some(decoder)); + Action::Ignore + } + Utf8DecoderStatus::Done(c) => { + self.state = State::Ground; + Action::Print(c) + } + Utf8DecoderStatus::Error => { + self.state = State::Ground; + Action::InvalidUtf8 + } + } + } + State::EscapeSequence => { + if byte == 0x5b { + self.state = State::CSIStart; + Action::Ignore + } else { + self.state = State::Ground; + Action::escape_sequence(byte) + } + } + State::CSIStart => match byte { + 0x30..=0x39 => { + let value: usize = (byte - 0x30) as usize; + self.state = State::CSIArg1(Some(value)); + Action::Ignore + } + 0x3b => { + self.state = State::CSIArg2(None, None); + Action::Ignore + } + 0x40..=0x7e => { + self.state = State::Ground; + Action::csi(byte, None, None) + } + _ => Action::Ignore, + }, + State::CSIArg1(value) => match byte { + 0x30..=0x39 => { + let value: usize = value.unwrap_or(0) * 10 + (byte - 0x30) as usize; + self.state = State::CSIArg1(Some(value)); + Action::Ignore + } + 0x3b => { + self.state = State::CSIArg2(value, None); + Action::Ignore + } + 0x40..=0x7e => { + self.state = State::Ground; + Action::csi(byte, value, None) + } + _ => Action::Ignore, + }, + State::CSIArg2(arg1, arg2) => match byte { + 0x30..=0x39 => { + let arg2: usize = arg2.unwrap_or(0) * 10 + (byte - 0x30) as usize; + self.state = State::CSIArg2(arg1, Some(arg2)); + Action::Ignore + } + 0x40..=0x7e => { + self.state = State::Ground; + Action::csi(byte, arg1, arg2) + } + _ => Action::Ignore, + }, + } + } +} diff --git a/noline/src/input/state.rs b/noline/src/input/state.rs new file mode 100644 index 0000000..a9c6310 --- /dev/null +++ b/noline/src/input/state.rs @@ -0,0 +1,11 @@ +use crate::utf8::Utf8Decoder; + +#[derive(Debug, Eq, PartialEq)] +pub(super) enum State { + Ground, + Utf8Sequence(Option), + EscapeSequence, + CSIStart, + CSIArg1(Option), + CSIArg2(Option, Option), +} diff --git a/noline/src/input/tests.rs b/noline/src/input/tests.rs new file mode 100644 index 0000000..fb277fa --- /dev/null +++ b/noline/src/input/tests.rs @@ -0,0 +1,113 @@ +use super::state::State; +use super::ControlCharacter::*; +use super::*; +use crate::testlib::ToByteVec; +use crate::utf8::Utf8Char; +use std::vec::Vec; + +fn input_sequence(parser: &mut Parser, seq: impl ToByteVec) -> Vec { + seq.to_byte_vec() + .into_iter() + .map(|b| parser.advance(b)) + .collect() +} + +#[test] +fn parser() { + let mut parser = Parser::new(); + + assert_eq!(parser.state, State::Ground); + + assert_eq!(parser.advance(b'a'), Action::Print(Utf8Char::from_str("a"))); + assert_eq!(parser.advance(0x7), Action::ControlCharacter(CtrlG)); + assert_eq!(parser.advance(0x3), Action::ControlCharacter(CtrlC)); + + let actions = input_sequence(&mut parser, "æ"); + assert_eq!( + actions, + [Action::Ignore, Action::Print(Utf8Char::from_str("æ"))] + ); + + let mut actions = input_sequence(&mut parser, "\x1b[312;836R"); + assert_eq!( + actions.pop().unwrap(), + Action::ControlSequenceIntroducer(CSI::CPR(312, 836)) + ); + while let Some(action) = actions.pop() { + assert_eq!(action, Action::Ignore); + } + + let mut actions = input_sequence(&mut parser, "\x1b[R"); + assert_eq!( + actions.pop().unwrap(), + Action::ControlSequenceIntroducer(CSI::Invalid) + ); + + let mut actions = input_sequence(&mut parser, "\x1b[32R"); + assert_eq!( + actions.pop().unwrap(), + Action::ControlSequenceIntroducer(CSI::Invalid) + ); + + let mut actions = input_sequence(&mut parser, "\x1b[32;R"); + assert_eq!( + actions.pop().unwrap(), + Action::ControlSequenceIntroducer(CSI::Invalid) + ); + + let mut actions = input_sequence(&mut parser, "\x1b[A"); + + assert_eq!( + actions.pop().unwrap(), + Action::ControlSequenceIntroducer(CSI::CUU(1)) + ); + + let mut actions = input_sequence(&mut parser, "\x1b[10B"); + + assert_eq!( + actions.pop().unwrap(), + Action::ControlSequenceIntroducer(CSI::CUD(10)) + ); + + let mut actions = input_sequence(&mut parser, "\x1b[H"); + + assert_eq!( + actions.pop().unwrap(), + Action::ControlSequenceIntroducer(CSI::CUP(1, 1)) + ); + + let mut actions = input_sequence(&mut parser, "\x1b[2;5H"); + + assert_eq!( + actions.pop().unwrap(), + Action::ControlSequenceIntroducer(CSI::CUP(2, 5)) + ); + + let mut actions = input_sequence(&mut parser, "\x1b[;5H"); + + assert_eq!( + actions.pop().unwrap(), + Action::ControlSequenceIntroducer(CSI::CUP(1, 5)) + ); + + let mut actions = input_sequence(&mut parser, "\x1b[17;H"); + + assert_eq!( + actions.pop().unwrap(), + Action::ControlSequenceIntroducer(CSI::CUP(17, 1)) + ); + + let mut actions = input_sequence(&mut parser, "\x1b[;H"); + + assert_eq!( + actions.pop().unwrap(), + Action::ControlSequenceIntroducer(CSI::CUP(1, 1)) + ); + + let mut actions = input_sequence(&mut parser, "\x1b[;10H"); + + assert_eq!( + actions.pop().unwrap(), + Action::ControlSequenceIntroducer(CSI::CUP(1, 10)) + ); +} diff --git a/noline/src/lib.rs b/noline/src/lib.rs index 87b2764..e9444cd 100644 --- a/noline/src/lib.rs +++ b/noline/src/lib.rs @@ -61,17 +61,17 @@ #![cfg_attr(not(test), no_std)] -pub mod async_editor; -pub mod builder; mod core; pub mod error; pub mod history; mod input; pub mod line_buffer; mod output; -pub mod sync_editor; pub(crate) mod terminal; mod utf8; +pub mod editor; +pub use editor::*; + #[cfg(test)] pub(crate) mod testlib; diff --git a/noline/src/line_buffer.rs b/noline/src/line_buffer.rs deleted file mode 100644 index 8f20fa2..0000000 --- a/noline/src/line_buffer.rs +++ /dev/null @@ -1,440 +0,0 @@ -//! Buffer to hold line. -//! -//! Can be backed by [`std::vec::Vec`] for dynamic allocation or -//! [`StaticBuffer`] for static allocation. Custom implementation can -//! be provided with the [`Buffer`] trait. - -use crate::utf8::Utf8Char; -use core::{ops::Range, str::from_utf8_unchecked}; - -/// Trait for defining underlying buffer -pub trait Buffer { - /// Return the current length of the buffer. This represents the - /// number of bytes currently in the buffer, not the capacity. - fn buffer_len(&self) -> usize; - - /// Return buffer capacity or None if unbounded. - fn capacity(&self) -> Option; - - /// Truncate buffer, setting lenght to 0. - fn truncate_buffer(&mut self, index: usize); - - /// Insert byte at index - fn insert_byte(&mut self, index: usize, byte: u8); - - /// Remove byte from index and return byte - fn remove_byte(&mut self, index: usize) -> u8; - - /// Return byte slice into buffer from 0 up to buffer length. - fn as_slice(&self) -> &[u8]; -} - -/// High level interface to line buffer -pub struct LineBuffer { - buf: B, -} - -impl<'a> LineBuffer> { - /// Create new static line buffer - pub fn from_slice(buffer: &'a mut [u8]) -> Self { - Self { - buf: SliceBuffer::new(buffer), - } - } -} - -impl LineBuffer { - /// Return buffer as bytes slice - pub fn as_slice(&self) -> &[u8] { - self.buf.as_slice() - } - - /// Return buffer length - pub fn len(&self) -> usize { - self.buf.buffer_len() - } - - /// Return buffer as string. The buffer should only hold a valid - /// UTF-8, so this function is infallible. - pub fn as_str(&self) -> &str { - // Pinky swear, it's only UTF-8! - unsafe { from_utf8_unchecked(self.as_slice()) } - } - - fn char_ranges(&self) -> impl Iterator, char)> + '_ { - let s = self.as_str(); - - s.char_indices() - .zip(s.char_indices().skip(1).chain([(s.len(), '\0')])) - .map(|((start, c), (end, _))| (start..end, c)) - } - - fn get_byte_position(&self, char_index: usize) -> usize { - let s = self.as_str(); - - s.char_indices() - .skip(char_index) - .map(|(pos, _)| pos) - .next() - .unwrap_or(s.len()) - } - - /// Delete character at character index. - pub fn delete(&mut self, char_index: usize) { - let mut ranges = self.char_ranges().skip(char_index); - - if let Some((range, _)) = ranges.next() { - drop(ranges); - - let pos = range.start; - - for _ in range { - self.buf.remove_byte(pos); - } - } - } - - /// Delete buffer after character index - pub fn delete_after_char(&mut self, char_index: usize) { - let pos = self.get_byte_position(char_index); - - self.buf.truncate_buffer(pos); - } - - /// Truncate buffer - pub fn truncate(&mut self) { - self.delete_after_char(0); - } - - fn delete_range(&mut self, range: Range) { - let pos = range.start; - for _ in range { - self.buf.remove_byte(pos); - } - } - - /// Delete previous word from character index - pub fn delete_previous_word(&mut self, char_index: usize) -> usize { - let mut word_start = 0; - let mut word_end = 0; - - for (i, (range, c)) in self.char_ranges().enumerate().take(char_index) { - if c == ' ' && i < char_index - 1 { - word_start = range.end; - } - - word_end = range.end; - } - - let deleted = self.as_str()[word_start..word_end].chars().count(); - - self.delete_range(word_start..word_end); - - deleted - } - - /// Swap characters at index - pub fn swap_chars(&mut self, char_index: usize) { - let mut ranges = self.char_ranges().skip(char_index - 1); - - if let Some((prev, _)) = ranges.next() { - if let Some((cur, _)) = ranges.next() { - drop(ranges); - - for (remove, insert) in cur.zip((prev.start)..) { - let byte = self.buf.remove_byte(remove); - self.buf.insert_byte(insert, byte); - } - } - } - } - - /// Insert bytes at index - /// - /// # Safety - /// - /// The caller must ensure that the input bytes are a valid UTF-8 - /// sequence and that the byte index aligns with a valid UTF-8 character index. - pub unsafe fn insert_bytes(&mut self, index: usize, bytes: &[u8]) -> Result<(), ()> { - if let Some(capacity) = self.buf.capacity() { - if bytes.len() > capacity - self.buf.buffer_len() { - return Err(()); - } - } - - for (i, byte) in bytes.iter().enumerate() { - self.buf.insert_byte(index + i, *byte); - } - - Ok(()) - } - - /// Insert UTF-8 char at position - pub fn insert_utf8_char(&mut self, char_index: usize, c: Utf8Char) -> Result<(), Utf8Char> { - unsafe { - self.insert_bytes(self.get_byte_position(char_index), c.as_bytes()) - .map_err(|_| c) - } - } - - /// Insert string at char position - pub fn insert_str(&mut self, char_index: usize, s: &str) -> Result<(), ()> { - unsafe { self.insert_bytes(self.get_byte_position(char_index), s.as_bytes()) } - } -} - -/// Emtpy buffer used for builder -pub struct NoBuffer {} - -impl Buffer for NoBuffer { - fn buffer_len(&self) -> usize { - unimplemented!() - } - - fn capacity(&self) -> Option { - unimplemented!() - } - - fn truncate_buffer(&mut self, _index: usize) { - unimplemented!() - } - - fn insert_byte(&mut self, _index: usize, _byte: u8) { - unimplemented!() - } - - fn remove_byte(&mut self, _index: usize) -> u8 { - unimplemented!() - } - - fn as_slice(&self) -> &[u8] { - unimplemented!() - } -} - -/// Static buffer backed by slice -pub struct SliceBuffer<'a> { - data: &'a mut [u8], - len: usize, -} - -impl<'a> SliceBuffer<'a> { - pub fn new(data: &'a mut [u8]) -> Self { - Self { data, len: 0 } - } -} - -impl<'a> Buffer for SliceBuffer<'a> { - fn buffer_len(&self) -> usize { - self.len - } - - fn capacity(&self) -> Option { - Some(self.data.len()) - } - - fn truncate_buffer(&mut self, index: usize) { - self.len = index; - } - - fn insert_byte(&mut self, index: usize, byte: u8) { - for i in (index..self.len).rev() { - self.data[i + 1] = self.data[i]; - } - - self.data[index] = byte; - self.len += 1; - } - - fn remove_byte(&mut self, index: usize) -> u8 { - let byte = self.data[index]; - - for i in index..(self.len - 1) { - self.data[i] = self.data[i + 1]; - } - - self.len -= 1; - - byte - } - - fn as_slice(&self) -> &[u8] { - &self.data[0..self.len] - } -} - -#[cfg(any(test, doc, feature = "alloc", feature = "std"))] -mod alloc { - extern crate alloc; - - use self::alloc::vec::Vec; - use super::*; - - impl LineBuffer { - /// Create new static line buffer - pub fn new_unbounded() -> Self { - Self { - buf: UnboundedBuffer::new(), - } - } - } - - pub struct UnboundedBuffer { - vec: Vec, - } - - impl UnboundedBuffer { - pub fn new() -> Self { - Self { vec: Vec::new() } - } - } - - impl Buffer for UnboundedBuffer { - fn buffer_len(&self) -> usize { - self.vec.len() - } - - fn capacity(&self) -> Option { - None - } - - fn truncate_buffer(&mut self, index: usize) { - self.vec.truncate(index) - } - - fn insert_byte(&mut self, index: usize, byte: u8) { - self.vec.insert(index, byte); - } - - fn remove_byte(&mut self, index: usize) -> u8 { - self.vec.remove(index) - } - - fn as_slice(&self) -> &[u8] { - self.vec.as_slice() - } - } -} - -#[cfg(any(test, doc, feature = "alloc", feature = "std"))] -pub use self::alloc::*; - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn slice_buffer() { - let mut array = [0; 20]; - let mut buf = SliceBuffer::new(&mut array); - - for i in 0..20 { - buf.insert_byte(i, 0x30); - } - - buf.remove_byte(19); - } - - fn insert_str(buf: &mut LineBuffer, index: usize, s: &str) { - buf.insert_str(index, s).unwrap(); - } - - fn test_line_buffer(buf: &mut LineBuffer) { - insert_str(buf, 0, "Hello, World!"); - - assert_eq!(buf.as_str(), "Hello, World!"); - - buf.delete(12); - - assert_eq!(buf.as_str(), "Hello, World"); - - buf.delete(12); - - assert_eq!(buf.as_str(), "Hello, World"); - - buf.delete(0); - insert_str(buf, 0, "h"); - - assert_eq!(buf.as_str(), "hello, World"); - - buf.delete(2); - insert_str(buf, 2, "L"); - - assert_eq!(buf.as_str(), "heLlo, World"); - - buf.delete(11); - - assert_eq!(buf.as_str(), "heLlo, Worl"); - - buf.delete(5); - - assert_eq!(buf.as_str(), "heLlo Worl"); - - for _ in 0..5 { - buf.delete(5); - } - - assert_eq!(buf.as_str(), "heLlo"); - - insert_str(buf, 5, " æå"); - - assert_eq!(buf.as_str(), "heLlo æå"); - - insert_str(buf, 7, "ø"); - - assert_eq!(buf.as_str(), "heLlo æøå"); - - buf.delete(8); - - assert_eq!(buf.as_str(), "heLlo æø"); - - buf.delete(7); - - assert_eq!(buf.as_str(), "heLlo æ"); - - buf.delete_previous_word(7); - - assert_eq!(buf.as_str(), "heLlo "); - - buf.delete_previous_word(6); - - assert_eq!(buf.as_str(), ""); - - insert_str(buf, 0, "word1 word2 word3"); - assert_eq!(buf.as_str(), "word1 word2 word3"); - buf.delete_previous_word(12); - - assert_eq!(buf.as_str(), "word1 word3"); - } - - #[test] - fn test_slice_line_buffer() { - let mut array = [0; 80]; - let mut buf = LineBuffer::from_slice(&mut array); - - test_line_buffer(&mut buf); - - buf.delete_after_char(0); - - assert_eq!(buf.len(), 0); - - for i in 0..80 { - assert!(buf.insert_utf8_char(i, Utf8Char::from_str("a")).is_ok()); - } - - assert!(buf.insert_utf8_char(80, Utf8Char::from_str("a")).is_err()); - } - - #[test] - fn test_alloc_line_buffer() { - let mut buf = LineBuffer::new_unbounded(); - - test_line_buffer(&mut buf); - - buf.delete_after_char(0); - - for i in 0..1000 { - assert!(buf.insert_utf8_char(i, Utf8Char::from_str("a")).is_ok()); - } - } -} diff --git a/noline/src/line_buffer/alloc.rs b/noline/src/line_buffer/alloc.rs new file mode 100644 index 0000000..b444725 --- /dev/null +++ b/noline/src/line_buffer/alloc.rs @@ -0,0 +1,49 @@ +extern crate alloc; + +use self::alloc::vec::Vec; +use super::*; + +impl LineBuffer { + /// Create new static line buffer + pub fn new_unbounded() -> Self { + Self { + buf: UnboundedBuffer::new(), + } + } +} + +pub struct UnboundedBuffer { + vec: Vec, +} + +impl UnboundedBuffer { + pub fn new() -> Self { + Self { vec: Vec::new() } + } +} + +impl Buffer for UnboundedBuffer { + fn buffer_len(&self) -> usize { + self.vec.len() + } + + fn capacity(&self) -> Option { + None + } + + fn truncate_buffer(&mut self, index: usize) { + self.vec.truncate(index) + } + + fn insert_byte(&mut self, index: usize, byte: u8) { + self.vec.insert(index, byte); + } + + fn remove_byte(&mut self, index: usize) -> u8 { + self.vec.remove(index) + } + + fn as_slice(&self) -> &[u8] { + self.vec.as_slice() + } +} diff --git a/noline/src/line_buffer/buffer.rs b/noline/src/line_buffer/buffer.rs new file mode 100644 index 0000000..4d28eed --- /dev/null +++ b/noline/src/line_buffer/buffer.rs @@ -0,0 +1,21 @@ +/// Trait for defining underlying buffer +pub trait Buffer { + /// Return the current length of the buffer. This represents the + /// number of bytes currently in the buffer, not the capacity. + fn buffer_len(&self) -> usize; + + /// Return buffer capacity or None if unbounded. + fn capacity(&self) -> Option; + + /// Truncate buffer, setting lenght to 0. + fn truncate_buffer(&mut self, index: usize); + + /// Insert byte at index + fn insert_byte(&mut self, index: usize, byte: u8); + + /// Remove byte from index and return byte + fn remove_byte(&mut self, index: usize) -> u8; + + /// Return byte slice into buffer from 0 up to buffer length. + fn as_slice(&self) -> &[u8]; +} diff --git a/noline/src/line_buffer/line_buffer.rs b/noline/src/line_buffer/line_buffer.rs new file mode 100644 index 0000000..6180df1 --- /dev/null +++ b/noline/src/line_buffer/line_buffer.rs @@ -0,0 +1,157 @@ +use super::{Buffer, SliceBuffer}; +use crate::utf8::Utf8Char; +use core::{ops::Range, str::from_utf8_unchecked}; + +/// High level interface to line buffer +pub struct LineBuffer { + pub(super) buf: B, +} + +impl<'a> LineBuffer> { + /// Create new static line buffer + pub fn from_slice(buffer: &'a mut [u8]) -> Self { + Self { + buf: SliceBuffer::new(buffer), + } + } +} + +impl LineBuffer { + /// Return buffer as bytes slice + pub fn as_slice(&self) -> &[u8] { + self.buf.as_slice() + } + + /// Return buffer length + pub fn len(&self) -> usize { + self.buf.buffer_len() + } + + /// Return buffer as string. The buffer should only hold a valid + /// UTF-8, so this function is infallible. + pub fn as_str(&self) -> &str { + // Pinky swear, it's only UTF-8! + unsafe { from_utf8_unchecked(self.as_slice()) } + } + + fn char_ranges(&self) -> impl Iterator, char)> + '_ { + let s = self.as_str(); + + s.char_indices() + .zip(s.char_indices().skip(1).chain([(s.len(), '\0')])) + .map(|((start, c), (end, _))| (start..end, c)) + } + + fn get_byte_position(&self, char_index: usize) -> usize { + let s = self.as_str(); + + s.char_indices() + .skip(char_index) + .map(|(pos, _)| pos) + .next() + .unwrap_or(s.len()) + } + + /// Delete character at character index. + pub fn delete(&mut self, char_index: usize) { + let mut ranges = self.char_ranges().skip(char_index); + + if let Some((range, _)) = ranges.next() { + drop(ranges); + + let pos = range.start; + + for _ in range { + self.buf.remove_byte(pos); + } + } + } + + /// Delete buffer after character index + pub fn delete_after_char(&mut self, char_index: usize) { + let pos = self.get_byte_position(char_index); + + self.buf.truncate_buffer(pos); + } + + /// Truncate buffer + pub fn truncate(&mut self) { + self.delete_after_char(0); + } + + fn delete_range(&mut self, range: Range) { + let pos = range.start; + for _ in range { + self.buf.remove_byte(pos); + } + } + + /// Delete previous word from character index + pub fn delete_previous_word(&mut self, char_index: usize) -> usize { + let mut word_start = 0; + let mut word_end = 0; + + for (i, (range, c)) in self.char_ranges().enumerate().take(char_index) { + if c == ' ' && i < char_index - 1 { + word_start = range.end; + } + + word_end = range.end; + } + + let deleted = self.as_str()[word_start..word_end].chars().count(); + + self.delete_range(word_start..word_end); + + deleted + } + + /// Swap characters at index + pub fn swap_chars(&mut self, char_index: usize) { + let mut ranges = self.char_ranges().skip(char_index - 1); + + if let Some((prev, _)) = ranges.next() { + if let Some((cur, _)) = ranges.next() { + drop(ranges); + + for (remove, insert) in cur.zip((prev.start)..) { + let byte = self.buf.remove_byte(remove); + self.buf.insert_byte(insert, byte); + } + } + } + } + + /// Insert bytes at index + /// + /// # Safety + /// + /// The caller must ensure that the input bytes are a valid UTF-8 + /// sequence and that the byte index aligns with a valid UTF-8 character index. + pub unsafe fn insert_bytes(&mut self, index: usize, bytes: &[u8]) -> Result<(), ()> { + if let Some(capacity) = self.buf.capacity() { + if bytes.len() > capacity - self.buf.buffer_len() { + return Err(()); + } + } + + for (i, byte) in bytes.iter().enumerate() { + self.buf.insert_byte(index + i, *byte); + } + + Ok(()) + } + + /// Insert UTF-8 char at position + pub fn insert_utf8_char(&mut self, char_index: usize, c: Utf8Char) -> Result<(), Utf8Char> { + unsafe { + self.insert_bytes(self.get_byte_position(char_index), c.as_bytes()) + .map_err(|_| c) + } + } + + /// Insert string at char position + pub fn insert_str(&mut self, char_index: usize, s: &str) -> Result<(), ()> { + unsafe { self.insert_bytes(self.get_byte_position(char_index), s.as_bytes()) } + } +} diff --git a/noline/src/line_buffer/mod.rs b/noline/src/line_buffer/mod.rs new file mode 100644 index 0000000..c8ddc52 --- /dev/null +++ b/noline/src/line_buffer/mod.rs @@ -0,0 +1,24 @@ +//! Buffer to hold line. +//! +//! Can be backed by [`std::vec::Vec`] for dynamic allocation or +//! [`StaticBuffer`] for static allocation. Custom implementation can +//! be provided with the [`Buffer`] trait. + +mod buffer; +mod line_buffer; +mod no_buffer; +mod slice_buffer; + +#[cfg(any(test, doc, feature = "alloc", feature = "std"))] +mod alloc; + +pub use buffer::*; +pub use line_buffer::*; +pub use no_buffer::*; +pub use slice_buffer::*; + +#[cfg(any(test, doc, feature = "alloc", feature = "std"))] +pub use self::alloc::*; + +#[cfg(test)] +pub(crate) mod tests; diff --git a/noline/src/line_buffer/no_buffer.rs b/noline/src/line_buffer/no_buffer.rs new file mode 100644 index 0000000..4b5f750 --- /dev/null +++ b/noline/src/line_buffer/no_buffer.rs @@ -0,0 +1,30 @@ +use super::Buffer; + +/// Emtpy buffer used for builder +pub struct NoBuffer {} + +impl Buffer for NoBuffer { + fn buffer_len(&self) -> usize { + unimplemented!() + } + + fn capacity(&self) -> Option { + unimplemented!() + } + + fn truncate_buffer(&mut self, _index: usize) { + unimplemented!() + } + + fn insert_byte(&mut self, _index: usize, _byte: u8) { + unimplemented!() + } + + fn remove_byte(&mut self, _index: usize) -> u8 { + unimplemented!() + } + + fn as_slice(&self) -> &[u8] { + unimplemented!() + } +} diff --git a/noline/src/line_buffer/slice_buffer.rs b/noline/src/line_buffer/slice_buffer.rs new file mode 100644 index 0000000..8e19886 --- /dev/null +++ b/noline/src/line_buffer/slice_buffer.rs @@ -0,0 +1,52 @@ +use super::Buffer; + +/// Static buffer backed by slice +pub struct SliceBuffer<'a> { + data: &'a mut [u8], + len: usize, +} + +impl<'a> SliceBuffer<'a> { + pub fn new(data: &'a mut [u8]) -> Self { + Self { data, len: 0 } + } +} + +impl<'a> Buffer for SliceBuffer<'a> { + fn buffer_len(&self) -> usize { + self.len + } + + fn capacity(&self) -> Option { + Some(self.data.len()) + } + + fn truncate_buffer(&mut self, index: usize) { + self.len = index; + } + + fn insert_byte(&mut self, index: usize, byte: u8) { + for i in (index..self.len).rev() { + self.data[i + 1] = self.data[i]; + } + + self.data[index] = byte; + self.len += 1; + } + + fn remove_byte(&mut self, index: usize) -> u8 { + let byte = self.data[index]; + + for i in index..(self.len - 1) { + self.data[i] = self.data[i + 1]; + } + + self.len -= 1; + + byte + } + + fn as_slice(&self) -> &[u8] { + &self.data[0..self.len] + } +} diff --git a/noline/src/line_buffer/tests.rs b/noline/src/line_buffer/tests.rs new file mode 100644 index 0000000..326b71b --- /dev/null +++ b/noline/src/line_buffer/tests.rs @@ -0,0 +1,117 @@ +use super::*; +use crate::utf8::Utf8Char; + +#[test] +fn slice_buffer() { + let mut array = [0; 20]; + let mut buf = SliceBuffer::new(&mut array); + + for i in 0..20 { + buf.insert_byte(i, 0x30); + } + + buf.remove_byte(19); +} + +fn insert_str(buf: &mut LineBuffer, index: usize, s: &str) { + buf.insert_str(index, s).unwrap(); +} + +fn test_line_buffer(buf: &mut LineBuffer) { + insert_str(buf, 0, "Hello, World!"); + + assert_eq!(buf.as_str(), "Hello, World!"); + + buf.delete(12); + + assert_eq!(buf.as_str(), "Hello, World"); + + buf.delete(12); + + assert_eq!(buf.as_str(), "Hello, World"); + + buf.delete(0); + insert_str(buf, 0, "h"); + + assert_eq!(buf.as_str(), "hello, World"); + + buf.delete(2); + insert_str(buf, 2, "L"); + + assert_eq!(buf.as_str(), "heLlo, World"); + + buf.delete(11); + + assert_eq!(buf.as_str(), "heLlo, Worl"); + + buf.delete(5); + + assert_eq!(buf.as_str(), "heLlo Worl"); + + for _ in 0..5 { + buf.delete(5); + } + + assert_eq!(buf.as_str(), "heLlo"); + + insert_str(buf, 5, " æå"); + + assert_eq!(buf.as_str(), "heLlo æå"); + + insert_str(buf, 7, "ø"); + + assert_eq!(buf.as_str(), "heLlo æøå"); + + buf.delete(8); + + assert_eq!(buf.as_str(), "heLlo æø"); + + buf.delete(7); + + assert_eq!(buf.as_str(), "heLlo æ"); + + buf.delete_previous_word(7); + + assert_eq!(buf.as_str(), "heLlo "); + + buf.delete_previous_word(6); + + assert_eq!(buf.as_str(), ""); + + insert_str(buf, 0, "word1 word2 word3"); + assert_eq!(buf.as_str(), "word1 word2 word3"); + buf.delete_previous_word(12); + + assert_eq!(buf.as_str(), "word1 word3"); +} + +#[test] +fn test_slice_line_buffer() { + let mut array = [0; 80]; + let mut buf = LineBuffer::from_slice(&mut array); + + test_line_buffer(&mut buf); + + buf.delete_after_char(0); + + assert_eq!(buf.len(), 0); + + for i in 0..80 { + assert!(buf.insert_utf8_char(i, Utf8Char::from_str("a")).is_ok()); + } + + assert!(buf.insert_utf8_char(80, Utf8Char::from_str("a")).is_err()); +} + +#[test] +fn test_alloc_line_buffer() { + let mut buf = LineBuffer::new_unbounded(); + + test_line_buffer(&mut buf); + + buf.delete_after_char(0); + + for i in 0..1000 { + assert!(buf.insert_utf8_char(i, Utf8Char::from_str("a")).is_ok()); + } +} diff --git a/noline/src/output.rs b/noline/src/output.rs deleted file mode 100644 index d33e2d2..0000000 --- a/noline/src/output.rs +++ /dev/null @@ -1,825 +0,0 @@ -use core::marker::PhantomData; - -use crate::{ - core::Prompt, - line_buffer::{Buffer, LineBuffer}, - terminal::{Cursor, Position, Terminal}, -}; - -#[cfg_attr(test, derive(Debug))] -pub enum OutputItem<'a> { - Slice(&'a [u8]), - UintToBytes(UintToBytes<4>), - EndOfString, - Abort, -} - -impl<'a> OutputItem<'a> { - pub fn get_bytes(&self) -> Option<&[u8]> { - match self { - Self::Slice(slice) => Some(slice), - Self::UintToBytes(uint) => Some(uint.as_bytes()), - Self::EndOfString | Self::Abort => None, - } - } -} - -#[cfg_attr(test, derive(Debug))] -#[derive(Copy, Clone)] -pub enum CursorMove { - Forward, - Back, - Start, - End, -} - -#[cfg_attr(test, derive(Debug))] -#[derive(Copy, Clone)] -pub enum OutputAction { - Nothing, - MoveCursor(CursorMove), - ClearAndPrintPrompt, - ClearAndPrintBuffer, - PrintBufferAndMoveCursorForward, - EraseAfterCursor, - EraseAndPrintBuffer, - ClearScreen, - ClearLine, - MoveCursorBackAndPrintBufferAndMoveForward, - MoveCursorAndEraseAndPrintBuffer(isize), - RingBell, - ProbeSize, - Done, - Abort, -} - -#[cfg_attr(test, derive(Debug))] -#[derive(Copy, Clone)] -pub struct UintToBytes { - bytes: [u8; N], -} - -impl UintToBytes { - fn from_uint>(n: I) -> Option { - let mut n: usize = n.into(); - - if n < 10_usize.pow(N as u32) { - let mut bytes = [0; N]; - - for i in (0..N).rev() { - bytes[i] = 0x30 + (n % 10) as u8; - n /= 10; - - if n == 0 { - break; - } - } - - Some(Self { bytes }) - } else { - None - } - } - - pub fn as_bytes(&self) -> &[u8] { - let start = self.bytes.iter().take_while(|&&b| b == 0).count(); - &self.bytes[start..] - } -} - -#[cfg_attr(test, derive(Debug))] -enum MoveCursorState { - New, - ScrollPrefix, - Scroll, - ScrollFinalByte, - MovePrefix, - Row, - Separator, - Column, - MoveFinalByte, - Done, -} - -#[cfg_attr(test, derive(Debug))] -struct MoveCursor { - state: MoveCursorState, - cursor: Cursor, - scroll: isize, -} - -impl MoveCursor { - fn new(cursor: Cursor, scroll: isize) -> Self { - Self { - state: MoveCursorState::New, - cursor, - scroll, - } - } -} - -impl Iterator for MoveCursor { - type Item = OutputItem<'static>; - - fn next(&mut self) -> Option { - loop { - match self.state { - MoveCursorState::ScrollPrefix => { - self.state = MoveCursorState::Scroll; - break Some(OutputItem::Slice("\x1b[".as_bytes())); - } - MoveCursorState::Scroll => { - self.state = MoveCursorState::ScrollFinalByte; - - break Some(OutputItem::UintToBytes( - UintToBytes::from_uint(self.scroll.unsigned_abs()).unwrap(), - )); - } - MoveCursorState::ScrollFinalByte => { - self.state = MoveCursorState::MovePrefix; - - break Some(OutputItem::Slice(if self.scroll > 0 { - "S".as_bytes() - } else { - "T".as_bytes() - })); - } - MoveCursorState::New => { - if self.scroll != 0 { - self.state = MoveCursorState::ScrollPrefix; - } else { - self.state = MoveCursorState::MovePrefix; - } - continue; - } - MoveCursorState::MovePrefix => { - self.state = MoveCursorState::Row; - break Some(OutputItem::Slice("\x1b[".as_bytes())); - } - MoveCursorState::Row => { - self.state = MoveCursorState::Separator; - break Some(OutputItem::UintToBytes( - UintToBytes::from_uint(self.cursor.row + 1).unwrap(), - )); - } - MoveCursorState::Separator => { - self.state = MoveCursorState::Column; - break Some(OutputItem::Slice(";".as_bytes())); - } - MoveCursorState::Column => { - self.state = MoveCursorState::MoveFinalByte; - - break Some(OutputItem::UintToBytes( - UintToBytes::from_uint(self.cursor.column + 1).unwrap(), - )); - } - MoveCursorState::MoveFinalByte => { - self.state = MoveCursorState::Done; - break Some(OutputItem::Slice("H".as_bytes())); - } - MoveCursorState::Done => break None, - } - } - } -} - -#[cfg_attr(test, derive(Debug))] -enum MoveCursorToPosition { - Position(Position), - Move(MoveCursor), -} - -impl MoveCursorToPosition { - fn new(position: Position) -> Self { - Self::Position(position) - } - - fn get_move_cursor(&mut self, terminal: &mut Terminal) -> Option<&mut MoveCursor> { - loop { - match self { - MoveCursorToPosition::Position(position) => { - let scroll = terminal.move_cursor(*position); - let cursor = terminal.get_cursor(); - - *self = MoveCursorToPosition::Move(MoveCursor::new(cursor, scroll)); - continue; - } - MoveCursorToPosition::Move(move_cursor) => break Some(move_cursor), - } - } - } -} - -enum PrintableItem<'a> { - Str(&'a str), - Newline, -} - -struct Printable<'a, I> { - s: &'a str, - newline: bool, - iter: Option, -} - -impl<'a, 'item, I> Printable<'a, I> -where - I: Iterator, - 'item: 'a, -{ - fn from_str(s: &'a str) -> Self { - Self { - s, - newline: false, - iter: None, - } - } - - fn from_iter(iter: I) -> Self { - Self { - s: "", - newline: false, - iter: Some(iter), - } - } - - fn next_item(&mut self, max_chars: usize) -> Option> { - if self.newline { - self.newline = false; - Some(PrintableItem::Newline) - } else { - let s = if self.s.is_empty() { - if let Some(iter) = &mut self.iter { - iter.next()? - } else { - return None; - } - } else { - self.s - }; - - let split_at_char = max_chars.min(s.chars().count()); - let split_at_byte = s - .char_indices() - .nth(split_at_char) - .map(|(index, _)| index) - .unwrap_or(s.len()); - - let (s, rest) = s.split_at(split_at_byte); - - if split_at_char == max_chars { - self.newline = true - } - - self.s = rest; - Some(PrintableItem::Str(s)) - } - } -} - -// #[cfg_attr(test, derive(Debug))] -enum Step<'a, I> { - Print(Printable<'a, I>), - Move(MoveCursorToPosition), - MoveCursorToEdge, - GetPosition, - SavePosition, - RestorePosition, - ClearLine, - Erase, - Newline, - Bell, - EndOfString, - Abort, - Done, -} - -impl<'a, 'item, I> Step<'a, I> -where - I: Iterator, - 'item: 'a, -{ - fn transition( - &mut self, - new_state: Step<'a, I>, - output: OutputItem<'a>, - ) -> Option> { - *self = new_state; - Some(output) - } - - fn advance(&mut self, terminal: &mut Terminal) -> Option> { - match self { - Print(printable) => { - if let Some(item) = printable.next_item(terminal.columns_remaining()) { - let s = match item { - PrintableItem::Str(s) => { - let position = terminal.relative_position(s.chars().count() as isize); - terminal.move_cursor(position); - - s - } - PrintableItem::Newline => "\n\r", - }; - - Some(OutputItem::Slice(s.as_bytes())) - } else { - *self = Step::Done; - None - } - } - Move(pos) => { - if let Some(move_cursor) = pos.get_move_cursor(terminal) { - if let Some(byte) = move_cursor.next() { - return Some(byte); - } - } - - *self = Step::Done; - None - } - MoveCursorToEdge => self.transition(Step::Done, OutputItem::Slice(b"\x1b[999;999H")), - Erase => self.transition(Step::Done, OutputItem::Slice("\x1b[J".as_bytes())), - Newline => { - let mut position = terminal.get_position(); - position.row += 1; - position.column = 0; - terminal.move_cursor(position); - - self.transition(Step::Done, OutputItem::Slice("\n\r".as_bytes())) - } - Bell => self.transition(Step::Done, OutputItem::Slice("\x07".as_bytes())), - EndOfString => self.transition(Step::Done, OutputItem::EndOfString), - Abort => self.transition(Step::Done, OutputItem::Abort), - ClearLine => { - terminal.move_cursor_to_start_of_line(); - - self.transition(Step::Done, OutputItem::Slice("\r\x1b[J".as_bytes())) - } - GetPosition => self.transition(Step::Done, OutputItem::Slice("\x1b[6n".as_bytes())), - SavePosition => self.transition(Step::Done, OutputItem::Slice(b"\x1b7")), - RestorePosition => self.transition(Step::Done, OutputItem::Slice(b"\x1b8")), - Done => None, - } - } -} - -use Step::*; - -pub struct OutputIter<'a, 'item, I> { - terminal: &'a mut Terminal, - steps: [Option>; 4], - pos: usize, - _marker: PhantomData<&'item ()>, -} - -impl<'a, 'item, I> Iterator for OutputIter<'a, 'item, I> -where - I: Iterator, - 'item: 'a, -{ - type Item = OutputItem<'a>; - - fn next(&mut self) -> Option { - loop { - if let Some(step) = self.steps.get_mut(self.pos) { - if let Some(step) = step.as_mut() { - if let Some(item) = step.advance(self.terminal) { - break Some(item); - } else { - self.pos += 1; - } - } else { - break None; - } - } else { - break None; - } - } - } -} - -fn byte_position(s: &str, char_pos: usize) -> usize { - s.char_indices() - .skip(char_pos) - .map(|(pos, _)| pos) - .next() - .unwrap_or(s.len()) -} - -pub struct Output<'a, B: Buffer, I> { - prompt: &'a Prompt, - buffer: &'a LineBuffer, - terminal: &'a mut Terminal, - action: OutputAction, -} - -impl<'a, 'item, B, I> Output<'a, B, I> -where - B: Buffer, - I: Iterator + Clone, -{ - pub fn new( - prompt: &'a Prompt, - buffer: &'a LineBuffer, - terminal: &'a mut Terminal, - action: OutputAction, - ) -> Self { - Self { - prompt, - buffer, - terminal, - action, - } - } - - fn offset_from_position(&self, position: Position) -> usize { - self.terminal.offset_from_position(position) as usize - self.prompt.len() - } - - fn current_offset(&self) -> usize { - self.offset_from_position(self.terminal.get_position()) - } - - fn buffer_after_position(&self, position: Position) -> &'a str { - let offset = self.offset_from_position(position); - let s = self.buffer.as_str(); - - let pos = byte_position(s, offset); - - &s[pos..] - } - - fn new_position(&self, cursor_move: CursorMove) -> Position { - match cursor_move { - CursorMove::Forward => self.terminal.relative_position(1), - CursorMove::Back => self.terminal.relative_position(-1), - CursorMove::Start => { - let pos = self.current_offset() as isize; - self.terminal.relative_position(-pos) - } - CursorMove::End => { - let pos = self.current_offset() as isize; - let len = self.buffer.as_str().chars().count() as isize; - #[cfg(test)] - dbg!(pos, len); - self.terminal.relative_position(len - pos) - } - } - } - - #[cfg(test)] - pub fn into_vec(self) -> Vec { - self.into_iter() - .flat_map(|item| item.get_bytes().unwrap().to_vec()) - .collect::>() - } -} - -impl<'a, 'item, B, I> IntoIterator for Output<'a, B, I> -where - B: Buffer, - I: Iterator + Clone, - 'item: 'a, -{ - type Item = OutputItem<'a>; - type IntoIter = OutputIter<'a, 'item, I>; - - fn into_iter(self) -> Self::IntoIter { - fn pack(array: [T; IN]) -> [Option; OUT] { - const { - assert!(IN <= OUT); - } - - let mut steps = [(); OUT].map(|()| None); - - for (i, step) in array.into_iter().enumerate() { - steps[i] = Some(step); - } - - steps - } - - let steps = match self.action { - OutputAction::MoveCursor(cursor_move) => { - let position = self.new_position(cursor_move); - - let offset = - self.terminal.offset_from_position(position) - self.prompt.len() as isize; - let buffer_len = self.buffer.as_str().chars().count() as isize; - - if offset >= 0 && offset <= buffer_len { - pack([Move(MoveCursorToPosition::new( - self.new_position(cursor_move), - ))]) - } else { - pack([Bell]) - } - } - OutputAction::PrintBufferAndMoveCursorForward => pack([ - Print(Printable::from_str( - self.buffer_after_position(self.terminal.get_position()), - )), - Move(MoveCursorToPosition::new( - self.terminal.relative_position(1), - )), - ]), - OutputAction::EraseAfterCursor => pack([Erase]), - OutputAction::EraseAndPrintBuffer => { - let position = self.terminal.get_position(); - - pack([ - Erase, - Print(Printable::from_str(self.buffer_after_position(position))), - Move(MoveCursorToPosition::new(position)), - ]) - } - - OutputAction::ClearScreen => { - let rows = self.terminal.scroll_to_top(); - self.terminal.move_cursor(Position::new(0, 0)); - - pack([ - Move(MoveCursorToPosition::Move(MoveCursor::new( - Cursor::new(0, 0), - rows, - ))), - Erase, - Print(Printable::from_iter(self.prompt.iter())), - ]) - } - OutputAction::ClearLine => pack([ - Move(MoveCursorToPosition::new( - self.new_position(CursorMove::Start), - )), - Erase, - ]), - OutputAction::MoveCursorBackAndPrintBufferAndMoveForward => { - let position = self.terminal.relative_position(-1); - - pack([ - Move(MoveCursorToPosition::new(position)), - Print(Printable::from_str(self.buffer_after_position(position))), - Move(MoveCursorToPosition::new(self.terminal.get_position())), - ]) - } - OutputAction::MoveCursorAndEraseAndPrintBuffer(steps) => { - let position = self.terminal.relative_position(steps); - - pack([ - Move(MoveCursorToPosition::new(position)), - Erase, - Print(Printable::from_str(self.buffer_after_position(position))), - Move(MoveCursorToPosition::new(position)), - ]) - } - OutputAction::RingBell => pack([Bell]), - OutputAction::ClearAndPrintPrompt => pack([ - ClearLine, - Print(Printable::from_iter(self.prompt.iter())), - GetPosition, - ]), - OutputAction::ClearAndPrintBuffer => { - let position = self.new_position(CursorMove::Start); - - pack([ - Move(MoveCursorToPosition::new(position)), - Erase, - Print(Printable::from_str(self.buffer.as_str())), - ]) - } - OutputAction::ProbeSize => { - pack([SavePosition, MoveCursorToEdge, GetPosition, RestorePosition]) - } - - OutputAction::Done => pack([Newline, EndOfString]), - OutputAction::Abort => pack([Newline, Abort]), - OutputAction::Nothing => pack([]), - }; - - OutputIter { - terminal: self.terminal, - steps, - pos: 0, - _marker: PhantomData, - } - } -} - -#[cfg(test)] -mod tests { - use std::string::String; - - use crate::core::StrIter; - - use super::*; - - use std::vec::Vec; - - #[test] - fn uint_to_bytes() { - fn to_string(n: usize) -> String { - let uint: UintToBytes = UintToBytes::from_uint(n).unwrap(); - - String::from_utf8(uint.as_bytes().to_vec()).unwrap() - } - - assert_eq!(to_string::<4>(0), "0"); - - assert_eq!(to_string::<4>(42), "42"); - - assert_eq!(to_string::<4>(10), "10"); - - assert_eq!(to_string::<4>(9999), "9999"); - } - - #[test] - fn move_cursor() { - fn to_string(cm: MoveCursor) -> String { - String::from_utf8( - cm.flat_map(|item| { - if let Some(bytes) = item.get_bytes() { - bytes.to_vec() - } else { - vec![] - } - }) - .collect(), - ) - .unwrap() - } - - assert_eq!( - to_string(MoveCursor::new(Cursor::new(42, 0), 0)), - "\x1b[43;1H" - ); - - assert_eq!( - to_string(MoveCursor::new(Cursor::new(0, 42), 0)), - "\x1b[1;43H" - ); - - assert_eq!( - to_string(MoveCursor::new(Cursor::new(42, 43), 0)), - "\x1b[43;44H" - ); - - assert_eq!( - to_string(MoveCursor::new(Cursor::new(0, 0), 0)), - "\x1b[1;1H" - ); - - assert_eq!( - to_string(MoveCursor::new(Cursor::new(0, 9), 0)), - "\x1b[1;10H" - ); - - assert_eq!( - to_string(MoveCursor::new(Cursor::new(0, 0), 1)), - "\x1b[1S\x1b[1;1H" - ); - - assert_eq!( - to_string(MoveCursor::new(Cursor::new(0, 0), -1)), - "\x1b[1T\x1b[1;1H" - ); - } - - #[test] - fn step() { - fn to_string<'a>(mut step: Step<'a, StrIter<'a>>, terminal: &mut Terminal) -> String { - let mut bytes = Vec::new(); - - while let Some(item) = step.advance(terminal) { - if let Some(slice) = item.get_bytes() { - for b in slice { - bytes.push(*b); - } - } - } - - String::from_utf8(bytes).unwrap() - } - - let mut terminal = Terminal::new(4, 10, Cursor::new(0, 0)); - - assert_eq!( - to_string( - Step::Print(Printable::from_str("01234567890123456789")), - &mut terminal - ), - "0123456789\n\r0123456789\n\r" - ); - - assert_eq!( - to_string(Step::Print(Printable::from_str("01234")), &mut terminal), - "01234" - ); - - assert_eq!( - to_string( - Step::Print(Printable::from_str("5678901234567890")), - &mut terminal - ), - "56789\n\r0123456789\n\r0" - ); - - assert_eq!(terminal.get_position(), Position::new(4, 1)); - - assert_eq!( - to_string( - Step::Move(MoveCursorToPosition::new(Position::new(0, 3))), - &mut terminal - ), - "\x1b[1T\x1b[1;4H" - ); - - assert_eq!(terminal.get_position(), Position::new(0, 3)); - - assert_eq!(to_string(Step::Erase, &mut terminal), "\x1b[J"); - assert_eq!(to_string(Step::Newline, &mut terminal), "\n\r"); - assert_eq!(to_string(Step::Bell, &mut terminal), "\x07"); - assert_eq!(to_string(Step::Done, &mut terminal), ""); - } - - #[test] - fn byte_iterator() { - fn to_string(output: Output<'_, B, StrIter>) -> String { - String::from_utf8( - output - .into_iter() - .flat_map(|item| { - if let Some(bytes) = item.get_bytes() { - bytes.to_vec() - } else { - vec![] - } - }) - .collect(), - ) - .unwrap() - } - - let prompt: Prompt = "> ".into(); - let mut line_buffer = LineBuffer::new_unbounded(); - let mut terminal = Terminal::new(4, 10, Cursor::new(0, 0)); - - let result = to_string(Output::new( - &prompt, - &line_buffer, - &mut terminal, - OutputAction::ClearAndPrintPrompt, - )); - - assert_eq!(result, "\r\x1b[J> \x1b[6n"); - - line_buffer.insert_str(0, "Hello, world!").unwrap(); - - let result = to_string(Output::new( - &prompt, - &line_buffer, - &mut terminal, - OutputAction::PrintBufferAndMoveCursorForward, - )); - - assert_eq!(result, "Hello, w\n\rorld!\x1b[1;4H"); - - assert_eq!(terminal.get_cursor(), Cursor::new(0, 3)); - - let result = to_string(Output::new( - &prompt, - &line_buffer, - &mut terminal, - OutputAction::MoveCursor(CursorMove::Start), - )); - - assert_eq!(result, "\x1b[1;3H"); - assert_eq!(terminal.get_cursor(), Cursor::new(0, 2)); - } - - #[test] - fn split_utf8() { - fn to_string<'a>(mut step: Step<'a, StrIter<'a>>, terminal: &mut Terminal) -> String { - let mut bytes = Vec::new(); - - while let Some(item) = step.advance(terminal) { - if let Some(slice) = item.get_bytes() { - for b in slice { - bytes.push(*b); - } - } - } - - String::from_utf8(bytes).unwrap() - } - - let mut terminal = Terminal::new(4, 10, Cursor::new(0, 0)); - - assert_eq!( - to_string( - Step::Print(Printable::from_str("aadfåpadfåaåfåaadåappaåadå")), - &mut terminal - ), - "aadfåpadfå\n\raåfåaadåap\n\rpaåadå" - ); - } -} diff --git a/noline/src/output/mod.rs b/noline/src/output/mod.rs new file mode 100644 index 0000000..f029b17 --- /dev/null +++ b/noline/src/output/mod.rs @@ -0,0 +1,16 @@ +mod move_cursor; +mod move_cursor_pos; +mod output; +mod output_item; +mod output_iter; +mod printable; +mod step; +mod uint_to_bytes; + +pub use output::*; +pub use output_item::*; +pub use output_iter::*; +pub use uint_to_bytes::*; + +#[cfg(test)] +pub(crate) mod tests; diff --git a/noline/src/output/move_cursor.rs b/noline/src/output/move_cursor.rs new file mode 100644 index 0000000..0b613d1 --- /dev/null +++ b/noline/src/output/move_cursor.rs @@ -0,0 +1,98 @@ +use super::{OutputItem, UintToBytes}; +use crate::terminal::Cursor; + +#[cfg_attr(test, derive(Debug))] +enum MoveCursorState { + New, + ScrollPrefix, + Scroll, + ScrollFinalByte, + MovePrefix, + Row, + Separator, + Column, + MoveFinalByte, + Done, +} + +#[cfg_attr(test, derive(Debug))] +pub(super) struct MoveCursor { + state: MoveCursorState, + cursor: Cursor, + scroll: isize, +} + +impl MoveCursor { + pub(super) fn new(cursor: Cursor, scroll: isize) -> Self { + Self { + state: MoveCursorState::New, + cursor, + scroll, + } + } +} + +impl Iterator for MoveCursor { + type Item = OutputItem<'static>; + + fn next(&mut self) -> Option { + loop { + match self.state { + MoveCursorState::ScrollPrefix => { + self.state = MoveCursorState::Scroll; + break Some(OutputItem::Slice("\x1b[".as_bytes())); + } + MoveCursorState::Scroll => { + self.state = MoveCursorState::ScrollFinalByte; + + break Some(OutputItem::UintToBytes( + UintToBytes::from_uint(self.scroll.unsigned_abs()).unwrap(), + )); + } + MoveCursorState::ScrollFinalByte => { + self.state = MoveCursorState::MovePrefix; + + break Some(OutputItem::Slice(if self.scroll > 0 { + "S".as_bytes() + } else { + "T".as_bytes() + })); + } + MoveCursorState::New => { + if self.scroll != 0 { + self.state = MoveCursorState::ScrollPrefix; + } else { + self.state = MoveCursorState::MovePrefix; + } + continue; + } + MoveCursorState::MovePrefix => { + self.state = MoveCursorState::Row; + break Some(OutputItem::Slice("\x1b[".as_bytes())); + } + MoveCursorState::Row => { + self.state = MoveCursorState::Separator; + break Some(OutputItem::UintToBytes( + UintToBytes::from_uint(self.cursor.row + 1).unwrap(), + )); + } + MoveCursorState::Separator => { + self.state = MoveCursorState::Column; + break Some(OutputItem::Slice(";".as_bytes())); + } + MoveCursorState::Column => { + self.state = MoveCursorState::MoveFinalByte; + + break Some(OutputItem::UintToBytes( + UintToBytes::from_uint(self.cursor.column + 1).unwrap(), + )); + } + MoveCursorState::MoveFinalByte => { + self.state = MoveCursorState::Done; + break Some(OutputItem::Slice("H".as_bytes())); + } + MoveCursorState::Done => break None, + } + } + } +} diff --git a/noline/src/output/move_cursor_pos.rs b/noline/src/output/move_cursor_pos.rs new file mode 100644 index 0000000..d90f868 --- /dev/null +++ b/noline/src/output/move_cursor_pos.rs @@ -0,0 +1,29 @@ +use super::move_cursor::MoveCursor; +use crate::terminal::{Position, Terminal}; + +#[cfg_attr(test, derive(Debug))] +pub(super) enum MoveCursorToPosition { + Position(Position), + Move(MoveCursor), +} + +impl MoveCursorToPosition { + pub(super) fn new(position: Position) -> Self { + Self::Position(position) + } + + pub(super) fn get_move_cursor(&mut self, terminal: &mut Terminal) -> Option<&mut MoveCursor> { + loop { + match self { + MoveCursorToPosition::Position(position) => { + let scroll = terminal.move_cursor(*position); + let cursor = terminal.get_cursor(); + + *self = MoveCursorToPosition::Move(MoveCursor::new(cursor, scroll)); + continue; + } + MoveCursorToPosition::Move(move_cursor) => break Some(move_cursor), + } + } + } +} diff --git a/noline/src/output/output.rs b/noline/src/output/output.rs new file mode 100644 index 0000000..e123960 --- /dev/null +++ b/noline/src/output/output.rs @@ -0,0 +1,246 @@ +use super::{ + move_cursor::MoveCursor, move_cursor_pos::MoveCursorToPosition, printable::Printable, + step::Step::*, OutputItem, OutputIter, +}; +use crate::{ + core::Prompt, + line_buffer::{Buffer, LineBuffer}, + terminal::{Cursor, Position, Terminal}, +}; +use core::marker::PhantomData; + +fn byte_position(s: &str, char_pos: usize) -> usize { + s.char_indices() + .skip(char_pos) + .map(|(pos, _)| pos) + .next() + .unwrap_or(s.len()) +} + +#[cfg_attr(test, derive(Debug))] +#[derive(Copy, Clone)] +pub enum CursorMove { + Forward, + Back, + Start, + End, +} + +#[cfg_attr(test, derive(Debug))] +#[derive(Copy, Clone)] +pub enum OutputAction { + Nothing, + MoveCursor(CursorMove), + ClearAndPrintPrompt, + ClearAndPrintBuffer, + PrintBufferAndMoveCursorForward, + EraseAfterCursor, + EraseAndPrintBuffer, + ClearScreen, + ClearLine, + MoveCursorBackAndPrintBufferAndMoveForward, + MoveCursorAndEraseAndPrintBuffer(isize), + RingBell, + ProbeSize, + Done, + Abort, +} + +pub struct Output<'a, B: Buffer, I> { + prompt: &'a Prompt, + buffer: &'a LineBuffer, + terminal: &'a mut Terminal, + action: OutputAction, +} + +impl<'a, 'item, B, I> Output<'a, B, I> +where + B: Buffer, + I: Iterator + Clone, +{ + pub fn new( + prompt: &'a Prompt, + buffer: &'a LineBuffer, + terminal: &'a mut Terminal, + action: OutputAction, + ) -> Self { + Self { + prompt, + buffer, + terminal, + action, + } + } + + fn offset_from_position(&self, position: Position) -> usize { + self.terminal.offset_from_position(position) as usize - self.prompt.len() + } + + fn current_offset(&self) -> usize { + self.offset_from_position(self.terminal.get_position()) + } + + fn buffer_after_position(&self, position: Position) -> &'a str { + let offset = self.offset_from_position(position); + let s = self.buffer.as_str(); + + let pos = byte_position(s, offset); + + &s[pos..] + } + + fn new_position(&self, cursor_move: CursorMove) -> Position { + match cursor_move { + CursorMove::Forward => self.terminal.relative_position(1), + CursorMove::Back => self.terminal.relative_position(-1), + CursorMove::Start => { + let pos = self.current_offset() as isize; + self.terminal.relative_position(-pos) + } + CursorMove::End => { + let pos = self.current_offset() as isize; + let len = self.buffer.as_str().chars().count() as isize; + #[cfg(test)] + dbg!(pos, len); + self.terminal.relative_position(len - pos) + } + } + } + + #[cfg(test)] + pub fn into_vec(self) -> Vec { + self.into_iter() + .flat_map(|item| item.get_bytes().unwrap().to_vec()) + .collect::>() + } +} + +impl<'a, 'item, B, I> IntoIterator for Output<'a, B, I> +where + B: Buffer, + I: Iterator + Clone, + 'item: 'a, +{ + type Item = OutputItem<'a>; + type IntoIter = OutputIter<'a, 'item, I>; + + fn into_iter(self) -> Self::IntoIter { + fn pack(array: [T; IN]) -> [Option; OUT] { + const { + assert!(IN <= OUT); + } + + let mut steps = [(); OUT].map(|()| None); + + for (i, step) in array.into_iter().enumerate() { + steps[i] = Some(step); + } + + steps + } + + let steps = match self.action { + OutputAction::MoveCursor(cursor_move) => { + let position = self.new_position(cursor_move); + + let offset = + self.terminal.offset_from_position(position) - self.prompt.len() as isize; + let buffer_len = self.buffer.as_str().chars().count() as isize; + + if offset >= 0 && offset <= buffer_len { + pack([Move(MoveCursorToPosition::new( + self.new_position(cursor_move), + ))]) + } else { + pack([Bell]) + } + } + OutputAction::PrintBufferAndMoveCursorForward => pack([ + Print(Printable::from_str( + self.buffer_after_position(self.terminal.get_position()), + )), + Move(MoveCursorToPosition::new( + self.terminal.relative_position(1), + )), + ]), + OutputAction::EraseAfterCursor => pack([Erase]), + OutputAction::EraseAndPrintBuffer => { + let position = self.terminal.get_position(); + + pack([ + Erase, + Print(Printable::from_str(self.buffer_after_position(position))), + Move(MoveCursorToPosition::new(position)), + ]) + } + + OutputAction::ClearScreen => { + let rows = self.terminal.scroll_to_top(); + self.terminal.move_cursor(Position::new(0, 0)); + + pack([ + Move(MoveCursorToPosition::Move(MoveCursor::new( + Cursor::new(0, 0), + rows, + ))), + Erase, + Print(Printable::from_iter(self.prompt.iter())), + ]) + } + OutputAction::ClearLine => pack([ + Move(MoveCursorToPosition::new( + self.new_position(CursorMove::Start), + )), + Erase, + ]), + OutputAction::MoveCursorBackAndPrintBufferAndMoveForward => { + let position = self.terminal.relative_position(-1); + + pack([ + Move(MoveCursorToPosition::new(position)), + Print(Printable::from_str(self.buffer_after_position(position))), + Move(MoveCursorToPosition::new(self.terminal.get_position())), + ]) + } + OutputAction::MoveCursorAndEraseAndPrintBuffer(steps) => { + let position = self.terminal.relative_position(steps); + + pack([ + Move(MoveCursorToPosition::new(position)), + Erase, + Print(Printable::from_str(self.buffer_after_position(position))), + Move(MoveCursorToPosition::new(position)), + ]) + } + OutputAction::RingBell => pack([Bell]), + OutputAction::ClearAndPrintPrompt => pack([ + ClearLine, + Print(Printable::from_iter(self.prompt.iter())), + GetPosition, + ]), + OutputAction::ClearAndPrintBuffer => { + let position = self.new_position(CursorMove::Start); + + pack([ + Move(MoveCursorToPosition::new(position)), + Erase, + Print(Printable::from_str(self.buffer.as_str())), + ]) + } + OutputAction::ProbeSize => { + pack([SavePosition, MoveCursorToEdge, GetPosition, RestorePosition]) + } + + OutputAction::Done => pack([Newline, EndOfString]), + OutputAction::Abort => pack([Newline, Abort]), + OutputAction::Nothing => pack([]), + }; + + OutputIter { + terminal: self.terminal, + steps, + pos: 0, + _marker: PhantomData, + } + } +} diff --git a/noline/src/output/output_item.rs b/noline/src/output/output_item.rs new file mode 100644 index 0000000..5bceb29 --- /dev/null +++ b/noline/src/output/output_item.rs @@ -0,0 +1,19 @@ +use super::UintToBytes; + +#[cfg_attr(test, derive(Debug))] +pub enum OutputItem<'a> { + Slice(&'a [u8]), + UintToBytes(UintToBytes<4>), + EndOfString, + Abort, +} + +impl<'a> OutputItem<'a> { + pub fn get_bytes(&self) -> Option<&[u8]> { + match self { + Self::Slice(slice) => Some(slice), + Self::UintToBytes(uint) => Some(uint.as_bytes()), + Self::EndOfString | Self::Abort => None, + } + } +} diff --git a/noline/src/output/output_iter.rs b/noline/src/output/output_iter.rs new file mode 100644 index 0000000..14d5cbb --- /dev/null +++ b/noline/src/output/output_iter.rs @@ -0,0 +1,36 @@ +use super::{step::Step, OutputItem}; +use crate::terminal::Terminal; +use core::marker::PhantomData; + +pub struct OutputIter<'a, 'item, I> { + pub(super) terminal: &'a mut Terminal, + pub(super) steps: [Option>; 4], + pub(super) pos: usize, + pub(super) _marker: PhantomData<&'item ()>, +} + +impl<'a, 'item, I> Iterator for OutputIter<'a, 'item, I> +where + I: Iterator, + 'item: 'a, +{ + type Item = OutputItem<'a>; + + fn next(&mut self) -> Option { + loop { + if let Some(step) = self.steps.get_mut(self.pos) { + if let Some(step) = step.as_mut() { + if let Some(item) = step.advance(self.terminal) { + break Some(item); + } else { + self.pos += 1; + } + } else { + break None; + } + } else { + break None; + } + } + } +} diff --git a/noline/src/output/printable.rs b/noline/src/output/printable.rs new file mode 100644 index 0000000..70e4129 --- /dev/null +++ b/noline/src/output/printable.rs @@ -0,0 +1,65 @@ +pub(super) enum PrintableItem<'a> { + Str(&'a str), + Newline, +} + +pub(super) struct Printable<'a, I> { + s: &'a str, + newline: bool, + iter: Option, +} + +impl<'a, 'item, I> Printable<'a, I> +where + I: Iterator, + 'item: 'a, +{ + pub(super) fn from_str(s: &'a str) -> Self { + Self { + s, + newline: false, + iter: None, + } + } + + pub(super) fn from_iter(iter: I) -> Self { + Self { + s: "", + newline: false, + iter: Some(iter), + } + } + + pub(super) fn next_item(&mut self, max_chars: usize) -> Option> { + if self.newline { + self.newline = false; + Some(PrintableItem::Newline) + } else { + let s = if self.s.is_empty() { + if let Some(iter) = &mut self.iter { + iter.next()? + } else { + return None; + } + } else { + self.s + }; + + let split_at_char = max_chars.min(s.chars().count()); + let split_at_byte = s + .char_indices() + .nth(split_at_char) + .map(|(index, _)| index) + .unwrap_or(s.len()); + + let (s, rest) = s.split_at(split_at_byte); + + if split_at_char == max_chars { + self.newline = true + } + + self.s = rest; + Some(PrintableItem::Str(s)) + } + } +} diff --git a/noline/src/output/step.rs b/noline/src/output/step.rs new file mode 100644 index 0000000..595bd09 --- /dev/null +++ b/noline/src/output/step.rs @@ -0,0 +1,94 @@ +use super::{ + move_cursor_pos::MoveCursorToPosition, + printable::{Printable, PrintableItem}, + step::Step::*, + OutputItem, +}; +use crate::terminal::Terminal; + +// #[cfg_attr(test, derive(Debug))] +pub(super) enum Step<'a, I> { + Print(Printable<'a, I>), + Move(MoveCursorToPosition), + MoveCursorToEdge, + GetPosition, + SavePosition, + RestorePosition, + ClearLine, + Erase, + Newline, + Bell, + EndOfString, + Abort, + Done, +} + +impl<'a, 'item, I> Step<'a, I> +where + I: Iterator, + 'item: 'a, +{ + fn transition( + &mut self, + new_state: Step<'a, I>, + output: OutputItem<'a>, + ) -> Option> { + *self = new_state; + Some(output) + } + + pub(super) fn advance(&mut self, terminal: &mut Terminal) -> Option> { + match self { + Print(printable) => { + if let Some(item) = printable.next_item(terminal.columns_remaining()) { + let s = match item { + PrintableItem::Str(s) => { + let position = terminal.relative_position(s.chars().count() as isize); + terminal.move_cursor(position); + + s + } + PrintableItem::Newline => "\n\r", + }; + + Some(OutputItem::Slice(s.as_bytes())) + } else { + *self = Step::Done; + None + } + } + Move(pos) => { + if let Some(move_cursor) = pos.get_move_cursor(terminal) { + if let Some(byte) = move_cursor.next() { + return Some(byte); + } + } + + *self = Step::Done; + None + } + MoveCursorToEdge => self.transition(Step::Done, OutputItem::Slice(b"\x1b[999;999H")), + Erase => self.transition(Step::Done, OutputItem::Slice("\x1b[J".as_bytes())), + Newline => { + let mut position = terminal.get_position(); + position.row += 1; + position.column = 0; + terminal.move_cursor(position); + + self.transition(Step::Done, OutputItem::Slice("\n\r".as_bytes())) + } + Bell => self.transition(Step::Done, OutputItem::Slice("\x07".as_bytes())), + EndOfString => self.transition(Step::Done, OutputItem::EndOfString), + Abort => self.transition(Step::Done, OutputItem::Abort), + ClearLine => { + terminal.move_cursor_to_start_of_line(); + + self.transition(Step::Done, OutputItem::Slice("\r\x1b[J".as_bytes())) + } + GetPosition => self.transition(Step::Done, OutputItem::Slice("\x1b[6n".as_bytes())), + SavePosition => self.transition(Step::Done, OutputItem::Slice(b"\x1b7")), + RestorePosition => self.transition(Step::Done, OutputItem::Slice(b"\x1b8")), + Done => None, + } + } +} diff --git a/noline/src/output/tests.rs b/noline/src/output/tests.rs new file mode 100644 index 0000000..462aa09 --- /dev/null +++ b/noline/src/output/tests.rs @@ -0,0 +1,220 @@ +use super::*; +use super::{ + move_cursor::MoveCursor, move_cursor_pos::MoveCursorToPosition, printable::Printable, + step::Step, +}; +use crate::{ + core::{Prompt, StrIter}, + line_buffer::{Buffer, LineBuffer}, + terminal::{Cursor, Position, Terminal}, +}; +use std::string::String; +use std::vec::Vec; + +#[test] +fn uint_to_bytes() { + fn to_string(n: usize) -> String { + let uint: UintToBytes = UintToBytes::from_uint(n).unwrap(); + + String::from_utf8(uint.as_bytes().to_vec()).unwrap() + } + + assert_eq!(to_string::<4>(0), "0"); + + assert_eq!(to_string::<4>(42), "42"); + + assert_eq!(to_string::<4>(10), "10"); + + assert_eq!(to_string::<4>(9999), "9999"); +} + +#[test] +fn move_cursor() { + fn to_string(cm: MoveCursor) -> String { + String::from_utf8( + cm.flat_map(|item| { + if let Some(bytes) = item.get_bytes() { + bytes.to_vec() + } else { + vec![] + } + }) + .collect(), + ) + .unwrap() + } + + assert_eq!( + to_string(MoveCursor::new(Cursor::new(42, 0), 0)), + "\x1b[43;1H" + ); + + assert_eq!( + to_string(MoveCursor::new(Cursor::new(0, 42), 0)), + "\x1b[1;43H" + ); + + assert_eq!( + to_string(MoveCursor::new(Cursor::new(42, 43), 0)), + "\x1b[43;44H" + ); + + assert_eq!( + to_string(MoveCursor::new(Cursor::new(0, 0), 0)), + "\x1b[1;1H" + ); + + assert_eq!( + to_string(MoveCursor::new(Cursor::new(0, 9), 0)), + "\x1b[1;10H" + ); + + assert_eq!( + to_string(MoveCursor::new(Cursor::new(0, 0), 1)), + "\x1b[1S\x1b[1;1H" + ); + + assert_eq!( + to_string(MoveCursor::new(Cursor::new(0, 0), -1)), + "\x1b[1T\x1b[1;1H" + ); +} + +#[test] +fn step() { + fn to_string<'a>(mut step: Step<'a, StrIter<'a>>, terminal: &mut Terminal) -> String { + let mut bytes = Vec::new(); + + while let Some(item) = step.advance(terminal) { + if let Some(slice) = item.get_bytes() { + for b in slice { + bytes.push(*b); + } + } + } + + String::from_utf8(bytes).unwrap() + } + + let mut terminal = Terminal::new(4, 10, Cursor::new(0, 0)); + + assert_eq!( + to_string( + Step::Print(Printable::from_str("01234567890123456789")), + &mut terminal + ), + "0123456789\n\r0123456789\n\r" + ); + + assert_eq!( + to_string(Step::Print(Printable::from_str("01234")), &mut terminal), + "01234" + ); + + assert_eq!( + to_string( + Step::Print(Printable::from_str("5678901234567890")), + &mut terminal + ), + "56789\n\r0123456789\n\r0" + ); + + assert_eq!(terminal.get_position(), Position::new(4, 1)); + + assert_eq!( + to_string( + Step::Move(MoveCursorToPosition::new(Position::new(0, 3))), + &mut terminal + ), + "\x1b[1T\x1b[1;4H" + ); + + assert_eq!(terminal.get_position(), Position::new(0, 3)); + + assert_eq!(to_string(Step::Erase, &mut terminal), "\x1b[J"); + assert_eq!(to_string(Step::Newline, &mut terminal), "\n\r"); + assert_eq!(to_string(Step::Bell, &mut terminal), "\x07"); + assert_eq!(to_string(Step::Done, &mut terminal), ""); +} + +#[test] +fn byte_iterator() { + fn to_string(output: Output<'_, B, StrIter>) -> String { + String::from_utf8( + output + .into_iter() + .flat_map(|item| { + if let Some(bytes) = item.get_bytes() { + bytes.to_vec() + } else { + vec![] + } + }) + .collect(), + ) + .unwrap() + } + + let prompt: Prompt = "> ".into(); + let mut line_buffer = LineBuffer::new_unbounded(); + let mut terminal = Terminal::new(4, 10, Cursor::new(0, 0)); + + let result = to_string(Output::new( + &prompt, + &line_buffer, + &mut terminal, + OutputAction::ClearAndPrintPrompt, + )); + + assert_eq!(result, "\r\x1b[J> \x1b[6n"); + + line_buffer.insert_str(0, "Hello, world!").unwrap(); + + let result = to_string(Output::new( + &prompt, + &line_buffer, + &mut terminal, + OutputAction::PrintBufferAndMoveCursorForward, + )); + + assert_eq!(result, "Hello, w\n\rorld!\x1b[1;4H"); + + assert_eq!(terminal.get_cursor(), Cursor::new(0, 3)); + + let result = to_string(Output::new( + &prompt, + &line_buffer, + &mut terminal, + OutputAction::MoveCursor(CursorMove::Start), + )); + + assert_eq!(result, "\x1b[1;3H"); + assert_eq!(terminal.get_cursor(), Cursor::new(0, 2)); +} + +#[test] +fn split_utf8() { + fn to_string<'a>(mut step: Step<'a, StrIter<'a>>, terminal: &mut Terminal) -> String { + let mut bytes = Vec::new(); + + while let Some(item) = step.advance(terminal) { + if let Some(slice) = item.get_bytes() { + for b in slice { + bytes.push(*b); + } + } + } + + String::from_utf8(bytes).unwrap() + } + + let mut terminal = Terminal::new(4, 10, Cursor::new(0, 0)); + + assert_eq!( + to_string( + Step::Print(Printable::from_str("aadfåpadfåaåfåaadåappaåadå")), + &mut terminal + ), + "aadfåpadfå\n\raåfåaadåap\n\rpaåadå" + ); +} diff --git a/noline/src/output/uint_to_bytes.rs b/noline/src/output/uint_to_bytes.rs new file mode 100644 index 0000000..fb353e5 --- /dev/null +++ b/noline/src/output/uint_to_bytes.rs @@ -0,0 +1,33 @@ +#[cfg_attr(test, derive(Debug))] +#[derive(Copy, Clone)] +pub struct UintToBytes { + bytes: [u8; N], +} + +impl UintToBytes { + pub(super) fn from_uint>(n: I) -> Option { + let mut n: usize = n.into(); + + if n < 10_usize.pow(N as u32) { + let mut bytes = [0; N]; + + for i in (0..N).rev() { + bytes[i] = 0x30 + (n % 10) as u8; + n /= 10; + + if n == 0 { + break; + } + } + + Some(Self { bytes }) + } else { + None + } + } + + pub fn as_bytes(&self) -> &[u8] { + let start = self.bytes.iter().take_while(|&&b| b == 0).count(); + &self.bytes[start..] + } +} diff --git a/noline/src/terminal.rs b/noline/src/terminal.rs deleted file mode 100644 index c5bd4bc..0000000 --- a/noline/src/terminal.rs +++ /dev/null @@ -1,303 +0,0 @@ -fn distance_from_window(start: isize, end: isize, point: isize) -> isize { - if point < start { - point - start - } else if point > end { - point - end - } else { - 0 - } -} - -#[cfg_attr(test, derive(Debug))] -#[derive(Copy, Clone, PartialEq, Eq)] -pub struct Cursor { - pub row: usize, - pub column: usize, -} - -impl Cursor { - pub fn new(row: usize, column: usize) -> Self { - Self { row, column } - } -} - -#[cfg_attr(test, derive(Debug))] -#[derive(Copy, Clone, PartialEq, Eq)] -pub struct Position { - pub row: usize, - pub column: usize, -} - -impl Position { - pub fn new(row: usize, column: usize) -> Self { - Self { row, column } - } -} - -#[cfg_attr(test, derive(Debug, PartialEq, Eq))] -pub struct Terminal { - rows: usize, - columns: usize, - cursor: Cursor, - row_offset: isize, -} - -impl Default for Terminal { - fn default() -> Self { - Self::new(24, 80, Cursor::new(0, 0)) - } -} - -impl Terminal { - pub fn new(rows: usize, columns: usize, cursor: Cursor) -> Self { - let row_offset = -(cursor.row as isize); - - Self { - rows, - columns, - cursor, - row_offset, - } - } - - pub fn resize(&mut self, rows: usize, columns: usize) { - self.rows = rows; - self.columns = columns; - } - - pub fn reset(&mut self, cursor: Cursor) { - self.cursor = cursor; - self.row_offset = -(cursor.row as isize); - } - - pub fn get_cursor(&self) -> Cursor { - self.cursor - } - - pub fn get_position(&self) -> Position { - self.cursor_to_position(self.cursor) - } - - pub fn scrolling_needed(&self, position: Position) -> isize { - distance_from_window( - self.row_offset, - self.row_offset + self.rows as isize - 1, - position.row as isize, - ) - } - - pub fn scroll_to_top(&mut self) -> isize { - let rows = self.row_offset; - self.row_offset = 0; - - rows - } - - pub fn scroll(&mut self, rows: isize) { - self.row_offset += rows; - } - - pub fn move_cursor(&mut self, position: Position) -> isize { - let rows = self.scrolling_needed(position); - self.scroll(rows); - - #[cfg(test)] - dbg!(rows, position); - - self.cursor = self - .position_to_cursor(position) - .unwrap_or_else(|| unreachable!()); - - rows - } - - pub fn move_cursor_to_start_of_line(&mut self) { - self.cursor.column = 0; - } - - pub fn position_to_cursor(&self, position: Position) -> Option { - let row = position.row as isize - self.row_offset; - - if row >= 0 && row < self.rows as isize { - Some(Cursor::new(row as usize, position.column)) - } else { - None - } - } - - pub fn cursor_to_position(&self, position: Cursor) -> Position { - #[cfg(test)] - dbg!(self.row_offset); - - Position::new( - (position.row as isize + self.row_offset) as usize, - position.column, - ) - } - - pub fn offset_from_position(&self, position: Position) -> isize { - position.row as isize * self.columns as isize + position.column as isize - } - - pub fn current_offset(&self) -> isize { - let position = self.cursor_to_position(self.cursor); - self.offset_from_position(position) - } - - fn position_from_offset(&self, offset: isize) -> Position { - let row = offset.div_euclid(self.columns as isize); - let column = offset.rem_euclid(self.columns as isize); - Position::new(row as usize, column as usize) - } - - pub fn relative_position(&self, steps: isize) -> Position { - let offset = self.offset_from_position(self.cursor_to_position(self.cursor)); - - self.position_from_offset(offset + steps) - } - - pub fn columns_remaining(&self) -> usize { - self.columns - self.cursor.column - } - - #[cfg(test)] - pub fn get_size(&self) -> (usize, usize) { - (self.rows, self.columns) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_distance_from_window() { - assert_eq!(distance_from_window(4, 8, 2), -2); - assert_eq!(distance_from_window(4, 8, 4), 0); - assert_eq!(distance_from_window(4, 8, 8), 0); - assert_eq!(distance_from_window(4, 8, 10), 2); - - assert_eq!(distance_from_window(-3, 8, 2), 0); - assert_eq!(distance_from_window(-3, 8, -5), -2); - assert_eq!(distance_from_window(-3, 8, 9), 1); - } - - #[test] - fn position_from_top() { - let term = Terminal::new(4, 10, Cursor::new(0, 0)); - - assert_eq!( - term.cursor_to_position(term.get_cursor()), - Position::new(0, 0) - ); - - assert_eq!( - term.cursor_to_position(Cursor::new(3, 9)), - Position::new(3, 9) - ); - - assert_eq!( - term.cursor_to_position(Cursor::new(4, 9)), - Position::new(4, 9) - ); - - assert_eq!( - term.position_to_cursor(Position::new(3, 9)), - Some(Cursor::new(3, 9)) - ); - - assert_eq!(term.position_to_cursor(Position::new(4, 9)), None); - } - - #[test] - fn position_from_second_line() { - let term = Terminal::new(4, 10, Cursor::new(1, 0)); - - assert_eq!( - term.cursor_to_position(term.get_cursor()), - Position::new(0, 0) - ); - - assert_eq!( - term.cursor_to_position(Cursor::new(3, 9)), - Position::new(2, 9) - ); - - assert_eq!( - term.position_to_cursor(Position::new(2, 9)), - Some(Cursor::new(3, 9)) - ); - } - - #[test] - fn position_scroll() { - let mut term = Terminal::new(4, 10, Cursor::new(0, 0)); - - assert_eq!(term.move_cursor(Position::new(7, 0)), 4); - - assert_eq!( - term.cursor_to_position(term.get_cursor()), - Position::new(7, 0) - ); - - assert_eq!( - term.cursor_to_position(Cursor::new(3, 9)), - Position::new(7, 9) - ); - - assert_eq!( - term.cursor_to_position(Cursor::new(0, 0)), - Position::new(4, 0) - ); - - assert_eq!(term.position_to_cursor(Position::new(2, 9)), None); - } - - #[test] - fn position_scroll_offset() { - let mut term = Terminal::new(4, 10, Cursor::new(3, 9)); - - let position = term.relative_position(1); - - assert_eq!(position, Position::new(1, 0)); - assert_eq!(term.position_to_cursor(position), None); - - assert_eq!(term.move_cursor(Position::new(1, 0)), 1); - } - - #[test] - fn move_cursor() { - let mut term = Terminal::new(4, 10, Cursor::new(0, 0)); - - let pos = Position::new(3, 9); - assert_eq!(term.scrolling_needed(pos), 0); - - assert_eq!(term.move_cursor(pos), 0); - - let pos = Position::new(4, 0); - assert_eq!(term.scrolling_needed(pos), 1); - - assert_eq!(term.move_cursor(pos), 1); - - assert_eq!(term.get_cursor(), Cursor::new(3, 0)); - assert_eq!(term.get_position(), Position::new(4, 0)); - assert_eq!(term.current_offset(), 40); - - let pos = Position::new(0, 0); - assert_eq!(term.scrolling_needed(pos), -1); - - assert_eq!(term.move_cursor(pos), -1); - - assert_eq!(term.get_cursor(), Cursor::new(0, 0)); - assert_eq!(term.get_position(), Position::new(0, 0)); - } - - #[test] - fn offset() { - let term = Terminal::new(4, 10, Cursor::new(1, 0)); - - assert_eq!(term.get_cursor(), Cursor::new(1, 0)); - assert_eq!(term.get_position(), Position::new(0, 0)); - assert_eq!(term.current_offset(), 0); - } -} diff --git a/noline/src/terminal/cursor.rs b/noline/src/terminal/cursor.rs new file mode 100644 index 0000000..5aeaa58 --- /dev/null +++ b/noline/src/terminal/cursor.rs @@ -0,0 +1,12 @@ +#[cfg_attr(test, derive(Debug))] +#[derive(Copy, Clone, PartialEq, Eq)] +pub struct Cursor { + pub row: usize, + pub column: usize, +} + +impl Cursor { + pub fn new(row: usize, column: usize) -> Self { + Self { row, column } + } +} diff --git a/noline/src/terminal/mod.rs b/noline/src/terminal/mod.rs new file mode 100644 index 0000000..945f301 --- /dev/null +++ b/noline/src/terminal/mod.rs @@ -0,0 +1,10 @@ +mod cursor; +mod position; +mod terminal; + +pub use cursor::*; +pub use position::*; +pub use terminal::*; + +#[cfg(test)] +pub(crate) mod tests; diff --git a/noline/src/terminal/position.rs b/noline/src/terminal/position.rs new file mode 100644 index 0000000..fda4030 --- /dev/null +++ b/noline/src/terminal/position.rs @@ -0,0 +1,12 @@ +#[cfg_attr(test, derive(Debug))] +#[derive(Copy, Clone, PartialEq, Eq)] +pub struct Position { + pub row: usize, + pub column: usize, +} + +impl Position { + pub fn new(row: usize, column: usize) -> Self { + Self { row, column } + } +} diff --git a/noline/src/terminal/terminal.rs b/noline/src/terminal/terminal.rs new file mode 100644 index 0000000..8d96f67 --- /dev/null +++ b/noline/src/terminal/terminal.rs @@ -0,0 +1,143 @@ +use super::{Cursor, Position}; + +pub(super) fn distance_from_window(start: isize, end: isize, point: isize) -> isize { + if point < start { + point - start + } else if point > end { + point - end + } else { + 0 + } +} + +#[cfg_attr(test, derive(Debug, PartialEq, Eq))] +pub struct Terminal { + rows: usize, + columns: usize, + cursor: Cursor, + row_offset: isize, +} + +impl Default for Terminal { + fn default() -> Self { + Self::new(24, 80, Cursor::new(0, 0)) + } +} + +impl Terminal { + pub fn new(rows: usize, columns: usize, cursor: Cursor) -> Self { + let row_offset = -(cursor.row as isize); + + Self { + rows, + columns, + cursor, + row_offset, + } + } + + pub fn resize(&mut self, rows: usize, columns: usize) { + self.rows = rows; + self.columns = columns; + } + + pub fn reset(&mut self, cursor: Cursor) { + self.cursor = cursor; + self.row_offset = -(cursor.row as isize); + } + + pub fn get_cursor(&self) -> Cursor { + self.cursor + } + + pub fn get_position(&self) -> Position { + self.cursor_to_position(self.cursor) + } + + pub fn scrolling_needed(&self, position: Position) -> isize { + distance_from_window( + self.row_offset, + self.row_offset + self.rows as isize - 1, + position.row as isize, + ) + } + + pub fn scroll_to_top(&mut self) -> isize { + let rows = self.row_offset; + self.row_offset = 0; + + rows + } + + pub fn scroll(&mut self, rows: isize) { + self.row_offset += rows; + } + + pub fn move_cursor(&mut self, position: Position) -> isize { + let rows = self.scrolling_needed(position); + self.scroll(rows); + + #[cfg(test)] + dbg!(rows, position); + + self.cursor = self + .position_to_cursor(position) + .unwrap_or_else(|| unreachable!()); + + rows + } + + pub fn move_cursor_to_start_of_line(&mut self) { + self.cursor.column = 0; + } + + pub fn position_to_cursor(&self, position: Position) -> Option { + let row = position.row as isize - self.row_offset; + + if row >= 0 && row < self.rows as isize { + Some(Cursor::new(row as usize, position.column)) + } else { + None + } + } + + pub fn cursor_to_position(&self, position: Cursor) -> Position { + #[cfg(test)] + dbg!(self.row_offset); + + Position::new( + (position.row as isize + self.row_offset) as usize, + position.column, + ) + } + + pub fn offset_from_position(&self, position: Position) -> isize { + position.row as isize * self.columns as isize + position.column as isize + } + + pub fn current_offset(&self) -> isize { + let position = self.cursor_to_position(self.cursor); + self.offset_from_position(position) + } + + fn position_from_offset(&self, offset: isize) -> Position { + let row = offset.div_euclid(self.columns as isize); + let column = offset.rem_euclid(self.columns as isize); + Position::new(row as usize, column as usize) + } + + pub fn relative_position(&self, steps: isize) -> Position { + let offset = self.offset_from_position(self.cursor_to_position(self.cursor)); + + self.position_from_offset(offset + steps) + } + + pub fn columns_remaining(&self) -> usize { + self.columns - self.cursor.column + } + + #[cfg(test)] + pub fn get_size(&self) -> (usize, usize) { + (self.rows, self.columns) + } +} diff --git a/noline/src/terminal/tests.rs b/noline/src/terminal/tests.rs new file mode 100644 index 0000000..4d10ebd --- /dev/null +++ b/noline/src/terminal/tests.rs @@ -0,0 +1,132 @@ +use super::*; + +#[test] +fn test_distance_from_window() { + assert_eq!(distance_from_window(4, 8, 2), -2); + assert_eq!(distance_from_window(4, 8, 4), 0); + assert_eq!(distance_from_window(4, 8, 8), 0); + assert_eq!(distance_from_window(4, 8, 10), 2); + + assert_eq!(distance_from_window(-3, 8, 2), 0); + assert_eq!(distance_from_window(-3, 8, -5), -2); + assert_eq!(distance_from_window(-3, 8, 9), 1); +} + +#[test] +fn position_from_top() { + let term = Terminal::new(4, 10, Cursor::new(0, 0)); + + assert_eq!( + term.cursor_to_position(term.get_cursor()), + Position::new(0, 0) + ); + + assert_eq!( + term.cursor_to_position(Cursor::new(3, 9)), + Position::new(3, 9) + ); + + assert_eq!( + term.cursor_to_position(Cursor::new(4, 9)), + Position::new(4, 9) + ); + + assert_eq!( + term.position_to_cursor(Position::new(3, 9)), + Some(Cursor::new(3, 9)) + ); + + assert_eq!(term.position_to_cursor(Position::new(4, 9)), None); +} + +#[test] +fn position_from_second_line() { + let term = Terminal::new(4, 10, Cursor::new(1, 0)); + + assert_eq!( + term.cursor_to_position(term.get_cursor()), + Position::new(0, 0) + ); + + assert_eq!( + term.cursor_to_position(Cursor::new(3, 9)), + Position::new(2, 9) + ); + + assert_eq!( + term.position_to_cursor(Position::new(2, 9)), + Some(Cursor::new(3, 9)) + ); +} + +#[test] +fn position_scroll() { + let mut term = Terminal::new(4, 10, Cursor::new(0, 0)); + + assert_eq!(term.move_cursor(Position::new(7, 0)), 4); + + assert_eq!( + term.cursor_to_position(term.get_cursor()), + Position::new(7, 0) + ); + + assert_eq!( + term.cursor_to_position(Cursor::new(3, 9)), + Position::new(7, 9) + ); + + assert_eq!( + term.cursor_to_position(Cursor::new(0, 0)), + Position::new(4, 0) + ); + + assert_eq!(term.position_to_cursor(Position::new(2, 9)), None); +} + +#[test] +fn position_scroll_offset() { + let mut term = Terminal::new(4, 10, Cursor::new(3, 9)); + + let position = term.relative_position(1); + + assert_eq!(position, Position::new(1, 0)); + assert_eq!(term.position_to_cursor(position), None); + + assert_eq!(term.move_cursor(Position::new(1, 0)), 1); +} + +#[test] +fn move_cursor() { + let mut term = Terminal::new(4, 10, Cursor::new(0, 0)); + + let pos = Position::new(3, 9); + assert_eq!(term.scrolling_needed(pos), 0); + + assert_eq!(term.move_cursor(pos), 0); + + let pos = Position::new(4, 0); + assert_eq!(term.scrolling_needed(pos), 1); + + assert_eq!(term.move_cursor(pos), 1); + + assert_eq!(term.get_cursor(), Cursor::new(3, 0)); + assert_eq!(term.get_position(), Position::new(4, 0)); + assert_eq!(term.current_offset(), 40); + + let pos = Position::new(0, 0); + assert_eq!(term.scrolling_needed(pos), -1); + + assert_eq!(term.move_cursor(pos), -1); + + assert_eq!(term.get_cursor(), Cursor::new(0, 0)); + assert_eq!(term.get_position(), Position::new(0, 0)); +} + +#[test] +fn offset() { + let term = Terminal::new(4, 10, Cursor::new(1, 0)); + + assert_eq!(term.get_cursor(), Cursor::new(1, 0)); + assert_eq!(term.get_position(), Position::new(0, 0)); + assert_eq!(term.current_offset(), 0); +} diff --git a/noline/src/testlib/byte_vec.rs b/noline/src/testlib/byte_vec.rs new file mode 100644 index 0000000..660d455 --- /dev/null +++ b/noline/src/testlib/byte_vec.rs @@ -0,0 +1,45 @@ +use crate::input::ControlCharacter; + +pub trait ToByteVec { + fn to_byte_vec(self) -> Vec; +} + +impl ToByteVec for &str { + fn to_byte_vec(self) -> Vec { + self.bytes().collect() + } +} + +impl ToByteVec for ControlCharacter { + fn to_byte_vec(self) -> Vec { + [self.into()].into_iter().collect() + } +} + +impl ToByteVec for Vec { + fn to_byte_vec(self) -> Vec { + self.into_iter().map(|c| c.into()).collect() + } +} + +impl ToByteVec for [ControlCharacter; N] { + fn to_byte_vec(self) -> Vec { + self.into_iter().map(|c| c.into()).collect() + } +} + +impl ToByteVec for Vec<&str> { + fn to_byte_vec(self) -> Vec { + self.into_iter() + .flat_map(|s| s.as_bytes().iter().copied()) + .collect() + } +} + +impl ToByteVec for [&str; N] { + fn to_byte_vec(self) -> Vec { + self.into_iter() + .flat_map(|s| s.as_bytes().iter().copied()) + .collect() + } +} diff --git a/noline/src/testlib/csi.rs b/noline/src/testlib/csi.rs new file mode 100644 index 0000000..fb6c56a --- /dev/null +++ b/noline/src/testlib/csi.rs @@ -0,0 +1,7 @@ +pub const UP: &str = "\x1b[A"; +pub const DOWN: &str = "\x1b[B"; +pub const LEFT: &str = "\x1b[D"; +pub const RIGHT: &str = "\x1b[C"; +pub const HOME: &str = "\x1b[1~"; +pub const DELETE: &str = "\x1b[3~"; +pub const END: &str = "\x1b[4~"; diff --git a/noline/src/testlib/input_builder.rs b/noline/src/testlib/input_builder.rs new file mode 100644 index 0000000..887ae9f --- /dev/null +++ b/noline/src/testlib/input_builder.rs @@ -0,0 +1,21 @@ +use super::ToByteVec; + +pub(super) struct InputBuilder { + items: Vec, +} + +impl InputBuilder { + pub(super) fn new() -> Self { + Self { items: Vec::new() } + } + + pub(super) fn add(&mut self, input: impl ToByteVec) { + self.items.extend(input.to_byte_vec().iter()); + } +} + +impl ToByteVec for InputBuilder { + fn to_byte_vec(self) -> Vec { + self.items + } +} diff --git a/noline/src/testlib.rs b/noline/src/testlib/mock_terminal.rs similarity index 57% rename from noline/src/testlib.rs rename to noline/src/testlib/mock_terminal.rs index 3120b12..eb41c2a 100644 --- a/noline/src/testlib.rs +++ b/noline/src/testlib/mock_terminal.rs @@ -1,26 +1,11 @@ -use core::time::Duration; +use crate::input::{Action, ControlCharacter, Parser, CSI}; +use crate::terminal::Cursor; +use crossbeam::channel::{unbounded, Receiver, Sender}; use std::string::String; use std::thread; use std::thread::JoinHandle; use std::vec::Vec; -use crossbeam::channel::{unbounded, Receiver, Sender}; - -use crate::input::{Action, ControlCharacter, Parser, CSI}; -use crate::terminal::Cursor; - -use ControlCharacter::*; - -pub mod csi { - pub const UP: &str = "\x1b[A"; - pub const DOWN: &str = "\x1b[B"; - pub const LEFT: &str = "\x1b[D"; - pub const RIGHT: &str = "\x1b[C"; - pub const HOME: &str = "\x1b[1~"; - pub const DELETE: &str = "\x1b[3~"; - pub const END: &str = "\x1b[4~"; -} - pub struct MockTerminal { parser: Parser, screen: Vec>, @@ -213,187 +198,3 @@ impl MockTerminal { (self.terminal_tx.take(), self.keyboard_rx.clone()) } } - -impl ToByteVec for &str { - fn to_byte_vec(self) -> Vec { - self.bytes().collect() - } -} - -impl ToByteVec for ControlCharacter { - fn to_byte_vec(self) -> Vec { - [self.into()].into_iter().collect() - } -} - -impl ToByteVec for Vec { - fn to_byte_vec(self) -> Vec { - self.into_iter().map(|c| c.into()).collect() - } -} - -impl ToByteVec for [ControlCharacter; N] { - fn to_byte_vec(self) -> Vec { - self.into_iter().map(|c| c.into()).collect() - } -} - -impl ToByteVec for Vec<&str> { - fn to_byte_vec(self) -> Vec { - self.into_iter() - .flat_map(|s| s.as_bytes().iter().copied()) - .collect() - } -} - -impl ToByteVec for [&str; N] { - fn to_byte_vec(self) -> Vec { - self.into_iter() - .flat_map(|s| s.as_bytes().iter().copied()) - .collect() - } -} - -pub trait ToByteVec { - fn to_byte_vec(self) -> Vec; -} - -#[derive(Debug)] -pub struct TestCase { - pub input: Vec>, - pub output: Vec, -} - -impl TestCase { - pub fn new( - input: impl IntoIterator, - output: impl IntoIterator>, - ) -> Self { - Self { - input: input.into_iter().map(|item| item.to_byte_vec()).collect(), - output: output.into_iter().map(|s| s.into()).collect(), - } - } - - pub fn screen_as_string(&self, prompt: &str, columns: usize) -> String { - let mut screen = Vec::new(); - let mut line = Vec::new(); - - line.extend(prompt.chars()); - - for s in &self.output { - for c in s.chars() { - line.push(c); - - if line.len() >= columns { - screen.extend(line.drain(0..)); - screen.push('\n'); - } - } - - if !line.is_empty() { - screen.extend(line.drain(0..)); - screen.push('\n'); - screen.extend(prompt.chars()); - } - } - - screen.into_iter().collect() - } -} - -struct InputBuilder { - items: Vec, -} - -impl InputBuilder { - fn new() -> Self { - Self { items: Vec::new() } - } - - fn add(&mut self, input: impl ToByteVec) { - self.items.extend(input.to_byte_vec().iter()); - } -} - -impl ToByteVec for InputBuilder { - fn to_byte_vec(self) -> Vec { - self.items - } -} - -pub fn test_cases() -> Vec { - vec![ - TestCase::new(["Hello, World!"], ["Hello, World!"]), - { - let mut input = InputBuilder::new(); - - input.add("abc"); - input.add(csi::LEFT); - input.add(CtrlD); - input.add("de"); - - TestCase::new([input], ["abde"]) - }, - TestCase::new(["abc", "def"], ["abc", "def"]), - ] -} - -pub fn test_editor_with_case( - case: TestCase, - prompt: &str, - get_io: impl FnOnce(&mut MockTerminal) -> IO, - spawn_thread: impl FnOnce(IO, Sender) -> JoinHandle<()>, -) { - let (rows, columns) = (20, 80); - - let (string_tx, string_rx) = unbounded(); - - let mut term = MockTerminal::new(rows, columns, Cursor::new(0, 0)); - - let keyboard_tx = term.keyboard_tx.clone(); - - let io = get_io(&mut term); - - let term = term.start_thread(); - let handle = spawn_thread(io, string_tx); - - let output: Vec = case - .input - .iter() - .map(|seq| { - // To avoid race with prompt reset, we need to wait a - // little. This is not ideal, but will do for now. - thread::sleep(core::time::Duration::from_millis(100)); - - for &b in seq { - keyboard_tx.send(b).unwrap(); - } - - keyboard_tx.send(0xd).unwrap(); - - string_rx.recv().unwrap() - }) - .collect(); - - // Added delay to prevent race with terminal reset - std::thread::sleep(Duration::from_millis(100)); - - keyboard_tx.send(0x3).unwrap(); - - drop(keyboard_tx); - let term = term.join().unwrap(); - - handle.join().unwrap(); - - assert_eq!(output.len(), case.output.len()); - - for (seen, expected) in output.iter().zip(case.output.iter()) { - assert_eq!(seen, expected); - } - - assert_eq!( - term.screen_as_string(), - case.screen_as_string(prompt, columns) - ); -} diff --git a/noline/src/testlib/mod.rs b/noline/src/testlib/mod.rs new file mode 100644 index 0000000..f3071de --- /dev/null +++ b/noline/src/testlib/mod.rs @@ -0,0 +1,9 @@ +pub mod byte_vec; +pub mod csi; +pub mod input_builder; +pub mod mock_terminal; +pub mod test_case; + +pub use byte_vec::*; +pub use mock_terminal::*; +pub use test_case::*; diff --git a/noline/src/testlib/test_case.rs b/noline/src/testlib/test_case.rs new file mode 100644 index 0000000..d0b1a07 --- /dev/null +++ b/noline/src/testlib/test_case.rs @@ -0,0 +1,130 @@ +use super::{byte_vec::ToByteVec, csi, input_builder::InputBuilder, MockTerminal}; +use crate::input::ControlCharacter; +use crate::terminal::Cursor; +use core::time::Duration; +use crossbeam::channel::{unbounded, Sender}; +use std::string::String; +use std::thread; +use std::thread::JoinHandle; +use std::vec::Vec; +use ControlCharacter::*; + +#[derive(Debug)] +pub struct TestCase { + pub input: Vec>, + pub output: Vec, +} + +impl TestCase { + pub fn new( + input: impl IntoIterator, + output: impl IntoIterator>, + ) -> Self { + Self { + input: input.into_iter().map(|item| item.to_byte_vec()).collect(), + output: output.into_iter().map(|s| s.into()).collect(), + } + } + + pub fn screen_as_string(&self, prompt: &str, columns: usize) -> String { + let mut screen = Vec::new(); + let mut line = Vec::new(); + + line.extend(prompt.chars()); + + for s in &self.output { + for c in s.chars() { + line.push(c); + + if line.len() >= columns { + screen.extend(line.drain(0..)); + screen.push('\n'); + } + } + + if !line.is_empty() { + screen.extend(line.drain(0..)); + screen.push('\n'); + screen.extend(prompt.chars()); + } + } + + screen.into_iter().collect() + } +} + +pub fn test_cases() -> Vec { + vec![ + TestCase::new(["Hello, World!"], ["Hello, World!"]), + { + let mut input = InputBuilder::new(); + + input.add("abc"); + input.add(csi::LEFT); + input.add(CtrlD); + input.add("de"); + + TestCase::new([input], ["abde"]) + }, + TestCase::new(["abc", "def"], ["abc", "def"]), + ] +} + +pub fn test_editor_with_case( + case: TestCase, + prompt: &str, + get_io: impl FnOnce(&mut MockTerminal) -> IO, + spawn_thread: impl FnOnce(IO, Sender) -> JoinHandle<()>, +) { + let (rows, columns) = (20, 80); + + let (string_tx, string_rx) = unbounded(); + + let mut term = MockTerminal::new(rows, columns, Cursor::new(0, 0)); + + let keyboard_tx = term.keyboard_tx.clone(); + + let io = get_io(&mut term); + + let term = term.start_thread(); + let handle = spawn_thread(io, string_tx); + + let output: Vec = case + .input + .iter() + .map(|seq| { + // To avoid race with prompt reset, we need to wait a + // little. This is not ideal, but will do for now. + thread::sleep(core::time::Duration::from_millis(100)); + + for &b in seq { + keyboard_tx.send(b).unwrap(); + } + + keyboard_tx.send(0xd).unwrap(); + + string_rx.recv().unwrap() + }) + .collect(); + + // Added delay to prevent race with terminal reset + std::thread::sleep(Duration::from_millis(100)); + + keyboard_tx.send(0x3).unwrap(); + + drop(keyboard_tx); + let term = term.join().unwrap(); + + handle.join().unwrap(); + + assert_eq!(output.len(), case.output.len()); + + for (seen, expected) in output.iter().zip(case.output.iter()) { + assert_eq!(seen, expected); + } + + assert_eq!( + term.screen_as_string(), + case.screen_as_string(prompt, columns) + ); +} diff --git a/noline/src/utf8.rs b/noline/src/utf8.rs deleted file mode 100644 index 67f2105..0000000 --- a/noline/src/utf8.rs +++ /dev/null @@ -1,289 +0,0 @@ -enum Utf8ByteType { - SingleByte, - StartTwoByte, - StartThreeByte, - StartFourByte, - Continuation, - Invalid, -} - -trait Utf8Byte { - fn utf8_byte_type(&self) -> Utf8ByteType; - fn utf8_is_continuation(&self) -> bool; -} - -impl Utf8Byte for u8 { - fn utf8_byte_type(&self) -> Utf8ByteType { - let byte = *self; - - if byte & 0b10000000 == 0 { - Utf8ByteType::SingleByte - } else if byte & 0b11000000 == 0b10000000 { - Utf8ByteType::Continuation - } else if byte & 0b11100000 == 0b11000000 { - Utf8ByteType::StartTwoByte - } else if byte & 0b11110000 == 0b11100000 { - Utf8ByteType::StartThreeByte - } else if byte & 0b11111000 == 0b11110000 { - Utf8ByteType::StartFourByte - } else { - Utf8ByteType::Invalid - } - } - - fn utf8_is_continuation(&self) -> bool { - matches!(self.utf8_byte_type(), Utf8ByteType::Continuation) - } -} - -#[derive(Debug, Eq, PartialEq)] -enum Utf8DecoderState { - New, - ExpectingOneByte, - ExpectingTwoBytes, - ExpectingThreeBytes, - Done, -} - -#[derive(Eq, PartialEq, Copy, Clone)] -pub struct Utf8Char { - buf: [u8; 4], - len: u8, -} - -#[cfg(test)] -impl std::fmt::Debug for Utf8Char { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.debug_tuple("Utf8Char").field(&self.as_char()).finish() - } -} - -impl Utf8Char { - fn new(bytes: &[u8; 4], len: usize) -> Self { - Self { - len: len as u8, - buf: *bytes, - } - } - - #[cfg(test)] - pub(crate) fn from_str(s: &str) -> Self { - let bytes = s.as_bytes(); - assert!(bytes.len() <= 4); - - let mut c = Self { - len: bytes.len() as u8, - buf: [0; 4], - }; - - for (i, b) in bytes.iter().enumerate() { - c.buf[i] = *b; - } - - c - } - - #[cfg(test)] - pub(crate) fn as_char(&self) -> char { - char::from_u32( - self.as_bytes() - .iter() - .fold(0, |codepoint, &b| match b.utf8_byte_type() { - Utf8ByteType::SingleByte => b as u32, - Utf8ByteType::StartTwoByte => (b & 0x1f) as u32, - Utf8ByteType::StartThreeByte => (b & 0xf) as u32, - Utf8ByteType::StartFourByte => (b & 0x7) as u32, - Utf8ByteType::Continuation => (codepoint << 6) | (b & 0x3f) as u32, - Utf8ByteType::Invalid => unreachable!(), - }), - ) - .unwrap() - } - - pub fn as_bytes(&self) -> &[u8] { - &self.buf[0..(self.len as usize)] - } -} - -#[cfg_attr(test, derive(Debug))] -#[derive(Eq, PartialEq)] -pub enum Utf8DecoderStatus { - Continuation, - Done(Utf8Char), - Error, -} - -#[derive(Debug, Eq, PartialEq)] -pub struct Utf8Decoder { - state: Utf8DecoderState, - buf: [u8; 4], - pos: usize, -} - -impl Utf8Decoder { - pub fn new() -> Self { - Self { - state: Utf8DecoderState::New, - buf: [0, 0, 0, 0], - pos: 0, - } - } - - fn insert_byte(&mut self, byte: u8) -> Result<(), ()> { - if self.pos > 0 && !byte.utf8_is_continuation() { - return Err(()); - } - - self.buf[self.pos] = byte; - self.pos += 1; - - Ok(()) - } - - pub fn advance(&mut self, byte: u8) -> Utf8DecoderStatus { - match self.state { - Utf8DecoderState::New => { - self.insert_byte(byte).unwrap(); - - match self.buf[0].utf8_byte_type() { - Utf8ByteType::SingleByte => { - self.state = Utf8DecoderState::Done; - Utf8DecoderStatus::Done(Utf8Char::new(&self.buf, 1)) - } - Utf8ByteType::StartTwoByte => { - self.state = Utf8DecoderState::ExpectingOneByte; - Utf8DecoderStatus::Continuation - } - Utf8ByteType::StartThreeByte => { - self.state = Utf8DecoderState::ExpectingTwoBytes; - Utf8DecoderStatus::Continuation - } - Utf8ByteType::StartFourByte => { - self.state = Utf8DecoderState::ExpectingThreeBytes; - Utf8DecoderStatus::Continuation - } - Utf8ByteType::Continuation | Utf8ByteType::Invalid => { - self.state = Utf8DecoderState::Done; - Utf8DecoderStatus::Error - } - } - } - Utf8DecoderState::ExpectingOneByte => { - if self.insert_byte(byte).is_ok() { - self.state = Utf8DecoderState::Done; - Utf8DecoderStatus::Done(Utf8Char::new(&self.buf, self.pos)) - } else { - Utf8DecoderStatus::Error - } - } - Utf8DecoderState::ExpectingTwoBytes => { - if self.insert_byte(byte).is_ok() { - self.state = Utf8DecoderState::ExpectingOneByte; - Utf8DecoderStatus::Continuation - } else { - Utf8DecoderStatus::Error - } - } - Utf8DecoderState::ExpectingThreeBytes => { - if self.insert_byte(byte).is_ok() { - self.state = Utf8DecoderState::ExpectingTwoBytes; - Utf8DecoderStatus::Continuation - } else { - Utf8DecoderStatus::Error - } - } - Utf8DecoderState::Done => Utf8DecoderStatus::Error, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn ascii() { - let mut parser = Utf8Decoder::new(); - - assert_eq!( - parser.advance(b'a'), - Utf8DecoderStatus::Done(Utf8Char::from_str("a")) - ); - - assert_eq!(parser.advance(b'a'), Utf8DecoderStatus::Error); - } - - #[test] - fn twobyte() { - let mut parser = Utf8Decoder::new(); - - let bytes = "æ".as_bytes(); - - assert_eq!(parser.advance(bytes[0]), Utf8DecoderStatus::Continuation); - - assert_eq!( - parser.advance(bytes[1]), - Utf8DecoderStatus::Done(Utf8Char::from_str("æ")) - ); - - assert_eq!(parser.advance(b'a'), Utf8DecoderStatus::Error); - } - - #[test] - fn threebyte() { - let mut parser = Utf8Decoder::new(); - - let bytes = "€".as_bytes(); - - assert_eq!(parser.advance(bytes[0]), Utf8DecoderStatus::Continuation); - assert_eq!(parser.advance(bytes[1]), Utf8DecoderStatus::Continuation); - - assert_eq!( - parser.advance(bytes[2]), - Utf8DecoderStatus::Done(Utf8Char::from_str("€")) - ); - - assert_eq!(parser.advance(b'a'), Utf8DecoderStatus::Error); - } - - #[test] - fn fourbyte() { - let mut parser = Utf8Decoder::new(); - - let symbol = "😂"; - - let bytes = symbol.as_bytes(); - dbg!(bytes); - - assert_eq!(parser.advance(bytes[0]), Utf8DecoderStatus::Continuation); - assert_eq!(parser.advance(bytes[1]), Utf8DecoderStatus::Continuation); - assert_eq!(parser.advance(bytes[2]), Utf8DecoderStatus::Continuation); - - assert_eq!( - parser.advance(bytes[3]), - Utf8DecoderStatus::Done(Utf8Char::from_str(symbol)) - ); - - assert_eq!(parser.advance(b'a'), Utf8DecoderStatus::Error); - } - - #[test] - fn invalid_start() { - let mut parser = Utf8Decoder::new(); - - assert_eq!(parser.advance(0b10000000), Utf8DecoderStatus::Error); - } - - #[test] - fn invalid_continuation() { - let mut parser = Utf8Decoder::new(); - - assert_eq!(parser.advance(0b11000000), Utf8DecoderStatus::Continuation); - assert_eq!(parser.advance(0b00000000), Utf8DecoderStatus::Error); - } - - #[test] - fn to_char() { - assert_eq!(Utf8Char::from_str("€").as_char(), '€'); - } -} diff --git a/noline/src/utf8/byte.rs b/noline/src/utf8/byte.rs new file mode 100644 index 0000000..09a55ee --- /dev/null +++ b/noline/src/utf8/byte.rs @@ -0,0 +1,37 @@ +pub(super) enum Utf8ByteType { + SingleByte, + StartTwoByte, + StartThreeByte, + StartFourByte, + Continuation, + Invalid, +} + +pub(super) trait Utf8Byte { + fn utf8_byte_type(&self) -> Utf8ByteType; + fn utf8_is_continuation(&self) -> bool; +} + +impl Utf8Byte for u8 { + fn utf8_byte_type(&self) -> Utf8ByteType { + let byte = *self; + + if byte & 0b10000000 == 0 { + Utf8ByteType::SingleByte + } else if byte & 0b11000000 == 0b10000000 { + Utf8ByteType::Continuation + } else if byte & 0b11100000 == 0b11000000 { + Utf8ByteType::StartTwoByte + } else if byte & 0b11110000 == 0b11100000 { + Utf8ByteType::StartThreeByte + } else if byte & 0b11111000 == 0b11110000 { + Utf8ByteType::StartFourByte + } else { + Utf8ByteType::Invalid + } + } + + fn utf8_is_continuation(&self) -> bool { + matches!(self.utf8_byte_type(), Utf8ByteType::Continuation) + } +} diff --git a/noline/src/utf8/char.rs b/noline/src/utf8/char.rs new file mode 100644 index 0000000..a2dd950 --- /dev/null +++ b/noline/src/utf8/char.rs @@ -0,0 +1,62 @@ +#[derive(Eq, PartialEq, Copy, Clone)] +pub struct Utf8Char { + buf: [u8; 4], + len: u8, +} + +#[cfg(test)] +impl std::fmt::Debug for Utf8Char { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_tuple("Utf8Char").field(&self.as_char()).finish() + } +} + +impl Utf8Char { + pub(super) fn new(bytes: &[u8; 4], len: usize) -> Self { + Self { + len: len as u8, + buf: *bytes, + } + } + + #[cfg(test)] + pub(crate) fn from_str(s: &str) -> Self { + let bytes = s.as_bytes(); + assert!(bytes.len() <= 4); + + let mut c = Self { + len: bytes.len() as u8, + buf: [0; 4], + }; + + for (i, b) in bytes.iter().enumerate() { + c.buf[i] = *b; + } + + c + } + + #[cfg(test)] + pub(crate) fn as_char(&self) -> char { + use super::byte::Utf8ByteType; + use crate::utf8::byte::Utf8Byte; + + char::from_u32( + self.as_bytes() + .iter() + .fold(0, |codepoint, &b| match b.utf8_byte_type() { + Utf8ByteType::SingleByte => b as u32, + Utf8ByteType::StartTwoByte => (b & 0x1f) as u32, + Utf8ByteType::StartThreeByte => (b & 0xf) as u32, + Utf8ByteType::StartFourByte => (b & 0x7) as u32, + Utf8ByteType::Continuation => (codepoint << 6) | (b & 0x3f) as u32, + Utf8ByteType::Invalid => unreachable!(), + }), + ) + .unwrap() + } + + pub fn as_bytes(&self) -> &[u8] { + &self.buf[0..(self.len as usize)] + } +} diff --git a/noline/src/utf8/decoder.rs b/noline/src/utf8/decoder.rs new file mode 100644 index 0000000..b45cf8f --- /dev/null +++ b/noline/src/utf8/decoder.rs @@ -0,0 +1,103 @@ +use super::byte::{Utf8Byte, Utf8ByteType}; +use super::Utf8Char; + +#[derive(Debug, Eq, PartialEq)] +enum Utf8DecoderState { + New, + ExpectingOneByte, + ExpectingTwoBytes, + ExpectingThreeBytes, + Done, +} + +#[cfg_attr(test, derive(Debug))] +#[derive(Eq, PartialEq)] +pub enum Utf8DecoderStatus { + Continuation, + Done(Utf8Char), + Error, +} + +#[derive(Debug, Eq, PartialEq)] +pub struct Utf8Decoder { + state: Utf8DecoderState, + buf: [u8; 4], + pos: usize, +} + +impl Utf8Decoder { + pub fn new() -> Self { + Self { + state: Utf8DecoderState::New, + buf: [0, 0, 0, 0], + pos: 0, + } + } + + fn insert_byte(&mut self, byte: u8) -> Result<(), ()> { + if self.pos > 0 && !byte.utf8_is_continuation() { + return Err(()); + } + + self.buf[self.pos] = byte; + self.pos += 1; + + Ok(()) + } + + pub fn advance(&mut self, byte: u8) -> Utf8DecoderStatus { + match self.state { + Utf8DecoderState::New => { + self.insert_byte(byte).unwrap(); + + match self.buf[0].utf8_byte_type() { + Utf8ByteType::SingleByte => { + self.state = Utf8DecoderState::Done; + Utf8DecoderStatus::Done(Utf8Char::new(&self.buf, 1)) + } + Utf8ByteType::StartTwoByte => { + self.state = Utf8DecoderState::ExpectingOneByte; + Utf8DecoderStatus::Continuation + } + Utf8ByteType::StartThreeByte => { + self.state = Utf8DecoderState::ExpectingTwoBytes; + Utf8DecoderStatus::Continuation + } + Utf8ByteType::StartFourByte => { + self.state = Utf8DecoderState::ExpectingThreeBytes; + Utf8DecoderStatus::Continuation + } + Utf8ByteType::Continuation | Utf8ByteType::Invalid => { + self.state = Utf8DecoderState::Done; + Utf8DecoderStatus::Error + } + } + } + Utf8DecoderState::ExpectingOneByte => { + if self.insert_byte(byte).is_ok() { + self.state = Utf8DecoderState::Done; + Utf8DecoderStatus::Done(Utf8Char::new(&self.buf, self.pos)) + } else { + Utf8DecoderStatus::Error + } + } + Utf8DecoderState::ExpectingTwoBytes => { + if self.insert_byte(byte).is_ok() { + self.state = Utf8DecoderState::ExpectingOneByte; + Utf8DecoderStatus::Continuation + } else { + Utf8DecoderStatus::Error + } + } + Utf8DecoderState::ExpectingThreeBytes => { + if self.insert_byte(byte).is_ok() { + self.state = Utf8DecoderState::ExpectingTwoBytes; + Utf8DecoderStatus::Continuation + } else { + Utf8DecoderStatus::Error + } + } + Utf8DecoderState::Done => Utf8DecoderStatus::Error, + } + } +} diff --git a/noline/src/utf8/mod.rs b/noline/src/utf8/mod.rs new file mode 100644 index 0000000..dc74405 --- /dev/null +++ b/noline/src/utf8/mod.rs @@ -0,0 +1,9 @@ +mod byte; +mod char; +mod decoder; + +pub use char::*; +pub use decoder::*; + +#[cfg(test)] +pub(crate) mod tests; diff --git a/noline/src/utf8/tests.rs b/noline/src/utf8/tests.rs new file mode 100644 index 0000000..fc21274 --- /dev/null +++ b/noline/src/utf8/tests.rs @@ -0,0 +1,87 @@ +use super::*; + +#[test] +fn ascii() { + let mut parser = Utf8Decoder::new(); + + assert_eq!( + parser.advance(b'a'), + Utf8DecoderStatus::Done(Utf8Char::from_str("a")) + ); + + assert_eq!(parser.advance(b'a'), Utf8DecoderStatus::Error); +} + +#[test] +fn twobyte() { + let mut parser = Utf8Decoder::new(); + + let bytes = "æ".as_bytes(); + + assert_eq!(parser.advance(bytes[0]), Utf8DecoderStatus::Continuation); + + assert_eq!( + parser.advance(bytes[1]), + Utf8DecoderStatus::Done(Utf8Char::from_str("æ")) + ); + + assert_eq!(parser.advance(b'a'), Utf8DecoderStatus::Error); +} + +#[test] +fn threebyte() { + let mut parser = Utf8Decoder::new(); + + let bytes = "€".as_bytes(); + + assert_eq!(parser.advance(bytes[0]), Utf8DecoderStatus::Continuation); + assert_eq!(parser.advance(bytes[1]), Utf8DecoderStatus::Continuation); + + assert_eq!( + parser.advance(bytes[2]), + Utf8DecoderStatus::Done(Utf8Char::from_str("€")) + ); + + assert_eq!(parser.advance(b'a'), Utf8DecoderStatus::Error); +} + +#[test] +fn fourbyte() { + let mut parser = Utf8Decoder::new(); + + let symbol = "😂"; + + let bytes = symbol.as_bytes(); + dbg!(bytes); + + assert_eq!(parser.advance(bytes[0]), Utf8DecoderStatus::Continuation); + assert_eq!(parser.advance(bytes[1]), Utf8DecoderStatus::Continuation); + assert_eq!(parser.advance(bytes[2]), Utf8DecoderStatus::Continuation); + + assert_eq!( + parser.advance(bytes[3]), + Utf8DecoderStatus::Done(Utf8Char::from_str(symbol)) + ); + + assert_eq!(parser.advance(b'a'), Utf8DecoderStatus::Error); +} + +#[test] +fn invalid_start() { + let mut parser = Utf8Decoder::new(); + + assert_eq!(parser.advance(0b10000000), Utf8DecoderStatus::Error); +} + +#[test] +fn invalid_continuation() { + let mut parser = Utf8Decoder::new(); + + assert_eq!(parser.advance(0b11000000), Utf8DecoderStatus::Continuation); + assert_eq!(parser.advance(0b00000000), Utf8DecoderStatus::Error); +} + +#[test] +fn to_char() { + assert_eq!(Utf8Char::from_str("€").as_char(), '€'); +}