Maintain workspace buffers state after saving untitled buffer

Max Brunsfeld created

Change summary

zed/src/editor/buffer/mod.rs  |  2 
zed/src/editor/buffer_view.rs |  8 +++-
zed/src/workspace.rs          | 67 ++++++++++++++++++++++++++++++------
zed/src/worktree.rs           | 20 +++++++---
4 files changed, 77 insertions(+), 20 deletions(-)

Detailed changes

zed/src/editor/buffer/mod.rs 🔗

@@ -436,7 +436,7 @@ impl Buffer {
         &mut self,
         file: &FileHandle,
         ctx: &mut ModelContext<Self>,
-    ) -> LocalBoxFuture<'static, Result<()>> {
+    ) -> LocalBoxFuture<'static, Result<u64>> {
         let snapshot = self.snapshot();
         let version = self.version.clone();
         let save_task = file.save(snapshot, ctx.as_ref());

zed/src/editor/buffer_view.rs 🔗

@@ -2058,6 +2058,10 @@ impl BufferView {
             buffer::Event::FileHandleChanged => ctx.emit(Event::FileHandleChanged),
         }
     }
+
+    pub fn file(&self) -> Option<&FileHandle> {
+        self.file.as_ref()
+    }
 }
 
 pub enum Event {
@@ -2138,7 +2142,7 @@ impl workspace::ItemView for BufferView {
         &mut self,
         new_file: Option<FileHandle>,
         ctx: &mut ViewContext<Self>,
-    ) -> LocalBoxFuture<'static, Result<()>> {
+    ) -> LocalBoxFuture<'static, Result<u64>> {
         if let Some(file) = new_file.as_ref().or(self.file.as_ref()) {
             let save = self.buffer.update(ctx, |b, ctx| b.save(file, ctx));
             ctx.spawn(save, move |this, result, ctx| {
@@ -2151,7 +2155,7 @@ impl workspace::ItemView for BufferView {
             })
             .boxed_local()
         } else {
-            Box::pin(async { Ok(()) })
+            Box::pin(async { Err(anyhow::anyhow!("can't save a buffer with no file")) })
         }
     }
 

zed/src/workspace.rs 🔗

@@ -114,9 +114,7 @@ pub trait ItemView: View {
         &mut self,
         _: Option<FileHandle>,
         _: &mut ViewContext<Self>,
-    ) -> LocalBoxFuture<'static, anyhow::Result<()>> {
-        Box::pin(async { Ok(()) })
-    }
+    ) -> LocalBoxFuture<'static, anyhow::Result<u64>>;
     fn should_activate_item_on_event(_: &Self::Event) -> bool {
         false
     }
@@ -138,7 +136,7 @@ pub trait ItemViewHandle: Send + Sync {
         &self,
         file: Option<FileHandle>,
         ctx: &mut MutableAppContext,
-    ) -> LocalBoxFuture<'static, anyhow::Result<()>>;
+    ) -> LocalBoxFuture<'static, anyhow::Result<u64>>;
 }
 
 impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
@@ -181,7 +179,7 @@ impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
         &self,
         file: Option<FileHandle>,
         ctx: &mut MutableAppContext,
-    ) -> LocalBoxFuture<'static, anyhow::Result<()>> {
+    ) -> LocalBoxFuture<'static, anyhow::Result<u64>> {
         self.update(ctx, |item, ctx| item.save(file, ctx))
     }
 
