Merge remote-tracking branch 'origin/configurer'
diff --git a/Cargo.toml b/Cargo.toml
index 47ad106..af6ebb2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "rustyline"
-version = "2.0.0-alpha"
+version = "2.0.0"
authors = ["Katsu Kawakami <kkawa1570@gmail.com>"]
description = "Rustyline, a readline implementation based on Antirez's Linenoise"
documentation = "http://docs.rs/rustyline"
@@ -21,6 +21,7 @@
log = "0.4"
unicode-width = "0.1"
unicode-segmentation = "1.0"
+memchr = "2.0"
[target.'cfg(unix)'.dependencies]
nix = "0.11"
diff --git a/TODO.md b/TODO.md
index 6690366..d4cd263 100644
--- a/TODO.md
+++ b/TODO.md
@@ -7,7 +7,7 @@
- [ ] bell-style
Color
-- [x] ANSI Colors & Windows 10+
+- [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)
@@ -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,6 +75,7 @@
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 ?)
diff --git a/examples/example.rs b/examples/example.rs
index 3e75024..ce41cf4 100644
--- a/examples/example.rs
+++ b/examples/example.rs
@@ -2,6 +2,7 @@
extern crate rustyline;
use log::{Level, LevelFilter, Metadata, Record, SetLoggerError};
+use std::borrow::Cow::{self, Borrowed, Owned};
use rustyline::completion::{Completer, FilenameCompleter, Pair};
use rustyline::error::ReadlineError;
@@ -9,13 +10,8 @@
use rustyline::hint::Hinter;
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 MyHelper(FilenameCompleter);
@@ -31,18 +27,22 @@
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 {}
+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 {}
diff --git a/src/completion.rs b/src/completion.rs
index aeb9a34..ab7a020 100644
--- a/src/completion.rs
+++ b/src/completion.rs
@@ -1,11 +1,11 @@
//! 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
@@ -108,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,
}
}
}
@@ -150,25 +164,32 @@
type Candidate = Pair;
fn complete(&self, line: &str, pos: usize) -> Result<(usize, Vec<Pair>)> {
- let (start, path, esc_char, break_chars) =
- if let Some((idx, double_quote)) = find_unclosed_quote(&line[..pos]) {
+ 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))
}
}
@@ -179,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 {
@@ -200,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);
@@ -223,7 +264,8 @@
fn filename_complete(
path: &str,
esc_char: Option<char>,
- break_chars: &BTreeSet<char>,
+ break_chars: &[u8],
+ quote: Quote,
) -> Result<Vec<Pair>> {
use dirs::home_dir;
use std::env::current_dir;
@@ -261,14 +303,16 @@
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(Pair {
- display: String::from(s),
- replacement: 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
}
}
}
@@ -283,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() {
@@ -300,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;
@@ -356,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;
@@ -366,6 +410,7 @@
if char == '"' {
mode = ScanMode::Normal;
} else if char == '\\' {
+ // both windows and unix support escape in double quote
mode = ScanMode::EscapeInDoubleQuote;
}
}
@@ -393,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"),
@@ -423,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]
@@ -480,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/edit.rs b/src/edit.rs
index 6a82da6..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,
}
}
@@ -96,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(())
}
@@ -109,6 +122,7 @@
hint,
self.cursor.row,
self.old_rows,
+ self.highlighter,
));
self.cursor = cursor;
@@ -175,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
@@ -490,6 +505,7 @@
byte_buffer: [0; 4],
changes: Rc::new(RefCell::new(Changeset::new())),
hinter: None,
+ highlighter: None,
}
}
diff --git a/src/highlight.rs b/src/highlight.rs
index 8e9c972..c09e5e1 100644
--- a/src/highlight.rs
+++ b/src/highlight.rs
@@ -1,13 +1,22 @@
-///! Syntax highlighting
+//! Syntax highlighting
+
+use config::CompletionType;
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). TODO to be used
+/// 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
+ /// Takes the currently edited `line` with the cursor `pos`ition and
/// returns the highlighted version (with ANSI color).
- fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> {
+ ///
+ /// 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
@@ -15,6 +24,11 @@
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> {
@@ -22,9 +36,25 @@
}
/// Takes the completion `canditate` and
/// returns the highlighted version (with ANSI color).
- fn highlight_canidate<'c>(&self, candidate: &'c str) -> Cow<'c, str> {
+ ///
+ /// 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/lib.rs b/src/lib.rs
index e3254b0..35f5b76 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -21,6 +21,7 @@
extern crate libc;
#[macro_use]
extern crate log;
+extern crate memchr;
#[cfg(unix)]
extern crate nix;
extern crate unicode_segmentation;
@@ -76,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
@@ -94,7 +96,14 @@
loop {
// Show completion or original buffer
if i < candidates.len() {
- completer.update(&mut s.line, start, candidates[i].replacement());
+ 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
@@ -171,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)
@@ -185,6 +194,7 @@
rdr: &mut R,
s: &mut State,
input_state: &mut InputState,
+ highlighter: Option<&Highlighter>,
candidates: &[C],
) -> Result<Option<Cmd>> {
use std::cmp;
@@ -239,8 +249,12 @@
let i = (col * num_rows) + row;
if i < candidates.len() {
let candidate = &candidates[i].display();
- ab.push_str(candidate);
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(' ');
@@ -344,7 +358,6 @@
/// 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)>,
@@ -353,11 +366,22 @@
) -> Result<String> {
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());
@@ -387,6 +411,7 @@
&mut s,
&mut input_state,
completer.unwrap(),
+ highlighter,
&editor.config,
));
if next.is_some() {
@@ -581,6 +606,9 @@
}
}
}
+ if cfg!(windows) {
+ let _ = original_mode; // silent warning
+ }
Ok(s.line.into_string())
}
@@ -647,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> {
diff --git a/src/line_buffer.rs b/src/line_buffer.rs
index 1c220ca..e17fd62 100644
--- a/src/line_buffer.rs
+++ b/src/line_buffer.rs
@@ -153,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 {
diff --git a/src/test/mod.rs b/src/test/mod.rs
index f2d6ea2..1ce9881 100644
--- a/src/test/mod.rs
+++ b/src/test/mod.rs
@@ -45,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 e96f676..e8c7db4 100644
--- a/src/tty/mod.rs
+++ b/src/tty/mod.rs
@@ -5,6 +5,7 @@
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
@@ -86,8 +88,17 @@
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 {
diff --git a/src/tty/test.rs b/src/tty/test.rs
index c4aa81c..75844be 100644
--- a/src/tty/test.rs
+++ b/src/tty/test.rs
@@ -7,6 +7,7 @@
use config::{ColorMode, Config};
use consts::KeyPress;
use error::ReadlineError;
+use highlight::Highlighter;
use line_buffer::LineBuffer;
use Result;
@@ -71,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 {
diff --git a/src/tty/unix.rs b/src/tty/unix.rs
index 1a07a0e..d3b6f76 100644
--- a/src/tty/unix.rs
+++ b/src/tty/unix.rs
@@ -17,6 +17,7 @@
use config::{ColorMode, Config};
use consts::{self, KeyPress};
use error;
+use highlight::Highlighter;
use line_buffer::LineBuffer;
use Result;
@@ -31,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),
}
@@ -427,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();
@@ -450,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 {
@@ -487,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;
diff --git a/src/tty/windows.rs b/src/tty/windows.rs
index 249efad..098cae4 100644
--- a/src/tty/windows.rs
+++ b/src/tty/windows.rs
@@ -12,6 +12,7 @@
use config::{ColorMode, Config};
use consts::{self, KeyPress};
use error;
+use highlight::Highlighter;
use line_buffer::LineBuffer;
use Result;
@@ -192,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 {
@@ -291,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);
@@ -307,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()));
@@ -450,8 +463,9 @@
}
fn colors_enabled(&self) -> bool {
+ // TODO ANSI Colors & Windows <10
match self.color_mode {
- ColorMode::Enabled => self.stdout_isatty,
+ ColorMode::Enabled => self.stdout_isatty && self.ansi_colors_supported,
ColorMode::Forced => true,
ColorMode::Disabled => false,
}