service_kit 0.1.2

A foundational toolkit for building high-performance, modular services in Rust.
Documentation
use clap::Command;

#[derive(Debug, Clone)]
pub struct CompletionSuggestion {
    pub value: String,
    pub description: Option<String>,
    pub start_pos: usize,
    pub end_pos: usize,
}

pub struct WasmCompleter {
    command: Command,
}

impl WasmCompleter {
    pub fn new(command: Command) -> Self {
        Self { command }
    }

    pub fn complete(&self, line: &str, pos: usize) -> Vec<CompletionSuggestion> {
        let mut suggestions = Vec::new();
        let line_to_cursor = &line[..pos];

        let parts: Vec<String> = shlex::split(line_to_cursor)
            .unwrap_or_else(|| line_to_cursor.split_whitespace().map(String::from).collect());

        let (current_word, span_start) = if line_to_cursor.ends_with(' ') || parts.is_empty() {
            ("", pos)
        } else {
            let last_part = parts.last().expect("Parts should not be empty");
            (last_part.as_str(), pos - last_part.len())
        };

        let mut current_cmd = &self.command;
        let mut last_arg_opt: Option<&clap::Arg> = None;
        let mut potential_value_completion_context = false;

        for (idx, part) in parts.iter().enumerate() {
            if idx == parts.len() - 1 && !line_to_cursor.ends_with(' ') {
                if let Some(last_arg) = last_arg_opt {
                    if last_arg.get_action().takes_values() {
                        potential_value_completion_context = true;
                    }
                }
                break;
            }

            if part.starts_with('-') {
                if let Some(arg_match) = current_cmd.get_arguments().find(|a| {
                    (a.get_long().map_or(false, |l| format!("--{}", l) == *part)) ||
                    (a.get_short().map_or(false, |s| format!("-{}", s) == *part))
                }) {
                    last_arg_opt = Some(arg_match);
                    if !arg_match.get_action().takes_values() {
                        last_arg_opt = None;
                    }
                } else {
                    last_arg_opt = None;
                    break;
                }
            } else {
                if let Some(prev_arg) = last_arg_opt {
                    if prev_arg.get_action().takes_values() {
                        last_arg_opt = None;
                    }
                }

                if let Some(sub_cmd) = current_cmd.get_subcommands().find(|sc| sc.get_name() == part) {
                    current_cmd = sub_cmd;
                    last_arg_opt = None;
                } else {
                    if !last_arg_opt.map_or(false, |arg| arg.get_action().takes_values()) {
                        break;
                    }
                    last_arg_opt = None;
                }
            }
        }

        if potential_value_completion_context {
            if let Some(arg_name_part_idx) = parts.len().checked_sub(2) {
                if let Some(arg_name_part) = parts.get(arg_name_part_idx) {
                    let path_to_arg_command = &parts[..arg_name_part_idx];
                    let command_for_arg = get_command_at_path(&self.command, path_to_arg_command);

                    if let Some(clap_arg) = command_for_arg.get_arguments().find(|a| {
                        a.get_long().map_or(false, |l| format!("--{}", l) == *arg_name_part) ||
                        a.get_short().map_or(false, |s| format!("-{}", s) == *arg_name_part)
                    }) {
                        if clap_arg.get_action().takes_values() {
                            suggestions.extend(find_value_suggestions(clap_arg, current_word, span_start, pos));
                        }
                    }
                }
            }
        }

        if line_to_cursor.ends_with(' ') && last_arg_opt.map_or(false, |arg| arg.get_action().takes_values()) {
            if let Some(arg_that_needs_value) = last_arg_opt {
                suggestions.extend(find_value_suggestions(arg_that_needs_value, "", span_start, pos));
            }
        }

        if suggestions.is_empty() {
            if current_word.starts_with('-') {
                suggestions.extend(find_argument_suggestions(current_cmd, current_word, span_start, pos));
            }

            suggestions.extend(find_subcommand_suggestions(current_cmd, &[], current_word, span_start, pos));

            if line_to_cursor.ends_with(' ') && current_word.is_empty() {
                let existing_flags: std::collections::HashSet<_> = parts.iter()
                    .filter(|p| p.starts_with("-"))
                    .map(|p| p.as_str())
                    .collect();

                let mut new_arg_suggestions = Vec::new();

                let long_args = find_argument_suggestions(current_cmd, "--", span_start, pos)
                    .into_iter()
                    .filter(|s| s.value.starts_with("--"));

                for sugg in long_args {
                    if let Some(arg_def) = current_cmd.get_arguments().find(|a| {
                        a.get_long().map_or(false, |l| format!("--{}", l) == sugg.value)
                    }) {
                        if matches!(*arg_def.get_action(), clap::ArgAction::SetTrue) && existing_flags.contains(sugg.value.as_str()) {
                            continue;
                        }
                    }
                    new_arg_suggestions.push(sugg);
                }

                let short_args = find_argument_suggestions(current_cmd, "-", span_start, pos)
                    .into_iter()
                    .filter(|s| s.value.starts_with('-') && !s.value.starts_with("--"));

                for sugg in short_args {
                    if let Some(arg_def) = current_cmd.get_arguments().find(|a| {
                        a.get_short().map_or(false, |s_char| format!("-{}", s_char) == sugg.value)
                    }) {
                        if matches!(*arg_def.get_action(), clap::ArgAction::SetTrue) && existing_flags.contains(sugg.value.as_str()) {
                            continue;
                        }
                    }
                    new_arg_suggestions.push(sugg);
                }
                suggestions.extend(new_arg_suggestions);
            }
        }

        let mut final_suggestions = Vec::new();
        let mut seen_values = std::collections::HashSet::new();
        for s in suggestions {
            if seen_values.insert(s.value.clone()) {
                final_suggestions.push(s);
            }
        }
        final_suggestions
    }
}

