diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index 27bfbe9a..338468c2 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -809,7 +809,115 @@ pub fn append_mode(cx: &mut Context) {
     doc.set_selection(view.id, selection);
 }
 
-const COMMAND_LIST: &[&str] = &["write", "open", "quit", "quit!"];
+mod cmd {
+    use super::*;
+    use std::collections::HashMap;
+
+    use helix_view::editor::Action;
+
+    #[derive(Clone)]
+    pub struct Command {
+        pub name: &'static str,
+        pub alias: Option<&'static str>,
+        pub doc: &'static str,
+        // params, flags, helper, completer
+        pub fun: fn(&mut Editor, &[&str], PromptEvent),
+    }
+
+    fn quit(editor: &mut Editor, args: &[&str], event: PromptEvent) {
+        // last view and we have unsaved changes
+        if editor.tree.views().count() == 1 {
+            let modified: Vec<_> = editor
+                .documents()
+                .filter(|doc| doc.is_modified())
+                .map(|doc| {
+                    doc.relative_path()
+                        .and_then(|path| path.to_str())
+                        .unwrap_or("[scratch]")
+                })
+                .collect();
+
+            if !modified.is_empty() {
+                let err = format!(
+                    "{} unsaved buffer(s) remaining: {:?}",
+                    modified.len(),
+                    modified
+                );
+                editor.set_error(err);
+                return;
+            }
+        }
+        editor.close(editor.view().id, /* close_buffer */ false);
+    }
+
+    fn force_quit(editor: &mut Editor, args: &[&str], event: PromptEvent) {
+        editor.close(editor.view().id, /* close_buffer */ false);
+    }
+
+    fn open(editor: &mut Editor, args: &[&str], event: PromptEvent) {
+        let path = args[0];
+        editor.open(path.into(), Action::Replace);
+    }
+
+    fn write(editor: &mut Editor, args: &[&str], event: PromptEvent) {
+        let id = editor.view().doc;
+        let doc = &mut editor.documents[id];
+        if doc.path().is_none() {
+            editor.set_error("cannot write a buffer without a filename".to_string());
+            return;
+        }
+        tokio::spawn(doc.save());
+    }
+    fn new_file(editor: &mut Editor, args: &[&str], event: PromptEvent) {
+        editor.new_file(Action::Replace);
+    }
+
+    pub const COMMAND_LIST: &[Command] = &[
+        Command {
+            name: "quit",
+            alias: Some("q"),
+            doc: "Close the current view.",
+            fun: quit,
+        },
+        Command {
+            name: "quit!",
+            alias: Some("q!"),
+            doc: "Close the current view.",
+            fun: force_quit,
+        },
+        Command {
+            name: "open",
+            alias: Some("o"),
+            doc: "Open a file from disk into the current view.",
+            fun: open,
+        },
+        Command {
+            name: "write",
+            alias: Some("w"),
+            doc: "Write changes to disk.",
+            fun: write,
+        },
+        Command {
+            name: "new",
+            alias: Some("n"),
+            doc: "Create a new scratch buffer.",
+            fun: new_file,
+        },
+    ];
+
+    pub static COMMANDS: Lazy<HashMap<&'static str, Command>> = Lazy::new(|| {
+        let mut map = HashMap::new();
+
+        for cmd in COMMAND_LIST {
+            map.insert(cmd.name, cmd.clone());
+            if let Some(alias) = cmd.alias {
+                map.insert(alias, cmd.clone());
+            }
+        }
+
+        map
+    });
+}
 
 pub fn command_mode(cx: &mut Context) {
     let prompt = Prompt::new(
@@ -823,10 +931,10 @@ pub fn command_mode(cx: &mut Context) {
             if parts.len() <= 1 {
                 use std::{borrow::Cow, ops::Range};
                 let end = 0..;
-                COMMAND_LIST
+                cmd::COMMAND_LIST
                     .iter()
-                    .filter(|command| command.contains(input))
-                    .map(|command| (end.clone(), Cow::Borrowed(*command)))
+                    .filter(|command| command.name.contains(input))
+                    .map(|command| (end.clone(), Cow::Borrowed(command.name)))
                     .collect()
             } else {
                 let part = parts.last().unwrap();
@@ -854,52 +962,11 @@ pub fn command_mode(cx: &mut Context) {
 
             let parts = input.split_ascii_whitespace().collect::<Vec<&str>>();
 
-            match *parts.as_slice() {
-                ["q"] | ["quit"] => {
-                    // last view and we have unsaved changes
-                    if editor.tree.views().count() == 1 {
-                        let modified: Vec<_> = editor
-                            .documents()
-                            .filter(|doc| doc.is_modified())
-                            .map(|doc| {
-                                doc.relative_path()
-                                    .and_then(|path| path.to_str())
-                                    .unwrap_or("[scratch]")
-                            })
-                            .collect();
-
-                        if !modified.is_empty() {
-                            let err = format!(
-                                "{} unsaved buffer(s) remaining: {:?}",
-                                modified.len(),
-                                modified
-                            );
-                            editor.set_error(err);
-                            return;
-                        }
-                    }
-                    editor.close(editor.view().id, /* close_buffer */ false);
-                }
-                ["q!"] | ["quit!"] => {
-                    editor.close(editor.view().id, /* close_buffer */ false);
-                }
-                ["o", path] | ["open", path] => {
-                    editor.open(path.into(), Action::Replace);
-                }
-                ["w"] | ["write"] => {
-                    let id = editor.view().doc;
-                    let doc = &mut editor.documents[id];
-                    if doc.path().is_none() {
-                        editor.set_error("cannot write a buffer without a filename".to_string());
-                        return;
-                    }
-                    tokio::spawn(doc.save());
-                }
-                ["new"] => {
-                    editor.new_file(Action::Replace);
-                }
-                _ => (),
-            }
+            if let Some(cmd) = cmd::COMMANDS.get(parts[0]) {
+                (cmd.fun)(editor, &parts[1..], event);
+            } else {
+                editor.set_error(format!("no such command: '{}'", parts[0]));
+            };
         },
     );
     cx.push_layer(Box::new(prompt));