WIP: Working modal, without navigation and search on every keystroke

KCaverly created

Change summary

Cargo.lock                              |   2 
crates/vector_store/Cargo.toml          |   2 
crates/vector_store/src/modal.rs        | 107 +++++++++++++++++++++++++++
crates/vector_store/src/vector_store.rs |  54 ++++++++----
4 files changed, 146 insertions(+), 19 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -7960,6 +7960,7 @@ dependencies = [
  "log",
  "matrixmultiply",
  "ndarray",
+ "picker",
  "project",
  "rand 0.8.5",
  "rusqlite",
@@ -7968,6 +7969,7 @@ dependencies = [
  "sha-1 0.10.1",
  "smol",
  "tempdir",
+ "theme",
  "tree-sitter",
  "tree-sitter-rust",
  "unindent",

crates/vector_store/Cargo.toml 🔗

@@ -14,6 +14,8 @@ language = { path = "../language" }
 project = { path = "../project" }
 workspace = { path = "../workspace" }
 util = { path = "../util" }
+picker = { path = "../picker" }
+theme = { path = "../theme" }
 anyhow.workspace = true
 futures.workspace = true
 smol.workspace = true

crates/vector_store/src/modal.rs 🔗

@@ -0,0 +1,107 @@
+use std::sync::Arc;
+
+use gpui::{
+    actions, elements::*, AnyElement, AppContext, ModelHandle, MouseState, Task, ViewContext,
+    WeakViewHandle,
+};
+use picker::{Picker, PickerDelegate, PickerEvent};
+use project::Project;
+use util::ResultExt;
+use workspace::Workspace;
+
+use crate::{SearchResult, VectorStore};
+
+actions!(semantic_search, [Toggle]);
+
+pub type SemanticSearch = Picker<SemanticSearchDelegate>;
+
+pub struct SemanticSearchDelegate {
+    workspace: WeakViewHandle<Workspace>,
+    project: ModelHandle<Project>,
+    vector_store: ModelHandle<VectorStore>,
+    selected_match_index: usize,
+    matches: Vec<SearchResult>,
+}
+
+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<Workspace>,
+        project: ModelHandle<Project>,
+        vector_store: ModelHandle<VectorStore>,
+    ) -> Self {
+        Self {
+            workspace,
+            project,
+            vector_store,
+            selected_match_index: 0,
+            matches: vec![],
+        }
+    }
+}
+
+impl PickerDelegate for SemanticSearchDelegate {
+    fn placeholder_text(&self) -> Arc<str> {
+        "Search repository in natural language...".into()
+    }
+
+    fn confirm(&mut self, cx: &mut ViewContext<SemanticSearch>) {
+        todo!()
+    }
+
+    fn dismissed(&mut self, _cx: &mut ViewContext<SemanticSearch>) {}
+
+    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<SemanticSearch>) {
+        self.selected_match_index = ix;
+    }
+
+    fn update_matches(&mut self, query: String, cx: &mut ViewContext<SemanticSearch>) -> Task<()> {
+        let task = self
+            .vector_store
+            .update(cx, |store, cx| store.search(query.to_string(), 10, cx));
+
+        cx.spawn(|this, mut cx| async move {
+            let results = task.await.log_err();
+            this.update(&mut cx, |this, cx| {
+                if let Some(results) = results {
+                    let delegate = this.delegate_mut();
+                    delegate.matches = results;
+                }
+            });
+        })
+    }
+
+    fn render_match(
+        &self,
+        ix: usize,
+        mouse_state: &mut MouseState,
+        selected: bool,
+        cx: &AppContext,
+    ) -> AnyElement<Picker<Self>> {
+        let theme = theme::current(cx);
+        let style = &theme.picker.item;
+        let current_style = style.style_for(mouse_state, selected);
+
+        let search_result = &self.matches[ix];
+
+        let mut 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.default.label.clone()))
+            .contained()
+            .with_style(current_style.container)
+            .into_any()
+    }
+}

crates/vector_store/src/vector_store.rs 🔗

@@ -1,5 +1,6 @@
 mod db;
 mod embedding;
+mod modal;
 mod search;
 
 #[cfg(test)]
@@ -10,6 +11,7 @@ use db::{FileSha1, VectorDatabase, VECTOR_DB_URL};
 use embedding::{EmbeddingProvider, OpenAIEmbeddings};
 use gpui::{actions, AppContext, Entity, ModelContext, ModelHandle, Task, ViewContext};
 use language::{Language, LanguageRegistry};
+use modal::{SemanticSearch, SemanticSearchDelegate, Toggle};
 use project::{Fs, Project};
 use smol::channel;
 use std::{cmp::Ordering, collections::HashMap, path::PathBuf, sync::Arc};
@@ -17,8 +19,6 @@ use tree_sitter::{Parser, QueryCursor};
 use util::{http::HttpClient, ResultExt, TryFutureExt};
 use workspace::{Workspace, WorkspaceCreated};
 
-actions!(semantic_search, [TestSearch]);
-
 #[derive(Debug)]
 pub struct Document {
     pub offset: usize,
@@ -60,24 +60,40 @@ pub fn init(
     .detach();
 
     cx.add_action({
-        let vector_store = vector_store.clone();
-        move |workspace: &mut Workspace, _: &TestSearch, cx: &mut ViewContext<Workspace>| {
-            let t0 = std::time::Instant::now();
-            let task = vector_store.update(cx, |store, cx| {
-                store.search("compute embeddings for all of the symbols in the codebase and write them to a database".to_string(), 10, cx)
-            });
-
-            cx.spawn(|this, cx| async move {
-                let results = task.await?;
-                let duration = t0.elapsed();
-
-                println!("search took {:?}", duration);
-                println!("results {:?}", results);
-
-                anyhow::Ok(())
-            }).detach()
+        move |workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>| {
+            let vector_store = vector_store.clone();
+            workspace.toggle_modal(cx, |workspace, cx| {
+                let project = workspace.project().clone();
+                let workspace = cx.weak_handle();
+                cx.add_view(|cx| {
+                    SemanticSearch::new(
+                        SemanticSearchDelegate::new(workspace, project, vector_store),
+                        cx,
+                    )
+                })
+            })
         }
     });
+    SemanticSearch::init(cx);
+    // cx.add_action({
+    //     let vector_store = vector_store.clone();
+    //     move |workspace: &mut Workspace, _: &TestSearch, cx: &mut ViewContext<Workspace>| {
+    //         let t0 = std::time::Instant::now();
+    //         let task = vector_store.update(cx, |store, cx| {
+    //             store.search("compute embeddings for all of the symbols in the codebase and write them to a database".to_string(), 10, cx)
+    //         });
+
+    //         cx.spawn(|this, cx| async move {
+    //             let results = task.await?;
+    //             let duration = t0.elapsed();
+
+    //             println!("search took {:?}", duration);
+    //             println!("results {:?}", results);
+
+    //             anyhow::Ok(())
+    //         }).detach()
+    //     }
+    // });
 }
 
 #[derive(Debug)]
@@ -87,7 +103,7 @@ pub struct IndexedFile {
     documents: Vec<Document>,
 }
 
-struct VectorStore {
+pub struct VectorStore {
     fs: Arc<dyn Fs>,
     database_url: Arc<str>,
     embedding_provider: Arc<dyn EmbeddingProvider>,