Merge remote-tracking branch 'kkawakam/master'
diff --git a/.travis.yml b/.travis.yml
index af46809..ea88427 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,16 +1,6 @@
language: rust
rust:
- - stable
- - beta
- nightly
script:
- cargo build --verbose
- cargo test --verbose
- - cargo doc
-after_success: |
- [ $TRAVIS_BRANCH = master ] &&
- [ $TRAVIS_PULL_REQUEST = false ] &&
- bash deploy-docs.sh
-env:
- global:
- secure: "XxaPXHiVplTwMaAytYC0VQR/nNnm7SJVzXiUuaVEjssHip0Uje/4f3vGqtJjnD70FfxwNWQKiSYOcbYjWPlsJeANRt4ZoCsRt5eLGUZ+wH79n1fOkp5EIpFT/isjCB51A4n8PRUvuWfQ2OtNNeGLL6akMxt19sHdXoiQkLOe338="
diff --git a/Cargo.toml b/Cargo.toml
index ec6586a..4a1c1bd 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -15,11 +15,10 @@
appveyor = { repository = "kkawakam/rustyline" }
[dependencies]
-libc = "0.2.7"
+libc = "0.2"
log = "0.3"
-unicode-width = "0.1.3"
+unicode-width = "0.1"
unicode-segmentation = "1.0"
-encode_unicode = "0.1.3"
[target.'cfg(unix)'.dependencies]
nix = "0.8"
@@ -29,4 +28,4 @@
kernel32-sys = "0.2"
[dev-dependencies]
-tempdir = "0.3.4"
+tempdir = "0.3"
diff --git a/README.md b/README.md
index f031d1b..511fc2b 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,6 @@
# RustyLine
-[![Build Status](https://travis-ci.org/kkawakam/rustyline.svg?branch=master)](https://travis-ci.org/kkawakam/rustyline)
-[![Build status](https://ci.appveyor.com/api/projects/status/ls7sty8nt25rdfkq/branch/master?svg=true)](https://ci.appveyor.com/project/kkawakam/rustyline/branch/master)
-[![Clippy Linting Result](https://clippy.bashy.io/github/kkawakam/rustyline/master/badge.svg)](https://clippy.bashy.io/github/kkawakam/rustyline/master/log)
+[![Build Status](https://travis-ci.org/gwenn/rustyline.svg?branch=master)](https://travis-ci.org/gwenn/rustyline)
+[![Build Status](https://ci.appveyor.com/api/projects/status/github/gwenn/rustyline?branch=master&svg=true)](https://ci.appveyor.com/project/gwenn/rustyline)
[![](http://meritbadge.herokuapp.com/rustyline)](https://crates.io/crates/rustyline)
Readline implementation in Rust that is based on [Antirez' Linenoise](https://github.com/antirez/linenoise)
@@ -11,7 +10,7 @@
[Documentation (Master)](https://kkawakam.github.io/rustyline/rustyline/)
**Supported Platforms**
-* Linux
+* Unix
* Windows
* cmd.exe
* Powershell
@@ -19,7 +18,7 @@
**Note**: Powershell ISE is not supported, check [issue #56](https://github.com/kkawakam/rustyline/issues/56)
## Build
-This project uses Cargo and Rust stable
+This project uses Cargo and Rust nightly
```bash
cargo build --release
```
@@ -78,7 +77,7 @@
- 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))
- - Multi line mode
+ - Multi line mode (line wrapping)
- Word commands
## Actions
@@ -100,8 +99,10 @@
Ctrl-U | Delete from start of line to cursor
Ctrl-V | Insert any special character without perfoming its associated action (#65)
Ctrl-W | Delete word leading up to cursor (using white space as a word boundary)
+Ctrl-X Ctrl-U | Undo
Ctrl-Y | Paste from Yank buffer
Ctrl-Z | Suspend (unix only)
+Ctrl-_ | Undo
### Emacs mode (default mode)
@@ -169,6 +170,7 @@
S | Change current line (equivalent to 0c$)
t<char> | Move right to the next occurance of `char`, then one char backward
T<char> | Move left to the previous occurance of `char`, then one char forward
+u | Undo
w | Move one word or token right
W | Move one non-blank word right
x | Delete a single character under the cursor
@@ -189,8 +191,6 @@
## ToDo
- - Undos
- - Read input with timeout to properly handle single ESC key
- expose an API callable from C
## Wine
@@ -221,3 +221,16 @@
- [liner](https://github.com/peterh/liner) (Go)
- [readline](https://github.com/chzyer/readline) (Go)
- [haskeline](https://github.com/judah/haskeline) (Haskell)
+ - [rb-readline](https://github.com/ConnorAtherton/rb-readline) (Ruby)
+ - [python-prompt-toolkit](https://github.com/jonathanslenders/python-prompt-toolkit) (Python)
+
+Library | Lang | OS | Term | Unicode | History | Completion | Keymap | Kill Ring | Undo |
+-------- | ---- | -- | ---- | ------- | ------- | ---------- | ------- | --------- | ---- |
+Haskeline | Haskell | Ux/Win | Any | Yes | Yes | | Emacs/Vi/conf | Yes | Yes |
+Linenoise | C | Ux | ANSI | No | Yes | only line | Emacs | No | No |
+Linenoise-ng | C | Ux/Win | ANSI | Yes | Yes | only line | Emacs | Yes | No |
+Linefeed | Rust | Ux/Win | Any | | Yes | | Emacs/conf | Yes | No |
+Liner | Rust | Ux | ANSI | | No inc search | only word | Emacs/Vi | No | Yes |
+prompt-toolkit | Python | Ux/Win | ANSI | Yes | Yes | | Emacs/Vi/conf | Yes | Yes |
+Rb-readline | Ruby | Ux/Win | ANSI | Yes | Yes | only word | Emacs/Vi/conf | Yes | Yes |
+Rustyline | Rust | Ux/Win | ANSI | Yes | Yes | | Emacs/Vi/bind | Yes | Yes |
diff --git a/appveyor.yml b/appveyor.yml
index 8934d30..862c484 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,14 +1,9 @@
environment:
- matrix:
- - TARGET: 1.16.0-x86_64-pc-windows-msvc
- - TARGET: 1.16.0-x86_64-pc-windows-gnu
- - TARGET: beta-x86_64-pc-windows-msvc
- - TARGET: beta-x86_64-pc-windows-gnu
+ TARGET: x86_64-pc-windows-msvc
install:
- - ps: Start-FileDownload "https://static.rust-lang.org/dist/rust-${env:TARGET}.exe"
- - rust-%TARGET%.exe /VERYSILENT /NORESTART /DIR="C:\Program Files (x86)\Rust"
- - SET PATH=%PATH%;C:\Program Files (x86)\Rust\bin
- - SET PATH=%PATH%;C:\MinGW\bin
+ - appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe
+ - rustup-init -yv --default-toolchain nightly --default-host %TARGET%
+ - set PATH=%PATH%;%USERPROFILE%\.cargo\bin
- rustc -V
- cargo -V
diff --git a/deploy-docs.sh b/deploy-docs.sh
deleted file mode 100755
index efd5a56..0000000
--- a/deploy-docs.sh
+++ /dev/null
@@ -1,21 +0,0 @@
-#!/bin/bash
-
-rev=$(git rev-parse --short HEAD)
-
-cd target/doc
-
-echo '<meta http-equiv=refresh content=0;url=rustyline/index.html>' > index.html
-
-git init
-git config user.name "Katsu Kawakami"
-git config user.email "kkawa1570@gmail.com"
-
-git remote add upstream "https://$GH_TOKEN@github.com/kkawakam/rustyline.git"
-git fetch upstream
-git push upstream --delete gh-pages > /dev/null 2>&1
-
-touch .
-
-git add -A .
-git commit -m "rebuild pages at ${rev}"
-git push -q upstream HEAD:gh-pages > /dev/null 2>&1
diff --git a/examples/example.rs b/examples/example.rs
index 8f02337..0caa464 100644
--- a/examples/example.rs
+++ b/examples/example.rs
@@ -2,11 +2,11 @@
extern crate rustyline;
use std::io::{self, Write};
-use log::{LogRecord, LogLevel, LogLevelFilter, LogMetadata, SetLoggerError};
+use log::{LogLevel, LogLevelFilter, LogMetadata, LogRecord, SetLoggerError};
use rustyline::completion::FilenameCompleter;
use rustyline::error::ReadlineError;
-use rustyline::{Cmd, Config, CompletionType, Editor, EditMode, KeyPress};
+use rustyline::{Cmd, CompletionType, Config, EditMode, Editor, KeyPress};
// On unix platforms you can use ANSI escape sequences
#[cfg(unix)]
@@ -72,7 +72,7 @@
fn init_logger() -> Result<(), SetLoggerError> {
log::set_logger(|max_log_level| {
- max_log_level.set(LogLevelFilter::Info);
- Box::new(Logger)
- })
+ max_log_level.set(LogLevelFilter::Info);
+ Box::new(Logger)
+ })
}
diff --git a/index.html b/index.html
deleted file mode 100644
index 269d4c6..0000000
--- a/index.html
+++ /dev/null
@@ -1 +0,0 @@
-<meta http-equiv=refresh content=0;url=rustyline/index.html>
diff --git a/rustfmt.toml b/rustfmt.toml
index 6496add..ea29d79 100644
--- a/rustfmt.toml
+++ b/rustfmt.toml
@@ -1,3 +1,6 @@
-reorder_imports = false
-normalise_comments = false
+closure_block_indent_threshold = 0
+wrap_comments = true
+force_format_strings = true
+format_strings = true
+reorder_imported_names = true
write_mode = "Overwrite"
\ No newline at end of file
diff --git a/src/char_iter.rs b/src/char_iter.rs
deleted file mode 100644
index 1866dbd..0000000
--- a/src/char_iter.rs
+++ /dev/null
@@ -1,111 +0,0 @@
-//! An iterator over the `char`s of a reader.
-//!
-//! A copy of the unstable code from the stdlib's std::io::Read::chars.
-//! TODO: Remove this once [Read::chars](https://github.com/rust-lang/rust/issues/27802) has been stabilized
-
-use std::error;
-use std::fmt;
-use std::io;
-use std::io::Read;
-use std::str;
-
-pub fn chars<R: Read>(read: R) -> Chars<R>
- where R: Sized
-{
- Chars { inner: read }
-}
-
-// https://tools.ietf.org/html/rfc3629
-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
-];
-
-/// Given a first byte, determine how many bytes are in this UTF-8 character
-#[inline]
-fn utf8_char_width(b: u8) -> usize {
- return UTF8_CHAR_WIDTH[b as usize] as usize;
-}
-
-pub struct Chars<R> {
- inner: R,
-}
-
-#[derive(Debug)]
-pub enum CharsError {
- NotUtf8,
- Other(io::Error),
-}
-
-impl<R: Read> Iterator for Chars<R> {
- type Item = Result<char, CharsError>;
-
- fn next(&mut self) -> Option<Result<char, CharsError>> {
- let mut buf = [0];
- let first_byte = match self.inner.read(&mut buf) {
- Ok(0) => return None,
- Ok(..) => buf[0],
- Err(e) => return Some(Err(CharsError::Other(e))),
- };
- let width = utf8_char_width(first_byte);
- if width == 1 {
- return Some(Ok(first_byte as char));
- }
- if width == 0 {
- return Some(Err(CharsError::NotUtf8));
- }
- let mut buf = [first_byte, 0, 0, 0];
- {
- let mut start = 1;
- while start < width {
- match self.inner.read(&mut buf[start..width]) {
- Ok(0) => return Some(Err(CharsError::NotUtf8)),
- Ok(n) => start += n,
- Err(e) => return Some(Err(CharsError::Other(e))),
- }
- }
- }
- Some(match str::from_utf8(&buf[..width]).ok() {
- Some(s) => Ok(s.chars().next().unwrap()),
- None => Err(CharsError::NotUtf8),
- })
- }
-}
-
-impl error::Error for CharsError {
- fn description(&self) -> &str {
- match *self {
- CharsError::NotUtf8 => "invalid utf8 encoding",
- CharsError::Other(ref e) => error::Error::description(e),
- }
- }
- fn cause(&self) -> Option<&error::Error> {
- match *self {
- CharsError::NotUtf8 => None,
- CharsError::Other(ref e) => e.cause(),
- }
- }
-}
-
-impl fmt::Display for CharsError {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- match *self {
- CharsError::NotUtf8 => "byte stream did not contain valid utf8".fmt(f),
- CharsError::Other(ref e) => e.fmt(f),
- }
- }
-}
diff --git a/src/completion.rs b/src/completion.rs
index 9c07c4d..fac3366 100644
--- a/src/completion.rs
+++ b/src/completion.rs
@@ -8,13 +8,15 @@
use line_buffer::LineBuffer;
// 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")
+// (line, pos) is like (rl_line_buffer, rl_point) to make contextual completion
+// ("select t.na| from tbl as t")
// TOOD: make &self &mut self ???
/// To be called for tab-completion.
pub trait Completer {
/// 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.
+ /// returns the start position and the completion candidates for the
+ /// partial word to be completed.
/// "ls /usr/loc" => Ok((3, vec!["/usr/local/"]))
fn complete(&self, line: &str, pos: usize) -> Result<(usize, Vec<String>)>;
/// Updates the edited `line` with the `elected` candidate.
@@ -66,14 +68,49 @@
}
#[cfg(unix)]
-static DEFAULT_BREAK_CHARS: [char; 18] = [' ', '\t', '\n', '"', '\\', '\'', '`', '@', '$', '>',
- '<', '=', ';', '|', '&', '{', '(', '\0'];
+static DEFAULT_BREAK_CHARS: [char; 18] = [
+ ' ',
+ '\t',
+ '\n',
+ '"',
+ '\\',
+ '\'',
+ '`',
+ '@',
+ '$',
+ '>',
+ '<',
+ '=',
+ ';',
+ '|',
+ '&',
+ '{',
+ '(',
+ '\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: [char; 17] = [
+ ' ',
+ '\t',
+ '\n',
+ '"',
+ '\'',
+ '`',
+ '@',
+ '$',
+ '>',
+ '<',
+ '=',
+ ';',
+ '|',
+ '&',
+ '{',
+ '(',
+ '\0',
+];
#[cfg(windows)]
static ESCAPE_CHAR: Option<char> = None;
@@ -124,7 +161,7 @@
/// 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.
+/// when space is a breaking char and '\\' the escape char.
pub fn escape(input: String, esc_char: Option<char>, break_chars: &BTreeSet<char>) -> String {
if esc_char.is_none() {
return input;
@@ -148,10 +185,11 @@
result
}
-fn filename_complete(path: &str,
- esc_char: Option<char>,
- break_chars: &BTreeSet<char>)
- -> Result<Vec<String>> {
+fn filename_complete(
+ path: &str,
+ esc_char: Option<char>,
+ break_chars: &BTreeSet<char>,
+) -> Result<Vec<String>> {
use std::env::{current_dir, home_dir};
let sep = path::MAIN_SEPARATOR;
@@ -202,11 +240,12 @@
/// try to find backward the start of a word.
/// Return (0, `line[..pos]`) if no break char has been found.
/// Return the word and its start position (idx, `line[idx..pos]`) otherwise.
-pub fn extract_word<'l>(line: &'l str,
- pos: usize,
- esc_char: Option<char>,
- break_chars: &BTreeSet<char>)
- -> (usize, &'l str) {
+pub fn extract_word<'l>(
+ line: &'l str,
+ pos: usize,
+ esc_char: Option<char>,
+ break_chars: &BTreeSet<char>,
+) -> (usize, &'l str) {
let line = &line[..pos];
if line.is_empty() {
return (0, line);
@@ -248,7 +287,8 @@
let b1 = c1.as_bytes();
let b2 = candidates[i + 1].as_bytes();
if b1.len() <= longest_common_prefix || b2.len() <= longest_common_prefix ||
- b1[longest_common_prefix] != b2[longest_common_prefix] {
+ b1[longest_common_prefix] != b2[longest_common_prefix]
+ {
break 'o;
}
}
@@ -271,11 +311,15 @@
pub fn extract_word() {
let break_chars: BTreeSet<char> = super::DEFAULT_BREAK_CHARS.iter().cloned().collect();
let line = "ls '/usr/local/b";
- assert_eq!((4, "/usr/local/b"),
- super::extract_word(line, line.len(), Some('\\'), &break_chars));
+ assert_eq!(
+ (4, "/usr/local/b"),
+ super::extract_word(line, line.len(), Some('\\'), &break_chars)
+ );
let line = "ls /User\\ Information";
- assert_eq!((3, "/User\\ Information"),
- super::extract_word(line, line.len(), Some('\\'), &break_chars));
+ assert_eq!(
+ (3, "/User\\ Information"),
+ super::extract_word(line, line.len(), Some('\\'), &break_chars)
+ );
}
#[test]
@@ -292,8 +336,10 @@
pub fn escape() {
let break_chars: BTreeSet<char> = super::DEFAULT_BREAK_CHARS.iter().cloned().collect();
let input = String::from("/usr/local/b");
- assert_eq!(input.clone(),
- super::escape(input, Some('\\'), &break_chars));
+ assert_eq!(
+ input.clone(),
+ super::escape(input, Some('\\'), &break_chars)
+ );
let input = String::from("/User Information");
let result = String::from("/User\\ Information");
assert_eq!(result, super::escape(input, Some('\\'), &break_chars));
diff --git a/src/config.rs b/src/config.rs
index 3be614c..b621a27 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1,6 +1,7 @@
//! Customize line editor
use std::default::Default;
+/// User preferences
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Config {
/// Maximum number of entries in History.
@@ -11,7 +12,8 @@
/// When listing completion alternatives, only display
/// one screen of possibilities at a time.
completion_prompt_limit: usize,
- /// Duration (milliseconds) Rustyline will wait for a character when reading an ambiguous key sequence.
+ /// Duration (milliseconds) Rustyline will wait for a character when
+ /// reading an ambiguous key sequence.
keyseq_timeout: i32,
// Emacs or Vi mode
edit_mode: EditMode,
@@ -27,13 +29,15 @@
self.max_history_size
}
- /// Tell if lines which match the previous history entry are saved or not in the history list.
+ /// Tell if lines which match the previous history entry are saved or not
+ /// in the history list.
/// By default, they are ignored.
pub fn history_duplicates(&self) -> HistoryDuplicates {
self.history_duplicates
}
- /// Tell if lines which begin with a space character are saved or not in the history list.
+ /// Tell if lines which begin with a space character are saved or not in
+ /// the history list.
/// By default, they are saved.
pub fn history_ignore_space(&self) -> bool {
self.history_ignore_space
@@ -73,6 +77,7 @@
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum HistoryDuplicates {
AlwaysAdd,
+ /// a line will not be added to the history if it matches the previous entry
IgnoreConsecutive,
}
@@ -86,12 +91,14 @@
List,
}
+/// Style of editing / Standard keymaps
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum EditMode {
Emacs,
Vi,
}
+/// Configuration builder
#[derive(Debug, Default)]
pub struct Builder {
p: Config,
@@ -108,7 +115,8 @@
self
}
- /// Tell if lines which match the previous history entry are saved or not in the history list.
+ /// Tell if lines which match the previous history entry are saved or not
+ /// in the history list.
/// By default, they are ignored.
pub fn history_ignore_dups(mut self, yes: bool) -> Builder {
self.p.history_duplicates = if yes {
@@ -119,7 +127,8 @@
self
}
- /// Tell if lines which begin with a space character are saved or not in the history list.
+ /// Tell if lines which begin with a space character are saved or not in
+ /// the history list.
/// By default, they are saved.
pub fn history_ignore_space(mut self, yes: bool) -> Builder {
self.p.history_ignore_space = yes;
@@ -132,7 +141,8 @@
self
}
- /// The number of possible completions that determines when the user is asked
+ /// The number of possible completions that determines when the user is
+ /// asked
/// whether the list of possibilities should be displayed.
pub fn completion_prompt_limit(mut self, completion_prompt_limit: usize) -> Builder {
self.p.completion_prompt_limit = completion_prompt_limit;
@@ -140,8 +150,10 @@
}
/// Timeout for ambiguous key sequences in milliseconds.
- /// Currently, it is used only to distinguish a single ESC from an ESC sequence.
- /// After seeing an ESC key, wait at most `keyseq_timeout_ms` for another byte.
+ /// Currently, it is used only to distinguish a single ESC from an ESC
+ /// sequence.
+ /// After seeing an ESC key, wait at most `keyseq_timeout_ms` for another
+ /// byte.
pub fn keyseq_timeout(mut self, keyseq_timeout_ms: i32) -> Builder {
self.p.keyseq_timeout = keyseq_timeout_ms;
self
diff --git a/src/consts.rs b/src/consts.rs
index aadac84..c20ff72 100644
--- a/src/consts.rs
+++ b/src/consts.rs
@@ -5,19 +5,29 @@
UnknownEscSeq,
Backspace,
Char(char),
+ ControlDown,
+ ControlLeft,
+ ControlRight,
+ ControlUp,
Ctrl(char),
Delete,
Down,
End,
Enter, // Ctrl('M')
Esc,
+ F(u8),
Home,
+ Insert,
Left,
Meta(char),
Null,
PageDown,
PageUp,
Right,
+ ShiftDown,
+ ShiftLeft,
+ ShiftRight,
+ ShiftUp,
Tab, // Ctrl('I')
Up,
}
@@ -28,7 +38,7 @@
return KeyPress::Char(c);
}
match c {
- '\x00' => KeyPress::Null,
+ '\x00' => KeyPress::Ctrl(' '),
'\x01' => KeyPress::Ctrl('A'),
'\x02' => KeyPress::Ctrl('B'),
'\x03' => KeyPress::Ctrl('C'),
@@ -37,12 +47,13 @@
'\x06' => KeyPress::Ctrl('F'),
'\x07' => KeyPress::Ctrl('G'),
'\x08' => KeyPress::Backspace, // '\b'
- '\x09' => KeyPress::Tab,
+ '\x09' => KeyPress::Tab, // '\t'
'\x0a' => KeyPress::Ctrl('J'), // '\n' (10)
'\x0b' => KeyPress::Ctrl('K'),
'\x0c' => KeyPress::Ctrl('L'),
'\x0d' => KeyPress::Enter, // '\r' (13)
'\x0e' => KeyPress::Ctrl('N'),
+ '\x0f' => KeyPress::Ctrl('O'),
'\x10' => KeyPress::Ctrl('P'),
'\x12' => KeyPress::Ctrl('R'),
'\x13' => KeyPress::Ctrl('S'),
@@ -50,17 +61,22 @@
'\x15' => KeyPress::Ctrl('U'),
'\x16' => KeyPress::Ctrl('V'),
'\x17' => KeyPress::Ctrl('W'),
+ '\x18' => KeyPress::Ctrl('X'),
'\x19' => KeyPress::Ctrl('Y'),
'\x1a' => KeyPress::Ctrl('Z'),
- '\x1b' => KeyPress::Esc,
- '\x7f' => KeyPress::Backspace,
+ '\x1b' => KeyPress::Esc, // Ctrl-[
+ '\x1c' => KeyPress::Ctrl('\\'),
+ '\x1d' => KeyPress::Ctrl(']'),
+ '\x1e' => KeyPress::Ctrl('^'),
+ '\x1f' => KeyPress::Ctrl('_'),
+ '\x7f' => KeyPress::Backspace, // Rubout
_ => KeyPress::Null,
}
}
#[cfg(test)]
mod tests {
- use super::{char_to_key_press, KeyPress};
+ use super::{KeyPress, char_to_key_press};
#[test]
fn char_to_key() {
diff --git a/src/error.rs b/src/error.rs
index 009ec65..3df6e70 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -7,9 +7,6 @@
#[cfg(unix)]
use nix;
-#[cfg(unix)]
-use char_iter;
-
/// The error type for Rustyline errors that can arise from
/// I/O related errors or Errno when using the nix-rust library
#[derive(Debug)]
@@ -22,7 +19,7 @@
Interrupted,
/// Chars Error
#[cfg(unix)]
- Char(char_iter::CharsError),
+ Char(io::CharsError),
/// Unix Error from syscall
#[cfg(unix)]
Errno(nix::Error),
@@ -82,8 +79,8 @@
}
#[cfg(unix)]
-impl From<char_iter::CharsError> for ReadlineError {
- fn from(err: char_iter::CharsError) -> ReadlineError {
+impl From<io::CharsError> for ReadlineError {
+ fn from(err: io::CharsError) -> ReadlineError {
ReadlineError::Char(err)
}
}
diff --git a/src/history.rs b/src/history.rs
index e283cb4..68d355c 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -12,6 +12,7 @@
use super::Result;
use config::{Config, HistoryDuplicates};
+/// Search direction
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Direction {
Forward,
@@ -56,11 +57,12 @@
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;
}
if self.ignore_dups {
@@ -88,7 +90,8 @@
/// Set the maximum length for the history. This function can be called even
/// if there is already some history, the function will make sure to retain
- /// just the latest `len` elements if the new history length value is smaller
+ /// just the latest `len` elements if the new history length value is
+ /// smaller
/// than the amount of items already inside the history.
pub fn set_max_len(&mut self, len: usize) {
self.max_len = len;
@@ -105,6 +108,10 @@
}
/// Save the history in the specified file.
+ /// TODO append_history
+ /// http://cnswww.cns.cwru.edu/php/chet/readline/history.html#IDX30
+ /// TODO history_truncate_file
+ /// http://cnswww.cns.cwru.edu/php/chet/readline/history.html#IDX31
pub fn save<P: AsRef<Path> + ?Sized>(&self, path: &P) -> Result<()> {
use std::io::{BufWriter, Write};
@@ -144,22 +151,26 @@
self.entries.clear()
}
- /// Search history (start position inclusive [0, len-1])
- /// Return the absolute index of the nearest history entry that matches `term`.
- /// Return None if no entry contains `term` between [start, len -1] for forward search
+ /// Search history (start position inclusive [0, len-1]).
+ /// Return the absolute index of the nearest history entry that matches
+ /// `term`.
+ /// Return None if no entry contains `term` between [start, len -1] for
+ /// forward search
/// or between [0, start] for reverse search.
pub fn search(&self, term: &str, start: usize, dir: Direction) -> Option<usize> {
let test = |entry: &String| entry.contains(term);
self.search_match(term, start, dir, test)
}
+ /// Anchored search
pub fn starts_with(&self, term: &str, start: usize, dir: Direction) -> Option<usize> {
let test = |entry: &String| entry.starts_with(term);
self.search_match(term, start, dir, test)
}
fn search_match<F>(&self, term: &str, start: usize, dir: Direction, test: F) -> Option<usize>
- where F: Fn(&String) -> bool
+ where
+ F: Fn(&String) -> bool,
{
if term.is_empty() || start >= self.len() {
return None;
@@ -255,8 +266,8 @@
mod tests {
extern crate tempdir;
use std::path::Path;
- use super::{Direction, History};
use config::Config;
+ use super::{Direction, History};
fn init() -> History {
let mut history = History::new();
@@ -289,7 +300,7 @@
let mut history = init();
history.set_max_len(1);
assert_eq!(1, history.entries.len());
- assert_eq!(Some(&"line3".to_string()), history.last());
+ assert_eq!(Some(&"line3".to_owned()), history.last());
}
#[test]
diff --git a/src/keymap.rs b/src/keymap.rs
index 7141066..4c57f73 100644
--- a/src/keymap.rs
+++ b/src/keymap.rs
@@ -9,40 +9,76 @@
use tty::RawReader;
use super::Result;
+/// The number of times one command should be repeated.
pub type RepeatCount = usize;
+/// Commands
#[derive(Debug, Clone, PartialEq)]
pub enum Cmd {
+ /// abort
Abort, // Miscellaneous Command
+ /// accept-line
AcceptLine,
+ /// beginning-of-history
BeginningOfHistory,
+ /// capitalize-word
CapitalizeWord,
+ /// clear-screen
ClearScreen,
+ /// complete
Complete,
+ /// downcase-word
DowncaseWord,
+ /// vi-eof-maybe
EndOfFile,
+ /// end-of-history
EndOfHistory,
+ /// forward-search-history
ForwardSearchHistory,
+ /// history-search-backward
HistorySearchBackward,
+ /// history-search-forward
HistorySearchForward,
Insert(RepeatCount, String),
Interrupt,
+ /// backward-delete-char, backward-kill-line, backward-kill-word
+ /// delete-char, kill-line, kill-word, unix-line-discard, unix-word-rubout,
+ /// vi-change-to, vi-delete, vi-delete-to, vi-rubout, vi-subst
Kill(Movement),
+ /// backward-char, backward-word, beginning-of-line, end-of-line,
+ /// forward-char, forward-word, vi-char-search, vi-end-word, vi-next-word,
+ /// vi-prev-word
Move(Movement),
+ /// next-history
NextHistory,
Noop,
+ /// vi-replace
+ Overwrite(char),
+ /// previous-history
PreviousHistory,
+ /// quoted-insert
QuotedInsert,
+ /// vi-change-char
Replace(RepeatCount, char),
+ /// reverse-search-history
ReverseSearchHistory,
+ /// self-insert
SelfInsert(RepeatCount, char),
Suspend,
+ /// transpose-chars
TransposeChars,
+ /// transpose-words
TransposeWords(RepeatCount),
+ /// undo
+ Undo,
Unknown,
+ /// upcase-word
UpcaseWord,
+ /// vi-yank-to
ViYankTo(Movement),
+ /// yank, vi-put
Yank(RepeatCount, Anchor),
+ /// yank-pop
YankPop,
}
@@ -59,13 +95,13 @@
fn is_repeatable_change(&self) -> bool {
match *self {
- Cmd::Insert(_, _) => true,
- Cmd::Kill(_) => true,
- Cmd::Replace(_, _) => true,
- Cmd::SelfInsert(_, _) => true,
- Cmd::TransposeChars => false, // TODO Validate
- Cmd::ViYankTo(_) => true,
+ Cmd::Insert(_, _) |
+ Cmd::Kill(_) |
+ Cmd::Replace(_, _) |
+ Cmd::SelfInsert(_, _) |
+ Cmd::ViYankTo(_) |
Cmd::Yank(_, _) => true,
+ Cmd::TransposeChars => false, // TODO Validate
_ => false,
}
}
@@ -85,7 +121,7 @@
Cmd::Move(ref mvt) => Cmd::Move(mvt.redo(new)),
Cmd::Replace(previous, c) => Cmd::Replace(repeat_count(previous, new), c),
Cmd::SelfInsert(previous, c) => Cmd::SelfInsert(repeat_count(previous, new), c),
- //Cmd::TransposeChars => Cmd::TransposeChars,
+ // Cmd::TransposeChars => Cmd::TransposeChars,
Cmd::ViYankTo(ref mvt) => Cmd::ViYankTo(mvt.redo(new)),
Cmd::Yank(previous, anchor) => Cmd::Yank(repeat_count(previous, new), anchor),
_ => unreachable!(),
@@ -100,16 +136,18 @@
}
}
+/// Different word definitions
#[derive(Debug, Clone, PartialEq, Copy)]
pub enum Word {
- // non-blanks characters
+ /// non-blanks characters
Big,
- // alphanumeric characters
+ /// alphanumeric characters
Emacs,
- // alphanumeric (and '_') characters
+ /// alphanumeric (and '_') characters
Vi,
}
+/// Where to move with respect to word boundary
#[derive(Debug, Clone, PartialEq, Copy)]
pub enum At {
Start,
@@ -117,12 +155,14 @@
AfterEnd,
}
+/// Where to paste (relative to cursor position)
#[derive(Debug, Clone, PartialEq, Copy)]
pub enum Anchor {
After,
Before,
}
+/// Vi character search
#[derive(Debug, Clone, PartialEq)]
pub enum CharSearch {
Forward(char),
@@ -144,17 +184,25 @@
}
}
-
+/// Where to move
#[derive(Debug, Clone, PartialEq)]
pub enum Movement {
WholeLine, // not really a movement
+ /// beginning-of-line
BeginningOfLine,
+ /// end-of-line
EndOfLine,
+ /// backward-word, vi-prev-word
BackwardWord(RepeatCount, Word), // Backward until start of word
+ /// forward-word, vi-end-word, vi-next-word
ForwardWord(RepeatCount, At, Word), // Forward until start/end of word
+ /// vi-char-search
ViCharSearch(RepeatCount, CharSearch),
+ /// vi-first-print
ViFirstPrint,
+ /// backward-char
BackwardChar(RepeatCount),
+ /// forward-char
ForwardChar(RepeatCount),
}
@@ -180,11 +228,21 @@
}
}
+#[derive(PartialEq)]
+enum InputMode {
+ /// Vi Command/Alternate
+ Command,
+ /// Insert/Input mode
+ Insert,
+ /// Overwrite mode
+ Replace,
+}
+
+/// Tranform key(s) to commands based on current input mode
pub struct EditState {
mode: EditMode,
custom_bindings: Rc<RefCell<HashMap<KeyPress, Cmd>>>,
- // Vi Command/Alternate, Insert/Input mode
- insert: bool, // vi only ?
+ input_mode: InputMode, // vi only ?
// numeric arguments: http://web.mit.edu/gnu/doc/html/rlman_1.html#SEC7
num_args: i16,
last_cmd: Cmd, // vi only
@@ -197,7 +255,7 @@
EditState {
mode: config.edit_mode(),
custom_bindings: custom_bindings,
- insert: true,
+ input_mode: InputMode::Insert,
num_args: 0,
last_cmd: Cmd::Noop,
consecutive_insert: false,
@@ -209,10 +267,11 @@
self.mode == EditMode::Emacs
}
+ /// Parse user input into one command
pub fn next_cmd<R: RawReader>(&mut self, rdr: &mut R) -> Result<Cmd> {
match self.mode {
EditMode::Emacs => self.emacs(rdr),
- EditMode::Vi if self.insert => self.vi_insert(rdr),
+ EditMode::Vi if self.input_mode != InputMode::Command => self.vi_insert(rdr),
EditMode::Vi => self.vi_command(rdr),
}
}
@@ -236,9 +295,10 @@
if self.num_args == -1 {
self.num_args *= digit.to_digit(10).unwrap() as i16;
} else {
- self.num_args = self.num_args
- .saturating_mul(10)
- .saturating_add(digit.to_digit(10).unwrap() as i16);
+ self.num_args = self.num_args.saturating_mul(10).saturating_add(
+ digit.to_digit(10).unwrap() as
+ i16,
+ );
}
}
_ => return Ok(key),
@@ -257,10 +317,10 @@
if let Some(cmd) = self.custom_bindings.borrow().get(&key) {
debug!(target: "rustyline", "Custom command: {:?}", cmd);
return Ok(if cmd.is_repeatable() {
- cmd.redo(Some(n))
- } else {
- cmd.clone()
- });
+ cmd.redo(Some(n))
+ } else {
+ cmd.clone()
+ });
}
let cmd = match key {
KeyPress::Char(c) => {
@@ -287,7 +347,8 @@
}
}
KeyPress::Ctrl('G') |
- KeyPress::Esc => Cmd::Abort,
+ KeyPress::Esc |
+ KeyPress::Meta('\x07') => Cmd::Abort,
KeyPress::Ctrl('H') |
KeyPress::Backspace => {
if positive {
@@ -307,6 +368,14 @@
KeyPress::Ctrl('L') => Cmd::ClearScreen,
KeyPress::Ctrl('N') => Cmd::NextHistory,
KeyPress::Ctrl('P') => Cmd::PreviousHistory,
+ KeyPress::Ctrl('X') => {
+ let snd_key = try!(rdr.next_key());
+ match snd_key {
+ KeyPress::Ctrl('G') => Cmd::Abort,
+ KeyPress::Ctrl('U') => Cmd::Undo,
+ _ => Cmd::Unknown,
+ }
+ }
KeyPress::Meta('\x08') |
KeyPress::Meta('\x7f') => {
if positive {
@@ -317,32 +386,40 @@
}
KeyPress::Meta('<') => Cmd::BeginningOfHistory,
KeyPress::Meta('>') => Cmd::EndOfHistory,
- KeyPress::Meta('B') => {
+ KeyPress::Meta('B') |
+ KeyPress::Meta('b') => {
if positive {
Cmd::Move(Movement::BackwardWord(n, Word::Emacs))
} else {
Cmd::Move(Movement::ForwardWord(n, At::AfterEnd, Word::Emacs))
}
}
- KeyPress::Meta('C') => Cmd::CapitalizeWord,
- KeyPress::Meta('D') => {
+ KeyPress::Meta('C') |
+ KeyPress::Meta('c') => Cmd::CapitalizeWord,
+ KeyPress::Meta('D') |
+ KeyPress::Meta('d') => {
if positive {
Cmd::Kill(Movement::ForwardWord(n, At::AfterEnd, Word::Emacs))
} else {
Cmd::Kill(Movement::BackwardWord(n, Word::Emacs))
}
}
- KeyPress::Meta('F') => {
+ KeyPress::Meta('F') |
+ KeyPress::Meta('f') => {
if positive {
Cmd::Move(Movement::ForwardWord(n, At::AfterEnd, Word::Emacs))
} else {
Cmd::Move(Movement::BackwardWord(n, Word::Emacs))
}
}
- KeyPress::Meta('L') => Cmd::DowncaseWord,
- KeyPress::Meta('T') => Cmd::TransposeWords(n),
- KeyPress::Meta('U') => Cmd::UpcaseWord,
- KeyPress::Meta('Y') => Cmd::YankPop,
+ KeyPress::Meta('L') |
+ KeyPress::Meta('l') => Cmd::DowncaseWord,
+ KeyPress::Meta('T') |
+ KeyPress::Meta('t') => Cmd::TransposeWords(n),
+ KeyPress::Meta('U') |
+ KeyPress::Meta('u') => Cmd::UpcaseWord,
+ KeyPress::Meta('Y') |
+ KeyPress::Meta('y') => Cmd::YankPop,
_ => self.common(key, n, positive),
};
debug!(target: "rustyline", "Emacs command: {:?}", cmd);
@@ -355,9 +432,10 @@
let key = try!(rdr.next_key());
match key {
KeyPress::Char(digit @ '0'...'9') => {
- self.num_args = self.num_args
- .saturating_mul(10)
- .saturating_add(digit.to_digit(10).unwrap() as i16);
+ self.num_args = self.num_args.saturating_mul(10).saturating_add(
+ digit.to_digit(10).unwrap() as
+ i16,
+ );
}
_ => return Ok(key),
};
@@ -374,14 +452,14 @@
if let Some(cmd) = self.custom_bindings.borrow().get(&key) {
debug!(target: "rustyline", "Custom command: {:?}", cmd);
return Ok(if cmd.is_repeatable() {
- if no_num_args {
- cmd.redo(None)
- } else {
- cmd.redo(Some(n))
- }
- } else {
- cmd.clone()
- });
+ if no_num_args {
+ cmd.redo(None)
+ } else {
+ cmd.redo(Some(n))
+ }
+ } else {
+ cmd.clone()
+ });
}
let cmd = match key {
KeyPress::Char('$') |
@@ -397,26 +475,26 @@
KeyPress::Char('0') => Cmd::Move(Movement::BeginningOfLine),
KeyPress::Char('^') => Cmd::Move(Movement::ViFirstPrint),
KeyPress::Char('a') => {
- // vi-append-mode: Vi enter insert mode after the cursor.
- self.insert = true;
+ // vi-append-mode
+ self.input_mode = InputMode::Insert;
Cmd::Move(Movement::ForwardChar(n))
}
KeyPress::Char('A') => {
- // vi-append-eol: Vi enter insert mode at end of line.
- self.insert = true;
+ // vi-append-eol
+ self.input_mode = InputMode::Insert;
Cmd::Move(Movement::EndOfLine)
}
KeyPress::Char('b') => Cmd::Move(Movement::BackwardWord(n, Word::Vi)), // vi-prev-word
KeyPress::Char('B') => Cmd::Move(Movement::BackwardWord(n, Word::Big)),
KeyPress::Char('c') => {
- self.insert = true;
+ self.input_mode = InputMode::Insert;
match try!(self.vi_cmd_motion(rdr, key, n)) {
Some(mvt) => Cmd::Kill(mvt),
None => Cmd::Unknown,
}
}
KeyPress::Char('C') => {
- self.insert = true;
+ self.input_mode = InputMode::Insert;
Cmd::Kill(Movement::EndOfLine)
}
KeyPress::Char('d') => {
@@ -431,12 +509,12 @@
KeyPress::Char('E') => Cmd::Move(Movement::ForwardWord(n, At::BeforeEnd, Word::Big)),
KeyPress::Char('i') => {
// vi-insertion-mode
- self.insert = true;
+ self.input_mode = InputMode::Insert;
Cmd::Noop
}
KeyPress::Char('I') => {
// vi-insert-beg
- self.insert = true;
+ self.input_mode = InputMode::Insert;
Cmd::Move(Movement::BeginningOfLine)
}
KeyPress::Char(c) if c == 'f' || c == 'F' || c == 't' || c == 'T' => {
@@ -463,7 +541,7 @@
KeyPress::Char('p') => Cmd::Yank(n, Anchor::After), // vi-put
KeyPress::Char('P') => Cmd::Yank(n, Anchor::Before), // vi-put
KeyPress::Char('r') => {
- // vi-replace-char: Vi replace character under the cursor with the next character typed.
+ // vi-replace-char:
let ch = try!(rdr.next_key());
match ch {
KeyPress::Char(c) => Cmd::Replace(n, c),
@@ -471,17 +549,22 @@
_ => Cmd::Unknown,
}
}
- // TODO KeyPress::Char('R') => Cmd::???, vi-replace-mode: Vi enter replace mode. Replaces characters under the cursor. (overwrite-mode)
+ KeyPress::Char('R') => {
+ // vi-replace-mode (overwrite-mode)
+ self.input_mode = InputMode::Replace;
+ Cmd::Noop
+ }
KeyPress::Char('s') => {
- // vi-substitute-char: Vi replace character under the cursor and enter insert mode.
- self.insert = true;
+ // vi-substitute-char:
+ self.input_mode = InputMode::Insert;
Cmd::Kill(Movement::ForwardChar(n))
}
KeyPress::Char('S') => {
- // vi-substitute-line: Vi substitute entire line.
- self.insert = true;
+ // vi-substitute-line:
+ self.input_mode = InputMode::Insert;
Cmd::Kill(Movement::WholeLine)
}
+ KeyPress::Char('u') => Cmd::Undo,
// KeyPress::Char('U') => Cmd::???, // revert-line
KeyPress::Char('w') => Cmd::Move(Movement::ForwardWord(n, At::Start, Word::Vi)), // vi-next-word
KeyPress::Char('W') => Cmd::Move(Movement::ForwardWord(n, At::Start, Word::Big)), // vi-next-word
@@ -508,11 +591,11 @@
KeyPress::Char('k') | // TODO: move to the start of the line.
KeyPress::Ctrl('P') => Cmd::PreviousHistory,
KeyPress::Ctrl('R') => {
- self.insert = true; // TODO Validate
+ self.input_mode = InputMode::Insert; // TODO Validate
Cmd::ReverseSearchHistory
}
KeyPress::Ctrl('S') => {
- self.insert = true; // TODO Validate
+ self.input_mode = InputMode::Insert; // TODO Validate
Cmd::ForwardSearchHistory
}
KeyPress::Esc => Cmd::Noop,
@@ -530,19 +613,25 @@
if let Some(cmd) = self.custom_bindings.borrow().get(&key) {
debug!(target: "rustyline", "Custom command: {:?}", cmd);
return Ok(if cmd.is_repeatable() {
- cmd.redo(None)
- } else {
- cmd.clone()
- });
+ cmd.redo(None)
+ } else {
+ cmd.clone()
+ });
}
let cmd = match key {
- KeyPress::Char(c) => Cmd::SelfInsert(1, c),
+ KeyPress::Char(c) => {
+ if self.input_mode == InputMode::Replace {
+ Cmd::Overwrite(c)
+ } else {
+ Cmd::SelfInsert(1, c)
+ }
+ }
KeyPress::Ctrl('H') |
KeyPress::Backspace => Cmd::Kill(Movement::BackwardChar(1)),
KeyPress::Tab => Cmd::Complete,
KeyPress::Esc => {
- // vi-movement-mode/vi-command-mode: Vi enter command mode (use alternative key bindings).
- self.insert = false;
+ // vi-movement-mode/vi-command-mode
+ self.input_mode = InputMode::Command;
Cmd::Move(Movement::BackwardChar(1))
}
_ => self.common(key, 1, true),
@@ -558,11 +647,12 @@
Ok(cmd)
}
- fn vi_cmd_motion<R: RawReader>(&mut self,
- rdr: &mut R,
- key: KeyPress,
- n: RepeatCount)
- -> Result<Option<Movement>> {
+ fn vi_cmd_motion<R: RawReader>(
+ &mut self,
+ rdr: &mut R,
+ key: KeyPress,
+ n: RepeatCount,
+ ) -> Result<Option<Movement>> {
let mut mvt = try!(rdr.next_key());
if mvt == key {
return Ok(Some(Movement::WholeLine));
@@ -574,76 +664,77 @@
n = self.vi_num_args().saturating_mul(n);
}
Ok(match mvt {
- KeyPress::Char('$') => Some(Movement::EndOfLine), // vi-change-to-eol: Vi change to end of line.
- KeyPress::Char('0') => Some(Movement::BeginningOfLine), // vi-kill-line-prev: Vi cut from beginning of line to cursor.
- KeyPress::Char('^') => Some(Movement::ViFirstPrint),
- KeyPress::Char('b') => Some(Movement::BackwardWord(n, Word::Vi)),
- KeyPress::Char('B') => Some(Movement::BackwardWord(n, Word::Big)),
- KeyPress::Char('e') => Some(Movement::ForwardWord(n, At::AfterEnd, Word::Vi)),
- KeyPress::Char('E') => Some(Movement::ForwardWord(n, At::AfterEnd, Word::Big)),
- KeyPress::Char(c) if c == 'f' || c == 'F' || c == 't' || c == 'T' => {
- let cs = try!(self.vi_char_search(rdr, c));
- match cs {
- Some(cs) => Some(Movement::ViCharSearch(n, cs)),
- None => None,
+ KeyPress::Char('$') => Some(Movement::EndOfLine),
+ KeyPress::Char('0') => Some(Movement::BeginningOfLine),
+ KeyPress::Char('^') => Some(Movement::ViFirstPrint),
+ KeyPress::Char('b') => Some(Movement::BackwardWord(n, Word::Vi)),
+ KeyPress::Char('B') => Some(Movement::BackwardWord(n, Word::Big)),
+ KeyPress::Char('e') => Some(Movement::ForwardWord(n, At::AfterEnd, Word::Vi)),
+ KeyPress::Char('E') => Some(Movement::ForwardWord(n, At::AfterEnd, Word::Big)),
+ KeyPress::Char(c) if c == 'f' || c == 'F' || c == 't' || c == 'T' => {
+ let cs = try!(self.vi_char_search(rdr, c));
+ match cs {
+ Some(cs) => Some(Movement::ViCharSearch(n, cs)),
+ None => None,
+ }
}
- }
- KeyPress::Char(';') => {
- match self.last_char_search {
- Some(ref cs) => Some(Movement::ViCharSearch(n, cs.clone())),
- None => None,
- }
- }
- KeyPress::Char(',') => {
- match self.last_char_search {
- Some(ref cs) => Some(Movement::ViCharSearch(n, cs.opposite())),
- None => None,
- }
- }
- KeyPress::Char('h') |
- KeyPress::Ctrl('H') |
- KeyPress::Backspace => Some(Movement::BackwardChar(n)), // vi-delete-prev-char: Vi move to previous character (backspace).
- KeyPress::Char('l') |
- KeyPress::Char(' ') => Some(Movement::ForwardChar(n)),
- KeyPress::Char('w') => {
- // 'cw' is 'ce'
- if key == KeyPress::Char('c') {
- Some(Movement::ForwardWord(n, At::AfterEnd, Word::Vi))
- } else {
- Some(Movement::ForwardWord(n, At::Start, Word::Vi))
+ KeyPress::Char(';') => {
+ match self.last_char_search {
+ Some(ref cs) => Some(Movement::ViCharSearch(n, cs.clone())),
+ None => None,
+ }
}
- }
- KeyPress::Char('W') => {
- // 'cW' is 'cE'
- if key == KeyPress::Char('c') {
- Some(Movement::ForwardWord(n, At::AfterEnd, Word::Big))
- } else {
- Some(Movement::ForwardWord(n, At::Start, Word::Big))
+ KeyPress::Char(',') => {
+ match self.last_char_search {
+ Some(ref cs) => Some(Movement::ViCharSearch(n, cs.opposite())),
+ None => None,
+ }
}
- }
- _ => None,
- })
+ KeyPress::Char('h') |
+ KeyPress::Ctrl('H') |
+ KeyPress::Backspace => Some(Movement::BackwardChar(n)),
+ KeyPress::Char('l') |
+ KeyPress::Char(' ') => Some(Movement::ForwardChar(n)),
+ KeyPress::Char('w') => {
+ // 'cw' is 'ce'
+ if key == KeyPress::Char('c') {
+ Some(Movement::ForwardWord(n, At::AfterEnd, Word::Vi))
+ } else {
+ Some(Movement::ForwardWord(n, At::Start, Word::Vi))
+ }
+ }
+ KeyPress::Char('W') => {
+ // 'cW' is 'cE'
+ if key == KeyPress::Char('c') {
+ Some(Movement::ForwardWord(n, At::AfterEnd, Word::Big))
+ } else {
+ Some(Movement::ForwardWord(n, At::Start, Word::Big))
+ }
+ }
+ _ => None,
+ })
}
- fn vi_char_search<R: RawReader>(&mut self,
- rdr: &mut R,
- cmd: char)
- -> Result<Option<CharSearch>> {
+ fn vi_char_search<R: RawReader>(
+ &mut self,
+ rdr: &mut R,
+ cmd: char,
+ ) -> Result<Option<CharSearch>> {
let ch = try!(rdr.next_key());
Ok(match ch {
- KeyPress::Char(ch) => {
- let cs = match cmd {
- 'f' => CharSearch::Forward(ch),
- 't' => CharSearch::ForwardBefore(ch),
- 'F' => CharSearch::Backward(ch),
- 'T' => CharSearch::BackwardAfter(ch),
- _ => unreachable!(),
- };
- self.last_char_search = Some(cs.clone());
- Some(cs)
- }
- _ => None,
- })
+ KeyPress::Char(ch) => {
+ let cs = match cmd {
+ 'f' => CharSearch::Forward(ch),
+ 't' => CharSearch::ForwardBefore(ch),
+ 'F' => CharSearch::Backward(ch),
+ 'T' => CharSearch::BackwardAfter(ch),
+ _ => unreachable!(),
+ };
+ self.last_char_search = Some(cs.clone());
+ Some(cs)
+ }
+ _ => None,
+ })
}
fn common(&mut self, key: KeyPress, n: RepeatCount, positive: bool) -> Cmd {
@@ -704,6 +795,7 @@
}
}
KeyPress::Ctrl('Z') => Cmd::Suspend,
+ KeyPress::Ctrl('_') => Cmd::Undo,
KeyPress::UnknownEscSeq => Cmd::Noop,
_ => Cmd::Unknown,
}
diff --git a/src/kill_ring.rs b/src/kill_ring.rs
index 635cb4f..5f18624 100644
--- a/src/kill_ring.rs
+++ b/src/kill_ring.rs
@@ -1,4 +1,5 @@
//! Kill Ring
+use line_buffer::{DeleteListener, Direction};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Action {
@@ -19,6 +20,7 @@
index: usize,
// whether or not the last command was a kill or a yank
last_action: Action,
+ killing: bool,
}
impl KillRing {
@@ -28,9 +30,17 @@
slots: Vec::with_capacity(size),
index: 0,
last_action: Action::Other,
+ killing: false,
}
}
+ pub fn start_killing(&mut self) {
+ self.killing = true;
+ }
+ pub fn stop_killing(&mut self) {
+ self.killing = false;
+ }
+
/// Reset `last_action` state.
pub fn reset(&mut self) {
self.last_action = Action::Other;
@@ -102,9 +112,22 @@
}
}
+impl DeleteListener for KillRing {
+ fn delete(&mut self, _: usize, string: &str, dir: Direction) {
+ if !self.killing {
+ return;
+ }
+ let mode = match dir {
+ Direction::Forward => Mode::Append,
+ Direction::Backward => Mode::Prepend,
+ };
+ self.kill(string, mode);
+ }
+}
+
#[cfg(test)]
mod tests {
- use super::{Action, Mode, KillRing};
+ use super::{Action, KillRing, Mode};
#[test]
fn disabled() {
@@ -187,9 +210,9 @@
kill_ring.reset();
kill_ring.kill("word2", Mode::Append);
- assert_eq!(Some(&"word2".to_string()), kill_ring.yank());
+ assert_eq!(Some(&"word2".to_owned()), kill_ring.yank());
assert_eq!(Action::Yank(5), kill_ring.last_action);
- assert_eq!(Some(&"word2".to_string()), kill_ring.yank());
+ assert_eq!(Some(&"word2".to_owned()), kill_ring.yank());
assert_eq!(Action::Yank(5), kill_ring.last_action);
}
@@ -202,8 +225,8 @@
assert_eq!(None, kill_ring.yank_pop());
kill_ring.yank();
- assert_eq!(Some((9, &"word1".to_string())), kill_ring.yank_pop());
- assert_eq!(Some((5, &"longword2".to_string())), kill_ring.yank_pop());
- assert_eq!(Some((9, &"word1".to_string())), kill_ring.yank_pop());
+ assert_eq!(Some((9, &"word1".to_owned())), kill_ring.yank_pop());
+ assert_eq!(Some((5, &"longword2".to_owned())), kill_ring.yank_pop());
+ assert_eq!(Some((9, &"word1".to_owned())), kill_ring.yank_pop());
}
}
diff --git a/src/lib.rs b/src/lib.rs
index 24829e0..7d51716 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,6 +1,7 @@
//! Readline for Rust
//!
-//! This implementation is based on [Antirez's Linenoise](https://github.com/antirez/linenoise)
+//! This implementation is based on [Antirez's
+//! Linenoise](https://github.com/antirez/linenoise)
//!
//! # Example
//!
@@ -14,12 +15,14 @@
//! Err(_) => println!("No input"),
//! }
//! ```
+#![feature(io)]
+#![feature(unicode)]
#![allow(unknown_lints)]
extern crate libc;
-extern crate encode_unicode;
#[macro_use]
extern crate log;
+extern crate std_unicode;
extern crate unicode_segmentation;
extern crate unicode_width;
#[cfg(unix)]
@@ -36,9 +39,8 @@
mod keymap;
mod kill_ring;
pub mod line_buffer;
-#[cfg(unix)]
-mod char_iter;
pub mod config;
+mod undo;
mod tty;
@@ -46,80 +48,73 @@
use std::collections::HashMap;
use std::fmt;
use std::io::{self, Write};
-use std::mem;
use std::path::Path;
-use std::rc::Rc;
use std::result;
+use std::rc::Rc;
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
-use tty::{RawMode, RawReader, Terminal, Term};
+use tty::{Position, RawMode, RawReader, Renderer, Term, Terminal};
-use encode_unicode::CharExt;
use completion::{Completer, longest_common_prefix};
use history::{Direction, History};
use line_buffer::{LineBuffer, MAX_LINE, WordAction};
pub use keymap::{Anchor, At, CharSearch, Cmd, Movement, RepeatCount, Word};
use keymap::EditState;
-use kill_ring::{Mode, KillRing};
+use kill_ring::{KillRing, Mode};
pub use config::{CompletionType, Config, EditMode, HistoryDuplicates};
+use undo::Changeset;
pub use consts::KeyPress;
/// The error type for I/O and Linux Syscalls (Errno)
pub type Result<T> = result::Result<T, error::ReadlineError>;
-// Represent the state during line editing.
+/// Represent the state during line editing.
+/// Implement rendering.
struct State<'out, 'prompt> {
- out: &'out mut Write,
+ out: &'out mut Renderer,
prompt: &'prompt str, // Prompt to display
- prompt_size: Position, // Prompt Unicode width and height
+ prompt_size: Position, // Prompt Unicode/visible width and height
line: LineBuffer, // Edited line buffer
cursor: Position, // Cursor position (relative to the start of the prompt for `row`)
- cols: usize, // Number of columns in terminal
old_rows: usize, // Number of rows used so far (from start of prompt to end of input)
history_index: usize, // The history index we are currently editing
- snapshot: LineBuffer, // Current edited line before history browsing/completion
- term: Terminal, // terminal
+ saved_line_for_history: LineBuffer, // Current edited line before history browsing
+ byte_buffer: [u8; 4],
edit_state: EditState,
-}
-
-#[derive(Copy, Clone, Debug, Default)]
-struct Position {
- col: usize,
- row: usize,
+ changes: Rc<RefCell<Changeset>>,
}
impl<'out, 'prompt> State<'out, 'prompt> {
- fn new(out: &'out mut Write,
- term: Terminal,
- config: &Config,
- prompt: &'prompt str,
- history_index: usize,
- custom_bindings: Rc<RefCell<HashMap<KeyPress, Cmd>>>)
- -> State<'out, 'prompt> {
+ fn new(
+ out: &'out mut Renderer,
+ config: &Config,
+ prompt: &'prompt str,
+ history_index: usize,
+ custom_bindings: Rc<RefCell<HashMap<KeyPress, Cmd>>>,
+ ) -> State<'out, 'prompt> {
let capacity = MAX_LINE;
- let cols = term.get_columns();
- let prompt_size = calculate_position(prompt, Position::default(), cols);
+ let prompt_size = out.calculate_position(prompt, Position::default());
State {
out: out,
prompt: prompt,
prompt_size: prompt_size,
line: LineBuffer::with_capacity(capacity),
cursor: prompt_size,
- cols: cols,
old_rows: prompt_size.row,
history_index: history_index,
- snapshot: LineBuffer::with_capacity(capacity),
- term: term,
+ saved_line_for_history: LineBuffer::with_capacity(capacity),
+ byte_buffer: [0; 4],
edit_state: EditState::new(config, custom_bindings),
+ changes: Rc::new(RefCell::new(Changeset::new())),
}
}
fn next_cmd<R: RawReader>(&mut self, rdr: &mut R) -> Result<Cmd> {
loop {
let rc = self.edit_state.next_cmd(rdr);
- if rc.is_err() && self.term.sigwinch() {
- self.update_columns();
+ if rc.is_err() && self.out.sigwinch() {
+ self.out.update_size();
try!(self.refresh_line());
continue;
}
@@ -127,12 +122,31 @@
}
}
- fn snapshot(&mut self) {
- mem::swap(&mut self.line, &mut self.snapshot);
+ fn backup(&mut self) {
+ self.saved_line_for_history.update(
+ self.line.as_str(),
+ self.line.pos(),
+ );
+ }
+ fn restore(&mut self) {
+ self.line.update(
+ self.saved_line_for_history.as_str(),
+ self.saved_line_for_history.pos(),
+ );
}
- fn backup(&mut self) {
- self.snapshot.backup(&self.line);
+ fn move_cursor(&mut self) -> Result<()> {
+ // calculate the desired position of the cursor
+ let cursor = self.out.calculate_position(
+ &self.line[..self.line.pos()],
+ self.prompt_size,
+ );
+ if self.cursor == cursor {
+ return Ok(());
+ }
+ try!(self.out.move_cursor(self.cursor, cursor));
+ self.cursor = cursor;
+ Ok(())
}
/// Rewrite the currently edited line accordingly to the buffer content,
@@ -143,101 +157,23 @@
}
fn refresh_prompt_and_line(&mut self, prompt: &str) -> Result<()> {
- let prompt_size = calculate_position(prompt, Position::default(), self.cols);
+ let prompt_size = self.out.calculate_position(prompt, Position::default());
self.refresh(prompt, prompt_size)
}
- #[cfg(unix)]
fn refresh(&mut self, prompt: &str, prompt_size: Position) -> Result<()> {
- use std::fmt::Write;
-
- // calculate the position of the end of the input line
- let end_pos = calculate_position(&self.line, prompt_size, self.cols);
- // calculate the desired position of the cursor
- let cursor = calculate_position(&self.line[..self.line.pos()], prompt_size, self.cols);
-
- let mut ab = String::new();
-
- let cursor_row_movement = self.old_rows - self.cursor.row;
- // move the cursor down as required
- if cursor_row_movement > 0 {
- write!(ab, "\x1b[{}B", cursor_row_movement).unwrap();
- }
- // clear old rows
- for _ in 0..self.old_rows {
- ab.push_str("\r\x1b[0K\x1b[1A");
- }
- // clear the line
- ab.push_str("\r\x1b[0K");
-
- // display the prompt
- ab.push_str(prompt);
- // display the input line
- ab.push_str(&self.line);
- // we have to generate our own newline on line wrap
- if end_pos.col == 0 && end_pos.row > 0 {
- ab.push_str("\n");
- }
- // position the cursor
- let cursor_row_movement = end_pos.row - cursor.row;
- // move the cursor up as required
- if cursor_row_movement > 0 {
- write!(ab, "\x1b[{}A", cursor_row_movement).unwrap();
- }
- // position the cursor within the line
- if cursor.col > 0 {
- write!(ab, "\r\x1b[{}C", cursor.col).unwrap();
- } else {
- ab.push('\r');
- }
+ let (cursor, end_pos) = try!(self.out.refresh_line(
+ prompt,
+ prompt_size,
+ &self.line,
+ self.cursor.row,
+ self.old_rows,
+ ));
self.cursor = cursor;
self.old_rows = end_pos.row;
-
- write_and_flush(self.out, ab.as_bytes())
- }
-
- #[cfg(windows)]
- fn refresh(&mut self, prompt: &str, prompt_size: Position) -> Result<()> {
- // calculate the position of the end of the input line
- let end_pos = calculate_position(&self.line, prompt_size, self.cols);
- // calculate the desired position of the cursor
- let cursor = calculate_position(&self.line[..self.line.pos()], prompt_size, self.cols);
-
- // position at the start of the prompt, clear to end of previous input
- let mut info = try!(self.term.get_console_screen_buffer_info());
- info.dwCursorPosition.X = 0;
- info.dwCursorPosition.Y -= self.cursor.row as i16;
- try!(self.term
- .set_console_cursor_position(info.dwCursorPosition));
- let mut _count = 0;
- try!(self.term
- .fill_console_output_character((info.dwSize.X * (self.old_rows as i16 + 1)) as
- u32,
- info.dwCursorPosition));
- let mut ab = String::new();
- // display the prompt
- ab.push_str(prompt); // TODO handle ansi escape code (SetConsoleTextAttribute)
- // display the input line
- ab.push_str(&self.line);
- try!(write_and_flush(self.out, ab.as_bytes()));
-
- // position the cursor
- let mut info = try!(self.term.get_console_screen_buffer_info());
- info.dwCursorPosition.X = cursor.col as i16;
- info.dwCursorPosition.Y -= (end_pos.row - cursor.row) as i16;
- try!(self.term
- .set_console_cursor_position(info.dwCursorPosition));
-
- self.cursor = cursor;
- self.old_rows = end_pos.row;
-
Ok(())
}
-
- fn update_columns(&mut self) {
- self.cols = self.term.get_columns();
- }
}
impl<'out, 'prompt> fmt::Debug for State<'out, 'prompt> {
@@ -247,88 +183,28 @@
.field("prompt_size", &self.prompt_size)
.field("buf", &self.line)
.field("cursor", &self.cursor)
- .field("cols", &self.cols)
+ .field("cols", &self.out.get_columns())
.field("old_rows", &self.old_rows)
.field("history_index", &self.history_index)
- .field("snapshot", &self.snapshot)
+ .field("saved_line_for_history", &self.saved_line_for_history)
.finish()
}
}
-fn write_and_flush(w: &mut Write, buf: &[u8]) -> Result<()> {
- try!(w.write_all(buf));
- try!(w.flush());
- Ok(())
-}
-
-/// Beep, used for completion when there is nothing to complete or when all
-/// the choices were already shown.
-fn beep() -> Result<()> {
- write_and_flush(&mut io::stderr(), b"\x07") // TODO bell-style
-}
-
-/// Calculate the number of columns and rows used to display `s` on a `cols` width terminal
-/// starting at `orig`.
-/// 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(s: &str, orig: Position, cols: usize) -> Position {
- let mut pos = orig;
- let mut esc_seq = 0;
- for c in s.chars() {
- let cw = if esc_seq == 1 {
- if c == '[' {
- // CSI
- esc_seq = 2;
- } else {
- // two-character sequence
- esc_seq = 0;
- }
- None
- } else if esc_seq == 2 {
- if c == ';' || (c >= '0' && c <= '9') {
- } else if c == 'm' {
- // last
- esc_seq = 0;
- } else {
- // not supported
- esc_seq = 0;
- }
- None
- } else if c == '\x1b' {
- esc_seq = 1;
- None
- } else if c == '\n' {
- pos.col = 0;
- pos.row += 1;
- None
- } else {
- c.width()
- };
- if let Some(cw) = cw {
- pos.col += cw;
- if pos.col > cols {
- pos.row += 1;
- pos.col = cw;
- }
- }
- }
- if pos.col == cols {
- pos.col = 0;
- pos.row += 1;
- }
- pos
-}
-
/// Insert the character `ch` at cursor current position.
fn edit_insert(s: &mut State, ch: char, n: RepeatCount) -> Result<()> {
if let Some(push) = s.line.insert(ch, n) {
if push {
- if n == 1 && s.cursor.col + ch.width().unwrap_or(0) < s.cols {
+ if n == 1 && s.cursor.col + ch.width().unwrap_or(0) < s.out.get_columns() {
// Avoid a full update of the line in the trivial case.
- let cursor = calculate_position(&s.line[..s.line.pos()], s.prompt_size, s.cols);
+ let cursor = s.out.calculate_position(
+ &s.line[..s.line.pos()],
+ s.prompt_size,
+ );
s.cursor = cursor;
- write_and_flush(s.out, ch.to_utf8().as_bytes())
+ let bits = ch.encode_utf8(&mut s.byte_buffer);
+ let bits = bits.as_bytes();
+ s.out.write_and_flush(bits)
} else {
s.refresh_line()
}
@@ -342,10 +218,27 @@
/// Replace a single (or n) character(s) under the cursor (Vi mode)
fn edit_replace_char(s: &mut State, ch: char, n: RepeatCount) -> Result<()> {
- if let Some(chars) = s.line.delete(n) {
+ s.changes.borrow_mut().begin();
+ let succeed = if let Some(chars) = s.line.delete(n) {
let count = chars.graphemes(true).count();
s.line.insert(ch, count);
s.line.move_backward(1);
+ true
+ } else {
+ false
+ };
+ s.changes.borrow_mut().end();
+ if succeed { s.refresh_line() } else { Ok(()) }
+}
+
+/// Overwrite the character under the cursor (Vi mode)
+fn edit_overwrite_char(s: &mut State, ch: char) -> Result<()> {
+ if let Some(end) = s.line.next_pos(1) {
+ {
+ let text = ch.encode_utf8(&mut s.byte_buffer);
+ let start = s.line.pos();
+ s.line.replace(start..end, text);
+ }
s.refresh_line()
} else {
Ok(())
@@ -369,14 +262,17 @@
// Delete previously yanked text and yank/paste `text` at current position.
fn edit_yank_pop(s: &mut State, yank_size: usize, text: &str) -> Result<()> {
+ s.changes.borrow_mut().begin();
s.line.yank_pop(yank_size, text);
- edit_yank(s, text, Anchor::Before, 1)
+ let result = edit_yank(s, text, Anchor::Before, 1);
+ s.changes.borrow_mut().end();
+ result
}
/// Move cursor on the left.
fn edit_move_backward(s: &mut State, n: RepeatCount) -> Result<()> {
if s.line.move_backward(n) {
- s.refresh_line()
+ s.move_cursor()
} else {
Ok(())
}
@@ -385,7 +281,7 @@
/// Move cursor on the right.
fn edit_move_forward(s: &mut State, n: RepeatCount) -> Result<()> {
if s.line.move_forward(n) {
- s.refresh_line()
+ s.move_cursor()
} else {
Ok(())
}
@@ -394,7 +290,7 @@
/// Move cursor to the start of the line.
fn edit_move_home(s: &mut State) -> Result<()> {
if s.line.move_home() {
- s.refresh_line()
+ s.move_cursor()
} else {
Ok(())
}
@@ -403,7 +299,7 @@
/// Move cursor to the end of the line.
fn edit_move_end(s: &mut State) -> Result<()> {
if s.line.move_end() {
- s.refresh_line()
+ s.move_cursor()
} else {
Ok(())
}
@@ -421,7 +317,7 @@
/// Backspace implementation.
fn edit_backspace(s: &mut State, n: RepeatCount) -> Result<()> {
- if s.line.backspace(n).is_some() {
+ if s.line.backspace(n) {
s.refresh_line()
} else {
Ok(())
@@ -429,37 +325,34 @@
}
/// Kill the text from point to the end of the line.
-fn edit_kill_line(s: &mut State) -> Result<Option<String>> {
- if let Some(text) = s.line.kill_line() {
- try!(s.refresh_line());
- Ok(Some(text))
- } else {
- Ok(None)
- }
-}
-
-/// Kill backward from point to the beginning of the line.
-fn edit_discard_line(s: &mut State) -> Result<Option<String>> {
- if let Some(text) = s.line.discard_line() {
- try!(s.refresh_line());
- Ok(Some(text))
- } else {
- Ok(None)
- }
-}
-
-/// Exchange the char before cursor with the character at cursor.
-fn edit_transpose_chars(s: &mut State) -> Result<()> {
- if s.line.transpose_chars() {
+fn edit_kill_line(s: &mut State) -> Result<()> {
+ if s.line.kill_line() {
s.refresh_line()
} else {
Ok(())
}
}
+/// Kill backward from point to the beginning of the line.
+fn edit_discard_line(s: &mut State) -> Result<()> {
+ if s.line.discard_line() {
+ s.refresh_line()
+ } else {
+ Ok(())
+ }
+}
+
+/// Exchange the char before cursor with the character at cursor.
+fn edit_transpose_chars(s: &mut State) -> Result<()> {
+ s.changes.borrow_mut().begin();
+ let succeed = s.line.transpose_chars();
+ s.changes.borrow_mut().end();
+ if succeed { s.refresh_line() } else { Ok(()) }
+}
+
fn edit_move_to_prev_word(s: &mut State, word_def: Word, n: RepeatCount) -> Result<()> {
if s.line.move_to_prev_word(word_def, n) {
- s.refresh_line()
+ s.move_cursor()
} else {
Ok(())
}
@@ -467,18 +360,17 @@
/// Delete the previous word, maintaining the cursor at the start of the
/// current word.
-fn edit_delete_prev_word(s: &mut State, word_def: Word, n: RepeatCount) -> Result<Option<String>> {
- if let Some(text) = s.line.delete_prev_word(word_def, n) {
- try!(s.refresh_line());
- Ok(Some(text))
+fn edit_delete_prev_word(s: &mut State, word_def: Word, n: RepeatCount) -> Result<()> {
+ if s.line.delete_prev_word(word_def, n) {
+ s.refresh_line()
} else {
- Ok(None)
+ Ok(())
}
}
fn edit_move_to_next_word(s: &mut State, at: At, word_def: Word, n: RepeatCount) -> Result<()> {
if s.line.move_to_next_word(at, word_def, n) {
- s.refresh_line()
+ s.move_cursor()
} else {
Ok(())
}
@@ -486,49 +378,42 @@
fn edit_move_to(s: &mut State, cs: CharSearch, n: RepeatCount) -> Result<()> {
if s.line.move_to(cs, n) {
+ s.move_cursor()
+ } else {
+ Ok(())
+ }
+}
+
+/// Kill from the cursor to the end of the current word, or, if between words,
+/// to the end of the next word.
+fn edit_delete_word(s: &mut State, at: At, word_def: Word, n: RepeatCount) -> Result<()> {
+ if s.line.delete_word(at, word_def, n) {
s.refresh_line()
} else {
Ok(())
}
}
-/// Kill from the cursor to the end of the current word, or, if between words, to the end of the next word.
-fn edit_delete_word(s: &mut State,
- at: At,
- word_def: Word,
- n: RepeatCount)
- -> Result<Option<String>> {
- if let Some(text) = s.line.delete_word(at, word_def, n) {
- try!(s.refresh_line());
- Ok(Some(text))
+fn edit_delete_to(s: &mut State, cs: CharSearch, n: RepeatCount) -> Result<()> {
+ if s.line.delete_to(cs, n) {
+ s.refresh_line()
} else {
- Ok(None)
- }
-}
-
-fn edit_delete_to(s: &mut State, cs: CharSearch, n: RepeatCount) -> Result<Option<String>> {
- if let Some(text) = s.line.delete_to(cs, n) {
- try!(s.refresh_line());
- Ok(Some(text))
- } else {
- Ok(None)
+ Ok(())
}
}
fn edit_word(s: &mut State, a: WordAction) -> Result<()> {
- if s.line.edit_word(a) {
- s.refresh_line()
- } else {
- Ok(())
- }
+ s.changes.borrow_mut().begin();
+ let succeed = s.line.edit_word(a);
+ s.changes.borrow_mut().end();
+ if succeed { s.refresh_line() } else { Ok(()) }
}
fn edit_transpose_words(s: &mut State, n: RepeatCount) -> Result<()> {
- if s.line.transpose_words(n) {
- s.refresh_line()
- } else {
- Ok(())
- }
+ s.changes.borrow_mut().begin();
+ let succeed = s.line.transpose_words(n);
+ s.changes.borrow_mut().end();
+ if succeed { s.refresh_line() } else { Ok(()) }
}
/// Substitute the currently edited line with the next or previous history
@@ -539,8 +424,8 @@
}
if s.history_index == history.len() {
if prev {
- // Save the current edited line before to overwrite it
- s.snapshot();
+ // Save the current edited line before overwriting it
+ s.backup();
} else {
return Ok(());
}
@@ -554,22 +439,25 @@
}
if s.history_index < history.len() {
let buf = history.get(s.history_index).unwrap();
+ s.changes.borrow_mut().begin();
s.line.update(buf, buf.len());
+ s.changes.borrow_mut().end();
} else {
// Restore current edited line
- s.snapshot();
+ s.restore();
}
s.refresh_line()
}
+// Non-incremental, anchored search
fn edit_history_search(s: &mut State, history: &History, dir: Direction) -> Result<()> {
if history.is_empty() {
- return beep();
+ return s.out.beep();
}
if s.history_index == history.len() && dir == Direction::Forward {
- return beep();
+ return s.out.beep();
} else if s.history_index == 0 && dir == Direction::Reverse {
- return beep();
+ return s.out.beep();
}
if dir == Direction::Reverse {
s.history_index -= 1;
@@ -577,13 +465,16 @@
s.history_index += 1;
}
if let Some(history_index) =
- history.starts_with(&s.line.as_str()[..s.line.pos()], s.history_index, dir) {
+ history.starts_with(&s.line.as_str()[..s.line.pos()], s.history_index, dir)
+ {
s.history_index = history_index;
let buf = history.get(history_index).unwrap();
+ s.changes.borrow_mut().begin();
s.line.update(buf, buf.len());
+ s.changes.borrow_mut().end();
s.refresh_line()
} else {
- beep()
+ s.out.beep()
}
}
@@ -594,8 +485,8 @@
}
if s.history_index == history.len() {
if first {
- // Save the current edited line before to overwrite it
- s.snapshot();
+ // Save the current edited line before overwriting it
+ s.backup();
} else {
return Ok(());
}
@@ -605,30 +496,35 @@
if first {
s.history_index = 0;
let buf = history.get(s.history_index).unwrap();
+ s.changes.borrow_mut().begin();
s.line.update(buf, buf.len());
+ s.changes.borrow_mut().end();
} else {
s.history_index = history.len();
// Restore current edited line
- s.snapshot();
+ s.restore();
}
s.refresh_line()
}
/// Completes the line/word
-fn complete_line<R: RawReader>(rdr: &mut R,
- s: &mut State,
- completer: &Completer,
- config: &Config)
- -> Result<Option<Cmd>> {
+fn complete_line<R: RawReader>(
+ rdr: &mut R,
+ s: &mut State,
+ completer: &Completer,
+ config: &Config,
+) -> Result<Option<Cmd>> {
// get a list of completions
let (start, candidates) = try!(completer.complete(&s.line, s.line.pos()));
// if no completions, we are done
if candidates.is_empty() {
- try!(beep());
+ try!(s.out.beep());
Ok(None)
} else if CompletionType::Circular == config.completion_type() {
- // Save the current edited line before to overwrite it
- s.backup();
+ let mark = s.changes.borrow_mut().begin();
+ // Save the current edited line before overwriting it
+ let backup = s.line.as_str().to_owned();
+ let backup_pos = s.line.pos();
let mut cmd;
let mut i = 0;
loop {
@@ -638,9 +534,8 @@
try!(s.refresh_line());
} else {
// Restore current edited line
- s.snapshot();
+ s.line.update(&backup, backup_pos);
try!(s.refresh_line());
- s.snapshot();
}
cmd = try!(s.next_cmd(rdr));
@@ -648,21 +543,20 @@
Cmd::Complete => {
i = (i + 1) % (candidates.len() + 1); // Circular
if i == candidates.len() {
- try!(beep());
+ try!(s.out.beep());
}
}
Cmd::Abort => {
// Re-show original buffer
- s.snapshot();
if i < candidates.len() {
+ s.line.update(&backup, backup_pos);
try!(s.refresh_line());
}
+ s.changes.borrow_mut().truncate(mark);
return Ok(None);
}
_ => {
- if i == candidates.len() {
- s.snapshot();
- }
+ s.changes.borrow_mut().end();
break;
}
}
@@ -671,7 +565,7 @@
} else if CompletionType::List == config.completion_type() {
// beep if ambiguous
if candidates.len() > 1 {
- try!(beep());
+ 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
@@ -694,12 +588,13 @@
// we got a second tab, maybe show list of possible completions
let show_completions = if candidates.len() > config.completion_prompt_limit() {
let msg = format!("\nDisplay all {} possibilities? (y or n)", candidates.len());
- try!(write_and_flush(s.out, msg.as_bytes()));
+ try!(s.out.write_and_flush(msg.as_bytes()));
s.old_rows += 1;
while cmd != Cmd::SelfInsert(1, 'y') && cmd != Cmd::SelfInsert(1, 'Y') &&
- cmd != Cmd::SelfInsert(1, 'n') &&
- cmd != Cmd::SelfInsert(1, 'N') &&
- cmd != Cmd::Kill(Movement::BackwardChar(1)) {
+ cmd != Cmd::SelfInsert(1, 'n') &&
+ cmd != Cmd::SelfInsert(1, 'N') &&
+ cmd != Cmd::Kill(Movement::BackwardChar(1))
+ {
cmd = try!(s.next_cmd(rdr));
}
match cmd {
@@ -721,52 +616,57 @@
}
}
-fn page_completions<R: RawReader>(rdr: &mut R,
- s: &mut State,
- candidates: &[String])
- -> Result<Option<Cmd>> {
+fn page_completions<R: RawReader>(
+ rdr: &mut R,
+ s: &mut State,
+ candidates: &[String],
+) -> Result<Option<Cmd>> {
use std::cmp;
let min_col_pad = 2;
- let max_width = cmp::min(s.cols,
- candidates
- .into_iter()
- .map(|s| s.as_str().width())
- .max()
- .unwrap() + min_col_pad);
- let num_cols = s.cols / max_width;
+ let cols = s.out.get_columns();
+ let max_width = cmp::min(
+ cols,
+ candidates
+ .into_iter()
+ .map(|s| s.as_str().width())
+ .max()
+ .unwrap() + min_col_pad,
+ );
+ let num_cols = cols / max_width;
- let mut pause_row = s.term.get_rows() - 1;
+ let mut pause_row = s.out.get_rows() - 1;
let num_rows = (candidates.len() + num_cols - 1) / num_cols;
let mut ab = String::new();
for row in 0..num_rows {
if row == pause_row {
- try!(write_and_flush(s.out, b"\n--More--"));
+ try!(s.out.write_and_flush(b"\n--More--"));
let mut cmd = Cmd::Noop;
- while cmd != Cmd::SelfInsert(1, 'y') && cmd != Cmd::SelfInsert(1_, 'Y') &&
- cmd != Cmd::SelfInsert(1, 'n') &&
- cmd != Cmd::SelfInsert(1_, 'N') &&
- cmd != Cmd::SelfInsert(1, 'q') &&
- cmd != Cmd::SelfInsert(1, 'Q') &&
- cmd != Cmd::SelfInsert(1, ' ') &&
- cmd != Cmd::Kill(Movement::BackwardChar(1)) &&
- cmd != Cmd::AcceptLine {
+ while cmd != Cmd::SelfInsert(1, 'y') && cmd != Cmd::SelfInsert(1, 'Y') &&
+ cmd != Cmd::SelfInsert(1, 'n') &&
+ cmd != Cmd::SelfInsert(1, 'N') &&
+ cmd != Cmd::SelfInsert(1, 'q') &&
+ cmd != Cmd::SelfInsert(1, 'Q') &&
+ cmd != Cmd::SelfInsert(1, ' ') &&
+ cmd != Cmd::Kill(Movement::BackwardChar(1)) &&
+ cmd != Cmd::AcceptLine
+ {
cmd = try!(s.next_cmd(rdr));
}
match cmd {
Cmd::SelfInsert(1, 'y') |
Cmd::SelfInsert(1, 'Y') |
Cmd::SelfInsert(1, ' ') => {
- pause_row += s.term.get_rows() - 1;
+ pause_row += s.out.get_rows() - 1;
}
Cmd::AcceptLine => {
pause_row += 1;
}
_ => break,
}
- try!(write_and_flush(s.out, b"\n"));
+ try!(s.out.write_and_flush(b"\n"));
} else {
- try!(write_and_flush(s.out, b"\n"));
+ try!(s.out.write_and_flush(b"\n"));
}
ab.clear();
for col in 0..num_cols {
@@ -782,23 +682,26 @@
}
}
}
- try!(write_and_flush(s.out, ab.as_bytes()));
+ try!(s.out.write_and_flush(ab.as_bytes()));
}
- try!(write_and_flush(s.out, b"\n"));
+ try!(s.out.write_and_flush(b"\n"));
try!(s.refresh_line());
Ok(None)
}
/// Incremental search
-fn reverse_incremental_search<R: RawReader>(rdr: &mut R,
- s: &mut State,
- history: &History)
- -> Result<Option<Cmd>> {
+fn reverse_incremental_search<R: RawReader>(
+ rdr: &mut R,
+ s: &mut State,
+ history: &History,
+) -> Result<Option<Cmd>> {
if history.is_empty() {
return Ok(None);
}
- // Save the current edited line (and cursor position) before to overwrite it
- s.snapshot();
+ let mark = s.changes.borrow_mut().begin();
+ // Save the current edited line (and cursor position) before overwriting it
+ let backup = s.line.as_str().to_owned();
+ let backup_pos = s.line.pos();
let mut search_buf = String::new();
let mut history_idx = history.len() - 1;
@@ -844,8 +747,9 @@
}
Cmd::Abort => {
// Restore current edited line (before search)
- s.snapshot();
+ s.line.update(&backup, backup_pos);
try!(s.refresh_line());
+ s.changes.borrow_mut().truncate(mark);
return Ok(None);
}
_ => break,
@@ -862,6 +766,7 @@
_ => false,
};
}
+ s.changes.borrow_mut().end();
Ok(Some(cmd))
}
@@ -869,36 +774,47 @@
/// It will also handle special inputs in an appropriate fashion
/// (e.g., C-c will exit readline)
#[allow(let_unit_value)]
-fn readline_edit<C: Completer>(prompt: &str,
- editor: &mut Editor<C>,
- original_mode: tty::Mode)
- -> Result<String> {
+fn readline_edit<C: Completer>(
+ prompt: &str,
+ editor: &mut Editor<C>,
+ original_mode: tty::Mode,
+) -> Result<String> {
let completer = editor.completer.as_ref().map(|c| c as &Completer);
let mut stdout = editor.term.create_writer();
- editor.kill_ring.reset();
- let mut s = State::new(&mut stdout,
- editor.term.clone(),
- &editor.config,
- prompt,
- editor.history.len(),
- editor.custom_bindings.clone());
+ editor.reset_kill_ring();
+ let mut s = State::new(
+ &mut stdout,
+ &editor.config,
+ prompt,
+ editor.history.len(),
+ editor.custom_bindings.clone(),
+ );
+
+ s.line.set_delete_listener(editor.kill_ring.clone());
+ s.line.set_change_listener(s.changes.clone());
+
try!(s.refresh_line());
- let mut rdr = try!(s.term.create_reader(&editor.config));
+ let mut rdr = try!(editor.term.create_reader(&editor.config));
loop {
let rc = s.next_cmd(&mut rdr);
let mut cmd = try!(rc);
if cmd.should_reset_kill_ring() {
- editor.kill_ring.reset();
+ editor.reset_kill_ring();
}
// autocomplete
if cmd == Cmd::Complete && completer.is_some() {
- let next = try!(complete_line(&mut rdr, &mut s, completer.unwrap(), &editor.config));
+ let next = try!(complete_line(
+ &mut rdr,
+ &mut s,
+ completer.unwrap(),
+ &editor.config,
+ ));
if next.is_some() {
cmd = next.unwrap();
} else {
@@ -916,7 +832,11 @@
if cmd == Cmd::ReverseSearchHistory {
// Search history backward
- let next = try!(reverse_incremental_search(&mut rdr, &mut s, &editor.history));
+ let next = try!(reverse_incremental_search(
+ &mut rdr,
+ &mut s,
+ &editor.history,
+ ));
if next.is_some() {
cmd = next.unwrap();
} else {
@@ -944,6 +864,9 @@
Cmd::Replace(n, c) => {
try!(edit_replace_char(&mut s, c, n));
}
+ Cmd::Overwrite(c) => {
+ try!(edit_overwrite_char(&mut s, c));
+ }
Cmd::EndOfFile => {
if !s.edit_state.is_emacs_mode() && !s.line.is_empty() {
try!(edit_move_end(&mut s));
@@ -968,19 +891,19 @@
}
Cmd::Kill(Movement::EndOfLine) => {
// Kill the text from point to the end of the line.
- if let Some(text) = try!(edit_kill_line(&mut s)) {
- editor.kill_ring.kill(&text, Mode::Append)
- }
+ editor.kill_ring.borrow_mut().start_killing();
+ try!(edit_kill_line(&mut s));
+ editor.kill_ring.borrow_mut().stop_killing();
}
Cmd::Kill(Movement::WholeLine) => {
try!(edit_move_home(&mut s));
- if let Some(text) = try!(edit_kill_line(&mut s)) {
- editor.kill_ring.kill(&text, Mode::Append)
- }
+ editor.kill_ring.borrow_mut().start_killing();
+ try!(edit_kill_line(&mut s));
+ editor.kill_ring.borrow_mut().stop_killing();
}
Cmd::ClearScreen => {
// Clear the screen leaving the current line at the top of the screen.
- try!(s.term.clear_screen(&mut s.out));
+ try!(s.out.clear_screen());
try!(s.refresh_line())
}
Cmd::NextHistory => {
@@ -992,10 +915,18 @@
try!(edit_history_next(&mut s, &editor.history, true))
}
Cmd::HistorySearchBackward => {
- try!(edit_history_search(&mut s, &editor.history, Direction::Reverse))
+ try!(edit_history_search(
+ &mut s,
+ &editor.history,
+ Direction::Reverse,
+ ))
}
Cmd::HistorySearchForward => {
- try!(edit_history_search(&mut s, &editor.history, Direction::Forward))
+ try!(edit_history_search(
+ &mut s,
+ &editor.history,
+ Direction::Forward,
+ ))
}
Cmd::TransposeChars => {
// Exchange the char before cursor with the character at cursor.
@@ -1003,9 +934,9 @@
}
Cmd::Kill(Movement::BeginningOfLine) => {
// Kill backward from point to the beginning of the line.
- if let Some(text) = try!(edit_discard_line(&mut s)) {
- editor.kill_ring.kill(&text, Mode::Prepend)
- }
+ editor.kill_ring.borrow_mut().start_killing();
+ try!(edit_discard_line(&mut s));
+ editor.kill_ring.borrow_mut().stop_killing();
}
#[cfg(unix)]
Cmd::QuotedInsert => {
@@ -1015,13 +946,13 @@
}
Cmd::Yank(n, anchor) => {
// retrieve (yank) last item killed
- if let Some(text) = editor.kill_ring.yank() {
+ if let Some(text) = editor.kill_ring.borrow_mut().yank() {
try!(edit_yank(&mut s, text, anchor, n))
}
}
Cmd::ViYankTo(mvt) => {
if let Some(text) = s.line.copy(mvt) {
- editor.kill_ring.kill(&text, Mode::Append)
+ editor.kill_ring.borrow_mut().kill(&text, Mode::Append)
}
}
// TODO CTRL-_ // undo
@@ -1032,9 +963,9 @@
}
Cmd::Kill(Movement::BackwardWord(n, word_def)) => {
// kill one word backward (until start of word)
- if let Some(text) = try!(edit_delete_prev_word(&mut s, word_def, n)) {
- editor.kill_ring.kill(&text, Mode::Prepend)
- }
+ editor.kill_ring.borrow_mut().start_killing();
+ try!(edit_delete_prev_word(&mut s, word_def, n));
+ editor.kill_ring.borrow_mut().stop_killing();
}
Cmd::BeginningOfHistory => {
// move to first entry in history
@@ -1054,9 +985,9 @@
}
Cmd::Kill(Movement::ForwardWord(n, at, word_def)) => {
// kill one word forward (until start/end of word)
- if let Some(text) = try!(edit_delete_word(&mut s, at, word_def, n)) {
- editor.kill_ring.kill(&text, Mode::Append)
- }
+ editor.kill_ring.borrow_mut().start_killing();
+ try!(edit_delete_word(&mut s, at, word_def, n));
+ editor.kill_ring.borrow_mut().stop_killing();
}
Cmd::Move(Movement::ForwardWord(n, at, word_def)) => {
// move forwards one word
@@ -1076,15 +1007,22 @@
}
Cmd::YankPop => {
// yank-pop
- if let Some((yank_size, text)) = editor.kill_ring.yank_pop() {
+ if let Some((yank_size, text)) = editor.kill_ring.borrow_mut().yank_pop() {
try!(edit_yank_pop(&mut s, yank_size, text))
}
}
Cmd::Move(Movement::ViCharSearch(n, cs)) => try!(edit_move_to(&mut s, cs, n)),
Cmd::Kill(Movement::ViCharSearch(n, cs)) => {
- if let Some(text) = try!(edit_delete_to(&mut s, cs, n)) {
- editor.kill_ring.kill(&text, Mode::Append)
+ editor.kill_ring.borrow_mut().start_killing();
+ try!(edit_delete_to(&mut s, cs, n));
+ editor.kill_ring.borrow_mut().stop_killing();
+ }
+ Cmd::Undo => {
+ s.line.remove_change_listener();
+ if s.changes.borrow_mut().undo(&mut s.line) {
+ try!(s.refresh_line());
}
+ s.line.set_change_listener(s.changes.clone());
}
Cmd::Interrupt => {
return Err(error::ReadlineError::Interrupted);
@@ -1093,7 +1031,7 @@
Cmd::Suspend => {
try!(original_mode.disable_raw_mode());
try!(tty::suspend());
- try!(s.term.enable_raw_mode()); // TODO original_mode may have changed
+ try!(editor.term.enable_raw_mode()); // TODO original_mode may have changed
try!(s.refresh_line());
continue;
}
@@ -1141,7 +1079,7 @@
term: Terminal,
history: History,
completer: Option<C>,
- kill_ring: KillRing,
+ kill_ring: Rc<RefCell<KillRing>>,
config: Config,
custom_bindings: Rc<RefCell<HashMap<KeyPress, Cmd>>>,
}
@@ -1157,7 +1095,7 @@
term: term,
history: History::with_config(config),
completer: None,
- kill_ring: KillRing::new(60),
+ kill_ring: Rc::new(RefCell::new(KillRing::new(60))),
config: config,
custom_bindings: Rc::new(RefCell::new(HashMap::new())),
}
@@ -1169,7 +1107,8 @@
debug!(target: "rustyline", "unsupported terminal");
// Write prompt and flush it to stdout
let mut stdout = io::stdout();
- try!(write_and_flush(&mut stdout, prompt.as_bytes()));
+ try!(stdout.write_all(prompt.as_bytes()));
+ try!(stdout.flush());
readline_direct()
} else if !self.term.is_stdin_tty() {
@@ -1240,6 +1179,10 @@
prompt: prompt,
}
}
+
+ fn reset_kill_ring(&self) {
+ self.kill_ring.borrow_mut().reset();
+ }
}
impl<C: Completer> fmt::Debug for Editor<C> {
@@ -1251,8 +1194,10 @@
}
}
+/// Edited lines iterator
pub struct Iter<'a, C: Completer>
- where C: 'a
+where
+ C: 'a,
{
editor: &'a mut Editor<C>,
prompt: &'a str,
@@ -1278,7 +1223,6 @@
mod test {
use std::cell::RefCell;
use std::collections::HashMap;
- use std::io::Write;
use std::rc::Rc;
use line_buffer::LineBuffer;
use history::History;
@@ -1287,27 +1231,23 @@
use consts::KeyPress;
use keymap::{Cmd, EditState};
use super::{Editor, Position, Result, State};
- use tty::{Terminal, Term};
+ use tty::Renderer;
+ use undo::Changeset;
- fn init_state<'out>(out: &'out mut Write,
- line: &str,
- pos: usize,
- cols: usize)
- -> State<'out, 'static> {
- let term = Terminal::new();
+ fn init_state<'out>(out: &'out mut Renderer, line: &str, pos: usize) -> State<'out, 'static> {
let config = Config::default();
State {
out: out,
prompt: "",
prompt_size: Position::default(),
- line: LineBuffer::init(line, pos),
+ line: LineBuffer::init(line, pos, None),
cursor: Position::default(),
- cols: cols,
old_rows: 0,
history_index: 0,
- snapshot: LineBuffer::with_capacity(100),
- term: term,
+ saved_line_for_history: LineBuffer::with_capacity(100),
+ byte_buffer: [0; 4],
edit_state: EditState::new(&config, Rc::new(RefCell::new(HashMap::new()))),
+ changes: Rc::new(RefCell::new(Changeset::new())),
}
}
@@ -1321,7 +1261,7 @@
fn edit_history_next() {
let mut out = ::std::io::sink();
let line = "current edited line";
- let mut s = init_state(&mut out, line, 6, 80);
+ let mut s = init_state(&mut out, line, 6);
let mut history = History::new();
history.add("line0");
history.add("line1");
@@ -1333,24 +1273,24 @@
}
super::edit_history_next(&mut s, &history, true).unwrap();
- assert_eq!(line, s.snapshot.as_str());
+ assert_eq!(line, s.saved_line_for_history.as_str());
assert_eq!(1, s.history_index);
assert_eq!("line1", s.line.as_str());
for _ in 0..2 {
super::edit_history_next(&mut s, &history, true).unwrap();
- assert_eq!(line, s.snapshot.as_str());
+ assert_eq!(line, s.saved_line_for_history.as_str());
assert_eq!(0, s.history_index);
assert_eq!("line0", s.line.as_str());
}
super::edit_history_next(&mut s, &history, false).unwrap();
- assert_eq!(line, s.snapshot.as_str());
+ assert_eq!(line, s.saved_line_for_history.as_str());
assert_eq!(1, s.history_index);
assert_eq!("line1", s.line.as_str());
super::edit_history_next(&mut s, &history, false).unwrap();
- // assert_eq!(line, s.snapshot);
+ // assert_eq!(line, s.saved_line_for_history);
assert_eq!(2, s.history_index);
assert_eq!(line, s.line.as_str());
}
@@ -1358,14 +1298,14 @@
struct SimpleCompleter;
impl Completer for SimpleCompleter {
fn complete(&self, line: &str, _pos: usize) -> Result<(usize, Vec<String>)> {
- Ok((0, vec![line.to_string() + "t"]))
+ Ok((0, vec![line.to_owned() + "t"]))
}
}
#[test]
fn complete_line() {
let mut out = ::std::io::sink();
- let mut s = init_state(&mut out, "rus", 3, 80);
+ let mut s = init_state(&mut out, "rus", 3);
let keys = &[KeyPress::Enter];
let mut rdr = keys.iter();
let completer = SimpleCompleter;
@@ -1375,13 +1315,6 @@
assert_eq!(4, s.line.pos());
}
- #[test]
- fn prompt_with_ansi_escape_codes() {
- let pos = super::calculate_position("\x1b[1;32m>>\x1b[0m ", Position::default(), 80);
- assert_eq!(3, pos.col);
- assert_eq!(0, pos.row);
- }
-
fn assert_line(keys: &[KeyPress], expected_line: &str) {
let mut editor = init_editor(keys);
let actual_line = editor.readline(&">>").unwrap();
@@ -1390,13 +1323,19 @@
#[test]
fn delete_key() {
- assert_line(&[KeyPress::Char('a'), KeyPress::Delete, KeyPress::Enter],
- "a");
- assert_line(&[KeyPress::Char('a'),
- KeyPress::Left,
- KeyPress::Delete,
- KeyPress::Enter],
- "");
+ assert_line(
+ &[KeyPress::Char('a'), KeyPress::Delete, KeyPress::Enter],
+ "a",
+ );
+ assert_line(
+ &[
+ KeyPress::Char('a'),
+ KeyPress::Left,
+ KeyPress::Delete,
+ KeyPress::Enter,
+ ],
+ "",
+ );
}
#[test]
diff --git a/src/line_buffer.rs b/src/line_buffer.rs
index 80699e2..9f8a41d 100644
--- a/src/line_buffer.rs
+++ b/src/line_buffer.rs
@@ -1,22 +1,66 @@
//! Line buffer with current cursor position
+use std::cell::RefCell;
+use std::fmt;
use std::iter;
-use std::ops::{Deref, Range};
+use std::ops::{Deref, Index, Range};
+use std::rc::Rc;
+use std::string::Drain;
+use std_unicode::str::UnicodeStr;
use unicode_segmentation::UnicodeSegmentation;
use keymap::{At, CharSearch, Movement, RepeatCount, Word};
/// Maximum buffer size for the line read
pub static MAX_LINE: usize = 4096;
+/// Word's case change
pub enum WordAction {
CAPITALIZE,
LOWERCASE,
UPPERCASE,
}
-#[derive(Debug)]
+/// Delete (kill) direction
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Direction {
+ Forward,
+ Backward,
+}
+
+impl Default for Direction {
+ fn default() -> Direction {
+ Direction::Forward
+ }
+}
+
+/// Listener to be notified when some text is deleted.
+pub trait DeleteListener {
+ fn delete(&mut self, idx: usize, string: &str, dir: Direction);
+}
+
+/// Listener to be notified when the line is modified.
+pub 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);
+}
+
+/// Represent the current input (text and cursor position).
+///
+/// The methods do text manipulations or/and cursor movements.
pub struct LineBuffer {
buf: String, // Edited line buffer
pos: usize, // Current cursor position (byte position)
+ dl: Option<Rc<RefCell<DeleteListener>>>,
+ cl: Option<Rc<RefCell<ChangeListener>>>,
+}
+
+impl fmt::Debug for LineBuffer {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ f.debug_struct("LineBuffer")
+ .field("buf", &self.buf)
+ .field("pos", &self.pos)
+ .finish()
+ }
}
impl LineBuffer {
@@ -25,17 +69,33 @@
LineBuffer {
buf: String::with_capacity(capacity),
pos: 0,
+ dl: None,
+ cl: None,
}
}
#[cfg(test)]
- pub fn init(line: &str, pos: usize) -> LineBuffer {
+ pub 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);
+ lb.cl = cl;
lb
}
+ pub fn set_delete_listener(&mut self, dl: Rc<RefCell<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>>) {
+ self.cl = Some(dl);
+ }
+ pub fn remove_change_listener(&mut self) {
+ self.cl = None;
+ }
+
/// Extracts a string slice containing the entire buffer.
pub fn as_str(&self) -> &str {
&self.buf
@@ -67,28 +127,22 @@
/// Set line content (`buf`) and cursor position (`pos`).
pub fn update(&mut self, buf: &str, pos: usize) {
assert!(pos <= buf.len());
- self.buf.clear();
+ let end = self.len();
+ self.drain(0..end, Direction::default());
let max = self.buf.capacity();
if buf.len() > max {
- self.buf.push_str(&buf[..max]);
+ self.insert_str(0, &buf[..max]);
if pos > max {
self.pos = max;
} else {
self.pos = pos;
}
} else {
- self.buf.push_str(buf);
+ self.insert_str(0, buf);
self.pos = pos;
}
}
- /// Backup `src`
- pub fn backup(&mut self, src: &LineBuffer) {
- self.buf.clear();
- self.buf.push_str(&src.buf);
- self.pos = src.pos;
- }
-
/// Returns the character at current cursor position.
fn grapheme_at_cursor(&self) -> Option<&str> {
if self.pos == self.buf.len() {
@@ -98,7 +152,7 @@
}
}
- fn next_pos(&self, n: RepeatCount) -> Option<usize> {
+ pub fn next_pos(&self, n: RepeatCount) -> Option<usize> {
if self.pos == self.buf.len() {
return None;
}
@@ -108,7 +162,8 @@
.last()
.map(|(i, s)| i + self.pos + s.len())
}
- /// Returns the position of the character just before the current cursor position.
+ /// Returns the position of the character just before the current cursor
+ /// position.
fn prev_pos(&self, n: RepeatCount) -> Option<usize> {
if self.pos == 0 {
return None;
@@ -131,13 +186,11 @@
return None;
}
let push = self.pos == self.buf.len();
- if push {
- self.buf.reserve(shift);
- for _ in 0..n {
- self.buf.push(ch);
- }
- } else if n == 1 {
+ if n == 1 {
self.buf.insert(self.pos, ch);
+ for cl in &self.cl {
+ cl.borrow_mut().insert_char(self.pos, ch);
+ }
} else {
let text = iter::repeat(ch).take(n).collect::<String>();
let pos = self.pos;
@@ -156,14 +209,11 @@
return None;
}
let push = self.pos == self.buf.len();
- if push {
- self.buf.reserve(shift);
- for _ in 0..n {
- self.buf.push_str(text);
- }
+ let pos = self.pos;
+ if n == 1 {
+ self.insert_str(pos, text);
} else {
let text = iter::repeat(text).take(n).collect::<String>();
- let pos = self.pos;
self.insert_str(pos, &text);
}
self.pos += shift;
@@ -172,7 +222,9 @@
/// Delete previously yanked text and yank/paste `text` at current position.
pub fn yank_pop(&mut self, yank_size: usize, text: &str) -> Option<bool> {
- self.buf.drain((self.pos - yank_size)..self.pos);
+ let end = self.pos;
+ let start = end - yank_size;
+ self.drain(start..end, Direction::default());
self.pos -= yank_size;
self.yank(text, 1)
}
@@ -219,13 +271,16 @@
}
}
- /// Delete the character at the right of the cursor without altering the cursor
+ /// Delete the character at the right of the cursor without altering the
+ /// cursor
/// position. Basically this is what happens with the "Delete" keyboard key.
/// Return the number of characters deleted.
pub fn delete(&mut self, n: RepeatCount) -> Option<String> {
match self.next_pos(n) {
Some(pos) => {
- let chars = self.buf.drain(self.pos..pos).collect::<String>();
+ let start = self.pos;
+ let chars = self.drain(start..pos, Direction::Forward)
+ .collect::<String>();
Some(chars)
}
None => None,
@@ -234,35 +289,39 @@
/// Delete the character at the left of the cursor.
/// Basically that is what happens with the "Backspace" keyboard key.
- pub fn backspace(&mut self, n: RepeatCount) -> Option<String> {
+ pub fn backspace(&mut self, n: RepeatCount) -> bool {
match self.prev_pos(n) {
Some(pos) => {
- let chars = self.buf.drain(pos..self.pos).collect::<String>();
+ let end = self.pos;
+ self.drain(pos..end, Direction::Backward);
self.pos = pos;
- Some(chars)
+ true
}
- None => None,
+ None => false,
}
}
/// Kill the text from point to the end of the line.
- pub fn kill_line(&mut self) -> Option<String> {
+ pub fn kill_line(&mut self) -> bool {
if !self.buf.is_empty() && self.pos < self.buf.len() {
- let text = self.buf.drain(self.pos..).collect();
- Some(text)
+ let start = self.pos;
+ let end = self.buf.len();
+ self.drain(start..end, Direction::Forward);
+ true
} else {
- None
+ false
}
}
/// Kill backward from point to the beginning of the line.
- pub fn discard_line(&mut self) -> Option<String> {
+ pub fn discard_line(&mut self) -> bool {
if self.pos > 0 && !self.buf.is_empty() {
- let text = self.buf.drain(..self.pos).collect();
+ let end = self.pos;
+ self.drain(0..end, Direction::Backward);
self.pos = 0;
- Some(text)
+ true
} else {
- None
+ false
}
}
@@ -328,13 +387,14 @@
/// Delete the previous word, maintaining the cursor at the start of the
/// current word.
- pub fn delete_prev_word(&mut self, word_def: Word, n: RepeatCount) -> Option<String> {
+ pub fn delete_prev_word(&mut self, word_def: Word, n: RepeatCount) -> bool {
if let Some(pos) = self.prev_word_pos(self.pos, word_def, n) {
- let word = self.buf.drain(pos..self.pos).collect();
+ let end = self.pos;
+ self.drain(pos..end, Direction::Backward);
self.pos = pos;
- Some(word)
+ true
} else {
- None
+ false
}
}
@@ -440,23 +500,25 @@
};
if let Some(pos) = search_result {
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()
- }
- })
+ 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()
+ }
+ })
} else {
None
}
}
+ /// 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) {
self.pos = pos;
@@ -468,34 +530,41 @@
/// Kill from the cursor to the end of the current word,
/// or, if between words, to the end of the next word.
- pub fn delete_word(&mut self, at: At, word_def: Word, n: RepeatCount) -> Option<String> {
+ pub fn delete_word(&mut self, at: At, word_def: Word, n: RepeatCount) -> bool {
if let Some(pos) = self.next_word_pos(self.pos, at, word_def, n) {
- let word = self.buf.drain(self.pos..pos).collect();
- Some(word)
+ let start = self.pos;
+ self.drain(start..pos, Direction::Forward);
+ true
} else {
- None
+ false
}
}
- pub fn delete_to(&mut self, cs: CharSearch, n: RepeatCount) -> Option<String> {
+ 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),
};
if let Some(pos) = search_result {
- let chunk = match cs {
+ match cs {
CharSearch::Backward(_) |
CharSearch::BackwardAfter(_) => {
let end = self.pos;
self.pos = pos;
- self.buf.drain(pos..end).collect()
+ self.drain(pos..end, Direction::Backward);
}
- CharSearch::ForwardBefore(_) => self.buf.drain(self.pos..pos).collect(),
- CharSearch::Forward(c) => self.buf.drain(self.pos..pos + c.len_utf8()).collect(),
+ CharSearch::ForwardBefore(_) => {
+ let start = self.pos;
+ self.drain(start..pos, Direction::Forward);
+ }
+ CharSearch::Forward(c) => {
+ let start = self.pos;
+ self.drain(start..pos + c.len_utf8(), Direction::Forward);
+ }
};
- Some(chunk)
+ true
} else {
- None
+ false
}
}
@@ -505,7 +574,7 @@
}
self.buf[self.pos..]
.grapheme_indices(true)
- .filter(|&(_, ch)| ch.chars().all(|c| c.is_alphanumeric()))
+ .filter(|&(_, ch)| ch.is_alphanumeric())
.map(|(i, _)| i)
.next()
.map(|i| i + self.pos)
@@ -517,7 +586,8 @@
if start == end {
return false;
}
- let word = self.buf.drain(start..end).collect::<String>();
+ let word = self.drain(start..end, Direction::default())
+ .collect::<String>();
let result = match a {
WordAction::CAPITALIZE => {
let ch = (&word).graphemes(true).next().unwrap();
@@ -550,12 +620,13 @@
return false;
}
- let w1 = self.buf[w1_beg..w1_end].to_string();
+ let w1 = self.buf[w1_beg..w1_end].to_owned();
- let w2 = self.buf.drain(w2_beg..w2_end).collect::<String>();
+ let w2 = self.drain(w2_beg..w2_end, Direction::default())
+ .collect::<String>();
self.insert_str(w2_beg, &w1);
- self.buf.drain(w1_beg..w1_end);
+ self.drain(w1_beg..w1_end, Direction::default());
self.insert_str(w1_beg, &w2);
self.pos = w2_end;
@@ -566,21 +637,63 @@
/// and positions the cursor to the end of text.
pub fn replace(&mut self, range: Range<usize>, text: &str) {
let start = range.start;
+ for cl in &self.cl {
+ cl.borrow_mut().replace(
+ start,
+ self.buf.index(range.clone()),
+ text,
+ );
+ }
self.buf.drain(range);
- self.insert_str(start, text);
+ if start == self.buf.len() {
+ self.buf.push_str(text);
+ } else {
+ self.buf.insert_str(start, text);
+ }
self.pos = start + text.len();
}
- fn insert_str(&mut self, idx: usize, s: &str) -> bool {
+ /// Insert the `s`tring at the specified position.
+ /// Return `true` if the text has been inserted at the end of the line.
+ pub fn insert_str(&mut self, idx: usize, s: &str) -> bool {
+ for cl in &self.cl {
+ cl.borrow_mut().insert_str(idx, s);
+ }
if idx == self.buf.len() {
self.buf.push_str(s);
true
} else {
- insert_str(&mut self.buf, idx, s);
+ self.buf.insert_str(idx, s);
false
}
}
+ /// Remove the specified `range` in the line.
+ pub fn delete_range(&mut self, range: Range<usize>) {
+ self.set_pos(range.start);
+ self.drain(range, Direction::default());
+ }
+
+ fn drain(&mut self, range: Range<usize>, dir: Direction) -> Drain {
+ for dl in &self.dl {
+ dl.borrow_mut().delete(
+ range.start,
+ &self.buf[range.start..range.end],
+ dir,
+ );
+ }
+ for cl in &self.cl {
+ cl.borrow_mut().delete(
+ range.start,
+ &self.buf[range.start..range.end],
+ dir,
+ );
+ }
+ self.buf.drain(range)
+ }
+
+ /// Return the content between current cursor position and `mvt` position.
+ /// Return `None` when the buffer is empty or when the movement fails.
pub fn copy(&self, mvt: Movement) -> Option<String> {
if self.is_empty() {
return None;
@@ -591,7 +704,7 @@
if self.pos == 0 {
None
} else {
- Some(self.buf[..self.pos].to_string())
+ Some(self.buf[..self.pos].to_owned())
}
}
Movement::ViFirstPrint => {
@@ -607,19 +720,19 @@
if self.pos == self.buf.len() {
None
} else {
- Some(self.buf[self.pos..].to_string())
+ Some(self.buf[self.pos..].to_owned())
}
}
Movement::BackwardWord(n, word_def) => {
if let Some(pos) = self.prev_word_pos(self.pos, word_def, n) {
- Some(self.buf[pos..self.pos].to_string())
+ Some(self.buf[pos..self.pos].to_owned())
} else {
None
}
}
Movement::ForwardWord(n, at, word_def) => {
if let Some(pos) = self.next_word_pos(self.pos, at, word_def, n) {
- Some(self.buf[self.pos..pos].to_string())
+ Some(self.buf[self.pos..pos].to_owned())
} else {
None
}
@@ -633,27 +746,25 @@
};
if let Some(pos) = search_result {
Some(match cs {
- CharSearch::Backward(_) |
- CharSearch::BackwardAfter(_) => self.buf[pos..self.pos].to_string(),
- CharSearch::ForwardBefore(_) => self.buf[self.pos..pos].to_string(),
- CharSearch::Forward(c) => {
- self.buf[self.pos..pos + c.len_utf8()].to_string()
- }
- })
+ CharSearch::Backward(_) |
+ CharSearch::BackwardAfter(_) => self.buf[pos..self.pos].to_owned(),
+ CharSearch::ForwardBefore(_) => self.buf[self.pos..pos].to_owned(),
+ CharSearch::Forward(c) => self.buf[self.pos..pos + c.len_utf8()].to_owned(),
+ })
} else {
None
}
}
Movement::BackwardChar(n) => {
if let Some(pos) = self.prev_pos(n) {
- Some(self.buf[pos..self.pos].to_string())
+ Some(self.buf[pos..self.pos].to_owned())
} else {
None
}
}
Movement::ForwardChar(n) => {
if let Some(pos) = self.next_pos(n) {
- Some(self.buf[self.pos..pos].to_string())
+ Some(self.buf[self.pos..pos].to_owned())
} else {
None
}
@@ -670,73 +781,83 @@
}
}
-fn insert_str(buf: &mut String, idx: usize, s: &str) {
- use std::ptr;
-
- let len = buf.len();
- assert!(idx <= len);
- assert!(buf.is_char_boundary(idx));
- let amt = s.len();
- buf.reserve(amt);
-
- unsafe {
- let v = buf.as_mut_vec();
- ptr::copy(v.as_ptr().offset(idx as isize),
- v.as_mut_ptr().offset((idx + amt) as isize),
- len - idx);
- ptr::copy_nonoverlapping(s.as_ptr(), v.as_mut_ptr().offset(idx as isize), amt);
- v.set_len(len + amt);
- }
-}
-
fn is_start_of_word(word_def: Word, previous: &str, grapheme: &str) -> bool {
(!is_word_char(word_def, previous) && is_word_char(word_def, grapheme)) ||
- (word_def == Word::Vi && !is_other_char(previous) && is_other_char(grapheme))
+ (word_def == Word::Vi && !is_other_char(previous) && is_other_char(grapheme))
}
fn is_end_of_word(word_def: Word, grapheme: &str, next: &str) -> bool {
(!is_word_char(word_def, next) && is_word_char(word_def, grapheme)) ||
- (word_def == Word::Vi && !is_other_char(next) && is_other_char(grapheme))
+ (word_def == Word::Vi && !is_other_char(next) && is_other_char(grapheme))
}
fn is_word_char(word_def: Word, grapheme: &str) -> bool {
match word_def {
- Word::Emacs => grapheme.chars().all(|c| c.is_alphanumeric()),
+ Word::Emacs => grapheme.is_alphanumeric(),
Word::Vi => is_vi_word_char(grapheme),
- Word::Big => !grapheme.chars().all(|c| c.is_whitespace()),
+ Word::Big => !grapheme.is_whitespace(),
}
}
fn is_vi_word_char(grapheme: &str) -> bool {
- grapheme.chars().all(|c| c.is_alphanumeric()) || grapheme == "_"
+ grapheme.is_alphanumeric() || grapheme == "_"
}
fn is_other_char(grapheme: &str) -> bool {
- !(grapheme.chars().all(|c| c.is_whitespace()) || is_vi_word_char(grapheme))
+ !(grapheme.is_whitespace() || is_vi_word_char(grapheme))
}
#[cfg(test)]
mod test {
+ use std::cell::RefCell;
+ use std::rc::Rc;
use keymap::{At, CharSearch, Word};
- use super::{LineBuffer, MAX_LINE, WordAction};
+ use super::{ChangeListener, DeleteListener, Direction, LineBuffer, MAX_LINE, WordAction};
+
+ struct Listener {
+ deleted_str: Option<String>,
+ }
+
+ impl Listener {
+ fn new() -> Rc<RefCell<Listener>> {
+ let l = Listener { deleted_str: None };
+ Rc::new(RefCell::new(l))
+ }
+
+ fn assert_deleted_str_eq(&self, expected: &str) {
+ let actual = self.deleted_str.as_ref().expect("no deleted string");
+ assert_eq!(expected, actual)
+ }
+ }
+
+ impl DeleteListener for Listener {
+ fn delete(&mut self, _: usize, string: &str, _: Direction) {
+ self.deleted_str = Some(string.to_owned());
+ }
+ }
+ 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) {}
+ }
#[test]
fn next_pos() {
- let s = LineBuffer::init("ö̲g̈", 0);
+ let s = LineBuffer::init("ö̲g̈", 0, None);
assert_eq!(7, s.len());
let pos = s.next_pos(1);
assert_eq!(Some(4), pos);
- let s = LineBuffer::init("ö̲g̈", 4);
+ let s = LineBuffer::init("ö̲g̈", 4, None);
let pos = s.next_pos(1);
assert_eq!(Some(7), pos);
}
#[test]
fn prev_pos() {
- let s = LineBuffer::init("ö̲g̈", 4);
+ let s = LineBuffer::init("ö̲g̈", 4, None);
assert_eq!(7, s.len());
let pos = s.prev_pos(1);
assert_eq!(Some(0), pos);
- let s = LineBuffer::init("ö̲g̈", 7);
+ let s = LineBuffer::init("ö̲g̈", 7, None);
let pos = s.prev_pos(1);
assert_eq!(Some(4), pos);
}
@@ -763,7 +884,7 @@
#[test]
fn yank_after() {
- let mut s = LineBuffer::init("αß", 2);
+ let mut s = LineBuffer::init("αß", 2, None);
s.move_forward(1);
let ok = s.yank("γδε", 1);
assert_eq!(Some(true), ok);
@@ -773,7 +894,7 @@
#[test]
fn yank_before() {
- let mut s = LineBuffer::init("αε", 2);
+ let mut s = LineBuffer::init("αε", 2, None);
let ok = s.yank("ßγδ", 1);
assert_eq!(Some(false), ok);
assert_eq!("αßγδε", s.buf);
@@ -782,7 +903,7 @@
#[test]
fn moves() {
- let mut s = LineBuffer::init("αß", 4);
+ let mut s = LineBuffer::init("αß", 4, None);
let ok = s.move_backward(1);
assert_eq!("αß", s.buf);
assert_eq!(2, s.pos);
@@ -806,7 +927,7 @@
#[test]
fn move_grapheme() {
- let mut s = LineBuffer::init("ag̈", 4);
+ let mut s = LineBuffer::init("ag̈", 4, None);
assert_eq!(4, s.len());
let ok = s.move_backward(1);
assert_eq!(true, ok);
@@ -819,36 +940,41 @@
#[test]
fn delete() {
- let mut s = LineBuffer::init("αß", 2);
+ let cl = Listener::new();
+ let mut s = LineBuffer::init("αß", 2, Some(cl.clone()));
let chars = s.delete(1);
assert_eq!("α", s.buf);
assert_eq!(2, s.pos);
- assert_eq!(Some("ß".to_string()), chars);
+ assert_eq!(Some("ß".to_owned()), chars);
- let chars = s.backspace(1);
+ let ok = s.backspace(1);
assert_eq!("", s.buf);
assert_eq!(0, s.pos);
- assert_eq!(Some("α".to_string()), chars);
+ assert_eq!(true, ok);
+ cl.borrow().assert_deleted_str_eq("α");
}
#[test]
fn kill() {
- let mut s = LineBuffer::init("αßγδε", 6);
- let text = s.kill_line();
+ let cl = Listener::new();
+ let mut s = LineBuffer::init("αßγδε", 6, Some(cl.clone()));
+ let ok = s.kill_line();
assert_eq!("αßγ", s.buf);
assert_eq!(6, s.pos);
- assert_eq!(Some("δε".to_string()), text);
+ assert_eq!(true, ok);
+ cl.borrow().assert_deleted_str_eq("δε");
s.pos = 4;
- let text = s.discard_line();
+ let ok = s.discard_line();
assert_eq!("γ", s.buf);
assert_eq!(0, s.pos);
- assert_eq!(Some("αß".to_string()), text);
+ assert_eq!(true, ok);
+ cl.borrow().assert_deleted_str_eq("αß");
}
#[test]
fn transpose() {
- let mut s = LineBuffer::init("aßc", 1);
+ let mut s = LineBuffer::init("aßc", 1, None);
let ok = s.transpose_chars();
assert_eq!("ßac", s.buf);
assert_eq!(3, s.pos);
@@ -871,7 +997,7 @@
#[test]
fn move_to_prev_word() {
- let mut s = LineBuffer::init("a ß c", 6);
+ let mut s = LineBuffer::init("a ß c", 6, None);
let ok = s.move_to_prev_word(Word::Emacs, 1);
assert_eq!("a ß c", s.buf);
assert_eq!(2, s.pos);
@@ -880,7 +1006,7 @@
#[test]
fn move_to_prev_vi_word() {
- let mut s = LineBuffer::init("alpha ,beta/rho; mu", 19);
+ let mut s = LineBuffer::init("alpha ,beta/rho; mu", 19, None);
let ok = s.move_to_prev_word(Word::Vi, 1);
assert!(ok);
assert_eq!(17, s.pos);
@@ -908,7 +1034,7 @@
#[test]
fn move_to_prev_big_word() {
- let mut s = LineBuffer::init("alpha ,beta/rho; mu", 19);
+ let mut s = LineBuffer::init("alpha ,beta/rho; mu", 19, None);
let ok = s.move_to_prev_word(Word::Big, 1);
assert!(ok);
assert_eq!(17, s.pos);
@@ -924,17 +1050,17 @@
#[test]
fn move_to_forward() {
- let mut s = LineBuffer::init("αßγδε", 2);
+ let mut s = LineBuffer::init("αßγδε", 2, None);
let ok = s.move_to(CharSearch::ForwardBefore('ε'), 1);
assert_eq!(true, ok);
assert_eq!(6, s.pos);
- let mut s = LineBuffer::init("αßγδε", 2);
+ let mut s = LineBuffer::init("αßγδε", 2, None);
let ok = s.move_to(CharSearch::Forward('ε'), 1);
assert_eq!(true, ok);
assert_eq!(8, s.pos);
- let mut s = LineBuffer::init("αßγδε", 2);
+ let mut s = LineBuffer::init("αßγδε", 2, None);
let ok = s.move_to(CharSearch::Forward('ε'), 10);
assert_eq!(true, ok);
assert_eq!(8, s.pos);
@@ -942,12 +1068,12 @@
#[test]
fn move_to_backward() {
- let mut s = LineBuffer::init("αßγδε", 8);
+ let mut s = LineBuffer::init("αßγδε", 8, None);
let ok = s.move_to(CharSearch::BackwardAfter('ß'), 1);
assert_eq!(true, ok);
assert_eq!(4, s.pos);
- let mut s = LineBuffer::init("αßγδε", 8);
+ let mut s = LineBuffer::init("αßγδε", 8, None);
let ok = s.move_to(CharSearch::Backward('ß'), 1);
assert_eq!(true, ok);
assert_eq!(2, s.pos);
@@ -955,16 +1081,18 @@
#[test]
fn delete_prev_word() {
- let mut s = LineBuffer::init("a ß c", 6);
- let text = s.delete_prev_word(Word::Big, 1);
+ let cl = Listener::new();
+ let mut s = LineBuffer::init("a ß c", 6, Some(cl.clone()));
+ let ok = s.delete_prev_word(Word::Big, 1);
assert_eq!("a c", s.buf);
assert_eq!(2, s.pos);
- assert_eq!(Some("ß ".to_string()), text);
+ assert_eq!(true, ok);
+ cl.borrow().assert_deleted_str_eq("ß ");
}
#[test]
fn move_to_next_word() {
- let mut s = LineBuffer::init("a ß c", 1);
+ let mut s = LineBuffer::init("a ß c", 1, None);
let ok = s.move_to_next_word(At::AfterEnd, Word::Emacs, 1);
assert_eq!("a ß c", s.buf);
assert_eq!(4, s.pos);
@@ -973,7 +1101,7 @@
#[test]
fn move_to_end_of_word() {
- let mut s = LineBuffer::init("a ßeta c", 1);
+ let mut s = LineBuffer::init("a ßeta c", 1, None);
let ok = s.move_to_next_word(At::BeforeEnd, Word::Vi, 1);
assert_eq!("a ßeta c", s.buf);
assert_eq!(6, s.pos);
@@ -982,7 +1110,7 @@
#[test]
fn move_to_end_of_vi_word() {
- let mut s = LineBuffer::init("alpha ,beta/rho; mu", 0);
+ let mut s = LineBuffer::init("alpha ,beta/rho; mu", 0, None);
let ok = s.move_to_next_word(At::BeforeEnd, Word::Vi, 1);
assert!(ok);
assert_eq!(4, s.pos);
@@ -1010,7 +1138,7 @@
#[test]
fn move_to_end_of_big_word() {
- let mut s = LineBuffer::init("alpha ,beta/rho; mu", 0);
+ let mut s = LineBuffer::init("alpha ,beta/rho; mu", 0, None);
let ok = s.move_to_next_word(At::BeforeEnd, Word::Big, 1);
assert!(ok);
assert_eq!(4, s.pos);
@@ -1026,7 +1154,7 @@
#[test]
fn move_to_start_of_word() {
- let mut s = LineBuffer::init("a ß c", 2);
+ let mut s = LineBuffer::init("a ß c", 2, None);
let ok = s.move_to_next_word(At::Start, Word::Emacs, 1);
assert_eq!("a ß c", s.buf);
assert_eq!(6, s.pos);
@@ -1035,7 +1163,7 @@
#[test]
fn move_to_start_of_vi_word() {
- let mut s = LineBuffer::init("alpha ,beta/rho; mu", 0);
+ let mut s = LineBuffer::init("alpha ,beta/rho; mu", 0, None);
let ok = s.move_to_next_word(At::Start, Word::Vi, 1);
assert!(ok);
assert_eq!(6, s.pos);
@@ -1063,7 +1191,7 @@
#[test]
fn move_to_start_of_big_word() {
- let mut s = LineBuffer::init("alpha ,beta/rho; mu", 0);
+ let mut s = LineBuffer::init("alpha ,beta/rho; mu", 0, None);
let ok = s.move_to_next_word(At::Start, Word::Big, 1);
assert!(ok);
assert_eq!(6, s.pos);
@@ -1079,76 +1207,87 @@
#[test]
fn delete_word() {
- let mut s = LineBuffer::init("a ß c", 1);
- let text = s.delete_word(At::AfterEnd, Word::Emacs, 1);
+ let cl = Listener::new();
+ let mut s = LineBuffer::init("a ß c", 1, Some(cl.clone()));
+ let ok = s.delete_word(At::AfterEnd, Word::Emacs, 1);
assert_eq!("a c", s.buf);
assert_eq!(1, s.pos);
- assert_eq!(Some(" ß".to_string()), text);
+ assert_eq!(true, ok);
+ cl.borrow().assert_deleted_str_eq(" ß");
- let mut s = LineBuffer::init("test", 0);
- let text = s.delete_word(At::AfterEnd, Word::Vi, 1);
+ let mut s = LineBuffer::init("test", 0, Some(cl.clone()));
+ let ok = s.delete_word(At::AfterEnd, Word::Vi, 1);
assert_eq!("", s.buf);
assert_eq!(0, s.pos);
- assert_eq!(Some("test".to_string()), text);
+ assert_eq!(true, ok);
+ cl.borrow().assert_deleted_str_eq("test");
}
#[test]
fn delete_til_start_of_word() {
- let mut s = LineBuffer::init("a ß c", 2);
- let text = s.delete_word(At::Start, Word::Emacs, 1);
+ let cl = Listener::new();
+ let mut s = LineBuffer::init("a ß c", 2, Some(cl.clone()));
+ let ok = s.delete_word(At::Start, Word::Emacs, 1);
assert_eq!("a c", s.buf);
assert_eq!(2, s.pos);
- assert_eq!(Some("ß ".to_string()), text);
+ assert_eq!(true, ok);
+ cl.borrow().assert_deleted_str_eq("ß ");
}
#[test]
fn delete_to_forward() {
- let mut s = LineBuffer::init("αßγδε", 2);
- let text = s.delete_to(CharSearch::ForwardBefore('ε'), 1);
- assert_eq!(Some("ßγδ".to_string()), text);
+ let cl = Listener::new();
+ let mut s = LineBuffer::init("αßγδε", 2, Some(cl.clone()));
+ let ok = s.delete_to(CharSearch::ForwardBefore('ε'), 1);
+ assert_eq!(true, ok);
+ cl.borrow().assert_deleted_str_eq("ßγδ");
assert_eq!("αε", s.buf);
assert_eq!(2, s.pos);
- let mut s = LineBuffer::init("αßγδε", 2);
- let text = s.delete_to(CharSearch::Forward('ε'), 1);
- assert_eq!(Some("ßγδε".to_string()), text);
+ let mut s = LineBuffer::init("αßγδε", 2, Some(cl.clone()));
+ let ok = s.delete_to(CharSearch::Forward('ε'), 1);
+ assert_eq!(true, ok);
+ cl.borrow().assert_deleted_str_eq("ßγδε");
assert_eq!("α", s.buf);
assert_eq!(2, s.pos);
}
#[test]
fn delete_to_backward() {
- let mut s = LineBuffer::init("αßγδε", 8);
- let text = s.delete_to(CharSearch::BackwardAfter('α'), 1);
- assert_eq!(Some("ßγδ".to_string()), text);
+ let cl = Listener::new();
+ let mut s = LineBuffer::init("αßγδε", 8, Some(cl.clone()));
+ let ok = s.delete_to(CharSearch::BackwardAfter('α'), 1);
+ assert_eq!(true, ok);
+ cl.borrow().assert_deleted_str_eq("ßγδ");
assert_eq!("αε", s.buf);
assert_eq!(2, s.pos);
- let mut s = LineBuffer::init("αßγδε", 8);
- let text = s.delete_to(CharSearch::Backward('ß'), 1);
- assert_eq!(Some("ßγδ".to_string()), text);
+ let mut s = LineBuffer::init("αßγδε", 8, Some(cl.clone()));
+ let ok = s.delete_to(CharSearch::Backward('ß'), 1);
+ assert_eq!(true, ok);
+ cl.borrow().assert_deleted_str_eq("ßγδ");
assert_eq!("αε", s.buf);
assert_eq!(2, s.pos);
}
#[test]
fn edit_word() {
- let mut s = LineBuffer::init("a ßeta c", 1);
+ let mut s = LineBuffer::init("a ßeta c", 1, None);
assert!(s.edit_word(WordAction::UPPERCASE));
assert_eq!("a SSETA c", s.buf);
assert_eq!(7, s.pos);
- let mut s = LineBuffer::init("a ßetA c", 1);
+ let mut s = LineBuffer::init("a ßetA c", 1, None);
assert!(s.edit_word(WordAction::LOWERCASE));
assert_eq!("a ßeta c", s.buf);
assert_eq!(7, s.pos);
- let mut s = LineBuffer::init("a ßETA c", 1);
+ let mut s = LineBuffer::init("a ßETA c", 1, None);
assert!(s.edit_word(WordAction::CAPITALIZE));
assert_eq!("a SSeta c", s.buf);
assert_eq!(7, s.pos);
- let mut s = LineBuffer::init("test", 1);
+ let mut s = LineBuffer::init("test", 1, None);
assert!(s.edit_word(WordAction::CAPITALIZE));
assert_eq!("tEst", s.buf);
assert_eq!(4, s.pos);
@@ -1156,20 +1295,20 @@
#[test]
fn transpose_words() {
- let mut s = LineBuffer::init("ßeta / δelta__", 15);
+ let mut s = LineBuffer::init("ßeta / δelta__", 15, None);
assert!(s.transpose_words(1));
assert_eq!("δelta__ / ßeta", s.buf);
assert_eq!(16, s.pos);
- let mut s = LineBuffer::init("ßeta / δelta", 14);
+ let mut s = LineBuffer::init("ßeta / δelta", 14, None);
assert!(s.transpose_words(1));
assert_eq!("δelta / ßeta", s.buf);
assert_eq!(14, s.pos);
- let mut s = LineBuffer::init(" / δelta", 8);
+ let mut s = LineBuffer::init(" / δelta", 8, None);
assert!(!s.transpose_words(1));
- let mut s = LineBuffer::init("ßeta / __", 9);
+ let mut s = LineBuffer::init("ßeta / __", 9, None);
assert!(!s.transpose_words(1));
}
}
diff --git a/src/tty/mod.rs b/src/tty/mod.rs
index fe3ac99..347d1d2 100644
--- a/src/tty/mod.rs
+++ b/src/tty/mod.rs
@@ -1,8 +1,10 @@
//! This module implements and describes common TTY methods & traits
-use std::io::Write;
+use std::io::{self, Write};
+
use Result;
use config::Config;
use consts::KeyPress;
+use line_buffer::LineBuffer;
/// Terminal state
pub trait RawMode: Copy + Sized {
@@ -19,31 +21,113 @@
fn next_char(&mut self) -> Result<char>;
}
-/// Terminal contract
-pub trait Term: Clone {
- type Reader: RawReader;
- type Writer: Write;
- type Mode: RawMode;
+#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
+pub struct Position {
+ pub col: usize,
+ pub row: usize,
+}
- fn new() -> 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;
- /// Get the number of columns in the current terminal.
+/// Display prompt, line and cursor in terminal output
+pub trait Renderer {
+ fn move_cursor(&mut self, old: Position, new: Position) -> Result<()>;
+
+ /// Display prompt, line and cursor in terminal output
+ fn refresh_line(
+ &mut self,
+ prompt: &str,
+ prompt_size: Position,
+ line: &LineBuffer,
+ current_row: usize,
+ old_rows: usize,
+ ) -> Result<(Position, Position)>;
+
+ /// Calculate the number of columns and rows used to display `s` on a
+ /// `cols` width terminal
+ /// starting at `orig`.
+ fn calculate_position(&self, s: &str, orig: Position) -> Position;
+
+ fn write_and_flush(&mut self, buf: &[u8]) -> Result<()>;
+
+ /// Beep, used for completion when there is nothing to complete or when all
+ /// the choices were already shown.
+ fn beep(&mut self) -> Result<()> {
+ // TODO bell-style
+ try!(io::stderr().write_all(b"\x07"));
+ try!(io::stderr().flush());
+ Ok(())
+ }
+
+ /// Clear the screen. Used to handle ctrl+l
+ fn clear_screen(&mut self) -> Result<()>;
+
+ /// Check if a SIGWINCH signal has been received
+ fn sigwinch(&self) -> bool;
+ /// Update the number of columns/rows in the current terminal.
+ fn update_size(&mut self);
+ /// Get the number of rows in the current terminal.
fn get_columns(&self) -> usize;
/// Get the number of rows in the current terminal.
fn get_rows(&self) -> usize;
- /// Check if a SIGWINCH signal has been received
- fn sigwinch(&self) -> bool;
+}
+
+impl<'a, R: Renderer + ?Sized> Renderer for &'a mut R {
+ fn move_cursor(&mut self, old: Position, new: Position) -> Result<()> {
+ (**self).move_cursor(old, new)
+ }
+ fn refresh_line(
+ &mut self,
+ prompt: &str,
+ prompt_size: Position,
+ line: &LineBuffer,
+ current_row: usize,
+ old_rows: usize,
+ ) -> Result<(Position, Position)> {
+ (**self).refresh_line(prompt, prompt_size, line, current_row, old_rows)
+ }
+ 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()
+ }
+}
+
+/// Terminal contract
+pub trait Term {
+ type Reader: RawReader;
+ type Writer: Renderer;
+ type Mode: RawMode;
+
+ fn new() -> 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;
/// Enable RAW mode for the terminal.
fn enable_raw_mode(&self) -> Result<Self::Mode>;
/// Create a RAW reader
fn create_reader(&self, config: &Config) -> Result<Self::Reader>;
/// Create a writer
fn create_writer(&self) -> Self::Writer;
- /// Clear the screen. Used to handle ctrl+l
- fn clear_screen(&mut self, w: &mut Write) -> Result<()>;
}
// If on Windows platform import Windows TTY module
diff --git a/src/tty/test.rs b/src/tty/test.rs
index 8813fa5..6a1e60c 100644
--- a/src/tty/test.rs
+++ b/src/tty/test.rs
@@ -4,14 +4,12 @@
use std::slice::Iter;
use std::vec::IntoIter;
-#[cfg(windows)]
-use winapi;
-
+use Result;
use config::Config;
use consts::KeyPress;
use error::ReadlineError;
-use Result;
-use super::{RawMode, RawReader, Term};
+use line_buffer::LineBuffer;
+use super::{Position, RawMode, RawReader, Renderer, Term};
pub type Mode = ();
@@ -47,46 +45,62 @@
}
}
-pub type Terminal = DummyTerminal;
+impl Renderer for Sink {
+ fn move_cursor(&mut self, _: Position, _: Position) -> Result<()> {
+ Ok(())
+ }
-#[derive(Clone,Debug)]
-pub struct DummyTerminal {
- pub keys: Vec<KeyPress>,
+ fn refresh_line(
+ &mut self,
+ prompt: &str,
+ prompt_size: Position,
+ line: &LineBuffer,
+ _: usize,
+ _: usize,
+ ) -> Result<(Position, Position)> {
+ try!(self.write_all(prompt.as_bytes()));
+ try!(self.write_all(line.as_bytes()));
+ Ok((prompt_size, prompt_size))
+ }
+
+ /// Characters with 2 column width are correctly handled (not splitted).
+ fn calculate_position(&self, _: &str, orig: Position) -> Position {
+ orig
+ }
+
+ fn write_and_flush(&mut self, buf: &[u8]) -> Result<()> {
+ try!(self.write_all(buf));
+ try!(self.flush());
+ Ok(())
+ }
+
+ fn beep(&mut self) -> Result<()> {
+ Ok(())
+ }
+
+ /// Clear the screen. Used to handle ctrl+l
+ fn clear_screen(&mut self) -> Result<()> {
+ Ok(())
+ }
+
+ /// Check if a SIGWINCH signal has been received
+ fn sigwinch(&self) -> bool {
+ false
+ }
+ fn update_size(&mut self) {}
+ fn get_columns(&self) -> usize {
+ 80
+ }
+ fn get_rows(&self) -> usize {
+ 24
+ }
}
-impl DummyTerminal {
- #[cfg(windows)]
- pub fn get_console_screen_buffer_info(&self) -> Result<winapi::CONSOLE_SCREEN_BUFFER_INFO> {
- let dw_size = winapi::COORD { X: 80, Y: 24 };
- let dw_cursor_osition = winapi::COORD { X: 0, Y: 0 };
- let sr_window = winapi::SMALL_RECT {
- Left: 0,
- Top: 0,
- Right: 0,
- Bottom: 0,
- };
- let info = winapi::CONSOLE_SCREEN_BUFFER_INFO {
- dwSize: dw_size,
- dwCursorPosition: dw_cursor_osition,
- wAttributes: 0,
- srWindow: sr_window,
- dwMaximumWindowSize: dw_size,
- };
- Ok(info)
- }
+pub type Terminal = DummyTerminal;
- #[cfg(windows)]
- pub fn set_console_cursor_position(&mut self, _: winapi::COORD) -> Result<()> {
- Ok(())
- }
-
- #[cfg(windows)]
- pub fn fill_console_output_character(&mut self,
- _: winapi::DWORD,
- _: winapi::COORD)
- -> Result<()> {
- Ok(())
- }
+#[derive(Clone, Debug)]
+pub struct DummyTerminal {
+ pub keys: Vec<KeyPress>,
}
impl Term for DummyTerminal {
@@ -100,7 +114,8 @@
// Init checks:
- /// Check if current terminal can provide a rich line-editing user interface.
+ /// Check if current terminal can provide a rich line-editing user
+ /// interface.
fn is_unsupported(&self) -> bool {
false
}
@@ -112,21 +127,6 @@
// Interactive loop:
- /// Get the number of columns in the current terminal.
- fn get_columns(&self) -> usize {
- 80
- }
-
- /// Get the number of rows in the current terminal.
- fn get_rows(&self) -> usize {
- 24
- }
-
- /// Check if a SIGWINCH signal has been received
- fn sigwinch(&self) -> bool {
- false
- }
-
fn enable_raw_mode(&self) -> Result<Mode> {
Ok(())
}
@@ -139,11 +139,6 @@
fn create_writer(&self) -> Sink {
io::sink()
}
-
- /// Clear the screen. Used to handle ctrl+l
- fn clear_screen(&mut self, _: &mut Write) -> Result<()> {
- Ok(())
- }
}
#[cfg(unix)]
diff --git a/src/tty/unix.rs b/src/tty/unix.rs
index 17a439e..107a8d3 100644
--- a/src/tty/unix.rs
+++ b/src/tty/unix.rs
@@ -1,20 +1,22 @@
//! Unix specific definitions
use std;
-use std::io::{self, Read, Stdout, Write};
+use std::io::{self, Chars, Read, Stdout, Write};
use std::sync;
use std::sync::atomic;
+
use libc;
use nix;
use nix::poll;
use nix::sys::signal;
use nix::sys::termios;
+use unicode_width::UnicodeWidthChar;
-use char_iter;
use config::Config;
use consts::{self, KeyPress};
use Result;
use error;
-use super::{RawMode, RawReader, Term};
+use line_buffer::LineBuffer;
+use super::{Position, RawMode, RawReader, Renderer, Term};
const STDIN_FILENO: libc::c_int = libc::STDIN_FILENO;
const STDOUT_FILENO: libc::c_int = libc::STDOUT_FILENO;
@@ -75,9 +77,11 @@
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
loop {
let res = unsafe {
- libc::read(STDIN_FILENO,
- buf.as_mut_ptr() as *mut libc::c_void,
- buf.len() as libc::size_t)
+ libc::read(
+ STDIN_FILENO,
+ buf.as_mut_ptr() as *mut libc::c_void,
+ buf.len() as libc::size_t,
+ )
};
if res == -1 {
let error = io::Error::last_os_error();
@@ -93,97 +97,152 @@
/// Console input reader
pub struct PosixRawReader {
- chars: char_iter::Chars<StdinRaw>,
+ chars: Chars<StdinRaw>,
timeout_ms: i32,
}
impl PosixRawReader {
- pub fn new(config: &Config) -> Result<PosixRawReader> {
+ fn new(config: &Config) -> Result<PosixRawReader> {
let stdin = StdinRaw {};
Ok(PosixRawReader {
- chars: char_iter::chars(stdin),
- timeout_ms: config.keyseq_timeout(),
- })
+ chars: stdin.chars(),
+ timeout_ms: config.keyseq_timeout(),
+ })
}
fn escape_sequence(&mut self) -> Result<KeyPress> {
// Read the next two bytes representing the escape sequence.
let seq1 = try!(self.next_char());
if seq1 == '[' {
- // ESC [ sequences.
+ // 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, // '1': xterm
- '3' => KeyPress::Delete,
- '4' | '8' => KeyPress::End, // '4': xterm
- '5' => KeyPress::PageUp,
- '6' => KeyPress::PageDown,
- _ => {
- debug!(target: "rustyline", "unsupported esc sequence: ESC{:?}{:?}{:?}", seq1, seq2, seq3);
- KeyPress::UnknownEscSeq
+ '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 {
- debug!(target: "rustyline", "unsupported esc sequence: ESC{:?}{:?}{:?}", seq1, seq2, seq3);
- Ok(KeyPress::UnknownEscSeq)
+ 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, // ANSI
- 'B' => KeyPress::Down,
- 'C' => KeyPress::Right,
- 'D' => KeyPress::Left,
- 'F' => KeyPress::End,
- 'H' => KeyPress::Home,
- _ => {
- debug!(target: "rustyline", "unsupported esc sequence: ESC{:?}{:?}", seq1, seq2);
- KeyPress::UnknownEscSeq
- }
- })
+ '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
+ }
+ })
}
} else if seq1 == 'O' {
- // ESC O sequences.
+ // xterm
+ // ESC O sequences. (SS3)
let seq2 = try!(self.next_char());
Ok(match seq2 {
- 'A' => KeyPress::Up,
- 'B' => KeyPress::Down,
- 'C' => KeyPress::Right,
- 'D' => KeyPress::Left,
- 'F' => KeyPress::End,
- 'H' => KeyPress::Home,
- _ => {
- debug!(target: "rustyline", "unsupported esc sequence: ESC{:?}{:?}", seq1, seq2);
- KeyPress::UnknownEscSeq
- }
- })
+ '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
+ }
+ })
} else {
// TODO ESC-R (r): Undo all changes made to this line.
- Ok(match seq1 {
- '\x08' => KeyPress::Meta('\x08'), // Backspace
- '-' => KeyPress::Meta('-'),
- '0'...'9' => KeyPress::Meta(seq1),
- '<' => KeyPress::Meta('<'),
- '>' => KeyPress::Meta('>'),
- 'b' | 'B' => KeyPress::Meta('B'),
- 'c' | 'C' => KeyPress::Meta('C'),
- 'd' | 'D' => KeyPress::Meta('D'),
- 'f' | 'F' => KeyPress::Meta('F'),
- 'l' | 'L' => KeyPress::Meta('L'),
- 'n' | 'N' => KeyPress::Meta('N'),
- 'p' | 'P' => KeyPress::Meta('P'),
- 't' | 'T' => KeyPress::Meta('T'),
- 'u' | 'U' => KeyPress::Meta('U'),
- 'y' | 'Y' => KeyPress::Meta('Y'),
- '\x7f' => KeyPress::Meta('\x7f'), // Delete
- _ => {
- debug!(target: "rustyline", "unsupported esc sequence: M-{:?}", seq1);
- KeyPress::UnknownEscSeq
- }
- })
+ Ok(KeyPress::Meta(seq1))
}
}
}
@@ -194,8 +253,9 @@
let mut key = consts::char_to_key_press(c);
if key == KeyPress::Esc {
- let mut fds =
- [poll::PollFd::new(STDIN_FILENO, poll::POLLIN, poll::EventFlags::empty())];
+ let mut fds = [
+ poll::PollFd::new(STDIN_FILENO, poll::POLLIN, poll::EventFlags::empty()),
+ ];
match poll::poll(&mut fds, self.timeout_ms) {
Ok(n) if n == 0 => {
// single escape
@@ -220,15 +280,210 @@
}
}
+/// Console output writer
+pub struct PosixRenderer {
+ out: Stdout,
+ cols: usize, // Number of columns in terminal
+}
+
+impl PosixRenderer {
+ fn new() -> PosixRenderer {
+ let (cols, _) = get_win_size();
+ PosixRenderer {
+ out: io::stdout(),
+ cols: cols,
+ }
+ }
+}
+
+impl Renderer for PosixRenderer {
+ fn move_cursor(&mut self, old: Position, new: Position) -> Result<()> {
+ use std::fmt::Write;
+ let mut ab = String::new();
+ if new.row > old.row {
+ // move down
+ let row_shift = new.row - old.row;
+ if row_shift == 1 {
+ ab.push_str("\x1b[B");
+ } else {
+ write!(ab, "\x1b[{}B", row_shift).unwrap();
+ }
+ } else if new.row < old.row {
+ // move up
+ let row_shift = old.row - new.row;
+ if row_shift == 1 {
+ ab.push_str("\x1b[A");
+ } else {
+ write!(ab, "\x1b[{}A", row_shift).unwrap();
+ }
+ }
+ if new.col > old.col {
+ // move right
+ let col_shift = new.col - old.col;
+ if col_shift == 1 {
+ ab.push_str("\x1b[C");
+ } else {
+ write!(ab, "\x1b[{}C", col_shift).unwrap();
+ }
+ } else if new.col < old.col {
+ // move left
+ let col_shift = old.col - new.col;
+ if col_shift == 1 {
+ ab.push_str("\x1b[D");
+ } else {
+ write!(ab, "\x1b[{}D", col_shift).unwrap();
+ }
+ }
+ self.write_and_flush(ab.as_bytes())
+ }
+
+ fn refresh_line(
+ &mut self,
+ prompt: &str,
+ prompt_size: Position,
+ line: &LineBuffer,
+ current_row: usize,
+ old_rows: usize,
+ ) -> Result<(Position, Position)> {
+ use std::fmt::Write;
+ let mut ab = String::new();
+
+ // calculate the position of the end of the input line
+ let end_pos = self.calculate_position(line, prompt_size);
+ // calculate the desired position of the cursor
+ let cursor = self.calculate_position(&line[..line.pos()], prompt_size);
+
+ let cursor_row_movement = old_rows - current_row;
+ // move the cursor down as required
+ if cursor_row_movement > 0 {
+ write!(ab, "\x1b[{}B", cursor_row_movement).unwrap();
+ }
+ // clear old rows
+ for _ in 0..old_rows {
+ ab.push_str("\r\x1b[0K\x1b[1A");
+ }
+ // clear the line
+ ab.push_str("\r\x1b[0K");
+
+ // display the prompt
+ ab.push_str(prompt);
+ // display the input line
+ ab.push_str(line);
+ // we have to generate our own newline on line wrap
+ if end_pos.col == 0 && end_pos.row > 0 {
+ ab.push_str("\n");
+ }
+ // position the cursor
+ let cursor_row_movement = end_pos.row - cursor.row;
+ // move the cursor up as required
+ if cursor_row_movement > 0 {
+ write!(ab, "\x1b[{}A", cursor_row_movement).unwrap();
+ }
+ // position the cursor within the line
+ if cursor.col > 0 {
+ write!(ab, "\r\x1b[{}C", cursor.col).unwrap();
+ } else {
+ ab.push('\r');
+ }
+
+ try!(self.write_and_flush(ab.as_bytes()));
+ Ok((cursor, end_pos))
+ }
+
+ fn write_and_flush(&mut self, buf: &[u8]) -> Result<()> {
+ try!(self.out.write_all(buf));
+ try!(self.out.flush());
+ Ok(())
+ }
+
+ /// 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;
+ for c in s.chars() {
+ let cw = if esc_seq == 1 {
+ if c == '[' {
+ // CSI
+ esc_seq = 2;
+ } else {
+ // two-character sequence
+ esc_seq = 0;
+ }
+ None
+ } else if esc_seq == 2 {
+ if c == ';' || (c >= '0' && c <= '9') {
+ } else if c == 'm' {
+ // last
+ esc_seq = 0;
+ } else {
+ // not supported
+ esc_seq = 0;
+ }
+ None
+ } else if c == '\x1b' {
+ esc_seq = 1;
+ None
+ } else if c == '\n' {
+ pos.col = 0;
+ pos.row += 1;
+ None
+ } else {
+ c.width()
+ };
+ if let Some(cw) = cw {
+ pos.col += cw;
+ if pos.col > self.cols {
+ pos.row += 1;
+ pos.col = cw;
+ }
+ }
+ }
+ if pos.col == self.cols {
+ pos.col = 0;
+ pos.row += 1;
+ }
+ pos
+ }
+
+ /// Clear the screen. Used to handle ctrl+l
+ fn clear_screen(&mut self) -> Result<()> {
+ self.write_and_flush(b"\x1b[H\x1b[2J")
+ }
+
+ /// Check if a SIGWINCH signal has been received
+ fn sigwinch(&self) -> bool {
+ SIGWINCH.compare_and_swap(true, false, atomic::Ordering::SeqCst)
+ }
+
+ /// Try to update the number of columns in the current terminal,
+ fn update_size(&mut self) {
+ let (cols, _) = get_win_size();
+ self.cols = cols;
+ }
+
+ 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 {
+ let (_, rows) = get_win_size();
+ rows
+ }
+}
static SIGWINCH_ONCE: sync::Once = sync::ONCE_INIT;
static SIGWINCH: atomic::AtomicBool = atomic::ATOMIC_BOOL_INIT;
fn install_sigwinch_handler() {
SIGWINCH_ONCE.call_once(|| unsafe {
- let sigwinch = signal::SigAction::new(signal::SigHandler::Handler(sigwinch_handler),
- signal::SaFlags::empty(),
- signal::SigSet::empty());
+ let sigwinch = signal::SigAction::new(
+ signal::SigHandler::Handler(sigwinch_handler),
+ signal::SaFlags::empty(),
+ signal::SigSet::empty(),
+ );
let _ = signal::sigaction(signal::SIGWINCH, &sigwinch);
});
}
@@ -240,7 +495,7 @@
pub type Terminal = PosixTerminal;
-#[derive(Clone,Debug)]
+#[derive(Clone, Debug)]
pub struct PosixTerminal {
unsupported: bool,
stdin_isatty: bool,
@@ -248,7 +503,7 @@
impl Term for PosixTerminal {
type Reader = PosixRawReader;
- type Writer = Stdout;
+ type Writer = PosixRenderer;
type Mode = Mode;
fn new() -> PosixTerminal {
@@ -264,7 +519,8 @@
// Init checks:
- /// Check if current terminal can provide a rich line-editing user interface.
+ /// Check if current terminal can provide a rich line-editing user
+ /// interface.
fn is_unsupported(&self) -> bool {
self.unsupported
}
@@ -276,20 +532,6 @@
// Interactive loop:
- /// Try to get the number of columns in the current terminal,
- /// or assume 80 if it fails.
- fn get_columns(&self) -> usize {
- let (cols, _) = get_win_size();
- cols
- }
-
- /// Try to get the number of rows in the current terminal,
- /// or assume 24 if it fails.
- fn get_rows(&self) -> usize {
- let (_, rows) = get_win_size();
- rows
- }
-
fn enable_raw_mode(&self) -> Result<Mode> {
use nix::errno::Errno::ENOTTY;
use nix::sys::termios::{BRKINT, CS8, ECHO, ICANON, ICRNL, IEXTEN, INPCK, ISIG, ISTRIP,
@@ -318,33 +560,31 @@
PosixRawReader::new(config)
}
- fn create_writer(&self) -> Stdout {
- io::stdout()
- }
-
- /// Check if a SIGWINCH signal has been received
- fn sigwinch(&self) -> bool {
- SIGWINCH.compare_and_swap(true, false, atomic::Ordering::SeqCst)
- }
-
- /// Clear the screen. Used to handle ctrl+l
- fn clear_screen(&mut self, w: &mut Write) -> Result<()> {
- try!(w.write_all(b"\x1b[H\x1b[2J"));
- try!(w.flush());
- Ok(())
+ fn create_writer(&self) -> PosixRenderer {
+ PosixRenderer::new()
}
}
#[cfg(unix)]
pub fn suspend() -> Result<()> {
- // For macos:
- try!(signal::kill(nix::unistd::getppid(), signal::SIGTSTP));
- try!(signal::kill(nix::unistd::getpid(), signal::SIGTSTP));
+ // suspend the whole process group
+ try!(signal::kill(0, signal::SIGTSTP));
Ok(())
}
-#[cfg(all(unix,test))]
+#[cfg(all(unix, test))]
mod test {
+ use std::io::{self, Stdout};
+ use super::{Position, Renderer};
+
+ #[test]
+ fn prompt_with_ansi_escape_codes() {
+ let out = io::stdout();
+ let pos = out.calculate_position("\x1b[1;32m>>\x1b[0m ", Position::default(), 80);
+ assert_eq!(3, pos.col);
+ assert_eq!(0, pos.row);
+ }
+
#[test]
fn test_unsupported_term() {
::std::env::set_var("TERM", "xterm");
diff --git a/src/tty/windows.rs b/src/tty/windows.rs
index 7b73e2f..62014ff 100644
--- a/src/tty/windows.rs
+++ b/src/tty/windows.rs
@@ -4,13 +4,15 @@
use std::sync::atomic;
use kernel32;
+use unicode_width::UnicodeWidthChar;
use winapi;
use config::Config;
use consts::{self, KeyPress};
use error;
use Result;
-use super::{RawMode, RawReader, Term};
+use line_buffer::LineBuffer;
+use super::{Position, RawMode, RawReader, Renderer, Term};
const STDIN_FILENO: winapi::DWORD = winapi::STD_INPUT_HANDLE;
const STDOUT_FILENO: winapi::DWORD = winapi::STD_OUTPUT_HANDLE;
@@ -20,8 +22,10 @@
if handle == winapi::INVALID_HANDLE_VALUE {
try!(Err(io::Error::last_os_error()));
} else if handle.is_null() {
- try!(Err(io::Error::new(io::ErrorKind::Other,
- "no stdio handle available for this process")));
+ try!(Err(io::Error::new(
+ io::ErrorKind::Other,
+ "no stdio handle available for this process",
+ )));
}
Ok(handle)
}
@@ -43,7 +47,10 @@
let mut info = unsafe { mem::zeroed() };
match unsafe { kernel32::GetConsoleScreenBufferInfo(handle, &mut info) } {
0 => (80, 24),
- _ => (info.dwSize.X as usize, (1 + info.srWindow.Bottom - info.srWindow.Top) as usize),
+ _ => (
+ info.dwSize.X as usize,
+ (1 + info.srWindow.Bottom - info.srWindow.Top) as usize,
+ ), // (info.srWindow.Right - info.srWindow.Left + 1)
}
}
@@ -55,7 +62,7 @@
pub type Mode = ConsoleMode;
-#[derive(Clone,Copy,Debug)]
+#[derive(Clone, Copy, Debug)]
pub struct ConsoleMode {
original_mode: winapi::DWORD,
stdin_handle: winapi::HANDLE,
@@ -64,7 +71,10 @@
impl RawMode for Mode {
/// Disable RAW mode for the terminal.
fn disable_raw_mode(&self) -> Result<()> {
- check!(kernel32::SetConsoleMode(self.stdin_handle, self.original_mode));
+ check!(kernel32::SetConsoleMode(
+ self.stdin_handle,
+ self.original_mode,
+ ));
Ok(())
}
}
@@ -88,17 +98,18 @@
impl RawReader for ConsoleRawReader {
fn next_key(&mut self) -> Result<KeyPress> {
use std::char::decode_utf16;
- // use winapi::{LEFT_ALT_PRESSED, LEFT_CTRL_PRESSED, RIGHT_ALT_PRESSED, RIGHT_CTRL_PRESSED};
- use winapi::{LEFT_ALT_PRESSED, RIGHT_ALT_PRESSED};
+ use winapi::{LEFT_ALT_PRESSED, LEFT_CTRL_PRESSED, RIGHT_ALT_PRESSED, RIGHT_CTRL_PRESSED};
let mut rec: winapi::INPUT_RECORD = unsafe { mem::zeroed() };
let mut count = 0;
loop {
// TODO GetNumberOfConsoleInputEvents
- check!(kernel32::ReadConsoleInputW(self.handle,
- &mut rec,
- 1 as winapi::DWORD,
- &mut count));
+ check!(kernel32::ReadConsoleInputW(
+ self.handle,
+ &mut rec,
+ 1 as winapi::DWORD,
+ &mut count,
+ ));
if rec.EventType == winapi::WINDOW_BUFFER_SIZE_EVENT {
SIGWINCH.store(true, atomic::Ordering::SeqCst);
@@ -110,27 +121,70 @@
let key_event = unsafe { rec.KeyEvent() };
// writeln!(io::stderr(), "key_event: {:?}", key_event).unwrap();
if key_event.bKeyDown == 0 &&
- key_event.wVirtualKeyCode != winapi::VK_MENU as winapi::WORD {
+ key_event.wVirtualKeyCode != winapi::VK_MENU as winapi::WORD
+ {
continue;
}
+ // 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) != 0;
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 ctrl = key_event.dwControlKeyState & (LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED) != 0;
let meta = alt;
let utf16 = key_event.UnicodeChar;
if utf16 == 0 {
match key_event.wVirtualKeyCode as i32 {
- winapi::VK_LEFT => return Ok(KeyPress::Left),
- winapi::VK_RIGHT => return Ok(KeyPress::Right),
- winapi::VK_UP => return Ok(KeyPress::Up),
- winapi::VK_DOWN => return Ok(KeyPress::Down),
+ winapi::VK_LEFT => {
+ return Ok(if ctrl {
+ KeyPress::ControlLeft
+ } else {
+ KeyPress::Left
+ })
+ }
+ winapi::VK_RIGHT => {
+ return Ok(if ctrl {
+ KeyPress::ControlRight
+ } else {
+ KeyPress::Right
+ })
+ }
+ winapi::VK_UP => {
+ return Ok(if ctrl {
+ KeyPress::ControlUp
+ } else {
+ KeyPress::Up
+ })
+ }
+ winapi::VK_DOWN => {
+ return Ok(if ctrl {
+ KeyPress::ControlDown
+ } else {
+ KeyPress::Down
+ })
+ }
winapi::VK_DELETE => return Ok(KeyPress::Delete),
winapi::VK_HOME => return Ok(KeyPress::Home),
winapi::VK_END => return Ok(KeyPress::End),
winapi::VK_PRIOR => return Ok(KeyPress::PageUp),
winapi::VK_NEXT => return Ok(KeyPress::PageDown),
+ winapi::VK_INSERT => return Ok(KeyPress::Insert),
+ winapi::VK_F1 => return Ok(KeyPress::F(1)),
+ winapi::VK_F2 => return Ok(KeyPress::F(2)),
+ winapi::VK_F3 => return Ok(KeyPress::F(3)),
+ winapi::VK_F4 => return Ok(KeyPress::F(4)),
+ winapi::VK_F5 => return Ok(KeyPress::F(5)),
+ winapi::VK_F6 => return Ok(KeyPress::F(6)),
+ winapi::VK_F7 => return Ok(KeyPress::F(7)),
+ winapi::VK_F8 => return Ok(KeyPress::F(8)),
+ winapi::VK_F9 => return Ok(KeyPress::F(9)),
+ winapi::VK_F10 => return Ok(KeyPress::F(10)),
+ winapi::VK_F11 => return Ok(KeyPress::F(11)),
+ winapi::VK_F12 => return Ok(KeyPress::F(12)),
+ // winapi::VK_BACK is correctly handled because the key_event.UnicodeChar is also
+ // set.
_ => continue,
};
} else if utf16 == 27 {
@@ -144,26 +198,7 @@
}
let c = try!(orc.unwrap());
if meta {
- return Ok(match c {
- '-' => KeyPress::Meta('-'),
- '0'...'9' => KeyPress::Meta(c),
- '<' => KeyPress::Meta('<'),
- '>' => KeyPress::Meta('>'),
- 'b' | 'B' => KeyPress::Meta('B'),
- 'c' | 'C' => KeyPress::Meta('C'),
- 'd' | 'D' => KeyPress::Meta('D'),
- 'f' | 'F' => KeyPress::Meta('F'),
- 'l' | 'L' => KeyPress::Meta('L'),
- 'n' | 'N' => KeyPress::Meta('N'),
- 'p' | 'P' => KeyPress::Meta('P'),
- 't' | 'T' => KeyPress::Meta('T'),
- 'u' | 'U' => KeyPress::Meta('U'),
- 'y' | 'Y' => KeyPress::Meta('Y'),
- _ => {
- debug!(target: "rustyline", "unsupported esc sequence: M-{:?}", c);
- KeyPress::UnknownEscSeq
- }
- });
+ return Ok(KeyPress::Meta(c));
} else {
return Ok(consts::char_to_key_press(c));
}
@@ -182,46 +217,192 @@
}
}
+pub struct ConsoleRenderer {
+ out: Stdout,
+ handle: winapi::HANDLE,
+ cols: usize, // Number of columns in terminal
+}
+
+impl ConsoleRenderer {
+ fn new(handle: winapi::HANDLE) -> ConsoleRenderer {
+ let (cols, _) = get_win_size(handle);
+ ConsoleRenderer {
+ out: io::stdout(),
+ handle: handle,
+ cols: cols,
+ }
+ }
+
+ fn get_console_screen_buffer_info(&self) -> Result<winapi::CONSOLE_SCREEN_BUFFER_INFO> {
+ let mut info = unsafe { mem::zeroed() };
+ check!(kernel32::GetConsoleScreenBufferInfo(self.handle, &mut info));
+ Ok(info)
+ }
+
+ fn set_console_cursor_position(&mut self, pos: winapi::COORD) -> Result<()> {
+ check!(kernel32::SetConsoleCursorPosition(self.handle, pos));
+ Ok(())
+ }
+
+ fn fill_console_output_character(
+ &mut self,
+ length: winapi::DWORD,
+ pos: winapi::COORD,
+ ) -> Result<()> {
+ let mut _count = 0;
+ check!(kernel32::FillConsoleOutputCharacterA(
+ self.handle,
+ ' ' as winapi::CHAR,
+ length,
+ pos,
+ &mut _count,
+ ));
+ Ok(())
+ }
+}
+
+impl Renderer for ConsoleRenderer {
+ fn move_cursor(&mut self, old: Position, new: Position) -> Result<()> {
+ let mut info = try!(self.get_console_screen_buffer_info());
+ if new.row > old.row {
+ info.dwCursorPosition.Y += (new.row - old.row) as i16;
+ } else {
+ info.dwCursorPosition.Y -= (old.row - new.row) as i16;
+ }
+ if new.col > old.col {
+ info.dwCursorPosition.X += (new.col - old.col) as i16;
+ } else {
+ info.dwCursorPosition.X -= (old.col - new.col) as i16;
+ }
+ self.set_console_cursor_position(info.dwCursorPosition)
+ }
+
+ fn refresh_line(
+ &mut self,
+ prompt: &str,
+ prompt_size: Position,
+ line: &LineBuffer,
+ current_row: usize,
+ old_rows: usize,
+ ) -> Result<(Position, Position)> {
+ // calculate the position of the end of the input line
+ let end_pos = self.calculate_position(line, prompt_size);
+ // calculate the desired position of the cursor
+ let cursor = self.calculate_position(&line[..line.pos()], prompt_size);
+
+ // position at the start of the prompt, clear to end of previous input
+ let mut info = try!(self.get_console_screen_buffer_info());
+ info.dwCursorPosition.X = 0;
+ info.dwCursorPosition.Y -= current_row as i16;
+ try!(self.set_console_cursor_position(info.dwCursorPosition));
+ let mut _count = 0;
+ try!(self.fill_console_output_character(
+ (info.dwSize.X * (old_rows as i16 + 1)) as u32,
+ info.dwCursorPosition,
+ ));
+ let mut ab = String::new();
+ // display the prompt
+ ab.push_str(prompt); // TODO handle ansi escape code (SetConsoleTextAttribute)
+ // display the input line
+ ab.push_str(&line);
+ try!(self.write_and_flush(ab.as_bytes()));
+
+ // position the cursor
+ let mut info = try!(self.get_console_screen_buffer_info());
+ info.dwCursorPosition.X = cursor.col as i16;
+ info.dwCursorPosition.Y -= (end_pos.row - cursor.row) as i16;
+ try!(self.set_console_cursor_position(info.dwCursorPosition));
+ Ok((cursor, end_pos))
+ }
+
+ fn write_and_flush(&mut self, buf: &[u8]) -> Result<()> {
+ try!(self.out.write_all(buf));
+ try!(self.out.flush());
+ Ok(())
+ }
+
+ /// Characters with 2 column width are correctly handled (not splitted).
+ fn calculate_position(&self, s: &str, orig: Position) -> Position {
+ let mut pos = orig;
+ for c in s.chars() {
+ let cw = if c == '\n' {
+ pos.col = 0;
+ pos.row += 1;
+ None
+ } else {
+ c.width()
+ };
+ if let Some(cw) = cw {
+ pos.col += cw;
+ if pos.col > self.cols {
+ pos.row += 1;
+ pos.col = cw;
+ }
+ }
+ }
+ if pos.col == self.cols {
+ pos.col = 0;
+ pos.row += 1;
+ }
+ pos
+ }
+
+ /// Clear the screen. Used to handle ctrl+l
+ fn clear_screen(&mut self) -> Result<()> {
+ let info = try!(self.get_console_screen_buffer_info());
+ let coord = winapi::COORD { X: 0, Y: 0 };
+ check!(kernel32::SetConsoleCursorPosition(self.handle, coord));
+ let mut _count = 0;
+ let n = info.dwSize.X as winapi::DWORD * info.dwSize.Y as winapi::DWORD;
+ check!(kernel32::FillConsoleOutputCharacterA(
+ self.handle,
+ ' ' as winapi::CHAR,
+ n,
+ coord,
+ &mut _count,
+ ));
+ Ok(())
+ }
+
+ fn sigwinch(&self) -> bool {
+ SIGWINCH.compare_and_swap(true, false, atomic::Ordering::SeqCst)
+ }
+
+ /// Try to get the number of columns in the current terminal,
+ /// or assume 80 if it fails.
+ fn update_size(&mut self) {
+ let (cols, _) = get_win_size(self.handle);
+ self.cols = cols;
+ }
+
+ 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 {
+ let (_, rows) = get_win_size(self.handle);
+ rows
+ }
+}
+
static SIGWINCH: atomic::AtomicBool = atomic::ATOMIC_BOOL_INIT;
pub type Terminal = Console;
-#[derive(Clone,Debug)]
+#[derive(Clone, Debug)]
pub struct Console {
stdin_isatty: bool,
stdin_handle: winapi::HANDLE,
stdout_handle: winapi::HANDLE,
}
-impl Console {
- pub fn get_console_screen_buffer_info(&self) -> Result<winapi::CONSOLE_SCREEN_BUFFER_INFO> {
- let mut info = unsafe { mem::zeroed() };
- check!(kernel32::GetConsoleScreenBufferInfo(self.stdout_handle, &mut info));
- Ok(info)
- }
-
- pub fn set_console_cursor_position(&mut self, pos: winapi::COORD) -> Result<()> {
- check!(kernel32::SetConsoleCursorPosition(self.stdout_handle, pos));
- Ok(())
- }
-
- pub fn fill_console_output_character(&mut self,
- length: winapi::DWORD,
- pos: winapi::COORD)
- -> Result<()> {
- let mut _count = 0;
- check!(kernel32::FillConsoleOutputCharacterA(self.stdout_handle,
- ' ' as winapi::CHAR,
- length,
- pos,
- &mut _count));
- Ok(())
- }
-}
+impl Console {}
impl Term for Console {
type Reader = ConsoleRawReader;
- type Writer = Stdout;
+ type Writer = ConsoleRenderer;
type Mode = Mode;
fn new() -> Console {
@@ -256,31 +437,19 @@
// See ReadConsoleInputW && WINDOW_BUFFER_SIZE_EVENT
// }
- /// Try to get the number of columns in the current terminal,
- /// or assume 80 if it fails.
- fn get_columns(&self) -> usize {
- let (cols, _) = get_win_size(self.stdout_handle);
- cols
- }
-
- /// Try to get the number of rows in the current terminal,
- /// or assume 24 if it fails.
- fn get_rows(&self) -> usize {
- let (_, rows) = get_win_size(self.stdout_handle);
- rows
- }
-
/// Enable RAW mode for the terminal.
fn enable_raw_mode(&self) -> Result<Mode> {
if !self.stdin_isatty {
- try!(Err(io::Error::new(io::ErrorKind::Other,
- "no stdio handle available for this process")));
+ try!(Err(io::Error::new(
+ io::ErrorKind::Other,
+ "no stdio handle available for this process",
+ )));
}
let original_mode = try!(get_console_mode(self.stdin_handle));
// Disable these modes
let raw = original_mode &
- !(winapi::wincon::ENABLE_LINE_INPUT | winapi::wincon::ENABLE_ECHO_INPUT |
- winapi::wincon::ENABLE_PROCESSED_INPUT);
+ !(winapi::wincon::ENABLE_LINE_INPUT | winapi::wincon::ENABLE_ECHO_INPUT |
+ winapi::wincon::ENABLE_PROCESSED_INPUT);
// Enable these modes
let raw = raw | winapi::wincon::ENABLE_EXTENDED_FLAGS;
let raw = raw | winapi::wincon::ENABLE_INSERT_MODE;
@@ -297,26 +466,7 @@
ConsoleRawReader::new()
}
- fn create_writer(&self) -> Stdout {
- io::stdout()
- }
-
- fn sigwinch(&self) -> bool {
- SIGWINCH.compare_and_swap(true, false, atomic::Ordering::SeqCst)
- }
-
- /// Clear the screen. Used to handle ctrl+l
- fn clear_screen(&mut self, _: &mut Write) -> Result<()> {
- let info = try!(self.get_console_screen_buffer_info());
- let coord = winapi::COORD { X: 0, Y: 0 };
- check!(kernel32::SetConsoleCursorPosition(self.stdout_handle, coord));
- let mut _count = 0;
- let n = info.dwSize.X as winapi::DWORD * info.dwSize.Y as winapi::DWORD;
- check!(kernel32::FillConsoleOutputCharacterA(self.stdout_handle,
- ' ' as winapi::CHAR,
- n,
- coord,
- &mut _count));
- Ok(())
+ fn create_writer(&self) -> ConsoleRenderer {
+ ConsoleRenderer::new(self.stdout_handle)
}
}
diff --git a/src/undo.rs b/src/undo.rs
new file mode 100644
index 0000000..80e6ee7
--- /dev/null
+++ b/src/undo.rs
@@ -0,0 +1,420 @@
+//! Undo API
+use std::fmt::Debug;
+
+use line_buffer::{ChangeListener, DeleteListener, Direction, LineBuffer};
+use std_unicode::str::UnicodeStr;
+use unicode_segmentation::UnicodeSegmentation;
+
+enum Change {
+ Begin,
+ End,
+ Insert { idx: usize, text: String }, // QuotedInsert, SelfInsert, Yank
+ Delete { idx: usize, text: String }, /* BackwardDeleteChar, BackwardKillWord, DeleteChar,
+ * KillLine, KillWholeLine, KillWord,
+ * UnixLikeDiscard, ViDeleteTo */
+ Replace {
+ idx: usize,
+ old: String,
+ new: String,
+ }, /* CapitalizeWord, Complete, DowncaseWord, Replace, TransposeChars, TransposeWords,
+ * UpcaseWord, YankPop */
+}
+
+impl Change {
+ fn undo(&self, line: &mut LineBuffer) {
+ match *self {
+ Change::Begin | Change::End => {
+ unreachable!();
+ }
+ Change::Insert { idx, ref text } => {
+ line.delete_range(idx..idx + text.len());
+ }
+ Change::Delete { idx, ref text } => {
+ line.insert_str(idx, text);
+ line.set_pos(idx + text.len());
+ }
+ Change::Replace {
+ idx,
+ ref old,
+ ref new,
+ } => {
+ line.replace(idx..idx + new.len(), old);
+ }
+ }
+ }
+
+ #[cfg(test)]
+ fn redo(&self, line: &mut LineBuffer) {
+ match *self {
+ Change::Begin | Change::End => {
+ unreachable!();
+ }
+ Change::Insert { idx, ref text } => {
+ line.insert_str(idx, text);
+ }
+ Change::Delete { idx, ref text } => {
+ line.delete_range(idx..idx + text.len());
+ }
+ Change::Replace {
+ idx,
+ ref old,
+ ref new,
+ } => {
+ line.replace(idx..idx + old.len(), new);
+ }
+ }
+ }
+
+ fn insert_seq(&self, indx: usize) -> bool {
+ if let Change::Insert { idx, ref text } = *self {
+ idx + text.len() == indx
+ } else {
+ false
+ }
+ }
+
+ fn delete_seq(&self, indx: usize, len: usize) -> bool {
+ if let Change::Delete { idx, .. } = *self {
+ // delete or backspace
+ idx == indx || idx == indx + len
+ } else {
+ false
+ }
+ }
+
+ fn replace_seq(&self, indx: usize) -> bool {
+ if let Change::Replace { idx, ref new, .. } = *self {
+ idx + new.len() == indx
+ } else {
+ false
+ }
+ }
+}
+
+pub struct Changeset {
+ undos: Vec<Change>, // undoable changes
+ redos: Vec<Change>, // undone changes, redoable
+}
+
+impl Changeset {
+ pub fn new() -> Changeset {
+ Changeset {
+ undos: Vec::new(),
+ redos: Vec::new(),
+ }
+ }
+
+ pub fn begin(&mut self) -> usize {
+ debug!(target: "rustyline", "Changeset::begin");
+ self.redos.clear();
+ let mark = self.undos.len();
+ self.undos.push(Change::Begin);
+ mark
+ }
+
+ pub fn end(&mut self) {
+ debug!(target: "rustyline", "Changeset::end");
+ self.redos.clear();
+ if let Some(&Change::Begin) = self.undos.last() {
+ // emtpy Begin..End
+ self.undos.pop();
+ } else {
+ self.undos.push(Change::End);
+ }
+ }
+
+ fn insert_char(idx: usize, c: char) -> Change {
+ let mut text = String::new();
+ text.push(c);
+ Change::Insert {
+ idx: idx,
+ text: text,
+ }
+ }
+
+ pub fn insert(&mut self, idx: usize, c: char) {
+ debug!(target: "rustyline", "Changeset::insert({}, {:?})", idx, c);
+ self.redos.clear();
+ if !c.is_alphanumeric() || !self.undos.last().map_or(false, |lc| lc.insert_seq(idx)) {
+ self.undos.push(Self::insert_char(idx, c));
+ return;
+ }
+ // merge consecutive char insertions when char is alphanumeric
+ let mut last_change = self.undos.pop().unwrap();
+ if let Change::Insert { ref mut text, .. } = last_change {
+ text.push(c);
+ } else {
+ unreachable!();
+ }
+ self.undos.push(last_change);
+ }
+
+ pub fn insert_str<S: Into<String> + Debug>(&mut self, idx: usize, string: S) {
+ debug!(target: "rustyline", "Changeset::insert_str({}, {:?})", idx, string);
+ self.redos.clear();
+ self.undos.push(Change::Insert {
+ idx: idx,
+ text: string.into(),
+ });
+ }
+
+ pub fn delete<S: AsRef<str> + Into<String> + Debug>(&mut self, indx: usize, string: S) {
+ debug!(target: "rustyline", "Changeset::delete({}, {:?})", indx, string);
+ self.redos.clear();
+
+ 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,
+ text: string.into(),
+ });
+ return;
+ }
+ // merge consecutive char deletions when char is alphanumeric
+ let mut last_change = self.undos.pop().unwrap();
+ if let Change::Delete {
+ ref mut idx,
+ ref mut text,
+ } = last_change
+ {
+ if *idx == indx {
+ text.push_str(string.as_ref());
+ } else {
+ text.insert_str(0, string.as_ref());
+ *idx = indx;
+ }
+ } else {
+ unreachable!();
+ }
+ self.undos.push(last_change);
+ }
+
+ fn single_char(s: &str) -> bool {
+ let mut graphemes = s.graphemes(true);
+ graphemes.next().map_or(
+ false,
+ |grapheme| grapheme.is_alphanumeric(),
+ ) && graphemes.next().is_none()
+ }
+
+ pub fn replace<S: AsRef<str> + Into<String> + Debug>(&mut self, indx: usize, old_: S, new_: S) {
+ debug!(target: "rustyline", "Changeset::replace({}, {:?}, {:?})", indx, old_, new_);
+ self.redos.clear();
+
+ if !self.undos.last().map_or(false, |lc| lc.replace_seq(indx)) {
+ self.undos.push(Change::Replace {
+ idx: indx,
+ old: old_.into(),
+ new: new_.into(),
+ });
+ return;
+ }
+
+ // merge consecutive char replacements
+ let mut last_change = self.undos.pop().unwrap();
+ if let Change::Replace {
+ ref mut old,
+ ref mut new,
+ ..
+ } = last_change
+ {
+ old.push_str(old_.as_ref());
+ new.push_str(new_.as_ref());
+ } else {
+ unreachable!();
+ }
+ self.undos.push(last_change);
+ }
+
+ pub fn undo(&mut self, line: &mut LineBuffer) -> bool {
+ debug!(target: "rustyline", "Changeset::undo");
+ let mut waiting_for_begin = 0;
+ let mut undone = false;
+ loop {
+ if let Some(change) = self.undos.pop() {
+ match change {
+ Change::Begin => {
+ waiting_for_begin -= 1;
+ }
+ Change::End => {
+ waiting_for_begin += 1;
+ }
+ _ => {
+ change.undo(line);
+ self.redos.push(change);
+ undone = true;
+ }
+ };
+ } else {
+ break;
+ }
+ if waiting_for_begin <= 0 {
+ break;
+ }
+ }
+ undone
+ }
+
+ pub fn truncate(&mut self, len: usize) {
+ debug!(target: "rustyline", "Changeset::truncate({})", len);
+ self.undos.truncate(len);
+ }
+
+ #[cfg(test)]
+ pub fn redo(&mut self, line: &mut LineBuffer) -> bool {
+ let mut waiting_for_end = 0;
+ let mut redone = false;
+ loop {
+ if let Some(change) = self.redos.pop() {
+ match change {
+ Change::Begin => {
+ waiting_for_end += 1;
+ }
+ Change::End => {
+ waiting_for_end -= 1;
+ }
+ _ => {
+ change.redo(line);
+ self.undos.push(change);
+ redone = true;
+ }
+ };
+ } else {
+ break;
+ }
+ if waiting_for_end <= 0 {
+ break;
+ }
+ }
+ redone
+ }
+}
+
+impl DeleteListener for Changeset {
+ fn delete(&mut self, idx: usize, string: &str, _: Direction) {
+ self.delete(idx, string);
+ }
+}
+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);
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::Changeset;
+ use line_buffer::LineBuffer;
+
+ #[test]
+ fn test_insert_chars() {
+ let mut cs = Changeset::new();
+ cs.insert(0, 'H');
+ cs.insert(1, 'i');
+ assert_eq!(1, cs.undos.len());
+ assert_eq!(0, cs.redos.len());
+ cs.insert(0, ' ');
+ assert_eq!(2, cs.undos.len());
+ }
+
+ #[test]
+ fn test_insert_strings() {
+ let mut cs = Changeset::new();
+ cs.insert_str(0, "Hello");
+ cs.insert_str(5, ", ");
+ assert_eq!(2, cs.undos.len());
+ assert_eq!(0, cs.redos.len());
+ }
+
+ #[test]
+ fn test_undo_insert() {
+ let mut buf = LineBuffer::init("", 0, None);
+ buf.insert_str(0, "Hello");
+ buf.insert_str(5, ", world!");
+ let mut cs = Changeset::new();
+ assert_eq!(buf.as_str(), "Hello, world!");
+
+ cs.insert_str(5, ", world!");
+
+ cs.undo(&mut buf);
+ assert_eq!(0, cs.undos.len());
+ assert_eq!(1, cs.redos.len());
+ assert_eq!(buf.as_str(), "Hello");
+
+ cs.redo(&mut buf);
+ assert_eq!(1, cs.undos.len());
+ assert_eq!(0, cs.redos.len());
+ assert_eq!(buf.as_str(), "Hello, world!");
+ }
+
+ #[test]
+ fn test_undo_delete() {
+ let mut buf = LineBuffer::init("", 0, None);
+ buf.insert_str(0, "Hello");
+ let mut cs = Changeset::new();
+ assert_eq!(buf.as_str(), "Hello");
+
+ cs.delete(5, ", world!");
+
+ cs.undo(&mut buf);
+ assert_eq!(buf.as_str(), "Hello, world!");
+
+ cs.redo(&mut buf);
+ assert_eq!(buf.as_str(), "Hello");
+ }
+
+ #[test]
+ fn test_delete_chars() {
+ let mut buf = LineBuffer::init("", 0, None);
+ buf.insert_str(0, "Hlo");
+
+ let mut cs = Changeset::new();
+ cs.delete(1, "e");
+ cs.delete(1, "l");
+ assert_eq!(1, cs.undos.len());
+
+ cs.undo(&mut buf);
+ assert_eq!(buf.as_str(), "Hello");
+ }
+
+ #[test]
+ fn test_backspace_chars() {
+ let mut buf = LineBuffer::init("", 0, None);
+ buf.insert_str(0, "Hlo");
+
+ let mut cs = Changeset::new();
+ cs.delete(2, "l");
+ cs.delete(1, "e");
+ assert_eq!(1, cs.undos.len());
+
+ cs.undo(&mut buf);
+ assert_eq!(buf.as_str(), "Hello");
+ }
+
+ #[test]
+ fn test_undo_replace() {
+ let mut buf = LineBuffer::init("", 0, None);
+ buf.insert_str(0, "Hello, world!");
+ let mut cs = Changeset::new();
+ assert_eq!(buf.as_str(), "Hello, world!");
+
+ buf.replace(1..5, "i");
+ assert_eq!(buf.as_str(), "Hi, world!");
+ cs.replace(1, "ello", "i");
+
+ cs.undo(&mut buf);
+ assert_eq!(buf.as_str(), "Hello, world!");
+
+ cs.redo(&mut buf);
+ assert_eq!(buf.as_str(), "Hi, world!");
+ }
+}