2022-02-28 09:34:11 +00:00
use crate ::{
2024-01-03 18:52:40 +00:00
history ::SearchHistory , mode ::SearchMode , ActivateRegexMode , ActivateSemanticMode ,
ActivateTextMode , CycleMode , NextHistoryQuery , PreviousHistoryQuery , ReplaceAll , ReplaceNext ,
SearchOptions , SelectNextMatch , SelectPrevMatch , ToggleCaseSensitive , ToggleIncludeIgnored ,
ToggleReplace , ToggleWholeWord ,
2022-02-28 09:34:11 +00:00
} ;
2024-01-03 18:52:40 +00:00
use anyhow ::{ Context as _ , Result } ;
2022-02-28 01:07:46 +00:00
use collections ::HashMap ;
2022-08-30 22:37:54 +00:00
use editor ::{
2024-01-17 23:48:37 +00:00
actions ::SelectAll , items ::active_match_index , scroll ::Autoscroll , Anchor , Editor , EditorEvent ,
MultiBuffer , MAX_TAB_TITLE_LEN ,
2022-08-30 22:37:54 +00:00
} ;
2024-01-03 18:52:40 +00:00
use editor ::{ EditorElement , EditorStyle } ;
2022-02-27 14:47:46 +00:00
use gpui ::{
2024-01-17 20:07:00 +00:00
actions , div , Action , AnyElement , AnyView , AppContext , Context as _ , Element , EntityId ,
2024-01-30 19:08:20 +00:00
EventEmitter , FocusHandle , FocusableView , FontStyle , FontWeight , Global , Hsla ,
InteractiveElement , IntoElement , KeyContext , Model , ModelContext , ParentElement , PromptLevel ,
Render , SharedString , Styled , Subscription , Task , TextStyle , View , ViewContext , VisualContext ,
WeakModel , WeakView , WhiteSpace , WindowContext ,
2022-02-27 14:47:46 +00:00
} ;
2022-05-26 14:36:30 +00:00
use menu ::Confirm ;
2023-07-28 09:56:44 +00:00
use project ::{
2023-11-01 09:53:00 +00:00
search ::{ SearchInputs , SearchQuery } ,
2023-07-28 09:56:44 +00:00
Entry , Project ,
} ;
2023-09-07 17:39:30 +00:00
use semantic_index ::{ SemanticIndex , SemanticIndexStatus } ;
2024-01-03 18:52:40 +00:00
2024-02-07 17:06:03 +00:00
use collections ::HashSet ;
2024-01-03 18:52:40 +00:00
use settings ::Settings ;
use smol ::stream ::StreamExt ;
2022-02-27 14:47:46 +00:00
use std ::{
any ::{ Any , TypeId } ,
2023-01-18 13:22:23 +00:00
mem ,
2023-06-01 09:03:02 +00:00
ops ::{ Not , Range } ,
2022-02-27 14:47:46 +00:00
path ::PathBuf ,
2023-09-11 14:26:14 +00:00
time ::{ Duration , Instant } ,
2022-02-27 14:47:46 +00:00
} ;
2024-01-03 18:52:40 +00:00
use theme ::ThemeSettings ;
2024-01-17 20:07:00 +00:00
use workspace ::{ DeploySearch , NewSearch } ;
2024-01-03 18:52:40 +00:00
use ui ::{
2024-01-15 16:34:06 +00:00
h_flex , prelude ::* , v_flex , Icon , IconButton , IconName , Label , LabelCommon , LabelSize ,
2024-01-05 16:48:52 +00:00
Selectable , ToggleButton , Tooltip ,
2024-01-03 18:52:40 +00:00
} ;
2023-11-01 09:53:00 +00:00
use util ::{ paths ::PathMatcher , ResultExt as _ } ;
2022-04-21 22:24:05 +00:00
use workspace ::{
2023-04-12 18:38:26 +00:00
item ::{ BreadcrumbText , Item , ItemEvent , ItemHandle } ,
2022-09-01 20:45:46 +00:00
searchable ::{ Direction , SearchableItem , SearchableItemHandle } ,
2024-01-03 18:52:40 +00:00
ItemNavHistory , Pane , ToolbarItemEvent , ToolbarItemLocation , ToolbarItemView , Workspace ,
WorkspaceId ,
2022-04-21 22:24:05 +00:00
} ;
2022-02-27 14:47:46 +00:00
2024-02-20 19:07:01 +00:00
const MIN_INPUT_WIDTH_REMS : f32 = 15. ;
const MAX_INPUT_WIDTH_REMS : f32 = 30. ;
2023-07-18 01:10:51 +00:00
actions! (
project_search ,
2024-01-03 18:52:40 +00:00
[ SearchInNew , ToggleFocus , NextField , ToggleFilters ]
2023-07-18 01:10:51 +00:00
) ;
2022-02-27 14:47:46 +00:00
2023-09-20 16:12:01 +00:00
#[ derive(Default) ]
2024-01-03 18:52:40 +00:00
struct ActiveSettings ( HashMap < WeakModel < Project > , ProjectSearchSettings > ) ;
2023-09-20 16:12:01 +00:00
2024-01-30 19:08:20 +00:00
impl Global for ActiveSettings { }
2023-04-06 21:49:03 +00:00
pub fn init ( cx : & mut AppContext ) {
2023-09-20 16:12:01 +00:00
cx . set_global ( ActiveSettings ::default ( ) ) ;
2024-01-03 18:52:40 +00:00
cx . observe_new_views ( | workspace : & mut Workspace , _cx | {
2024-01-17 20:07:00 +00:00
register_workspace_action ( workspace , move | search_bar , _ : & ToggleFilters , cx | {
search_bar . toggle_filters ( 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 , action : & ToggleReplace , cx | {
search_bar . toggle_replace ( action , cx )
} ) ;
register_workspace_action ( workspace , move | search_bar , _ : & ActivateRegexMode , cx | {
search_bar . activate_search_mode ( SearchMode ::Regex , cx )
} ) ;
register_workspace_action ( workspace , move | search_bar , _ : & ActivateTextMode , cx | {
search_bar . activate_search_mode ( SearchMode ::Text , cx )
} ) ;
register_workspace_action (
workspace ,
move | search_bar , _ : & ActivateSemanticMode , cx | {
search_bar . activate_search_mode ( SearchMode ::Semantic , cx )
} ,
) ;
register_workspace_action ( workspace , move | search_bar , action : & CycleMode , cx | {
search_bar . cycle_mode ( action , cx )
} ) ;
2024-01-25 12:28:21 +00:00
register_workspace_action (
workspace ,
move | search_bar , action : & SelectPrevMatch , cx | {
search_bar . select_prev_match ( action , cx )
} ,
) ;
2024-01-17 20:07:00 +00:00
register_workspace_action (
workspace ,
move | search_bar , action : & SelectNextMatch , cx | {
search_bar . select_next_match ( action , cx )
} ,
) ;
2024-01-17 13:11:33 +00:00
2024-01-18 14:04:37 +00:00
// Only handle search_in_new if there is a search present
register_workspace_action_for_present_search ( workspace , | workspace , action , cx | {
ProjectSearchView ::search_in_new ( workspace , action , 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 , cx | {
if workspace . has_active_modal ( cx ) {
cx . propagate ( ) ;
return ;
}
ProjectSearchView ::deploy_search ( workspace , action , cx ) ;
cx . notify ( ) ;
} ) ;
workspace . register_action ( move | workspace , action : & NewSearch , cx | {
if workspace . has_active_modal ( cx ) {
cx . propagate ( ) ;
return ;
}
ProjectSearchView ::new_search ( workspace , action , cx ) ;
cx . notify ( ) ;
} ) ;
2024-01-03 18:52:40 +00:00
} )
. detach ( ) ;
2022-02-27 14:47:46 +00:00
}
2022-02-27 15:15:38 +00:00
struct ProjectSearch {
2024-01-03 18:52:40 +00:00
project : Model < Project > ,
excerpts : Model < MultiBuffer > ,
2022-02-27 14:47:46 +00:00
pending_search : Option < Task < Option < ( ) > > > ,
2022-02-27 21:18:04 +00:00
match_ranges : Vec < Range < Anchor > > ,
2022-02-27 14:47:46 +00:00
active_query : Option < SearchQuery > ,
2023-01-18 13:22:23 +00:00
search_id : usize ,
2023-07-31 22:23:51 +00:00
search_history : SearchHistory ,
2023-08-02 21:14:15 +00:00
no_results : Option < bool > ,
2022-02-27 14:47:46 +00:00
}
2023-05-09 21:06:44 +00:00
#[ derive(Debug, Clone, Copy, PartialEq, Eq, Hash) ]
enum InputPanel {
Query ,
Exclude ,
Include ,
}
2022-03-31 16:36:39 +00:00
pub struct ProjectSearchView {
2024-01-03 18:52:40 +00:00
focus_handle : FocusHandle ,
model : Model < ProjectSearch > ,
query_editor : View < Editor > ,
replacement_editor : View < Editor > ,
results_editor : View < Editor > ,
2023-09-07 17:39:30 +00:00
semantic_state : Option < SemanticState > ,
2023-08-18 22:59:26 +00:00
semantic_permissioned : Option < bool > ,
2023-06-28 03:46:08 +00:00
search_options : SearchOptions ,
2023-05-09 21:06:44 +00:00
panels_with_errors : HashSet < InputPanel > ,
2022-02-27 21:18:04 +00:00
active_match_index : Option < usize > ,
2023-01-18 13:22:23 +00:00
search_id : usize ,
2023-04-25 16:28:50 +00:00
query_editor_was_focused : bool ,
2024-01-03 18:52:40 +00:00
included_files_editor : View < Editor > ,
excluded_files_editor : View < Editor > ,
2023-07-27 11:08:31 +00:00
filters_enabled : bool ,
2023-09-21 14:27:58 +00:00
replace_enabled : bool ,
2023-08-01 15:47:30 +00:00
current_mode : SearchMode ,
2024-01-03 18:52:40 +00:00
_subscriptions : Vec < Subscription > ,
2022-02-27 14:47:46 +00:00
}
2023-09-07 17:39:30 +00:00
struct SemanticState {
index_status : SemanticIndexStatus ,
2023-09-11 14:11:40 +00:00
maintain_rate_limit : Option < Task < ( ) > > ,
2023-09-07 17:39:30 +00:00
_subscription : Subscription ,
2023-07-18 01:10:51 +00:00
}
2023-09-20 16:12:01 +00:00
#[ derive(Debug, Clone) ]
struct ProjectSearchSettings {
search_options : SearchOptions ,
filters_enabled : bool ,
current_mode : SearchMode ,
}
2022-03-29 15:04:39 +00:00
pub struct ProjectSearchBar {
2024-01-03 18:52:40 +00:00
active_project_search : Option < View < ProjectSearchView > > ,
2022-03-29 15:04:39 +00:00
subscription : Option < Subscription > ,
}
2022-02-27 15:15:38 +00:00
impl ProjectSearch {
2024-01-03 18:52:40 +00:00
fn new ( project : Model < Project > , cx : & mut ModelContext < Self > ) -> Self {
2022-02-27 14:47:46 +00:00
let replica_id = project . read ( cx ) . replica_id ( ) ;
2024-01-03 19:08:07 +00:00
let capability = project . read ( cx ) . capability ( ) ;
2022-02-27 14:47:46 +00:00
Self {
project ,
2024-01-03 19:08:07 +00:00
excerpts : cx . new_model ( | _ | MultiBuffer ::new ( replica_id , capability ) ) ,
2022-02-27 14:47:46 +00:00
pending_search : Default ::default ( ) ,
2022-02-27 21:18:04 +00:00
match_ranges : Default ::default ( ) ,
2022-02-27 14:47:46 +00:00
active_query : None ,
2023-01-18 13:22:23 +00:00
search_id : 0 ,
2023-07-31 22:23:51 +00:00
search_history : SearchHistory ::default ( ) ,
2023-08-02 21:14:15 +00:00
no_results : None ,
2022-02-27 14:47:46 +00:00
}
}
2024-01-03 18:52:40 +00:00
fn clone ( & self , cx : & mut ModelContext < Self > ) -> Model < Self > {
cx . new_model ( | cx | Self {
2022-02-27 14:47:46 +00:00
project : self . project . clone ( ) ,
excerpts : self
. excerpts
2024-01-03 18:52:40 +00:00
. update ( cx , | excerpts , cx | cx . new_model ( | cx | excerpts . clone ( cx ) ) ) ,
2022-02-27 14:47:46 +00:00
pending_search : Default ::default ( ) ,
2022-02-27 21:18:04 +00:00
match_ranges : self . match_ranges . clone ( ) ,
2022-02-27 14:47:46 +00:00
active_query : self . active_query . clone ( ) ,
2023-01-18 13:22:23 +00:00
search_id : self . search_id ,
2023-07-31 22:23:51 +00:00
search_history : self . search_history . clone ( ) ,
2023-08-02 21:14:15 +00:00
no_results : self . no_results . clone ( ) ,
2022-02-27 16:49:16 +00:00
} )
2022-02-27 14:47:46 +00:00
}
fn search ( & mut self , query : SearchQuery , cx : & mut ModelContext < Self > ) {
let search = self
. project
. update ( cx , | project , cx | project . search ( query . clone ( ) , cx ) ) ;
2023-01-18 13:22:23 +00:00
self . search_id + = 1 ;
2023-07-31 22:23:51 +00:00
self . search_history . add ( query . as_str ( ) . to_string ( ) ) ;
2022-02-27 14:47:46 +00:00
self . active_query = Some ( query ) ;
2022-02-27 21:18:04 +00:00
self . match_ranges . clear ( ) ;
2024-01-03 18:52:40 +00:00
self . pending_search = Some ( cx . spawn ( | this , mut cx | async move {
2023-08-25 23:31:52 +00:00
let mut matches = search ;
2024-01-03 18:52:40 +00:00
let this = this . upgrade ( ) ? ;
2023-08-25 23:31:52 +00:00
this . update ( & mut cx , | this , cx | {
2023-01-18 13:22:23 +00:00
this . match_ranges . clear ( ) ;
2023-08-25 23:31:52 +00:00
this . excerpts . update ( cx , | this , cx | this . clear ( cx ) ) ;
2023-08-02 21:14:15 +00:00
this . no_results = Some ( true ) ;
2024-01-03 18:52:40 +00:00
} )
. ok ( ) ? ;
2023-01-18 13:22:23 +00:00
2023-08-25 23:31:52 +00:00
while let Some ( ( buffer , anchors ) ) = matches . next ( ) . await {
2024-01-03 18:52:40 +00:00
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 )
} )
2023-08-25 23:31:52 +00:00
} )
2024-01-03 18:52:40 +00:00
. ok ( ) ? ;
2023-08-25 23:31:52 +00:00
while let Some ( range ) = ranges . next ( ) . await {
2024-01-03 18:52:40 +00:00
this . update ( & mut cx , | this , _ | this . match_ranges . push ( range ) )
. ok ( ) ? ;
2023-08-25 23:31:52 +00:00
}
2024-01-03 18:52:40 +00:00
this . update ( & mut cx , | _ , cx | cx . notify ( ) ) . ok ( ) ? ;
2022-02-27 14:47:46 +00:00
}
2023-01-18 13:22:23 +00:00
this . update ( & mut cx , | this , cx | {
this . pending_search . take ( ) ;
cx . notify ( ) ;
2024-01-03 18:52:40 +00:00
} )
. ok ( ) ? ;
2023-01-18 13:22:23 +00:00
2022-02-27 14:47:46 +00:00
None
} ) ) ;
cx . notify ( ) ;
}
2023-07-18 18:44:58 +00:00
2023-08-18 22:59:26 +00:00
fn semantic_search ( & mut self , inputs : & SearchInputs , cx : & mut ModelContext < Self > ) {
2023-07-18 19:01:42 +00:00
let search = SemanticIndex ::global ( cx ) . map ( | index | {
index . update ( cx , | semantic_index , cx | {
2023-07-20 17:46:27 +00:00
semantic_index . search_project (
self . project . clone ( ) ,
2023-08-18 22:59:26 +00:00
inputs . as_str ( ) . to_owned ( ) ,
2023-07-20 17:46:27 +00:00
10 ,
2023-08-18 22:59:26 +00:00
inputs . files_to_include ( ) . to_vec ( ) ,
inputs . files_to_exclude ( ) . to_vec ( ) ,
2023-07-20 17:46:27 +00:00
cx ,
)
2023-07-18 19:01:42 +00:00
} )
2023-07-18 18:44:58 +00:00
} ) ;
self . search_id + = 1 ;
self . match_ranges . clear ( ) ;
2023-08-18 22:59:26 +00:00
self . search_history . add ( inputs . as_str ( ) . to_string ( ) ) ;
2023-09-07 17:04:45 +00:00
self . no_results = None ;
2023-07-18 18:44:58 +00:00
self . pending_search = Some ( cx . spawn ( | this , mut cx | async move {
2023-07-18 19:01:42 +00:00
let results = search ? . await . log_err ( ) ? ;
2023-08-25 23:31:52 +00:00
let matches = results
. into_iter ( )
. map ( | result | ( result . buffer , vec! [ result . range . start .. result . range . start ] ) ) ;
2023-07-18 18:44:58 +00:00
2023-08-25 23:31:52 +00:00
this . update ( & mut cx , | this , cx | {
2023-09-07 17:04:45 +00:00
this . no_results = Some ( true ) ;
2023-07-18 18:44:58 +00:00
this . excerpts . update ( cx , | excerpts , cx | {
excerpts . clear ( cx ) ;
2023-09-07 17:04:45 +00:00
} ) ;
2024-01-03 18:52:40 +00:00
} )
. ok ( ) ? ;
2023-08-25 23:31:52 +00:00
for ( buffer , ranges ) in matches {
2024-01-03 18:52:40 +00:00
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 )
} )
2023-08-25 23:31:52 +00:00
} )
2024-01-03 18:52:40 +00:00
. ok ( ) ? ;
2023-08-25 23:31:52 +00:00
while let Some ( match_range ) = match_ranges . next ( ) . await {
this . update ( & mut cx , | this , cx | {
this . match_ranges . push ( match_range ) ;
while let Ok ( Some ( match_range ) ) = match_ranges . try_next ( ) {
this . match_ranges . push ( match_range ) ;
}
cx . notify ( ) ;
2024-01-03 18:52:40 +00:00
} )
. ok ( ) ? ;
2023-08-25 23:31:52 +00:00
}
2023-07-18 18:44:58 +00:00
}
this . update ( & mut cx , | this , cx | {
this . pending_search . take ( ) ;
cx . notify ( ) ;
2024-01-03 18:52:40 +00:00
} )
. ok ( ) ? ;
2023-07-18 18:44:58 +00:00
None
} ) ) ;
2023-07-18 19:01:42 +00:00
cx . notify ( ) ;
2023-07-18 18:44:58 +00:00
}
2022-02-27 14:47:46 +00:00
}
2023-08-07 10:22:10 +00:00
#[ derive(Clone, Debug, PartialEq, Eq) ]
2022-03-31 16:36:39 +00:00
pub enum ViewEvent {
2022-02-27 14:47:46 +00:00
UpdateTab ,
2022-07-21 01:52:32 +00:00
Activate ,
2024-01-03 18:52:40 +00:00
EditorEvent ( editor ::EditorEvent ) ,
2023-08-07 10:22:10 +00:00
Dismiss ,
2022-02-27 14:47:46 +00:00
}
2024-01-03 18:52:40 +00:00
impl EventEmitter < ViewEvent > for ProjectSearchView { }
2023-08-01 17:28:21 +00:00
2024-01-03 18:52:40 +00:00
impl Render for ProjectSearchView {
fn render ( & mut self , cx : & mut ViewContext < Self > ) -> impl IntoElement {
2024-01-16 15:51:08 +00:00
const PLEASE_AUTHENTICATE : & str = " API Key Missing: Please set 'OPENAI_API_KEY' in Environment Variables. If you authenticated using the Assistant Panel, please restart Zed to Authenticate. " ;
2024-01-03 18:52:40 +00:00
if self . has_matches ( ) {
div ( )
. flex_1 ( )
. size_full ( )
. track_focus ( & self . focus_handle )
. 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 mut major_text = if is_search_underway {
Label ::new ( " Searching... " )
} else if has_no_results {
Label ::new ( " No results " )
2023-08-01 17:28:21 +00:00
} else {
2024-01-03 18:52:40 +00:00
Label ::new ( format! ( " {} search all files " , self . current_mode . label ( ) ) )
2023-08-01 17:28:21 +00:00
} ;
2023-09-21 17:40:01 +00:00
let mut show_minor_text = true ;
2023-09-07 17:39:30 +00:00
let semantic_status = self . semantic_state . as_ref ( ) . and_then ( | semantic | {
let status = semantic . index_status ;
2024-01-16 15:51:08 +00:00
match status {
SemanticIndexStatus ::NotAuthenticated = > {
major_text = Label ::new ( " Not Authenticated " ) ;
show_minor_text = false ;
Some ( PLEASE_AUTHENTICATE . to_string ( ) )
}
SemanticIndexStatus ::Indexed = > Some ( " Indexing complete " . to_string ( ) ) ,
SemanticIndexStatus ::Indexing {
remaining_files ,
rate_limit_expiry ,
} = > {
if remaining_files = = 0 {
Some ( " Indexing... " . to_string ( ) )
} 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 ( format! (
" Remaining files to index (rate limit resets in {}s): {} " ,
remaining_seconds . as_secs ( ) ,
remaining_files
) )
} else {
Some ( format! ( " Remaining files to index: {} " , remaining_files ) )
2023-09-08 19:04:50 +00:00
}
2024-01-16 15:51:08 +00:00
} else {
Some ( format! ( " Remaining files to index: {} " , remaining_files ) )
}
}
}
SemanticIndexStatus ::NotIndexed = > None ,
}
2023-09-07 17:04:45 +00:00
} ) ;
2024-01-03 18:52:40 +00:00
let major_text = div ( ) . justify_center ( ) . max_w_96 ( ) . child ( major_text ) ;
2023-08-18 22:59:26 +00:00
2024-01-03 18:52:40 +00:00
let minor_text : Option < SharedString > = if let Some ( no_results ) = model . no_results {
2023-08-03 14:22:31 +00:00
if model . pending_search . is_none ( ) & & no_results {
2024-01-03 18:52:40 +00:00
Some ( " No results found in this project for the provided query " . into ( ) )
2023-08-02 21:14:15 +00:00
} else {
2024-01-03 18:52:40 +00:00
None
2023-08-02 21:14:15 +00:00
}
} else {
2024-01-03 18:52:40 +00:00
if let Some ( mut semantic_status ) = semantic_status {
semantic_status . extend ( self . landing_text_minor ( ) . chars ( ) ) ;
Some ( semantic_status . into ( ) )
} else {
Some ( self . landing_text_minor ( ) )
2023-07-31 22:23:51 +00:00
}
} ;
2024-01-03 18:52:40 +00:00
let minor_text = minor_text . map ( | text | {
div ( )
. items_center ( )
. max_w_96 ( )
. child ( Label ::new ( text ) . size ( LabelSize ::Small ) )
2023-07-31 22:23:51 +00:00
} ) ;
2024-01-15 16:34:06 +00:00
v_flex ( )
2024-01-03 18:52:40 +00:00
. flex_1 ( )
. size_full ( )
. justify_center ( )
2024-01-05 16:42:40 +00:00
. bg ( cx . theme ( ) . colors ( ) . editor_background )
2024-01-03 18:52:40 +00:00
. track_focus ( & self . focus_handle )
. child (
2024-01-15 16:34:06 +00:00
h_flex ( )
2024-01-03 18:52:40 +00:00
. size_full ( )
. justify_center ( )
2024-01-15 16:34:06 +00:00
. child ( h_flex ( ) . flex_1 ( ) )
. child ( v_flex ( ) . child ( major_text ) . children ( minor_text ) )
. child ( h_flex ( ) . flex_1 ( ) ) ,
2024-01-03 18:52:40 +00:00
)
2022-03-29 15:04:39 +00:00
}
2022-02-27 14:47:46 +00:00
}
2024-01-03 18:52:40 +00:00
}
2022-02-27 14:47:46 +00:00
2024-01-03 18:52:40 +00:00
impl FocusableView for ProjectSearchView {
fn focus_handle ( & self , _ : & AppContext ) -> gpui ::FocusHandle {
self . focus_handle . clone ( )
2022-02-27 14:47:46 +00:00
}
}
2022-03-17 10:32:46 +00:00
impl Item for ProjectSearchView {
2024-01-03 18:52:40 +00:00
type Event = ViewEvent ;
fn tab_tooltip_text ( & self , cx : & AppContext ) -> Option < SharedString > {
2023-06-01 09:03:02 +00:00
let query_text = self . query_editor . read ( cx ) . text ( cx ) ;
query_text
. is_empty ( )
. not ( )
. then ( | | query_text . into ( ) )
2023-06-01 10:31:37 +00:00
. or_else ( | | Some ( " Project Search " . into ( ) ) )
2023-04-14 21:46:53 +00:00
}
2023-09-20 16:12:01 +00:00
2023-04-02 20:57:06 +00:00
fn act_as_type < ' a > (
& ' a self ,
2022-02-27 14:47:46 +00:00
type_id : TypeId ,
2024-01-03 18:52:40 +00:00
self_handle : & ' a View < Self > ,
2023-04-02 20:57:06 +00:00
_ : & ' a AppContext ,
2024-01-03 18:52:40 +00:00
) -> Option < AnyView > {
2022-02-27 14:47:46 +00:00
if type_id = = TypeId ::of ::< Self > ( ) {
2024-01-03 18:52:40 +00:00
Some ( self_handle . clone ( ) . into ( ) )
2022-02-27 14:47:46 +00:00
} else if type_id = = TypeId ::of ::< Editor > ( ) {
2024-01-03 18:52:40 +00:00
Some ( self . results_editor . clone ( ) . into ( ) )
2022-02-27 14:47:46 +00:00
} else {
None
}
}
fn deactivated ( & mut self , cx : & mut ViewContext < Self > ) {
self . results_editor
. update ( cx , | editor , cx | editor . deactivated ( cx ) ) ;
}
2024-01-03 18:52:40 +00:00
fn tab_content ( & self , _ : Option < usize > , selected : bool , cx : & WindowContext < '_ > ) -> AnyElement {
let last_query : Option < SharedString > = self
. model
. read ( cx )
. search_history
. current ( )
. as_ref ( )
. map ( | query | {
2024-01-09 16:16:25 +00:00
let query = query . replace ( '\n' , " " ) ;
let query_text = util ::truncate_and_trailoff ( & query , MAX_TAB_TITLE_LEN ) ;
2024-01-03 18:52:40 +00:00
query_text . into ( )
} ) ;
let tab_name = last_query
. filter ( | query | ! query . is_empty ( ) )
. unwrap_or_else ( | | " Project search " . into ( ) ) ;
2024-01-15 16:34:06 +00:00
h_flex ( )
2024-01-03 18:52:40 +00:00
. gap_2 ( )
2024-01-09 15:11:20 +00:00
. child ( Icon ::new ( IconName ::MagnifyingGlass ) . color ( if selected {
2024-01-03 18:52:40 +00:00
Color ::Default
} else {
Color ::Muted
} ) )
. child ( Label ::new ( tab_name ) . color ( if selected {
Color ::Default
} else {
Color ::Muted
} ) )
2023-04-21 19:04:03 +00:00
. into_any ( )
2022-02-27 14:47:46 +00:00
}
2024-01-15 21:26:04 +00:00
fn telemetry_event_text ( & self ) -> Option < & 'static str > {
Some ( " project search " )
}
2024-01-03 18:52:40 +00:00
fn for_each_project_item (
& self ,
cx : & AppContext ,
f : & mut dyn FnMut ( EntityId , & dyn project ::Item ) ,
) {
2023-01-18 01:21:06 +00:00
self . results_editor . for_each_project_item ( cx , f )
2022-03-17 17:58:20 +00:00
}
2022-05-23 23:03:00 +00:00
fn is_singleton ( & self , _ : & AppContext ) -> bool {
false
}
2023-04-02 20:57:06 +00:00
fn can_save ( & self , _ : & AppContext ) -> bool {
2022-02-27 14:47:46 +00:00
true
}
fn is_dirty ( & self , cx : & AppContext ) -> bool {
self . results_editor . read ( cx ) . is_dirty ( cx )
}
fn has_conflict ( & self , cx : & AppContext ) -> bool {
self . results_editor . read ( cx ) . has_conflict ( cx )
}
fn save (
& mut self ,
2024-01-03 18:52:40 +00:00
project : Model < Project > ,
2022-02-27 14:47:46 +00:00
cx : & mut ViewContext < Self > ,
) -> Task < anyhow ::Result < ( ) > > {
self . results_editor
. update ( cx , | editor , cx | editor . save ( project , cx ) )
}
fn save_as (
& mut self ,
2024-01-03 18:52:40 +00:00
_ : Model < Project > ,
2022-02-27 14:47:46 +00:00
_ : PathBuf ,
_ : & mut ViewContext < Self > ,
) -> Task < anyhow ::Result < ( ) > > {
unreachable! ( " save_as should not have been called " )
}
2022-04-01 12:02:49 +00:00
fn reload (
& mut self ,
2024-01-03 18:52:40 +00:00
project : Model < Project > ,
2022-04-01 12:02:49 +00:00
cx : & mut ViewContext < Self > ,
) -> Task < anyhow ::Result < ( ) > > {
self . results_editor
. update ( cx , | editor , cx | editor . reload ( project , cx ) )
}
2024-01-03 18:52:40 +00:00
fn clone_on_split (
& self ,
_workspace_id : WorkspaceId ,
cx : & mut ViewContext < Self > ,
) -> Option < View < Self > >
2022-02-27 14:47:46 +00:00
where
Self : Sized ,
{
2022-02-27 16:49:16 +00:00
let model = self . model . update ( cx , | model , cx | model . clone ( cx ) ) ;
2024-01-03 18:52:40 +00:00
Some ( cx . new_view ( | cx | Self ::new ( model , cx , None ) ) )
2022-03-16 20:34:04 +00:00
}
2023-04-28 12:02:44 +00:00
fn added_to_workspace ( & mut self , workspace : & mut Workspace , cx : & mut ViewContext < Self > ) {
self . results_editor
. update ( cx , | editor , cx | editor . added_to_workspace ( workspace , cx ) ) ;
}
2022-03-16 20:34:04 +00:00
fn set_nav_history ( & mut self , nav_history : ItemNavHistory , cx : & mut ViewContext < Self > ) {
self . results_editor . update ( cx , | editor , _ | {
editor . set_nav_history ( Some ( nav_history ) ) ;
} ) ;
2022-02-27 16:49:16 +00:00
}
2022-03-23 18:05:46 +00:00
fn navigate ( & mut self , data : Box < dyn Any > , cx : & mut ViewContext < Self > ) -> bool {
2022-02-27 16:49:16 +00:00
self . results_editor
2022-03-23 18:05:46 +00:00
. update ( cx , | editor , cx | editor . navigate ( data , cx ) )
2022-02-27 16:49:16 +00:00
}
2024-01-03 18:52:40 +00:00
fn to_item_events ( event : & Self ::Event , mut f : impl FnMut ( ItemEvent ) ) {
2022-09-06 21:39:58 +00:00
match event {
2023-02-13 20:49:57 +00:00
ViewEvent ::UpdateTab = > {
2024-01-03 18:52:40 +00:00
f ( ItemEvent ::UpdateBreadcrumbs ) ;
f ( ItemEvent ::UpdateTab ) ;
}
ViewEvent ::EditorEvent ( editor_event ) = > {
Editor ::to_item_events ( editor_event , f ) ;
2023-02-13 20:49:57 +00:00
}
2024-01-03 18:52:40 +00:00
ViewEvent ::Dismiss = > f ( ItemEvent ::CloseItem ) ,
_ = > { }
2022-07-06 14:11:06 +00:00
}
}
2022-09-06 23:05:36 +00:00
fn breadcrumb_location ( & self ) -> ToolbarItemLocation {
if self . has_matches ( ) {
ToolbarItemLocation ::Secondary
} else {
ToolbarItemLocation ::Hidden
}
}
2023-04-12 18:38:26 +00:00
fn breadcrumbs ( & self , theme : & theme ::Theme , cx : & AppContext ) -> Option < Vec < BreadcrumbText > > {
2022-09-06 23:05:36 +00:00
self . results_editor . breadcrumbs ( theme , cx )
}
2022-11-17 00:35:56 +00:00
fn serialized_item_kind ( ) -> Option < & 'static str > {
None
}
fn deserialize (
2024-01-03 18:52:40 +00:00
_project : Model < Project > ,
_workspace : WeakView < Workspace > ,
2022-11-17 00:35:56 +00:00
_workspace_id : workspace ::WorkspaceId ,
_item_id : workspace ::ItemId ,
_cx : & mut ViewContext < Pane > ,
2024-01-03 18:52:40 +00:00
) -> Task < anyhow ::Result < View < Self > > > {
2022-11-17 00:35:56 +00:00
unimplemented! ( )
}
2022-02-27 16:49:16 +00:00
}
impl ProjectSearchView {
2023-09-20 16:12:01 +00:00
fn toggle_filters ( & mut self , cx : & mut ViewContext < Self > ) {
self . filters_enabled = ! self . filters_enabled ;
cx . update_global ( | state : & mut ActiveSettings , cx | {
state . 0. insert (
self . model . read ( cx ) . project . downgrade ( ) ,
self . current_settings ( ) ,
) ;
} ) ;
}
fn current_settings ( & self ) -> ProjectSearchSettings {
ProjectSearchSettings {
search_options : self . search_options ,
filters_enabled : self . filters_enabled ,
current_mode : self . current_mode ,
}
}
fn toggle_search_option ( & mut self , option : SearchOptions , cx : & mut ViewContext < Self > ) {
2023-08-02 20:48:11 +00:00
self . search_options . toggle ( option ) ;
2023-09-20 16:12:01 +00:00
cx . update_global ( | state : & mut ActiveSettings , cx | {
state . 0. insert (
self . model . read ( cx ) . project . downgrade ( ) ,
self . current_settings ( ) ,
) ;
} ) ;
2023-08-02 20:48:11 +00:00
}
2023-08-03 16:52:20 +00:00
2023-08-18 22:59:26 +00:00
fn index_project ( & mut self , cx : & mut ViewContext < Self > ) {
if let Some ( semantic_index ) = SemanticIndex ::global ( cx ) {
// Semantic search uses no options
self . search_options = SearchOptions ::none ( ) ;
let project = self . model . read ( cx ) . project . clone ( ) ;
2023-08-22 09:58:48 +00:00
2023-09-07 17:39:30 +00:00
semantic_index . update ( cx , | semantic_index , cx | {
2023-09-06 09:40:59 +00:00
semantic_index
. index_project ( project . clone ( ) , cx )
. detach_and_log_err ( cx ) ;
2023-08-18 22:59:26 +00:00
} ) ;
2023-09-07 17:39:30 +00:00
self . semantic_state = Some ( SemanticState {
index_status : semantic_index . read ( cx ) . status ( & project ) ,
2023-09-11 14:11:40 +00:00
maintain_rate_limit : None ,
2023-09-07 17:39:30 +00:00
_subscription : cx . observe ( & semantic_index , Self ::semantic_index_changed ) ,
} ) ;
2023-09-11 14:11:40 +00:00
self . semantic_index_changed ( semantic_index , cx ) ;
2023-09-07 17:39:30 +00:00
}
}
fn semantic_index_changed (
& mut self ,
2024-01-03 18:52:40 +00:00
semantic_index : Model < SemanticIndex > ,
2023-09-07 17:39:30 +00:00
cx : & mut ViewContext < Self > ,
) {
let project = self . model . read ( cx ) . project . clone ( ) ;
if let Some ( semantic_state ) = self . semantic_state . as_mut ( ) {
cx . notify ( ) ;
2023-09-11 14:11:40 +00:00
semantic_state . index_status = semantic_index . read ( cx ) . status ( & project ) ;
if let SemanticIndexStatus ::Indexing {
rate_limit_expiry : Some ( _ ) ,
..
} = & semantic_state . index_status
{
if semantic_state . maintain_rate_limit . is_none ( ) {
semantic_state . maintain_rate_limit =
Some ( cx . spawn ( | this , mut cx | async move {
loop {
2024-01-03 18:52:40 +00:00
cx . background_executor ( ) . timer ( Duration ::from_secs ( 1 ) ) . await ;
2023-09-11 14:11:40 +00:00
this . update ( & mut cx , | _ , cx | cx . notify ( ) ) . log_err ( ) ;
}
} ) ) ;
return ;
}
2023-09-14 19:42:21 +00:00
} else {
semantic_state . maintain_rate_limit = None ;
2023-09-11 14:11:40 +00:00
}
2023-08-18 22:59:26 +00:00
}
}
2023-08-17 10:11:09 +00:00
fn clear_search ( & mut self , cx : & mut ViewContext < Self > ) {
self . model . update ( cx , | model , cx | {
model . pending_search = None ;
model . no_results = None ;
model . match_ranges . clear ( ) ;
model . excerpts . update ( cx , | excerpts , cx | {
excerpts . clear ( cx ) ;
} ) ;
} ) ;
}
2023-08-02 16:29:19 +00:00
fn activate_search_mode ( & mut self , mode : SearchMode , cx : & mut ViewContext < Self > ) {
2023-08-09 10:28:15 +00:00
let previous_mode = self . current_mode ;
if previous_mode = = mode {
return ;
}
2023-08-16 15:13:21 +00:00
2023-08-18 00:53:21 +00:00
self . clear_search ( cx ) ;
2023-08-02 16:29:19 +00:00
self . current_mode = mode ;
2023-08-18 00:53:21 +00:00
self . active_match_index = None ;
2023-08-02 16:29:19 +00:00
2023-08-18 22:59:26 +00:00
match mode {
SearchMode ::Semantic = > {
let has_permission = self . semantic_permissioned ( cx ) ;
self . active_match_index = None ;
cx . spawn ( | this , mut cx | async move {
let has_permission = has_permission . await ? ;
if ! has_permission {
2024-01-03 18:52:40 +00:00
let answer = this . update ( & mut cx , | this , cx | {
2023-08-18 22:59:26 +00:00
let project = this . model . read ( cx ) . project . clone ( ) ;
let project_name = project
. read ( cx )
. worktree_root_names ( cx )
. collect ::< Vec < & str > > ( )
. join ( " / " ) ;
let is_plural =
project_name . chars ( ) . filter ( | letter | * letter = = '/' ) . count ( ) > 0 ;
let prompt_text = format! ( " Would you like to index the ' {} ' project {} for semantic search? This requires sending code to the OpenAI API " , project_name ,
if is_plural {
" s "
} else { " " } ) ;
cx . prompt (
PromptLevel ::Info ,
prompt_text . as_str ( ) ,
2024-01-25 05:47:27 +00:00
None ,
2023-08-18 22:59:26 +00:00
& [ " Continue " , " Cancel " ] ,
)
} ) ? ;
2024-01-03 18:52:40 +00:00
if answer . await ? = = 0 {
2023-08-18 22:59:26 +00:00
this . update ( & mut cx , | this , _ | {
this . semantic_permissioned = Some ( true ) ;
} ) ? ;
} else {
this . update ( & mut cx , | this , cx | {
this . semantic_permissioned = Some ( false ) ;
debug_assert_ne! ( previous_mode , SearchMode ::Semantic , " Tried to re-enable semantic search mode after user modal was rejected " ) ;
this . activate_search_mode ( previous_mode , cx ) ;
} ) ? ;
return anyhow ::Ok ( ( ) ) ;
}
}
this . update ( & mut cx , | this , cx | {
this . index_project ( cx ) ;
} ) ? ;
anyhow ::Ok ( ( ) )
} ) . detach_and_log_err ( cx ) ;
}
SearchMode ::Regex | SearchMode ::Text = > {
self . semantic_state = None ;
self . active_match_index = None ;
self . search ( cx ) ;
}
}
2023-08-16 15:13:21 +00:00
2023-09-20 16:12:01 +00:00
cx . update_global ( | state : & mut ActiveSettings , cx | {
state . 0. insert (
self . model . read ( cx ) . project . downgrade ( ) ,
self . current_settings ( ) ,
) ;
} ) ;
2023-08-02 16:29:19 +00:00
cx . notify ( ) ;
}
2023-09-21 14:27:58 +00:00
fn replace_next ( & mut self , _ : & ReplaceNext , cx : & mut ViewContext < Self > ) {
let model = self . model . read ( cx ) ;
if let Some ( query ) = model . active_query . as_ref ( ) {
if model . match_ranges . is_empty ( ) {
return ;
}
if let Some ( active_index ) = self . active_match_index {
let query = query . clone ( ) . with_replacement ( self . replacement ( cx ) ) ;
self . results_editor . replace (
& ( Box ::new ( model . match_ranges [ active_index ] . clone ( ) ) as _ ) ,
& query ,
cx ,
) ;
self . select_match ( Direction ::Next , cx )
}
}
}
pub fn replacement ( & self , cx : & AppContext ) -> String {
self . replacement_editor . read ( cx ) . text ( cx )
}
fn replace_all ( & mut self , _ : & ReplaceAll , cx : & mut ViewContext < Self > ) {
let model = self . model . read ( cx ) ;
if let Some ( query ) = model . active_query . as_ref ( ) {
if model . match_ranges . is_empty ( ) {
return ;
}
if self . active_match_index . is_some ( ) {
let query = query . clone ( ) . with_replacement ( self . replacement ( cx ) ) ;
let matches = model
. match_ranges
. iter ( )
. map ( | item | Box ::new ( item . clone ( ) ) as _ )
. collect ::< Vec < _ > > ( ) ;
for item in matches {
self . results_editor . replace ( & item , & query , cx ) ;
}
}
}
}
2023-08-18 00:53:21 +00:00
2023-09-20 16:12:01 +00:00
fn new (
2024-01-03 18:52:40 +00:00
model : Model < ProjectSearch > ,
2023-09-20 16:12:01 +00:00
cx : & mut ViewContext < Self > ,
settings : Option < ProjectSearchSettings > ,
) -> Self {
2022-02-27 16:49:16 +00:00
let project ;
let excerpts ;
2023-09-21 14:27:58 +00:00
let mut replacement_text = None ;
2022-02-27 16:49:16 +00:00
let mut query_text = String ::new ( ) ;
2024-01-03 18:52:40 +00:00
let mut subscriptions = Vec ::new ( ) ;
2023-09-20 16:12:01 +00:00
// Read in settings if available
let ( mut options , current_mode , filters_enabled ) = if let Some ( settings ) = settings {
(
settings . search_options ,
settings . current_mode ,
settings . filters_enabled ,
)
} else {
( SearchOptions ::NONE , Default ::default ( ) , false )
} ;
2022-02-27 16:49:16 +00:00
{
let model = model . read ( cx ) ;
project = model . project . clone ( ) ;
excerpts = model . excerpts . clone ( ) ;
if let Some ( active_query ) = model . active_query . as_ref ( ) {
query_text = active_query . as_str ( ) . to_string ( ) ;
2023-09-21 14:27:58 +00:00
replacement_text = active_query . replacement ( ) . map ( ToOwned ::to_owned ) ;
2023-06-28 03:46:08 +00:00
options = SearchOptions ::from_query ( active_query ) ;
2022-02-27 16:49:16 +00:00
}
}
2024-01-03 18:52:40 +00:00
subscriptions . push ( cx . observe ( & model , | this , _ , cx | this . model_changed ( cx ) ) ) ;
2022-02-27 16:49:16 +00:00
2024-01-03 18:52:40 +00:00
let query_editor = cx . new_view ( | cx | {
let mut editor = Editor ::single_line ( cx ) ;
2023-07-27 09:43:32 +00:00
editor . set_placeholder_text ( " Text search all files " , cx ) ;
2022-02-27 16:49:16 +00:00
editor . set_text ( query_text , cx ) ;
2022-02-27 14:47:46 +00:00
editor
} ) ;
2023-05-09 20:33:55 +00:00
// Subscribe to query_editor in order to reraise editor events for workspace item activation purposes
2024-01-03 18:52:40 +00:00
subscriptions . push (
cx . subscribe ( & query_editor , | _ , _ , event : & EditorEvent , cx | {
cx . emit ( ViewEvent ::EditorEvent ( event . clone ( ) ) )
} ) ,
) ;
let replacement_editor = cx . new_view ( | cx | {
let mut editor = Editor ::single_line ( cx ) ;
2023-09-21 14:27:58 +00:00
editor . set_placeholder_text ( " Replace in project.. " , cx ) ;
if let Some ( text ) = replacement_text {
editor . set_text ( text , cx ) ;
}
editor
} ) ;
2024-01-03 18:52:40 +00:00
let results_editor = cx . new_view ( | cx | {
2023-08-03 17:18:14 +00:00
let mut editor = Editor ::for_multibuffer ( excerpts , Some ( project . clone ( ) ) , cx ) ;
2022-02-27 14:47:46 +00:00
editor . set_searchable ( false ) ;
editor
} ) ;
2024-01-03 18:52:40 +00:00
subscriptions . push ( cx . observe ( & results_editor , | _ , _ , cx | cx . emit ( ViewEvent ::UpdateTab ) ) ) ;
2022-08-09 22:09:38 +00:00
2024-01-03 18:52:40 +00:00
subscriptions . push (
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
cx . emit ( ViewEvent ::EditorEvent ( event . clone ( ) ) ) ;
} ) ,
) ;
2022-02-27 16:49:16 +00:00
2024-01-03 18:52:40 +00:00
let included_files_editor = cx . new_view ( | cx | {
let mut editor = Editor ::single_line ( cx ) ;
2023-05-09 19:31:38 +00:00
editor . set_placeholder_text ( " Include: crates/**/*.toml " , cx ) ;
editor
2023-05-07 19:17:26 +00:00
} ) ;
2023-05-09 20:33:55 +00:00
// Subscribe to include_files_editor in order to reraise editor events for workspace item activation purposes
2024-01-03 18:52:40 +00:00
subscriptions . push (
cx . subscribe ( & included_files_editor , | _ , _ , event : & EditorEvent , cx | {
cx . emit ( ViewEvent ::EditorEvent ( event . clone ( ) ) )
} ) ,
) ;
2023-05-07 19:17:26 +00:00
2024-01-03 18:52:40 +00:00
let excluded_files_editor = cx . new_view ( | cx | {
let mut editor = Editor ::single_line ( cx ) ;
2023-05-09 19:31:38 +00:00
editor . set_placeholder_text ( " Exclude: vendor/*, *.lock " , cx ) ;
editor
2023-05-07 19:17:26 +00:00
} ) ;
2023-05-09 20:33:55 +00:00
// Subscribe to excluded_files_editor in order to reraise editor events for workspace item activation purposes
2024-01-03 18:52:40 +00:00
subscriptions . push (
cx . subscribe ( & excluded_files_editor , | _ , _ , event : & EditorEvent , cx | {
cx . emit ( ViewEvent ::EditorEvent ( event . clone ( ) ) )
} ) ,
) ;
let focus_handle = cx . focus_handle ( ) ;
subscriptions . push ( cx . on_focus_in ( & focus_handle , | this , cx | {
if this . focus_handle . is_focused ( cx ) {
if this . has_matches ( ) {
this . results_editor . focus_handle ( cx ) . focus ( cx ) ;
} else {
this . query_editor . focus_handle ( cx ) . focus ( cx ) ;
}
}
} ) ) ;
2023-05-07 19:17:26 +00:00
2023-08-03 16:52:20 +00:00
// Check if Worktrees have all been previously indexed
2022-02-27 16:49:16 +00:00
let mut this = ProjectSearchView {
2024-01-03 18:52:40 +00:00
focus_handle ,
2023-09-21 14:27:58 +00:00
replacement_editor ,
2023-01-18 13:22:23 +00:00
search_id : model . read ( cx ) . search_id ,
2022-02-27 14:47:46 +00:00
model ,
query_editor ,
results_editor ,
2023-08-18 22:59:26 +00:00
semantic_state : None ,
semantic_permissioned : None ,
2023-06-28 03:46:08 +00:00
search_options : options ,
2024-02-07 17:06:03 +00:00
panels_with_errors : HashSet ::default ( ) ,
2022-02-27 21:18:04 +00:00
active_match_index : None ,
2023-04-25 16:28:50 +00:00
query_editor_was_focused : false ,
2023-05-07 19:17:26 +00:00
included_files_editor ,
excluded_files_editor ,
2023-07-27 11:08:31 +00:00
filters_enabled ,
2023-09-20 16:12:01 +00:00
current_mode ,
2023-09-21 14:27:58 +00:00
replace_enabled : false ,
2024-01-03 18:52:40 +00:00
_subscriptions : subscriptions ,
2022-02-27 14:47:46 +00:00
} ;
2023-01-18 13:22:23 +00:00
this . model_changed ( cx ) ;
2022-02-27 16:49:16 +00:00
this
2022-02-27 14:47:46 +00:00
}
2023-08-18 22:59:26 +00:00
fn semantic_permissioned ( & mut self , cx : & mut ViewContext < Self > ) -> Task < Result < bool > > {
if let Some ( value ) = self . semantic_permissioned {
return Task ::ready ( Ok ( value ) ) ;
}
SemanticIndex ::global ( cx )
. map ( | semantic | {
let project = self . model . read ( cx ) . project . clone ( ) ;
2023-09-07 16:49:57 +00:00
semantic . update ( cx , | this , cx | this . project_previously_indexed ( & project , cx ) )
2023-08-18 22:59:26 +00:00
} )
. unwrap_or ( Task ::ready ( Ok ( false ) ) )
}
2024-01-03 18:52:40 +00:00
2023-07-20 13:01:01 +00:00
pub fn new_search_in_directory (
workspace : & mut Workspace ,
dir_entry : & Entry ,
cx : & mut ViewContext < Workspace > ,
) {
if ! dir_entry . is_dir ( ) {
return ;
}
2023-08-25 17:11:32 +00:00
let Some ( filter_str ) = dir_entry . path . to_str ( ) else {
return ;
} ;
2023-07-20 13:01:01 +00:00
2024-01-03 18:52:40 +00:00
let model = cx . new_model ( | cx | ProjectSearch ::new ( workspace . project ( ) . clone ( ) , cx ) ) ;
let search = cx . new_view ( | cx | ProjectSearchView ::new ( model , cx , None ) ) ;
2024-02-29 02:23:36 +00:00
workspace . add_item_to_active_pane ( Box ::new ( search . clone ( ) ) , cx ) ;
2023-07-20 13:01:01 +00:00
search . update ( cx , | search , cx | {
search
. included_files_editor
. update ( cx , | editor , cx | editor . set_text ( filter_str , cx ) ) ;
2023-08-25 06:21:07 +00:00
search . filters_enabled = true ;
2023-07-20 13:01:01 +00:00
search . focus_query_editor ( cx )
} ) ;
}
2024-01-16 18:06:48 +00:00
// Re-activate the most recently activated search in this pane or the most recent if it has been closed.
2024-01-04 19:13:51 +00:00
// If no search exists in the workspace, create a new one.
fn deploy_search (
workspace : & mut Workspace ,
_ : & workspace ::DeploySearch ,
cx : & mut ViewContext < Workspace > ,
) {
2024-01-16 18:06:48 +00:00
let existing = workspace
. active_pane ( )
. read ( cx )
. items ( )
. find_map ( | item | item . downcast ::< ProjectSearchView > ( ) ) ;
2024-01-04 19:13:51 +00:00
Self ::existing_or_new_search ( workspace , existing , cx )
}
2024-01-17 20:07:00 +00:00
fn search_in_new ( workspace : & mut Workspace , _ : & SearchInNew , cx : & mut ViewContext < Workspace > ) {
if let Some ( search_view ) = workspace
. active_item ( cx )
. and_then ( | item | item . downcast ::< ProjectSearchView > ( ) )
{
let new_query = search_view . update ( cx , | search_view , cx | {
let new_query = search_view . build_search_query ( cx ) ;
if new_query . is_some ( ) {
if let Some ( old_query ) = search_view . model . read ( cx ) . active_query . clone ( ) {
search_view . query_editor . update ( cx , | editor , cx | {
editor . set_text ( old_query . as_str ( ) , cx ) ;
} ) ;
search_view . search_options = SearchOptions ::from_query ( & old_query ) ;
}
}
new_query
} ) ;
if let Some ( new_query ) = new_query {
let model = cx . new_model ( | cx | {
let mut model = ProjectSearch ::new ( workspace . project ( ) . clone ( ) , cx ) ;
model . search ( new_query , cx ) ;
model
} ) ;
2024-02-29 02:23:36 +00:00
workspace . add_item_to_active_pane (
2024-01-17 20:07:00 +00:00
Box ::new ( cx . new_view ( | cx | ProjectSearchView ::new ( model , cx , None ) ) ) ,
cx ,
) ;
}
}
}
2023-12-11 11:11:07 +00:00
// Add another search tab to the workspace.
2024-01-04 19:13:51 +00:00
fn new_search (
2022-08-04 15:42:42 +00:00
workspace : & mut Workspace ,
_ : & workspace ::NewSearch ,
cx : & mut ViewContext < Workspace > ,
2024-01-04 19:13:51 +00:00
) {
Self ::existing_or_new_search ( workspace , None , cx )
}
fn existing_or_new_search (
workspace : & mut Workspace ,
existing : Option < View < ProjectSearchView > > ,
cx : & mut ViewContext < Workspace > ,
2022-08-04 15:42:42 +00:00
) {
2022-06-07 08:02:04 +00:00
let query = workspace . active_item ( cx ) . and_then ( | item | {
let editor = item . act_as ::< Editor > ( cx ) ? ;
2022-08-30 22:37:54 +00:00
let query = editor . query_suggestion ( cx ) ;
2022-06-07 08:02:04 +00:00
if query . is_empty ( ) {
None
} else {
Some ( query )
}
} ) ;
2024-01-04 19:13:51 +00:00
let search = if let Some ( existing ) = existing {
workspace . activate_item ( & existing , cx ) ;
existing
2023-12-11 11:11:07 +00:00
} else {
2024-01-04 19:13:51 +00:00
let settings = cx
. global ::< ActiveSettings > ( )
. 0
. get ( & workspace . project ( ) . downgrade ( ) ) ;
2023-09-20 16:12:01 +00:00
2024-01-04 19:13:51 +00:00
let settings = if let Some ( settings ) = settings {
Some ( settings . clone ( ) )
} else {
None
} ;
let model = cx . new_model ( | cx | ProjectSearch ::new ( workspace . project ( ) . clone ( ) , cx ) ) ;
let view = cx . new_view ( | cx | ProjectSearchView ::new ( model , cx , settings ) ) ;
2024-02-29 02:23:36 +00:00
workspace . add_item_to_active_pane ( Box ::new ( view . clone ( ) ) , cx ) ;
2024-01-04 19:13:51 +00:00
view
} ;
2024-01-16 18:06:48 +00:00
2022-06-07 08:02:04 +00:00
search . update ( cx , | search , cx | {
if let Some ( query ) = query {
search . set_query ( & query , cx ) ;
}
search . focus_query_editor ( cx )
} ) ;
2022-02-27 14:47:46 +00:00
}
2022-03-29 15:04:39 +00:00
fn search ( & mut self , cx : & mut ViewContext < Self > ) {
2023-08-18 22:59:26 +00:00
let mode = self . current_mode ;
match mode {
SearchMode ::Semantic = > {
2023-09-07 17:04:45 +00:00
if self . semantic_state . is_some ( ) {
2023-08-18 22:59:26 +00:00
if let Some ( query ) = self . build_search_query ( cx ) {
self . model
. update ( cx , | model , cx | model . semantic_search ( query . as_inner ( ) , cx ) ) ;
}
}
2023-07-20 17:46:27 +00:00
}
2023-07-18 01:10:51 +00:00
2023-08-18 22:59:26 +00:00
_ = > {
if let Some ( query ) = self . build_search_query ( cx ) {
self . model . update ( cx , | model , cx | model . search ( query , cx ) ) ;
}
}
2022-02-27 14:47:46 +00:00
}
}
fn build_search_query ( & mut self , cx : & mut ViewContext < Self > ) -> Option < SearchQuery > {
2024-01-03 18:52:40 +00:00
// Do not bail early in this function, as we want to fill out `self.panels_with_errors`.
2022-02-27 14:47:46 +00:00
let text = self . query_editor . read ( cx ) . text ( cx ) ;
2023-05-19 16:13:31 +00:00
let included_files =
2023-07-28 09:56:44 +00:00
match Self ::parse_path_matches ( & self . included_files_editor . read ( cx ) . text ( cx ) ) {
2023-05-19 16:13:31 +00:00
Ok ( included_files ) = > {
2024-01-03 18:52:40 +00:00
let should_unmark_error = self . panels_with_errors . remove ( & InputPanel ::Include ) ;
if should_unmark_error {
cx . notify ( ) ;
}
2023-05-19 16:13:31 +00:00
included_files
}
Err ( _e ) = > {
2024-01-03 18:52:40 +00:00
let should_mark_error = self . panels_with_errors . insert ( InputPanel ::Include ) ;
if should_mark_error {
cx . notify ( ) ;
}
vec! [ ]
2023-05-19 16:13:31 +00:00
}
} ;
let excluded_files =
2023-07-28 09:56:44 +00:00
match Self ::parse_path_matches ( & self . excluded_files_editor . read ( cx ) . text ( cx ) ) {
2023-05-19 16:13:31 +00:00
Ok ( excluded_files ) = > {
2024-01-03 18:52:40 +00:00
let should_unmark_error = self . panels_with_errors . remove ( & InputPanel ::Exclude ) ;
if should_unmark_error {
cx . notify ( ) ;
}
2023-05-19 16:13:31 +00:00
excluded_files
}
Err ( _e ) = > {
2024-01-03 18:52:40 +00:00
let should_mark_error = self . panels_with_errors . insert ( InputPanel ::Exclude ) ;
if should_mark_error {
cx . notify ( ) ;
}
vec! [ ]
2023-05-19 16:13:31 +00:00
}
} ;
2024-01-03 18:52:40 +00:00
2023-08-16 14:50:54 +00:00
let current_mode = self . current_mode ;
2024-01-03 18:52:40 +00:00
let query = match current_mode {
2023-08-16 14:50:54 +00:00
SearchMode ::Regex = > {
match SearchQuery ::regex (
text ,
self . search_options . contains ( SearchOptions ::WHOLE_WORD ) ,
self . search_options . contains ( SearchOptions ::CASE_SENSITIVE ) ,
2023-11-10 08:56:28 +00:00
self . search_options . contains ( SearchOptions ::INCLUDE_IGNORED ) ,
2023-08-16 14:50:54 +00:00
included_files ,
excluded_files ,
) {
Ok ( query ) = > {
2024-01-03 18:52:40 +00:00
let should_unmark_error =
self . panels_with_errors . remove ( & InputPanel ::Query ) ;
if should_unmark_error {
cx . notify ( ) ;
}
2023-08-16 14:50:54 +00:00
Some ( query )
}
Err ( _e ) = > {
2024-01-03 18:52:40 +00:00
let should_mark_error = self . panels_with_errors . insert ( InputPanel ::Query ) ;
if should_mark_error {
cx . notify ( ) ;
}
2023-08-16 14:50:54 +00:00
None
}
2022-02-27 14:47:46 +00:00
}
}
2023-09-18 15:01:08 +00:00
_ = > match SearchQuery ::text (
2022-02-27 14:47:46 +00:00
text ,
2023-06-28 03:46:08 +00:00
self . search_options . contains ( SearchOptions ::WHOLE_WORD ) ,
self . search_options . contains ( SearchOptions ::CASE_SENSITIVE ) ,
2023-11-10 08:56:28 +00:00
self . search_options . contains ( SearchOptions ::INCLUDE_IGNORED ) ,
2023-05-07 19:17:26 +00:00
included_files ,
excluded_files ,
2023-09-18 15:01:08 +00:00
) {
Ok ( query ) = > {
2024-01-03 18:52:40 +00:00
let should_unmark_error = self . panels_with_errors . remove ( & InputPanel ::Query ) ;
if should_unmark_error {
cx . notify ( ) ;
}
2023-09-18 15:01:08 +00:00
Some ( query )
}
Err ( _e ) = > {
2024-01-03 18:52:40 +00:00
let should_mark_error = self . panels_with_errors . insert ( InputPanel ::Query ) ;
if should_mark_error {
cx . notify ( ) ;
}
2023-09-18 15:01:08 +00:00
None
}
} ,
2024-01-03 18:52:40 +00:00
} ;
if ! self . panels_with_errors . is_empty ( ) {
return None ;
2022-02-27 14:47:46 +00:00
}
2024-01-03 18:52:40 +00:00
query
2022-02-27 14:47:46 +00:00
}
2023-07-28 09:56:44 +00:00
fn parse_path_matches ( text : & str ) -> anyhow ::Result < Vec < PathMatcher > > {
2023-05-19 16:13:31 +00:00
text . split ( ',' )
. map ( str ::trim )
2023-07-28 09:56:44 +00:00
. filter ( | maybe_glob_str | ! maybe_glob_str . is_empty ( ) )
. map ( | maybe_glob_str | {
PathMatcher ::new ( maybe_glob_str )
. with_context ( | | format! ( " parsing {maybe_glob_str} as path matcher " ) )
} )
2023-05-19 16:13:31 +00:00
. collect ( )
}
2022-03-29 15:04:39 +00:00
fn select_match ( & mut self , direction : Direction , cx : & mut ViewContext < Self > ) {
2022-02-28 09:34:11 +00:00
if let Some ( index ) = self . active_match_index {
2022-09-01 20:45:46 +00:00
let match_ranges = self . model . read ( cx ) . match_ranges . clone ( ) ;
let new_index = self . results_editor . update ( cx , | editor , cx | {
2023-07-17 18:02:10 +00:00
editor . match_index_for_direction ( & match_ranges , index , direction , 1 , cx )
2022-09-01 20:45:46 +00:00
} ) ;
let range_to_select = match_ranges [ new_index ] . clone ( ) ;
2022-02-27 21:18:04 +00:00
self . results_editor . update ( cx , | editor , cx | {
2023-07-28 18:10:04 +00:00
let range_to_select = editor . range_for_match ( & range_to_select ) ;
2023-02-26 19:25:58 +00:00
editor . unfold_ranges ( [ range_to_select . clone ( ) ] , false , true , cx ) ;
2022-11-08 19:54:26 +00:00
editor . change_selections ( Some ( Autoscroll ::fit ( ) ) , cx , | s | {
2022-05-12 21:18:46 +00:00
s . select_ranges ( [ range_to_select ] )
2022-05-06 04:09:26 +00:00
} ) ;
2022-02-27 21:18:04 +00:00
} ) ;
}
}
2023-04-25 16:28:50 +00:00
fn focus_query_editor ( & mut self , cx : & mut ViewContext < Self > ) {
2022-02-28 01:07:46 +00:00
self . query_editor . update ( cx , | query_editor , cx | {
query_editor . select_all ( & SelectAll , cx ) ;
} ) ;
2023-04-25 16:28:50 +00:00
self . query_editor_was_focused = true ;
2024-01-03 18:52:40 +00:00
let editor_handle = self . query_editor . focus_handle ( cx ) ;
cx . focus ( & editor_handle ) ;
2022-02-28 01:07:46 +00:00
}
2022-06-07 08:02:04 +00:00
fn set_query ( & mut self , query : & str , cx : & mut ViewContext < Self > ) {
self . query_editor
. update ( cx , | query_editor , cx | query_editor . set_text ( query , cx ) ) ;
}
2023-04-25 16:28:50 +00:00
fn focus_results_editor ( & mut self , cx : & mut ViewContext < Self > ) {
2022-02-27 14:47:46 +00:00
self . query_editor . update ( cx , | query_editor , cx | {
2022-05-06 04:09:26 +00:00
let cursor = query_editor . selections . newest_anchor ( ) . head ( ) ;
2022-05-12 21:18:46 +00:00
query_editor . change_selections ( None , cx , | s | s . select_ranges ( [ cursor . clone ( ) .. cursor ] ) ) ;
2022-02-27 14:47:46 +00:00
} ) ;
2023-04-25 16:28:50 +00:00
self . query_editor_was_focused = false ;
2024-01-03 18:52:40 +00:00
let results_handle = self . results_editor . focus_handle ( cx ) ;
cx . focus ( & results_handle ) ;
2022-02-27 14:47:46 +00:00
}
2023-01-18 13:22:23 +00:00
fn model_changed ( & mut self , cx : & mut ViewContext < Self > ) {
2022-02-27 21:18:04 +00:00
let match_ranges = self . model . read ( cx ) . match_ranges . clone ( ) ;
if match_ranges . is_empty ( ) {
self . active_match_index = None ;
} else {
2023-07-05 08:54:28 +00:00
self . active_match_index = Some ( 0 ) ;
self . update_match_index ( cx ) ;
2023-01-18 13:22:23 +00:00
let prev_search_id = mem ::replace ( & mut self . search_id , self . model . read ( cx ) . search_id ) ;
2023-02-08 16:22:14 +00:00
let is_new_search = self . search_id ! = prev_search_id ;
2022-02-27 14:47:46 +00:00
self . results_editor . update ( cx , | editor , cx | {
2023-02-08 16:22:14 +00:00
if is_new_search {
2023-07-28 18:10:04 +00:00
let range_to_select = match_ranges
. first ( )
. clone ( )
. map ( | range | editor . range_for_match ( range ) ) ;
2022-11-08 19:54:26 +00:00
editor . change_selections ( Some ( Autoscroll ::fit ( ) ) , cx , | s | {
2023-07-28 18:10:04 +00:00
s . select_ranges ( range_to_select )
2022-05-06 04:09:26 +00:00
} ) ;
2022-02-27 14:47:46 +00:00
}
2022-04-13 22:30:57 +00:00
editor . highlight_background ::< Self > (
match_ranges ,
2024-01-03 18:52:40 +00:00
| theme | theme . search_match_background ,
2022-04-13 22:30:57 +00:00
cx ,
) ;
2022-02-27 14:47:46 +00:00
} ) ;
2024-01-03 18:52:40 +00:00
if is_new_search & & self . query_editor . focus_handle ( cx ) . is_focused ( cx ) {
2022-02-27 14:47:46 +00:00
self . focus_results_editor ( cx ) ;
}
}
cx . emit ( ViewEvent ::UpdateTab ) ;
cx . notify ( ) ;
}
2022-02-27 21:18:04 +00:00
fn update_match_index ( & mut self , cx : & mut ViewContext < Self > ) {
2022-02-28 09:34:11 +00:00
let results_editor = self . results_editor . read ( cx ) ;
let new_index = active_match_index (
& self . model . read ( cx ) . match_ranges ,
2022-05-06 04:09:26 +00:00
& results_editor . selections . newest_anchor ( ) . head ( ) ,
2022-05-13 23:58:30 +00:00
& results_editor . buffer ( ) . read ( cx ) . snapshot ( cx ) ,
2022-02-28 09:34:11 +00:00
) ;
if self . active_match_index ! = new_index {
self . active_match_index = new_index ;
cx . notify ( ) ;
2022-02-27 21:18:04 +00:00
}
}
2022-04-01 08:00:21 +00:00
pub fn has_matches ( & self ) -> bool {
self . active_match_index . is_some ( )
}
2023-05-29 13:58:06 +00:00
2024-01-03 18:52:40 +00:00
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 = > " \n Simply explain the code you are looking to find. ex. 'prompt user for permissions to index their project' " . into ( )
}
}
fn border_color_for ( & self , panel : InputPanel , cx : & WindowContext ) -> Hsla {
if self . panels_with_errors . contains ( & panel ) {
Color ::Error . color ( cx )
} else {
cx . theme ( ) . colors ( ) . border
}
}
fn move_focus_to_results ( & mut self , cx : & mut ViewContext < Self > ) {
if ! self . results_editor . focus_handle ( cx ) . is_focused ( cx )
& & ! self . model . read ( cx ) . match_ranges . is_empty ( )
2023-05-29 13:58:06 +00:00
{
2024-01-03 18:52:40 +00:00
cx . stop_propagation ( ) ;
return self . focus_results_editor ( cx ) ;
2023-05-29 13:58:06 +00:00
}
}
2022-03-29 15:04:39 +00:00
}
2022-02-27 21:18:04 +00:00
2022-03-29 15:04:39 +00:00
impl ProjectSearchBar {
pub fn new ( ) -> Self {
Self {
2024-01-17 20:07:00 +00:00
active_project_search : None ,
subscription : None ,
2022-03-29 15:04:39 +00:00
}
}
2024-01-03 18:52:40 +00:00
fn cycle_mode ( & self , _ : & CycleMode , cx : & mut ViewContext < Self > ) {
if let Some ( view ) = self . active_project_search . as_ref ( ) {
view . update ( cx , | this , cx | {
2023-08-18 22:59:26 +00:00
let new_mode =
crate ::mode ::next_mode ( & this . current_mode , SemanticIndex ::enabled ( cx ) ) ;
2023-08-02 16:29:19 +00:00
this . activate_search_mode ( new_mode , cx ) ;
2024-01-03 18:52:40 +00:00
let editor_handle = this . query_editor . focus_handle ( cx ) ;
cx . focus ( & editor_handle ) ;
} ) ;
2023-08-01 15:47:30 +00:00
}
}
2024-01-03 18:52:40 +00:00
2023-09-26 16:40:41 +00:00
fn confirm ( & mut self , _ : & Confirm , cx : & mut ViewContext < Self > ) {
2022-03-29 15:04:39 +00:00
if let Some ( search_view ) = self . active_project_search . as_ref ( ) {
2023-09-26 16:40:41 +00:00
search_view . update ( cx , | search_view , cx | {
2024-01-03 18:52:40 +00:00
if ! search_view
. replacement_editor
. focus_handle ( cx )
. is_focused ( cx )
{
cx . stop_propagation ( ) ;
2023-09-26 16:40:41 +00:00
search_view . search ( cx ) ;
}
} ) ;
}
2022-03-29 15:04:39 +00:00
}
2024-01-17 23:48:37 +00:00
fn tab ( & mut self , _ : & editor ::actions ::Tab , cx : & mut ViewContext < Self > ) {
2023-05-09 17:09:15 +00:00
self . cycle_field ( Direction ::Next , cx ) ;
}
2024-01-17 23:48:37 +00:00
fn tab_previous ( & mut self , _ : & editor ::actions ::TabPrev , cx : & mut ViewContext < Self > ) {
2023-05-09 17:09:15 +00:00
self . cycle_field ( Direction ::Prev , cx ) ;
}
fn cycle_field ( & mut self , direction : Direction , cx : & mut ViewContext < Self > ) {
let active_project_search = match & self . active_project_search {
Some ( active_project_search ) = > active_project_search ,
None = > {
return ;
}
} ;
active_project_search . update ( cx , | project_view , cx | {
2023-09-21 14:27:58 +00:00
let mut views = vec! [ & project_view . query_editor ] ;
2024-02-20 19:07:01 +00:00
if project_view . replace_enabled {
views . push ( & project_view . replacement_editor ) ;
}
2023-09-21 14:27:58 +00:00
if project_view . filters_enabled {
views . extend ( [
& project_view . included_files_editor ,
& project_view . excluded_files_editor ,
] ) ;
}
2023-05-09 17:09:15 +00:00
let current_index = match views
. iter ( )
. enumerate ( )
2024-01-03 18:52:40 +00:00
. find ( | ( _ , view ) | view . focus_handle ( cx ) . is_focused ( cx ) )
2023-05-09 17:09:15 +00:00
{
Some ( ( index , _ ) ) = > index ,
2024-02-20 19:07:01 +00:00
None = > return ,
2023-05-09 17:09:15 +00:00
} ;
let new_index = match direction {
Direction ::Next = > ( current_index + 1 ) % views . len ( ) ,
Direction ::Prev if current_index = = 0 = > views . len ( ) - 1 ,
Direction ::Prev = > ( current_index - 1 ) % views . len ( ) ,
} ;
2024-01-03 18:52:40 +00:00
let next_focus_handle = views [ new_index ] . focus_handle ( cx ) ;
cx . focus ( & next_focus_handle ) ;
cx . stop_propagation ( ) ;
2023-05-09 17:09:15 +00:00
} ) ;
2022-03-29 15:04:39 +00:00
}
2023-06-28 03:46:08 +00:00
fn toggle_search_option ( & mut self , option : SearchOptions , cx : & mut ViewContext < Self > ) -> bool {
2022-03-29 15:04:39 +00:00
if let Some ( search_view ) = self . active_project_search . as_ref ( ) {
search_view . update ( cx , | search_view , cx | {
2023-09-20 16:12:01 +00:00
search_view . toggle_search_option ( option , cx ) ;
2022-03-29 15:04:39 +00:00
search_view . search ( cx ) ;
} ) ;
2023-09-20 16:12:01 +00:00
2022-03-29 15:04:39 +00:00
cx . notify ( ) ;
2022-06-16 12:18:45 +00:00
true
} else {
false
2022-03-29 15:04:39 +00:00
}
}
2024-01-03 18:52:40 +00:00
2023-09-21 14:27:58 +00:00
fn toggle_replace ( & mut self , _ : & ToggleReplace , cx : & mut ViewContext < Self > ) {
if let Some ( search ) = & self . active_project_search {
search . update ( cx , | this , cx | {
this . replace_enabled = ! this . replace_enabled ;
2024-01-03 18:52:40 +00:00
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 ) ;
2023-09-21 14:27:58 +00:00
cx . notify ( ) ;
} ) ;
}
2023-09-20 13:48:27 +00:00
}
2023-07-27 11:08:31 +00:00
fn toggle_filters ( & mut self , cx : & mut ViewContext < Self > ) -> bool {
2023-07-18 01:10:51 +00:00
if let Some ( search_view ) = self . active_project_search . as_ref ( ) {
search_view . update ( cx , | search_view , cx | {
2023-09-20 16:12:01 +00:00
search_view . toggle_filters ( cx ) ;
2023-07-27 14:31:24 +00:00
search_view
. included_files_editor
. update ( cx , | _ , cx | cx . notify ( ) ) ;
search_view
. excluded_files_editor
. update ( cx , | _ , cx | cx . notify ( ) ) ;
2024-01-03 18:52:40 +00:00
cx . refresh ( ) ;
2023-07-18 19:01:42 +00:00
cx . notify ( ) ;
2023-07-18 01:10:51 +00:00
} ) ;
cx . notify ( ) ;
true
} else {
false
}
}
2024-01-03 18:52:40 +00:00
fn move_focus_to_results ( & self , cx : & mut ViewContext < Self > ) {
if let Some ( search_view ) = self . active_project_search . as_ref ( ) {
search_view . update ( cx , | search_view , cx | {
search_view . move_focus_to_results ( cx ) ;
} ) ;
cx . notify ( ) ;
}
}
2023-08-02 16:29:19 +00:00
fn activate_search_mode ( & self , mode : SearchMode , cx : & mut ViewContext < Self > ) {
// Update Current Mode
if let Some ( search_view ) = self . active_project_search . as_ref ( ) {
search_view . update ( cx , | search_view , cx | {
search_view . activate_search_mode ( mode , cx ) ;
} ) ;
cx . notify ( ) ;
}
2023-07-18 01:10:51 +00:00
}
2023-06-28 03:46:08 +00:00
fn is_option_enabled ( & self , option : SearchOptions , cx : & AppContext ) -> bool {
2022-03-29 15:04:39 +00:00
if let Some ( search ) = self . active_project_search . as_ref ( ) {
2023-06-28 03:46:08 +00:00
search . read ( cx ) . search_options . contains ( option )
2022-03-29 15:04:39 +00:00
} else {
false
2022-02-27 14:47:46 +00:00
}
}
2023-07-31 22:23:51 +00:00
fn next_history_query ( & mut self , _ : & NextHistoryQuery , cx : & mut ViewContext < Self > ) {
if let Some ( search_view ) = self . active_project_search . as_ref ( ) {
search_view . update ( cx , | search_view , cx | {
let new_query = search_view . model . update ( cx , | model , _ | {
if let Some ( new_query ) = model . search_history . next ( ) . map ( str ::to_string ) {
new_query
} else {
model . search_history . reset_selection ( ) ;
String ::new ( )
}
} ) ;
search_view . set_query ( & new_query , cx ) ;
} ) ;
}
}
fn previous_history_query ( & mut self , _ : & PreviousHistoryQuery , cx : & mut ViewContext < Self > ) {
if let Some ( search_view ) = self . active_project_search . as_ref ( ) {
search_view . update ( cx , | search_view , cx | {
if search_view . query_editor . read ( cx ) . text ( cx ) . is_empty ( ) {
if let Some ( new_query ) = search_view
. model
. read ( cx )
. search_history
. current ( )
. map ( str ::to_string )
{
search_view . set_query ( & new_query , cx ) ;
return ;
}
}
if let Some ( new_query ) = search_view . model . update ( cx , | model , _ | {
model . search_history . previous ( ) . map ( str ::to_string )
} ) {
search_view . set_query ( & new_query , cx ) ;
}
} ) ;
}
}
2022-03-29 15:04:39 +00:00
2024-01-25 12:28:21 +00:00
fn select_next_match ( & mut self , _ : & SelectNextMatch , cx : & mut ViewContext < Self > ) {
2024-01-17 13:11:33 +00:00
if let Some ( search ) = self . active_project_search . as_ref ( ) {
search . update ( cx , | this , cx | {
this . select_match ( Direction ::Next , cx ) ;
} )
}
}
fn select_prev_match ( & mut self , _ : & SelectPrevMatch , cx : & mut ViewContext < Self > ) {
if let Some ( search ) = self . active_project_search . as_ref ( ) {
search . update ( cx , | this , cx | {
this . select_match ( Direction ::Prev , cx ) ;
} )
}
}
2024-01-03 18:52:40 +00:00
fn new_placeholder_text ( & self , cx : & mut ViewContext < Self > ) -> Option < String > {
let previous_query_keystrokes = cx
. bindings_for_action ( & PreviousHistoryQuery { } )
. into_iter ( )
. next ( )
. map ( | binding | {
binding
. keystrokes ( )
. iter ( )
. map ( | k | k . to_string ( ) )
. collect ::< Vec < _ > > ( )
} ) ;
let next_query_keystrokes = cx
. bindings_for_action ( & NextHistoryQuery { } )
. into_iter ( )
. next ( )
. map ( | binding | {
binding
. keystrokes ( )
. iter ( )
. map ( | k | k . to_string ( ) )
. collect ::< Vec < _ > > ( )
} ) ;
let new_placeholder_text = match ( previous_query_keystrokes , next_query_keystrokes ) {
( Some ( previous_query_keystrokes ) , Some ( next_query_keystrokes ) ) = > 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
2023-09-26 16:40:41 +00:00
}
2024-01-03 18:52:40 +00:00
fn render_text_input ( & self , editor : & View < Editor > , cx : & ViewContext < Self > ) -> impl IntoElement {
let settings = ThemeSettings ::get_global ( cx ) ;
let text_style = TextStyle {
2024-01-03 19:08:07 +00:00
color : if editor . read ( cx ) . read_only ( cx ) {
2024-01-03 18:52:40 +00:00
cx . theme ( ) . colors ( ) . text_disabled
2022-02-27 21:18:04 +00:00
} else {
2024-01-03 18:52:40 +00:00
cx . theme ( ) . colors ( ) . text
} ,
font_family : settings . ui_font . family . clone ( ) ,
font_features : settings . ui_font . features ,
font_size : rems ( 0.875 ) . into ( ) ,
font_weight : FontWeight ::NORMAL ,
font_style : FontStyle ::Normal ,
line_height : relative ( 1.3 ) . into ( ) ,
background_color : None ,
underline : None ,
2024-02-07 14:51:27 +00:00
strikethrough : None ,
2024-01-03 18:52:40 +00:00
white_space : WhiteSpace ::Normal ,
} ;
2023-08-16 10:35:09 +00:00
2024-01-03 18:52:40 +00:00
EditorElement ::new (
& editor ,
EditorStyle {
background : cx . theme ( ) . colors ( ) . editor_background ,
local_player : cx . theme ( ) . players ( ) . local ( ) ,
text : text_style ,
.. Default ::default ( )
} ,
)
}
}
2023-05-07 19:17:26 +00:00
2024-01-03 18:52:40 +00:00
impl Render for ProjectSearchBar {
fn render ( & mut self , cx : & mut ViewContext < Self > ) -> impl IntoElement {
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 )
} )
2023-09-21 14:27:58 +00:00
} ) ;
2024-01-03 18:52:40 +00:00
}
let search = search . read ( cx ) ;
let semantic_is_available = SemanticIndex ::enabled ( cx ) ;
2024-02-20 19:07:01 +00:00
let query_column = h_flex ( )
. flex_1 ( )
. px_2 ( )
. py_1 ( )
. border_1 ( )
. border_color ( search . border_color_for ( InputPanel ::Query , cx ) )
. rounded_lg ( )
. min_w ( rems ( MIN_INPUT_WIDTH_REMS ) )
. max_w ( rems ( MAX_INPUT_WIDTH_REMS ) )
. on_action ( cx . listener ( | this , action , cx | this . confirm ( action , cx ) ) )
. on_action ( cx . listener ( | this , action , cx | this . previous_history_query ( action , cx ) ) )
. on_action ( cx . listener ( | this , action , cx | this . next_history_query ( action , cx ) ) )
. child ( self . render_text_input ( & search . query_editor , cx ) )
. child (
h_flex ( )
. child (
IconButton ::new ( " project-search-filter-button " , IconName ::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 ( ) ,
) ,
)
. when ( search . current_mode ! = SearchMode ::Semantic , | this | {
this . child (
IconButton ::new (
" project-search-case-sensitive " ,
IconName ::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 ) ;
} ) ) ,
)
2024-01-03 18:52:40 +00:00
. child (
2024-02-20 19:07:01 +00:00
IconButton ::new ( " project-search-whole-word " , IconName ::WholeWord )
2024-01-03 18:52:40 +00:00
. tooltip ( | cx | {
2024-02-20 19:07:01 +00:00
Tooltip ::for_action ( " Toggle whole word " , & ToggleWholeWord , cx )
2024-01-03 18:52:40 +00:00
} )
2024-02-20 19:07:01 +00:00
. selected ( self . is_option_enabled ( SearchOptions ::WHOLE_WORD , cx ) )
2024-01-03 18:52:40 +00:00
. on_click ( cx . listener ( | this , _ , cx | {
2024-02-20 19:07:01 +00:00
this . toggle_search_option ( SearchOptions ::WHOLE_WORD , cx ) ;
} ) ) ,
2023-08-28 21:20:09 +00:00
)
2024-02-20 19:07:01 +00:00
} ) ,
) ;
2024-01-03 18:52:40 +00:00
2024-01-15 16:34:06 +00:00
let mode_column = v_flex ( ) . items_start ( ) . justify_start ( ) . child (
h_flex ( )
2024-01-05 16:48:52 +00:00
. gap_2 ( )
2024-01-03 18:52:40 +00:00
. child (
2024-01-15 16:34:06 +00:00
h_flex ( )
2024-01-03 18:52:40 +00:00
. child (
2024-01-05 16:48:52 +00:00
ToggleButton ::new ( " project-search-text-button " , " Text " )
. style ( ButtonStyle ::Filled )
. size ( ButtonSize ::Large )
2024-01-03 18:52:40 +00:00
. 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 )
2024-01-05 16:48:52 +00:00
} )
. first ( ) ,
2023-08-28 21:20:09 +00:00
)
2024-01-03 18:52:40 +00:00
. child (
2024-01-05 16:48:52 +00:00
ToggleButton ::new ( " project-search-regex-button " , " Regex " )
. style ( ButtonStyle ::Filled )
. size ( ButtonSize ::Large )
2024-01-03 18:52:40 +00:00
. 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 ,
)
2024-01-05 16:48:52 +00:00
} )
. map ( | this | {
if semantic_is_available {
this . middle ( )
} else {
this . last ( )
}
2024-01-03 18:52:40 +00:00
} ) ,
2023-08-28 21:20:09 +00:00
)
2024-01-03 18:52:40 +00:00
. when ( semantic_is_available , | this | {
this . child (
2024-01-05 16:48:52 +00:00
ToggleButton ::new ( " project-search-semantic-button " , " Semantic " )
. style ( ButtonStyle ::Filled )
. size ( ButtonSize ::Large )
2024-01-03 18:52:40 +00:00
. selected ( search . current_mode = = SearchMode ::Semantic )
. on_click ( cx . listener ( | this , _ , cx | {
this . activate_search_mode ( SearchMode ::Semantic , cx )
} ) )
. tooltip ( | cx | {
Tooltip ::for_action (
" Toggle semantic search " ,
& ActivateSemanticMode ,
cx ,
)
2024-01-05 16:48:52 +00:00
} )
. last ( ) ,
2024-01-03 18:52:40 +00:00
)
} ) ,
)
. child (
2024-01-09 15:11:20 +00:00
IconButton ::new ( " project-search-toggle-replace " , IconName ::Replace )
2024-01-03 18:52:40 +00:00
. on_click ( cx . listener ( | this , _ , cx | {
this . toggle_replace ( & ToggleReplace , cx ) ;
} ) )
. tooltip ( | cx | Tooltip ::for_action ( " Toggle replace " , & ToggleReplace , cx ) ) ,
) ,
) ;
2024-02-20 19:07:01 +00:00
let match_text = search
. active_match_index
. and_then ( | index | {
2024-01-03 18:52:40 +00:00
let index = index + 1 ;
let match_quantity = search . model . read ( cx ) . match_ranges . len ( ) ;
if match_quantity > 0 {
debug_assert! ( match_quantity > = index ) ;
2024-02-20 19:07:01 +00:00
Some ( format! ( " {index} / {match_quantity} " ) . to_string ( ) )
} else {
None
2024-01-03 18:52:40 +00:00
}
} )
2024-02-20 19:07:01 +00:00
. unwrap_or_else ( | | " No matches " . to_string ( ) ) ;
let matches_column = h_flex ( )
. child ( div ( ) . min_w ( rems ( 6. ) ) . child ( Label ::new ( match_text ) ) )
2024-01-03 18:52:40 +00:00
. child (
2024-01-09 15:11:20 +00:00
IconButton ::new ( " project-search-prev-match " , IconName ::ChevronLeft )
2024-01-03 18:52:40 +00:00
. 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 ) ;
} )
}
2023-08-28 21:20:09 +00:00
} ) )
2024-01-03 18:52:40 +00:00
. tooltip ( | cx | {
Tooltip ::for_action ( " Go to previous match " , & SelectPrevMatch , cx )
} ) ,
)
. child (
2024-01-09 15:11:20 +00:00
IconButton ::new ( " project-search-next-match " , IconName ::ChevronRight )
2024-01-03 18:52:40 +00:00
. disabled ( search . active_match_index . is_none ( ) )
. on_click ( cx . listener ( | this , _ , cx | {
2023-08-08 23:29:22 +00:00
if let Some ( search ) = this . active_project_search . as_ref ( ) {
2024-01-03 18:52:40 +00:00
search . update ( cx , | this , cx | {
this . select_match ( Direction ::Next , cx ) ;
} )
2023-08-08 23:29:22 +00:00
}
2024-01-03 18:52:40 +00:00
} ) )
. tooltip ( | cx | Tooltip ::for_action ( " Go to next match " , & SelectNextMatch , cx ) ) ,
) ;
2023-08-16 10:35:09 +00:00
2024-02-20 19:07:01 +00:00
let search_line = h_flex ( )
. gap_2 ( )
. flex_1 ( )
. child ( query_column )
. child ( mode_column )
. child ( matches_column ) ;
let replace_line = search . replace_enabled . then ( | | {
let replace_column = h_flex ( )
. flex_1 ( )
. min_w ( rems ( MIN_INPUT_WIDTH_REMS ) )
. max_w ( rems ( MAX_INPUT_WIDTH_REMS ) )
. h_8 ( )
. px_2 ( )
. py_1 ( )
. border_1 ( )
. border_color ( cx . theme ( ) . colors ( ) . border )
. rounded_lg ( )
. child ( self . render_text_input ( & search . replacement_editor , cx ) ) ;
let replace_actions = h_flex ( ) . when ( search . replace_enabled , | this | {
this . child (
IconButton ::new ( " project-search-replace-next " , IconName ::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 ) ) ,
)
. child (
IconButton ::new ( " project-search-replace-all " , IconName ::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 ) ) ,
)
} ) ;
h_flex ( )
. gap_2 ( )
. child ( replace_column )
. child ( replace_actions )
} ) ;
let filter_line = search . filters_enabled . then ( | | {
h_flex ( )
. w_full ( )
. gap_2 ( )
. child (
h_flex ( )
. flex_1 ( )
. min_w ( rems ( MIN_INPUT_WIDTH_REMS ) )
. max_w ( rems ( MAX_INPUT_WIDTH_REMS ) )
. h_8 ( )
. px_2 ( )
. py_1 ( )
. border_1 ( )
. border_color ( search . border_color_for ( InputPanel ::Include , cx ) )
. rounded_lg ( )
. child ( self . render_text_input ( & search . included_files_editor , cx ) )
. when ( search . current_mode ! = SearchMode ::Semantic , | this | {
this . child (
SearchOptions ::INCLUDE_IGNORED . as_button (
search
. search_options
. contains ( SearchOptions ::INCLUDE_IGNORED ) ,
cx . listener ( | this , _ , cx | {
this . toggle_search_option (
SearchOptions ::INCLUDE_IGNORED ,
cx ,
) ;
} ) ,
) ,
)
} ) ,
)
. child (
h_flex ( )
. flex_1 ( )
. min_w ( rems ( MIN_INPUT_WIDTH_REMS ) )
. max_w ( rems ( MAX_INPUT_WIDTH_REMS ) )
. h_8 ( )
. px_2 ( )
. py_1 ( )
. border_1 ( )
. border_color ( search . border_color_for ( InputPanel ::Exclude , cx ) )
. rounded_lg ( )
. child ( self . render_text_input ( & search . excluded_files_editor , cx ) ) ,
)
} ) ;
2024-01-15 16:34:06 +00:00
v_flex ( )
2024-01-03 18:52:40 +00:00
. key_context ( key_context )
. on_action ( cx . listener ( | this , _ : & ToggleFocus , cx | this . move_focus_to_results ( cx ) ) )
. on_action ( cx . listener ( | this , _ : & ToggleFilters , cx | {
this . toggle_filters ( 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 , _ : & ActivateSemanticMode , cx | {
this . activate_search_mode ( SearchMode ::Semantic , cx )
} ) )
. capture_action ( cx . listener ( | this , action , cx | {
this . tab ( action , cx ) ;
cx . stop_propagation ( ) ;
} ) )
. capture_action ( cx . listener ( | this , action , cx | {
this . tab_previous ( action , cx ) ;
cx . stop_propagation ( ) ;
} ) )
. on_action ( cx . listener ( | this , action , cx | this . confirm ( action , cx ) ) )
. on_action ( cx . listener ( | this , action , cx | {
this . cycle_mode ( action , cx ) ;
} ) )
. when ( search . current_mode ! = SearchMode ::Semantic , | this | {
this . on_action ( cx . listener ( | this , action , cx | {
this . toggle_replace ( action , 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 | {
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 ) ;
} )
}
} ) )
. when ( search . filters_enabled , | this | {
this . on_action ( cx . listener ( | this , _ : & ToggleIncludeIgnored , cx | {
this . toggle_search_option ( SearchOptions ::INCLUDE_IGNORED , cx ) ;
} ) )
} )
} )
2024-01-17 13:11:33 +00:00
. on_action ( cx . listener ( Self ::select_next_match ) )
. on_action ( cx . listener ( Self ::select_prev_match ) )
2024-02-20 19:07:01 +00:00
. gap_2 ( )
. w_full ( )
. child ( search_line )
. children ( replace_line )
. children ( filter_line )
2022-03-29 15:04:39 +00:00
}
}
2024-01-03 18:52:40 +00:00
impl EventEmitter < ToolbarItemEvent > for ProjectSearchBar { }
2022-03-29 15:04:39 +00:00
impl ToolbarItemView for ProjectSearchBar {
fn set_active_pane_item (
& mut self ,
2022-11-15 01:31:12 +00:00
active_pane_item : Option < & dyn ItemHandle > ,
2022-03-29 15:04:39 +00:00
cx : & mut ViewContext < Self > ,
2022-03-31 16:36:39 +00:00
) -> ToolbarItemLocation {
cx . notify ( ) ;
2022-03-29 15:04:39 +00:00
self . subscription = None ;
self . active_project_search = None ;
if let Some ( search ) = active_pane_item . and_then ( | i | i . downcast ::< ProjectSearchView > ( ) ) {
2023-08-24 11:40:04 +00:00
search . update ( cx , | search , cx | {
if search . current_mode = = SearchMode ::Semantic {
search . index_project ( cx ) ;
}
} ) ;
2022-03-29 15:04:39 +00:00
self . subscription = Some ( cx . observe ( & search , | _ , _ , cx | cx . notify ( ) ) ) ;
self . active_project_search = Some ( search ) ;
2024-01-03 18:52:40 +00:00
ToolbarItemLocation ::PrimaryLeft { }
2022-03-31 16:36:39 +00:00
} else {
ToolbarItemLocation ::Hidden
2022-03-29 15:04:39 +00:00
}
2022-02-27 21:18:04 +00:00
}
2023-05-09 10:55:18 +00:00
2024-01-03 18:52:40 +00:00
fn row_count ( & self , cx : & WindowContext < '_ > ) -> usize {
2023-08-28 21:20:09 +00:00
if let Some ( search ) = self . active_project_search . as_ref ( ) {
if search . read ( cx ) . filters_enabled {
return 2 ;
}
}
1
2023-05-09 10:55:18 +00:00
}
2022-02-27 14:47:46 +00:00
}
2022-02-28 10:10:22 +00:00
2024-01-17 20:07:00 +00:00
fn register_workspace_action < A : Action > (
workspace : & mut Workspace ,
callback : fn ( & mut ProjectSearchBar , & A , & mut ViewContext < ProjectSearchBar > ) ,
) {
workspace . register_action ( move | workspace , action : & A , cx | {
if workspace . has_active_modal ( cx ) {
cx . propagate ( ) ;
return ;
}
workspace . active_pane ( ) . update ( cx , | pane , cx | {
pane . toolbar ( ) . update ( cx , move | workspace , cx | {
if let Some ( search_bar ) = workspace . item_of_type ::< ProjectSearchBar > ( ) {
search_bar . update ( cx , move | search_bar , cx | {
if search_bar . active_project_search . is_some ( ) {
callback ( search_bar , action , cx ) ;
cx . notify ( ) ;
} else {
cx . propagate ( ) ;
}
} ) ;
}
} ) ;
} )
} ) ;
}
2024-01-18 14:04:37 +00:00
fn register_workspace_action_for_present_search < A : Action > (
2024-01-17 20:07:00 +00:00
workspace : & mut Workspace ,
callback : fn ( & mut Workspace , & A , & mut ViewContext < Workspace > ) ,
) {
workspace . register_action ( move | workspace , action : & A , cx | {
if workspace . has_active_modal ( cx ) {
cx . propagate ( ) ;
return ;
}
let should_notify = workspace
. active_pane ( )
. read ( cx )
. toolbar ( )
. read ( cx )
. item_of_type ::< ProjectSearchBar > ( )
2024-01-18 14:04:37 +00:00
. map ( | search_bar | search_bar . read ( cx ) . active_project_search . is_some ( ) )
2024-01-17 20:07:00 +00:00
. unwrap_or ( false ) ;
if should_notify {
callback ( workspace , action , cx ) ;
cx . notify ( ) ;
} else {
cx . propagate ( ) ;
}
} ) ;
}
2022-02-28 10:10:22 +00:00
#[ cfg(test) ]
2023-05-11 21:40:35 +00:00
pub mod tests {
2022-02-28 10:10:22 +00:00
use super ::* ;
use editor ::DisplayPoint ;
2024-01-03 18:52:40 +00:00
use gpui ::{ Action , TestAppContext } ;
2022-02-28 10:10:22 +00:00
use project ::FakeFs ;
2023-07-31 22:23:51 +00:00
use semantic_index ::semantic_index_settings ::SemanticIndexSettings ;
2022-02-28 10:10:22 +00:00
use serde_json ::json ;
2024-01-03 18:52:40 +00:00
use settings ::{ Settings , SettingsStore } ;
2024-01-11 16:20:10 +00:00
use workspace ::DeploySearch ;
2022-02-28 10:10:22 +00:00
#[ gpui::test ]
2024-01-03 18:52:40 +00:00
async fn test_project_search ( cx : & mut TestAppContext ) {
2023-05-11 21:40:35 +00:00
init_test ( cx ) ;
2022-02-28 10:10:22 +00:00
2024-01-03 18:52:40 +00:00
let fs = FakeFs ::new ( cx . background_executor . clone ( ) ) ;
2022-02-28 10:10:22 +00:00
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 ;
2022-05-19 21:37:26 +00:00
let project = Project ::test ( fs . clone ( ) , [ " /dir " . as_ref ( ) ] , cx ) . await ;
2024-01-03 18:52:40 +00:00
let search = cx . new_model ( | cx | ProjectSearch ::new ( project , cx ) ) ;
let search_view = cx . add_window ( | cx | ProjectSearchView ::new ( search . clone ( ) , cx , None ) ) ;
2022-02-28 10:10:22 +00:00
2024-01-03 18:52:40 +00:00
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 ) ;
} )
. unwrap ( ) ;
cx . background_executor . run_until_parked ( ) ;
2022-03-01 11:01:02 +00:00
search_view . update ( cx , | search_view , cx | {
2022-02-28 10:10:22 +00:00
assert_eq! (
search_view
. results_editor
. update ( cx , | editor , cx | editor . display_text ( cx ) ) ,
" \n \n const THREE: usize = one::ONE + two::TWO; \n \n \n const TWO: usize = one::ONE + one::ONE; "
) ;
2024-01-03 18:52:40 +00:00
let match_background_color = cx . theme ( ) . colors ( ) . search_match_background ;
2022-02-28 10:10:22 +00:00
assert_eq! (
search_view
. results_editor
2023-09-14 09:28:42 +00:00
. update ( cx , | editor , cx | editor . all_text_background_highlights ( cx ) ) ,
2022-02-28 10:10:22 +00:00
& [
(
DisplayPoint ::new ( 2 , 32 ) .. DisplayPoint ::new ( 2 , 35 ) ,
2024-01-03 18:52:40 +00:00
match_background_color
2022-02-28 10:10:22 +00:00
) ,
(
DisplayPoint ::new ( 2 , 37 ) .. DisplayPoint ::new ( 2 , 40 ) ,
2024-01-03 18:52:40 +00:00
match_background_color
2022-02-28 10:10:22 +00:00
) ,
(
DisplayPoint ::new ( 5 , 6 ) .. DisplayPoint ::new ( 5 , 9 ) ,
2024-01-03 18:52:40 +00:00
match_background_color
2022-02-28 10:10:22 +00:00
)
]
) ;
assert_eq! ( search_view . active_match_index , Some ( 0 ) ) ;
assert_eq! (
search_view
. results_editor
2022-05-12 23:04:27 +00:00
. update ( cx , | editor , cx | editor . selections . display_ranges ( cx ) ) ,
2022-02-28 10:10:22 +00:00
[ DisplayPoint ::new ( 2 , 32 ) .. DisplayPoint ::new ( 2 , 35 ) ]
) ;
2022-03-29 15:04:39 +00:00
search_view . select_match ( Direction ::Next , cx ) ;
2024-01-03 18:52:40 +00:00
} ) . unwrap ( ) ;
2022-02-28 10:10:22 +00:00
2024-01-03 18:52:40 +00:00
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 ) ;
} )
. unwrap ( ) ;
2022-02-28 10:10:22 +00:00
2024-01-03 18:52:40 +00:00
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 ) ;
} )
. unwrap ( ) ;
2022-02-28 10:10:22 +00:00
2024-01-03 18:52:40 +00:00
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 ) ;
} )
. unwrap ( ) ;
2022-02-28 10:10:22 +00:00
2024-01-03 18:52:40 +00:00
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 ) ;
} )
. unwrap ( ) ;
2022-02-28 10:10:22 +00:00
2024-01-03 18:52:40 +00:00
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 ) ]
) ;
} )
. unwrap ( ) ;
2022-02-28 10:10:22 +00:00
}
2023-05-11 21:40:35 +00:00
2023-05-29 13:58:06 +00:00
#[ gpui::test ]
2024-01-04 19:13:51 +00:00
async fn test_deploy_project_search_focus ( cx : & mut TestAppContext ) {
init_test ( cx ) ;
let fs = FakeFs ::new ( cx . background_executor . clone ( ) ) ;
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 . clone ( ) ;
let search_bar = window . build_view ( cx , | _ | ProjectSearchBar ::new ( ) ) ;
let active_item = cx . read ( | cx | {
workspace
. read ( cx )
. unwrap ( )
. active_pane ( )
. read ( cx )
. active_item ( )
. and_then ( | item | item . downcast ::< ProjectSearchView > ( ) )
} ) ;
assert! (
active_item . is_none ( ) ,
" Expected no search panel to be active "
) ;
window
. update ( cx , move | workspace , cx | {
assert_eq! ( workspace . panes ( ) . len ( ) , 1 ) ;
workspace . panes ( ) [ 0 ] . update ( cx , move | pane , cx | {
pane . toolbar ( )
. update ( cx , | toolbar , cx | toolbar . add_item ( search_bar , cx ) )
} ) ;
ProjectSearchView ::deploy_search ( workspace , & workspace ::DeploySearch , cx )
} )
. unwrap ( ) ;
let Some ( search_view ) = cx . read ( | cx | {
workspace
. read ( cx )
. unwrap ( )
. active_pane ( )
. read ( cx )
. active_item ( )
. and_then ( | item | item . downcast ::< ProjectSearchView > ( ) )
} ) else {
panic! ( " Search view expected to appear after new search event trigger " )
} ;
cx . spawn ( | mut cx | async move {
window
. update ( & mut cx , | _ , cx | {
cx . dispatch_action ( ToggleFocus . boxed_clone ( ) )
} )
. unwrap ( ) ;
} )
. detach ( ) ;
cx . background_executor . run_until_parked ( ) ;
window
. update ( cx , | _ , cx | {
search_view . update ( cx , | search_view , cx | {
assert! (
search_view . query_editor . focus_handle ( cx ) . is_focused ( cx ) ,
" Empty search view should be focused after the toggle focus event: no results panel to focus on " ,
) ;
} ) ;
} ) . unwrap ( ) ;
window
. update ( cx , | _ , cx | {
search_view . update ( cx , | search_view , cx | {
let query_editor = & search_view . query_editor ;
assert! (
query_editor . focus_handle ( cx ) . 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}' "
) ;
} ) ;
} )
. unwrap ( ) ;
window
. update ( cx , | _ , cx | {
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 ) ;
} ) ;
} )
. unwrap ( ) ;
cx . background_executor . run_until_parked ( ) ;
window
. update ( cx , | _ , cx | {
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 . focus_handle ( cx ) . is_focused ( cx ) ,
" Search view should be focused after mismatching query had been used in search " ,
) ;
} ) ;
} ) . unwrap ( ) ;
cx . spawn ( | mut cx | async move {
window . update ( & mut cx , | _ , cx | {
cx . dispatch_action ( ToggleFocus . boxed_clone ( ) )
} )
} )
. detach ( ) ;
cx . background_executor . run_until_parked ( ) ;
window . update ( cx , | _ , cx | {
search_view . update ( cx , | search_view , cx | {
assert! (
search_view . query_editor . focus_handle ( cx ) . is_focused ( cx ) ,
" Search view with mismatching query should be focused after the toggle focus event: still no results panel to focus on " ,
) ;
} ) ;
} ) . unwrap ( ) ;
window
. update ( cx , | _ , 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 ) ;
} ) ;
} )
. unwrap ( ) ;
cx . background_executor . run_until_parked ( ) ;
window . update ( cx , | _ , cx | {
search_view . update ( cx , | search_view , cx | {
assert_eq! (
search_view
. results_editor
. update ( cx , | editor , cx | editor . display_text ( cx ) ) ,
" \n \n const THREE: usize = one::ONE + two::TWO; \n \n \n const TWO: usize = one::ONE + one::ONE; " ,
" Search view results should match the query "
) ;
assert! (
search_view . results_editor . focus_handle ( cx ) . is_focused ( cx ) ,
" Search view with mismatching query should be focused after search results are available " ,
) ;
} ) ;
} ) . unwrap ( ) ;
cx . spawn ( | mut cx | async move {
window
. update ( & mut cx , | _ , cx | {
cx . dispatch_action ( ToggleFocus . boxed_clone ( ) )
} )
. unwrap ( ) ;
} )
. detach ( ) ;
cx . background_executor . run_until_parked ( ) ;
window . update ( cx , | _ , cx | {
search_view . update ( cx , | search_view , cx | {
assert! (
search_view . results_editor . focus_handle ( cx ) . is_focused ( cx ) ,
" Search view with matching query should still have its results editor focused after the toggle focus event " ,
) ;
} ) ;
} ) . unwrap ( ) ;
workspace
. update ( cx , | workspace , cx | {
ProjectSearchView ::deploy_search ( workspace , & workspace ::DeploySearch , cx )
} )
. unwrap ( ) ;
window . update ( cx , | _ , cx | {
search_view . update ( cx , | search_view , cx | {
assert_eq! ( search_view . query_editor . read ( cx ) . text ( cx ) , " two " , " Query should be updated to first search result after search view 2nd open in a row " ) ;
assert_eq! (
search_view
. results_editor
. update ( cx , | editor , cx | editor . display_text ( cx ) ) ,
" \n \n const THREE: usize = one::ONE + two::TWO; \n \n \n const TWO: usize = one::ONE + one::ONE; " ,
" Results should be unchanged after search view 2nd open in a row "
) ;
assert! (
search_view . query_editor . focus_handle ( cx ) . is_focused ( cx ) ,
" Focus should be moved into query editor again after search view 2nd open in a row "
) ;
} ) ;
} ) . unwrap ( ) ;
cx . spawn ( | mut cx | async move {
window
. update ( & mut cx , | _ , cx | {
cx . dispatch_action ( ToggleFocus . boxed_clone ( ) )
} )
. unwrap ( ) ;
} )
. detach ( ) ;
cx . background_executor . run_until_parked ( ) ;
window . update ( cx , | _ , cx | {
search_view . update ( cx , | search_view , cx | {
assert! (
search_view . results_editor . focus_handle ( cx ) . is_focused ( cx ) ,
" Search view with matching query should switch focus to the results editor after the toggle focus event " ,
) ;
} ) ;
} ) . unwrap ( ) ;
}
#[ gpui::test ]
async fn test_new_project_search_focus ( cx : & mut TestAppContext ) {
2023-05-29 13:58:06 +00:00
init_test ( cx ) ;
2024-01-03 18:52:40 +00:00
let fs = FakeFs ::new ( cx . background_executor . clone ( ) ) ;
2023-05-29 13:58:06 +00:00
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 ;
2023-08-02 20:05:03 +00:00
let window = cx . add_window ( | cx | Workspace ::test_new ( project , cx ) ) ;
2024-01-03 18:52:40 +00:00
let workspace = window . clone ( ) ;
let search_bar = window . build_view ( cx , | _ | ProjectSearchBar ::new ( ) ) ;
2023-05-29 13:58:06 +00:00
let active_item = cx . read ( | cx | {
workspace
. read ( cx )
2024-01-03 18:52:40 +00:00
. unwrap ( )
2023-05-29 13:58:06 +00:00
. active_pane ( )
. read ( cx )
. active_item ( )
. and_then ( | item | item . downcast ::< ProjectSearchView > ( ) )
} ) ;
assert! (
active_item . is_none ( ) ,
2024-01-03 18:52:40 +00:00
" Expected no search panel to be active "
2023-05-29 13:58:06 +00:00
) ;
2024-01-03 18:52:40 +00:00
window
. update ( cx , move | workspace , cx | {
assert_eq! ( workspace . panes ( ) . len ( ) , 1 ) ;
workspace . panes ( ) [ 0 ] . update ( cx , move | pane , cx | {
pane . toolbar ( )
. update ( cx , | toolbar , cx | toolbar . add_item ( search_bar , cx ) )
} ) ;
2024-01-04 19:13:51 +00:00
ProjectSearchView ::new_search ( workspace , & workspace ::NewSearch , cx )
2024-01-03 18:52:40 +00:00
} )
. unwrap ( ) ;
2023-05-29 13:58:06 +00:00
let Some ( search_view ) = cx . read ( | cx | {
workspace
. read ( cx )
2024-01-03 18:52:40 +00:00
. unwrap ( )
2023-05-29 13:58:06 +00:00
. active_pane ( )
. read ( cx )
. active_item ( )
. and_then ( | item | item . downcast ::< ProjectSearchView > ( ) )
} ) else {
panic! ( " Search view expected to appear after new search event trigger " )
} ;
2023-08-08 17:08:37 +00:00
cx . spawn ( | mut cx | async move {
2024-01-03 18:52:40 +00:00
window
. update ( & mut cx , | _ , cx | {
cx . dispatch_action ( ToggleFocus . boxed_clone ( ) )
} )
. unwrap ( ) ;
2023-08-08 17:08:37 +00:00
} )
2023-05-29 13:58:06 +00:00
. detach ( ) ;
2024-01-03 18:52:40 +00:00
cx . background_executor . run_until_parked ( ) ;
2023-05-29 13:58:06 +00:00
2024-01-03 18:52:40 +00:00
window . update ( cx , | _ , cx | {
search_view . update ( cx , | search_view , cx | {
assert! (
search_view . query_editor . focus_handle ( cx ) . is_focused ( cx ) ,
" Empty search view should be focused after the toggle focus event: no results panel to focus on " ,
) ;
} ) ;
} ) . unwrap ( ) ;
window
. update ( cx , | _ , cx | {
search_view . update ( cx , | search_view , cx | {
let query_editor = & search_view . query_editor ;
assert! (
query_editor . focus_handle ( cx ) . 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}' "
) ;
} ) ;
} )
. unwrap ( ) ;
2023-05-29 13:58:06 +00:00
2024-01-03 18:52:40 +00:00
window
. update ( cx , | _ , cx | {
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 ) ;
} ) ;
} )
. unwrap ( ) ;
cx . background_executor . run_until_parked ( ) ;
window
. update ( cx , | _ , cx | {
search_view . update ( cx , | search_view , cx | {
let results_text = search_view
. results_editor
. update ( cx , | editor , cx | editor . display_text ( cx ) ) ;
assert! (
2023-05-29 13:58:06 +00:00
results_text . is_empty ( ) ,
" Search view for mismatching query should have no results but got '{results_text}' "
) ;
2024-01-03 18:52:40 +00:00
assert! (
search_view . query_editor . focus_handle ( cx ) . is_focused ( cx ) ,
2023-05-29 13:58:06 +00:00
" Search view should be focused after mismatching query had been used in search " ,
) ;
2024-01-03 18:52:40 +00:00
} ) ;
} )
. unwrap ( ) ;
cx . spawn ( | mut cx | async move {
window . update ( & mut cx , | _ , cx | {
cx . dispatch_action ( ToggleFocus . boxed_clone ( ) )
} )
} )
2023-05-29 13:58:06 +00:00
. detach ( ) ;
2024-01-03 18:52:40 +00:00
cx . background_executor . run_until_parked ( ) ;
window . update ( cx , | _ , cx | {
search_view . update ( cx , | search_view , cx | {
assert! (
search_view . query_editor . focus_handle ( cx ) . is_focused ( cx ) ,
" Search view with mismatching query should be focused after the toggle focus event: still no results panel to focus on " ,
) ;
} ) ;
} ) . unwrap ( ) ;
window
. update ( cx , | _ , 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 ) ;
} )
} )
. unwrap ( ) ;
cx . background_executor . run_until_parked ( ) ;
window . update ( cx , | _ , cx |
2023-05-29 13:58:06 +00:00
search_view . update ( cx , | search_view , cx | {
2024-01-03 18:52:40 +00:00
assert_eq! (
search_view
. results_editor
. update ( cx , | editor , cx | editor . display_text ( cx ) ) ,
" \n \n const THREE: usize = one::ONE + two::TWO; \n \n \n const TWO: usize = one::ONE + one::ONE; " ,
" Search view results should match the query "
) ;
assert! (
search_view . results_editor . focus_handle ( cx ) . is_focused ( cx ) ,
" Search view with mismatching query should be focused after search results are available " ,
) ;
} ) ) . unwrap ( ) ;
2023-08-08 17:08:37 +00:00
cx . spawn ( | mut cx | async move {
2024-01-03 18:52:40 +00:00
window
. update ( & mut cx , | _ , cx | {
cx . dispatch_action ( ToggleFocus . boxed_clone ( ) )
} )
. unwrap ( ) ;
2023-08-08 17:08:37 +00:00
} )
2023-05-29 13:58:06 +00:00
. detach ( ) ;
2024-01-03 18:52:40 +00:00
cx . background_executor . run_until_parked ( ) ;
window . update ( cx , | _ , cx | {
search_view . update ( cx , | search_view , cx | {
assert! (
search_view . results_editor . focus_handle ( cx ) . is_focused ( cx ) ,
" Search view with matching query should still have its results editor focused after the toggle focus event " ,
) ;
} ) ;
} ) . unwrap ( ) ;
2023-05-29 13:58:06 +00:00
2024-01-03 18:52:40 +00:00
workspace
. update ( cx , | workspace , cx | {
2024-01-04 19:13:51 +00:00
ProjectSearchView ::new_search ( workspace , & workspace ::NewSearch , cx )
2024-01-03 18:52:40 +00:00
} )
. unwrap ( ) ;
cx . background_executor . run_until_parked ( ) ;
2023-12-11 11:11:07 +00:00
let Some ( search_view_2 ) = cx . read ( | cx | {
workspace
. read ( cx )
2024-01-03 18:52:40 +00:00
. unwrap ( )
2023-12-11 11:11:07 +00:00
. active_pane ( )
. read ( cx )
. active_item ( )
. and_then ( | item | item . downcast ::< ProjectSearchView > ( ) )
} ) else {
panic! ( " Search view expected to appear after new search event trigger " )
} ;
2024-01-03 18:52:40 +00:00
assert! (
search_view_2 ! = search_view ,
2023-12-11 11:11:07 +00:00
" New search view should be open after `workspace::NewSearch` event "
) ;
2024-01-03 18:52:40 +00:00
window . update ( cx , | _ , cx | {
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 \n const THREE: usize = one::ONE + two::TWO; \n \n \n const TWO: usize = one::ONE + one::ONE; " ,
" Results of the first search view should not update too "
) ;
assert! (
! search_view . query_editor . focus_handle ( cx ) . is_focused ( cx ) ,
" Focus should be moved away from the first search view "
) ;
} ) ;
} ) . unwrap ( ) ;
window . update ( cx , | _ , cx | {
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 . focus_handle ( cx ) . is_focused ( cx ) ,
2024-01-17 22:31:21 +00:00
" Focus should be moved into query editor of the new window "
2024-01-03 18:52:40 +00:00
) ;
} ) ;
} ) . unwrap ( ) ;
window
. update ( cx , | _ , cx | {
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 ) ;
} ) ;
} )
. unwrap ( ) ;
cx . background_executor . run_until_parked ( ) ;
window . update ( cx , | _ , cx | {
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 \n const FOUR: usize = one::ONE + three::THREE; " ,
" New search view with the updated query should have new search results "
) ;
assert! (
search_view_2 . results_editor . focus_handle ( cx ) . is_focused ( cx ) ,
" Search view with mismatching query should be focused after search results are available " ,
) ;
} ) ;
} ) . unwrap ( ) ;
2023-05-29 13:58:06 +00:00
2023-08-08 17:08:37 +00:00
cx . spawn ( | mut cx | async move {
2024-01-03 18:52:40 +00:00
window
. update ( & mut cx , | _ , cx | {
cx . dispatch_action ( ToggleFocus . boxed_clone ( ) )
} )
. unwrap ( ) ;
2023-08-08 17:08:37 +00:00
} )
2023-05-29 13:58:06 +00:00
. detach ( ) ;
2024-01-03 18:52:40 +00:00
cx . background_executor . run_until_parked ( ) ;
window . update ( cx , | _ , cx | {
search_view_2 . update ( cx , | search_view_2 , cx | {
assert! (
search_view_2 . results_editor . focus_handle ( cx ) . is_focused ( cx ) ,
" Search view with matching query should switch focus to the results editor after the toggle focus event " ,
) ;
} ) ; } ) . unwrap ( ) ;
2023-05-29 13:58:06 +00:00
}
2023-07-20 13:01:01 +00:00
#[ gpui::test ]
2024-01-03 18:52:40 +00:00
async fn test_new_project_search_in_directory ( cx : & mut TestAppContext ) {
2023-07-20 13:01:01 +00:00
init_test ( cx ) ;
2024-01-03 18:52:40 +00:00
let fs = FakeFs ::new ( cx . background_executor . clone ( ) ) ;
2023-07-20 13:01:01 +00:00
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 | {
2024-01-03 18:52:40 +00:00
project . worktrees ( ) . next ( ) . unwrap ( ) . read ( cx ) . id ( )
2023-07-20 13:01:01 +00:00
} ) ;
2024-01-03 18:52:40 +00:00
let window = cx . add_window ( | cx | Workspace ::test_new ( project , cx ) ) ;
let workspace = window . root ( cx ) . unwrap ( ) ;
let search_bar = window . build_view ( cx , | _ | ProjectSearchBar ::new ( ) ) ;
2023-07-20 13:01:01 +00:00
let active_item = cx . read ( | cx | {
workspace
. read ( cx )
. active_pane ( )
. read ( cx )
. active_item ( )
. and_then ( | item | item . downcast ::< ProjectSearchView > ( ) )
} ) ;
assert! (
active_item . is_none ( ) ,
2024-01-03 18:52:40 +00:00
" Expected no search panel to be active "
2023-07-20 13:01:01 +00:00
) ;
2024-01-03 18:52:40 +00:00
window
. update ( cx , move | workspace , cx | {
assert_eq! ( workspace . panes ( ) . len ( ) , 1 ) ;
workspace . panes ( ) [ 0 ] . update ( cx , move | pane , cx | {
pane . toolbar ( )
. update ( cx , | toolbar , cx | toolbar . add_item ( search_bar , cx ) )
} ) ;
} )
. unwrap ( ) ;
2023-07-20 13:01:01 +00:00
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 ( ) ) ;
2024-01-03 18:52:40 +00:00
window
. update ( cx , | workspace , cx | {
ProjectSearchView ::new_search_in_directory ( workspace , & one_file_entry , cx )
} )
. unwrap ( ) ;
2023-07-20 13:01:01 +00:00
let active_search_entry = cx . read ( | cx | {
workspace
. read ( cx )
. active_pane ( )
. read ( cx )
. active_item ( )
. and_then ( | item | item . downcast ::< ProjectSearchView > ( ) )
} ) ;
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 ( ) ) ;
2024-01-03 18:52:40 +00:00
window
. update ( cx , | workspace , cx | {
ProjectSearchView ::new_search_in_directory ( workspace , & a_dir_entry , cx )
} )
. unwrap ( ) ;
2023-07-20 13:01:01 +00:00
let Some ( search_view ) = cx . read ( | cx | {
workspace
. read ( cx )
. active_pane ( )
. read ( cx )
. active_item ( )
. and_then ( | item | item . downcast ::< ProjectSearchView > ( ) )
} ) else {
panic! ( " Search view expected to appear after new search in directory event trigger " )
} ;
2024-01-03 18:52:40 +00:00
cx . background_executor . run_until_parked ( ) ;
window
. update ( cx , | _ , cx | {
search_view . update ( cx , | search_view , cx | {
assert! (
search_view . query_editor . focus_handle ( cx ) . 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 "
) ;
} ) ;
} ) ;
} )
. unwrap ( ) ;
window
. update ( cx , | _ , cx | {
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 ) ;
} ) ;
} )
. unwrap ( ) ;
cx . background_executor . run_until_parked ( ) ;
window
. update ( cx , | _ , cx | {
search_view . update ( cx , | search_view , cx | {
assert_eq! (
2023-07-20 13:01:01 +00:00
search_view
. results_editor
. update ( cx , | editor , cx | editor . display_text ( cx ) ) ,
" \n \n const ONE: usize = 1; \n \n \n const TWO: usize = one::ONE + one::ONE; " ,
" New search in directory should have a filter that matches a certain directory "
) ;
2024-01-03 18:52:40 +00:00
} )
} )
. unwrap ( ) ;
2023-07-20 13:01:01 +00:00
}
2023-07-31 22:23:51 +00:00
#[ gpui::test ]
async fn test_search_query_history ( cx : & mut TestAppContext ) {
init_test ( cx ) ;
2024-01-03 18:52:40 +00:00
let fs = FakeFs ::new ( cx . background_executor . clone ( ) ) ;
2023-07-31 22:23:51 +00:00
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 ;
2023-08-02 20:05:03 +00:00
let window = cx . add_window ( | cx | Workspace ::test_new ( project , cx ) ) ;
2024-01-03 18:52:40 +00:00
let workspace = window . root ( cx ) . unwrap ( ) ;
let search_bar = window . build_view ( cx , | _ | ProjectSearchBar ::new ( ) ) ;
window
. update ( cx , {
let search_bar = search_bar . clone ( ) ;
move | workspace , cx | {
assert_eq! ( workspace . panes ( ) . len ( ) , 1 ) ;
workspace . panes ( ) [ 0 ] . update ( cx , move | pane , cx | {
pane . toolbar ( )
. update ( cx , | toolbar , cx | toolbar . add_item ( search_bar , cx ) )
} ) ;
2024-01-04 19:13:51 +00:00
ProjectSearchView ::new_search ( workspace , & workspace ::NewSearch , cx )
2024-01-03 18:52:40 +00:00
}
} )
. unwrap ( ) ;
2023-07-31 22:23:51 +00:00
let search_view = cx . read ( | cx | {
workspace
. read ( cx )
. active_pane ( )
. read ( cx )
. active_item ( )
. and_then ( | item | item . downcast ::< ProjectSearchView > ( ) )
. expect ( " Search view expected to appear after new search event trigger " )
} ) ;
// Add 3 search items into the history + another unsubmitted one.
2024-01-03 18:52:40 +00:00
window
. update ( cx , | _ , cx | {
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 ) ;
} ) ;
} )
. unwrap ( ) ;
cx . background_executor . run_until_parked ( ) ;
window
. update ( cx , | _ , 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 ) ;
} ) ;
} )
. unwrap ( ) ;
cx . background_executor . run_until_parked ( ) ;
window
. update ( cx , | _ , cx | {
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 ) ;
} )
} )
. unwrap ( ) ;
cx . background_executor . run_until_parked ( ) ;
window
. update ( cx , | _ , cx | {
search_view . update ( cx , | search_view , cx | {
search_view . query_editor . update ( cx , | query_editor , cx | {
query_editor . set_text ( " JUST_TEXT_INPUT " , cx )
} ) ;
} )
} )
. unwrap ( ) ;
cx . background_executor . run_until_parked ( ) ;
2023-07-31 22:23:51 +00:00
// Ensure that the latest input with search settings is active.
2024-01-03 18:52:40 +00:00
window
. update ( cx , | _ , cx | {
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 ) ;
} ) ;
} )
. unwrap ( ) ;
2023-07-31 22:23:51 +00:00
// Next history query after the latest should set the query to the empty string.
2024-01-03 18:52:40 +00:00
window
. update ( cx , | _ , cx | {
search_bar . update ( cx , | search_bar , cx | {
search_bar . next_history_query ( & NextHistoryQuery , cx ) ;
} )
} )
. unwrap ( ) ;
window
. update ( cx , | _ , 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 ) ;
} ) ;
} )
. unwrap ( ) ;
window
. update ( cx , | _ , cx | {
search_bar . update ( cx , | search_bar , cx | {
search_bar . next_history_query ( & NextHistoryQuery , cx ) ;
} )
} )
. unwrap ( ) ;
window
. update ( cx , | _ , 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 ) ;
} ) ;
} )
. unwrap ( ) ;
2023-07-31 22:23:51 +00:00
// First previous query for empty current query should set the query to the latest submitted one.
2024-01-03 18:52:40 +00:00
window
. update ( cx , | _ , cx | {
search_bar . update ( cx , | search_bar , cx | {
search_bar . previous_history_query ( & PreviousHistoryQuery , cx ) ;
} ) ;
} )
. unwrap ( ) ;
window
. update ( cx , | _ , 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 ) ;
} ) ;
} )
. unwrap ( ) ;
2023-07-31 22:23:51 +00:00
// Further previous items should go over the history in reverse order.
2024-01-03 18:52:40 +00:00
window
. update ( cx , | _ , cx | {
search_bar . update ( cx , | search_bar , cx | {
search_bar . previous_history_query ( & PreviousHistoryQuery , cx ) ;
} ) ;
} )
. unwrap ( ) ;
window
. update ( cx , | _ , 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 ) ;
} ) ;
} )
. unwrap ( ) ;
2023-07-31 22:23:51 +00:00
// Previous items should never go behind the first history item.
2024-01-03 18:52:40 +00:00
window
. update ( cx , | _ , cx | {
search_bar . update ( cx , | search_bar , cx | {
search_bar . previous_history_query ( & PreviousHistoryQuery , cx ) ;
} ) ;
} )
. unwrap ( ) ;
window
. update ( cx , | _ , 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 ) ;
} ) ;
} )
. unwrap ( ) ;
window
. update ( cx , | _ , cx | {
search_bar . update ( cx , | search_bar , cx | {
search_bar . previous_history_query ( & PreviousHistoryQuery , cx ) ;
} ) ;
} )
. unwrap ( ) ;
window
. update ( cx , | _ , 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 ) ;
} ) ;
} )
. unwrap ( ) ;
2023-07-31 22:23:51 +00:00
// Next items should go over the history in the original order.
2024-01-03 18:52:40 +00:00
window
. update ( cx , | _ , cx | {
search_bar . update ( cx , | search_bar , cx | {
search_bar . next_history_query ( & NextHistoryQuery , cx ) ;
} ) ;
} )
. unwrap ( ) ;
window
. update ( cx , | _ , 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 ) ;
} ) ;
} )
. unwrap ( ) ;
window
. update ( cx , | _ , cx | {
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 ) ;
} ) ;
} )
. unwrap ( ) ;
cx . background_executor . run_until_parked ( ) ;
window
. update ( cx , | _ , 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 ) ;
} ) ;
} )
. unwrap ( ) ;
2023-07-31 22:23:51 +00:00
// New search input should add another entry to history and move the selection to the end of the history.
2024-01-03 18:52:40 +00:00
window
. update ( cx , | _ , cx | {
search_bar . update ( cx , | search_bar , cx | {
search_bar . previous_history_query ( & PreviousHistoryQuery , cx ) ;
} ) ;
} )
. unwrap ( ) ;
window
. update ( cx , | _ , 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 ) ;
} ) ;
} )
. unwrap ( ) ;
window
. update ( cx , | _ , cx | {
search_bar . update ( cx , | search_bar , cx | {
search_bar . previous_history_query ( & PreviousHistoryQuery , cx ) ;
} ) ;
} )
. unwrap ( ) ;
window
. update ( cx , | _ , 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 ) ;
} ) ;
} )
. unwrap ( ) ;
window
. update ( cx , | _ , cx | {
search_bar . update ( cx , | search_bar , cx | {
search_bar . next_history_query ( & NextHistoryQuery , cx ) ;
} ) ;
} )
. unwrap ( ) ;
window
. update ( cx , | _ , 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 ) ;
} ) ;
} )
. unwrap ( ) ;
window
. update ( cx , | _ , cx | {
search_bar . update ( cx , | search_bar , cx | {
search_bar . next_history_query ( & NextHistoryQuery , cx ) ;
} ) ;
} )
. unwrap ( ) ;
window
. update ( cx , | _ , 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 ) ;
} ) ;
} )
. unwrap ( ) ;
window
. update ( cx , | _ , cx | {
search_bar . update ( cx , | search_bar , cx | {
search_bar . next_history_query ( & NextHistoryQuery , cx ) ;
} ) ;
} )
. unwrap ( ) ;
window
. update ( cx , | _ , 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 ) ;
} ) ;
} )
. unwrap ( ) ;
2023-07-31 22:23:51 +00:00
}
2024-01-11 16:20:10 +00:00
#[ gpui::test ]
async fn test_deploy_search_with_multiple_panes ( cx : & mut TestAppContext ) {
init_test ( cx ) ;
2024-01-16 18:06:48 +00:00
// Setup 2 panes, both with a file open and one with a project search.
2024-01-11 16:20:10 +00:00
let fs = FakeFs ::new ( cx . background_executor . clone ( ) ) ;
fs . insert_tree (
" /dir " ,
json! ( {
" one.rs " : " const ONE: usize = 1; " ,
} ) ,
)
. await ;
let project = Project ::test ( fs . clone ( ) , [ " /dir " . as_ref ( ) ] , cx ) . await ;
let worktree_id = project . update ( cx , | this , cx | {
this . worktrees ( ) . next ( ) . unwrap ( ) . read ( cx ) . id ( )
} ) ;
let window = cx . add_window ( | cx | Workspace ::test_new ( project , cx ) ) ;
let panes : Vec < _ > = window
. update ( cx , | this , _ | this . panes ( ) . to_owned ( ) )
. unwrap ( ) ;
assert_eq! ( panes . len ( ) , 1 ) ;
let first_pane = panes . get ( 0 ) . cloned ( ) . unwrap ( ) ;
assert_eq! ( cx . update ( | cx | first_pane . read ( cx ) . items_len ( ) ) , 0 ) ;
window
. update ( cx , | workspace , cx | {
workspace . open_path (
( worktree_id , " one.rs " ) ,
Some ( first_pane . downgrade ( ) ) ,
true ,
cx ,
)
} )
. unwrap ( )
. await
. unwrap ( ) ;
assert_eq! ( cx . update ( | cx | first_pane . read ( cx ) . items_len ( ) ) , 1 ) ;
let second_pane = window
. update ( cx , | workspace , cx | {
workspace . split_and_clone ( first_pane . clone ( ) , workspace ::SplitDirection ::Right , cx )
} )
. unwrap ( )
. unwrap ( ) ;
assert_eq! ( cx . update ( | cx | second_pane . read ( cx ) . items_len ( ) ) , 1 ) ;
assert! ( window
. update ( cx , | _ , cx | second_pane
. focus_handle ( cx )
. contains_focused ( cx ) )
. unwrap ( ) ) ;
let search_bar = window . build_view ( cx , | _ | ProjectSearchBar ::new ( ) ) ;
window
. update ( cx , {
let search_bar = search_bar . clone ( ) ;
let pane = first_pane . clone ( ) ;
move | workspace , cx | {
assert_eq! ( workspace . panes ( ) . len ( ) , 2 ) ;
pane . update ( cx , move | pane , cx | {
pane . toolbar ( )
. update ( cx , | toolbar , cx | toolbar . add_item ( search_bar , cx ) )
} ) ;
}
} )
. unwrap ( ) ;
2024-01-16 18:06:48 +00:00
// Add a project search item to the second pane
2024-01-11 16:20:10 +00:00
window
. update ( cx , {
let search_bar = search_bar . clone ( ) ;
let pane = second_pane . clone ( ) ;
move | workspace , cx | {
assert_eq! ( workspace . panes ( ) . len ( ) , 2 ) ;
pane . update ( cx , move | pane , cx | {
pane . toolbar ( )
. update ( cx , | toolbar , cx | toolbar . add_item ( search_bar , cx ) )
} ) ;
ProjectSearchView ::new_search ( workspace , & workspace ::NewSearch , cx )
}
} )
. unwrap ( ) ;
cx . run_until_parked ( ) ;
assert_eq! ( cx . update ( | cx | second_pane . read ( cx ) . items_len ( ) ) , 2 ) ;
assert_eq! ( cx . update ( | cx | first_pane . read ( cx ) . items_len ( ) ) , 1 ) ;
2024-01-16 18:06:48 +00:00
// Focus the first pane
2024-01-11 16:20:10 +00:00
window
. update ( cx , | workspace , cx | {
assert_eq! ( workspace . active_pane ( ) , & second_pane ) ;
second_pane . update ( cx , | this , cx | {
assert_eq! ( this . active_item_index ( ) , 1 ) ;
this . activate_prev_item ( false , cx ) ;
assert_eq! ( this . active_item_index ( ) , 0 ) ;
} ) ;
workspace . activate_pane_in_direction ( workspace ::SplitDirection ::Left , cx ) ;
} )
. unwrap ( ) ;
window
. update ( cx , | workspace , cx | {
assert_eq! ( workspace . active_pane ( ) , & first_pane ) ;
assert_eq! ( first_pane . read ( cx ) . items_len ( ) , 1 ) ;
assert_eq! ( second_pane . read ( cx ) . items_len ( ) , 2 ) ;
} )
. unwrap ( ) ;
2024-01-16 18:06:48 +00:00
// Deploy a new search
2024-01-11 16:20:10 +00:00
cx . dispatch_action ( window . into ( ) , DeploySearch ) ;
2024-01-16 18:06:48 +00:00
// Both panes should now have a project search in them
2024-01-11 16:20:10 +00:00
window
. update ( cx , | workspace , cx | {
2024-01-16 18:06:48 +00:00
assert_eq! ( workspace . active_pane ( ) , & first_pane ) ;
first_pane . update ( cx , | this , _ | {
2024-01-11 16:20:10 +00:00
assert_eq! ( this . active_item_index ( ) , 1 ) ;
assert_eq! ( this . items_len ( ) , 2 ) ;
} ) ;
2024-01-16 18:06:48 +00:00
second_pane . update ( cx , | this , cx | {
2024-01-11 16:20:10 +00:00
assert! ( ! cx . focus_handle ( ) . contains_focused ( cx ) ) ;
2024-01-16 18:06:48 +00:00
assert_eq! ( this . items_len ( ) , 2 ) ;
} ) ;
} )
. unwrap ( ) ;
// Focus the second pane's non-search item
window
. update ( cx , | _workspace , cx | {
second_pane . update ( cx , | pane , cx | pane . activate_next_item ( true , cx ) ) ;
} )
. unwrap ( ) ;
// Deploy a new search
cx . dispatch_action ( window . into ( ) , DeploySearch ) ;
// The project search view should now be focused in the second pane
// And the number of items should be unchanged.
window
. update ( cx , | _workspace , cx | {
second_pane . update ( cx , | pane , _cx | {
assert! ( pane
. active_item ( )
. unwrap ( )
. downcast ::< ProjectSearchView > ( )
. is_some ( ) ) ;
assert_eq! ( pane . items_len ( ) , 2 ) ;
2024-01-11 16:20:10 +00:00
} ) ;
} )
. unwrap ( ) ;
}
2023-05-11 21:40:35 +00:00
pub fn init_test ( cx : & mut TestAppContext ) {
cx . update ( | cx | {
2024-01-03 18:52:40 +00:00
let settings = SettingsStore ::test ( cx ) ;
cx . set_global ( settings ) ;
2024-01-16 18:06:48 +00:00
2024-01-03 18:52:40 +00:00
SemanticIndexSettings ::register ( cx ) ;
2023-05-17 00:29:21 +00:00
2024-01-03 18:52:40 +00:00
theme ::init ( theme ::LoadThemes ::JustBase , cx ) ;
2023-05-11 21:40:35 +00:00
language ::init ( cx ) ;
2023-05-17 16:55:24 +00:00
client ::init_settings ( cx ) ;
2023-05-29 13:58:06 +00:00
editor ::init ( cx ) ;
2023-05-17 03:25:18 +00:00
workspace ::init_settings ( cx ) ;
2023-05-25 14:31:19 +00:00
Project ::init_settings ( cx ) ;
2023-05-29 13:58:06 +00:00
super ::init ( cx ) ;
2023-05-11 21:40:35 +00:00
} ) ;
}
2022-02-28 10:10:22 +00:00
}