use crate::{CompletionGroup, MenuAction, MenuResult, MenuState};
#[derive(Debug, Clone)]
pub struct ZleCompletionState {
pub menu: MenuState,
pub menu_active: bool,
pub original_word: String,
pub completions: Vec<CompletionGroup>,
pub last_action: Option<ZleAction>,
pub list_displayed: bool,
}
#[derive(Debug, Clone)]
pub enum ZleAction {
NoMatch,
SingleMatch(String),
MultipleMatches,
MenuCycle,
MenuAccept(String),
ListOnly,
Expanded(String),
Beep,
Refresh,
}
impl Default for ZleCompletionState {
fn default() -> Self {
Self::new()
}
}
impl ZleCompletionState {
pub fn new() -> Self {
Self {
menu: MenuState::new(),
menu_active: false,
original_word: String::new(),
completions: Vec::new(),
last_action: None,
list_displayed: false,
}
}
pub fn reset(&mut self) {
self.menu_active = false;
self.original_word.clear();
self.completions.clear();
self.last_action = None;
self.list_displayed = false;
}
pub fn set_term_size(&mut self, width: usize, height: usize) {
self.menu.set_term_size(width, height);
}
pub fn set_available_rows(&mut self, rows: usize) {
self.menu.set_available_rows(rows);
}
}
pub struct ZleWidgets;
impl ZleWidgets {
pub fn complete_word(
state: &mut ZleCompletionState,
buffer: &str,
cursor: usize,
completions: Vec<CompletionGroup>,
) -> (ZleAction, Option<String>) {
let word = Self::word_at_cursor(buffer, cursor);
state.original_word = word.clone();
state.completions = completions;
let total_matches: usize = state.completions.iter().map(|g| g.matches.len()).sum();
if total_matches == 0 {
state.last_action = Some(ZleAction::NoMatch);
return (ZleAction::NoMatch, None);
}
if total_matches == 1 {
let completion = &state.completions[0].matches[0];
let insert = completion.insert_str();
state.last_action = Some(ZleAction::SingleMatch(insert.clone()));
return (ZleAction::SingleMatch(insert.clone()), Some(insert));
}
let common = Self::find_common_prefix(&state.completions);
if common.len() > word.len() {
state.last_action = Some(ZleAction::Expanded(common.clone()));
return (ZleAction::Expanded(common.clone()), Some(common));
}
state.menu.set_prefix(&word);
state.menu.set_completions(&state.completions);
state.menu.set_show_headers(true);
state.list_displayed = true;
state.last_action = Some(ZleAction::MultipleMatches);
(ZleAction::MultipleMatches, None)
}
pub fn expand_or_complete(
state: &mut ZleCompletionState,
buffer: &str,
cursor: usize,
completions: Vec<CompletionGroup>,
try_expand: impl FnOnce(&str) -> Option<String>,
) -> (ZleAction, Option<String>) {
let word = Self::word_at_cursor(buffer, cursor);
if let Some(expanded) = try_expand(&word) {
if expanded != word {
state.last_action = Some(ZleAction::Expanded(expanded.clone()));
return (ZleAction::Expanded(expanded.clone()), Some(expanded));
}
}
Self::complete_word(state, buffer, cursor, completions)
}
pub fn expand_or_complete_prefix(
state: &mut ZleCompletionState,
buffer: &str,
cursor: usize,
completions: Vec<CompletionGroup>,
try_expand: impl FnOnce(&str) -> Option<String>,
) -> (ZleAction, Option<String>) {
let word = Self::word_before_cursor(buffer, cursor);
if let Some(expanded) = try_expand(&word) {
if expanded != word {
state.last_action = Some(ZleAction::Expanded(expanded.clone()));
return (ZleAction::Expanded(expanded.clone()), Some(expanded));
}
}
Self::complete_word(state, buffer, cursor, completions)
}
pub fn menu_complete(
state: &mut ZleCompletionState,
buffer: &str,
cursor: usize,
completions: Vec<CompletionGroup>,
) -> (ZleAction, Option<String>) {
if !state.menu_active {
let word = Self::word_at_cursor(buffer, cursor);
state.original_word = word.clone();
state.completions = completions;
let total: usize = state.completions.iter().map(|g| g.matches.len()).sum();
if total == 0 {
return (ZleAction::NoMatch, None);
}
state.menu.set_prefix(&word);
state.menu.set_completions(&state.completions);
state.menu.start();
state.menu_active = true;
} else {
state.menu.process_action(MenuAction::Next);
}
if let Some(insert) = state.menu.selected_insert_string() {
state.last_action = Some(ZleAction::MenuCycle);
(ZleAction::MenuCycle, Some(insert))
} else {
(ZleAction::NoMatch, None)
}
}
pub fn reverse_menu_complete(
state: &mut ZleCompletionState,
buffer: &str,
cursor: usize,
completions: Vec<CompletionGroup>,
) -> (ZleAction, Option<String>) {
if !state.menu_active {
let word = Self::word_at_cursor(buffer, cursor);
state.original_word = word.clone();
state.completions = completions;
let total: usize = state.completions.iter().map(|g| g.matches.len()).sum();
if total == 0 {
return (ZleAction::NoMatch, None);
}
state.menu.set_prefix(&word);
state.menu.set_completions(&state.completions);
state.menu.start();
state.menu.process_action(MenuAction::End); state.menu_active = true;
} else {
state.menu.process_action(MenuAction::Prev);
}
if let Some(insert) = state.menu.selected_insert_string() {
state.last_action = Some(ZleAction::MenuCycle);
(ZleAction::MenuCycle, Some(insert))
} else {
(ZleAction::NoMatch, None)
}
}
pub fn accept_and_menu_complete(state: &mut ZleCompletionState) -> (ZleAction, Option<String>) {
if !state.menu_active {
return (ZleAction::NoMatch, None);
}
match state.menu.process_action(MenuAction::AcceptAndMenuComplete) {
MenuResult::AcceptAndHold(s) => {
state.last_action = Some(ZleAction::MenuAccept(s.clone()));
(ZleAction::MenuAccept(s.clone()), Some(s))
}
_ => (ZleAction::NoMatch, None),
}
}
pub fn delete_char_or_list(
state: &mut ZleCompletionState,
buffer: &str,
cursor: usize,
at_eol: bool,
completions: Vec<CompletionGroup>,
) -> (ZleAction, Option<String>) {
if at_eol || cursor >= buffer.len() {
Self::list_choices(state, buffer, cursor, completions)
} else {
(ZleAction::Refresh, None)
}
}
pub fn list_choices(
state: &mut ZleCompletionState,
buffer: &str,
cursor: usize,
completions: Vec<CompletionGroup>,
) -> (ZleAction, Option<String>) {
let word = Self::word_at_cursor(buffer, cursor);
state.completions = completions;
let total: usize = state.completions.iter().map(|g| g.matches.len()).sum();
if total == 0 {
state.last_action = Some(ZleAction::NoMatch);
return (ZleAction::NoMatch, None);
}
state.menu.set_prefix(&word);
state.menu.set_completions(&state.completions);
state.menu.set_show_headers(true);
state.list_displayed = true;
state.last_action = Some(ZleAction::ListOnly);
(ZleAction::ListOnly, None)
}
pub fn list_expand(
state: &mut ZleCompletionState,
buffer: &str,
cursor: usize,
try_expand: impl FnOnce(&str) -> Vec<String>,
) -> (ZleAction, Option<String>) {
let word = Self::word_at_cursor(buffer, cursor);
let expansions = try_expand(&word);
if expansions.is_empty() {
return (ZleAction::NoMatch, None);
}
let mut group = CompletionGroup::new("expansions");
group.explanation = Some("expansions".to_string());
for exp in expansions {
group.matches.push(crate::Completion::new(exp));
}
state.completions = vec![group];
state.menu.set_prefix(&word);
state.menu.set_completions(&state.completions);
state.list_displayed = true;
state.last_action = Some(ZleAction::ListOnly);
(ZleAction::ListOnly, None)
}
pub fn expand_word(
buffer: &str,
cursor: usize,
try_expand: impl FnOnce(&str) -> Option<String>,
) -> (ZleAction, Option<String>) {
let word = Self::word_at_cursor(buffer, cursor);
if let Some(expanded) = try_expand(&word) {
if expanded != word {
return (ZleAction::Expanded(expanded.clone()), Some(expanded));
}
}
(ZleAction::NoMatch, None)
}
pub fn expand_cmd_path(
buffer: &str,
cursor: usize,
find_command: impl FnOnce(&str) -> Option<String>,
) -> (ZleAction, Option<String>) {
let word = Self::word_at_cursor(buffer, cursor);
if let Some(path) = find_command(&word) {
if path != word {
return (ZleAction::Expanded(path.clone()), Some(path));
}
}
(ZleAction::NoMatch, None)
}
pub fn expand_history(
buffer: &str,
expand_fn: impl FnOnce(&str) -> Option<String>,
) -> (ZleAction, Option<String>) {
if let Some(expanded) = expand_fn(buffer) {
if expanded != buffer {
return (ZleAction::Expanded(expanded.clone()), Some(expanded));
}
}
(ZleAction::NoMatch, None)
}
pub fn magic_space(
buffer: &str,
expand_fn: impl FnOnce(&str) -> Option<String>,
) -> (ZleAction, Option<String>) {
if let Some(expanded) = expand_fn(buffer) {
let with_space = format!("{} ", expanded);
return (ZleAction::Expanded(with_space.clone()), Some(with_space));
}
(ZleAction::Expanded(" ".to_string()), Some(" ".to_string()))
}
pub fn menu_expand_or_complete(
state: &mut ZleCompletionState,
buffer: &str,
cursor: usize,
completions: Vec<CompletionGroup>,
try_expand: impl FnOnce(&str) -> Option<String>,
) -> (ZleAction, Option<String>) {
let word = Self::word_at_cursor(buffer, cursor);
if let Some(expanded) = try_expand(&word) {
if expanded != word {
return (ZleAction::Expanded(expanded.clone()), Some(expanded));
}
}
Self::menu_complete(state, buffer, cursor, completions)
}
pub fn end_of_list(state: &mut ZleCompletionState) -> ZleAction {
if state.list_displayed {
ZleAction::Refresh
} else {
ZleAction::NoMatch
}
}
fn word_at_cursor(buffer: &str, cursor: usize) -> String {
let cursor = cursor.min(buffer.len());
let start = buffer[..cursor]
.rfind(|c: char| c.is_whitespace())
.map(|i| i + 1)
.unwrap_or(0);
let end = buffer[cursor..]
.find(|c: char| c.is_whitespace())
.map(|i| cursor + i)
.unwrap_or(buffer.len());
buffer[start..end].to_string()
}
fn word_before_cursor(buffer: &str, cursor: usize) -> String {
let cursor = cursor.min(buffer.len());
let start = buffer[..cursor]
.rfind(|c: char| c.is_whitespace())
.map(|i| i + 1)
.unwrap_or(0);
buffer[start..cursor].to_string()
}
fn find_common_prefix(groups: &[CompletionGroup]) -> String {
let all_matches: Vec<&str> = groups
.iter()
.flat_map(|g| g.matches.iter().map(|m| m.str_.as_str()))
.collect();
if all_matches.is_empty() {
return String::new();
}
if all_matches.len() == 1 {
return all_matches[0].to_string();
}
let first = all_matches[0];
let mut prefix_len = first.len();
for s in &all_matches[1..] {
let common = first
.chars()
.zip(s.chars())
.take_while(|(a, b)| a.eq_ignore_ascii_case(b))
.count();
prefix_len = prefix_len.min(common);
}
first[..prefix_len].to_string()
}
}
impl ZleCompletionState {
pub fn menu_navigate(&mut self, action: MenuAction) -> Option<String> {
if !self.menu_active {
return None;
}
match self.menu.process_action(action) {
MenuResult::Accept(s) => {
self.menu_active = false;
Some(s)
}
MenuResult::AcceptAndHold(s) => Some(s),
MenuResult::Cancel => {
self.menu_active = false;
None
}
MenuResult::Continue => self.menu.selected_insert_string(),
_ => None,
}
}
pub fn current_selection(&self) -> Option<String> {
if self.menu_active {
self.menu.selected_insert_string()
} else {
None
}
}
pub fn render_menu(&mut self) -> crate::MenuRendering {
self.menu.render()
}
pub fn is_menu_active(&self) -> bool {
self.menu_active
}
pub fn cancel_menu(&mut self) {
self.menu_active = false;
self.list_displayed = false;
}
pub fn completion_count(&self) -> usize {
self.menu.count()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_word_at_cursor() {
assert_eq!(ZleWidgets::word_at_cursor("hello world", 5), "hello");
assert_eq!(ZleWidgets::word_at_cursor("hello world", 6), "world");
assert_eq!(ZleWidgets::word_at_cursor("hello world", 11), "world");
assert_eq!(ZleWidgets::word_at_cursor("hello", 3), "hello");
assert_eq!(ZleWidgets::word_at_cursor("", 0), "");
}
#[test]
fn test_word_before_cursor() {
assert_eq!(ZleWidgets::word_before_cursor("hello world", 5), "hello");
assert_eq!(ZleWidgets::word_before_cursor("hello world", 8), "wo");
assert_eq!(ZleWidgets::word_before_cursor("hello", 3), "hel");
}
#[test]
fn test_find_common_prefix() {
let mut g = CompletionGroup::new("test");
g.matches.push(crate::Completion::new("hello"));
g.matches.push(crate::Completion::new("help"));
g.matches.push(crate::Completion::new("helicopter"));
assert_eq!(ZleWidgets::find_common_prefix(&[g]), "hel");
}
#[test]
fn test_complete_word_no_matches() {
let mut state = ZleCompletionState::new();
let (action, insert) = ZleWidgets::complete_word(&mut state, "xyz", 3, vec![]);
assert!(matches!(action, ZleAction::NoMatch));
assert!(insert.is_none());
}
#[test]
fn test_complete_word_single_match() {
let mut state = ZleCompletionState::new();
let mut g = CompletionGroup::new("test");
g.matches.push(crate::Completion::new("hello"));
let (action, insert) = ZleWidgets::complete_word(&mut state, "hel", 3, vec![g]);
assert!(matches!(action, ZleAction::SingleMatch(_)));
assert_eq!(insert, Some("hello".into()));
}
#[test]
fn test_menu_complete_cycle() {
let mut state = ZleCompletionState::new();
let mut g = CompletionGroup::new("test");
g.matches.push(crate::Completion::new("aaa"));
g.matches.push(crate::Completion::new("aab"));
g.matches.push(crate::Completion::new("aac"));
let (_, insert1) = ZleWidgets::menu_complete(&mut state, "aa", 2, vec![g.clone()]);
assert!(state.menu_active);
assert_eq!(insert1, Some("aaa".into()));
let (_, insert2) = ZleWidgets::menu_complete(&mut state, "aa", 2, vec![g.clone()]);
assert_eq!(insert2, Some("aab".into()));
let (_, insert3) = ZleWidgets::menu_complete(&mut state, "aa", 2, vec![g]);
assert_eq!(insert3, Some("aac".into()));
}
}