From dc8e216fcb30811d79f7294ea4bf2308c2eb16a2 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 22 Jun 2021 12:29:58 -0700 Subject: [PATCH] WIP - Maintain a set of open buffers on a LocalWorktree --- 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(-) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index 4901409310389bedc46840be4e058e97ba74ae24..6aac020daf7ce125e3396779c6b1ec3251b056ed 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -2245,6 +2245,20 @@ impl WeakModelHandle { } } +impl Hash for WeakModelHandle { + fn hash(&self, state: &mut H) { + self.model_id.hash(state) + } +} + +impl PartialEq for WeakModelHandle { + fn eq(&self, other: &Self) -> bool { + self.model_id == other.model_id + } +} + +impl Eq for WeakModelHandle {} + impl Clone for WeakModelHandle { fn clone(&self) -> Self { Self { diff --git a/zed-rpc/proto/zed.proto b/zed-rpc/proto/zed.proto index 0190f1ca64461ca18f9c269d367a44b482f99583..634f167fdce3c51401215d6385d2c4801afeec43 100644 --- a/zed-rpc/proto/zed.proto +++ b/zed-rpc/proto/zed.proto @@ -45,6 +45,7 @@ message OpenWorktree { message OpenWorktreeResponse { Worktree worktree = 1; + uint32 replica_id = 2; } message AddGuest { diff --git a/zed/src/editor/buffer.rs b/zed/src/editor/buffer.rs index b02be1541e005b76e70675aa7839e1eb459a02c5..dcd505f2153566713d35f1ac565018c0700a0cf8 100644 --- a/zed/src/editor/buffer.rs +++ b/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, ) { 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>, cx: &mut ModelContext) { - 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) { + cx.emit(Event::Dirtied); + } + + pub fn file_was_modified( + &mut self, + new_text: String, + mtime: SystemTime, + cx: &mut ModelContext, + ) { + 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 { diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index 169f7ebc08964590b56f41116fc82fcd9ee1cb47..060fd3be505cb0984d63b74e2841322f0ee83065 100644 --- a/zed/src/file_finder.rs +++ b/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(), diff --git a/zed/src/rpc.rs b/zed/src/rpc.rs index ee3f58f0055eafd09e25b93a5e89a7293e261ff3..a46fec1142ca04dec42aeb3a8c45e6fe3c105754 100644 --- a/zed/src/rpc.rs +++ b/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 = diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index 7c8f63ae199099f65f780f20833c875ccebfc65a..dc7b4c410bd8cb412dc4cbbbb6cd79ad726c24f0 100644 --- a/zed/src/workspace.rs +++ b/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>, active_pane: ViewHandle, - replica_id: ReplicaId, worktrees: HashSet>, items: Vec>, loading_items: HashMap< @@ -405,7 +403,6 @@ pub struct Workspace { impl Workspace { pub fn new( - replica_id: ReplicaId, settings: watch::Receiver, language_registry: Arc, 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) { - 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) - } - .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) + .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, diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 6ec6bff56dfe635b04c581beeccd4fcc1b653fe9..47f39b99b4853f5238829175e25f1064f53013e4 100644 --- a/zed/src/worktree.rs +++ b/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, ) -> 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, + cx: &mut ModelContext, + ) -> Task>> { + 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>, - next_handle_id: AtomicU64, scan_state: (watch::Sender, watch::Receiver), _event_stream_handle: fsevent::Handle, poll_scheduled: bool, rpc: Option, + open_buffers: HashSet>, } 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, + cx: &mut ModelContext, + ) -> Task>> { + 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 { 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) { - 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) { @@ -255,6 +369,16 @@ impl LocalWorktree { } } + fn load(&self, path: &Path, cx: &AppContext) -> Task> { + 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> { 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, ) -> 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> { - 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> { @@ -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]