Commit 2c587023 authored by AdminXVII's avatar AdminXVII

💥 Merge the EventHandler and Completer interface

This will reduce the hassle of dealing with lifetimes and makes it
possible to remove allocations when they are not needed
parent c3bc4640
Pipeline #4317 passed with stage
in 2 minutes and 46 seconds
[package]
name = "liner"
version = "0.4.5" # remember to update README
version = "0.5.0" # remember to update README
authors = ["MovingtoMars <definitelynotliam@gmail.com>"]
description = "A library offering readline-like functionality."
repository = "https://gitlab.redox-os.org/redox-os/liner"
......
......@@ -20,7 +20,7 @@ A Rust library offering readline-like functionality.
In `Cargo.toml`:
```toml
[dependencies]
liner = "0.4.5"
liner = "0.5.0"
...
```
......@@ -29,13 +29,21 @@ In `src/main.rs`:
```rust
extern crate liner;
use liner::Context;
use liner::{Context, Completer};
struct EmptyCompleter;
impl<W: std::io::Write> Completer<W> for EmptyCompleter {
fn completions(&mut self, _start: &str) -> Vec<String> {
Vec::new()
}
}
fn main() {
let mut con = Context::new();
loop {
let res = con.read_line("[prompt]$ ", &mut |_| {}).unwrap();
let res = con.read_line("[prompt]$ ", &mut EmptyCompleter).unwrap();
if res.is_empty() {
break;
......
extern crate liner;
extern crate termion;
extern crate regex;
extern crate termion;
use std::mem::replace;
use std::env::{args, current_dir};
use std::io;
use std::mem::replace;
use liner::{Context, CursorPosition, Event, EventKind, FilenameCompleter};
use termion::color;
use liner::{Completer, Context, CursorPosition, Event, EventKind, FilenameCompleter};
use regex::Regex;
use termion::color;
// This prints out the text back onto the screen
fn highlight_dodo(s: &str) -> String {
......@@ -17,8 +17,59 @@ fn highlight_dodo(s: &str) -> String {
reg_exp.replace_all(s, format.as_str()).to_string()
}
struct CommentCompleter {
inner: Option<FilenameCompleter>,
}
impl<W: io::Write> Completer<W> for CommentCompleter {
fn completions(&mut self, start: &str) -> Vec<String> {
if let Some(inner) = &mut self.inner {
Completer::<W>::completions(inner, start)
} else {
Vec::new()
}
}
fn on_event(&mut self, event: Event<W>) {
if let EventKind::BeforeComplete = event.kind {
let (_, pos) = event.editor.get_words_and_cursor_position();
// Figure out of we are completing a command (the first word) or a filename.
let filename = match pos {
// If we are inside of a word(i is the index inside of the text, and if that
// position is over zero, we return true
CursorPosition::InWord(i) => i > 0,
// If we are in a space like this `cat | cart` or cat |
// checks if there is a word to our left(indicated by there being Some value)
CursorPosition::InSpace(Some(_), _) => true,
// Checks if there is no word to our left(indicated by there being None value)
CursorPosition::InSpace(None, _) => false,
// If we are on the left edge of a word, and the position of the cursor is
// greater than or equal to 1, return true
CursorPosition::OnWordLeftEdge(i) => i >= 1,
// If we are on the right edge of the word
CursorPosition::OnWordRightEdge(i) => i >= 1,
};
// If we are not in a word with pos over zero, or in a space with text beforehand,
// or on the left edge of a word with pos >= to 1, or on the Right edge of a word
// under the same condition, then
// This condition is only false under the predicate that we are in a space with no
// word to the left
if filename {
let completer = FilenameCompleter::new(Some(current_dir().unwrap()));
replace(&mut self.inner, Some(completer));
} else {
// Delete the completer
replace(&mut self.inner, None);
}
}
}
}
fn main() {
let mut con = Context::new();
let mut completer = CommentCompleter { inner: None };
let history_file = match args().nth(1) {
Some(file_name) => {
......@@ -31,49 +82,14 @@ fn main() {
}
};
con.history.set_file_name_and_load_history(history_file).unwrap();
con.history
.set_file_name_and_load_history(history_file)
.unwrap();
loop {
// Reads the line, the first arg is the prompt, the second arg is a function called on every bit of text leaving liner, and the third is called on every key press
// Basically highlight_dodo(read_line()), where on every keypress, the lambda is called
let res = con.read_line("[prompt]$ ",
Some(Box::new(highlight_dodo)),
&mut |Event { editor, kind }| {
if let EventKind::BeforeComplete = kind {
let (_, pos) = editor.get_words_and_cursor_position();
// Figure out of we are completing a command (the first word) or a filename.
let filename = match pos {
// If we are inside of a word(i is the index inside of the text, and if that
// position is over zero, we return true
CursorPosition::InWord(i) => i > 0,
// If we are in a space like this `cat | cart` or cat |
// checks if there is a word to our left(indicated by there being Some value)
CursorPosition::InSpace(Some(_), _) => true,
// Checks if there is no word to our left(indicated by there being None value)
CursorPosition::InSpace(None, _) => false,
// If we are on the left edge of a word, and the position of the cursor is
// greater than or equal to 1, return true
CursorPosition::OnWordLeftEdge(i) => i >= 1,
// If we are on the right edge of the word
CursorPosition::OnWordRightEdge(i) => i >= 1,
};
// If we are not in a word with pos over zero, or in a space with text beforehand,
// or on the left edge of a word with pos >= to 1, or on the Right edge of a word
// under the same condition, then
// This condition is only false under the predicate that we are in a space with no
// word to the left
if filename {
let completer = FilenameCompleter::new(Some(current_dir().unwrap()));
replace(&mut editor.context().completer, Some(Box::new(completer)));
} else {
// Delete the completer
replace(&mut editor.context().completer, None);
}
}
});
let res = con.read_line("[prompt]$ ", Some(Box::new(highlight_dodo)), &mut completer);
// We are out of the lambda, and res is the result from read_line which is an Into<String>
match res {
......@@ -117,12 +133,12 @@ fn main() {
// Ensure that all writes to the history file
// are written before exiting due to error.
panic!("error: {:?}", e)
},
}
}
}
}
// End loop
// End loop
}
// Ensure that all writes to the history file are written before exiting.
......
extern crate liner;
extern crate termion;
extern crate regex;
extern crate termion;
use std::mem::replace;
use std::env::{args, current_dir};
use std::io;
use std::mem::replace;
use liner::{Context, CursorPosition, Event, EventKind, FilenameCompleter};
use termion::color;
use liner::{Completer, Context, CursorPosition, Event, EventKind, FilenameCompleter};
use regex::Regex;
use termion::color;
fn highlight_dodo(s: &str) -> String {
let reg_exp = Regex::new("(?P<k>dodo)").unwrap();
......@@ -16,8 +16,45 @@ fn highlight_dodo(s: &str) -> String {
reg_exp.replace_all(s, format.as_str()).to_string()
}
struct NoCommentCompleter {
inner: Option<FilenameCompleter>,
}
impl<W: io::Write> Completer<W> for NoCommentCompleter {
fn completions(&mut self, start: &str) -> Vec<String> {
if let Some(inner) = &mut self.inner {
Completer::<W>::completions(inner, start)
} else {
Vec::new()
}
}
fn on_event(&mut self, event: Event<W>) {
if let EventKind::BeforeComplete = event.kind {
let (_, pos) = event.editor.get_words_and_cursor_position();
// Figure out of we are completing a command (the first word) or a filename.
let filename = match pos {
CursorPosition::InWord(i) => i > 0,
CursorPosition::InSpace(Some(_), _) => true,
CursorPosition::InSpace(None, _) => false,
CursorPosition::OnWordLeftEdge(i) => i >= 1,
CursorPosition::OnWordRightEdge(i) => i >= 1,
};
if filename {
let completer = FilenameCompleter::new(Some(current_dir().unwrap()));
replace(&mut self.inner, Some(completer));
} else {
replace(&mut self.inner, None);
}
}
}
}
fn main() {
let mut con = Context::new();
let mut completer = NoCommentCompleter { inner: None };
let history_file = match args().nth(1) {
Some(file_name) => {
......@@ -30,32 +67,12 @@ fn main() {
}
};
con.history.set_file_name_and_load_history(history_file).unwrap();
con.history
.set_file_name_and_load_history(history_file)
.unwrap();
loop {
let res = con.read_line("[prompt]$ ",
Some(Box::new(highlight_dodo)),
&mut |Event { editor, kind }| {
if let EventKind::BeforeComplete = kind {
let (_, pos) = editor.get_words_and_cursor_position();
// Figure out of we are completing a command (the first word) or a filename.
let filename = match pos {
CursorPosition::InWord(i) => i > 0,
CursorPosition::InSpace(Some(_), _) => true,
CursorPosition::InSpace(None, _) => false,
CursorPosition::OnWordLeftEdge(i) => i >= 1,
CursorPosition::OnWordRightEdge(i) => i >= 1,
};
if filename {
let completer = FilenameCompleter::new(Some(current_dir().unwrap()));
replace(&mut editor.context().completer, Some(Box::new(completer)));
} else {
replace(&mut editor.context().completer, None);
}
}
});
let res = con.read_line("[prompt]$ ", Some(Box::new(highlight_dodo)), &mut completer);
match res {
Ok(res) => {
......@@ -94,7 +111,7 @@ fn main() {
// Ensure that all writes to the history file
// are written before exiting.
panic!("error: {:?}", e)
},
}
}
}
}
......
use super::event::Event;
use std::io::Write;
use std::path::PathBuf;
pub trait Completer {
fn completions(&self, start: &str) -> Vec<String>;
pub trait Completer<W: Write> {
fn completions(&mut self, start: &str) -> Vec<String>;
fn on_event(&mut self, _event: Event<W>) {}
}
pub struct BasicCompleter {
......@@ -10,12 +13,14 @@ pub struct BasicCompleter {
impl BasicCompleter {
pub fn new<T: Into<String>>(prefixes: Vec<T>) -> BasicCompleter {
BasicCompleter { prefixes: prefixes.into_iter().map(|s| s.into()).collect() }
BasicCompleter {
prefixes: prefixes.into_iter().map(|s| s.into()).collect(),
}
}
}
impl Completer for BasicCompleter {
fn completions(&self, start: &str) -> Vec<String> {
impl<T: Write> Completer<T> for BasicCompleter {
fn completions(&mut self, start: &str) -> Vec<String> {
self.prefixes
.iter()
.filter(|s| s.starts_with(start))
......@@ -30,17 +35,19 @@ pub struct FilenameCompleter {
impl FilenameCompleter {
pub fn new<T: Into<PathBuf>>(working_dir: Option<T>) -> Self {
FilenameCompleter { working_dir: working_dir.map(|p| p.into()) }
FilenameCompleter {
working_dir: working_dir.map(|p| p.into()),
}
}
}
impl Completer for FilenameCompleter {
fn completions(&self, mut start: &str) -> Vec<String> {
impl<T: Write> Completer<T> for FilenameCompleter {
fn completions(&mut self, mut start: &str) -> Vec<String> {
// XXX: this function is really bad, TODO rewrite
let start_owned: String = if start.starts_with('\"') || start.starts_with('\'') {
start = &start[1..];
if ! start.is_empty() {
if !start.is_empty() {
start = &start[..start.len() - 1];
}
start.into()
......@@ -64,23 +71,23 @@ impl Completer for FilenameCompleter {
let completing_dir;
match full_path.parent() {
// XXX non-unix separaor
Some(parent) if !start.is_empty() && !start_owned.ends_with('/') &&
!full_path.ends_with("..") => {
Some(parent)
if !start.is_empty()
&& !start_owned.ends_with('/')
&& !full_path.ends_with("..") =>
{
p = parent;
start_name = full_path
.file_name()
.unwrap()
.to_string_lossy();
start_name = full_path.file_name().unwrap().to_string_lossy();
completing_dir = false;
}
_ => {
p = full_path.as_path();
start_name = "".into();
completing_dir = start.is_empty() || start.ends_with('/') || full_path.ends_with("..");
completing_dir =
start.is_empty() || start.ends_with('/') || full_path.ends_with("..");
}
}
let read_dir = match p.read_dir() {
Ok(x) => x,
Err(_) => return vec![],
......
......@@ -48,7 +48,6 @@ pub enum KeyBindings {
pub struct Context {
pub history: History,
pub completer: Option<Box<Completer>>,
pub word_divider_fn: Box<Fn(&Buffer) -> Vec<(usize, usize)>>,
pub key_bindings: KeyBindings,
}
......@@ -57,7 +56,6 @@ impl Context {
pub fn new() -> Self {
Context {
history: History::new(),
completer: None,
word_divider_fn: Box::new(get_buffer_words),
key_bindings: KeyBindings::Emacs,
}
......@@ -67,11 +65,11 @@ impl Context {
/// The output is stdout.
/// The returned line has the newline removed.
/// Before returning, will revert all changes to the history buffers.
pub fn read_line<P: Into<String>>(
pub fn read_line<P: Into<String>, C: Completer<RawTerminal<Stdout>>>(
&mut self,
prompt: P,
f: Option<ColorClosure>,
handler: &mut EventHandler<RawTerminal<Stdout>>,
handler: &mut C,
) -> io::Result<String> {
self.read_line_with_init_buffer(prompt, handler, f, Buffer::new())
}
......@@ -79,18 +77,31 @@ impl Context {
/// Same as `Context.read_line()`, but passes the provided initial buffer to the editor.
///
/// ```no_run
/// use liner::Context;
/// use liner::{Context, Completer};
///
/// struct EmptyCompleter;
///
/// impl<W: std::io::Write> Completer<W> for EmptyCompleter {
/// fn completions(&mut self, _start: &str) -> Vec<String> {
/// Vec::new()
/// }
/// }
///
/// let mut context = Context::new();
/// let line =
/// context.read_line_with_init_buffer("[prompt]$ ",
/// &mut |_| {},
/// &mut EmptyCompleter,
/// Some(Box::new(|s| String::from(s))),
/// "some initial buffer");
/// ```
pub fn read_line_with_init_buffer<P: Into<String>, B: Into<Buffer>>(
pub fn read_line_with_init_buffer<
P: Into<String>,
B: Into<Buffer>,
C: Completer<RawTerminal<Stdout>>,
>(
&mut self,
prompt: P,
handler: &mut EventHandler<RawTerminal<Stdout>>,
handler: &mut C,
f: Option<ColorClosure>,
buffer: B,
) -> io::Result<String> {
......@@ -107,9 +118,9 @@ impl Context {
res
}
fn handle_keys<'a, T, W: Write, M: KeyMap<'a, W, T>>(
fn handle_keys<'a, T, W: Write, M: KeyMap<'a, W, T>, C: Completer<W>>(
mut keymap: M,
handler: &mut EventHandler<W>,
handler: &mut C,
) -> io::Result<String>
where
String: From<M>,
......
This diff is collapsed.
......@@ -2,8 +2,6 @@ use std::io::Write;
use termion::event::Key;
use Editor;
pub type EventHandler<'a, W> = FnMut(Event<W>) + 'a;
pub struct Event<'a, 'out: 'a, W: Write + 'a> {
pub editor: &'a mut Editor<'out, W>,
pub kind: EventKind,
......
use std::io::{self, Write};
use termion::event::Key;
use KeyMap;
use Editor;
use CursorPosition;
use Editor;
use KeyMap;
/// Emacs keybindings for `Editor`. This is the default for `Context::read_line()`.
///
......@@ -19,7 +19,10 @@ pub struct Emacs<'a, W: Write> {
impl<'a, W: Write> Emacs<'a, W> {
pub fn new(ed: Editor<'a, W>) -> Self {
Emacs { ed, last_arg_fetch_index: None }
Emacs {
ed,
last_arg_fetch_index: None,
}
}
fn handle_ctrl_key(&mut self, c: char) -> io::Result<()> {
......@@ -68,7 +71,10 @@ impl<'a, W: Write> Emacs<'a, W> {
let history_index = match self.last_arg_fetch_index {
Some(0) => return Ok(()),
Some(x) => x - 1,
None => self.ed.current_history_location().unwrap_or(self.ed.context().history.len() - 1),
None => self
.ed
.current_history_location()
.unwrap_or(self.ed.context().history.len() - 1),
};
// If did a last arg fetch just before this, we need to delete it so it can be replaced by
......@@ -96,7 +102,7 @@ impl<'a, W: Write> Emacs<'a, W> {
impl<'a, W: Write> KeyMap<'a, W, Emacs<'a, W>> for Emacs<'a, W> {
fn handle_key_core(&mut self, key: Key) -> io::Result<()> {
match key {
Key::Alt('.') => {},
Key::Alt('.') => {}
_ => self.last_arg_fetch_index = None,
}
......@@ -117,11 +123,11 @@ impl<'a, W: Write> KeyMap<'a, W, Emacs<'a, W>> for Emacs<'a, W> {
}
}
fn editor_mut(&mut self) -> &mut Editor<'a, W> {
fn editor_mut(&mut self) -> &mut Editor<'a, W> {
&mut self.ed
}
fn editor(&self) -> &Editor<'a, W> {
fn editor(&self) -> &Editor<'a, W> {
&self.ed
}
}
......@@ -142,26 +148,22 @@ fn emacs_move_word<W: Write>(ed: &mut Editor<W>, direction: EmacsMoveDir) -> io:
let (words, pos) = ed.get_words_and_cursor_position();
let word_index = match pos {
CursorPosition::InWord(i) => {
Some(i)
},
CursorPosition::InWord(i) => Some(i),
CursorPosition::OnWordLeftEdge(mut i) => {
if i > 0 && direction == EmacsMoveDir::Left {
i -= 1;
}
Some(i)
},
}
CursorPosition::OnWordRightEdge(mut i) => {
if i < words.len() - 1 && direction == EmacsMoveDir::Right {
i += 1;
}
Some(i)
},
CursorPosition::InSpace(left, right) => {
match direction {
EmacsMoveDir::Left => left,
EmacsMoveDir::Right => right,
}
}
CursorPosition::InSpace(left, right) => match direction {
EmacsMoveDir::Left => left,
EmacsMoveDir::Right => right,
},
};
......@@ -183,23 +185,25 @@ fn emacs_move_word<W: Write>(ed: &mut Editor<W>, direction: EmacsMoveDir) -> io:
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use termion::event::Key;
use Completer;
use Context;
use Editor;
use KeyMap;
use std::io::Write;
macro_rules! simulate_keys {
($keymap:ident, $keys:expr) => {{
simulate_keys(&mut $keymap, $keys.into_iter())
}}
}};
}
fn simulate_keys<'a, 'b, W: Write, T, M: KeyMap<'a, W, T>, I>(keymap: &mut M, keys: I) -> bool
where I: Iterator<Item = &'b Key>
where
I: Iterator<Item = &'b Key>,
{
for k in keys {
if keymap.handle_key(*k, &mut |_| {}).unwrap() {
if keymap.handle_key(*k, &mut EmptyCompleter).unwrap() {
return true;
}
}
......@@ -207,6 +211,14 @@ mod tests {
false
}
struct EmptyCompleter;
impl<W: Write> Completer<W> for EmptyCompleter {
fn completions(&mut self, _start: &str) -> Vec<String> {
Vec::default()
}
}
#[test]
fn enter_is_done() {
let mut context = Context::new();
......@@ -243,7 +255,9 @@ mod tests {
let out = Vec::new();
let ed = Editor::new(out, "prompt".to_owned(), None, &mut context).unwrap();
let mut map = Emacs::new(ed);
map.editor_mut().insert_str_after_cursor("abc def ghi").unwrap();
map.editor_mut()
.insert_str_after_cursor("abc def ghi")
.unwrap();
assert_eq!(map.ed.cursor(), 11);
simulate_keys!(map, [Key::Alt('b')]);
......@@ -280,7 +294,7 @@ mod tests {
let mut map = Emacs::new(ed);
map.ed.insert_str_after_cursor("not empty").unwrap();
let res = map.handle_key(Key::Ctrl('h'), &mut |_| {});
let res = map.handle_key(Key::Ctrl('h'), &mut EmptyCompleter);
assert_eq!(res.is_ok(), true);
assert_eq!(map.ed.current_buffer().to_string(), "not empt".to_string());
}
......
use std::io::{self, Write, ErrorKind};
use super::complete::Completer;
use event::*;
use std::io::{self, ErrorKind, Write};
use termion::event::Key;
use Editor;
use event::*;
pub trait KeyMap<'a, W: Write, T>: From<T> {
fn handle_key_core(&mut self, key: Key) -> io::Result<()>;
fn editor(&self) -> &Editor<'a, W>;
fn editor_mut(&mut self) -> &mut Editor<'a, W>;
fn handle_key(&mut self, mut key: Key, handler: &mut EventHandler<W>) -> io::Result<bool> {
fn handle_key<C: Completer<W>>(&mut self, mut key: Key, handler: &mut C) -> io::Result<bool> {
let mut done = false;
handler(Event::new(self.editor_mut(), EventKind::BeforeKey(key)));
handler.on_event(Event::new(self.editor_mut(), EventKind::BeforeKey(key)));
let is_empty = self.editor().current_buffer().is_empty();
......@@ -43,8 +44,10 @@ pub trait KeyMap<'a, W: Write, T>: From<T> {
Key::Ctrl('s') => {
self.editor_mut().search(true)?;
}
Key::Right if self.editor().is_currently_showing_autosuggestion() &&
self.editor().cursor_is_at_end_of_line() => {
Key::Right
if self.editor().is_currently_showing_autosuggestion()
&& self.editor().cursor_is_at_end_of_line() =>
{
self.editor_mut().accept_autosuggestion()?;
}
_ => {
......@@ -53,7 +56,7 @@ pub trait KeyMap<'a, W: Write, T>: From<T> {
}
};
handler(Event::new(self.editor_mut(), EventKind::AfterKey(key)));
handler.on_event(Event::new(self.editor_mut(), EventKind::AfterKey(key)));
self.editor_mut().flush()?;
......@@ -70,8 +73,8 @@ pub use emacs::Emacs;
#[cfg(test)]
mod tests {
use super::*;
use termion::event::Key::*;
use std::io::ErrorKind;
use termion::event::Key::*;
use Context;
struct TestKeyMap<'a, W: Write> {
......@@ -80,9 +83,7 @@ mod tests {
impl<'a, W: Write> TestKeyMap<'a, W> {
pub fn new(ed: Editor<'a, W>) -> Self {
TestKeyMap {
ed: ed,
}
TestKeyMap { ed: ed }
}
}
......@@ -91,15 +92,23 @@ mod tests {
Ok(())
}
fn editor_mut(&mut self) -> &mut Editor<'a, W> {
fn editor_mut(&mut self) -> &mut Editor<'a, W> {
&mut self.ed
}
fn editor(&self) -> &Editor<'a, W> {
fn editor(&self) -> &Editor<'a, W> {
&self.ed
}
}
struct EmptyCompleter;
impl<W: Write> Completer<W> for EmptyCompleter {
fn completions(&mut self, _start: &str) -> Vec<String> {
Vec::default()
}
}
#[test]
/// when the current buffer is empty, ctrl-d generates and eof error
fn ctrl_d_empty() {
......@@ -108,7 +117,7 @@ mod tests {
let ed = Editor::new(out, "prompt".to_owned(), None, &mut context).unwrap();
let mut map = TestKeyMap::new(ed);
let res = map.handle_key(Ctrl('d'), &mut |_| {});
let res = map.handle_key(Ctrl('d'), &mut EmptyCompleter);
assert_eq!(res.is_err(), true);
assert_eq!(res.err().unwrap().kind(), ErrorKind::UnexpectedEof);
}
......@@ -122,7 +131,7 @@ mod tests {
let mut map = TestKeyMap::new(ed);
map.ed.insert_str_after_cursor("not empty").unwrap();
let res = map.handle_key(Ctrl('d'), &mut |_| {});
let res = map.handle_key(Ctrl('d'), &mut EmptyCompleter);
assert_eq!(res.is_ok(), true);
}
......@@ -134,7 +143,7 @@ mod tests {
let ed = Editor::new(out, "prompt".to_owned(), None, &mut context).unwrap();
let mut map =