editor: Fix stale session path for renamed files (#52539)

saberoueslati created

## Context

When a file is renamed via the project panel while open in an editor,
Zed would restore the old (no longer existing) path on reload .

On rename, `language::Buffer` emits `BufferEvent::FileHandleChanged`,
which propagates to `multi_buffer::Event::FileHandleChanged` →
`EditorEvent::TitleChanged`. However, `should_serialize()` only matched
`Saved | DirtyChanged | BufferEdited`, so no re-serialization was
triggered and the `editors` DB table retained the stale `abs_path`.

The fix adds a dedicated `EditorEvent::FileHandleChanged` variant, emits
it alongside `TitleChanged` when the buffer's file handle changes, and
adds it to `should_serialize()`. Since `Editor::serialize()` already
reads the current path from the buffer at call time, this naturally
writes the new path to the DB.

Closes #51629

## How to Review

Three files edited:
- `editor.rs`: splits `FileHandleChanged` into its own arm and emits the
new event variant
- `items.rs`: adds `FileHandleChanged` to `should_serialize`
- `items.rs` (test): `test_file_handle_changed_on_rename` does a full
rename via `Project::rename_entry` and asserts the event fires and the
buffer path updates

## Self-Review Checklist

- [x] I've reviewed my own diff for quality, security, and reliability
- [ ] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- Fixed renamed files being reopened with their old path after a restart

**Important remark :**
This pull request is a follow-up on the review of @SomeoneToIgnore on
this pull request, https://github.com/zed-industries/zed/pull/51717,
which was inadvertently closed because I mistakenly deleted my previous
fork of the zed repo, sorry for any inconvenience caused by this

Manual test video below :

[Screencast from 2026-03-16
23-28-46.webm](https://github.com/user-attachments/assets/ff2e3259-ae26-4655-83b8-f693e84306d2)

Change summary

crates/editor/src/editor.rs | 11 +++-
crates/editor/src/items.rs  | 80 ++++++++++++++++++++++++++++++++++++++
2 files changed, 87 insertions(+), 4 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -24592,9 +24592,13 @@ impl Editor {
             }
             multi_buffer::Event::DirtyChanged => cx.emit(EditorEvent::DirtyChanged),
             multi_buffer::Event::Saved => cx.emit(EditorEvent::Saved),
-            multi_buffer::Event::FileHandleChanged
-            | multi_buffer::Event::Reloaded
-            | multi_buffer::Event::BufferDiffChanged => cx.emit(EditorEvent::TitleChanged),
+            multi_buffer::Event::FileHandleChanged => {
+                cx.emit(EditorEvent::TitleChanged);
+                cx.emit(EditorEvent::FileHandleChanged);
+            }
+            multi_buffer::Event::Reloaded | multi_buffer::Event::BufferDiffChanged => {
+                cx.emit(EditorEvent::TitleChanged)
+            }
             multi_buffer::Event::DiagnosticsUpdated => {
                 self.update_diagnostics_state(window, cx);
             }
@@ -28461,6 +28465,7 @@ pub enum EditorEvent {
     DirtyChanged,
     Saved,
     TitleChanged,
+    FileHandleChanged,
     SelectionsChanged {
         local: bool,
     },

crates/editor/src/items.rs 🔗

@@ -1415,7 +1415,10 @@ impl SerializableItem for Editor {
         self.should_serialize_buffer()
             && matches!(
                 event,
-                EditorEvent::Saved | EditorEvent::DirtyChanged | EditorEvent::BufferEdited
+                EditorEvent::Saved
+                    | EditorEvent::DirtyChanged
+                    | EditorEvent::BufferEdited
+                    | EditorEvent::FileHandleChanged
             )
     }
 }
@@ -2396,6 +2399,81 @@ mod tests {
         }
     }
 
+    // Verify that renaming an open file emits EditorEvent::FileHandleChanged so that
+    // the workspace re-serializes the editor with the updated path.
+    #[gpui::test]
+    async fn test_file_handle_changed_on_rename(cx: &mut gpui::TestAppContext) {
+        use serde_json::json;
+        use std::cell::RefCell;
+        use std::rc::Rc;
+        use util::rel_path::rel_path;
+
+        init_test(cx, |_| {});
+
+        let fs = FakeFs::new(cx.executor());
+        fs.insert_tree(path!("/root"), json!({ "file.rs": "fn main() {}" }))
+            .await;
+
+        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
+
+        let buffer = project
+            .update(cx, |project, cx| {
+                project.open_local_buffer(path!("/root/file.rs"), cx)
+            })
+            .await
+            .unwrap();
+
+        let received_file_handle_changed = Rc::new(RefCell::new(false));
+        let (editor, cx) = cx.add_window_view({
+            let project = project.clone();
+            let received_file_handle_changed = received_file_handle_changed.clone();
+            move |window, cx| {
+                let mut editor = Editor::for_buffer(buffer, Some(project), window, cx);
+                editor.set_should_serialize(true, cx);
+                let entity = cx.entity();
+                cx.subscribe_in(&entity, window, move |_, _, event: &EditorEvent, _, _| {
+                    if matches!(event, EditorEvent::FileHandleChanged) {
+                        *received_file_handle_changed.borrow_mut() = true;
+                    }
+                })
+                .detach();
+                editor
+            }
+        });
+
+        cx.run_until_parked();
+
+        let (entry_id, worktree_id) = project.update(cx, |project, cx| {
+            let worktree = project.worktrees(cx).next().unwrap();
+            let worktree = worktree.read(cx);
+            let entry = worktree.entry_for_path(rel_path("file.rs")).unwrap();
+            (entry.id, worktree.id())
+        });
+
+        project
+            .update(cx, |project, cx| {
+                project.rename_entry(entry_id, (worktree_id, rel_path("renamed.rs")).into(), cx)
+            })
+            .await
+            .unwrap();
+
+        cx.run_until_parked();
+
+        assert!(
+            *received_file_handle_changed.borrow(),
+            "EditorEvent::FileHandleChanged must be emitted when the open file is renamed"
+        );
+
+        editor.update(cx, |editor, cx| {
+            let buffer = editor.buffer().read(cx).as_singleton().unwrap();
+            let path = buffer.read(cx).file().unwrap().path();
+            assert!(
+                path.as_std_path().ends_with("renamed.rs"),
+                "buffer path must reflect the renamed file, got {path:?}"
+            );
+        });
+    }
+
     // Regression test for https://github.com/zed-industries/zed/issues/35947
     // Verifies that deserializing a non-worktree editor does not add the item
     // to any pane as a side effect.