Fixed player list tab completion

This commit is contained in:
CapsizeGlimmer 2020-05-09 16:41:29 -04:00
parent b486de28ac
commit 9d118b55a0
7 changed files with 158 additions and 149 deletions

121
client/src/cmd.rs Normal file
View File

@ -0,0 +1,121 @@
use crate::Client;
use common::cmd::*;
trait TabComplete {
fn complete(&self, part: &str, client: &Client) -> Vec<String>;
}
impl TabComplete for ArgumentSpec {
fn complete(&self, part: &str, client: &Client) -> Vec<String> {
match self {
ArgumentSpec::PlayerName(_) => complete_player(part, &client),
ArgumentSpec::Float(_, x, _) => {
if part.is_empty() {
vec![format!("{:.1}", x)]
} else {
vec![]
}
},
ArgumentSpec::Integer(_, x, _) => {
if part.is_empty() {
vec![format!("{}", x)]
} else {
vec![]
}
},
ArgumentSpec::Any(_, _) => vec![],
ArgumentSpec::Command(_) => complete_command(part),
ArgumentSpec::Message => complete_player(part, &client),
ArgumentSpec::SubCommand => complete_command(part),
ArgumentSpec::Enum(_, strings, _) => strings
.iter()
.filter(|string| string.starts_with(part))
.map(|c| c.to_string())
.collect(),
}
}
}
fn complete_player(part: &str, client: &Client) -> Vec<String> {
client
.player_list
.values()
.filter(|alias| alias.starts_with(part))
.cloned()
.collect()
}
fn complete_command(part: &str) -> Vec<String> {
CHAT_COMMANDS
.iter()
.map(|com| com.keyword())
.filter(|kwd| kwd.starts_with(part) || format!("/{}", kwd).starts_with(part))
.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;
j += 1;
},
(false, true) => {
is_space = true;
},
(false, false) => {},
}
if j == n {
return Some(i);
}
}
return None;
}
pub fn complete(line: &str, client: &Client) -> Vec<String> {
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();
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 - 1) {
// Complete ith argument
arg.complete(word, &client)
} 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()) {
complete(&line[index..], &client)
} else {
vec![]
}
},
Some(ArgumentSpec::Message) => complete_player(word, &client),
_ => vec![], // End of command. Nothing to complete
}
}
} else {
// Completing for unknown chat command
complete_player(word, &client)
}
}
} else {
// Not completing a command
complete_player(word, &client)
}
}

View File

@ -1,6 +1,7 @@
#![deny(unsafe_code)]
#![feature(label_break_value)]
pub mod cmd;
pub mod error;
// Reexports

View File

@ -12,7 +12,7 @@ use std::{
fmt,
fs::{self, File, ReadDir},
io::{BufReader, Read},
path::PathBuf,
path::{Path, PathBuf},
sync::{Arc, RwLock},
};
@ -59,6 +59,31 @@ lazy_static! {
/// The HashMap where all loaded assets are stored in.
pub static ref ASSETS: RwLock<HashMap<String, Arc<dyn Any + 'static + Sync + Send>>> =
RwLock::new(HashMap::new());
/// List of item specifiers. Used for tab completing
pub static ref ITEM_SPECS: Vec<String> = {
let base = ASSETS_PATH.join("common").join("items");
let mut items = vec![];
fn list_items (path: &Path, base: &Path, mut items: &mut Vec<String>) -> std::io::Result<()>{
for entry in std::fs::read_dir(path)? {
let path = entry?.path();
if path.is_dir(){
list_items(&path, &base, &mut items)?;
} else {
if let Ok(path) = path.strip_prefix(base) {
let path = path.to_string_lossy().trim_end_matches(".ron").replace('/', ".");
items.push(path);
}
}
}
Ok(())
}
if list_items(&base, &ASSETS_PATH, &mut items).is_err() {
warn!("There was a problem listing some item assets");
}
items.sort();
items
};
}
// TODO: Remove this function. It's only used in world/ in a really ugly way.To

View File

