workspace_view.rs

  1use super::{pane, Pane, PaneGroup, SplitDirection, Workspace};
  2use crate::{settings::Settings, watch};
  3use futures_core::future::LocalBoxFuture;
  4use gpui::{
  5    color::rgbu, elements::*, keymap::Binding, AnyViewHandle, App, AppContext, Entity, ModelHandle,
  6    MutableAppContext, View, ViewContext, ViewHandle,
  7};
  8use log::{error, info};
  9use std::{collections::HashSet, path::PathBuf};
 10
 11pub fn init(app: &mut App) {
 12    app.add_action("workspace:save", WorkspaceView::save_active_item);
 13    app.add_bindings(vec![Binding::new("cmd-s", "workspace:save", None)]);
 14}
 15
 16pub trait ItemView: View {
 17    fn title(&self, app: &AppContext) -> String;
 18    fn entry_id(&self, app: &AppContext) -> Option<(usize, usize)>;
 19    fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
 20    where
 21        Self: Sized,
 22    {
 23        None
 24    }
 25    fn is_dirty(&self, _: &AppContext) -> bool {
 26        false
 27    }
 28    fn save(&self, _: &mut ViewContext<Self>) -> LocalBoxFuture<'static, anyhow::Result<()>> {
 29        Box::pin(async { Ok(()) })
 30    }
 31    fn should_activate_item_on_event(_: &Self::Event) -> bool {
 32        false
 33    }
 34    fn should_update_tab_on_event(_: &Self::Event) -> bool {
 35        false
 36    }
 37}
 38
 39pub trait ItemViewHandle: Send + Sync {
 40    fn title(&self, app: &AppContext) -> String;
 41    fn entry_id(&self, app: &AppContext) -> Option<(usize, usize)>;
 42    fn boxed_clone(&self) -> Box<dyn ItemViewHandle>;
 43    fn clone_on_split(&self, app: &mut MutableAppContext) -> Option<Box<dyn ItemViewHandle>>;
 44    fn set_parent_pane(&self, pane: &ViewHandle<Pane>, app: &mut MutableAppContext);
 45    fn id(&self) -> usize;
 46    fn to_any(&self) -> AnyViewHandle;
 47    fn is_dirty(&self, ctx: &AppContext) -> bool;
 48    fn save(&self, ctx: &mut MutableAppContext) -> LocalBoxFuture<'static, anyhow::Result<()>>;
 49}
 50
 51impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
 52    fn title(&self, app: &AppContext) -> String {
 53        self.as_ref(app).title(app)
 54    }
 55
 56    fn entry_id(&self, app: &AppContext) -> Option<(usize, usize)> {
 57        self.as_ref(app).entry_id(app)
 58    }
 59
 60    fn boxed_clone(&self) -> Box<dyn ItemViewHandle> {
 61        Box::new(self.clone())
 62    }
 63
 64    fn clone_on_split(&self, app: &mut MutableAppContext) -> Option<Box<dyn ItemViewHandle>> {
 65        self.update(app, |item, ctx| {
 66            ctx.add_option_view(|ctx| item.clone_on_split(ctx))
 67        })
 68        .map(|handle| Box::new(handle) as Box<dyn ItemViewHandle>)
 69    }
 70
 71    fn set_parent_pane(&self, pane: &ViewHandle<Pane>, app: &mut MutableAppContext) {
 72        pane.update(app, |_, ctx| {
 73            ctx.subscribe_to_view(self, |pane, item, event, ctx| {
 74                if T::should_activate_item_on_event(event) {
 75                    if let Some(ix) = pane.item_index(&item) {
 76                        pane.activate_item(ix, ctx);
 77                        pane.activate(ctx);
 78                    }
 79                }
 80                if T::should_update_tab_on_event(event) {
 81                    ctx.notify()
 82                }
 83            })
 84        })
 85    }
 86
 87    fn save(&self, ctx: &mut MutableAppContext) -> LocalBoxFuture<'static, anyhow::Result<()>> {
 88        self.update(ctx, |item, ctx| item.save(ctx))
 89    }
 90
 91    fn is_dirty(&self, ctx: &AppContext) -> bool {
 92        self.as_ref(ctx).is_dirty(ctx)
 93    }
 94
 95    fn id(&self) -> usize {
 96        self.id()
 97    }
 98
 99    fn to_any(&self) -> AnyViewHandle {
100        self.into()
101    }
102}
103
104impl Clone for Box<dyn ItemViewHandle> {
105    fn clone(&self) -> Box<dyn ItemViewHandle> {
106        self.boxed_clone()
107    }
108}
109
110#[derive(Debug)]
111pub struct State {
112    pub modal: Option<usize>,
113    pub center: PaneGroup,
114}
115
116pub struct WorkspaceView {
117    pub workspace: ModelHandle<Workspace>,
118    pub settings: watch::Receiver<Settings>,
119    modal: Option<AnyViewHandle>,
120    center: PaneGroup,
121    panes: Vec<ViewHandle<Pane>>,
122    active_pane: ViewHandle<Pane>,
123    loading_entries: HashSet<(usize, usize)>,
124}
125
126impl WorkspaceView {
127    pub fn new(
128        workspace: ModelHandle<Workspace>,
129        settings: watch::Receiver<Settings>,
130        ctx: &mut ViewContext<Self>,
131    ) -> Self {
132        ctx.observe(&workspace, Self::workspace_updated);
133
134        let pane = ctx.add_view(|_| Pane::new(settings.clone()));
135        let pane_id = pane.id();
136        ctx.subscribe_to_view(&pane, move |me, _, event, ctx| {
137            me.handle_pane_event(pane_id, event, ctx)
138        });
139        ctx.focus(&pane);
140
141        WorkspaceView {
142            workspace,
143            modal: None,
144            center: PaneGroup::new(pane.id()),
145            panes: vec![pane.clone()],
146            active_pane: pane.clone(),
147            loading_entries: HashSet::new(),
148            settings,
149        }
150    }
151
152    pub fn contains_paths(&self, paths: &[PathBuf], app: &AppContext) -> bool {
153        self.workspace.as_ref(app).contains_paths(paths, app)
154    }
155
156    pub fn open_paths(&self, paths: &[PathBuf], app: &mut MutableAppContext) {
157        self.workspace
158            .update(app, |workspace, ctx| workspace.open_paths(paths, ctx));
159    }
160
161    pub fn toggle_modal<V, F>(&mut self, ctx: &mut ViewContext<Self>, add_view: F)
162    where
163        V: 'static + View,
164        F: FnOnce(&mut ViewContext<Self>, &mut Self) -> ViewHandle<V>,
165    {
166        if self.modal.as_ref().map_or(false, |modal| modal.is::<V>()) {
167            self.modal.take();
168            ctx.focus_self();
169        } else {
170            let modal = add_view(ctx, self);
171            ctx.focus(&modal);
172            self.modal = Some(modal.into());
173        }
174        ctx.notify();
175    }
176
177    pub fn modal(&self) -> Option<&AnyViewHandle> {
178        self.modal.as_ref()
179    }
180
181    pub fn dismiss_modal(&mut self, ctx: &mut ViewContext<Self>) {
182        if self.modal.take().is_some() {
183            ctx.focus(&self.active_pane);
184            ctx.notify();
185        }
186    }
187
188    pub fn open_entry(&mut self, entry: (usize, usize), ctx: &mut ViewContext<Self>) {
189        if self.loading_entries.contains(&entry) {
190            return;
191        }
192
193        if self
194            .active_pane()
195            .update(ctx, |pane, ctx| pane.activate_entry(entry, ctx))
196        {
197            return;
198        }
199
200        self.loading_entries.insert(entry);
201
202        match self
203            .workspace
204            .update(ctx, |workspace, ctx| workspace.open_entry(entry, ctx))
205        {
206            Err(error) => error!("{}", error),
207            Ok(item) => {
208                let settings = self.settings.clone();
209                ctx.spawn(item, move |me, item, ctx| {
210                    me.loading_entries.remove(&entry);
211                    match item {
212                        Ok(item) => {
213                            let item_view = item.add_view(ctx.window_id(), settings, ctx.app_mut());
214                            me.add_item(item_view, ctx);
215                        }
216                        Err(error) => {
217                            error!("{}", error);
218                        }
219                    }
220                })
221                .detach();
222            }
223        }
224    }
225
226    pub fn open_example_entry(&mut self, ctx: &mut ViewContext<Self>) {
227        if let Some(tree) = self.workspace.as_ref(ctx).worktrees().iter().next() {
228            if let Some(file) = tree.as_ref(ctx).files().next() {
229                info!("open_entry ({}, {})", tree.id(), file.entry_id);
230                self.open_entry((tree.id(), file.entry_id), ctx);
231            } else {
232                error!("No example file found for worktree {}", tree.id());
233            }
234        } else {
235            error!("No worktree found while opening example entry");
236        }
237    }
238
239    pub fn save_active_item(&mut self, _: &(), ctx: &mut ViewContext<Self>) {
240        self.active_pane.update(ctx, |pane, ctx| {
241            if let Some(item) = pane.active_item() {
242                let task = item.save(ctx.app_mut());
243                ctx.spawn(task, |_, result, _| {
244                    if let Err(e) = result {
245                        // TODO - present this error to the user
246                        error!("failed to save item: {:?}, ", e);
247                    }
248                })
249                .detach()
250            }
251        });
252    }
253
254    fn workspace_updated(&mut self, _: ModelHandle<Workspace>, ctx: &mut ViewContext<Self>) {
255        ctx.notify();
256    }
257
258    fn add_pane(&mut self, ctx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
259        let pane = ctx.add_view(|_| Pane::new(self.settings.clone()));
260        let pane_id = pane.id();
261        ctx.subscribe_to_view(&pane, move |me, _, event, ctx| {
262            me.handle_pane_event(pane_id, event, ctx)
263        });
264        self.panes.push(pane.clone());
265        self.activate_pane(pane.clone(), ctx);
266        pane
267    }
268
269    fn activate_pane(&mut self, pane: ViewHandle<Pane>, ctx: &mut ViewContext<Self>) {
270        self.active_pane = pane;
271        ctx.focus(&self.active_pane);
272        ctx.notify();
273    }
274
275    fn handle_pane_event(
276        &mut self,
277        pane_id: usize,
278        event: &pane::Event,
279        ctx: &mut ViewContext<Self>,
280    ) {
281        if let Some(pane) = self.pane(pane_id) {
282            match event {
283                pane::Event::Split(direction) => {
284                    self.split_pane(pane, *direction, ctx);
285                }
286                pane::Event::Remove => {
287                    self.remove_pane(pane, ctx);
288                }
289                pane::Event::Activate => {
290                    self.activate_pane(pane, ctx);
291                }
292            }
293        } else {
294            error!("pane {} not found", pane_id);
295        }
296    }
297
298    fn split_pane(
299        &mut self,
300        pane: ViewHandle<Pane>,
301        direction: SplitDirection,
302        ctx: &mut ViewContext<Self>,
303    ) -> ViewHandle<Pane> {
304        let new_pane = self.add_pane(ctx);
305        self.activate_pane(new_pane.clone(), ctx);
306        if let Some(item) = pane.as_ref(ctx).active_item() {
307            if let Some(clone) = item.clone_on_split(ctx.app_mut()) {
308                self.add_item(clone, ctx);
309            }
310        }
311        self.center
312            .split(pane.id(), new_pane.id(), direction)
313            .unwrap();
314        ctx.notify();
315        new_pane
316    }
317
318    fn remove_pane(&mut self, pane: ViewHandle<Pane>, ctx: &mut ViewContext<Self>) {
319        if self.center.remove(pane.id()).unwrap() {
320            self.panes.retain(|p| p != &pane);
321            self.activate_pane(self.panes.last().unwrap().clone(), ctx);
322        }
323    }
324
325    fn pane(&self, pane_id: usize) -> Option<ViewHandle<Pane>> {
326        self.panes.iter().find(|pane| pane.id() == pane_id).cloned()
327    }
328
329    pub fn active_pane(&self) -> &ViewHandle<Pane> {
330        &self.active_pane
331    }
332
333    fn add_item(&self, item: Box<dyn ItemViewHandle>, ctx: &mut ViewContext<Self>) {
334        let active_pane = self.active_pane();
335        item.set_parent_pane(&active_pane, ctx.app_mut());
336        active_pane.update(ctx, |pane, ctx| {
337            let item_idx = pane.add_item(item, ctx);
338            pane.activate_item(item_idx, ctx);
339        });
340    }
341}
342
343impl Entity for WorkspaceView {
344    type Event = ();
345}
346
347impl View for WorkspaceView {
348    fn ui_name() -> &'static str {
349        "Workspace"
350    }
351
352    fn render(&self, _: &AppContext) -> ElementBox {
353        Container::new(
354            // self.center.render(bump)
355            Stack::new()
356                .with_child(self.center.render())
357                .with_children(self.modal.as_ref().map(|m| ChildView::new(m.id()).boxed()))
358                .boxed(),
359        )
360        .with_background_color(rgbu(0xea, 0xea, 0xeb))
361        .boxed()
362    }
363
364    fn on_focus(&mut self, ctx: &mut ViewContext<Self>) {
365        ctx.focus(&self.active_pane);
366    }
367}
368
369#[cfg(test)]
370mod tests {
371    use super::{pane, Workspace, WorkspaceView};
372    use crate::{settings, test::temp_tree, workspace::WorkspaceHandle as _};
373    use anyhow::Result;
374    use gpui::App;
375    use serde_json::json;
376
377    #[test]
378    fn test_open_entry() -> Result<()> {
379        App::test((), |mut app| async move {
380            let dir = temp_tree(json!({
381                "a": {
382                    "aa": "aa contents",
383                    "ab": "ab contents",
384                    "ac": "ab contents",
385                },
386            }));
387
388            let settings = settings::channel(&app.font_cache()).unwrap().1;
389            let workspace = app.add_model(|ctx| Workspace::new(vec![dir.path().into()], ctx));
390            app.finish_pending_tasks().await; // Open and populate worktree.
391            let entries = workspace.file_entries(&app);
392
393            let (_, workspace_view) =
394                app.add_window(|ctx| WorkspaceView::new(workspace.clone(), settings, ctx));
395
396            // Open the first entry
397            workspace_view.update(&mut app, |w, ctx| w.open_entry(entries[0], ctx));
398            app.finish_pending_tasks().await;
399
400            workspace_view.read(&app, |w, app| {
401                assert_eq!(w.active_pane().as_ref(app).items().len(), 1);
402            });
403
404            // Open the second entry
405            workspace_view.update(&mut app, |w, ctx| w.open_entry(entries[1], ctx));
406            app.finish_pending_tasks().await;
407
408            workspace_view.read(&app, |w, app| {
409                let active_pane = w.active_pane().as_ref(app);
410                assert_eq!(active_pane.items().len(), 2);
411                assert_eq!(
412                    active_pane.active_item().unwrap().entry_id(app),
413                    Some(entries[1])
414                );
415            });
416
417            // Open the first entry again
418            workspace_view.update(&mut app, |w, ctx| w.open_entry(entries[0], ctx));
419            app.finish_pending_tasks().await;
420
421            workspace_view.read(&app, |w, app| {
422                let active_pane = w.active_pane().as_ref(app);
423                assert_eq!(active_pane.items().len(), 2);
424                assert_eq!(
425                    active_pane.active_item().unwrap().entry_id(app),
426                    Some(entries[0])
427                );
428            });
429
430            // Open the third entry twice concurrently
431            workspace_view.update(&mut app, |w, ctx| {
432                w.open_entry(entries[2], ctx);
433                w.open_entry(entries[2], ctx);
434            });
435            app.finish_pending_tasks().await;
436
437            workspace_view.read(&app, |w, app| {
438                assert_eq!(w.active_pane().as_ref(app).items().len(), 3);
439            });
440
441            Ok(())
442        })
443    }
444
445    #[test]
446    fn test_pane_actions() -> Result<()> {
447        App::test((), |mut app| async move {
448            pane::init(&mut app);
449
450            let dir = temp_tree(json!({
451                "a": {
452                    "aa": "aa contents",
453                    "ab": "ab contents",
454                    "ac": "ab contents",
455                },
456            }));
457
458            let settings = settings::channel(&app.font_cache()).unwrap().1;
459            let workspace = app.add_model(|ctx| Workspace::new(vec![dir.path().into()], ctx));
460            app.finish_pending_tasks().await; // Open and populate worktree.
461            let entries = workspace.file_entries(&app);
462
463            let (window_id, workspace_view) =
464                app.add_window(|ctx| WorkspaceView::new(workspace.clone(), settings, ctx));
465
466            workspace_view.update(&mut app, |w, ctx| w.open_entry(entries[0], ctx));
467            app.finish_pending_tasks().await;
468
469            let pane_1 = workspace_view.read(&app, |w, _| w.active_pane().clone());
470
471            app.dispatch_action(window_id, vec![pane_1.id()], "pane:split_right", ());
472            let pane_2 = workspace_view.read(&app, |w, _| w.active_pane().clone());
473            assert_ne!(pane_1, pane_2);
474
475            pane_2.read(&app, |p, app| {
476                assert_eq!(p.active_item().unwrap().entry_id(app), Some(entries[0]));
477            });
478
479            app.dispatch_action(window_id, vec![pane_2.id()], "pane:close_active_item", ());
480
481            workspace_view.read(&app, |w, _| {
482                assert_eq!(w.panes.len(), 1);
483                assert_eq!(w.active_pane(), &pane_1)
484            });
485
486            Ok(())
487        })
488    }
489}