diff --git a/noline/src/complete.rs b/noline/src/complete.rs new file mode 100644 index 0000000..d76ddad --- /dev/null +++ b/noline/src/complete.rs @@ -0,0 +1,93 @@ +use crate::line_buffer::{Buffer, LineBuffer}; + +/// A type that provides possible line completions +/// +/// ``` +/// use noline::complete::Completer; +/// +/// static FRUIT_LIST: [&str; 14] = [ +/// "Apple", +/// "Banana", +/// "Grape", +/// "Kiwi", +/// "Lemon", +/// "Lime", +/// "Mango", +/// "Melon", +/// "Nectarine", +/// "Orange", +/// "Peach", +/// "Pear", +/// "Pineapple", +/// "Plum", +/// ]; +/// +/// struct Fruit; +/// +/// impl Completer for Fruit { +/// fn complete(&self, line: &str, n: usize) -> Option<&str> { +/// FRUIT_LIST.iter() +/// .filter(|candidate| candidate.starts_with(line)) +/// .skip(n) +/// .next() +/// .map(|candidate| &candidate[line.len()..]) +/// } +/// } +/// +/// assert_eq!(Fruit.complete("Pe", 1), Some("ar")) +/// ``` +pub trait Completer { + /// Given `line` return the `n`'th possible continuation + fn complete(&self, line: &str, n: usize) -> Option<&str>; +} + +impl Completer for () { + fn complete(&self, _: &str, _: usize) -> Option<&str> { + None + } +} + +impl Completer for &T { + fn complete(&self, line: &str, n: usize) -> Option<&str> { + T::complete(self, line, n) + } +} + +impl Completer for &mut T { + fn complete(&self, line: &str, n: usize) -> Option<&str> { + T::complete(self, line, n) + } +} + +pub(crate) struct CompletionCycler { + completer: C, + pre_completion_len: Option, + n: usize, +} + +impl CompletionCycler { + pub(crate) fn new(completer: C) -> Self { + CompletionCycler { + completer, + pre_completion_len: None, + n: 0, + } + } + + pub(crate) fn complete(&mut self, line: &mut LineBuffer) -> Result<(), ()> { + let pre_completion_len = *self.pre_completion_len.get_or_insert(line.len()); + line.delete_after_char(pre_completion_len); + if let Some(completion) = self.completer.complete(line.as_str(), self.n) { + self.n += 1; + line.insert_str(pre_completion_len, completion) + } else { + self.n = 0; + Ok(()) + } + } + + pub(crate) fn reset(&mut self) { + self.pre_completion_len = None; + self.n = 0; + } +} diff --git a/noline/src/core.rs b/noline/src/core.rs index 1688aa3..a8fa525 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::complete::{Completer, CompletionCycler}; use crate::history::{History, HistoryNavigator}; use crate::input::{Action, ControlCharacter::*, Parser, CSI}; use crate::line_buffer::Buffer; @@ -88,20 +89,22 @@ 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, H: History> { +pub struct Line<'a, B: Buffer, H: History, C: Completer> { buffer: &'a mut LineBuffer, terminal: &'a mut Terminal, parser: Parser, prompt: &'a str, nav: HistoryNavigator<'a, H>, + completer: CompletionCycler, } -impl<'a, B: Buffer, H: History> Line<'a, B, H> { +impl<'a, B: Buffer, H: History, C: Completer> Line<'a, B, H, C> { pub fn new( prompt: &'a str, buffer: &'a mut LineBuffer, terminal: &'a mut Terminal, history: &'a mut H, + completer: C, ) -> Self { Self { buffer, @@ -109,6 +112,7 @@ impl<'a, B: Buffer, H: History> Line<'a, B, H> { parser: Parser::new(), prompt, nav: HistoryNavigator::new(history), + completer: CompletionCycler::new(completer), } } @@ -183,10 +187,19 @@ impl<'a, B: Buffer, H: History> Line<'a, B, H> { #[cfg(test)] dbg!(action); + let pos = self.current_position(); + + if self.buffer.len() == pos { + if let Action::ControlCharacter(Tab) = action { + if self.completer.complete(self.buffer).is_ok() { + return self.generate_output(ClearAndPrintBuffer); + } + } + } + self.completer.reset(); + 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 { @@ -201,8 +214,6 @@ impl<'a, B: Buffer, H: History> Line<'a, B, H> { let len = self.buffer.len(); if len > 0 { - let pos = self.current_position(); - if pos < len { self.buffer.delete(pos); @@ -217,8 +228,6 @@ impl<'a, B: Buffer, H: History> Line<'a, B, H> { 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) @@ -230,8 +239,6 @@ impl<'a, B: Buffer, H: History> Line<'a, B, H> { 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) @@ -244,7 +251,6 @@ impl<'a, B: Buffer, H: History> Line<'a, B, H> { 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)) } @@ -256,7 +262,6 @@ impl<'a, B: Buffer, H: History> Line<'a, B, H> { self.generate_output(Done) } CtrlH | Backspace => { - let pos = self.current_position(); if pos > 0 { self.buffer.delete(pos - 1); self.generate_output(MoveCursorAndEraseAndPrintBuffer(-1)) @@ -272,7 +277,6 @@ impl<'a, B: Buffer, H: History> Line<'a, B, H> { 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); @@ -352,13 +356,14 @@ pub(crate) mod tests { &'b mut self, prompt: &'b str, mockterm: &mut MockTerminal, - ) -> Line<'b, B, H> { + ) -> Line<'b, B, H, ()> { let cursor = mockterm.get_cursor(); let mut line = Line::new( prompt, &mut self.buffer, &mut self.terminal, &mut self.history, + (), ); let output: Vec = line @@ -388,7 +393,7 @@ pub(crate) mod tests { fn advance<'a, B: Buffer, H: History>( terminal: &mut MockTerminal, - noline: &mut Line<'a, B, H>, + noline: &mut Line<'a, B, H, ()>, input: impl AsByteVec, ) -> core::result::Result<(), ()> { terminal.bell = false; diff --git a/noline/src/lib.rs b/noline/src/lib.rs index f992412..6fe4086 100644 --- a/noline/src/lib.rs +++ b/noline/src/lib.rs @@ -49,7 +49,7 @@ //! .unwrap(); //! //! loop { -//! if let Ok(line) = editor.readline(prompt, &mut io) { +//! if let Ok(line) = editor.readline(prompt, &mut io, ()) { //! write!(io, "Read: '{}'\n\r", line).unwrap(); //! } else { //! break; @@ -64,6 +64,7 @@ #[macro_use] extern crate std; pub mod builder; +pub mod complete; mod core; pub mod error; pub mod history; diff --git a/noline/src/no_sync.rs b/noline/src/no_sync.rs index 6f47e35..192528e 100644 --- a/noline/src/no_sync.rs +++ b/noline/src/no_sync.rs @@ -5,6 +5,7 @@ pub mod tokio { //! Implementation for tokio use crate::{ + complete::Completer, core::{Initializer, InitializerResult, Line}, error::Error, history::{get_history_entries, CircularSlice, History}, @@ -92,12 +93,14 @@ pub mod tokio { prompt: &str, stdin: &mut R, stdout: &mut W, + completer: impl Completer, ) -> Result<&'b str, Error> { let mut line = Line::new( prompt, &mut self.buffer, &mut self.terminal, &mut self.history, + completer, ); for output in line.reset() { diff --git a/noline/src/sync.rs b/noline/src/sync.rs index c8920c3..53ad95c 100644 --- a/noline/src/sync.rs +++ b/noline/src/sync.rs @@ -10,6 +10,7 @@ use crate::error::Error; use crate::history::{get_history_entries, CircularSlice, History}; use crate::line_buffer::{Buffer, LineBuffer}; +use crate::complete::Completer; use crate::core::{Initializer, InitializerResult, Line}; use crate::output::{Output, OutputItem}; use crate::terminal::Terminal; @@ -98,12 +99,14 @@ where &'b mut self, prompt: &'b str, io: &mut IO, + completer: impl Completer, ) -> Result<&'b str, Error> { let mut line = Line::new( prompt, &mut self.buffer, &mut self.terminal, &mut self.history, + completer, ); Self::handle_output(line.reset(), io)?; @@ -190,7 +193,7 @@ mod tests { let handle = thread::spawn(move || { let mut editor = EditorBuilder::new_unbounded().build_sync(&mut io).unwrap(); - if let Ok(s) = editor.readline("> ", &mut io) { + if let Ok(s) = editor.readline("> ", &mut io, ()) { Some(s.to_string()) } else { None @@ -423,7 +426,7 @@ pub mod std { .build_sync(&mut io) .unwrap(); - while let Ok(s) = editor.readline(prompt, &mut io) { + while let Ok(s) = editor.readline(prompt, &mut io, ()) { string_tx.send(s.to_string()).unwrap(); } }) @@ -593,7 +596,7 @@ pub mod embedded { .build_sync(&mut io) .unwrap(); - while let Ok(s) = editor.readline(prompt, &mut io) { + while let Ok(s) = editor.readline(prompt, &mut io, ()) { string_tx.send(s.to_string()).unwrap(); } })