Project search2 (#3585)

Piotr Osiewicz created

Semantic search and tests are gonna be shipped in a separate PR.

Release Notes:

- N/A

Change summary

Cargo.lock                           |  23 
crates/editor2/src/editor.rs         |  12 
crates/search2/Cargo.toml            |   2 
crates/search2/src/project_search.rs | 726 +++++++++++++----------------
crates/search2/src/search.rs         |   5 
crates/ui2/src/components/icon.rs    |   2 
crates/zed2/src/zed2.rs              |   5 
7 files changed, 358 insertions(+), 417 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1388,11 +1388,10 @@ dependencies = [
 
 [[package]]
 name = "cc"
-version = "1.0.83"
+version = "1.0.84"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
+checksum = "0f8e7c90afad890484a21653d08b6e209ae34770fb5ee298f9c699fcc1e5c856"
 dependencies = [
- "jobserver",
  "libc",
 ]
 
@@ -4671,15 +4670,6 @@ version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
 
-[[package]]
-name = "jobserver"
-version = "0.1.26"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2"
-dependencies = [
- "libc",
-]
-
 [[package]]
 name = "journal"
 version = "0.1.0"
@@ -5055,18 +5045,18 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
 
 [[package]]
 name = "linkme"
-version = "0.3.18"
+version = "0.3.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1e6b0bb9ca88d3c5ae88240beb9683821f903b824ee8381ef9ab4e8522fbfa9"
+checksum = "91ed2ee9464ff9707af8e9ad834cffa4802f072caad90639c583dd3c62e6e608"
 dependencies = [
  "linkme-impl",
 ]
 
 [[package]]
 name = "linkme-impl"
-version = "0.3.18"
+version = "0.3.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b3b3f61e557a617ec6ba36c79431e1f3b5e100d67cfbdb61ed6ef384298af016"
+checksum = "ba125974b109d512fccbc6c0244e7580143e460895dfd6ea7f8bbb692fd94396"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -8300,6 +8290,7 @@ dependencies = [
  "menu2",
  "postage",
  "project2",
+ "semantic_index2",
  "serde",
  "serde_derive",
  "serde_json",

crates/editor2/src/editor.rs 🔗

@@ -8706,13 +8706,13 @@ impl Editor {
         );
     }
 
-    //     pub fn set_searchable(&mut self, searchable: bool) {
-    //         self.searchable = searchable;
-    //     }
+    pub fn set_searchable(&mut self, searchable: bool) {
+        self.searchable = searchable;
+    }
 
-    //     pub fn searchable(&self) -> bool {
-    //         self.searchable
-    //     }
+    pub fn searchable(&self) -> bool {
+        self.searchable
+    }
 
     fn open_excerpts(&mut self, _: &OpenExcerpts, cx: &mut ViewContext<Self>) {
         let buffer = self.buffer.read(cx);

crates/search2/Cargo.toml 🔗

@@ -21,7 +21,7 @@ theme = { package = "theme2", path = "../theme2" }
 util = { path = "../util" }
 ui = {package = "ui2", path = "../ui2"}
 workspace = { package = "workspace2", path = "../workspace2" }
-#semantic_index = { path = "../semantic_index" }
+semantic_index = { package = "semantic_index2", path = "../semantic_index2" }
 anyhow.workspace = true
 futures.workspace = true
 log.workspace = true

crates/search2/src/project_search.rs 🔗

@@ -1,24 +1,19 @@
 use crate::{
-    history::SearchHistory,
-    mode::{SearchMode, Side},
-    search_bar::{render_nav_button, render_option_button_icon, render_search_mode_button},
-    ActivateRegexMode, ActivateSemanticMode, ActivateTextMode, CycleMode, NextHistoryQuery,
-    PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions, SelectNextMatch, SelectPrevMatch,
-    ToggleCaseSensitive, ToggleReplace, ToggleWholeWord,
+    history::SearchHistory, mode::SearchMode, ActivateRegexMode, ActivateTextMode, CycleMode,
+    NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions,
+    SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleReplace, ToggleWholeWord,
 };
-use anyhow::{Context, Result};
+use anyhow::{Context as _, Result};
 use collections::HashMap;
 use editor::{
-    items::active_match_index, scroll::autoscroll::Autoscroll, Anchor, Editor, MultiBuffer,
-    SelectAll, MAX_TAB_TITLE_LEN,
+    items::active_match_index, scroll::autoscroll::Autoscroll, Anchor, Editor, EditorEvent,
+    MultiBuffer, SelectAll, MAX_TAB_TITLE_LEN,
 };
-use futures::StreamExt;
 use gpui::{
-    actions,
-    elements::*,
-    platform::{MouseButton, PromptLevel},
-    Action, AnyElement, AnyViewHandle, AppContext, Entity, ModelContext, ModelHandle, Subscription,
-    Task, View, ViewContext, ViewHandle, WeakModelHandle, WeakViewHandle,
+    actions, div, white, AnyElement, AnyView, AppContext, Context as _, Div, Element, EntityId,
+    EventEmitter, FocusableView, InteractiveElement, IntoElement, KeyContext, Model, ModelContext,
+    ParentElement, PromptLevel, Render, SharedString, Styled, Subscription, Task, View,
+    ViewContext, VisualContext, WeakModel, WeakView, WindowContext,
 };
 use menu::Confirm;
 use project::{
@@ -26,96 +21,55 @@ use project::{
     Entry, Project,
 };
 use semantic_index::{SemanticIndex, SemanticIndexStatus};
-use smallvec::SmallVec;
+
+use smol::stream::StreamExt;
 use std::{
     any::{Any, TypeId},
-    borrow::Cow,
     collections::HashSet,
     mem,
     ops::{Not, Range},
     path::PathBuf,
-    sync::Arc,
-    time::{Duration, Instant},
+    time::Duration,
+};
+
+use ui::{
+    h_stack, v_stack, Button, ButtonCommon, Clickable, Disableable, Icon, IconButton, IconElement,
+    Label, LabelCommon, LabelSize, Selectable, Tooltip,
 };
 use util::{paths::PathMatcher, ResultExt as _};
 use workspace::{
     item::{BreadcrumbText, Item, ItemEvent, ItemHandle},
     searchable::{Direction, SearchableItem, SearchableItemHandle},
-    ItemNavHistory, Pane, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId,
+    ItemNavHistory, Pane, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
+    WorkspaceId,
 };
 
 actions!(
     project_search,
-    [SearchInNew, ToggleFocus, NextField, ToggleFilters,]
+    [SearchInNew, ToggleFocus, NextField, ToggleFilters]
 );
 
 #[derive(Default)]
-struct ActiveSearches(HashMap<WeakModelHandle<Project>, WeakViewHandle<ProjectSearchView>>);
+struct ActiveSearches(HashMap<WeakModel<Project>, WeakView<ProjectSearchView>>);
 
 #[derive(Default)]
-struct ActiveSettings(HashMap<WeakModelHandle<Project>, ProjectSearchSettings>);
+struct ActiveSettings(HashMap<WeakModel<Project>, ProjectSearchSettings>);
 
 pub fn init(cx: &mut AppContext) {
+    // todo!() po
     cx.set_global(ActiveSearches::default());
     cx.set_global(ActiveSettings::default());
-    cx.add_action(ProjectSearchView::deploy);
-    cx.add_action(ProjectSearchView::move_focus_to_results);
-    cx.add_action(ProjectSearchBar::confirm);
-    cx.add_action(ProjectSearchBar::search_in_new);
-    cx.add_action(ProjectSearchBar::select_next_match);
-    cx.add_action(ProjectSearchBar::select_prev_match);
-    cx.add_action(ProjectSearchBar::replace_next);
-    cx.add_action(ProjectSearchBar::replace_all);
-    cx.add_action(ProjectSearchBar::cycle_mode);
-    cx.add_action(ProjectSearchBar::next_history_query);
-    cx.add_action(ProjectSearchBar::previous_history_query);
-    cx.add_action(ProjectSearchBar::activate_regex_mode);
-    cx.add_action(ProjectSearchBar::toggle_replace);
-    cx.add_action(ProjectSearchBar::toggle_replace_on_a_pane);
-    cx.add_action(ProjectSearchBar::activate_text_mode);
-
-    // This action should only be registered if the semantic index is enabled
-    // We are registering it all the time, as I dont want to introduce a dependency
-    // for Semantic Index Settings globally whenever search is tested.
-    cx.add_action(ProjectSearchBar::activate_semantic_mode);
-
-    cx.capture_action(ProjectSearchBar::tab);
-    cx.capture_action(ProjectSearchBar::tab_previous);
-    cx.capture_action(ProjectSearchView::replace_all);
-    cx.capture_action(ProjectSearchView::replace_next);
-    add_toggle_option_action::<ToggleCaseSensitive>(SearchOptions::CASE_SENSITIVE, cx);
-    add_toggle_option_action::<ToggleWholeWord>(SearchOptions::WHOLE_WORD, cx);
-    add_toggle_option_action::<ToggleIncludeIgnored>(SearchOptions::INCLUDE_IGNORED, cx);
-    add_toggle_filters_action::<ToggleFilters>(cx);
-}
-
-fn add_toggle_filters_action<A: Action>(cx: &mut AppContext) {
-    cx.add_action(move |pane: &mut Pane, _: &A, cx: &mut ViewContext<Pane>| {
-        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<ProjectSearchBar>() {
-            if search_bar.update(cx, |search_bar, cx| search_bar.toggle_filters(cx)) {
-                return;
-            }
-        }
-        cx.propagate_action();
-    });
-}
-
-fn add_toggle_option_action<A: Action>(option: SearchOptions, cx: &mut AppContext) {
-    cx.add_action(move |pane: &mut Pane, _: &A, cx: &mut ViewContext<Pane>| {
-        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<ProjectSearchBar>() {
-            if search_bar.update(cx, |search_bar, cx| {
-                search_bar.toggle_search_option(option, cx)
-            }) {
-                return;
-            }
-        }
-        cx.propagate_action();
-    });
+    cx.observe_new_views(|workspace: &mut Workspace, _cx| {
+        workspace
+            .register_action(ProjectSearchView::deploy)
+            .register_action(ProjectSearchBar::search_in_new);
+    })
+    .detach();
 }
 
 struct ProjectSearch {
-    project: ModelHandle<Project>,
-    excerpts: ModelHandle<MultiBuffer>,
+    project: Model<Project>,
+    excerpts: Model<MultiBuffer>,
     pending_search: Option<Task<Option<()>>>,
     match_ranges: Vec<Range<Anchor>>,
     active_query: Option<SearchQuery>,
@@ -132,10 +86,10 @@ enum InputPanel {
 }
 
 pub struct ProjectSearchView {
-    model: ModelHandle<ProjectSearch>,
-    query_editor: ViewHandle<Editor>,
-    replacement_editor: ViewHandle<Editor>,
-    results_editor: ViewHandle<Editor>,
+    model: Model<ProjectSearch>,
+    query_editor: View<Editor>,
+    replacement_editor: View<Editor>,
+    results_editor: View<Editor>,
     semantic_state: Option<SemanticState>,
     semantic_permissioned: Option<bool>,
     search_options: SearchOptions,
@@ -143,8 +97,8 @@ pub struct ProjectSearchView {
     active_match_index: Option<usize>,
     search_id: usize,
     query_editor_was_focused: bool,
-    included_files_editor: ViewHandle<Editor>,
-    excluded_files_editor: ViewHandle<Editor>,
+    included_files_editor: View<Editor>,
+    excluded_files_editor: View<Editor>,
     filters_enabled: bool,
     replace_enabled: bool,
     current_mode: SearchMode,
@@ -164,20 +118,16 @@ struct ProjectSearchSettings {
 }
 
 pub struct ProjectSearchBar {
-    active_project_search: Option<ViewHandle<ProjectSearchView>>,
+    active_project_search: Option<View<ProjectSearchView>>,
     subscription: Option<Subscription>,
 }
 
-impl Entity for ProjectSearch {
-    type Event = ();
-}
-
 impl ProjectSearch {
-    fn new(project: ModelHandle<Project>, cx: &mut ModelContext<Self>) -> Self {
+    fn new(project: Model<Project>, cx: &mut ModelContext<Self>) -> Self {
         let replica_id = project.read(cx).replica_id();
         Self {
             project,
-            excerpts: cx.add_model(|_| MultiBuffer::new(replica_id)),
+            excerpts: cx.build_model(|_| MultiBuffer::new(replica_id)),
             pending_search: Default::default(),
             match_ranges: Default::default(),
             active_query: None,
@@ -187,12 +137,12 @@ impl ProjectSearch {
         }
     }
 
-    fn clone(&self, cx: &mut ModelContext<Self>) -> ModelHandle<Self> {
-        cx.add_model(|cx| Self {
+    fn clone(&self, cx: &mut ModelContext<Self>) -> Model<Self> {
+        cx.build_model(|cx| Self {
             project: self.project.clone(),
             excerpts: self
                 .excerpts
-                .update(cx, |excerpts, cx| cx.add_model(|cx| excerpts.clone(cx))),
+                .update(cx, |excerpts, cx| cx.build_model(|cx| excerpts.clone(cx))),
             pending_search: Default::default(),
             match_ranges: self.match_ranges.clone(),
             active_query: self.active_query.clone(),
@@ -210,33 +160,38 @@ impl ProjectSearch {
         self.search_history.add(query.as_str().to_string());
         self.active_query = Some(query);
         self.match_ranges.clear();
-        self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move {
+        self.pending_search = Some(cx.spawn(|this, mut cx| async move {
             let mut matches = search;
-            let this = this.upgrade(&cx)?;
+            let this = this.upgrade()?;
             this.update(&mut cx, |this, cx| {
                 this.match_ranges.clear();
                 this.excerpts.update(cx, |this, cx| this.clear(cx));
                 this.no_results = Some(true);
-            });
+            })
+            .ok()?;
 
             while let Some((buffer, anchors)) = matches.next().await {
-                let mut ranges = this.update(&mut cx, |this, cx| {
-                    this.no_results = Some(false);
-                    this.excerpts.update(cx, |excerpts, cx| {
-                        excerpts.stream_excerpts_with_context_lines(buffer, anchors, 1, cx)
+                let mut ranges = this
+                    .update(&mut cx, |this, cx| {
+                        this.no_results = Some(false);
+                        this.excerpts.update(cx, |excerpts, cx| {
+                            excerpts.stream_excerpts_with_context_lines(buffer, anchors, 1, cx)
+                        })
                     })
-                });
+                    .ok()?;
 
                 while let Some(range) = ranges.next().await {
-                    this.update(&mut cx, |this, _| this.match_ranges.push(range));
+                    this.update(&mut cx, |this, _| this.match_ranges.push(range))
+                        .ok()?;
                 }
-                this.update(&mut cx, |_, cx| cx.notify());
+                this.update(&mut cx, |_, cx| cx.notify()).ok()?;
             }
 
             this.update(&mut cx, |this, cx| {
                 this.pending_search.take();
                 cx.notify();
-            });
+            })
+            .ok()?;
 
             None
         }));
@@ -271,14 +226,17 @@ impl ProjectSearch {
                 this.excerpts.update(cx, |excerpts, cx| {
                     excerpts.clear(cx);
                 });
-            });
+            })
+            .ok()?;
             for (buffer, ranges) in matches {
-                let mut match_ranges = this.update(&mut cx, |this, cx| {
-                    this.no_results = Some(false);
-                    this.excerpts.update(cx, |excerpts, cx| {
-                        excerpts.stream_excerpts_with_context_lines(buffer, ranges, 3, cx)
+                let mut match_ranges = this
+                    .update(&mut cx, |this, cx| {
+                        this.no_results = Some(false);
+                        this.excerpts.update(cx, |excerpts, cx| {
+                            excerpts.stream_excerpts_with_context_lines(buffer, ranges, 3, cx)
+                        })
                     })
-                });
+                    .ok()?;
                 while let Some(match_range) = match_ranges.next().await {
                     this.update(&mut cx, |this, cx| {
                         this.match_ranges.push(match_range);
@@ -286,14 +244,16 @@ impl ProjectSearch {
                             this.match_ranges.push(match_range);
                         }
                         cx.notify();
-                    });
+                    })
+                    .ok()?;
                 }
             }
 
             this.update(&mut cx, |this, cx| {
                 this.pending_search.take();
                 cx.notify();
-            });
+            })
+            .ok()?;
 
             None
         }));
@@ -305,221 +265,223 @@ impl ProjectSearch {
 pub enum ViewEvent {
     UpdateTab,
     Activate,
-    EditorEvent(editor::Event),
+    EditorEvent(editor::EditorEvent),
     Dismiss,
 }
 
-impl Entity for ProjectSearchView {
-    type Event = ViewEvent;
-}
-
-impl View for ProjectSearchView {
-    fn ui_name() -> &'static str {
-        "ProjectSearchView"
-    }
-
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let model = &self.model.read(cx);
-        if model.match_ranges.is_empty() {
-            enum Status {}
-
-            let theme = theme::current(cx).clone();
-
-            // If Search is Active -> Major: Searching..., Minor: None
-            // If Semantic -> Major: "Search using Natural Language", Minor: {Status}/n{ex...}/n{ex...}
-            // If Regex -> Major: "Search using Regex", Minor: {ex...}
-            // If Text -> Major: "Text search all files and folders", Minor: {...}
-
-            let current_mode = self.current_mode;
-            let mut major_text = if model.pending_search.is_some() {
-                Cow::Borrowed("Searching...")
-            } else if model.no_results.is_some_and(|v| v) {
-                Cow::Borrowed("No Results")
-            } else {
-                match current_mode {
-                    SearchMode::Text => Cow::Borrowed("Text search all files and folders"),
-                    SearchMode::Semantic => {
-                        Cow::Borrowed("Search all code objects using Natural Language")
-                    }
-                    SearchMode::Regex => Cow::Borrowed("Regex search all files and folders"),
-                }
-            };
-
-            let mut show_minor_text = true;
-            let semantic_status = self.semantic_state.as_ref().and_then(|semantic| {
-                let status = semantic.index_status;
-                match status {
-                    SemanticIndexStatus::NotAuthenticated => {
-                        major_text = Cow::Borrowed("Not Authenticated");
-                        show_minor_text = false;
-                        Some(vec![
-                            "API Key Missing: Please set 'OPENAI_API_KEY' in Environment Variables."
-                                .to_string(), "If you authenticated using the Assistant Panel, please restart Zed to Authenticate.".to_string()])
-                    }
-                    SemanticIndexStatus::Indexed => Some(vec!["Indexing complete".to_string()]),
-                    SemanticIndexStatus::Indexing {
-                        remaining_files,
-                        rate_limit_expiry,
-                    } => {
-                        if remaining_files == 0 {
-                            Some(vec![format!("Indexing...")])
-                        } else {
-                            if let Some(rate_limit_expiry) = rate_limit_expiry {
-                                let remaining_seconds =
-                                    rate_limit_expiry.duration_since(Instant::now());
-                                if remaining_seconds > Duration::from_secs(0) {
-                                    Some(vec![format!(
-                                        "Remaining files to index (rate limit resets in {}s): {}",
-                                        remaining_seconds.as_secs(),
-                                        remaining_files
-                                    )])
-                                } else {
-                                    Some(vec![format!("Remaining files to index: {}", remaining_files)])
-                                }
-                            } else {
-                                Some(vec![format!("Remaining files to index: {}", remaining_files)])
-                            }
-                        }
-                    }
-                    SemanticIndexStatus::NotIndexed => None,
-                }
-            });
+impl EventEmitter<ViewEvent> for ProjectSearchView {}
 
-            let minor_text = if let Some(no_results) = model.no_results {
-                if model.pending_search.is_none() && no_results {
-                    vec!["No results found in this project for the provided query".to_owned()]
-                } else {
-                    vec![]
-                }
+impl Render for ProjectSearchView {
+    type Element = Div;
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+        if self.has_matches() {
+            div()
+                .flex_1()
+                .size_full()
+                .child(self.results_editor.clone())
+        } else {
+            let model = self.model.read(cx);
+            let has_no_results = model.no_results.unwrap_or(false);
+            let is_search_underway = model.pending_search.is_some();
+            let major_text = if is_search_underway {
+                Label::new("Searching...")
+            } else if has_no_results {
+                Label::new("No results for a given query")
             } else {
-                match current_mode {
-                    SearchMode::Semantic => {
-                        let mut minor_text: Vec<String> = Vec::new();
-                        minor_text.push("".into());
-                        if let Some(semantic_status) = semantic_status {
-                            minor_text.extend(semantic_status);
-                        }
-                        if show_minor_text {
-                            minor_text
-                                .push("Simply explain the code you are looking to find.".into());
-                            minor_text.push(
-                                "ex. 'prompt user for permissions to index their project'".into(),
-                            );
-                        }
-                        minor_text
-                    }
-                    _ => vec![
-                        "".to_owned(),
-                        "Include/exclude specific paths with the filter option.".to_owned(),
-                        "Matching exact word and/or casing is available too.".to_owned(),
-                    ],
-                }
+                Label::new(format!("{} search all files", self.current_mode.label()))
             };
-
-            let previous_query_keystrokes =
-                cx.binding_for_action(&PreviousHistoryQuery {})
-                    .map(|binding| {
-                        binding
-                            .keystrokes()
-                            .iter()
-                            .map(|k| k.to_string())
-                            .collect::<Vec<_>>()
-                    });
-            let next_query_keystrokes =
-                cx.binding_for_action(&NextHistoryQuery {}).map(|binding| {
-                    binding
-                        .keystrokes()
-                        .iter()
-                        .map(|k| k.to_string())
-                        .collect::<Vec<_>>()
-                });
-            let new_placeholder_text = match (previous_query_keystrokes, next_query_keystrokes) {
-                (Some(previous_query_keystrokes), Some(next_query_keystrokes)) => {
-                    format!(
-                        "Search ({}/{} for previous/next query)",
-                        previous_query_keystrokes.join(" "),
-                        next_query_keystrokes.join(" ")
-                    )
-                }
-                (None, Some(next_query_keystrokes)) => {
-                    format!(
-                        "Search ({} for next query)",
-                        next_query_keystrokes.join(" ")
-                    )
-                }
-                (Some(previous_query_keystrokes), None) => {
-                    format!(
-                        "Search ({} for previous query)",
-                        previous_query_keystrokes.join(" ")
-                    )
-                }
-                (None, None) => String::new(),
-            };
-            self.query_editor.update(cx, |editor, cx| {
-                editor.set_placeholder_text(new_placeholder_text, cx);
-            });
-
-            MouseEventHandler::new::<Status, _>(0, cx, |_, _| {
-                Flex::column()
-                    .with_child(Flex::column().contained().flex(1., true))
-                    .with_child(
-                        Flex::column()
-                            .align_children_center()
-                            .with_child(Label::new(
-                                major_text,
-                                theme.search.major_results_status.clone(),
-                            ))
-                            .with_children(
-                                minor_text.into_iter().map(|x| {
-                                    Label::new(x, theme.search.minor_results_status.clone())
-                                }),
-                            )
-                            .aligned()
-                            .top()
-                            .contained()
-                            .flex(7., true),
-                    )
-                    .contained()
-                    .with_background_color(theme.editor.background)
-            })
-            .on_down(MouseButton::Left, |_, _, cx| {
-                cx.focus_parent();
-            })
-            .into_any_named("project search view")
-        } else {
-            ChildView::new(&self.results_editor, cx)
-                .flex(1., true)
-                .into_any_named("project search view")
+            let major_text = div().justify_center().max_w_96().child(major_text);
+            let middle_text = div()
+                .items_center()
+                .max_w_96()
+                .child(Label::new(self.landing_text_minor()).size(LabelSize::Small));
+            v_stack().flex_1().size_full().justify_center().child(
+                h_stack()
+                    .size_full()
+                    .justify_center()
+                    .child(h_stack().flex_1())
+                    .child(v_stack().child(major_text).child(middle_text))
+                    .child(h_stack().flex_1()),
+            )
         }
     }
+}
 
-    fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
-        let handle = cx.weak_handle();
-        cx.update_global(|state: &mut ActiveSearches, cx| {
-            state
-                .0
-                .insert(self.model.read(cx).project.downgrade(), handle)
-        });
-
-        cx.update_global(|state: &mut ActiveSettings, cx| {
-            state.0.insert(
-                self.model.read(cx).project.downgrade(),
-                self.current_settings(),
-            );
-        });
-
-        if cx.is_self_focused() {
-            if self.query_editor_was_focused {
-                cx.focus(&self.query_editor);
-            } else {
-                cx.focus(&self.results_editor);
-            }
-        }
+// impl Entity for ProjectSearchView {
+//     type Event = ViewEvent;
+// }
+
+// impl View for ProjectSearchView {
+//     fn ui_name() -> &'static str {
+//         "ProjectSearchView"
+//     }
+
+//     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+//         let model = &self.model.read(cx);
+//         if model.match_ranges.is_empty() {
+//             enum Status {}
+
+//             let theme = theme::current(cx).clone();
+
+//             // If Search is Active -> Major: Searching..., Minor: None
+//             // If Semantic -> Major: "Search using Natural Language", Minor: {Status}/n{ex...}/n{ex...}
+//             // If Regex -> Major: "Search using Regex", Minor: {ex...}
+//             // If Text -> Major: "Text search all files and folders", Minor: {...}
+
+//             let current_mode = self.current_mode;
+//             let mut major_text = if model.pending_search.is_some() {
+//                 Cow::Borrowed("Searching...")
+//             } else if model.no_results.is_some_and(|v| v) {
+//                 Cow::Borrowed("No Results")
+//             } else {
+//                 match current_mode {
+//                     SearchMode::Text => Cow::Borrowed("Text search all files and folders"),
+//                     SearchMode::Semantic => {
+//                         Cow::Borrowed("Search all code objects using Natural Language")
+//                     }
+//                     SearchMode::Regex => Cow::Borrowed("Regex search all files and folders"),
+//                 }
+//             };
+
+//             let mut show_minor_text = true;
+//             let semantic_status = self.semantic_state.as_ref().and_then(|semantic| {
+//                 let status = semantic.index_status;
+//                 match status {
+//                     SemanticIndexStatus::NotAuthenticated => {
+//                         major_text = Cow::Borrowed("Not Authenticated");
+//                         show_minor_text = false;
+//                         Some(vec![
+//                             "API Key Missing: Please set 'OPENAI_API_KEY' in Environment Variables."
+//                                 .to_string(), "If you authenticated using the Assistant Panel, please restart Zed to Authenticate.".to_string()])
+//                     }
+//                     SemanticIndexStatus::Indexed => Some(vec!["Indexing complete".to_string()]),
+//                     SemanticIndexStatus::Indexing {
+//                         remaining_files,
+//                         rate_limit_expiry,
+//                     } => {
+//                         if remaining_files == 0 {
+//                             Some(vec![format!("Indexing...")])
+//                         } else {
+//                             if let Some(rate_limit_expiry) = rate_limit_expiry {
+//                                 let remaining_seconds =
+//                                     rate_limit_expiry.duration_since(Instant::now());
+//                                 if remaining_seconds > Duration::from_secs(0) {
+//                                     Some(vec![format!(
+//                                         "Remaining files to index (rate limit resets in {}s): {}",
+//                                         remaining_seconds.as_secs(),
+//                                         remaining_files
+//                                     )])
+//                                 } else {
+//                                     Some(vec![format!("Remaining files to index: {}", remaining_files)])
+//                                 }
+//                             } else {
+//                                 Some(vec![format!("Remaining files to index: {}", remaining_files)])
+//                             }
+//                         }
+//                     }
+//                     SemanticIndexStatus::NotIndexed => None,
+//                 }
+//             });
+
+//             let minor_text = if let Some(no_results) = model.no_results {
+//                 if model.pending_search.is_none() && no_results {
+//                     vec!["No results found in this project for the provided query".to_owned()]
+//                 } else {
+//                     vec![]
+//                 }
+//             } else {
+//                 match current_mode {
+//                     SearchMode::Semantic => {
+//                         let mut minor_text: Vec<String> = Vec::new();
+//                         minor_text.push("".into());
+//                         if let Some(semantic_status) = semantic_status {
+//                             minor_text.extend(semantic_status);
+//                         }
+//                         if show_minor_text {
+//                             minor_text
+//                                 .push("Simply explain the code you are looking to find.".into());
+//                             minor_text.push(
+//                                 "ex. 'prompt user for permissions to index their project'".into(),
+//                             );
+//                         }
+//                         minor_text
+//                     }
+//                     _ => vec![
+//                         "".to_owned(),
+//                         "Include/exclude specific paths with the filter option.".to_owned(),
+//                         "Matching exact word and/or casing is available too.".to_owned(),
+//                     ],
+//                 }
+//             };
+
+//             MouseEventHandler::new::<Status, _>(0, cx, |_, _| {
+//                 Flex::column()
+//                     .with_child(Flex::column().contained().flex(1., true))
+//                     .with_child(
+//                         Flex::column()
+//                             .align_children_center()
+//                             .with_child(Label::new(
+//                                 major_text,
+//                                 theme.search.major_results_status.clone(),
+//                             ))
+//                             .with_children(
+//                                 minor_text.into_iter().map(|x| {
+//                                     Label::new(x, theme.search.minor_results_status.clone())
+//                                 }),
+//                             )
+//                             .aligned()
+//                             .top()
+//                             .contained()
+//                             .flex(7., true),
+//                     )
+//                     .contained()
+//                     .with_background_color(theme.editor.background)
+//             })
+//             .on_down(MouseButton::Left, |_, _, cx| {
+//                 cx.focus_parent();
+//             })
+//             .into_any_named("project search view")
+//         } else {
+//             ChildView::new(&self.results_editor, cx)
+//                 .flex(1., true)
+//                 .into_any_named("project search view")
+//         }
+//     }
+
+//     fn focus_in(&mut self, _: AnyView, cx: &mut ViewContext<Self>) {
+//         let handle = cx.weak_handle();
+//         cx.update_global(|state: &mut ActiveSearches, cx| {
+//             state
+//                 .0
+//                 .insert(self.model.read(cx).project.downgrade(), handle)
+//         });
+
+//         cx.update_global(|state: &mut ActiveSettings, cx| {
+//             state.0.insert(
+//                 self.model.read(cx).project.downgrade(),
+//                 self.current_settings(),
+//             );
+//         });
+
+//         if cx.is_self_focused() {
+//             if self.query_editor_was_focused {
+//                 cx.focus(&self.query_editor);
+//             } else {
+//                 cx.focus(&self.results_editor);
+//             }
+//         }
+//     }
+// }
+
+impl FocusableView for ProjectSearchView {
+    fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
+        self.results_editor.focus_handle(cx)
     }
 }
 
 impl Item for ProjectSearchView {
-    fn tab_tooltip_text(&self, cx: &AppContext) -> Option<Cow<str>> {
+    type Event = ViewEvent;
+    fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString> {
         let query_text = self.query_editor.read(cx).text(cx);
 
         query_text
@@ -528,20 +490,17 @@ impl Item for ProjectSearchView {
             .then(|| query_text.into())
             .or_else(|| Some("Project Search".into()))
     }
-    fn should_close_item_on_event(event: &Self::Event) -> bool {
-        event == &Self::Event::Dismiss
-    }
 
     fn act_as_type<'a>(
         &'a self,
         type_id: TypeId,
-        self_handle: &'a ViewHandle<Self>,
+        self_handle: &'a View<Self>,
         _: &'a AppContext,
-    ) -> Option<&'a AnyViewHandle> {
+    ) -> Option<AnyView> {
         if type_id == TypeId::of::<Self>() {
-            Some(self_handle)
+            Some(self_handle.clone().into())
         } else if type_id == TypeId::of::<Editor>() {
-            Some(&self.results_editor)
+            Some(self.results_editor.clone().into())
         } else {
             None
         }
@@ -552,45 +511,31 @@ impl Item for ProjectSearchView {
             .update(cx, |editor, cx| editor.deactivated(cx));
     }
 
-    fn tab_content<T: 'static>(
-        &self,
-        _detail: Option<usize>,
-        tab_theme: &theme::Tab,
-        cx: &AppContext,
-    ) -> AnyElement<T> {
-        Flex::row()
-            .with_child(
-                Svg::new("icons/magnifying_glass.svg")
-                    .with_color(tab_theme.label.text.color)
-                    .constrained()
-                    .with_width(tab_theme.type_icon_width)
-                    .aligned()
-                    .contained()
-                    .with_margin_right(tab_theme.spacing),
-            )
-            .with_child({
-                let tab_name: Option<Cow<_>> = self
-                    .model
-                    .read(cx)
-                    .search_history
-                    .current()
-                    .as_ref()
-                    .map(|query| {
-                        let query_text = util::truncate_and_trailoff(query, MAX_TAB_TITLE_LEN);
-                        query_text.into()
-                    });
-                Label::new(
-                    tab_name
-                        .filter(|name| !name.is_empty())
-                        .unwrap_or("Project search".into()),
-                    tab_theme.label.clone(),
-                )
-                .aligned()
-            })
+    fn tab_content(&self, _: Option<usize>, cx: &WindowContext<'_>) -> AnyElement {
+        let last_query: Option<SharedString> = self
+            .model
+            .read(cx)
+            .search_history
+            .current()
+            .as_ref()
+            .map(|query| {
+                let query_text = util::truncate_and_trailoff(query, MAX_TAB_TITLE_LEN);
+                query_text.into()
+            });
+        let tab_name = last_query
+            .filter(|query| !query.is_empty())
+            .unwrap_or_else(|| "Project search".into());
+        h_stack()
+            .child(IconElement::new(Icon::MagnifyingGlass))
+            .child(Label::new(tab_name))
             .into_any()
     }
 
-    fn for_each_project_item(&self, cx: &AppContext, f: &mut dyn FnMut(usize, &dyn project::Item)) {
+    fn for_each_project_item(
+        &self,
+        cx: &AppContext,
+        f: &mut dyn FnMut(EntityId, &dyn project::Item),
+    ) {
         self.results_editor.for_each_project_item(cx, f)
     }
 
@@ -612,7 +557,7 @@ impl Item for ProjectSearchView {
 
     fn save(
         &mut self,
-        project: ModelHandle<Project>,
+        project: Model<Project>,
         cx: &mut ViewContext<Self>,
     ) -> Task<anyhow::Result<()>> {
         self.results_editor
@@ -621,7 +566,7 @@ impl Item for ProjectSearchView {
 
     fn save_as(
         &mut self,
-        _: ModelHandle<Project>,
+        _: Model<Project>,
         _: PathBuf,
         _: &mut ViewContext<Self>,
     ) -> Task<anyhow::Result<()>> {
@@ -630,19 +575,23 @@ impl Item for ProjectSearchView {
 
     fn reload(
         &mut self,
-        project: ModelHandle<Project>,
+        project: Model<Project>,
         cx: &mut ViewContext<Self>,
     ) -> Task<anyhow::Result<()>> {
         self.results_editor
             .update(cx, |editor, cx| editor.reload(project, cx))
     }
 
-    fn clone_on_split(&self, _workspace_id: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<Self>
+    fn clone_on_split(
+        &self,
+        _workspace_id: WorkspaceId,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<View<Self>>
     where
         Self: Sized,
     {
         let model = self.model.update(cx, |model, cx| model.clone(cx));
-        Some(Self::new(model, cx, None))
+        Some(cx.build_view(|cx| Self::new(model, cx, None)))
     }
 
     fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
@@ -661,14 +610,17 @@ impl Item for ProjectSearchView {
             .update(cx, |editor, cx| editor.navigate(data, cx))
     }
 
-    fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
+    fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {
         match event {
             ViewEvent::UpdateTab => {
-                smallvec::smallvec![ItemEvent::UpdateBreadcrumbs, ItemEvent::UpdateTab]
+                f(ItemEvent::UpdateBreadcrumbs);
+                f(ItemEvent::UpdateTab);
+            }
+            ViewEvent::EditorEvent(editor_event) => {
+                Editor::to_item_events(editor_event, f);
             }
-            ViewEvent::EditorEvent(editor_event) => Editor::to_item_events(editor_event),
-            ViewEvent::Dismiss => smallvec::smallvec![ItemEvent::CloseItem],
-            _ => SmallVec::new(),
+            ViewEvent::Dismiss => f(ItemEvent::CloseItem),
+            _ => {}
         }
     }
 
@@ -689,12 +641,12 @@ impl Item for ProjectSearchView {
     }
 
     fn deserialize(
-        _project: ModelHandle<Project>,
-        _workspace: WeakViewHandle<Workspace>,
+        _project: Model<Project>,
+        _workspace: WeakView<Workspace>,
         _workspace_id: workspace::WorkspaceId,
         _item_id: workspace::ItemId,
         _cx: &mut ViewContext<Pane>,
-    ) -> Task<anyhow::Result<ViewHandle<Self>>> {
+    ) -> Task<anyhow::Result<View<Self>>> {
         unimplemented!()
     }
 }
@@ -751,7 +703,7 @@ impl ProjectSearchView {
 
     fn semantic_index_changed(
         &mut self,
-        semantic_index: ModelHandle<SemanticIndex>,
+        semantic_index: Model<SemanticIndex>,
         cx: &mut ViewContext<Self>,
     ) {
         let project = self.model.read(cx).project.clone();
@@ -767,7 +719,7 @@ impl ProjectSearchView {
                     semantic_state.maintain_rate_limit =
                         Some(cx.spawn(|this, mut cx| async move {
                             loop {
-                                cx.background().timer(Duration::from_secs(1)).await;
+                                cx.background_executor().timer(Duration::from_secs(1)).await;
                                 this.update(&mut cx, |_, cx| cx.notify()).log_err();
                             }
                         }));
@@ -809,7 +761,7 @@ impl ProjectSearchView {
                     let has_permission = has_permission.await?;
 
                     if !has_permission {
-                        let mut answer = this.update(&mut cx, |this, cx| {
+                        let answer = this.update(&mut cx, |this, cx| {
                             let project = this.model.read(cx).project.clone();
                             let project_name = project
                                 .read(cx)
@@ -829,7 +781,7 @@ impl ProjectSearchView {
                             )
                         })?;
 
-                        if answer.next().await == Some(0) {
+                        if answer.await? == 0 {
                             this.update(&mut cx, |this, _| {
                                 this.semantic_permissioned = Some(true);
                             })?;
@@ -907,7 +859,7 @@ impl ProjectSearchView {
     }
 
     fn new(
-        model: ModelHandle<ProjectSearch>,
+        model: Model<ProjectSearch>,
         cx: &mut ViewContext<Self>,
         settings: Option<ProjectSearchSettings>,
     ) -> Self {
@@ -940,32 +892,26 @@ impl ProjectSearchView {
         cx.observe(&model, |this, _, cx| this.model_changed(cx))
             .detach();
 
-        let query_editor = cx.add_view(|cx| {
-            let mut editor = Editor::single_line(
-                Some(Arc::new(|theme| theme.search.editor.input.clone())),
-                cx,
-            );
+        let query_editor = cx.build_view(|cx| {
+            let mut editor = Editor::single_line(cx);
             editor.set_placeholder_text("Text search all files", cx);
             editor.set_text(query_text, cx);
             editor
         });
         // Subscribe to query_editor in order to reraise editor events for workspace item activation purposes
-        cx.subscribe(&query_editor, |_, _, event, cx| {
+        cx.subscribe(&query_editor, |_, _, event: &EditorEvent, cx| {
             cx.emit(ViewEvent::EditorEvent(event.clone()))
         })
         .detach();
-        let replacement_editor = cx.add_view(|cx| {
-            let mut editor = Editor::single_line(
-                Some(Arc::new(|theme| theme.search.editor.input.clone())),
-                cx,
-            );
+        let replacement_editor = cx.build_view(|cx| {
+            let mut editor = Editor::single_line(cx);
             editor.set_placeholder_text("Replace in project..", cx);
             if let Some(text) = replacement_text {
                 editor.set_text(text, cx);
             }
             editor
         });
-        let results_editor = cx.add_view(|cx| {
+        let results_editor = cx.build_view(|cx| {
             let mut editor = Editor::for_multibuffer(excerpts, Some(project.clone()), cx);
             editor.set_searchable(false);
             editor
@@ -973,8 +919,8 @@ impl ProjectSearchView {
         cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab))
             .detach();
 
-        cx.subscribe(&results_editor, |this, _, event, cx| {
-            if matches!(event, editor::Event::SelectionsChanged { .. }) {
+        cx.subscribe(&results_editor, |this, _, event: &EditorEvent, cx| {
+            if matches!(event, editor::EditorEvent::SelectionsChanged { .. }) {
                 this.update_match_index(cx);
             }
             // Reraise editor events for workspace item activation purposes

crates/search2/src/search.rs 🔗

@@ -13,12 +13,12 @@ use ui::{ButtonStyle, Icon, IconButton};
 pub mod buffer_search;
 mod history;
 mod mode;
-//pub mod project_search;
+pub mod project_search;
 pub(crate) mod search_bar;
 
 pub fn init(cx: &mut AppContext) {
     buffer_search::init(cx);
-    //project_search::init(cx);
+    project_search::init(cx);
 }
 
 actions!(
@@ -47,6 +47,7 @@ bitflags! {
         const NONE = 0b000;
         const WHOLE_WORD = 0b001;
         const CASE_SENSITIVE = 0b010;
+        const INCLUDE_IGNORED = 0b100;
     }
 }
 

crates/ui2/src/components/icon.rs 🔗

@@ -62,6 +62,7 @@ pub enum Icon {
     FileRust,
     FileToml,
     FileTree,
+    Filter,
     Folder,
     FolderOpen,
     FolderX,
@@ -140,6 +141,7 @@ impl Icon {
             Icon::FileRust => "icons/file_icons/rust.svg",
             Icon::FileToml => "icons/file_icons/toml.svg",
             Icon::FileTree => "icons/project.svg",
+            Icon::Filter => "icons/filter.svg",
             Icon::Folder => "icons/file_icons/folder.svg",
             Icon::FolderOpen => "icons/file_icons/folder_open.svg",
             Icon::FolderX => "icons/stop_sharing.svg",

crates/zed2/src/zed2.rs 🔗

@@ -24,6 +24,7 @@ use anyhow::{anyhow, Context as _};
 use futures::{channel::mpsc, StreamExt};
 use project_panel::ProjectPanel;
 use quick_action_bar::QuickActionBar;
+use search::project_search::ProjectSearchBar;
 use settings::{initial_local_settings_content, load_default_keymap, KeymapFile, Settings};
 use std::{borrow::Cow, ops::Deref, sync::Arc};
 use terminal_view::terminal_panel::TerminalPanel;
@@ -429,8 +430,8 @@ fn initialize_pane(workspace: &mut Workspace, pane: &View<Pane>, cx: &mut ViewCo
             toolbar.add_item(quick_action_bar, cx);
             let diagnostic_editor_controls = cx.build_view(|_| diagnostics::ToolbarControls::new());
             //     toolbar.add_item(diagnostic_editor_controls, cx);
-            //     let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
-            //     toolbar.add_item(project_search_bar, cx);
+            let project_search_bar = cx.build_view(|_| ProjectSearchBar::new());
+            toolbar.add_item(project_search_bar, cx);
             //     let lsp_log_item =
             //         cx.add_view(|_| language_tools::LspLogToolbarItemView::new());
             //     toolbar.add_item(lsp_log_item, cx);