Skip to content

Commit

Permalink
Merge branch 'tab_complete' of github.com:kernelPanic0x/magic-wormhol…
Browse files Browse the repository at this point in the history
…e.rs into tab_complete
  • Loading branch information
kernelPanic0x committed Oct 8, 2024
2 parents 711875a + 0e349cd commit 585a198
Showing 1 changed file with 191 additions and 0 deletions.
191 changes: 191 additions & 0 deletions cli/src/reedline.rs
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");
}
}

0 comments on commit 585a198

Please sign in to comment.