Finish tab completion implementation

This commit is contained in:
CapsizeGlimmer 2020-05-08 17:38:58 -04:00
parent b0f0d716be
commit 28e94afd3f
3 changed files with 137 additions and 78 deletions

View File

@ -183,7 +183,7 @@ impl ChatCommand {
}, },
ChatCommand::Spawn => cmd(vec![/*TODO*/], "Spawn a test entity", true), ChatCommand::Spawn => cmd(vec![/*TODO*/], "Spawn a test entity", true),
ChatCommand::Sudo => cmd( ChatCommand::Sudo => cmd(
vec![PlayerName(false), Command(false), SubCommand], vec![PlayerName(false), SubCommand],
"Run command as if you were another player", "Run command as if you were another player",
true, true,
), ),
@ -251,12 +251,12 @@ impl ChatCommand {
.map(|arg| match arg { .map(|arg| match arg {
ArgumentSpec::PlayerName(_) => "{}", ArgumentSpec::PlayerName(_) => "{}",
ArgumentSpec::ItemSpec(_) => "{}", ArgumentSpec::ItemSpec(_) => "{}",
ArgumentSpec::Float(_, _, _) => "{f}", ArgumentSpec::Float(_, _, _) => "{}",
ArgumentSpec::Integer(_, _, _) => "{d}", ArgumentSpec::Integer(_, _, _) => "{d}",
ArgumentSpec::Any(_, _) => "{}", ArgumentSpec::Any(_, _) => "{}",
ArgumentSpec::Command(_) => "{}", ArgumentSpec::Command(_) => "{}",
ArgumentSpec::Message => "{/.*/}", ArgumentSpec::Message => "{/.*/}",
ArgumentSpec::SubCommand => "{/.*/}", ArgumentSpec::SubCommand => "{} {/.*/}",
ArgumentSpec::OneOf(_, _, _, _) => "{}", // TODO ArgumentSpec::OneOf(_, _, _, _) => "{}", // TODO
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
@ -406,34 +406,43 @@ impl ArgumentSpec {
} }
fn complete_player(part: &str, state: &State) -> Vec<String> { fn complete_player(part: &str, state: &State) -> Vec<String> {
println!("Player completion: '{}'", part); let storage = state.ecs().read_storage::<Player>();
state.ecs().read_storage::<Player>() let mut iter = storage.join();
.join() if let Some(first) = iter.next() {
.inspect(|player| println!(" player: {}", player.alias)) std::iter::once(first)
.chain(iter)
.filter(|player| player.alias.starts_with(part)) .filter(|player| player.alias.starts_with(part))
.map(|player| player.alias.clone()) .map(|player| player.alias.clone())
.collect() .collect()
} else {
vec!["singleplayer".to_string()]
}
} }
fn complete_command(part: &str) -> Vec<String> { fn complete_command(part: &str) -> Vec<String> {
println!("Command completion: '{}'", part);
CHAT_COMMANDS CHAT_COMMANDS
.iter() .iter()
.map(|com| com.keyword()) .map(|com| com.keyword())
.filter(|kwd| kwd.starts_with(part) || format!("/{}", kwd).starts_with(part)) .filter(|kwd| kwd.starts_with(part) || format!("/{}", kwd).starts_with(part))
.map(|c| c.to_string()) .map(|c| format!("/{}", c))
.collect() .collect()
} }
// Get the byte index of the nth word. Used in completing "/sudo p /subcmd"
fn nth_word(line: &str, n: usize) -> Option<usize> { fn nth_word(line: &str, n: usize) -> Option<usize> {
let mut is_space = false; let mut is_space = false;
let mut j = 0; let mut j = 0;
for (i, c) in line.char_indices() { for (i, c) in line.char_indices() {
match (is_space, c.is_whitespace()) { match (is_space, c.is_whitespace()) {
(true, true) => {} (true, true) => {},
(true, false) => { is_space = false; } (true, false) => {
(false, true) => { is_space = true; j += 1; } is_space = false;
(false, false) => {} j += 1;
},
(false, true) => {
is_space = true;
},
(false, false) => {},
} }
if j == n { if j == n {
return Some(i); return Some(i);
@ -443,16 +452,25 @@ fn nth_word(line: &str, n: usize) -> Option<usize> {
} }
pub fn complete(line: &str, state: &State) -> Vec<String> { pub fn complete(line: &str, state: &State) -> Vec<String> {
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('/') { if line.chars().next() == Some('/') {
let mut iter = line.split_whitespace(); let mut iter = line.split_whitespace();
let cmd = iter.next().unwrap(); 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::<ChatCommand>() { if let Ok(cmd) = cmd.parse::<ChatCommand>() {
if let Some(arg) = cmd.data().args.get(i) { if let Some(arg) = cmd.data().args.get(i - 1) {
println!("Arg completion: {}", word); // Complete ith argument
arg.complete(word, &state) arg.complete(word, &state)
} else { } else {
// Complete past the last argument
match cmd.data().args.last() { match cmd.data().args.last() {
Some(ArgumentSpec::SubCommand) => { Some(ArgumentSpec::SubCommand) => {
if let Some(index) = nth_word(line, cmd.data().args.len()) { if let Some(index) = nth_word(line, cmd.data().args.len()) {
@ -461,22 +479,18 @@ pub fn complete(line: &str, state: &State) -> Vec<String> {
error!("Could not tab-complete SubCommand"); error!("Could not tab-complete SubCommand");
vec![] vec![]
} }
} },
Some(ArgumentSpec::Message) => complete_player(word, &state), Some(ArgumentSpec::Message) => complete_player(word, &state),
_ => { vec![] } // End of command. Nothing to complete _ => vec![], // End of command. Nothing to complete
} }
} }
} else { } else {
// Completing for unknown chat command // Completing for unknown chat command
complete_player(word, &state) complete_player(word, &state)
} }
} else {
// Completing chat command name
complete_command(word)
} }
} else { } else {
// Not completing a command // Not completing a command
complete_player(word, &state) complete_player(word, &state)
} }
} }

View File

@ -37,6 +37,7 @@ pub struct Chat<'a> {
new_messages: &'a mut VecDeque<ClientEvent>, new_messages: &'a mut VecDeque<ClientEvent>,
force_input: Option<String>, force_input: Option<String>,
force_cursor: Option<Index>, force_cursor: Option<Index>,
force_completions: Option<Vec<String>>,
global_state: &'a GlobalState, global_state: &'a GlobalState,
imgs: &'a Imgs, imgs: &'a Imgs,
@ -60,6 +61,7 @@ impl<'a> Chat<'a> {
new_messages, new_messages,
force_input: None, force_input: None,
force_cursor: None, force_cursor: None,
force_completions: None,
imgs, imgs,
fonts, fonts,
global_state, 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 { pub fn input(mut self, input: String) -> Self {
if let Ok(()) = validate_chat_msg(&input) { if let Ok(()) = validate_chat_msg(&input) {
if input.contains('\t') {
println!("Contains tab: '{}'", input);
}
self.force_input = Some(input); self.force_input = Some(input);
} }
self self
@ -107,12 +115,14 @@ pub struct State {
// otherwise index is history_pos -1 // otherwise index is history_pos -1
history_pos: usize, history_pos: usize,
completions: Vec<String>, completions: Vec<String>,
// Index into the completion Vec, completions_pos == 0 means not in use // Index into the completion Vec
// otherwise index is completions_pos -1 completions_index: Option<usize>,
completions_pos: usize, // At which character is tab completion happening
completion_cursor: Option<usize>,
} }
pub enum Event { pub enum Event {
TabCompletionStart(String),
SendMessage(String), SendMessage(String),
Focus(Id), Focus(Id),
} }
@ -129,7 +139,8 @@ impl<'a> Widget for Chat<'a> {
history: VecDeque::new(), history: VecDeque::new(),
history_pos: 0, history_pos: 0,
completions: Vec::new(), completions: Vec::new(),
completions_pos: 0, completions_index: None,
completion_cursor: None,
ids: Ids::new(id_gen), 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; let mut force_cursor = self.force_cursor;
// If up or down are pressed move through history // If up or down are pressed: move through history
let history_move = // If any key other than up, down, or tab is pressed: stop completion.
ui.widget_input(state.ids.chat_input) let (history_dir, tab_dir, stop_tab_completion) =
.presses() ui.widget_input(state.ids.chat_input).presses().key().fold(
.key() (0isize, 0isize, false),
.fold(0, |n, key_press| match key_press.key { |(n, m, tc), key_press| match key_press.key {
Key::Up => n + 1, Key::Up => (n + 1, m - 1, tc),
Key::Down => n - 1, Key::Down => (n - 1, m + 1, tc),
_ => n, Key::Tab => (n, m + 1, tc),
}); _ => (n, m, true),
if history_move != 0 { },
);
// Handle tab completion
let request_tab_completions = if stop_tab_completion {
// End tab completion
state.update(|s| { 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() { if s.history_pos < s.history.len() {
s.history_pos += 1; 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; let keyboard_capturer = ui.global_input().current.widget_capturing_keyboard;
if let Some(input) = &self.force_input { if let Some(input) = &self.force_input {
@ -255,9 +292,6 @@ impl<'a> Widget for Chat<'a> {
let mut input = str.to_owned(); let mut input = str.to_owned();
input.retain(|c| c != '\n'); input.retain(|c| c != '\n');
if let Ok(()) = validate_chat_msg(&input) { if let Ok(()) = validate_chat_msg(&input) {
if input.contains('\t') {
println!("Contains tab: '{}'", input);
}
state.update(|s| s.input = 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 // We've started a new tab completion. Populate tab completion suggestions.
// input box. if request_tab_completions {
if keyboard_capturer == Some(id) { 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)) Some(Event::Focus(state.ids.chat_input))
} }
// If enter is pressed and the input box is not empty, send the current message. // If enter is pressed and the input box is not empty, send the current message.

View File

@ -443,6 +443,7 @@ pub struct Hud {
force_ungrab: bool, force_ungrab: bool,
force_chat_input: Option<String>, force_chat_input: Option<String>,
force_chat_cursor: Option<Index>, force_chat_cursor: Option<Index>,
tab_complete: Option<String>,
pulse: f32, pulse: f32,
velocity: f32, velocity: f32,
voxygen_i18n: std::sync::Arc<VoxygenLocalization>, voxygen_i18n: std::sync::Arc<VoxygenLocalization>,
@ -516,6 +517,7 @@ impl Hud {
force_ungrab: false, force_ungrab: false,
force_chat_input: None, force_chat_input: None,
force_chat_cursor: None, force_chat_cursor: None,
tab_complete: None,
pulse: 0.0, pulse: 0.0,
velocity: 0.0, velocity: 0.0,
voxygen_i18n, voxygen_i18n,
@ -1724,9 +1726,15 @@ impl Hud {
&self.fonts, &self.fonts,
) )
.and_then(self.force_chat_input.take(), |c, input| c.input(input)) .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)) .and_then(self.force_chat_cursor.take(), |c, pos| c.cursor_pos(pos))
.set(self.ids.chat, ui_widgets) .set(self.ids.chat, ui_widgets)
{ {
Some(chat::Event::TabCompletionStart(input)) => {
self.tab_complete = Some(input);
},
Some(chat::Event::SendMessage(message)) => { Some(chat::Event::SendMessage(message)) => {
events.push(Event::SendMessage(message)); events.push(Event::SendMessage(message));
}, },