From 307e47867122d8ff18b8cd4b6b9fa45e6eb1369a Mon Sep 17 00:00:00 2001
From: CapsizeGlimmer <>
Date: Tue, 5 May 2020 00:06:36 -0400
Subject: [PATCH 1/9] Groundwork for tab completion of player names and command
 arguments

---
 common/src/assets/mod.rs |   7 ++
 common/src/cmd.rs        | 156 +++++++++++++++++++++++++++++++++++++++
 common/src/lib.rs        |   1 +
 3 files changed, 164 insertions(+)
 create mode 100644 common/src/cmd.rs

diff --git a/common/src/assets/mod.rs b/common/src/assets/mod.rs
index b7a3e63627..a14ca93cf2 100644
--- a/common/src/assets/mod.rs
+++ b/common/src/assets/mod.rs
@@ -61,6 +61,13 @@ lazy_static! {
         RwLock::new(HashMap::new());
 }
 
+const ASSETS_TMP: [&'static str; 1] = ["common/items/lantern/black_0"];
+pub fn iterate() -> impl Iterator<Item = &'static str> {
+    // TODO FIXME implement this
+    //ASSETS.read().iter().flat_map(|e| e.keys())
+    ASSETS_TMP.iter().map(|k| *k)
+}
+
 // TODO: Remove this function. It's only used in world/ in a really ugly way.To
 // do this properly assets should have all their necessary data in one file. A
 // ron file could be used to combine voxel data with positioning data for
