Merge branch 'master' into get_helper
diff --git a/Cargo.toml b/Cargo.toml
index 9b940d8..7819abd 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -13,15 +13,19 @@
[badges]
travis-ci = { repository = "kkawakam/rustyline" }
appveyor = { repository = "kkawakam/rustyline" }
+maintenance = { status = "actively-developed" }
[dependencies]
+dirs = "1.0"
libc = "0.2"
log = "0.4"
unicode-width = "0.1"
unicode-segmentation = "1.0"
+memchr = "2.0"
[target.'cfg(unix)'.dependencies]
nix = "0.11"
+utf8parse = "0.1"
[target.'cfg(windows)'.dependencies]
winapi = { version = "0.3", features = ["consoleapi", "handleapi", "minwindef", "processenv", "winbase", "wincon", "winuser"] }
diff --git a/README.md b/README.md
index 99f345b..5f31310 100644
--- a/README.md
+++ b/README.md
@@ -69,8 +69,8 @@
- Unicode (UTF-8) (linenoise supports only ASCII)
- Word completion (linenoise supports only line completion)
- Filename completion
- - History search ([Searching for Commands in the History](http://cnswww.cns.cwru.edu/php/chet/readline/readline.html#SEC8))
- - Kill ring ([Killing Commands](http://cnswww.cns.cwru.edu/php/chet/readline/readline.html#IDX3))
+ - History search ([Searching for Commands in the History](http://tiswww.case.edu/php/chet/readline/readline.html#SEC8))
+ - Kill ring ([Killing Commands](http://tiswww.case.edu/php/chet/readline/readline.html#IDX3))
- Multi line mode (line wrapping)
- Word commands
- Hints
@@ -207,6 +207,7 @@
Library | Lang | OS | Term | Unicode | History | Completion | Keymap | Kill Ring | Undo | Colors | Hint/Auto suggest |
-------- | ---- | -- | ---- | ------- | ------- | ---------- | ------- | --------- | ---- | ------ | ----------------- |
+[Go-prompt][] | Go | Ux/win | ANSI | Yes | Yes | any | Emacs/prog | No | No | Yes | Yes |
[Haskeline][] | Haskell | Ux/Win | Any | Yes | Yes | any | Emacs/Vi/conf | Yes | Yes | ? | ? |
[Linenoise][] | C | Ux | ANSI | No | Yes | only line | Emacs | No | No | Ux | Yes |
[Linenoise-ng][] | C | Ux/Win | ANSI | Yes | Yes | only line | Emacs | Yes | No | ? | ? |
@@ -217,11 +218,12 @@
[Replxx][] | C/C++ | Ux/Win | ANSI | Yes | Yes | only line | Emacs | Yes | No | Ux/Win | Yes |
Rustyline | Rust | Ux/Win | ANSI | Yes | Yes | any | Emacs/Vi/bind | Yes | Yes | Ux/Win 10+ | Yes |
+[Go-prompt]: https://github.com/c-bata/go-prompt
[Haskeline]: https://github.com/judah/haskeline
[Linefeed]: https://github.com/murarth/linefeed
[Linenoise]: https://github.com/antirez/linenoise
[Linenoise-ng]: https://github.com/arangodb/linenoise-ng
-[Liner]: https://github.com/MovingtoMars/liner
+[Liner]: https://github.com/redox-os/liner
[Prompt-toolkit]: https://github.com/jonathanslenders/python-prompt-toolkit
[Rb-readline]: https://github.com/ConnorAtherton/rb-readline
[Replxx]: https://github.com/AmokHuginnsson/replxx
diff --git a/TODO.md b/TODO.md
index 948d42b..d4cd263 100644
--- a/TODO.md
+++ b/TODO.md
@@ -7,8 +7,8 @@
- [ ] bell-style
Color
-- [x] ANSI Colors & Windows 10+
-- [ ] ANSI Colors & Windows <10 (https://docs.rs/console/0.6.1/console/fn.strip_ansi_codes.html ?)
+- [X] ANSI Colors & Windows 10+
+- [ ] ANSI Colors & Windows <10 (https://docs.rs/console/0.6.1/console/fn.strip_ansi_codes.html ? https://github.com/mattn/go-colorable/blob/master/colorable_windows.go)
- [ ] Syntax highlighting
- [ ] clicolors spec (https://docs.rs/console/0.6.1/console/fn.colors_enabled.html)
@@ -17,7 +17,7 @@
- [ ] Windows escape/unescape space in path
- [ ] file completion & escape/unescape (#106)
- [ ] file completion & tilde (#62)
-- [ ] display versus replacement
+- [X] display versus replacement
- [ ] composite/alternate completer (if the current completer returns nothing, try the next one)
Config
@@ -31,7 +31,8 @@
- [ ] grapheme & input auto-wrap are buggy
Hints Callback
-- [x] Not implemented on windows
+- [X] Not implemented on windows
+- [ ] Do an implementation based on previous history
History
- [ ] Move to the history line n
@@ -42,9 +43,11 @@
Input
- [ ] Password input (#58)
-- [ ] quoted insert (#65)
+- [X] quoted insert (#65)
+- [ ] quoted TAB (`\t`) insert and width
- [ ] Overwrite mode (em-toggle-overwrite, vi-replace-mode, rl_insert_mode)
- [ ] Encoding
+- [ ] [Ctrl-][Alt-][Shift-]<Key> (#121)
Mouse
- [ ] Mouse support
@@ -53,10 +56,10 @@
- [ ] Move to the corresponding opening/closing bracket
Redo
-- [X] redo substitue
+- [X] redo substitute
Repeat
-- [x] dynamic prompt (arg: ?)
+- [X] dynamic prompt (arg: ?)
- [ ] transpose chars
Syntax
@@ -72,8 +75,9 @@
Unix
- [ ] Terminfo (https://github.com/Stebalien/term)
+- [ ] [ncurses](https://crates.io/crates/ncurses) alternative backend ?
Windows
- [ ] is_atty is not working with cygwin/msys (https://github.com/softprops/atty works but then how to make `enable_raw_mode` works ?)
- [X] UTF-16 surrogate pair
-- [ ] handle ansi escape code (https://docs.rs/console/0.6.1/console/fn.strip_ansi_codes.html ?)
+- [ ] handle ansi escape code (https://docs.rs/console/0.6.1/console/fn.strip_ansi_codes.html ? https://github.com/mattn/go-colorable/blob/master/colorable_windows.go)
diff --git a/examples/example.rs b/examples/example.rs
index 4e9c407..ce41cf4 100644
--- a/examples/example.rs
+++ b/examples/example.rs
@@ -2,37 +2,50 @@
extern crate rustyline;
use log::{Level, LevelFilter, Metadata, Record, SetLoggerError};
+use std::borrow::Cow::{self, Borrowed, Owned};
-use rustyline::completion::FilenameCompleter;
+use rustyline::completion::{Completer, FilenameCompleter, Pair};
use rustyline::error::ReadlineError;
+use rustyline::highlight::Highlighter;
use rustyline::hint::Hinter;
-use rustyline::{Cmd, CompletionType, Config, EditMode, Editor, KeyPress};
+use rustyline::{Cmd, CompletionType, Config, EditMode, Editor, Helper, KeyPress};
-// On unix platforms you can use ANSI escape sequences
-#[cfg(unix)]
-static PROMPT: &'static str = "\x1b[1;32m>>\x1b[0m ";
+static COLORED_PROMPT: &'static str = "\x1b[1;32m>>\x1b[0m ";
-// Windows consoles typically don't support ANSI escape sequences out
-// of the box
-#[cfg(windows)]
static PROMPT: &'static str = ">> ";
-struct Hints {}
+struct MyHelper(FilenameCompleter);
-impl Hinter for Hints {
+impl Completer for MyHelper {
+ type Candidate = Pair;
+
+ fn complete(&self, line: &str, pos: usize) -> Result<(usize, Vec<Pair>), ReadlineError> {
+ self.0.complete(line, pos)
+ }
+}
+
+impl Hinter for MyHelper {
fn hint(&self, line: &str, _pos: usize) -> Option<String> {
if line == "hello" {
- if cfg!(target_os = "windows") {
- Some(" World".to_owned())
- } else {
- Some(" \x1b[1mWorld\x1b[m".to_owned())
- }
+ Some(" World".to_owned())
} else {
None
}
}
}
+impl Highlighter for MyHelper {
+ fn highlight_prompt<'p>(&self, _: &str) -> Cow<'static, str> {
+ Borrowed(COLORED_PROMPT)
+ }
+
+ fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> {
+ Owned("\x1b[1m".to_owned() + hint + "\x1b[m")
+ }
+}
+
+impl Helper for MyHelper {}
+
fn main() {
init_logger().is_ok();
let config = Config::builder()
@@ -40,9 +53,9 @@
.completion_type(CompletionType::List)
.edit_mode(EditMode::Emacs)
.build();
- let c = FilenameCompleter::new();
+ let h = MyHelper(FilenameCompleter::new());
let mut rl = Editor::with_config(config);
- rl.set_helper(Some((c, Hints {})));
+ rl.set_helper(Some(h));
rl.bind_sequence(KeyPress::Meta('N'), Cmd::HistorySearchForward);
rl.bind_sequence(KeyPress::Meta('P'), Cmd::HistorySearchBackward);
if rl.load_history("history.txt").is_err() {
diff --git a/rustfmt.toml b/rustfmt.toml
index 34977e4..83697e3 100644
--- a/rustfmt.toml
+++ b/rustfmt.toml
@@ -1,3 +1,4 @@
wrap_comments = true
format_strings = true
error_on_unformatted = false
+reorder_impl_items = true
diff --git a/src/completion.rs b/src/completion.rs
index c6e65ed..ab7a020 100644
--- a/src/completion.rs
+++ b/src/completion.rs
@@ -1,25 +1,60 @@
//! Completion API
use std::borrow::Cow::{self, Borrowed, Owned};
-use std::collections::BTreeSet;
use std::fs;
use std::path::{self, Path};
use super::Result;
use line_buffer::LineBuffer;
+use memchr::memchr;
// TODO: let the implementers choose/find word boudaries ???
// (line, pos) is like (rl_line_buffer, rl_point) to make contextual completion
// ("select t.na| from tbl as t")
// TODO: make &self &mut self ???
+/// A completion candidate.
+pub trait Candidate {
+ /// Text to display when listing alternatives.
+ fn display(&self) -> &str;
+ /// Text to insert in line.
+ fn replacement(&self) -> &str;
+}
+
+impl Candidate for String {
+ fn display(&self) -> &str {
+ self.as_str()
+ }
+
+ fn replacement(&self) -> &str {
+ self.as_str()
+ }
+}
+
+pub struct Pair {
+ pub display: String,
+ pub replacement: String,
+}
+
+impl Candidate for Pair {
+ fn display(&self) -> &str {
+ self.display.as_str()
+ }
+
+ fn replacement(&self) -> &str {
+ self.replacement.as_str()
+ }
+}
+
/// To be called for tab-completion.
pub trait Completer {
+ type Candidate: Candidate;
+
/// Takes the currently edited `line` with the cursor `pos`ition and
/// returns the start position and the completion candidates for the
/// partial word to be completed.
///
/// ("ls /usr/loc", 11) => Ok((3, vec!["/usr/local/"]))
- fn complete(&self, line: &str, pos: usize) -> Result<(usize, Vec<String>)>;
+ fn complete(&self, line: &str, pos: usize) -> Result<(usize, Vec<Self::Candidate>)>;
/// Updates the edited `line` with the `elected` candidate.
fn update(&self, line: &mut LineBuffer, start: usize, elected: &str) {
let end = line.pos();
@@ -28,18 +63,24 @@
}
impl Completer for () {
+ type Candidate = String;
+
fn complete(&self, _line: &str, _pos: usize) -> Result<(usize, Vec<String>)> {
Ok((0, Vec::with_capacity(0)))
}
+
fn update(&self, _line: &mut LineBuffer, _start: usize, _elected: &str) {
unreachable!()
}
}
impl<'c, C: ?Sized + Completer> Completer for &'c C {
- fn complete(&self, line: &str, pos: usize) -> Result<(usize, Vec<String>)> {
+ type Candidate = C::Candidate;
+
+ fn complete(&self, line: &str, pos: usize) -> Result<(usize, Vec<Self::Candidate>)> {
(**self).complete(line, pos)
}
+
fn update(&self, line: &mut LineBuffer, start: usize, elected: &str) {
(**self).update(line, start, elected)
}
@@ -48,7 +89,9 @@
($($id: ident)*) => {
$(
impl<C: ?Sized + Completer> Completer for $id<C> {
- fn complete(&self, line: &str, pos: usize) -> Result<(usize, Vec<String>)> {
+ type Candidate = C::Candidate;
+
+ fn complete(&self, line: &str, pos: usize) -> Result<(usize, Vec<Self::Candidate>)> {
(**self).complete(line, pos)
}
fn update(&self, line: &mut LineBuffer, start: usize, elected: &str) {
@@ -65,34 +108,48 @@
/// A `Completer` for file and folder names.
pub struct FilenameCompleter {
- break_chars: BTreeSet<char>,
- double_quotes_special_chars: BTreeSet<char>,
+ break_chars: &'static [u8],
+ double_quotes_special_chars: &'static [u8],
}
+static DOUBLE_QUOTES_ESCAPE_CHAR: Option<char> = Some('\\');
+
// rl_basic_word_break_characters, rl_completer_word_break_characters
#[cfg(unix)]
-static DEFAULT_BREAK_CHARS: [char; 18] = [
- ' ', '\t', '\n', '"', '\\', '\'', '`', '@', '$', '>', '<', '=', ';', '|', '&', '{', '(', '\0',
+static DEFAULT_BREAK_CHARS: [u8; 18] = [
+ b' ', b'\t', b'\n', b'"', b'\\', b'\'', b'`', b'@', b'$', b'>', b'<', b'=', b';', b'|', b'&',
+ b'{', b'(', b'\0',
];
#[cfg(unix)]
static ESCAPE_CHAR: Option<char> = Some('\\');
// Remove \ to make file completion works on windows
#[cfg(windows)]
-static DEFAULT_BREAK_CHARS: [char; 17] = [
- ' ', '\t', '\n', '"', '\'', '`', '@', '$', '>', '<', '=', ';', '|', '&', '{', '(', '\0',
+static DEFAULT_BREAK_CHARS: [u8; 17] = [
+ b' ', b'\t', b'\n', b'"', b'\'', b'`', b'@', b'$', b'>', b'<', b'=', b';', b'|', b'&', b'{',
+ b'(', b'\0',
];
#[cfg(windows)]
static ESCAPE_CHAR: Option<char> = None;
// In double quotes, not all break_chars need to be escaped
// https://www.gnu.org/software/bash/manual/html_node/Double-Quotes.html
-static DOUBLE_QUOTES_SPECIAL_CHARS: [char; 4] = ['"', '$', '\\', '`'];
+#[cfg(unix)]
+static DOUBLE_QUOTES_SPECIAL_CHARS: [u8; 4] = [b'"', b'$', b'\\', b'`'];
+#[cfg(windows)]
+static DOUBLE_QUOTES_SPECIAL_CHARS: [u8; 1] = [b'"']; // TODO Validate: only '"' ?
+
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub enum Quote {
+ Double,
+ Single,
+ None,
+}
impl FilenameCompleter {
pub fn new() -> FilenameCompleter {
FilenameCompleter {
- break_chars: DEFAULT_BREAK_CHARS.iter().cloned().collect(),
- double_quotes_special_chars: DOUBLE_QUOTES_SPECIAL_CHARS.iter().cloned().collect(),
+ break_chars: &DEFAULT_BREAK_CHARS,
+ double_quotes_special_chars: &DOUBLE_QUOTES_SPECIAL_CHARS,
}
}
}
@@ -104,26 +161,35 @@
}
impl Completer for FilenameCompleter {
- fn complete(&self, line: &str, pos: usize) -> Result<(usize, Vec<String>)> {
- let (start, path, esc_char, break_chars) =
- if let Some((idx, double_quote)) = find_unclosed_quote(&line[..pos]) {
+ type Candidate = Pair;
+
+ fn complete(&self, line: &str, pos: usize) -> Result<(usize, Vec<Pair>)> {
+ let (start, path, esc_char, break_chars, quote) =
+ if let Some((idx, quote)) = find_unclosed_quote(&line[..pos]) {
let start = idx + 1;
- if double_quote {
+ if quote == Quote::Double {
(
start,
- unescape(&line[start..pos], ESCAPE_CHAR),
- ESCAPE_CHAR,
+ unescape(&line[start..pos], DOUBLE_QUOTES_ESCAPE_CHAR),
+ DOUBLE_QUOTES_ESCAPE_CHAR,
&self.double_quotes_special_chars,
+ quote,
)
} else {
- (start, Borrowed(&line[start..pos]), None, &self.break_chars)
+ (
+ start,
+ Borrowed(&line[start..pos]),
+ None,
+ &self.break_chars,
+ quote,
+ )
}
} else {
let (start, path) = extract_word(line, pos, ESCAPE_CHAR, &self.break_chars);
let path = unescape(path, ESCAPE_CHAR);
- (start, path, ESCAPE_CHAR, &self.break_chars)
+ (start, path, ESCAPE_CHAR, &self.break_chars, Quote::None)
};
- let matches = try!(filename_complete(&path, esc_char, break_chars));
+ let matches = try!(filename_complete(&path, esc_char, break_chars, quote));
Ok((start, matches))
}
}
@@ -134,15 +200,20 @@
return Borrowed(input);
}
let esc_char = esc_char.unwrap();
- let n = input.chars().filter(|&c| c == esc_char).count();
- if n == 0 {
+ if !input.chars().any(|c| c == esc_char) {
return Borrowed(input);
}
- let mut result = String::with_capacity(input.len() - n);
+ let mut result = String::with_capacity(input.len());
let mut chars = input.chars();
while let Some(ch) = chars.next() {
if ch == esc_char {
if let Some(ch) = chars.next() {
+ if cfg!(windows) && ch != '"' {
+ // TODO Validate: only '"' ?
+ result.push(esc_char);
+ }
+ result.push(ch);
+ } else if cfg!(windows) {
result.push(ch);
}
} else {
@@ -155,19 +226,34 @@
/// Escape any `break_chars` in `input` string with `esc_char`.
/// For example, '/User Information' becomes '/User\ Information'
/// when space is a breaking char and '\\' the escape char.
-pub fn escape(input: String, esc_char: Option<char>, break_chars: &BTreeSet<char>) -> String {
+pub fn escape(
+ mut input: String,
+ esc_char: Option<char>,
+ break_chars: &[u8],
+ quote: Quote,
+) -> String {
+ if quote == Quote::Single {
+ return input; // no escape in single quotes
+ }
+ let n = input
+ .bytes()
+ .filter(|b| memchr(*b, break_chars).is_some())
+ .count();
+ if n == 0 {
+ return input; // no need to escape
+ }
if esc_char.is_none() {
+ if cfg!(windows) && quote == Quote::None {
+ input.insert(0, '"'); // force double quote
+ return input;
+ }
return input;
}
let esc_char = esc_char.unwrap();
- let n = input.chars().filter(|c| break_chars.contains(c)).count();
- if n == 0 {
- return input;
- }
let mut result = String::with_capacity(input.len() + n);
for c in input.chars() {
- if break_chars.contains(&c) {
+ if c.is_ascii() && memchr(c as u8, break_chars).is_some() {
result.push(esc_char);
}
result.push(c);
@@ -178,9 +264,11 @@
fn filename_complete(
path: &str,
esc_char: Option<char>,
- break_chars: &BTreeSet<char>,
-) -> Result<Vec<String>> {
- use std::env::{current_dir, home_dir};
+ break_chars: &[u8],
+ quote: Quote,
+) -> Result<Vec<Pair>> {
+ use dirs::home_dir;
+ use std::env::current_dir;
let sep = path::MAIN_SEPARATOR;
let (dir_name, file_name) = match path.rfind(sep) {
@@ -210,16 +298,21 @@
dir_path.to_path_buf()
};
- let mut entries: Vec<String> = Vec::new();
+ let mut entries: Vec<Pair> = Vec::new();
for entry in try!(dir.read_dir()) {
let entry = try!(entry);
if let Some(s) = entry.file_name().to_str() {
if s.starts_with(file_name) {
- let mut path = String::from(dir_name) + s;
- if try!(fs::metadata(entry.path())).is_dir() {
- path.push(sep);
- }
- entries.push(escape(path, esc_char, break_chars));
+ if let Ok(metadata) = fs::metadata(entry.path()) {
+ let mut path = String::from(dir_name) + s;
+ if metadata.is_dir() {
+ path.push(sep);
+ }
+ entries.push(Pair {
+ display: String::from(s),
+ replacement: escape(path, esc_char, break_chars, quote),
+ });
+ } // else ignore PermissionDenied
}
}
}
@@ -234,7 +327,7 @@
line: &'l str,
pos: usize,
esc_char: Option<char>,
- break_chars: &BTreeSet<char>,
+ break_chars: &[u8],
) -> (usize, &'l str) {
let line = &line[..pos];
if line.is_empty() {
@@ -251,7 +344,7 @@
break;
}
}
- if break_chars.contains(&c) {
+ if c.is_ascii() && memchr(c as u8, break_chars).is_some() {
start = Some(i + c.len_utf8());
if esc_char.is_none() {
break;
@@ -265,17 +358,17 @@
}
}
-pub fn longest_common_prefix(candidates: &[String]) -> Option<&str> {
+pub fn longest_common_prefix<C: Candidate>(candidates: &[C]) -> Option<&str> {
if candidates.is_empty() {
return None;
} else if candidates.len() == 1 {
- return Some(&candidates[0]);
+ return Some(&candidates[0].replacement());
}
let mut longest_common_prefix = 0;
'o: loop {
for (i, c1) in candidates.iter().enumerate().take(candidates.len() - 1) {
- let b1 = c1.as_bytes();
- let b2 = candidates[i + 1].as_bytes();
+ let b1 = c1.replacement().as_bytes();
+ let b2 = candidates[i + 1].replacement().as_bytes();
if b1.len() <= longest_common_prefix
|| b2.len() <= longest_common_prefix
|| b1[longest_common_prefix] != b2[longest_common_prefix]
@@ -285,13 +378,14 @@
}
longest_common_prefix += 1;
}
- while !candidates[0].is_char_boundary(longest_common_prefix) {
+ let candidate = candidates[0].replacement();
+ while !candidate.is_char_boundary(longest_common_prefix) {
longest_common_prefix -= 1;
}
if longest_common_prefix == 0 {
return None;
}
- Some(&candidates[0][0..longest_common_prefix])
+ Some(&candidate[0..longest_common_prefix])
}
#[derive(PartialEq)]
@@ -306,7 +400,7 @@
/// try to find an unclosed single/double quote in `s`.
/// Return `None` if no unclosed quote is found.
/// Return the unclosed quote position and if it is a double quote.
-fn find_unclosed_quote(s: &str) -> Option<(usize, bool)> {
+fn find_unclosed_quote(s: &str) -> Option<(usize, Quote)> {
let char_indices = s.char_indices();
let mut mode = ScanMode::Normal;
let mut quote_index = 0;
@@ -316,6 +410,7 @@
if char == '"' {
mode = ScanMode::Normal;
} else if char == '\\' {
+ // both windows and unix support escape in double quote
mode = ScanMode::EscapeInDoubleQuote;
}
}
@@ -343,19 +438,19 @@
}
};
}
- if ScanMode::DoubleQuote == mode || ScanMode::SingleQuote == mode {
- return Some((quote_index, ScanMode::DoubleQuote == mode));
+ if ScanMode::DoubleQuote == mode || ScanMode::EscapeInDoubleQuote == mode {
+ return Some((quote_index, Quote::Double));
+ } else if ScanMode::SingleQuote == mode {
+ return Some((quote_index, Quote::Single));
}
None
}
#[cfg(test)]
mod tests {
- use std::collections::BTreeSet;
-
#[test]
pub fn extract_word() {
- let break_chars: BTreeSet<char> = super::DEFAULT_BREAK_CHARS.iter().cloned().collect();
+ let break_chars: &[u8] = &super::DEFAULT_BREAK_CHARS;
let line = "ls '/usr/local/b";
assert_eq!(
(4, "/usr/local/b"),
@@ -373,22 +468,31 @@
use std::borrow::Cow::{self, Borrowed, Owned};
let input = "/usr/local/b";
assert_eq!(Borrowed(input), super::unescape(input, Some('\\')));
- let input = "/User\\ Information";
- let result: Cow<str> = Owned(String::from("/User Information"));
- assert_eq!(result, super::unescape(input, Some('\\')));
+ if cfg!(windows) {
+ let input = "c:\\users\\All Users\\";
+ let result: Cow<str> = Borrowed(input);
+ assert_eq!(result, super::unescape(input, Some('\\')));
+ } else {
+ let input = "/User\\ Information";
+ let result: Cow<str> = Owned(String::from("/User Information"));
+ assert_eq!(result, super::unescape(input, Some('\\')));
+ }
}
#[test]
pub fn escape() {
- let break_chars: BTreeSet<char> = super::DEFAULT_BREAK_CHARS.iter().cloned().collect();
+ let break_chars: &[u8] = &super::DEFAULT_BREAK_CHARS;
let input = String::from("/usr/local/b");
assert_eq!(
input.clone(),
- super::escape(input, Some('\\'), &break_chars)
+ super::escape(input, Some('\\'), &break_chars, super::Quote::None)
);
let input = String::from("/User Information");
let result = String::from("/User\\ Information");
- assert_eq!(result, super::escape(input, Some('\\'), &break_chars));
+ assert_eq!(
+ result,
+ super::escape(input, Some('\\'), &break_chars, super::Quote::None)
+ );
}
#[test]
@@ -430,12 +534,16 @@
pub fn find_unclosed_quote() {
assert_eq!(None, super::find_unclosed_quote("ls /etc"));
assert_eq!(
- Some((3, true)),
+ Some((3, super::Quote::Double)),
super::find_unclosed_quote("ls \"User Information")
);
assert_eq!(
None,
super::find_unclosed_quote("ls \"/User Information\" /etc")
);
+ assert_eq!(
+ Some((0, super::Quote::Double)),
+ super::find_unclosed_quote("\"c:\\users\\All Users\\")
+ )
}
}
diff --git a/src/config.rs b/src/config.rs
index d1dbc42..86c04dc 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -20,6 +20,8 @@
/// If true, each nonblank line returned by `readline` will be
/// automatically added to the history.
auto_add_history: bool,
+ /// if colors should be enabled.
+ color_mode: ColorMode,
}
impl Config {
@@ -70,6 +72,13 @@
pub fn auto_add_history(&self) -> bool {
self.auto_add_history
}
+
+ /// Tell if colors should be enabled.
+ ///
+ /// By default, they are except if stdout is not a tty.
+ pub fn color_mode(&self) -> ColorMode {
+ self.color_mode
+ }
}
impl Default for Config {
@@ -83,6 +92,7 @@
keyseq_timeout: -1,
edit_mode: EditMode::Emacs,
auto_add_history: false,
+ color_mode: ColorMode::Enabled,
}
}
}
@@ -111,6 +121,14 @@
Vi,
}
+/// Colorization mode
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum ColorMode {
+ Enabled,
+ Forced,
+ Disabled,
+}
+
/// Configuration builder
#[derive(Debug, Default)]
pub struct Builder {
@@ -193,6 +211,14 @@
self
}
+ /// Forces colorization on or off.
+ ///
+ /// By default, colorization is on except if stdout is not a tty.
+ pub fn color_mode(mut self, color_mode: ColorMode) -> Builder {
+ self.p.color_mode = color_mode;
+ self
+ }
+
pub fn build(self) -> Config {
self.p
}
diff --git a/src/edit.rs b/src/edit.rs
index 7dae766..f9fcca8 100644
--- a/src/edit.rs
+++ b/src/edit.rs
@@ -7,6 +7,7 @@
use unicode_width::UnicodeWidthChar;
use super::Result;
+use highlight::Highlighter;
use hint::Hinter;
use history::{Direction, History};
use keymap::{Anchor, At, CharSearch, Cmd, Movement, RepeatCount, Word};
@@ -30,6 +31,7 @@
byte_buffer: [u8; 4],
pub changes: Rc<RefCell<Changeset>>, // changes to line, for undo/redo
pub hinter: Option<&'out Hinter>,
+ pub highlighter: Option<&'out Highlighter>,
}
impl<'out, 'prompt> State<'out, 'prompt> {
@@ -38,6 +40,7 @@
prompt: &'prompt str,
history_index: usize,
hinter: Option<&'out Hinter>,
+ highlighter: Option<&'out Highlighter>,
) -> State<'out, 'prompt> {
let capacity = MAX_LINE;
let prompt_size = out.calculate_position(prompt, Position::default());
@@ -53,6 +56,7 @@
byte_buffer: [0; 4],
changes: Rc::new(RefCell::new(Changeset::new())),
hinter,
+ highlighter,
}
}
@@ -80,6 +84,7 @@
self.saved_line_for_history
.update(self.line.as_str(), self.line.pos());
}
+
pub fn restore(&mut self) {
self.line.update(
self.saved_line_for_history.as_str(),
@@ -95,7 +100,16 @@
if self.cursor == cursor {
return Ok(());
}
- try!(self.out.move_cursor(self.cursor, cursor));
+ if self.highlighter.map_or(false, |h| {
+ self.line
+ .grapheme_at_cursor()
+ .map_or(false, |s| h.highlight_char(s))
+ }) {
+ let prompt_size = self.prompt_size;
+ try!(self.refresh(self.prompt, prompt_size, None));
+ } else {
+ try!(self.out.move_cursor(self.cursor, cursor));
+ }
self.cursor = cursor;
Ok(())
}
@@ -108,6 +122,7 @@
hint,
self.cursor.row,
self.old_rows,
+ self.highlighter,
));
self.cursor = cursor;
@@ -136,12 +151,15 @@
let hint = self.hint();
self.refresh(prompt, prompt_size, hint)
}
+
fn doing_insert(&mut self) {
self.changes.borrow_mut().begin();
}
+
fn done_inserting(&mut self) {
self.changes.borrow_mut().end();
}
+
fn last_insert(&self) -> Option<String> {
self.changes.borrow().last_insert()
}
@@ -171,7 +189,8 @@
let hint = self.hint();
if n == 1
&& self.cursor.col + ch.width().unwrap_or(0) < self.out.get_columns()
- && hint.is_none()
+ && hint.is_none() // TODO refresh only current line
+ && !self.highlighter.map_or(true, |h| h.highlight_char(ch.encode_utf8(&mut self.byte_buffer)))
{
// Avoid a full update of the line in the trivial case.
let cursor = self
@@ -486,6 +505,7 @@
byte_buffer: [0; 4],
changes: Rc::new(RefCell::new(Changeset::new())),
hinter: None,
+ highlighter: None,
}
}
diff --git a/src/error.rs b/src/error.rs
index d9cfdb8..f7c1abd 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -20,7 +20,7 @@
Interrupted,
/// Chars Error
#[cfg(unix)]
- Char(str::Utf8Error),
+ Utf8Error,
/// Unix Error from syscall
#[cfg(unix)]
Errno(nix::Error),
@@ -37,7 +37,7 @@
ReadlineError::Eof => write!(f, "EOF"),
ReadlineError::Interrupted => write!(f, "Interrupted"),
#[cfg(unix)]
- ReadlineError::Char(ref err) => err.fmt(f),
+ ReadlineError::Utf8Error => write!(f, "invalid utf-8: corrupt contents"),
#[cfg(unix)]
ReadlineError::Errno(ref err) => err.fmt(f),
#[cfg(windows)]
@@ -55,7 +55,7 @@
ReadlineError::Eof => "EOF",
ReadlineError::Interrupted => "Interrupted",
#[cfg(unix)]
- ReadlineError::Char(ref err) => err.description(),
+ ReadlineError::Utf8Error => "invalid utf-8: corrupt contents",
#[cfg(unix)]
ReadlineError::Errno(ref err) => err.description(),
#[cfg(windows)]
@@ -79,13 +79,6 @@
}
}
-#[cfg(unix)]
-impl From<str::Utf8Error> for ReadlineError {
- fn from(err: str::Utf8Error) -> ReadlineError {
- ReadlineError::Char(err)
- }
-}
-
#[cfg(windows)]
impl From<char::DecodeUtf16Error> for ReadlineError {
fn from(err: char::DecodeUtf16Error) -> ReadlineError {
diff --git a/src/highlight.rs b/src/highlight.rs
new file mode 100644
index 0000000..04bc042
--- /dev/null
+++ b/src/highlight.rs
@@ -0,0 +1,59 @@
+use config::CompletionType;
+///! Syntax highlighting
+use std::borrow::Cow::{self, Borrowed};
+
+/// Syntax highlighter with [ansi color](https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters).
+/// Rustyline will try to handle escape sequence for ansi color on windows
+/// when not supported natively (windows <10).
+///
+/// Currently, the highlighted version *must* have the same display width as
+/// the original input.
+pub trait Highlighter {
+ /// Takes the currently edited `line` with the cursor `pos`ition and
+ /// returns the highlighted version (with ANSI color).
+ ///
+ /// For example, you can implement
+ /// [blink-matching-paren](https://www.gnu.org/software/bash/manual/html_node/Readline-Init-File-Syntax.html).
+ fn highlight<'l>(&self, line: &'l str, pos: usize) -> Cow<'l, str> {
+ let _ = pos;
+ Borrowed(line)
+ }
+ /// Takes the `prompt` and
+ /// returns the highlighted version (with ANSI color).
+ fn highlight_prompt<'p>(&self, prompt: &'p str) -> Cow<'p, str> {
+ Borrowed(prompt)
+ }
+ /// Takes the dynamic `prompt` and
+ /// returns the highlighted version (with ANSI color).
+ fn highlight_dynamic_prompt<'p>(&self, prompt: &'p str) -> Cow<'p, str> {
+ Borrowed(prompt)
+ }
+ /// Takes the `hint` and
+ /// returns the highlighted version (with ANSI color).
+ fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> {
+ Borrowed(hint)
+ }
+ /// Takes the completion `canditate` and
+ /// returns the highlighted version (with ANSI color).
+ ///
+ /// Currently, used only with `CompletionType::List`.
+ fn highlight_candidate<'c>(
+ &self,
+ candidate: &'c str,
+ completion: CompletionType,
+ ) -> Cow<'c, str> {
+ let _ = completion;
+ Borrowed(candidate)
+ }
+ /// Tells if the `ch`ar needs to be highlighted when typed or when cursor
+ /// is moved under.
+ ///
+ /// Used to optimize refresh when a character is inserted or the cursor is
+ /// moved.
+ fn highlight_char(&self, grapheme: &str) -> bool {
+ let _ = grapheme;
+ false
+ }
+}
+
+impl Highlighter for () {}
diff --git a/src/history.rs b/src/history.rs
index d4e0c94..76a5bcf 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -32,12 +32,13 @@
pub fn new() -> History {
Self::with_config(Config::default())
}
+
pub fn with_config(config: Config) -> History {
History {
entries: VecDeque::new(),
max_len: config.max_history_size(),
- ignore_space: config.history_duplicates() == HistoryDuplicates::IgnoreConsecutive,
- ignore_dups: config.history_ignore_space(),
+ ignore_space: config.history_ignore_space(),
+ ignore_dups: config.history_duplicates() == HistoryDuplicates::IgnoreConsecutive,
}
}
@@ -57,12 +58,11 @@
return false;
}
if line.as_ref().is_empty()
- || (self.ignore_space
- && line
- .as_ref()
- .chars()
- .next()
- .map_or(true, |c| c.is_whitespace()))
+ || (self.ignore_space && line
+ .as_ref()
+ .chars()
+ .next()
+ .map_or(true, |c| c.is_whitespace()))
{
return false;
}
@@ -84,6 +84,7 @@
pub fn len(&self) -> usize {
self.entries.len()
}
+
/// Return true if the history has no entry.
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
@@ -214,8 +215,8 @@
}
impl<'a> IntoIterator for &'a History {
- type Item = &'a String;
type IntoIter = Iter<'a>;
+ type Item = &'a String;
fn into_iter(self) -> Iter<'a> {
self.iter()
diff --git a/src/keymap.rs b/src/keymap.rs
index 3652493..877a12d 100644
--- a/src/keymap.rs
+++ b/src/keymap.rs
@@ -111,6 +111,7 @@
_ => false,
}
}
+
fn is_repeatable(&self) -> bool {
match *self {
Cmd::Move(_) => true,
@@ -205,8 +206,8 @@
}
impl CharSearch {
- fn opposite(&self) -> CharSearch {
- match *self {
+ fn opposite(self) -> CharSearch {
+ match self {
CharSearch::Forward(c) => CharSearch::Backward(c),
CharSearch::ForwardBefore(c) => CharSearch::BackwardAfter(c),
CharSearch::Backward(c) => CharSearch::Forward(c),
diff --git a/src/kill_ring.rs b/src/kill_ring.rs
index 15b6649..b5d8778 100644
--- a/src/kill_ring.rs
+++ b/src/kill_ring.rs
@@ -109,6 +109,7 @@
fn start_killing(&mut self) {
self.killing = true;
}
+
fn delete(&mut self, _: usize, string: &str, dir: Direction) {
if !self.killing {
return;
@@ -119,6 +120,7 @@
};
self.kill(string, mode);
}
+
fn stop_killing(&mut self) {
self.killing = false;
}
diff --git a/src/lib.rs b/src/lib.rs
index 1221361..6c61a05 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -17,13 +17,17 @@
//! ```
#![allow(unknown_lints)]
+extern crate dirs;
extern crate libc;
#[macro_use]
extern crate log;
+extern crate memchr;
#[cfg(unix)]
extern crate nix;
extern crate unicode_segmentation;
extern crate unicode_width;
+#[cfg(unix)]
+extern crate utf8parse;
#[cfg(windows)]
extern crate winapi;
@@ -32,6 +36,7 @@
mod consts;
mod edit;
pub mod error;
+pub mod highlight;
pub mod hint;
pub mod history;
mod keymap;
@@ -51,10 +56,11 @@
use tty::{RawMode, RawReader, Renderer, Term, Terminal};
-use completion::{longest_common_prefix, Completer};
+use completion::{longest_common_prefix, Candidate, Completer};
pub use config::{CompletionType, Config, EditMode, HistoryDuplicates};
pub use consts::KeyPress;
use edit::State;
+use highlight::Highlighter;
use hint::Hinter;
use history::{Direction, History};
pub use keymap::{Anchor, At, CharSearch, Cmd, Movement, RepeatCount, Word};
@@ -71,6 +77,7 @@
s: &mut State,
input_state: &mut InputState,
completer: &C,
+ highlighter: Option<&Highlighter>,
config: &Config,
) -> Result<Option<Cmd>> {
// get a list of completions
@@ -89,7 +96,14 @@
loop {
// Show completion or original buffer
if i < candidates.len() {
- completer.update(&mut s.line, start, &candidates[i]);
+ let candidate = candidates[i].replacement();
+ // TODO we can't highlight the line buffer directly
+ /*let candidate = if let Some(highlighter) = s.highlighter {
+ highlighter.highlight_candidate(candidate, CompletionType::Circular)
+ } else {
+ Borrowed(candidate)
+ };*/
+ completer.update(&mut s.line, start, candidate);
try!(s.refresh_line());
} else {
// Restore current edited line
@@ -122,18 +136,19 @@
}
Ok(Some(cmd))
} else if CompletionType::List == config.completion_type() {
- // beep if ambiguous
- if candidates.len() > 1 {
- try!(s.out.beep());
- }
if let Some(lcp) = longest_common_prefix(&candidates) {
- // if we can extend the item, extend it and return to main loop
+ // if we can extend the item, extend it
if lcp.len() > s.line.pos() - start {
completer.update(&mut s.line, start, lcp);
try!(s.refresh_line());
- return Ok(None);
}
}
+ // beep if ambiguous
+ if candidates.len() > 1 {
+ try!(s.out.beep());
+ } else {
+ return Ok(None);
+ }
// we can't complete any further, wait for second tab
let mut cmd = try!(s.next_cmd(input_state, rdr, true));
// if any character other than tab, pass it to the main loop
@@ -165,7 +180,7 @@
true
};
if show_completions {
- page_completions(rdr, s, input_state, &candidates)
+ page_completions(rdr, s, input_state, highlighter, &candidates)
} else {
try!(s.refresh_line());
Ok(None)
@@ -175,11 +190,12 @@
}
}
-fn page_completions<R: RawReader>(
+fn page_completions<R: RawReader, C: Candidate>(
rdr: &mut R,
s: &mut State,
input_state: &mut InputState,
- candidates: &[String],
+ highlighter: Option<&Highlighter>,
+ candidates: &[C],
) -> Result<Option<Cmd>> {
use std::cmp;
@@ -189,9 +205,10 @@
cols,
candidates
.into_iter()
- .map(|s| s.as_str().width())
+ .map(|s| s.display().width())
.max()
- .unwrap() + min_col_pad,
+ .unwrap()
+ + min_col_pad,
);
let num_cols = cols / max_width;
@@ -231,9 +248,13 @@
for col in 0..num_cols {
let i = (col * num_rows) + row;
if i < candidates.len() {
- let candidate = &candidates[i];
- ab.push_str(candidate);
- let width = candidate.as_str().width();
+ let candidate = &candidates[i].display();
+ let width = candidate.width();
+ if let Some(highlighter) = highlighter {
+ ab.push_str(&highlighter.highlight_candidate(candidate, CompletionType::List));
+ } else {
+ ab.push_str(candidate);
+ }
if ((col + 1) * num_rows) + row < candidates.len() {
for _ in width..max_width {
ab.push(' ');
@@ -337,20 +358,30 @@
/// Handles reading and editting the readline buffer.
/// It will also handle special inputs in an appropriate fashion
/// (e.g., C-c will exit readline)
-#[allow(let_unit_value)]
fn readline_edit<H: Helper>(
prompt: &str,
initial: Option<(&str, &str)>,
editor: &mut Editor<H>,
original_mode: &tty::Mode,
) -> Result<String> {
- let completer = editor.helper.as_ref().map(|h| h.completer());
- let hinter = editor.helper.as_ref().map(|h| h.hinter() as &Hinter);
+ let completer = editor.helper.as_ref();
+ let hinter = editor.helper.as_ref().map(|h| h as &Hinter);
+ let highlighter = if editor.term.colors_enabled() {
+ editor.helper.as_ref().map(|h| h as &Highlighter)
+ } else {
+ None
+ };
let mut stdout = editor.term.create_writer();
editor.reset_kill_ring(); // TODO recreate a new kill ring vs Arc<Mutex<KillRing>>
- let mut s = State::new(&mut stdout, prompt, editor.history.len(), hinter);
+ let mut s = State::new(
+ &mut stdout,
+ prompt,
+ editor.history.len(),
+ hinter,
+ highlighter,
+ );
let mut input_state = InputState::new(&editor.config, Arc::clone(&editor.custom_bindings));
s.line.set_delete_listener(editor.kill_ring.clone());
@@ -380,6 +411,7 @@
&mut s,
&mut input_state,
completer.unwrap(),
+ highlighter,
&editor.config,
));
if next.is_some() {
@@ -574,6 +606,9 @@
}
}
}
+ if cfg!(windows) {
+ let _ = original_mode; // silent warning
+ }
Ok(s.line.into_string())
}
@@ -618,37 +653,17 @@
/// Syntax specific helper.
///
-/// TODO Tokenizer/parser used for both completion, suggestion, highlighting
-pub trait Helper {
- type Completer: Completer;
- type Hinter: Hinter;
-
- fn completer(&self) -> &Self::Completer;
- fn hinter(&self) -> &Self::Hinter;
+/// TODO Tokenizer/parser used for both completion, suggestion, highlighting.
+/// (parse current line once)
+pub trait Helper
+where
+ Self: Completer,
+ Self: Hinter,
+ Self: Highlighter,
+{
}
-impl<C: Completer, H: Hinter> Helper for (C, H) {
- type Completer = C;
- type Hinter = H;
-
- fn completer(&self) -> &C {
- &self.0
- }
- fn hinter(&self) -> &H {
- &self.1
- }
-}
-impl<C: Completer> Helper for C {
- type Completer = C;
- type Hinter = ();
-
- fn completer(&self) -> &C {
- self
- }
- fn hinter(&self) -> &() {
- &()
- }
-}
+impl Helper for () {}
/// Line editor
pub struct Editor<H: Helper> {
@@ -660,6 +675,7 @@
custom_bindings: Arc<RwLock<HashMap<KeyPress, Cmd>>>,
}
+#[allow(new_without_default)]
impl<H: Helper> Editor<H> {
/// Create an editor with the default configuration
pub fn new() -> Editor<H> {
@@ -668,7 +684,7 @@
/// Create an editor with a specific configuration.
pub fn with_config(config: Config) -> Editor<H> {
- let term = Terminal::new();
+ let term = Terminal::new(config.color_mode());
Editor {
term,
history: History::with_config(config),
@@ -688,6 +704,7 @@
pub fn readline(&mut self, prompt: &str) -> Result<String> {
self.readline_with(prompt, None)
}
+
/// This function behaves in the exact same manner as `readline`, except
/// that it pre-populates the input area.
///
@@ -721,24 +738,29 @@
pub fn load_history<P: AsRef<Path> + ?Sized>(&mut self, path: &P) -> Result<()> {
self.history.load(path)
}
+
/// Save the history in the specified file.
pub fn save_history<P: AsRef<Path> + ?Sized>(&self, path: &P) -> Result<()> {
self.history.save(path)
}
+
/// Add a new entry in the history.
pub fn add_history_entry<S: AsRef<str> + Into<String>>(&mut self, line: S) -> bool {
self.history.add(line)
}
+
/// Clear history.
pub fn clear_history(&mut self) {
self.history.clear()
}
+
/// Return a mutable reference to the history object.
- pub fn get_history(&mut self) -> &mut History {
+ pub fn history_mut(&mut self) -> &mut History {
&mut self.history
}
+
/// Return an immutable reference to the history object.
- pub fn get_history_const(&self) -> &History {
+ pub fn history(&self) -> &History {
&self.history
}
@@ -749,25 +771,21 @@
}
/// Return a mutable reference to the helper.
- pub fn get_helper(&mut self) -> Option<&mut H> {
+ pub fn helper_mut(&mut self) -> Option<&mut H> {
self.helper.as_mut()
}
/// Return an immutable reference to the helper.
- pub fn get_helper_const(&self) -> Option<&H> {
+ pub fn helper(&self) -> Option<&H> {
self.helper.as_ref()
}
- #[deprecated(since = "2.0.0", note = "Use set_helper instead")]
- pub fn set_completer(&mut self, completer: Option<H>) {
- self.helper = completer;
- }
-
/// Bind a sequence to a command.
pub fn bind_sequence(&mut self, key_seq: KeyPress, cmd: Cmd) -> Option<Cmd> {
let mut bindings = self.custom_bindings.write().unwrap();
bindings.insert(key_seq, cmd)
}
+
/// Remove a binding for the given sequence.
pub fn unbind_sequence(&mut self, key_seq: KeyPress) -> Option<Cmd> {
let mut bindings = self.custom_bindings.write().unwrap();
diff --git a/src/line_buffer.rs b/src/line_buffer.rs
index 7c16bb1..e17fd62 100644
--- a/src/line_buffer.rs
+++ b/src/line_buffer.rs
@@ -10,7 +10,7 @@
use unicode_segmentation::UnicodeSegmentation;
/// Maximum buffer size for the line read
-pub static MAX_LINE: usize = 4096;
+pub(crate) static MAX_LINE: usize = 4096;
/// Word's case change
#[derive(Clone, Copy)]
@@ -22,7 +22,7 @@
/// Delete (kill) direction
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum Direction {
+pub(crate) enum Direction {
Forward,
Backward,
}
@@ -34,14 +34,14 @@
}
/// Listener to be notified when some text is deleted.
-pub trait DeleteListener {
+pub(crate) trait DeleteListener {
fn start_killing(&mut self);
fn delete(&mut self, idx: usize, string: &str, dir: Direction);
fn stop_killing(&mut self);
}
/// Listener to be notified when the line is modified.
-pub trait ChangeListener: DeleteListener {
+pub(crate) trait ChangeListener: DeleteListener {
fn insert_char(&mut self, idx: usize, c: char);
fn insert_str(&mut self, idx: usize, string: &str);
fn replace(&mut self, idx: usize, old: &str, new: &str);
@@ -78,7 +78,11 @@
}
#[cfg(test)]
- pub fn init(line: &str, pos: usize, cl: Option<Rc<RefCell<ChangeListener>>>) -> LineBuffer {
+ pub(crate) fn init(
+ line: &str,
+ pos: usize,
+ cl: Option<Rc<RefCell<ChangeListener>>>,
+ ) -> LineBuffer {
let mut lb = Self::with_capacity(MAX_LINE);
assert!(lb.insert_str(0, line));
lb.set_pos(pos);
@@ -86,16 +90,15 @@
lb
}
- pub fn set_delete_listener(&mut self, dl: Arc<Mutex<DeleteListener>>) {
+ pub(crate) fn set_delete_listener(&mut self, dl: Arc<Mutex<DeleteListener>>) {
self.dl = Some(dl);
}
- pub fn remove_delete_listener(&mut self) {
- self.dl = None;
- }
- pub fn set_change_listener(&mut self, dl: Rc<RefCell<ChangeListener>>) {
+
+ pub(crate) fn set_change_listener(&mut self, dl: Rc<RefCell<ChangeListener>>) {
self.cl = Some(dl);
}
- pub fn remove_change_listener(&mut self) {
+
+ pub(crate) fn remove_change_listener(&mut self) {
self.cl = None;
}
@@ -113,6 +116,7 @@
pub fn pos(&self) -> usize {
self.pos
}
+
/// Set cursor position (byte position)
pub fn set_pos(&mut self, pos: usize) {
assert!(pos <= self.buf.len());
@@ -123,6 +127,7 @@
pub fn len(&self) -> usize {
self.buf.len()
}
+
/// Returns `true` if this buffer has a length of zero.
pub fn is_empty(&self) -> bool {
self.buf.is_empty()
@@ -148,7 +153,7 @@
}
/// Returns the character at current cursor position.
- fn grapheme_at_cursor(&self) -> Option<&str> {
+ pub(crate) fn grapheme_at_cursor(&self) -> Option<&str> {
if self.pos == self.buf.len() {
None
} else {
@@ -168,6 +173,7 @@
.last()
.map(|(i, s)| i + self.pos + s.len())
}
+
/// Returns the position of the character just before the current cursor
/// position.
fn prev_pos(&self, n: RepeatCount) -> Option<usize> {
@@ -475,9 +481,9 @@
}
}
- fn search_char_pos(&self, cs: &CharSearch, n: RepeatCount) -> Option<usize> {
+ fn search_char_pos(&self, cs: CharSearch, n: RepeatCount) -> Option<usize> {
let mut shift = 0;
- let search_result = match *cs {
+ let search_result = match cs {
CharSearch::Backward(c) | CharSearch::BackwardAfter(c) => self.buf[..self.pos]
.char_indices()
.rev()
@@ -504,17 +510,16 @@
}
};
if let Some(pos) = search_result {
- Some(match *cs {
+ Some(match cs {
CharSearch::Backward(_) => pos,
CharSearch::BackwardAfter(c) => pos + c.len_utf8(),
CharSearch::Forward(_) => shift + pos,
CharSearch::ForwardBefore(_) => {
- shift + pos
- - self.buf[..shift + pos]
- .chars()
- .next_back()
- .unwrap()
- .len_utf8()
+ shift + pos - self.buf[..shift + pos]
+ .chars()
+ .next_back()
+ .unwrap()
+ .len_utf8()
}
})
} else {
@@ -525,7 +530,7 @@
/// Move cursor to the matching character position.
/// Return `true` when the search succeeds.
pub fn move_to(&mut self, cs: CharSearch, n: RepeatCount) -> bool {
- if let Some(pos) = self.search_char_pos(&cs, n) {
+ if let Some(pos) = self.search_char_pos(cs, n) {
self.pos = pos;
true
} else {
@@ -547,8 +552,8 @@
pub fn delete_to(&mut self, cs: CharSearch, n: RepeatCount) -> bool {
let search_result = match cs {
- CharSearch::ForwardBefore(c) => self.search_char_pos(&CharSearch::Forward(c), n),
- _ => self.search_char_pos(&cs, n),
+ CharSearch::ForwardBefore(c) => self.search_char_pos(CharSearch::Forward(c), n),
+ _ => self.search_char_pos(cs, n),
};
if let Some(pos) = search_result {
match cs {
@@ -583,6 +588,7 @@
.next()
.map(|i| i + self.pos)
}
+
/// Alter the next word.
pub fn edit_word(&mut self, a: WordAction) -> bool {
if let Some(start) = self.skip_whitespace() {
@@ -732,10 +738,8 @@
}
Movement::ViCharSearch(n, cs) => {
let search_result = match cs {
- CharSearch::ForwardBefore(c) => {
- self.search_char_pos(&CharSearch::Forward(c), n)
- }
- _ => self.search_char_pos(&cs, n),
+ CharSearch::ForwardBefore(c) => self.search_char_pos(CharSearch::Forward(c), n),
+ _ => self.search_char_pos(cs, n),
};
if let Some(pos) = search_result {
Some(match cs {
@@ -874,14 +878,18 @@
impl DeleteListener for Listener {
fn start_killing(&mut self) {}
+
fn delete(&mut self, _: usize, string: &str, _: Direction) {
self.deleted_str = Some(string.to_owned());
}
+
fn stop_killing(&mut self) {}
}
impl ChangeListener for Listener {
fn insert_char(&mut self, _: usize, _: char) {}
+
fn insert_str(&mut self, _: usize, _: &str) {}
+
fn replace(&mut self, _: usize, _: &str, _: &str) {}
}
diff --git a/src/test/mod.rs b/src/test/mod.rs
index d3216de..1ce9881 100644
--- a/src/test/mod.rs
+++ b/src/test/mod.rs
@@ -24,6 +24,8 @@
struct SimpleCompleter;
impl Completer for SimpleCompleter {
+ type Candidate = String;
+
fn complete(&self, line: &str, _pos: usize) -> Result<(usize, Vec<String>)> {
Ok((0, vec![line.to_owned() + "t"]))
}
@@ -43,6 +45,7 @@
&mut s,
&mut input_state,
&completer,
+ None,
&Config::default(),
).unwrap();
assert_eq!(Some(Cmd::AcceptLine), cmd);
diff --git a/src/tty/mod.rs b/src/tty/mod.rs
index e6c42dc..e8c7db4 100644
--- a/src/tty/mod.rs
+++ b/src/tty/mod.rs
@@ -3,8 +3,9 @@
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
-use config::Config;
+use config::{ColorMode, Config};
use consts::KeyPress;
+use highlight::Highlighter;
use line_buffer::LineBuffer;
use Result;
@@ -42,6 +43,7 @@
hint: Option<String>,
current_row: usize,
old_rows: usize,
+ highlighter: Option<&Highlighter>,
) -> Result<(Position, Position)>;
/// Calculate the number of columns and rows used to display `s` on a
@@ -77,6 +79,7 @@
fn move_cursor(&mut self, old: Position, new: Position) -> Result<()> {
(**self).move_cursor(old, new)
}
+
fn refresh_line(
&mut self,
prompt: &str,
@@ -85,30 +88,47 @@
hint: Option<String>,
current_row: usize,
old_rows: usize,
+ highlighter: Option<&Highlighter>,
) -> Result<(Position, Position)> {
- (**self).refresh_line(prompt, prompt_size, line, hint, current_row, old_rows)
+ (**self).refresh_line(
+ prompt,
+ prompt_size,
+ line,
+ hint,
+ current_row,
+ old_rows,
+ highlighter,
+ )
}
+
fn calculate_position(&self, s: &str, orig: Position) -> Position {
(**self).calculate_position(s, orig)
}
+
fn write_and_flush(&mut self, buf: &[u8]) -> Result<()> {
(**self).write_and_flush(buf)
}
+
fn beep(&mut self) -> Result<()> {
(**self).beep()
}
+
fn clear_screen(&mut self) -> Result<()> {
(**self).clear_screen()
}
+
fn sigwinch(&self) -> bool {
(**self).sigwinch()
}
+
fn update_size(&mut self) {
(**self).update_size()
}
+
fn get_columns(&self) -> usize {
(**self).get_columns()
}
+
fn get_rows(&self) -> usize {
(**self).get_rows()
}
@@ -120,14 +140,16 @@
type Writer: Renderer; // rl_outstream
type Mode: RawMode;
- fn new() -> Self;
+ fn new(color_mode: ColorMode) -> Self;
/// Check if current terminal can provide a rich line-editing user
/// interface.
fn is_unsupported(&self) -> bool;
/// check if stdin is connected to a terminal.
fn is_stdin_tty(&self) -> bool;
+ /// Check if output supports colors.
+ fn colors_enabled(&self) -> bool;
/// Enable RAW mode for the terminal.
- fn enable_raw_mode(&self) -> Result<Self::Mode>;
+ fn enable_raw_mode(&mut self) -> Result<Self::Mode>;
/// Create a RAW reader
fn create_reader(&self, config: &Config) -> Result<Self::Reader>;
/// Create a writer
diff --git a/src/tty/test.rs b/src/tty/test.rs
index 29a1e79..752bae3 100644
--- a/src/tty/test.rs
+++ b/src/tty/test.rs
@@ -4,9 +4,10 @@
use std::vec::IntoIter;
use super::{truncate, Position, RawMode, RawReader, Renderer, Term};
-use config::Config;
+use config::{ColorMode, Config};
use consts::KeyPress;
use error::ReadlineError;
+use highlight::Highlighter;
use line_buffer::LineBuffer;
use Result;
@@ -25,6 +26,7 @@
None => Err(ReadlineError::Eof),
}
}
+
#[cfg(unix)]
fn next_char(&mut self) -> Result<char> {
unimplemented!();
@@ -38,6 +40,7 @@
None => Err(ReadlineError::Eof),
}
}
+
#[cfg(unix)]
fn next_char(&mut self) -> Result<char> {
match self.next() {
@@ -69,6 +72,7 @@
hint: Option<String>,
_: usize,
_: usize,
+ _: Option<&Highlighter>,
) -> Result<(Position, Position)> {
let cursor = self.calculate_position(&line[..line.pos()], prompt_size);
if let Some(hint) = hint {
@@ -99,10 +103,13 @@
fn sigwinch(&self) -> bool {
false
}
+
fn update_size(&mut self) {}
+
fn get_columns(&self) -> usize {
80
}
+
fn get_rows(&self) -> usize {
24
}
@@ -117,11 +124,11 @@
}
impl Term for DummyTerminal {
+ type Mode = Mode;
type Reader = IntoIter<KeyPress>;
type Writer = Sink;
- type Mode = Mode;
- fn new() -> DummyTerminal {
+ fn new(_color_mode: ColorMode) -> DummyTerminal {
DummyTerminal {
keys: Vec::new(),
cursor: 0,
@@ -138,9 +145,13 @@
true
}
+ fn colors_enabled(&self) -> bool {
+ false
+ }
+
// Interactive loop:
- fn enable_raw_mode(&self) -> Result<Mode> {
+ fn enable_raw_mode(&mut self) -> Result<Mode> {
Ok(())
}
diff --git a/src/tty/unix.rs b/src/tty/unix.rs
index b1d5b78..5cc97e3 100644
--- a/src/tty/unix.rs
+++ b/src/tty/unix.rs
@@ -11,11 +11,13 @@
use nix::sys::termios;
use nix::sys::termios::SetArg;
use unicode_segmentation::UnicodeSegmentation;
+use utf8parse::{Parser, Receiver};
use super::{truncate, width, Position, RawMode, RawReader, Renderer, Term};
-use config::Config;
+use config::{ColorMode, Config};
use consts::{self, KeyPress};
use error;
+use highlight::Highlighter;
use line_buffer::LineBuffer;
use Result;
@@ -30,8 +32,8 @@
unsafe {
let mut size: libc::winsize = zeroed();
- match libc::ioctl(STDOUT_FILENO, libc::TIOCGWINSZ.into(), &mut size) {
- // .into() for FreeBSD
+ // https://github.com/rust-lang/libc/pull/704
+ match libc::ioctl(STDOUT_FILENO, libc::TIOCGWINSZ as libc::c_ulong, &mut size) {
0 => (size.ws_col as usize, size.ws_row as usize), // TODO getCursorPosition
_ => (80, 24),
}
@@ -101,7 +103,14 @@
pub struct PosixRawReader {
stdin: StdinRaw,
timeout_ms: i32,
- buf: [u8; 4],
+ buf: [u8; 1],
+ parser: Parser,
+ receiver: Utf8,
+}
+
+struct Utf8 {
+ c: Option<char>,
+ valid: bool,
}
impl PosixRawReader {
@@ -109,149 +118,26 @@
Ok(PosixRawReader {
stdin: StdinRaw {},
timeout_ms: config.keyseq_timeout(),
- buf: [0; 4],
+ buf: [0; 1],
+ parser: Parser::new(),
+ receiver: Utf8 {
+ c: None,
+ valid: true,
+ },
})
}
+ /// Handle ESC <seq1> sequences
fn escape_sequence(&mut self) -> Result<KeyPress> {
- // Read the next two bytes representing the escape sequence.
+ // Read the next byte representing the escape sequence.
let seq1 = try!(self.next_char());
if seq1 == '[' {
// ESC [ sequences. (CSI)
- let seq2 = try!(self.next_char());
- if seq2.is_digit(10) {
- // Extended escape, read additional byte.
- let seq3 = try!(self.next_char());
- if seq3 == '~' {
- Ok(match seq2 {
- '1' | '7' => KeyPress::Home, // tmux, xrvt
- '2' => KeyPress::Insert,
- '3' => KeyPress::Delete, // kdch1
- '4' | '8' => KeyPress::End, // tmux, xrvt
- '5' => KeyPress::PageUp, // kpp
- '6' => KeyPress::PageDown, // knp
- _ => {
- debug!(target: "rustyline",
- "unsupported esc sequence: ESC [ {} ~", seq2);
- KeyPress::UnknownEscSeq
- }
- })
- } else if seq3.is_digit(10) {
- let seq4 = try!(self.next_char());
- if seq4 == '~' {
- Ok(match (seq2, seq3) {
- ('1', '1') => KeyPress::F(1), // rxvt-unicode
- ('1', '2') => KeyPress::F(2), // rxvt-unicode
- ('1', '3') => KeyPress::F(3), // rxvt-unicode
- ('1', '4') => KeyPress::F(4), // rxvt-unicode
- ('1', '5') => KeyPress::F(5), // kf5
- ('1', '7') => KeyPress::F(6), // kf6
- ('1', '8') => KeyPress::F(7), // kf7
- ('1', '9') => KeyPress::F(8), // kf8
- ('2', '0') => KeyPress::F(9), // kf9
- ('2', '1') => KeyPress::F(10), // kf10
- ('2', '3') => KeyPress::F(11), // kf11
- ('2', '4') => KeyPress::F(12), // kf12
- _ => {
- debug!(target: "rustyline",
- "unsupported esc sequence: ESC [ {}{} ~", seq1, seq2);
- KeyPress::UnknownEscSeq
- }
- })
- } else if seq4 == ';' {
- let seq5 = try!(self.next_char());
- if seq5.is_digit(10) {
- let seq6 = try!(self.next_char()); // '~' expected
- debug!(target: "rustyline",
- "unsupported esc sequence: ESC [ {}{} ; {} {}", seq2, seq3, seq5, seq6);
- } else {
- debug!(target: "rustyline",
- "unsupported esc sequence: ESC [ {}{} ; {:?}", seq2, seq3, seq5);
- }
- Ok(KeyPress::UnknownEscSeq)
- } else {
- debug!(target: "rustyline",
- "unsupported esc sequence: ESC [ {}{} {:?}", seq2, seq3, seq4);
- Ok(KeyPress::UnknownEscSeq)
- }
- } else if seq3 == ';' {
- let seq4 = try!(self.next_char());
- if seq4.is_digit(10) {
- let seq5 = try!(self.next_char());
- if seq2 == '1' {
- Ok(match (seq4, seq5) {
- ('5', 'A') => KeyPress::ControlUp,
- ('5', 'B') => KeyPress::ControlDown,
- ('5', 'C') => KeyPress::ControlRight,
- ('5', 'D') => KeyPress::ControlLeft,
- ('2', 'A') => KeyPress::ShiftUp,
- ('2', 'B') => KeyPress::ShiftDown,
- ('2', 'C') => KeyPress::ShiftRight,
- ('2', 'D') => KeyPress::ShiftLeft,
- _ => {
- debug!(target: "rustyline",
- "unsupported esc sequence: ESC [ {} ; {} {}", seq2, seq4, seq5);
- KeyPress::UnknownEscSeq
- }
- })
- } else {
- debug!(target: "rustyline",
- "unsupported esc sequence: ESC [ {} ; {} {}", seq2, seq4, seq5);
- Ok(KeyPress::UnknownEscSeq)
- }
- } else {
- debug!(target: "rustyline",
- "unsupported esc sequence: ESC [ {} ; {:?}", seq2, seq4);
- Ok(KeyPress::UnknownEscSeq)
- }
- } else {
- Ok(match (seq2, seq3) {
- ('5', 'A') => KeyPress::ControlUp,
- ('5', 'B') => KeyPress::ControlDown,
- ('5', 'C') => KeyPress::ControlRight,
- ('5', 'D') => KeyPress::ControlLeft,
- _ => {
- debug!(target: "rustyline",
- "unsupported esc sequence: ESC [ {} {:?}", seq2, seq3);
- KeyPress::UnknownEscSeq
- }
- })
- }
- } else {
- // ANSI
- Ok(match seq2 {
- 'A' => KeyPress::Up, // kcuu1
- 'B' => KeyPress::Down, // kcud1
- 'C' => KeyPress::Right, // kcuf1
- 'D' => KeyPress::Left, // kcub1
- 'F' => KeyPress::End,
- 'H' => KeyPress::Home, // khome
- _ => {
- debug!(target: "rustyline", "unsupported esc sequence: ESC [ {:?}", seq2);
- KeyPress::UnknownEscSeq
- }
- })
- }
+ self.escape_csi()
} else if seq1 == 'O' {
// xterm
// ESC O sequences. (SS3)
- let seq2 = try!(self.next_char());
- Ok(match seq2 {
- 'A' => KeyPress::Up, // kcuu1
- 'B' => KeyPress::Down, // kcud1
- 'C' => KeyPress::Right, // kcuf1
- 'D' => KeyPress::Left, // kcub1
- 'F' => KeyPress::End, // kend
- 'H' => KeyPress::Home, // khome
- 'P' => KeyPress::F(1), // kf1
- 'Q' => KeyPress::F(2), // kf2
- 'R' => KeyPress::F(3), // kf3
- 'S' => KeyPress::F(4), // kf4
- _ => {
- debug!(target: "rustyline", "unsupported esc sequence: ESC O {:?}", seq2);
- KeyPress::UnknownEscSeq
- }
- })
+ self.escape_o()
} else if seq1 == '\x1b' {
// ESC ESC
Ok(KeyPress::Esc)
@@ -260,28 +146,163 @@
Ok(KeyPress::Meta(seq1))
}
}
-}
-// https://tools.ietf.org/html/rfc3629
-#[cfg_attr(rustfmt, rustfmt_skip)]
-static UTF8_CHAR_WIDTH: [u8; 256] = [
-1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
-1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // 0x1F
-1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
-1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // 0x3F
-1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
-1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // 0x5F
-1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
-1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // 0x7F
-0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
-0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 0x9F
-0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
-0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 0xBF
-0,0,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
-2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, // 0xDF
-3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, // 0xEF
-4,4,4,4,4,0,0,0,0,0,0,0,0,0,0,0, // 0xFF
-];
+ /// Handle ESC [ <seq2> escape sequences
+ fn escape_csi(&mut self) -> Result<KeyPress> {
+ let seq2 = try!(self.next_char());
+ if seq2.is_digit(10) {
+ match seq2 {
+ '0' | '9' => {
+ debug!(target: "rustyline", "unsupported esc sequence: ESC [ {:?}", seq2);
+ Ok(KeyPress::UnknownEscSeq)
+ }
+ _ => {
+ // Extended escape, read additional byte.
+ self.extended_escape(seq2)
+ }
+ }
+ } else {
+ // ANSI
+ Ok(match seq2 {
+ 'A' => KeyPress::Up, // kcuu1
+ 'B' => KeyPress::Down, // kcud1
+ 'C' => KeyPress::Right, // kcuf1
+ 'D' => KeyPress::Left, // kcub1
+ 'F' => KeyPress::End,
+ 'H' => KeyPress::Home, // khome
+ _ => {
+ debug!(target: "rustyline", "unsupported esc sequence: ESC [ {:?}", seq2);
+ KeyPress::UnknownEscSeq
+ }
+ })
+ }
+ }
+
+ /// Handle ESC [ <seq2:digit> escape sequences
+ fn extended_escape(&mut self, seq2: char) -> Result<KeyPress> {
+ let seq3 = try!(self.next_char());
+ if seq3 == '~' {
+ Ok(match seq2 {
+ '1' | '7' => KeyPress::Home, // tmux, xrvt
+ '2' => KeyPress::Insert,
+ '3' => KeyPress::Delete, // kdch1
+ '4' | '8' => KeyPress::End, // tmux, xrvt
+ '5' => KeyPress::PageUp, // kpp
+ '6' => KeyPress::PageDown, // knp
+ _ => {
+ debug!(target: "rustyline",
+ "unsupported esc sequence: ESC [ {} ~", seq2);
+ KeyPress::UnknownEscSeq
+ }
+ })
+ } else if seq3.is_digit(10) {
+ let seq4 = try!(self.next_char());
+ if seq4 == '~' {
+ Ok(match (seq2, seq3) {
+ ('1', '1') => KeyPress::F(1), // rxvt-unicode
+ ('1', '2') => KeyPress::F(2), // rxvt-unicode
+ ('1', '3') => KeyPress::F(3), // rxvt-unicode
+ ('1', '4') => KeyPress::F(4), // rxvt-unicode
+ ('1', '5') => KeyPress::F(5), // kf5
+ ('1', '7') => KeyPress::F(6), // kf6
+ ('1', '8') => KeyPress::F(7), // kf7
+ ('1', '9') => KeyPress::F(8), // kf8
+ ('2', '0') => KeyPress::F(9), // kf9
+ ('2', '1') => KeyPress::F(10), // kf10
+ ('2', '3') => KeyPress::F(11), // kf11
+ ('2', '4') => KeyPress::F(12), // kf12
+ _ => {
+ debug!(target: "rustyline",
+ "unsupported esc sequence: ESC [ {}{} ~", seq2, seq3);
+ KeyPress::UnknownEscSeq
+ }
+ })
+ } else if seq4 == ';' {
+ let seq5 = try!(self.next_char());
+ if seq5.is_digit(10) {
+ let seq6 = try!(self.next_char()); // '~' expected
+ debug!(target: "rustyline",
+ "unsupported esc sequence: ESC [ {}{} ; {} {}", seq2, seq3, seq5, seq6);
+ } else {
+ debug!(target: "rustyline",
+ "unsupported esc sequence: ESC [ {}{} ; {:?}", seq2, seq3, seq5);
+ }
+ Ok(KeyPress::UnknownEscSeq)
+ } else {
+ debug!(target: "rustyline",
+ "unsupported esc sequence: ESC [ {}{} {:?}", seq2, seq3, seq4);
+ Ok(KeyPress::UnknownEscSeq)
+ }
+ } else if seq3 == ';' {
+ let seq4 = try!(self.next_char());
+ if seq4.is_digit(10) {
+ let seq5 = try!(self.next_char());
+ if seq2 == '1' {
+ Ok(match (seq4, seq5) {
+ ('5', 'A') => KeyPress::ControlUp,
+ ('5', 'B') => KeyPress::ControlDown,
+ ('5', 'C') => KeyPress::ControlRight,
+ ('5', 'D') => KeyPress::ControlLeft,
+ ('2', 'A') => KeyPress::ShiftUp,
+ ('2', 'B') => KeyPress::ShiftDown,
+ ('2', 'C') => KeyPress::ShiftRight,
+ ('2', 'D') => KeyPress::ShiftLeft,
+ _ => {
+ debug!(target: "rustyline",
+ "unsupported esc sequence: ESC [ {} ; {} {}", seq2, seq4, seq5);
+ KeyPress::UnknownEscSeq
+ }
+ })
+ } else {
+ debug!(target: "rustyline",
+ "unsupported esc sequence: ESC [ {} ; {} {}", seq2, seq4, seq5);
+ Ok(KeyPress::UnknownEscSeq)
+ }
+ } else {
+ debug!(target: "rustyline",
+ "unsupported esc sequence: ESC [ {} ; {:?}", seq2, seq4);
+ Ok(KeyPress::UnknownEscSeq)
+ }
+ } else {
+ Ok(match (seq2, seq3) {
+ ('5', 'A') => KeyPress::ControlUp,
+ ('5', 'B') => KeyPress::ControlDown,
+ ('5', 'C') => KeyPress::ControlRight,
+ ('5', 'D') => KeyPress::ControlLeft,
+ _ => {
+ debug!(target: "rustyline",
+ "unsupported esc sequence: ESC [ {} {:?}", seq2, seq3);
+ KeyPress::UnknownEscSeq
+ }
+ })
+ }
+ }
+
+ /// Handle ESC O <seq2> escape sequences
+ fn escape_o(&mut self) -> Result<KeyPress> {
+ let seq2 = try!(self.next_char());
+ Ok(match seq2 {
+ 'A' => KeyPress::Up, // kcuu1
+ 'B' => KeyPress::Down, // kcud1
+ 'C' => KeyPress::Right, // kcuf1
+ 'D' => KeyPress::Left, // kcub1
+ 'F' => KeyPress::End, // kend
+ 'H' => KeyPress::Home, // khome
+ 'P' => KeyPress::F(1), // kf1
+ 'Q' => KeyPress::F(2), // kf2
+ 'R' => KeyPress::F(3), // kf3
+ 'S' => KeyPress::F(4), // kf4
+ 'a' => KeyPress::ControlUp,
+ 'b' => KeyPress::ControlDown,
+ 'c' => KeyPress::ControlRight,
+ 'd' => KeyPress::ControlLeft,
+ _ => {
+ debug!(target: "rustyline", "unsupported esc sequence: ESC O {:?}", seq2);
+ KeyPress::UnknownEscSeq
+ }
+ })
+ }
+}
impl RawReader for PosixRawReader {
fn next_key(&mut self, single_esc_abort: bool) -> Result<KeyPress> {
@@ -312,26 +333,36 @@
}
fn next_char(&mut self) -> Result<char> {
- let n = try!(self.stdin.read(&mut self.buf[..1]));
- if n == 0 {
- return Err(error::ReadlineError::Eof);
- }
- let first = self.buf[0];
- if first >= 128 {
- let width = UTF8_CHAR_WIDTH[first as usize] as usize;
- if width == 0 {
- try!(std::str::from_utf8(&self.buf[..1]));
- unreachable!()
+ loop {
+ let n = try!(self.stdin.read(&mut self.buf));
+ if n == 0 {
+ return Err(error::ReadlineError::Eof);
}
- try!(self.stdin.read_exact(&mut self.buf[1..width]));
- let s = try!(std::str::from_utf8(&self.buf[..width]));
- Ok(s.chars().next().unwrap())
- } else {
- Ok(first as char)
+ let b = self.buf[0];
+ self.parser.advance(&mut self.receiver, b);
+ if !self.receiver.valid {
+ return Err(error::ReadlineError::Utf8Error);
+ } else if self.receiver.c.is_some() {
+ return Ok(self.receiver.c.take().unwrap());
+ }
}
}
}
+impl Receiver for Utf8 {
+ /// Called whenever a codepoint is parsed successfully
+ fn codepoint(&mut self, c: char) {
+ self.c = Some(c);
+ self.valid = true;
+ }
+
+ /// Called when an invalid_sequence is detected
+ fn invalid_sequence(&mut self) {
+ self.c = None;
+ self.valid = false;
+ }
+}
+
/// Console output writer
pub struct PosixRenderer {
out: Stdout,
@@ -397,6 +428,7 @@
hint: Option<String>,
current_row: usize,
old_rows: usize,
+ highlighter: Option<&Highlighter>,
) -> Result<(Position, Position)> {
use std::fmt::Write;
let mut ab = String::new();
@@ -420,13 +452,25 @@
// clear the line
ab.push_str("\r\x1b[0K");
- // display the prompt
- ab.push_str(prompt);
- // display the input line
- ab.push_str(line);
+ if let Some(highlighter) = highlighter {
+ // display the prompt
+ ab.push_str(&highlighter.highlight_prompt(prompt));
+ // display the input line
+ ab.push_str(&highlighter.highlight(line, line.pos()));
+ } else {
+ // display the prompt
+ ab.push_str(prompt);
+ // display the input line
+ ab.push_str(line);
+ }
// display hint
if let Some(hint) = hint {
- ab.push_str(truncate(&hint, end_pos.col, self.cols));
+ let truncate = truncate(&hint, end_pos.col, self.cols);
+ if let Some(highlighter) = highlighter {
+ ab.push_str(&highlighter.highlight_hint(truncate));
+ } else {
+ ab.push_str(truncate);
+ }
}
// we have to generate our own newline on line wrap
if end_pos.col == 0 && end_pos.row > 0 {
@@ -457,7 +501,6 @@
/// Control characters are treated as having zero width.
/// Characters with 2 column width are correctly handled (not splitted).
- #[allow(if_same_then_else)]
fn calculate_position(&self, s: &str, orig: Position) -> Position {
let mut pos = orig;
let mut esc_seq = 0;
@@ -500,6 +543,7 @@
fn get_columns(&self) -> usize {
self.cols
}
+
/// Try to get the number of rows in the current terminal,
/// or assume 24 if it fails.
fn get_rows(&self) -> usize {
@@ -533,19 +577,23 @@
pub struct PosixTerminal {
unsupported: bool,
stdin_isatty: bool,
+ stdout_isatty: bool,
+ color_mode: ColorMode,
}
impl Term for PosixTerminal {
+ type Mode = Mode;
type Reader = PosixRawReader;
type Writer = PosixRenderer;
- type Mode = Mode;
- fn new() -> PosixTerminal {
+ fn new(color_mode: ColorMode) -> PosixTerminal {
let term = PosixTerminal {
unsupported: is_unsupported_term(),
stdin_isatty: is_a_tty(STDIN_FILENO),
+ stdout_isatty: is_a_tty(STDOUT_FILENO),
+ color_mode,
};
- if !term.unsupported && term.stdin_isatty && is_a_tty(STDOUT_FILENO) {
+ if !term.unsupported && term.stdin_isatty && term.stdout_isatty {
install_sigwinch_handler();
}
term
@@ -564,9 +612,18 @@
self.stdin_isatty
}
+ /// Check if output supports colors.
+ fn colors_enabled(&self) -> bool {
+ match self.color_mode {
+ ColorMode::Enabled => self.stdout_isatty,
+ ColorMode::Forced => true,
+ ColorMode::Disabled => false,
+ }
+ }
+
// Interactive loop:
- fn enable_raw_mode(&self) -> Result<Mode> {
+ fn enable_raw_mode(&mut self) -> Result<Mode> {
use nix::errno::Errno::ENOTTY;
use nix::sys::termios::{ControlFlags, InputFlags, LocalFlags, SpecialCharacterIndices};
if !self.stdin_isatty {
diff --git a/src/tty/windows.rs b/src/tty/windows.rs
index 6d9fa36..e3a3aa5 100644
--- a/src/tty/windows.rs
+++ b/src/tty/windows.rs
@@ -9,9 +9,10 @@
use winapi::um::{consoleapi, handleapi, processenv, winbase, wincon, winuser};
use super::{truncate, Position, RawMode, RawReader, Renderer, Term};
-use config::Config;
+use config::{ColorMode, Config};
use consts::{self, KeyPress};
use error;
+use highlight::Highlighter;
use line_buffer::LineBuffer;
use Result;
@@ -65,7 +66,7 @@
pub struct ConsoleMode {
original_stdin_mode: DWORD,
stdin_handle: HANDLE,
- original_stdout_mode: DWORD,
+ original_stdout_mode: Option<DWORD>,
stdout_handle: HANDLE,
}
@@ -76,10 +77,12 @@
self.stdin_handle,
self.original_stdin_mode,
));
- check!(consoleapi::SetConsoleMode(
- self.stdout_handle,
- self.original_stdout_mode,
- ));
+ if let Some(original_stdout_mode) = self.original_stdout_mode {
+ check!(consoleapi::SetConsoleMode(
+ self.stdout_handle,
+ original_stdout_mode,
+ ));
+ }
Ok(())
}
}
@@ -134,11 +137,11 @@
// key_event.wRepeatCount seems to be always set to 1 (maybe because we only
// read one character at a time)
- // let alt_gr = key_event.dwControlKeyState & (LEFT_CTRL_PRESSED |
- // RIGHT_ALT_PRESSED) != 0;
+ let alt_gr = key_event.dwControlKeyState & (LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED)
+ == (LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED);
let alt = key_event.dwControlKeyState & (LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED) != 0;
let ctrl = key_event.dwControlKeyState & (LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED) != 0;
- let meta = alt;
+ let meta = alt && !alt_gr;
let utf16 = unsafe { *key_event.uChar.UnicodeChar() };
if utf16 == 0 {
@@ -190,8 +193,7 @@
winuser::VK_F11 => return Ok(KeyPress::F(11)),
winuser::VK_F12 => return Ok(KeyPress::F(12)),
// winuser::VK_BACK is correctly handled because the key_event.UnicodeChar is
- // also
- // set.
+ // also set.
_ => continue,
};
} else if utf16 == 27 {
@@ -289,6 +291,7 @@
hint: Option<String>,
current_row: usize,
old_rows: usize,
+ highlighter: Option<&Highlighter>,
) -> Result<(Position, Position)> {
// calculate the position of the end of the input line
let end_pos = self.calculate_position(line, prompt_size);
@@ -305,14 +308,26 @@
info.dwCursorPosition,
));
let mut ab = String::new();
- // display the prompt
- // TODO handle ansi escape code (SetConsoleTextAttribute)
- ab.push_str(prompt);
- // display the input line
- ab.push_str(&line);
+ if let Some(highlighter) = highlighter {
+ // TODO handle ansi escape code (SetConsoleTextAttribute)
+ // display the prompt
+ ab.push_str(&highlighter.highlight_prompt(prompt));
+ // display the input line
+ ab.push_str(&highlighter.highlight(line, line.pos()));
+ } else {
+ // display the prompt
+ ab.push_str(prompt);
+ // display the input line
+ ab.push_str(line);
+ }
// display hint
if let Some(hint) = hint {
- ab.push_str(truncate(&hint, end_pos.col, self.cols));
+ let truncate = truncate(&hint, end_pos.col, self.cols);
+ if let Some(highlighter) = highlighter {
+ ab.push_str(&highlighter.highlight_hint(truncate));
+ } else {
+ ab.push_str(truncate);
+ }
}
try!(self.write_and_flush(ab.as_bytes()));
@@ -396,17 +411,20 @@
pub struct Console {
stdin_isatty: bool,
stdin_handle: HANDLE,
+ stdout_isatty: bool,
stdout_handle: HANDLE,
+ color_mode: ColorMode,
+ ansi_colors_supported: bool,
}
impl Console {}
impl Term for Console {
+ type Mode = Mode;
type Reader = ConsoleRawReader;
type Writer = ConsoleRenderer;
- type Mode = Mode;
- fn new() -> Console {
+ fn new(color_mode: ColorMode) -> Console {
use std::ptr;
let stdin_handle = get_std_handle(STDIN_FILENO);
let stdin_isatty = match stdin_handle {
@@ -416,12 +434,22 @@
}
Err(_) => false,
};
+ let stdout_handle = get_std_handle(STDOUT_FILENO);
+ let stdout_isatty = match stdout_handle {
+ Ok(handle) => {
+ // If this function doesn't fail then fd is a TTY
+ get_console_mode(handle).is_ok()
+ }
+ Err(_) => false,
+ };
- let stdout_handle = get_std_handle(STDOUT_FILENO).unwrap_or(ptr::null_mut());
Console {
stdin_isatty,
stdin_handle: stdin_handle.unwrap_or(ptr::null_mut()),
- stdout_handle,
+ stdout_isatty,
+ stdout_handle: stdout_handle.unwrap_or(ptr::null_mut()),
+ color_mode,
+ ansi_colors_supported: false,
}
}
@@ -434,12 +462,21 @@
self.stdin_isatty
}
+ fn colors_enabled(&self) -> bool {
+ // TODO ANSI Colors & Windows <10
+ match self.color_mode {
+ ColorMode::Enabled => self.stdout_isatty && self.ansi_colors_supported,
+ ColorMode::Forced => true,
+ ColorMode::Disabled => false,
+ }
+ }
+
// pub fn install_sigwinch_handler(&mut self) {
// See ReadConsoleInputW && WINDOW_BUFFER_SIZE_EVENT
// }
/// Enable RAW mode for the terminal.
- fn enable_raw_mode(&self) -> Result<Mode> {
+ fn enable_raw_mode(&mut self) -> Result<Mode> {
if !self.stdin_isatty {
try!(Err(io::Error::new(
io::ErrorKind::Other,
@@ -448,10 +485,9 @@
}
let original_stdin_mode = try!(get_console_mode(self.stdin_handle));
// Disable these modes
- let raw = original_stdin_mode
- & !(wincon::ENABLE_LINE_INPUT
- | wincon::ENABLE_ECHO_INPUT
- | wincon::ENABLE_PROCESSED_INPUT);
+ let raw = original_stdin_mode & !(wincon::ENABLE_LINE_INPUT
+ | wincon::ENABLE_ECHO_INPUT
+ | wincon::ENABLE_PROCESSED_INPUT);
// Enable these modes
let raw = raw | wincon::ENABLE_EXTENDED_FLAGS;
let raw = raw | wincon::ENABLE_INSERT_MODE;
@@ -459,13 +495,19 @@
let raw = raw | wincon::ENABLE_WINDOW_INPUT;
check!(consoleapi::SetConsoleMode(self.stdin_handle, raw));
- let original_stdout_mode = try!(get_console_mode(self.stdout_handle));
- // To enable ANSI colors (Windows 10 only):
- // https://docs.microsoft.com/en-us/windows/console/setconsolemode
- if original_stdout_mode & wincon::ENABLE_VIRTUAL_TERMINAL_PROCESSING == 0 {
- let raw = original_stdout_mode | wincon::ENABLE_VIRTUAL_TERMINAL_PROCESSING;
- check!(consoleapi::SetConsoleMode(self.stdout_handle, raw));
- }
+ let original_stdout_mode = if self.stdout_isatty {
+ let original_stdout_mode = try!(get_console_mode(self.stdout_handle));
+ // To enable ANSI colors (Windows 10 only):
+ // https://docs.microsoft.com/en-us/windows/console/setconsolemode
+ if original_stdout_mode & wincon::ENABLE_VIRTUAL_TERMINAL_PROCESSING == 0 {
+ let raw = original_stdout_mode | wincon::ENABLE_VIRTUAL_TERMINAL_PROCESSING;
+ self.ansi_colors_supported =
+ unsafe { consoleapi::SetConsoleMode(self.stdout_handle, raw) != 0 };
+ }
+ Some(original_stdout_mode)
+ } else {
+ None
+ };
Ok(Mode {
original_stdin_mode,
diff --git a/src/undo.rs b/src/undo.rs
index 84c8f77..7f7c9ab 100644
--- a/src/undo.rs
+++ b/src/undo.rs
@@ -177,11 +177,10 @@
return;
}
- if !Self::single_char(string.as_ref())
- || !self
- .undos
- .last()
- .map_or(false, |lc| lc.delete_seq(indx, string.as_ref().len()))
+ if !Self::single_char(string.as_ref()) || !self
+ .undos
+ .last()
+ .map_or(false, |lc| lc.delete_seq(indx, string.as_ref().len()))
{
self.undos.push(Change::Delete {
idx: indx,
@@ -330,18 +329,22 @@
impl DeleteListener for Changeset {
fn start_killing(&mut self) {}
+
fn delete(&mut self, idx: usize, string: &str, _: Direction) {
self.delete(idx, string);
}
+
fn stop_killing(&mut self) {}
}
impl ChangeListener for Changeset {
fn insert_char(&mut self, idx: usize, c: char) {
self.insert(idx, c);
}
+
fn insert_str(&mut self, idx: usize, string: &str) {
self.insert_str(idx, string);
}
+
fn replace(&mut self, idx: usize, old: &str, new: &str) {
self.replace(idx, old, new);
}