Introduce a File trait object to buffer

Nathan Sobo created

This will remove the dependency of buffer on `worktree::File`

Change summary

server/src/rpc.rs        |   4 
zed/src/editor.rs        |  19 ++-
zed/src/editor/buffer.rs |  81 +++++++++++---
zed/src/workspace.rs     |  23 ++--
zed/src/worktree.rs      | 227 ++++++++++++++++++++++++-----------------
5 files changed, 216 insertions(+), 138 deletions(-)

Detailed changes

server/src/rpc.rs 🔗

@@ -1383,7 +1383,7 @@ mod tests {
             .update(&mut cx_b, |worktree, cx| worktree.open_buffer("a.txt", cx))
             .await
             .unwrap();
-        let mtime = buffer_b.read_with(&cx_b, |buf, _| buf.file().unwrap().mtime);
+        let mtime = buffer_b.read_with(&cx_b, |buf, _| buf.file().unwrap().mtime());
 
         buffer_b.update(&mut cx_b, |buf, cx| buf.edit([0..0], "world ", cx));
         buffer_b.read_with(&cx_b, |buf, _| {
@@ -1398,7 +1398,7 @@ mod tests {
             .unwrap();
         worktree_b
             .condition(&cx_b, |_, cx| {
-                buffer_b.read(cx).file().unwrap().mtime != mtime
+                buffer_b.read(cx).file().unwrap().mtime() != mtime
             })
             .await;
         buffer_b.read_with(&cx_b, |buf, _| {

zed/src/editor.rs 🔗

@@ -10,7 +10,7 @@ use crate::{
     time::ReplicaId,
     util::{post_inc, Bias},
     workspace,
-    worktree::{File, Worktree},
+    worktree::Worktree,
 };
 use anyhow::Result;
 pub use buffer::*;
@@ -2554,10 +2554,6 @@ impl View for Editor {
 impl workspace::Item for Buffer {
     type View = Editor;
 
-    fn file(&self) -> Option<&File> {
-        self.file()
-    }
-
     fn build_view(
         handle: ModelHandle<Self>,
         settings: watch::Receiver<Settings>,
@@ -2592,6 +2588,10 @@ impl workspace::Item for Buffer {
             cx,
         )
     }
+
+    fn worktree_id_and_path(&self) -> Option<(usize, Arc<Path>)> {
+        self.file().map(|f| (f.worktree_id(), f.path().clone()))
+    }
 }
 
 impl workspace::ItemView for Editor {
@@ -2623,8 +2623,11 @@ impl workspace::ItemView for Editor {
         }
     }
 
-    fn entry_id(&self, cx: &AppContext) -> Option<(usize, Arc<Path>)> {
-        self.buffer.read(cx).file().map(|file| file.entry_id())
+    fn worktree_id_and_path(&self, cx: &AppContext) -> Option<(usize, Arc<Path>)> {
+        self.buffer
+            .read(cx)
+            .file()
+            .map(|file| (file.worktree_id(), file.path().clone()))
     }
 
     fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
@@ -2678,7 +2681,7 @@ impl workspace::ItemView for Editor {
                     });
 
                     buffer.update(&mut cx, |buffer, cx| {
-                        buffer.did_save(version, new_file.mtime, Some(new_file), cx);
+                        buffer.did_save(version, new_file.mtime, Some(Box::new(new_file)), cx);
                         buffer.set_language(language, cx);
                     });
                 })

zed/src/editor/buffer.rs 🔗

@@ -9,11 +9,10 @@ use crate::{
     settings::{HighlightId, HighlightMap},
     time::{self, ReplicaId},
     util::Bias,
-    worktree::File,
 };
 pub use anchor::*;
 use anyhow::{anyhow, Result};
-use gpui::{AppContext, Entity, ModelContext, Task};
+use gpui::{AppContext, Entity, ModelContext, MutableAppContext, Task};
 use lazy_static::lazy_static;
 use operation_queue::OperationQueue;
 use parking_lot::Mutex;
@@ -23,13 +22,15 @@ use seahash::SeaHasher;
 pub use selection::*;
 use similar::{ChangeTag, TextDiff};
 use std::{
+    any::Any,
     cell::RefCell,
     cmp,
     convert::{TryFrom, TryInto},
+    ffi::OsString,
     hash::BuildHasher,
     iter::Iterator,
     ops::{Deref, DerefMut, Range},
-    path::Path,
+    path::{Path, PathBuf},
     str,
     sync::Arc,
     time::{Duration, Instant, SystemTime, UNIX_EPOCH},
@@ -38,6 +39,46 @@ use sum_tree::{self, FilterCursor, SumTree};
 use tree_sitter::{InputEdit, Parser, QueryCursor};
 use zrpc::proto;
 
+pub trait File {
+    fn worktree_id(&self) -> usize;
+
+    fn entry_id(&self) -> Option<usize>;
+
+    fn set_entry_id(&mut self, entry_id: Option<usize>);
+
+    fn mtime(&self) -> SystemTime;
+
+    fn set_mtime(&mut self, mtime: SystemTime);
+
+    fn path(&self) -> &Arc<Path>;
+
+    fn set_path(&mut self, path: Arc<Path>);
+
+    fn full_path(&self, cx: &AppContext) -> PathBuf;
+
+    /// Returns the last component of this handle's absolute path. If this handle refers to the root
+    /// of its worktree, then this method will return the name of the worktree itself.
+    fn file_name<'a>(&'a self, cx: &'a AppContext) -> Option<OsString>;
+
+    fn is_deleted(&self) -> bool;
+
+    fn save(
+        &self,
+        buffer_id: u64,
+        text: Rope,
+        version: time::Global,
+        cx: &mut MutableAppContext,
+    ) -> Task<Result<(time::Global, SystemTime)>>;
+
+    fn buffer_updated(&self, buffer_id: u64, operation: Operation, cx: &mut MutableAppContext);
+
+    fn buffer_removed(&self, buffer_id: u64, cx: &mut MutableAppContext);
+
+    fn boxed_clone(&self) -> Box<dyn File>;
+
+    fn as_any(&self) -> &dyn Any;
+}
+
 #[derive(Clone, Default)]
 struct DeterministicState;
 
@@ -115,7 +156,7 @@ pub struct Buffer {
     last_edit: time::Local,
     undo_map: UndoMap,
     history: History,
-    file: Option<File>,
+    file: Option<Box<dyn File>>,
     language: Option<Arc<Language>>,
     sync_parse_timeout: Duration,
     syntax_tree: Mutex<Option<SyntaxTree>>,
@@ -524,7 +565,7 @@ impl Buffer {
     pub fn from_history(
         replica_id: ReplicaId,
         history: History,
-        file: Option<File>,
+        file: Option<Box<dyn File>>,
         language: Option<Arc<Language>>,
         cx: &mut ModelContext<Self>,
     ) -> Self {
@@ -541,14 +582,14 @@ impl Buffer {
     fn build(
         replica_id: ReplicaId,
         history: History,
-        file: Option<File>,
+        file: Option<Box<dyn File>>,
         remote_id: u64,
         language: Option<Arc<Language>>,
         cx: &mut ModelContext<Self>,
     ) -> Self {
         let saved_mtime;
         if let Some(file) = file.as_ref() {
-            saved_mtime = file.mtime;
+            saved_mtime = file.mtime();
         } else {
             saved_mtime = UNIX_EPOCH;
         }
@@ -619,7 +660,7 @@ impl Buffer {
     pub fn from_proto(
         replica_id: ReplicaId,
         message: proto::Buffer,
-        file: Option<File>,
+        file: Option<Box<dyn File>>,
         language: Option<Arc<Language>>,
         cx: &mut ModelContext<Self>,
     ) -> Result<Self> {
@@ -678,12 +719,12 @@ impl Buffer {
         }
     }
 
-    pub fn file(&self) -> Option<&File> {
-        self.file.as_ref()
+    pub fn file(&self) -> Option<&dyn File> {
+        self.file.as_deref()
     }
 
-    pub fn file_mut(&mut self) -> Option<&mut File> {
-        self.file.as_mut()
+    pub fn file_mut(&mut self) -> Option<&mut dyn File> {
+        self.file.as_mut().map(|f| f.deref_mut() as &mut dyn File)
     }
 
     pub fn save(
@@ -719,7 +760,7 @@ impl Buffer {
         &mut self,
         version: time::Global,
         mtime: SystemTime,
-        new_file: Option<File>,
+        new_file: Option<Box<dyn File>>,
         cx: &mut ModelContext<Self>,
     ) {
         self.saved_mtime = mtime;
@@ -739,13 +780,13 @@ impl Buffer {
     ) {
         let file = self.file.as_mut().unwrap();
         let mut changed = false;
-        if path != file.path {
-            file.path = path;
+        if path != *file.path() {
+            file.set_path(path);
             changed = true;
         }
 
-        if mtime != file.mtime {
-            file.mtime = mtime;
+        if mtime != file.mtime() {
+            file.set_mtime(mtime);
             changed = true;
             if let Some(new_text) = new_text {
                 if self.version == self.saved_version {
@@ -1015,7 +1056,7 @@ impl Buffer {
             && self
                 .file
                 .as_ref()
-                .map_or(false, |file| file.mtime > self.saved_mtime)
+                .map_or(false, |file| file.mtime() > self.saved_mtime)
     }
 
     pub fn remote_id(&self) -> u64 {
@@ -1930,7 +1971,7 @@ impl Clone for Buffer {
             history: self.history.clone(),
             selections: self.selections.clone(),
             deferred_ops: self.deferred_ops.clone(),
-            file: self.file.clone(),
+            file: self.file.as_ref().map(|f| f.boxed_clone()),
             language: self.language.clone(),
             syntax_tree: Mutex::new(self.syntax_tree.lock().clone()),
             parsing_in_background: false,
@@ -3415,7 +3456,7 @@ mod tests {
             assert!(buffer.is_dirty());
             assert_eq!(*events.borrow(), &[Event::Edited, Event::Dirtied]);
             events.borrow_mut().clear();
-            buffer.did_save(buffer.version(), buffer.file().unwrap().mtime, None, cx);
+            buffer.did_save(buffer.version(), buffer.file().unwrap().mtime(), None, cx);
         });
 
         // after saving, the buffer is not dirty, and emits a saved event.

zed/src/workspace.rs 🔗

@@ -13,7 +13,7 @@ use crate::{
     settings::Settings,
     user,
     workspace::sidebar::{Side, Sidebar, SidebarItemId, ToggleSidebarItem, ToggleSidebarItemFocus},
-    worktree::{File, Worktree},
+    worktree::Worktree,
     AppState, Authenticate,
 };
 use anyhow::Result;
@@ -164,12 +164,12 @@ pub trait Item: Entity + Sized {
         cx: &mut ViewContext<Self::View>,
     ) -> Self::View;
 
-    fn file(&self) -> Option<&File>;
+    fn worktree_id_and_path(&self) -> Option<(usize, Arc<Path>)>;
 }
 
 pub trait ItemView: View {
     fn title(&self, cx: &AppContext) -> String;
-    fn entry_id(&self, cx: &AppContext) -> Option<(usize, Arc<Path>)>;
+    fn worktree_id_and_path(&self, cx: &AppContext) -> Option<(usize, Arc<Path>)>;
     fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
     where
         Self: Sized,
@@ -206,7 +206,6 @@ pub trait ItemHandle: Send + Sync {
 }
 
 pub trait WeakItemHandle {
-    fn file<'a>(&'a self, cx: &'a AppContext) -> Option<&'a File>;
     fn add_view(
         &self,
         window_id: usize,
@@ -214,6 +213,7 @@ pub trait WeakItemHandle {
         cx: &mut MutableAppContext,
     ) -> Option<Box<dyn ItemViewHandle>>;
     fn alive(&self, cx: &AppContext) -> bool;
+    fn worktree_id_and_path(&self, cx: &AppContext) -> Option<(usize, Arc<Path>)>;
 }
 
 pub trait ItemViewHandle {
@@ -246,10 +246,6 @@ impl<T: Item> ItemHandle for ModelHandle<T> {
 }
 
 impl<T: Item> WeakItemHandle for WeakModelHandle<T> {
-    fn file<'a>(&'a self, cx: &'a AppContext) -> Option<&'a File> {
-        self.upgrade(cx).and_then(|h| h.read(cx).file())
-    }
-
     fn add_view(
         &self,
         window_id: usize,
@@ -268,6 +264,11 @@ impl<T: Item> WeakItemHandle for WeakModelHandle<T> {
     fn alive(&self, cx: &AppContext) -> bool {
         self.upgrade(cx).is_some()
     }
+
+    fn worktree_id_and_path(&self, cx: &AppContext) -> Option<(usize, Arc<Path>)> {
+        self.upgrade(cx)
+            .and_then(|h| h.read(cx).worktree_id_and_path())
+    }
 }
 
 impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
@@ -276,7 +277,7 @@ impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
     }
 
     fn entry_id(&self, cx: &AppContext) -> Option<(usize, Arc<Path>)> {
-        self.read(cx).entry_id(cx)
+        self.read(cx).worktree_id_and_path(cx)
     }
 
     fn boxed_clone(&self) -> Box<dyn ItemViewHandle> {
@@ -717,9 +718,7 @@ impl Workspace {
         self.items.retain(|item| {
             if item.alive(cx.as_ref()) {
                 if view_for_existing_item.is_none()
-                    && item
-                        .file(cx.as_ref())
-                        .map_or(false, |file| file.entry_id() == entry)
+                    && item.worktree_id_and_path(cx).map_or(false, |e| e == entry)
                 {
                     view_for_existing_item = Some(
                         item.add_view(cx.window_id(), settings.clone(), cx.as_mut())

zed/src/worktree.rs 🔗

@@ -2,7 +2,7 @@ mod ignore;
 
 use self::ignore::IgnoreStack;
 use crate::{
-    editor::{self, Buffer, History, Operation, Rope},
+    editor::{self, buffer, Buffer, History, Operation, Rope},
     fs::{self, Fs},
     fuzzy::CharBag,
     language::LanguageRegistry,
@@ -25,21 +25,10 @@ use postage::{
 };
 use serde::Deserialize;
 use smol::channel::{self, Sender};
-use std::{
-    cmp::{self, Ordering},
-    collections::HashMap,
-    convert::{TryFrom, TryInto},
-    ffi::{OsStr, OsString},
-    fmt,
-    future::Future,
-    ops::Deref,
-    path::{Path, PathBuf},
-    sync::{
+use std::{any::Any, cmp::{self, Ordering}, collections::HashMap, convert::{TryFrom, TryInto}, ffi::{OsStr, OsString}, fmt, future::Future, ops::Deref, path::{Path, PathBuf}, sync::{
         atomic::{AtomicUsize, Ordering::SeqCst},
         Arc,
-    },
-    time::{Duration, SystemTime},
-};
+    }, time::{Duration, SystemTime}};
 use sum_tree::{self, Edit, SeekTarget, SumTree};
 use zrpc::{PeerId, TypedEnvelope};
 
@@ -404,7 +393,7 @@ impl Worktree {
         open_buffers
             .find(|buffer| {
                 if let Some(file) = buffer.upgrade(cx).and_then(|buffer| buffer.read(cx).file()) {
-                    file.path.as_ref() == path
+                    file.path().as_ref() == path
                 } else {
                     false
                 }
@@ -599,30 +588,30 @@ impl Worktree {
                         let mut file_changed = false;
 
                         if let Some(entry) = file
-                            .entry_id
+                            .entry_id()
                             .and_then(|entry_id| self.entry_for_id(entry_id))
                         {
-                            if entry.path != file.path {
-                                file.path = entry.path.clone();
+                            if entry.path != *file.path() {
+                                file.set_path(entry.path.clone());
                                 file_changed = true;
                             }
 
-                            if entry.mtime != file.mtime {
-                                file.mtime = entry.mtime;
+                            if entry.mtime != file.mtime() {
+                                file.set_mtime(entry.mtime);
                                 file_changed = true;
                                 if let Some(worktree) = self.as_local() {
                                     if buffer_is_clean {
-                                        let abs_path = worktree.absolutize(&file.path);
+                                        let abs_path = worktree.absolutize(file.path().as_ref());
                                         refresh_buffer(abs_path, &worktree.fs, cx);
                                     }
                                 }
                             }
-                        } else if let Some(entry) = self.entry_for_path(&file.path) {
-                            file.entry_id = Some(entry.id);
-                            file.mtime = entry.mtime;
+                        } else if let Some(entry) = self.entry_for_path(file.path().as_ref()) {
+                            file.set_entry_id(Some(entry.id));
+                            file.set_mtime(entry.mtime);
                             if let Some(worktree) = self.as_local() {
                                 if buffer_is_clean {
-                                    let abs_path = worktree.absolutize(&file.path);
+                                    let abs_path = worktree.absolutize(file.path().as_ref());
                                     refresh_buffer(abs_path, &worktree.fs, cx);
                                 }
                             }
@@ -631,7 +620,7 @@ impl Worktree {
                             if buffer_is_clean {
                                 cx.emit(editor::buffer::Event::Dirtied);
                             }
-                            file.entry_id = None;
+                            file.set_entry_id(None);
                             file_changed = true;
                         }
 
@@ -845,7 +834,7 @@ impl LocalWorktree {
         self.open_buffers.retain(|_buffer_id, buffer| {
             if let Some(buffer) = buffer.upgrade(cx.as_ref()) {
                 if let Some(file) = buffer.read(cx.as_ref()).file() {
-                    if file.worktree_id() == handle.id() && file.path.as_ref() == path {
+                    if file.worktree_id() == handle.id() && file.path().as_ref() == path {
                         existing_buffer = Some(buffer);
                     }
                 }
@@ -864,12 +853,20 @@ impl LocalWorktree {
                     .update(&mut cx, |this, cx| this.as_local().unwrap().load(&path, cx))
                     .await?;
                 let language = this.read_with(&cx, |this, cx| {
+                    use buffer::File;
+
                     this.languages()
                         .select_language(file.full_path(cx))
                         .cloned()
                 });
                 let buffer = cx.add_model(|cx| {
-                    Buffer::from_history(0, History::new(contents.into()), Some(file), language, cx)
+                    Buffer::from_history(
+                        0,
+                        History::new(contents.into()),
+                        Some(Box::new(file)),
+                        language,
+                        cx,
+                    )
                 });
                 this.update(&mut cx, |this, _| {
                     let this = this
@@ -1238,7 +1235,7 @@ impl RemoteWorktree {
         self.open_buffers.retain(|_buffer_id, buffer| {
             if let Some(buffer) = buffer.upgrade(cx.as_ref()) {
                 if let Some(file) = buffer.read(cx.as_ref()).file() {
-                    if file.worktree_id() == cx.model_id() && file.path.as_ref() == path {
+                    if file.worktree_id() == cx.model_id() && file.path().as_ref() == path {
                         existing_buffer = Some(buffer);
                     }
                 }
@@ -1273,6 +1270,8 @@ impl RemoteWorktree {
                     .ok_or_else(|| anyhow!("worktree was closed"))?;
                 let file = File::new(entry.id, this.clone(), entry.path, entry.mtime);
                 let language = this.read_with(&cx, |this, cx| {
+                    use buffer::File;
+
                     this.languages()
                         .select_language(file.full_path(cx))
                         .cloned()
@@ -1280,7 +1279,14 @@ impl RemoteWorktree {
                 let remote_buffer = response.buffer.ok_or_else(|| anyhow!("empty buffer"))?;
                 let buffer_id = remote_buffer.id as usize;
                 let buffer = cx.add_model(|cx| {
-                    Buffer::from_proto(replica_id, remote_buffer, Some(file), language, cx).unwrap()
+                    Buffer::from_proto(
+                        replica_id,
+                        remote_buffer,
+                        Some(Box::new(file)),
+                        language,
+                        cx,
+                    )
+                    .unwrap()
                 });
                 this.update(&mut cx, |this, cx| {
                     let this = this.as_remote_mut().unwrap();
@@ -1774,67 +1780,45 @@ impl File {
         }
     }
 
-    pub fn buffer_updated(&self, buffer_id: u64, operation: Operation, cx: &mut MutableAppContext) {
-        self.worktree.update(cx, |worktree, cx| {
-            if let Some((rpc, remote_id)) = match worktree {
-                Worktree::Local(worktree) => worktree
-                    .remote_id
-                    .borrow()
-                    .map(|id| (worktree.rpc.clone(), id)),
-                Worktree::Remote(worktree) => Some((worktree.rpc.clone(), worktree.remote_id)),
-            } {
-                cx.spawn(|worktree, mut cx| async move {
-                    if let Err(error) = rpc
-                        .request(proto::UpdateBuffer {
-                            worktree_id: remote_id,
-                            buffer_id,
-                            operations: vec![(&operation).into()],
-                        })
-                        .await
-                    {
-                        worktree.update(&mut cx, |worktree, _| {
-                            log::error!("error sending buffer operation: {}", error);
-                            match worktree {
-                                Worktree::Local(t) => &mut t.queued_operations,
-                                Worktree::Remote(t) => &mut t.queued_operations,
-                            }
-                            .push((buffer_id, operation));
-                        });
-                    }
-                })
-                .detach();
-            }
-        });
+    // pub fn exists(&self) -> bool {
+    //     !self.is_deleted()
+    // }
+
+    pub fn worktree_id_and_path(&self) -> (usize, Arc<Path>) {
+        (self.worktree.id(), self.path.clone())
     }
+}
 
-    pub fn buffer_removed(&self, buffer_id: u64, cx: &mut MutableAppContext) {
-        self.worktree.update(cx, |worktree, cx| {
-            if let Worktree::Remote(worktree) = worktree {
-                let worktree_id = worktree.remote_id;
-                let rpc = worktree.rpc.clone();
-                cx.background()
-                    .spawn(async move {
-                        if let Err(error) = rpc
-                            .send(proto::CloseBuffer {
-                                worktree_id,
-                                buffer_id,
-                            })
-                            .await
-                        {
-                            log::error!("error closing remote buffer: {}", error);
-                        }
-                    })
-                    .detach();
-            }
-        });
+impl buffer::File for File {
+    fn worktree_id(&self) -> usize {
+        self.worktree.id()
+    }
+
+    fn entry_id(&self) -> Option<usize> {
+        self.entry_id
+    }
+
+    fn set_entry_id(&mut self, entry_id: Option<usize>) {
+        self.entry_id = entry_id;
     }
 
-    /// Returns this file's path relative to the root of its worktree.
-    pub fn path(&self) -> Arc<Path> {
-        self.path.clone()
+    fn mtime(&self) -> SystemTime {
+        self.mtime
     }
 
-    pub fn full_path(&self, cx: &AppContext) -> PathBuf {
+    fn set_mtime(&mut self, mtime: SystemTime) {
+        self.mtime = mtime;
+    }
+
+    fn path(&self) -> &Arc<Path> {
+        &self.path
+    }
+
+    fn set_path(&mut self, path: Arc<Path>) {
+        self.path = path;
+    }
+
+    fn full_path(&self, cx: &AppContext) -> PathBuf {
         let worktree = self.worktree.read(cx);
         let mut full_path = PathBuf::new();
         full_path.push(worktree.root_name());
@@ -1844,22 +1828,18 @@ impl File {
 
     /// Returns the last component of this handle's absolute path. If this handle refers to the root
     /// of its worktree, then this method will return the name of the worktree itself.
-    pub fn file_name<'a>(&'a self, cx: &'a AppContext) -> Option<OsString> {
+    fn file_name<'a>(&'a self, cx: &'a AppContext) -> Option<OsString> {
         self.path
             .file_name()
             .or_else(|| Some(OsStr::new(self.worktree.read(cx).root_name())))
             .map(Into::into)
     }
 
-    pub fn is_deleted(&self) -> bool {
+    fn is_deleted(&self) -> bool {
         self.entry_id.is_none()
     }
 
-    pub fn exists(&self) -> bool {
-        !self.is_deleted()
-    }
-
-    pub fn save(
+    fn save(
         &self,
         buffer_id: u64,
         text: Rope,
@@ -1906,12 +1886,67 @@ impl File {
         })
     }
 
-    pub fn worktree_id(&self) -> usize {
-        self.worktree.id()
+    fn buffer_updated(&self, buffer_id: u64, operation: Operation, cx: &mut MutableAppContext) {
+        self.worktree.update(cx, |worktree, cx| {
+            if let Some((rpc, remote_id)) = match worktree {
+                Worktree::Local(worktree) => worktree
+                    .remote_id
+                    .borrow()
+                    .map(|id| (worktree.rpc.clone(), id)),
+                Worktree::Remote(worktree) => Some((worktree.rpc.clone(), worktree.remote_id)),
+            } {
+                cx.spawn(|worktree, mut cx| async move {
+                    if let Err(error) = rpc
+                        .request(proto::UpdateBuffer {
+                            worktree_id: remote_id,
+                            buffer_id,
+                            operations: vec![(&operation).into()],
+                        })
+                        .await
+                    {
+                        worktree.update(&mut cx, |worktree, _| {
+                            log::error!("error sending buffer operation: {}", error);
+                            match worktree {
+                                Worktree::Local(t) => &mut t.queued_operations,
+                                Worktree::Remote(t) => &mut t.queued_operations,
+                            }
+                            .push((buffer_id, operation));
+                        });
+                    }
+                })
+                .detach();
+            }
+        });
     }
 
-    pub fn entry_id(&self) -> (usize, Arc<Path>) {
-        (self.worktree.id(), self.path.clone())
+    fn buffer_removed(&self, buffer_id: u64, cx: &mut MutableAppContext) {
+        self.worktree.update(cx, |worktree, cx| {
+            if let Worktree::Remote(worktree) = worktree {
+                let worktree_id = worktree.remote_id;
+                let rpc = worktree.rpc.clone();
+                cx.background()
+                    .spawn(async move {
+                        if let Err(error) = rpc
+                            .send(proto::CloseBuffer {
+                                worktree_id,
+                                buffer_id,
+                            })
+                            .await
+                        {
+                            log::error!("error closing remote buffer: {}", error);
+                        }
+                    })
+                    .detach();
+            }
+        });
+    }
+
+    fn boxed_clone(&self) -> Box<dyn buffer::File> {
+        Box::new(self.clone())
+    }
+
+    fn as_any(&self) -> &dyn Any {
+        self
     }
 }