mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Finish tab completion implementation
This commit is contained in:
parent
b0f0d716be
commit
28e94afd3f
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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));
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user