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,
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<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>) {
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<Self>) -> 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<Self>,
) -> 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<Self>) {
@ -1621,19 +1701,33 @@ impl ProjectSearchBar {
}
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() {
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))

View file

@ -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<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 {

View file

@ -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<Project>,
@ -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(()))