WIP - Maintain a set of open buffers on a LocalWorktree

Max Brunsfeld created

Change summary

gpui/src/app.rs          |  14 ++
zed-rpc/proto/zed.proto  |   1 
zed/src/editor/buffer.rs |  69 +++++------
zed/src/file_finder.rs   |   4 
zed/src/rpc.rs           |   9 
zed/src/workspace.rs     |  43 ++-----
zed/src/worktree.rs      | 241 ++++++++++++++++++++++++++++-------------
7 files changed, 226 insertions(+), 155 deletions(-)

Detailed changes

gpui/src/app.rs 🔗

@@ -2245,6 +2245,20 @@ impl<T: Entity> WeakModelHandle<T> {
     }
 }
 
+impl<T> Hash for WeakModelHandle<T> {
+    fn hash<H: Hasher>(&self, state: &mut H) {
+        self.model_id.hash(state)
+    }
+}
+
+impl<T> PartialEq for WeakModelHandle<T> {
+    fn eq(&self, other: &Self) -> bool {
+        self.model_id == other.model_id
+    }
+}
+
+impl<T> Eq for WeakModelHandle<T> {}
+
 impl<T> Clone for WeakModelHandle<T> {
     fn clone(&self) -> Self {
         Self {

zed-rpc/proto/zed.proto 🔗

@@ -45,6 +45,7 @@ message OpenWorktree {
 
 message OpenWorktreeResponse {
     Worktree worktree = 1;
+    uint32 replica_id = 2;
 }
 
 message AddGuest {

zed/src/editor/buffer.rs 🔗

@@ -32,7 +32,7 @@ use std::{
     ops::{Deref, DerefMut, Range},
     str,
     sync::Arc,
-    time::{Duration, Instant},
+    time::{Duration, Instant, SystemTime, UNIX_EPOCH},
 };
 
 const UNDO_GROUP_INTERVAL: Duration = Duration::from_millis(300);
@@ -110,7 +110,7 @@ pub struct Buffer {
     deleted_text: Rope,
     pub version: time::Global,
     saved_version: time::Global,
-    saved_mtime: Duration,
+    saved_mtime: SystemTime,
     last_edit: time::Local,
     undo_map: UndoMap,
     history: History,
@@ -438,7 +438,7 @@ impl Buffer {
         if let Some(file) = file.as_ref() {
             saved_mtime = file.read(cx).mtime(cx.as_ref());
         } else {
-            saved_mtime = Duration::ZERO;
+            saved_mtime = UNIX_EPOCH;
         }
 
         let mut fragments = SumTree::new();
@@ -466,7 +466,7 @@ impl Buffer {
             last_edit: time::Local::default(),
             undo_map: Default::default(),
             history,
-            file: None,
+            file,
             syntax_tree: Mutex::new(None),
             is_parsing: false,
             language,
@@ -479,7 +479,6 @@ impl Buffer {
             local_clock: time::Local::new(replica_id),
             lamport_clock: time::Lamport::new(replica_id),
         };
-        result.set_file(file, cx);
         result.reparse(cx);
         result
     }
@@ -526,7 +525,7 @@ impl Buffer {
         cx: &mut ModelContext<Self>,
     ) {
         if file.is_some() {
-            self.set_file(file, cx);
+            self.file = file;
         }
         if let Some(file) = &self.file {
             self.saved_mtime = file.read(cx).mtime(cx.as_ref());
@@ -535,42 +534,32 @@ impl Buffer {
         cx.emit(Event::Saved);
     }
 
-    fn set_file(&mut self, file: Option<ModelHandle<File>>, cx: &mut ModelContext<Self>) {
-        self.file = file;
-        if let Some(file) = &self.file {
-            cx.observe(file, |this, file, cx| {
-                let version = this.version.clone();
-                if this.version == this.saved_version {
-                    if file.read(cx).is_deleted(cx.as_ref()) {
-                        cx.emit(Event::Dirtied);
-                    } else {
-                        cx.spawn(|this, mut cx| async move {
-                            let (current_version, history) = this.read_with(&cx, |this, cx| {
-                                (
-                                    this.version.clone(),
-                                    file.read(cx).load_history(cx.as_ref()),
-                                )
-                            });
-                            if let (Ok(history), true) = (history.await, current_version == version)
-                            {
-                                let diff = this
-                                    .read_with(&cx, |this, cx| this.diff(history.base_text, cx))
-                                    .await;
-                                this.update(&mut cx, |this, cx| {
-                                    if let Some(_ops) = this.set_text_via_diff(diff, cx) {
-                                        this.saved_version = this.version.clone();
-                                        this.saved_mtime = file.read(cx).mtime(cx.as_ref());
-                                        cx.emit(Event::Reloaded);
-                                    }
-                                });
-                            }
-                        })
-                        .detach();
+    pub fn file_was_deleted(&mut self, cx: &mut ModelContext<Self>) {
+        cx.emit(Event::Dirtied);
+    }
+
+    pub fn file_was_modified(
+        &mut self,
+        new_text: String,
+        mtime: SystemTime,
+        cx: &mut ModelContext<Self>,
+    ) {
+        if self.version == self.saved_version {
+            cx.spawn(|this, mut cx| async move {
+                let diff = this
+                    .read_with(&cx, |this, cx| this.diff(new_text.into(), cx))
+                    .await;
+                this.update(&mut cx, |this, cx| {
+                    if let Some(_ops) = this.set_text_via_diff(diff, cx) {
+                        this.saved_version = this.version.clone();
+                        this.saved_mtime = mtime;
+                        cx.emit(Event::Reloaded);
                     }
-                }
-                cx.emit(Event::FileHandleChanged);
-            });
+                });
+            })
+            .detach();
         }
+        cx.emit(Event::FileHandleChanged);
     }
 
     pub fn syntax_tree(&self) -> Option<Tree> {

zed/src/file_finder.rs 🔗

@@ -480,7 +480,6 @@ mod tests {
         let app_state = cx.read(build_app_state);
         let (window_id, workspace) = cx.add_window(|cx| {
             let mut workspace = Workspace::new(
-                0,
                 app_state.settings,
                 app_state.language_registry,
                 app_state.rpc,
@@ -553,7 +552,6 @@ mod tests {
         let app_state = cx.read(build_app_state);
         let (_, workspace) = cx.add_window(|cx| {
             let mut workspace = Workspace::new(
-                0,
                 app_state.settings.clone(),
                 app_state.language_registry.clone(),
                 app_state.rpc.clone(),
@@ -617,7 +615,6 @@ mod tests {
         let app_state = cx.read(build_app_state);
         let (_, workspace) = cx.add_window(|cx| {
             let mut workspace = Workspace::new(
-                0,
                 app_state.settings.clone(),
                 app_state.language_registry.clone(),
                 app_state.rpc.clone(),
@@ -669,7 +666,6 @@ mod tests {
 
         let (_, workspace) = cx.add_window(|cx| {
             Workspace::new(
-                0,
                 app_state.settings.clone(),
                 app_state.language_registry.clone(),
                 app_state.rpc.clone(),

zed/src/rpc.rs 🔗

@@ -11,11 +11,12 @@ use std::collections::HashMap;
 use std::time::Duration;
 use std::{convert::TryFrom, future::Future, sync::Arc};
 use surf::Url;
-use zed_rpc::proto::EnvelopedMessage;
-use zed_rpc::{proto::RequestMessage, rest, Peer, TypedEnvelope};
-use zed_rpc::{PeerId, Receipt};
+use zed_rpc::{
+    proto::{EnvelopedMessage, RequestMessage},
+    rest, Peer, Receipt, TypedEnvelope,
+};
 
-pub use zed_rpc::{proto, ConnectionId};
+pub use zed_rpc::{proto, ConnectionId, PeerId};
 
 lazy_static! {
     static ref ZED_SERVER_URL: String =

zed/src/workspace.rs 🔗

@@ -96,7 +96,6 @@ fn open_paths(params: &OpenParams, cx: &mut MutableAppContext) {
     // Add a new workspace if necessary
     cx.add_window(|cx| {
         let mut view = Workspace::new(
-            0,
             params.app_state.settings.clone(),
             params.app_state.language_registry.clone(),
             params.app_state.rpc.clone(),
@@ -394,7 +393,6 @@ pub struct Workspace {
     center: PaneGroup,
     panes: Vec<ViewHandle<Pane>>,
     active_pane: ViewHandle<Pane>,
-    replica_id: ReplicaId,
     worktrees: HashSet<ModelHandle<Worktree>>,
     items: Vec<Box<dyn WeakItemHandle>>,
     loading_items: HashMap<
@@ -405,7 +403,6 @@ pub struct Workspace {
 
 impl Workspace {
     pub fn new(
-        replica_id: ReplicaId,
         settings: watch::Receiver<Settings>,
         language_registry: Arc<LanguageRegistry>,
         rpc: rpc::Client,
@@ -426,7 +423,6 @@ impl Workspace {
             settings,
             language_registry,
             rpc,
-            replica_id,
             worktrees: Default::default(),
             items: Default::default(),
             loading_items: Default::default(),
@@ -557,7 +553,7 @@ impl Workspace {
     }
 
     pub fn open_new_file(&mut self, _: &(), cx: &mut ViewContext<Self>) {
-        let buffer = cx.add_model(|cx| Buffer::new(self.replica_id, "", cx));
+        let buffer = cx.add_model(|cx| Buffer::new(0, "", cx));
         let buffer_view =
             cx.add_view(|cx| Editor::for_buffer(buffer.clone(), self.settings.clone(), cx));
         self.items.push(ItemHandle::downgrade(&buffer));
@@ -615,32 +611,23 @@ impl Workspace {
             }
         };
 
-        let file = worktree.file(path.clone(), cx.as_mut());
         if let Entry::Vacant(entry) = self.loading_items.entry(entry.clone()) {
             let (mut tx, rx) = postage::watch::channel();
             entry.insert(rx);
-            let replica_id = self.replica_id;
             let language_registry = self.language_registry.clone();
 
             cx.as_mut()
                 .spawn(|mut cx| async move {
-                    let buffer = async move {
-                        let history = cx.read(|cx| file.read(cx).load_history(cx));
-                        let history = cx.background_executor().spawn(history).await?;
-                        let buffer = cx.add_model(|cx| {
-                            let language = language_registry.select_language(path);
-                            Buffer::from_history(
-                                replica_id,
-                                history,
-                                Some(file),
-                                language.cloned(),
-                                cx,
-                            )
-                        });
-                        Ok(Box::new(buffer) as Box<dyn ItemHandle>)
-                    }
-                    .await;
-                    *tx.borrow_mut() = Some(buffer.map_err(Arc::new));
+                    let buffer = worktree
+                        .update(&mut cx, |worktree, cx| {
+                            worktree.open_buffer(path.as_ref(), language_registry, cx)
+                        })
+                        .await;
+                    *tx.borrow_mut() = Some(
+                        buffer
+                            .map(|buffer| Box::new(buffer) as Box<dyn ItemHandle>)
+                            .map_err(Arc::new),
+                    );
                 })
                 .detach();
         }
@@ -809,11 +796,12 @@ impl Workspace {
             let worktree = open_worktree_response
                 .worktree
                 .ok_or_else(|| anyhow!("empty worktree"))?;
+            let replica_id = open_worktree_response.replica_id as ReplicaId;
 
             let worktree_id = worktree_id.try_into().unwrap();
             this.update(&mut cx, |workspace, cx| {
                 let worktree = cx.add_model(|cx| {
-                    Worktree::remote(worktree_id, worktree, rpc, connection_id, cx)
+                    Worktree::remote(worktree_id, worktree, rpc, connection_id, replica_id, cx)
                 });
                 cx.observe_model(&worktree, |_, _, cx| cx.notify());
                 workspace.worktrees.insert(worktree);
@@ -1047,7 +1035,6 @@ mod tests {
 
         let (_, workspace) = cx.add_window(|cx| {
             let mut workspace = Workspace::new(
-                0,
                 app_state.settings,
                 app_state.language_registry,
                 app_state.rpc,
@@ -1156,7 +1143,6 @@ mod tests {
         let app_state = cx.read(build_app_state);
         let (_, workspace) = cx.add_window(|cx| {
             let mut workspace = Workspace::new(
-                0,
                 app_state.settings,
                 app_state.language_registry,
                 app_state.rpc,
@@ -1230,7 +1216,6 @@ mod tests {
         let app_state = cx.read(build_app_state);
         let (window_id, workspace) = cx.add_window(|cx| {
             let mut workspace = Workspace::new(
-                0,
                 app_state.settings,
                 app_state.language_registry,
                 app_state.rpc,
@@ -1279,7 +1264,6 @@ mod tests {
         let app_state = cx.read(build_app_state);
         let (_, workspace) = cx.add_window(|cx| {
             let mut workspace = Workspace::new(
-                0,
                 app_state.settings,
                 app_state.language_registry,
                 app_state.rpc,
@@ -1385,7 +1369,6 @@ mod tests {
         let app_state = cx.read(build_app_state);
         let (window_id, workspace) = cx.add_window(|cx| {
             let mut workspace = Workspace::new(
-                0,
                 app_state.settings,
                 app_state.language_registry,
                 app_state.rpc,

zed/src/worktree.rs 🔗

@@ -4,15 +4,20 @@ mod ignore;
 
 use self::{char_bag::CharBag, ignore::IgnoreStack};
 use crate::{
-    editor::{History, Rope},
-    rpc::{self, proto, ConnectionId},
+    editor::{Buffer, History, Rope},
+    language::LanguageRegistry,
+    rpc::{self, proto, ConnectionId, PeerId},
     sum_tree::{self, Cursor, Edit, SumTree},
+    time::ReplicaId,
     util::Bias,
 };
 use ::ignore::gitignore::Gitignore;
 use anyhow::{Context, Result};
 pub use fuzzy::{match_paths, PathMatch};
-use gpui::{scoped_pool, AppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task};
+use gpui::{
+    scoped_pool, AppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task,
+    WeakModelHandle,
+};
 use lazy_static::lazy_static;
 use parking_lot::Mutex;
 use postage::{
@@ -30,7 +35,7 @@ use std::{
     ops::Deref,
     os::unix::fs::MetadataExt,
     path::{Path, PathBuf},
-    sync::{atomic::AtomicU64, Arc},
+    sync::Arc,
     time::{Duration, SystemTime, UNIX_EPOCH},
 };
 
@@ -64,9 +69,17 @@ impl Worktree {
         worktree: proto::Worktree,
         rpc: rpc::Client,
         connection_id: ConnectionId,
+        replica_id: ReplicaId,
         cx: &mut ModelContext<Worktree>,
     ) -> Self {
-        Worktree::Remote(RemoteWorktree::new(id, worktree, rpc, connection_id, cx))
+        Worktree::Remote(RemoteWorktree::new(
+            id,
+            worktree,
+            rpc,
+            connection_id,
+            replica_id,
+            cx,
+        ))
     }
 
     pub fn as_local(&self) -> Option<&LocalWorktree> {
@@ -92,6 +105,18 @@ impl Worktree {
         }
     }
 
+    pub fn open_buffer(
+        &mut self,
+        path: &Path,
+        language_registry: Arc<LanguageRegistry>,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<ModelHandle<Buffer>>> {
+        match self {
+            Worktree::Local(worktree) => worktree.open_buffer(path, language_registry, cx),
+            Worktree::Remote(_) => todo!(),
+        }
+    }
+
     pub fn save(
         &self,
         path: &Path,
@@ -119,11 +144,11 @@ impl Deref for Worktree {
 pub struct LocalWorktree {
     snapshot: Snapshot,
     background_snapshot: Arc<Mutex<Snapshot>>,
-    next_handle_id: AtomicU64,
     scan_state: (watch::Sender<ScanState>, watch::Receiver<ScanState>),
     _event_stream_handle: fsevent::Handle,
     poll_scheduled: bool,
     rpc: Option<rpc::Client>,
+    open_buffers: HashSet<WeakModelHandle<Buffer>>,
 }
 
 impl LocalWorktree {
@@ -147,7 +172,7 @@ impl LocalWorktree {
         let tree = Self {
             snapshot,
             background_snapshot: background_snapshot.clone(),
-            next_handle_id: Default::default(),
+            open_buffers: Default::default(),
             scan_state: watch::channel_with(ScanState::Scanning),
             _event_stream_handle: event_stream_handle,
             poll_scheduled: false,
@@ -189,6 +214,47 @@ impl LocalWorktree {
         tree
     }
 
+    pub fn open_buffer(
+        &mut self,
+        path: &Path,
+        language_registry: Arc<LanguageRegistry>,
+        cx: &mut ModelContext<Worktree>,
+    ) -> Task<Result<ModelHandle<Buffer>>> {
+        let handle = cx.handle();
+
+        // If there is already a buffer for the given path, then return it.
+        let mut existing_buffer = None;
+        self.open_buffers.retain(|buffer| {
+            if let Some(buffer) = buffer.upgrade(cx.as_ref()) {
+                if let Some(file) = buffer.read(cx.as_ref()).file() {
+                    let file = file.read(cx.as_ref());
+                    if file.worktree_id() == handle.id() && file.path.as_ref() == path {
+                        existing_buffer = Some(buffer);
+                    }
+                }
+                true
+            } else {
+                false
+            }
+        });
+
+        let path = Arc::from(path);
+        let contents = self.load(&path, cx.as_ref());
+        cx.spawn(|this, mut cx| async move {
+            let contents = contents.await?;
+            let language = language_registry.select_language(&path).cloned();
+            let file = cx.add_model(|cx| File::new(handle, path.into(), cx));
+            let buffer = cx.add_model(|cx| {
+                Buffer::from_history(0, History::new(contents.into()), Some(file), language, cx)
+            });
+            this.update(&mut cx, |this, _| {
+                let this = this.as_local_mut().unwrap();
+                this.open_buffers.insert(buffer.downgrade());
+            });
+            Ok(buffer)
+        })
+    }
+
     pub fn scan_complete(&self) -> impl Future<Output = ()> {
         let mut scan_state_rx = self.scan_state.1.clone();
         async move {
@@ -200,13 +266,61 @@ impl LocalWorktree {
     }
 
     fn observe_scan_state(&mut self, mut scan_state: ScanState, cx: &mut ModelContext<Worktree>) {
-        if let ScanState::Idle(diff) = &mut scan_state {
-            if let Some(diff) = diff.take() {
-                cx.emit(diff);
-            }
-        }
-        let _ = self.scan_state.0.blocking_send(scan_state);
+        let diff = if let ScanState::Idle(diff) = &mut scan_state {
+            diff.take()
+        } else {
+            None
+        };
+
+        self.scan_state.0.blocking_send(scan_state).ok();
         self.poll_snapshot(cx);
+
+        if let Some(diff) = diff {
+            let handle = cx.handle();
+            self.open_buffers.retain(|buffer| {
+                if let Some(buffer) = buffer.upgrade(cx.as_ref()) {
+                    buffer.update(cx, |buffer, cx| {
+                        let handle = handle.clone();
+                        if let Some(file) = buffer.file() {
+                            let path = file.read(cx.as_ref()).path.clone();
+                            if diff.added.contains(&path) {
+                                cx.notify();
+                            }
+                            // Notify any buffers whose files were deleted.
+                            else if diff.removed.contains(&path) {
+                                buffer.file_was_deleted(cx);
+                            }
+                            // Notify any buffers whose files were modified.
+                            else if diff.modified.contains(&path) {
+                                cx.spawn(|buffer, mut cx| async move {
+                                    let new_contents = handle
+                                        .read_with(&cx, |this, cx| {
+                                            let this = this.as_local().unwrap();
+                                            this.load(&path, cx)
+                                        })
+                                        .await?;
+                                    let mtime = handle.read_with(&cx, |this, _| {
+                                        let this = this.as_local().unwrap();
+                                        this.entry_for_path(&path).map(|entry| entry.mtime)
+                                    });
+                                    if let Some(mtime) = mtime {
+                                        buffer.update(&mut cx, |buffer, cx| {
+                                            buffer.file_was_modified(new_contents, mtime, cx)
+                                        });
+                                    }
+                                    Result::<_, anyhow::Error>::Ok(())
+                                })
+                                .detach();
+                            }
+                        }
+                    });
+                    true
+                } else {
+                    false
+                }
+            });
+            cx.emit(diff);
+        }
     }
 
     fn poll_snapshot(&mut self, cx: &mut ModelContext<Worktree>) {
@@ -255,6 +369,16 @@ impl LocalWorktree {
         }
     }
 
+    fn load(&self, path: &Path, cx: &AppContext) -> Task<Result<String>> {
+        let abs_path = self.absolutize(path);
+        cx.background_executor().spawn(async move {
+            let mut file = fs::File::open(&abs_path)?;
+            let mut contents = String::new();
+            file.read_to_string(&mut contents)?;
+            Result::<_, anyhow::Error>::Ok(contents)
+        })
+    }
+
     pub fn save(&self, path: &Path, content: Rope, cx: &AppContext) -> Task<Result<()>> {
         let path = path.to_path_buf();
         let abs_path = self.absolutize(&path);
@@ -340,6 +464,7 @@ pub struct RemoteWorktree {
     snapshot: Snapshot,
     rpc: rpc::Client,
     connection_id: ConnectionId,
+    replica_id: ReplicaId,
 }
 
 impl RemoteWorktree {
@@ -348,6 +473,7 @@ impl RemoteWorktree {
         worktree: proto::Worktree,
         rpc: rpc::Client,
         connection_id: ConnectionId,
+        replica_id: ReplicaId,
         cx: &mut ModelContext<Worktree>,
     ) -> Self {
         let root_char_bag: CharBag = worktree
@@ -394,6 +520,7 @@ impl RemoteWorktree {
             snapshot,
             rpc,
             connection_id,
+            replica_id,
         }
     }
 }
@@ -699,44 +826,11 @@ impl File {
         !self.is_deleted(cx)
     }
 
-    pub fn mtime(&self, cx: &AppContext) -> Duration {
+    pub fn mtime(&self, cx: &AppContext) -> SystemTime {
         let snapshot = self.worktree.read(cx).snapshot();
         snapshot
             .entry_for_path(&self.path)
-            .map_or(Duration::ZERO, |entry| {
-                entry.mtime.duration_since(UNIX_EPOCH).unwrap()
-            })
-    }
-
-    pub fn load_history(&self, cx: &AppContext) -> Task<Result<History>> {
-        match self.worktree.read(cx) {
-            Worktree::Local(worktree) => {
-                let abs_path = worktree.absolutize(&self.path);
-                cx.background_executor().spawn(async move {
-                    let mut file = fs::File::open(&abs_path)?;
-                    let mut base_text = String::new();
-                    file.read_to_string(&mut base_text)?;
-                    Ok(History::new(Arc::from(base_text)))
-                })
-            }
-            Worktree::Remote(worktree) => {
-                todo!()
-                // let state = self.state.lock();
-                // let id = state.id;
-                // let worktree_id = worktree.remote_id as u64;
-                // let (connection_id, rpc) = state.rpc.clone().unwrap();
-                // cx.background_executor().spawn(async move {
-                //     let response = rpc
-                //         .request(connection_id, proto::OpenBuffer { worktree_id, id })
-                //         .await?;
-                //     let buffer = response
-                //         .buffer
-                //         .ok_or_else(|| anyhow!("buffer must be present"))?;
-                //     let history = History::new(buffer.content.into());
-                //     Ok(history)
-                // })
-            }
-        }
+            .map_or(UNIX_EPOCH, |entry| entry.mtime)
     }
 
     pub fn save(&self, content: Rope, cx: &AppContext) -> impl Future<Output = Result<()>> {
@@ -1579,26 +1673,21 @@ mod tests {
         }));
 
         let tree = cx.add_model(|cx| Worktree::local(dir.path(), cx));
-        cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+        tree.read_with(&cx, |tree, _| tree.as_local().unwrap().scan_complete())
             .await;
-        cx.read(|cx| assert_eq!(tree.read(cx).file_count(), 1));
-
-        let buffer = cx.add_model(|cx| Buffer::new(1, "a line of text.\n".repeat(10 * 1024), cx));
-
-        let path = tree.update(&mut cx, |tree, cx| {
-            let path = tree.files(0).next().unwrap().path().clone();
-            assert_eq!(path.file_name().unwrap(), "file1");
-            smol::block_on(tree.save(&path, buffer.read(cx).snapshot().text(), cx.as_ref()))
-                .unwrap();
-            path
+        let path = tree.read_with(&cx, |tree, _| {
+            assert_eq!(tree.file_count(), 1);
+            tree.files(0).next().unwrap().path().clone()
         });
-
-        let history = cx
-            .update(|cx| tree.file(&path, cx).read(cx).load_history(cx.as_ref()))
-            .await
-            .unwrap();
-        cx.read(|cx| {
-            assert_eq!(history.base_text.as_ref(), buffer.read(cx).text());
+        assert_eq!(path.file_name().unwrap(), "file1");
+
+        tree.update(&mut cx, |tree, cx| {
+            let buffer =
+                cx.add_model(|cx| Buffer::new(1, "a line of text.\n".repeat(10 * 1024), cx));
+            let text = buffer.read(cx).snapshot().text();
+            smol::block_on(tree.save(&path, text, cx.as_ref())).unwrap();
+            let new_contents = fs::read_to_string(dir.path().join(path)).unwrap();
+            assert_eq!(new_contents, buffer.read(cx).text());
         });
     }
 
@@ -1607,8 +1696,9 @@ mod tests {
         let dir = temp_tree(json!({
             "file1": "the old contents",
         }));
+        let file_path = dir.path().join("file1");
 
-        let tree = cx.add_model(|cx| Worktree::local(dir.path().join("file1"), cx));
+        let tree = cx.add_model(|cx| Worktree::local(file_path.clone(), cx));
         cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
             .await;
         cx.read(|cx| assert_eq!(tree.read(cx).file_count(), 1));
@@ -1616,16 +1706,13 @@ mod tests {
         let buffer = cx.add_model(|cx| Buffer::new(1, "a line of text.\n".repeat(10 * 1024), cx));
         let file = cx.update(|cx| tree.file("", cx));
 
-        let history = file
-            .read_with(&cx, |file, cx| {
-                assert_eq!(file.path().file_name(), None);
-                smol::block_on(file.save(buffer.read(cx).snapshot().text(), cx.as_ref())).unwrap();
-                file.load_history(cx)
-            })
-            .await
-            .unwrap();
-
-        cx.read(|cx| assert_eq!(history.base_text.as_ref(), buffer.read(cx).text()));
+        file.read_with(&cx, |file, cx| {
+            assert_eq!(file.path().file_name(), None);
+            let text = buffer.read(cx).snapshot().text();
+            smol::block_on(file.save(text, cx.as_ref())).unwrap();
+            let new_contents = fs::read_to_string(file_path).unwrap();
+            assert_eq!(new_contents, buffer.read(cx).text());
+        });
     }
 
     #[gpui::test]