diff --git a/zed/src/editor/buffer/mod.rs b/zed/src/editor/buffer/mod.rs index b1cf5a09575bd64f20a04521e9c9d277f5effed2..6d87dd588019ca5db889a6cff883830216f9db8d 100644 --- a/zed/src/editor/buffer/mod.rs +++ b/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, selections: HashMap>, pub selections_last_update: SelectionsVersion, deferred_ops: OperationQueue, @@ -351,15 +353,33 @@ pub struct UndoOperation { } impl Buffer { - pub fn new>>(replica_id: ReplicaId, base_text: T) -> Self { - Self::build(replica_id, History::new(base_text.into())) + pub fn new>>( + replica_id: ReplicaId, + base_text: T, + ctx: &mut ModelContext, + ) -> 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, + ctx: &mut ModelContext, + ) -> 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, + ctx: &mut ModelContext, + ) -> 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, ctx: &mut ModelContext, - ) -> LocalBoxFuture<'static, Result> { + ) -> 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) { + fn did_save( + &mut self, + version: time::Global, + file: Option, + ctx: &mut ModelContext, + ) { + 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::(); - 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::(), "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); diff --git a/zed/src/editor/buffer_view.rs b/zed/src/editor/buffer_view.rs index 15ce63120ef6832966d3bbbf15c77ff8286a1aee..4fc58fb67324abf9d763efcc7709b6c0360f919a 100644 --- a/zed/src/editor/buffer_view.rs +++ b/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, buffer: ModelHandle, - file: Option, display_map: ModelHandle, selection_set_id: SelectionSetId, pending_selection: Option, @@ -275,24 +274,19 @@ struct ClipboardSelection { impl BufferView { pub fn single_line(settings: watch::Receiver, ctx: &mut ViewContext) -> 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, - file: Option, settings: watch::Receiver, ctx: &mut ViewContext, ) -> 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, + settings: watch::Receiver, + ctx: &mut ViewContext, + ) -> 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)> { - self.file.as_ref().map(|file| file.entry_id()) + fn entry_id(&self, ctx: &AppContext) -> Option<(usize, Arc)> { + self.buffer.read(ctx).file().map(|file| file.entry_id()) } fn clone_on_split(&self, ctx: &mut ViewContext) -> Option 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, ctx: &mut ViewContext, - ) -> LocalBoxFuture<'static, Result> { - 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()), diff --git a/zed/src/editor/display_map/fold_map.rs b/zed/src/editor/display_map/fold_map.rs index 8a105f8af39d76ccebce43558ab9273ba50ae906..98c47ff08a84ea1a79ded4a0edf03da7ad0a681a 100644 --- a/zed/src/editor/display_map/fold_map.rs +++ b/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::(); - 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()); diff --git a/zed/src/editor/display_map/mod.rs b/zed/src/editor/display_map/mod.rs index e2c422ad36169ebedc44ef7367003ca1f1af3cdb..b69a70f3979870eb061f72c0cfdff25931165ccf 100644 --- a/zed/src/editor/display_map/mod.rs +++ b/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()), diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index 24b6d8447b3be4593a9f2b688aeb60ffc51c4ec5..1d1cfaf0476908b05fd66de0bf325f674f141b40 100644 --- a/zed/src/workspace.rs +++ b/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, + settings: watch::Receiver, + ctx: &mut ViewContext, + ) -> 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)>; @@ -114,7 +127,7 @@ pub trait ItemView: View { &mut self, _: Option, _: &mut ViewContext, - ) -> LocalBoxFuture<'static, anyhow::Result>; + ) -> 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, + app: &mut MutableAppContext, + ) -> Box; + fn boxed_clone(&self) -> Box; +} + pub trait ItemViewHandle: Send + Sync { fn title(&self, app: &AppContext) -> String; fn entry_id(&self, app: &AppContext) -> Option<(usize, Arc)>; @@ -136,7 +161,30 @@ pub trait ItemViewHandle: Send + Sync { &self, file: Option, ctx: &mut MutableAppContext, - ) -> LocalBoxFuture<'static, anyhow::Result>; + ) -> LocalBoxFuture<'static, anyhow::Result<()>>; +} + +impl ItemHandle for ModelHandle { + 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, + app: &mut MutableAppContext, + ) -> Box { + Box::new(app.add_view(window_id, |ctx| T::build_view(self.clone(), settings, ctx))) + } + + fn boxed_clone(&self) -> Box { + Box::new(self.clone()) + } } impl ItemViewHandle for ViewHandle { @@ -179,7 +227,7 @@ impl ItemViewHandle for ViewHandle { &self, file: Option, ctx: &mut MutableAppContext, - ) -> LocalBoxFuture<'static, anyhow::Result> { + ) -> LocalBoxFuture<'static, anyhow::Result<()>> { self.update(ctx, |item, ctx| item.save(file, ctx)) } @@ -202,6 +250,18 @@ impl Clone for Box { } } +impl Clone for Box { + fn clone(&self) -> Box { + self.boxed_clone() + } +} + +impl fmt::Debug for Box { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "ItemHandle {{id: {}}}", self.id()) + } +} + #[derive(Debug)] pub struct State { pub modal: Option, @@ -214,14 +274,13 @@ pub struct Workspace { center: PaneGroup, panes: Vec>, active_pane: ViewHandle, - loading_entries: HashSet<(usize, Arc)>, replica_id: ReplicaId, worktrees: HashSet>, - buffers: HashMap< - (usize, u64), - postage::watch::Receiver, Arc>>>, + items: Vec>, + loading_items: HashMap< + (usize, Arc), + postage::watch::Receiver, Arc>>>, >, - untitled_buffers: HashMap>, } 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) { - 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), ctx: &mut ViewContext, ) -> Option> { - 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::>(); + 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()); }) }); } diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 00900cdb9d35b0ba6c6f8ca1939868d8e2eb0b51..ba6c37bac1bab31485bec9f98b874f41e8ec6ab8 100644 --- a/zed/src/worktree.rs +++ b/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, state: Arc>, @@ -191,18 +191,17 @@ impl Worktree { path: &Path, content: BufferSnapshot, ctx: &AppContext, - ) -> Task> { + ) -> Task> { 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> { + pub fn save<'a>(&self, content: BufferSnapshot, ctx: &AppContext) -> Task> { 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( + pub fn observe_from_model( &self, - ctx: &mut ViewContext, - mut callback: impl FnMut(&mut T, FileHandle, &mut ViewContext) + 'static, + ctx: &mut ModelContext, + mut callback: impl FnMut(&mut T, FileHandle, &mut ModelContext) + '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();