| + |
use std::collections::HashMap;
|
| + |
|
| + |
// Import all command HELP constants for auto-generation
|
| + |
use crate::commands::{
|
| + |
auth, block, checkout, clean, clone, cob, completion, config, debug, diff, follow, fork, help,
|
| + |
id, inbox, init, inspect, issue, ls, node, patch, path, publish, rad_self, remote, seed, stats,
|
| + |
sync, unblock, unfollow, unseed, watch,
|
| + |
};
|
| + |
|
| + |
/// Command metadata structure
|
| + |
#[derive(Debug, Clone)]
|
| + |
pub struct CommandMetadata {
|
| + |
pub name: String,
|
| + |
pub description: String,
|
| + |
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 metadata structure
|
| + |
#[derive(Debug, Clone)]
|
| + |
pub struct SubcommandInfo {
|
| + |
pub name: String,
|
| + |
pub description: String,
|
| + |
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", auth::HELP),
|
| + |
("block", block::HELP),
|
| + |
("checkout", checkout::HELP),
|
| + |
("clone", clone::HELP),
|
| + |
("cob", cob::HELP),
|
| + |
("completion", completion::HELP),
|
| + |
("config", config::HELP),
|
| + |
("debug", debug::HELP),
|
| + |
("diff", diff::HELP),
|
| + |
("follow", follow::HELP),
|
| + |
("fork", fork::HELP),
|
| + |
("help", help::HELP),
|
| + |
("id", id::HELP),
|
| + |
("inbox", inbox::HELP),
|
| + |
("init", init::HELP),
|
| + |
("inspect", inspect::HELP),
|
| + |
("issue", issue::HELP),
|
| + |
("ls", ls::HELP),
|
| + |
("node", node::HELP),
|
| + |
("patch", patch::HELP),
|
| + |
("path", path::HELP),
|
| + |
("publish", publish::HELP),
|
| + |
("clean", clean::HELP),
|
| + |
("self", rad_self::HELP),
|
| + |
("seed", seed::HELP),
|
| + |
("sync", sync::HELP),
|
| + |
("unblock", unblock::HELP),
|
| + |
("unfollow", unfollow::HELP),
|
| + |
("unseed", unseed::HELP),
|
| + |
("remote", remote::HELP),
|
| + |
("stats", stats::HELP),
|
| + |
("watch", watch::HELP),
|
| + |
];
|
| + |
|
| + |
for (name, help) in command_helps {
|
| + |
commands.insert(
|
| + |
name,
|
| + |
CommandMetadata {
|
| + |
name: name.to_string(),
|
| + |
description: help.description.to_string(),
|
| + |
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 sub-subcommands from help usage text for a specific subcommand
|
| + |
fn extract_sub_subcommands_from_help(usage: &str, subcommand: &str) -> Vec<SubcommandInfo> {
|
| + |
let mut sub_subcommands = vec![];
|
| + |
|
| + |
// Look for patterns like "rad <command> <subcommand> <sub-subcommand> [<option>...]"
|
| + |
let lines: Vec<&str> = usage.lines().collect();
|
| + |
|
| + |
for line in lines {
|
| + |
let line = line.trim();
|
| + |
|
| + |
// Look for lines that contain the specific subcommand pattern
|
| + |
// We need to be more flexible here since we don't know the exact command name
|
| + |
let parts: Vec<&str> = line.split_whitespace().collect();
|
| + |
|
| + |
// We need at least 4 parts: "rad", "<command>", "<subcommand>", "<sub-subcommand>"
|
| + |
if parts.len() >= 4 && parts[0] == "rad" {
|
| + |
// Check if this line contains our subcommand
|
| + |
if parts.len() > 2 && parts[2] == subcommand {
|
| + |
// Look for the sub-subcommand (4th part)
|
| + |
if parts.len() > 3 {
|
| + |
let sub_subcommand_name = parts[3];
|
| + |
|
| + |
// Skip if it's an option, placeholder, or already known
|
| + |
if !sub_subcommand_name.starts_with('-')
|
| + |
&& !sub_subcommand_name.starts_with('<')
|
| + |
&& !sub_subcommand_name.starts_with('[')
|
| + |
&& !sub_subcommands
|
| + |
.iter()
|
| + |
.any(|s: &SubcommandInfo| s.name == sub_subcommand_name)
|
| + |
{
|
| + |
sub_subcommands.push(SubcommandInfo {
|
| + |
name: sub_subcommand_name.to_string(),
|
| + |
description: "Sub-subcommand".to_string(),
|
| + |
options: vec![],
|
| + |
subcommands: vec![], // Could be enhanced for even deeper nesting
|
| + |
dynamic_completion: None,
|
| + |
});
|
| + |
}
|
| + |
}
|
| + |
}
|
| + |
}
|
| + |
}
|
| + |
|
| + |
sub_subcommands
|
| + |
}
|
| + |
|
| + |
/// Extract subcommands from help usage text
|
| + |
fn extract_subcommands_from_help(usage: &str) -> Vec<SubcommandInfo> {
|
| + |
let mut subcommands = vec![];
|
| + |
|
| + |
// Parse the usage text to find subcommands at any level
|
| + |
// Look for patterns like "rad <command> <subcommand> [<option>...]"
|
| + |
// and "rad <command> <subcommand> <sub-subcommand> [<option>...]"
|
| + |
let lines: Vec<&str> = usage.lines().collect();
|
| + |
|
| + |
for line in lines {
|
| + |
let line = line.trim();
|
| + |
|
| + |
// Skip lines that don't contain the command pattern
|
| + |
if !line.contains("rad ") {
|
| + |
continue;
|
| + |
}
|
| + |
|
| + |
// Look for subcommand patterns in lines like:
|
| + |
// "rad patch list [<option>...]"
|
| + |
// "rad node db <command> [<option>..]"
|
| + |
// "rad issue edit <issue-id> [--title <title>] [<option>...]"
|
| + |
let parts: Vec<&str> = line.split_whitespace().collect();
|
| + |
|
| + |
// We need at least 3 parts: "rad", "<command>", "<subcommand>"
|
| + |
if parts.len() >= 3 {
|
| + |
let subcommand = parts[2];
|
| + |
|
| + |
// Skip if it's an option (starts with - or --) or placeholder (starts with < or [)
|
| + |
if !subcommand.starts_with('-')
|
| + |
&& !subcommand.starts_with('<')
|
| + |
&& !subcommand.starts_with('[')
|
| + |
{
|
| + |
// Check if we already have this subcommand
|
| + |
if !subcommands
|
| + |
.iter()
|
| + |
.any(|s: &SubcommandInfo| s.name == subcommand)
|
| + |
{
|
| + |
// Try to extract description from the line
|
| + |
let description = Self::extract_description_from_line(line);
|
| + |
|
| + |
subcommands.push(SubcommandInfo {
|
| + |
name: subcommand.to_string(),
|
| + |
description: description.to_string(),
|
| + |
options: vec![],
|
| + |
subcommands: Self::extract_sub_subcommands_from_help(usage, subcommand),
|
| + |
dynamic_completion: None,
|
| + |
});
|
| + |
}
|
| + |
}
|
| + |
}
|
| + |
}
|
| + |
|
| + |
subcommands
|
| + |
}
|
| + |
|
| + |
/// Extract description from a help line
|
| + |
fn extract_description_from_line(line: &str) -> &'static str {
|
| + |
// Try to find a description after the command
|
| + |
// Look for patterns like "rad patch list [<option>...] # List patches"
|
| + |
if let Some(comment_start) = line.find('#') {
|
| + |
let comment = &line[comment_start + 1..].trim();
|
| + |
if !comment.is_empty() {
|
| + |
// For now, return a generic description since we can't store dynamic strings
|
| + |
// In the future, we could enhance this to parse actual descriptions
|
| + |
return "Subcommand";
|
| + |
}
|
| + |
}
|
| + |
|
| + |
// Default description
|
| + |
"Subcommand"
|
| + |
}
|
| + |
|
| + |
/// 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.as_str())
|
| + |
.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()
|
| + |
}
|