use crate::{ buffer_search::Deploy, BufferSearchBar, FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleIncludeIgnored, ToggleRegex, ToggleReplace, ToggleWholeWord,};use anyhow::Context as _;use collections::{HashMap, HashSet};use editor::{ actions::SelectAll, items::active_match_index, scroll::Autoscroll, Anchor, Editor, EditorElement, EditorEvent, EditorSettings, EditorStyle, MultiBuffer, MAX_TAB_TITLE_LEN,};use futures::StreamExt;use gpui::{ actions, div, Action, AnyElement, AnyView, App, Axis, Context, Entity, EntityId, EventEmitter, FocusHandle, Focusable, Global, Hsla, InteractiveElement, IntoElement, KeyContext, ParentElement, Point, Render, SharedString, Styled, Subscription, Task, TextStyle, UpdateGlobal, WeakEntity, Window,};use language::{Buffer, Language};use menu::Confirm;use project::{ search::{SearchInputKind, SearchQuery}, search_history::SearchHistoryCursor, Project, ProjectPath,};use settings::Settings;use std::{ any::{Any, TypeId}, mem, ops::{Not, Range}, path::Path, pin::pin, sync::Arc,};use theme::ThemeSettings;use ui::{ h_flex, prelude::*, utils::SearchInputWidth, v_flex, Icon, IconButton, IconButtonShape, IconName, KeyBinding, Label, LabelCommon, LabelSize, Toggleable, Tooltip,};use util::paths::PathMatcher;use workspace::{ item::{BreadcrumbText, Item, ItemEvent, ItemHandle}, searchable::{Direction, SearchableItem, SearchableItemHandle}, DeploySearch, ItemNavHistory, NewSearch, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId,}; actions!( project_search, [SearchInNew, ToggleFocus, NextField, ToggleFilters]); #[derive(Default)]struct ActiveSettings(HashMap<WeakEntity<Project>, ProjectSearchSettings>); impl Global for ActiveSettings {} pub fn init(cx: &mut App) { cx.set_global(ActiveSettings::default()); cx.observe_new(|workspace: &mut Workspace, _window, _cx| { register_workspace_action(workspace, move |search_bar, _: &Deploy, window, cx| { search_bar.focus_search(window, cx); }); register_workspace_action(workspace, move |search_bar, _: &FocusSearch, window, cx| { search_bar.focus_search(window, cx); }); register_workspace_action( workspace, move |search_bar, _: &ToggleFilters, window, cx| { search_bar.toggle_filters(window, cx); }, ); register_workspace_action( workspace, move |search_bar, _: &ToggleCaseSensitive, _, cx| { search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx); }, ); register_workspace_action(workspace, move |search_bar, _: &ToggleWholeWord, _, cx| { search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx); }); register_workspace_action(workspace, move |search_bar, _: &ToggleRegex, _, cx| { search_bar.toggle_search_option(SearchOptions::REGEX, cx); }); register_workspace_action( workspace, move |search_bar, action: &ToggleReplace, window, cx| { search_bar.toggle_replace(action, window, cx) }, ); register_workspace_action( workspace, move |search_bar, action: &SelectPrevMatch, window, cx| { search_bar.select_prev_match(action, window, cx) }, ); register_workspace_action( workspace, move |search_bar, action: &SelectNextMatch, window, cx| { search_bar.select_next_match(action, window, cx) }, ); // Only handle search_in_new if there is a search present register_workspace_action_for_present_search(workspace, |workspace, action, window, cx| { ProjectSearchView::search_in_new(workspace, action, window, cx) }); // Both on present and dismissed search, we need to unconditionally handle those actions to focus from the editor. workspace.register_action(move |workspace, action: &DeploySearch, window, cx| { if workspace.has_active_modal(window, cx) { cx.propagate(); return; } ProjectSearchView::deploy_search(workspace, action, window, cx); cx.notify(); }); workspace.register_action(move |workspace, action: &NewSearch, window, cx| { if workspace.has_active_modal(window, cx) { cx.propagate(); return; } ProjectSearchView::new_search(workspace, action, window, cx); cx.notify(); }); }) .detach();} fn is_contains_uppercase(str: &str) -> bool { str.chars().any(|c| c.is_uppercase())} pub struct ProjectSearch { project: Entity<Project>, excerpts: Entity<MultiBuffer>, pending_search: Option<Task<Option<()>>>, match_ranges: Vec<Range<Anchor>>, active_query: Option<SearchQuery>, last_search_query_text: Option<String>, search_id: usize, no_results: Option<bool>, limit_reached: bool, search_history_cursor: SearchHistoryCursor, search_included_history_cursor: SearchHistoryCursor, search_excluded_history_cursor: SearchHistoryCursor,} #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]enum InputPanel { Query, Exclude, Include,} pub struct ProjectSearchView { workspace: WeakEntity<Workspace>, focus_handle: FocusHandle, entity: Entity<ProjectSearch>, query_editor: Entity<Editor>, replacement_editor: Entity<Editor>, results_editor: Entity<Editor>, search_options: SearchOptions, panels_with_errors: HashSet<InputPanel>, active_match_index: Option<usize>, search_id: usize, included_files_editor: Entity<Editor>, excluded_files_editor: Entity<Editor>, filters_enabled: bool, replace_enabled: bool, included_opened_only: bool, regex_language: Option<Arc<Language>>, _subscriptions: Vec<Subscription>,} #[derive(Debug, Clone)]pub struct ProjectSearchSettings { search_options: SearchOptions, filters_enabled: bool,} pub struct ProjectSearchBar { active_project_search: Option<Entity<ProjectSearchView>>, subscription: Option<Subscription>,} impl ProjectSearch { pub fn new(project: Entity<Project>, cx: &mut Context<Self>) -> Self { let capability = project.read(cx).capability(); Self { project, excerpts: cx.new(|_| MultiBuffer::new(capability)), pending_search: Default::default(), match_ranges: Default::default(), active_query: None, last_search_query_text: None, search_id: 0, no_results: None, limit_reached: false, search_history_cursor: Default::default(), search_included_history_cursor: Default::default(), search_excluded_history_cursor: Default::default(), } } fn clone(&self, cx: &mut Context<Self>) -> Entity<Self> { cx.new(|cx| Self { project: self.project.clone(), excerpts: self .excerpts .update(cx, |excerpts, cx| cx.new(|cx| excerpts.clone(cx))), pending_search: Default::default(), match_ranges: self.match_ranges.clone(), active_query: self.active_query.clone(), last_search_query_text: self.last_search_query_text.clone(), search_id: self.search_id, no_results: self.no_results, limit_reached: self.limit_reached, search_history_cursor: self.search_history_cursor.clone(), search_included_history_cursor: self.search_included_history_cursor.clone(), search_excluded_history_cursor: self.search_excluded_history_cursor.clone(), }) } fn cursor(&self, kind: SearchInputKind) -> &SearchHistoryCursor { match kind { SearchInputKind::Query => &self.search_history_cursor, SearchInputKind::Include => &self.search_included_history_cursor, SearchInputKind::Exclude => &self.search_excluded_history_cursor, } } fn cursor_mut(&mut self, kind: SearchInputKind) -> &mut SearchHistoryCursor { match kind { SearchInputKind::Query => &mut self.search_history_cursor, SearchInputKind::Include => &mut self.search_included_history_cursor, SearchInputKind::Exclude => &mut self.search_excluded_history_cursor, } } fn search(&mut self, query: SearchQuery, cx: &mut Context<Self>) { let search = self.project.update(cx, |project, cx| { project .search_history_mut(SearchInputKind::Query) .add(&mut self.search_history_cursor, query.as_str().to_string()); let included = query.as_inner().files_to_include().sources().join(","); if !included.is_empty() { project .search_history_mut(SearchInputKind::Include) .add(&mut self.search_included_history_cursor, included); } let excluded = query.as_inner().files_to_exclude().sources().join(","); if !excluded.is_empty() { project .search_history_mut(SearchInputKind::Exclude) .add(&mut self.search_excluded_history_cursor, excluded); } project.search(query.clone(), cx) }); self.last_search_query_text = Some(query.as_str().to_string()); self.search_id += 1; self.active_query = Some(query); self.match_ranges.clear(); self.pending_search = Some(cx.spawn(|this, mut cx| async move { let mut matches = pin!(search.ready_chunks(1024)); 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); this.limit_reached = false; }) .ok()?; let mut limit_reached = false; while let Some(results) = matches.next().await { let mut buffers_with_ranges = Vec::with_capacity(results.len()); for result in results { match result { project::search::SearchResult::Buffer { buffer, ranges } => { buffers_with_ranges.push((buffer, ranges)); } project::search::SearchResult::LimitReached => { limit_reached = true; } } } let match_ranges = this .update(&mut cx, |this, cx| { this.excerpts.update(cx, |excerpts, cx| { excerpts.push_multiple_excerpts_with_context_lines( buffers_with_ranges, editor::DEFAULT_MULTIBUFFER_CONTEXT, cx, ) }) }) .ok()? .await; this.update(&mut cx, |this, cx| { this.match_ranges.extend(match_ranges); cx.notify(); }) .ok()?; } this.update(&mut cx, |this, cx| { if !this.match_ranges.is_empty() { this.no_results = Some(false); } this.limit_reached = limit_reached; this.pending_search.take(); cx.notify(); }) .ok()?; None })); cx.notify(); }} #[derive(Clone, Debug, PartialEq, Eq)]pub enum ViewEvent { UpdateTab, Activate, EditorEvent(editor::EditorEvent), Dismiss,} impl EventEmitter<ViewEvent> for ProjectSearchView {} impl Render for ProjectSearchView { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { if self.has_matches() { div() .flex_1() .size_full() .track_focus(&self.focus_handle(cx)) .child(self.results_editor.clone()) } else { let model = self.entity.read(cx); let has_no_results = model.no_results.unwrap_or(false); let is_search_underway = model.pending_search.is_some(); let heading_text = if is_search_underway { "Searching…" } else if has_no_results { "No Results" } else { "Search All Files" }; let heading_text = div() .justify_center() .child(Label::new(heading_text).size(LabelSize::Large)); let page_content: Option<AnyElement> = if let Some(no_results) = model.no_results { if model.pending_search.is_none() && no_results { Some( Label::new("No results found in this project for the provided query") .size(LabelSize::Small) .into_any_element(), ) } else { None } } else { Some(self.landing_text_minor(window, cx).into_any_element()) }; let page_content = page_content.map(|text| div().child(text)); v_flex() .size_full() .items_center() .justify_center() .overflow_hidden() .bg(cx.theme().colors().editor_background) .track_focus(&self.focus_handle(cx)) .child( v_flex() .id("project-search-landing-page") .overflow_y_scroll() .max_w_80() .gap_1() .child(heading_text) .children(page_content), ) } }} impl Focusable for ProjectSearchView { fn focus_handle(&self, _: &App) -> gpui::FocusHandle { self.focus_handle.clone() }} impl Item for ProjectSearchView { type Event = ViewEvent; fn tab_tooltip_text(&self, cx: &App) -> Option<SharedString> { let query_text = self.query_editor.read(cx).text(cx); query_text .is_empty() .not() .then(|| query_text.into()) .or_else(|| Some("Project Search".into())) } fn act_as_type<'a>( &'a self, type_id: TypeId, self_handle: &'a Entity<Self>, _: &'a App, ) -> Option<AnyView> { if type_id == TypeId::of::<Self>() { Some(self_handle.clone().into()) } else if type_id == TypeId::of::<Editor>() { Some(self.results_editor.clone().into()) } else { None } } fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> { Some(Box::new(self.results_editor.clone())) } fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) { self.results_editor .update(cx, |editor, cx| editor.deactivated(window, cx)); } fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> { Some(Icon::new(IconName::MagnifyingGlass)) } fn tab_content_text(&self, _: &Window, cx: &App) -> Option<SharedString> { let last_query: Option<SharedString> = self .entity .read(cx) .last_search_query_text .as_ref() .map(|query| { let query = query.replace('\n', ""); let query_text = util::truncate_and_trailoff(&query, MAX_TAB_TITLE_LEN); query_text.into() }); Some( last_query .filter(|query| !query.is_empty()) .unwrap_or_else(|| "Project Search".into()), ) } fn telemetry_event_text(&self) -> Option<&'static str> { Some("Project Search Opened") } fn for_each_project_item( &self, cx: &App, f: &mut dyn FnMut(EntityId, &dyn project::ProjectItem), ) { self.results_editor.for_each_project_item(cx, f) } fn is_singleton(&self, _: &App) -> bool { false } fn can_save(&self, _: &App) -> bool { true } fn is_dirty(&self, cx: &App) -> bool { self.results_editor.read(cx).is_dirty(cx) } fn has_conflict(&self, cx: &App) -> bool { self.results_editor.read(cx).has_conflict(cx) } fn save( &mut self, format: bool, project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>, ) -> Task<anyhow::Result<()>> { self.results_editor .update(cx, |editor, cx| editor.save(format, project, window, cx)) } fn save_as( &mut self, _: Entity<Project>, _: ProjectPath,Lines 1–500 of 3930. Load more
use crate::{ buffer_search::Deploy, BufferSearchBar, FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleIncludeIgnored, ToggleRegex, ToggleReplace, ToggleWholeWord, }; use anyhow::Context as _; use collections::{HashMap, HashSet}; use editor::{ actions::SelectAll, items::active_match_index, scroll::Autoscroll, Anchor, Editor, EditorElement, EditorEvent, EditorSettings, EditorStyle, MultiBuffer, MAX_TAB_TITLE_LEN, }; use futures::StreamExt; use gpui::{ actions, div, Action, AnyElement, AnyView, App, Axis, Context, Entity, EntityId, EventEmitter, FocusHandle, Focusable, Global, Hsla, InteractiveElement, IntoElement, KeyContext, ParentElement, Point, Render, SharedString, Styled, Subscription, Task, TextStyle, UpdateGlobal, WeakEntity, Window, }; use language::{Buffer, Language}; use menu::Confirm; use project::{ search::{SearchInputKind, SearchQuery}, search_history::SearchHistoryCursor, Project, ProjectPath, }; use settings::Settings; use std::{ any::{Any, TypeId}, mem, ops::{Not, Range}, path::Path, pin::pin, sync::Arc, }; use theme::ThemeSettings; use ui::{ h_flex, prelude::*, utils::SearchInputWidth, v_flex, Icon, IconButton, IconButtonShape, IconName, KeyBinding, Label, LabelCommon, LabelSize, Toggleable, Tooltip, }; use util::paths::PathMatcher; use workspace::{ item::{BreadcrumbText, Item, ItemEvent, ItemHandle}, searchable::{Direction, SearchableItem, SearchableItemHandle}, DeploySearch, ItemNavHistory, NewSearch, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId, }; actions!( project_search, [SearchInNew, ToggleFocus, NextField, ToggleFilters] ); #[derive(Default)] struct ActiveSettings(HashMap<WeakEntity<Project>, ProjectSearchSettings>); impl Global for ActiveSettings {} pub fn init(cx: &mut App) { cx.set_global(ActiveSettings::default()); cx.observe_new(|workspace: &mut Workspace, _window, _cx| { register_workspace_action(workspace, move |search_bar, _: &Deploy, window, cx| { search_bar.focus_search(window, cx); }); register_workspace_action(workspace, move |search_bar, _: &FocusSearch, window, cx| { search_bar.focus_search(window, cx); }); register_workspace_action( workspace, move |search_bar, _: &ToggleFilters, window, cx| { search_bar.toggle_filters(window, cx); }, ); register_workspace_action( workspace, move |search_bar, _: &ToggleCaseSensitive, _, cx| { search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx); }, ); register_workspace_action(workspace, move |search_bar, _: &ToggleWholeWord, _, cx| { search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx); }); register_workspace_action(workspace, move |search_bar, _: &ToggleRegex, _, cx| { search_bar.toggle_search_option(SearchOptions::REGEX, cx); }); register_workspace_action( workspace, move |search_bar, action: &ToggleReplace, window, cx| { search_bar.toggle_replace(action, window, cx) }, ); register_workspace_action( workspace, move |search_bar, action: &SelectPrevMatch, window, cx| { search_bar.select_prev_match(action, window, cx) }, ); register_workspace_action( workspace, move |search_bar, action: &SelectNextMatch, window, cx| { search_bar.select_next_match(action, window, cx) }, ); // Only handle search_in_new if there is a search present register_workspace_action_for_present_search(workspace, |workspace, action, window, cx| { ProjectSearchView::search_in_new(workspace, action, window, cx) }); // Both on present and dismissed search, we need to unconditionally handle those actions to focus from the editor. workspace.register_action(move |workspace, action: &DeploySearch, window, cx| { if workspace.has_active_modal(window, cx) { cx.propagate(); return; } ProjectSearchView::deploy_search(workspace, action, window, cx); cx.notify(); }); workspace.register_action(move |workspace, action: &NewSearch, window, cx| { if workspace.has_active_modal(window, cx) { cx.propagate(); return; } ProjectSearchView::new_search(workspace, action, window, cx); cx.notify(); }); }) .detach(); } fn is_contains_uppercase(str: &str) -> bool { str.chars().any(|c| c.is_uppercase()) } pub struct ProjectSearch { project: Entity<Project>, excerpts: Entity<MultiBuffer>, pending_search: Option<Task<Option<()>>>, match_ranges: Vec<Range<Anchor>>, active_query: Option<SearchQuery>, last_search_query_text: Option<String>, search_id: usize, no_results: Option<bool>, limit_reached: bool, search_history_cursor: SearchHistoryCursor, search_included_history_cursor: SearchHistoryCursor, search_excluded_history_cursor: SearchHistoryCursor, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] enum InputPanel { Query, Exclude, Include, } pub struct ProjectSearchView { workspace: WeakEntity<Workspace>, focus_handle: FocusHandle, entity: Entity<ProjectSearch>, query_editor: Entity<Editor>, replacement_editor: Entity<Editor>, results_editor: Entity<Editor>, search_options: SearchOptions, panels_with_errors: HashSet<InputPanel>, active_match_index: Option<usize>, search_id: usize, included_files_editor: Entity<Editor>, excluded_files_editor: Entity<Editor>, filters_enabled: bool, replace_enabled: bool, included_opened_only: bool, regex_language: Option<Arc<Language>>, _subscriptions: Vec<Subscription>, } #[derive(Debug, Clone)] pub struct ProjectSearchSettings { search_options: SearchOptions, filters_enabled: bool, } pub struct ProjectSearchBar { active_project_search: Option<Entity<ProjectSearchView>>, subscription: Option<Subscription>, } impl ProjectSearch { pub fn new(project: Entity<Project>, cx: &mut Context<Self>) -> Self { let capability = project.read(cx).capability(); Self { project, excerpts: cx.new(|_| MultiBuffer::new(capability)), pending_search: Default::default(), match_ranges: Default::default(), active_query: None, last_search_query_text: None, search_id: 0, no_results: None, limit_reached: false, search_history_cursor: Default::default(), search_included_history_cursor: Default::default(), search_excluded_history_cursor: Default::default(), } } fn clone(&self, cx: &mut Context<Self>) -> Entity<Self> { cx.new(|cx| Self { project: self.project.clone(), excerpts: self .excerpts .update(cx, |excerpts, cx| cx.new(|cx| excerpts.clone(cx))), pending_search: Default::default(), match_ranges: self.match_ranges.clone(), active_query: self.active_query.clone(), last_search_query_text: self.last_search_query_text.clone(), search_id: self.search_id, no_results: self.no_results, limit_reached: self.limit_reached, search_history_cursor: self.search_history_cursor.clone(), search_included_history_cursor: self.search_included_history_cursor.clone(), search_excluded_history_cursor: self.search_excluded_history_cursor.clone(), }) } fn cursor(&self, kind: SearchInputKind) -> &SearchHistoryCursor { match kind { SearchInputKind::Query => &self.search_history_cursor, SearchInputKind::Include => &self.search_included_history_cursor, SearchInputKind::Exclude => &self.search_excluded_history_cursor, } } fn cursor_mut(&mut self, kind: SearchInputKind) -> &mut SearchHistoryCursor { match kind { SearchInputKind::Query => &mut self.search_history_cursor, SearchInputKind::Include => &mut self.search_included_history_cursor, SearchInputKind::Exclude => &mut self.search_excluded_history_cursor, } } fn search(&mut self, query: SearchQuery, cx: &mut Context<Self>) { let search = self.project.update(cx, |project, cx| { project .search_history_mut(SearchInputKind::Query) .add(&mut self.search_history_cursor, query.as_str().to_string()); let included = query.as_inner().files_to_include().sources().join(","); if !included.is_empty() { project .search_history_mut(SearchInputKind::Include) .add(&mut self.search_included_history_cursor, included); } let excluded = query.as_inner().files_to_exclude().sources().join(","); if !excluded.is_empty() { project .search_history_mut(SearchInputKind::Exclude) .add(&mut self.search_excluded_history_cursor, excluded); } project.search(query.clone(), cx) }); self.last_search_query_text = Some(query.as_str().to_string()); self.search_id += 1; self.active_query = Some(query); self.match_ranges.clear(); self.pending_search = Some(cx.spawn(|this, mut cx| async move { let mut matches = pin!(search.ready_chunks(1024)); 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); this.limit_reached = false; }) .ok()?; let mut limit_reached = false; while let Some(results) = matches.next().await { let mut buffers_with_ranges = Vec::with_capacity(results.len()); for result in results { match result { project::search::SearchResult::Buffer { buffer, ranges } => { buffers_with_ranges.push((buffer, ranges)); } project::search::SearchResult::LimitReached => { limit_reached = true; } } } let match_ranges = this .update(&mut cx, |this, cx| { this.excerpts.update(cx, |excerpts, cx| { excerpts.push_multiple_excerpts_with_context_lines( buffers_with_ranges, editor::DEFAULT_MULTIBUFFER_CONTEXT, cx, ) }) }) .ok()? .await; this.update(&mut cx, |this, cx| { this.match_ranges.extend(match_ranges); cx.notify(); }) .ok()?; } this.update(&mut cx, |this, cx| { if !this.match_ranges.is_empty() { this.no_results = Some(false); } this.limit_reached = limit_reached; this.pending_search.take(); cx.notify(); }) .ok()?; None })); cx.notify(); } } #[derive(Clone, Debug, PartialEq, Eq)] pub enum ViewEvent { UpdateTab, Activate, EditorEvent(editor::EditorEvent), Dismiss, } impl EventEmitter<ViewEvent> for ProjectSearchView {} impl Render for ProjectSearchView { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { if self.has_matches() { div() .flex_1() .size_full() .track_focus(&self.focus_handle(cx)) .child(self.results_editor.clone()) } else { let model = self.entity.read(cx); let has_no_results = model.no_results.unwrap_or(false); let is_search_underway = model.pending_search.is_some(); let heading_text = if is_search_underway { "Searching…" } else if has_no_results { "No Results" } else { "Search All Files" }; let heading_text = div() .justify_center() .child(Label::new(heading_text).size(LabelSize::Large)); let page_content: Option<AnyElement> = if let Some(no_results) = model.no_results { if model.pending_search.is_none() && no_results { Some( Label::new("No results found in this project for the provided query") .size(LabelSize::Small) .into_any_element(), ) } else { None } } else { Some(self.landing_text_minor(window, cx).into_any_element()) }; let page_content = page_content.map(|text| div().child(text)); v_flex() .size_full() .items_center() .justify_center() .overflow_hidden() .bg(cx.theme().colors().editor_background) .track_focus(&self.focus_handle(cx)) .child( v_flex() .id("project-search-landing-page") .overflow_y_scroll() .max_w_80() .gap_1() .child(heading_text) .children(page_content), ) } } } impl Focusable for ProjectSearchView { fn focus_handle(&self, _: &App) -> gpui::FocusHandle { self.focus_handle.clone() } } impl Item for ProjectSearchView { type Event = ViewEvent; fn tab_tooltip_text(&self, cx: &App) -> Option<SharedString> { let query_text = self.query_editor.read(cx).text(cx); query_text .is_empty() .not() .then(|| query_text.into()) .or_else(|| Some("Project Search".into())) } fn act_as_type<'a>( &'a self, type_id: TypeId, self_handle: &'a Entity<Self>, _: &'a App, ) -> Option<AnyView> { if type_id == TypeId::of::<Self>() { Some(self_handle.clone().into()) } else if type_id == TypeId::of::<Editor>() { Some(self.results_editor.clone().into()) } else { None } } fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> { Some(Box::new(self.results_editor.clone())) } fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) { self.results_editor .update(cx, |editor, cx| editor.deactivated(window, cx)); } fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> { Some(Icon::new(IconName::MagnifyingGlass)) } fn tab_content_text(&self, _: &Window, cx: &App) -> Option<SharedString> { let last_query: Option<SharedString> = self .entity .read(cx) .last_search_query_text .as_ref() .map(|query| { let query = query.replace('\n', ""); let query_text = util::truncate_and_trailoff(&query, MAX_TAB_TITLE_LEN); query_text.into() }); Some( last_query .filter(|query| !query.is_empty()) .unwrap_or_else(|| "Project Search".into()), ) } fn telemetry_event_text(&self) -> Option<&'static str> { Some("Project Search Opened") } fn for_each_project_item( &self, cx: &App, f: &mut dyn FnMut(EntityId, &dyn project::ProjectItem), ) { self.results_editor.for_each_project_item(cx, f) } fn is_singleton(&self, _: &App) -> bool { false } fn can_save(&self, _: &App) -> bool { true } fn is_dirty(&self, cx: &App) -> bool { self.results_editor.read(cx).is_dirty(cx) } fn has_conflict(&self, cx: &App) -> bool { self.results_editor.read(cx).has_conflict(cx) } fn save( &mut self, format: bool, project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>, ) -> Task<anyhow::Result<()>> { self.results_editor .update(cx, |editor, cx| editor.save(format, project, window, cx)) } fn save_as( &mut self, _: Entity<Project>, _: ProjectPath,