modal.rs

  1use crate::{SearchResult, VectorStore};
  2use editor::{scroll::autoscroll::Autoscroll, Editor};
  3use gpui::{
  4    actions, elements::*, AnyElement, AppContext, ModelHandle, MouseState, Task, ViewContext,
  5    WeakViewHandle,
  6};
  7use picker::{Picker, PickerDelegate, PickerEvent};
  8use project::{Project, ProjectPath};
  9use std::{collections::HashMap, sync::Arc, time::Duration};
 10use util::ResultExt;
 11use workspace::Workspace;
 12
 13const MIN_QUERY_LEN: usize = 5;
 14const EMBEDDING_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(500);
 15
 16actions!(semantic_search, [Toggle]);
 17
 18pub type SemanticSearch = Picker<SemanticSearchDelegate>;
 19
 20pub struct SemanticSearchDelegate {
 21    workspace: WeakViewHandle<Workspace>,
 22    project: ModelHandle<Project>,
 23    vector_store: ModelHandle<VectorStore>,
 24    selected_match_index: usize,
 25    matches: Vec<SearchResult>,
 26    history: HashMap<String, Vec<SearchResult>>,
 27}
 28
 29impl SemanticSearchDelegate {
 30    // This is currently searching on every keystroke,
 31    // This is wildly overkill, and has the potential to get expensive
 32    // We will need to update this to throttle searching
 33    pub fn new(
 34        workspace: WeakViewHandle<Workspace>,
 35        project: ModelHandle<Project>,
 36        vector_store: ModelHandle<VectorStore>,
 37    ) -> Self {
 38        Self {
 39            workspace,
 40            project,
 41            vector_store,
 42            selected_match_index: 0,
 43            matches: vec![],
 44            history: HashMap::new(),
 45        }
 46    }
 47}
 48
 49impl PickerDelegate for SemanticSearchDelegate {
 50    fn placeholder_text(&self) -> Arc<str> {
 51        "Search repository in natural language...".into()
 52    }
 53
 54    fn confirm(&mut self, cx: &mut ViewContext<SemanticSearch>) {
 55        if let Some(search_result) = self.matches.get(self.selected_match_index) {
 56            // Open Buffer
 57            let search_result = search_result.clone();
 58            let buffer = self.project.update(cx, |project, cx| {
 59                project.open_buffer(
 60                    ProjectPath {
 61                        worktree_id: search_result.worktree_id,
 62                        path: search_result.file_path.clone().into(),
 63                    },
 64                    cx,
 65                )
 66            });
 67
 68            let workspace = self.workspace.clone();
 69            let position = search_result.clone().offset;
 70            cx.spawn(|_, mut cx| async move {
 71                let buffer = buffer.await?;
 72                workspace.update(&mut cx, |workspace, cx| {
 73                    let editor = workspace.open_project_item::<Editor>(buffer, cx);
 74                    editor.update(cx, |editor, cx| {
 75                        editor.change_selections(Some(Autoscroll::center()), cx, |s| {
 76                            s.select_ranges([position..position])
 77                        });
 78                    });
 79                })?;
 80                Ok::<_, anyhow::Error>(())
 81            })
 82            .detach_and_log_err(cx);
 83            cx.emit(PickerEvent::Dismiss);
 84        }
 85    }
 86
 87    fn dismissed(&mut self, _cx: &mut ViewContext<SemanticSearch>) {}
 88
 89    fn match_count(&self) -> usize {
 90        self.matches.len()
 91    }
 92
 93    fn selected_index(&self) -> usize {
 94        self.selected_match_index
 95    }
 96
 97    fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<SemanticSearch>) {
 98        self.selected_match_index = ix;
 99    }
100
101    fn update_matches(&mut self, query: String, cx: &mut ViewContext<SemanticSearch>) -> Task<()> {
102        log::info!("Searching for {:?}...", query);
103        if query.len() < MIN_QUERY_LEN {
104            log::info!("Query below minimum length");
105            return Task::ready(());
106        }
107
108        let vector_store = self.vector_store.clone();
109        let project = self.project.clone();
110        cx.spawn(|this, mut cx| async move {
111            cx.background().timer(EMBEDDING_DEBOUNCE_INTERVAL).await;
112
113            let retrieved_cached = this.update(&mut cx, |this, _| {
114                let delegate = this.delegate_mut();
115                if delegate.history.contains_key(&query) {
116                    let historic_results = delegate.history.get(&query).unwrap().to_owned();
117                    delegate.matches = historic_results.clone();
118                    true
119                } else {
120                    false
121                }
122            });
123
124            if let Some(retrieved) = retrieved_cached.log_err() {
125                if !retrieved {
126                    let task = vector_store.update(&mut cx, |store, cx| {
127                        store.search(project.clone(), query.to_string(), 10, cx)
128                    });
129
130                    if let Some(results) = task.await.log_err() {
131                        log::info!("Not queried previously, searching...");
132                        this.update(&mut cx, |this, _| {
133                            let delegate = this.delegate_mut();
134                            delegate.matches = results.clone();
135                            delegate.history.insert(query, results);
136                        })
137                        .ok();
138                    }
139                } else {
140                    log::info!("Already queried, retrieved directly from cached history");
141                }
142            }
143        })
144    }
145
146    fn render_match(
147        &self,
148        ix: usize,
149        mouse_state: &mut MouseState,
150        selected: bool,
151        cx: &AppContext,
152    ) -> AnyElement<Picker<Self>> {
153        let theme = theme::current(cx);
154        let style = &theme.picker.item;
155        let current_style = style.in_state(selected).style_for(mouse_state);
156
157        let search_result = &self.matches[ix];
158
159        let path = search_result.file_path.to_string_lossy();
160        let name = search_result.name.clone();
161
162        Flex::column()
163            .with_child(Text::new(name, current_style.label.text.clone()).with_soft_wrap(false))
164            .with_child(Label::new(
165                path.to_string(),
166                style.inactive_state().default.label.clone(),
167            ))
168            .contained()
169            .with_style(current_style.container)
170            .into_any()
171    }
172}