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::{path::Path, 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}
 27
 28impl SemanticSearchDelegate {
 29    // This is currently searching on every keystroke,
 30    // This is wildly overkill, and has the potential to get expensive
 31    // We will need to update this to throttle searching
 32    pub fn new(
 33        workspace: WeakViewHandle<Workspace>,
 34        project: ModelHandle<Project>,
 35        vector_store: ModelHandle<VectorStore>,
 36    ) -> Self {
 37        Self {
 38            workspace,
 39            project,
 40            vector_store,
 41            selected_match_index: 0,
 42            matches: vec![],
 43        }
 44    }
 45}
 46
 47impl PickerDelegate for SemanticSearchDelegate {
 48    fn placeholder_text(&self) -> Arc<str> {
 49        "Search repository in natural language...".into()
 50    }
 51
 52    fn confirm(&mut self, cx: &mut ViewContext<SemanticSearch>) {
 53        if let Some(search_result) = self.matches.get(self.selected_match_index) {
 54            // Open Buffer
 55            let search_result = search_result.clone();
 56            let buffer = self.project.update(cx, |project, cx| {
 57                project.open_buffer(
 58                    ProjectPath {
 59                        worktree_id: search_result.worktree_id,
 60                        path: search_result.file_path.clone().into(),
 61                    },
 62                    cx,
 63                )
 64            });
 65
 66            let workspace = self.workspace.clone();
 67            let position = search_result.clone().offset;
 68            cx.spawn(|_, mut cx| async move {
 69                let buffer = buffer.await?;
 70                workspace.update(&mut cx, |workspace, cx| {
 71                    let editor = workspace.open_project_item::<Editor>(buffer, cx);
 72                    editor.update(cx, |editor, cx| {
 73                        editor.change_selections(Some(Autoscroll::center()), cx, |s| {
 74                            s.select_ranges([position..position])
 75                        });
 76                    });
 77                })?;
 78                Ok::<_, anyhow::Error>(())
 79            })
 80            .detach_and_log_err(cx);
 81            cx.emit(PickerEvent::Dismiss);
 82        }
 83    }
 84
 85    fn dismissed(&mut self, _cx: &mut ViewContext<SemanticSearch>) {}
 86
 87    fn match_count(&self) -> usize {
 88        self.matches.len()
 89    }
 90
 91    fn selected_index(&self) -> usize {
 92        self.selected_match_index
 93    }
 94
 95    fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<SemanticSearch>) {
 96        self.selected_match_index = ix;
 97    }
 98
 99    fn update_matches(&mut self, query: String, cx: &mut ViewContext<SemanticSearch>) -> Task<()> {
100        if query.len() < MIN_QUERY_LEN {
101            return Task::ready(());
102        }
103
104        let vector_store = self.vector_store.clone();
105        let project = self.project.clone();
106        cx.spawn(|this, mut cx| async move {
107            cx.background().timer(EMBEDDING_DEBOUNCE_INTERVAL).await;
108
109            log::info!("Searching for {:?}", &query);
110
111            let task = vector_store.update(&mut cx, |store, cx| {
112                store.search(&project, query.to_string(), 10, cx)
113            });
114
115            if let Some(results) = task.await.log_err() {
116                this.update(&mut cx, |this, _| {
117                    let delegate = this.delegate_mut();
118                    delegate.matches = results;
119                })
120                .ok();
121            }
122        })
123    }
124
125    fn render_match(
126        &self,
127        ix: usize,
128        mouse_state: &mut MouseState,
129        selected: bool,
130        cx: &AppContext,
131    ) -> AnyElement<Picker<Self>> {
132        let theme = theme::current(cx);
133        let style = &theme.picker.item;
134        let current_style = style.in_state(selected).style_for(mouse_state);
135
136        let search_result = &self.matches[ix];
137
138        let mut path = search_result.file_path.to_string_lossy();
139        let name = search_result.name.clone();
140
141        Flex::column()
142            .with_child(Text::new(name, current_style.label.text.clone()).with_soft_wrap(false))
143            .with_child(Label::new(
144                path.to_string(),
145                style.inactive_state().default.label.clone(),
146            ))
147            .contained()
148            .with_style(current_style.container)
149            .into_any()
150    }
151}