@@ -223,7 +221,7 @@ pub struct Workspace {
         (usize, u64),
         postage::watch::Receiver<Option<Result<ModelHandle<Buffer>, Arc<anyhow::Error>>>>,
     >,
-    untitled_buffers: HashSet<ModelHandle<Buffer>>,
+    untitled_buffers: HashMap<usize, ModelHandle<Buffer>>,
 }
 
 impl Workspace {
@@ -372,7 +370,7 @@ impl Workspace {
         let buffer_view = ctx.add_view(|ctx| {
             BufferView::for_buffer(buffer.clone(), None, self.settings.clone(), ctx)
         });
-        self.untitled_buffers.insert(buffer);
+        self.untitled_buffers.insert(buffer_view.id(), buffer);
         self.add_item(Box::new(buffer_view), ctx);
     }
 
@@ -486,11 +484,20 @@ impl Workspace {
                     if let Some(path) = path {
                         handle.update(ctx, move |this, ctx| {
                             let file = this.file_for_path(&path, ctx);
+                            let worktree_id = file.worktree_id();
                             let task = item.save(Some(file), ctx.as_mut());
-                            ctx.spawn(task, |_, result, _| {
-                                if let Err(e) = result {
+                            let item_id = item.id();
+                            ctx.spawn(task, move |this, result, _| match result {
+                                Err(e) => {
                                     error!("failed to save item: {:?}, ", e);
                                 }
+                                Ok(inode) => {
+                                    if let Some(buffer) = this.untitled_buffers.remove(&item_id) {
+                                        let (_, rx) =
+                                            postage::watch::channel_with(Some(Ok(buffer)));
+                                        this.buffers.insert((worktree_id, inode), rx);
+                                    }
+                                }
                             })
                             .detach()
                         })
@@ -984,9 +991,47 @@ mod tests {
             app.read(|ctx| assert_eq!(editor.title(ctx), "untitled"));
 
             // When the save completes, the buffer's title is updated.
-            editor
-                .condition(&app, |editor, ctx| editor.title(ctx) == "the-new-name")
+            let worktree = app.read(|ctx| {
+                workspace
+                    .read(ctx)
+                    .worktrees()
+                    .iter()
+                    .next()
+                    .unwrap()
+                    .clone()
+            });
+            worktree
+                .condition(&app, |worktree, _| {
+                    worktree.inode_for_path("the-new-name").is_some()
+                })
                 .await;
+
+            // Open the same newly-created file in another pane item.
+            // The new editor should reuse the same buffer.
+            workspace
+                .update(&mut app, |workspace, ctx| {
+                    workspace.open_new_file(&(), ctx);
+                    workspace.split_pane(
+                        workspace.active_pane().clone(),
+                        SplitDirection::Right,
+                        ctx,
+                    );
+                    workspace
+                        .open_entry((worktree.id(), Path::new("the-new-name").into()), ctx)
+                        .unwrap()
+                })
+                .await;
+            let editor2 = workspace.update(&mut app, |workspace, ctx| {
+                workspace
+                    .active_item(ctx)
+                    .unwrap()
+                    .to_any()
+                    .downcast::<BufferView>()
+                    .unwrap()
+            });
+            app.read(|ctx| {
+                assert_eq!(editor.read(ctx).buffer(), editor2.read(ctx).buffer());
+            })
         });
     }
 

zed/src/worktree.rs 🔗

@@ -191,17 +191,18 @@ impl Worktree {
         path: &Path,
         content: BufferSnapshot,
         ctx: &AppContext,
-    ) -> Task<Result<()>> {
+    ) -> Task<Result<u64>> {
         let abs_path = self.snapshot.abs_path.join(path);
         ctx.background_executor().spawn(async move {
             let buffer_size = content.text_summary().bytes.min(10 * 1024);
             let file = std::fs::File::create(&abs_path)?;
+            let metadata = file.metadata()?;
             let mut writer = std::io::BufWriter::with_capacity(buffer_size, file);
             for chunk in content.fragments() {
                 writer.write(chunk.as_bytes())?;
             }
             writer.flush()?;
-            Ok(())
+            Ok(metadata.ino())
         })
     }
 }
@@ -406,15 +407,23 @@ impl FileHandle {
         self.state.lock().is_deleted
     }
 
+    pub fn exists(&self) -> bool {
+        !self.is_deleted()
+    }
+
     pub fn load_history(&self, ctx: &AppContext) -> impl Future<Output = Result<History>> {
         self.worktree.read(ctx).load_history(&self.path(), ctx)
     }
 
-    pub fn save<'a>(&self, content: BufferSnapshot, ctx: &AppContext) -> Task<Result<()>> {
+    pub fn save<'a>(&self, content: BufferSnapshot, ctx: &AppContext) -> Task<Result<u64>> {
         let worktree = self.worktree.read(ctx);
         worktree.save(&self.path(), content, ctx)
     }
 
+    pub fn worktree_id(&self) -> usize {
+        self.worktree.id()
+    }
+
     pub fn entry_id(&self) -> (usize, Arc<Path>) {
         (self.worktree.id(), self.path())
     }
@@ -971,9 +980,8 @@ impl BackgroundScanner {
         let snapshot = self.snapshot.lock();
         handles.retain(|path, handle_state| {
             if let Some(handle_state) = Weak::upgrade(&handle_state) {
-                if snapshot.entry_for_path(&path).is_none() {
-                    handle_state.lock().is_deleted = true;
-                }
+                let mut handle_state = handle_state.lock();
+                handle_state.is_deleted = snapshot.entry_for_path(&path).is_none();
                 true
             } else {
                 false