diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index f50e945df3..7ee10e238f 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -37,7 +37,7 @@ use ui::{ Icon, IconButton, IconButtonShape, IconName, KeyBinding, Label, LabelCommon, LabelSize, Toggleable, Tooltip, h_flex, prelude::*, utils::SearchInputWidth, v_flex, }; -use util::paths::PathMatcher; +use util::{ResultExt as _, paths::PathMatcher}; use workspace::{ DeploySearch, ItemNavHistory, NewSearch, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId, @@ -72,15 +72,18 @@ pub fn init(cx: &mut App) { ); register_workspace_action( workspace, - move |search_bar, _: &ToggleCaseSensitive, _, cx| { - search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx); + move |search_bar, _: &ToggleCaseSensitive, window, cx| { + search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx); }, ); - register_workspace_action(workspace, move |search_bar, _: &ToggleWholeWord, _, cx| { - search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx); - }); - register_workspace_action(workspace, move |search_bar, _: &ToggleRegex, _, cx| { - search_bar.toggle_search_option(SearchOptions::REGEX, cx); + register_workspace_action( + workspace, + move |search_bar, _: &ToggleWholeWord, window, cx| { + search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx); + }, + ); + register_workspace_action(workspace, move |search_bar, _: &ToggleRegex, window, cx| { + search_bar.toggle_search_option(SearchOptions::REGEX, window, cx); }); register_workspace_action( workspace, @@ -1032,6 +1035,61 @@ impl ProjectSearchView { }); } + fn prompt_to_save_if_dirty_then_search( + &mut self, + window: &mut Window, + cx: &mut Context, + ) -> Task> { + use workspace::AutosaveSetting; + + let project = self.entity.read(cx).project.clone(); + + let can_autosave = self.results_editor.can_autosave(cx); + let autosave_setting = self.results_editor.workspace_settings(cx).autosave; + + let will_autosave = can_autosave + && matches!( + autosave_setting, + AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange + ); + + let is_dirty = self.is_dirty(cx); + + let should_confirm_save = !will_autosave && is_dirty; + + cx.spawn_in(window, async move |this, cx| { + let should_search = if should_confirm_save { + let options = &["Save", "Don't Save", "Cancel"]; + let result_channel = this.update_in(cx, |_, window, cx| { + window.prompt( + gpui::PromptLevel::Warning, + "Project search buffer contains unsaved edits. Do you want to save it?", + None, + options, + cx, + ) + })?; + let result = result_channel.await?; + let should_save = result == 0; + if should_save { + this.update_in(cx, |this, window, cx| this.save(true, project, window, cx))? + .await + .log_err(); + } + let should_search = result != 2; + should_search + } else { + true + }; + if should_search { + this.update(cx, |this, cx| { + this.search(cx); + })?; + } + anyhow::Ok(()) + }) + } + fn search(&mut self, cx: &mut Context) { if let Some(query) = self.build_search_query(cx) { self.entity.update(cx, |model, cx| model.search(query, cx)); @@ -1503,7 +1561,9 @@ impl ProjectSearchBar { .is_focused(window) { cx.stop_propagation(); - search_view.search(cx); + search_view + .prompt_to_save_if_dirty_then_search(window, cx) + .detach_and_log_err(cx); } }); } @@ -1570,19 +1630,39 @@ impl ProjectSearchBar { }); } - fn toggle_search_option(&mut self, option: SearchOptions, cx: &mut Context) -> bool { - if let Some(search_view) = self.active_project_search.as_ref() { - search_view.update(cx, |search_view, cx| { - search_view.toggle_search_option(option, cx); - if search_view.entity.read(cx).active_query.is_some() { - search_view.search(cx); - } - }); - cx.notify(); - true - } else { - false + fn toggle_search_option( + &mut self, + option: SearchOptions, + window: &mut Window, + cx: &mut Context, + ) -> bool { + if self.active_project_search.is_none() { + return false; } + + cx.spawn_in(window, async move |this, cx| { + let task = this.update_in(cx, |this, window, cx| { + let search_view = this.active_project_search.as_ref()?; + search_view.update(cx, |search_view, cx| { + search_view.toggle_search_option(option, cx); + search_view + .entity + .read(cx) + .active_query + .is_some() + .then(|| search_view.prompt_to_save_if_dirty_then_search(window, cx)) + }) + })?; + if let Some(task) = task { + task.await?; + } + this.update(cx, |_, cx| { + cx.notify(); + })?; + anyhow::Ok(()) + }) + .detach(); + true } fn toggle_replace(&mut self, _: &ToggleReplace, window: &mut Window, cx: &mut Context) { @@ -1621,19 +1701,33 @@ impl ProjectSearchBar { } fn toggle_opened_only(&mut self, window: &mut Window, cx: &mut Context) -> bool { - if let Some(search_view) = self.active_project_search.as_ref() { - search_view.update(cx, |search_view, cx| { - search_view.toggle_opened_only(window, cx); - if search_view.entity.read(cx).active_query.is_some() { - search_view.search(cx); - } - }); - - cx.notify(); - true - } else { - false + if self.active_project_search.is_none() { + return false; } + + cx.spawn_in(window, async move |this, cx| { + let task = this.update_in(cx, |this, window, cx| { + let search_view = this.active_project_search.as_ref()?; + search_view.update(cx, |search_view, cx| { + search_view.toggle_opened_only(window, cx); + search_view + .entity + .read(cx) + .active_query + .is_some() + .then(|| search_view.prompt_to_save_if_dirty_then_search(window, cx)) + }) + })?; + if let Some(task) = task { + task.await?; + } + this.update(cx, |_, cx| { + cx.notify(); + })?; + anyhow::Ok(()) + }) + .detach(); + true } fn is_opened_only_enabled(&self, cx: &App) -> bool { @@ -1860,22 +1954,22 @@ impl Render for ProjectSearchBar { .child(SearchOptions::CASE_SENSITIVE.as_button( self.is_option_enabled(SearchOptions::CASE_SENSITIVE, cx), focus_handle.clone(), - cx.listener(|this, _, _, cx| { - this.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx); + cx.listener(|this, _, window, cx| { + this.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx); }), )) .child(SearchOptions::WHOLE_WORD.as_button( self.is_option_enabled(SearchOptions::WHOLE_WORD, cx), focus_handle.clone(), - cx.listener(|this, _, _, cx| { - this.toggle_search_option(SearchOptions::WHOLE_WORD, cx); + cx.listener(|this, _, window, cx| { + this.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx); }), )) .child(SearchOptions::REGEX.as_button( self.is_option_enabled(SearchOptions::REGEX, cx), focus_handle.clone(), - cx.listener(|this, _, _, cx| { - this.toggle_search_option(SearchOptions::REGEX, cx); + cx.listener(|this, _, window, cx| { + this.toggle_search_option(SearchOptions::REGEX, window, cx); }), )), ); @@ -2147,8 +2241,12 @@ impl Render for ProjectSearchBar { .search_options .contains(SearchOptions::INCLUDE_IGNORED), focus_handle.clone(), - cx.listener(|this, _, _, cx| { - this.toggle_search_option(SearchOptions::INCLUDE_IGNORED, cx); + cx.listener(|this, _, window, cx| { + this.toggle_search_option( + SearchOptions::INCLUDE_IGNORED, + window, + cx, + ); }), ), ), @@ -2188,11 +2286,11 @@ impl Render for ProjectSearchBar { .on_action(cx.listener(|this, action, window, cx| { this.toggle_replace(action, window, cx); })) - .on_action(cx.listener(|this, _: &ToggleWholeWord, _, cx| { - this.toggle_search_option(SearchOptions::WHOLE_WORD, cx); + .on_action(cx.listener(|this, _: &ToggleWholeWord, window, cx| { + this.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx); })) - .on_action(cx.listener(|this, _: &ToggleCaseSensitive, _, cx| { - this.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx); + .on_action(cx.listener(|this, _: &ToggleCaseSensitive, window, cx| { + this.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx); })) .on_action(cx.listener(|this, action, window, cx| { if let Some(search) = this.active_project_search.as_ref() { @@ -2209,8 +2307,8 @@ impl Render for ProjectSearchBar { } })) .when(search.filters_enabled, |this| { - this.on_action(cx.listener(|this, _: &ToggleIncludeIgnored, _, cx| { - this.toggle_search_option(SearchOptions::INCLUDE_IGNORED, cx); + this.on_action(cx.listener(|this, _: &ToggleIncludeIgnored, window, cx| { + this.toggle_search_option(SearchOptions::INCLUDE_IGNORED, window, cx); })) }) .on_action(cx.listener(Self::select_next_match)) diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index ebc6123af9..ad7f769fd5 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -564,6 +564,10 @@ pub trait ItemHandle: 'static + Send { fn preserve_preview(&self, cx: &App) -> bool; fn include_in_nav_history(&self) -> bool; fn relay_action(&self, action: Box, window: &mut Window, cx: &mut App); + fn can_autosave(&self, cx: &App) -> bool { + let is_deleted = self.project_entry_ids(cx).is_empty(); + self.is_dirty(cx) && !self.has_conflict(cx) && self.can_save(cx) && !is_deleted + } } pub trait WeakItemHandle: Send + Sync { diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index a4afba20d7..41cf81d897 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1857,7 +1857,7 @@ impl Pane { matches!( item.workspace_settings(cx).autosave, AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange - ) && Self::can_autosave_item(item, cx) + ) && item.can_autosave(cx) })?; if !will_autosave { let item_id = item.item_id(); @@ -1945,11 +1945,6 @@ impl Pane { }) } - fn can_autosave_item(item: &dyn ItemHandle, cx: &App) -> bool { - let is_deleted = item.project_entry_ids(cx).is_empty(); - item.is_dirty(cx) && !item.has_conflict(cx) && item.can_save(cx) && !is_deleted - } - pub fn autosave_item( item: &dyn ItemHandle, project: Entity, @@ -1960,7 +1955,7 @@ impl Pane { item.workspace_settings(cx).autosave, AutosaveSetting::AfterDelay { .. } ); - if Self::can_autosave_item(item, cx) { + if item.can_autosave(cx) { item.save(format, project, window, cx) } else { Task::ready(Ok(()))