From 28e94afd3fa6eeee67c4180be26d9bb0763241c4 Mon Sep 17 00:00:00 2001 From: CapsizeGlimmer <> Date: Fri, 8 May 2020 17:38:58 -0400 Subject: [PATCH] Finish tab completion implementation --- common/src/cmd.rs | 66 +++++++++++-------- voxygen/src/hud/chat.rs | 141 +++++++++++++++++++++++++--------------- voxygen/src/hud/mod.rs | 8 +++ 3 files changed, 137 insertions(+), 78 deletions(-) diff --git a/common/src/cmd.rs b/common/src/cmd.rs index e3b47609dc..b99a3e0442 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -183,7 +183,7 @@ impl ChatCommand { }, ChatCommand::Spawn => cmd(vec![/*TODO*/], "Spawn a test entity", true), ChatCommand::Sudo => cmd( - vec![PlayerName(false), Command(false), SubCommand], + vec![PlayerName(false), SubCommand], "Run command as if you were another player", true, ), @@ -251,12 +251,12 @@ impl ChatCommand { .map(|arg| match arg { ArgumentSpec::PlayerName(_) => "{}", ArgumentSpec::ItemSpec(_) => "{}", - ArgumentSpec::Float(_, _, _) => "{f}", + ArgumentSpec::Float(_, _, _) => "{}", ArgumentSpec::Integer(_, _, _) => "{d}", ArgumentSpec::Any(_, _) => "{}", ArgumentSpec::Command(_) => "{}", ArgumentSpec::Message => "{/.*/}", - ArgumentSpec::SubCommand => "{/.*/}", + ArgumentSpec::SubCommand => "{} {/.*/}", ArgumentSpec::OneOf(_, _, _, _) => "{}", // TODO }) .collect::>() @@ -406,34 +406,43 @@ impl ArgumentSpec { } fn complete_player(part: &str, state: &State) -> Vec { - println!("Player completion: '{}'", part); - state.ecs().read_storage::() - .join() - .inspect(|player| println!(" player: {}", player.alias)) - .filter(|player| player.alias.starts_with(part)) - .map(|player| player.alias.clone()) - .collect() + let storage = state.ecs().read_storage::(); + let mut iter = storage.join(); + if let Some(first) = iter.next() { + std::iter::once(first) + .chain(iter) + .filter(|player| player.alias.starts_with(part)) + .map(|player| player.alias.clone()) + .collect() + } else { + vec!["singleplayer".to_string()] + } } fn complete_command(part: &str) -> Vec { - println!("Command completion: '{}'", part); CHAT_COMMANDS .iter() .map(|com| com.keyword()) .filter(|kwd| kwd.starts_with(part) || format!("/{}", kwd).starts_with(part)) - .map(|c| c.to_string()) + .map(|c| format!("/{}", c)) .collect() } +// Get the byte index of the nth word. Used in completing "/sudo p /subcmd" fn nth_word(line: &str, n: usize) -> Option { let mut is_space = false; let mut j = 0; for (i, c) in line.char_indices() { match (is_space, c.is_whitespace()) { - (true, true) => {} - (true, false) => { is_space = false; } - (false, true) => { is_space = true; j += 1; } - (false, false) => {} + (true, true) => {}, + (true, false) => { + is_space = false; + j += 1; + }, + (false, true) => { + is_space = true; + }, + (false, false) => {}, } if j == n { return Some(i); @@ -443,16 +452,25 @@ fn nth_word(line: &str, n: usize) -> Option { } pub fn complete(line: &str, state: &State) -> Vec { - let word = line.split_whitespace().last().unwrap_or(""); + let word = if line.chars().last().map_or(true, char::is_whitespace) { + "" + } else { + line.split_whitespace().last().unwrap_or("") + }; if line.chars().next() == Some('/') { let mut iter = line.split_whitespace(); let cmd = iter.next().unwrap(); - if let Some((i, word)) = iter.enumerate().last() { + let i = iter.count() + if word.is_empty() { 1 } else { 0 }; + if i == 0 { + // Completing chat command name + complete_command(word) + } else { if let Ok(cmd) = cmd.parse::() { - if let Some(arg) = cmd.data().args.get(i) { - println!("Arg completion: {}", word); + if let Some(arg) = cmd.data().args.get(i - 1) { + // Complete ith argument arg.complete(word, &state) } else { + // Complete past the last argument match cmd.data().args.last() { Some(ArgumentSpec::SubCommand) => { if let Some(index) = nth_word(line, cmd.data().args.len()) { @@ -461,22 +479,18 @@ pub fn complete(line: &str, state: &State) -> Vec { error!("Could not tab-complete SubCommand"); vec![] } - } + }, Some(ArgumentSpec::Message) => complete_player(word, &state), - _ => { vec![] } // End of command. Nothing to complete + _ => vec![], // End of command. Nothing to complete } } } else { // Completing for unknown chat command complete_player(word, &state) } - } else { - // Completing chat command name - complete_command(word) } } else { // Not completing a command complete_player(word, &state) } } - diff --git a/voxygen/src/hud/chat.rs b/voxygen/src/hud/chat.rs index 0e12c808bd..50310bfe94 100644 --- a/voxygen/src/hud/chat.rs +++ b/voxygen/src/hud/chat.rs @@ -37,6 +37,7 @@ pub struct Chat<'a> { new_messages: &'a mut VecDeque, force_input: Option, force_cursor: Option, + force_completions: Option>, global_state: &'a GlobalState, imgs: &'a Imgs, @@ -60,6 +61,7 @@ impl<'a> Chat<'a> { new_messages, force_input: None, force_cursor: None, + force_completions: None, imgs, fonts, global_state, @@ -68,11 +70,17 @@ impl<'a> Chat<'a> { } } + pub fn prepare_tab_completion(mut self, input: String, state: &common::state::State) -> Self { + if let Some(index) = input.find('\t') { + self.force_completions = Some(common::cmd::complete(&input[..index], &state)); + } else { + self.force_completions = None; + } + self + } + pub fn input(mut self, input: String) -> Self { if let Ok(()) = validate_chat_msg(&input) { - if input.contains('\t') { - println!("Contains tab: '{}'", input); - } self.force_input = Some(input); } self @@ -107,12 +115,14 @@ pub struct State { // otherwise index is history_pos -1 history_pos: usize, completions: Vec, - // Index into the completion Vec, completions_pos == 0 means not in use - // otherwise index is completions_pos -1 - completions_pos: usize, + // Index into the completion Vec + completions_index: Option, + // At which character is tab completion happening + completion_cursor: Option, } pub enum Event { + TabCompletionStart(String), SendMessage(String), Focus(Id), } @@ -129,7 +139,8 @@ impl<'a> Widget for Chat<'a> { history: VecDeque::new(), history_pos: 0, completions: Vec::new(), - completions_pos: 0, + completions_index: None, + completion_cursor: None, ids: Ids::new(id_gen), } } @@ -152,21 +163,74 @@ impl<'a> Widget for Chat<'a> { } }); + if let Some(comps) = &self.force_completions { + state.update(|s| s.completions = comps.clone()); + } + let mut force_cursor = self.force_cursor; - // If up or down are pressed move through history - let history_move = - ui.widget_input(state.ids.chat_input) - .presses() - .key() - .fold(0, |n, key_press| match key_press.key { - Key::Up => n + 1, - Key::Down => n - 1, - _ => n, - }); - if history_move != 0 { + // If up or down are pressed: move through history + // If any key other than up, down, or tab is pressed: stop completion. + let (history_dir, tab_dir, stop_tab_completion) = + ui.widget_input(state.ids.chat_input).presses().key().fold( + (0isize, 0isize, false), + |(n, m, tc), key_press| match key_press.key { + Key::Up => (n + 1, m - 1, tc), + Key::Down => (n - 1, m + 1, tc), + Key::Tab => (n, m + 1, tc), + _ => (n, m, true), + }, + ); + + // Handle tab completion + let request_tab_completions = if stop_tab_completion { + // End tab completion state.update(|s| { - if history_move > 0 { + if s.completion_cursor.is_some() { + s.completion_cursor = None; + } + s.completions_index = None; + }); + false + } else if let Some(cursor) = state.completion_cursor { + // Cycle through tab completions of the current word + if state.input.contains('\t') { + state.update(|s| s.input.retain(|c| c != '\t')); + //tab_dir + 1 + } + if !state.completions.is_empty() { + if tab_dir != 0 || state.completions_index.is_none() { + state.update(|s| { + let len = s.completions.len(); + s.completions_index = Some( + (s.completions_index.unwrap_or(0) + (tab_dir + len as isize) as usize) + % len, + ); + if let Some(replacement) = &s.completions.get(s.completions_index.unwrap()) + { + let (completed, offset) = + do_tab_completion(cursor, &s.input, replacement); + force_cursor = + cursor_offset_to_index(offset, &completed, &ui, &self.fonts); + s.input = completed; + } + }); + } + } + false + } else if let Some(cursor) = state.input.find('\t') { + // Begin tab completion + state.update(|s| s.completion_cursor = Some(cursor)); + true + } else { + // Not tab completing + false + }; + + // Move through history + if history_dir != 0 && state.completion_cursor.is_none() { + state.update(|s| { + if history_dir > 0 { if s.history_pos < s.history.len() { s.history_pos += 1; } @@ -185,33 +249,6 @@ impl<'a> Widget for Chat<'a> { }); } - // Handle tab-completion - if let Some(cursor) = state.input.find('\t') { - state.update(|s| { - if s.completions_pos > 0 { - if s.completions_pos >= s.completions.len() { - s.completions_pos = 1; - } else { - s.completions_pos += 1; - } - } else { - // TODO FIXME pull completions from common::cmd - s.completions = "a,bc,def,ghi,jklm,nop,qr,stu,v,w,xyz" - .split(",") - .map(|x| x.to_string()) - .collect(); - s.completions_pos = 1; - } - //let index = force_cursor; - //let cursor = index.and_then(|index| cursor_index_to_offset(index, &s.input, - // ui, &self.fonts)).unwrap_or(0); - let replacement = &s.completions[s.completions_pos - 1]; - let (completed, offset) = do_tab_completion(cursor, &s.input, replacement); - force_cursor = cursor_offset_to_index(offset, &completed, &ui, &self.fonts); - s.input = completed; - }); - } - let keyboard_capturer = ui.global_input().current.widget_capturing_keyboard; if let Some(input) = &self.force_input { @@ -255,9 +292,6 @@ impl<'a> Widget for Chat<'a> { let mut input = str.to_owned(); input.retain(|c| c != '\n'); if let Ok(()) = validate_chat_msg(&input) { - if input.contains('\t') { - println!("Contains tab: '{}'", input); - } state.update(|s| s.input = input); } } @@ -345,9 +379,12 @@ impl<'a> Widget for Chat<'a> { } } - // If the chat widget is focused, return a focus event to pass the focus to the - // input box. - if keyboard_capturer == Some(id) { + // We've started a new tab completion. Populate tab completion suggestions. + if request_tab_completions { + Some(Event::TabCompletionStart(state.input.to_string())) + // If the chat widget is focused, return a focus event to pass the focus + // to the input box. + } else if keyboard_capturer == Some(id) { Some(Event::Focus(state.ids.chat_input)) } // If enter is pressed and the input box is not empty, send the current message. diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index 5f802f22e3..eff6563521 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -443,6 +443,7 @@ pub struct Hud { force_ungrab: bool, force_chat_input: Option, force_chat_cursor: Option, + tab_complete: Option, pulse: f32, velocity: f32, voxygen_i18n: std::sync::Arc, @@ -516,6 +517,7 @@ impl Hud { force_ungrab: false, force_chat_input: None, force_chat_cursor: None, + tab_complete: None, pulse: 0.0, velocity: 0.0, voxygen_i18n, @@ -1724,9 +1726,15 @@ impl Hud { &self.fonts, ) .and_then(self.force_chat_input.take(), |c, input| c.input(input)) + .and_then(self.tab_complete.take(), |c, input| { + c.prepare_tab_completion(input, &client.state()) + }) .and_then(self.force_chat_cursor.take(), |c, pos| c.cursor_pos(pos)) .set(self.ids.chat, ui_widgets) { + Some(chat::Event::TabCompletionStart(input)) => { + self.tab_complete = Some(input); + }, Some(chat::Event::SendMessage(message)) => { events.push(Event::SendMessage(message)); },