Merge pull request #672 from pka/sublime-style-search

Sublime style history search
This commit is contained in:
Jonathan Turner 2019-09-20 07:54:11 +12:00 committed by GitHub
commit 70ac2381c5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 229 additions and 2 deletions

7
Cargo.lock generated
View file

@ -1566,6 +1566,7 @@ dependencies = [
"serde_urlencoded 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", "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)", "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)", "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)", "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)", "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)", "syntect 3.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
@ -2443,6 +2444,11 @@ name = "strsim"
version = "0.8.0" version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" 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]] [[package]]
name = "subprocess" name = "subprocess"
version = "0.1.18" 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 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 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 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 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 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" "checksum syn 0.15.43 (registry+https://github.com/rust-lang/crates.io-index)" = "ee06ea4b620ab59a2267c6b48be16244a3389f8bfa0986bdd15c35b890b00af3"

View file

@ -73,6 +73,7 @@ num-bigint = { version = "0.2.3", features = ["serde"] }
bigdecimal = { version = "0.1.0", features = ["serde"] } bigdecimal = { version = "0.1.0", features = ["serde"] }
natural = "0.3.0" natural = "0.3.0"
serde_urlencoded = "0.6.1" serde_urlencoded = "0.6.1"
sublime_fuzzy = "0.5"
neso = { version = "0.5.0", optional = true } neso = { version = "0.5.0", optional = true }
crossterm = { version = "0.10.2", optional = true } crossterm = { version = "0.10.2", optional = true }

View file

@ -9,6 +9,7 @@ use crate::commands::whole_stream_command;
use crate::context::Context; use crate::context::Context;
use crate::data::Value; use crate::data::Value;
pub(crate) use crate::errors::ShellError; pub(crate) use crate::errors::ShellError;
use crate::fuzzysearch::{interactive_fuzzy_search, SelectionResult};
use crate::git::current_branch; use crate::git::current_branch;
use crate::parser::registry::Signature; use crate::parser::registry::Signature;
use crate::parser::{hir, CallNode, Pipeline, PipelineElement, TokenNode}; use crate::parser::{hir, CallNode, Pipeline, PipelineElement, TokenNode};
@ -333,14 +334,47 @@ pub async fn cli() -> Result<(), Box<dyn Error>> {
rl.set_edit_mode(edit_mode); rl.set_edit_mode(edit_mode);
let readline = rl.readline(&format!( // 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);
let prompt = &format!(
"{}{}> ", "{}{}> ",
cwd, cwd,
match current_branch() { match current_branch() {
Some(s) => format!("({})", s), Some(s) => format!("({})", s),
None => "".to_string(), 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 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 {
SelectionResult::Selected(line) => {
println!("{}{}", &prompt, &line); // TODO: colorize prompt
readline = Ok(line.clone());
initial_command = None;
}
SelectionResult::Edit(line) => {
initial_command = Some(line);
}
SelectionResult::NoSelection => {
readline = Ok("".to_string());
initial_command = None;
}
}
} else {
initial_command = None;
}
}
match process_line(readline, &mut context).await { match process_line(readline, &mut context).await {
LineResult::Success(line) => { LineResult::Success(line) => {

184
src/fuzzysearch.rs Normal file
View file

@ -0,0 +1,184 @@
use ansi_term::{ANSIString, ANSIStrings, Colour, Style};
use crossterm::{cursor, terminal, ClearType, InputEvent, KeyEvent, RawScreen};
use std::io::Write;
use sublime_fuzzy::best_match;
pub enum SelectionResult {
Selected(String),
Edit(String),
NoSelection,
}
pub fn interactive_fuzzy_search(lines: &Vec<&str>, max_results: usize) -> SelectionResult {
#[derive(PartialEq)]
enum State {
Selecting,
Quit,
Selected(String),
Edit(String),
}
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 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 {
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 = if selected_lines.len() > 0 {
State::Selected(selected_lines.remove(selected).text)
} else {
State::Edit("".to_string())
};
}
KeyEvent::Char('\t') | KeyEvent::Right => {
state = if selected_lines.len() > 0 {
State::Edit(selected_lines.remove(selected).text)
} else {
State::Edit("".to_string())
};
}
KeyEvent::Char(ch) => {
searchinput.push(ch);
selected = 0;
}
KeyEvent::Backspace => {
searchinput.pop();
selected = 0;
}
_ => {
// println!("OTHER InputEvent: {:?}", k);
}
},
_ => {}
}
}
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();
}
terminal().clear(ClearType::FromCursorDown).unwrap();
match state {
State::Selected(line) => SelectionResult::Selected(line),
State::Edit(line) => SelectionResult::Edit(line),
_ => SelectionResult::NoSelection,
}
}
pub struct Match {
text: String,
char_matches: Vec<(usize, usize)>,
}
pub fn fuzzy_search(searchstr: &str, lines: &Vec<&str>, max_results: usize) -> Vec<Match> {
if searchstr.is_empty() {
return lines
.iter()
.take(max_results)
.map(|line| Match {
text: line.to_string(),
char_matches: Vec::new(),
})
.collect();
}
let mut matches = lines
.iter()
.enumerate()
.map(|(idx, line)| (idx, best_match(&searchstr, line)))
.filter(|(_i, m)| m.is_some())
.map(|(i, m)| (i, m.unwrap()))
.collect::<Vec<_>>();
matches.sort_by(|a, b| b.1.score().cmp(&a.1.score()));
let results: Vec<Match> = matches
.iter()
.take(max_results)
.map(|(i, m)| Match {
text: lines[*i].to_string(),
char_matches: m.continuous_matches(),
})
.collect();
results
}
fn highlight(textmatch: &Match, normal: Style, highlighted: Style) -> Vec<ANSIString> {
let text = &textmatch.text;
let mut ansi_strings = vec![];
let mut idx = 0;
for (match_idx, len) in &textmatch.char_matches {
ansi_strings.push(normal.paint(&text[idx..*match_idx]));
idx = match_idx + len;
ansi_strings.push(highlighted.paint(&text[*match_idx..idx]));
}
if idx < text.len() {
ansi_strings.push(normal.paint(&text[idx..text.len()]));
}
ansi_strings
}
fn paint_selection_list(lines: &Vec<Match>, 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));
let (style, highlighted) = if selected == i {
(Colour::White.normal(), Colour::Cyan.normal())
} else {
(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!(
"{}",
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();
}
#[test]
fn fuzzy_match() {
let matches = fuzzy_search("cb", &vec!["abc", "cargo build"], 1);
assert_eq!(matches[0].text, "cargo build");
}

View file

@ -12,6 +12,7 @@ mod env;
mod errors; mod errors;
mod evaluate; mod evaluate;
mod format; mod format;
mod fuzzysearch;
mod git; mod git;
mod parser; mod parser;
mod plugin; mod plugin;