use super::path::PathCompletionOptions;
use super::string_context::path_to_suggestions;
use crate::external::rig;
use crate::fuzzy::fuzzy_match;
use reedline::{Completer, CompletionResult, Span, Suggestion};
struct MetaCommandDef {
name: &'static str,
description: &'static str,
takes_argument: bool,
}
const META_COMMANDS: &[MetaCommandDef] = &[
MetaCommandDef {
name: "help",
description: "Search R help",
takes_argument: false,
},
MetaCommandDef {
name: "h",
description: "Search R help (alias)",
takes_argument: false,
},
MetaCommandDef {
name: "info",
description: "Show session information",
takes_argument: false,
},
MetaCommandDef {
name: "session",
description: "Show session information (alias)",
takes_argument: false,
},
MetaCommandDef {
name: "shell",
description: "Enter shell mode",
takes_argument: false,
},
MetaCommandDef {
name: "r",
description: "Return to R mode",
takes_argument: false,
},
MetaCommandDef {
name: "system",
description: "Execute system command",
takes_argument: true,
},
MetaCommandDef {
name: "reprex",
description: "Set reprex mode (on, off, or format)",
takes_argument: true,
},
MetaCommandDef {
name: "commands",
description: "Show available commands",
takes_argument: false,
},
MetaCommandDef {
name: "cmds",
description: "Show available commands (alias)",
takes_argument: false,
},
MetaCommandDef {
name: "restart",
description: "Restart R session",
takes_argument: false,
},
MetaCommandDef {
name: "restart!",
description: "Restart R session (skip confirmation)",
takes_argument: false,
},
MetaCommandDef {
name: "switch",
description: "Restart with different R version (requires rig)",
takes_argument: true,
},
MetaCommandDef {
name: "switch!",
description: "Restart with different R version, skip confirmation (requires rig)",
takes_argument: true,
},
MetaCommandDef {
name: "changelog",
description: "Show arf changelog",
takes_argument: false,
},
MetaCommandDef {
name: "history",
description: "Manage command history",
takes_argument: true,
},
MetaCommandDef {
name: "cd",
description: "Change working directory",
takes_argument: true,
},
MetaCommandDef {
name: "pushd",
description: "Push directory and change to it",
takes_argument: true,
},
MetaCommandDef {
name: "popd",
description: "Pop directory from stack",
takes_argument: false,
},
MetaCommandDef {
name: "ipc",
description: "IPC server and send-policy management",
takes_argument: true,
},
MetaCommandDef {
name: "quit",
description: "Quit arf",
takes_argument: false,
},
MetaCommandDef {
name: "exit",
description: "Quit arf",
takes_argument: false,
},
];
pub struct MetaCommandCompleter {
excluded_commands: Vec<&'static str>,
}
impl MetaCommandCompleter {
pub fn new() -> Self {
MetaCommandCompleter {
excluded_commands: vec![],
}
}
pub fn with_exclusions(excluded_commands: Vec<&'static str>) -> Self {
MetaCommandCompleter { excluded_commands }
}
pub(crate) fn shell_mode_exclusions() -> Vec<&'static str> {
vec![
"shell", "system", "restart", "reprex", "switch", "h", "help",
]
}
fn complete_commands(&self, line: &str, pos: usize) -> Vec<Suggestion> {
let trimmed = line.trim_start();
if !trimmed.starts_with(':') {
return vec![];
}
let after_colon = &trimmed[1..];
let has_trailing_space = after_colon.ends_with(' ') || after_colon.ends_with('\t');
let parts: Vec<&str> = after_colon.split_whitespace().collect();
let leading_whitespace = line.len() - trimmed.len();
if let Some(cmd) = parts.first()
&& (*cmd == "cd" || *cmd == "pushd")
&& (parts.len() > 1 || has_trailing_space)
{
let cmd_end = 1 + cmd.len(); let rest = &trimmed[cmd_end..];
let ws_after = rest.len() - rest.trim_start().len();
let arg_start = leading_whitespace + cmd_end + ws_after;
let partial = if pos > arg_start {
&line[arg_start..pos]
} else {
""
};
return path_to_suggestions(
partial,
pos,
arg_start,
&PathCompletionOptions {
directories_only: true,
..Default::default()
},
);
}
match (parts.len(), has_trailing_space) {
(0, _) => {
let start = leading_whitespace + 1; let mut suggestions: Vec<Suggestion> = META_COMMANDS
.iter()
.filter(|cmd| !self.excluded_commands.contains(&cmd.name))
.map(|cmd| Suggestion {
value: cmd.name.to_string(),
display_override: None,
description: Some(cmd.description.to_string()),
extra: None,
span: Span { start, end: pos },
append_whitespace: cmd.takes_argument,
style: None,
match_indices: None,
})
.collect();
suggestions.sort_by_key(|s| s.value.len());
suggestions
}
(1, false) => {
let partial = parts[0];
let start = leading_whitespace + 1; let mut suggestions: Vec<Suggestion> = META_COMMANDS
.iter()
.filter(|cmd| !self.excluded_commands.contains(&cmd.name))
.filter_map(|cmd| {
fuzzy_match(partial, cmd.name).map(|m| Suggestion {
value: cmd.name.to_string(),
display_override: None,
description: Some(cmd.description.to_string()),
extra: None,
span: Span { start, end: pos },
append_whitespace: cmd.takes_argument,
style: None,
match_indices: if m.indices.is_empty() {
None
} else {
Some(m.indices)
},
})
})
.collect();
suggestions.sort_by_key(|s| s.value.len());
suggestions
}
(1, true) => {
let cmd = parts[0];
if cmd == "switch" || cmd == "switch!" {
self.complete_switch_versions(line, pos, leading_whitespace, "")
} else if cmd == "history" {
self.complete_history_subcommands(pos, "")
} else if cmd == "ipc" {
self.complete_ipc_subcommands(pos, "")
} else if cmd == "reprex" {
self.complete_targets(
pos,
"",
&[
("on", "Enable reprex output"),
("off", "Disable reprex output"),
("format", "Format code before reprex evaluation"),
],
)
} else {
vec![]
}
}
(2, false) => {
let cmd = parts[0];
let partial = parts[1];
if cmd == "switch" || cmd == "switch!" {
self.complete_switch_versions(line, pos, leading_whitespace, partial)
} else if cmd == "history" {
self.complete_history_subcommands(pos, partial)
} else if cmd == "ipc" {
self.complete_ipc_subcommands(pos, partial)
} else if cmd == "reprex" {
self.complete_targets(
pos,
partial,
&[
("on", "Enable reprex output"),
("off", "Disable reprex output"),
("format", "Format code before reprex evaluation"),
],
)
} else {
vec![]
}
}
(2, true) => {
let cmd = parts[0];
let subcmd = parts[1];
if cmd == "history" && subcmd == "clear" {
self.complete_history_clear_targets(pos, "")
} else if cmd == "history" && subcmd == "browse" {
self.complete_history_browse_targets(pos, "")
} else if cmd == "ipc" && subcmd == "send-policy" {
self.complete_targets(
pos,
"",
&[
("prompt", "Require approval for every interactive send"),
("allow", "Allow sends for the current REPL process"),
],
)
} else {
vec![]
}
}
(3, false) => {
let cmd = parts[0];
let subcmd = parts[1];
let partial = parts[2];
if cmd == "history" && subcmd == "clear" {
self.complete_history_clear_targets(pos, partial)
} else if cmd == "history" && subcmd == "browse" {
self.complete_history_browse_targets(pos, partial)
} else if cmd == "ipc" && subcmd == "send-policy" {
self.complete_targets(
pos,
partial,
&[
("prompt", "Require approval for every interactive send"),
("allow", "Allow sends for the current REPL process"),
],
)
} else {
vec![]
}
}
_ => {
vec![]
}
}
}
fn complete_switch_versions(
&self,
line: &str,
pos: usize,
_leading_whitespace: usize,
partial: &str,
) -> Vec<Suggestion> {
if rig::rig_available().is_err() {
return vec![];
}
let versions = match rig::list_versions() {
Ok(v) => v,
Err(_) => return vec![],
};
let start = if partial.is_empty() {
pos
} else {
line.rfind(partial).unwrap_or(pos)
};
let match_len = partial.len();
let mut suggestions: Vec<Suggestion> = versions
.iter()
.filter(|v| v.name.starts_with(partial) || v.version.starts_with(partial))
.map(|v| {
let description = if v.default {
format!("R {} (default)", v.version)
} else {
format!("R {}", v.version)
};
let indices = if match_len > 0 {
Some((0..match_len).collect())
} else {
None
};
Suggestion {
value: v.name.clone(),
display_override: None,
description: Some(description),
extra: None,
span: Span { start, end: pos },
append_whitespace: false,
style: None,
match_indices: indices,
}
})
.collect();
for v in &versions {
for alias in &v.aliases {
if alias.starts_with(partial) {
let indices = if match_len > 0 {
Some((0..match_len).collect())
} else {
None
};
suggestions.push(Suggestion {
value: alias.clone(),
display_override: None,
description: Some(format!("R {} (alias)", v.version)),
extra: None,
span: Span { start, end: pos },
append_whitespace: false,
style: None,
match_indices: indices,
});
}
}
}
suggestions
}
fn complete_history_subcommands(&self, pos: usize, partial: &str) -> Vec<Suggestion> {
let subcommands = [
("browse", "Browse and manage command history"),
("clear", "Clear command history"),
("schema", "Display database schema and R examples"),
];
let match_len = partial.len();
subcommands
.iter()
.filter(|(name, _)| name.starts_with(partial))
.map(|(name, desc)| {
let indices = if match_len > 0 {
Some((0..match_len).collect())
} else {
None
};
Suggestion {
value: name.to_string(),
display_override: None,
description: Some(desc.to_string()),
extra: None,
span: Span {
start: pos - match_len,
end: pos,
},
append_whitespace: true,
style: None,
match_indices: indices,
}
})
.collect()
}
fn complete_ipc_subcommands(&self, pos: usize, partial: &str) -> Vec<Suggestion> {
let mut suggestions = self.complete_targets(
pos,
partial,
&[
("start", "Start the IPC server"),
("stop", "Stop the IPC server"),
("status", "Show IPC server status"),
(
"send-policy",
"Change interactive send approval for this process",
),
],
);
for suggestion in &mut suggestions {
suggestion.append_whitespace = suggestion.value == "send-policy";
}
suggestions
}
fn complete_targets(
&self,
pos: usize,
partial: &str,
targets: &[(&str, &str)],
) -> Vec<Suggestion> {
let match_len = partial.len();
targets
.iter()
.filter(|(name, _)| name.starts_with(partial))
.map(|(name, desc)| {
let indices = if match_len > 0 {
Some((0..match_len).collect())
} else {
None
};
Suggestion {
value: name.to_string(),
display_override: None,
description: Some(desc.to_string()),
extra: None,
span: Span {
start: pos - match_len,
end: pos,
},
append_whitespace: false,
style: None,
match_indices: indices,
}
})
.collect()
}
fn complete_history_clear_targets(&self, pos: usize, partial: &str) -> Vec<Suggestion> {
self.complete_targets(
pos,
partial,
&[
("r", "Clear R mode history"),
("shell", "Clear shell mode history"),
("all", "Clear all history"),
],
)
}
fn complete_history_browse_targets(&self, pos: usize, partial: &str) -> Vec<Suggestion> {
self.complete_targets(
pos,
partial,
&[
("r", "Browse R mode history"),
("shell", "Browse shell mode history"),
],
)
}
}
impl Default for MetaCommandCompleter {
fn default() -> Self {
Self::new()
}
}
impl Completer for MetaCommandCompleter {
fn complete(&mut self, line: &str, pos: usize) -> CompletionResult {
CompletionResult::fresh(self.complete_commands(line, pos))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn completion_suggestions(
completer: &mut MetaCommandCompleter,
line: &str,
pos: usize,
) -> Vec<Suggestion> {
completer.complete(line, pos).suggestions().to_vec()
}
#[test]
fn test_meta_command_completer_empty_colon() {
let mut completer = MetaCommandCompleter::new();
let suggestions = completion_suggestions(&mut completer, ":", 1);
assert!(!suggestions.is_empty());
let values: Vec<&str> = suggestions.iter().map(|s| s.value.as_str()).collect();
assert!(values.contains(&"shell"));
assert!(values.contains(&"r"));
assert!(values.contains(&"system"));
assert!(values.contains(&"reprex"));
assert!(values.contains(&"commands"));
assert!(values.contains(&"cmds"));
assert!(values.contains(&"restart"));
assert!(values.contains(&"restart!"));
assert!(values.contains(&"switch"));
assert!(values.contains(&"switch!"));
assert!(values.contains(&"quit"));
assert!(values.contains(&"exit"));
}
#[test]
fn test_meta_command_completer_partial_command() {
let mut completer = MetaCommandCompleter::new();
let suggestions = completion_suggestions(&mut completer, ":rep", 4);
assert_eq!(suggestions.len(), 1);
assert_eq!(suggestions[0].value, "reprex");
}
#[test]
fn test_meta_command_completer_no_subcommands() {
let mut completer = MetaCommandCompleter::new();
let suggestions = completion_suggestions(&mut completer, ":reprex ", 8);
assert_eq!(
suggestions
.iter()
.map(|s| s.value.as_str())
.collect::<Vec<_>>(),
["on", "off", "format"]
);
let suggestions = completion_suggestions(&mut completer, ":commands ", 10);
assert!(suggestions.is_empty());
}
#[test]
fn test_meta_command_completer_not_meta_command() {
let mut completer = MetaCommandCompleter::new();
let suggestions = completion_suggestions(&mut completer, "print(x)", 8);
assert!(suggestions.is_empty());
}
#[test]
fn test_meta_command_completer_has_descriptions() {
let mut completer = MetaCommandCompleter::new();
let suggestions = completion_suggestions(&mut completer, ":", 1);
let reprex = suggestions.iter().find(|s| s.value == "reprex").unwrap();
assert!(reprex.description.is_some());
assert!(reprex.description.as_ref().unwrap().contains("reprex"));
}
#[test]
fn test_meta_command_completer_excludes_r_command() {
let mut completer = MetaCommandCompleter::with_exclusions(vec!["r"]);
let suggestions = completion_suggestions(&mut completer, ":", 1);
let values: Vec<&str> = suggestions.iter().map(|s| s.value.as_str()).collect();
assert!(!values.contains(&"r"), "`:r` should be excluded in R mode");
assert!(
values.contains(&"shell"),
"`:shell` should still be present"
);
}
#[test]
fn test_meta_command_completer_excludes_shell_mode_commands() {
let mut completer =
MetaCommandCompleter::with_exclusions(MetaCommandCompleter::shell_mode_exclusions());
let suggestions = completion_suggestions(&mut completer, ":", 1);
let values: Vec<&str> = suggestions.iter().map(|s| s.value.as_str()).collect();
assert!(!values.contains(&"shell"), "`:shell` should be excluded");
assert!(!values.contains(&"system"), "`:system` should be excluded");
assert!(
!values.contains(&"restart"),
"`:restart` should be excluded"
);
assert!(!values.contains(&"reprex"), "`:reprex` should be excluded");
assert!(!values.contains(&"switch"), "`:switch` should be excluded");
assert!(!values.contains(&"h"), "`:h` should be excluded");
assert!(!values.contains(&"help"), "`:help` should be excluded");
assert!(values.contains(&"r"), "`:r` should be present");
assert!(
values.contains(&"commands"),
"`:commands` should be present"
);
assert!(values.contains(&"quit"), "`:quit` should be present");
assert!(values.contains(&"exit"), "`:exit` should be present");
}
#[test]
fn test_meta_command_completer_exclusion_affects_partial_match() {
let mut completer = MetaCommandCompleter::with_exclusions(vec!["r"]);
let suggestions = completion_suggestions(&mut completer, ":r", 2);
let values: Vec<&str> = suggestions.iter().map(|s| s.value.as_str()).collect();
assert!(
!values.contains(&"r"),
"`:r` should not appear even with partial match"
);
assert!(values.contains(&"restart"));
assert!(values.contains(&"reprex"));
}
#[test]
fn test_meta_command_append_whitespace_for_commands_with_arguments() {
let mut completer = MetaCommandCompleter::new();
let suggestions = completion_suggestions(&mut completer, ":sw", 3);
let switch = suggestions.iter().find(|s| s.value == "switch").unwrap();
assert!(
switch.append_whitespace,
"`:switch` should append whitespace because it takes an argument"
);
let suggestions = completion_suggestions(&mut completer, ":sys", 4);
let system = suggestions.iter().find(|s| s.value == "system").unwrap();
assert!(
system.append_whitespace,
"`:system` should append whitespace because it takes an argument"
);
}
#[test]
fn test_meta_command_no_append_whitespace_for_commands_without_arguments() {
let mut completer = MetaCommandCompleter::new();
let suggestions = completion_suggestions(&mut completer, ":", 1);
for cmd_name in &["shell", "r", "commands", "cmds", "restart", "quit", "exit"] {
if let Some(cmd) = suggestions.iter().find(|s| s.value == *cmd_name) {
assert!(
!cmd.append_whitespace,
"`:{}` should NOT append whitespace because it takes no argument",
cmd_name
);
}
}
}
#[test]
fn test_meta_command_match_indices_for_partial_command() {
let mut completer = MetaCommandCompleter::new();
let suggestions = completion_suggestions(&mut completer, ":rep", 4);
assert_eq!(suggestions.len(), 1);
let reprex = &suggestions[0];
assert_eq!(reprex.value, "reprex");
assert_eq!(reprex.match_indices, Some(vec![0, 1, 2]));
}
#[test]
fn test_meta_command_match_indices_none_for_empty_input() {
let mut completer = MetaCommandCompleter::new();
let suggestions = completion_suggestions(&mut completer, ":", 1);
assert!(!suggestions.is_empty());
for suggestion in &suggestions {
assert_eq!(
suggestion.match_indices, None,
"`:` with no partial input should have match_indices: None"
);
}
}
#[test]
fn test_meta_command_match_indices_single_char() {
let mut completer = MetaCommandCompleter::new();
let suggestions = completion_suggestions(&mut completer, ":r", 2);
let values: Vec<&str> = suggestions.iter().map(|s| s.value.as_str()).collect();
assert!(values.contains(&"r"));
assert!(values.contains(&"reprex"));
assert!(values.contains(&"restart"));
for suggestion in &suggestions {
let expected_pos = suggestion
.value
.find('r')
.or_else(|| suggestion.value.find('R'));
assert_eq!(
suggestion.match_indices,
expected_pos.map(|p| vec![p]),
"`:r` should highlight first 'r' position in `{}`",
suggestion.value
);
}
}
#[test]
fn test_meta_command_fuzzy_matching() {
let mut completer = MetaCommandCompleter::new();
let suggestions = completion_suggestions(&mut completer, ":rst", 4);
let values: Vec<&str> = suggestions.iter().map(|s| s.value.as_str()).collect();
assert!(
values.contains(&"restart"),
"`:rst` should fuzzy match `restart`, got: {:?}",
values
);
let restart = suggestions.iter().find(|s| s.value == "restart").unwrap();
assert_eq!(
restart.match_indices,
Some(vec![0, 2, 3]),
"`:rst` should highlight positions 0, 2, 3 in `restart`"
);
}
#[test]
fn test_meta_command_fuzzy_matching_cms_cmds() {
let mut completer = MetaCommandCompleter::new();
let suggestions = completion_suggestions(&mut completer, ":cms", 4);
let values: Vec<&str> = suggestions.iter().map(|s| s.value.as_str()).collect();
assert!(values.contains(&"cmds"), "`:cms` should fuzzy match `cmds`");
let cmds = suggestions.iter().find(|s| s.value == "cmds").unwrap();
assert_eq!(
cmds.match_indices,
Some(vec![0, 1, 3]),
"`:cms` should highlight positions 0, 1, 3 in `cmds`"
);
}
#[test]
fn test_meta_command_fuzzy_no_match() {
let mut completer = MetaCommandCompleter::new();
let suggestions = completion_suggestions(&mut completer, ":xyz", 4);
assert!(
suggestions.is_empty(),
"`:xyz` should not match any command"
);
}
#[test]
fn test_meta_command_help_no_append_whitespace() {
let mut completer = MetaCommandCompleter::new();
let suggestions = completion_suggestions(&mut completer, ":hel", 4);
let help = suggestions.iter().find(|s| s.value == "help").unwrap();
assert!(
!help.append_whitespace,
"`:help` should NOT append whitespace"
);
let suggestions = completion_suggestions(&mut completer, ":h", 2);
let h = suggestions.iter().find(|s| s.value == "h").unwrap();
assert!(!h.append_whitespace, "`:h` should NOT append whitespace");
}
#[test]
fn test_meta_cd_completes_directories() {
let _guard = crate::test_utils::lock_cwd();
let tmp = tempfile::tempdir().unwrap();
std::fs::create_dir(tmp.path().join("subdir")).unwrap();
std::fs::File::create(tmp.path().join("file.txt")).unwrap();
std::env::set_current_dir(tmp.path()).unwrap();
let mut completer = MetaCommandCompleter::new();
let suggestions = completion_suggestions(&mut completer, ":cd ", 4);
assert!(!suggestions.is_empty(), "Should have directory completions");
for s in &suggestions {
assert!(
s.value.ends_with('/'),
"cd completion should only show directories, got: {}",
s.value
);
}
assert!(
suggestions.iter().any(|s| s.value == "subdir/"),
"Should contain subdir/"
);
assert!(
!suggestions.iter().any(|s| s.value == "file.txt"),
"Should not contain files"
);
}
#[test]
fn test_meta_cd_partial_path() {
let _guard = crate::test_utils::lock_cwd();
let tmp = tempfile::tempdir().unwrap();
std::fs::create_dir(tmp.path().join("subdir")).unwrap();
std::env::set_current_dir(tmp.path()).unwrap();
let mut completer = MetaCommandCompleter::new();
let suggestions = completion_suggestions(&mut completer, ":cd su", 6);
assert!(
suggestions.iter().any(|s| s.value == "subdir/"),
"Should fuzzy-match subdir/, got: {:?}",
suggestions.iter().map(|s| &s.value).collect::<Vec<_>>()
);
}
#[test]
fn test_meta_pushd_completes_directories() {
let _guard = crate::test_utils::lock_cwd();
let tmp = tempfile::tempdir().unwrap();
std::fs::create_dir(tmp.path().join("mydir")).unwrap();
std::env::set_current_dir(tmp.path()).unwrap();
let mut completer = MetaCommandCompleter::new();
let suggestions = completion_suggestions(&mut completer, ":pushd ", 7);
assert!(
suggestions.iter().any(|s| s.value == "mydir/"),
"Should contain mydir/"
);
}
#[test]
fn test_meta_popd_no_completion() {
let mut completer = MetaCommandCompleter::new();
let suggestions = completion_suggestions(&mut completer, ":popd ", 6);
assert!(
suggestions.is_empty(),
"popd should not have completions, got: {:?}",
suggestions.iter().map(|s| &s.value).collect::<Vec<_>>()
);
}
#[test]
fn test_meta_cd_nested_path() {
let _guard = crate::test_utils::lock_cwd();
let tmp = tempfile::tempdir().unwrap();
let src = tmp.path().join("src");
std::fs::create_dir(&src).unwrap();
std::fs::create_dir(src.join("inner")).unwrap();
std::env::set_current_dir(tmp.path()).unwrap();
let mut completer = MetaCommandCompleter::new();
let suggestions = completion_suggestions(&mut completer, ":cd src/", 8);
assert!(
suggestions.iter().any(|s| s.value == "src/inner/"),
"Should list src/inner/, got: {:?}",
suggestions.iter().map(|s| &s.value).collect::<Vec<_>>()
);
}
#[test]
fn test_meta_command_switch_bang_appears_in_completions() {
let mut completer = MetaCommandCompleter::new();
let suggestions = completion_suggestions(&mut completer, ":sw", 3);
let values: Vec<&str> = suggestions.iter().map(|s| s.value.as_str()).collect();
assert!(values.contains(&"switch"), "should match :switch");
assert!(values.contains(&"switch!"), "should match :switch!");
let switch_bang = suggestions.iter().find(|s| s.value == "switch!").unwrap();
assert!(
switch_bang.append_whitespace,
":switch! should append whitespace because it takes an argument"
);
}
#[test]
fn test_meta_command_restart_bang_appears_in_completions() {
let mut completer = MetaCommandCompleter::new();
let suggestions = completion_suggestions(&mut completer, ":restart", 8);
let values: Vec<&str> = suggestions.iter().map(|s| s.value.as_str()).collect();
assert!(values.contains(&"restart"), "should match :restart");
assert!(values.contains(&"restart!"), "should match :restart!");
let restart_bang = suggestions.iter().find(|s| s.value == "restart!").unwrap();
assert!(
!restart_bang.append_whitespace,
":restart! should NOT append whitespace"
);
}
#[test]
fn test_meta_command_switch_bang_subcommand_completion() {
let mut completer = MetaCommandCompleter::new();
let switch_suggestions = completion_suggestions(&mut completer, ":switch ", 8);
let switch_bang_suggestions = completion_suggestions(&mut completer, ":switch! ", 9);
assert_eq!(
switch_suggestions.len(),
switch_bang_suggestions.len(),
":switch! should produce the same version completions as :switch"
);
let switch_partial = completion_suggestions(&mut completer, ":switch 4", 9);
let switch_bang_partial = completion_suggestions(&mut completer, ":switch! 4", 10);
assert_eq!(
switch_partial.len(),
switch_bang_partial.len(),
":switch! with partial version should match :switch"
);
}
#[test]
fn test_meta_command_reprex_format_subcommand() {
let mut completer = MetaCommandCompleter::new();
let suggestions = completion_suggestions(&mut completer, ":reprex ", 8);
assert_eq!(
suggestions
.iter()
.map(|s| s.value.as_str())
.collect::<Vec<_>>(),
["on", "off", "format"]
);
}
#[test]
fn test_meta_command_history_bang_no_subcommands() {
let mut completer = MetaCommandCompleter::new();
let suggestions = completion_suggestions(&mut completer, ":history! ", 10);
assert!(
suggestions.is_empty(),
":history! should not offer subcommands, got: {:?}",
suggestions.iter().map(|s| &s.value).collect::<Vec<_>>()
);
}
}