Restore Buffer::file field and workspace::Item trait

Max Brunsfeld created

Change summary

zed/src/editor/buffer/mod.rs           | 121 +++++++++----
zed/src/editor/buffer_view.rs          | 146 ++++++++--------
zed/src/editor/display_map/fold_map.rs |  14 
zed/src/editor/display_map/mod.rs      |   4 
zed/src/workspace.rs                   | 243 +++++++++++++++++----------
zed/src/worktree.rs                    |  22 +-
6 files changed, 319 insertions(+), 231 deletions(-)

Detailed changes

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

@@ -8,6 +8,7 @@ use futures_core::future::LocalBoxFuture;
 pub use point::*;
 use seahash::SeaHasher;
 pub use selection::*;
+use smol::future::FutureExt;
 pub use text::*;
 
 use crate::{
@@ -64,6 +65,7 @@ pub struct Buffer {
     last_edit: time::Local,
     undo_map: UndoMap,
     history: History,
+    file: Option<FileHandle>,
     selections: HashMap<SelectionSetId, Arc<[Selection]>>,
     pub selections_last_update: SelectionsVersion,
     deferred_ops: OperationQueue<Operation>,
@@ -351,15 +353,33 @@ pub struct UndoOperation {
 }
 
 impl Buffer {
-    pub fn new<T: Into<Arc<str>>>(replica_id: ReplicaId, base_text: T) -> Self {
-        Self::build(replica_id, History::new(base_text.into()))
+    pub fn new<T: Into<Arc<str>>>(
+        replica_id: ReplicaId,
+        base_text: T,
+        ctx: &mut ModelContext<Self>,
+    ) -> Self {
+        Self::build(replica_id, History::new(base_text.into()), None, ctx)
     }
 
-    pub fn from_history(replica_id: ReplicaId, history: History) -> Self {
-        Self::build(replica_id, history)
+    pub fn from_history(
+        replica_id: ReplicaId,
+        history: History,
+        file: Option<FileHandle>,
+        ctx: &mut ModelContext<Self>,
+    ) -> Self {
+        Self::build(replica_id, history, file, ctx)
     }
 
-    fn build(replica_id: ReplicaId, history: History) -> Self {
+    fn build(
+        replica_id: ReplicaId,
+        history: History,
+        file: Option<FileHandle>,
+        ctx: &mut ModelContext<Self>,
+    ) -> Self {
+        if let Some(file) = file.as_ref() {
+            file.observe_from_model(ctx, |_, _, ctx| ctx.emit(Event::FileHandleChanged));
+        }
+
         let mut insertion_splits = HashMap::default();
         let mut fragments = SumTree::new();
 
@@ -416,6 +436,7 @@ impl Buffer {
             last_edit: time::Local::default(),
             undo_map: Default::default(),
             history,
+            file,
             selections: HashMap::default(),
             selections_last_update: 0,
             deferred_ops: OperationQueue::new(),
@@ -432,24 +453,38 @@ impl Buffer {
         }
     }
 
+    pub fn file(&self) -> Option<&FileHandle> {
+        self.file.as_ref()
+    }
+
     pub fn save(
         &mut self,
-        file: &FileHandle,
+        new_file: Option<FileHandle>,
         ctx: &mut ModelContext<Self>,
-    ) -> LocalBoxFuture<'static, Result<u64>> {
+    ) -> LocalBoxFuture<'static, Result<()>> {
         let snapshot = self.snapshot();
         let version = self.version.clone();
-        let save_task = file.save(snapshot, ctx.as_ref());
-        let task = ctx.spawn(save_task, |me, save_result, ctx| {
-            if save_result.is_ok() {
-                me.did_save(version, ctx);
-            }
-            save_result
-        });
-        Box::pin(task)
+        if let Some(file) = new_file.as_ref().or(self.file.as_ref()) {
+            let save_task = file.save(snapshot, ctx.as_ref());
+            ctx.spawn(save_task, |me, save_result, ctx| {
+                if save_result.is_ok() {
+                    me.did_save(version, new_file, ctx);
+                }
+                save_result
+            })
+            .boxed_local()
+        } else {
+            async { Ok(()) }.boxed_local()
+        }
     }
 
-    fn did_save(&mut self, version: time::Global, ctx: &mut ModelContext<Buffer>) {
+    fn did_save(
+        &mut self,
+        version: time::Global,
+        file: Option<FileHandle>,
+        ctx: &mut ModelContext<Buffer>,
+    ) {
+        self.file = file;
         self.saved_version = version;
         ctx.emit(Event::Saved);
     }
@@ -1737,6 +1772,7 @@ impl Clone for Buffer {
             selections: self.selections.clone(),
             selections_last_update: self.selections_last_update.clone(),
             deferred_ops: self.deferred_ops.clone(),
+            file: self.file.clone(),
             deferred_replicas: self.deferred_replicas.clone(),
             replica_id: self.replica_id,
             local_clock: self.local_clock.clone(),
@@ -2296,8 +2332,8 @@ mod tests {
     #[test]
     fn test_edit() {
         App::test((), |ctx| {
-            ctx.add_model(|_| {
-                let mut buffer = Buffer::new(0, "abc");
+            ctx.add_model(|ctx| {
+                let mut buffer = Buffer::new(0, "abc", ctx);
                 assert_eq!(buffer.text(), "abc");
                 buffer.edit(vec![3..3], "def", None).unwrap();
                 assert_eq!(buffer.text(), "abcdef");
@@ -2321,8 +2357,8 @@ mod tests {
             let buffer_1_events = Rc::new(RefCell::new(Vec::new()));
             let buffer_2_events = Rc::new(RefCell::new(Vec::new()));
 
-            let buffer1 = app.add_model(|_| Buffer::new(0, "abcdef"));
-            let buffer2 = app.add_model(|_| Buffer::new(1, "abcdef"));
+            let buffer1 = app.add_model(|ctx| Buffer::new(0, "abcdef", ctx));
+            let buffer2 = app.add_model(|ctx| Buffer::new(1, "abcdef", ctx));
             let mut buffer_ops = Vec::new();
             buffer1.update(app, |buffer, ctx| {
                 let buffer_1_events = buffer_1_events.clone();
@@ -2408,8 +2444,8 @@ mod tests {
                 let mut reference_string = RandomCharIter::new(&mut rng)
                     .take(reference_string_len)
                     .collect::<String>();
-                ctx.add_model(|_| {
-                    let mut buffer = Buffer::new(0, reference_string.as_str());
+                ctx.add_model(|ctx| {
+                    let mut buffer = Buffer::new(0, reference_string.as_str(), ctx);
                     let mut buffer_versions = Vec::new();
                     for _i in 0..10 {
                         let (old_ranges, new_text, _) = buffer.randomly_mutate(rng, None);
@@ -2494,8 +2530,8 @@ mod tests {
     #[test]
     fn test_line_len() {
         App::test((), |ctx| {
-            ctx.add_model(|_| {
-                let mut buffer = Buffer::new(0, "");
+            ctx.add_model(|ctx| {
+                let mut buffer = Buffer::new(0, "", ctx);
                 buffer.edit(vec![0..0], "abcd\nefg\nhij", None).unwrap();
                 buffer.edit(vec![12..12], "kl\nmno", None).unwrap();
                 buffer.edit(vec![18..18], "\npqrs\n", None).unwrap();
@@ -2516,8 +2552,8 @@ mod tests {
     #[test]
     fn test_rightmost_point() {
         App::test((), |ctx| {
-            ctx.add_model(|_| {
-                let mut buffer = Buffer::new(0, "");
+            ctx.add_model(|ctx| {
+                let mut buffer = Buffer::new(0, "", ctx);
                 assert_eq!(buffer.rightmost_point().row, 0);
                 buffer.edit(vec![0..0], "abcd\nefg\nhij", None).unwrap();
                 assert_eq!(buffer.rightmost_point().row, 0);
@@ -2537,8 +2573,8 @@ mod tests {
     #[test]
     fn test_text_summary_for_range() {
         App::test((), |ctx| {
-            ctx.add_model(|_| {
-                let buffer = Buffer::new(0, "ab\nefg\nhklm\nnopqrs\ntuvwxyz");
+            ctx.add_model(|ctx| {
+                let buffer = Buffer::new(0, "ab\nefg\nhklm\nnopqrs\ntuvwxyz", ctx);
                 let text = Text::from(buffer.text());
                 assert_eq!(
                     buffer.text_summary_for_range(1..3),
@@ -2568,8 +2604,8 @@ mod tests {
     #[test]
     fn test_chars_at() {
         App::test((), |ctx| {
-            ctx.add_model(|_| {
-                let mut buffer = Buffer::new(0, "");
+            ctx.add_model(|ctx| {
+                let mut buffer = Buffer::new(0, "", ctx);
                 buffer.edit(vec![0..0], "abcd\nefgh\nij", None).unwrap();
                 buffer.edit(vec![12..12], "kl\nmno", None).unwrap();
                 buffer.edit(vec![18..18], "\npqrs", None).unwrap();
@@ -2591,7 +2627,7 @@ mod tests {
                 assert_eq!(chars.collect::<String>(), "PQrs");
 
                 // Regression test:
-                let mut buffer = Buffer::new(0, "");
+                let mut buffer = Buffer::new(0, "", ctx);
                 buffer.edit(vec![0..0], "[workspace]\nmembers = [\n    \"xray_core\",\n    \"xray_server\",\n    \"xray_cli\",\n    \"xray_wasm\",\n]\n", None).unwrap();
                 buffer.edit(vec![60..60], "\n", None).unwrap();
 
@@ -2720,8 +2756,8 @@ mod tests {
     #[test]
     fn test_anchors() {
         App::test((), |ctx| {
-            ctx.add_model(|_| {
-                let mut buffer = Buffer::new(0, "");
+            ctx.add_model(|ctx| {
+                let mut buffer = Buffer::new(0, "", ctx);
                 buffer.edit(vec![0..0], "abc", None).unwrap();
                 let left_anchor = buffer.anchor_before(2).unwrap();
                 let right_anchor = buffer.anchor_after(2).unwrap();
@@ -2885,8 +2921,8 @@ mod tests {
     #[test]
     fn test_anchors_at_start_and_end() {
         App::test((), |ctx| {
-            ctx.add_model(|_| {
-                let mut buffer = Buffer::new(0, "");
+            ctx.add_model(|ctx| {
+                let mut buffer = Buffer::new(0, "", ctx);
                 let before_start_anchor = buffer.anchor_before(0).unwrap();
                 let after_end_anchor = buffer.anchor_after(0).unwrap();
 
@@ -2913,7 +2949,7 @@ mod tests {
     #[test]
     fn test_is_modified() {
         App::test((), |app| {
-            let model = app.add_model(|_| Buffer::new(0, "abc"));
+            let model = app.add_model(|ctx| Buffer::new(0, "abc", ctx));
             let events = Rc::new(RefCell::new(Vec::new()));
 
             // initially, the buffer isn't dirty.
@@ -2945,7 +2981,7 @@ mod tests {
                 );
                 events.borrow_mut().clear();
 
-                buffer.did_save(buffer.version(), ctx);
+                buffer.did_save(buffer.version(), None, ctx);
             });
 
             // after saving, the buffer is not dirty, and emits a saved event.
@@ -3000,8 +3036,8 @@ mod tests {
     #[test]
     fn test_undo_redo() {
         App::test((), |app| {
-            app.add_model(|_| {
-                let mut buffer = Buffer::new(0, "1234");
+            app.add_model(|ctx| {
+                let mut buffer = Buffer::new(0, "1234", ctx);
 
                 let edit1 = buffer.edit(vec![1..1], "abx", None).unwrap();
                 let edit2 = buffer.edit(vec![3..4], "yzef", None).unwrap();
@@ -3037,9 +3073,9 @@ mod tests {
     #[test]
     fn test_history() {
         App::test((), |app| {
-            app.add_model(|_| {
+            app.add_model(|ctx| {
                 let mut now = Instant::now();
-                let mut buffer = Buffer::new(0, "123456");
+                let mut buffer = Buffer::new(0, "123456", ctx);
 
                 let (set_id, _) = buffer
                     .add_selection_set(buffer.selections_from_ranges(vec![4..4]).unwrap(), None);
@@ -3123,7 +3159,8 @@ mod tests {
                 let mut buffers = Vec::new();
                 let mut network = Network::new();
                 for i in 0..PEERS {
-                    let buffer = ctx.add_model(|_| Buffer::new(i as ReplicaId, base_text.as_str()));
+                    let buffer =
+                        ctx.add_model(|ctx| Buffer::new(i as ReplicaId, base_text.as_str(), ctx));
                     buffers.push(buffer);
                     replica_ids.push(i as u16);
                     network.add_peer(i as u16);

zed/src/editor/buffer_view.rs 🔗

@@ -13,7 +13,7 @@ use gpui::{
 use parking_lot::Mutex;
 use serde::{Deserialize, Serialize};
 use smallvec::SmallVec;
-use smol::{future::FutureExt, Timer};
+use smol::Timer;
 use std::{
     cmp::{self, Ordering},
     fmt::Write,
@@ -253,7 +253,6 @@ pub enum SelectAction {
 pub struct BufferView {
     handle: WeakViewHandle<Self>,
     buffer: ModelHandle<Buffer>,
-    file: Option<FileHandle>,
     display_map: ModelHandle<DisplayMap>,
     selection_set_id: SelectionSetId,
     pending_selection: Option<Selection>,
@@ -275,24 +274,19 @@ struct ClipboardSelection {
 
 impl BufferView {
     pub fn single_line(settings: watch::Receiver<Settings>, ctx: &mut ViewContext<Self>) -> Self {
-        let buffer = ctx.add_model(|_| Buffer::new(0, String::new()));
-        let mut view = Self::for_buffer(buffer, None, settings, ctx);
+        let buffer = ctx.add_model(|ctx| Buffer::new(0, String::new(), ctx));
+        let mut view = Self::for_buffer(buffer, settings, ctx);
         view.single_line = true;
         view
     }
 
     pub fn for_buffer(
         buffer: ModelHandle<Buffer>,
-        file: Option<FileHandle>,
         settings: watch::Receiver<Settings>,
         ctx: &mut ViewContext<Self>,
     ) -> Self {
         settings.notify_view_on_change(ctx);
 
-        if let Some(file) = file.as_ref() {
-            file.observe_from_view(ctx, |_, _, ctx| ctx.emit(Event::FileHandleChanged));
-        }
-
         ctx.observe_model(&buffer, Self::on_buffer_changed);
         ctx.subscribe_to_model(&buffer, Self::on_buffer_event);
         let display_map = ctx.add_model(|ctx| {
@@ -318,7 +312,6 @@ impl BufferView {
         Self {
             handle: ctx.handle().downgrade(),
             buffer,
-            file,
             display_map,
             selection_set_id,
             pending_selection: None,
@@ -2058,10 +2051,6 @@ impl BufferView {
             buffer::Event::FileHandleChanged => ctx.emit(Event::FileHandleChanged),
         }
     }
-
-    pub fn file(&self) -> Option<&FileHandle> {
-        self.file.as_ref()
-    }
 }
 
 pub enum Event {
@@ -2099,6 +2088,22 @@ impl View for BufferView {
     }
 }
 
+impl workspace::Item for Buffer {
+    type View = BufferView;
+
+    fn file(&self) -> Option<&FileHandle> {
+        self.file()
+    }
+
+    fn build_view(
+        handle: ModelHandle<Self>,
+        settings: watch::Receiver<Settings>,
+        ctx: &mut ViewContext<Self::View>,
+    ) -> Self::View {
+        BufferView::for_buffer(handle, settings, ctx)
+    }
+}
+
 impl workspace::ItemView for BufferView {
     fn should_activate_item_on_event(event: &Self::Event) -> bool {
         matches!(event, Event::Activate)
@@ -2112,7 +2117,11 @@ impl workspace::ItemView for BufferView {
     }
 
     fn title(&self, app: &AppContext) -> std::string::String {
-        let filename = self.file.as_ref().and_then(|file| file.file_name(app));
+        let filename = self
+            .buffer
+            .read(app)
+            .file()
+            .and_then(|file| file.file_name(app));
         if let Some(name) = filename {
             name.to_string_lossy().into()
         } else {
@@ -2120,20 +2129,15 @@ impl workspace::ItemView for BufferView {
         }
     }
 
-    fn entry_id(&self, _: &AppContext) -> Option<(usize, Arc<Path>)> {
-        self.file.as_ref().map(|file| file.entry_id())
+    fn entry_id(&self, ctx: &AppContext) -> Option<(usize, Arc<Path>)> {
+        self.buffer.read(ctx).file().map(|file| file.entry_id())
     }
 
     fn clone_on_split(&self, ctx: &mut ViewContext<Self>) -> Option<Self>
     where
         Self: Sized,
     {
-        let clone = BufferView::for_buffer(
-            self.buffer.clone(),
-            self.file.clone(),
-            self.settings.clone(),
-            ctx,
-        );
+        let clone = BufferView::for_buffer(self.buffer.clone(), self.settings.clone(), ctx);
         *clone.scroll_position.lock() = *self.scroll_position.lock();
         Some(clone)
     }
@@ -2142,21 +2146,8 @@ impl workspace::ItemView for BufferView {
         &mut self,
         new_file: Option<FileHandle>,
         ctx: &mut ViewContext<Self>,
-    ) -> 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| {
-                if new_file.is_some() && result.is_ok() {
-                    this.file = new_file;
-                    ctx.emit(Event::FileHandleChanged);
-                    ctx.notify();
-                }
-                result
-            })
-            .boxed_local()
-        } else {
-            Box::pin(async { Err(anyhow::anyhow!("can't save a buffer with no file")) })
-        }
+    ) -> LocalBoxFuture<'static, Result<()>> {
+        self.buffer.update(ctx, |b, ctx| b.save(new_file, ctx))
     }
 
     fn is_dirty(&self, ctx: &AppContext) -> bool {
@@ -2174,10 +2165,11 @@ mod tests {
     #[test]
     fn test_selection_with_mouse() {
         App::test((), |app| {
-            let buffer = app.add_model(|_| Buffer::new(0, "aaaaaa\nbbbbbb\ncccccc\ndddddd\n"));
+            let buffer =
+                app.add_model(|ctx| Buffer::new(0, "aaaaaa\nbbbbbb\ncccccc\ndddddd\n", ctx));
             let settings = settings::channel(&app.font_cache()).unwrap().1;
             let (_, buffer_view) =
-                app.add_window(|ctx| BufferView::for_buffer(buffer, None, settings, ctx));
+                app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx));
 
             buffer_view.update(app, |view, ctx| {
                 view.begin_selection(DisplayPoint::new(2, 2), false, ctx);
@@ -2288,11 +2280,11 @@ mod tests {
             let layout_cache = TextLayoutCache::new(app.platform().fonts());
             let font_cache = app.font_cache().clone();
 
-            let buffer = app.add_model(|_| Buffer::new(0, sample_text(6, 6)));
+            let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(6, 6), ctx));
 
             let settings = settings::channel(&font_cache).unwrap().1;
             let (_, view) =
-                app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), None, settings, ctx));
+                app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx));
 
             let layouts = view
                 .read(app)
@@ -2305,7 +2297,7 @@ mod tests {
     #[test]
     fn test_fold() {
         App::test((), |app| {
-            let buffer = app.add_model(|_| {
+            let buffer = app.add_model(|ctx| {
                 Buffer::new(
                     0,
                     "
@@ -2326,11 +2318,12 @@ mod tests {
                     }
                 "
                     .unindent(),
+                    ctx,
                 )
             });
             let settings = settings::channel(&app.font_cache()).unwrap().1;
             let (_, view) =
-                app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), None, settings, ctx));
+                app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx));
 
             view.update(app, |view, ctx| {
                 view.select_display_ranges(
@@ -2399,10 +2392,10 @@ mod tests {
     #[test]
     fn test_move_cursor() {
         App::test((), |app| {
-            let buffer = app.add_model(|_| Buffer::new(0, sample_text(6, 6)));
+            let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(6, 6), ctx));
             let settings = settings::channel(&app.font_cache()).unwrap().1;
             let (_, view) =
-                app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), None, settings, ctx));
+                app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx));
 
             buffer.update(app, |buffer, ctx| {
                 buffer
@@ -2477,10 +2470,9 @@ mod tests {
     #[test]
     fn test_beginning_end_of_line() {
         App::test((), |app| {
-            let buffer = app.add_model(|_| Buffer::new(0, "abc\n  def"));
+            let buffer = app.add_model(|ctx| Buffer::new(0, "abc\n  def", ctx));
             let settings = settings::channel(&app.font_cache()).unwrap().1;
-            let (_, view) =
-                app.add_window(|ctx| BufferView::for_buffer(buffer, None, settings, ctx));
+            let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx));
             view.update(app, |view, ctx| {
                 view.select_display_ranges(
                     &[
@@ -2606,11 +2598,10 @@ mod tests {
     #[test]
     fn test_prev_next_word_boundary() {
         App::test((), |app| {
-            let buffer =
-                app.add_model(|_| Buffer::new(0, "use std::str::{foo, bar}\n\n  {baz.qux()}"));
+            let buffer = app
+                .add_model(|ctx| Buffer::new(0, "use std::str::{foo, bar}\n\n  {baz.qux()}", ctx));
             let settings = settings::channel(&app.font_cache()).unwrap().1;
-            let (_, view) =
-                app.add_window(|ctx| BufferView::for_buffer(buffer, None, settings, ctx));
+            let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx));
             view.update(app, |view, ctx| {
                 view.select_display_ranges(
                     &[
@@ -2789,12 +2780,16 @@ mod tests {
     #[test]
     fn test_backspace() {
         App::test((), |app| {
-            let buffer = app.add_model(|_| {
-                Buffer::new(0, "one two three\nfour five six\nseven eight nine\nten\n")
+            let buffer = app.add_model(|ctx| {
+                Buffer::new(
+                    0,
+                    "one two three\nfour five six\nseven eight nine\nten\n",
+                    ctx,
+                )
             });
             let settings = settings::channel(&app.font_cache()).unwrap().1;
             let (_, view) =
-                app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), None, settings, ctx));
+                app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx));
 
             view.update(app, |view, ctx| {
                 view.select_display_ranges(
@@ -2822,12 +2817,16 @@ mod tests {
     #[test]
     fn test_delete() {
         App::test((), |app| {
-            let buffer = app.add_model(|_| {
-                Buffer::new(0, "one two three\nfour five six\nseven eight nine\nten\n")
+            let buffer = app.add_model(|ctx| {
+                Buffer::new(
+                    0,
+                    "one two three\nfour five six\nseven eight nine\nten\n",
+                    ctx,
+                )
             });
             let settings = settings::channel(&app.font_cache()).unwrap().1;
             let (_, view) =
-                app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), None, settings, ctx));
+                app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx));
 
             view.update(app, |view, ctx| {
                 view.select_display_ranges(
@@ -2856,9 +2855,8 @@ mod tests {
     fn test_delete_line() {
         App::test((), |app| {
             let settings = settings::channel(&app.font_cache()).unwrap().1;
-            let buffer = app.add_model(|_| Buffer::new(0, "abc\ndef\nghi\n"));
-            let (_, view) =
-                app.add_window(|ctx| BufferView::for_buffer(buffer, None, settings, ctx));
+            let buffer = app.add_model(|ctx| Buffer::new(0, "abc\ndef\nghi\n", ctx));
+            let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx));
             view.update(app, |view, ctx| {
                 view.select_display_ranges(
                     &[
@@ -2881,9 +2879,8 @@ mod tests {
             );
 
             let settings = settings::channel(&app.font_cache()).unwrap().1;
-            let buffer = app.add_model(|_| Buffer::new(0, "abc\ndef\nghi\n"));
-            let (_, view) =
-                app.add_window(|ctx| BufferView::for_buffer(buffer, None, settings, ctx));
+            let buffer = app.add_model(|ctx| Buffer::new(0, "abc\ndef\nghi\n", ctx));
+            let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx));
             view.update(app, |view, ctx| {
                 view.select_display_ranges(
                     &[DisplayPoint::new(2, 0)..DisplayPoint::new(0, 1)],
@@ -2904,9 +2901,8 @@ mod tests {
     fn test_duplicate_line() {
         App::test((), |app| {
             let settings = settings::channel(&app.font_cache()).unwrap().1;
-            let buffer = app.add_model(|_| Buffer::new(0, "abc\ndef\nghi\n"));
-            let (_, view) =
-                app.add_window(|ctx| BufferView::for_buffer(buffer, None, settings, ctx));
+            let buffer = app.add_model(|ctx| Buffer::new(0, "abc\ndef\nghi\n", ctx));
+            let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx));
             view.update(app, |view, ctx| {
                 view.select_display_ranges(
                     &[
@@ -2935,9 +2931,8 @@ mod tests {
             );
 
             let settings = settings::channel(&app.font_cache()).unwrap().1;
-            let buffer = app.add_model(|_| Buffer::new(0, "abc\ndef\nghi\n"));
-            let (_, view) =
-                app.add_window(|ctx| BufferView::for_buffer(buffer, None, settings, ctx));
+            let buffer = app.add_model(|ctx| Buffer::new(0, "abc\ndef\nghi\n", ctx));
+            let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx));
             view.update(app, |view, ctx| {
                 view.select_display_ranges(
                     &[
@@ -2966,10 +2961,10 @@ mod tests {
     #[test]
     fn test_clipboard() {
         App::test((), |app| {
-            let buffer = app.add_model(|_| Buffer::new(0, "one two three four five six "));
+            let buffer = app.add_model(|ctx| Buffer::new(0, "one two three four five six ", ctx));
             let settings = settings::channel(&app.font_cache()).unwrap().1;
             let view = app
-                .add_window(|ctx| BufferView::for_buffer(buffer.clone(), None, settings, ctx))
+                .add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx))
                 .1;
 
             // Cut with three selections. Clipboard text is divided into three slices.
@@ -3107,10 +3102,9 @@ mod tests {
     #[test]
     fn test_select_all() {
         App::test((), |app| {
-            let buffer = app.add_model(|_| Buffer::new(0, "abc\nde\nfgh"));
+            let buffer = app.add_model(|ctx| Buffer::new(0, "abc\nde\nfgh", ctx));
             let settings = settings::channel(&app.font_cache()).unwrap().1;
-            let (_, view) =
-                app.add_window(|ctx| BufferView::for_buffer(buffer, None, settings, ctx));
+            let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx));
             view.update(app, |b, ctx| b.select_all(&(), ctx));
             assert_eq!(
                 view.read(app).selection_ranges(app.as_ref()),

zed/src/editor/display_map/fold_map.rs 🔗

@@ -505,7 +505,7 @@ mod tests {
     #[test]
     fn test_basic_folds() {
         App::test((), |app| {
-            let buffer = app.add_model(|_| Buffer::new(0, sample_text(5, 6)));
+            let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(5, 6), ctx));
             let mut map = FoldMap::new(buffer.clone(), app.as_ref());
 
             map.fold(
@@ -556,7 +556,7 @@ mod tests {
     #[test]
     fn test_adjacent_folds() {
         App::test((), |app| {
-            let buffer = app.add_model(|_| Buffer::new(0, "abcdefghijkl"));
+            let buffer = app.add_model(|ctx| Buffer::new(0, "abcdefghijkl", ctx));
 
             {
                 let mut map = FoldMap::new(buffer.clone(), app.as_ref());
@@ -600,7 +600,7 @@ mod tests {
     #[test]
     fn test_overlapping_folds() {
         App::test((), |app| {
-            let buffer = app.add_model(|_| Buffer::new(0, sample_text(5, 6)));
+            let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(5, 6), ctx));
             let mut map = FoldMap::new(buffer.clone(), app.as_ref());
             map.fold(
                 vec![
@@ -619,7 +619,7 @@ mod tests {
     #[test]
     fn test_merging_folds_via_edit() {
         App::test((), |app| {
-            let buffer = app.add_model(|_| Buffer::new(0, sample_text(5, 6)));
+            let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(5, 6), ctx));
             let mut map = FoldMap::new(buffer.clone(), app.as_ref());
 
             map.fold(
@@ -670,10 +670,10 @@ mod tests {
             let mut rng = StdRng::seed_from_u64(seed);
 
             App::test((), |app| {
-                let buffer = app.add_model(|_| {
+                let buffer = app.add_model(|ctx| {
                     let len = rng.gen_range(0..10);
                     let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
-                    Buffer::new(0, text)
+                    Buffer::new(0, text, ctx)
                 });
                 let mut map = FoldMap::new(buffer.clone(), app.as_ref());
 
@@ -749,7 +749,7 @@ mod tests {
     fn test_buffer_rows() {
         App::test((), |app| {
             let text = sample_text(6, 6) + "\n";
-            let buffer = app.add_model(|_| Buffer::new(0, text));
+            let buffer = app.add_model(|ctx| Buffer::new(0, text, ctx));
 
             let mut map = FoldMap::new(buffer.clone(), app.as_ref());
 

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

@@ -324,7 +324,7 @@ mod tests {
     fn test_chars_at() {
         App::test((), |app| {
             let text = sample_text(6, 6);
-            let buffer = app.add_model(|_| Buffer::new(0, text));
+            let buffer = app.add_model(|ctx| Buffer::new(0, text, ctx));
             let map = app.add_model(|ctx| DisplayMap::new(buffer.clone(), 4, ctx));
             buffer
                 .update(app, |buffer, ctx| {
@@ -391,7 +391,7 @@ mod tests {
     #[test]
     fn test_max_point() {
         App::test((), |app| {
-            let buffer = app.add_model(|_| Buffer::new(0, "aaa\n\t\tbbb"));
+            let buffer = app.add_model(|ctx| Buffer::new(0, "aaa\n\t\tbbb", ctx));
             let map = app.add_model(|ctx| DisplayMap::new(buffer.clone(), 4, ctx));
             assert_eq!(
                 map.read(app).max_point(app.as_ref()),

zed/src/workspace.rs 🔗

@@ -8,8 +8,8 @@ use crate::{
     watch::{self, Receiver},
     worktree::FileHandle,
 };
-use gpui::{MutableAppContext, PathPromptOptions};
-use std::path::PathBuf;
+use std::{collections::HashMap, fmt, path::PathBuf};
+
 pub fn init(app: &mut MutableAppContext) {
     app.add_global_action("workspace:open", open);
     app.add_global_action("workspace:open_paths", open_paths);
@@ -31,12 +31,13 @@ use crate::{
 use futures_core::{future::LocalBoxFuture, Future};
 use gpui::{
     color::rgbu, elements::*, json::to_string_pretty, keymap::Binding, AnyViewHandle, AppContext,
-    ClipboardItem, Entity, EntityTask, ModelHandle, View, ViewContext, ViewHandle,
+    ClipboardItem, Entity, EntityTask, ModelHandle, MutableAppContext, PathPromptOptions, View,
+    ViewContext, ViewHandle,
 };
 use log::error;
 use smol::prelude::*;
 use std::{
-    collections::{hash_map::Entry, HashMap, HashSet},
+    collections::{hash_map::Entry, HashSet},
     path::Path,
     sync::Arc,
 };
@@ -98,6 +99,18 @@ fn quit(_: &(), app: &mut MutableAppContext) {
     app.platform().quit();
 }
 
+pub trait Item: Entity + Sized {
+    type View: ItemView;
+
+    fn build_view(
+        handle: ModelHandle<Self>,
+        settings: watch::Receiver<Settings>,
+        ctx: &mut ViewContext<Self::View>,
+    ) -> Self::View;
+
+    fn file(&self) -> Option<&FileHandle>;
+}
+
 pub trait ItemView: View {
     fn title(&self, app: &AppContext) -> String;
     fn entry_id(&self, app: &AppContext) -> Option<(usize, Arc<Path>)>;
@@ -114,7 +127,7 @@ pub trait ItemView: View {
         &mut self,
         _: Option<FileHandle>,
         _: &mut ViewContext<Self>,
-    ) -> LocalBoxFuture<'static, anyhow::Result<u64>>;
+    ) -> LocalBoxFuture<'static, anyhow::Result<()>>;
     fn should_activate_item_on_event(_: &Self::Event) -> bool {
         false
     }
@@ -123,6 +136,18 @@ pub trait ItemView: View {
     }
 }
 
+pub trait ItemHandle: Send + Sync {
+    fn id(&self) -> usize;
+    fn file<'a>(&'a self, ctx: &'a AppContext) -> Option<&'a FileHandle>;
+    fn add_view(
+        &self,
+        window_id: usize,
+        settings: watch::Receiver<Settings>,
+        app: &mut MutableAppContext,
+    ) -> Box<dyn ItemViewHandle>;
+    fn boxed_clone(&self) -> Box<dyn ItemHandle>;
+}
+
 pub trait ItemViewHandle: Send + Sync {
     fn title(&self, app: &AppContext) -> String;
     fn entry_id(&self, app: &AppContext) -> Option<(usize, Arc<Path>)>;
@@ -136,7 +161,30 @@ pub trait ItemViewHandle: Send + Sync {
         &self,
         file: Option<FileHandle>,
         ctx: &mut MutableAppContext,
-    ) -> LocalBoxFuture<'static, anyhow::Result<u64>>;
+    ) -> LocalBoxFuture<'static, anyhow::Result<()>>;
+}
+
+impl<T: Item> ItemHandle for ModelHandle<T> {
+    fn id(&self) -> usize {
+        self.id()
+    }
+
+    fn file<'a>(&'a self, ctx: &'a AppContext) -> Option<&'a FileHandle> {
+        self.read(ctx).file()
+    }
+
+    fn add_view(
+        &self,
+        window_id: usize,
+        settings: watch::Receiver<Settings>,
+        app: &mut MutableAppContext,
+    ) -> Box<dyn ItemViewHandle> {
+        Box::new(app.add_view(window_id, |ctx| T::build_view(self.clone(), settings, ctx)))
+    }
+
+    fn boxed_clone(&self) -> Box<dyn ItemHandle> {
+        Box::new(self.clone())
+    }
 }
 
 impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
@@ -179,7 +227,7 @@ impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
         &self,
         file: Option<FileHandle>,
         ctx: &mut MutableAppContext,
-    ) -> LocalBoxFuture<'static, anyhow::Result<u64>> {
+    ) -> LocalBoxFuture<'static, anyhow::Result<()>> {
         self.update(ctx, |item, ctx| item.save(file, ctx))
     }
 
@@ -202,6 +250,18 @@ impl Clone for Box<dyn ItemViewHandle> {
     }
 }
 
+impl Clone for Box<dyn ItemHandle> {
+    fn clone(&self) -> Box<dyn ItemHandle> {
+        self.boxed_clone()
+    }
+}
+
+impl fmt::Debug for Box<dyn ItemHandle> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "ItemHandle {{id: {}}}", self.id())
+    }
+}
+
 #[derive(Debug)]
 pub struct State {
     pub modal: Option<usize>,
@@ -214,14 +274,13 @@ pub struct Workspace {
     center: PaneGroup,
     panes: Vec<ViewHandle<Pane>>,
     active_pane: ViewHandle<Pane>,
-    loading_entries: HashSet<(usize, Arc<Path>)>,
     replica_id: ReplicaId,
     worktrees: HashSet<ModelHandle<Worktree>>,
-    buffers: HashMap<
-        (usize, u64),
-        postage::watch::Receiver<Option<Result<ModelHandle<Buffer>, Arc<anyhow::Error>>>>,
+    items: Vec<Box<dyn ItemHandle>>,
+    loading_items: HashMap<
+        (usize, Arc<Path>),
+        postage::watch::Receiver<Option<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>>>,
     >,
-    untitled_buffers: HashMap<usize, ModelHandle<Buffer>>,
 }
 
 impl Workspace {
@@ -242,12 +301,11 @@ impl Workspace {
             center: PaneGroup::new(pane.id()),
             panes: vec![pane.clone()],
             active_pane: pane.clone(),
-            loading_entries: HashSet::new(),
             settings,
             replica_id,
             worktrees: Default::default(),
-            buffers: Default::default(),
-            untitled_buffers: Default::default(),
+            items: Default::default(),
+            loading_items: Default::default(),
         }
     }
 
@@ -366,11 +424,10 @@ impl Workspace {
     }
 
     pub fn open_new_file(&mut self, _: &(), ctx: &mut ViewContext<Self>) {
-        let buffer = ctx.add_model(|_| Buffer::new(self.replica_id, ""));
-        let buffer_view = ctx.add_view(|ctx| {
-            BufferView::for_buffer(buffer.clone(), None, self.settings.clone(), ctx)
-        });
-        self.untitled_buffers.insert(buffer_view.id(), buffer);
+        let buffer = ctx.add_model(|ctx| Buffer::new(self.replica_id, "", ctx));
+        let buffer_view =
+            ctx.add_view(|ctx| BufferView::for_buffer(buffer.clone(), self.settings.clone(), ctx));
+        self.items.push(Box::new(buffer));
         self.add_item(Box::new(buffer_view), ctx);
     }
 
@@ -380,10 +437,8 @@ impl Workspace {
         entry: (usize, Arc<Path>),
         ctx: &mut ViewContext<Self>,
     ) -> Option<EntityTask<()>> {
-        if self.loading_entries.contains(&entry) {
-            return None;
-        }
-
+        // If the active pane contains a view for this file, then activate
+        // that item view.
         if self
             .active_pane()
             .update(ctx, |pane, ctx| pane.activate_entry(entry.clone(), ctx))
@@ -391,6 +446,19 @@ impl Workspace {
             return None;
         }
 
+        let window_id = ctx.window_id();
+        let settings = self.settings.clone();
+
+        // Otherwise, if this file is already open somewhere in the workspace,
+        // then add another view for it.
+        if let Some(item) = self.items.iter().find(|item| {
+            item.file(ctx.as_ref())
+                .map_or(false, |f| f.entry_id() == entry)
+        }) {
+            self.add_item(item.add_view(window_id, settings, ctx.as_mut()), ctx);
+            return None;
+        }
+
         let (worktree_id, path) = entry.clone();
 
         let worktree = match self.worktrees.get(&worktree_id).cloned() {
@@ -401,40 +469,31 @@ impl Workspace {
             }
         };
 
-        let inode = match worktree.read(ctx).inode_for_path(&path) {
-            Some(inode) => inode,
-            None => {
-                log::error!("path {:?} does not exist", path);
-                return None;
-            }
-        };
-
         let file = worktree.file(path.clone(), ctx.as_ref());
         if file.is_deleted() {
             log::error!("path {:?} does not exist", path);
             return None;
         }
 
-        self.loading_entries.insert(entry.clone());
-
-        if let Entry::Vacant(entry) = self.buffers.entry((worktree_id, inode)) {
+        if let Entry::Vacant(entry) = self.loading_items.entry(entry.clone()) {
             let (mut tx, rx) = postage::watch::channel();
             entry.insert(rx);
-            let history = file.load_history(ctx.as_ref());
             let replica_id = self.replica_id;
-            let buffer = ctx
+            let history = ctx
                 .background_executor()
-                .spawn(async move { Ok(Buffer::from_history(replica_id, history.await?)) });
-            ctx.spawn(buffer, move |_, from_history_result, ctx| {
-                *tx.borrow_mut() = Some(match from_history_result {
-                    Ok(buffer) => Ok(ctx.add_model(|_| buffer)),
+                .spawn(file.load_history(ctx.as_ref()));
+            ctx.spawn(history, move |_, history, ctx| {
+                *tx.borrow_mut() = Some(match history {
+                    Ok(history) => Ok(Box::new(ctx.add_model(|ctx| {
+                        Buffer::from_history(replica_id, history, Some(file), ctx)
+                    }))),
                     Err(error) => Err(Arc::new(error)),
                 })
             })
             .detach()
         }
 
-        let mut watch = self.buffers.get(&(worktree_id, inode)).unwrap().clone();
+        let mut watch = self.loading_items.get(&entry).unwrap().clone();
         Some(ctx.spawn(
             async move {
                 loop {
@@ -445,18 +504,12 @@ impl Workspace {
                 }
             },
             move |me, load_result, ctx| {
-                me.loading_entries.remove(&entry);
+                me.loading_items.remove(&entry);
                 match load_result {
-                    Ok(buffer_handle) => {
-                        let buffer_view = Box::new(ctx.add_view(|ctx| {
-                            BufferView::for_buffer(
-                                buffer_handle,
-                                Some(file),
-                                me.settings.clone(),
-                                ctx,
-                            )
-                        }));
-                        me.add_item(buffer_view, ctx);
+                    Ok(item) => {
+                        me.items.push(item.clone());
+                        let view = item.add_view(window_id, settings, ctx.as_mut());
+                        me.add_item(view, ctx);
                     }
                     Err(error) => {
                         log::error!("error opening item: {}", error);
@@ -484,20 +537,11 @@ 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());
-                            let item_id = item.id();
-                            ctx.spawn(task, move |this, result, _| match result {
-                                Err(e) => {
+                            ctx.spawn(task, move |_, result, _| {
+                                if let Err(e) = result {
                                     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()
                         })
@@ -760,15 +804,13 @@ mod tests {
             let file2 = entries[1].clone();
             let file3 = entries[2].clone();
 
-            let pane = app.read(|ctx| workspace.read(ctx).active_pane().clone());
-
             // Open the first entry
             workspace
                 .update(&mut app, |w, ctx| w.open_entry(file1.clone(), ctx))
                 .unwrap()
                 .await;
             app.read(|ctx| {
-                let pane = pane.read(ctx);
+                let pane = workspace.read(ctx).active_pane().read(ctx);
                 assert_eq!(
                     pane.active_item().unwrap().entry_id(ctx),
                     Some(file1.clone())
@@ -782,7 +824,7 @@ mod tests {
                 .unwrap()
                 .await;
             app.read(|ctx| {
-                let pane = pane.read(ctx);
+                let pane = workspace.read(ctx).active_pane().read(ctx);
                 assert_eq!(
                     pane.active_item().unwrap().entry_id(ctx),
                     Some(file2.clone())
@@ -795,7 +837,7 @@ mod tests {
                 assert!(w.open_entry(file1.clone(), ctx).is_none())
             });
             app.read(|ctx| {
-                let pane = pane.read(ctx);
+                let pane = workspace.read(ctx).active_pane().read(ctx);
                 assert_eq!(
                     pane.active_item().unwrap().entry_id(ctx),
                     Some(file1.clone())
@@ -803,21 +845,42 @@ mod tests {
                 assert_eq!(pane.items().len(), 2);
             });
 
-            // Open the third entry twice concurrently. Only one pane item is added.
-            workspace
-                .update(&mut app, |w, ctx| {
-                    let task = w.open_entry(file3.clone(), ctx).unwrap();
-                    assert!(w.open_entry(file3.clone(), ctx).is_none());
-                    task
-                })
-                .await;
+            // Split the pane with the first entry, then open the second entry again.
+            workspace.update(&mut app, |w, ctx| {
+                w.split_pane(w.active_pane().clone(), SplitDirection::Right, ctx);
+                assert!(w.open_entry(file2.clone(), ctx).is_none());
+                assert_eq!(
+                    w.active_pane()
+                        .read(ctx)
+                        .active_item()
+                        .unwrap()
+                        .entry_id(ctx.as_ref()),
+                    Some(file2.clone())
+                );
+            });
+
+            // Open the third entry twice concurrently. Two pane items
+            // are added.
+            let (t1, t2) = workspace.update(&mut app, |w, ctx| {
+                (
+                    w.open_entry(file3.clone(), ctx).unwrap(),
+                    w.open_entry(file3.clone(), ctx).unwrap(),
+                )
+            });
+            t1.await;
+            t2.await;
             app.read(|ctx| {
-                let pane = pane.read(ctx);
+                let pane = workspace.read(ctx).active_pane().read(ctx);
                 assert_eq!(
                     pane.active_item().unwrap().entry_id(ctx),
                     Some(file3.clone())
                 );
-                assert_eq!(pane.items().len(), 3);
+                let pane_entries = pane
+                    .items()
+                    .iter()
+                    .map(|i| i.entry_id(ctx).unwrap())
+                    .collect::<Vec<_>>();
+                assert_eq!(pane_entries, &[file1, file2, file3.clone(), file3]);
             });
         });
     }
@@ -1008,19 +1071,13 @@ mod tests {
 
             // 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;
+            workspace.update(&mut app, |workspace, ctx| {
+                workspace.open_new_file(&(), ctx);
+                workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, ctx);
+                assert!(workspace
+                    .open_entry((worktree.id(), Path::new("the-new-name").into()), ctx)
+                    .is_none());
+            });
             let editor2 = workspace.update(&mut app, |workspace, ctx| {
                 workspace
                     .active_item(ctx)
@@ -1030,7 +1087,7 @@ mod tests {
                     .unwrap()
             });
             app.read(|ctx| {
-                assert_eq!(editor.read(ctx).buffer(), editor2.read(ctx).buffer());
+                assert_eq!(editor2.read(ctx).buffer(), editor.read(ctx).buffer());
             })
         });
     }

zed/src/worktree.rs 🔗

@@ -9,7 +9,7 @@ use crate::{
 use ::ignore::gitignore::Gitignore;
 use anyhow::{Context, Result};
 pub use fuzzy::{match_paths, PathMatch};
-use gpui::{scoped_pool, AppContext, Entity, ModelContext, ModelHandle, Task, View, ViewContext};
+use gpui::{scoped_pool, AppContext, Entity, ModelContext, ModelHandle, Task};
 use lazy_static::lazy_static;
 use parking_lot::Mutex;
 use postage::{
@@ -53,7 +53,7 @@ pub struct Worktree {
     poll_scheduled: bool,
 }
 
-#[derive(Clone)]
+#[derive(Clone, Debug)]
 pub struct FileHandle {
     worktree: ModelHandle<Worktree>,
     state: Arc<Mutex<FileHandleState>>,
@@ -191,18 +191,17 @@ impl Worktree {
         path: &Path,
         content: BufferSnapshot,
         ctx: &AppContext,
-    ) -> Task<Result<u64>> {
+    ) -> Task<Result<()>> {
         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(metadata.ino())
+            Ok(())
         })
     }
 }
@@ -415,7 +414,7 @@ impl FileHandle {
         self.worktree.read(ctx).load_history(&self.path(), ctx)
     }
 
-    pub fn save<'a>(&self, content: BufferSnapshot, ctx: &AppContext) -> Task<Result<u64>> {
+    pub fn save<'a>(&self, content: BufferSnapshot, ctx: &AppContext) -> Task<Result<()>> {
         let worktree = self.worktree.read(ctx);
         worktree.save(&self.path(), content, ctx)
     }
@@ -428,14 +427,14 @@ impl FileHandle {
         (self.worktree.id(), self.path())
     }
 
-    pub fn observe_from_view<T: View>(
+    pub fn observe_from_model<T: Entity>(
         &self,
-        ctx: &mut ViewContext<T>,
-        mut callback: impl FnMut(&mut T, FileHandle, &mut ViewContext<T>) + 'static,
+        ctx: &mut ModelContext<T>,
+        mut callback: impl FnMut(&mut T, FileHandle, &mut ModelContext<T>) + 'static,
     ) {
         let mut prev_state = self.state.lock().clone();
         let cur_state = Arc::downgrade(&self.state);
-        ctx.observe_model(&self.worktree, move |observer, worktree, ctx| {
+        ctx.observe(&self.worktree, move |observer, worktree, ctx| {
             if let Some(cur_state) = cur_state.upgrade() {
                 let cur_state_unlocked = cur_state.lock();
                 if *cur_state_unlocked != prev_state {
@@ -1361,7 +1360,8 @@ mod tests {
             app.read(|ctx| tree.read(ctx).scan_complete()).await;
             app.read(|ctx| assert_eq!(tree.read(ctx).file_count(), 1));
 
-            let buffer = app.add_model(|_| Buffer::new(1, "a line of text.\n".repeat(10 * 1024)));
+            let buffer =
+                app.add_model(|ctx| Buffer::new(1, "a line of text.\n".repeat(10 * 1024), ctx));
 
             let path = tree.update(&mut app, |tree, ctx| {
                 let path = tree.files(0).next().unwrap().path().clone();