diff --git a/Cargo.lock b/Cargo.lock index a7fa8a95f7f79083e0f7cbc126f6561305aea5f7..1fba36fd1210a8ddb0427046923b468f11463463 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -449,7 +449,7 @@ dependencies = [ [[package]] name = "cocoa" version = "0.24.0" -source = "git+https://github.com/servo/core-foundation-rs?rev=e9a65bb15d591ec22649e03659db8095d4f2dd60#e9a65bb15d591ec22649e03659db8095d4f2dd60" +source = "git+https://github.com/servo/core-foundation-rs?rev=025dcb3c0d1ef01530f57ef65f3b1deb948f5737#025dcb3c0d1ef01530f57ef65f3b1deb948f5737" dependencies = [ "bitflags 1.2.1", "block", @@ -464,7 +464,7 @@ dependencies = [ [[package]] name = "cocoa-foundation" version = "0.1.0" -source = "git+https://github.com/servo/core-foundation-rs?rev=e9a65bb15d591ec22649e03659db8095d4f2dd60#e9a65bb15d591ec22649e03659db8095d4f2dd60" +source = "git+https://github.com/servo/core-foundation-rs?rev=025dcb3c0d1ef01530f57ef65f3b1deb948f5737#025dcb3c0d1ef01530f57ef65f3b1deb948f5737" dependencies = [ "bitflags 1.2.1", "block", @@ -499,7 +499,7 @@ checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" [[package]] name = "core-foundation" version = "0.9.1" -source = "git+https://github.com/servo/core-foundation-rs?rev=e9a65bb15d591ec22649e03659db8095d4f2dd60#e9a65bb15d591ec22649e03659db8095d4f2dd60" +source = "git+https://github.com/servo/core-foundation-rs?rev=025dcb3c0d1ef01530f57ef65f3b1deb948f5737#025dcb3c0d1ef01530f57ef65f3b1deb948f5737" dependencies = [ "core-foundation-sys", "libc", @@ -508,12 +508,12 @@ dependencies = [ [[package]] name = "core-foundation-sys" version = "0.8.2" -source = "git+https://github.com/servo/core-foundation-rs?rev=e9a65bb15d591ec22649e03659db8095d4f2dd60#e9a65bb15d591ec22649e03659db8095d4f2dd60" +source = "git+https://github.com/servo/core-foundation-rs?rev=025dcb3c0d1ef01530f57ef65f3b1deb948f5737#025dcb3c0d1ef01530f57ef65f3b1deb948f5737" [[package]] name = "core-graphics" version = "0.22.2" -source = "git+https://github.com/servo/core-foundation-rs?rev=e9a65bb15d591ec22649e03659db8095d4f2dd60#e9a65bb15d591ec22649e03659db8095d4f2dd60" +source = "git+https://github.com/servo/core-foundation-rs?rev=025dcb3c0d1ef01530f57ef65f3b1deb948f5737#025dcb3c0d1ef01530f57ef65f3b1deb948f5737" dependencies = [ "bitflags 1.2.1", "core-foundation", @@ -525,7 +525,7 @@ dependencies = [ [[package]] name = "core-graphics-types" version = "0.1.1" -source = "git+https://github.com/servo/core-foundation-rs?rev=e9a65bb15d591ec22649e03659db8095d4f2dd60#e9a65bb15d591ec22649e03659db8095d4f2dd60" +source = "git+https://github.com/servo/core-foundation-rs?rev=025dcb3c0d1ef01530f57ef65f3b1deb948f5737#025dcb3c0d1ef01530f57ef65f3b1deb948f5737" dependencies = [ "bitflags 1.2.1", "core-foundation", diff --git a/Cargo.toml b/Cargo.toml index 7dcf616a67dec6e21da7f96b35b1e87882cd0403..bf2d5c814372d3fa4184b285cc78e0ad3dfa42d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,11 +4,11 @@ members = ["zed", "gpui", "fsevent", "scoped_pool"] [patch.crates-io] async-task = {git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e"} -# TODO - Remove when a version is released with this PR: https://github.com/servo/core-foundation-rs/pull/454 -cocoa = {git = "https://github.com/servo/core-foundation-rs", rev = "e9a65bb15d591ec22649e03659db8095d4f2dd60"} -cocoa-foundation = {git = "https://github.com/servo/core-foundation-rs", rev = "e9a65bb15d591ec22649e03659db8095d4f2dd60"} -core-foundation = {git = "https://github.com/servo/core-foundation-rs", rev = "e9a65bb15d591ec22649e03659db8095d4f2dd60"} -core-graphics = {git = "https://github.com/servo/core-foundation-rs", rev = "e9a65bb15d591ec22649e03659db8095d4f2dd60"} +# TODO - Remove when a version is released with this PR: https://github.com/servo/core-foundation-rs/pull/457 +cocoa = {git = "https://github.com/servo/core-foundation-rs", rev = "025dcb3c0d1ef01530f57ef65f3b1deb948f5737"} +cocoa-foundation = {git = "https://github.com/servo/core-foundation-rs", rev = "025dcb3c0d1ef01530f57ef65f3b1deb948f5737"} +core-foundation = {git = "https://github.com/servo/core-foundation-rs", rev = "025dcb3c0d1ef01530f57ef65f3b1deb948f5737"} +core-graphics = {git = "https://github.com/servo/core-foundation-rs", rev = "025dcb3c0d1ef01530f57ef65f3b1deb948f5737"} [profile.dev] split-debuginfo = "unpacked" diff --git a/gpui/src/app.rs b/gpui/src/app.rs index ac4c4e69b1782a416f5058025db0eafb974b7e7a..2fe6af421230e27b67d83449c1e18a7562960745 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -12,16 +12,16 @@ use keymap::MatchResult; use parking_lot::{Mutex, RwLock}; use pathfinder_geometry::{rect::RectF, vector::vec2f}; use platform::Event; -use postage::{sink::Sink as _, stream::Stream as _}; +use postage::{mpsc, sink::Sink as _, stream::Stream as _}; use smol::prelude::*; use std::{ any::{type_name, Any, TypeId}, cell::RefCell, - collections::{hash_map::Entry, HashMap, HashSet, VecDeque}, + collections::{HashMap, HashSet, VecDeque}, fmt::{self, Debug}, hash::{Hash, Hasher}, marker::PhantomData, - path::PathBuf, + path::{Path, PathBuf}, rc::{self, Rc}, sync::{Arc, Weak}, time::Duration, @@ -87,7 +87,7 @@ pub enum MenuItem<'a> { pub struct App(Rc>); #[derive(Clone)] -pub struct TestAppContext(Rc>); +pub struct TestAppContext(Rc>, Rc); impl App { pub fn test T>( @@ -111,13 +111,16 @@ impl App { Fn: FnOnce(TestAppContext) -> F, F: Future, { - let platform = platform::test::platform(); + let platform = Rc::new(platform::test::platform()); let foreground = Rc::new(executor::Foreground::test()); - let ctx = TestAppContext(Rc::new(RefCell::new(MutableAppContext::new( - foreground.clone(), - Rc::new(platform), - asset_source, - )))); + let ctx = TestAppContext( + Rc::new(RefCell::new(MutableAppContext::new( + foreground.clone(), + platform.clone(), + asset_source, + ))), + platform, + ); ctx.0.borrow_mut().weak_self = Some(Rc::downgrade(&ctx.0)); let future = f(ctx); @@ -332,6 +335,14 @@ impl TestAppContext { pub fn platform(&self) -> Rc { self.0.borrow().platform.clone() } + + pub fn simulate_new_path_selection(&self, result: impl FnOnce(PathBuf) -> Option) { + self.1.as_ref().simulate_new_path_selection(result); + } + + pub fn did_prompt_for_new_path(&self) -> bool { + self.1.as_ref().did_prompt_for_new_path() + } } impl UpdateModel for TestAppContext { @@ -381,7 +392,6 @@ pub struct MutableAppContext { subscriptions: HashMap>, model_observations: HashMap>, view_observations: HashMap>, - async_observations: HashMap>, window_invalidations: HashMap, presenters_and_platform_windows: HashMap>, Box)>, @@ -423,7 +433,6 @@ impl MutableAppContext { subscriptions: HashMap::new(), model_observations: HashMap::new(), view_observations: HashMap::new(), - async_observations: HashMap::new(), window_invalidations: HashMap::new(), presenters_and_platform_windows: HashMap::new(), debug_elements_callbacks: HashMap::new(), @@ -586,6 +595,22 @@ impl MutableAppContext { ); } + pub fn prompt_for_new_path(&self, directory: &Path, done_fn: F) + where + F: 'static + FnOnce(Option, &mut MutableAppContext), + { + let app = self.weak_self.as_ref().unwrap().upgrade().unwrap(); + let foreground = self.foreground.clone(); + self.platform().prompt_for_new_path( + directory, + Box::new(move |path| { + foreground + .spawn(async move { (done_fn)(path, &mut *app.borrow_mut()) }) + .detach(); + }), + ); + } + pub(crate) fn notify_view(&mut self, window_id: usize, view_id: usize) { self.pending_effects .push_back(Effect::ViewNotification { window_id, view_id }); @@ -874,13 +899,11 @@ impl MutableAppContext { self.ctx.models.remove(&model_id); self.subscriptions.remove(&model_id); self.model_observations.remove(&model_id); - self.async_observations.remove(&model_id); } for (window_id, view_id) in dropped_views { self.subscriptions.remove(&view_id); self.model_observations.remove(&view_id); - self.async_observations.remove(&view_id); if let Some(window) = self.ctx.windows.get_mut(&window_id) { self.window_invalidations .entry(window_id) @@ -1059,12 +1082,6 @@ impl MutableAppContext { } } } - - if let Entry::Occupied(mut entry) = self.async_observations.entry(observed_id) { - if entry.get_mut().blocking_send(()).is_err() { - entry.remove_entry(); - } - } } fn notify_view_observers(&mut self, window_id: usize, view_id: usize) { @@ -1075,7 +1092,12 @@ impl MutableAppContext { .insert(view_id); if let Some(observations) = self.view_observations.remove(&view_id) { - if self.ctx.models.contains_key(&view_id) { + if self + .ctx + .windows + .get(&window_id) + .map_or(false, |w| w.views.contains_key(&view_id)) + { for mut observation in observations { let alive = if let Some(mut view) = self .ctx @@ -1111,12 +1133,6 @@ impl MutableAppContext { } } } - - if let Entry::Occupied(mut entry) = self.async_observations.entry(view_id) { - if entry.get_mut().blocking_send(()).is_err() { - entry.remove_entry(); - } - } } fn focus(&mut self, window_id: usize, focused_id: usize) { @@ -1757,6 +1773,10 @@ impl<'a, T: View> ViewContext<'a, T> { self.window_id } + pub fn view_id(&self) -> usize { + self.view_id + } + pub fn foreground(&self) -> &Rc { self.app.foreground_executor() } @@ -1765,6 +1785,20 @@ impl<'a, T: View> ViewContext<'a, T> { &self.app.ctx.background } + pub fn prompt_for_paths(&self, options: PathPromptOptions, done_fn: F) + where + F: 'static + FnOnce(Option>, &mut MutableAppContext), + { + self.app.prompt_for_paths(options, done_fn) + } + + pub fn prompt_for_new_path(&self, directory: &Path, done_fn: F) + where + F: 'static + FnOnce(Option, &mut MutableAppContext), + { + self.app.prompt_for_new_path(directory, done_fn) + } + pub fn debug_elements(&self) -> crate::json::Value { self.app.debug_elements(self.window_id).unwrap() } @@ -1818,22 +1852,11 @@ impl<'a, T: View> ViewContext<'a, T> { F: 'static + FnMut(&mut T, ModelHandle, &E::Event, &mut ViewContext), { let emitter_handle = handle.downgrade(); - self.app - .subscriptions - .entry(handle.id()) - .or_default() - .push(Subscription::FromView { - window_id: self.window_id, - view_id: self.view_id, - callback: Box::new(move |view, payload, app, window_id, view_id| { - if let Some(emitter_handle) = emitter_handle.upgrade(app.as_ref()) { - let model = view.downcast_mut().expect("downcast is type safe"); - let payload = payload.downcast_ref().expect("downcast is type safe"); - let mut ctx = ViewContext::new(app, window_id, view_id); - callback(model, emitter_handle, payload, &mut ctx); - } - }), - }); + self.subscribe(handle, move |model, payload, ctx| { + if let Some(emitter_handle) = emitter_handle.upgrade(ctx.as_ref()) { + callback(model, emitter_handle, payload, ctx); + } + }); } pub fn subscribe_to_view(&mut self, handle: &ViewHandle, mut callback: F) @@ -1843,7 +1866,19 @@ impl<'a, T: View> ViewContext<'a, T> { F: 'static + FnMut(&mut T, ViewHandle, &V::Event, &mut ViewContext), { let emitter_handle = handle.downgrade(); + self.subscribe(handle, move |view, payload, ctx| { + if let Some(emitter_handle) = emitter_handle.upgrade(ctx.as_ref()) { + callback(view, emitter_handle, payload, ctx); + } + }); + } + pub fn subscribe(&mut self, handle: &impl Handle, mut callback: F) + where + E: Entity, + E::Event: 'static, + F: 'static + FnMut(&mut T, &E::Event, &mut ViewContext), + { self.app .subscriptions .entry(handle.id()) @@ -1851,13 +1886,11 @@ impl<'a, T: View> ViewContext<'a, T> { .push(Subscription::FromView { window_id: self.window_id, view_id: self.view_id, - callback: Box::new(move |view, payload, app, window_id, view_id| { - if let Some(emitter_handle) = emitter_handle.upgrade(&app) { - let model = view.downcast_mut().expect("downcast is type safe"); - let payload = payload.downcast_ref().expect("downcast is type safe"); - let mut ctx = ViewContext::new(app, window_id, view_id); - callback(model, emitter_handle, payload, &mut ctx); - } + callback: Box::new(move |entity, payload, app, window_id, view_id| { + let entity = entity.downcast_mut().expect("downcast is type safe"); + let payload = payload.downcast_ref().expect("downcast is type safe"); + let mut ctx = ViewContext::new(app, window_id, view_id); + callback(entity, payload, &mut ctx); }), }); } @@ -2067,7 +2100,7 @@ impl ModelHandle { } } - fn downgrade(&self) -> WeakModelHandle { + pub fn downgrade(&self) -> WeakModelHandle { WeakModelHandle::new(self.model_id) } @@ -2101,12 +2134,24 @@ impl ModelHandle { ctx: &TestAppContext, mut predicate: impl FnMut(&T, &AppContext) -> bool, ) -> impl Future { + let (tx, mut rx) = mpsc::channel(1024); + let mut ctx = ctx.0.borrow_mut(); - let tx = ctx - .async_observations - .entry(self.id()) - .or_insert_with(|| postage::broadcast::channel(128).0); - let mut rx = tx.subscribe(); + self.update(&mut *ctx, |_, ctx| { + ctx.observe(self, { + let mut tx = tx.clone(); + move |_, _, _| { + tx.blocking_send(()).ok(); + } + }); + ctx.subscribe(self, { + let mut tx = tx.clone(); + move |_, _, _| { + tx.blocking_send(()).ok(); + } + }) + }); + let ctx = ctx.weak_self.as_ref().unwrap().upgrade().unwrap(); let handle = self.downgrade(); @@ -2223,6 +2268,15 @@ impl WeakModelHandle { } } +impl Clone for WeakModelHandle { + fn clone(&self) -> Self { + Self { + model_id: self.model_id, + model_type: PhantomData, + } + } +} + pub struct ViewHandle { window_id: usize, view_id: usize, @@ -2273,19 +2327,41 @@ impl ViewHandle { pub fn condition( &self, ctx: &TestAppContext, - mut predicate: impl 'static + FnMut(&T, &AppContext) -> bool, - ) -> impl 'static + Future { + predicate: impl FnMut(&T, &AppContext) -> bool, + ) -> impl Future { + self.condition_with_duration(Duration::from_millis(500), ctx, predicate) + } + + pub fn condition_with_duration( + &self, + duration: Duration, + ctx: &TestAppContext, + mut predicate: impl FnMut(&T, &AppContext) -> bool, + ) -> impl Future { + let (tx, mut rx) = mpsc::channel(1024); + let mut ctx = ctx.0.borrow_mut(); - let tx = ctx - .async_observations - .entry(self.id()) - .or_insert_with(|| postage::broadcast::channel(128).0); - let mut rx = tx.subscribe(); + self.update(&mut *ctx, |_, ctx| { + ctx.observe_view(self, { + let mut tx = tx.clone(); + move |_, _, _| { + tx.blocking_send(()).ok(); + } + }); + + ctx.subscribe(self, { + let mut tx = tx.clone(); + move |_, _, _| { + tx.blocking_send(()).ok(); + } + }) + }); + let ctx = ctx.weak_self.as_ref().unwrap().upgrade().unwrap(); let handle = self.downgrade(); async move { - timeout(Duration::from_millis(200), async move { + timeout(duration, async move { loop { { let ctx = ctx.borrow(); @@ -2293,7 +2369,7 @@ impl ViewHandle { if predicate( handle .upgrade(ctx) - .expect("model dropped with pending condition") + .expect("view dropped with pending condition") .read(ctx), ctx, ) { @@ -2303,7 +2379,7 @@ impl ViewHandle { rx.recv() .await - .expect("model dropped with pending condition"); + .expect("view dropped with pending condition"); } }) .await @@ -3500,9 +3576,7 @@ mod tests { model.update(&mut app, |model, ctx| model.inc(ctx)); assert_eq!(poll_once(&mut condition2).await, Some(())); - // Broadcast channel should be removed if no conditions remain on next notification. model.update(&mut app, |_, ctx| ctx.notify()); - app.update(|ctx| assert!(ctx.async_observations.get(&model.id()).is_none())); }); } @@ -3580,10 +3654,7 @@ mod tests { view.update(&mut app, |view, ctx| view.inc(ctx)); assert_eq!(poll_once(&mut condition2).await, Some(())); - - // Broadcast channel should be removed if no conditions remain on next notification. view.update(&mut app, |_, ctx| ctx.notify()); - app.update(|ctx| assert!(ctx.async_observations.get(&view.id()).is_none())); }); } @@ -3613,7 +3684,7 @@ mod tests { } #[test] - #[should_panic(expected = "model dropped with pending condition")] + #[should_panic(expected = "view dropped with pending condition")] fn test_view_condition_panic_on_drop() { struct View; diff --git a/gpui/src/platform/mac/platform.rs b/gpui/src/platform/mac/platform.rs index e9bf34d684373fb695c3340ae731ca43e73bbf26..29f75ed3ac81d5fc5a4d8342d5e7b08d0b54c435 100644 --- a/gpui/src/platform/mac/platform.rs +++ b/gpui/src/platform/mac/platform.rs @@ -5,7 +5,7 @@ use cocoa::{ appkit::{ NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular, NSEventModifierFlags, NSMenu, NSMenuItem, NSModalResponse, NSOpenPanel, NSPasteboard, - NSPasteboardTypeString, NSWindow, + NSPasteboardTypeString, NSSavePanel, NSWindow, }, base::{id, nil, selector}, foundation::{NSArray, NSAutoreleasePool, NSData, NSInteger, NSString, NSURL}, @@ -25,7 +25,7 @@ use std::{ convert::TryInto, ffi::{c_void, CStr}, os::raw::c_char, - path::PathBuf, + path::{Path, PathBuf}, ptr, rc::Rc, slice, str, @@ -305,6 +305,43 @@ impl platform::Platform for MacPlatform { } } + fn prompt_for_new_path( + &self, + directory: &Path, + done_fn: Box)>, + ) { + unsafe { + let panel = NSSavePanel::savePanel(nil); + let path = ns_string(directory.to_string_lossy().as_ref()); + let url = NSURL::fileURLWithPath_isDirectory_(nil, path, true.to_objc()); + panel.setDirectoryURL(url); + + let done_fn = Cell::new(Some(done_fn)); + let block = ConcreteBlock::new(move |response: NSModalResponse| { + let result = if response == NSModalResponse::NSModalResponseOk { + let url = panel.URL(); + let string = url.absoluteString(); + let string = std::ffi::CStr::from_ptr(string.UTF8String()) + .to_string_lossy() + .to_string(); + if let Some(path) = string.strip_prefix("file://") { + Some(PathBuf::from(path)) + } else { + None + } + } else { + None + }; + + if let Some(done_fn) = done_fn.take() { + (done_fn)(result); + } + }); + let block = block.copy(); + let _: () = msg_send![panel, beginWithCompletionHandler: block]; + } + } + fn fonts(&self) -> Arc { self.fonts.clone() } diff --git a/gpui/src/platform/mod.rs b/gpui/src/platform/mod.rs index b98d3a687bcd35a2dc22a4ca6cff600d5fea87ea..e5e81c424e729f3214f8af0b0ca07126d16e0e20 100644 --- a/gpui/src/platform/mod.rs +++ b/gpui/src/platform/mod.rs @@ -19,7 +19,13 @@ use crate::{ }; use async_task::Runnable; pub use event::Event; -use std::{any::Any, ops::Range, path::PathBuf, rc::Rc, sync::Arc}; +use std::{ + any::Any, + ops::Range, + path::{Path, PathBuf}, + rc::Rc, + sync::Arc, +}; pub trait Platform { fn on_menu_command(&self, callback: Box)>); @@ -45,6 +51,11 @@ pub trait Platform { options: PathPromptOptions, done_fn: Box>)>, ); + fn prompt_for_new_path( + &self, + directory: &Path, + done_fn: Box)>, + ); fn quit(&self); fn write_to_clipboard(&self, item: ClipboardItem); fn read_from_clipboard(&self) -> Option; diff --git a/gpui/src/platform/test.rs b/gpui/src/platform/test.rs index 878449a0216e9a0a95447d9abaef16e089f8672c..c0e1057409cb8474d8c59be018f402a9bd9e7b50 100644 --- a/gpui/src/platform/test.rs +++ b/gpui/src/platform/test.rs @@ -1,11 +1,18 @@ use crate::ClipboardItem; use pathfinder_geometry::vector::Vector2F; -use std::{any::Any, cell::RefCell, rc::Rc, sync::Arc}; - -struct Platform { +use std::{ + any::Any, + cell::RefCell, + path::{Path, PathBuf}, + rc::Rc, + sync::Arc, +}; + +pub(crate) struct Platform { dispatcher: Arc, fonts: Arc, current_clipboard_item: RefCell>, + last_prompt_for_new_path_args: RefCell)>)>>, } struct Dispatcher; @@ -23,9 +30,25 @@ impl Platform { Self { dispatcher: Arc::new(Dispatcher), fonts: Arc::new(super::current::FontSystem::new()), - current_clipboard_item: RefCell::new(None), + current_clipboard_item: Default::default(), + last_prompt_for_new_path_args: Default::default(), } } + + pub(crate) fn simulate_new_path_selection( + &self, + result: impl FnOnce(PathBuf) -> Option, + ) { + let (dir_path, callback) = self + .last_prompt_for_new_path_args + .take() + .expect("prompt_for_new_path was not called"); + callback(result(dir_path)); + } + + pub(crate) fn did_prompt_for_new_path(&self) -> bool { + self.last_prompt_for_new_path_args.borrow().is_some() + } } impl super::Platform for Platform { @@ -77,6 +100,10 @@ impl super::Platform for Platform { ) { } + fn prompt_for_new_path(&self, path: &Path, f: Box)>) { + *self.last_prompt_for_new_path_args.borrow_mut() = Some((path.to_path_buf(), f)); + } + fn write_to_clipboard(&self, item: ClipboardItem) { *self.current_clipboard_item.borrow_mut() = Some(item); } @@ -132,6 +159,6 @@ impl super::Window for Window { } } -pub fn platform() -> impl super::Platform { +pub(crate) fn platform() -> Platform { Platform::new() } diff --git a/zed/src/editor/buffer/mod.rs b/zed/src/editor/buffer/mod.rs index 84c47ed6d7611810c7bcc9e4da3360cb943e3132..e4a06bfda95f90b5dcf3eb4f6d0a940f4e7886f3 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(); @@ -425,6 +445,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(), @@ -441,24 +462,40 @@ 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<()>> { 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, + ) { + if file.is_some() { + self.file = file; + } self.saved_version = version; ctx.emit(Event::Saved); } @@ -1783,6 +1820,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(), @@ -2346,8 +2384,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"); @@ -2371,8 +2409,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(); @@ -2435,8 +2473,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); @@ -2521,8 +2559,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(); @@ -2543,8 +2581,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); @@ -2564,8 +2602,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), @@ -2595,8 +2633,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(); @@ -2618,7 +2656,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(); @@ -2747,8 +2785,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(); @@ -2912,8 +2950,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(); @@ -2940,7 +2978,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. @@ -2963,7 +3001,7 @@ mod tests { assert_eq!(*events.borrow(), &[Event::Edited, Event::Dirtied]); 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. @@ -3002,8 +3040,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(); @@ -3039,9 +3077,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); @@ -3125,7 +3163,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 18e7a4d0c00ea78afe1850b149824d05190039d7..605744b37a2987e5ebde032a80842b1809668fcb 100644 --- a/zed/src/editor/buffer_view.rs +++ b/zed/src/editor/buffer_view.rs @@ -6,11 +6,10 @@ use crate::{settings::Settings, watch, workspace, worktree::FileHandle}; use anyhow::Result; use futures_core::future::LocalBoxFuture; use gpui::{ - fonts::Properties as FontProperties, keymap::Binding, text_layout, AppContext, ClipboardItem, - Element, ElementBox, Entity, FontCache, ModelHandle, MutableAppContext, View, ViewContext, - WeakViewHandle, + fonts::Properties as FontProperties, geometry::vector::Vector2F, keymap::Binding, text_layout, + AppContext, ClipboardItem, Element, ElementBox, Entity, FontCache, ModelHandle, + MutableAppContext, TextLayoutCache, View, ViewContext, WeakViewHandle, }; -use gpui::{geometry::vector::Vector2F, TextLayoutCache}; use parking_lot::Mutex; use serde::{Deserialize, Serialize}; use smallvec::SmallVec; @@ -265,7 +264,6 @@ pub enum SelectAction { pub struct BufferView { handle: WeakViewHandle, buffer: ModelHandle, - file: Option, display_map: DisplayMap, selection_set_id: SelectionSetId, pending_selection: Option, @@ -287,24 +285,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 = DisplayMap::new( @@ -327,7 +320,6 @@ impl BufferView { Self { handle: ctx.handle().downgrade(), buffer, - file, display_map, selection_set_id, pending_selection: None, @@ -2251,6 +2243,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) @@ -2264,7 +2272,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 { @@ -2272,31 +2284,25 @@ 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) } - fn save(&self, ctx: &mut ViewContext) -> LocalBoxFuture<'static, Result<()>> { - if let Some(file) = self.file.as_ref() { - self.buffer - .update(ctx, |buffer, ctx| buffer.save(file, ctx)) - } else { - Box::pin(async { Ok(()) }) - } + fn save( + &mut self, + new_file: Option, + ctx: &mut ViewContext, + ) -> LocalBoxFuture<'static, Result<()>> { + self.buffer.update(ctx, |b, ctx| b.save(new_file, ctx)) } fn is_dirty(&self, ctx: &AppContext) -> bool { @@ -2314,10 +2320,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); @@ -2428,11 +2435,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) @@ -2445,7 +2452,7 @@ mod tests { #[test] fn test_fold() { App::test((), |app| { - let buffer = app.add_model(|_| { + let buffer = app.add_model(|ctx| { Buffer::new( 0, " @@ -2466,11 +2473,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( @@ -2539,10 +2547,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 @@ -2617,10 +2625,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( &[ @@ -2746,11 +2753,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( &[ @@ -2929,12 +2935,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( @@ -2962,12 +2972,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( @@ -2996,9 +3010,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( &[ @@ -3021,9 +3034,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)], @@ -3044,9 +3056,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( &[ @@ -3075,9 +3086,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( &[ @@ -3107,9 +3117,8 @@ mod tests { fn test_move_line_up_down() { App::test((), |app| { let settings = settings::channel(&app.font_cache()).unwrap().1; - let buffer = app.add_model(|_| Buffer::new(0, sample_text(10, 5))); - let (_, view) = - app.add_window(|ctx| BufferView::for_buffer(buffer, None, settings, ctx)); + let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(10, 5), ctx)); + let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); view.update(app, |view, ctx| { view.fold_ranges( vec![ @@ -3200,10 +3209,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. @@ -3341,10 +3350,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()), @@ -3357,9 +3365,8 @@ mod tests { fn test_select_line() { App::test((), |app| { let settings = settings::channel(&app.font_cache()).unwrap().1; - let buffer = app.add_model(|_| Buffer::new(0, sample_text(6, 5))); - let (_, view) = - app.add_window(|ctx| BufferView::for_buffer(buffer, None, settings, ctx)); + let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(6, 5), ctx)); + let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); view.update(app, |view, ctx| { view.select_display_ranges( &[ @@ -3402,9 +3409,8 @@ mod tests { fn test_split_selection_into_lines() { App::test((), |app| { let settings = settings::channel(&app.font_cache()).unwrap().1; - let buffer = app.add_model(|_| Buffer::new(0, sample_text(9, 5))); - let (_, view) = - app.add_window(|ctx| BufferView::for_buffer(buffer, None, settings, ctx)); + let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(9, 5), ctx)); + let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); view.update(app, |view, ctx| { view.fold_ranges( vec![ diff --git a/zed/src/editor/display_map/fold_map.rs b/zed/src/editor/display_map/fold_map.rs index a14a0a5bbb26b092b680800c62a01f1833023cbc..e7f538c686e2a60bc8c9f06e4f9f111609ed9171 100644 --- a/zed/src/editor/display_map/fold_map.rs +++ b/zed/src/editor/display_map/fold_map.rs @@ -676,7 +676,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( @@ -721,7 +721,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()); @@ -764,7 +764,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![ @@ -783,7 +783,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( @@ -808,7 +808,7 @@ mod tests { #[test] fn test_folds_in_range() { 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()); let buffer = buffer.read(app); @@ -864,10 +864,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()); @@ -1031,7 +1031,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 aaafd771f9d63cee8027a09821967dda8a92dfcd..97c1c0891b0ee1fa74001c9fd3b8bffed3185298 100644 --- a/zed/src/editor/display_map/mod.rs +++ b/zed/src/editor/display_map/mod.rs @@ -345,7 +345,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 = DisplayMap::new(buffer.clone(), 4, app.as_ref()); buffer .update(app, |buffer, ctx| { @@ -414,7 +414,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 = DisplayMap::new(buffer.clone(), 4, app.as_ref()); assert_eq!(map.max_point(app.as_ref()), DisplayPoint::new(1, 11)) }); diff --git a/zed/src/menus.rs b/zed/src/menus.rs index 08afb1e990c413f2fc89fae3d820d9c1d56dc462..8def5fafbac1ff246734a06819947571bff7db5e 100644 --- a/zed/src/menus.rs +++ b/zed/src/menus.rs @@ -24,12 +24,21 @@ pub fn menus(settings: Receiver) -> Vec> { }, Menu { name: "File", - items: vec![MenuItem::Action { - name: "Open…", - keystroke: Some("cmd-o"), - action: "workspace:open", - arg: Some(Box::new(settings)), - }], + items: vec![ + MenuItem::Action { + name: "New", + keystroke: Some("cmd-n"), + action: "workspace:new_file", + arg: None, + }, + MenuItem::Separator, + MenuItem::Action { + name: "Open…", + keystroke: Some("cmd-o"), + action: "workspace:open", + arg: Some(Box::new(settings)), + }, + ], }, Menu { name: "Edit", diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index 629d23beecce0bdf1e1c59a76cd6fbe465f92f9b..00166073e2a1c99fcac7cf2c27c5511447693427 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -1,43 +1,42 @@ pub mod pane; pub mod pane_group; -pub use pane::*; -pub use pane_group::*; - use crate::{ + editor::{Buffer, BufferView}, settings::Settings, + time::ReplicaId, watch::{self, Receiver}, + worktree::{FileHandle, Worktree, WorktreeHandle}, }; -use gpui::{MutableAppContext, PathPromptOptions}; -use std::path::PathBuf; +use futures_core::{future::LocalBoxFuture, Future}; +use gpui::{ + color::rgbu, elements::*, json::to_string_pretty, keymap::Binding, AnyViewHandle, AppContext, + ClipboardItem, Entity, EntityTask, ModelHandle, MutableAppContext, PathPromptOptions, View, + ViewContext, ViewHandle, WeakModelHandle, +}; +use log::error; +pub use pane::*; +pub use pane_group::*; +use smol::prelude::*; +use std::{collections::HashMap, path::PathBuf}; +use std::{ + collections::{hash_map::Entry, HashSet}, + path::Path, + sync::Arc, +}; + pub fn init(app: &mut MutableAppContext) { app.add_global_action("workspace:open", open); app.add_global_action("workspace:open_paths", open_paths); app.add_global_action("app:quit", quit); app.add_action("workspace:save", Workspace::save_active_item); app.add_action("workspace:debug_elements", Workspace::debug_elements); + app.add_action("workspace:new_file", Workspace::open_new_file); app.add_bindings(vec![ Binding::new("cmd-s", "workspace:save", None), Binding::new("cmd-alt-i", "workspace:debug_elements", None), ]); pane::init(app); } -use crate::{ - editor::{Buffer, BufferView}, - time::ReplicaId, - worktree::{Worktree, WorktreeHandle}, -}; -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, -}; -use log::error; -use smol::prelude::*; -use std::{ - collections::{hash_map::Entry, HashMap, HashSet}, - path::Path, - sync::Arc, -}; pub struct OpenParams { pub paths: Vec, @@ -96,6 +95,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)>; @@ -108,9 +119,11 @@ pub trait ItemView: View { fn is_dirty(&self, _: &AppContext) -> bool { false } - fn save(&self, _: &mut ViewContext) -> LocalBoxFuture<'static, anyhow::Result<()>> { - Box::pin(async { Ok(()) }) - } + fn save( + &mut self, + _: Option, + _: &mut ViewContext, + ) -> LocalBoxFuture<'static, anyhow::Result<()>>; fn should_activate_item_on_event(_: &Self::Event) -> bool { false } @@ -119,6 +132,22 @@ pub trait ItemView: View { } } +pub trait ItemHandle: Send + Sync { + fn boxed_clone(&self) -> Box; + fn downgrade(&self) -> Box; +} + +pub trait WeakItemHandle: Send + Sync { + fn file<'a>(&'a self, ctx: &'a AppContext) -> Option<&'a FileHandle>; + fn add_view( + &self, + window_id: usize, + settings: watch::Receiver, + app: &mut MutableAppContext, + ) -> Option>; + fn alive(&self, ctx: &AppContext) -> bool; +} + pub trait ItemViewHandle: Send + Sync { fn title(&self, app: &AppContext) -> String; fn entry_id(&self, app: &AppContext) -> Option<(usize, Arc)>; @@ -128,7 +157,46 @@ pub trait ItemViewHandle: Send + Sync { fn id(&self) -> usize; fn to_any(&self) -> AnyViewHandle; fn is_dirty(&self, ctx: &AppContext) -> bool; - fn save(&self, ctx: &mut MutableAppContext) -> LocalBoxFuture<'static, anyhow::Result<()>>; + fn save( + &self, + file: Option, + ctx: &mut MutableAppContext, + ) -> LocalBoxFuture<'static, anyhow::Result<()>>; +} + +impl ItemHandle for ModelHandle { + fn boxed_clone(&self) -> Box { + Box::new(self.clone()) + } + + fn downgrade(&self) -> Box { + Box::new(self.downgrade()) + } +} + +impl WeakItemHandle for WeakModelHandle { + fn file<'a>(&'a self, ctx: &'a AppContext) -> Option<&'a FileHandle> { + self.upgrade(ctx).and_then(|h| h.read(ctx).file()) + } + + fn add_view( + &self, + window_id: usize, + settings: Receiver, + ctx: &mut MutableAppContext, + ) -> Option> { + if let Some(handle) = self.upgrade(ctx.as_ref()) { + Some(Box::new(ctx.add_view(window_id, |ctx| { + T::build_view(handle, settings, ctx) + }))) + } else { + None + } + } + + fn alive(&self, ctx: &AppContext) -> bool { + self.upgrade(ctx).is_some() + } } impl ItemViewHandle for ViewHandle { @@ -167,8 +235,12 @@ impl ItemViewHandle for ViewHandle { }) } - fn save(&self, ctx: &mut MutableAppContext) -> LocalBoxFuture<'static, anyhow::Result<()>> { - self.update(ctx, |item, ctx| item.save(ctx)) + fn save( + &self, + file: Option, + ctx: &mut MutableAppContext, + ) -> LocalBoxFuture<'static, anyhow::Result<()>> { + self.update(ctx, |item, ctx| item.save(file, ctx)) } fn is_dirty(&self, ctx: &AppContext) -> bool { @@ -190,6 +262,12 @@ impl Clone for Box { } } +impl Clone for Box { + fn clone(&self) -> Box { + self.boxed_clone() + } +} + #[derive(Debug)] pub struct State { pub modal: Option, @@ -202,12 +280,12 @@ 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>>>, >, } @@ -229,11 +307,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(), + items: Default::default(), + loading_items: Default::default(), } } @@ -272,15 +350,7 @@ impl Workspace { let entries = paths .iter() .cloned() - .map(|path| { - for tree in self.worktrees.iter() { - if let Ok(relative_path) = path.strip_prefix(tree.read(ctx).abs_path()) { - return (tree.id(), relative_path.into()); - } - } - let worktree_id = self.add_worktree(&path, ctx); - (worktree_id, Path::new("").into()) - }) + .map(|path| self.file_for_path(&path, ctx)) .collect::>(); let bg = ctx.background_executor().clone(); @@ -288,12 +358,12 @@ impl Workspace { .iter() .cloned() .zip(entries.into_iter()) - .map(|(path, entry)| { + .map(|(abs_path, file)| { ctx.spawn( - bg.spawn(async move { path.is_file() }), - |me, is_file, ctx| { + bg.spawn(async move { abs_path.is_file() }), + move |me, is_file, ctx| { if is_file { - me.open_entry(entry, ctx) + me.open_entry(file.entry_id(), ctx) } else { None } @@ -310,13 +380,26 @@ impl Workspace { } } - pub fn add_worktree(&mut self, path: &Path, ctx: &mut ViewContext) -> usize { + fn file_for_path(&mut self, abs_path: &Path, ctx: &mut ViewContext) -> FileHandle { + for tree in self.worktrees.iter() { + if let Ok(relative_path) = abs_path.strip_prefix(tree.read(ctx).abs_path()) { + return tree.file(relative_path, ctx.as_ref()); + } + } + let worktree = self.add_worktree(&abs_path, ctx); + worktree.file(Path::new(""), ctx.as_ref()) + } + + pub fn add_worktree( + &mut self, + path: &Path, + ctx: &mut ViewContext, + ) -> ModelHandle { let worktree = ctx.add_model(|ctx| Worktree::new(path, ctx)); - let worktree_id = worktree.id(); ctx.observe_model(&worktree, |_, _, ctx| ctx.notify()); - self.worktrees.insert(worktree); + self.worktrees.insert(worktree.clone()); ctx.notify(); - worktree_id + worktree } pub fn toggle_modal(&mut self, ctx: &mut ViewContext, add_view: F) @@ -346,16 +429,22 @@ impl Workspace { } } + pub fn open_new_file(&mut self, _: &(), ctx: &mut ViewContext) { + 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(ItemHandle::downgrade(&buffer)); + self.add_item_view(Box::new(buffer_view), ctx); + } + #[must_use] pub fn open_entry( &mut self, 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)) @@ -363,6 +452,32 @@ impl Workspace { return None; } + // Otherwise, if this file is already open somewhere in the workspace, + // then add another view for it. + let settings = self.settings.clone(); + let mut view_for_existing_item = None; + self.items.retain(|item| { + if item.alive(ctx.as_ref()) { + if view_for_existing_item.is_none() + && item + .file(ctx.as_ref()) + .map_or(false, |f| f.entry_id() == entry) + { + view_for_existing_item = Some( + item.add_view(ctx.window_id(), settings.clone(), ctx.as_mut()) + .unwrap(), + ); + } + true + } else { + false + } + }); + if let Some(view) = view_for_existing_item { + self.add_item_view(view, ctx); + return None; + } + let (worktree_id, path) = entry.clone(); let worktree = match self.worktrees.get(&worktree_id).cloned() { @@ -373,42 +488,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 = match worktree.file(path.clone(), ctx.as_ref()) { - Some(file) => file, - None => { - log::error!("path {:?} does not exist", path); - return None; - } - }; - - self.loading_entries.insert(entry.clone()); + let file = worktree.file(path.clone(), ctx.as_ref()); + if file.is_deleted() { + log::error!("path {:?} does not exist", path); + return None; + } - 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 { @@ -419,18 +523,15 @@ 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) => { + let weak_item = item.downgrade(); + let view = weak_item + .add_view(ctx.window_id(), settings, ctx.as_mut()) + .unwrap(); + me.items.push(weak_item); + me.add_item_view(view, ctx); } Err(error) => { log::error!("error opening item: {}", error); @@ -440,19 +541,45 @@ impl Workspace { )) } + pub fn active_item(&self, ctx: &ViewContext) -> Option> { + self.active_pane().read(ctx).active_item() + } + pub fn save_active_item(&mut self, _: &(), ctx: &mut ViewContext) { - self.active_pane.update(ctx, |pane, ctx| { - if let Some(item) = pane.active_item() { - let task = item.save(ctx.as_mut()); - ctx.spawn(task, |_, result, _| { - if let Err(e) = result { - // TODO - present this error to the user - error!("failed to save item: {:?}, ", e); + if let Some(item) = self.active_item(ctx) { + if item.entry_id(ctx.as_ref()).is_none() { + let handle = ctx.handle(); + let start_path = self + .worktrees + .iter() + .next() + .map_or(Path::new(""), |h| h.read(ctx).abs_path()) + .to_path_buf(); + ctx.prompt_for_new_path(&start_path, move |path, ctx| { + if let Some(path) = path { + handle.update(ctx, move |this, ctx| { + let file = this.file_for_path(&path, ctx); + let task = item.save(Some(file), ctx.as_mut()); + ctx.spawn(task, move |_, result, _| { + if let Err(e) = result { + error!("failed to save item: {:?}, ", e); + } + }) + .detach() + }) } - }) - .detach() + }); + return; } - }); + + let task = item.save(None, ctx.as_mut()); + ctx.spawn(task, |_, result, _| { + if let Err(e) = result { + error!("failed to save item: {:?}, ", e); + } + }) + .detach() + } } pub fn debug_elements(&mut self, _: &(), ctx: &mut ViewContext) { @@ -521,7 +648,7 @@ impl Workspace { self.activate_pane(new_pane.clone(), ctx); if let Some(item) = pane.read(ctx).active_item() { if let Some(clone) = item.clone_on_split(ctx.as_mut()) { - self.add_item(clone, ctx); + self.add_item_view(clone, ctx); } } self.center @@ -546,7 +673,7 @@ impl Workspace { &self.active_pane } - fn add_item(&self, item: Box, ctx: &mut ViewContext) { + fn add_item_view(&self, item: Box, ctx: &mut ViewContext) { let active_pane = self.active_pane(); item.set_parent_pane(&active_pane, ctx.as_mut()); active_pane.update(ctx, |pane, ctx| { @@ -609,7 +736,9 @@ mod tests { use crate::{editor::BufferView, settings, test::temp_tree}; use gpui::App; use serde_json::json; - use std::{collections::HashSet, os::unix}; + use std::collections::HashSet; + use std::time; + use tempdir::TempDir; #[test] fn test_open_paths_action() { @@ -698,15 +827,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()) @@ -720,7 +847,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()) @@ -733,7 +860,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()) @@ -741,21 +868,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]); }); }); } @@ -832,63 +980,100 @@ mod tests { } #[test] - fn test_open_two_paths_to_the_same_file() { - use crate::workspace::ItemViewHandle; - + fn test_open_and_save_new_file() { App::test_async((), |mut app| async move { - // Create a worktree with a symlink: - // dir - // ├── hello.txt - // └── hola.txt -> hello.txt - let temp_dir = temp_tree(json!({ "hello.txt": "hi" })); - let dir = temp_dir.path(); - unix::fs::symlink(dir.join("hello.txt"), dir.join("hola.txt")).unwrap(); - + let dir = TempDir::new("test-new-file").unwrap(); let settings = settings::channel(&app.font_cache()).unwrap().1; let (_, workspace) = app.add_window(|ctx| { let mut workspace = Workspace::new(0, settings, ctx); - workspace.add_worktree(dir, ctx); + workspace.add_worktree(dir.path(), ctx); workspace }); - app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx)) - .await; - - // Simultaneously open both the original file and the symlink to the same file. - app.update(|ctx| { - workspace.update(ctx, |view, ctx| { - view.open_paths(&[dir.join("hello.txt"), dir.join("hola.txt")], ctx) - }) - }) - .await; - - // The same content shows up with two different editors. - let buffer_views = app.read(|ctx| { + let worktree = app.read(|ctx| { workspace .read(ctx) - .active_pane() - .read(ctx) - .items() + .worktrees() .iter() - .map(|i| i.to_any().downcast::().unwrap()) - .collect::>() + .next() + .unwrap() + .clone() }); - app.read(|ctx| { - assert_eq!(buffer_views[0].title(ctx), "hello.txt"); - assert_eq!(buffer_views[1].title(ctx), "hola.txt"); - assert_eq!(buffer_views[0].read(ctx).text(ctx), "hi"); - assert_eq!(buffer_views[1].read(ctx).text(ctx), "hi"); + + // Create a new untitled buffer + let editor = workspace.update(&mut app, |workspace, ctx| { + workspace.open_new_file(&(), ctx); + workspace + .active_item(ctx) + .unwrap() + .to_any() + .downcast::() + .unwrap() + }); + editor.update(&mut app, |editor, ctx| { + assert!(!editor.is_dirty(ctx.as_ref())); + assert_eq!(editor.title(ctx.as_ref()), "untitled"); + editor.insert(&"hi".to_string(), ctx); + assert!(editor.is_dirty(ctx.as_ref())); }); - // When modifying one buffer, the changes appear in both editors. - app.update(|ctx| { - buffer_views[0].update(ctx, |buf, ctx| { - buf.insert(&"oh, ".to_string(), ctx); - }); + // Save the buffer. This prompts for a filename. + workspace.update(&mut app, |workspace, ctx| { + workspace.save_active_item(&(), ctx) + }); + app.simulate_new_path_selection(|parent_dir| { + assert_eq!(parent_dir, dir.path()); + Some(parent_dir.join("the-new-name")) }); app.read(|ctx| { - assert_eq!(buffer_views[0].read(ctx).text(ctx), "oh, hi"); - assert_eq!(buffer_views[1].read(ctx).text(ctx), "oh, hi"); + assert!(editor.is_dirty(ctx)); + assert_eq!(editor.title(ctx), "untitled"); + }); + + // When the save completes, the buffer's title is updated. + editor + .condition(&app, |editor, ctx| !editor.is_dirty(ctx)) + .await; + worktree + .condition_with_duration(time::Duration::from_millis(500), &app, |worktree, _| { + worktree.inode_for_path("the-new-name").is_some() + }) + .await; + app.read(|ctx| assert_eq!(editor.title(ctx), "the-new-name")); + + // Edit the file and save it again. This time, there is no filename prompt. + editor.update(&mut app, |editor, ctx| { + editor.insert(&" there".to_string(), ctx); + assert_eq!(editor.is_dirty(ctx.as_ref()), true); + }); + workspace.update(&mut app, |workspace, ctx| { + workspace.save_active_item(&(), ctx) + }); + assert!(!app.did_prompt_for_new_path()); + editor + .condition(&app, |editor, ctx| !editor.is_dirty(ctx)) + .await; + app.read(|ctx| assert_eq!(editor.title(ctx), "the-new-name")); + + // 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); + 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) + .unwrap() + .to_any() + .downcast::() + .unwrap() }); + app.read(|ctx| { + assert_eq!(editor2.read(ctx).buffer(), editor.read(ctx).buffer()); + }) }); } diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 787d9fd404971fbee8c8b8692a7d27f4f131c214..135fb3bedf4799a953a199243bb33472c0e941fd 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>, @@ -407,6 +407,10 @@ impl FileHandle { self.state.lock().is_deleted } + pub fn exists(&self) -> bool { + !self.is_deleted() + } + pub fn load_history(&self, ctx: &AppContext) -> impl Future> { self.worktree.read(ctx).load_history(&self.path(), ctx) } @@ -416,18 +420,22 @@ impl FileHandle { worktree.save(&self.path(), content, ctx) } + pub fn worktree_id(&self) -> usize { + self.worktree.id() + } + pub fn entry_id(&self) -> (usize, Arc) { (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 { @@ -974,9 +982,8 @@ impl BackgroundScanner { let snapshot = self.snapshot.lock(); handles.retain(|path, handle_state| { if let Some(handle_state) = Weak::upgrade(&handle_state) { - if snapshot.entry_for_path(&path).is_none() { - handle_state.lock().is_deleted = true; - } + let mut handle_state = handle_state.lock(); + handle_state.is_deleted = snapshot.entry_for_path(&path).is_none(); true } else { false @@ -1129,31 +1136,38 @@ struct UpdateIgnoreStatusJob { } pub trait WorktreeHandle { - fn file(&self, path: impl AsRef, app: &AppContext) -> Option; + fn file(&self, path: impl AsRef, app: &AppContext) -> FileHandle; } impl WorktreeHandle for ModelHandle { - fn file(&self, path: impl AsRef, app: &AppContext) -> Option { + fn file(&self, path: impl AsRef, app: &AppContext) -> FileHandle { + let path = path.as_ref(); let tree = self.read(app); - let entry = tree.entry_for_path(&path)?; - - let path = entry.path().clone(); let mut handles = tree.handles.lock(); - let state = if let Some(state) = handles.get(&path).and_then(Weak::upgrade) { + let state = if let Some(state) = handles.get(path).and_then(Weak::upgrade) { state } else { - let state = Arc::new(Mutex::new(FileHandleState { - path: path.clone(), - is_deleted: false, - })); - handles.insert(path, Arc::downgrade(&state)); + let handle_state = if let Some(entry) = tree.entry_for_path(path) { + FileHandleState { + path: entry.path().clone(), + is_deleted: false, + } + } else { + FileHandleState { + path: path.into(), + is_deleted: true, + } + }; + + let state = Arc::new(Mutex::new(handle_state.clone())); + handles.insert(handle_state.path, Arc::downgrade(&state)); state }; - Some(FileHandle { + FileHandle { worktree: self.clone(), state, - }) + } } } @@ -1349,7 +1363,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(); @@ -1392,10 +1407,10 @@ mod tests { let (file2, file3, file4, file5) = app.read(|ctx| { ( - tree.file("a/file2", ctx).unwrap(), - tree.file("a/file3", ctx).unwrap(), - tree.file("b/c/file4", ctx).unwrap(), - tree.file("b/c/file5", ctx).unwrap(), + tree.file("a/file2", ctx), + tree.file("a/file3", ctx), + tree.file("b/c/file4", ctx), + tree.file("b/c/file5", ctx), ) });