fn find_subcommand_suggestions(
    command: &Command,
    parts: &[String],
    current_word: &str,
    span_start: usize,
    span_end: usize,
) -> Vec<CompletionSuggestion> {
    let mut current_cmd = command;
    let mut suggestions = Vec::new();

    let relevant_parts_count = if current_word.is_empty() && !parts.is_empty()
        && parts.last().map_or(false, |p| !p.is_empty()) {
        parts.len()
    } else {
        parts.len().saturating_sub(1)
    };

    for part in parts.iter().take(relevant_parts_count) {
        if let Some(sub_cmd) = current_cmd.get_subcommands().find(|sc| sc.get_name() == part) {
            current_cmd = sub_cmd;
        } else {
            return suggestions;
        }
    }

    for sub_cmd in current_cmd.get_subcommands() {
        if sub_cmd.get_name().starts_with(current_word) {
            suggestions.push(CompletionSuggestion {
                value: sub_cmd.get_name().to_string(),
                description: sub_cmd.get_about().map(|s| s.to_string()),
                start_pos: span_start,
                end_pos: span_end,
            });
        }
    }
    suggestions
}

fn find_argument_suggestions(
    command: &Command,
    current_word: &str,
    span_start: usize,
    span_end: usize,
) -> Vec<CompletionSuggestion> {
    let mut suggestions = Vec::new();
    if !current_word.starts_with('-') {
        return suggestions;
    }

    for arg in command.get_arguments() {
        if let Some(long) = arg.get_long() {
            let long_flag = format!("--{}", long);
            if long_flag.starts_with(current_word) {
                suggestions.push(CompletionSuggestion {
                    value: long_flag,
                    description: arg.get_help().map(|s| s.to_string()),
                    start_pos: span_start,
                    end_pos: span_end,
                });
            }
        }
        if let Some(short) = arg.get_short() {
            let short_flag = format!("-{}", short);
            if short_flag.starts_with(current_word) {
                if !suggestions.iter().any(|s| s.value == short_flag) {
                    suggestions.push(CompletionSuggestion {
                        value: short_flag,
                        description: arg.get_help().map(|s| s.to_string()),
                        start_pos: span_start,
                        end_pos: span_end,
                    });
                }
            }
        }
    }
    suggestions
}

fn find_value_suggestions(
    arg: &clap::Arg,
    current_word: &str,
    span_start: usize,
    span_end: usize,
) -> Vec<CompletionSuggestion> {
    let mut suggestions = Vec::new();
    for pv in arg.get_possible_values() {
        if pv.get_name().starts_with(current_word) {
            suggestions.push(CompletionSuggestion {
                value: pv.get_name().to_string(),
                description: pv.get_help().map(|s| s.to_string()),
                start_pos: span_start,
                end_pos: span_end,
            });
        }
    }
    suggestions
}

fn get_command_at_path<'a>(base_cmd: &'a Command, parts: &[String]) -> &'a Command {
    let mut current_cmd = base_cmd;
    for part_name in parts {
        if !part_name.starts_with('-') {
            if let Some(sub_cmd) = current_cmd.get_subcommands().find(|sc| sc.get_name() == part_name) {
                current_cmd = sub_cmd;
            } else {
                break;
            }
        } else {
            break;
        }
    }
    current_cmd
}