diff --git a/common/src/cmd.rs b/common/src/cmd.rs
new file mode 100644
index 0000000000..2ab72f83a9
--- /dev/null
+++ b/common/src/cmd.rs
@@ -0,0 +1,156 @@
+use crate::{assets, comp::Player, state::State};
+use lazy_static::lazy_static;
+use specs::prelude::{Join, WorldExt};
+
+
+/// Struct representing a command that a user can run from server chat.
+pub struct ChatCommand {
+    /// The keyword used to invoke the command, omitting the leading '/'.
+    pub keyword: &'static str,
+    /// A format string for parsing arguments.
+    pub args: Vec<ArgumentSyntax>,
+    /// A one-line message that explains what the command does
+    pub description: &'static str,
+    /// A boolean that is used to check whether the command requires
+    /// administrator permissions or not.
+    pub needs_admin: bool,
+}
+
+impl ChatCommand {
+    pub fn new(
+        keyword: &'static str,
+        args: Vec<ArgumentSyntax>,
+        description: &'static str,
+        needs_admin: bool,
+    ) -> Self {
+        Self {
+            keyword,
+            args,
+            description,
+            needs_admin,
+        }
+    }
+}
+
+lazy_static! {
+    static ref CHAT_COMMANDS: Vec<ChatCommand> = {
+        use ArgumentSyntax::*;
+        vec![
+            ChatCommand::new("help", vec![Command(true)], "Display information about commands", false),
+        ]
+    };
+}
+
+/// Representation for chat command arguments
+pub enum ArgumentSyntax {
+    /// The argument refers to a player by alias
+    PlayerName(bool),
+    /// The argument refers to an item asset by path
+    ItemSpec(bool),
+    /// The argument is a float. The associated values are
+    /// * label
+    /// * default tab-completion
+    /// * whether it's optional
+    Float(&'static str, f32, bool),
+    /// The argument is a float. The associated values are
+    /// * label
+    /// * default tab-completion
+    /// * whether it's optional
+    Integer(&'static str, f32, bool),
+    /// The argument is a command name
+    Command(bool),
+    /// This is the final argument, consuming all characters until the end of input.
+    Message,
+    /// The argument is likely an enum. The associated values are
+    /// * label
+    /// * Predefined string completions
+    /// * Other completion types
+    /// * whether it's optional
+    OneOf(&'static str, &'static [&'static str], Vec<Box<ArgumentSyntax>>, bool),
+}
+
+impl ArgumentSyntax {
+    pub fn help_string(arg: &ArgumentSyntax) -> String {
+        match arg {
+            ArgumentSyntax::PlayerName(optional) => {
+                if *optional {
+                    "[player]".to_string()
+                } else {
+                    "<player>".to_string()
+                }
+            },
+            ArgumentSyntax::ItemSpec(optional) => {
+                if *optional {
+                    "[item]".to_string()
+                } else {
+                    "<item>".to_string()
+                }
+            },
+            ArgumentSyntax::Float(label, _, optional) => {
+                if *optional {
+                    format!("[{}]", label)
+                } else {
+                    format!("<{}>", label)
+                }
+            },
+            ArgumentSyntax::Integer(label, _, optional) => {
+                if *optional {
+                    format!("[{}]", label)
+                } else {
+                    format!("<{}>", label)
+                }
+            },
+            ArgumentSyntax::Command(optional) => {
+                if *optional {
+                    "[[/]command]".to_string()
+                } else {
+                    "<[/]command>".to_string()
+                }
+            },
+            ArgumentSyntax::Message => {
+                "<message>".to_string()
+            },
+            ArgumentSyntax::OneOf(label, _, _, optional) => {
+                if *optional {
+                    format! {"[{}]", label}
+                } else {
+                    format! {"<{}>", label}
+                }
+            },
+        }
+    }
+
+    pub fn complete(&self, state: &State, part: &String) -> Vec<String> {
+        match self {
+            ArgumentSyntax::PlayerName(_) => (&state.ecs().read_storage::<Player>())
+                .join()
+                .filter(|player| player.alias.starts_with(part))
+                .map(|player| player.alias.clone())
+                .collect(),
+            ArgumentSyntax::ItemSpec(_) => assets::iterate()
+                .filter(|asset| asset.starts_with(part))
+                .map(|c| c.to_string())
+                .collect(),
+            ArgumentSyntax::Float(_, x, _) => vec![format!("{}", x)],
+            ArgumentSyntax::Integer(_, x, _) => vec![format!("{}", x)],
+            ArgumentSyntax::Command(_) => CHAT_COMMANDS
+                .iter()
+                .map(|com| com.keyword.clone())
+                .filter(|kwd| kwd.starts_with(part) || format!("/{}", kwd).starts_with(part))
+                .map(|c| c.to_string())
+                .collect(),
+            ArgumentSyntax::Message => vec![],
+            ArgumentSyntax::OneOf(_, strings, alts, _) => {
+                let string_completions = strings
+                    .iter()
+                    .filter(|string| string.starts_with(part))
+                    .map(|c| c.to_string());
+                let alt_completions = alts
+                    .iter()
+                    .flat_map(|b| (*b).complete(&state, part))
+                    .map(|c| c.to_string());
+                string_completions.chain(alt_completions).collect()
+            }
+        }
+    }
+}
diff --git a/common/src/lib.rs b/common/src/lib.rs
index 1ea3df4c51..12600ed3d4 100644
--- a/common/src/lib.rs
+++ b/common/src/lib.rs
@@ -16,6 +16,7 @@ pub mod assets;
 pub mod astar;
 pub mod character;
 pub mod clock;
+pub mod cmd;
 pub mod comp;
 pub mod effect;
 pub mod event;

From 7ecea34f8525d85c72f5616f3d30a43da29ca669 Mon Sep 17 00:00:00 2001
From: CapsizeGlimmer <>
Date: Tue, 5 May 2020 18:33:16 -0400
Subject: [PATCH 2/9] Server server::cmd depends on common::cmd

---
 common/src/cmd.rs | 346 ++++++++++++++++++++++++++++++++-----
 server/src/cmd.rs | 422 ++++++++++++++--------------------------------
 server/src/lib.rs |  25 ++-
 3 files changed, 440 insertions(+), 353 deletions(-)

diff --git a/common/src/cmd.rs b/common/src/cmd.rs
index 2ab72f83a9..bb134a5741 100644
--- a/common/src/cmd.rs
+++ b/common/src/cmd.rs
@@ -1,14 +1,10 @@
 use crate::{assets, comp::Player, state::State};
-use lazy_static::lazy_static;
 use specs::prelude::{Join, WorldExt};
 
-
 /// Struct representing a command that a user can run from server chat.
-pub struct ChatCommand {
-    /// The keyword used to invoke the command, omitting the leading '/'.
-    pub keyword: &'static str,
+pub struct ChatCommandData {
     /// A format string for parsing arguments.
-    pub args: Vec<ArgumentSyntax>,
+    pub args: Vec<ArgumentSpec>,
     /// A one-line message that explains what the command does
     pub description: &'static str,
     /// A boolean that is used to check whether the command requires
@@ -16,15 +12,9 @@ pub struct ChatCommand {
     pub needs_admin: bool,
 }
 
-impl ChatCommand {
-    pub fn new(
-        keyword: &'static str,
-        args: Vec<ArgumentSyntax>,
-        description: &'static str,
-        needs_admin: bool,
-    ) -> Self {
+impl ChatCommandData {
+    pub fn new(args: Vec<ArgumentSpec>, description: &'static str, needs_admin: bool) -> Self {
         Self {
-            keyword,
             args,
             description,
             needs_admin,
@@ -32,17 +22,267 @@ impl ChatCommand {
     }
 }
 
-lazy_static! {
-    static ref CHAT_COMMANDS: Vec<ChatCommand> = {
-        use ArgumentSyntax::*;
-        vec![
-            ChatCommand::new("help", vec![Command(true)], "Display information about commands", false),
-        ]
-    };
+// Please keep this sorted alphabetically :-)
+#[derive(Copy, Clone)]
+pub enum ChatCommand {
+    Adminify,
+    Alias,
+    Build,
+    Debug,
+    DebugColumn,
+    Explosion,
+    GiveExp,
+    GiveItem,
+    Goto,
+    Health,
+    Help,
+    Jump,
+    Kill,
+    KillNpcs,
+    Lantern,
+    Light,
+    Object,
+    Players,
+    RemoveLights,
+    SetLevel,
+    Spawn,
+    Sudo,
+    Tell,
+    Time,
+    Tp,
+    Version,
+    Waypoint,
+}
+
+// Thank you for keeping this sorted alphabetically :-)
+pub static CHAT_COMMANDS: &'static [ChatCommand] = &[
+    ChatCommand::Adminify,
+    ChatCommand::Alias,
+    ChatCommand::Build,
+    ChatCommand::Debug,
+    ChatCommand::DebugColumn,
+    ChatCommand::Explosion,
+    ChatCommand::GiveExp,
+    ChatCommand::GiveItem,
+    ChatCommand::Goto,
+    ChatCommand::Health,
+    ChatCommand::Help,
+    ChatCommand::Jump,
+    ChatCommand::Kill,
+    ChatCommand::KillNpcs,
+    ChatCommand::Lantern,
+    ChatCommand::Light,
+    ChatCommand::Object,
+    ChatCommand::Players,
+    ChatCommand::RemoveLights,
+    ChatCommand::SetLevel,
+    ChatCommand::Spawn,
+    ChatCommand::Sudo,
+    ChatCommand::Tell,
+    ChatCommand::Time,
+    ChatCommand::Tp,
+    ChatCommand::Version,
+    ChatCommand::Waypoint,
+];
+
+impl ChatCommand {
+    pub fn data(&self) -> ChatCommandData {
+        use ArgumentSpec::*;
+        let cmd = ChatCommandData::new;
+        match self {
+            ChatCommand::Adminify => cmd(
+                vec![PlayerName(false)],
+                "Temporarily gives a player admin permissions or removes them",
+                true,
+            ),
+            ChatCommand::Alias => cmd(vec![Any("name", false)], "Change your alias", false),
+            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![Float("x", f32::NAN, false), Float("y", f32::NAN, false)],
+                "Prints some debug information about a column",
+                false,
+            ),
+            ChatCommand::Explosion => cmd(
+                vec![Float("radius", 5.0, false)],
+                "Explodes the ground around you",
+                true,
+            ),
+            ChatCommand::GiveExp => cmd(
+                vec![Integer("amount", 50, false)],
+                "Give experience to yourself",
+                true,
+            ),
+            ChatCommand::GiveItem => cmd(
+                vec![ItemSpec(false), Integer("num", 1, true)],
+                "Give yourself some items",
+                true,
+            ),
+            ChatCommand::Goto => cmd(
+                vec![
+                    Float("x", 0.0, false),
+                    Float("y", 0.0, false),
+                    Float("z", 0.0, false),
+                ],
+                "Teleport to a position",
+                true,
+            ),
+            ChatCommand::Health => cmd(
+                vec![Integer("hp", 100, false)],
+                "Set your current health",
+                true,
+            ),
+            ChatCommand::Help => ChatCommandData::new(
+                vec![Command(true)],
+                "Display information about commands",
+                false,
+            ),
+            ChatCommand::Jump => cmd(
+                vec![
+                    Float("x", 0.0, false),
+                    Float("y", 0.0, false),
+                    Float("z", 0.0, false),
+                ],
+                "Offset your current position",
+                true,
+            ),
+            ChatCommand::Kill => cmd(vec![], "Kill yourself", false),
+            ChatCommand::KillNpcs => cmd(vec![], "Kill the NPCs", true),
+            ChatCommand::Lantern => cmd(
+                vec![
+                    Float("strength", 5.0, false),
+                    Float("r", 1.0, true),
+                    Float("g", 1.0, true),
+                    Float("b", 1.0, true),
+                ],
+                "Change your lantern's strength and color",
+                true,
+            ),
+            ChatCommand::Light => cmd(
+                vec![
+                    Float("r", 1.0, true),
+                    Float("g", 1.0, true),
+                    Float("b", 1.0, true),
+                    Float("x", 0.0, true),
+                    Float("y", 0.0, true),
+                    Float("z", 0.0, true),
+                    Float("strength", 5.0, true),
+                ],
+                "Spawn entity with light",
+                true,
+            ),
+            ChatCommand::Object => cmd(vec![/*TODO*/], "Spawn an object", true),
+            ChatCommand::Players => cmd(vec![], "Lists players currently online", false),
+            ChatCommand::RemoveLights => cmd(
+                vec![Float("radius", 20.0, true)],
+                "Removes all lights spawned by players",
+                true,
+            ),
+            ChatCommand::SetLevel => {
+                cmd(vec![Integer("level", 10, false)], "Set player Level", true)
+            },
+            ChatCommand::Spawn => cmd(vec![/*TODO*/], "Spawn a test entity", true),
+            ChatCommand::Sudo => cmd(
+                vec![PlayerName(false), Command(false), Message /* TODO */],
+                "Run command as if you were another player",
+                true,
+            ),
+            ChatCommand::Tell => cmd(
+                vec![PlayerName(false), Message],
+                "Send a message to another player",
+                false,
+            ),
+            ChatCommand::Time => cmd(vec![/*TODO*/], "Set the time of day", true),
+            ChatCommand::Tp => cmd(vec![PlayerName(true)], "Teleport to another player", true),
+            ChatCommand::Version => cmd(vec![], "Prints server version", false),
+            ChatCommand::Waypoint => {
+                cmd(vec![], "Set your waypoint to your current position", true)
+            },
+        }
+    }
+
+    pub fn keyword(&self) -> &'static str {
+        match self {
+            ChatCommand::Adminify => "adminify",
+            ChatCommand::Alias => "alias",
+            ChatCommand::Build => "build",
+            ChatCommand::Debug => "debug",
+            ChatCommand::DebugColumn => "debug_column",
+            ChatCommand::Explosion => "explosion",
+            ChatCommand::GiveExp => "give_exp",
+            ChatCommand::GiveItem => "give_item",
+            ChatCommand::Goto => "goto",
+            ChatCommand::Health => "health",
+            ChatCommand::Help => "help",
+            ChatCommand::Jump => "jump",
+            ChatCommand::Kill => "kill",
+            ChatCommand::KillNpcs => "kill_npcs",
+            ChatCommand::Lantern => "lantern",
+            ChatCommand::Light => "light",
+            ChatCommand::Object => "object",
+            ChatCommand::Players => "players",
+            ChatCommand::RemoveLights => "remove_lights",
+            ChatCommand::SetLevel => "set_level",
+            ChatCommand::Spawn => "spawn",
+            ChatCommand::Sudo => "sudo",
+            ChatCommand::Tell => "tell",
+            ChatCommand::Time => "time",
+            ChatCommand::Tp => "tp",
+            ChatCommand::Version => "version",
+            ChatCommand::Waypoint => "waypoint",
+        }
+    }
+
+    pub fn help_string(&self) -> String {
+        let data = self.data();
+        let usage = std::iter::once(format!("/{}", self.keyword()))
+            .chain(data.args.iter().map(|arg| arg.usage_string()))
+            .collect::<Vec<_>>()
+            .join(" ");
+        format!("{}: {}", usage, data.description)
+    }
+
+    pub fn needs_admin(&self) -> bool { self.data().needs_admin }
+
+    pub fn arg_fmt(&self) -> String {
+        self.data()
+            .args
+            .iter()
+            .map(|arg| match arg {
+                ArgumentSpec::PlayerName(_) => "{}",
+                ArgumentSpec::ItemSpec(_) => "{}",
+                ArgumentSpec::Float(_, _, _) => "{f}",
+                ArgumentSpec::Integer(_, _, _) => "{d}",
+                ArgumentSpec::Any(_, _) => "{}",
+                ArgumentSpec::Command(_) => "{}",
+                ArgumentSpec::Message => "{/.*/}",
+                ArgumentSpec::OneOf(_, _, _, _) => "{}", // TODO
+            })
+            .collect::<Vec<_>>()
+            .join(" ")
+    }
+}
+
+impl std::str::FromStr for ChatCommand {
+    type Err = ();
+
+    fn from_str(keyword: &str) -> Result<ChatCommand, ()> {
+        let kwd = if keyword.chars().next() == Some('/') {
+            &keyword[1..]
+        } else {
+            &keyword[..]
+        };
+        for c in CHAT_COMMANDS {
+            if kwd == c.keyword() {
+                return Ok(*c);
+            }
+        }
+        return Err(());
+    }
 }
 
 /// Representation for chat command arguments
-pub enum ArgumentSyntax {
+pub enum ArgumentSpec {
     /// The argument refers to a player by alias
     PlayerName(bool),
     /// The argument refers to an item asset by path
@@ -56,61 +296,74 @@ pub enum ArgumentSyntax {
     /// * label
     /// * default tab-completion
     /// * whether it's optional
-    Integer(&'static str, f32, bool),
+    Integer(&'static str, i32, bool),
+    /// The argument is any string that doesn't contain spaces
+    Any(&'static str, bool),
     /// The argument is a command name
     Command(bool),
-    /// This is the final argument, consuming all characters until the end of input.
+    /// This is the final argument, consuming all characters until the end of
+    /// input.
     Message,
     /// The argument is likely an enum. The associated values are
     /// * label
     /// * Predefined string completions
     /// * Other completion types
     /// * whether it's optional
-    OneOf(&'static str, &'static [&'static str], Vec<Box<ArgumentSyntax>>, bool),
+    OneOf(
+        &'static str,
+        &'static [&'static str],
+        Vec<Box<ArgumentSpec>>,
+        bool,
+    ),
 }
 
-impl ArgumentSyntax {
-    pub fn help_string(arg: &ArgumentSyntax) -> String {
-        match arg {
-            ArgumentSyntax::PlayerName(optional) => {
+impl ArgumentSpec {
+    pub fn usage_string(&self) -> String {
+        match self {
+            ArgumentSpec::PlayerName(optional) => {
                 if *optional {
                     "[player]".to_string()
                 } else {
                     "<player>".to_string()
                 }
             },
-            ArgumentSyntax::ItemSpec(optional) => {
+            ArgumentSpec::ItemSpec(optional) => {
                 if *optional {
                     "[item]".to_string()
                 } else {
                     "<item>".to_string()
                 }
             },
-            ArgumentSyntax::Float(label, _, optional) => {
+            ArgumentSpec::Float(label, _, optional) => {
                 if *optional {
                     format!("[{}]", label)
                 } else {
                     format!("<{}>", label)
                 }
             },
-            ArgumentSyntax::Integer(label, _, optional) => {
+            ArgumentSpec::Integer(label, _, optional) => {
                 if *optional {
                     format!("[{}]", label)
                 } else {
                     format!("<{}>", label)
                 }
             },
-            ArgumentSyntax::Command(optional) => {
+            ArgumentSpec::Any(label, optional) => {
+                if *optional {
+                    format!("[{}]", label)
+                } else {
+                    format!("<{}>", label)
+                }
+            },
+            ArgumentSpec::Command(optional) => {
                 if *optional {
                     "[[/]command]".to_string()
                 } else {
                     "<[/]command>".to_string()
                 }
             },
-            ArgumentSyntax::Message => {
-                "<message>".to_string()
-            },
-            ArgumentSyntax::OneOf(label, _, _, optional) => {
+            ArgumentSpec::Message => "<message>".to_string(),
+            ArgumentSpec::OneOf(label, _, _, optional) => {
                 if *optional {
                     format! {"[{}]", label}
                 } else {
@@ -122,25 +375,26 @@ impl ArgumentSyntax {
 
     pub fn complete(&self, state: &State, part: &String) -> Vec<String> {
         match self {
-            ArgumentSyntax::PlayerName(_) => (&state.ecs().read_storage::<Player>())
+            ArgumentSpec::PlayerName(_) => (&state.ecs().read_storage::<Player>())
                 .join()
                 .filter(|player| player.alias.starts_with(part))
                 .map(|player| player.alias.clone())
                 .collect(),
-            ArgumentSyntax::ItemSpec(_) => assets::iterate()
+            ArgumentSpec::ItemSpec(_) => assets::iterate()
                 .filter(|asset| asset.starts_with(part))
                 .map(|c| c.to_string())
                 .collect(),
-            ArgumentSyntax::Float(_, x, _) => vec![format!("{}", x)],
-            ArgumentSyntax::Integer(_, x, _) => vec![format!("{}", x)],
-            ArgumentSyntax::Command(_) => CHAT_COMMANDS
+            ArgumentSpec::Float(_, x, _) => vec![format!("{}", x)],
+            ArgumentSpec::Integer(_, x, _) => vec![format!("{}", x)],
+            ArgumentSpec::Any(_, _) => vec![],
+            ArgumentSpec::Command(_) => CHAT_COMMANDS
                 .iter()
-                .map(|com| com.keyword.clone())
+                .map(|com| com.keyword())
                 .filter(|kwd| kwd.starts_with(part) || format!("/{}", kwd).starts_with(part))
                 .map(|c| c.to_string())
                 .collect(),
-            ArgumentSyntax::Message => vec![],
-            ArgumentSyntax::OneOf(_, strings, alts, _) => {
+            ArgumentSpec::Message => vec![],
+            ArgumentSpec::OneOf(_, strings, alts, _) => {
                 let string_completions = strings
                     .iter()
                     .filter(|string| string.starts_with(part))
@@ -150,7 +404,7 @@ impl ArgumentSyntax {
                     .flat_map(|b| (*b).complete(&state, part))
                     .map(|c| c.to_string());
                 string_completions.chain(alt_completions).collect()
-            }
+            },
         }
     }
 }
diff --git a/server/src/cmd.rs b/server/src/cmd.rs
index 9b63633afc..44a64db825 100644
--- a/server/src/cmd.rs
+++ b/server/src/cmd.rs
@@ -5,7 +5,9 @@
 use crate::{Server, StateExt};
 use chrono::{NaiveTime, Timelike};
 use common::{
-    assets, comp,
+    assets,
+    cmd::{ChatCommand, CHAT_COMMANDS},
+    comp,
     event::{EventBus, ServerEvent},
     msg::{PlayerListUpdate, ServerMsg},
     npc::{self, get_npc_name},
@@ -20,274 +22,70 @@ use specs::{Builder, Entity as EcsEntity, Join, WorldExt};
 use vek::*;
 use world::util::Sampler;
 
-use lazy_static::lazy_static;
 use log::error;
 use scan_fmt::{scan_fmt, scan_fmt_some};
 
-/// Struct representing a command that a user can run from server chat.
-pub struct ChatCommand {
-    /// The keyword used to invoke the command, omitting the leading '/'.
-    pub keyword: &'static str,
-    /// A format string for parsing arguments.
-    arg_fmt: &'static str,
-    /// A message that explains how the command is used.
-    help_string: &'static str,
-    /// A boolean that is used to check whether the command requires
-    /// administrator permissions or not.
-    needs_admin: bool,
-    /// Handler function called when the command is executed.
-    /// # Arguments
-    /// * `&mut Server` - the `Server` instance executing the command.
-    /// * `EcsEntity` - an `Entity` corresponding to the player that invoked the
-    ///   command.
-    /// * `EcsEntity` - an `Entity` for the player on whom the command is
-    ///   invoked. This differs from the previous argument when using /sudo
-    /// * `String` - a `String` containing the part of the command after the
-    ///   keyword.
-    /// * `&ChatCommand` - the command to execute with the above arguments.
-    /// Handler functions must parse arguments from the the given `String`
-    /// (`scan_fmt!` is included for this purpose).
-    handler: fn(&mut Server, EcsEntity, EcsEntity, String, &ChatCommand),
+pub trait ChatCommandExt {
+    fn execute(&self, server: &mut Server, entity: EcsEntity, args: String);
 }
-
-impl ChatCommand {
-    /// Creates a new chat command.
-    pub fn new(
-        keyword: &'static str,
-        arg_fmt: &'static str,
-        help_string: &'static str,
-        needs_admin: bool,
-        handler: fn(&mut Server, EcsEntity, EcsEntity, String, &ChatCommand),
-    ) -> Self {
-        Self {
-            keyword,
-            arg_fmt,
-            help_string,
-            needs_admin,
-            handler,
-        }
-    }
-
-    /// Calls the contained handler function, passing `&self` as the last
-    /// argument.
-    pub fn execute(&self, server: &mut Server, entity: EcsEntity, args: String) {
-        if self.needs_admin {
-            if !server.entity_is_admin(entity) {
-                server.notify_client(
-                    entity,
-                    ServerMsg::private(format!(
-                        "You don't have permission to use '/{}'.",
-                        self.keyword
-                    )),
-                );
-                return;
-            } else {
-                (self.handler)(server, entity, entity, args, self);
-            }
+impl ChatCommandExt for ChatCommand {
+    fn execute(&self, server: &mut Server, entity: EcsEntity, args: String) {
+        let cmd_data = self.data();
+        if cmd_data.needs_admin && !server.entity_is_admin(entity) {
+            server.notify_client(
+                entity,
+                ServerMsg::private(format!(
+                    "You don't have permission to use '/{}'.",
+                    self.keyword()
+                )),
+            );
+            return;
         } else {
-            (self.handler)(server, entity, entity, args, self);
+            get_handler(self)(server, entity, entity, args, &self);
         }
     }
 }
 
-lazy_static! {
-    /// Static list of chat commands available to the server.
-    pub static ref CHAT_COMMANDS: Vec<ChatCommand> = vec![
-        ChatCommand::new(
-            "give_item",
-            "{} {d}",
-            "/give_item <path to item> [num]\n\
-            Example items: common/items/apple, common/items/debug/boost",
-            true,
-            handle_give,),
-        ChatCommand::new(
-            "jump",
-            "{d} {d} {d}",
-            "/jump <dx> <dy> <dz> : Offset your current position",
-            true,
-            handle_jump,
-        ),
-        ChatCommand::new(
-            "goto",
-            "{d} {d} {d}",
-            "/goto <x> <y> <z> : Teleport to a position",
-            true,
-            handle_goto,
-        ),
-        ChatCommand::new(
-            "alias",
-            "{}",
-            "/alias <name> : Change your alias",
-            false,
-            handle_alias,
-        ),
-        ChatCommand::new(
-            "tp",
-            "{}",
-            "/tp <alias> : Teleport to another player",
-            true,
-            handle_tp,
-        ),
-        ChatCommand::new(
-            "kill",
-            "{}",
-            "/kill : Kill yourself",
-            false,
-            handle_kill,
-        ),
-        ChatCommand::new(
-            "time",
-            "{} {s}",
-            "/time <XY:XY> or [Time of day] : Set the time of day",
-            true,
-            handle_time,
-        ),
-        ChatCommand::new(
-            "spawn",
-            "{} {} {d}",
-            "/spawn <alignment> <entity> [amount] : Spawn a test entity",
-            true,
-            handle_spawn,
-        ),
-        ChatCommand::new(
-             "players",
-             "{}",
-             "/players : Lists players currently online",
-             false,
-             handle_players,
-         ),
-        ChatCommand::new(
-            "help", "", "/help: Display this message", false, handle_help),
-        ChatCommand::new(
-            "health",
-            "{}",
-            "/health : Set your current health",
-            true,
-            handle_health,
-        ),
-        ChatCommand::new(
-            "build",
-            "",
-            "/build : Toggles build mode on and off",
-            true,
-            handle_build,
-        ),
-        ChatCommand::new(
-            "tell",
-            "{}",
-            "/tell <alias> <message>: Send a message to another player",
-            false,
-            handle_tell,
-        ),
-        ChatCommand::new(
-            "killnpcs",
-            "{}",
-            "/killnpcs : Kill the NPCs",
-            true,
-            handle_killnpcs,
-        ),
-        ChatCommand::new(
-            "object",
-            "{}",
-            "/object [Name]: Spawn an object",
-            true,
-            handle_object,
-        ),
-        ChatCommand::new(
-            "light",
-            "{} {} {} {} {} {} {}",
-            "/light <opt:  <<cr> <cg> <cb>> <<ox> <oy> <oz>> <<strength>>>: Spawn entity with light",
-            true,
-            handle_light,
-        ),
-        ChatCommand::new(
-            "lantern",
-            "{} {} {} {}",
-            "/lantern <strength> [<r> <g> <b>]: Change your lantern's strength and color",
-            true,
-            handle_lantern,
-        ),
-        ChatCommand::new(
-            "explosion",
-            "{}",
-            "/explosion <radius> : Explodes the ground around you",
-            true,
-            handle_explosion,
-        ),
-        ChatCommand::new(
-            "waypoint",
-            "{}",
-            "/waypoint : Set your waypoint to your current position",
-            true,
-            handle_waypoint,
-        ),
-        ChatCommand::new(
-            "adminify",
-            "{}",
-            "/adminify <playername> : Temporarily gives a player admin permissions or removes them",
-            true,
-            handle_adminify,
-        ),
-        ChatCommand::new(
-             "debug_column",
-             "{} {}",
-             "/debug_column <x> <y> : Prints some debug information about a column",
-             false,
-             handle_debug_column,
-         ),
-         ChatCommand::new(
-             "give_exp",
-             "{d} {}",
-             "/give_exp <amount> <playername?> : Give experience to yourself or specify a target player",
-             true,
-             handle_exp,
-         ),
-         ChatCommand::new(
-             "set_level",
-             "{d} {}",
-             "/set_level <level> <playername?> : Set own Level or specify a target player",
-             true,
-             handle_level
-         ),
-        ChatCommand::new(
-             "removelights",
-             "{}",
-             "/removelights [radius] : Removes all lights spawned by players",
-             true,
-             handle_remove_lights,
-         ),
-        ChatCommand::new(
-            "debug",
-            "",
-            "/debug : Place all debug items into your pack.",
-            true,
-            handle_debug,
-        ),
-        ChatCommand::new(
-            "sudo",
-            "{} {} {/.*/}",
-            "/sudo <player> /<command> [args...] : Run command as if you were another player",
-            true,
-            handle_sudo,
-        ),
-        ChatCommand::new(
-            "version",
-            "",
-            "/version : Prints server version",
-            false,
-            handle_version,
-        ),
-    ];
+type CommandHandler = fn(&mut Server, EcsEntity, EcsEntity, String, &ChatCommand);
+fn get_handler(cmd: &ChatCommand) -> CommandHandler {
+    match cmd {
+        ChatCommand::Adminify => handle_adminify,
+        ChatCommand::Alias => handle_alias,
+        ChatCommand::Build => handle_build,
+        ChatCommand::Debug => handle_debug,
+        ChatCommand::DebugColumn => handle_debug_column,
+        ChatCommand::Explosion => handle_explosion,
+        ChatCommand::GiveExp => handle_give_exp,
+        ChatCommand::GiveItem => handle_give_item,
+        ChatCommand::Goto => handle_goto,
+        ChatCommand::Health => handle_health,
+        ChatCommand::Help => handle_help,
+        ChatCommand::Jump => handle_jump,
+        ChatCommand::Kill => handle_kill,
+        ChatCommand::KillNpcs => handle_kill_npcs,
+        ChatCommand::Lantern => handle_lantern,
+        ChatCommand::Light => handle_light,
+        ChatCommand::Object => handle_object,
+        ChatCommand::Players => handle_players,
+        ChatCommand::RemoveLights => handle_remove_lights,
+        ChatCommand::SetLevel => handle_set_level,
+        ChatCommand::Spawn => handle_spawn,
+        ChatCommand::Sudo => handle_sudo,
+        ChatCommand::Tell => handle_tell,
+        ChatCommand::Time => handle_time,
+        ChatCommand::Tp => handle_tp,
+        ChatCommand::Version => handle_version,
+        ChatCommand::Waypoint => handle_waypoint,
+    }
 }
-
-fn handle_give(
+fn handle_give_item(
     server: &mut Server,
     client: EcsEntity,
     target: EcsEntity,
     args: String,
     action: &ChatCommand,
 ) {
-    if let (Some(item_name), give_amount_opt) = scan_fmt_some!(&args, action.arg_fmt, String, u32) {
+    if let (Some(item_name), give_amount_opt) = scan_fmt_some!(&args, &action.arg_fmt(), String, u32) {
         let give_amount = give_amount_opt.unwrap_or(1);
         if let Ok(item) = assets::load_cloned(&item_name) {
             let mut item: Item = item;
@@ -346,7 +144,7 @@ fn handle_give(
             );
         }
     } else {
-        server.notify_client(client, ServerMsg::private(String::from(action.help_string)));
+        server.notify_client(client, ServerMsg::private(String::from(action.help_string())));
     }
 }
 
@@ -357,7 +155,7 @@ fn handle_jump(
     args: String,
     action: &ChatCommand,
 ) {
-    if let Ok((x, y, z)) = scan_fmt!(&args, action.arg_fmt, f32, f32, f32) {
+    if let Ok((x, y, z)) = scan_fmt!(&args, &action.arg_fmt(), f32, f32, f32) {
         match server.state.read_component_cloned::<comp::Pos>(target) {
             Some(current_pos) => {
                 server
@@ -380,7 +178,7 @@ fn handle_goto(
     args: String,
     action: &ChatCommand,
 ) {
-    if let Ok((x, y, z)) = scan_fmt!(&args, action.arg_fmt, f32, f32, f32) {
+    if let Ok((x, y, z)) = scan_fmt!(&args, &action.arg_fmt(), f32, f32, f32) {
         if server
             .state
             .read_component_cloned::<comp::Pos>(target)
@@ -397,7 +195,10 @@ fn handle_goto(
             );
         }
     } else {
-        server.notify_client(client, ServerMsg::private(String::from(action.help_string)));
+        server.notify_client(
+            client,
+            ServerMsg::private(String::from(action.help_string())),
+        );
     }
 }
 
@@ -432,7 +233,7 @@ fn handle_time(
     args: String,
     action: &ChatCommand,
 ) {
-    let time = scan_fmt_some!(&args, action.arg_fmt, String);
+    let time = scan_fmt_some!(&args, &action.arg_fmt(), String);
     let new_time = match time.as_ref().map(|s| s.as_str()) {
         Some("midnight") => NaiveTime::from_hms(0, 0, 0),
         Some("night") => NaiveTime::from_hms(20, 0, 0),
@@ -490,7 +291,7 @@ fn handle_health(
     args: String,
     action: &ChatCommand,
 ) {
-    if let Ok(hp) = scan_fmt!(&args, action.arg_fmt, u32) {
+    if let Ok(hp) = scan_fmt!(&args, &action.arg_fmt(), u32) {
         if let Some(stats) = server
             .state
             .ecs()
@@ -519,7 +320,7 @@ fn handle_alias(
     args: String,
     action: &ChatCommand,
 ) {
-    if let Ok(alias) = scan_fmt!(&args, action.arg_fmt, String) {
+    if let Ok(alias) = scan_fmt!(&args, &action.arg_fmt(), String) {
         server
             .state
             .ecs_mut()
@@ -540,7 +341,10 @@ fn handle_alias(
             server.state.notify_registered_clients(msg);
         }
     } else {
-        server.notify_client(client, ServerMsg::private(String::from(action.help_string)));
+        server.notify_client(
+            client,
+            ServerMsg::private(String::from(action.help_string())),
+        );
     }
 }
 
@@ -551,7 +355,7 @@ fn handle_tp(
     args: String,
     action: &ChatCommand,
 ) {
-    if let Ok(alias) = scan_fmt!(&args, action.arg_fmt, String) {
+    if let Ok(alias) = scan_fmt!(&args, &action.arg_fmt(), String) {
         let ecs = server.state.ecs();
         let opt_player = (&ecs.entities(), &ecs.read_storage::<comp::Player>())
             .join()
@@ -576,7 +380,7 @@ fn handle_tp(
                     );
                     server.notify_client(
                         client,
-                        ServerMsg::private(String::from(action.help_string)),
+                        ServerMsg::private(String::from(action.help_string())),
                     );
                 },
             },
@@ -585,7 +389,10 @@ fn handle_tp(
             },
         }
     } else {
-        server.notify_client(client, ServerMsg::private(String::from(action.help_string)));
+        server.notify_client(
+            client,
+            ServerMsg::private(String::from(action.help_string())),
+        );
     }
 }
 
@@ -596,7 +403,7 @@ fn handle_spawn(
     args: String,
     action: &ChatCommand,
 ) {
-    match scan_fmt_some!(&args, action.arg_fmt, String, npc::NpcBody, String) {
+    match scan_fmt_some!(&args, &action.arg_fmt(), String, npc::NpcBody, String) {
         (Some(opt_align), Some(npc::NpcBody(id, mut body)), opt_amount) => {
             if let Some(alignment) = parse_alignment(target, &opt_align) {
                 let amount = opt_amount
@@ -659,7 +466,10 @@ fn handle_spawn(
             }
         },
         _ => {
-            server.notify_client(client, ServerMsg::private(String::from(action.help_string)));
+            server.notify_client(
+                client,
+                ServerMsg::private(String::from(action.help_string())),
+            );
         },
     }
 }
@@ -733,12 +543,16 @@ fn handle_help(
     server: &mut Server,
     client: EcsEntity,
     _target: EcsEntity,
-    _args: String,
-    _action: &ChatCommand,
+    args: String,
+    action: &ChatCommand,
 ) {
-    for cmd in CHAT_COMMANDS.iter() {
-        if !cmd.needs_admin || server.entity_is_admin(client) {
-            server.notify_client(client, ServerMsg::private(String::from(cmd.help_string)));
+    if let Some(cmd) = scan_fmt_some!(&args, &action.arg_fmt(), ChatCommand) {
+        server.notify_client(client, ServerMsg::private(String::from(cmd.help_string())));
+    } else {
+        for cmd in CHAT_COMMANDS.iter() {
+            if !cmd.needs_admin() || server.entity_is_admin(client) {
+                server.notify_client(client, ServerMsg::private(String::from(cmd.help_string())));
+            }
         }
     }
 }
@@ -753,7 +567,7 @@ fn parse_alignment(owner: EcsEntity, alignment: &str) -> Option<comp::Alignment>
     }
 }
 
-fn handle_killnpcs(
+fn handle_kill_npcs(
     server: &mut Server,
     client: EcsEntity,
     _target: EcsEntity,
@@ -781,9 +595,9 @@ fn handle_object(
     client: EcsEntity,
     target: EcsEntity,
     args: String,
-    _action: &ChatCommand,
+    action: &ChatCommand,
 ) {
-    let obj_type = scan_fmt!(&args, _action.arg_fmt, String);
+    let obj_type = scan_fmt!(&args, &action.arg_fmt(), String);
 
     let pos = server
         .state
@@ -894,7 +708,7 @@ fn handle_light(
     action: &ChatCommand,
 ) {
     let (opt_r, opt_g, opt_b, opt_x, opt_y, opt_z, opt_s) =
-        scan_fmt_some!(&args, action.arg_fmt, f32, f32, f32, f32, f32, f32, f32);
+        scan_fmt_some!(&args, &action.arg_fmt(), f32, f32, f32, f32, f32, f32, f32);
 
     let mut light_emitter = comp::LightEmitter::default();
     let mut light_offset_opt = None;
@@ -955,7 +769,8 @@ fn handle_lantern(
     args: String,
     action: &ChatCommand,
 ) {
-    if let (Some(s), r, g, b) = scan_fmt_some!(&args, action.arg_fmt, f32, f32, f32, f32) {
+    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
             .ecs()
@@ -987,7 +802,10 @@ fn handle_lantern(
             );
         }
     } else {
-        server.notify_client(client, ServerMsg::private(String::from(action.help_string)));
+        server.notify_client(
+            client,
+            ServerMsg::private(String::from(action.help_string())),
+        );
     }
 }
 
@@ -998,7 +816,7 @@ fn handle_explosion(
     args: String,
     action: &ChatCommand,
 ) {
-    let power = scan_fmt!(&args, action.arg_fmt, f32).unwrap_or(8.0);
+    let power = scan_fmt!(&args, &action.arg_fmt(), f32).unwrap_or(8.0);
 
     if power > 512.0 {
         server.notify_client(
@@ -1062,7 +880,7 @@ fn handle_adminify(
     args: String,
     action: &ChatCommand,
 ) {
-    if let Ok(alias) = scan_fmt!(&args, action.arg_fmt, String) {
+    if let Ok(alias) = scan_fmt!(&args, &action.arg_fmt(), String) {
         let ecs = server.state.ecs();
         let opt_player = (&ecs.entities(), &ecs.read_storage::<comp::Player>())
             .join()
@@ -1082,11 +900,17 @@ fn handle_adminify(
                     client,
                     ServerMsg::private(format!("Player '{}' not found!", alias)),
                 );
-                server.notify_client(client, ServerMsg::private(String::from(action.help_string)));
+                server.notify_client(
+                    client,
+                    ServerMsg::private(String::from(action.help_string())),
+                );
             },
         }
     } else {
-        server.notify_client(client, ServerMsg::private(String::from(action.help_string)));
+        server.notify_client(
+            client,
+            ServerMsg::private(String::from(action.help_string())),
+        );
     }
 }
 
@@ -1104,7 +928,7 @@ fn handle_tell(
         );
         return;
     }
-    if let Ok(alias) = scan_fmt!(&args, action.arg_fmt, String) {
+    if let Ok(alias) = scan_fmt!(&args, &action.arg_fmt(), String) {
         let ecs = server.state.ecs();
         let msg = &args[alias.len()..args.len()];
         if let Some(player) = (&ecs.entities(), &ecs.read_storage::<comp::Player>())
@@ -1152,7 +976,10 @@ fn handle_tell(
             );
         }
     } else {
-        server.notify_client(client, ServerMsg::private(String::from(action.help_string)));
+        server.notify_client(
+            client,
+            ServerMsg::private(String::from(action.help_string())),
+        );
     }
 }
 
@@ -1180,7 +1007,7 @@ fn handle_debug_column(
 ) {
     let sim = server.world.sim();
     let sampler = server.world.sample_columns();
-    if let Ok((x, y)) = scan_fmt!(&args, action.arg_fmt, i32, i32) {
+    if let Ok((x, y)) = scan_fmt!(&args, &action.arg_fmt(), i32, i32) {
         let wpos = Vec2::new(x, y);
         /* let chunk_pos = wpos.map2(TerrainChunkSize::RECT_SIZE, |e, sz: u32| {
             e / sz as i32
@@ -1244,7 +1071,10 @@ spawn_rate {:?} "#,
             );
         }
     } else {
-        server.notify_client(client, ServerMsg::private(String::from(action.help_string)));
+        server.notify_client(
+            client,
+            ServerMsg::private(String::from(action.help_string())),
+        );
     }
 }
 
@@ -1264,14 +1094,14 @@ fn find_target(
     }
 }
 
-fn handle_exp(
+fn handle_give_exp(
     server: &mut Server,
     client: EcsEntity,
     target: EcsEntity,
     args: String,
     action: &ChatCommand,
 ) {
-    let (a_exp, a_alias) = scan_fmt_some!(&args, action.arg_fmt, i64, String);
+    let (a_exp, a_alias) = scan_fmt_some!(&args, &action.arg_fmt(), i64, String);
 
     if let Some(exp) = a_exp {
         let ecs = server.state.ecs_mut();
@@ -1298,14 +1128,14 @@ fn handle_exp(
     }
 }
 
-fn handle_level(
+fn handle_set_level(
     server: &mut Server,
     client: EcsEntity,
     target: EcsEntity,
     args: String,
     action: &ChatCommand,
 ) {
-    let (a_lvl, a_alias) = scan_fmt_some!(&args, action.arg_fmt, u32, String);
+    let (a_lvl, a_alias) = scan_fmt_some!(&args, &action.arg_fmt(), u32, String);
 
     if let Some(lvl) = a_lvl {
         let ecs = server.state.ecs_mut();
@@ -1378,7 +1208,7 @@ fn handle_remove_lights(
     args: String,
     action: &ChatCommand,
 ) {
-    let opt_radius = scan_fmt_some!(&args, action.arg_fmt, f32);
+    let opt_radius = scan_fmt_some!(&args, &action.arg_fmt(), f32);
     let opt_player_pos = server.state.read_component_cloned::<comp::Pos>(target);
     let mut to_delete = vec![];
 
@@ -1430,7 +1260,7 @@ fn handle_sudo(
     action: &ChatCommand,
 ) {
     if let (Some(player_alias), Some(cmd), cmd_args) =
-        scan_fmt_some!(&args, action.arg_fmt, String, String, String)
+        scan_fmt_some!(&args, &action.arg_fmt(), String, String, String)
     {
         let cmd_args = cmd_args.unwrap_or(String::from(""));
         let cmd = if cmd.chars().next() == Some('/') {
@@ -1438,14 +1268,14 @@ fn handle_sudo(
         } else {
             cmd
         };
-        if let Some(action) = CHAT_COMMANDS.iter().find(|c| c.keyword == cmd) {
+        if let Some(action) = CHAT_COMMANDS.iter().find(|c| c.keyword() == cmd) {
             let ecs = server.state.ecs();
             let entity_opt = (&ecs.entities(), &ecs.read_storage::<comp::Player>())
                 .join()
                 .find(|(_, player)| player.alias == player_alias)
                 .map(|(entity, _)| entity);
             if let Some(entity) = entity_opt {
-                (action.handler)(server, client, entity, cmd_args, action);
+                get_handler(action)(server, client, entity, cmd_args, action);
             } else {
                 server.notify_client(
                     client,
@@ -1459,7 +1289,10 @@ fn handle_sudo(
             );
         }
     } else {
-        server.notify_client(client, ServerMsg::private(String::from(action.help_string)));
+        server.notify_client(
+            client,
+            ServerMsg::private(String::from(action.help_string())),
+        );
     }
 }
 
@@ -1479,3 +1312,4 @@ fn handle_version(
         )),
     );
 }
+
diff --git a/server/src/lib.rs b/server/src/lib.rs
index 97126cb648..eb29844b80 100644
--- a/server/src/lib.rs
+++ b/server/src/lib.rs
@@ -22,11 +22,12 @@ use crate::{
     auth_provider::AuthProvider,
     chunk_generator::ChunkGenerator,
     client::{Client, RegionSubscription},
-    cmd::CHAT_COMMANDS,
+    cmd::ChatCommandExt,
     state_ext::StateExt,
     sys::sentinel::{DeletedEntities, TrackedComps},
 };
 use common::{
+    cmd::ChatCommand,
     comp,
     event::{EventBus, ServerEvent},
     msg::{ClientMsg, ClientState, ServerInfo, ServerMsg},
@@ -534,18 +535,16 @@ impl Server {
         };
 
         // Find the command object and run its handler.
-        let action_opt = CHAT_COMMANDS.iter().find(|x| x.keyword == kwd);
-        match action_opt {
-            Some(action) => action.execute(self, entity, args),
-            // Unknown command
-            None => {
-                if let Some(client) = self.state.ecs().write_storage::<Client>().get_mut(entity) {
-                    client.notify(ServerMsg::private(format!(
-                        "Unknown command '/{}'.\nType '/help' for available commands",
-                        kwd
-                    )));
-                }
-            },
+        if let Ok(command) = kwd.parse::<ChatCommand>() {
+            command.execute(self, entity, args);
+        } else {
+            self.notify_client(
+                entity,
+                ServerMsg::private(format!(
+                    "Unknown command '/{}'.\nType '/help' for available commands",
+                    kwd
+                )),
+            );
         }
     }
 

From 58b0e9ef75cc7dc6dabb60375927a3b16515e91b Mon Sep 17 00:00:00 2001
From: CapsizeGlimmer <>
Date: Tue, 5 May 2020 18:56:58 -0400
Subject: [PATCH 3/9] Move cursor to end when moving through chat history

---
 voxygen/src/hud/chat.rs | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/voxygen/src/hud/chat.rs b/voxygen/src/hud/chat.rs
index 2a3197fdaa..c41ba122d9 100644
--- a/voxygen/src/hud/chat.rs
+++ b/voxygen/src/hud/chat.rs
@@ -137,8 +137,9 @@ impl<'a> Widget for Chat<'a> {
             }
         });
 
+        let mut force_cursor = self.force_cursor;
+
         // If up or down are pressed move through history
-        // TODO: move cursor to the end of the last line
         match ui.widget_input(state.ids.input).presses().key().fold(
             (false, false),
             |(up, down), key_press| match key_press.key {
@@ -152,6 +153,7 @@ impl<'a> Widget for Chat<'a> {
                     state.update(|s| {
                         s.history_pos += 1;
                         s.input = s.history.get(s.history_pos - 1).unwrap().to_owned();
+                        force_cursor = Some(Index{line: 0, char: s.input.len()});
                     });
                 }
             },
@@ -161,6 +163,7 @@ impl<'a> Widget for Chat<'a> {
                         s.history_pos -= 1;
                         if s.history_pos > 0 {
                             s.input = s.history.get(s.history_pos - 1).unwrap().to_owned();
+                            force_cursor = Some(Index{line: 0, char: s.input.len()});
                         } else {
                             s.input.clear();
                         }
@@ -190,7 +193,7 @@ impl<'a> Widget for Chat<'a> {
                 .font_size(self.fonts.opensans.scale(15))
                 .font_id(self.fonts.opensans.conrod_id);
 
-            if let Some(pos) = self.force_cursor {
+            if let Some(pos) = force_cursor {
                 text_edit = text_edit.cursor_pos(pos);
             }
 

From 24fa23fef4a9bd2dcc32da13fd5f034b81901033 Mon Sep 17 00:00:00 2001
From: CapsizeGlimmer <>
Date: Wed, 6 May 2020 14:45:58 -0400
Subject: [PATCH 4/9] Initial client implementation of tab completion

---
 server/src/cmd.rs       |   1 -
 voxygen/src/hud/chat.rs | 200 ++++++++++++++++++++++++++++++++--------
 voxygen/src/hud/mod.rs  |  21 +++++
 3 files changed, 181 insertions(+), 41 deletions(-)

diff --git a/server/src/cmd.rs b/server/src/cmd.rs
index 44a64db825..26a6263484 100644
--- a/server/src/cmd.rs
+++ b/server/src/cmd.rs
@@ -1312,4 +1312,3 @@ fn handle_version(
         )),
     );
 }
-
diff --git a/voxygen/src/hud/chat.rs b/voxygen/src/hud/chat.rs
index c41ba122d9..0e12c808bd 100644
--- a/voxygen/src/hud/chat.rs
+++ b/voxygen/src/hud/chat.rs
@@ -8,9 +8,12 @@ use common::{msg::validate_chat_msg, ChatType};
 use conrod_core::{
     input::Key,
     position::Dimension,
-    text::cursor::Index,
+    text::{
+        self,
+        cursor::{self, Index},
+    },
     widget::{self, Button, Id, List, Rectangle, Text, TextEdit},
-    widget_ids, Colorable, Positionable, Sizeable, UiCell, Widget, WidgetCommon,
+    widget_ids, Colorable, Positionable, Sizeable, Ui, UiCell, Widget, WidgetCommon,
 };
 use std::collections::VecDeque;
 
@@ -18,13 +21,16 @@ widget_ids! {
     struct Ids {
         message_box,
         message_box_bg,
-        input,
-        input_bg,
+        chat_input,
+        chat_input_bg,
         chat_arrow,
+        completion_box,
     }
 }
 
 const MAX_MESSAGES: usize = 100;
+// Maximum completions shown at once
+const MAX_COMPLETIONS: usize = 10;
 
 #[derive(WidgetCommon)]
 pub struct Chat<'a> {
@@ -64,6 +70,9 @@ impl<'a> Chat<'a> {
 
     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
@@ -97,6 +106,10 @@ pub struct State {
     // Index into the history Vec, history_pos == 0 is history not in use
     // 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,
 }
 
 pub enum Event {
@@ -115,6 +128,8 @@ impl<'a> Widget for Chat<'a> {
             messages: VecDeque::new(),
             history: VecDeque::new(),
             history_pos: 0,
+            completions: Vec::new(),
+            completions_pos: 0,
             ids: Ids::new(id_gen),
         }
     }
@@ -140,51 +155,77 @@ impl<'a> Widget for Chat<'a> {
         let mut force_cursor = self.force_cursor;
 
         // If up or down are pressed move through history
-        match ui.widget_input(state.ids.input).presses().key().fold(
-            (false, false),
-            |(up, down), key_press| match key_press.key {
-                Key::Up => (true, down),
-                Key::Down => (up, true),
-                _ => (up, down),
-            },
-        ) {
-            (true, false) => {
-                if state.history_pos < state.history.len() {
-                    state.update(|s| {
+        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 {
+            state.update(|s| {
+                if history_move > 0 {
+                    if s.history_pos < s.history.len() {
                         s.history_pos += 1;
-                        s.input = s.history.get(s.history_pos - 1).unwrap().to_owned();
-                        force_cursor = Some(Index{line: 0, char: s.input.len()});
-                    });
-                }
-            },
-            (false, true) => {
-                if state.history_pos > 0 {
-                    state.update(|s| {
+                    }
+                } else {
+                    if s.history_pos > 0 {
                         s.history_pos -= 1;
-                        if s.history_pos > 0 {
-                            s.input = s.history.get(s.history_pos - 1).unwrap().to_owned();
-                            force_cursor = Some(Index{line: 0, char: s.input.len()});
-                        } else {
-                            s.input.clear();
-                        }
-                    });
+                    }
                 }
-            },
-            _ => {},
+                if s.history_pos > 0 {
+                    s.input = s.history.get(s.history_pos - 1).unwrap().to_owned();
+                    force_cursor =
+                        cursor_offset_to_index(s.input.len(), &s.input, &ui, &self.fonts);
+                } else {
+                    s.input.clear();
+                }
+            });
+        }
+
+        // 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 {
-            state.update(|s| s.input = input.clone());
+            state.update(|s| s.input = input.to_string());
         }
 
         let input_focused =
-            keyboard_capturer == Some(state.ids.input) || keyboard_capturer == Some(id);
+            keyboard_capturer == Some(state.ids.chat_input) || keyboard_capturer == Some(id);
 
         // Only show if it has the keyboard captured.
         // Chat input uses a rectangle as its background.
         if input_focused {
+            // Any changes to this TextEdit's width and font size must be reflected in
+            // `cursor_offset_to_index` below.
             let mut text_edit = TextEdit::new(&state.input)
                 .w(460.0)
                 .restrict_to_height(false)
@@ -205,15 +246,18 @@ impl<'a> Widget for Chat<'a> {
                 .rgba(0.0, 0.0, 0.0, transp + 0.1)
                 .bottom_left_with_margins_on(ui.window, 10.0, 10.0)
                 .w(470.0)
-                .set(state.ids.input_bg, ui);
+                .set(state.ids.chat_input_bg, ui);
 
             if let Some(str) = text_edit
-                .top_left_with_margins_on(state.ids.input_bg, 1.0, 1.0)
-                .set(state.ids.input, ui)
+                .top_left_with_margins_on(state.ids.chat_input_bg, 1.0, 1.0)
+                .set(state.ids.chat_input, ui)
             {
                 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);
                 }
             }
@@ -224,7 +268,7 @@ impl<'a> Widget for Chat<'a> {
             .rgba(0.0, 0.0, 0.0, transp)
             .and(|r| {
                 if input_focused {
-                    r.up_from(state.ids.input_bg, 0.0)
+                    r.up_from(state.ids.chat_input_bg, 0.0)
                 } else {
                     r.bottom_left_with_margins_on(ui.window, 10.0, 10.0)
                 }
@@ -304,11 +348,11 @@ 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) {
-            Some(Event::Focus(state.ids.input))
+            Some(Event::Focus(state.ids.chat_input))
         }
         // If enter is pressed and the input box is not empty, send the current message.
         else if ui
-            .widget_input(state.ids.input)
+            .widget_input(state.ids.chat_input)
             .presses()
             .key()
             .any(|key_press| match key_press.key {
@@ -333,3 +377,79 @@ impl<'a> Widget for Chat<'a> {
         }
     }
 }
+
+fn do_tab_completion(cursor: usize, input: &str, word: &str) -> (String, usize) {
+    let mut pre_ws = None;
+    let mut post_ws = None;
+    for (char_i, (byte_i, c)) in input.char_indices().enumerate() {
+        if c.is_whitespace() && c != '\t' {
+            if char_i < cursor {
+                pre_ws = Some(byte_i);
+            } else {
+                assert_eq!(post_ws, None); // TODO debug
+                post_ws = Some(byte_i);
+                break;
+            }
+        }
+    }
+
+    match (pre_ws, post_ws) {
+        (None, None) => (word.to_string(), word.chars().count()),
+        (None, Some(i)) => (
+            format!("{}{}", word, input.split_at(i).1),
+            word.chars().count(),
+        ),
+        (Some(i), None) => {
+            let l_split = input.split_at(i).0;
+            let completed = format!("{} {}", l_split, word);
+            (
+                completed,
+                l_split.chars().count() + 1 + word.chars().count(),
+            )
+        },
+        (Some(i), Some(j)) => {
+            let l_split = input.split_at(i).0;
+            let r_split = input.split_at(j).1;
+            let completed = format!("{} {}{}", l_split, word, r_split);
+            (
+                completed,
+                l_split.chars().count() + 1 + word.chars().count(),
+            )
+        },
+    }
+}
+
+fn cursor_index_to_offset(
+    index: text::cursor::Index,
+    text: &str,
+    ui: &Ui,
+    fonts: &ConrodVoxygenFonts,
+) -> Option<usize> {
+    // Width and font must match that of the chat TextEdit
+    let width = 460.0;
+    let font = ui.fonts.get(fonts.opensans.conrod_id)?;
+    let font_size = fonts.opensans.scale(15);
+    let infos = text::line::infos(&text, &font, font_size).wrap_by_whitespace(width);
+
+    text::glyph::index_after_cursor(infos, index)
+}
+
+fn cursor_offset_to_index(
+    offset: usize,
+    text: &str,
+    ui: &Ui,
+    fonts: &ConrodVoxygenFonts,
+) -> Option<Index> {
+    // This moves the cursor to the given offset. Conrod is a pain.
+    //let iter = cursor::xys_per_line_from_text(&text, &[], &font, font_size,
+    // Justify::Left, Align::Start, 2.0, Rect{x: Range{start: 0.0, end: width}, y:
+    // Range{start: 0.0, end: 12.345}});
+    // cursor::closest_cursor_index_and_xy([f64::MAX, f64::MAX], iter).map(|(i, _)|
+    // i) Width and font must match that of the chat TextEdit
+    let width = 460.0;
+    let font = ui.fonts.get(fonts.opensans.conrod_id)?;
+    let font_size = fonts.opensans.scale(15);
+    let infos = text::line::infos(&text, &font, font_size).wrap_by_whitespace(width);
+
+    cursor::index_before_char(infos, offset)
+}
diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs
index 4b8f951c89..5f802f22e3 100644
--- a/voxygen/src/hud/mod.rs
+++ b/voxygen/src/hud/mod.rs
@@ -2296,6 +2296,27 @@ impl Hud {
         camera: &Camera,
         dt: Duration,
     ) -> Vec<Event> {
+        // conrod eats tabs. Un-eat a tabstop so tab completion can work
+        if self.ui.ui.global_input().events().any(|event| {
+            use conrod_core::{event, input};
+            match event {
+                //event::Event::Raw(event::Input::Press(input::Button::Keyboard(input::Key::Tab)))
+                // => true,
+                event::Event::Ui(event::Ui::Press(
+                    _,
+                    event::Press {
+                        button: event::Button::Keyboard(input::Key::Tab),
+                        ..
+                    },
+                )) => true,
+                _ => false,
+            }
+        }) {
+            self.ui
+                .ui
+                .handle_event(conrod_core::event::Input::Text("\t".to_string()));
+        }
+
         if let Some(maybe_id) = self.to_focus.take() {
             self.ui.focus_widget(maybe_id);
         }

From b0f0d716be4e71c565ed1d59406eebb49b4b88f8 Mon Sep 17 00:00:00 2001
From: CapsizeGlimmer <>
Date: Fri, 8 May 2020 01:35:07 -0400
Subject: [PATCH 5/9] Tab completion returns real results

---
 common/src/cmd.rs | 102 +++++++++++++++++++++++++++++++++++++++-------
 1 file changed, 87 insertions(+), 15 deletions(-)

diff --git a/common/src/cmd.rs b/common/src/cmd.rs
index bb134a5741..e3b47609dc 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), Message /* TODO */],
+                vec![PlayerName(false), Command(false), SubCommand],
                 "Run command as if you were another player",
                 true,
             ),
@@ -256,6 +256,7 @@ impl ChatCommand {
                 ArgumentSpec::Any(_, _) => "{}",
                 ArgumentSpec::Command(_) => "{}",
                 ArgumentSpec::Message => "{/.*/}",
+                ArgumentSpec::SubCommand => "{/.*/}",
                 ArgumentSpec::OneOf(_, _, _, _) => "{}", // TODO
             })
             .collect::<Vec<_>>()
@@ -304,6 +305,8 @@ pub enum ArgumentSpec {
     /// This is the final argument, consuming all characters until the end of
     /// input.
     Message,
+    /// This command is followed by another command (such as in /sudo)
+    SubCommand,
     /// The argument is likely an enum. The associated values are
     /// * label
     /// * Predefined string completions
@@ -363,6 +366,7 @@ impl ArgumentSpec {
                 }
             },
             ArgumentSpec::Message => "<message>".to_string(),
+            ArgumentSpec::SubCommand => "<[/]command> [args...]".to_string(),
             ArgumentSpec::OneOf(label, _, _, optional) => {
                 if *optional {
                     format! {"[{}]", label}
@@ -373,13 +377,9 @@ impl ArgumentSpec {
         }
     }
 
-    pub fn complete(&self, state: &State, part: &String) -> Vec<String> {
+    pub fn complete(&self, part: &str, state: &State) -> Vec<String> {
         match self {
-            ArgumentSpec::PlayerName(_) => (&state.ecs().read_storage::<Player>())
-                .join()
-                .filter(|player| player.alias.starts_with(part))
-                .map(|player| player.alias.clone())
-                .collect(),
+            ArgumentSpec::PlayerName(_) => complete_player(part, &state),
             ArgumentSpec::ItemSpec(_) => assets::iterate()
                 .filter(|asset| asset.starts_with(part))
                 .map(|c| c.to_string())
@@ -387,13 +387,9 @@ impl ArgumentSpec {
             ArgumentSpec::Float(_, x, _) => vec![format!("{}", x)],
             ArgumentSpec::Integer(_, x, _) => vec![format!("{}", x)],
             ArgumentSpec::Any(_, _) => vec![],
-            ArgumentSpec::Command(_) => CHAT_COMMANDS
-                .iter()
-                .map(|com| com.keyword())
-                .filter(|kwd| kwd.starts_with(part) || format!("/{}", kwd).starts_with(part))
-                .map(|c| c.to_string())
-                .collect(),
-            ArgumentSpec::Message => vec![],
+            ArgumentSpec::Command(_) => complete_command(part),
+            ArgumentSpec::Message => complete_player(part, &state),
+            ArgumentSpec::SubCommand => complete_command(part),
             ArgumentSpec::OneOf(_, strings, alts, _) => {
                 let string_completions = strings
                     .iter()
@@ -401,10 +397,86 @@ impl ArgumentSpec {
                     .map(|c| c.to_string());
                 let alt_completions = alts
                     .iter()
-                    .flat_map(|b| (*b).complete(&state, part))
+                    .flat_map(|b| (*b).complete(part, &state))
                     .map(|c| c.to_string());
                 string_completions.chain(alt_completions).collect()
             },
         }
     }
 }
+
+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()
+}
+
+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())
+        .collect()
+}
+
+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) => {}
+        }
+        if j == n {
+            return Some(i);
+        }
+    }
+    return None;
+}
+
+pub fn complete(line: &str, state: &State) -> Vec<String> {
+    let word = 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() {
+            if let Ok(cmd) = cmd.parse::<ChatCommand>() {
+                if let Some(arg) = cmd.data().args.get(i) {
+                    println!("Arg completion: {}", word);
+                    arg.complete(word, &state)
+                } else {
+                    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 {
+            // Completing chat command name
+            complete_command(word)
+        }
+    } else {
+        // Not completing a command
+        complete_player(word, &state)
+    }
+}
+

From 28e94afd3fa6eeee67c4180be26d9bb0763241c4 Mon Sep 17 00:00:00 2001
From: CapsizeGlimmer <>
Date: Fri, 8 May 2020 17:38:58 -0400
Subject: [PATCH 6/9] 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::<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)
     }
 }
-
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<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.
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<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));
             },

From b486de28ac9b63ac7670621ac6d4c2b07fca8fd1 Mon Sep 17 00:00:00 2001
From: CapsizeGlimmer <>
Date: Fri, 8 May 2020 22:42:51 -0400
Subject: [PATCH 7/9] Implement tab completion of enums (/object /time /spawn)
 and numbers

---
 common/src/assets/mod.rs       |   9 +-
 common/src/cmd.rs              | 282 ++++++++++++++++++++-------------
 common/src/comp/body/object.rs |  63 +++++++-
 server/src/cmd.rs              | 113 ++++---------
 voxygen/src/hud/chat.rs        |  17 --
 5 files changed, 273 insertions(+), 211 deletions(-)

diff --git a/common/src/assets/mod.rs b/common/src/assets/mod.rs
index a14ca93cf2..2810ed50eb 100644
--- a/common/src/assets/mod.rs
+++ b/common/src/assets/mod.rs
@@ -57,17 +57,10 @@ impl From<std::io::Error> for Error {
 
 lazy_static! {
     /// The HashMap where all loaded assets are stored in.
-    static ref ASSETS: RwLock<HashMap<String, Arc<dyn Any + 'static + Sync + Send>>> =
+    pub static ref ASSETS: RwLock<HashMap<String, Arc<dyn Any + 'static + Sync + Send>>> =
         RwLock::new(HashMap::new());
 }
 
-const ASSETS_TMP: [&'static str; 1] = ["common/items/lantern/black_0"];
-pub fn iterate() -> impl Iterator<Item = &'static str> {
-    // TODO FIXME implement this
-    //ASSETS.read().iter().flat_map(|e| e.keys())
-    ASSETS_TMP.iter().map(|k| *k)
-}
-
 // TODO: Remove this function. It's only used in world/ in a really ugly way.To
 // do this properly assets should have all their necessary data in one file. A
 // ron file could be used to combine voxel data with positioning data for
diff --git a/common/src/cmd.rs b/common/src/cmd.rs
index b99a3e0442..ddbe7faec1 100644
--- a/common/src/cmd.rs
+++ b/common/src/cmd.rs
@@ -1,5 +1,7 @@
-use crate::{assets, comp::Player, state::State};
+use crate::{assets, comp, npc, state::State};
+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.
 pub struct ChatCommandData {
@@ -85,63 +87,108 @@ pub static CHAT_COMMANDS: &'static [ChatCommand] = &[
     ChatCommand::Waypoint,
 ];
 
+lazy_static! {
+    static ref ALIGNMENTS: Vec<String> = vec!["wild", "enemy", "npc", "pet"]
+        .iter()
+        .map(|s| s.to_string())
+        .collect();
+    static ref ENTITIES: Vec<String> = {
+        let npc_names = &*npc::NPC_NAMES;
+        npc::ALL_NPCS
+            .iter()
+            .map(|&npc| npc_names[npc].keyword.clone())
+            .collect()
+    };
+    static ref OBJECTS: Vec<String> = comp::object::ALL_OBJECTS
+        .iter()
+        .map(|o| o.to_string().to_string())
+        .collect();
+    static ref TIMES: Vec<String> = vec![
+        "midnight", "night", "dawn", "morning", "day", "noon", "dusk"
+    ]
+    .iter()
+    .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 {
         use ArgumentSpec::*;
+        use Requirement::*;
         let cmd = ChatCommandData::new;
         match self {
             ChatCommand::Adminify => cmd(
-                vec![PlayerName(false)],
+                vec![PlayerName(Required)],
                 "Temporarily gives a player admin permissions or removes them",
                 true,
             ),
-            ChatCommand::Alias => cmd(vec![Any("name", false)], "Change your alias", false),
+            ChatCommand::Alias => cmd(vec![Any("name", Required)], "Change your alias", false),
             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![Float("x", f32::NAN, false), Float("y", f32::NAN, false)],
+                vec![
+                    Integer("x", 15000, Required),
+                    Integer("y", 15000, Required),
+                ],
                 "Prints some debug information about a column",
                 false,
             ),
             ChatCommand::Explosion => cmd(
-                vec![Float("radius", 5.0, false)],
+                vec![Float("radius", 5.0, Required)],
                 "Explodes the ground around you",
                 true,
             ),
             ChatCommand::GiveExp => cmd(
-                vec![Integer("amount", 50, false)],
+                vec![Integer("amount", 50, Required)],
                 "Give experience to yourself",
                 true,
             ),
             ChatCommand::GiveItem => cmd(
-                vec![ItemSpec(false), Integer("num", 1, true)],
+                vec![Enum("item", items(), Required), Integer("num", 1, Optional)],
                 "Give yourself some items",
                 true,
             ),
             ChatCommand::Goto => cmd(
                 vec![
-                    Float("x", 0.0, false),
-                    Float("y", 0.0, false),
-                    Float("z", 0.0, false),
+                    Float("x", 0.0, Required),
+                    Float("y", 0.0, Required),
+                    Float("z", 0.0, Required),
                 ],
                 "Teleport to a position",
                 true,
             ),
             ChatCommand::Health => cmd(
-                vec![Integer("hp", 100, false)],
+                vec![Integer("hp", 100, Required)],
                 "Set your current health",
                 true,
             ),
             ChatCommand::Help => ChatCommandData::new(
-                vec![Command(true)],
+                vec![Command(Optional)],
                 "Display information about commands",
                 false,
             ),
             ChatCommand::Jump => cmd(
                 vec![
-                    Float("x", 0.0, false),
-                    Float("y", 0.0, false),
-                    Float("z", 0.0, false),
+                    Float("x", 0.0, Required),
+                    Float("y", 0.0, Required),
+                    Float("z", 0.0, Required),
                 ],
                 "Offset your current position",
                 true,
@@ -150,50 +197,72 @@ impl ChatCommand {
             ChatCommand::KillNpcs => cmd(vec![], "Kill the NPCs", true),
             ChatCommand::Lantern => cmd(
                 vec![
-                    Float("strength", 5.0, false),
-                    Float("r", 1.0, true),
-                    Float("g", 1.0, true),
-                    Float("b", 1.0, true),
+                    Float("strength", 5.0, Required),
+                    Float("r", 1.0, Optional),
+                    Float("g", 1.0, Optional),
+                    Float("b", 1.0, Optional),
                 ],
                 "Change your lantern's strength and color",
                 true,
             ),
             ChatCommand::Light => cmd(
                 vec![
-                    Float("r", 1.0, true),
-                    Float("g", 1.0, true),
-                    Float("b", 1.0, true),
-                    Float("x", 0.0, true),
-                    Float("y", 0.0, true),
-                    Float("z", 0.0, true),
-                    Float("strength", 5.0, true),
+                    Float("r", 1.0, Optional),
+                    Float("g", 1.0, Optional),
+                    Float("b", 1.0, Optional),
+                    Float("x", 0.0, Optional),
+                    Float("y", 0.0, Optional),
+                    Float("z", 0.0, Optional),
+                    Float("strength", 5.0, Optional),
                 ],
                 "Spawn entity with light",
                 true,
             ),
-            ChatCommand::Object => cmd(vec![/*TODO*/], "Spawn an object", true),
+            ChatCommand::Object => cmd(
+                vec![Enum("object", OBJECTS.clone(), Required)],
+                "Spawn an object",
+                true,
+            ),
             ChatCommand::Players => cmd(vec![], "Lists players currently online", false),
             ChatCommand::RemoveLights => cmd(
-                vec![Float("radius", 20.0, true)],
+                vec![Float("radius", 20.0, Optional)],
                 "Removes all lights spawned by players",
                 true,
             ),
-            ChatCommand::SetLevel => {
-                cmd(vec![Integer("level", 10, false)], "Set player Level", true)
-            },
-            ChatCommand::Spawn => cmd(vec![/*TODO*/], "Spawn a test entity", true),
+            ChatCommand::SetLevel => cmd(
+                vec![Integer("level", 10, Required)],
+                "Set player Level",
+                true,
+            ),
+            ChatCommand::Spawn => cmd(
+                vec![
+                    Enum("alignment", ALIGNMENTS.clone(), Required),
+                    Enum("entity", ENTITIES.clone(), Required),
+                    Integer("amount", 1, Optional),
+                ],
+                "Spawn a test entity",
+                true,
+            ),
             ChatCommand::Sudo => cmd(
-                vec![PlayerName(false), SubCommand],
+                vec![PlayerName(Required), SubCommand],
                 "Run command as if you were another player",
                 true,
             ),
             ChatCommand::Tell => cmd(
-                vec![PlayerName(false), Message],
+                vec![PlayerName(Required), Message],
                 "Send a message to another player",
                 false,
             ),
-            ChatCommand::Time => cmd(vec![/*TODO*/], "Set the time of day", true),
-            ChatCommand::Tp => cmd(vec![PlayerName(true)], "Teleport to another player", true),
+            ChatCommand::Time => cmd(
+                vec![Enum("time", TIMES.clone(), Optional)],
+                "Set the time of day",
+                true,
+            ),
+            ChatCommand::Tp => cmd(
+                vec![PlayerName(Optional)],
+                "Teleport to another player",
+                true,
+            ),
             ChatCommand::Version => cmd(vec![], "Prints server version", false),
             ChatCommand::Waypoint => {
                 cmd(vec![], "Set your waypoint to your current position", true)
@@ -250,21 +319,20 @@ impl ChatCommand {
             .iter()
             .map(|arg| match arg {
                 ArgumentSpec::PlayerName(_) => "{}",
-                ArgumentSpec::ItemSpec(_) => "{}",
                 ArgumentSpec::Float(_, _, _) => "{}",
                 ArgumentSpec::Integer(_, _, _) => "{d}",
                 ArgumentSpec::Any(_, _) => "{}",
                 ArgumentSpec::Command(_) => "{}",
                 ArgumentSpec::Message => "{/.*/}",
                 ArgumentSpec::SubCommand => "{} {/.*/}",
-                ArgumentSpec::OneOf(_, _, _, _) => "{}", // TODO
+                ArgumentSpec::Enum(_, _, _) => "{}", // TODO
             })
             .collect::<Vec<_>>()
             .join(" ")
     }
 }
 
-impl std::str::FromStr for ChatCommand {
+impl FromStr for ChatCommand {
     type Err = ();
 
     fn from_str(keyword: &str) -> Result<ChatCommand, ()> {
@@ -282,26 +350,39 @@ impl std::str::FromStr for ChatCommand {
     }
 }
 
+pub enum Requirement {
+    Required,
+    Optional,
+}
+impl Deref for Requirement {
+    type Target = bool;
+
+    fn deref(&self) -> &bool {
+        match self {
+            Requirement::Required => &true,
+            Requirement::Optional => &false,
+        }
+    }
+}
+
 /// Representation for chat command arguments
 pub enum ArgumentSpec {
     /// The argument refers to a player by alias
-    PlayerName(bool),
-    /// The argument refers to an item asset by path
-    ItemSpec(bool),
+    PlayerName(Requirement),
     /// The argument is a float. The associated values are
     /// * label
-    /// * default tab-completion
+    /// * suggested tab-completion
     /// * whether it's optional
-    Float(&'static str, f32, bool),
+    Float(&'static str, f32, Requirement),
     /// The argument is a float. The associated values are
     /// * label
-    /// * default tab-completion
+    /// * suggested tab-completion
     /// * whether it's optional
-    Integer(&'static str, i32, bool),
+    Integer(&'static str, i32, Requirement),
     /// The argument is any string that doesn't contain spaces
-    Any(&'static str, bool),
-    /// The argument is a command name
-    Command(bool),
+    Any(&'static str, Requirement),
+    /// The argument is a command name (such as in /help)
+    Command(Requirement),
     /// This is the final argument, consuming all characters until the end of
     /// input.
     Message,
@@ -310,68 +391,55 @@ pub enum ArgumentSpec {
     /// The argument is likely an enum. The associated values are
     /// * label
     /// * Predefined string completions
-    /// * Other completion types
     /// * whether it's optional
-    OneOf(
-        &'static str,
-        &'static [&'static str],
-        Vec<Box<ArgumentSpec>>,
-        bool,
-    ),
+    Enum(&'static str, Vec<String>, Requirement),
 }
 
 impl ArgumentSpec {
     pub fn usage_string(&self) -> String {
         match self {
-            ArgumentSpec::PlayerName(optional) => {
-                if *optional {
-                    "[player]".to_string()
-                } else {
+            ArgumentSpec::PlayerName(req) => {
+                if **req {
                     "<player>".to_string()
+                } else {
+                    "[player]".to_string()
                 }
             },
-            ArgumentSpec::ItemSpec(optional) => {
-                if *optional {
-                    "[item]".to_string()
-                } else {
-                    "<item>".to_string()
-                }
-            },
-            ArgumentSpec::Float(label, _, optional) => {
-                if *optional {
-                    format!("[{}]", label)
-                } else {
+            ArgumentSpec::Float(label, _, req) => {
+                if **req {
                     format!("<{}>", label)
-                }
-            },
-            ArgumentSpec::Integer(label, _, optional) => {
-                if *optional {
+                } else {
                     format!("[{}]", label)
-                } else {
-                    format!("<{}>", label)
                 }
             },
-            ArgumentSpec::Any(label, optional) => {
-                if *optional {
+            ArgumentSpec::Integer(label, _, req) => {
+                if **req {
+                    format!("<{}>", label)
+                } else {
                     format!("[{}]", label)
-                } else {
-                    format!("<{}>", label)
                 }
             },
-            ArgumentSpec::Command(optional) => {
-                if *optional {
-                    "[[/]command]".to_string()
+            ArgumentSpec::Any(label, req) => {
+                if **req {
+                    format!("<{}>", label)
                 } else {
+                    format!("[{}]", label)
+                }
+            },
+            ArgumentSpec::Command(req) => {
+                if **req {
                     "<[/]command>".to_string()
+                } else {
+                    "[[/]command]".to_string()
                 }
             },
             ArgumentSpec::Message => "<message>".to_string(),
             ArgumentSpec::SubCommand => "<[/]command> [args...]".to_string(),
-            ArgumentSpec::OneOf(label, _, _, optional) => {
-                if *optional {
-                    format! {"[{}]", label}
-                } else {
+            ArgumentSpec::Enum(label, _, req) => {
+                if **req {
                     format! {"<{}>", label}
+                } else {
+                    format! {"[{}]", label}
                 }
             },
         }
@@ -380,33 +448,35 @@ impl ArgumentSpec {
     pub fn complete(&self, part: &str, state: &State) -> Vec<String> {
         match self {
             ArgumentSpec::PlayerName(_) => complete_player(part, &state),
-            ArgumentSpec::ItemSpec(_) => assets::iterate()
-                .filter(|asset| asset.starts_with(part))
-                .map(|c| c.to_string())
-                .collect(),
-            ArgumentSpec::Float(_, x, _) => vec![format!("{}", x)],
-            ArgumentSpec::Integer(_, x, _) => vec![format!("{}", x)],
+            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::OneOf(_, strings, alts, _) => {
-                let string_completions = strings
-                    .iter()
-                    .filter(|string| string.starts_with(part))
-                    .map(|c| c.to_string());
-                let alt_completions = alts
-                    .iter()
-                    .flat_map(|b| (*b).complete(part, &state))
-                    .map(|c| c.to_string());
-                string_completions.chain(alt_completions).collect()
-            },
+            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::<Player>();
+    let storage = state.ecs().read_storage::<comp::Player>();
     let mut iter = storage.join();
     if let Some(first) = iter.next() {
         std::iter::once(first)
diff --git a/common/src/comp/body/object.rs b/common/src/comp/body/object.rs
index d179884062..e3121eb9a5 100644
--- a/common/src/comp/body/object.rs
+++ b/common/src/comp/body/object.rs
@@ -65,7 +65,7 @@ impl Body {
     }
 }
 
-const ALL_OBJECTS: [Body; 52] = [
+pub const ALL_OBJECTS: [Body; 53] = [
     Body::Arrow,
     Body::Bomb,
     Body::Scarecrow,
@@ -114,8 +114,69 @@ const ALL_OBJECTS: [Body; 52] = [
     Body::CarpetHumanSquare,
     Body::CarpetHumanSquare2,
     Body::CarpetHumanSquircle,
+    Body::Pouch,
     Body::CraftingBench,
     Body::BoltFire,
     Body::BoltFireBig,
     Body::ArrowSnake,
 ];
+
+impl Body {
+    pub fn to_string(&self) -> &str {
+        match self {
+            Body::Arrow => "arrow",
+            Body::Bomb => "bomb",
+            Body::Scarecrow => "scarecrow",
+            Body::Cauldron => "cauldron",
+            Body::ChestVines => "chest_vines",
+            Body::Chest => "chest",
+            Body::ChestDark => "chest_dark",
+            Body::ChestDemon => "chest_demon",
+            Body::ChestGold => "chest_gold",
+            Body::ChestLight => "chest_light",
+            Body::ChestOpen => "chest_open",
+            Body::ChestSkull => "chest_skull",
+            Body::Pumpkin => "pumpkin",
+            Body::Pumpkin2 => "pumpkin_2",
+            Body::Pumpkin3 => "pumpkin_3",
+            Body::Pumpkin4 => "pumpkin_4",
+            Body::Pumpkin5 => "pumpkin_5",
+            Body::Campfire => "campfire",
+            Body::CampfireLit => "campfire_lit",
+            Body::LanternGround => "lantern_ground",
+            Body::LanternGroundOpen => "lantern_ground_open",
+            Body::LanternStanding => "lantern_standing",
+            Body::LanternStanding2 => "lantern_standing_2",
+            Body::PotionRed => "potion_red",
+            Body::PotionBlue => "potion_blue",
+            Body::PotionGreen => "potion_green",
+            Body::Crate => "crate",
+            Body::Tent => "tent",
+            Body::WindowSpooky => "window_spooky",
+            Body::DoorSpooky => "door_spooky",
+            Body::Anvil => "anvil",
+            Body::Gravestone => "gravestone",
+            Body::Gravestone2 => "gravestone_2",
+            Body::Bench => "bench",
+            Body::Chair => "chair",
+            Body::Chair2 => "chair_2",
+            Body::Chair3 => "chair_3",
+            Body::Table => "table",
+            Body::Table2 => "table_2",
+            Body::Table3 => "table_3",
+            Body::Drawer => "drawer",
+            Body::BedBlue => "bed_blue",
+            Body::Carpet => "carpet",
+            Body::Bedroll => "bedroll",
+            Body::CarpetHumanRound => "carpet_human_round",
+            Body::CarpetHumanSquare => "carpet_human_square",
+            Body::CarpetHumanSquare2 => "carpet_human_square_2",
+            Body::CarpetHumanSquircle => "carpet_human_squircle",
+            Body::Pouch => "pouch",
+            Body::CraftingBench => "crafting_bench",
+            Body::BoltFire => "bolt_fire",
+            Body::BoltFireBig => "bolt_fire_big",
+            Body::ArrowSnake => "arrow_snake",
+        }
+    }
+}
diff --git a/server/src/cmd.rs b/server/src/cmd.rs
index 26a6263484..bb86e079f3 100644
--- a/server/src/cmd.rs
+++ b/server/src/cmd.rs
@@ -78,6 +78,7 @@ fn get_handler(cmd: &ChatCommand) -> CommandHandler {
         ChatCommand::Waypoint => handle_waypoint,
     }
 }
+
 fn handle_give_item(
     server: &mut Server,
     client: EcsEntity,
@@ -616,85 +617,39 @@ fn handle_object(
     .with(ori);*/
     if let (Some(pos), Some(ori)) = (pos, ori) {
         let obj_str_res = obj_type.as_ref().map(String::as_str);
-        let obj_type = match obj_str_res {
-            Ok("scarecrow") => comp::object::Body::Scarecrow,
-            Ok("cauldron") => comp::object::Body::Cauldron,
-            Ok("chest_vines") => comp::object::Body::ChestVines,
-            Ok("chest") => comp::object::Body::Chest,
-            Ok("chest_dark") => comp::object::Body::ChestDark,
-            Ok("chest_demon") => comp::object::Body::ChestDemon,
-            Ok("chest_gold") => comp::object::Body::ChestGold,
-            Ok("chest_light") => comp::object::Body::ChestLight,
-            Ok("chest_open") => comp::object::Body::ChestOpen,
-            Ok("chest_skull") => comp::object::Body::ChestSkull,
-            Ok("pumpkin") => comp::object::Body::Pumpkin,
-            Ok("pumpkin_2") => comp::object::Body::Pumpkin2,
-            Ok("pumpkin_3") => comp::object::Body::Pumpkin3,
-            Ok("pumpkin_4") => comp::object::Body::Pumpkin4,
-            Ok("pumpkin_5") => comp::object::Body::Pumpkin5,
-            Ok("campfire") => comp::object::Body::Campfire,
-            Ok("campfire_lit") => comp::object::Body::CampfireLit,
-            Ok("lantern_ground") => comp::object::Body::LanternGround,
-            Ok("lantern_ground_open") => comp::object::Body::LanternGroundOpen,
-            Ok("lantern_2") => comp::object::Body::LanternStanding2,
-            Ok("lantern") => comp::object::Body::LanternStanding,
-            Ok("potion_blue") => comp::object::Body::PotionBlue,
-            Ok("potion_green") => comp::object::Body::PotionGreen,
-            Ok("potion_red") => comp::object::Body::PotionRed,
-            Ok("crate") => comp::object::Body::Crate,
-            Ok("tent") => comp::object::Body::Tent,
-            Ok("bomb") => comp::object::Body::Bomb,
-            Ok("window_spooky") => comp::object::Body::WindowSpooky,
-            Ok("door_spooky") => comp::object::Body::DoorSpooky,
-            Ok("carpet") => comp::object::Body::Carpet,
-            Ok("table_human") => comp::object::Body::Table,
-            Ok("table_human_2") => comp::object::Body::Table2,
-            Ok("table_human_3") => comp::object::Body::Table3,
-            Ok("drawer") => comp::object::Body::Drawer,
-            Ok("bed_human_blue") => comp::object::Body::BedBlue,
-            Ok("anvil") => comp::object::Body::Anvil,
-            Ok("gravestone") => comp::object::Body::Gravestone,
-            Ok("gravestone_2") => comp::object::Body::Gravestone2,
-            Ok("chair") => comp::object::Body::Chair,
-            Ok("chair_2") => comp::object::Body::Chair2,
-            Ok("chair_3") => comp::object::Body::Chair3,
-            Ok("bench_human") => comp::object::Body::Bench,
-            Ok("bedroll") => comp::object::Body::Bedroll,
-            Ok("carpet_human_round") => comp::object::Body::CarpetHumanRound,
-            Ok("carpet_human_square") => comp::object::Body::CarpetHumanSquare,
-            Ok("carpet_human_square_2") => comp::object::Body::CarpetHumanSquare2,
-            Ok("carpet_human_squircle") => comp::object::Body::CarpetHumanSquircle,
-            Ok("crafting_bench") => comp::object::Body::CraftingBench,
-            _ => {
-                return server.notify_client(
-                    client,
-                    ServerMsg::private(String::from("Object not found!")),
-                );
-            },
-        };
-        server
-            .state
-            .create_object(pos, obj_type)
-            .with(comp::Ori(
-                // converts player orientation into a 90° rotation for the object by using the axis
-                // with the highest value
-                Dir::from_unnormalized(ori.0.map(|e| {
-                    if e.abs() == ori.0.map(|e| e.abs()).reduce_partial_max() {
-                        e
-                    } else {
-                        0.0
-                    }
-                }))
-                .unwrap_or_default(),
-            ))
-            .build();
-        server.notify_client(
-            client,
-            ServerMsg::private(format!(
-                "Spawned: {}",
-                obj_str_res.unwrap_or("<Unknown object>")
-            )),
-        );
+        if let Some(obj_type) = comp::object::ALL_OBJECTS
+            .iter()
+            .find(|o| Ok(o.to_string()) == obj_str_res)
+        {
+            server
+                .state
+                .create_object(pos, *obj_type)
+                .with(comp::Ori(
+                    // converts player orientation into a 90° rotation for the object by using the
+                    // axis with the highest value
+                    Dir::from_unnormalized(ori.0.map(|e| {
+                        if e.abs() == ori.0.map(|e| e.abs()).reduce_partial_max() {
+                            e
+                        } else {
+                            0.0
+                        }
+                    }))
+                    .unwrap_or_default(),
+                ))
+                .build();
+            server.notify_client(
+                client,
+                ServerMsg::private(format!(
+                    "Spawned: {}",
+                    obj_str_res.unwrap_or("<Unknown object>")
+                )),
+            );
+        } else {
+            return server.notify_client(
+                client,
+                ServerMsg::private(String::from("Object not found!")),
+            );
+        }
     } else {
         server.notify_client(client, ServerMsg::private(format!("You have no position!")));
     }
diff --git a/voxygen/src/hud/chat.rs b/voxygen/src/hud/chat.rs
index 50310bfe94..bdbb0d8e89 100644
--- a/voxygen/src/hud/chat.rs
+++ b/voxygen/src/hud/chat.rs
@@ -29,8 +29,6 @@ widget_ids! {
 }
 
 const MAX_MESSAGES: usize = 100;
-// Maximum completions shown at once
-const MAX_COMPLETIONS: usize = 10;
 
 #[derive(WidgetCommon)]
 pub struct Chat<'a> {
@@ -456,21 +454,6 @@ fn do_tab_completion(cursor: usize, input: &str, word: &str) -> (String, usize)
     }
 }
 
-fn cursor_index_to_offset(
-    index: text::cursor::Index,
-    text: &str,
-    ui: &Ui,
-    fonts: &ConrodVoxygenFonts,
-) -> Option<usize> {
-    // Width and font must match that of the chat TextEdit
-    let width = 460.0;
-    let font = ui.fonts.get(fonts.opensans.conrod_id)?;
-    let font_size = fonts.opensans.scale(15);
-    let infos = text::line::infos(&text, &font, font_size).wrap_by_whitespace(width);
-
-    text::glyph::index_after_cursor(infos, index)
-}
-
 fn cursor_offset_to_index(
     offset: usize,
     text: &str,

From 9d118b55a03e0866b30ad699121ffd20df9ca46b Mon Sep 17 00:00:00 2001
From: CapsizeGlimmer <>
Date: Sat, 9 May 2020 16:41:29 -0400
Subject: [PATCH 8/9] Fixed player list tab completion

---
 client/src/cmd.rs        | 121 +++++++++++++++++++++++++++++++
 client/src/lib.rs        |   1 +
 common/src/assets/mod.rs |  27 ++++++-
 common/src/cmd.rs        | 149 ++-------------------------------------
 server/src/cmd.rs        |   1 -
 voxygen/src/hud/chat.rs  |   6 +-
 voxygen/src/hud/mod.rs   |   2 +-
 7 files changed, 158 insertions(+), 149 deletions(-)
 create mode 100644 client/src/cmd.rs

diff --git a/client/src/cmd.rs b/client/src/cmd.rs
new file mode 100644
index 0000000000..9b05f9b779
--- /dev/null
+++ b/client/src/cmd.rs
@@ -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)
+    }
+}
diff --git a/client/src/lib.rs b/client/src/lib.rs
index df4b3bd8bb..3c57766420 100644
--- a/client/src/lib.rs
+++ b/client/src/lib.rs
@@ -1,6 +1,7 @@
 #![deny(unsafe_code)]
 #![feature(label_break_value)]
 
+pub mod cmd;
 pub mod error;
 
 // Reexports
diff --git a/common/src/assets/mod.rs b/common/src/assets/mod.rs
index 2810ed50eb..3f2ba97805 100644
--- a/common/src/assets/mod.rs
+++ b/common/src/assets/mod.rs
@@ -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
diff --git a/common/src/cmd.rs b/common/src/cmd.rs
index ddbe7faec1..be265b5203 100644
--- a/common/src/cmd.rs
+++ b/common/src/cmd.rs
@@ -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)
-    }
 }
diff --git a/server/src/cmd.rs b/server/src/cmd.rs
index bb86e079f3..0d1dca0958 100644
--- a/server/src/cmd.rs
+++ b/server/src/cmd.rs
@@ -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
diff --git a/voxygen/src/hud/chat.rs b/voxygen/src/hud/chat.rs
index bdbb0d8e89..82b5d571eb 100644
--- a/voxygen/src/hud/chat.rs
+++ b/voxygen/src/hud/chat.rs
@@ -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;
         }
diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs
index eff6563521..2bc2e85544 100644
--- a/voxygen/src/hud/mod.rs
+++ b/voxygen/src/hud/mod.rs
@@ -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)

From 3f76d1d702e7e37b353e094614ca6b86842a1084 Mon Sep 17 00:00:00 2001
From: CapsizeGlimmer <>
Date: Sat, 9 May 2020 21:17:03 -0400
Subject: [PATCH 9/9] Rework tp command - "/sudo player /tp" is short for
 "/sudo player /tp sudoer"

---
 CHANGELOG.md             |  4 +++
 common/src/assets/mod.rs |  2 +-
 server/src/cmd.rs        | 78 ++++++++++++++++++++++------------------
 3 files changed, 48 insertions(+), 36 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1978b3b1ed..00cde7ef01 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -70,6 +70,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Cultists clothing
 - You can start the game by pressing "enter" from the character selection menu
 - Added server-side character saving
+- Added tab completion in chat for player names and chat commands
 
 ### Changed
 
@@ -87,6 +88,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Rewrote the humanoid skeleton to be more ideal for attack animations
 - Arrows can no longer hurt their owners
 - Increased overall character scale
+- `/sudo player /tp` is short for `/sudo player /tp me`
+- The `/object` command can create any object in comp::object::Body
+- The `/help` command takes an optional argument. `/help /sudo` will show you information about only the sudo command.
 
 ### Removed
 
diff --git a/common/src/assets/mod.rs b/common/src/assets/mod.rs
index 3f2ba97805..003ec94a54 100644
--- a/common/src/assets/mod.rs
+++ b/common/src/assets/mod.rs
@@ -60,7 +60,7 @@ lazy_static! {
     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
+    /// List of item specifiers. Useful for tab completing
     pub static ref ITEM_SPECS: Vec<String> = {
         let base = ASSETS_PATH.join("common").join("items");
         let mut items = vec![];
diff --git a/server/src/cmd.rs b/server/src/cmd.rs
index 0d1dca0958..9beaf292c3 100644
--- a/server/src/cmd.rs
+++ b/server/src/cmd.rs
@@ -86,7 +86,9 @@ fn handle_give_item(
     args: String,
     action: &ChatCommand,
 ) {
-    if let (Some(item_name), give_amount_opt) = scan_fmt_some!(&args, &action.arg_fmt(), String, u32) {
+    if let (Some(item_name), give_amount_opt) =
+        scan_fmt_some!(&args, &action.arg_fmt(), String, u32)
+    {
         let give_amount = give_amount_opt.unwrap_or(1);
         if let Ok(item) = assets::load_cloned(&item_name) {
             let mut item: Item = item;
@@ -145,7 +147,10 @@ fn handle_give_item(
             );
         }
     } else {
-        server.notify_client(client, ServerMsg::private(String::from(action.help_string())));
+        server.notify_client(
+            client,
+            ServerMsg::private(String::from(action.help_string())),
+        );
     }
 }
 
@@ -356,44 +361,47 @@ fn handle_tp(
     args: String,
     action: &ChatCommand,
 ) {
-    if let Ok(alias) = scan_fmt!(&args, &action.arg_fmt(), String) {
+    let opt_player = if let Some(alias) = scan_fmt_some!(&args, &action.arg_fmt(), String) {
         let ecs = server.state.ecs();
-        let opt_player = (&ecs.entities(), &ecs.read_storage::<comp::Player>())
+        (&ecs.entities(), &ecs.read_storage::<comp::Player>())
             .join()
             .find(|(_, player)| player.alias == alias)
-            .map(|(entity, _)| entity);
-        match server.state.read_component_cloned::<comp::Pos>(target) {
-            Some(_pos) => match opt_player {
-                Some(player) => match server.state.read_component_cloned::<comp::Pos>(player) {
-                    Some(pos) => {
-                        server.state.write_component(target, pos);
-                        server.state.write_component(target, comp::ForceUpdate);
-                    },
-                    None => server.notify_client(
-                        client,
-                        ServerMsg::private(format!("Unable to teleport to player '{}'!", alias)),
-                    ),
-                },
-                None => {
-                    server.notify_client(
-                        client,
-                        ServerMsg::private(format!("Player '{}' not found!", alias)),
-                    );
-                    server.notify_client(
-                        client,
-                        ServerMsg::private(String::from(action.help_string())),
-                    );
-                },
-            },
-            None => {
-                server.notify_client(client, ServerMsg::private(format!("You have no position!")));
-            },
+            .map(|(entity, _)| entity)
+    } else {
+        if client != target {
+            Some(client)
+        } else {
+            server.notify_client(
+                client,
+                ServerMsg::private("You must specify a player name".to_string()),
+            );
+            server.notify_client(
+                client,
+                ServerMsg::private(String::from(action.help_string())),
+            );
+            return;
+        }
+    };
+    if let Some(_pos) = server.state.read_component_cloned::<comp::Pos>(target) {
+        if let Some(player) = opt_player {
+            if let Some(pos) = server.state.read_component_cloned::<comp::Pos>(player) {
+                server.state.write_component(target, pos);
+                server.state.write_component(target, comp::ForceUpdate);
+            } else {
+                server.notify_client(
+                    client,
+                    ServerMsg::private(format!("Unable to teleport to player!")),
+                );
+            }
+        } else {
+            server.notify_client(client, ServerMsg::private(format!("Player not found!")));
+            server.notify_client(
+                client,
+                ServerMsg::private(String::from(action.help_string())),
+            );
         }
     } else {
-        server.notify_client(
-            client,
-            ServerMsg::private(String::from(action.help_string())),
-        );
+        server.notify_client(client, ServerMsg::private(format!("You have no position!")));
     }
 }