diff --git a/Cargo.lock b/Cargo.lock index 849eaab25fe8bc72d14e52fb82bd971189312444..69337d67a214733661eba5dbace9f4dc780df608 100644 --- a/Cargo.lock +++ b/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", diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 54624a694566ad5efc7fbf8444886ea48e5e15fe..caac96b24679be7493ab1a8aa2acf0acb4893249 100644 --- a/crates/editor2/src/editor.rs +++ b/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) { let buffer = self.buffer.read(cx); diff --git a/crates/search2/Cargo.toml b/crates/search2/Cargo.toml index 97cfdd6494099eb802a2fd629df1e74edada4232..34158e7416d67d368cca763931baf89412c76174 100644 --- a/crates/search2/Cargo.toml +++ b/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 diff --git a/crates/search2/src/project_search.rs b/crates/search2/src/project_search.rs index 13b53f4dc2d81bf0f4431831bb3a000ec25578a6..dd871131595e340c68edf1a5c94df24a7a73af32 100644 --- a/crates/search2/src/project_search.rs +++ b/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, WeakViewHandle>); +struct ActiveSearches(HashMap, WeakView>); #[derive(Default)] -struct ActiveSettings(HashMap, ProjectSearchSettings>); +struct ActiveSettings(HashMap, 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::(SearchOptions::CASE_SENSITIVE, cx); - add_toggle_option_action::(SearchOptions::WHOLE_WORD, cx); - add_toggle_option_action::(SearchOptions::INCLUDE_IGNORED, cx); - add_toggle_filters_action::(cx); -} - -fn add_toggle_filters_action(cx: &mut AppContext) { - cx.add_action(move |pane: &mut Pane, _: &A, cx: &mut ViewContext| { - if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { - if search_bar.update(cx, |search_bar, cx| search_bar.toggle_filters(cx)) { - return; - } - } - cx.propagate_action(); - }); -} - -fn add_toggle_option_action(option: SearchOptions, cx: &mut AppContext) { - cx.add_action(move |pane: &mut Pane, _: &A, cx: &mut ViewContext| { - if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { - 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, - excerpts: ModelHandle, + project: Model, + excerpts: Model, pending_search: Option>>, match_ranges: Vec>, active_query: Option, @@ -132,10 +86,10 @@ enum InputPanel { } pub struct ProjectSearchView { - model: ModelHandle, - query_editor: ViewHandle, - replacement_editor: ViewHandle, - results_editor: ViewHandle, + model: Model, + query_editor: View, + replacement_editor: View, + results_editor: View, semantic_state: Option, semantic_permissioned: Option, search_options: SearchOptions, @@ -143,8 +97,8 @@ pub struct ProjectSearchView { active_match_index: Option, search_id: usize, query_editor_was_focused: bool, - included_files_editor: ViewHandle, - excluded_files_editor: ViewHandle, + included_files_editor: View, + excluded_files_editor: View, filters_enabled: bool, replace_enabled: bool, current_mode: SearchMode, @@ -164,20 +118,16 @@ struct ProjectSearchSettings { } pub struct ProjectSearchBar { - active_project_search: Option>, + active_project_search: Option>, subscription: Option, } -impl Entity for ProjectSearch { - type Event = (); -} - impl ProjectSearch { - fn new(project: ModelHandle, cx: &mut ModelContext) -> Self { + fn new(project: Model, cx: &mut ModelContext) -> 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) -> ModelHandle { - cx.add_model(|cx| Self { + fn clone(&self, cx: &mut ModelContext) -> Model { + 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) -> AnyElement { - 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 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::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 = 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::>() - }); - let next_query_keystrokes = - cx.binding_for_action(&NextHistoryQuery {}).map(|binding| { - binding - .keystrokes() - .iter() - .map(|k| k.to_string()) - .collect::>() - }); - 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::(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) { - 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) -> AnyElement { +// 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 = 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::(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) { +// 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> { + type Event = ViewEvent; + fn tab_tooltip_text(&self, cx: &AppContext) -> Option { 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_handle: &'a View, _: &'a AppContext, - ) -> Option<&'a AnyViewHandle> { + ) -> Option { if type_id == TypeId::of::() { - Some(self_handle) + Some(self_handle.clone().into()) } else if type_id == TypeId::of::() { - 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( - &self, - _detail: Option, - tab_theme: &theme::Tab, - cx: &AppContext, - ) -> AnyElement { - 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> = 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, cx: &WindowContext<'_>) -> AnyElement { + let last_query: Option = 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: Model, cx: &mut ViewContext, ) -> Task> { self.results_editor @@ -621,7 +566,7 @@ impl Item for ProjectSearchView { fn save_as( &mut self, - _: ModelHandle, + _: Model, _: PathBuf, _: &mut ViewContext, ) -> Task> { @@ -630,19 +575,23 @@ impl Item for ProjectSearchView { fn reload( &mut self, - project: ModelHandle, + project: Model, cx: &mut ViewContext, ) -> Task> { self.results_editor .update(cx, |editor, cx| editor.reload(project, cx)) } - fn clone_on_split(&self, _workspace_id: WorkspaceId, cx: &mut ViewContext) -> Option + fn clone_on_split( + &self, + _workspace_id: WorkspaceId, + cx: &mut ViewContext, + ) -> Option> 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) { @@ -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, - _workspace: WeakViewHandle, + _project: Model, + _workspace: WeakView, _workspace_id: workspace::WorkspaceId, _item_id: workspace::ItemId, _cx: &mut ViewContext, - ) -> Task>> { + ) -> Task>> { unimplemented!() } } @@ -751,7 +703,7 @@ impl ProjectSearchView { fn semantic_index_changed( &mut self, - semantic_index: ModelHandle, + semantic_index: Model, cx: &mut ViewContext, ) { 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, + model: Model, cx: &mut ViewContext, settings: Option, ) -> 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 @@ -982,36 +928,26 @@ impl ProjectSearchView { }) .detach(); - let included_files_editor = cx.add_view(|cx| { - let mut editor = Editor::single_line( - Some(Arc::new(|theme| { - theme.search.include_exclude_editor.input.clone() - })), - cx, - ); + let included_files_editor = cx.build_view(|cx| { + let mut editor = Editor::single_line(cx); editor.set_placeholder_text("Include: crates/**/*.toml", cx); editor }); // Subscribe to include_files_editor in order to reraise editor events for workspace item activation purposes - cx.subscribe(&included_files_editor, |_, _, event, cx| { + cx.subscribe(&included_files_editor, |_, _, event: &EditorEvent, cx| { cx.emit(ViewEvent::EditorEvent(event.clone())) }) .detach(); - let excluded_files_editor = cx.add_view(|cx| { - let mut editor = Editor::single_line( - Some(Arc::new(|theme| { - theme.search.include_exclude_editor.input.clone() - })), - cx, - ); + let excluded_files_editor = cx.build_view(|cx| { + let mut editor = Editor::single_line(cx); editor.set_placeholder_text("Exclude: vendor/*, *.lock", cx); editor }); // Subscribe to excluded_files_editor in order to reraise editor events for workspace item activation purposes - cx.subscribe(&excluded_files_editor, |_, _, event, cx| { + cx.subscribe(&excluded_files_editor, |_, _, event: &EditorEvent, cx| { cx.emit(ViewEvent::EditorEvent(event.clone())) }) .detach(); @@ -1063,8 +999,8 @@ impl ProjectSearchView { return; }; - let model = cx.add_model(|cx| ProjectSearch::new(workspace.project().clone(), cx)); - let search = cx.add_view(|cx| ProjectSearchView::new(model, cx, None)); + let model = cx.build_model(|cx| ProjectSearch::new(workspace.project().clone(), cx)); + let search = cx.build_view(|cx| ProjectSearchView::new(model, cx, None)); workspace.add_item(Box::new(search.clone()), cx); search.update(cx, |search, cx| { search @@ -1082,8 +1018,8 @@ impl ProjectSearchView { cx: &mut ViewContext, ) { // Clean up entries for dropped projects - cx.update_global(|state: &mut ActiveSearches, cx| { - state.0.retain(|project, _| project.is_upgradable(cx)) + cx.update_global(|state: &mut ActiveSearches, _cx| { + state.0.retain(|project, _| project.is_upgradable()) }); let query = workspace.active_item(cx).and_then(|item| { @@ -1107,8 +1043,8 @@ impl ProjectSearchView { None }; - let model = cx.add_model(|cx| ProjectSearch::new(workspace.project().clone(), cx)); - let search = cx.add_view(|cx| ProjectSearchView::new(model, cx, settings)); + let model = cx.build_model(|cx| ProjectSearch::new(workspace.project().clone(), cx)); + let search = cx.build_view(|cx| ProjectSearchView::new(model, cx, settings)); workspace.add_item(Box::new(search.clone()), cx); @@ -1243,7 +1179,8 @@ impl ProjectSearchView { query_editor.select_all(&SelectAll, cx); }); self.query_editor_was_focused = true; - cx.focus(&self.query_editor); + let editor_handle = self.query_editor.focus_handle(cx); + cx.focus(&editor_handle); } fn set_query(&mut self, query: &str, cx: &mut ViewContext) { @@ -1257,7 +1194,8 @@ impl ProjectSearchView { query_editor.change_selections(None, cx, |s| s.select_ranges([cursor.clone()..cursor])); }); self.query_editor_was_focused = false; - cx.focus(&self.results_editor); + let results_handle = self.results_editor.focus_handle(cx); + cx.focus(&results_handle); } fn model_changed(&mut self, cx: &mut ViewContext) { @@ -1281,11 +1219,11 @@ impl ProjectSearchView { } editor.highlight_background::( match_ranges, - |theme| theme.search.match_background, + |theme| theme.search_match_background, cx, ); }); - if is_new_search && self.query_editor.is_focused(cx) { + if is_new_search && self.query_editor.focus_handle(cx).is_focused(cx) { self.focus_results_editor(cx); } } @@ -1311,21 +1249,11 @@ impl ProjectSearchView { self.active_match_index.is_some() } - fn move_focus_to_results(pane: &mut Pane, _: &ToggleFocus, cx: &mut ViewContext) { - if let Some(search_view) = pane - .active_item() - .and_then(|item| item.downcast::()) - { - search_view.update(cx, |search_view, cx| { - if !search_view.results_editor.is_focused(cx) - && !search_view.model.read(cx).match_ranges.is_empty() - { - return search_view.focus_results_editor(cx); - } - }); + fn landing_text_minor(&self) -> SharedString { + match self.current_mode { + SearchMode::Text | SearchMode::Regex => "Include/exclude specific paths with the filter option. Matching exact word and/or casing is available too.".into(), + SearchMode::Semantic => ".Simply explain the code you are looking to find. ex. 'prompt user for permissions to index their project'".into() } - - cx.propagate_action(); } } @@ -1342,32 +1270,30 @@ impl ProjectSearchBar { subscription: Default::default(), } } - fn cycle_mode(workspace: &mut Workspace, _: &CycleMode, cx: &mut ViewContext) { - if let Some(search_view) = workspace - .active_item(cx) - .and_then(|item| item.downcast::()) - { - search_view.update(cx, |this, cx| { - let new_mode = - crate::mode::next_mode(&this.current_mode, SemanticIndex::enabled(cx)); + fn cycle_mode(&self, _: &CycleMode, cx: &mut ViewContext) { + if let Some(view) = self.active_project_search.as_ref() { + view.update(cx, |this, cx| { + // todo: po: 2nd argument of `next_mode` should be `SemanticIndex::enabled(cx))`, but we need to flesh out port of semantic_index first. + let new_mode = crate::mode::next_mode(&this.current_mode, false); this.activate_search_mode(new_mode, cx); - cx.focus(&this.query_editor); - }) + let editor_handle = this.query_editor.focus_handle(cx); + cx.focus(&editor_handle); + }); } } fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { - let mut should_propagate = true; if let Some(search_view) = self.active_project_search.as_ref() { search_view.update(cx, |search_view, cx| { - if !search_view.replacement_editor.is_focused(cx) { - should_propagate = false; + if !search_view + .replacement_editor + .focus_handle(cx) + .is_focused(cx) + { + cx.stop_propagation(); search_view.search(cx); } }); } - if should_propagate { - cx.propagate_action(); - } } fn search_in_new(workspace: &mut Workspace, _: &SearchInNew, cx: &mut ViewContext) { @@ -1388,61 +1314,19 @@ impl ProjectSearchBar { new_query }); if let Some(new_query) = new_query { - let model = cx.add_model(|cx| { + let model = cx.build_model(|cx| { let mut model = ProjectSearch::new(workspace.project().clone(), cx); model.search(new_query, cx); model }); workspace.add_item( - Box::new(cx.add_view(|cx| ProjectSearchView::new(model, cx, None))), + Box::new(cx.build_view(|cx| ProjectSearchView::new(model, cx, None))), cx, ); } } } - fn select_next_match(pane: &mut Pane, _: &SelectNextMatch, cx: &mut ViewContext) { - if let Some(search_view) = pane - .active_item() - .and_then(|item| item.downcast::()) - { - search_view.update(cx, |view, cx| view.select_match(Direction::Next, cx)); - } else { - cx.propagate_action(); - } - } - - fn replace_next(pane: &mut Pane, _: &ReplaceNext, cx: &mut ViewContext) { - if let Some(search_view) = pane - .active_item() - .and_then(|item| item.downcast::()) - { - search_view.update(cx, |view, cx| view.replace_next(&ReplaceNext, cx)); - } else { - cx.propagate_action(); - } - } - fn replace_all(pane: &mut Pane, _: &ReplaceAll, cx: &mut ViewContext) { - if let Some(search_view) = pane - .active_item() - .and_then(|item| item.downcast::()) - { - search_view.update(cx, |view, cx| view.replace_all(&ReplaceAll, cx)); - } else { - cx.propagate_action(); - } - } - fn select_prev_match(pane: &mut Pane, _: &SelectPrevMatch, cx: &mut ViewContext) { - if let Some(search_view) = pane - .active_item() - .and_then(|item| item.downcast::()) - { - search_view.update(cx, |view, cx| view.select_match(Direction::Prev, cx)); - } else { - cx.propagate_action(); - } - } - fn tab(&mut self, _: &editor::Tab, cx: &mut ViewContext) { self.cycle_field(Direction::Next, cx); } @@ -1456,7 +1340,6 @@ impl ProjectSearchBar { Some(active_project_search) => active_project_search, None => { - cx.propagate_action(); return; } }; @@ -1475,12 +1358,11 @@ impl ProjectSearchBar { let current_index = match views .iter() .enumerate() - .find(|(_, view)| view.is_focused(cx)) + .find(|(_, view)| view.focus_handle(cx).is_focused(cx)) { Some((index, _)) => index, None => { - cx.propagate_action(); return; } }; @@ -1490,7 +1372,9 @@ impl ProjectSearchBar { Direction::Prev if current_index == 0 => views.len() - 1, Direction::Prev => (current_index - 1) % views.len(), }; - cx.focus(views[new_index]); + let next_focus_handle = views[new_index].focus_handle(cx); + cx.focus(&next_focus_handle); + cx.stop_propagation(); }); } @@ -1511,75 +1395,15 @@ impl ProjectSearchBar { if let Some(search) = &self.active_project_search { search.update(cx, |this, cx| { this.replace_enabled = !this.replace_enabled; - if !this.replace_enabled { - cx.focus(&this.query_editor); - } - cx.notify(); - }); - } - } - fn toggle_replace_on_a_pane(pane: &mut Pane, _: &ToggleReplace, cx: &mut ViewContext) { - let mut should_propagate = true; - if let Some(search_view) = pane - .active_item() - .and_then(|item| item.downcast::()) - { - search_view.update(cx, |this, cx| { - should_propagate = false; - this.replace_enabled = !this.replace_enabled; - if !this.replace_enabled { - cx.focus(&this.query_editor); - } + let editor_to_focus = if !this.replace_enabled { + this.query_editor.focus_handle(cx) + } else { + this.replacement_editor.focus_handle(cx) + }; + cx.focus(&editor_to_focus); cx.notify(); }); } - if should_propagate { - cx.propagate_action(); - } - } - fn activate_text_mode(pane: &mut Pane, _: &ActivateTextMode, cx: &mut ViewContext) { - if let Some(search_view) = pane - .active_item() - .and_then(|item| item.downcast::()) - { - search_view.update(cx, |view, cx| { - view.activate_search_mode(SearchMode::Text, cx) - }); - } else { - cx.propagate_action(); - } - } - - fn activate_regex_mode(pane: &mut Pane, _: &ActivateRegexMode, cx: &mut ViewContext) { - if let Some(search_view) = pane - .active_item() - .and_then(|item| item.downcast::()) - { - search_view.update(cx, |view, cx| { - view.activate_search_mode(SearchMode::Regex, cx) - }); - } else { - cx.propagate_action(); - } - } - - fn activate_semantic_mode( - pane: &mut Pane, - _: &ActivateSemanticMode, - cx: &mut ViewContext, - ) { - if SemanticIndex::enabled(cx) { - if let Some(search_view) = pane - .active_item() - .and_then(|item| item.downcast::()) - { - search_view.update(cx, |view, cx| { - view.activate_search_mode(SearchMode::Semantic, cx) - }); - } else { - cx.propagate_action(); - } - } } fn toggle_filters(&mut self, cx: &mut ViewContext) -> bool { @@ -1592,7 +1416,7 @@ impl ProjectSearchBar { search_view .excluded_files_editor .update(cx, |_, cx| cx.notify()); - cx.refresh_windows(); + cx.refresh(); cx.notify(); }); cx.notify(); @@ -1660,316 +1484,627 @@ impl ProjectSearchBar { }); } } -} - -impl Entity for ProjectSearchBar { - type Event = (); -} - -impl View for ProjectSearchBar { - fn ui_name() -> &'static str { - "ProjectSearchBar" - } - - fn update_keymap_context( - &self, - keymap: &mut gpui::keymap_matcher::KeymapContext, - cx: &AppContext, - ) { - Self::reset_to_default_keymap_context(keymap); - let in_replace = self - .active_project_search - .as_ref() - .map(|search| { - search - .read(cx) - .replacement_editor - .read_with(cx, |_, cx| cx.is_self_focused()) - }) - .flatten() - .unwrap_or(false); - if in_replace { - keymap.add_identifier("in_replace"); - } - } - - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - if let Some(_search) = self.active_project_search.as_ref() { - let search = _search.read(cx); - let theme = theme::current(cx).clone(); - let query_container_style = if search.panels_with_errors.contains(&InputPanel::Query) { - theme.search.invalid_editor - } else { - theme.search.editor.input.container - }; - - let search = _search.read(cx); - let filter_button = render_option_button_icon( - search.filters_enabled, - "icons/filter.svg", - 0, - "Toggle filters", - Box::new(ToggleFilters), - move |_, this, cx| { - this.toggle_filters(cx); - }, - cx, - ); - - let search = _search.read(cx); - let is_semantic_available = SemanticIndex::enabled(cx); - let is_semantic_disabled = search.semantic_state.is_none(); - let icon_style = theme.search.editor_icon.clone(); - let is_active = search.active_match_index.is_some(); - - let render_option_button_icon = |path, option, cx: &mut ViewContext| { - crate::search_bar::render_option_button_icon( - self.is_option_enabled(option, cx), - path, - option.bits as usize, - format!("Toggle {}", option.label()), - option.to_toggle_action(), - move |_, this, cx| { - this.toggle_search_option(option, cx); - }, - cx, - ) - }; - let case_sensitive = is_semantic_disabled.then(|| { - render_option_button_icon( - "icons/case_insensitive.svg", - SearchOptions::CASE_SENSITIVE, - cx, - ) - }); - - let whole_word = is_semantic_disabled.then(|| { - render_option_button_icon("icons/word_search.svg", SearchOptions::WHOLE_WORD, cx) + fn new_placeholder_text(&self, cx: &mut ViewContext) -> Option { + let previous_query_keystrokes = cx + .bindings_for_action(&PreviousHistoryQuery {}) + .into_iter() + .next() + .map(|binding| { + binding + .keystrokes() + .iter() + .map(|k| k.to_string()) + .collect::>() }); - - let include_ignored = is_semantic_disabled.then(|| { - render_option_button_icon( - "icons/file_icons/git.svg", - SearchOptions::INCLUDE_IGNORED, - cx, - ) + let next_query_keystrokes = cx + .bindings_for_action(&NextHistoryQuery {}) + .into_iter() + .next() + .map(|binding| { + binding + .keystrokes() + .iter() + .map(|k| k.to_string()) + .collect::>() }); + let new_placeholder_text = match (previous_query_keystrokes, next_query_keystrokes) { + (Some(previous_query_keystrokes), Some(next_query_keystrokes)) => Some(format!( + "Search ({}/{} for previous/next query)", + previous_query_keystrokes.join(" "), + next_query_keystrokes.join(" ") + )), + (None, Some(next_query_keystrokes)) => Some(format!( + "Search ({} for next query)", + next_query_keystrokes.join(" ") + )), + (Some(previous_query_keystrokes), None) => Some(format!( + "Search ({} for previous query)", + previous_query_keystrokes.join(" ") + )), + (None, None) => None, + }; + new_placeholder_text + } +} - let search_button_for_mode = |mode, side, cx: &mut ViewContext| { - let is_active = if let Some(search) = self.active_project_search.as_ref() { - let search = search.read(cx); - search.current_mode == mode - } else { - false - }; - render_search_mode_button( - mode, - side, - is_active, - move |_, this, cx| { - this.activate_search_mode(mode, cx); - }, - cx, - ) - }; - - let search = _search.read(cx); - - let include_container_style = - if search.panels_with_errors.contains(&InputPanel::Include) { - theme.search.invalid_include_exclude_editor - } else { - theme.search.include_exclude_editor.input.container - }; - - let exclude_container_style = - if search.panels_with_errors.contains(&InputPanel::Exclude) { - theme.search.invalid_include_exclude_editor - } else { - theme.search.include_exclude_editor.input.container - }; +impl Render for ProjectSearchBar { + type Element = Div; - let matches = search.active_match_index.map(|match_ix| { - Label::new( - format!( - "{}/{}", - match_ix + 1, - search.model.read(cx).match_ranges.len() - ), - theme.search.match_index.text.clone(), - ) - .contained() - .with_style(theme.search.match_index.container) - .aligned() + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + let Some(search) = self.active_project_search.clone() else { + return div(); + }; + let mut key_context = KeyContext::default(); + key_context.add("ProjectSearchBar"); + if let Some(placeholder_text) = self.new_placeholder_text(cx) { + search.update(cx, |search, cx| { + search.query_editor.update(cx, |this, cx| { + this.set_placeholder_text(placeholder_text, cx) + }) }); - let should_show_replace_input = search.replace_enabled; - let replacement = should_show_replace_input.then(|| { - Flex::row() - .with_child( - Svg::for_style(theme.search.replace_icon.clone().icon) - .contained() - .with_style(theme.search.replace_icon.clone().container), + } + let search = search.read(cx); + + let query_column = v_stack() + //.flex_1() + .child( + h_stack() + .min_w_80() + .on_action(cx.listener(|this, action, cx| this.confirm(action, cx))) + .on_action( + cx.listener(|this, action, cx| this.previous_history_query(action, cx)), ) - .with_child(ChildView::new(&search.replacement_editor, cx).flex(1., true)) - .align_children_center() - .flex(1., true) - .contained() - .with_style(query_container_style) - .constrained() - .with_min_width(theme.search.editor.min_width) - .with_max_width(theme.search.editor.max_width) - .with_height(theme.search.search_bar_row_height) - .flex(1., false) - }); - let replace_all = should_show_replace_input.then(|| { - super::replace_action( - ReplaceAll, - "Replace all", - "icons/replace_all.svg", - theme.tooltip.clone(), - theme.search.action_button.clone(), - ) - }); - let replace_next = should_show_replace_input.then(|| { - super::replace_action( - ReplaceNext, - "Replace next", - "icons/replace_next.svg", - theme.tooltip.clone(), - theme.search.action_button.clone(), + .on_action(cx.listener(|this, action, cx| this.next_history_query(action, cx))) + .child(IconElement::new(Icon::MagnifyingGlass)) + .child(search.query_editor.clone()) + .child( + h_stack() + .child( + IconButton::new("project-search-filter-button", Icon::Filter) + .tooltip(|cx| { + Tooltip::for_action("Toggle filters", &ToggleFilters, cx) + }) + .on_click(cx.listener(|this, _, cx| { + this.toggle_filters(cx); + })) + .selected( + self.active_project_search + .as_ref() + .map(|search| search.read(cx).filters_enabled) + .unwrap_or_default(), + ), + ) + .child( + IconButton::new( + "project-search-case-sensitive", + Icon::CaseSensitive, + ) + .tooltip(|cx| { + Tooltip::for_action( + "Toggle case sensitive", + &ToggleCaseSensitive, + cx, + ) + }) + .selected(self.is_option_enabled(SearchOptions::CASE_SENSITIVE, cx)) + .on_click(cx.listener( + |this, _, cx| { + this.toggle_search_option( + SearchOptions::CASE_SENSITIVE, + cx, + ); + }, + )), + ) + .child( + IconButton::new("project-search-whole-word", Icon::WholeWord) + .tooltip(|cx| { + Tooltip::for_action( + "Toggle whole word", + &ToggleWholeWord, + cx, + ) + }) + .selected(self.is_option_enabled(SearchOptions::WHOLE_WORD, cx)) + .on_click(cx.listener(|this, _, cx| { + this.toggle_search_option(SearchOptions::WHOLE_WORD, cx); + })), + ), + ) + .border_2() + .bg(white()) + .rounded_lg(), + ) + .when(search.filters_enabled, |this| { + this.child( + h_stack() + .mt_2() + .flex_1() + .justify_between() + .child( + h_stack() + .flex_1() + .border_1() + .mr_2() + .child(search.included_files_editor.clone()), + ) + .child( + h_stack() + .flex_1() + .border_1() + .ml_2() + .child(search.excluded_files_editor.clone()), + ), ) }); - let query_column = Flex::column() - .with_spacing(theme.search.search_row_spacing) - .with_child( - Flex::row() - .with_child( - Svg::for_style(icon_style.icon) - .contained() - .with_style(icon_style.container), - ) - .with_child(ChildView::new(&search.query_editor, cx).flex(1., true)) - .with_child( - Flex::row() - .with_child(filter_button) - .with_children(case_sensitive) - .with_children(whole_word) - .flex(1., false) - .constrained() - .contained(), + let mode_column = v_stack().items_start().justify_start().child( + h_stack() + .child( + h_stack() + .child( + Button::new("project-search-text-button", "Text") + .selected(search.current_mode == SearchMode::Text) + .on_click(cx.listener(|this, _, cx| { + this.activate_search_mode(SearchMode::Text, cx) + })) + .tooltip(|cx| { + Tooltip::for_action("Toggle text search", &ActivateTextMode, cx) + }), ) - .align_children_center() - .contained() - .with_style(query_container_style) - .constrained() - .with_min_width(theme.search.editor.min_width) - .with_max_width(theme.search.editor.max_width) - .with_height(theme.search.search_bar_row_height) - .flex(1., false), + .child( + Button::new("project-search-regex-button", "Regex") + .selected(search.current_mode == SearchMode::Regex) + .on_click(cx.listener(|this, _, cx| { + this.activate_search_mode(SearchMode::Regex, cx) + })) + .tooltip(|cx| { + Tooltip::for_action( + "Toggle regular expression search", + &ActivateRegexMode, + cx, + ) + }), + ), ) - .with_children(search.filters_enabled.then(|| { - Flex::row() - .with_child( - Flex::row() - .with_child( - ChildView::new(&search.included_files_editor, cx) - .contained() - .constrained() - .with_height(theme.search.search_bar_row_height) - .flex(1., true), - ) - .with_children(include_ignored) - .contained() - .with_style(include_container_style) - .constrained() - .with_height(theme.search.search_bar_row_height) - .flex(1., true), - ) - .with_child( - ChildView::new(&search.excluded_files_editor, cx) - .contained() - .with_style(exclude_container_style) - .constrained() - .with_height(theme.search.search_bar_row_height) - .flex(1., true), - ) - .constrained() - .with_min_width(theme.search.editor.min_width) - .with_max_width(theme.search.editor.max_width) - .flex(1., false) - })) - .flex(1., false); - let switches_column = Flex::row() - .align_children_center() - .with_child(super::toggle_replace_button( - search.replace_enabled, - theme.tooltip.clone(), - theme.search.option_button_component.clone(), - )) - .constrained() - .with_height(theme.search.search_bar_row_height) - .contained() - .with_style(theme.search.option_button_group); - let mode_column = - Flex::row() - .with_child(search_button_for_mode( - SearchMode::Text, - Some(Side::Left), - cx, - )) - .with_child(search_button_for_mode( - SearchMode::Regex, - if is_semantic_available { - None - } else { - Some(Side::Right) - }, - cx, - )) - .with_children(is_semantic_available.then(|| { - search_button_for_mode(SearchMode::Semantic, Some(Side::Right), cx) + .child( + IconButton::new("project-search-toggle-replace", Icon::Replace) + .on_click(cx.listener(|this, _, cx| { + this.toggle_replace(&ToggleReplace, cx); + })) + .tooltip(|cx| Tooltip::for_action("Toggle replace", &ToggleReplace, cx)), + ), + ); + let replace_column = if search.replace_enabled { + h_stack() + .p_1() + .flex_1() + .border_2() + .rounded_lg() + .child(IconElement::new(Icon::Replace).size(ui::IconSize::Small)) + .child(search.replacement_editor.clone()) + } else { + // Fill out the space if we don't have a replacement editor. + h_stack().flex_1() + }; + let actions_column = h_stack() + .when(search.replace_enabled, |this| { + this.children([ + IconButton::new("project-search-replace-next", Icon::ReplaceNext) + .on_click(cx.listener(|this, _, cx| { + if let Some(search) = this.active_project_search.as_ref() { + search.update(cx, |this, cx| { + this.replace_next(&ReplaceNext, cx); + }) + } + })) + .tooltip(|cx| Tooltip::for_action("Replace next match", &ReplaceNext, cx)), + IconButton::new("project-search-replace-all", Icon::ReplaceAll) + .on_click(cx.listener(|this, _, cx| { + if let Some(search) = this.active_project_search.as_ref() { + search.update(cx, |this, cx| { + this.replace_all(&ReplaceAll, cx); + }) + } + })) + .tooltip(|cx| Tooltip::for_action("Replace all matches", &ReplaceAll, cx)), + ]) + }) + .when_some(search.active_match_index, |mut this, index| { + let index = index + 1; + let match_quantity = search.model.read(cx).match_ranges.len(); + if match_quantity > 0 { + debug_assert!(match_quantity >= index); + this = this.child(Label::new(format!("{index}/{match_quantity}"))) + } + this + }) + .children([ + IconButton::new("project-search-prev-match", Icon::ChevronLeft) + .disabled(search.active_match_index.is_none()) + .on_click(cx.listener(|this, _, cx| { + if let Some(search) = this.active_project_search.as_ref() { + search.update(cx, |this, cx| { + this.select_match(Direction::Prev, cx); + }) + } })) - .contained() - .with_style(theme.search.modes_container); - - let nav_button_for_direction = |label, direction, cx: &mut ViewContext| { - render_nav_button( - label, - direction, - is_active, - move |_, this, cx| { + .tooltip(|cx| { + Tooltip::for_action("Go to previous match", &SelectPrevMatch, cx) + }), + IconButton::new("project-search-next-match", Icon::ChevronRight) + .disabled(search.active_match_index.is_none()) + .on_click(cx.listener(|this, _, cx| { if let Some(search) = this.active_project_search.as_ref() { - search.update(cx, |search, cx| search.select_match(direction, cx)); + search.update(cx, |this, cx| { + this.select_match(Direction::Next, cx); + }) } - }, - cx, - ) - }; - - let nav_column = Flex::row() - .with_children(replace_next) - .with_children(replace_all) - .with_child(Flex::row().with_children(matches)) - .with_child(nav_button_for_direction("<", Direction::Prev, cx)) - .with_child(nav_button_for_direction(">", Direction::Next, cx)) - .constrained() - .with_height(theme.search.search_bar_row_height) - .flex_float(); - - Flex::row() - .with_child(query_column) - .with_child(mode_column) - .with_child(switches_column) - .with_children(replacement) - .with_child(nav_column) - .contained() - .with_style(theme.search.container) - .into_any_named("project search") - } else { - Empty::new().into_any() - } + })) + .tooltip(|cx| Tooltip::for_action("Go to next match", &SelectNextMatch, cx)), + ]); + h_stack() + .key_context(key_context) + .size_full() + .p_1() + .m_2() + .justify_between() + .on_action(cx.listener(|this, _: &ToggleFilters, cx| { + this.toggle_filters(cx); + })) + .on_action(cx.listener(|this, _: &ToggleWholeWord, cx| { + this.toggle_search_option(SearchOptions::WHOLE_WORD, cx); + })) + .on_action(cx.listener(|this, _: &ToggleCaseSensitive, cx| { + this.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx); + })) + .on_action(cx.listener(|this, action, cx| { + this.toggle_replace(action, cx); + })) + .on_action(cx.listener(|this, _: &ActivateTextMode, cx| { + this.activate_search_mode(SearchMode::Text, cx) + })) + .on_action(cx.listener(|this, _: &ActivateRegexMode, cx| { + this.activate_search_mode(SearchMode::Regex, cx) + })) + .on_action(cx.listener(|this, action, cx| { + if let Some(search) = this.active_project_search.as_ref() { + search.update(cx, |this, cx| { + this.replace_next(action, cx); + }) + } + })) + .on_action(cx.listener(|this, action, cx| { + if let Some(search) = this.active_project_search.as_ref() { + search.update(cx, |this, cx| { + this.replace_all(action, cx); + }) + } + })) + .on_action(cx.listener(|this, action, cx| { + this.tab(action, cx); + })) + .on_action(cx.listener(|this, action, cx| { + this.tab_previous(action, cx); + })) + .on_action(cx.listener(|this, action, cx| { + this.cycle_mode(action, cx); + })) + .child(query_column) + .child(mode_column) + .child(replace_column) + .child(actions_column) } } +// impl Entity for ProjectSearchBar { +// type Event = (); +// } + +// impl View for ProjectSearchBar { +// fn ui_name() -> &'static str { +// "ProjectSearchBar" +// } + +// fn update_keymap_context( +// &self, +// keymap: &mut gpui::keymap_matcher::KeymapContext, +// cx: &AppContext, +// ) { +// Self::reset_to_default_keymap_context(keymap); +// let in_replace = self +// .active_project_search +// .as_ref() +// .map(|search| { +// search +// .read(cx) +// .replacement_editor +// .read_with(cx, |_, cx| cx.is_self_focused()) +// }) +// .flatten() +// .unwrap_or(false); +// if in_replace { +// keymap.add_identifier("in_replace"); +// } +// } + +// fn render(&mut self, cx: &mut ViewContext) -> AnyElement { +// if let Some(_search) = self.active_project_search.as_ref() { +// let search = _search.read(cx); +// let theme = theme::current(cx).clone(); +// let query_container_style = if search.panels_with_errors.contains(&InputPanel::Query) { +// theme.search.invalid_editor +// } else { +// theme.search.editor.input.container +// }; + +// let search = _search.read(cx); +// let filter_button = render_option_button_icon( +// search.filters_enabled, +// "icons/filter.svg", +// 0, +// "Toggle filters", +// Box::new(ToggleFilters), +// move |_, this, cx| { +// this.toggle_filters(cx); +// }, +// cx, +// ); + +// let search = _search.read(cx); +// let is_semantic_available = SemanticIndex::enabled(cx); +// let is_semantic_disabled = search.semantic_state.is_none(); +// let icon_style = theme.search.editor_icon.clone(); +// let is_active = search.active_match_index.is_some(); + +// let render_option_button_icon = |path, option, cx: &mut ViewContext| { +// crate::search_bar::render_option_button_icon( +// self.is_option_enabled(option, cx), +// path, +// option.bits as usize, +// format!("Toggle {}", option.label()), +// option.to_toggle_action(), +// move |_, this, cx| { +// this.toggle_search_option(option, cx); +// }, +// cx, +// ) +// }; +// let case_sensitive = is_semantic_disabled.then(|| { +// render_option_button_icon( +// "icons/case_insensitive.svg", +// SearchOptions::CASE_SENSITIVE, +// cx, +// ) +// }); + +// let whole_word = is_semantic_disabled.then(|| { +// render_option_button_icon("icons/word_search.svg", SearchOptions::WHOLE_WORD, cx) +// }); + +// let include_ignored = is_semantic_disabled.then(|| { +// render_option_button_icon( +// "icons/file_icons/git.svg", +// SearchOptions::INCLUDE_IGNORED, +// cx, +// ) +// }); + +// let search_button_for_mode = |mode, side, cx: &mut ViewContext| { +// let is_active = if let Some(search) = self.active_project_search.as_ref() { +// let search = search.read(cx); +// search.current_mode == mode +// } else { +// false +// }; +// render_search_mode_button( +// mode, +// side, +// is_active, +// move |_, this, cx| { +// this.activate_search_mode(mode, cx); +// }, +// cx, +// ) +// }; + +// let search = _search.read(cx); + +// let include_container_style = +// if search.panels_with_errors.contains(&InputPanel::Include) { +// theme.search.invalid_include_exclude_editor +// } else { +// theme.search.include_exclude_editor.input.container +// }; + +// let exclude_container_style = +// if search.panels_with_errors.contains(&InputPanel::Exclude) { +// theme.search.invalid_include_exclude_editor +// } else { +// theme.search.include_exclude_editor.input.container +// }; + +// let matches = search.active_match_index.map(|match_ix| { +// Label::new( +// format!( +// "{}/{}", +// match_ix + 1, +// search.model.read(cx).match_ranges.len() +// ), +// theme.search.match_index.text.clone(), +// ) +// .contained() +// .with_style(theme.search.match_index.container) +// .aligned() +// }); +// let should_show_replace_input = search.replace_enabled; +// let replacement = should_show_replace_input.then(|| { +// Flex::row() +// .with_child( +// Svg::for_style(theme.search.replace_icon.clone().icon) +// .contained() +// .with_style(theme.search.replace_icon.clone().container), +// ) +// .with_child(ChildView::new(&search.replacement_editor, cx).flex(1., true)) +// .align_children_center() +// .flex(1., true) +// .contained() +// .with_style(query_container_style) +// .constrained() +// .with_min_width(theme.search.editor.min_width) +// .with_max_width(theme.search.editor.max_width) +// .with_height(theme.search.search_bar_row_height) +// .flex(1., false) +// }); +// let replace_all = should_show_replace_input.then(|| { +// super::replace_action( +// ReplaceAll, +// "Replace all", +// "icons/replace_all.svg", +// theme.tooltip.clone(), +// theme.search.action_button.clone(), +// ) +// }); +// let replace_next = should_show_replace_input.then(|| { +// super::replace_action( +// ReplaceNext, +// "Replace next", +// "icons/replace_next.svg", +// theme.tooltip.clone(), +// theme.search.action_button.clone(), +// ) +// }); +// let query_column = Flex::column() +// .with_spacing(theme.search.search_row_spacing) +// .with_child( +// Flex::row() +// .with_child( +// Svg::for_style(icon_style.icon) +// .contained() +// .with_style(icon_style.container), +// ) +// .with_child(ChildView::new(&search.query_editor, cx).flex(1., true)) +// .with_child( +// Flex::row() +// .with_child(filter_button) +// .with_children(case_sensitive) +// .with_children(whole_word) +// .flex(1., false) +// .constrained() +// .contained(), +// ) +// .align_children_center() +// .contained() +// .with_style(query_container_style) +// .constrained() +// .with_min_width(theme.search.editor.min_width) +// .with_max_width(theme.search.editor.max_width) +// .with_height(theme.search.search_bar_row_height) +// .flex(1., false), +// ) +// .with_children(search.filters_enabled.then(|| { +// Flex::row() +// .with_child( +// Flex::row() +// .with_child( +// ChildView::new(&search.included_files_editor, cx) +// .contained() +// .constrained() +// .with_height(theme.search.search_bar_row_height) +// .flex(1., true), +// ) +// .with_children(include_ignored) +// .contained() +// .with_style(include_container_style) +// .constrained() +// .with_height(theme.search.search_bar_row_height) +// .flex(1., true), +// ) +// .with_child( +// ChildView::new(&search.excluded_files_editor, cx) +// .contained() +// .with_style(exclude_container_style) +// .constrained() +// .with_height(theme.search.search_bar_row_height) +// .flex(1., true), +// ) +// .constrained() +// .with_min_width(theme.search.editor.min_width) +// .with_max_width(theme.search.editor.max_width) +// .flex(1., false) +// })) +// .flex(1., false); +// let switches_column = Flex::row() +// .align_children_center() +// .with_child(super::toggle_replace_button( +// search.replace_enabled, +// theme.tooltip.clone(), +// theme.search.option_button_component.clone(), +// )) +// .constrained() +// .with_height(theme.search.search_bar_row_height) +// .contained() +// .with_style(theme.search.option_button_group); +// let mode_column = +// Flex::row() +// .with_child(search_button_for_mode( +// SearchMode::Text, +// Some(Side::Left), +// cx, +// )) +// .with_child(search_button_for_mode( +// SearchMode::Regex, +// if is_semantic_available { +// None +// } else { +// Some(Side::Right) +// }, +// cx, +// )) +// .with_children(is_semantic_available.then(|| { +// search_button_for_mode(SearchMode::Semantic, Some(Side::Right), cx) +// })) +// .contained() +// .with_style(theme.search.modes_container); + +// let nav_button_for_direction = |label, direction, cx: &mut ViewContext| { +// render_nav_button( +// label, +// direction, +// is_active, +// move |_, this, cx| { +// if let Some(search) = this.active_project_search.as_ref() { +// search.update(cx, |search, cx| search.select_match(direction, cx)); +// } +// }, +// cx, +// ) +// }; + +// let nav_column = Flex::row() +// .with_children(replace_next) +// .with_children(replace_all) +// .with_child(Flex::row().with_children(matches)) +// .with_child(nav_button_for_direction("<", Direction::Prev, cx)) +// .with_child(nav_button_for_direction(">", Direction::Next, cx)) +// .constrained() +// .with_height(theme.search.search_bar_row_height) +// .flex_float(); + +// Flex::row() +// .with_child(query_column) +// .with_child(mode_column) +// .with_child(switches_column) +// .with_children(replacement) +// .with_child(nav_column) +// .contained() +// .with_style(theme.search.container) +// .into_any_named("project search") +// } else { +// Empty::new().into_any() +// } +// } +// } + +impl EventEmitter for ProjectSearchBar {} impl ToolbarItemView for ProjectSearchBar { fn set_active_pane_item( @@ -1989,15 +2124,13 @@ impl ToolbarItemView for ProjectSearchBar { self.subscription = Some(cx.observe(&search, |_, _, cx| cx.notify())); self.active_project_search = Some(search); - ToolbarItemLocation::PrimaryLeft { - flex: Some((1., true)), - } + ToolbarItemLocation::PrimaryLeft {} } else { ToolbarItemLocation::Hidden } } - fn row_count(&self, cx: &ViewContext) -> usize { + fn row_count(&self, cx: &WindowContext<'_>) -> usize { if let Some(search) = self.active_project_search.as_ref() { if search.read(cx).filters_enabled { return 2; @@ -2007,711 +2140,711 @@ impl ToolbarItemView for ProjectSearchBar { } } -#[cfg(test)] -pub mod tests { - use super::*; - use editor::DisplayPoint; - use gpui::{color::Color, executor::Deterministic, TestAppContext}; - use project::FakeFs; - use semantic_index::semantic_index_settings::SemanticIndexSettings; - use serde_json::json; - use settings::SettingsStore; - use std::sync::Arc; - use theme::ThemeSettings; - - #[gpui::test] - async fn test_project_search(deterministic: Arc, cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.background()); - fs.insert_tree( - "/dir", - json!({ - "one.rs": "const ONE: usize = 1;", - "two.rs": "const TWO: usize = one::ONE + one::ONE;", - "three.rs": "const THREE: usize = one::ONE + two::TWO;", - "four.rs": "const FOUR: usize = one::ONE + three::THREE;", - }), - ) - .await; - let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; - let search = cx.add_model(|cx| ProjectSearch::new(project, cx)); - let search_view = cx - .add_window(|cx| ProjectSearchView::new(search.clone(), cx, None)) - .root(cx); - - search_view.update(cx, |search_view, cx| { - search_view - .query_editor - .update(cx, |query_editor, cx| query_editor.set_text("TWO", cx)); - search_view.search(cx); - }); - deterministic.run_until_parked(); - search_view.update(cx, |search_view, cx| { - assert_eq!( - search_view - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)), - "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;" - ); - assert_eq!( - search_view - .results_editor - .update(cx, |editor, cx| editor.all_text_background_highlights(cx)), - &[ - ( - DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35), - Color::red() - ), - ( - DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40), - Color::red() - ), - ( - DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9), - Color::red() - ) - ] - ); - assert_eq!(search_view.active_match_index, Some(0)); - assert_eq!( - search_view - .results_editor - .update(cx, |editor, cx| editor.selections.display_ranges(cx)), - [DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35)] - ); - - search_view.select_match(Direction::Next, cx); - }); - - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.active_match_index, Some(1)); - assert_eq!( - search_view - .results_editor - .update(cx, |editor, cx| editor.selections.display_ranges(cx)), - [DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40)] - ); - search_view.select_match(Direction::Next, cx); - }); - - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.active_match_index, Some(2)); - assert_eq!( - search_view - .results_editor - .update(cx, |editor, cx| editor.selections.display_ranges(cx)), - [DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9)] - ); - search_view.select_match(Direction::Next, cx); - }); - - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.active_match_index, Some(0)); - assert_eq!( - search_view - .results_editor - .update(cx, |editor, cx| editor.selections.display_ranges(cx)), - [DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35)] - ); - search_view.select_match(Direction::Prev, cx); - }); - - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.active_match_index, Some(2)); - assert_eq!( - search_view - .results_editor - .update(cx, |editor, cx| editor.selections.display_ranges(cx)), - [DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9)] - ); - search_view.select_match(Direction::Prev, cx); - }); - - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.active_match_index, Some(1)); - assert_eq!( - search_view - .results_editor - .update(cx, |editor, cx| editor.selections.display_ranges(cx)), - [DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40)] - ); - }); - } - - #[gpui::test] - async fn test_project_search_focus(deterministic: Arc, cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.background()); - fs.insert_tree( - "/dir", - json!({ - "one.rs": "const ONE: usize = 1;", - "two.rs": "const TWO: usize = one::ONE + one::ONE;", - "three.rs": "const THREE: usize = one::ONE + two::TWO;", - "four.rs": "const FOUR: usize = one::ONE + three::THREE;", - }), - ) - .await; - let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; - let window = cx.add_window(|cx| Workspace::test_new(project, cx)); - let workspace = window.root(cx); - - let active_item = cx.read(|cx| { - workspace - .read(cx) - .active_pane() - .read(cx) - .active_item() - .and_then(|item| item.downcast::()) - }); - assert!( - active_item.is_none(), - "Expected no search panel to be active, but got: {active_item:?}" - ); - - workspace.update(cx, |workspace, cx| { - ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx) - }); - - let Some(search_view) = cx.read(|cx| { - workspace - .read(cx) - .active_pane() - .read(cx) - .active_item() - .and_then(|item| item.downcast::()) - }) else { - panic!("Search view expected to appear after new search event trigger") - }; - let search_view_id = search_view.id(); - - cx.spawn(|mut cx| async move { - window.dispatch_action(search_view_id, &ToggleFocus, &mut cx); - }) - .detach(); - deterministic.run_until_parked(); - search_view.update(cx, |search_view, cx| { - assert!( - search_view.query_editor.is_focused(cx), - "Empty search view should be focused after the toggle focus event: no results panel to focus on", - ); - }); - - search_view.update(cx, |search_view, cx| { - let query_editor = &search_view.query_editor; - assert!( - query_editor.is_focused(cx), - "Search view should be focused after the new search view is activated", - ); - let query_text = query_editor.read(cx).text(cx); - assert!( - query_text.is_empty(), - "New search query should be empty but got '{query_text}'", - ); - let results_text = search_view - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)); - assert!( - results_text.is_empty(), - "Empty search view should have no results but got '{results_text}'" - ); - }); - - search_view.update(cx, |search_view, cx| { - search_view.query_editor.update(cx, |query_editor, cx| { - query_editor.set_text("sOMETHINGtHATsURELYdOESnOTeXIST", cx) - }); - search_view.search(cx); - }); - deterministic.run_until_parked(); - search_view.update(cx, |search_view, cx| { - let results_text = search_view - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)); - assert!( - results_text.is_empty(), - "Search view for mismatching query should have no results but got '{results_text}'" - ); - assert!( - search_view.query_editor.is_focused(cx), - "Search view should be focused after mismatching query had been used in search", - ); - }); - cx.spawn( - |mut cx| async move { window.dispatch_action(search_view_id, &ToggleFocus, &mut cx) }, - ) - .detach(); - deterministic.run_until_parked(); - search_view.update(cx, |search_view, cx| { - assert!( - search_view.query_editor.is_focused(cx), - "Search view with mismatching query should be focused after the toggle focus event: still no results panel to focus on", - ); - }); - - search_view.update(cx, |search_view, cx| { - search_view - .query_editor - .update(cx, |query_editor, cx| query_editor.set_text("TWO", cx)); - search_view.search(cx); - }); - deterministic.run_until_parked(); - search_view.update(cx, |search_view, cx| { - assert_eq!( - search_view - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)), - "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;", - "Search view results should match the query" - ); - assert!( - search_view.results_editor.is_focused(cx), - "Search view with mismatching query should be focused after search results are available", - ); - }); - cx.spawn(|mut cx| async move { - window.dispatch_action(search_view_id, &ToggleFocus, &mut cx); - }) - .detach(); - deterministic.run_until_parked(); - search_view.update(cx, |search_view, cx| { - assert!( - search_view.results_editor.is_focused(cx), - "Search view with matching query should still have its results editor focused after the toggle focus event", - ); - }); - - workspace.update(cx, |workspace, cx| { - ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx) - }); - deterministic.run_until_parked(); - let Some(search_view_2) = cx.read(|cx| { - workspace - .read(cx) - .active_pane() - .read(cx) - .active_item() - .and_then(|item| item.downcast::()) - }) else { - panic!("Search view expected to appear after new search event trigger") - }; - let search_view_id_2 = search_view_2.id(); - assert_ne!( - search_view_2, search_view, - "New search view should be open after `workspace::NewSearch` event" - ); - - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO", "First search view should not have an updated query"); - assert_eq!( - search_view - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)), - "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;", - "Results of the first search view should not update too" - ); - assert!( - !search_view.query_editor.is_focused(cx), - "Focus should be moved away from the first search view" - ); - }); - - search_view_2.update(cx, |search_view_2, cx| { - assert_eq!( - search_view_2.query_editor.read(cx).text(cx), - "two", - "New search view should get the query from the text cursor was at during the event spawn (first search view's first result)" - ); - assert_eq!( - search_view_2 - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)), - "", - "No search results should be in the 2nd view yet, as we did not spawn a search for it" - ); - assert!( - search_view_2.query_editor.is_focused(cx), - "Focus should be moved into query editor fo the new window" - ); - }); - - search_view_2.update(cx, |search_view_2, cx| { - search_view_2 - .query_editor - .update(cx, |query_editor, cx| query_editor.set_text("FOUR", cx)); - search_view_2.search(cx); - }); - deterministic.run_until_parked(); - search_view_2.update(cx, |search_view_2, cx| { - assert_eq!( - search_view_2 - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)), - "\n\nconst FOUR: usize = one::ONE + three::THREE;", - "New search view with the updated query should have new search results" - ); - assert!( - search_view_2.results_editor.is_focused(cx), - "Search view with mismatching query should be focused after search results are available", - ); - }); - - cx.spawn(|mut cx| async move { - window.dispatch_action(search_view_id_2, &ToggleFocus, &mut cx); - }) - .detach(); - deterministic.run_until_parked(); - search_view_id_2.update(cx, |search_view_2, cx| { - assert!( - search_view_2.results_editor.is_focused(cx), - "Search view with matching query should switch focus to the results editor after the toggle focus event", - ); - }); - } - - #[gpui::test] - async fn test_new_project_search_in_directory( - deterministic: Arc, - cx: &mut TestAppContext, - ) { - init_test(cx); - - let fs = FakeFs::new(cx.background()); - fs.insert_tree( - "/dir", - json!({ - "a": { - "one.rs": "const ONE: usize = 1;", - "two.rs": "const TWO: usize = one::ONE + one::ONE;", - }, - "b": { - "three.rs": "const THREE: usize = one::ONE + two::TWO;", - "four.rs": "const FOUR: usize = one::ONE + three::THREE;", - }, - }), - ) - .await; - let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; - let worktree_id = project.read_with(cx, |project, cx| { - project.worktrees(cx).next().unwrap().read(cx).id() - }); - let workspace = cx - .add_window(|cx| Workspace::test_new(project, cx)) - .root(cx); - - let active_item = cx.read(|cx| { - workspace - .read(cx) - .active_pane() - .read(cx) - .active_item() - .and_then(|item| item.downcast::()) - }); - assert!( - active_item.is_none(), - "Expected no search panel to be active, but got: {active_item:?}" - ); - - let one_file_entry = cx.update(|cx| { - workspace - .read(cx) - .project() - .read(cx) - .entry_for_path(&(worktree_id, "a/one.rs").into(), cx) - .expect("no entry for /a/one.rs file") - }); - assert!(one_file_entry.is_file()); - workspace.update(cx, |workspace, cx| { - ProjectSearchView::new_search_in_directory(workspace, &one_file_entry, cx) - }); - let active_search_entry = cx.read(|cx| { - workspace - .read(cx) - .active_pane() - .read(cx) - .active_item() - .and_then(|item| item.downcast::()) - }); - assert!( - active_search_entry.is_none(), - "Expected no search panel to be active for file entry" - ); - - let a_dir_entry = cx.update(|cx| { - workspace - .read(cx) - .project() - .read(cx) - .entry_for_path(&(worktree_id, "a").into(), cx) - .expect("no entry for /a/ directory") - }); - assert!(a_dir_entry.is_dir()); - workspace.update(cx, |workspace, cx| { - ProjectSearchView::new_search_in_directory(workspace, &a_dir_entry, cx) - }); - - let Some(search_view) = cx.read(|cx| { - workspace - .read(cx) - .active_pane() - .read(cx) - .active_item() - .and_then(|item| item.downcast::()) - }) else { - panic!("Search view expected to appear after new search in directory event trigger") - }; - deterministic.run_until_parked(); - search_view.update(cx, |search_view, cx| { - assert!( - search_view.query_editor.is_focused(cx), - "On new search in directory, focus should be moved into query editor" - ); - search_view.excluded_files_editor.update(cx, |editor, cx| { - assert!( - editor.display_text(cx).is_empty(), - "New search in directory should not have any excluded files" - ); - }); - search_view.included_files_editor.update(cx, |editor, cx| { - assert_eq!( - editor.display_text(cx), - a_dir_entry.path.to_str().unwrap(), - "New search in directory should have included dir entry path" - ); - }); - }); - - search_view.update(cx, |search_view, cx| { - search_view - .query_editor - .update(cx, |query_editor, cx| query_editor.set_text("const", cx)); - search_view.search(cx); - }); - deterministic.run_until_parked(); - search_view.update(cx, |search_view, cx| { - assert_eq!( - search_view - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)), - "\n\nconst ONE: usize = 1;\n\n\nconst TWO: usize = one::ONE + one::ONE;", - "New search in directory should have a filter that matches a certain directory" - ); - }); - } - - #[gpui::test] - async fn test_search_query_history(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.background()); - fs.insert_tree( - "/dir", - json!({ - "one.rs": "const ONE: usize = 1;", - "two.rs": "const TWO: usize = one::ONE + one::ONE;", - "three.rs": "const THREE: usize = one::ONE + two::TWO;", - "four.rs": "const FOUR: usize = one::ONE + three::THREE;", - }), - ) - .await; - let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; - let window = cx.add_window(|cx| Workspace::test_new(project, cx)); - let workspace = window.root(cx); - workspace.update(cx, |workspace, cx| { - ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx) - }); - - let search_view = cx.read(|cx| { - workspace - .read(cx) - .active_pane() - .read(cx) - .active_item() - .and_then(|item| item.downcast::()) - .expect("Search view expected to appear after new search event trigger") - }); - - let search_bar = window.add_view(cx, |cx| { - let mut search_bar = ProjectSearchBar::new(); - search_bar.set_active_pane_item(Some(&search_view), cx); - // search_bar.show(cx); - search_bar - }); - - // Add 3 search items into the history + another unsubmitted one. - search_view.update(cx, |search_view, cx| { - search_view.search_options = SearchOptions::CASE_SENSITIVE; - search_view - .query_editor - .update(cx, |query_editor, cx| query_editor.set_text("ONE", cx)); - search_view.search(cx); - }); - cx.foreground().run_until_parked(); - search_view.update(cx, |search_view, cx| { - search_view - .query_editor - .update(cx, |query_editor, cx| query_editor.set_text("TWO", cx)); - search_view.search(cx); - }); - cx.foreground().run_until_parked(); - search_view.update(cx, |search_view, cx| { - search_view - .query_editor - .update(cx, |query_editor, cx| query_editor.set_text("THREE", cx)); - search_view.search(cx); - }); - cx.foreground().run_until_parked(); - search_view.update(cx, |search_view, cx| { - search_view.query_editor.update(cx, |query_editor, cx| { - query_editor.set_text("JUST_TEXT_INPUT", cx) - }); - }); - cx.foreground().run_until_parked(); - - // Ensure that the latest input with search settings is active. - search_view.update(cx, |search_view, cx| { - assert_eq!( - search_view.query_editor.read(cx).text(cx), - "JUST_TEXT_INPUT" - ); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - - // Next history query after the latest should set the query to the empty string. - search_bar.update(cx, |search_bar, cx| { - search_bar.next_history_query(&NextHistoryQuery, cx); - }); - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), ""); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - search_bar.update(cx, |search_bar, cx| { - search_bar.next_history_query(&NextHistoryQuery, cx); - }); - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), ""); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - - // First previous query for empty current query should set the query to the latest submitted one. - search_bar.update(cx, |search_bar, cx| { - search_bar.previous_history_query(&PreviousHistoryQuery, cx); - }); - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE"); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - - // Further previous items should go over the history in reverse order. - search_bar.update(cx, |search_bar, cx| { - search_bar.previous_history_query(&PreviousHistoryQuery, cx); - }); - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO"); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - - // Previous items should never go behind the first history item. - search_bar.update(cx, |search_bar, cx| { - search_bar.previous_history_query(&PreviousHistoryQuery, cx); - }); - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "ONE"); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - search_bar.update(cx, |search_bar, cx| { - search_bar.previous_history_query(&PreviousHistoryQuery, cx); - }); - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "ONE"); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - - // Next items should go over the history in the original order. - search_bar.update(cx, |search_bar, cx| { - search_bar.next_history_query(&NextHistoryQuery, cx); - }); - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO"); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - - search_view.update(cx, |search_view, cx| { - search_view - .query_editor - .update(cx, |query_editor, cx| query_editor.set_text("TWO_NEW", cx)); - search_view.search(cx); - }); - cx.foreground().run_until_parked(); - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO_NEW"); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - - // New search input should add another entry to history and move the selection to the end of the history. - search_bar.update(cx, |search_bar, cx| { - search_bar.previous_history_query(&PreviousHistoryQuery, cx); - }); - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE"); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - search_bar.update(cx, |search_bar, cx| { - search_bar.previous_history_query(&PreviousHistoryQuery, cx); - }); - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO"); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - search_bar.update(cx, |search_bar, cx| { - search_bar.next_history_query(&NextHistoryQuery, cx); - }); - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE"); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - search_bar.update(cx, |search_bar, cx| { - search_bar.next_history_query(&NextHistoryQuery, cx); - }); - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO_NEW"); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - search_bar.update(cx, |search_bar, cx| { - search_bar.next_history_query(&NextHistoryQuery, cx); - }); - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), ""); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - } - - pub fn init_test(cx: &mut TestAppContext) { - cx.foreground().forbid_parking(); - let fonts = cx.font_cache(); - let mut theme = gpui::fonts::with_font_cache(fonts.clone(), theme::Theme::default); - theme.search.match_background = Color::red(); - - cx.update(|cx| { - cx.set_global(SettingsStore::test(cx)); - cx.set_global(ActiveSearches::default()); - settings::register::(cx); - - theme::init((), cx); - cx.update_global::(|store, _| { - let mut settings = store.get::(None).clone(); - settings.theme = Arc::new(theme); - store.override_global(settings) - }); - - language::init(cx); - client::init_settings(cx); - editor::init(cx); - workspace::init_settings(cx); - Project::init_settings(cx); - super::init(cx); - }); - } -} +// #[cfg(test)] +// pub mod tests { +// use super::*; +// use editor::DisplayPoint; +// use gpui::{color::Color, executor::Deterministic, TestAppContext}; +// use project::FakeFs; +// use semantic_index::semantic_index_settings::SemanticIndexSettings; +// use serde_json::json; +// use settings::SettingsStore; +// use std::sync::Arc; +// use theme::ThemeSettings; + +// #[gpui::test] +// async fn test_project_search(deterministic: Arc, cx: &mut TestAppContext) { +// init_test(cx); + +// let fs = FakeFs::new(cx.background_executor()); +// fs.insert_tree( +// "/dir", +// json!({ +// "one.rs": "const ONE: usize = 1;", +// "two.rs": "const TWO: usize = one::ONE + one::ONE;", +// "three.rs": "const THREE: usize = one::ONE + two::TWO;", +// "four.rs": "const FOUR: usize = one::ONE + three::THREE;", +// }), +// ) +// .await; +// let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; +// let search = cx.add_model(|cx| ProjectSearch::new(project, cx)); +// let search_view = cx +// .add_window(|cx| ProjectSearchView::new(search.clone(), cx, None)) +// .root(cx); + +// search_view.update(cx, |search_view, cx| { +// search_view +// .query_editor +// .update(cx, |query_editor, cx| query_editor.set_text("TWO", cx)); +// search_view.search(cx); +// }); +// deterministic.run_until_parked(); +// search_view.update(cx, |search_view, cx| { +// assert_eq!( +// search_view +// .results_editor +// .update(cx, |editor, cx| editor.display_text(cx)), +// "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;" +// ); +// assert_eq!( +// search_view +// .results_editor +// .update(cx, |editor, cx| editor.all_text_background_highlights(cx)), +// &[ +// ( +// DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35), +// Color::red() +// ), +// ( +// DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40), +// Color::red() +// ), +// ( +// DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9), +// Color::red() +// ) +// ] +// ); +// assert_eq!(search_view.active_match_index, Some(0)); +// assert_eq!( +// search_view +// .results_editor +// .update(cx, |editor, cx| editor.selections.display_ranges(cx)), +// [DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35)] +// ); + +// search_view.select_match(Direction::Next, cx); +// }); + +// search_view.update(cx, |search_view, cx| { +// assert_eq!(search_view.active_match_index, Some(1)); +// assert_eq!( +// search_view +// .results_editor +// .update(cx, |editor, cx| editor.selections.display_ranges(cx)), +// [DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40)] +// ); +// search_view.select_match(Direction::Next, cx); +// }); + +// search_view.update(cx, |search_view, cx| { +// assert_eq!(search_view.active_match_index, Some(2)); +// assert_eq!( +// search_view +// .results_editor +// .update(cx, |editor, cx| editor.selections.display_ranges(cx)), +// [DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9)] +// ); +// search_view.select_match(Direction::Next, cx); +// }); + +// search_view.update(cx, |search_view, cx| { +// assert_eq!(search_view.active_match_index, Some(0)); +// assert_eq!( +// search_view +// .results_editor +// .update(cx, |editor, cx| editor.selections.display_ranges(cx)), +// [DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35)] +// ); +// search_view.select_match(Direction::Prev, cx); +// }); + +// search_view.update(cx, |search_view, cx| { +// assert_eq!(search_view.active_match_index, Some(2)); +// assert_eq!( +// search_view +// .results_editor +// .update(cx, |editor, cx| editor.selections.display_ranges(cx)), +// [DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9)] +// ); +// search_view.select_match(Direction::Prev, cx); +// }); + +// search_view.update(cx, |search_view, cx| { +// assert_eq!(search_view.active_match_index, Some(1)); +// assert_eq!( +// search_view +// .results_editor +// .update(cx, |editor, cx| editor.selections.display_ranges(cx)), +// [DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40)] +// ); +// }); +// } + +// #[gpui::test] +// async fn test_project_search_focus(deterministic: Arc, cx: &mut TestAppContext) { +// init_test(cx); + +// let fs = FakeFs::new(cx.background_executor()); +// fs.insert_tree( +// "/dir", +// json!({ +// "one.rs": "const ONE: usize = 1;", +// "two.rs": "const TWO: usize = one::ONE + one::ONE;", +// "three.rs": "const THREE: usize = one::ONE + two::TWO;", +// "four.rs": "const FOUR: usize = one::ONE + three::THREE;", +// }), +// ) +// .await; +// let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); +// let workspace = window.root(cx); + +// let active_item = cx.read(|cx| { +// workspace +// .read(cx) +// .active_pane() +// .read(cx) +// .active_item() +// .and_then(|item| item.downcast::()) +// }); +// assert!( +// active_item.is_none(), +// "Expected no search panel to be active, but got: {active_item:?}" +// ); + +// workspace.update(cx, |workspace, cx| { +// ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx) +// }); + +// let Some(search_view) = cx.read(|cx| { +// workspace +// .read(cx) +// .active_pane() +// .read(cx) +// .active_item() +// .and_then(|item| item.downcast::()) +// }) else { +// panic!("Search view expected to appear after new search event trigger") +// }; +// let search_view_id = search_view.id(); + +// cx.spawn(|mut cx| async move { +// window.dispatch_action(search_view_id, &ToggleFocus, &mut cx); +// }) +// .detach(); +// deterministic.run_until_parked(); +// search_view.update(cx, |search_view, cx| { +// assert!( +// search_view.query_editor.is_focused(cx), +// "Empty search view should be focused after the toggle focus event: no results panel to focus on", +// ); +// }); + +// search_view.update(cx, |search_view, cx| { +// let query_editor = &search_view.query_editor; +// assert!( +// query_editor.is_focused(cx), +// "Search view should be focused after the new search view is activated", +// ); +// let query_text = query_editor.read(cx).text(cx); +// assert!( +// query_text.is_empty(), +// "New search query should be empty but got '{query_text}'", +// ); +// let results_text = search_view +// .results_editor +// .update(cx, |editor, cx| editor.display_text(cx)); +// assert!( +// results_text.is_empty(), +// "Empty search view should have no results but got '{results_text}'" +// ); +// }); + +// search_view.update(cx, |search_view, cx| { +// search_view.query_editor.update(cx, |query_editor, cx| { +// query_editor.set_text("sOMETHINGtHATsURELYdOESnOTeXIST", cx) +// }); +// search_view.search(cx); +// }); +// deterministic.run_until_parked(); +// search_view.update(cx, |search_view, cx| { +// let results_text = search_view +// .results_editor +// .update(cx, |editor, cx| editor.display_text(cx)); +// assert!( +// results_text.is_empty(), +// "Search view for mismatching query should have no results but got '{results_text}'" +// ); +// assert!( +// search_view.query_editor.is_focused(cx), +// "Search view should be focused after mismatching query had been used in search", +// ); +// }); +// cx.spawn( +// |mut cx| async move { window.dispatch_action(search_view_id, &ToggleFocus, &mut cx) }, +// ) +// .detach(); +// deterministic.run_until_parked(); +// search_view.update(cx, |search_view, cx| { +// assert!( +// search_view.query_editor.is_focused(cx), +// "Search view with mismatching query should be focused after the toggle focus event: still no results panel to focus on", +// ); +// }); + +// search_view.update(cx, |search_view, cx| { +// search_view +// .query_editor +// .update(cx, |query_editor, cx| query_editor.set_text("TWO", cx)); +// search_view.search(cx); +// }); +// deterministic.run_until_parked(); +// search_view.update(cx, |search_view, cx| { +// assert_eq!( +// search_view +// .results_editor +// .update(cx, |editor, cx| editor.display_text(cx)), +// "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;", +// "Search view results should match the query" +// ); +// assert!( +// search_view.results_editor.is_focused(cx), +// "Search view with mismatching query should be focused after search results are available", +// ); +// }); +// cx.spawn(|mut cx| async move { +// window.dispatch_action(search_view_id, &ToggleFocus, &mut cx); +// }) +// .detach(); +// deterministic.run_until_parked(); +// search_view.update(cx, |search_view, cx| { +// assert!( +// search_view.results_editor.is_focused(cx), +// "Search view with matching query should still have its results editor focused after the toggle focus event", +// ); +// }); + +// workspace.update(cx, |workspace, cx| { +// ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx) +// }); +// deterministic.run_until_parked(); +// let Some(search_view_2) = cx.read(|cx| { +// workspace +// .read(cx) +// .active_pane() +// .read(cx) +// .active_item() +// .and_then(|item| item.downcast::()) +// }) else { +// panic!("Search view expected to appear after new search event trigger") +// }; +// let search_view_id_2 = search_view_2.id(); +// assert_ne!( +// search_view_2, search_view, +// "New search view should be open after `workspace::NewSearch` event" +// ); + +// search_view.update(cx, |search_view, cx| { +// assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO", "First search view should not have an updated query"); +// assert_eq!( +// search_view +// .results_editor +// .update(cx, |editor, cx| editor.display_text(cx)), +// "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;", +// "Results of the first search view should not update too" +// ); +// assert!( +// !search_view.query_editor.is_focused(cx), +// "Focus should be moved away from the first search view" +// ); +// }); + +// search_view_2.update(cx, |search_view_2, cx| { +// assert_eq!( +// search_view_2.query_editor.read(cx).text(cx), +// "two", +// "New search view should get the query from the text cursor was at during the event spawn (first search view's first result)" +// ); +// assert_eq!( +// search_view_2 +// .results_editor +// .update(cx, |editor, cx| editor.display_text(cx)), +// "", +// "No search results should be in the 2nd view yet, as we did not spawn a search for it" +// ); +// assert!( +// search_view_2.query_editor.is_focused(cx), +// "Focus should be moved into query editor fo the new window" +// ); +// }); + +// search_view_2.update(cx, |search_view_2, cx| { +// search_view_2 +// .query_editor +// .update(cx, |query_editor, cx| query_editor.set_text("FOUR", cx)); +// search_view_2.search(cx); +// }); +// deterministic.run_until_parked(); +// search_view_2.update(cx, |search_view_2, cx| { +// assert_eq!( +// search_view_2 +// .results_editor +// .update(cx, |editor, cx| editor.display_text(cx)), +// "\n\nconst FOUR: usize = one::ONE + three::THREE;", +// "New search view with the updated query should have new search results" +// ); +// assert!( +// search_view_2.results_editor.is_focused(cx), +// "Search view with mismatching query should be focused after search results are available", +// ); +// }); + +// cx.spawn(|mut cx| async move { +// window.dispatch_action(search_view_id_2, &ToggleFocus, &mut cx); +// }) +// .detach(); +// deterministic.run_until_parked(); +// search_view_id_2.update(cx, |search_view_2, cx| { +// assert!( +// search_view_2.results_editor.is_focused(cx), +// "Search view with matching query should switch focus to the results editor after the toggle focus event", +// ); +// }); +// } + +// #[gpui::test] +// async fn test_new_project_search_in_directory( +// deterministic: Arc, +// cx: &mut TestAppContext, +// ) { +// init_test(cx); + +// let fs = FakeFs::new(cx.background_executor()); +// fs.insert_tree( +// "/dir", +// json!({ +// "a": { +// "one.rs": "const ONE: usize = 1;", +// "two.rs": "const TWO: usize = one::ONE + one::ONE;", +// }, +// "b": { +// "three.rs": "const THREE: usize = one::ONE + two::TWO;", +// "four.rs": "const FOUR: usize = one::ONE + three::THREE;", +// }, +// }), +// ) +// .await; +// let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; +// let worktree_id = project.read_with(cx, |project, cx| { +// project.worktrees(cx).next().unwrap().read(cx).id() +// }); +// let workspace = cx +// .add_window(|cx| Workspace::test_new(project, cx)) +// .root(cx); + +// let active_item = cx.read(|cx| { +// workspace +// .read(cx) +// .active_pane() +// .read(cx) +// .active_item() +// .and_then(|item| item.downcast::()) +// }); +// assert!( +// active_item.is_none(), +// "Expected no search panel to be active, but got: {active_item:?}" +// ); + +// let one_file_entry = cx.update(|cx| { +// workspace +// .read(cx) +// .project() +// .read(cx) +// .entry_for_path(&(worktree_id, "a/one.rs").into(), cx) +// .expect("no entry for /a/one.rs file") +// }); +// assert!(one_file_entry.is_file()); +// workspace.update(cx, |workspace, cx| { +// ProjectSearchView::new_search_in_directory(workspace, &one_file_entry, cx) +// }); +// let active_search_entry = cx.read(|cx| { +// workspace +// .read(cx) +// .active_pane() +// .read(cx) +// .active_item() +// .and_then(|item| item.downcast::()) +// }); +// assert!( +// active_search_entry.is_none(), +// "Expected no search panel to be active for file entry" +// ); + +// let a_dir_entry = cx.update(|cx| { +// workspace +// .read(cx) +// .project() +// .read(cx) +// .entry_for_path(&(worktree_id, "a").into(), cx) +// .expect("no entry for /a/ directory") +// }); +// assert!(a_dir_entry.is_dir()); +// workspace.update(cx, |workspace, cx| { +// ProjectSearchView::new_search_in_directory(workspace, &a_dir_entry, cx) +// }); + +// let Some(search_view) = cx.read(|cx| { +// workspace +// .read(cx) +// .active_pane() +// .read(cx) +// .active_item() +// .and_then(|item| item.downcast::()) +// }) else { +// panic!("Search view expected to appear after new search in directory event trigger") +// }; +// deterministic.run_until_parked(); +// search_view.update(cx, |search_view, cx| { +// assert!( +// search_view.query_editor.is_focused(cx), +// "On new search in directory, focus should be moved into query editor" +// ); +// search_view.excluded_files_editor.update(cx, |editor, cx| { +// assert!( +// editor.display_text(cx).is_empty(), +// "New search in directory should not have any excluded files" +// ); +// }); +// search_view.included_files_editor.update(cx, |editor, cx| { +// assert_eq!( +// editor.display_text(cx), +// a_dir_entry.path.to_str().unwrap(), +// "New search in directory should have included dir entry path" +// ); +// }); +// }); + +// search_view.update(cx, |search_view, cx| { +// search_view +// .query_editor +// .update(cx, |query_editor, cx| query_editor.set_text("const", cx)); +// search_view.search(cx); +// }); +// deterministic.run_until_parked(); +// search_view.update(cx, |search_view, cx| { +// assert_eq!( +// search_view +// .results_editor +// .update(cx, |editor, cx| editor.display_text(cx)), +// "\n\nconst ONE: usize = 1;\n\n\nconst TWO: usize = one::ONE + one::ONE;", +// "New search in directory should have a filter that matches a certain directory" +// ); +// }); +// } + +// #[gpui::test] +// async fn test_search_query_history(cx: &mut TestAppContext) { +// init_test(cx); + +// let fs = FakeFs::new(cx.background_executor()); +// fs.insert_tree( +// "/dir", +// json!({ +// "one.rs": "const ONE: usize = 1;", +// "two.rs": "const TWO: usize = one::ONE + one::ONE;", +// "three.rs": "const THREE: usize = one::ONE + two::TWO;", +// "four.rs": "const FOUR: usize = one::ONE + three::THREE;", +// }), +// ) +// .await; +// let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); +// let workspace = window.root(cx); +// workspace.update(cx, |workspace, cx| { +// ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx) +// }); + +// let search_view = cx.read(|cx| { +// workspace +// .read(cx) +// .active_pane() +// .read(cx) +// .active_item() +// .and_then(|item| item.downcast::()) +// .expect("Search view expected to appear after new search event trigger") +// }); + +// let search_bar = window.add_view(cx, |cx| { +// let mut search_bar = ProjectSearchBar::new(); +// search_bar.set_active_pane_item(Some(&search_view), cx); +// // search_bar.show(cx); +// search_bar +// }); + +// // Add 3 search items into the history + another unsubmitted one. +// search_view.update(cx, |search_view, cx| { +// search_view.search_options = SearchOptions::CASE_SENSITIVE; +// search_view +// .query_editor +// .update(cx, |query_editor, cx| query_editor.set_text("ONE", cx)); +// search_view.search(cx); +// }); +// cx.foreground().run_until_parked(); +// search_view.update(cx, |search_view, cx| { +// search_view +// .query_editor +// .update(cx, |query_editor, cx| query_editor.set_text("TWO", cx)); +// search_view.search(cx); +// }); +// cx.foreground().run_until_parked(); +// search_view.update(cx, |search_view, cx| { +// search_view +// .query_editor +// .update(cx, |query_editor, cx| query_editor.set_text("THREE", cx)); +// search_view.search(cx); +// }); +// cx.foreground().run_until_parked(); +// search_view.update(cx, |search_view, cx| { +// search_view.query_editor.update(cx, |query_editor, cx| { +// query_editor.set_text("JUST_TEXT_INPUT", cx) +// }); +// }); +// cx.foreground().run_until_parked(); + +// // Ensure that the latest input with search settings is active. +// search_view.update(cx, |search_view, cx| { +// assert_eq!( +// search_view.query_editor.read(cx).text(cx), +// "JUST_TEXT_INPUT" +// ); +// assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); +// }); + +// // Next history query after the latest should set the query to the empty string. +// search_bar.update(cx, |search_bar, cx| { +// search_bar.next_history_query(&NextHistoryQuery, cx); +// }); +// search_view.update(cx, |search_view, cx| { +// assert_eq!(search_view.query_editor.read(cx).text(cx), ""); +// assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); +// }); +// search_bar.update(cx, |search_bar, cx| { +// search_bar.next_history_query(&NextHistoryQuery, cx); +// }); +// search_view.update(cx, |search_view, cx| { +// assert_eq!(search_view.query_editor.read(cx).text(cx), ""); +// assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); +// }); + +// // First previous query for empty current query should set the query to the latest submitted one. +// search_bar.update(cx, |search_bar, cx| { +// search_bar.previous_history_query(&PreviousHistoryQuery, cx); +// }); +// search_view.update(cx, |search_view, cx| { +// assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE"); +// assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); +// }); + +// // Further previous items should go over the history in reverse order. +// search_bar.update(cx, |search_bar, cx| { +// search_bar.previous_history_query(&PreviousHistoryQuery, cx); +// }); +// search_view.update(cx, |search_view, cx| { +// assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO"); +// assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); +// }); + +// // Previous items should never go behind the first history item. +// search_bar.update(cx, |search_bar, cx| { +// search_bar.previous_history_query(&PreviousHistoryQuery, cx); +// }); +// search_view.update(cx, |search_view, cx| { +// assert_eq!(search_view.query_editor.read(cx).text(cx), "ONE"); +// assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); +// }); +// search_bar.update(cx, |search_bar, cx| { +// search_bar.previous_history_query(&PreviousHistoryQuery, cx); +// }); +// search_view.update(cx, |search_view, cx| { +// assert_eq!(search_view.query_editor.read(cx).text(cx), "ONE"); +// assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); +// }); + +// // Next items should go over the history in the original order. +// search_bar.update(cx, |search_bar, cx| { +// search_bar.next_history_query(&NextHistoryQuery, cx); +// }); +// search_view.update(cx, |search_view, cx| { +// assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO"); +// assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); +// }); + +// search_view.update(cx, |search_view, cx| { +// search_view +// .query_editor +// .update(cx, |query_editor, cx| query_editor.set_text("TWO_NEW", cx)); +// search_view.search(cx); +// }); +// cx.foreground().run_until_parked(); +// search_view.update(cx, |search_view, cx| { +// assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO_NEW"); +// assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); +// }); + +// // New search input should add another entry to history and move the selection to the end of the history. +// search_bar.update(cx, |search_bar, cx| { +// search_bar.previous_history_query(&PreviousHistoryQuery, cx); +// }); +// search_view.update(cx, |search_view, cx| { +// assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE"); +// assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); +// }); +// search_bar.update(cx, |search_bar, cx| { +// search_bar.previous_history_query(&PreviousHistoryQuery, cx); +// }); +// search_view.update(cx, |search_view, cx| { +// assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO"); +// assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); +// }); +// search_bar.update(cx, |search_bar, cx| { +// search_bar.next_history_query(&NextHistoryQuery, cx); +// }); +// search_view.update(cx, |search_view, cx| { +// assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE"); +// assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); +// }); +// search_bar.update(cx, |search_bar, cx| { +// search_bar.next_history_query(&NextHistoryQuery, cx); +// }); +// search_view.update(cx, |search_view, cx| { +// assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO_NEW"); +// assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); +// }); +// search_bar.update(cx, |search_bar, cx| { +// search_bar.next_history_query(&NextHistoryQuery, cx); +// }); +// search_view.update(cx, |search_view, cx| { +// assert_eq!(search_view.query_editor.read(cx).text(cx), ""); +// assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); +// }); +// } + +// pub fn init_test(cx: &mut TestAppContext) { +// cx.foreground().forbid_parking(); +// let fonts = cx.font_cache(); +// let mut theme = gpui::fonts::with_font_cache(fonts.clone(), theme::Theme::default); +// theme.search.match_background = Color::red(); + +// cx.update(|cx| { +// cx.set_global(SettingsStore::test(cx)); +// cx.set_global(ActiveSearches::default()); +// settings::register::(cx); + +// theme::init((), cx); +// cx.update_global::(|store, _| { +// let mut settings = store.get::(None).clone(); +// settings.theme = Arc::new(theme); +// store.override_global(settings) +// }); + +// language::init(cx); +// client::init_settings(cx); +// editor::init(cx); +// workspace::init_settings(cx); +// Project::init_settings(cx); +// super::init(cx); +// }); +// } +// } diff --git a/crates/search2/src/search.rs b/crates/search2/src/search.rs index 5921bb059a4a7b9f56ee1c09ba02013ba0bf0bce..202893d52d1290656655ab556d3330d881a11922 100644 --- a/crates/search2/src/search.rs +++ b/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; } } diff --git a/crates/ui2/src/components/icon.rs b/crates/ui2/src/components/icon.rs index 8cea775aed4fae6001fe7c0c5b424baf0da549e1..b68aa579c026d518f33e9eabc94824270c8104e7 100644 --- a/crates/ui2/src/components/icon.rs +++ b/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", diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index 0a44bf6ec0fc7e25fb3135fd91379dc8a4fd7ba9..d7150c4824e79ae155654b99ab00de804b24005d 100644 --- a/crates/zed2/src/zed2.rs +++ b/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, 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);