Commit 2ff5739f authored by MovingtoMars's avatar MovingtoMars

initial commit

parents
target
Cargo.lock
[package]
name = "liner"
version = "0.1.0"
authors = ["MovingtoMars <liam@bumblebee.net.nz>"]
[lib]
name = "liner"
[[bin]]
name = "liner_test"
[dependencies]
[dependencies.termion]
git = "https://github.com/Ticki/termion.git"
features = ["nightly"]
use termion::TermWrite;
use std::io::{self, Write};
use std::iter::FromIterator;
#[derive(Debug,Clone)]
pub enum Action {
Insert {
start: usize,
text: Vec<char>,
},
Remove {
start: usize,
text: Vec<char>,
},
}
impl Action {
pub fn do_on(&self, buf: &mut Buffer) {
match *self {
Action::Insert { start, ref text } => buf.insert_raw(start, &text[..]),
Action::Remove { start, ref text } => {
buf.remove_raw(start, start + text.len());
}
}
}
pub fn undo(&self, buf: &mut Buffer) {
match *self {
Action::Insert { start, ref text } => {
buf.remove_raw(start, start + text.len());
}
Action::Remove { start, ref text } => buf.insert_raw(start, &text[..]),
}
}
}
#[derive(Debug, Clone)]
pub struct Buffer {
data: Vec<char>,
actions: Vec<Action>,
undone_actions: Vec<Action>,
}
impl From<Buffer> for String {
fn from(buf: Buffer) -> Self {
String::from_iter(buf.data)
}
}
impl From<String> for Buffer {
fn from(s: String) -> Self {
Buffer::from_iter(s.chars())
}
}
impl<'a> From<&'a str> for Buffer {
fn from(s: &'a str) -> Self {
Buffer::from_iter(s.chars())
}
}
impl FromIterator<char> for Buffer {
fn from_iter<T: IntoIterator<Item = char>>(t: T) -> Self {
Buffer {
data: t.into_iter().collect(),
actions: Vec::new(),
undone_actions: Vec::new(),
}
}
}
impl Buffer {
pub fn new() -> Self {
Buffer {
data: Vec::new(),
actions: Vec::new(),
undone_actions: Vec::new(),
}
}
pub fn clear_actions(&mut self) {
self.actions.clear();
self.undone_actions.clear();
}
pub fn undo(&mut self) -> bool {
match self.actions.pop() {
None => false,
Some(act) => {
act.undo(self);
self.undone_actions.push(act);
true
}
}
}
pub fn redo(&mut self) -> bool {
match self.undone_actions.pop() {
None => false,
Some(act) => {
act.do_on(self);
self.actions.push(act);
true
}
}
}
pub fn revert(&mut self) -> bool {
if self.actions.len() == 0 {
return false;
}
while self.undo() {}
true
}
fn push_action(&mut self, act: Action) {
self.actions.push(act);
self.undone_actions.clear();
}
pub fn num_chars(&self) -> usize {
self.data.len()
}
pub fn num_bytes(&self) -> usize {
let s: String = self.clone().into();
s.len()
}
pub fn char_before(&self, cursor: usize) -> Option<char> {
if cursor == 0 {
None
} else {
self.data.get(cursor - 1).cloned()
}
}
pub fn char_after(&self, cursor: usize) -> Option<char> {
self.data.get(cursor).cloned()
}
/// Returns the number of characters removed.
pub fn remove(&mut self, start: usize, end: usize) -> usize {
let s = self.remove_raw(start, end);
let num_removed = s.len();
let act = Action::Remove {
start: start,
text: s,
};
self.push_action(act);
num_removed
}
pub fn insert(&mut self, start: usize, text: &[char]) {
let act = Action::Insert {
start: start,
text: text.into(),
};
act.do_on(self);
self.push_action(act);
}
pub fn range(&self, start: usize, end: usize) -> String {
self.data[start..end].iter().cloned().collect()
}
pub fn range_chars(&self, start: usize, end: usize) -> Vec<char> {
self.data[start..end].iter().cloned().collect()
}
pub fn chars(&self) -> ::std::slice::Iter<char> {
self.data.iter()
}
pub fn truncate(&mut self, num: usize) {
self.data.truncate(num);
}
pub fn print<W>(&self, out: &mut W) -> io::Result<()>
where W: TermWrite + Write
{
let string: String = self.data.iter().cloned().collect();
try!(out.write(string.as_bytes()));
Ok(())
}
fn remove_raw(&mut self, start: usize, end: usize) -> Vec<char> {
self.data.drain(start..end).collect()
}
fn insert_raw(&mut self, start: usize, text: &[char]) {
for (i, &c) in text.iter().enumerate() {
self.data.insert(start + i, c)
}
}
}
use std::path::PathBuf;
pub trait Completer {
fn completions(&self, start: &str) -> Vec<String>;
}
pub struct BasicCompleter {
prefixes: Vec<String>,
}
impl BasicCompleter {
pub fn new<T: Into<String>>(prefixes: Vec<T>) -> BasicCompleter {
BasicCompleter { prefixes: prefixes.into_iter().map(|s| s.into()).collect() }
}
}
impl Completer for BasicCompleter {
fn completions(&self, start: &str) -> Vec<String> {
self.prefixes.iter().filter(|s| s.starts_with(start)).cloned().collect()
}
}
pub struct FilenameCompleter {
working_dir: Option<PathBuf>,
}
impl FilenameCompleter {
pub fn new<T: Into<PathBuf>>(working_dir: Option<T>) -> Self {
FilenameCompleter { working_dir: working_dir.map(|p| p.into()) }
}
}
impl Completer for FilenameCompleter {
fn completions(&self, mut start: &str) -> Vec<String> {
// XXX: this function is really bad, TODO rewrite
let start_owned;
if start.starts_with("\"") || start.starts_with("'") {
start = &start[1..];
if start.len() >= 1 {
start = &start[..start.len() - 1];
}
start_owned = start.into();
} else {
start_owned = start.replace("\\ ", " ");
}
let full_path;
let start_path = PathBuf::from(&start_owned[..]);
if let Some(ref wd) = self.working_dir {
let mut fp = PathBuf::from(wd);
fp.push(start_owned.clone());
full_path = fp;
} else {
full_path = PathBuf::from(start_owned.clone());
}
if full_path.is_relative() {
return vec![];
}
let p;
let start_name;
let completing_dir;
match full_path.parent() {
// XXX non-unix separaor
Some(parent) if start != "" && !start_owned.ends_with("/") => {
p = PathBuf::from(parent);
start_name = full_path.file_name().unwrap().to_string_lossy().into_owned();
completing_dir = false;
}
_ => {
p = full_path.clone();
start_name = "".into();
completing_dir = start == "" || start.ends_with("/");
}
}
let read_dir = match p.read_dir() {
Ok(x) => x,
Err(_) => return vec![],
};
let mut matches = vec![];
for dir in read_dir {
let dir = match dir {
Ok(x) => x,
Err(_) => continue,
};
let file_name = dir.file_name();
let file_name = file_name.to_string_lossy();
if start_name == "" || file_name.starts_with(&*start_name) {
let mut a = start_path.clone();
if !a.is_absolute() {
a = PathBuf::new();
} else if !completing_dir && !a.pop() {
return vec![];
}
a.push(dir.file_name());
let mut s = a.to_string_lossy().into_owned();
if dir.path().is_dir() {
s = s + "/";
}
let mut b = PathBuf::from(start_owned.clone());
if !completing_dir {
b.pop();
}
b.push(s);
matches.push(b.to_string_lossy().to_owned().replace(" ", "\\ "));
}
}
matches
}
}
use std::io::{self, Stdout, stdout, stdin};
use termion::{TermRead, IntoRawMode, RawTerminal};
use super::*;
/// The default for `Context.word_fn`.
pub fn get_buffer_words(buf: &Buffer) -> Vec<(usize, usize)> {
let mut res = Vec::new();
let mut word_start = None;
let mut just_had_backslash = false;
for (i, &c) in buf.chars().enumerate() {
if c == '\\' {
just_had_backslash = true;
continue;
}
if let Some(start) = word_start {
if c == ' ' && !just_had_backslash {
res.push((start, i));
word_start = None;
}
} else {
if c != ' ' {
word_start = Some(i);
}
}
}
if let Some(start) = word_start {
res.push((start, buf.num_chars()));
}
res
}
pub struct Context {
pub history: Vec<Buffer>,
pub completer: Option<Box<Completer>>,
pub word_fn: Box<Fn(&Buffer) -> Vec<(usize, usize)>>,
}
impl Context {
pub fn new() -> Self {
Context {
history: vec![],
completer: None,
word_fn: Box::new(get_buffer_words),
}
}
/// Creates an `Editor` and feeds it keypresses from stdin until the line is entered.
/// 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>>(&mut self,
prompt: P,
mut handler: &mut EventHandler<RawTerminal<Stdout>>)
-> io::Result<String> {
let res = {
let stdin = stdin();
let stdout = stdout().into_raw_mode().unwrap();
let mut ed = try!(Editor::new(stdout, prompt.into(), self));
for c in stdin.keys() {
if try!(ed.handle_key(c.unwrap(), handler)) {
break;
}
}
Ok(ed.into())
};
self.revert_all_history();
res
}
pub fn revert_all_history(&mut self) {
for buf in &mut self.history {
buf.revert();
}
}
}
This diff is collapsed.
use std::io::Write;
use termion::{Key, TermWrite};
use Editor;
pub type EventHandler<'a, W> = FnMut(Event<W>) + 'a;
pub struct Event<'a, 'out: 'a, W: TermWrite + Write + 'a> {
pub editor: &'a mut Editor<'out, W>,
pub kind: EventKind,
}
impl<'a, 'out: 'a, W: TermWrite + Write + 'a> Event<'a, 'out, W> {
pub fn new(editor: &'a mut Editor<'out, W>, kind: EventKind) -> Self {
Event {
editor: editor,
kind: kind,
}
}
}
#[derive(Debug)]
pub enum EventKind {
/// Sent in `Editor.handle_key()`, before handling the key.
BeforeKey(Key),
/// Sent in `Editor.handle_key()`, after handling the key.
AfterKey(Key),
/// Sent in `Editor.complete()`, before processing the completion.
BeforeComplete,
}
extern crate termion;
mod event;
pub use event::*;
mod editor;
pub use editor::*;
mod complete;
pub use complete::*;
mod context;
pub use context::*;
mod buffer;
pub use buffer::*;
mod util;
#[cfg(test)]
mod test;
extern crate liner;
extern crate termion;
use std::mem::replace;
use std::env::current_dir;
use liner::{Context, CursorPosition, Event, EventKind, FilenameCompleter};
fn main() {
let mut con = Context::new();
loop {
let res = con.read_line("[prompt]$ ",
&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);
}
}
})
.unwrap();
if res.len() == 0 {
break;
}
con.history.push(res.into());
}
}
use super::*;
fn assert_cursor_pos(s: &str, cursor: usize, expected_pos: CursorPosition) {
let buf = Buffer::from(s.to_owned());
let words = buf.words();
let pos = CursorPosition::get(cursor, &words[..]);
assert!(expected_pos == pos,
format!("buffer: {:?}, cursor: {}, expected pos: {:?}, pos: {:?}",
s,
cursor,
expected_pos,
pos));
}
#[test]
fn test_get_cursor_position() {
use CursorPosition::*;
let tests = &[("hi", 0, OnWordLeftEdge(0)),
("hi", 1, InWord(0)),
("hi", 2, OnWordRightEdge(0)),
("abc abc", 4, InSpace(Some(0), Some(1))),
("abc abc", 5, OnWordLeftEdge(1)),
("abc abc", 6, InWord(1)),
("abc abc", 8, OnWordRightEdge(1)),
(" a", 0, InSpace(None, Some(0))),
("a ", 2, InSpace(Some(0), None))];
for t in tests {
assert_cursor_pos(t.0, t.1, t.2);
}
}
fn assert_buffer_actions(start: &str, expected: &str, actions: &[Action]) {
let mut buf = Buffer::from(start.to_owned());
for a in actions {
a.do_on(&mut buf);
}
assert_eq!(expected, String::from(buf));
}
#[test]
fn test_buffer_actions() {
assert_buffer_actions("",
"h",
&[Action::Insert {
start: 0,
text: "hi".chars().collect(),
},
Action::Remove {
start: 1,
text: ".".chars().collect(),
}]);
}
pub fn find_longest_common_prefix<T: Clone + Eq>(among: &[Vec<T>]) -> Option<Vec<T>> {
if among.len() == 0 {
return None;
} else if among.len() == 1 {
return Some(among[0].clone());
}
for s in among {
if s.len() == 0 {
return None;
}
}
let shortest_word = among.iter().min_by_key(|x| x.len()).unwrap();
let mut end = shortest_word.len();
while end > 0 {
let prefix = &shortest_word[..end];
let mut failed = false;
for s in among {
if !s.starts_with(prefix) {
failed = true;
break;
}
}
if !failed {
return Some(prefix.into());
}
end -= 1;
}
None
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment