use fuzzy_match::FuzzyMatchResult; use galaxyui::elements::{ ConstrainedBox, Container, CrossAxisAlignment, Flex, Highlight, Icon, ParentElement, Text, }; use galaxyui::fonts::{Properties, Weight}; use galaxyui::{AppContext, Element, SingletonEntity}; use ordered_float::OrderedFloat; // Import CodeSymbol from the data_source module use super::data_source::CodeSymbol; use crate::appearance::Appearance; use crate::search::ai_context_menu::mixer::AIContextMenuSearchableAction; use crate::search::ai_context_menu::{safe_truncate, styles}; use crate::search::item::{IconLocation, SearchItem}; use crate::search::result_renderer::ItemHighlightState; const MAX_COMBINED_LENGTH: usize = 55; #[derive(Debug, Clone)] pub struct CodeSearchItem { pub code_symbol: CodeSymbol, pub match_result: FuzzyMatchResult, } impl SearchItem for CodeSearchItem { type Action = AIContextMenuSearchableAction; fn render_icon( &self, _highlight_state: ItemHighlightState, appearance: &Appearance, ) -> Box { Container::new( ConstrainedBox::new( Icon::new( "bundled/svg/code-01.svg", _highlight_state.icon_fill(appearance).into_solid(), ) .finish(), ) .with_width(styles::ICON_SIZE) .with_height(styles::ICON_SIZE) .finish(), ) .with_margin_right(styles::MARGIN_RIGHT) .finish() } fn icon_location(&self, _appearance: &Appearance) -> IconLocation { IconLocation::Centered } fn render_item( &self, highlight_state: ItemHighlightState, app: &AppContext, ) -> Box { let appearance = Appearance::as_ref(app); // Build the symbol name with type prefix let mut symbol_name = String::new(); if let Some(symbol_type) = &self.code_symbol.symbol.type_prefix { symbol_name.push_str(&format!("{symbol_type} ")); } symbol_name.push_str(&self.code_symbol.symbol.name); // Get file path for display let file_path = self.code_symbol.file_path.to_string_lossy().to_string(); let mut path_display = file_path.clone(); // Track truncation for highlight adjustment let mut symbol_truncated = false; // Ensure combined length is less than MAX_COMBINED_LENGTH characters let combined_length = symbol_name.len() + path_display.len(); if combined_length > MAX_COMBINED_LENGTH { // If combined length is too long, prioritize showing the symbol name if symbol_name.len() >= MAX_COMBINED_LENGTH { // If symbol name itself is too long, truncate it and add ellipsis safe_truncate(&mut symbol_name, MAX_COMBINED_LENGTH - 3); symbol_name.push_str("..."); symbol_truncated = true; path_display.clear(); } else { // Symbol name fits, truncate path display let available_for_path = MAX_COMBINED_LENGTH - symbol_name.len(); if path_display.len() > available_for_path { let new_path_len = available_for_path.saturating_sub(3); safe_truncate(&mut path_display, new_path_len); path_display.push_str("..."); } } } // Calculate highlight indices, adjusting for display format // The fuzzy matching is done on concatenated "typeprefix" + "symbolname" (no space) // But display shows "typeprefix " + "symbolname" (with space) // So we need to adjust indices to account for the added space in display let symbol_highlights: Vec = if !symbol_truncated { self.match_result .matched_indices .iter() .map(|&i| { if let Some(symbol_type) = &self.code_symbol.symbol.type_prefix { // If we have a type prefix, adjust indices: // - Indices 0 to type_prefix.len()-1 map directly (type prefix part) // - Indices type_prefix.len() and beyond need +1 offset (for the added space) if i < symbol_type.len() { i // Direct mapping for type prefix } else { i + 1 // Add 1 for the space between type and name } } else { i // No type prefix, direct mapping } }) .collect() } else { // Only include highlights that fall within the truncated range self.match_result .matched_indices .iter() .filter_map(|&i| { let adjusted_i = if let Some(symbol_type) = &self.code_symbol.symbol.type_prefix { if i < symbol_type.len() { i } else { i + 1 } } else { i }; if adjusted_i < MAX_COMBINED_LENGTH - 3 { Some(adjusted_i) } else { None } }) .collect() }; // Create symbol name text with highlighting let mut symbol_text = Text::new( symbol_name, appearance.ui_font_family(), appearance.monospace_font_size() - 1.0, ) .with_color(highlight_state.main_text_fill(appearance).into_solid()); if !symbol_highlights.is_empty() { symbol_text = symbol_text.with_single_highlight( Highlight::new().with_properties(Properties::default().weight(Weight::Bold)), symbol_highlights, ); } // Create path text with lighter color let path_text = if !path_display.is_empty() { Some( Text::new( path_display, appearance.ui_font_family(), appearance.monospace_font_size() - 2.0, ) .with_color(highlight_state.sub_text_fill(appearance).into_solid()) .finish(), ) } else { None }; // Create row with symbol name and path on the same line let mut row = Flex::row() .with_child(symbol_text.finish()) .with_cross_axis_alignment(CrossAxisAlignment::Center); if let Some(path) = path_text { row.add_child(Container::new(path).with_padding_left(8.0).finish()); } row.finish() } fn render_details(&self, ctx: &AppContext) -> Option> { let appearance = Appearance::as_ref(ctx); let theme = appearance.theme(); // Create main text: symbol type + name (e.g., "fn initialize_logger") let mut main_text = String::new(); if let Some(symbol_type) = &self.code_symbol.symbol.type_prefix { main_text.push_str(symbol_type); main_text.push(' '); } main_text.push_str(&self.code_symbol.symbol.name); // Create sub text: path + line number (e.g., "core/logging.rs (44)") let sub_text = format!( "{} ({})", self.code_symbol.file_path.to_string_lossy(), self.code_symbol.symbol.line_number ); // Create main text element - use slightly smaller font that scales with user settings let main_text_element = Text::new( main_text, appearance.monospace_font_family(), // Use monospace font for consistency appearance.monospace_font_size() - 1.0, // Slightly smaller than normal ) .with_color(theme.active_ui_text_color().into()) .with_style(Properties::default().weight(Weight::Medium)) .finish(); // Create sub text element - even smaller for sub information let sub_text_element = Text::new( sub_text, appearance.monospace_font_family(), // Use monospace font for consistency appearance.monospace_font_size() - 3.0, // Smaller than main text ) .with_color(theme.nonactive_ui_text_color().into()) .finish(); // Create modal content with reduced spacing let content = Flex::column() .with_child(main_text_element) .with_child( Container::new(sub_text_element) .with_padding_top(2.0) .finish(), ) .finish(); Some(content) } fn score(&self) -> OrderedFloat { OrderedFloat(self.match_result.score as f64) } fn accept_result(&self) -> Self::Action { // Format the text as "{symbol_type} {symbol_name} in {path}:{line_number}" let mut text = String::new(); if let Some(symbol_type) = &self.code_symbol.symbol.type_prefix { text.push_str(symbol_type); text.push(' '); } text.push_str(&self.code_symbol.symbol.name); text.push_str(" in "); text.push_str(&self.code_symbol.file_path.to_string_lossy()); text.push(':'); text.push_str(&self.code_symbol.symbol.line_number.to_string()); AIContextMenuSearchableAction::InsertText { text } } fn execute_result(&self) -> Self::Action { self.accept_result() } fn accessibility_label(&self) -> String { format!( "Code symbol: {} in {}:{}", self.code_symbol.symbol.name, self.code_symbol.file_path.to_string_lossy(), self.code_symbol.symbol.line_number ) } }