Commit df0f67e5 authored by Michael Aaron Murphy's avatar Michael Aaron Murphy

Merge branch 'completer-event-listener' into 'master'

💥 Merge the EventHandler and Completer interface

See merge request redox-os/liner!15
parents c3bc4640 2c587023
Pipeline #4323 passed with stage
in 3 minutes and 3 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>,
......
......@@ -3,11 +3,12 @@ use std::cmp;
use std::io::{self, Write};
use termion::{self, clear, color, cursor};
use super::complete::Completer;
use context::ColorClosure;
use Context;
use Buffer;
use event::*;
use util;
use Buffer;
use Context;
/// Buffer for prompt writes, meant to be shared between prompt creations.
struct LocalBuffer(pub RefCell<Vec<u8>>);
......@@ -141,13 +142,13 @@ macro_rules! cur_buf_mut {
Some(i) => {
$s.buffer_changed = true;
&mut $s.context.history[i]
},
}
_ => {
$s.buffer_changed = true;
&mut $s.new_buf
},
}
}
}
};
}
macro_rules! cur_buf {
......@@ -156,7 +157,7 @@ macro_rules! cur_buf {
Some(i) => &$s.context.history[i],
_ => &$s.new_buf,
}
}
};
}
impl<'a, W: Write> Editor<'a, W> {
......@@ -164,7 +165,7 @@ impl<'a, W: Write> Editor<'a, W> {
out: W,
prompt: P,
f: Option<ColorClosure>,
context: &'a mut Context
context: &'a mut Context,
) -> io::Result<Self> {
Editor::new_with_init_buffer(out, prompt, f, context, Buffer::new())
}
......@@ -267,7 +268,8 @@ impl<'a, W: Write> Editor<'a, W> {
fn search_history_loc(&self) -> Option<usize> {
if self.history_subset_index.len() > 0 {
self.history_subset_loc.map(|i| self.history_subset_index[i])
self.history_subset_loc
.map(|i| self.history_subset_index[i])
} else {
None
}
......@@ -289,12 +291,11 @@ impl<'a, W: Write> Editor<'a, W> {
if forward || target_loc == *history_loc || i == 0 {
self.history_subset_loc = Some(i);
} else {
self.history_subset_loc = Some(i-1);
self.history_subset_loc = Some(i - 1);
}
break;
}
}
}
} else {
self.history_subset_loc = None;
......@@ -317,9 +318,17 @@ impl<'a, W: Write> Editor<'a, W> {
} else if self.history_subset_index.len() > 0 {
self.history_subset_loc = if let Some(p) = self.history_subset_loc {
if forward {
if p < self.history_subset_index.len() - 1 { Some(p + 1) } else { Some(0) }
if p < self.history_subset_index.len() - 1 {
Some(p + 1)
} else {
Some(0)
}
} else {
if p > 0 { Some(p - 1) } else { Some(self.history_subset_index.len() - 1) }
if p > 0 {
Some(p - 1)
} else {
Some(self.history_subset_index.len() - 1)
}
}
} else {
None
......@@ -370,7 +379,10 @@ impl<'a, W: Write> Editor<'a, W> {
Ok(did)
}
fn print_completion_list(completions: &[String], highlighted: Option<usize>) -> io::Result<usize> {
fn print_completion_list(
completions: &[String],
highlighted: Option<usize>,
) -> io::Result<usize> {
use std::cmp::max;
let (w, _) = termion::terminal_size()?;
......@@ -409,18 +421,17 @@ impl<'a, W: Write> Editor<'a, W> {
Ok(lines)
})
}
pub fn skip_completions_hint(&mut self) {
self.show_completions_hint = None;
}
pub fn complete(&mut self, handler: &mut EventHandler<W>) -> io::Result<()> {
handler(Event::new(self, EventKind::BeforeComplete));
pub fn complete<T: Completer<W>>(&mut self, handler: &mut T) -> io::Result<()> {
handler.on_event(Event::new(self, EventKind::BeforeComplete));
if let Some((completions, i)) = self.show_completions_hint.take() {
let i = i.map_or(0, |i| (i+1) % completions.len());
let i = i.map_or(0, |i| (i + 1) % completions.len());
self.delete_word_before_cursor(false)?;
self.insert_str_after_cursor(&completions[i])?;
......@@ -442,14 +453,10 @@ impl<'a, W: Write> Editor<'a, W> {
None => "".into(),
};
if let Some(ref completer) = self.context.completer {
let mut completions = completer.completions(word.as_ref());
completions.sort();
completions.dedup();
(word, completions)
} else {
return Ok(());
}
let mut completions = handler.completions(word.as_ref());
completions.sort();
completions.dedup();
(word, completions)
};
if completions.is_empty() {
......@@ -489,17 +496,21 @@ impl<'a, W: Write> Editor<'a, W> {
let (words, pos) = self.get_words_and_cursor_position();
match pos {
CursorPosition::InWord(i) => Some(words[i]),
CursorPosition::InSpace(Some(i), _) => if ignore_space_before_cursor {
Some(words[i])
} else {
None
},
CursorPosition::InSpace(Some(i), _) => {
if ignore_space_before_cursor {
Some(words[i])
} else {
None
}
}
CursorPosition::InSpace(None, _) => None,
CursorPosition::OnWordLeftEdge(i) => if ignore_space_before_cursor && i > 0 {