-
Notifications
You must be signed in to change notification settings - Fork 81
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'tab_complete' of github.com:kernelPanic0x/magic-wormhol…
…e.rs into tab_complete
- Loading branch information
Showing
1 changed file
with
191 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,191 @@ | ||
use std::{borrow::Cow, sync::LazyLock}; | ||
|
||
use color_eyre::eyre::{self, bail}; | ||
use fuzzt::{algorithms::JaroWinkler, get_top_n}; | ||
use magic_wormhole::wordlist::default_wordlist_flatned; | ||
use reedline::{ | ||
default_emacs_keybindings, ColumnarMenu, Completer, Emacs, KeyCode, KeyModifiers, MenuBuilder, | ||
Prompt, PromptEditMode, PromptHistorySearch, Reedline, ReedlineEvent, ReedlineMenu, Signal, | ||
Span, Suggestion, | ||
}; | ||
|
||
static WORDLIST: LazyLock<Vec<String>> = LazyLock::new(|| default_wordlist_flatned()); | ||
|
||
struct CodePrompt {} | ||
|
||
impl CodePrompt { | ||
fn default() -> Self { | ||
CodePrompt {} | ||
} | ||
} | ||
|
||
impl Prompt for CodePrompt { | ||
fn render_prompt_left(&self) -> Cow<'_, str> { | ||
Cow::Borrowed("Wormhole Code: ") | ||
} | ||
|
||
// Not needed | ||
fn render_prompt_right(&self) -> Cow<'_, str> { | ||
Cow::Borrowed("") | ||
} | ||
|
||
// Not needed | ||
fn render_prompt_indicator(&self, _prompt_mode: PromptEditMode) -> Cow<'_, str> { | ||
Cow::Borrowed("") | ||
} | ||
|
||
// Not needed | ||
fn render_prompt_multiline_indicator(&self) -> Cow<'_, str> { | ||
Cow::Borrowed("... ") | ||
} | ||
|
||
fn render_prompt_history_search_indicator( | ||
&self, | ||
_history_search: PromptHistorySearch, | ||
) -> Cow<'_, str> { | ||
Cow::Borrowed("") | ||
} | ||
|
||
fn get_prompt_color(&self) -> reedline::Color { | ||
reedline::Color::Grey | ||
} | ||
} | ||
|
||
struct CodeCompleter {} | ||
|
||
impl CodeCompleter { | ||
fn default() -> Self { | ||
CodeCompleter {} | ||
} | ||
} | ||
|
||
impl Completer for CodeCompleter { | ||
fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> { | ||
// Skip autocomplete for the channel number (first part) | ||
if !line.contains('-') { | ||
return Vec::new(); | ||
} | ||
|
||
// Find the start and end of the current word | ||
let current_word_start = line[..pos].rfind('-').map(|i| i + 1).unwrap_or(0); | ||
let current_word_end = line[pos..] | ||
.find('-') | ||
.map(|i| i + pos) | ||
.unwrap_or_else(|| line.len()); | ||
|
||
let current_part = &line[current_word_start..current_word_end]; | ||
|
||
let list = &*WORDLIST.iter().map(|s| s.as_str()).collect::<Vec<&str>>(); | ||
|
||
// Use fuzzy matching to find the best matches | ||
// Use cutoff for the menu system to be useful | ||
// If cutoff is high enough and only one word is left, use Tab to complete directly | ||
// We use the Jaro-Winkler algorithm because it places more emphasis on the beginning part of the code word | ||
let matches = get_top_n( | ||
current_part, | ||
list, | ||
Some(0.8), | ||
None, | ||
None, | ||
Some(&JaroWinkler), | ||
); | ||
|
||
matches | ||
.into_iter() | ||
.map(|word| { | ||
let suggestion = word.to_string(); | ||
|
||
// Incase suggestion word length is larger then the current typed part | ||
// Otherwise we get index out of range error in Span | ||
let span_end = | ||
std::cmp::min(current_word_start + suggestion.len(), current_word_end); | ||
|
||
Suggestion { | ||
value: suggestion, | ||
description: None, | ||
extra: None, | ||
span: Span { | ||
start: current_word_start, | ||
end: span_end, | ||
}, | ||
append_whitespace: false, | ||
style: None, | ||
} | ||
}) | ||
.collect() | ||
} | ||
} | ||
|
||
pub fn enter_code() -> eyre::Result<String> { | ||
// Set up the required keybindings | ||
let mut keybindings = default_emacs_keybindings(); | ||
|
||
keybindings.add_binding( | ||
KeyModifiers::NONE, | ||
KeyCode::Tab, | ||
ReedlineEvent::UntilFound(vec![ | ||
ReedlineEvent::Menu("completion_menu".to_string()), | ||
ReedlineEvent::MenuNext, | ||
]), | ||
); | ||
|
||
let edit_mode = Box::new(Emacs::new(keybindings)); | ||
|
||
let completion_menu = Box::new(ColumnarMenu::default().with_name("completion_menu")); | ||
|
||
let mut line_editor = Reedline::create() | ||
.with_completer(Box::new(CodeCompleter::default())) | ||
.with_menu(ReedlineMenu::EngineCompleter(completion_menu)) | ||
.with_quick_completions(true) | ||
.with_edit_mode(edit_mode); | ||
|
||
let prompt = CodePrompt::default(); | ||
|
||
let sig = line_editor.read_line(&prompt); | ||
match sig { | ||
Ok(Signal::Success(buffer)) => return Ok(buffer), | ||
// TODO: fix temporary work around | ||
Ok(Signal::CtrlC) => bail!("Ctrl-C received"), | ||
Ok(Signal::CtrlD) => bail!("Ctrl-D received"), | ||
Err(e) => bail!(e), | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod test { | ||
use super::*; | ||
#[test] | ||
fn test_tab_completion_complete_word() { | ||
let mut completer = CodeCompleter::default(); | ||
let input = "22-trombonist"; | ||
let cursor_pos = input.len(); | ||
|
||
let suggestions = completer.complete(&input, cursor_pos); | ||
|
||
assert_eq!(suggestions.len(), 3); | ||
|
||
assert_eq!(suggestions.first().unwrap().value, "trombonist"); | ||
} | ||
|
||
#[test] | ||
fn test_tab_completion_partial_word() { | ||
let mut completer = CodeCompleter::default(); | ||
let input = "22-trmbn"; | ||
let cursor_pos = input.len(); | ||
|
||
let suggestions = completer.complete(&input, cursor_pos); | ||
|
||
assert_eq!(suggestions.first().unwrap().value, "trombonist"); | ||
} | ||
|
||
#[test] | ||
fn test_tab_completion_partial_in_middle() { | ||
let mut completer = CodeCompleter::default(); | ||
let input = "22-trbis-zulu"; | ||
let cursor_pos = input.len() - "-zulu".len(); | ||
|
||
let suggestions = completer.complete(&input, cursor_pos); | ||
|
||
assert_eq!(suggestions.first().unwrap().value, "trombonist"); | ||
} | ||
} |