add semantic search prompt for the indexing permission on first search

KCaverly and Piotr created

Co-authored-by: Piotr <piotr@zed.dev>

Change summary

crates/search/src/project_search.rs         | 132 +++++++++++++++-------
crates/semantic_index/src/semantic_index.rs |   7 
2 files changed, 93 insertions(+), 46 deletions(-)

Detailed changes

crates/search/src/project_search.rs 🔗

@@ -2,7 +2,7 @@ use crate::{
     NextHistoryQuery, PreviousHistoryQuery, SearchHistory, SearchOptions, SelectNextMatch,
     SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord,
 };
-use anyhow::Context;
+use anyhow::{Context, Result};
 use collections::HashMap;
 use editor::{
     items::active_match_index, scroll::autoscroll::Autoscroll, Anchor, Editor, MultiBuffer,
@@ -13,6 +13,7 @@ use gpui::color::Color;
 use gpui::geometry::rect::RectF;
 use gpui::geometry::vector::IntoVector2F;
 use gpui::json::{self, ToJson};
+use gpui::platform::PromptLevel;
 use gpui::SceneBuilder;
 use gpui::{
     actions,
@@ -127,7 +128,8 @@ pub struct ProjectSearchView {
     model: ModelHandle<ProjectSearch>,
     query_editor: ViewHandle<Editor>,
     results_editor: ViewHandle<Editor>,
-    semantic: Option<SemanticSearchState>,
+    semantic_state: Option<SemanticSearchState>,
+    semantic_permissioned: bool,
     search_options: SearchOptions,
     panels_with_errors: HashSet<InputPanel>,
     active_match_index: Option<usize>,
@@ -402,7 +404,7 @@ impl View for ProjectSearchView {
                 }
             };
 
-            let semantic_status = if let Some(semantic) = &self.semantic {
+            let semantic_status = if let Some(semantic) = &self.semantic_state {
                 if semantic.outstanding_file_count > 0 {
                     let dots_count = semantic.outstanding_file_count % 3 + 1;
                     let dots: String = std::iter::repeat('.').take(dots_count).collect();
@@ -709,65 +711,108 @@ impl ProjectSearchView {
     fn toggle_search_option(&mut self, option: SearchOptions) {
         self.search_options.toggle(option);
     }
+
+    fn index_project(&mut self, cx: &mut ViewContext<Self>) {
+        if let Some(semantic_index) = SemanticIndex::global(cx) {
+            // Semantic search uses no options
+            self.search_options = SearchOptions::none();
+
+            let project = self.model.read(cx).project.clone();
+            let index_task = semantic_index.update(cx, |semantic_index, cx| {
+                semantic_index.index_project(project, cx)
+            });
+
+            cx.spawn(|search_view, mut cx| async move {
+                let (files_to_index, mut files_remaining_rx) = index_task.await?;
+
+                search_view.update(&mut cx, |search_view, cx| {
+                    cx.notify();
+                    search_view.semantic_state = Some(SemanticSearchState {
+                        file_count: files_to_index,
+                        outstanding_file_count: files_to_index,
+                        _progress_task: cx.spawn(|search_view, mut cx| async move {
+                            while let Some(count) = files_remaining_rx.recv().await {
+                                search_view
+                                    .update(&mut cx, |search_view, cx| {
+                                        if let Some(semantic_search_state) =
+                                            &mut search_view.semantic_state
+                                        {
+                                            semantic_search_state.outstanding_file_count = count;
+                                            cx.notify();
+                                            if count == 0 {
+                                                return;
+                                            }
+                                        }
+                                    })
+                                    .ok();
+                            }
+                        }),
+                    });
+                })?;
+                anyhow::Ok(())
+            })
+            .detach_and_log_err(cx);
+        }
+    }
+
     fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext<Self>) {
         self.model.update(cx, |model, _| model.kill_search());
         self.current_mode = mode;
 
         match mode {
             SearchMode::Semantic => {
-                if let Some(semantic_index) = SemanticIndex::global(cx) {
-                    // Semantic search uses no options
-                    self.search_options = SearchOptions::none();
-
+                // let semantic_permissioned = self.semantic_permissioned.await;
+                // if semantic_permissioned.is_ok_and(|permission| !permission) {
+                if !self.semantic_permissioned {
+                    // TODO: Change this to read from the project name
                     let project = self.model.read(cx).project.clone();
-                    let index_task = semantic_index.update(cx, |semantic_index, cx| {
-                        semantic_index.index_project(project, cx)
-                    });
+                    let project_name = project
+                        .read(cx)
+                        .worktree_root_names(cx)
+                        .collect::<Vec<&str>>()
+                        .join("/");
+                    let is_plural =
+                        project_name.chars().filter(|letter| *letter == '/').count() > 0;
+                    let prompt_text = format!("Would you like to index the '{}' project{} for semantic search? This requires sending code to the OpenAI API", project_name,
+                        if is_plural {
+                            "s"
+                        } else {""});
+                    let mut answer = cx.prompt(
+                        PromptLevel::Info,
+                        prompt_text.as_str(),
+                        &["Continue", "Cancel"],
+                    );
 
                     cx.spawn(|search_view, mut cx| async move {
-                        let (files_to_index, mut files_remaining_rx) = index_task.await?;
-
-                        search_view.update(&mut cx, |search_view, cx| {
-                            cx.notify();
-                            search_view.semantic = Some(SemanticSearchState {
-                                file_count: files_to_index,
-                                outstanding_file_count: files_to_index,
-                                _progress_task: cx.spawn(|search_view, mut cx| async move {
-                                    while let Some(count) = files_remaining_rx.recv().await {
-                                        search_view
-                                            .update(&mut cx, |search_view, cx| {
-                                                if let Some(semantic_search_state) =
-                                                    &mut search_view.semantic
-                                                {
-                                                    semantic_search_state.outstanding_file_count =
-                                                        count;
-                                                    cx.notify();
-                                                    if count == 0 {
-                                                        return;
-                                                    }
-                                                }
-                                            })
-                                            .ok();
-                                    }
-                                }),
+                        if answer.next().await == Some(0) {
+                            search_view.update(&mut cx, |search_view, cx| {
+                                search_view.semantic_permissioned = true;
+                                search_view.index_project(cx);
+                            })?;
+                            anyhow::Ok(())
+                        } else {
+                            search_view.update(&mut cx, |search_view, cx| {
+                                search_view.activate_search_mode(SearchMode::Regex, cx);
                             });
-                        })?;
-                        anyhow::Ok(())
+                            anyhow::Ok(())
+                        }
                     })
                     .detach_and_log_err(cx);
+                } else {
+                    self.index_project(cx);
                 }
             }
             SearchMode::Regex => {
                 if !self.is_option_enabled(SearchOptions::REGEX) {
                     self.toggle_search_option(SearchOptions::REGEX);
                 }
-                self.semantic = None;
+                self.semantic_state = None;
             }
             SearchMode::Text => {
                 if self.is_option_enabled(SearchOptions::REGEX) {
                     self.toggle_search_option(SearchOptions::REGEX);
                 }
-                self.semantic = None;
+                self.semantic_state = None;
             }
         }
         cx.notify();
@@ -856,12 +901,15 @@ impl ProjectSearchView {
         })
         .detach();
         let filters_enabled = false;
+
+        // Check if Worktrees have all been previously indexed
         let mut this = ProjectSearchView {
             search_id: model.read(cx).search_id,
             model,
             query_editor,
             results_editor,
-            semantic: None,
+            semantic_state: None,
+            semantic_permissioned: false,
             search_options: options,
             panels_with_errors: HashSet::new(),
             active_match_index: None,
@@ -953,7 +1001,7 @@ impl ProjectSearchView {
         let mode = self.current_mode;
         match mode {
             SearchMode::Semantic => {
-                if let Some(semantic) = &mut self.semantic {
+                if let Some(semantic) = &mut self.semantic_state {
                     if semantic.outstanding_file_count > 0 {
                         return;
                     }
@@ -1747,7 +1795,7 @@ impl View for ProjectSearchBar {
                 .into_any()
             };
             let search = _search.read(cx);
-            let is_semantic_disabled = search.semantic.is_none();
+            let is_semantic_disabled = search.semantic_state.is_none();
 
             let case_sensitive = if is_semantic_disabled {
                 Some(self.render_option_button_icon(

crates/semantic_index/src/semantic_index.rs 🔗

@@ -16,7 +16,7 @@ use language::{Anchor, Buffer, Language, LanguageRegistry};
 use parking_lot::Mutex;
 use parsing::{CodeContextRetriever, Document, PARSEABLE_ENTIRE_FILE_TYPES};
 use postage::watch;
-use project::{search::PathMatcher, Fs, Project, WorktreeId};
+use project::{project_settings, search::PathMatcher, Fs, Project, WorktreeId};
 use smol::channel;
 use std::{
     cmp::Ordering,
@@ -49,9 +49,8 @@ pub fn init(
         .join(Path::new(RELEASE_CHANNEL_NAME.as_str()))
         .join("embeddings_db");
 
-    if *RELEASE_CHANNEL == ReleaseChannel::Stable
-        || !settings::get::<SemanticIndexSettings>(cx).enabled
-    {
+    // This needs to be removed at some point before stable.
+    if *RELEASE_CHANNEL == ReleaseChannel::Stable {
         return;
     }