├── .gitignore ├── .gitlab-ci.yml ├── .travis.yml ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── comments.rs └── nocomments.rs ├── hist └── src ├── buffer.rs ├── complete.rs ├── context.rs ├── editor.rs ├── event.rs ├── history.rs ├── keymap ├── emacs.rs ├── mod.rs └── vi.rs ├── lib.rs ├── test.rs └── util.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | history 4 | 5 | # vim swap files 6 | [._]*.s[a-w][a-z] 7 | [._]s[a-w][a-z] 8 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | before_script: 2 | - apt-get update -qq 3 | - apt-get install -qq build-essential curl git 4 | - curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain nightly 5 | - source "$HOME/.cargo/env" 6 | 7 | build: 8 | script: 9 | - cargo build 10 | - cargo test -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - nightly 4 | sudo: false 5 | notifications: 6 | email: false 7 | script: 8 | - cargo build 9 | - cargo test 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to liner 2 | Anyone is welcome to contribute to liner. 3 | 4 | Before starting work on something, you should make an issue if there isn't one already, and say you will work on it. 5 | 6 | ## Pull Request Checklist 7 | - if your branch does not merge cleanly with master, rebase your branch 8 | - run [rustfmt](https://github.com/rust-lang-nursery/rustfmt) on your code please 9 | - make sure tests pass 10 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "redox_liner" 3 | version = "0.5.3" # remember to update README 4 | authors = [ 5 | "MovingtoMars ", 6 | "Michael Aaron Murphy ", 7 | "AdminXVII ", 8 | "Jeremy Soller ", 9 | ] 10 | description = "A library offering readline-like functionality." 11 | repository = "https://gitlab.redox-os.org/redox-os/liner" 12 | homepage = "https://gitlab.redox-os.org/redox-os/liner" 13 | keywords = ["readline", "line", "input", "editor", "completion"] 14 | license = "MIT" 15 | exclude = [ 16 | "CONTRIBUTING.md", 17 | ".travis.yml", 18 | ] 19 | edition = "2018" 20 | 21 | [lib] 22 | name = "liner" 23 | 24 | [dependencies] 25 | bytecount = "0.6.0" 26 | termion = "4" 27 | unicode-width = "0.1.6" 28 | itertools = "0.13.0" 29 | strip-ansi-escapes = "0.2.0" 30 | 31 | [dev-dependencies] 32 | regex = "1.3.1" 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2017 The Liner Developers (https://github.com/MovingtoMars/liner/network/members) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # liner 2 | 3 | A Rust library offering readline-like functionality. 4 | 5 | [CONTRIBUTING.md](/CONTRIBUTING.md) 6 | 7 | [![crates.io](https://meritbadge.herokuapp.com/liner)](https://crates.io/crates/liner) 8 | [![Build Status](https://travis-ci.org/MovingtoMars/liner.svg)](https://travis-ci.org/MovingtoMars/liner) 9 | [![Docs](https://docs.rs/liner/badge.svg)](https://docs.rs/liner/) 10 | 11 | ## Features 12 | 13 | - [x] Autosuggestions 14 | - [x] Emacs and Vi keybindings 15 | - [x] Multi-line editing 16 | - [x] History 17 | - [x] (Incomplete) basic and filename completions 18 | - [ ] Reverse search 19 | - [ ] Remappable keybindings 20 | 21 | ## Basic Usage 22 | 23 | In `Cargo.toml`: 24 | 25 | ```toml 26 | [dependencies] 27 | redox_liner = "0.5.3" 28 | ... 29 | ``` 30 | 31 | In `src/main.rs`: 32 | 33 | ```rust 34 | extern crate liner; 35 | 36 | use liner::{Context, Completer}; 37 | 38 | struct EmptyCompleter; 39 | 40 | impl Completer for EmptyCompleter { 41 | fn completions(&mut self, _start: &str) -> Vec { 42 | Vec::new() 43 | } 44 | } 45 | 46 | fn main() { 47 | let mut con = Context::new(); 48 | 49 | loop { 50 | let res = con.read_line("[prompt]$ ", &mut EmptyCompleter).unwrap(); 51 | 52 | if res.is_empty() { 53 | break; 54 | } 55 | 56 | con.history.push(res.into()); 57 | } 58 | } 59 | ``` 60 | 61 | **See src/main.rs for a more sophisticated example.** 62 | 63 | ## License 64 | 65 | MIT licensed. See the `LICENSE` file. 66 | -------------------------------------------------------------------------------- /examples/comments.rs: -------------------------------------------------------------------------------- 1 | extern crate liner; 2 | extern crate regex; 3 | extern crate termion; 4 | 5 | use std::env::{args, current_dir}; 6 | use std::io; 7 | 8 | use liner::{Completer, Context, CursorPosition, Event, EventKind, FilenameCompleter, Prompt}; 9 | use regex::Regex; 10 | use termion::color; 11 | 12 | // This prints out the text back onto the screen 13 | fn highlight_dodo(s: &str) -> String { 14 | let reg_exp = Regex::new("(?Pdodo)").unwrap(); 15 | let format = format!("{}$k{}", color::Fg(color::Red), color::Fg(color::Reset)); 16 | reg_exp.replace_all(s, format.as_str()).to_string() 17 | } 18 | 19 | struct CommentCompleter { 20 | inner: Option, 21 | } 22 | 23 | impl Completer for CommentCompleter { 24 | fn completions(&mut self, start: &str) -> Vec { 25 | if let Some(inner) = &mut self.inner { 26 | inner.completions(start) 27 | } else { 28 | Vec::new() 29 | } 30 | } 31 | 32 | fn on_event(&mut self, event: Event) { 33 | if let EventKind::BeforeComplete = event.kind { 34 | let (_, pos) = event.editor.get_words_and_cursor_position(); 35 | 36 | // Figure out of we are completing a command (the first word) or a filename. 37 | let filename = match pos { 38 | // If we are inside of a word(i is the index inside of the text, and if that 39 | // position is over zero, we return true 40 | CursorPosition::InWord(i) => i > 0, 41 | // If we are in a space like this `cat | cart` or cat | 42 | // checks if there is a word to our left(indicated by there being Some value) 43 | CursorPosition::InSpace(Some(_), _) => true, 44 | // Checks if there is no word to our left(indicated by there being None value) 45 | CursorPosition::InSpace(None, _) => false, 46 | // If we are on the left edge of a word, and the position of the cursor is 47 | // greater than or equal to 1, return true 48 | CursorPosition::OnWordLeftEdge(i) => i >= 1, 49 | // If we are on the right edge of the word 50 | CursorPosition::OnWordRightEdge(i) => i >= 1, 51 | }; 52 | 53 | // If we are not in a word with pos over zero, or in a space with text beforehand, 54 | // or on the left edge of a word with pos >= to 1, or on the Right edge of a word 55 | // under the same condition, then 56 | // This condition is only false under the predicate that we are in a space with no 57 | // word to the left 58 | self.inner = if filename { 59 | let completer = FilenameCompleter::new(Some(current_dir().unwrap())); 60 | Some(completer) 61 | } else { 62 | // Delete the completer 63 | None 64 | } 65 | } 66 | } 67 | } 68 | 69 | fn main() { 70 | let mut con = Context::new(); 71 | let mut completer = CommentCompleter { inner: None }; 72 | 73 | let history_file = match args().nth(1) { 74 | Some(file_name) => { 75 | println!("History file: {}", file_name); 76 | file_name 77 | } 78 | None => { 79 | eprintln!("No history file provided. Ending example early."); 80 | return; 81 | } 82 | }; 83 | 84 | con.history 85 | .set_file_name_and_load_history(history_file) 86 | .unwrap(); 87 | 88 | loop { 89 | // Reads the line, the first arg is the prompt, the second arg is a function called on every bit of text leaving liner, and the third is called on every key press 90 | // Basically highlight_dodo(read_line()), where on every keypress, the lambda is called 91 | let res = con.read_line( 92 | Prompt::from("[prompt]\n% "), 93 | Some(Box::new(highlight_dodo)), 94 | &mut completer, 95 | ); 96 | 97 | // We are out of the lambda, and res is the result from read_line which is an Into 98 | match res { 99 | Ok(res) => { 100 | match res.as_str() { 101 | "emacs" => { 102 | con.key_bindings = liner::KeyBindings::Emacs; 103 | println!("emacs mode"); 104 | } 105 | "vi" => { 106 | con.key_bindings = liner::KeyBindings::Vi; 107 | println!("vi mode"); 108 | } 109 | "exit" | "" => { 110 | println!("exiting..."); 111 | break; 112 | } 113 | // If all else fails, do nothing 114 | _ => {} 115 | } 116 | 117 | // If we typed nothing, don't continue down to pushing to history 118 | if res.is_empty() { 119 | break; 120 | } 121 | 122 | con.history.push(res.into()).unwrap(); 123 | } 124 | // If there was an error, get what type it was(remember, we still are in the match{} 125 | // from waaay above) 126 | Err(e) => { 127 | match e.kind() { 128 | // ctrl-c pressed 129 | io::ErrorKind::Interrupted => {} 130 | // ctrl-d pressed 131 | io::ErrorKind::UnexpectedEof => { 132 | println!("exiting..."); 133 | break; 134 | } 135 | _ => { 136 | // Ensure that all writes to the history file 137 | // are written before exiting due to error. 138 | panic!("error: {:?}", e) 139 | } 140 | } 141 | } 142 | } 143 | 144 | // End loop 145 | } 146 | 147 | // Ensure that all writes to the history file are written before exiting. 148 | con.history.commit_to_file(); 149 | } 150 | -------------------------------------------------------------------------------- /examples/nocomments.rs: -------------------------------------------------------------------------------- 1 | extern crate liner; 2 | extern crate regex; 3 | extern crate termion; 4 | 5 | use std::env::{args, current_dir}; 6 | use std::io; 7 | 8 | use liner::{Completer, Context, CursorPosition, Event, EventKind, FilenameCompleter, Prompt}; 9 | use regex::Regex; 10 | use termion::color; 11 | 12 | fn highlight_dodo(s: &str) -> String { 13 | let reg_exp = Regex::new("(?Pdodo)").unwrap(); 14 | let format = format!("{}$k{}", color::Fg(color::Red), color::Fg(color::Reset)); 15 | reg_exp.replace_all(s, format.as_str()).to_string() 16 | } 17 | 18 | struct NoCommentCompleter { 19 | inner: Option, 20 | } 21 | 22 | impl Completer for NoCommentCompleter { 23 | fn completions(&mut self, start: &str) -> Vec { 24 | if let Some(inner) = &mut self.inner { 25 | inner.completions(start) 26 | } else { 27 | Vec::new() 28 | } 29 | } 30 | 31 | fn on_event(&mut self, event: Event) { 32 | if let EventKind::BeforeComplete = event.kind { 33 | let (_, pos) = event.editor.get_words_and_cursor_position(); 34 | 35 | // Figure out of we are completing a command (the first word) or a filename. 36 | let filename = match pos { 37 | CursorPosition::InWord(i) => i > 0, 38 | CursorPosition::InSpace(Some(_), _) => true, 39 | CursorPosition::InSpace(None, _) => false, 40 | CursorPosition::OnWordLeftEdge(i) => i >= 1, 41 | CursorPosition::OnWordRightEdge(i) => i >= 1, 42 | }; 43 | 44 | self.inner = if filename { 45 | let completer = FilenameCompleter::new(Some(current_dir().unwrap())); 46 | Some(completer) 47 | } else { 48 | None 49 | } 50 | } 51 | } 52 | } 53 | 54 | fn main() { 55 | let mut con = Context::new(); 56 | let mut completer = NoCommentCompleter { inner: None }; 57 | 58 | let history_file = match args().nth(1) { 59 | Some(file_name) => { 60 | println!("History file: {}", file_name); 61 | file_name 62 | } 63 | None => { 64 | eprintln!("No history file provided. Ending example early."); 65 | return; 66 | } 67 | }; 68 | 69 | con.history 70 | .set_file_name_and_load_history(history_file) 71 | .unwrap(); 72 | 73 | loop { 74 | let res = con.read_line( 75 | Prompt::from("[prompt]$ "), 76 | Some(Box::new(highlight_dodo)), 77 | &mut completer, 78 | ); 79 | 80 | match res { 81 | Ok(res) => { 82 | match res.as_str() { 83 | "emacs" => { 84 | con.key_bindings = liner::KeyBindings::Emacs; 85 | println!("emacs mode"); 86 | } 87 | "vi" => { 88 | con.key_bindings = liner::KeyBindings::Vi; 89 | println!("vi mode"); 90 | } 91 | "exit" | "" => { 92 | println!("exiting..."); 93 | break; 94 | } 95 | _ => {} 96 | } 97 | 98 | if res.is_empty() { 99 | break; 100 | } 101 | 102 | con.history.push(res.into()).unwrap(); 103 | } 104 | Err(e) => { 105 | match e.kind() { 106 | // ctrl-c pressed 107 | io::ErrorKind::Interrupted => {} 108 | // ctrl-d pressed 109 | io::ErrorKind::UnexpectedEof => { 110 | println!("exiting..."); 111 | break; 112 | } 113 | _ => { 114 | // Ensure that all writes to the history file 115 | // are written before exiting. 116 | panic!("error: {:?}", e) 117 | } 118 | } 119 | } 120 | } 121 | } 122 | // Ensure that all writes to the history file are written before exiting. 123 | con.history.commit_to_file(); 124 | } 125 | -------------------------------------------------------------------------------- /hist: -------------------------------------------------------------------------------- 1 | auie 2 | -------------------------------------------------------------------------------- /src/buffer.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{self, Write as FmtWrite}; 2 | use std::io::{self, Write}; 3 | use std::iter::FromIterator; 4 | use unicode_width::UnicodeWidthStr; 5 | 6 | /// A modification performed on a `Buffer`. These are used for the purpose of undo/redo. 7 | #[derive(Debug, Clone)] 8 | pub enum Action { 9 | Insert { start: usize, text: Vec }, 10 | Remove { start: usize, text: Vec }, 11 | StartGroup, 12 | EndGroup, 13 | } 14 | 15 | impl Action { 16 | pub fn do_on(&self, buf: &mut Buffer) { 17 | match *self { 18 | Action::Insert { start, ref text } => buf.insert_raw(start, &text[..]), 19 | Action::Remove { start, ref text } => { 20 | buf.remove_raw(start, start + text.len()); 21 | } 22 | Action::StartGroup | Action::EndGroup => {} 23 | } 24 | } 25 | 26 | pub fn undo(&self, buf: &mut Buffer) { 27 | match *self { 28 | Action::Insert { start, ref text } => { 29 | buf.remove_raw(start, start + text.len()); 30 | } 31 | Action::Remove { start, ref text } => buf.insert_raw(start, &text[..]), 32 | Action::StartGroup | Action::EndGroup => {} 33 | } 34 | } 35 | } 36 | 37 | /// A buffer for text in the line editor. 38 | /// 39 | /// It keeps track of each action performed on it for use with undo/redo. 40 | #[derive(Debug, Clone)] 41 | pub struct Buffer { 42 | data: Vec, 43 | actions: Vec, 44 | undone_actions: Vec, 45 | } 46 | 47 | impl PartialEq for Buffer { 48 | fn eq(&self, other: &Self) -> bool { 49 | self.data == other.data 50 | } 51 | } 52 | impl Eq for Buffer {} 53 | 54 | impl From for String { 55 | fn from(buf: Buffer) -> Self { 56 | String::from_iter(buf.data) 57 | } 58 | } 59 | 60 | impl From for Buffer { 61 | fn from(s: String) -> Self { 62 | Buffer::from_iter(s.chars()) 63 | } 64 | } 65 | 66 | impl<'a> From<&'a str> for Buffer { 67 | fn from(s: &'a str) -> Self { 68 | Buffer::from_iter(s.chars()) 69 | } 70 | } 71 | 72 | impl fmt::Display for Buffer { 73 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 74 | for &c in &self.data { 75 | f.write_char(c)?; 76 | } 77 | Ok(()) 78 | } 79 | } 80 | 81 | impl FromIterator for Buffer { 82 | fn from_iter>(t: T) -> Self { 83 | Buffer { 84 | data: t.into_iter().collect(), 85 | actions: Vec::new(), 86 | undone_actions: Vec::new(), 87 | } 88 | } 89 | } 90 | 91 | impl Default for Buffer { 92 | fn default() -> Self { 93 | Self::new() 94 | } 95 | } 96 | 97 | impl Buffer { 98 | pub fn new() -> Self { 99 | Buffer { 100 | data: Vec::new(), 101 | actions: Vec::new(), 102 | undone_actions: Vec::new(), 103 | } 104 | } 105 | 106 | pub fn clear_actions(&mut self) { 107 | self.actions.clear(); 108 | self.undone_actions.clear(); 109 | } 110 | 111 | pub fn start_undo_group(&mut self) { 112 | self.actions.push(Action::StartGroup); 113 | } 114 | 115 | pub fn end_undo_group(&mut self) { 116 | self.actions.push(Action::EndGroup); 117 | } 118 | 119 | pub fn undo(&mut self) -> bool { 120 | use Action::*; 121 | 122 | let did = !self.actions.is_empty(); 123 | let mut group_nest = 0; 124 | let mut group_count = 0; 125 | while let Some(act) = self.actions.pop() { 126 | act.undo(self); 127 | self.undone_actions.push(act.clone()); 128 | match act { 129 | EndGroup => { 130 | group_nest += 1; 131 | group_count = 0; 132 | } 133 | StartGroup => group_nest -= 1, 134 | // count the actions in this group so we can ignore empty groups below 135 | _ => group_count += 1, 136 | } 137 | 138 | // if we aren't in a group, and the last group wasn't empty 139 | if group_nest == 0 && group_count > 0 { 140 | break; 141 | } 142 | } 143 | did 144 | } 145 | 146 | pub fn redo(&mut self) -> bool { 147 | use Action::*; 148 | 149 | let did = !self.undone_actions.is_empty(); 150 | let mut group_nest = 0; 151 | let mut group_count = 0; 152 | while let Some(act) = self.undone_actions.pop() { 153 | act.do_on(self); 154 | self.actions.push(act.clone()); 155 | match act { 156 | StartGroup => { 157 | group_nest += 1; 158 | group_count = 0; 159 | } 160 | EndGroup => group_nest -= 1, 161 | // count the actions in this group so we can ignore empty groups below 162 | _ => group_count += 1, 163 | } 164 | 165 | // if we aren't in a group, and the last group wasn't empty 166 | if group_nest == 0 && group_count > 0 { 167 | break; 168 | } 169 | } 170 | did 171 | } 172 | 173 | pub fn revert(&mut self) -> bool { 174 | if self.actions.is_empty() { 175 | return false; 176 | } 177 | 178 | while self.undo() {} 179 | true 180 | } 181 | 182 | fn push_action(&mut self, act: Action) { 183 | self.actions.push(act); 184 | self.undone_actions.clear(); 185 | } 186 | 187 | pub fn last_arg(&self) -> Option<&[char]> { 188 | self.data 189 | .split(|&c| c == ' ') 190 | .filter(|s| !s.is_empty()) 191 | .last() 192 | } 193 | 194 | pub fn num_chars(&self) -> usize { 195 | self.data.len() 196 | } 197 | 198 | pub fn num_bytes(&self) -> usize { 199 | let s: String = self.clone().into(); 200 | s.len() 201 | } 202 | 203 | pub fn char_before(&self, cursor: usize) -> Option { 204 | if cursor == 0 { 205 | None 206 | } else { 207 | self.data.get(cursor - 1).cloned() 208 | } 209 | } 210 | 211 | pub fn char_after(&self, cursor: usize) -> Option { 212 | self.data.get(cursor).cloned() 213 | } 214 | 215 | /// Returns the number of characters removed. 216 | pub fn remove(&mut self, start: usize, end: usize) -> usize { 217 | let s = self.remove_raw(start, end); 218 | let num_removed = s.len(); 219 | self.push_action(Action::Remove { start, text: s }); 220 | num_removed 221 | } 222 | 223 | pub fn insert(&mut self, start: usize, text: &[char]) { 224 | let act = Action::Insert { 225 | start, 226 | text: text.into(), 227 | }; 228 | act.do_on(self); 229 | self.push_action(act); 230 | } 231 | 232 | // XXX rename, too confusing 233 | pub fn insert_from_buffer(&mut self, other: &Buffer) { 234 | let start = self.data.len(); 235 | self.insert(start, &other.data[start..]) 236 | } 237 | 238 | pub fn copy_buffer(&mut self, other: &Buffer) { 239 | let data_len = self.data.len(); 240 | self.remove(0, data_len); 241 | self.insert(0, &other.data[0..]) 242 | } 243 | 244 | pub fn range(&self, start: usize, end: usize) -> String { 245 | self.data[start..end].iter().cloned().collect() 246 | } 247 | 248 | pub fn range_chars(&self, start: usize, end: usize) -> Vec { 249 | self.data[start..end].to_owned() 250 | } 251 | 252 | pub fn width(&self) -> Vec { 253 | self.range_width(0, self.num_chars()) 254 | } 255 | 256 | pub fn range_width(&self, start: usize, end: usize) -> Vec { 257 | self.range(start, end) 258 | .split('\n') 259 | .map(|s| s.width()) 260 | .collect() 261 | } 262 | 263 | pub fn lines(&self) -> Vec { 264 | self.data 265 | .split(|&c| c == '\n') 266 | .map(|s| s.iter().cloned().collect()) 267 | .collect() 268 | } 269 | 270 | pub fn chars(&self) -> ::std::slice::Iter { 271 | self.data.iter() 272 | } 273 | 274 | pub fn truncate(&mut self, num: usize) { 275 | let end = self.data.len(); 276 | self.remove(num, end); 277 | } 278 | 279 | pub fn print(&self, out: &mut W) -> io::Result<()> 280 | where 281 | W: Write, 282 | { 283 | let string: String = self.data.iter().cloned().collect(); 284 | out.write_all(string.as_bytes()) 285 | } 286 | 287 | pub fn as_bytes(&self) -> Vec { 288 | // NOTE: not particularly efficient. Could make a proper byte iterator with minimal 289 | // allocations if performance becomes an issue. 290 | self.to_string().into_bytes() 291 | } 292 | 293 | /// Takes other buffer, measures its length and prints this buffer from the point where 294 | /// the other stopped. 295 | /// Used to implement autosuggestions. 296 | pub fn print_rest(&self, out: &mut W, after: usize) -> io::Result 297 | where 298 | W: Write, 299 | { 300 | let string: String = self.data.iter().skip(after).cloned().collect(); 301 | out.write_all(string.as_bytes())?; 302 | 303 | Ok(string.len()) 304 | } 305 | 306 | fn remove_raw(&mut self, start: usize, end: usize) -> Vec { 307 | self.data.drain(start..end).collect() 308 | } 309 | 310 | fn insert_raw(&mut self, start: usize, text: &[char]) { 311 | for (i, &c) in text.iter().enumerate() { 312 | self.data.insert(start + i, c) 313 | } 314 | } 315 | 316 | /// Check if the other buffer starts with the same content as this one. 317 | /// Used to implement autosuggestions. 318 | pub fn starts_with(&self, other: &Buffer) -> bool { 319 | let other_len = other.data.len(); 320 | let self_len = self.data.len(); 321 | if !other.data.is_empty() && self_len != other_len { 322 | let match_let = self 323 | .data 324 | .iter() 325 | .zip(&other.data) 326 | .take_while(|&(s, o)| *s == *o) 327 | .count(); 328 | match_let == other_len 329 | } else { 330 | false 331 | } 332 | } 333 | 334 | /// Check if the buffer contains pattern. 335 | /// Used to implement history search. 336 | pub fn contains(&self, pattern: &Buffer) -> bool { 337 | let search_term: &[char] = &pattern.data; 338 | if search_term.is_empty() { 339 | return false; 340 | } 341 | self.data 342 | .windows(search_term.len()) 343 | .any(|window| window == search_term) 344 | } 345 | 346 | /// Return true if the buffer is empty. 347 | pub fn is_empty(&self) -> bool { 348 | self.data.is_empty() 349 | } 350 | } 351 | 352 | #[cfg(test)] 353 | mod tests { 354 | use super::*; 355 | 356 | #[test] 357 | fn test_insert() { 358 | let mut buf = Buffer::new(); 359 | buf.insert(0, &['a', 'b', 'c', 'd', 'e', 'f', 'g']); 360 | assert_eq!(String::from(buf), "abcdefg"); 361 | } 362 | 363 | #[test] 364 | fn test_truncate_empty() { 365 | let mut buf = Buffer::new(); 366 | buf.truncate(0); 367 | assert_eq!(String::from(buf), ""); 368 | } 369 | 370 | #[test] 371 | fn test_truncate_all() { 372 | let mut buf = Buffer::new(); 373 | buf.insert(0, &['a', 'b', 'c', 'd', 'e', 'f', 'g']); 374 | buf.truncate(0); 375 | assert_eq!(String::from(buf), ""); 376 | } 377 | 378 | #[test] 379 | fn test_truncate_end() { 380 | let mut buf = Buffer::new(); 381 | buf.insert(0, &['a', 'b', 'c', 'd', 'e', 'f', 'g']); 382 | let end = buf.num_chars(); 383 | buf.truncate(end); 384 | assert_eq!(String::from(buf), "abcdefg"); 385 | } 386 | 387 | #[test] 388 | fn test_truncate_part() { 389 | let mut buf = Buffer::new(); 390 | buf.insert(0, &['a', 'b', 'c', 'd', 'e', 'f', 'g']); 391 | buf.truncate(3); 392 | assert_eq!(String::from(buf), "abc"); 393 | } 394 | 395 | #[test] 396 | fn test_truncate_empty_undo() { 397 | let mut buf = Buffer::new(); 398 | buf.truncate(0); 399 | buf.undo(); 400 | assert_eq!(String::from(buf), ""); 401 | } 402 | 403 | #[test] 404 | fn test_truncate_all_then_undo() { 405 | let mut buf = Buffer::new(); 406 | buf.insert(0, &['a', 'b', 'c', 'd', 'e', 'f', 'g']); 407 | buf.truncate(0); 408 | buf.undo(); 409 | assert_eq!(String::from(buf), "abcdefg"); 410 | } 411 | 412 | #[test] 413 | fn test_truncate_end_then_undo() { 414 | let mut buf = Buffer::new(); 415 | buf.insert(0, &['a', 'b', 'c', 'd', 'e', 'f', 'g']); 416 | let end = buf.num_chars(); 417 | buf.truncate(end); 418 | buf.undo(); 419 | assert_eq!(String::from(buf), "abcdefg"); 420 | } 421 | 422 | #[test] 423 | fn test_truncate_part_then_undo() { 424 | let mut buf = Buffer::new(); 425 | buf.insert(0, &['a', 'b', 'c', 'd', 'e', 'f', 'g']); 426 | buf.truncate(3); 427 | buf.undo(); 428 | assert_eq!(String::from(buf), "abcdefg"); 429 | } 430 | 431 | #[test] 432 | fn test_undo_group() { 433 | let mut buf = Buffer::new(); 434 | buf.insert(0, &['a', 'b', 'c', 'd', 'e', 'f', 'g']); 435 | buf.start_undo_group(); 436 | buf.remove(0, 1); 437 | buf.remove(0, 1); 438 | buf.remove(0, 1); 439 | buf.end_undo_group(); 440 | assert_eq!(buf.undo(), true); 441 | assert_eq!(String::from(buf), "abcdefg"); 442 | } 443 | 444 | #[test] 445 | fn test_redo_group() { 446 | let mut buf = Buffer::new(); 447 | buf.insert(0, &['a', 'b', 'c', 'd', 'e', 'f', 'g']); 448 | buf.start_undo_group(); 449 | buf.remove(0, 1); 450 | buf.remove(0, 1); 451 | buf.remove(0, 1); 452 | buf.end_undo_group(); 453 | assert_eq!(buf.undo(), true); 454 | assert_eq!(buf.redo(), true); 455 | assert_eq!(String::from(buf), "defg"); 456 | } 457 | 458 | #[test] 459 | fn test_nested_undo_group() { 460 | let mut buf = Buffer::new(); 461 | buf.insert(0, &['a', 'b', 'c', 'd', 'e', 'f', 'g']); 462 | buf.start_undo_group(); 463 | buf.remove(0, 1); 464 | buf.start_undo_group(); 465 | buf.remove(0, 1); 466 | buf.end_undo_group(); 467 | buf.remove(0, 1); 468 | buf.end_undo_group(); 469 | assert_eq!(buf.undo(), true); 470 | assert_eq!(String::from(buf), "abcdefg"); 471 | } 472 | 473 | #[test] 474 | fn test_nested_redo_group() { 475 | let mut buf = Buffer::new(); 476 | buf.insert(0, &['a', 'b', 'c', 'd', 'e', 'f', 'g']); 477 | buf.start_undo_group(); 478 | buf.remove(0, 1); 479 | buf.start_undo_group(); 480 | buf.remove(0, 1); 481 | buf.end_undo_group(); 482 | buf.remove(0, 1); 483 | buf.end_undo_group(); 484 | assert_eq!(buf.undo(), true); 485 | assert_eq!(buf.redo(), true); 486 | assert_eq!(String::from(buf), "defg"); 487 | } 488 | 489 | #[test] 490 | fn test_starts_with() { 491 | let mut buf = Buffer::new(); 492 | buf.insert(0, &['a', 'b', 'c', 'd', 'e', 'f', 'g']); 493 | let mut buf2 = Buffer::new(); 494 | buf2.insert(0, &['a', 'b', 'c']); 495 | assert_eq!(buf.starts_with(&buf2), true); 496 | } 497 | 498 | #[test] 499 | fn test_does_not_start_with() { 500 | let mut buf = Buffer::new(); 501 | buf.insert(0, &['a', 'b', 'c']); 502 | let mut buf2 = Buffer::new(); 503 | buf2.insert(0, &['a', 'b', 'c']); 504 | assert_eq!(buf.starts_with(&buf2), false); 505 | } 506 | 507 | #[test] 508 | fn test_is_not_match2() { 509 | let mut buf = Buffer::new(); 510 | buf.insert(0, &['a', 'b', 'c', 'd', 'e', 'f', 'g']); 511 | let mut buf2 = Buffer::new(); 512 | buf2.insert(0, &['x', 'y', 'z']); 513 | assert_eq!(buf.starts_with(&buf2), false); 514 | } 515 | 516 | #[test] 517 | fn test_contains() { 518 | let mut buf = Buffer::new(); 519 | buf.insert(0, &['a', 'b', 'c', 'd', 'e', 'f', 'g']); 520 | let mut buf2 = Buffer::new(); 521 | buf2.insert(0, &['a', 'b', 'c']); 522 | assert_eq!(buf.contains(&buf2), true); 523 | let mut buf2 = Buffer::new(); 524 | buf2.insert(0, &['c', 'd', 'e']); 525 | assert_eq!(buf.contains(&buf2), true); 526 | let mut buf2 = Buffer::new(); 527 | buf2.insert(0, &['e', 'f', 'g']); 528 | assert_eq!(buf.contains(&buf2), true); 529 | } 530 | 531 | #[test] 532 | fn test_does_not_contain() { 533 | let mut buf = Buffer::new(); 534 | buf.insert(0, &['a', 'b', 'c', 'd', 'e', 'f', 'g']); 535 | let mut buf2 = Buffer::new(); 536 | buf2.insert(0, &['x', 'b', 'c']); 537 | assert_eq!(buf.contains(&buf2), false); 538 | let mut buf2 = Buffer::new(); 539 | buf2.insert(0, &['a', 'b', 'd']); 540 | assert_eq!(buf.contains(&buf2), false); 541 | } 542 | 543 | #[test] 544 | fn test_print_rest() { 545 | let mut buf = Buffer::new(); 546 | buf.insert(0, &['a', 'b', 'c', 'd', 'e', 'f', 'g']); 547 | let mut buf2 = Buffer::new(); 548 | buf2.insert(0, &['a', 'b', 'c']); 549 | let mut out: Vec = vec![]; 550 | buf.print_rest(&mut out, buf2.data.len()).unwrap(); 551 | assert_eq!(out.len(), 4); 552 | } 553 | } 554 | -------------------------------------------------------------------------------- /src/complete.rs: -------------------------------------------------------------------------------- 1 | use super::event::Event; 2 | use std::io::Write; 3 | use std::path::PathBuf; 4 | 5 | pub trait Completer { 6 | fn completions(&mut self, start: &str) -> Vec; 7 | fn on_event(&mut self, _event: Event) {} 8 | } 9 | 10 | pub struct BasicCompleter { 11 | prefixes: Vec, 12 | } 13 | 14 | impl BasicCompleter { 15 | pub fn new>(prefixes: Vec) -> BasicCompleter { 16 | BasicCompleter { 17 | prefixes: prefixes.into_iter().map(|s| s.into()).collect(), 18 | } 19 | } 20 | } 21 | 22 | impl Completer for BasicCompleter { 23 | fn completions(&mut self, start: &str) -> Vec { 24 | self.prefixes 25 | .iter() 26 | .filter(|s| s.starts_with(start)) 27 | .cloned() 28 | .collect() 29 | } 30 | } 31 | 32 | pub struct FilenameCompleter { 33 | working_dir: Option, 34 | case_sensitive: bool, 35 | } 36 | 37 | impl FilenameCompleter { 38 | pub fn new>(working_dir: Option) -> Self { 39 | FilenameCompleter { 40 | working_dir: working_dir.map(|p| p.into()), 41 | case_sensitive: true, 42 | } 43 | } 44 | 45 | pub fn with_case_sensitivity>( 46 | working_dir: Option, 47 | case_sensitive: bool, 48 | ) -> Self { 49 | FilenameCompleter { 50 | working_dir: working_dir.map(|p| p.into()), 51 | case_sensitive, 52 | } 53 | } 54 | } 55 | 56 | impl Completer for FilenameCompleter { 57 | fn completions(&mut self, mut start: &str) -> Vec { 58 | // XXX: this function is really bad, TODO rewrite 59 | 60 | let start_owned: String = if start.starts_with('\"') || start.starts_with('\'') { 61 | start = &start[1..]; 62 | if !start.is_empty() { 63 | start = &start[..start.len() - 1]; 64 | } 65 | start.into() 66 | } else { 67 | start.replace(r"\ ", " ") 68 | }; 69 | 70 | let start_path = PathBuf::from(start_owned.as_str()); 71 | 72 | let full_path = match self.working_dir { 73 | Some(ref wd) => { 74 | let mut fp = PathBuf::from(wd); 75 | fp.push(start_owned.as_str()); 76 | fp 77 | } 78 | None => PathBuf::from(start_owned.as_str()), 79 | }; 80 | 81 | let p; 82 | let start_name; 83 | let completing_dir; 84 | match full_path.parent() { 85 | // XXX non-unix separaor 86 | Some(parent) 87 | if !start.is_empty() 88 | && !start_owned.ends_with('/') 89 | && !full_path.ends_with("..") => 90 | { 91 | p = parent; 92 | start_name = if self.case_sensitive { 93 | full_path.file_name().unwrap().to_string_lossy() 94 | } else { 95 | let sn = full_path.file_name().unwrap().to_string_lossy(); 96 | sn.to_lowercase().into() 97 | }; 98 | completing_dir = false; 99 | } 100 | _ => { 101 | p = full_path.as_path(); 102 | start_name = "".into(); 103 | completing_dir = 104 | start.is_empty() || start.ends_with('/') || full_path.ends_with(".."); 105 | } 106 | } 107 | 108 | let read_dir = match p.read_dir() { 109 | Ok(x) => x, 110 | Err(_) => return vec![], 111 | }; 112 | 113 | let mut matches = vec![]; 114 | for dir in read_dir { 115 | let dir = match dir { 116 | Ok(x) => x, 117 | Err(_) => continue, 118 | }; 119 | let file_name = dir.file_name(); 120 | let file_name = if self.case_sensitive { 121 | file_name.to_string_lossy().to_string() 122 | } else { 123 | file_name.to_string_lossy().to_lowercase() 124 | }; 125 | 126 | if start_name.is_empty() || file_name.starts_with(&*start_name) { 127 | let mut a = start_path.clone(); 128 | if !a.is_absolute() { 129 | a = PathBuf::new(); 130 | } else if !completing_dir && !a.pop() { 131 | return vec![]; 132 | } 133 | 134 | a.push(dir.file_name()); 135 | let mut s = a.to_string_lossy(); 136 | if dir.path().is_dir() { 137 | let mut string = s.into_owned(); 138 | string.push('/'); 139 | s = string.into(); 140 | } 141 | 142 | let mut b = PathBuf::from(&start_owned); 143 | if !completing_dir { 144 | b.pop(); 145 | } 146 | b.push(s.as_ref()); 147 | 148 | matches.push(b.to_string_lossy().replace(" ", r"\ ")); 149 | } 150 | } 151 | 152 | matches 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/context.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, stdin, stdout, Write}; 2 | use termion::input::TermRead; 3 | use termion::raw::IntoRawMode; 4 | 5 | use super::*; 6 | use crate::editor::Prompt; 7 | 8 | pub type ColorClosure = Box String>; 9 | 10 | /// The default for `Context.word_divider_fn`. 11 | pub fn get_buffer_words(buf: &Buffer) -> Vec<(usize, usize)> { 12 | let mut res = Vec::new(); 13 | 14 | let mut word_start = None; 15 | let mut just_had_backslash = false; 16 | 17 | for (i, &c) in buf.chars().enumerate() { 18 | if c == '\\' { 19 | just_had_backslash = true; 20 | continue; 21 | } 22 | 23 | if let Some(start) = word_start { 24 | if c == ' ' && !just_had_backslash { 25 | res.push((start, i)); 26 | word_start = None; 27 | } 28 | } else if c != ' ' { 29 | word_start = Some(i); 30 | } 31 | 32 | just_had_backslash = false; 33 | } 34 | 35 | if let Some(start) = word_start { 36 | res.push((start, buf.num_chars())); 37 | } 38 | 39 | res 40 | } 41 | 42 | /// The key bindings to use. 43 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 44 | pub enum KeyBindings { 45 | Vi, 46 | Emacs, 47 | } 48 | 49 | pub struct Context { 50 | pub history: History, 51 | pub word_divider_fn: Box Vec<(usize, usize)>>, 52 | pub key_bindings: KeyBindings, 53 | pub buf: String, 54 | } 55 | 56 | impl Default for Context { 57 | fn default() -> Self { 58 | Self::new() 59 | } 60 | } 61 | 62 | impl Context { 63 | pub fn new() -> Self { 64 | Context { 65 | history: History::new(), 66 | word_divider_fn: Box::new(get_buffer_words), 67 | key_bindings: KeyBindings::Emacs, 68 | buf: String::with_capacity(512), 69 | } 70 | } 71 | 72 | /// Creates an `Editor` and feeds it keypresses from stdin until the line is entered. 73 | /// The output is stdout. 74 | /// The returned line has the newline removed. 75 | /// Before returning, will revert all changes to the history buffers. 76 | pub fn read_line( 77 | &mut self, 78 | prompt: Prompt, 79 | f: Option, 80 | handler: &mut C, 81 | ) -> io::Result { 82 | self.read_line_with_init_buffer(prompt, handler, f, Buffer::new()) 83 | } 84 | 85 | /// Same as `Context.read_line()`, but passes the provided initial buffer to the editor. 86 | /// 87 | /// ```no_run 88 | /// use liner::{Context, Completer, Prompt}; 89 | /// 90 | /// struct EmptyCompleter; 91 | /// 92 | /// impl Completer for EmptyCompleter { 93 | /// fn completions(&mut self, _start: &str) -> Vec { 94 | /// Vec::new() 95 | /// } 96 | /// } 97 | /// 98 | /// let mut context = Context::new(); 99 | /// let line = 100 | /// context.read_line_with_init_buffer(Prompt::from("[prompt]$ "), 101 | /// &mut EmptyCompleter, 102 | /// Some(Box::new(|s| String::from(s))), 103 | /// "some initial buffer"); 104 | /// ``` 105 | pub fn read_line_with_init_buffer, C: Completer>( 106 | &mut self, 107 | prompt: Prompt, 108 | handler: &mut C, 109 | f: Option, 110 | buffer: B, 111 | ) -> io::Result { 112 | let stdout = stdout().into_raw_mode()?; 113 | let keybindings = self.key_bindings; 114 | let ed = Editor::new_with_init_buffer(stdout, prompt, f, self, buffer)?; 115 | match keybindings { 116 | KeyBindings::Emacs => Self::handle_keys(keymap::Emacs::new(), ed, handler), 117 | KeyBindings::Vi => Self::handle_keys(keymap::Vi::new(), ed, handler), 118 | } 119 | 120 | //self.revert_all_history(); 121 | } 122 | 123 | fn handle_keys( 124 | mut keymap: M, 125 | mut ed: Editor<'_, W>, 126 | handler: &mut C, 127 | ) -> io::Result { 128 | keymap.init(&mut ed); 129 | for c in stdin().keys() { 130 | if keymap.handle_key(c?, &mut ed, handler)? { 131 | break; 132 | } 133 | } 134 | 135 | Ok(ed.into()) 136 | } 137 | 138 | pub fn revert_all_history(&mut self) { 139 | for buf in &mut self.history.buffers { 140 | buf.revert(); 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/editor.rs: -------------------------------------------------------------------------------- 1 | use std::cmp; 2 | use std::fmt::{self, Write}; 3 | use std::io; 4 | use strip_ansi_escapes::strip; 5 | use termion::{self, clear, color, cursor}; 6 | 7 | use super::complete::Completer; 8 | use crate::context::ColorClosure; 9 | use crate::event::*; 10 | use crate::util; 11 | use crate::Buffer; 12 | use crate::Context; 13 | use itertools::Itertools; 14 | 15 | /// Indicates the mode that should be currently displayed in the propmpt. 16 | #[derive(Clone, Copy, Debug)] 17 | pub enum ViPromptMode { 18 | Normal, 19 | Insert, 20 | } 21 | 22 | /// Holds the current mode and the indicators for all modes. 23 | #[derive(Debug)] 24 | pub struct ViStatus { 25 | pub mode: ViPromptMode, 26 | normal: String, 27 | insert: String, 28 | } 29 | 30 | impl ViStatus { 31 | pub fn new(mode: ViPromptMode, normal: N, insert: I) -> Self 32 | where 33 | N: Into, 34 | I: Into, 35 | { 36 | Self { 37 | mode, 38 | normal: normal.into(), 39 | insert: insert.into(), 40 | } 41 | } 42 | 43 | pub fn as_str(&self) -> &str { 44 | use ViPromptMode::*; 45 | match self.mode { 46 | Normal => &self.normal, 47 | Insert => &self.insert, 48 | } 49 | } 50 | } 51 | 52 | impl Default for ViStatus { 53 | fn default() -> Self { 54 | ViStatus { 55 | mode: ViPromptMode::Insert, 56 | normal: String::from("[N] "), 57 | insert: String::from("[I] "), 58 | } 59 | } 60 | } 61 | 62 | impl fmt::Display for ViStatus { 63 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 64 | use ViPromptMode::*; 65 | match self.mode { 66 | Normal => write!(f, "{}", self.normal), 67 | Insert => write!(f, "{}", self.insert), 68 | } 69 | } 70 | } 71 | 72 | /// User-defined prompt. 73 | /// 74 | /// # Examples 75 | /// 76 | /// For Emacs mode, you simply define a static prompt that holds a string. 77 | /// ``` 78 | /// # use liner::Prompt; 79 | /// let prompt = Prompt::from("prompt$ "); 80 | /// assert_eq!(&prompt.to_string(), "prompt$ "); 81 | /// ``` 82 | /// 83 | /// You can also display Vi mode indicator in your prompt. 84 | /// ``` 85 | /// # use liner::{Prompt, ViStatus, ViPromptMode}; 86 | /// let prompt = Prompt { 87 | /// prompt: "prompt$ ".into(), 88 | /// vi_status: Some(ViStatus::default()), 89 | /// }; 90 | /// assert_eq!(&prompt.to_string(), "[I] prompt$ "); 91 | /// ``` 92 | pub struct Prompt { 93 | pub prompt: String, 94 | pub vi_status: Option, 95 | } 96 | 97 | impl Prompt { 98 | /// Constructs a static prompt. 99 | pub fn from>(prompt: P) -> Self { 100 | Prompt { 101 | prompt: prompt.into(), 102 | vi_status: None, 103 | } 104 | } 105 | 106 | pub fn prefix(&self) -> &str { 107 | match &self.vi_status { 108 | Some(status) => status.as_str(), 109 | None => "", 110 | } 111 | } 112 | } 113 | 114 | impl fmt::Display for Prompt { 115 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 116 | if let Some(status) = &self.vi_status { 117 | write!(f, "{}", status)? 118 | } 119 | write!(f, "{}", self.prompt) 120 | } 121 | } 122 | 123 | /// Represents the position of the cursor relative to words in the buffer. 124 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 125 | pub enum CursorPosition { 126 | /// The cursor is in the word with the specified index. 127 | InWord(usize), 128 | 129 | /// The cursor is on the left edge of the word with the specified index. 130 | /// For example: `abc |hi`, where `|` is the cursor. 131 | OnWordLeftEdge(usize), 132 | 133 | /// The cursor is on the right edge of the word with the specified index. 134 | /// For example: `abc| hi`, where `|` is the cursor. 135 | OnWordRightEdge(usize), 136 | 137 | /// The cursor is not in contact with any word. Each `Option` specifies the index of the 138 | /// closest word to the left and right, respectively, or `None` if there is no word on that side. 139 | InSpace(Option, Option), 140 | } 141 | 142 | impl CursorPosition { 143 | pub fn get(cursor: usize, words: &[(usize, usize)]) -> CursorPosition { 144 | use CursorPosition::*; 145 | 146 | if words.is_empty() { 147 | return InSpace(None, None); 148 | } else if cursor == words[0].0 { 149 | return OnWordLeftEdge(0); 150 | } else if cursor < words[0].0 { 151 | return InSpace(None, Some(0)); 152 | } 153 | 154 | for (i, &(start, end)) in words.iter().enumerate() { 155 | if start == cursor { 156 | return OnWordLeftEdge(i); 157 | } else if end == cursor { 158 | return OnWordRightEdge(i); 159 | } else if start < cursor && cursor < end { 160 | return InWord(i); 161 | } else if cursor < start { 162 | return InSpace(Some(i - 1), Some(i)); 163 | } 164 | } 165 | 166 | InSpace(Some(words.len() - 1), None) 167 | } 168 | } 169 | 170 | /// The core line editor. Displays and provides editing for history and the new buffer. 171 | pub struct Editor<'a, W: io::Write> { 172 | prompt: Prompt, 173 | out: W, 174 | context: &'a mut Context, 175 | 176 | // A closure that is evaluated just before we write to out. 177 | // This allows us to do custom syntax highlighting and other fun stuff. 178 | closure: Option, 179 | 180 | // The location of the cursor. Note that the cursor does not lie on a char, but between chars. 181 | // So, if `cursor == 0` then the cursor is before the first char, 182 | // and if `cursor == 1` ten the cursor is after the first char and before the second char. 183 | cursor: usize, 184 | 185 | // Buffer for the new line (ie. not from editing history) 186 | new_buf: Buffer, 187 | 188 | // Buffer to use when editing history so we do not overwrite it. 189 | hist_buf: Buffer, 190 | hist_buf_valid: bool, 191 | 192 | // None if we're on the new buffer, else the index of history 193 | cur_history_loc: Option, 194 | 195 | // The line of the cursor relative to the prompt. 1-indexed. 196 | // So if the cursor is on the same line as the prompt, `term_cursor_line == 1`. 197 | // If the cursor is on the line below the prompt, `term_cursor_line == 2`. 198 | term_cursor_line: usize, 199 | 200 | // The next completion to suggest, or none 201 | show_completions_hint: Option<(Vec, Option)>, 202 | 203 | // Show autosuggestions based on history 204 | show_autosuggestions: bool, 205 | 206 | // if set, the cursor will not be allow to move one past the end of the line, this is necessary 207 | // for Vi's normal mode. 208 | pub no_eol: bool, 209 | 210 | reverse_search: bool, 211 | forward_search: bool, 212 | buffer_changed: bool, 213 | 214 | history_subset_index: Vec, 215 | history_subset_loc: Option, 216 | 217 | autosuggestion: Option, 218 | 219 | history_fresh: bool, 220 | } 221 | 222 | macro_rules! cur_buf_mut { 223 | ($s:expr) => {{ 224 | $s.buffer_changed = true; 225 | match $s.cur_history_loc { 226 | Some(i) => { 227 | if !$s.hist_buf_valid { 228 | $s.hist_buf.copy_buffer(&$s.context.history[i]); 229 | $s.hist_buf_valid = true; 230 | } 231 | &mut $s.hist_buf 232 | } 233 | _ => &mut $s.new_buf, 234 | } 235 | }}; 236 | } 237 | 238 | macro_rules! cur_buf { 239 | ($s:expr) => { 240 | match $s.cur_history_loc { 241 | Some(_) if $s.hist_buf_valid => &$s.hist_buf, 242 | Some(i) => &$s.context.history[i], 243 | _ => &$s.new_buf, 244 | } 245 | }; 246 | } 247 | 248 | impl<'a, W: io::Write> Editor<'a, W> { 249 | pub fn new( 250 | out: W, 251 | prompt: Prompt, 252 | f: Option, 253 | context: &'a mut Context, 254 | ) -> io::Result { 255 | Editor::new_with_init_buffer(out, prompt, f, context, Buffer::new()) 256 | } 257 | 258 | pub fn new_with_init_buffer>( 259 | mut out: W, 260 | prompt: Prompt, 261 | f: Option, 262 | context: &'a mut Context, 263 | buffer: B, 264 | ) -> io::Result { 265 | out.write_all("⏎".as_bytes())?; 266 | for _ in 0..(util::terminal_width().unwrap_or(80) - 1) { 267 | out.write_all(b" ")?; // if the line is not empty, overflow on next line 268 | } 269 | out.write_all("\r \r".as_bytes())?; // Erase the "⏎" if nothing overwrites it 270 | let Prompt { 271 | mut prompt, 272 | vi_status, 273 | } = prompt; 274 | out.write_all(prompt.split('\n').join("\r\n").as_bytes())?; 275 | if let Some(index) = prompt.rfind('\n') { 276 | prompt = prompt.split_at(index + 1).1.into() 277 | } 278 | let prompt = Prompt { prompt, vi_status }; 279 | let mut ed = Editor { 280 | prompt, 281 | cursor: 0, 282 | out, 283 | closure: f, 284 | new_buf: buffer.into(), 285 | hist_buf: Buffer::new(), 286 | hist_buf_valid: false, 287 | cur_history_loc: None, 288 | context, 289 | show_completions_hint: None, 290 | show_autosuggestions: true, 291 | term_cursor_line: 1, 292 | no_eol: false, 293 | reverse_search: false, 294 | forward_search: false, 295 | buffer_changed: false, 296 | history_subset_index: vec![], 297 | history_subset_loc: None, 298 | autosuggestion: None, 299 | history_fresh: false, 300 | }; 301 | 302 | if !ed.new_buf.is_empty() { 303 | ed.move_cursor_to_end_of_line()?; 304 | } 305 | ed.display()?; 306 | Ok(ed) 307 | } 308 | 309 | fn is_search(&self) -> bool { 310 | self.reverse_search || self.forward_search 311 | } 312 | 313 | fn clear_search(&mut self) { 314 | self.reverse_search = false; 315 | self.forward_search = false; 316 | self.history_subset_loc = None; 317 | self.history_subset_index.clear(); 318 | } 319 | 320 | /// None if we're on the new buffer, else the index of history 321 | pub fn current_history_location(&self) -> Option { 322 | self.cur_history_loc 323 | } 324 | 325 | pub fn get_words_and_cursor_position(&self) -> (Vec<(usize, usize)>, CursorPosition) { 326 | let word_fn = &self.context.word_divider_fn; 327 | let words = word_fn(cur_buf!(self)); 328 | let pos = CursorPosition::get(self.cursor, &words); 329 | (words, pos) 330 | } 331 | 332 | pub fn set_prompt(&mut self, mut prompt: Prompt) { 333 | if let Some(passed_status) = &mut prompt.vi_status { 334 | if let Some(old_status) = &self.prompt.vi_status { 335 | passed_status.mode = old_status.mode; 336 | } 337 | } 338 | self.prompt = prompt; 339 | } 340 | 341 | pub fn context(&mut self) -> &mut Context { 342 | self.context 343 | } 344 | 345 | pub fn cursor(&self) -> usize { 346 | self.cursor 347 | } 348 | 349 | // XXX: Returning a bool to indicate doneness is a bit awkward, maybe change it 350 | pub fn handle_newline(&mut self) -> io::Result { 351 | self.history_fresh = false; 352 | if self.is_search() { 353 | self.accept_autosuggestion()?; 354 | } 355 | self.clear_search(); 356 | if self.show_completions_hint.is_some() { 357 | self.show_completions_hint = None; 358 | return Ok(false); 359 | } 360 | 361 | let char_before_cursor = cur_buf!(self).char_before(self.cursor); 362 | if char_before_cursor == Some('\\') { 363 | // self.insert_after_cursor('\r')?; 364 | self.insert_after_cursor('\n')?; 365 | Ok(false) 366 | } else { 367 | self.cursor = cur_buf!(self).num_chars(); 368 | self._display(false)?; 369 | self.out.write_all(b"\r\n")?; 370 | self.show_completions_hint = None; 371 | Ok(true) 372 | } 373 | } 374 | 375 | fn search_history_loc(&self) -> Option { 376 | self.history_subset_loc 377 | .and_then(|i| self.history_subset_index.get(i).cloned()) 378 | } 379 | 380 | fn freshen_history(&mut self) { 381 | if self.context.history.share && !self.history_fresh { 382 | let _ = self.context.history.load_history(false); 383 | self.history_fresh = true; 384 | } 385 | } 386 | 387 | /// Refresh incremental search, either when started or when the buffer changes. 388 | fn refresh_search(&mut self, forward: bool) { 389 | let search_history_loc = self.search_history_loc(); 390 | self.history_subset_index = self.context.history.search_index(&self.new_buf); 391 | if !self.history_subset_index.is_empty() { 392 | self.history_subset_loc = if forward { 393 | Some(0) 394 | } else { 395 | Some(self.history_subset_index.len() - 1) 396 | }; 397 | if let Some(target_loc) = search_history_loc { 398 | for (i, history_loc) in self.history_subset_index.iter().enumerate() { 399 | if target_loc <= *history_loc { 400 | if forward || target_loc == *history_loc || i == 0 { 401 | self.history_subset_loc = Some(i); 402 | } else { 403 | self.history_subset_loc = Some(i - 1); 404 | } 405 | break; 406 | } 407 | } 408 | } 409 | } else { 410 | self.history_subset_loc = None; 411 | } 412 | 413 | self.reverse_search = !forward; 414 | self.forward_search = forward; 415 | self.cur_history_loc = None; 416 | self.hist_buf_valid = false; 417 | self.buffer_changed = false; 418 | } 419 | 420 | /// Begin or continue a search through history. If forward is true then start at top (or 421 | /// current_history_loc if set). If started with forward true then incremental search goes 422 | /// forward (top to bottom) other wise reverse (bottom to top). It is valid to continue a 423 | /// search with forward changed (i.e. reverse search direction for one result). 424 | pub fn search(&mut self, forward: bool) -> io::Result<()> { 425 | if !self.is_search() { 426 | self.freshen_history(); 427 | self.refresh_search(forward); 428 | } else if !self.history_subset_index.is_empty() { 429 | self.history_subset_loc = if let Some(p) = self.history_subset_loc { 430 | if forward { 431 | if p < self.history_subset_index.len() - 1 { 432 | Some(p + 1) 433 | } else { 434 | Some(0) 435 | } 436 | } else if p > 0 { 437 | Some(p - 1) 438 | } else { 439 | Some(self.history_subset_index.len() - 1) 440 | } 441 | } else { 442 | None 443 | }; 444 | } 445 | self.display()?; 446 | Ok(()) 447 | } 448 | 449 | pub fn flush(&mut self) -> io::Result<()> { 450 | self.out.flush() 451 | } 452 | 453 | /// Attempts to undo an action on the current buffer. 454 | /// 455 | /// Returns `Ok(true)` if an action was undone. 456 | /// Returns `Ok(false)` if there was no action to undo. 457 | pub fn undo(&mut self) -> io::Result { 458 | let did = cur_buf_mut!(self).undo(); 459 | if did { 460 | self.move_cursor_to_end_of_line()?; 461 | } else { 462 | self.display()?; 463 | } 464 | Ok(did) 465 | } 466 | 467 | pub fn redo(&mut self) -> io::Result { 468 | let did = cur_buf_mut!(self).redo(); 469 | if did { 470 | self.move_cursor_to_end_of_line()?; 471 | } else { 472 | self.display()?; 473 | } 474 | Ok(did) 475 | } 476 | 477 | pub fn revert(&mut self) -> io::Result { 478 | let did = cur_buf_mut!(self).revert(); 479 | if did { 480 | self.move_cursor_to_end_of_line()?; 481 | } else { 482 | self.display()?; 483 | } 484 | Ok(did) 485 | } 486 | 487 | fn print_completion_list( 488 | completions: &[String], 489 | highlighted: Option, 490 | output_buf: &mut String, 491 | ) -> io::Result { 492 | use std::cmp::max; 493 | 494 | let (w, _) = termion::terminal_size()?; 495 | 496 | // XXX wide character support 497 | let max_word_size = completions.iter().fold(1, |m, x| max(m, x.chars().count())); 498 | let cols = max(1, w as usize / (max_word_size)); 499 | let col_width = 2 + w as usize / cols; 500 | let cols = max(1, w as usize / col_width); 501 | 502 | let lines = completions.len() / cols; 503 | 504 | let mut i = 0; 505 | for (index, com) in completions.iter().enumerate() { 506 | if i == cols { 507 | output_buf.push_str("\r\n"); 508 | i = 0; 509 | } else if i > cols { 510 | unreachable!() 511 | } 512 | 513 | if Some(index) == highlighted { 514 | write!( 515 | output_buf, 516 | "{}{}", 517 | color::Black.fg_str(), 518 | color::White.bg_str() 519 | ) 520 | .unwrap(); 521 | } 522 | write!(output_buf, "{:<1$}", com, col_width).unwrap(); 523 | if Some(index) == highlighted { 524 | write!( 525 | output_buf, 526 | "{}{}", 527 | color::Reset.bg_str(), 528 | color::Reset.fg_str() 529 | ) 530 | .unwrap(); 531 | } 532 | 533 | i += 1; 534 | } 535 | 536 | Ok(lines) 537 | } 538 | 539 | pub fn skip_completions_hint(&mut self) { 540 | self.show_completions_hint = None; 541 | } 542 | 543 | pub fn complete(&mut self, handler: &mut T) -> io::Result<()> { 544 | handler.on_event(Event::new(self, EventKind::BeforeComplete)); 545 | 546 | if let Some((completions, i_in)) = self.show_completions_hint.take() { 547 | let i = i_in.map_or(0, |i| (i + 1) % completions.len()); 548 | 549 | match i_in { 550 | Some(x) if cur_buf!(self) == &Buffer::from(&completions[x][..]) => { 551 | cur_buf_mut!(self).truncate(0); 552 | self.cursor = 0; 553 | } 554 | _ => self.delete_word_before_cursor(false)?, 555 | } 556 | self.insert_str_after_cursor(&completions[i])?; 557 | 558 | self.show_completions_hint = Some((completions, Some(i))); 559 | } 560 | if self.show_completions_hint.is_some() { 561 | self.display()?; 562 | return Ok(()); 563 | } 564 | 565 | let (word, completions) = { 566 | let word_range = self.get_word_before_cursor(false); 567 | let buf = cur_buf_mut!(self); 568 | 569 | let word = match word_range { 570 | Some((start, end)) => buf.range(start, end), 571 | None => "".into(), 572 | }; 573 | 574 | let mut completions = handler.completions(word.as_ref()); 575 | completions.sort(); 576 | completions.dedup(); 577 | (word, completions) 578 | }; 579 | 580 | if completions.is_empty() { 581 | // Do nothing. 582 | self.show_completions_hint = None; 583 | Ok(()) 584 | } else if completions.len() == 1 { 585 | self.show_completions_hint = None; 586 | self.delete_word_before_cursor(false)?; 587 | self.insert_str_after_cursor(completions[0].as_ref()) 588 | } else { 589 | let common_prefix = util::find_longest_common_prefix( 590 | &completions 591 | .iter() 592 | .map(|x| x.chars().collect()) 593 | .collect::>>()[..], 594 | ); 595 | 596 | if let Some(p) = common_prefix { 597 | let s = p.iter().cloned().collect::(); 598 | 599 | if s.len() > word.len() && s.starts_with(&word[..]) { 600 | self.delete_word_before_cursor(false)?; 601 | return self.insert_str_after_cursor(s.as_ref()); 602 | } 603 | } 604 | 605 | self.show_completions_hint = Some((completions, None)); 606 | self.display()?; 607 | 608 | Ok(()) 609 | } 610 | } 611 | 612 | fn get_word_before_cursor(&self, ignore_space_before_cursor: bool) -> Option<(usize, usize)> { 613 | let (words, pos) = self.get_words_and_cursor_position(); 614 | match pos { 615 | CursorPosition::InWord(i) => Some(words[i]), 616 | CursorPosition::InSpace(Some(i), _) => { 617 | if ignore_space_before_cursor { 618 | Some(words[i]) 619 | } else { 620 | None 621 | } 622 | } 623 | CursorPosition::InSpace(None, _) => None, 624 | CursorPosition::OnWordLeftEdge(i) => { 625 | if ignore_space_before_cursor && i > 0 { 626 | Some(words[i - 1]) 627 | } else { 628 | None 629 | } 630 | } 631 | CursorPosition::OnWordRightEdge(i) => Some(words[i]), 632 | } 633 | } 634 | 635 | /// Deletes the word preceding the cursor. 636 | /// If `ignore_space_before_cursor` is true and there is space directly before the cursor, 637 | /// this method ignores that space until it finds a word. 638 | /// If `ignore_space_before_cursor` is false and there is space directly before the cursor, 639 | /// nothing is deleted. 640 | pub fn delete_word_before_cursor( 641 | &mut self, 642 | ignore_space_before_cursor: bool, 643 | ) -> io::Result<()> { 644 | if let Some((start, _)) = self.get_word_before_cursor(ignore_space_before_cursor) { 645 | let moved = cur_buf_mut!(self).remove(start, self.cursor); 646 | self.cursor -= moved; 647 | } 648 | self.display() 649 | } 650 | 651 | /// Clears the screen then prints the prompt and current buffer. 652 | pub fn clear(&mut self) -> io::Result<()> { 653 | write!( 654 | &mut self.context.buf, 655 | "{}{}", 656 | clear::All, 657 | cursor::Goto(1, 1) 658 | ) 659 | .unwrap(); 660 | 661 | self.term_cursor_line = 1; 662 | self.clear_search(); 663 | self.display() 664 | } 665 | 666 | /// Move up (backwards) in history. 667 | pub fn move_up(&mut self) -> io::Result<()> { 668 | if self.is_search() { 669 | self.search(false) 670 | } else { 671 | self.hist_buf_valid = false; 672 | self.freshen_history(); 673 | if self.new_buf.num_chars() > 0 { 674 | match self.history_subset_loc { 675 | Some(i) if i > 0 => { 676 | self.history_subset_loc = Some(i - 1); 677 | self.cur_history_loc = Some(self.history_subset_index[i - 1]); 678 | } 679 | None => { 680 | self.history_subset_index = 681 | self.context.history.get_history_subset(&self.new_buf); 682 | if !self.history_subset_index.is_empty() { 683 | self.history_subset_loc = Some(self.history_subset_index.len() - 1); 684 | self.cur_history_loc = Some( 685 | self.history_subset_index[self.history_subset_index.len() - 1], 686 | ); 687 | } 688 | } 689 | _ => (), 690 | } 691 | } else { 692 | match self.cur_history_loc { 693 | Some(i) if i > 0 => self.cur_history_loc = Some(i - 1), 694 | None if !self.context.history.is_empty() => { 695 | self.cur_history_loc = Some(self.context.history.len() - 1) 696 | } 697 | _ => (), 698 | } 699 | } 700 | self.move_cursor_to_end_of_line() 701 | } 702 | } 703 | 704 | /// Move down (forwards) in history, or to the new buffer if we reach the end of history. 705 | pub fn move_down(&mut self) -> io::Result<()> { 706 | if self.is_search() { 707 | self.search(true) 708 | } else { 709 | self.hist_buf_valid = false; 710 | if self.new_buf.num_chars() > 0 { 711 | if let Some(i) = self.history_subset_loc { 712 | if i < self.history_subset_index.len() - 1 { 713 | self.history_subset_loc = Some(i + 1); 714 | self.cur_history_loc = Some(self.history_subset_index[i + 1]); 715 | } else { 716 | self.cur_history_loc = None; 717 | self.history_subset_loc = None; 718 | self.history_subset_index.clear(); 719 | self.history_fresh = false; 720 | } 721 | } 722 | } else { 723 | match self.cur_history_loc.take() { 724 | Some(i) if i < self.context.history.len() - 1 => { 725 | self.cur_history_loc = Some(i + 1) 726 | } 727 | _ => self.history_fresh = false, 728 | } 729 | } 730 | self.move_cursor_to_end_of_line() 731 | } 732 | } 733 | 734 | /// Moves to the start of history (ie. the earliest history entry). 735 | pub fn move_to_start_of_history(&mut self) -> io::Result<()> { 736 | self.hist_buf_valid = false; 737 | if self.context.history.is_empty() { 738 | self.cur_history_loc = None; 739 | self.display() 740 | } else { 741 | self.cur_history_loc = Some(0); 742 | self.move_cursor_to_end_of_line() 743 | } 744 | } 745 | 746 | /// Moves to the end of history (ie. the new buffer). 747 | pub fn move_to_end_of_history(&mut self) -> io::Result<()> { 748 | self.hist_buf_valid = false; 749 | if self.cur_history_loc.is_some() { 750 | self.cur_history_loc = None; 751 | self.move_cursor_to_end_of_line() 752 | } else { 753 | self.display() 754 | } 755 | } 756 | 757 | /// Inserts a string directly after the cursor, moving the cursor to the right. 758 | /// 759 | /// Note: it is more efficient to call `insert_chars_after_cursor()` directly. 760 | pub fn insert_str_after_cursor(&mut self, s: &str) -> io::Result<()> { 761 | self.insert_chars_after_cursor(&s.chars().collect::>()[..]) 762 | } 763 | 764 | /// Inserts a character directly after the cursor, moving the cursor to the right. 765 | pub fn insert_after_cursor(&mut self, c: char) -> io::Result<()> { 766 | self.insert_chars_after_cursor(&[c]) 767 | } 768 | 769 | /// Inserts characters directly after the cursor, moving the cursor to the right. 770 | pub fn insert_chars_after_cursor(&mut self, cs: &[char]) -> io::Result<()> { 771 | { 772 | let buf = cur_buf_mut!(self); 773 | buf.insert(self.cursor, cs); 774 | } 775 | 776 | self.cursor += cs.len(); 777 | self.display() 778 | } 779 | 780 | /// Deletes the character directly before the cursor, moving the cursor to the left. 781 | /// If the cursor is at the start of the line, nothing happens. 782 | pub fn delete_before_cursor(&mut self) -> io::Result<()> { 783 | if self.cursor > 0 { 784 | let buf = cur_buf_mut!(self); 785 | buf.remove(self.cursor - 1, self.cursor); 786 | self.cursor -= 1; 787 | } 788 | 789 | self.display() 790 | } 791 | 792 | /// Deletes the character directly after the cursor. The cursor does not move. 793 | /// If the cursor is at the end of the line, nothing happens. 794 | pub fn delete_after_cursor(&mut self) -> io::Result<()> { 795 | { 796 | let buf = cur_buf_mut!(self); 797 | 798 | if self.cursor < buf.num_chars() { 799 | buf.remove(self.cursor, self.cursor + 1); 800 | } 801 | } 802 | self.display() 803 | } 804 | 805 | /// Deletes every character preceding the cursor until the beginning of the line. 806 | pub fn delete_all_before_cursor(&mut self) -> io::Result<()> { 807 | cur_buf_mut!(self).remove(0, self.cursor); 808 | self.cursor = 0; 809 | self.display() 810 | } 811 | 812 | /// Deletes every character after the cursor until the end of the line. 813 | pub fn delete_all_after_cursor(&mut self) -> io::Result<()> { 814 | { 815 | let buf = cur_buf_mut!(self); 816 | buf.truncate(self.cursor); 817 | } 818 | self.display() 819 | } 820 | 821 | /// Deletes every character from the cursor until the given position. 822 | pub fn delete_until(&mut self, position: usize) -> io::Result<()> { 823 | { 824 | let buf = cur_buf_mut!(self); 825 | buf.remove( 826 | cmp::min(self.cursor, position), 827 | cmp::max(self.cursor, position), 828 | ); 829 | self.cursor = cmp::min(self.cursor, position); 830 | } 831 | self.display() 832 | } 833 | 834 | /// Deletes every character from the cursor until the given position, inclusive. 835 | pub fn delete_until_inclusive(&mut self, position: usize) -> io::Result<()> { 836 | { 837 | let buf = cur_buf_mut!(self); 838 | buf.remove( 839 | cmp::min(self.cursor, position), 840 | cmp::max(self.cursor + 1, position + 1), 841 | ); 842 | self.cursor = cmp::min(self.cursor, position); 843 | } 844 | self.display() 845 | } 846 | 847 | /// Moves the cursor to the left by `count` characters. 848 | /// The cursor will not go past the start of the buffer. 849 | pub fn move_cursor_left(&mut self, mut count: usize) -> io::Result<()> { 850 | if count > self.cursor { 851 | count = self.cursor; 852 | } 853 | 854 | self.cursor -= count; 855 | 856 | self.display() 857 | } 858 | 859 | /// Moves the cursor to the right by `count` characters. 860 | /// The cursor will not go past the end of the buffer. 861 | pub fn move_cursor_right(&mut self, mut count: usize) -> io::Result<()> { 862 | { 863 | let buf = cur_buf!(self); 864 | 865 | if count > buf.num_chars() - self.cursor { 866 | count = buf.num_chars() - self.cursor; 867 | } 868 | 869 | self.cursor += count; 870 | } 871 | 872 | self.display() 873 | } 874 | 875 | /// Moves the cursor to `pos`. If `pos` is past the end of the buffer, it will be clamped. 876 | pub fn move_cursor_to(&mut self, pos: usize) -> io::Result<()> { 877 | self.cursor = pos; 878 | let buf_len = cur_buf!(self).num_chars(); 879 | if self.cursor > buf_len { 880 | self.cursor = buf_len; 881 | } 882 | self.display() 883 | } 884 | 885 | /// Moves the cursor to the start of the line. 886 | pub fn move_cursor_to_start_of_line(&mut self) -> io::Result<()> { 887 | self.cursor = 0; 888 | self.display() 889 | } 890 | 891 | /// Moves the cursor to the end of the line. 892 | pub fn move_cursor_to_end_of_line(&mut self) -> io::Result<()> { 893 | self.cursor = cur_buf!(self).num_chars(); 894 | self.display() 895 | } 896 | 897 | pub fn cursor_is_at_end_of_line(&self) -> bool { 898 | let num_chars = cur_buf!(self).num_chars(); 899 | if self.no_eol { 900 | self.cursor == num_chars - 1 901 | } else { 902 | self.cursor == num_chars 903 | } 904 | } 905 | 906 | /// Returns a reference to the current buffer being edited. 907 | /// This may be the new buffer or a buffer from history. 908 | pub fn current_buffer(&self) -> &Buffer { 909 | cur_buf!(self) 910 | } 911 | 912 | /// Returns a mutable reference to the current buffer being edited. 913 | /// This may be the new buffer or a buffer from history. 914 | pub fn current_buffer_mut(&mut self) -> &mut Buffer { 915 | cur_buf_mut!(self) 916 | } 917 | 918 | /// Accept autosuggestion and copy its content into current buffer 919 | pub fn accept_autosuggestion(&mut self) -> io::Result<()> { 920 | if self.show_autosuggestions { 921 | { 922 | let autosuggestion = self.autosuggestion.clone(); 923 | let search = self.is_search(); 924 | let buf = self.current_buffer_mut(); 925 | match autosuggestion { 926 | Some(ref x) if search => buf.copy_buffer(x), 927 | Some(ref x) => buf.insert_from_buffer(x), 928 | None => (), 929 | } 930 | } 931 | } 932 | self.clear_search(); 933 | self.move_cursor_to_end_of_line() 934 | } 935 | 936 | /// Returns current auto suggestion, for history search this is the current match if not 937 | /// searching the first history entry to start with current text (reverse order). 938 | /// Return None if nothing found. 939 | fn current_autosuggestion(&mut self) -> Option { 940 | // If we are editing a previous history item no autosuggestion. 941 | if self.hist_buf_valid { 942 | return None; 943 | } 944 | let context_history = &self.context.history; 945 | let autosuggestion = if self.is_search() { 946 | self.search_history_loc().map(|i| &context_history[i]) 947 | } else if self.show_autosuggestions { 948 | self.cur_history_loc 949 | .map(|i| &context_history[i]) 950 | .or_else(|| { 951 | context_history 952 | .get_newest_match(Some(context_history.len()), &self.new_buf) 953 | .map(|i| &context_history[i]) 954 | }) 955 | } else { 956 | None 957 | }; 958 | autosuggestion.cloned() 959 | } 960 | 961 | pub fn is_currently_showing_autosuggestion(&self) -> bool { 962 | self.autosuggestion.is_some() 963 | } 964 | 965 | /// Override the prompt for incremental search if needed. 966 | fn search_prompt(&mut self) -> (String, usize) { 967 | if self.is_search() { 968 | // If we are searching override prompt to search prompt. 969 | let (hplace, color) = if self.history_subset_index.is_empty() { 970 | (0, color::Red.fg_str()) 971 | } else { 972 | ( 973 | self.history_subset_loc.unwrap_or(0) + 1, 974 | color::Green.fg_str(), 975 | ) 976 | }; 977 | let prefix = self.prompt.prefix(); 978 | ( 979 | format!( 980 | "{}(search)'{}{}{}` ({}/{}): ", 981 | &prefix, 982 | color, 983 | self.current_buffer(), 984 | color::Reset.fg_str(), 985 | hplace, 986 | self.history_subset_index.len() 987 | ), 988 | strip(prefix).len() + 9, 989 | ) 990 | } else { 991 | (self.prompt.to_string(), 0) 992 | } 993 | } 994 | 995 | fn _display(&mut self, show_autosuggest: bool) -> io::Result<()> { 996 | fn calc_width(prompt_width: usize, buf_widths: &[usize], terminal_width: usize) -> usize { 997 | let mut total = 0; 998 | 999 | for line in buf_widths { 1000 | if total % terminal_width != 0 { 1001 | total = ((total / terminal_width) + 1) * terminal_width; 1002 | } 1003 | 1004 | total += prompt_width + line; 1005 | } 1006 | 1007 | total 1008 | } 1009 | 1010 | let (prompt, rev_prompt_width) = self.search_prompt(); 1011 | 1012 | let terminal_width = util::terminal_width()?; 1013 | let prompt_width = util::last_prompt_line_width(&prompt); 1014 | 1015 | let buf = cur_buf!(self); 1016 | let buf_width = buf.width(); 1017 | 1018 | // Don't let the cursor go over the end! 1019 | let buf_num_chars = buf.num_chars(); 1020 | if buf_num_chars < self.cursor { 1021 | self.cursor = buf_num_chars; 1022 | } 1023 | 1024 | // Can't move past the last character in vi normal mode 1025 | if self.no_eol && self.cursor != 0 && self.cursor == buf_num_chars { 1026 | self.cursor -= 1; 1027 | } 1028 | 1029 | let buf_widths = match self.autosuggestion { 1030 | Some(ref suggestion) => suggestion.width(), 1031 | None => buf_width, 1032 | }; 1033 | // Width of the current buffer lines (including autosuggestion) from the start to the cursor 1034 | let buf_widths_to_cursor = match self.autosuggestion { 1035 | // Cursor might overrun autosuggestion with history search. 1036 | Some(ref suggestion) if self.cursor < suggestion.num_chars() => { 1037 | suggestion.range_width(0, self.cursor) 1038 | } 1039 | _ => buf.range_width(0, self.cursor), 1040 | }; 1041 | 1042 | // Total number of terminal spaces taken up by prompt and buffer 1043 | let new_total_width = calc_width(prompt_width, &buf_widths, terminal_width); 1044 | let new_total_width_to_cursor = if self.is_search() { 1045 | calc_width(rev_prompt_width, &buf_widths_to_cursor, terminal_width) 1046 | } else { 1047 | calc_width(prompt_width, &buf_widths_to_cursor, terminal_width) 1048 | }; 1049 | 1050 | let new_num_lines = (new_total_width + terminal_width) / terminal_width; 1051 | 1052 | self.context.buf.push_str("\x1B[?1000l\x1B[?1l"); 1053 | 1054 | // Move the term cursor to the same line as the prompt. 1055 | if self.term_cursor_line > 1 { 1056 | write!( 1057 | &mut self.context.buf, 1058 | "{}", 1059 | cursor::Up(self.term_cursor_line as u16 - 1) 1060 | ) 1061 | .unwrap(); 1062 | } 1063 | 1064 | write!(&mut self.context.buf, "\r{}", clear::AfterCursor).unwrap(); 1065 | 1066 | // If we're cycling through completions, show those 1067 | let mut completion_lines = 0; 1068 | if let Some((completions, i)) = self.show_completions_hint.as_ref() { 1069 | completion_lines = 1070 | 1 + Self::print_completion_list(completions, *i, &mut self.context.buf)?; 1071 | self.context.buf.push_str("\r\n"); 1072 | } 1073 | 1074 | // Write the prompt 1075 | write!(&mut self.context.buf, "{}", prompt).unwrap(); 1076 | 1077 | // If we have an autosuggestion, we make the autosuggestion the buffer we print out. 1078 | // We get the number of bytes in the buffer (but NOT the autosuggestion). 1079 | // Then, we loop and subtract from that number until it's 0, in which case we are printing 1080 | // the autosuggestion from here on (in a different color). 1081 | let lines = match self.autosuggestion { 1082 | Some(ref suggestion) if show_autosuggest => suggestion.lines(), 1083 | _ => buf.lines(), 1084 | }; 1085 | let mut buf_num_remaining_bytes = buf.num_bytes(); 1086 | 1087 | let lines_len = lines.len(); 1088 | for (i, line) in lines.into_iter().enumerate() { 1089 | if i > 0 { 1090 | write!( 1091 | &mut self.context.buf, 1092 | "{}", 1093 | cursor::Right(prompt_width as u16) 1094 | ) 1095 | .unwrap(); 1096 | } 1097 | 1098 | if buf_num_remaining_bytes == 0 { 1099 | self.context.buf.push_str(&line); 1100 | } else if line.len() > buf_num_remaining_bytes { 1101 | let start = &line[..buf_num_remaining_bytes]; 1102 | let start = match self.closure { 1103 | Some(ref f) => f(start), 1104 | None => start.to_owned(), 1105 | }; 1106 | if self.is_search() { 1107 | write!(&mut self.context.buf, "{}", color::Yellow.fg_str()).unwrap(); 1108 | } 1109 | write!(&mut self.context.buf, "{}", start).unwrap(); 1110 | if !self.is_search() { 1111 | write!(&mut self.context.buf, "{}", color::Yellow.fg_str()).unwrap(); 1112 | } 1113 | self.context.buf.push_str(&line[buf_num_remaining_bytes..]); 1114 | buf_num_remaining_bytes = 0; 1115 | } else { 1116 | buf_num_remaining_bytes -= line.len(); 1117 | let written_line = match self.closure { 1118 | Some(ref f) => f(&line), 1119 | None => line, 1120 | }; 1121 | if self.is_search() { 1122 | write!(&mut self.context.buf, "{}", color::Yellow.fg_str()).unwrap(); 1123 | } 1124 | self.context.buf.push_str(&written_line); 1125 | } 1126 | 1127 | if i + 1 < lines_len { 1128 | self.context.buf.push_str("\r\n"); 1129 | } 1130 | } 1131 | 1132 | if self.is_currently_showing_autosuggestion() || self.is_search() { 1133 | write!(&mut self.context.buf, "{}", color::Reset.fg_str()).unwrap(); 1134 | } 1135 | 1136 | // at the end of the line, move the cursor down a line 1137 | if new_total_width % terminal_width == 0 { 1138 | self.context.buf.push_str("\r\n"); 1139 | } 1140 | 1141 | self.term_cursor_line = (new_total_width_to_cursor + terminal_width) / terminal_width; 1142 | 1143 | // The term cursor is now on the bottom line. We may need to move the term cursor up 1144 | // to the line where the true cursor is. 1145 | let cursor_line_diff = new_num_lines as isize - self.term_cursor_line as isize; 1146 | if cursor_line_diff > 0 { 1147 | write!( 1148 | &mut self.context.buf, 1149 | "{}", 1150 | cursor::Up(cursor_line_diff as u16) 1151 | ) 1152 | .unwrap(); 1153 | } else if cursor_line_diff < 0 { 1154 | unreachable!(); 1155 | } 1156 | 1157 | // Now that we are on the right line, we must move the term cursor left or right 1158 | // to match the true cursor. 1159 | let cursor_col_diff = new_total_width as isize 1160 | - new_total_width_to_cursor as isize 1161 | - cursor_line_diff * terminal_width as isize; 1162 | if cursor_col_diff > 0 { 1163 | write!( 1164 | &mut self.context.buf, 1165 | "{}", 1166 | cursor::Left(cursor_col_diff as u16) 1167 | ) 1168 | .unwrap(); 1169 | } else if cursor_col_diff < 0 { 1170 | write!( 1171 | &mut self.context.buf, 1172 | "{}", 1173 | cursor::Right((-cursor_col_diff) as u16) 1174 | ) 1175 | .unwrap(); 1176 | } 1177 | 1178 | self.term_cursor_line += completion_lines; 1179 | 1180 | { 1181 | let out = &mut self.out; 1182 | out.write_all(self.context.buf.as_bytes())?; 1183 | self.context.buf.clear(); 1184 | out.flush() 1185 | } 1186 | } 1187 | 1188 | /// Deletes the displayed prompt and buffer, replacing them with the current prompt and buffer 1189 | pub fn display(&mut self) -> io::Result<()> { 1190 | if self.is_search() && self.buffer_changed { 1191 | // Refresh incremental search. 1192 | let forward = self.forward_search; 1193 | self.refresh_search(forward); 1194 | } 1195 | self.autosuggestion = self.current_autosuggestion(); 1196 | 1197 | self._display(true) 1198 | } 1199 | 1200 | /// Modifies the prompt to reflect the Vi mode. 1201 | /// 1202 | /// This operation will be ignored if a static prompt is used, as mode changes will have no 1203 | /// side effect. 1204 | pub fn set_vi_mode(&mut self, mode: ViPromptMode) { 1205 | if let Some(status) = &mut self.prompt.vi_status { 1206 | status.mode = mode; 1207 | } 1208 | } 1209 | } 1210 | 1211 | impl<'a, W: io::Write> From> for String { 1212 | fn from(ed: Editor<'a, W>) -> String { 1213 | match ed.cur_history_loc { 1214 | Some(i) => { 1215 | if ed.hist_buf_valid { 1216 | ed.hist_buf 1217 | } else { 1218 | ed.context.history[i].clone() 1219 | } 1220 | } 1221 | _ => ed.new_buf, 1222 | } 1223 | .into() 1224 | } 1225 | } 1226 | 1227 | #[cfg(test)] 1228 | mod tests { 1229 | use super::*; 1230 | use Context; 1231 | 1232 | #[test] 1233 | /// test undoing delete_all_after_cursor 1234 | fn delete_all_after_cursor_undo() { 1235 | let mut context = Context::new(); 1236 | let out = Vec::new(); 1237 | let mut ed = Editor::new(out, Prompt::from("prompt"), None, &mut context).unwrap(); 1238 | ed.insert_str_after_cursor("delete all of this").unwrap(); 1239 | ed.move_cursor_to_start_of_line().unwrap(); 1240 | ed.delete_all_after_cursor().unwrap(); 1241 | ed.undo().unwrap(); 1242 | assert_eq!(String::from(ed), "delete all of this"); 1243 | } 1244 | 1245 | #[test] 1246 | fn move_cursor_left() { 1247 | let mut context = Context::new(); 1248 | let out = Vec::new(); 1249 | let mut ed = Editor::new(out, Prompt::from("prompt"), None, &mut context).unwrap(); 1250 | ed.insert_str_after_cursor("let").unwrap(); 1251 | assert_eq!(ed.cursor, 3); 1252 | 1253 | ed.move_cursor_left(1).unwrap(); 1254 | assert_eq!(ed.cursor, 2); 1255 | 1256 | ed.insert_after_cursor('f').unwrap(); 1257 | assert_eq!(ed.cursor, 3); 1258 | assert_eq!(String::from(ed), "left"); 1259 | } 1260 | 1261 | #[test] 1262 | fn cursor_movement() { 1263 | let mut context = Context::new(); 1264 | let out = Vec::new(); 1265 | let mut ed = Editor::new(out, Prompt::from("prompt"), None, &mut context).unwrap(); 1266 | ed.insert_str_after_cursor("right").unwrap(); 1267 | assert_eq!(ed.cursor, 5); 1268 | 1269 | ed.move_cursor_left(2).unwrap(); 1270 | ed.move_cursor_right(1).unwrap(); 1271 | assert_eq!(ed.cursor, 4); 1272 | } 1273 | 1274 | #[test] 1275 | fn delete_until_backwards() { 1276 | let mut context = Context::new(); 1277 | let out = Vec::new(); 1278 | let mut ed = Editor::new(out, Prompt::from("prompt"), None, &mut context).unwrap(); 1279 | ed.insert_str_after_cursor("right").unwrap(); 1280 | assert_eq!(ed.cursor, 5); 1281 | 1282 | ed.delete_until(0).unwrap(); 1283 | assert_eq!(ed.cursor, 0); 1284 | assert_eq!(String::from(ed), ""); 1285 | } 1286 | 1287 | #[test] 1288 | fn delete_until_forwards() { 1289 | let mut context = Context::new(); 1290 | let out = Vec::new(); 1291 | let mut ed = Editor::new(out, Prompt::from("prompt"), None, &mut context).unwrap(); 1292 | ed.insert_str_after_cursor("right").unwrap(); 1293 | ed.cursor = 0; 1294 | 1295 | ed.delete_until(5).unwrap(); 1296 | assert_eq!(ed.cursor, 0); 1297 | assert_eq!(String::from(ed), ""); 1298 | } 1299 | 1300 | #[test] 1301 | fn delete_until() { 1302 | let mut context = Context::new(); 1303 | let out = Vec::new(); 1304 | let mut ed = Editor::new(out, Prompt::from("prompt"), None, &mut context).unwrap(); 1305 | ed.insert_str_after_cursor("right").unwrap(); 1306 | ed.cursor = 4; 1307 | 1308 | ed.delete_until(1).unwrap(); 1309 | assert_eq!(ed.cursor, 1); 1310 | assert_eq!(String::from(ed), "rt"); 1311 | } 1312 | 1313 | #[test] 1314 | fn delete_until_inclusive() { 1315 | let mut context = Context::new(); 1316 | let out = Vec::new(); 1317 | let mut ed = Editor::new(out, Prompt::from("prompt"), None, &mut context).unwrap(); 1318 | ed.insert_str_after_cursor("right").unwrap(); 1319 | ed.cursor = 4; 1320 | 1321 | ed.delete_until_inclusive(1).unwrap(); 1322 | assert_eq!(ed.cursor, 1); 1323 | assert_eq!(String::from(ed), "r"); 1324 | } 1325 | } 1326 | -------------------------------------------------------------------------------- /src/event.rs: -------------------------------------------------------------------------------- 1 | use crate::Editor; 2 | use std::io::Write; 3 | use termion::event::Key; 4 | 5 | pub struct Event<'a, 'out: 'a, W: Write + 'a> { 6 | pub editor: &'a mut Editor<'out, W>, 7 | pub kind: EventKind, 8 | } 9 | 10 | impl<'a, 'out: 'a, W: Write + 'a> Event<'a, 'out, W> { 11 | pub fn new(editor: &'a mut Editor<'out, W>, kind: EventKind) -> Self { 12 | Event { editor, kind } 13 | } 14 | } 15 | 16 | #[derive(Debug)] 17 | pub enum EventKind { 18 | /// Sent before handling a keypress. 19 | BeforeKey(Key), 20 | /// Sent after handling a keypress. 21 | AfterKey(Key), 22 | /// Sent in `Editor.complete()`, before processing the completion. 23 | BeforeComplete, 24 | } 25 | -------------------------------------------------------------------------------- /src/history.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | use std::{ 4 | collections::{vec_deque, VecDeque}, 5 | fs::File, 6 | io::{self, Write}, 7 | io::{BufRead, BufReader, BufWriter}, 8 | iter::IntoIterator, 9 | ops::Index, 10 | ops::IndexMut, 11 | path::Path, 12 | //time::Duration, 13 | }; 14 | 15 | const DEFAULT_MAX_SIZE: usize = 1000; 16 | 17 | /// Structure encapsulating command history 18 | pub struct History { 19 | // TODO: this should eventually be private 20 | /// Vector of buffers to store history in 21 | pub buffers: VecDeque, 22 | /// Store a filename to save history into; if None don't save history 23 | file_name: Option, 24 | /// Maximal number of buffers stored in the memory 25 | /// TODO: just make this public? 26 | max_buffers_size: usize, 27 | /// Maximal number of lines stored in the file 28 | // TODO: just make this public? 29 | max_file_size: usize, 30 | // TODO set from environment variable? 31 | pub append_duplicate_entries: bool, 32 | /// Append each entry to history file as entered? 33 | pub inc_append: bool, 34 | /// Share history across ion's with the same history file (combine with inc_append). 35 | pub share: bool, 36 | /// Last filesize of history file, used to optimize history sharing. 37 | pub file_size: u64, 38 | /// Allow loading duplicate entries, need to know this for loading history files. 39 | pub load_duplicates: bool, 40 | /// Writes between history compaction. 41 | compaction_writes: usize, 42 | } 43 | 44 | impl Default for History { 45 | fn default() -> Self { 46 | Self::new() 47 | } 48 | } 49 | 50 | impl History { 51 | /// Create new History structure. 52 | pub fn new() -> History { 53 | History { 54 | buffers: VecDeque::with_capacity(DEFAULT_MAX_SIZE), 55 | file_name: None, 56 | max_buffers_size: DEFAULT_MAX_SIZE, 57 | max_file_size: DEFAULT_MAX_SIZE, 58 | append_duplicate_entries: false, 59 | inc_append: false, 60 | share: false, 61 | file_size: 0, 62 | load_duplicates: true, 63 | compaction_writes: 0, 64 | } 65 | } 66 | 67 | /// Clears out the history. 68 | pub fn clear_history(&mut self) { 69 | self.buffers.clear(); 70 | } 71 | 72 | /// Loads the history file from the saved path and appends it to the end of the history if append 73 | /// is true otherwise replace history. 74 | pub fn load_history(&mut self, append: bool) -> io::Result { 75 | if let Some(path) = self.file_name.clone() { 76 | let file_size = self.file_size; 77 | self.load_history_file_test(&path, file_size, append) 78 | .inspect(|&l| { 79 | self.file_size = l; 80 | }) 81 | } else { 82 | Err(io::Error::new( 83 | io::ErrorKind::Other, 84 | "History filename not set!", 85 | )) 86 | } 87 | } 88 | 89 | /// Loads the history file from path and appends it to the end of the history if append is true. 90 | pub fn load_history_file>(&mut self, path: P, append: bool) -> io::Result { 91 | self.load_history_file_test(path, 0, append) 92 | } 93 | 94 | /// Loads the history file from path and appends it to the end of the history.f append is true 95 | /// (replaces if false). Only loads if length is not equal to current file size. 96 | fn load_history_file_test>( 97 | &mut self, 98 | path: P, 99 | length: u64, 100 | append: bool, 101 | ) -> io::Result { 102 | let path = path.as_ref(); 103 | let file = if path.exists() { 104 | File::open(path)? 105 | } else { 106 | let status = format!("File not found {:?}", path); 107 | return Err(io::Error::new(io::ErrorKind::Other, status)); 108 | }; 109 | let new_length = file.metadata()?.len(); 110 | if new_length == 0 && length == 0 && !append { 111 | // Special case, trying to load nothing and not appending- just clear. 112 | self.clear_history(); 113 | } 114 | if new_length != length { 115 | if !append { 116 | self.clear_history(); 117 | } 118 | let reader = BufReader::new(file); 119 | for line in reader.lines() { 120 | match line { 121 | Ok(line) => { 122 | if !line.starts_with('#') { 123 | self.buffers.push_back(Buffer::from(line)); 124 | } 125 | } 126 | Err(_) => break, 127 | } 128 | } 129 | self.truncate(); 130 | if !self.load_duplicates { 131 | let mut tmp_buffers: Vec = Vec::with_capacity(self.buffers.len()); 132 | // Remove duplicates from loaded history if we do not want it. 133 | while let Some(buf) = self.buffers.pop_back() { 134 | self.remove_duplicates(&buf.to_string()[..]); 135 | tmp_buffers.push(buf); 136 | } 137 | while let Some(buf) = tmp_buffers.pop() { 138 | self.buffers.push_back(buf); 139 | } 140 | } 141 | } 142 | Ok(new_length) 143 | } 144 | 145 | /// Removes duplicates and trims a history file to max_file_size. 146 | /// Primarily if inc_append is set without shared history. 147 | /// Static because it should have no side effects on a history object. 148 | fn deduplicate_history_file>( 149 | path: P, 150 | max_file_size: usize, 151 | ) -> io::Result { 152 | let path = path.as_ref(); 153 | let file = if path.exists() { 154 | File::open(path)? 155 | } else { 156 | let status = format!("File not found {:?}", path); 157 | return Err(io::Error::new(io::ErrorKind::Other, status)); 158 | }; 159 | let mut buf: VecDeque = VecDeque::new(); 160 | let reader = BufReader::new(file); 161 | for line in reader.lines() { 162 | match line { 163 | Ok(line) => { 164 | if !line.starts_with('#') { 165 | buf.push_back(line); 166 | } 167 | } 168 | Err(_) => break, 169 | } 170 | } 171 | let org_length = buf.len(); 172 | if buf.len() >= max_file_size { 173 | let pop_out = buf.len() - max_file_size; 174 | for _ in 0..pop_out { 175 | buf.pop_front(); 176 | } 177 | } 178 | let mut tmp_buffers: Vec = Vec::with_capacity(buf.len()); 179 | // Remove duplicates from loaded history if we do not want it. 180 | while let Some(line) = buf.pop_back() { 181 | buf.retain(|buffer| *buffer != line); 182 | tmp_buffers.push(line); 183 | } 184 | while let Some(line) = tmp_buffers.pop() { 185 | buf.push_back(line); 186 | } 187 | 188 | if org_length != buf.len() { 189 | // Overwrite the history file with the deduplicated version if it changed. 190 | let mut file = BufWriter::new(File::create(path)?); 191 | // Write the commands to the history file. 192 | for command in buf.into_iter() { 193 | let _ = file.write_all(command.as_bytes()); 194 | let _ = file.write_all(b"\n"); 195 | } 196 | } 197 | Ok("De-duplicated history file.".to_string()) 198 | } 199 | 200 | /// Set history file name and at the same time load the history. 201 | pub fn set_file_name_and_load_history>(&mut self, path: P) -> io::Result { 202 | let path = path.as_ref(); 203 | self.file_name = path.to_str().map(|s| s.to_owned()); 204 | self.file_size = 0; 205 | if path.exists() { 206 | self.load_history_file(path, false).inspect(|&l| { 207 | self.file_size = l; 208 | }) 209 | } else { 210 | File::create(path)?; 211 | Ok(0) 212 | } 213 | } 214 | 215 | /// Set maximal number of buffers stored in memory 216 | pub fn set_max_buffers_size(&mut self, size: usize) { 217 | self.max_buffers_size = size; 218 | } 219 | 220 | /// Set maximal number of entries in history file 221 | pub fn set_max_file_size(&mut self, size: usize) { 222 | self.max_file_size = size; 223 | } 224 | 225 | /// Number of items in history. 226 | #[inline(always)] 227 | pub fn len(&self) -> usize { 228 | self.buffers.len() 229 | } 230 | 231 | /// Is the history empty 232 | pub fn is_empty(&self) -> bool { 233 | self.buffers.is_empty() 234 | } 235 | 236 | /// Add a command to the history buffer and remove the oldest commands when the max history 237 | /// size has been met. If writing to the disk is enabled, this function will be used for 238 | /// logging history to the designated history file. 239 | pub fn push(&mut self, new_item: Buffer) -> io::Result<()> { 240 | // buffers[0] is the oldest entry 241 | // the new entry goes to the end 242 | if !self.append_duplicate_entries 243 | && self.buffers.back().map(|b| b.to_string()) == Some(new_item.to_string()) 244 | { 245 | return Ok(()); 246 | } 247 | 248 | let item_str = String::from(new_item.clone()); 249 | self.buffers.push_back(new_item); 250 | //self.to_max_size(); 251 | while self.buffers.len() > self.max_buffers_size { 252 | self.buffers.pop_front(); 253 | } 254 | 255 | if self.inc_append && self.file_name.is_some() { 256 | if !self.load_duplicates { 257 | // Do not want duplicates so periodically compact the history file. 258 | self.compaction_writes += 1; 259 | // Every 30 writes "compact" the history file by writing just in memory history. This 260 | // is to keep the history file clean and at a reasonable size (not much over max 261 | // history size at it's worst). 262 | if self.compaction_writes > 29 { 263 | if self.share { 264 | // Reload history, we may be out of sync. 265 | let _ = self.load_history(false); 266 | // Commit the duplicated history. 267 | if let Some(file_name) = self.file_name.clone() { 268 | let _ = self.overwrite_history(file_name); 269 | } 270 | } else { 271 | // Not using shared history so just de-dup the file without messing with 272 | // our history. 273 | if let Some(file_name) = self.file_name.clone() { 274 | let _ = 275 | History::deduplicate_history_file(file_name, self.max_file_size); 276 | } 277 | } 278 | self.compaction_writes = 0; 279 | } 280 | } else { 281 | // If allowing duplicates then no need for compaction. 282 | self.compaction_writes = 1; 283 | } 284 | let file_name = self.file_name.clone().unwrap(); 285 | if let Ok(inner_file) = std::fs::OpenOptions::new().append(true).open(&file_name) { 286 | // Leave file size alone, if it is not right trigger a reload later. 287 | if self.compaction_writes > 0 { 288 | // If 0 we "compacted" and nothing to write. 289 | let mut file = BufWriter::new(inner_file); 290 | let _ = file.write_all(item_str.as_bytes()); 291 | let _ = file.write_all(b"\n"); 292 | // Save the filesize after each append so we do not reload when we do not need to. 293 | self.file_size += item_str.len() as u64 + 1; 294 | } 295 | } 296 | } 297 | Ok(()) 298 | } 299 | 300 | /// Removes duplicate entries in the history 301 | pub fn remove_duplicates(&mut self, input: &str) { 302 | self.buffers.retain(|buffer| { 303 | let command = buffer.lines().concat(); 304 | command != input 305 | }); 306 | } 307 | 308 | fn get_match(&self, vals: I, search_term: &Buffer) -> Option 309 | where 310 | I: Iterator, 311 | { 312 | vals.filter_map(|i| self.buffers.get(i).map(|t| (i, t))) 313 | .find(|(_i, tested)| tested.starts_with(search_term)) 314 | .map(|(i, _)| i) 315 | } 316 | 317 | /// Go through the history and try to find an index (newest to oldest) which starts the same 318 | /// as the new buffer given to this function as argument. Starts at curr_position. Does no wrap. 319 | pub fn get_newest_match( 320 | &self, 321 | curr_position: Option, 322 | new_buff: &Buffer, 323 | ) -> Option { 324 | let pos = curr_position.unwrap_or(self.buffers.len()); 325 | if pos > 0 { 326 | self.get_match((0..pos).rev(), new_buff) 327 | } else { 328 | None 329 | } 330 | } 331 | 332 | pub fn get_history_subset(&self, search_term: &Buffer) -> Vec { 333 | let mut v: Vec = Vec::new(); 334 | let mut ret: Vec = (0..self.len()) 335 | .filter(|i| { 336 | if let Some(tested) = self.buffers.get(*i) { 337 | let starts = tested.starts_with(search_term); 338 | let contains = tested.contains(search_term); 339 | if starts { 340 | v.push(*i); 341 | } 342 | contains && !starts && tested != search_term 343 | } else { 344 | false 345 | } 346 | }) 347 | .collect(); 348 | ret.append(&mut v); 349 | ret 350 | } 351 | 352 | pub fn search_index(&self, search_term: &Buffer) -> Vec { 353 | (0..self.len()) 354 | .filter_map(|i| self.buffers.get(i).map(|t| (i, t))) 355 | .filter(|(_i, tested)| tested.contains(search_term)) 356 | .map(|(i, _)| i) 357 | .collect() 358 | } 359 | 360 | /// Get the history file name. 361 | #[inline(always)] 362 | pub fn file_name(&self) -> Option<&str> { 363 | self.file_name.as_deref() 364 | } 365 | 366 | fn truncate(&mut self) { 367 | // Find how many lines we need to move backwards 368 | // in the file to remove all the old commands. 369 | if self.buffers.len() >= self.max_file_size { 370 | let pop_out = self.buffers.len() - self.max_file_size; 371 | for _ in 0..pop_out { 372 | self.buffers.pop_front(); 373 | } 374 | } 375 | } 376 | 377 | fn overwrite_history>(&mut self, path: P) -> io::Result { 378 | self.truncate(); 379 | let mut file = BufWriter::new(File::create(&path)?); 380 | 381 | // Write the commands to the history file. 382 | for command in self.buffers.iter().cloned() { 383 | let _ = file.write_all(String::from(command).as_bytes()); 384 | let _ = file.write_all(b"\n"); 385 | } 386 | Ok("Wrote history to file.".to_string()) 387 | } 388 | 389 | pub fn commit_to_file_path>(&mut self, path: P) -> io::Result { 390 | if self.inc_append { 391 | Ok("Nothing to commit.".to_string()) 392 | } else { 393 | self.overwrite_history(path) 394 | } 395 | } 396 | 397 | pub fn commit_to_file(&mut self) { 398 | if let Some(file_name) = self.file_name.clone() { 399 | let _ = self.commit_to_file_path(file_name); 400 | } 401 | } 402 | } 403 | 404 | impl<'a> IntoIterator for &'a History { 405 | type Item = &'a Buffer; 406 | type IntoIter = vec_deque::Iter<'a, Buffer>; 407 | 408 | fn into_iter(self) -> Self::IntoIter { 409 | self.buffers.iter() 410 | } 411 | } 412 | 413 | impl Index for History { 414 | type Output = Buffer; 415 | 416 | fn index(&self, index: usize) -> &Buffer { 417 | &self.buffers[index] 418 | } 419 | } 420 | 421 | impl IndexMut for History { 422 | fn index_mut(&mut self, index: usize) -> &mut Buffer { 423 | &mut self.buffers[index] 424 | } 425 | } 426 | -------------------------------------------------------------------------------- /src/keymap/emacs.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, Write}; 2 | use termion::event::Key; 3 | 4 | use crate::CursorPosition; 5 | use crate::Editor; 6 | use crate::KeyMap; 7 | 8 | /// Emacs keybindings for `Editor`. This is the default for `Context::read_line()`. 9 | /// 10 | /// ``` 11 | /// use liner::*; 12 | /// let mut context = Context::new(); 13 | /// context.key_bindings = KeyBindings::Emacs; 14 | /// ``` 15 | #[derive(Default)] 16 | pub struct Emacs { 17 | last_arg_fetch_index: Option, 18 | } 19 | 20 | impl Emacs { 21 | pub fn new() -> Self { 22 | Self::default() 23 | } 24 | 25 | fn handle_ctrl_key(&mut self, c: char, ed: &mut Editor<'_, W>) -> io::Result<()> { 26 | match c { 27 | 'l' => ed.clear(), 28 | 'a' => ed.move_cursor_to_start_of_line(), 29 | 'e' => ed.move_cursor_to_end_of_line(), 30 | 'b' => ed.move_cursor_left(1), 31 | 'f' => ed.move_cursor_right(1), 32 | 'd' => ed.delete_after_cursor(), 33 | 'p' => ed.move_up(), 34 | 'n' => ed.move_down(), 35 | 'u' => ed.delete_all_before_cursor(), 36 | 'k' => ed.delete_all_after_cursor(), 37 | 'w' => ed.delete_word_before_cursor(true), 38 | 'x' => { 39 | ed.undo()?; 40 | Ok(()) 41 | } 42 | _ => Ok(()), 43 | } 44 | } 45 | 46 | fn handle_alt_key(&mut self, c: char, ed: &mut Editor<'_, W>) -> io::Result<()> { 47 | match c { 48 | '<' => ed.move_to_start_of_history(), 49 | '>' => ed.move_to_end_of_history(), 50 | '\x7F' => ed.delete_word_before_cursor(true), 51 | 'f' => emacs_move_word(ed, EmacsMoveDir::Right), 52 | 'b' => emacs_move_word(ed, EmacsMoveDir::Left), 53 | 'r' => { 54 | ed.revert()?; 55 | Ok(()) 56 | } 57 | '.' => self.handle_last_arg_fetch(ed), 58 | _ => Ok(()), 59 | } 60 | } 61 | 62 | fn handle_last_arg_fetch(&mut self, ed: &mut Editor<'_, W>) -> io::Result<()> { 63 | // Empty history means no last arg to fetch. 64 | if ed.context().history.is_empty() { 65 | return Ok(()); 66 | } 67 | 68 | let history_index = match self.last_arg_fetch_index { 69 | Some(0) => return Ok(()), 70 | Some(x) => x - 1, 71 | None => ed 72 | .current_history_location() 73 | .unwrap_or(ed.context().history.len() - 1), 74 | }; 75 | 76 | // If did a last arg fetch just before this, we need to delete it so it can be replaced by 77 | // this last arg fetch. 78 | if self.last_arg_fetch_index.is_some() { 79 | let buffer_len = ed.current_buffer().num_chars(); 80 | if let Some(last_arg_len) = ed.current_buffer().last_arg().map(|x| x.len()) { 81 | ed.delete_until(buffer_len - last_arg_len)?; 82 | } 83 | } 84 | 85 | // Actually insert it 86 | let buf = ed.context().history[history_index].clone(); 87 | if let Some(last_arg) = buf.last_arg() { 88 | ed.insert_chars_after_cursor(last_arg)?; 89 | } 90 | 91 | // Edit the index in case the user does a last arg fetch again. 92 | self.last_arg_fetch_index = Some(history_index); 93 | 94 | Ok(()) 95 | } 96 | } 97 | 98 | impl KeyMap for Emacs { 99 | fn handle_key_core(&mut self, key: Key, ed: &mut Editor<'_, W>) -> io::Result<()> { 100 | match key { 101 | Key::Alt('.') => {} 102 | _ => self.last_arg_fetch_index = None, 103 | } 104 | 105 | match key { 106 | Key::Char(c) => ed.insert_after_cursor(c), 107 | Key::Alt(c) => self.handle_alt_key(c, ed), 108 | Key::Ctrl(c) => self.handle_ctrl_key(c, ed), 109 | Key::Left => ed.move_cursor_left(1), 110 | Key::Right => ed.move_cursor_right(1), 111 | Key::Up => ed.move_up(), 112 | Key::Down => ed.move_down(), 113 | Key::Home => ed.move_cursor_to_start_of_line(), 114 | Key::End => ed.move_cursor_to_end_of_line(), 115 | Key::Backspace => ed.delete_before_cursor(), 116 | Key::Delete => ed.delete_after_cursor(), 117 | Key::Null => Ok(()), 118 | _ => Ok(()), 119 | } 120 | } 121 | } 122 | 123 | #[derive(PartialEq, Clone, Copy)] 124 | enum EmacsMoveDir { 125 | Left, 126 | Right, 127 | } 128 | 129 | fn emacs_move_word(ed: &mut Editor, direction: EmacsMoveDir) -> io::Result<()> { 130 | let (words, pos) = ed.get_words_and_cursor_position(); 131 | 132 | let word_index = match pos { 133 | CursorPosition::InWord(i) => Some(i), 134 | CursorPosition::OnWordLeftEdge(mut i) => { 135 | if i > 0 && direction == EmacsMoveDir::Left { 136 | i -= 1; 137 | } 138 | Some(i) 139 | } 140 | CursorPosition::OnWordRightEdge(mut i) => { 141 | if i < words.len() - 1 && direction == EmacsMoveDir::Right { 142 | i += 1; 143 | } 144 | Some(i) 145 | } 146 | CursorPosition::InSpace(left, right) => match direction { 147 | EmacsMoveDir::Left => left, 148 | EmacsMoveDir::Right => right, 149 | }, 150 | }; 151 | 152 | match word_index { 153 | None => Ok(()), 154 | Some(i) => { 155 | let (start, end) = words[i]; 156 | 157 | let new_cursor_pos = match direction { 158 | EmacsMoveDir::Left => start, 159 | EmacsMoveDir::Right => end, 160 | }; 161 | 162 | ed.move_cursor_to(new_cursor_pos) 163 | } 164 | } 165 | } 166 | 167 | #[cfg(test)] 168 | mod tests { 169 | use super::*; 170 | use crate::editor::Prompt; 171 | use crate::{Completer, Context, Editor, KeyMap}; 172 | use std::io::Write; 173 | use termion::event::Key; 174 | 175 | fn simulate_keys<'a, 'b, W: Write, M: KeyMap, I>( 176 | keymap: &mut M, 177 | ed: &mut Editor<'a, W>, 178 | keys: I, 179 | ) -> bool 180 | where 181 | I: IntoIterator, 182 | { 183 | for k in keys { 184 | if keymap.handle_key(*k, ed, &mut EmptyCompleter).unwrap() { 185 | return true; 186 | } 187 | } 188 | 189 | false 190 | } 191 | 192 | struct EmptyCompleter; 193 | 194 | impl Completer for EmptyCompleter { 195 | fn completions(&mut self, _start: &str) -> Vec { 196 | Vec::default() 197 | } 198 | } 199 | 200 | #[test] 201 | fn enter_is_done() { 202 | let mut context = Context::new(); 203 | let out = Vec::new(); 204 | let mut ed = Editor::new(out, Prompt::from("prompt"), None, &mut context).unwrap(); 205 | let mut map = Emacs::new(); 206 | ed.insert_str_after_cursor("done").unwrap(); 207 | assert_eq!(ed.cursor(), 4); 208 | 209 | assert!(simulate_keys(&mut map, &mut ed, [Key::Char('\n')].iter())); 210 | 211 | assert_eq!(ed.cursor(), 4); 212 | assert_eq!(String::from(ed), "done"); 213 | } 214 | 215 | #[test] 216 | fn move_cursor_left() { 217 | let mut context = Context::new(); 218 | let out = Vec::new(); 219 | let mut ed = Editor::new(out, Prompt::from("prompt"), None, &mut context).unwrap(); 220 | let mut map = Emacs::new(); 221 | ed.insert_str_after_cursor("let").unwrap(); 222 | assert_eq!(ed.cursor(), 3); 223 | 224 | simulate_keys(&mut map, &mut ed, [Key::Left, Key::Char('f')].iter()); 225 | 226 | assert_eq!(ed.cursor(), 3); 227 | assert_eq!(String::from(ed), "left"); 228 | } 229 | 230 | #[test] 231 | fn move_word() { 232 | let mut context = Context::new(); 233 | let out = Vec::new(); 234 | 235 | let mut ed = Editor::new(out, Prompt::from("prompt"), None, &mut context).unwrap(); 236 | let mut map = Emacs::new(); 237 | ed.insert_str_after_cursor("abc def ghi").unwrap(); 238 | assert_eq!(ed.cursor(), 11); 239 | 240 | simulate_keys(&mut map, &mut ed, [Key::Alt('b')].iter()); 241 | 242 | // Move to `g` 243 | assert_eq!(ed.cursor(), 8); 244 | 245 | simulate_keys(&mut map, &mut ed, [Key::Alt('b'), Key::Alt('f')].iter()); 246 | 247 | // Move to the char after `f` 248 | assert_eq!(ed.cursor(), 7); 249 | } 250 | 251 | #[test] 252 | fn cursor_movement() { 253 | let mut context = Context::new(); 254 | let out = Vec::new(); 255 | let mut ed = Editor::new(out, Prompt::from("prompt"), None, &mut context).unwrap(); 256 | let mut map = Emacs::new(); 257 | ed.insert_str_after_cursor("right").unwrap(); 258 | assert_eq!(ed.cursor(), 5); 259 | 260 | simulate_keys(&mut map, &mut ed, [Key::Left, Key::Left, Key::Right].iter()); 261 | 262 | assert_eq!(ed.cursor(), 4); 263 | } 264 | 265 | #[test] 266 | /// ctrl-h should act as backspace 267 | fn ctrl_h() { 268 | let mut context = Context::new(); 269 | let out = Vec::new(); 270 | let mut ed = Editor::new(out, Prompt::from("prompt"), None, &mut context).unwrap(); 271 | let mut map = Emacs::new(); 272 | ed.insert_str_after_cursor("not empty").unwrap(); 273 | 274 | let res = map.handle_key(Key::Ctrl('h'), &mut ed, &mut EmptyCompleter); 275 | assert_eq!(res.is_ok(), true); 276 | assert_eq!(ed.current_buffer().to_string(), "not empt".to_string()); 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /src/keymap/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::complete::Completer; 2 | use crate::event::*; 3 | use crate::Editor; 4 | use std::io::{self, ErrorKind, Write}; 5 | use termion::event::Key; 6 | 7 | pub trait KeyMap: Default { 8 | fn handle_key_core(&mut self, key: Key, editor: &mut Editor<'_, W>) 9 | -> io::Result<()>; 10 | 11 | fn init(&mut self, _editor: &mut Editor<'_, W>) {} 12 | 13 | fn handle_key( 14 | &mut self, 15 | mut key: Key, 16 | editor: &mut Editor<'_, W>, 17 | handler: &mut C, 18 | ) -> io::Result { 19 | let mut done = false; 20 | 21 | handler.on_event(Event::new(editor, EventKind::BeforeKey(key))); 22 | 23 | let is_empty = editor.current_buffer().is_empty(); 24 | 25 | if key == Key::Ctrl('h') { 26 | // XXX: Might need to change this when remappable keybindings are added. 27 | key = Key::Backspace; 28 | } 29 | 30 | match key { 31 | Key::Ctrl('c') => { 32 | editor.handle_newline()?; 33 | return Err(io::Error::new(ErrorKind::Interrupted, "ctrl-c")); 34 | } 35 | // if the current buffer is empty, treat ctrl-d as eof 36 | Key::Ctrl('d') if is_empty => { 37 | editor.handle_newline()?; 38 | return Err(io::Error::new(ErrorKind::UnexpectedEof, "ctrl-d")); 39 | } 40 | Key::Char('\t') => editor.complete(handler)?, 41 | Key::Char('\n') => { 42 | done = editor.handle_newline()?; 43 | } 44 | Key::Ctrl('f') if editor.is_currently_showing_autosuggestion() => { 45 | editor.accept_autosuggestion()?; 46 | } 47 | Key::Ctrl('r') => { 48 | editor.search(false)?; 49 | } 50 | Key::Ctrl('s') => { 51 | editor.search(true)?; 52 | } 53 | Key::Right 54 | if editor.is_currently_showing_autosuggestion() 55 | && editor.cursor_is_at_end_of_line() => 56 | { 57 | editor.accept_autosuggestion()?; 58 | } 59 | _ => { 60 | self.handle_key_core(key, editor)?; 61 | editor.skip_completions_hint(); 62 | } 63 | }; 64 | 65 | handler.on_event(Event::new(editor, EventKind::AfterKey(key))); 66 | 67 | editor.flush()?; 68 | 69 | Ok(done) 70 | } 71 | } 72 | 73 | pub mod vi; 74 | pub use vi::Vi; 75 | 76 | pub mod emacs; 77 | pub use emacs::Emacs; 78 | 79 | #[cfg(test)] 80 | mod tests { 81 | use super::*; 82 | use crate::editor::Prompt; 83 | use crate::Context; 84 | use std::io::ErrorKind; 85 | use termion::event::Key::*; 86 | 87 | #[derive(Default)] 88 | struct TestKeyMap; 89 | 90 | impl KeyMap for TestKeyMap { 91 | fn handle_key_core(&mut self, _: Key, _: &mut Editor<'_, W>) -> io::Result<()> { 92 | Ok(()) 93 | } 94 | } 95 | 96 | struct EmptyCompleter; 97 | 98 | impl Completer for EmptyCompleter { 99 | fn completions(&mut self, _start: &str) -> Vec { 100 | Vec::default() 101 | } 102 | } 103 | 104 | #[test] 105 | /// when the current buffer is empty, ctrl-d generates and eof error 106 | fn ctrl_d_empty() { 107 | let mut context = Context::new(); 108 | let out = Vec::new(); 109 | let mut ed = Editor::new(out, Prompt::from("prompt"), None, &mut context).unwrap(); 110 | let mut map = TestKeyMap; 111 | 112 | let res = map.handle_key(Ctrl('d'), &mut ed, &mut EmptyCompleter); 113 | assert_eq!(res.is_err(), true); 114 | assert_eq!(res.err().unwrap().kind(), ErrorKind::UnexpectedEof); 115 | } 116 | 117 | #[test] 118 | /// when the current buffer is not empty, ctrl-d should be ignored 119 | fn ctrl_d_non_empty() { 120 | let mut context = Context::new(); 121 | let out = Vec::new(); 122 | let mut ed = Editor::new(out, Prompt::from("prompt"), None, &mut context).unwrap(); 123 | let mut map = TestKeyMap; 124 | ed.insert_str_after_cursor("not empty").unwrap(); 125 | 126 | let res = map.handle_key(Ctrl('d'), &mut ed, &mut EmptyCompleter); 127 | assert_eq!(res.is_ok(), true); 128 | } 129 | 130 | #[test] 131 | /// ctrl-c should generate an error 132 | fn ctrl_c() { 133 | let mut context = Context::new(); 134 | let out = Vec::new(); 135 | let mut ed = Editor::new(out, Prompt::from("prompt"), None, &mut context).unwrap(); 136 | let mut map = TestKeyMap; 137 | 138 | let res = map.handle_key(Ctrl('c'), &mut ed, &mut EmptyCompleter); 139 | assert_eq!(res.is_err(), true); 140 | assert_eq!(res.err().unwrap().kind(), ErrorKind::Interrupted); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate bytecount; 2 | extern crate itertools; 3 | extern crate termion; 4 | extern crate unicode_width; 5 | 6 | mod event; 7 | pub use event::*; 8 | 9 | mod editor; 10 | pub use editor::*; 11 | 12 | mod complete; 13 | pub use complete::*; 14 | 15 | mod context; 16 | pub use context::*; 17 | 18 | mod buffer; 19 | pub use buffer::*; 20 | 21 | mod history; 22 | pub use history::*; 23 | 24 | mod keymap; 25 | pub use keymap::*; 26 | 27 | mod util; 28 | 29 | #[cfg(test)] 30 | mod test; 31 | -------------------------------------------------------------------------------- /src/test.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | use std::env; 4 | use std::fs; 5 | use std::io::{BufRead, BufReader, Write}; 6 | 7 | fn assert_cursor_pos(s: &str, cursor: usize, expected_pos: CursorPosition) { 8 | let buf = Buffer::from(s.to_owned()); 9 | let words = get_buffer_words(&buf); 10 | let pos = CursorPosition::get(cursor, &words[..]); 11 | assert_eq!( 12 | expected_pos, pos, 13 | "buffer: {:?}, cursor: {}, expected pos: {:?}, pos: {:?}", 14 | s, cursor, expected_pos, pos 15 | ); 16 | } 17 | 18 | #[test] 19 | fn test_get_cursor_position() { 20 | use CursorPosition::*; 21 | 22 | let tests = &[ 23 | ("hi", 0, OnWordLeftEdge(0)), 24 | ("hi", 1, InWord(0)), 25 | ("hi", 2, OnWordRightEdge(0)), 26 | ("abc abc", 4, InSpace(Some(0), Some(1))), 27 | ("abc abc", 5, OnWordLeftEdge(1)), 28 | ("abc abc", 6, InWord(1)), 29 | ("abc abc", 8, OnWordRightEdge(1)), 30 | (" a", 0, InSpace(None, Some(0))), 31 | ("a ", 2, InSpace(Some(0), None)), 32 | ]; 33 | 34 | for t in tests { 35 | assert_cursor_pos(t.0, t.1, t.2); 36 | } 37 | } 38 | 39 | fn assert_buffer_actions(start: &str, expected: &str, actions: &[Action]) { 40 | let mut buf = Buffer::from(start.to_owned()); 41 | for a in actions { 42 | a.do_on(&mut buf); 43 | } 44 | 45 | assert_eq!(expected, String::from(buf)); 46 | } 47 | 48 | #[test] 49 | fn test_buffer_actions() { 50 | assert_buffer_actions( 51 | "", 52 | "h", 53 | &[ 54 | Action::Insert { 55 | start: 0, 56 | text: "hi".chars().collect(), 57 | }, 58 | Action::Remove { 59 | start: 1, 60 | text: ".".chars().collect(), 61 | }, 62 | ], 63 | ); 64 | } 65 | 66 | #[test] 67 | fn test_history_indexing() { 68 | let mut h = History::new(); 69 | h.push(Buffer::from("a")).unwrap(); 70 | h.push(Buffer::from("b")).unwrap(); 71 | h.push(Buffer::from("c")).unwrap(); 72 | assert_eq!(h.len(), 3); 73 | assert_eq!(String::from(h.buffers[0].clone()), "a".to_string()); 74 | assert_eq!(String::from(h.buffers[1].clone()), "b".to_string()); 75 | assert_eq!(String::from(h.buffers[2].clone()), "c".to_string()); 76 | } 77 | 78 | #[test] 79 | fn test_in_memory_history_truncating() { 80 | let mut h = History::new(); 81 | h.set_max_buffers_size(2); 82 | for _ in 0..4 { 83 | h.push(Buffer::from("a")).unwrap(); 84 | h.push(Buffer::from("b")).unwrap(); 85 | } 86 | h.commit_to_file(); 87 | assert_eq!(h.len(), 2); 88 | } 89 | 90 | #[test] 91 | fn test_in_file_history_truncating() { 92 | let mut tmp_file = env::temp_dir(); 93 | tmp_file.push("liner_test_file123.txt"); 94 | 95 | { 96 | let mut h = History::new(); 97 | let _ = h.set_file_name_and_load_history(&tmp_file).unwrap(); 98 | h.set_max_file_size(5); 99 | for bytes in b'a'..b'z' { 100 | h.push(Buffer::from(format!("{}", bytes as char))).unwrap(); 101 | } 102 | h.commit_to_file(); 103 | } 104 | 105 | let f = fs::File::open(&tmp_file).unwrap(); 106 | let r = BufReader::new(f); 107 | let count = r.lines().count(); 108 | assert_eq!(count, 5); 109 | 110 | fs::remove_file(tmp_file).unwrap(); 111 | } 112 | 113 | static TEXT: &'static str = "a 114 | b 115 | c 116 | d 117 | "; 118 | 119 | #[test] 120 | fn test_reading_from_file() { 121 | let mut tmp_file = env::temp_dir(); 122 | tmp_file.push("liner_test_file456.txt"); 123 | { 124 | let mut f = fs::OpenOptions::new() 125 | .read(true) 126 | .write(true) 127 | .create(true) 128 | .open(tmp_file.clone()) 129 | .unwrap(); 130 | f.write_all(TEXT.as_bytes()).unwrap(); 131 | } 132 | let mut h = History::new(); 133 | h.set_file_name_and_load_history(tmp_file).unwrap(); 134 | assert_eq!(String::from(h.buffers[0].clone()), "a".to_string()); 135 | assert_eq!(String::from(h.buffers[1].clone()), "b".to_string()); 136 | assert_eq!(String::from(h.buffers[2].clone()), "c".to_string()); 137 | assert_eq!(String::from(h.buffers[3].clone()), "d".to_string()); 138 | } 139 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, io}; 2 | use unicode_width::*; 3 | 4 | pub fn last_prompt_line_width>(s: S) -> usize { 5 | let last_prompt_line_width = handle_prompt(s.as_ref()); 6 | remove_codes(last_prompt_line_width).width() 7 | } 8 | 9 | pub fn find_longest_common_prefix(among: &[Vec]) -> Option> { 10 | if among.is_empty() { 11 | return None; 12 | } else if among.len() == 1 { 13 | return Some(among[0].clone()); 14 | } 15 | 16 | for s in among { 17 | if s.is_empty() { 18 | return None; 19 | } 20 | } 21 | 22 | let shortest_word = among.iter().min_by_key(|x| x.len()).unwrap(); 23 | 24 | let mut end = shortest_word.len(); 25 | while end > 0 { 26 | let prefix = &shortest_word[..end]; 27 | 28 | let mut failed = false; 29 | for s in among { 30 | if !s.starts_with(prefix) { 31 | failed = true; 32 | break; 33 | } 34 | } 35 | 36 | if !failed { 37 | return Some(prefix.into()); 38 | } 39 | 40 | end -= 1; 41 | } 42 | 43 | None 44 | } 45 | 46 | pub enum AnsiState { 47 | Norm, 48 | Esc, 49 | Csi, 50 | Osc, 51 | } 52 | 53 | pub fn remove_codes(input: &str) -> Cow { 54 | if input.contains('\x1B') { 55 | let mut clean = String::new(); 56 | 57 | let mut s = AnsiState::Norm; 58 | for c in input.chars() { 59 | match s { 60 | AnsiState::Norm => match c { 61 | '\x1B' => s = AnsiState::Esc, 62 | _ => clean.push(c), 63 | }, 64 | AnsiState::Esc => match c { 65 | '[' => s = AnsiState::Csi, 66 | ']' => s = AnsiState::Osc, 67 | _ => s = AnsiState::Norm, 68 | }, 69 | AnsiState::Csi if c.is_ascii_alphabetic() => s = AnsiState::Norm, 70 | AnsiState::Osc if c == '\x07' => s = AnsiState::Norm, 71 | _ => (), 72 | } 73 | } 74 | 75 | Cow::Owned(clean) 76 | } else { 77 | Cow::Borrowed(input) 78 | } 79 | } 80 | 81 | /// Returns the last prompt line. 82 | pub fn handle_prompt(full_prompt: &str) -> &str { 83 | if let Some(index) = full_prompt.rfind('\n') { 84 | let (_, prompt) = full_prompt.split_at(index + 1); 85 | prompt 86 | } else { 87 | full_prompt 88 | } 89 | } 90 | 91 | pub fn terminal_width() -> io::Result { 92 | if cfg!(test) { 93 | Ok(80) 94 | } else { 95 | let (mut size_col, _) = ::termion::terminal_size()?; 96 | if size_col == 0 { 97 | size_col = 80; 98 | } 99 | Ok(size_col as usize) 100 | } 101 | } 102 | --------------------------------------------------------------------------------