Codex Handbook
tui/src/bottom_pane/mentions_v2/search_catalog.rs 215 lines
use codex_core_skills::model::SkillMetadata;use codex_plugin::PluginCapabilitySummary;use crate::skills_helpers::skill_description;use crate::skills_helpers::skill_display_name;use super::candidate::Candidate;use super::candidate::MentionType;use super::candidate::Selection;pub(crate) fn build_search_catalog(    skills: Option<&[SkillMetadata]>,    plugins: Option<&[PluginCapabilitySummary]>,) -> Vec<Candidate> {    let mut candidates = Vec::new();    if let Some(skills) = skills {        candidates.extend(skills.iter().map(skill_candidate));    }    if let Some(plugins) = plugins {        candidates.extend(plugins.iter().map(plugin_candidate));    }    candidates}fn skill_candidate(skill: &SkillMetadata) -> Candidate {    let display_name = skill_display_name(skill);    let description = optional_skill_description(skill);    let skill_name = skill.name.clone();    let search_terms = if display_name == skill.name {        vec![skill_name.clone()]    } else {        vec![skill_name.clone(), display_name.clone()]    };    Candidate {        display_name,        description,        search_terms,        mention_type: MentionType::Skill,        selection: Selection::Tool {            insert_text: format!("${skill_name}"),            path: Some(skill.path_to_skills_md.to_string_lossy().into_owned()),        },    }}fn plugin_candidate(plugin: &PluginCapabilitySummary) -> Candidate {    let (plugin_name, marketplace_name) = plugin        .config_name        .split_once('@')        .unwrap_or((plugin.config_name.as_str(), ""));    let mention_name = plugin_mention_name(plugin_name, plugin.display_name.as_str());    let mut search_terms = vec![plugin_name.to_string(), plugin.config_name.clone()];    if plugin.display_name != plugin_name {        search_terms.push(plugin.display_name.clone());    }    if !marketplace_name.is_empty() {        search_terms.push(marketplace_name.to_string());    }    Candidate {        display_name: plugin.display_name.clone(),        description: plugin_description(plugin),        search_terms,        mention_type: MentionType::Plugin,        selection: Selection::Tool {            insert_text: format!("@{mention_name}"),            path: Some(format!("plugin://{}", plugin.config_name)),        },    }}fn plugin_mention_name(plugin_name: &str, display_name: &str) -> String {    let plugin_segments = split_plugin_name_segments(plugin_name);    let display_segments = split_display_name_segments(display_name);    if plugin_segments.len() == display_segments.len()        && plugin_segments.iter().zip(&display_segments).all(            |((plugin_segment, _), display_segment)| {                plugin_segment.eq_ignore_ascii_case(display_segment.as_str())            },        )    {        let mut result = String::new();        for ((_, separator), display_segment) in plugin_segments.into_iter().zip(display_segments) {            result.push_str(display_segment.as_str());            if let Some(separator) = separator {                result.push(separator);            }        }        return result;    }    title_case_plugin_name(plugin_name)}fn split_plugin_name_segments(plugin_name: &str) -> Vec<(String, Option<char>)> {    let mut segments = Vec::new();    let mut current = String::new();    for ch in plugin_name.chars() {        if matches!(ch, '-' | '_') {            if !current.is_empty() {                segments.push((std::mem::take(&mut current), Some(ch)));            }        } else {            current.push(ch);        }    }    if !current.is_empty() {        segments.push((current, None));    }    segments}fn split_display_name_segments(display_name: &str) -> Vec<String> {    display_name        .split(|ch: char| !ch.is_ascii_alphanumeric())        .filter(|segment| !segment.is_empty())        .map(ToString::to_string)        .collect()}fn title_case_plugin_name(plugin_name: &str) -> String {    let mut result = String::with_capacity(plugin_name.len());    let mut capitalize_next = true;    for ch in plugin_name.chars() {        if matches!(ch, '-' | '_') {            capitalize_next = true;            result.push(ch);            continue;        }        if capitalize_next && ch.is_ascii_alphabetic() {            result.push(ch.to_ascii_uppercase());            capitalize_next = false;        } else {            result.push(ch);            capitalize_next = false;        }    }    result}fn plugin_description(plugin: &PluginCapabilitySummary) -> Option<String> {    let capability_labels = plugin_capability_labels(plugin);    plugin.description.clone().or_else(|| {        Some(if capability_labels.is_empty() {            "Plugin".to_string()        } else {            format!("Plugin - {}", capability_labels.join(" - "))        })    })}fn plugin_capability_labels(plugin: &PluginCapabilitySummary) -> Vec<String> {    let mut labels = Vec::new();    if plugin.has_skills {        labels.push("skills".to_string());    }    if !plugin.mcp_server_names.is_empty() {        let mcp_server_count = plugin.mcp_server_names.len();        labels.push(if mcp_server_count == 1 {            "1 MCP server".to_string()        } else {            format!("{mcp_server_count} MCP servers")        });    }    if !plugin.app_connector_ids.is_empty() {        let app_count = plugin.app_connector_ids.len();        labels.push(if app_count == 1 {            "1 app".to_string()        } else {            format!("{app_count} apps")        });    }    labels}fn optional_skill_description(skill: &SkillMetadata) -> Option<String> {    let description = skill_description(skill).trim();    (!description.is_empty()).then(|| description.to_string())}#[cfg(test)]mod tests {    use super::*;    use pretty_assertions::assert_eq;    #[test]    fn plugin_mention_name_uses_display_segments_when_they_match_plugin_name() {        assert_eq!(            plugin_mention_name("mcp-search", "MCP Search"),            "MCP-Search"        );        assert_eq!(            plugin_mention_name("google_calendar", "Google Calendar"),            "Google_Calendar"        );    }    #[test]    fn plugin_mention_name_falls_back_to_title_cased_plugin_name() {        assert_eq!(plugin_mention_name("sample", "Sample Plugin"), "Sample");        assert_eq!(            plugin_mention_name("browser-use", "Browser Use"),            "Browser-Use"        );    }}