diff --git a/gpui/src/app.rs b/gpui/src/app.rs index 02a1626a304c546f89fd8385fbef057e643ab985..ac4c4e69b1782a416f5058025db0eafb974b7e7a 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -379,7 +379,8 @@ pub struct MutableAppContext { next_window_id: usize, next_task_id: usize, subscriptions: HashMap>, - observations: HashMap>, + model_observations: HashMap>, + view_observations: HashMap>, async_observations: HashMap>, window_invalidations: HashMap, presenters_and_platform_windows: @@ -420,7 +421,8 @@ impl MutableAppContext { next_window_id: 0, next_task_id: 0, subscriptions: HashMap::new(), - observations: HashMap::new(), + model_observations: HashMap::new(), + view_observations: HashMap::new(), async_observations: HashMap::new(), window_invalidations: HashMap::new(), presenters_and_platform_windows: HashMap::new(), @@ -871,13 +873,13 @@ impl MutableAppContext { for model_id in dropped_models { self.ctx.models.remove(&model_id); self.subscriptions.remove(&model_id); - self.observations.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.observations.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 @@ -1004,11 +1006,11 @@ impl MutableAppContext { } fn notify_model_observers(&mut self, observed_id: usize) { - if let Some(observations) = self.observations.remove(&observed_id) { + if let Some(observations) = self.model_observations.remove(&observed_id) { if self.ctx.models.contains_key(&observed_id) { for mut observation in observations { let alive = match &mut observation { - Observation::FromModel { model_id, callback } => { + ModelObservation::FromModel { model_id, callback } => { if let Some(mut model) = self.ctx.models.remove(model_id) { callback(model.as_any_mut(), observed_id, self, *model_id); self.ctx.models.insert(*model_id, model); @@ -1017,7 +1019,7 @@ impl MutableAppContext { false } } - Observation::FromView { + ModelObservation::FromView { window_id, view_id, callback, @@ -1049,7 +1051,7 @@ impl MutableAppContext { }; if alive { - self.observations + self.model_observations .entry(observed_id) .or_default() .push(observation); @@ -1072,6 +1074,44 @@ impl MutableAppContext { .updated .insert(view_id); + if let Some(observations) = self.view_observations.remove(&view_id) { + if self.ctx.models.contains_key(&view_id) { + for mut observation in observations { + let alive = if let Some(mut view) = self + .ctx + .windows + .get_mut(&observation.window_id) + .and_then(|w| w.views.remove(&observation.view_id)) + { + (observation.callback)( + view.as_any_mut(), + view_id, + window_id, + self, + observation.window_id, + observation.view_id, + ); + self.ctx + .windows + .get_mut(&observation.window_id) + .unwrap() + .views + .insert(observation.view_id, view); + true + } else { + false + }; + + if alive { + self.view_observations + .entry(view_id) + .or_default() + .push(observation); + } + } + } + } + if let Entry::Occupied(mut entry) = self.async_observations.entry(view_id) { if entry.get_mut().blocking_send(()).is_err() { entry.remove_entry(); @@ -1576,10 +1616,10 @@ impl<'a, T: Entity> ModelContext<'a, T> { F: 'static + FnMut(&mut T, ModelHandle, &mut ModelContext), { self.app - .observations + .model_observations .entry(handle.model_id) .or_default() - .push(Observation::FromModel { + .push(ModelObservation::FromModel { model_id: self.model_id, callback: Box::new(move |model, observed_id, app, model_id| { let model = model.downcast_mut().expect("downcast is type safe"); @@ -1812,7 +1852,7 @@ impl<'a, T: View> ViewContext<'a, T> { 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()) { + 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); @@ -1829,16 +1869,16 @@ impl<'a, T: View> ViewContext<'a, T> { }); } - pub fn observe(&mut self, handle: &ModelHandle, mut callback: F) + pub fn observe_model(&mut self, handle: &ModelHandle, mut callback: F) where S: Entity, F: 'static + FnMut(&mut T, ModelHandle, &mut ViewContext), { self.app - .observations + .model_observations .entry(handle.id()) .or_default() - .push(Observation::FromView { + .push(ModelObservation::FromView { window_id: self.window_id, view_id: self.view_id, callback: Box::new(move |view, observed_id, app, window_id, view_id| { @@ -1850,6 +1890,38 @@ impl<'a, T: View> ViewContext<'a, T> { }); } + pub fn observe_view(&mut self, handle: &ViewHandle, mut callback: F) + where + S: View, + F: 'static + FnMut(&mut T, ViewHandle, &mut ViewContext), + { + self.app + .view_observations + .entry(handle.id()) + .or_default() + .push(ViewObservation { + window_id: self.window_id, + view_id: self.view_id, + callback: Box::new( + move |view, + observed_view_id, + observed_window_id, + app, + observing_window_id, + observing_view_id| { + let view = view.downcast_mut().expect("downcast is type safe"); + let observed_handle = ViewHandle::new( + observed_view_id, + observed_window_id, + &app.ctx.ref_counts, + ); + let mut ctx = ViewContext::new(app, observing_window_id, observing_view_id); + callback(view, observed_handle, &mut ctx); + }, + ), + }); + } + pub fn notify(&mut self) { self.app.notify_view(self.window_id, self.view_id); } @@ -1918,6 +1990,12 @@ impl<'a, T: View> ViewContext<'a, T> { } } +impl AsRef for &AppContext { + fn as_ref(&self) -> &AppContext { + self + } +} + impl AsRef for ViewContext<'_, M> { fn as_ref(&self) -> &AppContext { &self.app.ctx @@ -2346,8 +2424,9 @@ impl WeakViewHandle { } } - pub fn upgrade(&self, app: &AppContext) -> Option> { - if app + pub fn upgrade(&self, ctx: impl AsRef) -> Option> { + let ctx = ctx.as_ref(); + if ctx .windows .get(&self.window_id) .and_then(|w| w.views.get(&self.view_id)) @@ -2356,7 +2435,7 @@ impl WeakViewHandle { Some(ViewHandle::new( self.window_id, self.view_id, - &app.ref_counts, + &ctx.ref_counts, )) } else { None @@ -2496,7 +2575,7 @@ enum Subscription { }, } -enum Observation { +enum ModelObservation { FromModel { model_id: usize, callback: Box, @@ -2508,6 +2587,12 @@ enum Observation { }, } +struct ViewObservation { + window_id: usize, + view_id: usize, + callback: Box, +} + type FutureHandler = Box, &mut MutableAppContext) -> Box>; struct StreamHandler { @@ -2639,7 +2724,7 @@ mod tests { assert_eq!(app.ctx.models.len(), 1); assert!(app.subscriptions.is_empty()); - assert!(app.observations.is_empty()); + assert!(app.model_observations.is_empty()); }); } @@ -2842,7 +2927,7 @@ mod tests { assert_eq!(app.ctx.windows[&window_id].views.len(), 2); assert!(app.subscriptions.is_empty()); - assert!(app.observations.is_empty()); + assert!(app.model_observations.is_empty()); }) } @@ -2988,7 +3073,7 @@ mod tests { let model = app.add_model(|_| Model::default()); view.update(app, |_, c| { - c.observe(&model, |me, observed, c| { + c.observe_model(&model, |me, observed, c| { me.events.push(observed.read(c).count) }); }); @@ -3032,7 +3117,7 @@ mod tests { let observed_model = app.add_model(|_| Model); observing_view.update(app, |_, ctx| { - ctx.observe(&observed_model, |_, _, _| {}); + ctx.observe_model(&observed_model, |_, _, _| {}); }); observing_model.update(app, |_, ctx| { ctx.observe(&observed_model, |_, _, _| {}); diff --git a/zed/src/editor/buffer/mod.rs b/zed/src/editor/buffer/mod.rs index 55ffaec02809f4234beca977fcd17f8069ae1c3f..59b0bf9b36cee6bab3d03b00e3212d81b85fcd9e 100644 --- a/zed/src/editor/buffer/mod.rs +++ b/zed/src/editor/buffer/mod.rs @@ -18,16 +18,14 @@ use crate::{ worktree::FileHandle, }; use anyhow::{anyhow, Result}; -use gpui::{AppContext, Entity, ModelContext}; +use gpui::{Entity, ModelContext}; use lazy_static::lazy_static; use rand::prelude::*; use std::{ cmp, - ffi::OsString, hash::BuildHasher, iter::{self, Iterator}, ops::{AddAssign, Range}, - path::Path, str, sync::Arc, time::{Duration, Instant}, @@ -59,7 +57,6 @@ type HashMap = std::collections::HashMap; type HashSet = std::collections::HashSet; pub struct Buffer { - file: Option, fragments: SumTree, insertion_splits: HashMap>, pub version: time::Global, @@ -354,29 +351,15 @@ pub struct UndoOperation { } impl Buffer { - pub fn new>>( - replica_id: ReplicaId, - base_text: T, - ctx: &mut ModelContext, - ) -> Self { - Self::build(replica_id, None, History::new(base_text.into()), ctx) + pub fn new>>(replica_id: ReplicaId, base_text: T) -> Self { + Self::build(replica_id, History::new(base_text.into())) } - pub fn from_history( - replica_id: ReplicaId, - file: FileHandle, - history: History, - ctx: &mut ModelContext, - ) -> Self { - Self::build(replica_id, Some(file), history, ctx) + pub fn from_history(replica_id: ReplicaId, history: History) -> Self { + Self::build(replica_id, history) } - fn build( - replica_id: ReplicaId, - file: Option, - history: History, - ctx: &mut ModelContext, - ) -> Self { + fn build(replica_id: ReplicaId, history: History) -> Self { let mut insertion_splits = HashMap::default(); let mut fragments = SumTree::new(); @@ -425,12 +408,7 @@ impl Buffer { }); } - if let Some(file) = file.as_ref() { - file.observe_from_model(ctx, |_, _, ctx| ctx.emit(Event::FileHandleChanged)); - } - Self { - file, fragments, insertion_splits, version: time::Global::new(), @@ -448,41 +426,27 @@ impl Buffer { } } - pub fn file_name(&self, ctx: &AppContext) -> Option { - self.file.as_ref().and_then(|file| file.file_name(ctx)) - } - - pub fn path(&self) -> Option> { - self.file.as_ref().map(|file| file.path()) - } - - pub fn entry_id(&self) -> Option<(usize, Arc)> { - self.file.as_ref().map(|file| file.entry_id()) - } - pub fn snapshot(&self) -> Snapshot { Snapshot { fragments: self.fragments.clone(), } } - pub fn save(&mut self, ctx: &mut ModelContext) -> LocalBoxFuture<'static, Result<()>> { - if let Some(file) = &self.file { - dbg!(file.path()); - - 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) - } else { - Box::pin(async { Ok(()) }) - } + pub fn save( + &mut self, + file: &FileHandle, + 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) } fn did_save(&mut self, version: time::Global, ctx: &mut ModelContext) { @@ -1763,7 +1727,6 @@ impl Buffer { impl Clone for Buffer { fn clone(&self) -> Self { Self { - file: self.file.clone(), fragments: self.fragments.clone(), insertion_splits: self.insertion_splits.clone(), version: self.version.clone(), @@ -2333,8 +2296,8 @@ mod tests { #[test] fn test_edit() { App::test((), |ctx| { - ctx.add_model(|ctx| { - let mut buffer = Buffer::new(0, "abc", ctx); + ctx.add_model(|_| { + let mut buffer = Buffer::new(0, "abc"); assert_eq!(buffer.text(), "abc"); buffer.edit(vec![3..3], "def", None).unwrap(); assert_eq!(buffer.text(), "abcdef"); @@ -2358,8 +2321,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(|ctx| Buffer::new(0, "abcdef", ctx)); - let buffer2 = app.add_model(|ctx| Buffer::new(1, "abcdef", ctx)); + let buffer1 = app.add_model(|_| Buffer::new(0, "abcdef")); + let buffer2 = app.add_model(|_| Buffer::new(1, "abcdef")); let mut buffer_ops = Vec::new(); buffer1.update(app, |buffer, ctx| { let buffer_1_events = buffer_1_events.clone(); @@ -2445,8 +2408,8 @@ mod tests { let mut reference_string = RandomCharIter::new(&mut rng) .take(reference_string_len) .collect::(); - ctx.add_model(|ctx| { - let mut buffer = Buffer::new(0, reference_string.as_str(), ctx); + ctx.add_model(|_| { + let mut buffer = Buffer::new(0, reference_string.as_str()); let mut buffer_versions = Vec::new(); for _i in 0..10 { let (old_ranges, new_text, _) = buffer.randomly_mutate(rng, None); @@ -2531,8 +2494,8 @@ mod tests { #[test] fn test_line_len() { App::test((), |ctx| { - ctx.add_model(|ctx| { - let mut buffer = Buffer::new(0, "", ctx); + ctx.add_model(|_| { + let mut buffer = Buffer::new(0, ""); 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(); @@ -2553,8 +2516,8 @@ mod tests { #[test] fn test_rightmost_point() { App::test((), |ctx| { - ctx.add_model(|ctx| { - let mut buffer = Buffer::new(0, "", ctx); + ctx.add_model(|_| { + let mut buffer = Buffer::new(0, ""); assert_eq!(buffer.rightmost_point().row, 0); buffer.edit(vec![0..0], "abcd\nefg\nhij", None).unwrap(); assert_eq!(buffer.rightmost_point().row, 0); @@ -2574,8 +2537,8 @@ mod tests { #[test] fn test_text_summary_for_range() { App::test((), |ctx| { - ctx.add_model(|ctx| { - let buffer = Buffer::new(0, "ab\nefg\nhklm\nnopqrs\ntuvwxyz", ctx); + ctx.add_model(|_| { + let buffer = Buffer::new(0, "ab\nefg\nhklm\nnopqrs\ntuvwxyz"); let text = Text::from(buffer.text()); assert_eq!( buffer.text_summary_for_range(1..3), @@ -2605,8 +2568,8 @@ mod tests { #[test] fn test_chars_at() { App::test((), |ctx| { - ctx.add_model(|ctx| { - let mut buffer = Buffer::new(0, "", ctx); + ctx.add_model(|_| { + let mut buffer = Buffer::new(0, ""); 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(); @@ -2628,7 +2591,7 @@ mod tests { assert_eq!(chars.collect::(), "PQrs"); // Regression test: - let mut buffer = Buffer::new(0, "", ctx); + let mut buffer = Buffer::new(0, ""); 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(); @@ -2757,8 +2720,8 @@ mod tests { #[test] fn test_anchors() { App::test((), |ctx| { - ctx.add_model(|ctx| { - let mut buffer = Buffer::new(0, "", ctx); + ctx.add_model(|_| { + let mut buffer = Buffer::new(0, ""); buffer.edit(vec![0..0], "abc", None).unwrap(); let left_anchor = buffer.anchor_before(2).unwrap(); let right_anchor = buffer.anchor_after(2).unwrap(); @@ -2922,8 +2885,8 @@ mod tests { #[test] fn test_anchors_at_start_and_end() { App::test((), |ctx| { - ctx.add_model(|ctx| { - let mut buffer = Buffer::new(0, "", ctx); + ctx.add_model(|_| { + let mut buffer = Buffer::new(0, ""); let before_start_anchor = buffer.anchor_before(0).unwrap(); let after_end_anchor = buffer.anchor_after(0).unwrap(); @@ -2950,7 +2913,7 @@ mod tests { #[test] fn test_is_modified() { App::test((), |app| { - let model = app.add_model(|ctx| Buffer::new(0, "abc", ctx)); + let model = app.add_model(|_| Buffer::new(0, "abc")); let events = Rc::new(RefCell::new(Vec::new())); // initially, the buffer isn't dirty. @@ -3037,8 +3000,8 @@ mod tests { #[test] fn test_undo_redo() { App::test((), |app| { - app.add_model(|ctx| { - let mut buffer = Buffer::new(0, "1234", ctx); + app.add_model(|_| { + let mut buffer = Buffer::new(0, "1234"); let edit1 = buffer.edit(vec![1..1], "abx", None).unwrap(); let edit2 = buffer.edit(vec![3..4], "yzef", None).unwrap(); @@ -3074,9 +3037,9 @@ mod tests { #[test] fn test_history() { App::test((), |app| { - app.add_model(|ctx| { + app.add_model(|_| { let mut now = Instant::now(); - let mut buffer = Buffer::new(0, "123456", ctx); + let mut buffer = Buffer::new(0, "123456"); let (set_id, _) = buffer .add_selection_set(buffer.selections_from_ranges(vec![4..4]).unwrap(), None); @@ -3161,7 +3124,7 @@ mod tests { let mut network = Network::new(); for i in 0..PEERS { let buffer = - ctx.add_model(|ctx| Buffer::new(i as ReplicaId, base_text.as_str(), ctx)); + ctx.add_model(|_| Buffer::new(i as ReplicaId, base_text.as_str())); 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 f313edad0b2efcdc65505dddf0bb6062b7f78729..40055103c62e3c9529eb24f8b14250200c7d0633 100644 --- a/zed/src/editor/buffer_view.rs +++ b/zed/src/editor/buffer_view.rs @@ -2,7 +2,7 @@ use super::{ buffer, movement, Anchor, Bias, Buffer, BufferElement, DisplayMap, DisplayPoint, Point, Selection, SelectionSetId, ToOffset, ToPoint, }; -use crate::{settings::Settings, watch, workspace}; +use crate::{settings::Settings, watch, workspace, worktree::FileHandle}; use anyhow::Result; use futures_core::future::LocalBoxFuture; use gpui::{ @@ -254,6 +254,7 @@ pub enum SelectAction { pub struct BufferView { handle: WeakViewHandle, buffer: ModelHandle, + file: Option, display_map: ModelHandle, selection_set_id: SelectionSetId, pending_selection: Option, @@ -275,20 +276,25 @@ struct ClipboardSelection { impl BufferView { pub fn single_line(settings: watch::Receiver, ctx: &mut ViewContext) -> Self { - let buffer = ctx.add_model(|ctx| Buffer::new(0, String::new(), ctx)); - let mut view = Self::for_buffer(buffer, settings, ctx); + let buffer = ctx.add_model(|_| Buffer::new(0, String::new())); + let mut view = Self::for_buffer(buffer, None, 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); - ctx.observe(&buffer, Self::on_buffer_changed); + 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| { DisplayMap::new( @@ -297,7 +303,7 @@ impl BufferView { ctx, ) }); - ctx.observe(&display_map, Self::on_display_map_changed); + ctx.observe_model(&display_map, Self::on_display_map_changed); let (selection_set_id, _) = buffer.update(ctx, |buffer, ctx| { buffer.add_selection_set( @@ -313,6 +319,7 @@ impl BufferView { Self { handle: ctx.handle().downgrade(), buffer, + file, display_map, selection_set_id, pending_selection: None, @@ -577,7 +584,7 @@ impl BufferView { Ok(()) } - fn insert(&mut self, text: &String, ctx: &mut ViewContext) { + pub fn insert(&mut self, text: &String, ctx: &mut ViewContext) { let mut offset_ranges = SmallVec::<[Range; 32]>::new(); { let buffer = self.buffer.read(ctx); @@ -2089,18 +2096,6 @@ impl View for BufferView { } } -impl workspace::Item for Buffer { - type View = BufferView; - - fn build_view( - buffer: ModelHandle, - settings: watch::Receiver, - ctx: &mut ViewContext, - ) -> Self::View { - BufferView::for_buffer(buffer, settings, ctx) - } -} - impl workspace::ItemView for BufferView { fn should_activate_item_on_event(event: &Self::Event) -> bool { matches!(event, Event::Activate) @@ -2114,28 +2109,39 @@ impl workspace::ItemView for BufferView { } fn title(&self, app: &AppContext) -> std::string::String { - if let Some(name) = self.buffer.read(app).file_name(app) { + let filename = self.file.as_ref().and_then(|file| file.file_name(app)); + if let Some(name) = filename { name.to_string_lossy().into() } else { "untitled".into() } } - fn entry_id(&self, app: &AppContext) -> Option<(usize, Arc)> { - self.buffer.read(app).entry_id() + fn entry_id(&self, _: &AppContext) -> Option<(usize, Arc)> { + self.file.as_ref().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.settings.clone(), ctx); + let clone = BufferView::for_buffer( + self.buffer.clone(), + self.file.clone(), + self.settings.clone(), + ctx, + ); *clone.scroll_position.lock() = *self.scroll_position.lock(); Some(clone) } fn save(&self, ctx: &mut ViewContext) -> LocalBoxFuture<'static, Result<()>> { - self.buffer.update(ctx, |buffer, ctx| buffer.save(ctx)) + if let Some(file) = self.file.as_ref() { + self.buffer + .update(ctx, |buffer, ctx| buffer.save(file, ctx)) + } else { + Box::pin(async { Ok(()) }) + } } fn is_dirty(&self, ctx: &AppContext) -> bool { @@ -2153,11 +2159,10 @@ mod tests { #[test] fn test_selection_with_mouse() { App::test((), |app| { - let buffer = - app.add_model(|ctx| Buffer::new(0, "aaaaaa\nbbbbbb\ncccccc\ndddddd\n", ctx)); + let buffer = app.add_model(|_| Buffer::new(0, "aaaaaa\nbbbbbb\ncccccc\ndddddd\n")); let settings = settings::channel(&app.font_cache()).unwrap().1; let (_, buffer_view) = - app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); + app.add_window(|ctx| BufferView::for_buffer(buffer, None, settings, ctx)); buffer_view.update(app, |view, ctx| { view.begin_selection(DisplayPoint::new(2, 2), false, ctx); @@ -2268,11 +2273,11 @@ mod tests { let layout_cache = TextLayoutCache::new(app.platform().fonts()); let font_cache = app.font_cache().clone(); - let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(6, 6), ctx)); + let buffer = app.add_model(|_| Buffer::new(0, sample_text(6, 6))); let settings = settings::channel(&font_cache).unwrap().1; let (_, view) = - app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx)); + app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), None, settings, ctx)); let layouts = view .read(app) @@ -2285,7 +2290,7 @@ mod tests { #[test] fn test_fold() { App::test((), |app| { - let buffer = app.add_model(|ctx| { + let buffer = app.add_model(|_| { Buffer::new( 0, " @@ -2306,12 +2311,11 @@ mod tests { } " .unindent(), - ctx, ) }); let settings = settings::channel(&app.font_cache()).unwrap().1; let (_, view) = - app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx)); + app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), None, settings, ctx)); view.update(app, |view, ctx| { view.select_display_ranges( @@ -2380,10 +2384,10 @@ mod tests { #[test] fn test_move_cursor() { App::test((), |app| { - let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(6, 6), ctx)); + let buffer = app.add_model(|_| Buffer::new(0, sample_text(6, 6))); let settings = settings::channel(&app.font_cache()).unwrap().1; let (_, view) = - app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx)); + app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), None, settings, ctx)); buffer.update(app, |buffer, ctx| { buffer @@ -2458,9 +2462,10 @@ mod tests { #[test] fn test_beginning_end_of_line() { App::test((), |app| { - let buffer = app.add_model(|ctx| Buffer::new(0, "abc\n def", ctx)); + let buffer = app.add_model(|_| Buffer::new(0, "abc\n def")); let settings = settings::channel(&app.font_cache()).unwrap().1; - let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); + let (_, view) = + app.add_window(|ctx| BufferView::for_buffer(buffer, None, settings, ctx)); view.update(app, |view, ctx| { view.select_display_ranges( &[ @@ -2586,10 +2591,11 @@ mod tests { #[test] fn test_prev_next_word_boundary() { App::test((), |app| { - let buffer = app - .add_model(|ctx| Buffer::new(0, "use std::str::{foo, bar}\n\n {baz.qux()}", ctx)); + let buffer = + app.add_model(|_| Buffer::new(0, "use std::str::{foo, bar}\n\n {baz.qux()}")); let settings = settings::channel(&app.font_cache()).unwrap().1; - let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); + let (_, view) = + app.add_window(|ctx| BufferView::for_buffer(buffer, None, settings, ctx)); view.update(app, |view, ctx| { view.select_display_ranges( &[ @@ -2768,16 +2774,12 @@ mod tests { #[test] fn test_backspace() { App::test((), |app| { - let buffer = app.add_model(|ctx| { - Buffer::new( - 0, - "one two three\nfour five six\nseven eight nine\nten\n", - ctx, - ) + let buffer = app.add_model(|_| { + Buffer::new(0, "one two three\nfour five six\nseven eight nine\nten\n") }); let settings = settings::channel(&app.font_cache()).unwrap().1; let (_, view) = - app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx)); + app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), None, settings, ctx)); view.update(app, |view, ctx| { view.select_display_ranges( @@ -2805,16 +2807,12 @@ mod tests { #[test] fn test_delete() { App::test((), |app| { - let buffer = app.add_model(|ctx| { - Buffer::new( - 0, - "one two three\nfour five six\nseven eight nine\nten\n", - ctx, - ) + let buffer = app.add_model(|_| { + Buffer::new(0, "one two three\nfour five six\nseven eight nine\nten\n") }); let settings = settings::channel(&app.font_cache()).unwrap().1; let (_, view) = - app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx)); + app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), None, settings, ctx)); view.update(app, |view, ctx| { view.select_display_ranges( @@ -2843,8 +2841,9 @@ mod tests { fn test_delete_line() { App::test((), |app| { let settings = settings::channel(&app.font_cache()).unwrap().1; - 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)); + 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)); view.update(app, |view, ctx| { view.select_display_ranges( &[ @@ -2867,8 +2866,9 @@ mod tests { ); let settings = settings::channel(&app.font_cache()).unwrap().1; - 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)); + 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)); view.update(app, |view, ctx| { view.select_display_ranges( &[DisplayPoint::new(2, 0)..DisplayPoint::new(0, 1)], @@ -2889,8 +2889,9 @@ mod tests { fn test_duplicate_line() { App::test((), |app| { let settings = settings::channel(&app.font_cache()).unwrap().1; - 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)); + 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)); view.update(app, |view, ctx| { view.select_display_ranges( &[ @@ -2919,8 +2920,9 @@ mod tests { ); let settings = settings::channel(&app.font_cache()).unwrap().1; - 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)); + 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)); view.update(app, |view, ctx| { view.select_display_ranges( &[ @@ -2949,10 +2951,10 @@ mod tests { #[test] fn test_clipboard() { App::test((), |app| { - let buffer = app.add_model(|ctx| Buffer::new(0, "one two three four five six ", ctx)); + let buffer = app.add_model(|_| Buffer::new(0, "one two three four five six ")); let settings = settings::channel(&app.font_cache()).unwrap().1; let view = app - .add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx)) + .add_window(|ctx| BufferView::for_buffer(buffer.clone(), None, settings, ctx)) .1; // Cut with three selections. Clipboard text is divided into three slices. @@ -3090,9 +3092,10 @@ mod tests { #[test] fn test_select_all() { App::test((), |app| { - let buffer = app.add_model(|ctx| Buffer::new(0, "abc\nde\nfgh", ctx)); + let buffer = app.add_model(|_| Buffer::new(0, "abc\nde\nfgh")); let settings = settings::channel(&app.font_cache()).unwrap().1; - let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); + let (_, view) = + app.add_window(|ctx| BufferView::for_buffer(buffer, None, 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 98c47ff08a84ea1a79ded4a0edf03da7ad0a681a..8a105f8af39d76ccebce43558ab9273ba50ae906 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(|ctx| Buffer::new(0, sample_text(5, 6), ctx)); + let buffer = app.add_model(|_| Buffer::new(0, sample_text(5, 6))); 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(|ctx| Buffer::new(0, "abcdefghijkl", ctx)); + let buffer = app.add_model(|_| Buffer::new(0, "abcdefghijkl")); { 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(|ctx| Buffer::new(0, sample_text(5, 6), ctx)); + let buffer = app.add_model(|_| Buffer::new(0, sample_text(5, 6))); 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(|ctx| Buffer::new(0, sample_text(5, 6), ctx)); + let buffer = app.add_model(|_| Buffer::new(0, sample_text(5, 6))); 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(|ctx| { + let buffer = app.add_model(|_| { let len = rng.gen_range(0..10); let text = RandomCharIter::new(&mut rng).take(len).collect::(); - Buffer::new(0, text, ctx) + Buffer::new(0, text) }); 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(|ctx| Buffer::new(0, text, ctx)); + let buffer = app.add_model(|_| Buffer::new(0, text)); 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 b69a70f3979870eb061f72c0cfdff25931165ccf..e2c422ad36169ebedc44ef7367003ca1f1af3cdb 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(|ctx| Buffer::new(0, text, ctx)); + let buffer = app.add_model(|_| Buffer::new(0, text)); 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(|ctx| Buffer::new(0, "aaa\n\t\tbbb", ctx)); + let buffer = app.add_model(|_| Buffer::new(0, "aaa\n\t\tbbb")); 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/file_finder.rs b/zed/src/file_finder.rs index 0884cbdf5a118be6dd4a499cecc66ee92893c538..0965cdcda5bc77ca4d35a7840855e53a2004a688 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -2,7 +2,7 @@ use crate::{ editor::{buffer_view, BufferView}, settings::Settings, util, watch, - workspace::{Workspace, WorkspaceView}, + workspace::Workspace, worktree::{match_paths, PathMatch, Worktree}, }; use gpui::{ @@ -11,8 +11,8 @@ use gpui::{ fonts::{Properties, Weight}, geometry::vector::vec2f, keymap::{self, Binding}, - AppContext, Axis, Border, Entity, ModelHandle, MutableAppContext, View, ViewContext, - ViewHandle, WeakViewHandle, + AppContext, Axis, Border, Entity, MutableAppContext, View, ViewContext, ViewHandle, + WeakViewHandle, }; use std::{ cmp, @@ -26,7 +26,7 @@ use std::{ pub struct FileFinder { handle: WeakViewHandle, settings: watch::Receiver, - workspace: ModelHandle, + workspace: WeakViewHandle, query_buffer: ViewHandle, search_count: usize, latest_search_id: usize, @@ -253,25 +253,21 @@ impl FileFinder { }) } - fn toggle(workspace_view: &mut WorkspaceView, _: &(), ctx: &mut ViewContext) { + fn toggle(workspace_view: &mut Workspace, _: &(), ctx: &mut ViewContext) { workspace_view.toggle_modal(ctx, |ctx, workspace_view| { - let handle = ctx.add_view(|ctx| { - Self::new( - workspace_view.settings.clone(), - workspace_view.workspace.clone(), - ctx, - ) - }); - ctx.subscribe_to_view(&handle, Self::on_event); - handle + let workspace = ctx.handle(); + let finder = + ctx.add_view(|ctx| Self::new(workspace_view.settings.clone(), workspace, ctx)); + ctx.subscribe_to_view(&finder, Self::on_event); + finder }); } fn on_event( - workspace_view: &mut WorkspaceView, + workspace_view: &mut Workspace, _: ViewHandle, event: &Event, - ctx: &mut ViewContext, + ctx: &mut ViewContext, ) { match event { Event::Selected(tree_id, path) => { @@ -288,10 +284,10 @@ impl FileFinder { pub fn new( settings: watch::Receiver, - workspace: ModelHandle, + workspace: ViewHandle, ctx: &mut ViewContext, ) -> Self { - ctx.observe(&workspace, Self::workspace_updated); + ctx.observe_view(&workspace, Self::workspace_updated); let query_buffer = ctx.add_view(|ctx| BufferView::single_line(settings.clone(), ctx)); ctx.subscribe_to_view(&query_buffer, Self::on_query_buffer_event); @@ -301,7 +297,7 @@ impl FileFinder { Self { handle: ctx.handle().downgrade(), settings, - workspace, + workspace: workspace.downgrade(), query_buffer, search_count: 0, latest_search_id: 0, @@ -314,7 +310,7 @@ impl FileFinder { } } - fn workspace_updated(&mut self, _: ModelHandle, ctx: &mut ViewContext) { + fn workspace_updated(&mut self, _: ViewHandle, ctx: &mut ViewContext) { self.spawn_search(self.query_buffer.read(ctx).text(ctx.as_ref()), ctx); } @@ -390,9 +386,10 @@ impl FileFinder { ctx.emit(Event::Selected(*tree_id, path.clone())); } - fn spawn_search(&mut self, query: String, ctx: &mut ViewContext) { + fn spawn_search(&mut self, query: String, ctx: &mut ViewContext) -> Option<()> { let snapshots = self .workspace + .upgrade(&ctx)? .read(ctx) .worktrees() .iter() @@ -420,6 +417,8 @@ impl FileFinder { }); ctx.spawn(task, Self::update_matches).detach(); + + Some(()) } fn update_matches( @@ -443,6 +442,7 @@ impl FileFinder { fn worktree<'a>(&'a self, tree_id: usize, app: &'a AppContext) -> Option<&'a Worktree> { self.workspace + .upgrade(app)? .read(app) .worktrees() .get(&tree_id) @@ -453,11 +453,7 @@ impl FileFinder { #[cfg(test)] mod tests { use super::*; - use crate::{ - editor, settings, - test::temp_tree, - workspace::{Workspace, WorkspaceView}, - }; + use crate::{editor, settings, test::temp_tree, workspace::Workspace}; use gpui::App; use serde_json::json; use std::fs; @@ -476,20 +472,22 @@ mod tests { }); let settings = settings::channel(&app.font_cache()).unwrap().1; - let workspace = app.add_model(|ctx| Workspace::new(vec![tmp_dir.path().into()], ctx)); - let (window_id, workspace_view) = - app.add_window(|ctx| WorkspaceView::new(workspace.clone(), settings, ctx)); + let (window_id, workspace) = app.add_window(|ctx| { + let mut workspace = Workspace::new(0, settings, ctx); + workspace.add_worktree(tmp_dir.path(), ctx); + workspace + }); app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx)) .await; app.dispatch_action( window_id, - vec![workspace_view.id()], + vec![workspace.id()], "file_finder:toggle".into(), (), ); let finder = app.read(|ctx| { - workspace_view + workspace .read(ctx) .modal() .cloned() @@ -507,16 +505,16 @@ mod tests { .condition(&app, |finder, _| finder.matches.len() == 2) .await; - let active_pane = app.read(|ctx| workspace_view.read(ctx).active_pane().clone()); + let active_pane = app.read(|ctx| workspace.read(ctx).active_pane().clone()); app.dispatch_action( window_id, - vec![workspace_view.id(), finder.id()], + vec![workspace.id(), finder.id()], "menu:select_next", (), ); app.dispatch_action( window_id, - vec![workspace_view.id(), finder.id()], + vec![workspace.id(), finder.id()], "file_finder:confirm", (), ); @@ -543,7 +541,11 @@ mod tests { "hiccup": "", })); let settings = settings::channel(&app.font_cache()).unwrap().1; - let workspace = app.add_model(|ctx| Workspace::new(vec![tmp_dir.path().into()], ctx)); + let (_, workspace) = app.add_window(|ctx| { + let mut workspace = Workspace::new(0, settings.clone(), ctx); + workspace.add_worktree(tmp_dir.path(), ctx); + workspace + }); app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx)) .await; let (_, finder) = @@ -596,7 +598,11 @@ mod tests { fs::write(&file_path, "").unwrap(); let settings = settings::channel(&app.font_cache()).unwrap().1; - let workspace = app.add_model(|ctx| Workspace::new(vec![file_path], ctx)); + let (_, workspace) = app.add_window(|ctx| { + let mut workspace = Workspace::new(0, settings.clone(), ctx); + workspace.add_worktree(&file_path, ctx); + workspace + }); app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx)) .await; let (_, finder) = @@ -633,14 +639,20 @@ mod tests { "dir2": { "a.txt": "" } })); let settings = settings::channel(&app.font_cache()).unwrap().1; - let workspace = app.add_model(|ctx| { - Workspace::new( - vec![tmp_dir.path().join("dir1"), tmp_dir.path().join("dir2")], - ctx, - ) - }); + + let (_, workspace) = app.add_window(|ctx| Workspace::new(0, settings.clone(), ctx)); + + workspace + .update(&mut app, |workspace, ctx| { + workspace.open_paths( + &[tmp_dir.path().join("dir1"), tmp_dir.path().join("dir2")], + ctx, + ) + }) + .await; app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx)) .await; + let (_, finder) = app.add_window(|ctx| FileFinder::new(settings, workspace.clone(), ctx)); diff --git a/zed/src/workspace/workspace_view.rs b/zed/src/workspace.rs similarity index 56% rename from zed/src/workspace/workspace_view.rs rename to zed/src/workspace.rs index 6815a1e2c95778da781e5a671d802489dbc4e593..629d23beecce0bdf1e1c59a76cd6fbe465f92f9b 100644 --- a/zed/src/workspace/workspace_view.rs +++ b/zed/src/workspace.rs @@ -1,25 +1,99 @@ -use super::{pane, Pane, PaneGroup, SplitDirection, Workspace}; -use crate::{settings::Settings, watch}; +pub mod pane; +pub mod pane_group; +pub use pane::*; +pub use pane_group::*; + +use crate::{ + settings::Settings, + watch::{self, Receiver}, +}; +use gpui::{MutableAppContext, PathPromptOptions}; +use std::path::PathBuf; +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_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, MutableAppContext, View, ViewContext, - ViewHandle, + ClipboardItem, Entity, EntityTask, ModelHandle, View, ViewContext, ViewHandle, }; use log::error; +use smol::prelude::*; use std::{ - collections::HashSet, - path::{Path, PathBuf}, + collections::{hash_map::Entry, HashMap, HashSet}, + path::Path, sync::Arc, }; -pub fn init(app: &mut MutableAppContext) { - app.add_action("workspace:save", WorkspaceView::save_active_item); - app.add_action("workspace:debug_elements", WorkspaceView::debug_elements); - app.add_bindings(vec![ - Binding::new("cmd-s", "workspace:save", None), - Binding::new("cmd-alt-i", "workspace:debug_elements", None), - ]); +pub struct OpenParams { + pub paths: Vec, + pub settings: watch::Receiver, +} + +fn open(settings: &Receiver, ctx: &mut MutableAppContext) { + let settings = settings.clone(); + ctx.prompt_for_paths( + PathPromptOptions { + files: true, + directories: true, + multiple: true, + }, + move |paths, ctx| { + if let Some(paths) = paths { + ctx.dispatch_global_action("workspace:open_paths", OpenParams { paths, settings }); + } + }, + ); +} + +fn open_paths(params: &OpenParams, app: &mut MutableAppContext) { + log::info!("open paths {:?}", params.paths); + + // Open paths in existing workspace if possible + for window_id in app.window_ids().collect::>() { + if let Some(handle) = app.root_view::(window_id) { + if handle.update(app, |view, ctx| { + if view.contains_paths(¶ms.paths, ctx.as_ref()) { + let open_paths = view.open_paths(¶ms.paths, ctx); + ctx.foreground().spawn(open_paths).detach(); + log::info!("open paths on existing workspace"); + true + } else { + false + } + }) { + return; + } + } + } + + log::info!("open new workspace"); + + // Add a new workspace if necessary + app.add_window(|ctx| { + let mut view = Workspace::new(0, params.settings.clone(), ctx); + let open_paths = view.open_paths(¶ms.paths, ctx); + ctx.foreground().spawn(open_paths).detach(); + view + }); +} + +fn quit(_: &(), app: &mut MutableAppContext) { + app.platform().quit(); } pub trait ItemView: View { @@ -122,24 +196,27 @@ pub struct State { pub center: PaneGroup, } -pub struct WorkspaceView { - pub workspace: ModelHandle, +pub struct Workspace { pub settings: watch::Receiver, modal: Option, 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>>>, + >, } -impl WorkspaceView { +impl Workspace { pub fn new( - workspace: ModelHandle, + replica_id: ReplicaId, settings: watch::Receiver, ctx: &mut ViewContext, ) -> Self { - ctx.observe(&workspace, Self::workspace_updated); - let pane = ctx.add_view(|_| Pane::new(settings.clone())); let pane_id = pane.id(); ctx.subscribe_to_view(&pane, move |me, _, event, ctx| { @@ -147,29 +224,65 @@ impl WorkspaceView { }); ctx.focus(&pane); - WorkspaceView { - workspace, + Workspace { modal: None, 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(), } } + pub fn worktrees(&self) -> &HashSet> { + &self.worktrees + } + pub fn contains_paths(&self, paths: &[PathBuf], app: &AppContext) -> bool { - self.workspace.read(app).contains_paths(paths, app) + paths.iter().all(|path| self.contains_path(&path, app)) + } + + pub fn contains_path(&self, path: &Path, app: &AppContext) -> bool { + self.worktrees + .iter() + .any(|worktree| worktree.read(app).contains_abs_path(path)) + } + + pub fn worktree_scans_complete(&self, ctx: &AppContext) -> impl Future + 'static { + let futures = self + .worktrees + .iter() + .map(|worktree| worktree.read(ctx).scan_complete()) + .collect::>(); + async move { + for future in futures { + future.await; + } + } } pub fn open_paths( - &self, + &mut self, paths: &[PathBuf], ctx: &mut ViewContext, ) -> impl Future { - let entries = self - .workspace - .update(ctx, |workspace, ctx| workspace.open_paths(paths, ctx)); + 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()) + }) + .collect::>(); + let bg = ctx.background_executor().clone(); let tasks = paths .iter() @@ -197,6 +310,15 @@ impl WorkspaceView { } } + pub fn add_worktree(&mut self, path: &Path, ctx: &mut ViewContext) -> usize { + 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); + ctx.notify(); + worktree_id + } + pub fn toggle_modal(&mut self, ctx: &mut ViewContext, add_view: F) where V: 'static + View, @@ -241,31 +363,81 @@ impl WorkspaceView { return None; } - self.loading_entries.insert(entry.clone()); + let (worktree_id, path) = entry.clone(); - match self.workspace.update(ctx, |workspace, ctx| { - workspace.open_entry(entry.clone(), ctx) - }) { - Err(error) => { - error!("{}", error); - None + let worktree = match self.worktrees.get(&worktree_id).cloned() { + Some(worktree) => worktree, + None => { + log::error!("worktree {} does not exist", worktree_id); + return None; } - Ok(item) => { - let settings = self.settings.clone(); - Some(ctx.spawn(item, move |me, item, ctx| { - me.loading_entries.remove(&entry); - match item { - Ok(item) => { - let item_view = item.add_view(ctx.window_id(), settings, ctx.as_mut()); - me.add_item(item_view, ctx); - } - Err(error) => { - error!("{}", error); - } - } - })) + }; + + 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()); + + if let Entry::Vacant(entry) = self.buffers.entry((worktree_id, inode)) { + 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 + .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)), + Err(error) => Err(Arc::new(error)), + }) + }) + .detach() } + + let mut watch = self.buffers.get(&(worktree_id, inode)).unwrap().clone(); + Some(ctx.spawn( + async move { + loop { + if let Some(load_result) = watch.borrow().as_ref() { + return load_result.clone(); + } + watch.next().await; + } + }, + move |me, load_result, ctx| { + me.loading_entries.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); + } + Err(error) => { + log::error!("error opening item: {}", error); + } + } + }, + )) } pub fn save_active_item(&mut self, _: &(), ctx: &mut ViewContext) { @@ -299,10 +471,6 @@ impl WorkspaceView { }; } - fn workspace_updated(&mut self, _: ModelHandle, ctx: &mut ViewContext) { - ctx.notify(); - } - fn add_pane(&mut self, ctx: &mut ViewContext) -> ViewHandle { let pane = ctx.add_view(|_| Pane::new(self.settings.clone())); let pane_id = pane.id(); @@ -388,11 +556,11 @@ impl WorkspaceView { } } -impl Entity for WorkspaceView { +impl Entity for Workspace { type Event = (); } -impl View for WorkspaceView { +impl View for Workspace { fn ui_name() -> &'static str { "Workspace" } @@ -414,13 +582,95 @@ impl View for WorkspaceView { } } +#[cfg(test)] +pub trait WorkspaceHandle { + fn file_entries(&self, app: &AppContext) -> Vec<(usize, Arc)>; +} + +#[cfg(test)] +impl WorkspaceHandle for ViewHandle { + fn file_entries(&self, app: &AppContext) -> Vec<(usize, Arc)> { + self.read(app) + .worktrees() + .iter() + .flat_map(|tree| { + let tree_id = tree.id(); + tree.read(app) + .files(0) + .map(move |f| (tree_id, f.path().clone())) + }) + .collect::>() + } +} + #[cfg(test)] mod tests { - use super::{pane, Workspace, WorkspaceView}; - use crate::{settings, test::temp_tree, workspace::WorkspaceHandle as _}; + use super::*; + use crate::{editor::BufferView, settings, test::temp_tree}; use gpui::App; use serde_json::json; - use std::collections::HashSet; + use std::{collections::HashSet, os::unix}; + + #[test] + fn test_open_paths_action() { + App::test((), |app| { + let settings = settings::channel(&app.font_cache()).unwrap().1; + + init(app); + + let dir = temp_tree(json!({ + "a": { + "aa": null, + "ab": null, + }, + "b": { + "ba": null, + "bb": null, + }, + "c": { + "ca": null, + "cb": null, + }, + })); + + app.dispatch_global_action( + "workspace:open_paths", + OpenParams { + paths: vec![ + dir.path().join("a").to_path_buf(), + dir.path().join("b").to_path_buf(), + ], + settings: settings.clone(), + }, + ); + assert_eq!(app.window_ids().count(), 1); + + app.dispatch_global_action( + "workspace:open_paths", + OpenParams { + paths: vec![dir.path().join("a").to_path_buf()], + settings: settings.clone(), + }, + ); + assert_eq!(app.window_ids().count(), 1); + let workspace_view_1 = app + .root_view::(app.window_ids().next().unwrap()) + .unwrap(); + assert_eq!(workspace_view_1.read(app).worktrees().len(), 2); + + app.dispatch_global_action( + "workspace:open_paths", + OpenParams { + paths: vec![ + dir.path().join("b").to_path_buf(), + dir.path().join("c").to_path_buf(), + ], + settings: settings.clone(), + }, + ); + assert_eq!(app.window_ids().count(), 2); + }); + } #[test] fn test_open_entry() { @@ -434,7 +684,13 @@ mod tests { })); let settings = settings::channel(&app.font_cache()).unwrap().1; - let workspace = app.add_model(|ctx| Workspace::new(vec![dir.path().into()], ctx)); + + let (_, workspace) = app.add_window(|ctx| { + let mut workspace = Workspace::new(0, settings, ctx); + workspace.add_worktree(dir.path(), ctx); + workspace + }); + app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx)) .await; let entries = app.read(|ctx| workspace.file_entries(ctx)); @@ -442,12 +698,10 @@ mod tests { let file2 = entries[1].clone(); let file3 = entries[2].clone(); - let (_, workspace_view) = - app.add_window(|ctx| WorkspaceView::new(workspace.clone(), settings, ctx)); - let pane = app.read(|ctx| workspace_view.read(ctx).active_pane().clone()); + let pane = app.read(|ctx| workspace.read(ctx).active_pane().clone()); // Open the first entry - workspace_view + workspace .update(&mut app, |w, ctx| w.open_entry(file1.clone(), ctx)) .unwrap() .await; @@ -461,7 +715,7 @@ mod tests { }); // Open the second entry - workspace_view + workspace .update(&mut app, |w, ctx| w.open_entry(file2.clone(), ctx)) .unwrap() .await; @@ -475,7 +729,7 @@ mod tests { }); // Open the first entry again. The existing pane item is activated. - workspace_view.update(&mut app, |w, ctx| { + workspace.update(&mut app, |w, ctx| { assert!(w.open_entry(file1.clone(), ctx).is_none()) }); app.read(|ctx| { @@ -488,7 +742,7 @@ mod tests { }); // Open the third entry twice concurrently. Only one pane item is added. - workspace_view + workspace .update(&mut app, |w, ctx| { let task = w.open_entry(file3.clone(), ctx).unwrap(); assert!(w.open_entry(file3.clone(), ctx).is_none()); @@ -516,22 +770,24 @@ mod tests { "b.txt": "", })); - let workspace = app.add_model(|ctx| Workspace::new(vec![dir1.path().into()], ctx)); let settings = settings::channel(&app.font_cache()).unwrap().1; - let (_, workspace_view) = - app.add_window(|ctx| WorkspaceView::new(workspace.clone(), settings, ctx)); + let (_, workspace) = app.add_window(|ctx| { + let mut workspace = Workspace::new(0, settings, ctx); + workspace.add_worktree(dir1.path(), ctx); + workspace + }); app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx)) .await; // Open a file within an existing worktree. app.update(|ctx| { - workspace_view.update(ctx, |view, ctx| { + workspace.update(ctx, |view, ctx| { view.open_paths(&[dir1.path().join("a.txt")], ctx) }) }) .await; app.read(|ctx| { - workspace_view + workspace .read(ctx) .active_pane() .read(ctx) @@ -543,7 +799,7 @@ mod tests { // Open a file outside of any existing worktree. app.update(|ctx| { - workspace_view.update(ctx, |view, ctx| { + workspace.update(ctx, |view, ctx| { view.open_paths(&[dir2.path().join("b.txt")], ctx) }) }) @@ -563,7 +819,7 @@ mod tests { ); }); app.read(|ctx| { - workspace_view + workspace .read(ctx) .active_pane() .read(ctx) @@ -575,6 +831,67 @@ mod tests { }); } + #[test] + fn test_open_two_paths_to_the_same_file() { + use crate::workspace::ItemViewHandle; + + 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 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 + }); + 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| { + workspace + .read(ctx) + .active_pane() + .read(ctx) + .items() + .iter() + .map(|i| i.to_any().downcast::().unwrap()) + .collect::>() + }); + 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"); + }); + + // 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); + }); + }); + 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"); + }); + }); + } + #[test] fn test_pane_actions() { App::test_async((), |mut app| async move { @@ -589,17 +906,19 @@ mod tests { })); let settings = settings::channel(&app.font_cache()).unwrap().1; - let workspace = app.add_model(|ctx| Workspace::new(vec![dir.path().into()], ctx)); + let (window_id, workspace) = app.add_window(|ctx| { + let mut workspace = Workspace::new(0, settings, ctx); + workspace.add_worktree(dir.path(), ctx); + workspace + }); app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx)) .await; let entries = app.read(|ctx| workspace.file_entries(ctx)); let file1 = entries[0].clone(); - let (window_id, workspace_view) = - app.add_window(|ctx| WorkspaceView::new(workspace.clone(), settings, ctx)); - let pane_1 = app.read(|ctx| workspace_view.read(ctx).active_pane().clone()); + let pane_1 = app.read(|ctx| workspace.read(ctx).active_pane().clone()); - workspace_view + workspace .update(&mut app, |w, ctx| w.open_entry(file1.clone(), ctx)) .unwrap() .await; @@ -612,14 +931,14 @@ mod tests { app.dispatch_action(window_id, vec![pane_1.id()], "pane:split_right", ()); app.update(|ctx| { - let pane_2 = workspace_view.read(ctx).active_pane().clone(); + let pane_2 = workspace.read(ctx).active_pane().clone(); assert_ne!(pane_1, pane_2); let pane2_item = pane_2.read(ctx).active_item().unwrap(); assert_eq!(pane2_item.entry_id(ctx.as_ref()), Some(file1.clone())); ctx.dispatch_action(window_id, vec![pane_2.id()], "pane:close_active_item", ()); - let workspace_view = workspace_view.read(ctx); + let workspace_view = workspace.read(ctx); assert_eq!(workspace_view.panes.len(), 1); assert_eq!(workspace_view.active_pane(), &pane_1); }); diff --git a/zed/src/workspace/mod.rs b/zed/src/workspace/mod.rs deleted file mode 100644 index 504bb9a8c05b4965487820d745682e49395a0165..0000000000000000000000000000000000000000 --- a/zed/src/workspace/mod.rs +++ /dev/null @@ -1,159 +0,0 @@ -pub mod pane; -pub mod pane_group; -pub mod workspace; -pub mod workspace_view; - -pub use pane::*; -pub use pane_group::*; -pub use workspace::*; -pub use workspace_view::*; - -use crate::{ - settings::Settings, - watch::{self, Receiver}, -}; -use gpui::{MutableAppContext, PathPromptOptions}; -use std::path::PathBuf; - -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); - pane::init(app); - workspace_view::init(app); -} - -pub struct OpenParams { - pub paths: Vec, - pub settings: watch::Receiver, -} - -fn open(settings: &Receiver, ctx: &mut MutableAppContext) { - let settings = settings.clone(); - ctx.prompt_for_paths( - PathPromptOptions { - files: true, - directories: true, - multiple: true, - }, - move |paths, ctx| { - if let Some(paths) = paths { - ctx.dispatch_global_action("workspace:open_paths", OpenParams { paths, settings }); - } - }, - ); -} - -fn open_paths(params: &OpenParams, app: &mut MutableAppContext) { - log::info!("open paths {:?}", params.paths); - - // Open paths in existing workspace if possible - for window_id in app.window_ids().collect::>() { - if let Some(handle) = app.root_view::(window_id) { - if handle.update(app, |view, ctx| { - if view.contains_paths(¶ms.paths, ctx.as_ref()) { - let open_paths = view.open_paths(¶ms.paths, ctx); - ctx.foreground().spawn(open_paths).detach(); - log::info!("open paths on existing workspace"); - true - } else { - false - } - }) { - return; - } - } - } - - log::info!("open new workspace"); - - // Add a new workspace if necessary - let workspace = app.add_model(|ctx| Workspace::new(vec![], ctx)); - app.add_window(|ctx| { - let view = WorkspaceView::new(workspace, params.settings.clone(), ctx); - let open_paths = view.open_paths(¶ms.paths, ctx); - ctx.foreground().spawn(open_paths).detach(); - view - }); -} - -fn quit(_: &(), app: &mut MutableAppContext) { - app.platform().quit(); -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{settings, test::*}; - use gpui::App; - use serde_json::json; - - #[test] - fn test_open_paths_action() { - App::test((), |app| { - let settings = settings::channel(&app.font_cache()).unwrap().1; - - init(app); - - let dir = temp_tree(json!({ - "a": { - "aa": null, - "ab": null, - }, - "b": { - "ba": null, - "bb": null, - }, - "c": { - "ca": null, - "cb": null, - }, - })); - - app.dispatch_global_action( - "workspace:open_paths", - OpenParams { - paths: vec![ - dir.path().join("a").to_path_buf(), - dir.path().join("b").to_path_buf(), - ], - settings: settings.clone(), - }, - ); - assert_eq!(app.window_ids().count(), 1); - - app.dispatch_global_action( - "workspace:open_paths", - OpenParams { - paths: vec![dir.path().join("a").to_path_buf()], - settings: settings.clone(), - }, - ); - assert_eq!(app.window_ids().count(), 1); - let workspace_view_1 = app - .root_view::(app.window_ids().next().unwrap()) - .unwrap(); - assert_eq!( - workspace_view_1 - .read(app) - .workspace - .read(app) - .worktrees() - .len(), - 2 - ); - - app.dispatch_global_action( - "workspace:open_paths", - OpenParams { - paths: vec![ - dir.path().join("b").to_path_buf(), - dir.path().join("c").to_path_buf(), - ], - settings: settings.clone(), - }, - ); - assert_eq!(app.window_ids().count(), 2); - }); - } -} diff --git a/zed/src/workspace/workspace.rs b/zed/src/workspace/workspace.rs deleted file mode 100644 index 0f0272957260f4c4dbb952ab44eb3ab3475ea0a4..0000000000000000000000000000000000000000 --- a/zed/src/workspace/workspace.rs +++ /dev/null @@ -1,298 +0,0 @@ -use super::{ItemView, ItemViewHandle}; -use crate::{ - editor::{Buffer, History}, - settings::Settings, - time::ReplicaId, - watch, - worktree::{Worktree, WorktreeHandle as _}, -}; -use anyhow::anyhow; -use gpui::{AppContext, Entity, Handle, ModelContext, ModelHandle, MutableAppContext, ViewContext}; -use smol::prelude::*; -use std::{ - collections::{HashMap, HashSet}, - fmt::Debug, - path::{Path, PathBuf}, - pin::Pin, - sync::Arc, -}; - -pub trait Item -where - Self: Sized, -{ - type View: ItemView; - fn build_view( - handle: ModelHandle, - settings: watch::Receiver, - ctx: &mut ViewContext, - ) -> Self::View; -} - -pub trait ItemHandle: Debug + Send + Sync { - fn add_view( - &self, - window_id: usize, - settings: watch::Receiver, - app: &mut MutableAppContext, - ) -> Box; - fn id(&self) -> usize; - fn boxed_clone(&self) -> Box; -} - -impl ItemHandle for ModelHandle { - 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 id(&self) -> usize { - Handle::id(self) - } - - fn boxed_clone(&self) -> Box { - Box::new(self.clone()) - } -} - -impl Clone for Box { - fn clone(&self) -> Self { - self.boxed_clone() - } -} - -pub type OpenResult = Result, Arc>; - -#[derive(Clone)] -enum OpenedItem { - Loading(watch::Receiver>), - Loaded(Box), -} - -pub struct Workspace { - replica_id: ReplicaId, - worktrees: HashSet>, - items: HashMap<(usize, u64), OpenedItem>, -} - -impl Workspace { - pub fn new(paths: Vec, ctx: &mut ModelContext) -> Self { - let mut workspace = Self { - replica_id: 0, - worktrees: HashSet::new(), - items: HashMap::new(), - }; - workspace.open_paths(&paths, ctx); - workspace - } - - pub fn worktrees(&self) -> &HashSet> { - &self.worktrees - } - - pub fn worktree_scans_complete(&self, ctx: &AppContext) -> impl Future + 'static { - let futures = self - .worktrees - .iter() - .map(|worktree| worktree.read(ctx).scan_complete()) - .collect::>(); - async move { - for future in futures { - future.await; - } - } - } - - pub fn contains_paths(&self, paths: &[PathBuf], app: &AppContext) -> bool { - paths.iter().all(|path| self.contains_path(&path, app)) - } - - pub fn contains_path(&self, path: &Path, app: &AppContext) -> bool { - self.worktrees - .iter() - .any(|worktree| worktree.read(app).contains_abs_path(path)) - } - - pub fn open_paths( - &mut self, - paths: &[PathBuf], - ctx: &mut ModelContext, - ) -> Vec<(usize, Arc)> { - paths - .iter() - .cloned() - .map(move |path| self.open_path(path, ctx)) - .collect() - } - - fn open_path(&mut self, path: PathBuf, ctx: &mut ModelContext) -> (usize, Arc) { - 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 = ctx.add_model(|ctx| Worktree::new(path.clone(), ctx)); - let worktree_id = worktree.id(); - ctx.observe(&worktree, Self::on_worktree_updated); - self.worktrees.insert(worktree); - ctx.notify(); - (worktree_id, Path::new("").into()) - } - - pub fn open_entry( - &mut self, - (worktree_id, path): (usize, Arc), - ctx: &mut ModelContext<'_, Self>, - ) -> anyhow::Result + Send>>> { - let worktree = self - .worktrees - .get(&worktree_id) - .cloned() - .ok_or_else(|| anyhow!("worktree {} does not exist", worktree_id,))?; - - let inode = worktree - .read(ctx) - .inode_for_path(&path) - .ok_or_else(|| anyhow!("path {:?} does not exist", path))?; - - let item_key = (worktree_id, inode); - if let Some(item) = self.items.get(&item_key).cloned() { - return Ok(async move { - match item { - OpenedItem::Loaded(handle) => { - return Ok(handle); - } - OpenedItem::Loading(rx) => loop { - rx.updated().await; - - if let Some(result) = smol::block_on(rx.read()).clone() { - return result; - } - }, - } - } - .boxed()); - } - - let replica_id = self.replica_id; - let file = worktree.file(path.clone(), ctx.as_ref())?; - let history = file.load_history(ctx.as_ref()); - - let (mut tx, rx) = watch::channel(None); - self.items.insert(item_key, OpenedItem::Loading(rx)); - ctx.spawn( - history, - move |me, history: anyhow::Result, ctx| match history { - Ok(history) => { - let handle = Box::new( - ctx.add_model(|ctx| Buffer::from_history(replica_id, file, history, ctx)), - ) as Box; - me.items - .insert(item_key, OpenedItem::Loaded(handle.clone())); - ctx.spawn( - async move { - tx.update(|value| *value = Some(Ok(handle))).await; - }, - |_, _, _| {}, - ) - .detach(); - } - Err(error) => { - ctx.spawn( - async move { - tx.update(|value| *value = Some(Err(Arc::new(error)))).await; - }, - |_, _, _| {}, - ) - .detach(); - } - }, - ) - .detach(); - - self.open_entry((worktree_id, path), ctx) - } - - fn on_worktree_updated(&mut self, _: ModelHandle, ctx: &mut ModelContext) { - ctx.notify(); - } -} - -impl Entity for Workspace { - type Event = (); -} - -#[cfg(test)] -pub trait WorkspaceHandle { - fn file_entries(&self, app: &AppContext) -> Vec<(usize, Arc)>; -} - -#[cfg(test)] -impl WorkspaceHandle for ModelHandle { - fn file_entries(&self, app: &AppContext) -> Vec<(usize, Arc)> { - self.read(app) - .worktrees() - .iter() - .flat_map(|tree| { - let tree_id = tree.id(); - tree.read(app) - .files(0) - .map(move |f| (tree_id, f.path().clone())) - }) - .collect::>() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::test::temp_tree; - use gpui::App; - use serde_json::json; - - #[test] - fn test_open_entry() { - App::test_async((), |mut app| async move { - let dir = temp_tree(json!({ - "a": { - "aa": "aa contents", - "ab": "ab contents", - }, - })); - - let workspace = app.add_model(|ctx| Workspace::new(vec![dir.path().into()], ctx)); - app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx)) - .await; - - // Get the first file entry. - let tree = app.read(|ctx| workspace.read(ctx).worktrees.iter().next().unwrap().clone()); - let path = app.read(|ctx| tree.read(ctx).files(0).next().unwrap().path().clone()); - let entry = (tree.id(), path); - - // Open the same entry twice before it finishes loading. - let (future_1, future_2) = workspace.update(&mut app, |w, app| { - ( - w.open_entry(entry.clone(), app).unwrap(), - w.open_entry(entry.clone(), app).unwrap(), - ) - }); - - let handle_1 = future_1.await.unwrap(); - let handle_2 = future_2.await.unwrap(); - assert_eq!(handle_1.id(), handle_2.id()); - - // Open the same entry again now that it has loaded - let handle_3 = workspace - .update(&mut app, |w, app| w.open_entry(entry, app).unwrap()) - .await - .unwrap(); - - assert_eq!(handle_3.id(), handle_1.id()); - }) - } -} diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 7152316938a9a7b917d11c6ef61d18cbeb79d53c..ea023fb813d49f2260abbea835aa97b07a33470e 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -7,9 +7,9 @@ use crate::{ sum_tree::{self, Cursor, Edit, SeekBias, SumTree}, }; use ::ignore::gitignore::Gitignore; -use anyhow::{anyhow, Context, Result}; +use anyhow::{Context, Result}; pub use fuzzy::{match_paths, PathMatch}; -use gpui::{scoped_pool, AppContext, Entity, ModelContext, ModelHandle, Task}; +use gpui::{scoped_pool, AppContext, Entity, ModelContext, ModelHandle, Task, View, ViewContext}; use lazy_static::lazy_static; use parking_lot::Mutex; use postage::{ @@ -419,14 +419,14 @@ impl FileHandle { (self.worktree.id(), self.path()) } - pub fn observe_from_model( + pub fn observe_from_view( &self, - ctx: &mut ModelContext, - mut callback: impl FnMut(&mut T, FileHandle, &mut ModelContext) + 'static, + ctx: &mut ViewContext, + mut callback: impl FnMut(&mut T, FileHandle, &mut ViewContext) + 'static, ) { let mut prev_state = self.state.lock().clone(); let cur_state = Arc::downgrade(&self.state); - ctx.observe(&self.worktree, move |observer, worktree, ctx| { + ctx.observe_model(&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 { @@ -1126,15 +1126,14 @@ struct UpdateIgnoreStatusJob { } pub trait WorktreeHandle { - fn file(&self, path: impl AsRef, app: &AppContext) -> Result; + fn file(&self, path: impl AsRef, app: &AppContext) -> Option; } impl WorktreeHandle for ModelHandle { - fn file(&self, path: impl AsRef, app: &AppContext) -> Result { + fn file(&self, path: impl AsRef, app: &AppContext) -> Option { let tree = self.read(app); - let entry = tree - .entry_for_path(&path) - .ok_or_else(|| anyhow!("path does not exist in tree"))?; + 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) { @@ -1148,7 +1147,7 @@ impl WorktreeHandle for ModelHandle { state }; - Ok(FileHandle { + Some(FileHandle { worktree: self.clone(), state, }) @@ -1347,8 +1346,7 @@ 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(|ctx| Buffer::new(1, "a line of text.\n".repeat(10 * 1024), ctx)); + let buffer = app.add_model(|_| Buffer::new(1, "a line of text.\n".repeat(10 * 1024))); let path = tree.update(&mut app, |tree, ctx| { let path = tree.files(0).next().unwrap().path().clone();