| + |
use std::collections::HashMap;
|
| + |
|
| + |
// Import all command HELP constants for auto-generation
|
| + |
use crate::commands::{
|
| + |
rad_auth, rad_block, rad_checkout, rad_clean, rad_clone, rad_cob, rad_completion, rad_config,
|
| + |
rad_debug, rad_diff, rad_follow, rad_fork, rad_help, rad_id, rad_inbox, rad_init, rad_inspect,
|
| + |
rad_issue, rad_ls, rad_node, rad_patch, rad_path, rad_publish, rad_remote, rad_seed, rad_self,
|
| + |
rad_stats, rad_sync, rad_unblock, rad_unfollow, rad_unseed, rad_watch,
|
| + |
};
|
| + |
|
| + |
/// Command metadata structure
|
| + |
#[derive(Debug, Clone)]
|
| + |
pub struct CommandMetadata {
|
| + |
pub name: &'static str,
|
| + |
pub description: &'static str,
|
| + |
pub options: Vec<OptionInfo>,
|
| + |
pub subcommands: Vec<SubcommandInfo>,
|
| + |
pub dynamic_completion: Option<DynamicCompletionType>,
|
| + |
}
|
| + |
|
| + |
/// Option information
|
| + |
#[derive(Debug, Clone)]
|
| + |
pub struct OptionInfo {
|
| + |
pub long: &'static str,
|
| + |
pub short: Option<char>,
|
| + |
pub description: &'static str,
|
| + |
pub takes_value: bool,
|
| + |
}
|
| + |
|
| + |
/// Subcommand information
|
| + |
#[derive(Debug, Clone)]
|
| + |
pub struct SubcommandInfo {
|
| + |
pub name: &'static str,
|
| + |
pub description: &'static str,
|
| + |
pub options: Vec<OptionInfo>,
|
| + |
pub subcommands: Vec<SubcommandInfo>,
|
| + |
pub dynamic_completion: Option<DynamicCompletionType>,
|
| + |
}
|
| + |
|
| + |
/// Type of dynamic completion for a command
|
| + |
#[derive(Debug, Clone)]
|
| + |
pub enum DynamicCompletionType {
|
| + |
PatchIds,
|
| + |
IssueIds,
|
| + |
BranchNames,
|
| + |
RemoteNames,
|
| + |
NodeIds,
|
| + |
}
|
| + |
|
| + |
/// Command registry that holds all completion metadata
|
| + |
#[derive(Debug)]
|
| + |
pub struct CommandRegistry {
|
| + |
commands: HashMap<&'static str, CommandMetadata>,
|
| + |
}
|
| + |
|
| + |
impl CommandRegistry {
|
| + |
/// Initialize the command registry with auto-generated metadata
|
| + |
pub fn init() -> Self {
|
| + |
let mut commands = HashMap::new();
|
| + |
|
| + |
// Auto-generate command metadata from the actual command HELP constants
|
| + |
// This ensures completion stays in sync with command implementations
|
| + |
let command_helps = [
|
| + |
("auth", rad_auth::HELP),
|
| + |
("block", rad_block::HELP),
|
| + |
("checkout", rad_checkout::HELP),
|
| + |
("clone", rad_clone::HELP),
|
| + |
("cob", rad_cob::HELP),
|
| + |
("completion", rad_completion::HELP),
|
| + |
("config", rad_config::HELP),
|
| + |
("debug", rad_debug::HELP),
|
| + |
("diff", rad_diff::HELP),
|
| + |
("follow", rad_follow::HELP),
|
| + |
("fork", rad_fork::HELP),
|
| + |
("help", rad_help::HELP),
|
| + |
("id", rad_id::HELP),
|
| + |
("inbox", rad_inbox::HELP),
|
| + |
("init", rad_init::HELP),
|
| + |
("inspect", rad_inspect::HELP),
|
| + |
("issue", rad_issue::HELP),
|
| + |
("ls", rad_ls::HELP),
|
| + |
("node", rad_node::HELP),
|
| + |
("patch", rad_patch::HELP),
|
| + |
("path", rad_path::HELP),
|
| + |
("publish", rad_publish::HELP),
|
| + |
("clean", rad_clean::HELP),
|
| + |
("self", rad_self::HELP),
|
| + |
("seed", rad_seed::HELP),
|
| + |
("sync", rad_sync::HELP),
|
| + |
("unblock", rad_unblock::HELP),
|
| + |
("unfollow", rad_unfollow::HELP),
|
| + |
("unseed", rad_unseed::HELP),
|
| + |
("remote", rad_remote::HELP),
|
| + |
("stats", rad_stats::HELP),
|
| + |
("watch", rad_watch::HELP),
|
| + |
];
|
| + |
|
| + |
for (name, help) in command_helps {
|
| + |
commands.insert(
|
| + |
name,
|
| + |
CommandMetadata {
|
| + |
name: help.name,
|
| + |
description: help.description,
|
| + |
options: Self::extract_options_from_help(help.usage),
|
| + |
subcommands: Self::extract_subcommands_from_help(help.usage),
|
| + |
dynamic_completion: Self::get_dynamic_completion_type(name),
|
| + |
},
|
| + |
);
|
| + |
}
|
| + |
|
| + |
Self { commands }
|
| + |
}
|
| + |
|
| + |
/// Extract options from help usage text
|
| + |
fn extract_options_from_help(usage: &str) -> Vec<OptionInfo> {
|
| + |
let mut options = vec![
|
| + |
// Common options that most commands support
|
| + |
OptionInfo {
|
| + |
long: "help",
|
| + |
short: Some('h'),
|
| + |
description: "Print help",
|
| + |
takes_value: false,
|
| + |
},
|
| + |
];
|
| + |
|
| + |
// Parse usage text to find additional options
|
| + |
// This is a simple parser - could be enhanced
|
| + |
if usage.contains("--json") {
|
| + |
options.push(OptionInfo {
|
| + |
long: "json",
|
| + |
short: None,
|
| + |
description: "Output as JSON",
|
| + |
takes_value: false,
|
| + |
});
|
| + |
}
|
| + |
|
| + |
if usage.contains("--verbose") || usage.contains("-v") {
|
| + |
options.push(OptionInfo {
|
| + |
long: "verbose",
|
| + |
short: Some('v'),
|
| + |
description: "Verbose output",
|
| + |
takes_value: false,
|
| + |
});
|
| + |
}
|
| + |
|
| + |
options
|
| + |
}
|
| + |
|
| + |
/// Extract subcommands from help usage text
|
| + |
fn extract_subcommands_from_help(usage: &str) -> Vec<SubcommandInfo> {
|
| + |
let mut subcommands = vec![];
|
| + |
|
| + |
// For patch command specifically, add known subcommands
|
| + |
if usage.contains("rad patch") {
|
| + |
let known_subcommands = [
|
| + |
("list", "List patches in the current repository"),
|
| + |
("show", "Shows information on the given patch"),
|
| + |
("diff", "Outputs the patch diff"),
|
| + |
("archive", "Archive a patch"),
|
| + |
("update", "Updates a patch to the current repository HEAD"),
|
| + |
("checkout", "Switch to a given patch"),
|
| + |
("delete", "Delete a patch"),
|
| + |
("redact", "Redact a revision"),
|
| + |
("ready", "Mark a patch as ready to review"),
|
| + |
("review", "Review a patch"),
|
| + |
("edit", "Edits a patch revision comment"),
|
| + |
(
|
| + |
"set",
|
| + |
"Set the current branch upstream to a patch reference",
|
| + |
),
|
| + |
("comment", "Comment on a patch revision"),
|
| + |
("label", "Label a patch"),
|
| + |
];
|
| + |
|
| + |
for (name, description) in known_subcommands {
|
| + |
subcommands.push(SubcommandInfo {
|
| + |
name,
|
| + |
description,
|
| + |
options: vec![],
|
| + |
subcommands: vec![],
|
| + |
dynamic_completion: None,
|
| + |
});
|
| + |
}
|
| + |
}
|
| + |
|
| + |
subcommands
|
| + |
}
|
| + |
|
| + |
/// Get dynamic completion type based on command name
|
| + |
fn get_dynamic_completion_type(command: &str) -> Option<DynamicCompletionType> {
|
| + |
match command {
|
| + |
"patch" => Some(DynamicCompletionType::PatchIds),
|
| + |
"issue" => Some(DynamicCompletionType::IssueIds),
|
| + |
"checkout" => Some(DynamicCompletionType::BranchNames),
|
| + |
"remote" => Some(DynamicCompletionType::RemoteNames),
|
| + |
"node" => Some(DynamicCompletionType::NodeIds),
|
| + |
_ => None,
|
| + |
}
|
| + |
}
|
| + |
|
| + |
/// Generate bash completion script
|
| + |
pub fn generate_bash_completion(&self) -> String {
|
| + |
let mut script = String::new();
|
| + |
|
| + |
// Generate the main completion function
|
| + |
script.push_str(&self.generate_main_completion_function());
|
| + |
|
| + |
// Generate subcommand completion functions
|
| + |
for (name, metadata) in &self.commands {
|
| + |
script.push_str(&self.generate_subcommand_completion_function(name, metadata));
|
| + |
}
|
| + |
|
| + |
// Add the completion entry that tells bash to use _rad for the rad command
|
| + |
script.push_str("\n# Register the completion function for the 'rad' command\n");
|
| + |
script.push_str("complete -F _rad rad\n");
|
| + |
|
| + |
script
|
| + |
}
|
| + |
|
| + |
/// Generate the main completion function
|
| + |
fn generate_main_completion_function(&self) -> String {
|
| + |
let mut script = String::new();
|
| + |
|
| + |
script.push_str("# bash completion for rad\n");
|
| + |
script.push_str("# Auto-generated from command implementations\n\n");
|
| + |
|
| + |
// Generate the main _rad function
|
| + |
script.push_str("_rad() {\n");
|
| + |
script.push_str(" local cur prev words cword\n");
|
| + |
script.push_str(" _init_completion -n =: 2>/dev/null || {\n");
|
| + |
script.push_str(" # Fallback for environments without _init_completion\n");
|
| + |
script.push_str(" cur=${COMP_WORDS[COMP_CWORD]}\n");
|
| + |
script.push_str(" prev=${COMP_WORDS[COMP_CWORD-1]}\n");
|
| + |
script.push_str(" words=(\"${COMP_WORDS[@]}\")\n");
|
| + |
script.push_str(" cword=$COMP_CWORD\n");
|
| + |
script.push_str(" }\n");
|
| + |
script.push('\n');
|
| + |
|
| + |
// Generate command list
|
| + |
let command_names: Vec<&str> = self.commands.keys().copied().collect();
|
| + |
|
| + |
// Split long command lists across multiple lines for better readability
|
| + |
if command_names.len() > 10 {
|
| + |
script.push_str(" local commands=\"");
|
| + |
for (i, name) in command_names.iter().enumerate() {
|
| + |
if i > 0 && i % 8 == 0 {
|
| + |
script.push_str(" \\\n ");
|
| + |
}
|
| + |
script.push_str(name);
|
| + |
if i < command_names.len() - 1 {
|
| + |
script.push_str(" ");
|
| + |
}
|
| + |
}
|
| + |
script.push_str("\"\n");
|
| + |
} else {
|
| + |
script.push_str(&format!(
|
| + |
" local commands=\"{}\"\n",
|
| + |
command_names.join(" ")
|
| + |
));
|
| + |
}
|
| + |
script.push('\n');
|
| + |
|
| + |
// Generate the main completion logic
|
| + |
script.push_str(" case $cword in\n");
|
| + |
script.push_str(" 1)\n");
|
| + |
script.push_str(" COMPREPLY=($(compgen -W \"$commands\" -- \"$cur\"))\n");
|
| + |
script.push_str(" ;;\n");
|
| + |
script.push_str(" *)\n");
|
| + |
script.push_str(" case \"${words[1]}\" in\n");
|
| + |
|
| + |
// Generate cases for each command
|
| + |
for name in self.commands.keys() {
|
| + |
script.push_str(&format!(" {})\n", name));
|
| + |
script.push_str(&format!(" _rad_{}\n", name));
|
| + |
script.push_str(" ;;\n");
|
| + |
}
|
| + |
|
| + |
script.push_str(" *)\n");
|
| + |
script.push_str(" COMPREPLY=($(compgen -W \"--help --version --json\" -- \"$cur\"))\n");
|
| + |
script.push_str(" ;;\n");
|
| + |
script.push_str(" esac\n");
|
| + |
script.push_str(" ;;\n");
|
| + |
script.push_str(" esac\n");
|
| + |
script.push_str("}\n\n");
|
| + |
|
| + |
script
|
| + |
}
|
| + |
|
| + |
/// Generate subcommand completion function
|
| + |
fn generate_subcommand_completion_function(
|
| + |
&self,
|
| + |
name: &str,
|
| + |
metadata: &CommandMetadata,
|
| + |
) -> String {
|
| + |
let mut script = String::new();
|
| + |
|
| + |
script.push_str(&format!("_rad_{}() {{\n", name));
|
| + |
script.push_str(" local cur prev words cword\n");
|
| + |
script.push_str(" _init_completion -n =: 2>/dev/null || {\n");
|
| + |
script.push_str(" # Fallback for environments without _init_completion\n");
|
| + |
script.push_str(" cur=${COMP_WORDS[COMP_CWORD]}\n");
|
| + |
script.push_str(" prev=${COMP_WORDS[COMP_CWORD-1]}\n");
|
| + |
script.push_str(" words=(\"${COMP_WORDS[@]}\")\n");
|
| + |
script.push_str(" cword=$COMP_CWORD\n");
|
| + |
script.push_str(" }\n");
|
| + |
script.push('\n');
|
| + |
|
| + |
// Generate subcommand list if any
|
| + |
if !metadata.subcommands.is_empty() {
|
| + |
let subcommand_names: Vec<&str> = metadata.subcommands.iter().map(|s| s.name).collect();
|
| + |
script.push_str(&format!(
|
| + |
" local subcommands=\"{}\"\n",
|
| + |
subcommand_names.join(" ")
|
| + |
));
|
| + |
script.push('\n');
|
| + |
}
|
| + |
|
| + |
// Generate options list
|
| + |
let option_strings: Vec<String> = metadata
|
| + |
.options
|
| + |
.iter()
|
| + |
.map(|opt| {
|
| + |
if let Some(short) = opt.short {
|
| + |
format!("--{} -{}", opt.long, short)
|
| + |
} else {
|
| + |
format!("--{}", opt.long)
|
| + |
}
|
| + |
})
|
| + |
.collect();
|
| + |
|
| + |
if !option_strings.is_empty() {
|
| + |
script.push_str(&format!(
|
| + |
" local options=\"{}\"\n",
|
| + |
option_strings.join(" ")
|
| + |
));
|
| + |
script.push('\n');
|
| + |
}
|
| + |
|
| + |
// Generate completion logic
|
| + |
script.push_str(" case $cword in\n");
|
| + |
script.push_str(" 2)\n");
|
| + |
|
| + |
if !metadata.subcommands.is_empty() {
|
| + |
script.push_str(" COMPREPLY=($(compgen -W \"$subcommands\" -- \"$cur\"))\n");
|
| + |
} else {
|
| + |
script.push_str(" COMPREPLY=($(compgen -W \"$options\" -- \"$cur\"))\n");
|
| + |
}
|
| + |
|
| + |
script.push_str(" ;;\n");
|
| + |
script.push_str(" *)\n");
|
| + |
|
| + |
// Handle dynamic completion
|
| + |
if let Some(dynamic_type) = &metadata.dynamic_completion {
|
| + |
match dynamic_type {
|
| + |
DynamicCompletionType::PatchIds => {
|
| + |
script.push_str(" # Dynamic completion for patch IDs\n");
|
| + |
script.push_str(" local patch_ids=$(rad patch list 2>/dev/null | awk 'NR>2 {print $3}' | grep \"^$cur\" || echo \"\")\n");
|
| + |
script.push_str(" if [[ -n \"$patch_ids\" ]]; then\n");
|
| + |
script.push_str(
|
| + |
" COMPREPLY=($(compgen -W \"$patch_ids\" -- \"$cur\"))\n",
|
| + |
);
|
| + |
script.push_str(" else\n");
|
| + |
script.push_str(
|
| + |
" COMPREPLY=($(compgen -W \"$options\" -- \"$cur\"))\n",
|
| + |
);
|
| + |
script.push_str(" fi\n");
|
| + |
}
|
| + |
DynamicCompletionType::IssueIds => {
|
| + |
script.push_str(" # Dynamic completion for issue IDs\n");
|
| + |
script.push_str(" local issue_ids=$(rad issue list 2>/dev/null | awk 'NR>2 {print $3}' | grep \"^$cur\" || echo \"\")\n");
|
| + |
script.push_str(" if [[ -n \"$issue_ids\" ]]; then\n");
|
| + |
script.push_str(
|
| + |
" COMPREPLY=($(compgen -W \"$issue_ids\" -- \"$cur\"))\n",
|
| + |
);
|
| + |
script.push_str(" else\n");
|
| + |
script.push_str(
|
| + |
" COMPREPLY=($(compgen -W \"$options\" -- \"$cur\"))\n",
|
| + |
);
|
| + |
script.push_str(" fi\n");
|
| + |
}
|
| + |
_ => {
|
| + |
script.push_str(
|
| + |
" COMPREPLY=($(compgen -W \"$options\" -- \"$cur\"))\n",
|
| + |
);
|
| + |
}
|
| + |
}
|
| + |
} else {
|
| + |
script.push_str(" COMPREPLY=($(compgen -W \"$options\" -- \"$cur\"))\n");
|
| + |
}
|
| + |
|
| + |
script.push_str(" ;;\n");
|
| + |
script.push_str(" esac\n");
|
| + |
script.push_str("}\n\n");
|
| + |
|
| + |
script
|
| + |
}
|
| + |
}
|
| + |
|
| + |
impl Default for CommandRegistry {
|
| + |
fn default() -> Self {
|
| + |
Self::init()
|
| + |
}
|
| + |
}
|
| + |
|
| + |
/// Initialize the command registry
|
| + |
pub fn init_command_registry() -> CommandRegistry {
|
| + |
CommandRegistry::init()
|
| + |
}
|