use std::sync::Arc;
use super::{palette::PALETTE_CACHE_TTL, App, CommandChoice, CommandChoiceKind};
use crate::commands;
impl App {
pub(super) fn visible_command_matches(&mut self) -> Option<Vec<CommandChoice>> {
if self.input_ui.command_palette_dismissed() {
return None;
}
let matches = self.command_matches();
(!matches.is_empty()
&& (self.cursor_in_command_token()
|| !commands::argument_choices(self.input_ui.text(), self.input_ui.cursor())
.is_empty()
|| !self.mcp_argument_choices().is_empty()))
.then_some(matches)
}
pub(super) fn command_matches(&mut self) -> Vec<CommandChoice> {
let argument_choices =
commands::argument_choices(self.input_ui.text(), self.input_ui.cursor());
if !argument_choices.is_empty() {
return argument_choices
.iter()
.map(argument_command_choice)
.collect();
}
let mcp_argument_choices = self.mcp_argument_choices();
if !mcp_argument_choices.is_empty() {
return mcp_argument_choices;
}
let Some(prefix) = commands::command_prefix(self.input_ui.text()) else {
return Vec::new();
};
let prefix = prefix
.strip_prefix('/')
.unwrap_or(prefix)
.to_ascii_lowercase();
let builtin_matches = commands::matching_commands(&prefix);
let exact_builtin = builtin_matches
.iter()
.find(|command| command.name.eq_ignore_ascii_case(&prefix))
.copied();
let mut matches = builtin_matches
.into_iter()
.map(|command| CommandChoice {
name: command.name.to_string(),
usage: command.usage.to_string(),
description: command.description.to_string(),
kind: CommandChoiceKind::Builtin(command),
})
.collect::<Vec<_>>();
if let Some(command) = exact_builtin {
matches.extend(command.argument_choices.iter().map(argument_command_choice));
}
let mut template_matches = self
.info
.runtime
.prompt_templates
.iter()
.filter(|(name, _)| crate::prompt_templates::matches_search(name, &prefix))
.map(|(name, template)| {
let command_name = format!("prompt:{name}");
CommandChoice {
usage: format!("/{command_name} [text]"),
name: command_name,
description: crate::prompt_templates::description(template),
kind: CommandChoiceKind::PromptTemplate(template.clone()),
}
})
.collect::<Vec<_>>();
if let Some(index) = template_matches.iter().position(|choice| {
choice
.name
.strip_prefix("prompt:")
.is_some_and(|name| name.eq_ignore_ascii_case(&prefix))
}) {
let exact = template_matches.remove(index);
matches.insert(0, exact);
}
matches.extend(template_matches);
matches.extend(
self.mcp_catalog
.prompts()
.into_iter()
.filter(|prompt| prompt.command_name().starts_with(&prefix))
.map(|prompt| CommandChoice {
usage: prompt.usage(),
description: prompt.description.clone().unwrap_or_else(|| {
format!("{} · from MCP server `{}`", prompt.label(), prompt.server)
}),
name: prompt.command_name(),
kind: CommandChoiceKind::McpPrompt,
}),
);
matches.extend(
self.discovered_skills()
.iter()
.filter(|skill| {
skill.name.starts_with(&prefix)
|| format!("skill:{}", skill.name).starts_with(&prefix)
})
.map(|skill| {
let command_name = format!("skill:{}", skill.name);
CommandChoice {
usage: format!("/{command_name}"),
name: command_name,
description: skill.description.clone(),
kind: CommandChoiceKind::Skill,
}
}),
);
matches
}
fn discovered_skills(&mut self) -> Arc<Vec<crate::skills::Skill>> {
if let Some(skills) = self.palette_caches.fresh_skills(PALETTE_CACHE_TTL) {
return skills;
}
let skills = Arc::new(crate::skills::discover(&self.info.runtime.cwd));
self.palette_caches.store_skills(Arc::clone(&skills));
skills
}
#[cfg(test)]
pub(super) fn selected_command(&mut self) -> Option<CommandChoice> {
let matches = self.command_matches();
selected_command(&matches, self.input_ui.command_selection())
}
pub(super) fn complete_command_choice(&mut self, choice: &CommandChoice) {
let (input, cursor) = match &choice.kind {
CommandChoiceKind::Builtin(spec) => {
self.input_ui
.set_submission_mode(super::InputSubmissionMode::ParseCommands);
commands::complete_command(self.input_ui.text(), self.input_ui.cursor(), spec)
}
CommandChoiceKind::BuiltinArgument(choice) => {
self.input_ui
.set_submission_mode(super::InputSubmissionMode::ParseCommands);
commands::complete_argument_choice(choice)
}
CommandChoiceKind::PromptTemplate(template) => {
let expanded_input = self.expanded_input();
let mut input =
crate::prompt_templates::expand(template, slash_command_args(&expanded_input));
input.push(' ');
let cursor = input.chars().count();
self.input_ui.clear_paste_segments();
self.input_ui
.set_submission_mode(super::InputSubmissionMode::Prompt);
(input, cursor)
}
CommandChoiceKind::Skill | CommandChoiceKind::McpPrompt => {
self.input_ui
.set_submission_mode(super::InputSubmissionMode::ParseCommands);
complete_slash_command(self.input_ui.text(), self.input_ui.cursor(), &choice.name)
}
CommandChoiceKind::McpPromptArgument { value } => {
self.input_ui
.set_submission_mode(super::InputSubmissionMode::ParseCommands);
super::mcp_argument_completion::replace_value(
self.input_ui.text(),
value,
&choice.name,
)
}
};
self.input_ui.set_text_and_cursor(input, cursor);
self.input_ui.set_shell_mode(None);
}
}
pub(super) fn selected_command(
matches: &[CommandChoice],
selection: usize,
) -> Option<CommandChoice> {
matches
.get(selection.min(matches.len().saturating_sub(1)))
.cloned()
}
fn argument_command_choice(choice: &'static commands::CommandArgumentChoice) -> CommandChoice {
CommandChoice {
name: choice.completion.to_string(),
usage: choice.usage.to_string(),
description: choice.description.to_string(),
kind: CommandChoiceKind::BuiltinArgument(choice),
}
}
pub(super) fn slash_command_args(input: &str) -> &str {
let token_end = input
.char_indices()
.find_map(|(index, ch)| ch.is_whitespace().then_some(index))
.unwrap_or(input.len());
input[token_end..].trim_start()
}
pub(super) fn complete_slash_command(input: &str, cursor: usize, name: &str) -> (String, usize) {
let token_end = input
.char_indices()
.find_map(|(index, ch)| ch.is_whitespace().then_some(index))
.unwrap_or(input.len());
let token_len = input[..token_end].chars().count();
let args = slash_command_args(input);
let completed = if args.is_empty() {
format!("/{name}")
} else {
format!("/{name} {args}")
};
let completed_token_len = name.chars().count() + 1;
let new_cursor = if cursor <= token_len {
completed_token_len
} else {
completed
.chars()
.count()
.min(completed_token_len.saturating_add(cursor.saturating_sub(token_len)))
};
(completed, new_cursor)
}
#[cfg(test)]
#[path = "command_palette_tests.rs"]
mod tests;