use crate::ai::conversation_navigation::ConversationNavigationData; use crate::search::command_palette::conversations::search_item::ConversationAction; use crate::search::command_palette::conversations::search_item::ConversationSearchItem; use crate::search::command_palette::conversations::DataSource; use crate::search::data_source::QueryResult; use crate::search::SyncDataSource; use fuzzy_match::match_indices_case_insensitive; use galaxyui::AppContext; /// A conversation that was fuzzy matched against a search term. #[derive(Debug)] pub struct MatchedConversation { pub conversation: ConversationNavigationData, pub match_result: ConversationMatchResult, } impl MatchedConversation { /// Returns the score for the [`MatchedConversation`]. If there was no match result, a score of `0` /// is returned. pub fn score(&self) -> i64 { self.match_result.score } /// Returns the [`ConversationHighlightIndices`] belonging to the matched conversation. pub fn highlight_indices(&self) -> &ConversationHighlightIndices { &self.match_result.highlight_indices } } /// Result from matching a conversation. #[derive(Debug)] pub struct ConversationMatchResult { score: i64, highlight_indices: ConversationHighlightIndices, } impl ConversationMatchResult { /// Returns a dummy match result when there is no match. pub fn no_match() -> Self { ConversationMatchResult { score: 0, highlight_indices: ConversationHighlightIndices { title_indices: vec![], initial_query_indices: vec![], working_directory_indices: vec![], }, } } pub fn score(&self) -> i64 { self.score } } /// Matching indices for a matched conversation. #[derive(Debug)] pub struct ConversationHighlightIndices { pub(super) title_indices: Vec, pub(super) initial_query_indices: Vec, pub(super) working_directory_indices: Vec, } impl ConversationHighlightIndices { fn new( title_indices: Vec, initial_query_indices: Vec, working_directory_indices: Vec, ) -> ConversationHighlightIndices { ConversationHighlightIndices { title_indices, initial_query_indices, working_directory_indices, } } /// Returns the highlight indices for the conversation title. pub fn title_indices(&self) -> &Vec { &self.title_indices } /// Returns the highlight indices for the initial query. pub fn initial_query_indices(&self) -> &Vec { &self.initial_query_indices } /// Returns the highlight indices for the working directory. pub fn working_directory_indices(&self) -> &Vec { &self.working_directory_indices } } /// Returns an iterator of conversations that match `search_term`. pub fn filter_conversations<'a, 'b, I>( conversations_iter: I, search_term: &'b str, ) -> impl Iterator + use<'a, 'b, I> where I: IntoIterator, { conversations_iter .into_iter() .filter_map(move |conversation| { if search_term.is_empty() { Some((ConversationMatchResult::no_match(), conversation.clone())) } else { // Match against title, initial_query, and initial_working_directory let title_match = match_indices_case_insensitive(&conversation.title, search_term); let initial_query_match = conversation .initial_query .as_deref() .and_then(|initial_query| { match_indices_case_insensitive(initial_query, search_term) }); let working_directory_match = conversation .initial_working_directory .as_deref() .and_then(|initial_working_directory| { match_indices_case_insensitive(initial_working_directory, search_term) }); // If none of the fields match, filter this conversation out if title_match.is_none() && initial_query_match.is_none() && working_directory_match.is_none() { return None; } // Determine the best score among all matches let best_score = [ title_match.as_ref(), initial_query_match.as_ref(), working_directory_match.as_ref(), ] .into_iter() .flatten() .map(|r| r.score) .max() .unwrap_or(0); let title_indices = title_match.map(|r| r.matched_indices).unwrap_or_default(); let initial_query_indices = initial_query_match .map(|r| r.matched_indices) .unwrap_or_default(); let working_directory_indices = working_directory_match .map(|r| r.matched_indices) .unwrap_or_default(); let highlight_indices = ConversationHighlightIndices::new( title_indices, initial_query_indices, working_directory_indices, ); Some(( ConversationMatchResult { score: best_score, highlight_indices, }, conversation.clone(), )) } }) .map(|(match_result, conversation)| MatchedConversation { conversation, match_result, }) } type SearcherAction = ::Action; pub trait ConversationSearcher { fn search( &self, _search_term: &str, _app: &AppContext, ) -> anyhow::Result>>; } #[derive(PartialEq)] pub enum ConversationType { All, Historical, } pub struct FuzzyConversationSearcher { filter: ConversationType, } impl FuzzyConversationSearcher { pub fn new() -> Self { Self { filter: ConversationType::All, } } pub fn historical() -> Self { Self { filter: ConversationType::Historical, } } pub fn searchable_conversations(&self, app: &AppContext) -> Vec { match self.filter { ConversationType::Historical => { ConversationNavigationData::historical_conversations(app) } ConversationType::All => ConversationNavigationData::all_conversations(app), } } } impl ConversationSearcher for FuzzyConversationSearcher { fn search( &self, search_term: &str, app: &AppContext, ) -> anyhow::Result>> { let conversations = self.searchable_conversations(app); Ok(filter_conversations(conversations.as_slice(), search_term) .map(|matched_conversation| { ConversationSearchItem::new(ConversationAction::Resume(Box::new( matched_conversation, ))) .into() }) .collect()) } }