diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index b9c38bc..579a5e3 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -14,6 +14,25 @@ jobs: runs-on: ubuntu-latest + defaults: + run: + working-directory: ./noline + + steps: + - uses: actions/checkout@v2 + - name: Format + run: cargo fmt --all -- --check + - name: Doc + run: cargo doc --verbose --all-features + - name: Build + run: cargo build --verbose --all-features + - name: Run tests + run: cargo test --verbose --all-features + + readme: + + runs-on: ubuntu-latest + defaults: run: working-directory: ./noline @@ -25,23 +44,26 @@ jobs: crate: cargo-readme version: latest use-tool-cache: true + - name: Readme + run: cargo readme > ../README.md && git diff --exit-code + + cargo-outdated: + + runs-on: ubuntu-latest + + defaults: + run: + working-directory: ./noline + + steps: + - uses: actions/checkout@v2 - uses: actions-rs/install@v0.1 with: crate: cargo-outdated version: latest use-tool-cache: true - - name: Readme - run: cargo readme > ../README.md && git diff --exit-code - name: Outdated dependencies run: cargo outdated --exit-code 1 - - name: Format - run: cargo fmt --all -- --check - - name: Doc - run: cargo doc --verbose --all-features - - name: Build - run: cargo build --verbose --all-features - - name: Run tests - run: cargo test --verbose --all-features examples-std: diff --git a/CHANGELOG.md b/CHANGELOG.md index 739ff00..2820f39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- Added basic line history + +- Added EditorBuilder for more ergonomic construction of editors + ## [0.1.0] - 2022-03-14 - Initial release diff --git a/README.md b/README.md index 07b3507..92b8004 100644 --- a/README.md +++ b/README.md @@ -15,37 +15,44 @@ Features: - No allocation needed - Both heap-based and static buffers are provided - UTF-8 support - Emacs keybindings +- Line history Possible future features: - Auto-completion and hints -- Line history The API should be considered experimental and will change in the future. +The core implementation consists of a state machie taking bytes as +input and yielding iterators over byte slices. Because this is +done without any IO, Noline can be adapted to work on any platform. + +Noline comes with multiple implemenations: +- [`sync::Editor`] – Editor for asynchronous IO with two separate IO wrappers: + - [`sync::std::IO`] – IO wrapper for [`std::io::Read`] and [`std::io::Write`] traits + - [`sync::embedded::IO`] – IO wrapper for [`embedded_hal::serial::Read`] and [`embedded_hal::serial::Write`] +- [`no_sync::tokio::Editor`] - Editor for [`tokio::io::AsyncRead`] and [`tokio::io::AsyncWrite`] -The core consists of a massive state machine taking bytes as input -and returning an iterator over byte slices. There are, however, -some convenience wrappers: -- [`sync::Editor`] - - [`sync::std::IO`] - - [`sync::embedded::IO`] -- [`no_sync::tokio::Editor`] +Editors can be built using [`builder::EditorBuilder`]. ## Example ```rust -use noline::sync::{std::IO, Editor}; -use std::io; +use noline::{sync::std::IO, builder::EditorBuilder}; use std::fmt::Write; +use std::io; use termion::raw::IntoRawMode; fn main() { - let mut stdin = io::stdin(); - let mut stdout = io::stdout().into_raw_mode().unwrap(); + let stdin = io::stdin(); + let stdout = io::stdout().into_raw_mode().unwrap(); let prompt = "> "; let mut io = IO::new(stdin, stdout); - let mut editor = Editor::, _>::new(&mut io).unwrap(); + + let mut editor = EditorBuilder::new_unbounded() + .with_unbounded_history() + .build_sync(&mut io) + .unwrap(); loop { if let Ok(line) = editor.readline(prompt, &mut io) { diff --git a/examples/no_std/stm32f103/src/bin/uart.rs b/examples/no_std/stm32f103/src/bin/uart.rs index f0b68be..209425c 100644 --- a/examples/no_std/stm32f103/src/bin/uart.rs +++ b/examples/no_std/stm32f103/src/bin/uart.rs @@ -7,11 +7,7 @@ #![no_std] use heapless::spsc::{Consumer, Producer, Queue}; -use noline::{ - error::Error, - line_buffer::StaticBuffer, - sync::{embedded::IO, Editor}, -}; +use noline::{builder::EditorBuilder, error::Error, sync::embedded::IO}; use panic_halt as _; use cortex_m::asm; @@ -130,8 +126,11 @@ fn main() -> ! { let mut io = IO::new(SerialWrapper::new(tx, rx_consumer)); let prompt = "> "; - let mut editor: Editor, _> = loop { - match Editor::new(&mut io) { + let mut editor = loop { + match EditorBuilder::new_static::<128>() + .with_static_history::<128>() + .build_sync(&mut io) + { Ok(editor) => break editor, Err(err) => { let error = match err { diff --git a/examples/no_std/stm32f103/src/bin/usb.rs b/examples/no_std/stm32f103/src/bin/usb.rs index 95920af..7d38fe0 100644 --- a/examples/no_std/stm32f103/src/bin/usb.rs +++ b/examples/no_std/stm32f103/src/bin/usb.rs @@ -13,10 +13,9 @@ use core::fmt::Write as FmtWrite; use embedded_hal::serial::{Read, Write}; use nb::block; +use noline::builder::EditorBuilder; use noline::error::Error; -use noline::line_buffer::StaticBuffer; use noline::sync::embedded::IO; -use noline::sync::Editor; use panic_halt as _; use cortex_m::asm::delay; @@ -158,10 +157,10 @@ fn main() -> ! { let prompt = "> "; - let mut wrapper = IO::new(SerialWrapper::new(&mut usb_dev, &mut serial)); + let mut io = IO::new(SerialWrapper::new(&mut usb_dev, &mut serial)); - let mut editor: Editor, _> = loop { - if !wrapper.poll() || !wrapper.serial.dtr() || !wrapper.serial.rts() { + let mut editor = loop { + if !io.inner().poll() || !io.inner().serial.dtr() || !io.inner().serial.rts() { continue; } @@ -171,18 +170,21 @@ fn main() -> ! { // usbd-serial. Becase noline needs to write during // initialization, I've added this blocking read here to wait // for user input before proceeding. - block!(wrapper.read()).unwrap(); - break Editor::new(&mut wrapper).unwrap(); + block!(io.inner().read()).unwrap(); + break EditorBuilder::new_static::<128>() + .with_static_history::<128>() + .build_sync(&mut io) + .unwrap(); }; loop { - match editor.readline(prompt, &mut wrapper) { + match editor.readline(prompt, &mut io) { Ok(s) => { if s.len() > 0 { - writeln!(wrapper, "Echo: {}\r", s).unwrap(); + writeln!(io, "Echo: {}\r", s).unwrap(); } else { // Writing emtpy slice causes panic - writeln!(wrapper, "Echo: \r").unwrap(); + writeln!(io, "Echo: \r").unwrap(); } } Err(err) => { @@ -201,7 +203,7 @@ fn main() -> ! { Error::Aborted => "Aborted", }; - writeln!(wrapper, "Error: {}\r", error).unwrap(); + writeln!(io, "Error: {}\r", error).unwrap(); } } } diff --git a/examples/std/src/bin/std-async-tokio.rs b/examples/std/src/bin/std-async-tokio.rs index a2c2a4c..82aed32 100644 --- a/examples/std/src/bin/std-async-tokio.rs +++ b/examples/std/src/bin/std-async-tokio.rs @@ -1,4 +1,4 @@ -use noline::no_sync::tokio::Editor; +use noline::builder::EditorBuilder; use termion::raw::IntoRawMode; use tokio::io::{self, AsyncWriteExt}; @@ -10,7 +10,9 @@ async fn main() { let prompt = "> "; - let mut editor = Editor::>::new(&mut stdin, &mut stdout) + let mut editor = EditorBuilder::new_unbounded() + .with_unbounded_history() + .build_async_tokio(&mut stdin, &mut stdout) .await .unwrap(); diff --git a/examples/std/src/bin/std-sync.rs b/examples/std/src/bin/std-sync.rs index b8572e2..f81f710 100644 --- a/examples/std/src/bin/std-sync.rs +++ b/examples/std/src/bin/std-sync.rs @@ -1,4 +1,4 @@ -use noline::sync::{std::IO, Editor}; +use noline::{builder::EditorBuilder, sync::std::IO}; use std::fmt::Write; use std::io; use termion::raw::IntoRawMode; @@ -10,7 +10,10 @@ fn main() { let mut io = IO::new(stdin, stdout); - let mut editor = Editor::, _>::new(&mut io).unwrap(); + let mut editor = EditorBuilder::new_unbounded() + .with_unbounded_history() + .build_sync(&mut io) + .unwrap(); loop { if let Ok(line) = editor.readline(prompt, &mut io) { diff --git a/noline/src/builder.rs b/noline/src/builder.rs new file mode 100644 index 0000000..a6c05e6 --- /dev/null +++ b/noline/src/builder.rs @@ -0,0 +1,118 @@ +//! Builder for editors + +use core::marker::PhantomData; + +use crate::{ + error::Error, + history::{History, NoHistory, StaticHistory}, + line_buffer::{Buffer, NoBuffer, StaticBuffer}, + sync::{self, Read, Write}, +}; + +#[cfg(any(test, feature = "alloc", feature = "std"))] +use crate::line_buffer::UnboundedBuffer; + +#[cfg(any(test, feature = "alloc", feature = "std"))] +use crate::history::UnboundedHistory; + +#[cfg(any(test, doc, feature = "tokio"))] +use ::tokio::io::{AsyncReadExt, AsyncWriteExt}; + +#[cfg(any(test, doc, feature = "tokio"))] +use crate::no_sync; + +/// Builder for [`sync::Editor`] and [`no_sync::tokio::Editor`]. +/// +/// # Example +/// ```no_run +/// # use noline::sync::{Read, Write}; +/// # struct IO {} +/// # impl Write for IO { +/// # type Error = (); +/// # fn write(&mut self, buf: &[u8]) -> Result<(), Self::Error> { unimplemented!() } +/// # fn flush(&mut self) -> Result<(), Self::Error> { unimplemented!() } +/// # } +/// # impl Read for IO { +/// # type Error = (); +/// # fn read(&mut self) -> Result { unimplemented!() } +/// # } +/// # let mut io = IO {}; +/// use noline::builder::EditorBuilder; +/// +/// let mut editor = EditorBuilder::new_static::<100>() +/// .with_static_history::<200>() +/// .build_sync(&mut io) +/// .unwrap(); +/// ``` +pub struct EditorBuilder { + _marker: PhantomData<(B, H)>, +} + +impl EditorBuilder { + /// Create builder for editor with static buffer + /// + /// # Example + /// ``` + /// use noline::builder::EditorBuilder; + /// + /// let builder = EditorBuilder::new_static::<100>(); + /// ``` + pub fn new_static() -> EditorBuilder, NoHistory> { + EditorBuilder { + _marker: PhantomData, + } + } + + #[cfg(any(test, feature = "alloc", feature = "std"))] + /// Create builder for editor with unbounded buffer + /// + /// # Example + /// ``` + /// use noline::builder::EditorBuilder; + /// + /// let builder = EditorBuilder::new_unbounded(); + /// ``` + pub fn new_unbounded() -> EditorBuilder { + EditorBuilder { + _marker: PhantomData, + } + } +} + +impl EditorBuilder { + /// Add static history + pub fn with_static_history(self) -> EditorBuilder> { + EditorBuilder { + _marker: PhantomData, + } + } + + #[cfg(any(test, feature = "alloc", feature = "std"))] + /// Add unbounded history + pub fn with_unbounded_history(self) -> EditorBuilder { + EditorBuilder { + _marker: PhantomData, + } + } + + /// Build [`sync::Editor`]. Is equivalent of calling [`sync::Editor::new()`]. + pub fn build_sync( + self, + io: &mut IO, + ) -> Result, Error> + where + IO: Read + Write, + { + sync::Editor::new(io) + } + + #[cfg(any(test, doc, feature = "tokio"))] + /// Build [`no_sync::tokio::Editor`]. Is equivalent of calling [`no_sync::tokio::Editor::new()`]. + pub async fn build_async_tokio( + self, + stdin: &mut R, + stdout: &mut W, + ) -> Result, Error> { + no_sync::tokio::Editor::new(stdin, stdout).await + } +} diff --git a/noline/src/core.rs b/noline/src/core.rs index b93f358..1688aa3 100644 --- a/noline/src/core.rs +++ b/noline/src/core.rs @@ -3,6 +3,7 @@ //! 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; @@ -87,20 +88,27 @@ impl Initializer { // 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> { +pub struct Line<'a, B: Buffer, H: History> { buffer: &'a mut LineBuffer, terminal: &'a mut Terminal, parser: Parser, prompt: &'a str, + nav: HistoryNavigator<'a, H>, } -impl<'a, B: Buffer> Line<'a, B> { - pub fn new(prompt: &'a str, buffer: &'a mut LineBuffer, terminal: &'a mut Terminal) -> Self { +impl<'a, B: Buffer, H: History> Line<'a, B, H> { + pub fn new( + prompt: &'a str, + buffer: &'a mut LineBuffer, + terminal: &'a mut Terminal, + history: &'a mut H, + ) -> Self { Self { buffer, terminal, parser: Parser::new(), prompt, + nav: HistoryNavigator::new(history), } } @@ -119,6 +127,54 @@ impl<'a, B: Buffer> Line<'a, B> { pos - self.prompt.len() } + fn history_move_up<'b>(&'b mut self) -> Output<'b, B> { + 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<'b>(&'b mut self) -> Output<'b, B> { + 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<'b>(&'b mut self, byte: u8) -> Output<'b, B> { @@ -171,6 +227,8 @@ impl<'a, B: Buffer> Line<'a, B> { 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(); @@ -190,7 +248,13 @@ impl<'a, B: Buffer> Line<'a, B> { let move_cursor = -(self.buffer.delete_previous_word(pos) as isize); self.generate_output(MoveCursorAndEraseAndPrintBuffer(move_cursor)) } - CarriageReturn => self.generate_output(Done), + CarriageReturn => { + 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 { @@ -225,8 +289,8 @@ impl<'a, B: Buffer> Line<'a, B> { self.generate_output(Nothing) } CSI::Unknown(_) => self.generate_output(RingBell), - CSI::CUU(_) => self.generate_output(RingBell), - CSI::CUD(_) => 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), @@ -246,18 +310,20 @@ pub(crate) mod tests { use std::string::String; + use crate::history::{NoHistory, StaticHistory, UnboundedHistory}; use crate::line_buffer::StaticBuffer; use crate::terminal::Cursor; use crate::testlib::{csi, AsByteVec, MockTerminal}; use super::*; - struct Editor { + struct Editor { buffer: LineBuffer, terminal: Terminal, + history: H, } - impl Editor { + impl Editor { fn new(term: &mut MockTerminal) -> Self { let mut initializer = Initializer::new(); @@ -278,12 +344,22 @@ pub(crate) mod tests { Self { buffer: LineBuffer::new(), terminal, + history: H::default(), } } - fn get_line<'b>(&'b mut self, prompt: &'b str, mockterm: &mut MockTerminal) -> Line<'b, B> { + fn get_line<'b>( + &'b mut self, + prompt: &'b str, + mockterm: &mut MockTerminal, + ) -> Line<'b, B, H> { let cursor = mockterm.get_cursor(); - let mut line = Line::new(prompt, &mut self.buffer, &mut self.terminal); + let mut line = Line::new( + prompt, + &mut self.buffer, + &mut self.terminal, + &mut self.history, + ); let output: Vec = line .reset() @@ -303,16 +379,16 @@ pub(crate) mod tests { .for_each(|output| assert!(output.get_bytes().is_none())) }); - assert_eq!(mockterm.screen_as_string(), prompt); + 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>( + fn advance<'a, B: Buffer, H: History>( terminal: &mut MockTerminal, - noline: &mut Line<'a, B>, + noline: &mut Line<'a, B, H>, input: impl AsByteVec, ) -> core::result::Result<(), ()> { terminal.bell = false; @@ -342,7 +418,7 @@ pub(crate) mod tests { rows: usize, columns: usize, origin: Cursor, - ) -> (MockTerminal, Editor>) { + ) -> (MockTerminal, Editor, NoHistory>) { let mut terminal = MockTerminal::new(rows, columns, origin); let editor = Editor::new(&mut terminal); @@ -628,7 +704,7 @@ pub(crate) mod tests { #[test] fn static_buffer() { let mut terminal = MockTerminal::new(20, 80, Cursor::new(0, 0)); - let mut editor: Editor> = Editor::new(&mut terminal); + let mut editor: Editor, NoHistory> = Editor::new(&mut terminal); let mut line = editor.get_line("> ", &mut terminal); @@ -642,4 +718,101 @@ pub(crate) mod tests { advance(&mut terminal, &mut line, Backspace).unwrap(); } + + #[test] + fn history() { + fn test() { + let mut terminal = MockTerminal::new(20, 80, Cursor::new(0, 0)); + let mut editor: Editor, H> = Editor::new(&mut terminal); + + 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::(); + test::>(); + } } diff --git a/noline/src/history.rs b/noline/src/history.rs new file mode 100644 index 0000000..84c901a --- /dev/null +++ b/noline/src/history.rs @@ -0,0 +1,582 @@ +//! 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, +} + +impl CircularIndex { + fn new(index: usize) -> Self { + Self { index } + } + + 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 % N + } + + fn diff(&self, other: CircularIndex) -> isize { + self.index as isize - other.index as isize + } +} + +struct Window { + start: CircularIndex, + end: CircularIndex, +} + +impl Window { + fn new(start: CircularIndex, end: CircularIndex) -> Self { + assert!(start <= end); + Self { 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 > N { + 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.into_iter()) + .chain(range2.zip(slice2.into_iter())) + } +} + +/// Trait for line history +pub trait History: Default { + /// Return entry at index, or None if out of bounds + fn get_entry<'a>(&'a 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<'a, H: History>( + history: &'a H, +) -> impl Iterator> { + (0..(history.number_of_entries())).filter_map(|index| history.get_entry(index)) +} + +/// Static history backed by array +pub struct StaticHistory { + buffer: [u8; N], + window: Window, +} + +impl StaticHistory { + /// Create new static history + pub fn new() -> Self { + Self { + buffer: [0; N], + window: Window::new(CircularIndex::new(0), CircularIndex::new(0)), + } + } + + fn get_available_range(&self) -> CircularRange { + CircularRange::new(self.window.end(), self.window.end(), N, N) + } + + fn get_buffer<'a>(&'a self) -> CircularSlice<'a> { + CircularSlice::new( + &self.buffer, + self.window.start(), + self.window.end(), + self.window.len(), + ) + } + + fn get_entry_ranges<'a>(&'a self) -> impl Iterator + 'a { + 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()].into_iter())) + .filter_map(|(start, end)| { + if start != end { + Some(CircularRange::new(start, end, self.window.len(), N)) + } else { + None + } + }) + } + + fn get_entries<'a>(&'a self) -> impl Iterator> { + self.get_entry_ranges() + .map(|range| CircularSlice::from_range(&self.buffer, range)) + } +} + +impl Default for StaticHistory { + fn default() -> Self { + Self::new() + } +} + +impl History for StaticHistory { + fn add_entry<'a>(&mut self, entry: &'a str) -> Result<(), &'a str> { + if entry.len() + 1 > N { + 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<'a>(&'a 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<'a>(&'a 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<'b>(&'b 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<'b>(&'b mut self) -> Result, ()> { + let position = self.get_position(); + + if position < self.history.number_of_entries() - 1 { + let position = self.set_position(position + 1); + + 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, 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<'a>(&'a 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, 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 history: StaticHistory<10> = StaticHistory::new(); + + 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"] + ); + } +} diff --git a/noline/src/lib.rs b/noline/src/lib.rs index 5c67e83..f992412 100644 --- a/noline/src/lib.rs +++ b/noline/src/lib.rs @@ -9,37 +9,44 @@ //! - No allocation needed - Both heap-based and static buffers are provided //! - UTF-8 support //! - Emacs keybindings +//! - Line history //! //! Possible future features: //! - Auto-completion and hints -//! - Line history //! //! The API should be considered experimental and will change in the //! future. //! +//! The core implementation consists of a state machie taking bytes as +//! input and yielding iterators over byte slices. Because this is +//! done without any IO, Noline can be adapted to work on any platform. //! -//! The core consists of a massive state machine taking bytes as input -//! and returning an iterator over byte slices. There are, however, -//! some convenience wrappers: -//! - [`sync::Editor`] -//! - [`sync::std::IO`] -//! - [`sync::embedded::IO`] -//! - [`no_sync::tokio::Editor`] +//! Noline comes with multiple implemenations: +//! - [`sync::Editor`] – Editor for asynchronous IO with two separate IO wrappers: +//! - [`sync::std::IO`] – IO wrapper for [`std::io::Read`] and [`std::io::Write`] traits +//! - [`sync::embedded::IO`] – IO wrapper for [`embedded_hal::serial::Read`] and [`embedded_hal::serial::Write`] +//! - [`no_sync::tokio::Editor`] - Editor for [`tokio::io::AsyncRead`] and [`tokio::io::AsyncWrite`] +//! +//! Editors can be built using [`builder::EditorBuilder`]. //! //! # Example //! ```no_run -//! use noline::sync::{std::IO, Editor}; -//! use std::io; +//! use noline::{sync::std::IO, builder::EditorBuilder}; //! use std::fmt::Write; +//! use std::io; //! use termion::raw::IntoRawMode; //! //! fn main() { -//! let mut stdin = io::stdin(); -//! let mut stdout = io::stdout().into_raw_mode().unwrap(); +//! let stdin = io::stdin(); +//! let stdout = io::stdout().into_raw_mode().unwrap(); //! let prompt = "> "; //! //! let mut io = IO::new(stdin, stdout); -//! let mut editor = Editor::, _>::new(&mut io).unwrap(); +//! +//! let mut editor = EditorBuilder::new_unbounded() +//! .with_unbounded_history() +//! .build_sync(&mut io) +//! .unwrap(); //! //! loop { //! if let Ok(line) = editor.readline(prompt, &mut io) { @@ -56,9 +63,10 @@ #[cfg(any(test, doc, feature = "std"))] #[macro_use] extern crate std; - +pub mod builder; mod core; pub mod error; +pub mod history; mod input; pub mod line_buffer; pub mod no_sync; diff --git a/noline/src/line_buffer.rs b/noline/src/line_buffer.rs index 209ae32..e387ae6 100644 --- a/noline/src/line_buffer.rs +++ b/noline/src/line_buffer.rs @@ -149,35 +149,73 @@ impl LineBuffer { } } - /// Insert UTF-8 char at position - pub fn insert_utf8_char(&mut self, char_index: usize, c: Utf8Char) -> Result<(), Utf8Char> { - let pos = self.get_byte_position(char_index); - + /// 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 c.as_bytes().len() > capacity - self.buf.buffer_len() { - return Err(c); + if bytes.len() > capacity - self.buf.buffer_len() { + return Err(()); } } - for (i, byte) in c.as_bytes().iter().enumerate() { - self.buf.insert_byte(pos + i, *byte); + for (i, byte) in bytes.iter().enumerate() { + self.buf.insert_byte(index + i, *byte); } Ok(()) } - #[cfg(test)] - pub fn insert_str(&mut self, char_index: usize, s: &str) { - use ::std::string::ToString; - - for (pos, c) in s - .chars() - .map(|c| Utf8Char::from_str(&c.to_string())) - .enumerate() - { - assert!(self.insert_utf8_char(char_index + pos, c).is_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()) + .or_else(|_| 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 Default for NoBuffer { + fn default() -> Self { + unimplemented!() + } +} + +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 array @@ -247,7 +285,9 @@ mod alloc { use self::alloc::vec::Vec; use super::*; - impl Buffer for Vec { + pub type UnboundedBuffer = Vec; + + impl Buffer for UnboundedBuffer { fn buffer_len(&self) -> usize { self.len() } @@ -295,7 +335,7 @@ mod tests { } fn insert_str(buf: &mut LineBuffer, index: usize, s: &str) { - buf.insert_str(index, s); + buf.insert_str(index, s).unwrap(); } fn test_line_buffer(buf: &mut LineBuffer) { diff --git a/noline/src/no_sync.rs b/noline/src/no_sync.rs index b8c4485..6f47e35 100644 --- a/noline/src/no_sync.rs +++ b/noline/src/no_sync.rs @@ -7,6 +7,7 @@ pub mod tokio { use crate::{ core::{Initializer, InitializerResult, Line}, error::Error, + history::{get_history_entries, CircularSlice, History}, line_buffer::{Buffer, LineBuffer}, output::OutputItem, terminal::Terminal, @@ -44,21 +45,25 @@ pub mod tokio { .or_else(|err| Error::read_error(err))?) } - // Line editor for async IO - pub struct Editor { + /// Line editor for async IO + /// + /// It is recommended to use [`crate::builder::EditorBuilder`] to build an editor. + pub struct Editor { buffer: LineBuffer, terminal: Terminal, + history: H, } - impl Editor + impl Editor where B: Buffer, + H: History, { /// Create and initialize line editor pub async fn new( stdin: &mut R, stdout: &mut W, - ) -> Result, Error> { + ) -> Result, Error> { let mut initializer = Initializer::new(); write(stdout, Initializer::init()).await?; @@ -77,6 +82,7 @@ pub mod tokio { Ok(Self { buffer: LineBuffer::new(), terminal, + history: H::default(), }) } @@ -87,7 +93,12 @@ pub mod tokio { stdin: &mut R, stdout: &mut W, ) -> Result<&'b str, Error> { - let mut line = Line::new(prompt, &mut self.buffer, &mut self.terminal); + let mut line = Line::new( + prompt, + &mut self.buffer, + &mut self.terminal, + &mut self.history, + ); for output in line.reset() { write(stdout, output.get_bytes().unwrap_or_else(|| unreachable!())).await?; @@ -121,5 +132,15 @@ pub mod tokio { Err(Error::Aborted) } } + + /// Load history from iterator + pub fn load_history<'a>(&mut self, entries: impl Iterator) -> usize { + self.history.load_entries(entries) + } + + /// Get history as iterator over circular slices + pub fn get_history<'a>(&'a self) -> impl Iterator> { + get_history_entries(&self.history) + } } } diff --git a/noline/src/output.rs b/noline/src/output.rs index 22df2a6..6ee146c 100644 --- a/noline/src/output.rs +++ b/noline/src/output.rs @@ -37,6 +37,7 @@ pub enum OutputAction { Nothing, MoveCursor(CursorMove), ClearAndPrintPrompt, + ClearAndPrintBuffer, PrintBufferAndMoveCursorForward, EraseAfterCursor, EraseAndPrintBuffer, @@ -267,7 +268,14 @@ impl<'a> Step<'a> { None } Erase => self.transition(Step::Done, OutputItem::Slice("\x1b[J".as_bytes())), - Newline => self.transition(Step::Done, OutputItem::Slice("\n\r".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), @@ -489,6 +497,18 @@ impl<'a, B: Buffer> Iterator for Output<'a, B> { OutputAction::ClearAndPrintPrompt => OutputState::ThreeSteps( [ClearLine, Print(self.prompt), GetPosition].into_iter(), ), + OutputAction::ClearAndPrintBuffer => { + let position = self.new_position(CursorMove::Start); + + OutputState::ThreeSteps( + [ + Move(MoveCursorToPosition::new(position)), + Erase, + Print(self.buffer.as_str()), + ] + .into_iter(), + ) + } OutputAction::Done => { OutputState::TwoSteps([Newline, EndOfString].into_iter()) } @@ -693,7 +713,7 @@ mod tests { assert_eq!(result, "\r\x1b[J> \x1b[6n"); - line_buffer.insert_str(0, "Hello, world!"); + line_buffer.insert_str(0, "Hello, world!").unwrap(); let result = to_string(Output::new( prompt, diff --git a/noline/src/sync.rs b/noline/src/sync.rs index 35c389f..c8920c3 100644 --- a/noline/src/sync.rs +++ b/noline/src/sync.rs @@ -2,10 +2,12 @@ //! //! The editor takes a struct implementing the [`Read`] and [`Write`] //! traits. There are ready made implementations in [`std::IO`] and [`embedded::IO`]. - +//! +//! Use the [`crate::builder::EditorBuilder`] to build an editor. use ::core::marker::PhantomData; use crate::error::Error; +use crate::history::{get_history_entries, CircularSlice, History}; use crate::line_buffer::{Buffer, LineBuffer}; use crate::core::{Initializer, InitializerResult, Line}; @@ -16,7 +18,7 @@ use crate::terminal::Terminal; pub trait Read { type Error; - // Read single byte from input + /// Read single byte from input fn read(&mut self) -> Result; } @@ -27,20 +29,24 @@ pub trait Write { /// Write byte slice to output fn write(&mut self, buf: &[u8]) -> Result<(), Self::Error>; - // Flush output + /// Flush output fn flush(&mut self) -> Result<(), Self::Error>; } /// Line editor for synchronous IO -pub struct Editor { +/// +/// It is recommended to use [`crate::builder::EditorBuilder`] to build an Editor. +pub struct Editor { buffer: LineBuffer, terminal: Terminal, + history: H, _marker: PhantomData, } -impl Editor +impl Editor where B: Buffer, + H: History, IO: Read + Write, { /// Create and initialize line editor @@ -64,6 +70,7 @@ where Ok(Self { buffer: LineBuffer::new(), terminal, + history: H::default(), _marker: PhantomData, }) } @@ -92,7 +99,12 @@ where prompt: &'b str, io: &mut IO, ) -> Result<&'b str, Error> { - let mut line = Line::new(prompt, &mut self.buffer, &mut self.terminal); + let mut line = Line::new( + prompt, + &mut self.buffer, + &mut self.terminal, + &mut self.history, + ); Self::handle_output(line.reset(), io)?; loop { @@ -104,10 +116,22 @@ where Ok(self.buffer.as_str()) } + + /// Load history from iterator + pub fn load_history<'a>(&mut self, entries: impl Iterator) -> usize { + self.history.load_entries(entries) + } + + /// Get history as iterator over circular slices + pub fn get_history<'a>(&'a self) -> impl Iterator> { + get_history_entries(&self.history) + } } #[cfg(test)] mod tests { + use crate::builder::EditorBuilder; + use super::*; use ::std::string::ToString; use ::std::{thread, vec::Vec}; @@ -164,7 +188,7 @@ mod tests { let mut io = IO::new(input_rx, output_tx); let handle = thread::spawn(move || { - let mut editor: Editor, _> = Editor::new(&mut io).unwrap(); + let mut editor = EditorBuilder::new_unbounded().build_sync(&mut io).unwrap(); if let Ok(s) = editor.readline("> ", &mut io) { Some(s.to_string()) @@ -285,7 +309,7 @@ pub mod std { use crossbeam::channel::{unbounded, Receiver, Sender}; - use crate::sync::Editor; + use crate::builder::EditorBuilder; use crate::testlib::{test_cases, test_editor_with_case, MockTerminal}; use ::std::io::Read as IoRead; @@ -394,7 +418,10 @@ pub mod std { |(stdin, stdout), string_tx| { thread::spawn(move || { let mut io = IO::new(stdin, stdout); - let mut editor = Editor::, _>::new(&mut io).unwrap(); + let mut editor = EditorBuilder::new_unbounded() + .with_unbounded_history() + .build_sync(&mut io) + .unwrap(); while let Ok(s) = editor.readline(prompt, &mut io) { string_tx.send(s.to_string()).unwrap(); @@ -409,18 +436,16 @@ pub mod std { #[cfg(any(test, feature = "embedded"))] pub mod embedded { - //! Implementation for embedded systems. Requires feature `embedded`. + //! IO implementation using traits from [`embedded_hal::serial`]. Requires feature `embedded`. - use core::{ - fmt, - ops::{Deref, DerefMut}, - }; + use core::fmt; use embedded_hal::serial; use nb::block; use super::*; + /// IO wrapper for [`embedded_hal::serial::Read`] and [`embedded_hal::serial::Write`] pub struct IO where RW: serial::Read + serial::Write, @@ -436,9 +461,15 @@ pub mod embedded { Self { rw } } + /// Consume self and return wrapped object pub fn take(self) -> RW { self.rw } + + /// Return mutable reference to wrapped object + pub fn inner(&mut self) -> &mut RW { + &mut self.rw + } } impl Read for IO @@ -480,26 +511,6 @@ pub mod embedded { } } - impl Deref for IO - where - RW: serial::Read + serial::Write, - { - type Target = RW; - - fn deref(&self) -> &Self::Target { - &self.rw - } - } - - impl DerefMut for IO - where - RW: serial::Read + serial::Write, - { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.rw - } - } - #[cfg(test)] mod tests { use ::std::string::ToString; @@ -507,8 +518,7 @@ pub mod embedded { use crossbeam::channel::{Receiver, Sender, TryRecvError}; - use crate::line_buffer::StaticBuffer; - use crate::sync::Editor; + use crate::builder::EditorBuilder; use crate::testlib::test_editor_with_case; use crate::testlib::{test_cases, MockTerminal}; @@ -578,7 +588,10 @@ pub mod embedded { |serial, string_tx| { thread::spawn(move || { let mut io = IO::new(serial); - let mut editor = Editor::, _>::new(&mut io).unwrap(); + let mut editor = EditorBuilder::new_static::<100>() + .with_static_history::<128>() + .build_sync(&mut io) + .unwrap(); while let Ok(s) = editor.readline(prompt, &mut io) { string_tx.send(s.to_string()).unwrap(); diff --git a/noline/src/testlib.rs b/noline/src/testlib.rs index 06b509f..d507fd3 100644 --- a/noline/src/testlib.rs +++ b/noline/src/testlib.rs @@ -11,6 +11,8 @@ 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~"; @@ -67,6 +69,13 @@ impl MockTerminal { .join("\n") } + pub fn current_line_as_string(&self) -> String { + self.screen[self.cursor.row] + .iter() + .take_while(|&&c| c != '\0') + .collect() + } + fn move_column(&mut self, steps: isize) { self.cursor.column = 0.max((self.cursor.column as isize + steps).min(self.columns as isize - 1)) as usize; @@ -154,13 +163,11 @@ impl MockTerminal { match ctrl { ControlCharacter::CarriageReturn => self.cursor.column = 0, ControlCharacter::LineFeed => { - dbg!(self.cursor); if self.cursor.row + 1 == self.rows { self.scroll_up(1); } else { self.cursor.row += 1; } - dbg!(self.cursor); } ControlCharacter::CtrlG => self.bell = true, _ => (),