From 44b7e07569edf6259a656166b203052400071bbe Mon Sep 17 00:00:00 2001 From: Pirmin Kalberer Date: Sun, 15 Sep 2019 23:05:13 +0200 Subject: [PATCH 1/7] Add Sublime style history search demo --- src/histsearch.rs | 171 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 src/histsearch.rs diff --git a/src/histsearch.rs b/src/histsearch.rs new file mode 100644 index 0000000000..d5db186809 --- /dev/null +++ b/src/histsearch.rs @@ -0,0 +1,171 @@ +use ansi_term::Colour; +use crossterm::{cursor, terminal, ClearType, InputEvent, KeyEvent, RawScreen}; +use std::io::Write; +use sublime_fuzzy::best_match; + +fn select_from_list(lines: &Vec<&str>) { + const MAX_RESULTS: usize = 5; + #[derive(PartialEq)] + enum State { + Selecting, + Quit, + Selected(usize), + Edit(usize), + } + let mut state = State::Selecting; + if let Ok(_raw) = RawScreen::into_raw_mode() { + // User input for search + let mut searchinput = String::new(); + let mut selected = 0; + + let mut cursor = cursor(); + let _ = cursor.hide(); + let input = crossterm::input(); + let mut sync_stdin = input.read_sync(); + + while state == State::Selecting { + let search_result = search(&searchinput, &lines, MAX_RESULTS); + let selected_lines: Vec<&str> = search_result + .iter() + .map(|item| &item.highlighted_text as &str) + .collect(); + paint_selection_list(&selected_lines, selected); + if let Some(ev) = sync_stdin.next() { + match ev { + InputEvent::Keyboard(k) => match k { + KeyEvent::Esc | KeyEvent::Ctrl('c') => { + state = State::Quit; + } + KeyEvent::Up => { + if selected > 0 { + selected -= 1; + } + } + KeyEvent::Down => { + if selected + 1 < selected_lines.len() { + selected += 1; + } + } + KeyEvent::Char('\n') => { + state = State::Selected(search_result[selected].text_idx); + } + KeyEvent::Char('\t') | KeyEvent::Right => { + state = State::Edit(search_result[selected].text_idx); + } + KeyEvent::Char(ch) => { + searchinput.push(ch); + selected = 0; + } + KeyEvent::Backspace => { + searchinput.pop(); + selected = 0; + } + _ => { + // println!("{}", format!("OTHER InputEvent: {:?}\n\n", k)); + } + }, + _ => {} + } + } + cursor.move_up(selected_lines.len() as u16); + } + let (_x, y) = cursor.pos(); + let _ = cursor.goto(0, y); + let _ = cursor.show(); + + let _ = RawScreen::disable_raw_mode(); + } + let terminal = terminal(); + terminal.clear(ClearType::FromCursorDown).unwrap(); + + match state { + State::Selected(idx) => { + print!("{}", lines[idx]); + } + State::Edit(idx) => { + print!("{}", lines[idx]); + } + _ => {} + } +} + +struct Match { + highlighted_text: String, + text_idx: usize, +} + +fn search(input: &String, lines: &Vec<&str>, max_results: usize) -> Vec { + if input.is_empty() { + return lines + .iter() + .take(max_results) + .enumerate() + .map(|(i, line)| Match { + highlighted_text: line.to_string(), + text_idx: i, + }) + .collect(); + } + + let mut matches = lines + .iter() + .enumerate() + .map(|(idx, line)| (idx, best_match(&input, line))) + .filter(|(_i, m)| m.is_some()) + .map(|(i, m)| (i, m.unwrap())) + .collect::>(); + matches.sort_by(|a, b| b.1.score().cmp(&a.1.score())); + + let highlight = Colour::Cyan; + let results: Vec = matches + .iter() + .take(max_results) + .map(|(i, m)| { + let r = &lines[*i]; + let mut outstr = String::with_capacity(r.len()); + let mut idx = 0; + for (match_idx, len) in m.continuous_matches() { + outstr.push_str(&r[idx..match_idx]); + idx = match_idx + len; + outstr.push_str(&format!("{}", highlight.paint(&r[match_idx..idx]))); + } + if idx < r.len() { + outstr.push_str(&r[idx..r.len()]); + } + Match { + highlighted_text: outstr, + text_idx: *i, + } + }) + .collect(); + results +} + +fn paint_selection_list(lines: &Vec<&str>, selected: usize) { + let dimmed = Colour::White.dimmed(); + let cursor = cursor(); + let (_x, y) = cursor.pos(); + for (i, line) in lines.iter().enumerate() { + let _ = cursor.goto(0, y + (i as u16)); + if selected == i { + println!("{}", *line); + } else { + println!("{}", dimmed.paint(*line)); + } + } + let _ = cursor.goto(0, y + (lines.len() as u16)); + print!( + "{}", + Colour::Blue.paint("[ESC to quit, Enter to execute, Tab to edit]") + ); + + let _ = std::io::stdout().flush(); + // Clear additional lines from previous selection + terminal().clear(ClearType::FromCursorDown).unwrap(); +} + +fn main() { + let hist = std::fs::read_to_string("history.txt").expect("Cannot open history.txt"); + let lines = hist.lines().rev().collect(); + select_from_list(&lines); +} From 1e3549571c41eb9ad4bcb7e1a8a1eca323f6b6b3 Mon Sep 17 00:00:00 2001 From: Pirmin Kalberer Date: Mon, 16 Sep 2019 22:48:22 +0200 Subject: [PATCH 2/7] Bind fuzzy history search to Ctrl-R --- Cargo.lock | 7 +++++++ Cargo.toml | 1 + src/cli.rs | 16 +++++++++++++++- src/histsearch.rs | 8 +------- src/lib.rs | 1 + 5 files changed, 25 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 075a712032..45ab8ebe26 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1566,6 +1566,7 @@ dependencies = [ "serde_urlencoded 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", "serde_yaml 0.8.9 (registry+https://github.com/rust-lang/crates.io-index)", "shellexpand 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "sublime_fuzzy 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", "subprocess 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)", "surf 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", "syntect 3.2.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2443,6 +2444,11 @@ name = "strsim" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "sublime_fuzzy" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "subprocess" version = "0.1.18" @@ -3247,6 +3253,7 @@ dependencies = [ "checksum stackvector 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "1c4725650978235083241fab0fdc8e694c3de37821524e7534a1a9061d1068af" "checksum static_assertions 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "b4f8de36da215253eb5f24020bfaa0646613b48bf7ebe36cdfa37c3b3b33b241" "checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +"checksum sublime_fuzzy 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "97bd7ad698ea493a3a7f60c2ffa117c234f341e09f8cc2d39cef10cdde077acf" "checksum subprocess 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)" = "28fc0f40f0c0da73339d347aa7d6d2b90341a95683a47722bc4eebed71ff3c00" "checksum surf 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "018eed64aede455beb88505d50c5c64882bebbe0996d4b660c272e3d8bb6f883" "checksum syn 0.15.43 (registry+https://github.com/rust-lang/crates.io-index)" = "ee06ea4b620ab59a2267c6b48be16244a3389f8bfa0986bdd15c35b890b00af3" diff --git a/Cargo.toml b/Cargo.toml index cfe107e9be..e81bc0ee69 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,6 +73,7 @@ num-bigint = { version = "0.2.3", features = ["serde"] } bigdecimal = { version = "0.1.0", features = ["serde"] } natural = "0.3.0" serde_urlencoded = "0.6.1" +sublime_fuzzy = "0.5" neso = { version = "0.5.0", optional = true } crossterm = { version = "0.10.2", optional = true } diff --git a/src/cli.rs b/src/cli.rs index 8ed2b9bd55..af5a02a313 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -10,6 +10,7 @@ use crate::context::Context; use crate::data::Value; pub(crate) use crate::errors::ShellError; use crate::git::current_branch; +use crate::histsearch; use crate::parser::registry::Signature; use crate::parser::{hir, CallNode, Pipeline, PipelineElement, TokenNode}; use crate::prelude::*; @@ -333,6 +334,12 @@ pub async fn cli() -> Result<(), Box> { rl.set_edit_mode(edit_mode); + // Register Ctrl-r for history fuzzy search + // rustyline doesn't support custom commands, so we override Ctrl-D (EOF) + rl.bind_sequence(rustyline::KeyPress::Ctrl('R'), rustyline::Cmd::EndOfFile); + // Redefine Ctrl-D to same command as Ctrl-C + rl.bind_sequence(rustyline::KeyPress::Ctrl('D'), rustyline::Cmd::Interrupt); + let readline = rl.readline(&format!( "{}{}> ", cwd, @@ -347,6 +354,12 @@ pub async fn cli() -> Result<(), Box> { rl.add_history_entry(line.clone()); } + LineResult::SearchHist => { + let hist = std::fs::read_to_string("history.txt").expect("Cannot open history.txt"); + let lines = hist.lines().rev().collect(); + histsearch::select_from_list(&lines); + } + LineResult::CtrlC => { if ctrlcbreak { std::process::exit(0); @@ -390,6 +403,7 @@ pub async fn cli() -> Result<(), Box> { enum LineResult { Success(String), + SearchHist, Error(String, ShellError), CtrlC, Break, @@ -517,7 +531,7 @@ async fn process_line(readline: Result, ctx: &mut Context LineResult::Success(line.clone()) } Err(ReadlineError::Interrupted) => LineResult::CtrlC, - Err(ReadlineError::Eof) => LineResult::Break, + Err(ReadlineError::Eof) => LineResult::SearchHist, // Override LineResult::Break Err(err) => { println!("Error: {:?}", err); LineResult::Break diff --git a/src/histsearch.rs b/src/histsearch.rs index d5db186809..495b0d5a4e 100644 --- a/src/histsearch.rs +++ b/src/histsearch.rs @@ -3,7 +3,7 @@ use crossterm::{cursor, terminal, ClearType, InputEvent, KeyEvent, RawScreen}; use std::io::Write; use sublime_fuzzy::best_match; -fn select_from_list(lines: &Vec<&str>) { +pub fn select_from_list(lines: &Vec<&str>) { const MAX_RESULTS: usize = 5; #[derive(PartialEq)] enum State { @@ -163,9 +163,3 @@ fn paint_selection_list(lines: &Vec<&str>, selected: usize) { // Clear additional lines from previous selection terminal().clear(ClearType::FromCursorDown).unwrap(); } - -fn main() { - let hist = std::fs::read_to_string("history.txt").expect("Cannot open history.txt"); - let lines = hist.lines().rev().collect(); - select_from_list(&lines); -} diff --git a/src/lib.rs b/src/lib.rs index 1aedc2e11f..9802e2f686 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,6 +13,7 @@ mod errors; mod evaluate; mod format; mod git; +mod histsearch; mod parser; mod plugin; mod shell; From 1c95bf05dc134aac493d1c7ce8a99de3d458561b Mon Sep 17 00:00:00 2001 From: Pirmin Kalberer Date: Wed, 18 Sep 2019 00:21:39 +0200 Subject: [PATCH 3/7] Process selected command --- src/cli.rs | 40 ++++++++++++++++++++++++++++++---------- src/histsearch.rs | 20 +++++++++++--------- 2 files changed, 41 insertions(+), 19 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index af5a02a313..5cfcb1475a 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -340,26 +340,47 @@ pub async fn cli() -> Result<(), Box> { // Redefine Ctrl-D to same command as Ctrl-C rl.bind_sequence(rustyline::KeyPress::Ctrl('D'), rustyline::Cmd::Interrupt); - let readline = rl.readline(&format!( + let prompt = &format!( "{}{}> ", cwd, match current_branch() { Some(s) => format!("({})", s), None => "".to_string(), } - )); + ); + let mut initial_command = Some(String::new()); + let mut readline = Err(ReadlineError::Eof); + while let Some(ref cmd) = initial_command { + readline = rl.readline_with_initial(prompt, (&cmd, "")); + if let Err(ReadlineError::Eof) = &readline { + // Fuzzy search in history + let hist = std::fs::read_to_string("history.txt").expect("Cannot open history.txt"); + let lines = hist.lines().rev().collect(); + let selection = histsearch::select_from_list(&lines); // Clears last line with prompt + match selection { + histsearch::SelectionResult::Selected(line) => { + println!("{}{}", &prompt, &line); // TODO: colorize prompt + readline = Ok(line.clone()); + initial_command = None; + } + histsearch::SelectionResult::Edit(line) => { + initial_command = Some(line); + } + histsearch::SelectionResult::NoSelection => { + readline = Ok("".to_string()); + initial_command = None; + } + } + } else { + initial_command = None; + } + } match process_line(readline, &mut context).await { LineResult::Success(line) => { rl.add_history_entry(line.clone()); } - LineResult::SearchHist => { - let hist = std::fs::read_to_string("history.txt").expect("Cannot open history.txt"); - let lines = hist.lines().rev().collect(); - histsearch::select_from_list(&lines); - } - LineResult::CtrlC => { if ctrlcbreak { std::process::exit(0); @@ -403,7 +424,6 @@ pub async fn cli() -> Result<(), Box> { enum LineResult { Success(String), - SearchHist, Error(String, ShellError), CtrlC, Break, @@ -531,7 +551,7 @@ async fn process_line(readline: Result, ctx: &mut Context LineResult::Success(line.clone()) } Err(ReadlineError::Interrupted) => LineResult::CtrlC, - Err(ReadlineError::Eof) => LineResult::SearchHist, // Override LineResult::Break + Err(ReadlineError::Eof) => LineResult::Break, Err(err) => { println!("Error: {:?}", err); LineResult::Break diff --git a/src/histsearch.rs b/src/histsearch.rs index 495b0d5a4e..25e1fb7196 100644 --- a/src/histsearch.rs +++ b/src/histsearch.rs @@ -3,7 +3,13 @@ use crossterm::{cursor, terminal, ClearType, InputEvent, KeyEvent, RawScreen}; use std::io::Write; use sublime_fuzzy::best_match; -pub fn select_from_list(lines: &Vec<&str>) { +pub enum SelectionResult { + Selected(String), + Edit(String), + NoSelection, +} + +pub fn select_from_list(lines: &Vec<&str>) -> SelectionResult { const MAX_RESULTS: usize = 5; #[derive(PartialEq)] enum State { @@ -70,7 +76,7 @@ pub fn select_from_list(lines: &Vec<&str>) { cursor.move_up(selected_lines.len() as u16); } let (_x, y) = cursor.pos(); - let _ = cursor.goto(0, y); + let _ = cursor.goto(0, y - 1); let _ = cursor.show(); let _ = RawScreen::disable_raw_mode(); @@ -79,13 +85,9 @@ pub fn select_from_list(lines: &Vec<&str>) { terminal.clear(ClearType::FromCursorDown).unwrap(); match state { - State::Selected(idx) => { - print!("{}", lines[idx]); - } - State::Edit(idx) => { - print!("{}", lines[idx]); - } - _ => {} + State::Selected(idx) => SelectionResult::Selected(lines[idx].to_string()), + State::Edit(idx) => SelectionResult::Edit(lines[idx].to_string()), + _ => SelectionResult::NoSelection, } } From 0a0be19bed94f960b8b427e0bda75ef739cbb4cc Mon Sep 17 00:00:00 2001 From: Pirmin Kalberer Date: Wed, 18 Sep 2019 18:27:53 +0200 Subject: [PATCH 4/7] Rename histsearch to fuzzysearch --- src/cli.rs | 13 ++++++------- src/{histsearch.rs => fuzzysearch.rs} | 11 +++++------ src/lib.rs | 2 +- 3 files changed, 12 insertions(+), 14 deletions(-) rename src/{histsearch.rs => fuzzysearch.rs} (93%) diff --git a/src/cli.rs b/src/cli.rs index 5cfcb1475a..36ef87dcb7 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -9,8 +9,8 @@ use crate::commands::whole_stream_command; use crate::context::Context; use crate::data::Value; pub(crate) use crate::errors::ShellError; +use crate::fuzzysearch::{interactive_fuzzy_search, SelectionResult}; use crate::git::current_branch; -use crate::histsearch; use crate::parser::registry::Signature; use crate::parser::{hir, CallNode, Pipeline, PipelineElement, TokenNode}; use crate::prelude::*; @@ -354,19 +354,18 @@ pub async fn cli() -> Result<(), Box> { readline = rl.readline_with_initial(prompt, (&cmd, "")); if let Err(ReadlineError::Eof) = &readline { // Fuzzy search in history - let hist = std::fs::read_to_string("history.txt").expect("Cannot open history.txt"); - let lines = hist.lines().rev().collect(); - let selection = histsearch::select_from_list(&lines); // Clears last line with prompt + let lines = rl.history().iter().rev().map(|s| s.as_str()).collect(); + let selection = interactive_fuzzy_search(&lines, 5); // Clears last line with prompt match selection { - histsearch::SelectionResult::Selected(line) => { + SelectionResult::Selected(line) => { println!("{}{}", &prompt, &line); // TODO: colorize prompt readline = Ok(line.clone()); initial_command = None; } - histsearch::SelectionResult::Edit(line) => { + SelectionResult::Edit(line) => { initial_command = Some(line); } - histsearch::SelectionResult::NoSelection => { + SelectionResult::NoSelection => { readline = Ok("".to_string()); initial_command = None; } diff --git a/src/histsearch.rs b/src/fuzzysearch.rs similarity index 93% rename from src/histsearch.rs rename to src/fuzzysearch.rs index 25e1fb7196..5a253328e4 100644 --- a/src/histsearch.rs +++ b/src/fuzzysearch.rs @@ -9,8 +9,7 @@ pub enum SelectionResult { NoSelection, } -pub fn select_from_list(lines: &Vec<&str>) -> SelectionResult { - const MAX_RESULTS: usize = 5; +pub fn interactive_fuzzy_search(lines: &Vec<&str>, max_results: usize) -> SelectionResult { #[derive(PartialEq)] enum State { Selecting, @@ -30,7 +29,7 @@ pub fn select_from_list(lines: &Vec<&str>) -> SelectionResult { let mut sync_stdin = input.read_sync(); while state == State::Selecting { - let search_result = search(&searchinput, &lines, MAX_RESULTS); + let search_result = fuzzy_search(&searchinput, &lines, max_results); let selected_lines: Vec<&str> = search_result .iter() .map(|item| &item.highlighted_text as &str) @@ -96,8 +95,8 @@ struct Match { text_idx: usize, } -fn search(input: &String, lines: &Vec<&str>, max_results: usize) -> Vec { - if input.is_empty() { +fn fuzzy_search(searchstr: &str, lines: &Vec<&str>, max_results: usize) -> Vec { + if searchstr.is_empty() { return lines .iter() .take(max_results) @@ -112,7 +111,7 @@ fn search(input: &String, lines: &Vec<&str>, max_results: usize) -> Vec { let mut matches = lines .iter() .enumerate() - .map(|(idx, line)| (idx, best_match(&input, line))) + .map(|(idx, line)| (idx, best_match(&searchstr, line))) .filter(|(_i, m)| m.is_some()) .map(|(i, m)| (i, m.unwrap())) .collect::>(); diff --git a/src/lib.rs b/src/lib.rs index 9802e2f686..f4ccb4e4e3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,8 +12,8 @@ mod env; mod errors; mod evaluate; mod format; +mod fuzzysearch; mod git; -mod histsearch; mod parser; mod plugin; mod shell; From 0c9a62aeecedee3f62dd2e1f33d22f11040d2d6b Mon Sep 17 00:00:00 2001 From: Pirmin Kalberer Date: Wed, 18 Sep 2019 22:18:16 +0200 Subject: [PATCH 5/7] Separate highlighting from fuzzy search --- src/fuzzysearch.rs | 85 ++++++++++++++++++++++++---------------------- 1 file changed, 45 insertions(+), 40 deletions(-) diff --git a/src/fuzzysearch.rs b/src/fuzzysearch.rs index 5a253328e4..d01e725d02 100644 --- a/src/fuzzysearch.rs +++ b/src/fuzzysearch.rs @@ -14,8 +14,8 @@ pub fn interactive_fuzzy_search(lines: &Vec<&str>, max_results: usize) -> Select enum State { Selecting, Quit, - Selected(usize), - Edit(usize), + Selected(String), + Edit(String), } let mut state = State::Selecting; if let Ok(_raw) = RawScreen::into_raw_mode() { @@ -29,11 +29,9 @@ pub fn interactive_fuzzy_search(lines: &Vec<&str>, max_results: usize) -> Select let mut sync_stdin = input.read_sync(); while state == State::Selecting { - let search_result = fuzzy_search(&searchinput, &lines, max_results); - let selected_lines: Vec<&str> = search_result - .iter() - .map(|item| &item.highlighted_text as &str) - .collect(); + let mut search_result = fuzzy_search(&searchinput, &lines, max_results); + let selected_lines: Vec = + search_result.iter().map(|item| highlight(&item)).collect(); paint_selection_list(&selected_lines, selected); if let Some(ev) = sync_stdin.next() { match ev { @@ -52,10 +50,10 @@ pub fn interactive_fuzzy_search(lines: &Vec<&str>, max_results: usize) -> Select } } KeyEvent::Char('\n') => { - state = State::Selected(search_result[selected].text_idx); + state = State::Selected(search_result.remove(selected).text); } KeyEvent::Char('\t') | KeyEvent::Right => { - state = State::Edit(search_result[selected].text_idx); + state = State::Edit(search_result.remove(selected).text); } KeyEvent::Char(ch) => { searchinput.push(ch); @@ -66,7 +64,7 @@ pub fn interactive_fuzzy_search(lines: &Vec<&str>, max_results: usize) -> Select selected = 0; } _ => { - // println!("{}", format!("OTHER InputEvent: {:?}\n\n", k)); + // println!("OTHER InputEvent: {:?}", k); } }, _ => {} @@ -84,26 +82,25 @@ pub fn interactive_fuzzy_search(lines: &Vec<&str>, max_results: usize) -> Select terminal.clear(ClearType::FromCursorDown).unwrap(); match state { - State::Selected(idx) => SelectionResult::Selected(lines[idx].to_string()), - State::Edit(idx) => SelectionResult::Edit(lines[idx].to_string()), + State::Selected(line) => SelectionResult::Selected(line), + State::Edit(line) => SelectionResult::Edit(line), _ => SelectionResult::NoSelection, } } -struct Match { - highlighted_text: String, - text_idx: usize, +pub struct Match { + text: String, + char_matches: Vec<(usize, usize)>, } -fn fuzzy_search(searchstr: &str, lines: &Vec<&str>, max_results: usize) -> Vec { +pub fn fuzzy_search(searchstr: &str, lines: &Vec<&str>, max_results: usize) -> Vec { if searchstr.is_empty() { return lines .iter() .take(max_results) - .enumerate() - .map(|(i, line)| Match { - highlighted_text: line.to_string(), - text_idx: i, + .map(|line| Match { + text: line.to_string(), + char_matches: Vec::new(), }) .collect(); } @@ -117,41 +114,43 @@ fn fuzzy_search(searchstr: &str, lines: &Vec<&str>, max_results: usize) -> Vec>(); matches.sort_by(|a, b| b.1.score().cmp(&a.1.score())); - let highlight = Colour::Cyan; let results: Vec = matches .iter() .take(max_results) - .map(|(i, m)| { - let r = &lines[*i]; - let mut outstr = String::with_capacity(r.len()); - let mut idx = 0; - for (match_idx, len) in m.continuous_matches() { - outstr.push_str(&r[idx..match_idx]); - idx = match_idx + len; - outstr.push_str(&format!("{}", highlight.paint(&r[match_idx..idx]))); - } - if idx < r.len() { - outstr.push_str(&r[idx..r.len()]); - } - Match { - highlighted_text: outstr, - text_idx: *i, - } + .map(|(i, m)| Match { + text: lines[*i].to_string(), + char_matches: m.continuous_matches(), }) .collect(); results } -fn paint_selection_list(lines: &Vec<&str>, selected: usize) { +fn highlight(textmatch: &Match) -> String { + let hlcol = Colour::Cyan; + let text = &textmatch.text; + let mut outstr = String::with_capacity(text.len()); + let mut idx = 0; + for (match_idx, len) in &textmatch.char_matches { + outstr.push_str(&text[idx..*match_idx]); + idx = match_idx + len; + outstr.push_str(&format!("{}", hlcol.paint(&text[*match_idx..idx]))); + } + if idx < text.len() { + outstr.push_str(&text[idx..text.len()]); + } + outstr +} + +fn paint_selection_list(lines: &Vec, selected: usize) { let dimmed = Colour::White.dimmed(); let cursor = cursor(); let (_x, y) = cursor.pos(); for (i, line) in lines.iter().enumerate() { let _ = cursor.goto(0, y + (i as u16)); if selected == i { - println!("{}", *line); + println!("{}", line); } else { - println!("{}", dimmed.paint(*line)); + println!("{}", dimmed.paint(line)); } } let _ = cursor.goto(0, y + (lines.len() as u16)); @@ -164,3 +163,9 @@ fn paint_selection_list(lines: &Vec<&str>, selected: usize) { // Clear additional lines from previous selection terminal().clear(ClearType::FromCursorDown).unwrap(); } + +#[test] +fn fuzzy_match() { + let matches = fuzzy_search("cb", &vec!["abc", "cargo build"], 1); + assert_eq!(matches[0].text, "cargo build"); +} From 639a31667705078d5d0fe4050a1f440b70a8adfc Mon Sep 17 00:00:00 2001 From: Pirmin Kalberer Date: Wed, 18 Sep 2019 23:08:00 +0200 Subject: [PATCH 6/7] Fix selection list display glitches --- src/fuzzysearch.rs | 59 ++++++++++++++++++++++++++++------------------ 1 file changed, 36 insertions(+), 23 deletions(-) diff --git a/src/fuzzysearch.rs b/src/fuzzysearch.rs index d01e725d02..b27745590e 100644 --- a/src/fuzzysearch.rs +++ b/src/fuzzysearch.rs @@ -1,4 +1,4 @@ -use ansi_term::Colour; +use ansi_term::{ANSIString, ANSIStrings, Colour, Style}; use crossterm::{cursor, terminal, ClearType, InputEvent, KeyEvent, RawScreen}; use std::io::Write; use sublime_fuzzy::best_match; @@ -29,9 +29,8 @@ pub fn interactive_fuzzy_search(lines: &Vec<&str>, max_results: usize) -> Select let mut sync_stdin = input.read_sync(); while state == State::Selecting { - let mut search_result = fuzzy_search(&searchinput, &lines, max_results); - let selected_lines: Vec = - search_result.iter().map(|item| highlight(&item)).collect(); + let mut selected_lines = fuzzy_search(&searchinput, &lines, max_results); + let num_lines = selected_lines.len(); paint_selection_list(&selected_lines, selected); if let Some(ev) = sync_stdin.next() { match ev { @@ -50,10 +49,18 @@ pub fn interactive_fuzzy_search(lines: &Vec<&str>, max_results: usize) -> Select } } KeyEvent::Char('\n') => { - state = State::Selected(search_result.remove(selected).text); + state = if selected_lines.len() > 0 { + State::Selected(selected_lines.remove(selected).text) + } else { + State::Edit("".to_string()) + }; } KeyEvent::Char('\t') | KeyEvent::Right => { - state = State::Edit(search_result.remove(selected).text); + state = if selected_lines.len() > 0 { + State::Edit(selected_lines.remove(selected).text) + } else { + State::Edit("".to_string()) + }; } KeyEvent::Char(ch) => { searchinput.push(ch); @@ -70,16 +77,16 @@ pub fn interactive_fuzzy_search(lines: &Vec<&str>, max_results: usize) -> Select _ => {} } } - cursor.move_up(selected_lines.len() as u16); + if num_lines > 0 { + cursor.move_up(num_lines as u16); + } } let (_x, y) = cursor.pos(); let _ = cursor.goto(0, y - 1); let _ = cursor.show(); - let _ = RawScreen::disable_raw_mode(); } - let terminal = terminal(); - terminal.clear(ClearType::FromCursorDown).unwrap(); + terminal().clear(ClearType::FromCursorDown).unwrap(); match state { State::Selected(line) => SelectionResult::Selected(line), @@ -125,33 +132,39 @@ pub fn fuzzy_search(searchstr: &str, lines: &Vec<&str>, max_results: usize) -> V results } -fn highlight(textmatch: &Match) -> String { - let hlcol = Colour::Cyan; +fn highlight(textmatch: &Match, normal: Style, highlighted: Style) -> Vec { let text = &textmatch.text; - let mut outstr = String::with_capacity(text.len()); + let mut ansi_strings = vec![]; let mut idx = 0; for (match_idx, len) in &textmatch.char_matches { - outstr.push_str(&text[idx..*match_idx]); + ansi_strings.push(normal.paint(&text[idx..*match_idx])); idx = match_idx + len; - outstr.push_str(&format!("{}", hlcol.paint(&text[*match_idx..idx]))); + ansi_strings.push(highlighted.paint(&text[*match_idx..idx])); } if idx < text.len() { - outstr.push_str(&text[idx..text.len()]); + ansi_strings.push(normal.paint(&text[idx..text.len()])); } - outstr + ansi_strings } -fn paint_selection_list(lines: &Vec, selected: usize) { - let dimmed = Colour::White.dimmed(); +fn paint_selection_list(lines: &Vec, selected: usize) { + let terminal = terminal(); + let size = terminal.terminal_size(); + let width = size.0 as usize; let cursor = cursor(); let (_x, y) = cursor.pos(); for (i, line) in lines.iter().enumerate() { let _ = cursor.goto(0, y + (i as u16)); - if selected == i { - println!("{}", line); + let (style, highlighted) = if selected == i { + (Colour::White.normal(), Colour::Cyan.normal()) } else { - println!("{}", dimmed.paint(line)); + (Colour::White.dimmed(), Colour::Cyan.normal()) + }; + let mut ansi_strings = highlight(line, style, highlighted); + for _ in line.text.len()..width { + ansi_strings.push(style.paint(' '.to_string())); } + println!("{}", ANSIStrings(&ansi_strings)); } let _ = cursor.goto(0, y + (lines.len() as u16)); print!( @@ -161,7 +174,7 @@ fn paint_selection_list(lines: &Vec, selected: usize) { let _ = std::io::stdout().flush(); // Clear additional lines from previous selection - terminal().clear(ClearType::FromCursorDown).unwrap(); + terminal.clear(ClearType::FromCursorDown).unwrap(); } #[test] From d7e7f48aaa5edf27c94f2ab8485c0d331a440130 Mon Sep 17 00:00:00 2001 From: Pirmin Kalberer Date: Thu, 19 Sep 2019 20:45:58 +0200 Subject: [PATCH 7/7] Deactivate fuzzy search on Windows for now --- src/cli.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cli.rs b/src/cli.rs index 36ef87dcb7..fb158b1af6 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -336,6 +336,7 @@ pub async fn cli() -> Result<(), Box> { // Register Ctrl-r for history fuzzy search // rustyline doesn't support custom commands, so we override Ctrl-D (EOF) + #[cfg(not(windows))] // https://github.com/nushell/nushell/issues/689 rl.bind_sequence(rustyline::KeyPress::Ctrl('R'), rustyline::Cmd::EndOfFile); // Redefine Ctrl-D to same command as Ctrl-C rl.bind_sequence(rustyline::KeyPress::Ctrl('D'), rustyline::Cmd::Interrupt);