@ -1,6 +1,5 @@
use crate::{assets, comp, npc, state::State};
use crate::{assets, comp, npc};
use lazy_static::lazy_static;
use specs::prelude::{Join, WorldExt};
use std::{ops::Deref, str::FromStr};
/// Struct representing a command that a user can run from server chat.
@ -110,23 +109,6 @@ lazy_static! {
.map(|s| s.to_string())
.collect();
}
fn items() -> Vec<String> {
if let Ok(assets) = assets::ASSETS.read() {
assets
.iter()
.flat_map(|(k, v)| {
if v.is::<comp::item::Item>() {
Some(k.clone())
} else {
None
}
})
.collect()
} else {
error!("Assets not found");
vec![]
}
}
impl ChatCommand {
pub fn data(&self) -> ChatCommandData {
@ -143,10 +125,7 @@ impl ChatCommand {
ChatCommand::Build => cmd(vec![], "Toggles build mode on and off", true),
ChatCommand::Debug => cmd(vec![], "Place all debug items into your pack.", true),
ChatCommand::DebugColumn => cmd(
vec![
Integer("x", 15000, Required),
Integer("y", 15000, Required),
],
vec![Integer("x", 15000, Required), Integer("y", 15000, Required)],
"Prints some debug information about a column",
false,
),
@ -161,7 +140,10 @@ impl ChatCommand {
true,
),
ChatCommand::GiveItem => cmd(
vec![Enum("item", items(), Required), Integer("num", 1, Optional)],
vec![
Enum("item", assets::ITEM_SPECS.clone(), Required),
Integer("num", 1, Optional),
],
"Give yourself some items",
true,
),
@ -444,123 +426,4 @@ impl ArgumentSpec {
},
}
}
pub fn complete(&self, part: &str, state: &State) -> Vec<String> {
match self {
ArgumentSpec::PlayerName(_) => complete_player(part, &state),
ArgumentSpec::Float(_, x, _) => {
if part.is_empty() {
vec![format!("{:.1}", x)]
} else {
vec![]
}
},
ArgumentSpec::Integer(_, x, _) => {
if part.is_empty() {
vec![format!("{}", x)]
} else {
vec![]
}
},
ArgumentSpec::Any(_, _) => vec![],
ArgumentSpec::Command(_) => complete_command(part),
ArgumentSpec::Message => complete_player(part, &state),
ArgumentSpec::SubCommand => complete_command(part),
ArgumentSpec::Enum(_, strings, _) => strings
.iter()
.filter(|string| string.starts_with(part))
.map(|c| c.to_string())
.collect(),
}
}
}
fn complete_player(part: &str, state: &State) -> Vec<String> {
let storage = state.ecs().read_storage::<comp::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> {
CHAT_COMMANDS
.iter()
.map(|com| com.keyword())
.filter(|kwd| kwd.starts_with(part) || format!("/{}", kwd).starts_with(part))
.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;
j += 1;
},
(false, true) => {
is_space = true;
},
(false, false) => {},
}
if j == n {
return Some(i);
}
}
return None;
}
pub fn complete(line: &str, state: &State) -> Vec<String> {
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();
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 - 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()) {
complete(&line[index..], &state)
} else {
error!("Could not tab-complete SubCommand");
vec![]
}
},
Some(ArgumentSpec::Message) => complete_player(word, &state),
_ => vec![], // End of command. Nothing to complete
}
}
} else {
// Completing for unknown chat command
complete_player(word, &state)
}
}
} else {
// Not completing a command
complete_player(word, &state)
}
}

View File

@ -724,7 +724,6 @@ fn handle_lantern(
args: String,
action: &ChatCommand,
) {
println!("args: '{}', fmt: '{}'", &args, &action.arg_fmt());
if let (Some(s), r, g, b) = scan_fmt_some!(&args, &action.arg_fmt(), f32, f32, f32, f32) {
if let Some(light) = server
.state

View File

@ -3,7 +3,7 @@ use super::{
META_COLOR, PRIVATE_COLOR, SAY_COLOR, TELL_COLOR, TEXT_COLOR,
};
use crate::{ui::fonts::ConrodVoxygenFonts, GlobalState};
use client::Event as ClientEvent;
use client::{cmd, Client, Event as ClientEvent};
use common::{msg::validate_chat_msg, ChatType};
use conrod_core::{
input::Key,
@ -68,9 +68,9 @@ impl<'a> Chat<'a> {
}
}
pub fn prepare_tab_completion(mut self, input: String, state: &common::state::State) -> Self {
pub fn prepare_tab_completion(mut self, input: String, client: &Client) -> Self {
if let Some(index) = input.find('\t') {
self.force_completions = Some(common::cmd::complete(&input[..index], &state));
self.force_completions = Some(cmd::complete(&input[..index], &client));
} else {
self.force_completions = None;
}

View File

@ -1727,7 +1727,7 @@ impl Hud {
)
.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())
c.prepare_tab_completion(input, &client)
})
.and_then(self.force_chat_cursor.take(), |c, pos| c.cursor_pos(pos))
.set(self.ids.chat, ui_widgets)