nushell/src/fuzzysearch.rs

190 lines
6.5 KiB
Rust
Raw Normal View History

2019-09-18 23:08:00 +02:00
use ansi_term::{ANSIString, ANSIStrings, Colour, Style};
2019-09-24 23:33:30 +02:00
#[cfg(feature = "crossterm")]
2019-09-15 23:05:13 +02:00
use crossterm::{cursor, terminal, ClearType, InputEvent, KeyEvent, RawScreen};
use std::io::Write;
use sublime_fuzzy::best_match;
2019-09-18 00:21:39 +02:00
pub enum SelectionResult {
Selected(String),
Edit(String),
NoSelection,
}
2019-09-18 18:27:53 +02:00
pub fn interactive_fuzzy_search(lines: &Vec<&str>, max_results: usize) -> SelectionResult {
2019-09-15 23:05:13 +02:00
#[derive(PartialEq)]
enum State {
Selecting,
Quit,
Selected(String),
Edit(String),
2019-09-15 23:05:13 +02:00
}
let mut state = State::Selecting;
2019-09-24 23:33:30 +02:00
#[cfg(feature = "crossterm")]
{
if let Ok(_raw) = RawScreen::into_raw_mode() {
// User input for search
let mut searchinput = String::new();
let mut selected = 0;
2019-09-15 23:05:13 +02:00
2019-09-24 23:33:30 +02:00
let mut cursor = cursor();
let _ = cursor.hide();
let input = crossterm::input();
let mut sync_stdin = input.read_sync();
2019-09-15 23:05:13 +02:00
2019-09-24 23:33:30 +02:00
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;
2019-09-15 23:05:13 +02:00
}
2019-09-24 23:33:30 +02:00
KeyEvent::Up => {
if selected > 0 {
selected -= 1;
}
2019-09-15 23:05:13 +02:00
}
2019-09-24 23:33:30 +02:00
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);
2019-09-15 23:05:13 +02:00
}
}
2019-09-24 23:33:30 +02:00
let (_x, y) = cursor.pos();
let _ = cursor.goto(0, y - 1);
let _ = cursor.show();
let _ = RawScreen::disable_raw_mode();
2019-09-15 23:05:13 +02:00
}
2019-09-24 23:33:30 +02:00
terminal().clear(ClearType::FromCursorDown).unwrap();
2019-09-15 23:05:13 +02:00
}
match state {
State::Selected(line) => SelectionResult::Selected(line),
State::Edit(line) => SelectionResult::Edit(line),
2019-09-18 00:21:39 +02:00
_ => SelectionResult::NoSelection,
2019-09-15 23:05:13 +02:00
}
}
pub struct Match {
text: String,
char_matches: Vec<(usize, usize)>,
2019-09-15 23:05:13 +02:00
}
pub fn fuzzy_search(searchstr: &str, lines: &Vec<&str>, max_results: usize) -> Vec<Match> {
2019-09-18 18:27:53 +02:00
if searchstr.is_empty() {
2019-09-15 23:05:13 +02:00
return lines
.iter()
.take(max_results)
.map(|line| Match {
text: line.to_string(),
char_matches: Vec::new(),
2019-09-15 23:05:13 +02:00
})
.collect();
}
let mut matches = lines
.iter()
.enumerate()
2019-09-18 18:27:53 +02:00
.map(|(idx, line)| (idx, best_match(&searchstr, line)))
2019-09-15 23:05:13 +02:00
.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(),
2019-09-15 23:05:13 +02:00
})
.collect();
results
}
2019-09-24 23:33:30 +02:00
#[cfg(feature = "crossterm")]
2019-09-18 23:08:00 +02:00
fn highlight(textmatch: &Match, normal: Style, highlighted: Style) -> Vec<ANSIString> {
let text = &textmatch.text;
2019-09-18 23:08:00 +02:00
let mut ansi_strings = vec![];
let mut idx = 0;
for (match_idx, len) in &textmatch.char_matches {
2019-09-18 23:08:00 +02:00
ansi_strings.push(normal.paint(&text[idx..*match_idx]));
idx = match_idx + len;
2019-09-18 23:08:00 +02:00
ansi_strings.push(highlighted.paint(&text[*match_idx..idx]));
}
if idx < text.len() {
2019-09-18 23:08:00 +02:00
ansi_strings.push(normal.paint(&text[idx..text.len()]));
}
2019-09-18 23:08:00 +02:00
ansi_strings
}
2019-09-24 23:33:30 +02:00
#[cfg(feature = "crossterm")]
2019-09-18 23:08:00 +02:00
fn paint_selection_list(lines: &Vec<Match>, selected: usize) {
let terminal = terminal();
let size = terminal.terminal_size();
let width = size.0 as usize;
2019-09-15 23:05:13 +02:00
let cursor = cursor();
let (_x, y) = cursor.pos();
for (i, line) in lines.iter().enumerate() {
let _ = cursor.goto(0, y + (i as u16));
2019-09-18 23:08:00 +02:00
let (style, highlighted) = if selected == i {
(Colour::White.normal(), Colour::Cyan.normal())
2019-09-15 23:05:13 +02:00
} else {
2019-09-18 23:08:00 +02:00
(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()));
2019-09-15 23:05:13 +02:00
}
2019-09-18 23:08:00 +02:00
println!("{}", ANSIStrings(&ansi_strings));
2019-09-15 23:05:13 +02:00
}
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
2019-09-18 23:08:00 +02:00
terminal.clear(ClearType::FromCursorDown).unwrap();
2019-09-15 23:05:13 +02:00
}
#[test]
fn fuzzy_match() {
let matches = fuzzy_search("cb", &vec!["abc", "cargo build"], 1);
assert_eq!(matches[0].text, "cargo build");
}