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::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::<Vec<_>>()
@ -406,34 +406,43 @@ impl ArgumentSpec {
}
fn complete_player(part: &str, state: &State) -> Vec<String> {
println!("Player completion: '{}'", part);
state.ecs().read_storage::<Player>()
.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::<Player>();
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<String> {
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<usize> {
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<usize> {
}
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('/') {
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::<ChatCommand>() {
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<String> {
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)
}
}

View File

@ -37,6 +37,7 @@ pub struct Chat<'a> {
new_messages: &'a mut VecDeque<ClientEvent>,
force_input: Option<String>,
force_cursor: Option<Index>,
force_completions: Option<Vec<String>>,
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<String>,
// 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<usize>,
// At which character is tab completion happening
completion_cursor: Option<usize>,
}
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.

View File

@ -443,6 +443,7 @@ pub struct Hud {
force_ungrab: bool,
force_chat_input: Option<String>,
force_chat_cursor: Option<Index>,
tab_complete: Option<String>,
pulse: f32,
velocity: f32,
voxygen_i18n: std::sync::Arc<VoxygenLocalization>,
@ -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));
},