@@ -925,10 +925,10 @@ impl<T: Item> ItemHandle for Entity<T> {
},
));
- cx.on_blur(
+ cx.on_focus_out(
&self.read(cx).focus_handle(cx),
window,
- move |workspace, window, cx| {
+ move |workspace, _event, window, cx| {
if let Some(item) = weak_item.upgrade()
&& item.workspace_settings(cx).autosave == AutosaveSetting::OnFocusChange
{
@@ -1371,7 +1371,8 @@ pub mod test {
};
use gpui::{
AnyElement, App, AppContext as _, Context, Entity, EntityId, EventEmitter, Focusable,
- InteractiveElement, IntoElement, Render, SharedString, Task, WeakEntity, Window,
+ InteractiveElement, IntoElement, ParentElement, Render, SharedString, Task, WeakEntity,
+ Window,
};
use project::{Project, ProjectEntryId, ProjectPath, WorktreeId};
use std::{any::Any, cell::Cell, sync::Arc};
@@ -1400,6 +1401,7 @@ pub mod test {
pub tab_detail: Cell<Option<usize>>,
serialize: Option<Box<dyn Fn() -> Option<Task<anyhow::Result<()>>>>>,
focus_handle: gpui::FocusHandle,
+ pub child_focus_handles: Vec<gpui::FocusHandle>,
}
impl project::ProjectItem for TestProjectItem {
@@ -1482,6 +1484,7 @@ pub mod test {
workspace_id: Default::default(),
focus_handle: cx.focus_handle(),
serialize: None,
+ child_focus_handles: Vec::new(),
}
}
@@ -1529,6 +1532,11 @@ pub mod test {
self
}
+ pub fn with_child_focus_handles(mut self, count: usize, cx: &mut Context<Self>) -> Self {
+ self.child_focus_handles = (0..count).map(|_| cx.focus_handle()).collect();
+ self
+ }
+
pub fn set_state(&mut self, state: String, cx: &mut Context<Self>) {
self.push_to_nav_history(cx);
self.state = state;
@@ -1543,7 +1551,12 @@ pub mod test {
impl Render for TestItem {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
- gpui::div().track_focus(&self.focus_handle(cx))
+ let parent = gpui::div().track_focus(&self.focus_handle(cx));
+ self.child_focus_handles
+ .iter()
+ .fold(parent, |parent, child_handle| {
+ parent.child(gpui::div().track_focus(child_handle))
+ })
}
}
@@ -1641,23 +1654,30 @@ pub mod test {
where
Self: Sized,
{
- Task::ready(Some(cx.new(|cx| Self {
- state: self.state.clone(),
- label: self.label.clone(),
- save_count: self.save_count,
- save_as_count: self.save_as_count,
- reload_count: self.reload_count,
- is_dirty: self.is_dirty,
- buffer_kind: self.buffer_kind,
- has_conflict: self.has_conflict,
- has_deleted_file: self.has_deleted_file,
- project_items: self.project_items.clone(),
- nav_history: None,
- tab_descriptions: None,
- tab_detail: Default::default(),
- workspace_id: self.workspace_id,
- focus_handle: cx.focus_handle(),
- serialize: None,
+ Task::ready(Some(cx.new(|cx| {
+ Self {
+ state: self.state.clone(),
+ label: self.label.clone(),
+ save_count: self.save_count,
+ save_as_count: self.save_as_count,
+ reload_count: self.reload_count,
+ is_dirty: self.is_dirty,
+ buffer_kind: self.buffer_kind,
+ has_conflict: self.has_conflict,
+ has_deleted_file: self.has_deleted_file,
+ project_items: self.project_items.clone(),
+ nav_history: None,
+ tab_descriptions: None,
+ tab_detail: Default::default(),
+ workspace_id: self.workspace_id,
+ focus_handle: cx.focus_handle(),
+ serialize: None,
+ child_focus_handles: self
+ .child_focus_handles
+ .iter()
+ .map(|_| cx.focus_handle())
+ .collect(),
+ }
})))
}
@@ -10651,6 +10651,85 @@ mod tests {
item.read_with(cx, |item, _| assert_eq!(item.save_count, 6));
}
+ #[gpui::test]
+ async fn test_autosave_on_focus_change_in_multibuffer(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.executor());
+ let project = Project::test(fs, [], cx).await;
+ let (workspace, cx) =
+ cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
+
+ // Create a multibuffer-like item with two child focus handles,
+ // simulating individual buffer editors within a multibuffer.
+ let item = cx.new(|cx| {
+ TestItem::new(cx)
+ .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
+ .with_child_focus_handles(2, cx)
+ });
+ workspace.update_in(cx, |workspace, window, cx| {
+ workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
+ });
+
+ // Set autosave to OnFocusChange and focus the first child handle,
+ // simulating the user's cursor being inside one of the multibuffer's excerpts.
+ item.update_in(cx, |item, window, cx| {
+ SettingsStore::update_global(cx, |settings, cx| {
+ settings.update_user_settings(cx, |settings| {
+ settings.workspace.autosave = Some(AutosaveSetting::OnFocusChange);
+ })
+ });
+ item.is_dirty = true;
+ window.focus(&item.child_focus_handles[0], cx);
+ });
+ cx.executor().run_until_parked();
+ item.read_with(cx, |item, _| assert_eq!(item.save_count, 0));
+
+ // Moving focus from one child to another within the same item should
+ // NOT trigger autosave — focus is still within the item's focus hierarchy.
+ item.update_in(cx, |item, window, cx| {
+ window.focus(&item.child_focus_handles[1], cx);
+ });
+ cx.executor().run_until_parked();
+ item.read_with(cx, |item, _| {
+ assert_eq!(
+ item.save_count, 0,
+ "Switching focus between children within the same item should not autosave"
+ );
+ });
+
+ // Blurring the item saves the file. This is the core regression scenario:
+ // with `on_blur`, this would NOT trigger because `on_blur` only fires when
+ // the item's own focus handle is the leaf that lost focus. In a multibuffer,
+ // the leaf is always a child focus handle, so `on_blur` never detected
+ // focus leaving the item.
+ item.update_in(cx, |_, window, _| window.blur());
+ cx.executor().run_until_parked();
+ item.read_with(cx, |item, _| {
+ assert_eq!(
+ item.save_count, 1,
+ "Blurring should trigger autosave when focus was on a child of the item"
+ );
+ });
+
+ // Deactivating the window should also trigger autosave when a child of
+ // the multibuffer item currently owns focus.
+ item.update_in(cx, |item, window, cx| {
+ item.is_dirty = true;
+ window.focus(&item.child_focus_handles[0], cx);
+ });
+ cx.executor().run_until_parked();
+ item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
+
+ cx.deactivate_window();
+ item.read_with(cx, |item, _| {
+ assert_eq!(
+ item.save_count, 2,
+ "Deactivating window should trigger autosave when focus was on a child"
+ );
+ });
+ }
+
#[gpui::test]
async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
init_test(cx);