use crate::{SearchResult, VectorStore}; use editor::{scroll::autoscroll::Autoscroll, Editor}; use gpui::{ actions, elements::*, AnyElement, AppContext, ModelHandle, MouseState, Task, ViewContext, WeakViewHandle, }; use picker::{Picker, PickerDelegate, PickerEvent}; use project::{Project, ProjectPath}; use std::{collections::HashMap, sync::Arc, time::Duration}; use util::ResultExt; use workspace::Workspace; const MIN_QUERY_LEN: usize = 5; const EMBEDDING_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(500); actions!(semantic_search, [Toggle]); pub type SemanticSearch = Picker; pub struct SemanticSearchDelegate { workspace: WeakViewHandle, project: ModelHandle, vector_store: ModelHandle, selected_match_index: usize, matches: Vec, history: HashMap>, } impl SemanticSearchDelegate { // This is currently searching on every keystroke, // This is wildly overkill, and has the potential to get expensive // We will need to update this to throttle searching pub fn new( workspace: WeakViewHandle, project: ModelHandle, vector_store: ModelHandle, ) -> Self { Self { workspace, project, vector_store, selected_match_index: 0, matches: vec![], history: HashMap::new(), } } } impl PickerDelegate for SemanticSearchDelegate { fn placeholder_text(&self) -> Arc { "Search repository in natural language...".into() } fn confirm(&mut self, _: bool, cx: &mut ViewContext) { if let Some(search_result) = self.matches.get(self.selected_match_index) { // Open Buffer let search_result = search_result.clone(); let buffer = self.project.update(cx, |project, cx| { project.open_buffer( ProjectPath { worktree_id: search_result.worktree_id, path: search_result.file_path.clone().into(), }, cx, ) }); let workspace = self.workspace.clone(); let position = search_result.clone().offset; cx.spawn(|_, mut cx| async move { let buffer = buffer.await?; workspace.update(&mut cx, |workspace, cx| { let editor = workspace.open_project_item::(buffer, cx); editor.update(cx, |editor, cx| { editor.change_selections(Some(Autoscroll::center()), cx, |s| { s.select_ranges([position..position]) }); }); })?; Ok::<_, anyhow::Error>(()) }) .detach_and_log_err(cx); cx.emit(PickerEvent::Dismiss); } } fn dismissed(&mut self, _cx: &mut ViewContext) {} fn match_count(&self) -> usize { self.matches.len() } fn selected_index(&self) -> usize { self.selected_match_index } fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext) { self.selected_match_index = ix; } fn update_matches(&mut self, query: String, cx: &mut ViewContext) -> Task<()> { log::info!("Searching for {:?}...", query); if query.len() < MIN_QUERY_LEN { log::info!("Query below minimum length"); return Task::ready(()); } let vector_store = self.vector_store.clone(); let project = self.project.clone(); cx.spawn(|this, mut cx| async move { cx.background().timer(EMBEDDING_DEBOUNCE_INTERVAL).await; let retrieved_cached = this.update(&mut cx, |this, _| { let delegate = this.delegate_mut(); if delegate.history.contains_key(&query) { let historic_results = delegate.history.get(&query).unwrap().to_owned(); delegate.matches = historic_results.clone(); true } else { false } }); if let Some(retrieved) = retrieved_cached.log_err() { if !retrieved { let task = vector_store.update(&mut cx, |store, cx| { store.search(project.clone(), query.to_string(), 10, cx) }); if let Some(results) = task.await.log_err() { log::info!("Not queried previously, searching..."); this.update(&mut cx, |this, _| { let delegate = this.delegate_mut(); delegate.matches = results.clone(); delegate.history.insert(query, results); }) .ok(); } } else { log::info!("Already queried, retrieved directly from cached history"); } } }) } fn render_match( &self, ix: usize, mouse_state: &mut MouseState, selected: bool, cx: &AppContext, ) -> AnyElement> { let theme = theme::current(cx); let style = &theme.picker.item; let current_style = style.in_state(selected).style_for(mouse_state); let search_result = &self.matches[ix]; let path = search_result.file_path.to_string_lossy(); let name = search_result.name.clone(); Flex::column() .with_child(Text::new(name, current_style.label.text.clone()).with_soft_wrap(false)) .with_child(Label::new( path.to_string(), style.inactive_state().default.label.clone(), )) .contained() .with_style(current_style.container) .into_any() } }