@@ -134,47 +134,76 @@ fn align_view(doc: &Document, view: &mut View, align: Align) {
134134 view.offset.row = line.saturating_sub(relative);
135135}
136136
137−/// A command is composed of a static name, and a function that takes the current state plus a count,
138−/// and does a side-effect on the state (usually by creating and applying a transaction).
139−#[derive(Copy, Clone)]
140−pub struct Command {
141− name: &'static str,
142− fun: fn(cx: &mut Context),
143− doc: &'static str,
144−}
145−
146−macro_rules! commands {
137+/// A MappableCommand is either a static command like "jump_view_up" or a Typable command like
138+/// :format. It causes a side-effect on the state (usually by creating and applying a transaction).
139+/// Both of these types of commands can be mapped with keybindings in the config.toml.
140+#[derive(Clone)]
141+pub enum MappableCommand {
142+ Typable {
143+ name: String,
144+ args: Vec<String>,
145+ doc: String,
146+ },
147+ Static {
148+ name: &'static str,
149+ fun: fn(cx: &mut Context),
150+ doc: &'static str,
151+ },
152+}
153+
154+macro_rules! static_commands {
147155 ( $($name:ident, $doc:literal,)* ) => {
148156 $(
149157 #[allow(non_upper_case_globals)]
150− pub const $name: Self = Self {
158+ pub const $name: Self = Self::Static {
151159 name: stringify!($name),
152160 fun: $name,
153161 doc: $doc
154162 };
155163 )*
156164
157− pub const COMMAND_LIST: &'static [Self] = &[
165+ pub const STATIC_COMMAND_LIST: &'static [Self] = &[
158166 $( Self::$name, )*
159167 ];
160168 }
161169}
162170
163−impl Command {
171+impl MappableCommand {
164172 pub fn execute(&self, cx: &mut Context) {
165− (self.fun)(cx);
173+ match &self {
174+ MappableCommand::Typable { name, args, doc: _ } => {
175+ let args: Vec<&str> = args.iter().map(|arg| arg.as_str()).collect();
176+ if let Some(command) = cmd::TYPABLE_COMMAND_MAP.get(name.as_str()) {
177+ let mut cx = compositor::Context {
178+ editor: cx.editor,
179+ jobs: cx.jobs,
180+ scroll: None,
181+ };
182+ if let Err(e) = (command.fun)(&mut cx, &args, PromptEvent::Validate) {
183+ cx.editor.set_error(format!("{}", e));
184+ }
185+ }
186+ }
187+ MappableCommand::Static { fun, .. } => (fun)(cx),
188+ }
166189 }
167190
168− pub fn name(&self) -> &'static str {
169− self.name
191+ pub fn name(&self) -> &str {
192+ match &self {
193+ MappableCommand::Typable { name, .. } => name,
194+ MappableCommand::Static { name, .. } => name,
195+ }
170196 }
171197
172− pub fn doc(&self) -> &'static str {
173− self.doc
198+ pub fn doc(&self) -> &str {
199+ match &self {
200+ MappableCommand::Typable { doc, .. } => doc,
201+ MappableCommand::Static { doc, .. } => doc,
202+ }
174203 }
175204
176205 #[rustfmt::skip]
177− commands!(
206+ static_commands!(
178207 no_op, "Do nothing",
179208 move_char_left, "Move left",
180209 move_char_right, "Move right",
@@ -367,33 +396,51 @@ impl Command {
367396 );
368397}
369398
370−impl fmt::Debug for Command {
399+impl fmt::Debug for MappableCommand {
371400 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
372− let Command { name, .. } = self;
373− f.debug_tuple("Command").field(name).finish()
401+ f.debug_tuple("MappableCommand")
402+ .field(&self.name())
403+ .finish()
374404 }
375405}
376406
377−impl fmt::Display for Command {
407+impl fmt::Display for MappableCommand {
378408 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
379− let Command { name, .. } = self;
380− f.write_str(name)
409+ f.write_str(self.name())
381410 }
382411}
383412
384−impl std::str::FromStr for Command {
413+impl std::str::FromStr for MappableCommand {
385414 type Err = anyhow::Error;
386415
387416 fn from_str(s: &str) -> Result<Self, Self::Err> {
388− Command::COMMAND_LIST
389− .iter()
390− .copied()
391− .find(|cmd| cmd.name == s)
392− .ok_or_else(|| anyhow!("No command named '{}'", s))
417+ if let Some(suffix) = s.strip_prefix(':') {
418+ let mut typable_command = suffix.split(' ').into_iter().map(|arg| arg.trim());
419+ let name = typable_command
420+ .next()
421+ .ok_or_else(|| anyhow!("Expected typable command name"))?;
422+ let args = typable_command
423+ .map(|s| s.to_owned())
424+ .collect::<Vec<String>>();
425+ cmd::TYPABLE_COMMAND_MAP
426+ .get(name)
427+ .map(|cmd| MappableCommand::Typable {
428+ name: cmd.name.to_owned(),
429+ doc: format!(":{} {:?}", cmd.name, args),
430+ args,
431+ })
432+ .ok_or_else(|| anyhow!("No TypableCommand named '{}'", s))
433+ } else {
434+ MappableCommand::STATIC_COMMAND_LIST
435+ .iter()
436+ .cloned()
437+ .find(|cmd| cmd.name() == s)
438+ .ok_or_else(|| anyhow!("No command named '{}'", s))
439+ }
393440 }
394441}
395442
396−impl<'de> Deserialize<'de> for Command {
443+impl<'de> Deserialize<'de> for MappableCommand {
397444 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
398445 where
399446 D: Deserializer<'de>,
@@ -403,9 +450,27 @@ impl<'de> Deserialize<'de> for Command {
403450 }
404451}
405452
406−impl PartialEq for Command {
453+impl PartialEq for MappableCommand {
407454 fn eq(&self, other: &Self) -> bool {
408− self.name() == other.name()
455+ match (self, other) {
456+ (
457+ MappableCommand::Typable {
458+ name: first_name, ..
459+ },
460+ MappableCommand::Typable {
461+ name: second_name, ..
462+ },
463+ ) => first_name == second_name,
464+ (
465+ MappableCommand::Static {
466+ name: first_name, ..
467+ },
468+ MappableCommand::Static {
469+ name: second_name, ..
470+ },
471+ ) => first_name == second_name,
472+ _ => false,
473+ }
409474 }
410475}
411476
@@ -2843,15 +2908,16 @@ mod cmd {
28432908 }
28442909 ];
28452910
2846− pub static COMMANDS: Lazy<HashMap<&'static str, &'static TypableCommand>> = Lazy::new(|| {
2847− TYPABLE_COMMAND_LIST
2848− .iter()
2849− .flat_map(|cmd| {
2850− std::iter::once((cmd.name, cmd))
2851− .chain(cmd.aliases.iter().map(move |&alias| (alias, cmd)))
2852− })
2853− .collect()
2854− });
2911+ pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableCommand>> =
2912+ Lazy::new(|| {
2913+ TYPABLE_COMMAND_LIST
2914+ .iter()
2915+ .flat_map(|cmd| {
2916+ std::iter::once((cmd.name, cmd))
2917+ .chain(cmd.aliases.iter().map(move |&alias| (alias, cmd)))
2918+ })
2919+ .collect()
2920+ });
28552921}
28562922
28572923fn command_mode(cx: &mut Context) {
@@ -2877,7 +2943,7 @@ fn command_mode(cx: &mut Context) {
28772943 if let Some(cmd::TypableCommand {
28782944 completer: Some(completer),
28792945 ..
2880− }) = cmd::COMMANDS.get(parts[0])
2946+ }) = cmd::TYPABLE_COMMAND_MAP.get(parts[0])
28812947 {
28822948 completer(part)
28832949 .into_iter()
@@ -2912,7 +2978,7 @@ fn command_mode(cx: &mut Context) {
29122978 }
29132979
29142980 // Handle typable commands
2915− if let Some(cmd) = cmd::COMMANDS.get(parts[0]) {
2981+ if let Some(cmd) = cmd::TYPABLE_COMMAND_MAP.get(parts[0]) {
29162982 if let Err(e) = (cmd.fun)(cx, &parts[1..], event) {
29172983 cx.editor.set_error(format!("{}", e));
29182984 }
@@ -2925,7 +2991,7 @@ fn command_mode(cx: &mut Context) {
29252991 prompt.doc_fn = Box::new(|input: &str| {
29262992 let part = input.split(' ').next().unwrap_or_default();
29272993
2928− if let Some(cmd::TypableCommand { doc, .. }) = cmd::COMMANDS.get(part) {
2994+ if let Some(cmd::TypableCommand { doc, .. }) = cmd::TYPABLE_COMMAND_MAP.get(part) {
29292995 return Some(doc);
29302996 }
29312997