Fix project search unsaved edits (#30864)

Closes #30820

Release Notes:

- Fixed an issue where entering a new search in the project search would
drop unsaved edits in the project search buffer

---------

Co-authored-by: Mark Janssen <20283+praseodym@users.noreply.github.com>
This commit is contained in:
Ben Kunkle 2025-05-17 04:59:51 -05:00 committed by GitHub
parent 4d827924f0
commit f56960ab5b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 151 additions and 54 deletions

View file

@ -37,7 +37,7 @@ use ui::{
Icon, IconButton, IconButtonShape, IconName, KeyBinding, Label, LabelCommon, LabelSize, Icon, IconButton, IconButtonShape, IconName, KeyBinding, Label, LabelCommon, LabelSize,
Toggleable, Tooltip, h_flex, prelude::*, utils::SearchInputWidth, v_flex, Toggleable, Tooltip, h_flex, prelude::*, utils::SearchInputWidth, v_flex,
}; };
use util::paths::PathMatcher; use util::{ResultExt as _, paths::PathMatcher};
use workspace::{ use workspace::{
DeploySearch, ItemNavHistory, NewSearch, ToolbarItemEvent, ToolbarItemLocation, DeploySearch, ItemNavHistory, NewSearch, ToolbarItemEvent, ToolbarItemLocation,
ToolbarItemView, Workspace, WorkspaceId, ToolbarItemView, Workspace, WorkspaceId,
@ -72,15 +72,18 @@ pub fn init(cx: &mut App) {
); );
register_workspace_action( register_workspace_action(
workspace, workspace,
move |search_bar, _: &ToggleCaseSensitive, _, cx| { move |search_bar, _: &ToggleCaseSensitive, window, cx| {
search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx); search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
}, },
); );
register_workspace_action(workspace, move |search_bar, _: &ToggleWholeWord, _, cx| { register_workspace_action(
search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx); workspace,
}); move |search_bar, _: &ToggleWholeWord, window, cx| {
register_workspace_action(workspace, move |search_bar, _: &ToggleRegex, _, cx| { search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
search_bar.toggle_search_option(SearchOptions::REGEX, cx); },
);
register_workspace_action(workspace, move |search_bar, _: &ToggleRegex, window, cx| {
search_bar.toggle_search_option(SearchOptions::REGEX, window, cx);
}); });
register_workspace_action( register_workspace_action(
workspace, workspace,
@ -1032,6 +1035,61 @@ impl ProjectSearchView {
}); });
} }
fn prompt_to_save_if_dirty_then_search(
&mut self,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<anyhow::Result<()>> {
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<Self>) { fn search(&mut self, cx: &mut Context<Self>) {
if let Some(query) = self.build_search_query(cx) { if let Some(query) = self.build_search_query(cx) {
self.entity.update(cx, |model, cx| model.search(query, cx)); self.entity.update(cx, |model, cx| model.search(query, cx));
@ -1503,7 +1561,9 @@ impl ProjectSearchBar {
.is_focused(window) .is_focused(window)
{ {
cx.stop_propagation(); 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<Self>) -> bool { fn toggle_search_option(
if let Some(search_view) = self.active_project_search.as_ref() { &mut self,
search_view.update(cx, |search_view, cx| { option: SearchOptions,
search_view.toggle_search_option(option, cx); window: &mut Window,
if search_view.entity.read(cx).active_query.is_some() { cx: &mut Context<Self>,
search_view.search(cx); ) -> bool {
} if self.active_project_search.is_none() {
}); return false;
cx.notify();
true
} else {
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<Self>) { fn toggle_replace(&mut self, _: &ToggleReplace, window: &mut Window, cx: &mut Context<Self>) {
@ -1621,19 +1701,33 @@ impl ProjectSearchBar {
} }
fn toggle_opened_only(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool { fn toggle_opened_only(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
if let Some(search_view) = self.active_project_search.as_ref() { if self.active_project_search.is_none() {
search_view.update(cx, |search_view, cx| { return false;
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
} }
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 { fn is_opened_only_enabled(&self, cx: &App) -> bool {
@ -1860,22 +1954,22 @@ impl Render for ProjectSearchBar {
.child(SearchOptions::CASE_SENSITIVE.as_button( .child(SearchOptions::CASE_SENSITIVE.as_button(
self.is_option_enabled(SearchOptions::CASE_SENSITIVE, cx), self.is_option_enabled(SearchOptions::CASE_SENSITIVE, cx),
focus_handle.clone(), focus_handle.clone(),
cx.listener(|this, _, _, cx| { cx.listener(|this, _, window, cx| {
this.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx); this.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
}), }),
)) ))
.child(SearchOptions::WHOLE_WORD.as_button( .child(SearchOptions::WHOLE_WORD.as_button(
self.is_option_enabled(SearchOptions::WHOLE_WORD, cx), self.is_option_enabled(SearchOptions::WHOLE_WORD, cx),
focus_handle.clone(), focus_handle.clone(),
cx.listener(|this, _, _, cx| { cx.listener(|this, _, window, cx| {
this.toggle_search_option(SearchOptions::WHOLE_WORD, cx); this.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
}), }),
)) ))
.child(SearchOptions::REGEX.as_button( .child(SearchOptions::REGEX.as_button(
self.is_option_enabled(SearchOptions::REGEX, cx), self.is_option_enabled(SearchOptions::REGEX, cx),
focus_handle.clone(), focus_handle.clone(),
cx.listener(|this, _, _, cx| { cx.listener(|this, _, window, cx| {
this.toggle_search_option(SearchOptions::REGEX, cx); this.toggle_search_option(SearchOptions::REGEX, window, cx);
}), }),
)), )),
); );
@ -2147,8 +2241,12 @@ impl Render for ProjectSearchBar {
.search_options .search_options
.contains(SearchOptions::INCLUDE_IGNORED), .contains(SearchOptions::INCLUDE_IGNORED),
focus_handle.clone(), focus_handle.clone(),
cx.listener(|this, _, _, cx| { cx.listener(|this, _, window, cx| {
this.toggle_search_option(SearchOptions::INCLUDE_IGNORED, 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| { .on_action(cx.listener(|this, action, window, cx| {
this.toggle_replace(action, window, cx); this.toggle_replace(action, window, cx);
})) }))
.on_action(cx.listener(|this, _: &ToggleWholeWord, _, cx| { .on_action(cx.listener(|this, _: &ToggleWholeWord, window, cx| {
this.toggle_search_option(SearchOptions::WHOLE_WORD, cx); this.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
})) }))
.on_action(cx.listener(|this, _: &ToggleCaseSensitive, _, cx| { .on_action(cx.listener(|this, _: &ToggleCaseSensitive, window, cx| {
this.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx); this.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
})) }))
.on_action(cx.listener(|this, action, window, cx| { .on_action(cx.listener(|this, action, window, cx| {
if let Some(search) = this.active_project_search.as_ref() { if let Some(search) = this.active_project_search.as_ref() {
@ -2209,8 +2307,8 @@ impl Render for ProjectSearchBar {
} }
})) }))
.when(search.filters_enabled, |this| { .when(search.filters_enabled, |this| {
this.on_action(cx.listener(|this, _: &ToggleIncludeIgnored, _, cx| { this.on_action(cx.listener(|this, _: &ToggleIncludeIgnored, window, cx| {
this.toggle_search_option(SearchOptions::INCLUDE_IGNORED, cx); this.toggle_search_option(SearchOptions::INCLUDE_IGNORED, window, cx);
})) }))
}) })
.on_action(cx.listener(Self::select_next_match)) .on_action(cx.listener(Self::select_next_match))

View file

@ -564,6 +564,10 @@ pub trait ItemHandle: 'static + Send {
fn preserve_preview(&self, cx: &App) -> bool; fn preserve_preview(&self, cx: &App) -> bool;
fn include_in_nav_history(&self) -> bool; fn include_in_nav_history(&self) -> bool;
fn relay_action(&self, action: Box<dyn Action>, window: &mut Window, cx: &mut App); fn relay_action(&self, action: Box<dyn Action>, 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 { pub trait WeakItemHandle: Send + Sync {

View file

@ -1857,7 +1857,7 @@ impl Pane {
matches!( matches!(
item.workspace_settings(cx).autosave, item.workspace_settings(cx).autosave,
AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange
) && Self::can_autosave_item(item, cx) ) && item.can_autosave(cx)
})?; })?;
if !will_autosave { if !will_autosave {
let item_id = item.item_id(); 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( pub fn autosave_item(
item: &dyn ItemHandle, item: &dyn ItemHandle,
project: Entity<Project>, project: Entity<Project>,
@ -1960,7 +1955,7 @@ impl Pane {
item.workspace_settings(cx).autosave, item.workspace_settings(cx).autosave,
AutosaveSetting::AfterDelay { .. } AutosaveSetting::AfterDelay { .. }
); );
if Self::can_autosave_item(item, cx) { if item.can_autosave(cx) {
item.save(format, project, window, cx) item.save(format, project, window, cx)
} else { } else {
Task::ready(Ok(())) Task::ready(Ok(()))