workspace.rs

   1pub mod pane;
   2pub mod pane_group;
   3use crate::{
   4    editor::{Buffer, BufferView},
   5    settings::Settings,
   6    time::ReplicaId,
   7    worktree::{FileHandle, Worktree, WorktreeHandle},
   8};
   9use futures_core::Future;
  10use gpui::{
  11    color::rgbu, elements::*, json::to_string_pretty, keymap::Binding, AnyViewHandle, AppContext,
  12    ClipboardItem, Entity, ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, Task,
  13    View, ViewContext, ViewHandle, WeakModelHandle,
  14};
  15use log::error;
  16pub use pane::*;
  17pub use pane_group::*;
  18use postage::watch;
  19use smol::prelude::*;
  20use std::{collections::HashMap, path::PathBuf};
  21use std::{
  22    collections::{hash_map::Entry, HashSet},
  23    path::Path,
  24    sync::Arc,
  25};
  26
  27pub fn init(app: &mut MutableAppContext) {
  28    app.add_global_action("workspace:open", open);
  29    app.add_global_action("workspace:open_paths", open_paths);
  30    app.add_global_action("app:quit", quit);
  31    app.add_action("workspace:save", Workspace::save_active_item);
  32    app.add_action("workspace:debug_elements", Workspace::debug_elements);
  33    app.add_action("workspace:new_file", Workspace::open_new_file);
  34    app.add_bindings(vec![
  35        Binding::new("cmd-s", "workspace:save", None),
  36        Binding::new("cmd-alt-i", "workspace:debug_elements", None),
  37    ]);
  38    pane::init(app);
  39}
  40
  41pub struct OpenParams {
  42    pub paths: Vec<PathBuf>,
  43    pub settings: watch::Receiver<Settings>,
  44}
  45
  46fn open(settings: &watch::Receiver<Settings>, ctx: &mut MutableAppContext) {
  47    let settings = settings.clone();
  48    ctx.prompt_for_paths(
  49        PathPromptOptions {
  50            files: true,
  51            directories: true,
  52            multiple: true,
  53        },
  54        move |paths, ctx| {
  55            if let Some(paths) = paths {
  56                ctx.dispatch_global_action("workspace:open_paths", OpenParams { paths, settings });
  57            }
  58        },
  59    );
  60}
  61
  62fn open_paths(params: &OpenParams, app: &mut MutableAppContext) {
  63    log::info!("open paths {:?}", params.paths);
  64
  65    // Open paths in existing workspace if possible
  66    for window_id in app.window_ids().collect::<Vec<_>>() {
  67        if let Some(handle) = app.root_view::<Workspace>(window_id) {
  68            if handle.update(app, |view, ctx| {
  69                if view.contains_paths(&params.paths, ctx.as_ref()) {
  70                    let open_paths = view.open_paths(&params.paths, ctx);
  71                    ctx.foreground().spawn(open_paths).detach();
  72                    log::info!("open paths on existing workspace");
  73                    true
  74                } else {
  75                    false
  76                }
  77            }) {
  78                return;
  79            }
  80        }
  81    }
  82
  83    log::info!("open new workspace");
  84
  85    // Add a new workspace if necessary
  86    app.add_window(|ctx| {
  87        let mut view = Workspace::new(0, params.settings.clone(), ctx);
  88        let open_paths = view.open_paths(&params.paths, ctx);
  89        ctx.foreground().spawn(open_paths).detach();
  90        view
  91    });
  92}
  93
  94fn quit(_: &(), app: &mut MutableAppContext) {
  95    app.platform().quit();
  96}
  97
  98pub trait Item: Entity + Sized {
  99    type View: ItemView;
 100
 101    fn build_view(
 102        handle: ModelHandle<Self>,
 103        settings: watch::Receiver<Settings>,
 104        ctx: &mut ViewContext<Self::View>,
 105    ) -> Self::View;
 106
 107    fn file(&self) -> Option<&FileHandle>;
 108}
 109
 110pub trait ItemView: View {
 111    fn title(&self, app: &AppContext) -> String;
 112    fn entry_id(&self, app: &AppContext) -> Option<(usize, Arc<Path>)>;
 113    fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
 114    where
 115        Self: Sized,
 116    {
 117        None
 118    }
 119    fn is_dirty(&self, _: &AppContext) -> bool {
 120        false
 121    }
 122    fn has_conflict(&self, _: &AppContext) -> bool {
 123        false
 124    }
 125    fn save(
 126        &mut self,
 127        _: Option<FileHandle>,
 128        _: &mut ViewContext<Self>,
 129    ) -> Task<anyhow::Result<()>>;
 130    fn should_activate_item_on_event(_: &Self::Event) -> bool {
 131        false
 132    }
 133    fn should_update_tab_on_event(_: &Self::Event) -> bool {
 134        false
 135    }
 136}
 137
 138pub trait ItemHandle: Send + Sync {
 139    fn boxed_clone(&self) -> Box<dyn ItemHandle>;
 140    fn downgrade(&self) -> Box<dyn WeakItemHandle>;
 141}
 142
 143pub trait WeakItemHandle: Send + Sync {
 144    fn file<'a>(&'a self, ctx: &'a AppContext) -> Option<&'a FileHandle>;
 145    fn add_view(
 146        &self,
 147        window_id: usize,
 148        settings: watch::Receiver<Settings>,
 149        app: &mut MutableAppContext,
 150    ) -> Option<Box<dyn ItemViewHandle>>;
 151    fn alive(&self, ctx: &AppContext) -> bool;
 152}
 153
 154pub trait ItemViewHandle: Send + Sync {
 155    fn title(&self, app: &AppContext) -> String;
 156    fn entry_id(&self, app: &AppContext) -> Option<(usize, Arc<Path>)>;
 157    fn boxed_clone(&self) -> Box<dyn ItemViewHandle>;
 158    fn clone_on_split(&self, app: &mut MutableAppContext) -> Option<Box<dyn ItemViewHandle>>;
 159    fn set_parent_pane(&self, pane: &ViewHandle<Pane>, app: &mut MutableAppContext);
 160    fn id(&self) -> usize;
 161    fn to_any(&self) -> AnyViewHandle;
 162    fn is_dirty(&self, ctx: &AppContext) -> bool;
 163    fn has_conflict(&self, ctx: &AppContext) -> bool;
 164    fn save(
 165        &self,
 166        file: Option<FileHandle>,
 167        ctx: &mut MutableAppContext,
 168    ) -> Task<anyhow::Result<()>>;
 169}
 170
 171impl<T: Item> ItemHandle for ModelHandle<T> {
 172    fn boxed_clone(&self) -> Box<dyn ItemHandle> {
 173        Box::new(self.clone())
 174    }
 175
 176    fn downgrade(&self) -> Box<dyn WeakItemHandle> {
 177        Box::new(self.downgrade())
 178    }
 179}
 180
 181impl<T: Item> WeakItemHandle for WeakModelHandle<T> {
 182    fn file<'a>(&'a self, ctx: &'a AppContext) -> Option<&'a FileHandle> {
 183        self.upgrade(ctx).and_then(|h| h.read(ctx).file())
 184    }
 185
 186    fn add_view(
 187        &self,
 188        window_id: usize,
 189        settings: watch::Receiver<Settings>,
 190        ctx: &mut MutableAppContext,
 191    ) -> Option<Box<dyn ItemViewHandle>> {
 192        if let Some(handle) = self.upgrade(ctx.as_ref()) {
 193            Some(Box::new(ctx.add_view(window_id, |ctx| {
 194                T::build_view(handle, settings, ctx)
 195            })))
 196        } else {
 197            None
 198        }
 199    }
 200
 201    fn alive(&self, ctx: &AppContext) -> bool {
 202        self.upgrade(ctx).is_some()
 203    }
 204}
 205
 206impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
 207    fn title(&self, app: &AppContext) -> String {
 208        self.read(app).title(app)
 209    }
 210
 211    fn entry_id(&self, app: &AppContext) -> Option<(usize, Arc<Path>)> {
 212        self.read(app).entry_id(app)
 213    }
 214
 215    fn boxed_clone(&self) -> Box<dyn ItemViewHandle> {
 216        Box::new(self.clone())
 217    }
 218
 219    fn clone_on_split(&self, app: &mut MutableAppContext) -> Option<Box<dyn ItemViewHandle>> {
 220        self.update(app, |item, ctx| {
 221            ctx.add_option_view(|ctx| item.clone_on_split(ctx))
 222        })
 223        .map(|handle| Box::new(handle) as Box<dyn ItemViewHandle>)
 224    }
 225
 226    fn set_parent_pane(&self, pane: &ViewHandle<Pane>, app: &mut MutableAppContext) {
 227        pane.update(app, |_, ctx| {
 228            ctx.subscribe_to_view(self, |pane, item, event, ctx| {
 229                if T::should_activate_item_on_event(event) {
 230                    if let Some(ix) = pane.item_index(&item) {
 231                        pane.activate_item(ix, ctx);
 232                        pane.activate(ctx);
 233                    }
 234                }
 235                if T::should_update_tab_on_event(event) {
 236                    ctx.notify()
 237                }
 238            })
 239        })
 240    }
 241
 242    fn save(
 243        &self,
 244        file: Option<FileHandle>,
 245        ctx: &mut MutableAppContext,
 246    ) -> Task<anyhow::Result<()>> {
 247        self.update(ctx, |item, ctx| item.save(file, ctx))
 248    }
 249
 250    fn is_dirty(&self, ctx: &AppContext) -> bool {
 251        self.read(ctx).is_dirty(ctx)
 252    }
 253
 254    fn has_conflict(&self, ctx: &AppContext) -> bool {
 255        self.read(ctx).has_conflict(ctx)
 256    }
 257
 258    fn id(&self) -> usize {
 259        self.id()
 260    }
 261
 262    fn to_any(&self) -> AnyViewHandle {
 263        self.into()
 264    }
 265}
 266
 267impl Clone for Box<dyn ItemViewHandle> {
 268    fn clone(&self) -> Box<dyn ItemViewHandle> {
 269        self.boxed_clone()
 270    }
 271}
 272
 273impl Clone for Box<dyn ItemHandle> {
 274    fn clone(&self) -> Box<dyn ItemHandle> {
 275        self.boxed_clone()
 276    }
 277}
 278
 279#[derive(Debug)]
 280pub struct State {
 281    pub modal: Option<usize>,
 282    pub center: PaneGroup,
 283}
 284
 285pub struct Workspace {
 286    pub settings: watch::Receiver<Settings>,
 287    modal: Option<AnyViewHandle>,
 288    center: PaneGroup,
 289    panes: Vec<ViewHandle<Pane>>,
 290    active_pane: ViewHandle<Pane>,
 291    replica_id: ReplicaId,
 292    worktrees: HashSet<ModelHandle<Worktree>>,
 293    items: Vec<Box<dyn WeakItemHandle>>,
 294    loading_items: HashMap<
 295        (usize, Arc<Path>),
 296        postage::watch::Receiver<Option<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>>>,
 297    >,
 298}
 299
 300impl Workspace {
 301    pub fn new(
 302        replica_id: ReplicaId,
 303        settings: watch::Receiver<Settings>,
 304        ctx: &mut ViewContext<Self>,
 305    ) -> Self {
 306        let pane = ctx.add_view(|_| Pane::new(settings.clone()));
 307        let pane_id = pane.id();
 308        ctx.subscribe_to_view(&pane, move |me, _, event, ctx| {
 309            me.handle_pane_event(pane_id, event, ctx)
 310        });
 311        ctx.focus(&pane);
 312
 313        Workspace {
 314            modal: None,
 315            center: PaneGroup::new(pane.id()),
 316            panes: vec![pane.clone()],
 317            active_pane: pane.clone(),
 318            settings,
 319            replica_id,
 320            worktrees: Default::default(),
 321            items: Default::default(),
 322            loading_items: Default::default(),
 323        }
 324    }
 325
 326    pub fn worktrees(&self) -> &HashSet<ModelHandle<Worktree>> {
 327        &self.worktrees
 328    }
 329
 330    pub fn contains_paths(&self, paths: &[PathBuf], app: &AppContext) -> bool {
 331        paths.iter().all(|path| self.contains_path(&path, app))
 332    }
 333
 334    pub fn contains_path(&self, path: &Path, app: &AppContext) -> bool {
 335        self.worktrees
 336            .iter()
 337            .any(|worktree| worktree.read(app).contains_abs_path(path))
 338    }
 339
 340    pub fn worktree_scans_complete(&self, ctx: &AppContext) -> impl Future<Output = ()> + 'static {
 341        let futures = self
 342            .worktrees
 343            .iter()
 344            .map(|worktree| worktree.read(ctx).scan_complete())
 345            .collect::<Vec<_>>();
 346        async move {
 347            for future in futures {
 348                future.await;
 349            }
 350        }
 351    }
 352
 353    pub fn open_paths(
 354        &mut self,
 355        abs_paths: &[PathBuf],
 356        ctx: &mut ViewContext<Self>,
 357    ) -> impl Future<Output = ()> {
 358        let entries = abs_paths
 359            .iter()
 360            .cloned()
 361            .map(|path| self.file_for_path(&path, ctx))
 362            .collect::<Vec<_>>();
 363
 364        let bg = ctx.background_executor().clone();
 365        let tasks = abs_paths
 366            .iter()
 367            .cloned()
 368            .zip(entries.into_iter())
 369            .map(|(abs_path, file)| {
 370                let is_file = bg.spawn(async move { abs_path.is_file() });
 371                ctx.spawn(|this, mut ctx| async move {
 372                    let is_file = is_file.await;
 373                    this.update(&mut ctx, |this, ctx| {
 374                        if is_file {
 375                            this.open_entry(file.entry_id(), ctx)
 376                        } else {
 377                            None
 378                        }
 379                    })
 380                })
 381            })
 382            .collect::<Vec<_>>();
 383        async move {
 384            for task in tasks {
 385                if let Some(task) = task.await {
 386                    task.await;
 387                }
 388            }
 389        }
 390    }
 391
 392    fn file_for_path(&mut self, abs_path: &Path, ctx: &mut ViewContext<Self>) -> FileHandle {
 393        for tree in self.worktrees.iter() {
 394            if let Ok(relative_path) = abs_path.strip_prefix(tree.read(ctx).abs_path()) {
 395                return tree.file(relative_path, ctx.as_ref());
 396            }
 397        }
 398        let worktree = self.add_worktree(&abs_path, ctx);
 399        worktree.file(Path::new(""), ctx.as_ref())
 400    }
 401
 402    pub fn add_worktree(
 403        &mut self,
 404        path: &Path,
 405        ctx: &mut ViewContext<Self>,
 406    ) -> ModelHandle<Worktree> {
 407        let worktree = ctx.add_model(|ctx| Worktree::new(path, ctx));
 408        ctx.observe_model(&worktree, |_, _, ctx| ctx.notify());
 409        self.worktrees.insert(worktree.clone());
 410        ctx.notify();
 411        worktree
 412    }
 413
 414    pub fn toggle_modal<V, F>(&mut self, ctx: &mut ViewContext<Self>, add_view: F)
 415    where
 416        V: 'static + View,
 417        F: FnOnce(&mut ViewContext<Self>, &mut Self) -> ViewHandle<V>,
 418    {
 419        if self.modal.as_ref().map_or(false, |modal| modal.is::<V>()) {
 420            self.modal.take();
 421            ctx.focus_self();
 422        } else {
 423            let modal = add_view(ctx, self);
 424            ctx.focus(&modal);
 425            self.modal = Some(modal.into());
 426        }
 427        ctx.notify();
 428    }
 429
 430    pub fn modal(&self) -> Option<&AnyViewHandle> {
 431        self.modal.as_ref()
 432    }
 433
 434    pub fn dismiss_modal(&mut self, ctx: &mut ViewContext<Self>) {
 435        if self.modal.take().is_some() {
 436            ctx.focus(&self.active_pane);
 437            ctx.notify();
 438        }
 439    }
 440
 441    pub fn open_new_file(&mut self, _: &(), ctx: &mut ViewContext<Self>) {
 442        let buffer = ctx.add_model(|ctx| Buffer::new(self.replica_id, "", ctx));
 443        let buffer_view =
 444            ctx.add_view(|ctx| BufferView::for_buffer(buffer.clone(), self.settings.clone(), ctx));
 445        self.items.push(ItemHandle::downgrade(&buffer));
 446        self.add_item_view(Box::new(buffer_view), ctx);
 447    }
 448
 449    #[must_use]
 450    pub fn open_entry(
 451        &mut self,
 452        entry: (usize, Arc<Path>),
 453        ctx: &mut ViewContext<Self>,
 454    ) -> Option<Task<()>> {
 455        // If the active pane contains a view for this file, then activate
 456        // that item view.
 457        if self
 458            .active_pane()
 459            .update(ctx, |pane, ctx| pane.activate_entry(entry.clone(), ctx))
 460        {
 461            return None;
 462        }
 463
 464        // Otherwise, if this file is already open somewhere in the workspace,
 465        // then add another view for it.
 466        let settings = self.settings.clone();
 467        let mut view_for_existing_item = None;
 468        self.items.retain(|item| {
 469            if item.alive(ctx.as_ref()) {
 470                if view_for_existing_item.is_none()
 471                    && item
 472                        .file(ctx.as_ref())
 473                        .map_or(false, |f| f.entry_id() == entry)
 474                {
 475                    view_for_existing_item = Some(
 476                        item.add_view(ctx.window_id(), settings.clone(), ctx.as_mut())
 477                            .unwrap(),
 478                    );
 479                }
 480                true
 481            } else {
 482                false
 483            }
 484        });
 485        if let Some(view) = view_for_existing_item {
 486            self.add_item_view(view, ctx);
 487            return None;
 488        }
 489
 490        let (worktree_id, path) = entry.clone();
 491
 492        let worktree = match self.worktrees.get(&worktree_id).cloned() {
 493            Some(worktree) => worktree,
 494            None => {
 495                log::error!("worktree {} does not exist", worktree_id);
 496                return None;
 497            }
 498        };
 499
 500        let file = worktree.file(path.clone(), ctx.as_ref());
 501        if let Entry::Vacant(entry) = self.loading_items.entry(entry.clone()) {
 502            let (mut tx, rx) = postage::watch::channel();
 503            entry.insert(rx);
 504            let replica_id = self.replica_id;
 505            let history = ctx
 506                .background_executor()
 507                .spawn(file.load_history(ctx.as_ref()));
 508
 509            ctx.as_mut()
 510                .spawn(|mut ctx| async move {
 511                    *tx.borrow_mut() = Some(match history.await {
 512                        Ok(history) => Ok(Box::new(ctx.add_model(|ctx| {
 513                            Buffer::from_history(replica_id, history, Some(file), ctx)
 514                        }))),
 515                        Err(error) => Err(Arc::new(error)),
 516                    })
 517                })
 518                .detach();
 519        }
 520
 521        let mut watch = self.loading_items.get(&entry).unwrap().clone();
 522
 523        Some(ctx.spawn(|this, mut ctx| async move {
 524            let load_result = loop {
 525                if let Some(load_result) = watch.borrow().as_ref() {
 526                    break load_result.clone();
 527                }
 528                watch.next().await;
 529            };
 530
 531            this.update(&mut ctx, |this, ctx| {
 532                this.loading_items.remove(&entry);
 533                match load_result {
 534                    Ok(item) => {
 535                        let weak_item = item.downgrade();
 536                        let view = weak_item
 537                            .add_view(ctx.window_id(), settings, ctx.as_mut())
 538                            .unwrap();
 539                        this.items.push(weak_item);
 540                        this.add_item_view(view, ctx);
 541                    }
 542                    Err(error) => {
 543                        log::error!("error opening item: {}", error);
 544                    }
 545                }
 546            })
 547        }))
 548    }
 549
 550    pub fn active_item(&self, ctx: &ViewContext<Self>) -> Option<Box<dyn ItemViewHandle>> {
 551        self.active_pane().read(ctx).active_item()
 552    }
 553
 554    pub fn save_active_item(&mut self, _: &(), ctx: &mut ViewContext<Self>) {
 555        if let Some(item) = self.active_item(ctx) {
 556            let handle = ctx.handle();
 557            if item.entry_id(ctx.as_ref()).is_none() {
 558                let start_path = self
 559                    .worktrees
 560                    .iter()
 561                    .next()
 562                    .map_or(Path::new(""), |h| h.read(ctx).abs_path())
 563                    .to_path_buf();
 564                ctx.prompt_for_new_path(&start_path, move |path, ctx| {
 565                    if let Some(path) = path {
 566                        ctx.spawn(|mut ctx| async move {
 567                            let file =
 568                                handle.update(&mut ctx, |me, ctx| me.file_for_path(&path, ctx));
 569                            if let Err(error) = ctx.update(|ctx| item.save(Some(file), ctx)).await {
 570                                error!("failed to save item: {:?}, ", error);
 571                            }
 572                        })
 573                        .detach()
 574                    }
 575                });
 576                return;
 577            } else if item.has_conflict(ctx.as_ref()) {
 578                const CONFLICT_MESSAGE: &'static str = "This file has changed on disk since you started editing it. Do you want to overwrite it?";
 579
 580                ctx.prompt(
 581                    PromptLevel::Warning,
 582                    CONFLICT_MESSAGE,
 583                    &["Overwrite", "Cancel"],
 584                    move |answer, ctx| {
 585                        if answer == 0 {
 586                            ctx.spawn(|mut ctx| async move {
 587                                if let Err(error) = ctx.update(|ctx| item.save(None, ctx)).await {
 588                                    error!("failed to save item: {:?}, ", error);
 589                                }
 590                            })
 591                            .detach();
 592                        }
 593                    },
 594                );
 595            } else {
 596                ctx.spawn(|_, mut ctx| async move {
 597                    if let Err(error) = ctx.update(|ctx| item.save(None, ctx)).await {
 598                        error!("failed to save item: {:?}, ", error);
 599                    }
 600                })
 601                .detach();
 602            }
 603        }
 604    }
 605
 606    pub fn debug_elements(&mut self, _: &(), ctx: &mut ViewContext<Self>) {
 607        match to_string_pretty(&ctx.debug_elements()) {
 608            Ok(json) => {
 609                let kib = json.len() as f32 / 1024.;
 610                ctx.as_mut().write_to_clipboard(ClipboardItem::new(json));
 611                log::info!(
 612                    "copied {:.1} KiB of element debug JSON to the clipboard",
 613                    kib
 614                );
 615            }
 616            Err(error) => {
 617                log::error!("error debugging elements: {}", error);
 618            }
 619        };
 620    }
 621
 622    fn add_pane(&mut self, ctx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
 623        let pane = ctx.add_view(|_| Pane::new(self.settings.clone()));
 624        let pane_id = pane.id();
 625        ctx.subscribe_to_view(&pane, move |me, _, event, ctx| {
 626            me.handle_pane_event(pane_id, event, ctx)
 627        });
 628        self.panes.push(pane.clone());
 629        self.activate_pane(pane.clone(), ctx);
 630        pane
 631    }
 632
 633    fn activate_pane(&mut self, pane: ViewHandle<Pane>, ctx: &mut ViewContext<Self>) {
 634        self.active_pane = pane;
 635        ctx.focus(&self.active_pane);
 636        ctx.notify();
 637    }
 638
 639    fn handle_pane_event(
 640        &mut self,
 641        pane_id: usize,
 642        event: &pane::Event,
 643        ctx: &mut ViewContext<Self>,
 644    ) {
 645        if let Some(pane) = self.pane(pane_id) {
 646            match event {
 647                pane::Event::Split(direction) => {
 648                    self.split_pane(pane, *direction, ctx);
 649                }
 650                pane::Event::Remove => {
 651                    self.remove_pane(pane, ctx);
 652                }
 653                pane::Event::Activate => {
 654                    self.activate_pane(pane, ctx);
 655                }
 656            }
 657        } else {
 658            error!("pane {} not found", pane_id);
 659        }
 660    }
 661
 662    fn split_pane(
 663        &mut self,
 664        pane: ViewHandle<Pane>,
 665        direction: SplitDirection,
 666        ctx: &mut ViewContext<Self>,
 667    ) -> ViewHandle<Pane> {
 668        let new_pane = self.add_pane(ctx);
 669        self.activate_pane(new_pane.clone(), ctx);
 670        if let Some(item) = pane.read(ctx).active_item() {
 671            if let Some(clone) = item.clone_on_split(ctx.as_mut()) {
 672                self.add_item_view(clone, ctx);
 673            }
 674        }
 675        self.center
 676            .split(pane.id(), new_pane.id(), direction)
 677            .unwrap();
 678        ctx.notify();
 679        new_pane
 680    }
 681
 682    fn remove_pane(&mut self, pane: ViewHandle<Pane>, ctx: &mut ViewContext<Self>) {
 683        if self.center.remove(pane.id()).unwrap() {
 684            self.panes.retain(|p| p != &pane);
 685            self.activate_pane(self.panes.last().unwrap().clone(), ctx);
 686        }
 687    }
 688
 689    fn pane(&self, pane_id: usize) -> Option<ViewHandle<Pane>> {
 690        self.panes.iter().find(|pane| pane.id() == pane_id).cloned()
 691    }
 692
 693    pub fn active_pane(&self) -> &ViewHandle<Pane> {
 694        &self.active_pane
 695    }
 696
 697    fn add_item_view(&self, item: Box<dyn ItemViewHandle>, ctx: &mut ViewContext<Self>) {
 698        let active_pane = self.active_pane();
 699        item.set_parent_pane(&active_pane, ctx.as_mut());
 700        active_pane.update(ctx, |pane, ctx| {
 701            let item_idx = pane.add_item(item, ctx);
 702            pane.activate_item(item_idx, ctx);
 703        });
 704    }
 705}
 706
 707impl Entity for Workspace {
 708    type Event = ();
 709}
 710
 711impl View for Workspace {
 712    fn ui_name() -> &'static str {
 713        "Workspace"
 714    }
 715
 716    fn render(&self, _: &AppContext) -> ElementBox {
 717        Container::new(
 718            // self.center.render(bump)
 719            Stack::new()
 720                .with_child(self.center.render())
 721                .with_children(self.modal.as_ref().map(|m| ChildView::new(m.id()).boxed()))
 722                .boxed(),
 723        )
 724        .with_background_color(rgbu(0xea, 0xea, 0xeb))
 725        .named("workspace")
 726    }
 727
 728    fn on_focus(&mut self, ctx: &mut ViewContext<Self>) {
 729        ctx.focus(&self.active_pane);
 730    }
 731}
 732
 733#[cfg(test)]
 734pub trait WorkspaceHandle {
 735    fn file_entries(&self, app: &AppContext) -> Vec<(usize, Arc<Path>)>;
 736}
 737
 738#[cfg(test)]
 739impl WorkspaceHandle for ViewHandle<Workspace> {
 740    fn file_entries(&self, app: &AppContext) -> Vec<(usize, Arc<Path>)> {
 741        self.read(app)
 742            .worktrees()
 743            .iter()
 744            .flat_map(|tree| {
 745                let tree_id = tree.id();
 746                tree.read(app)
 747                    .files(0)
 748                    .map(move |f| (tree_id, f.path().clone()))
 749            })
 750            .collect::<Vec<_>>()
 751    }
 752}
 753
 754#[cfg(test)]
 755mod tests {
 756    use super::*;
 757    use crate::{editor::BufferView, settings, test::temp_tree};
 758    use serde_json::json;
 759    use std::{collections::HashSet, fs};
 760    use tempdir::TempDir;
 761
 762    #[gpui::test]
 763    fn test_open_paths_action(app: &mut gpui::MutableAppContext) {
 764        let settings = settings::channel(&app.font_cache()).unwrap().1;
 765
 766        init(app);
 767
 768        let dir = temp_tree(json!({
 769            "a": {
 770                "aa": null,
 771                "ab": null,
 772            },
 773            "b": {
 774                "ba": null,
 775                "bb": null,
 776            },
 777            "c": {
 778                "ca": null,
 779                "cb": null,
 780            },
 781        }));
 782
 783        app.dispatch_global_action(
 784            "workspace:open_paths",
 785            OpenParams {
 786                paths: vec![
 787                    dir.path().join("a").to_path_buf(),
 788                    dir.path().join("b").to_path_buf(),
 789                ],
 790                settings: settings.clone(),
 791            },
 792        );
 793        assert_eq!(app.window_ids().count(), 1);
 794
 795        app.dispatch_global_action(
 796            "workspace:open_paths",
 797            OpenParams {
 798                paths: vec![dir.path().join("a").to_path_buf()],
 799                settings: settings.clone(),
 800            },
 801        );
 802        assert_eq!(app.window_ids().count(), 1);
 803        let workspace_view_1 = app
 804            .root_view::<Workspace>(app.window_ids().next().unwrap())
 805            .unwrap();
 806        assert_eq!(workspace_view_1.read(app).worktrees().len(), 2);
 807
 808        app.dispatch_global_action(
 809            "workspace:open_paths",
 810            OpenParams {
 811                paths: vec![
 812                    dir.path().join("b").to_path_buf(),
 813                    dir.path().join("c").to_path_buf(),
 814                ],
 815                settings: settings.clone(),
 816            },
 817        );
 818        assert_eq!(app.window_ids().count(), 2);
 819    }
 820
 821    #[gpui::test]
 822    async fn test_open_entry(mut app: gpui::TestAppContext) {
 823        let dir = temp_tree(json!({
 824            "a": {
 825                "file1": "contents 1",
 826                "file2": "contents 2",
 827                "file3": "contents 3",
 828            },
 829        }));
 830
 831        let settings = settings::channel(&app.font_cache()).unwrap().1;
 832
 833        let (_, workspace) = app.add_window(|ctx| {
 834            let mut workspace = Workspace::new(0, settings, ctx);
 835            workspace.add_worktree(dir.path(), ctx);
 836            workspace
 837        });
 838
 839        app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx))
 840            .await;
 841        let entries = app.read(|ctx| workspace.file_entries(ctx));
 842        let file1 = entries[0].clone();
 843        let file2 = entries[1].clone();
 844        let file3 = entries[2].clone();
 845
 846        // Open the first entry
 847        workspace
 848            .update(&mut app, |w, ctx| w.open_entry(file1.clone(), ctx))
 849            .unwrap()
 850            .await;
 851        app.read(|ctx| {
 852            let pane = workspace.read(ctx).active_pane().read(ctx);
 853            assert_eq!(
 854                pane.active_item().unwrap().entry_id(ctx),
 855                Some(file1.clone())
 856            );
 857            assert_eq!(pane.items().len(), 1);
 858        });
 859
 860        // Open the second entry
 861        workspace
 862            .update(&mut app, |w, ctx| w.open_entry(file2.clone(), ctx))
 863            .unwrap()
 864            .await;
 865        app.read(|ctx| {
 866            let pane = workspace.read(ctx).active_pane().read(ctx);
 867            assert_eq!(
 868                pane.active_item().unwrap().entry_id(ctx),
 869                Some(file2.clone())
 870            );
 871            assert_eq!(pane.items().len(), 2);
 872        });
 873
 874        // Open the first entry again. The existing pane item is activated.
 875        workspace.update(&mut app, |w, ctx| {
 876            assert!(w.open_entry(file1.clone(), ctx).is_none())
 877        });
 878        app.read(|ctx| {
 879            let pane = workspace.read(ctx).active_pane().read(ctx);
 880            assert_eq!(
 881                pane.active_item().unwrap().entry_id(ctx),
 882                Some(file1.clone())
 883            );
 884            assert_eq!(pane.items().len(), 2);
 885        });
 886
 887        // Split the pane with the first entry, then open the second entry again.
 888        workspace.update(&mut app, |w, ctx| {
 889            w.split_pane(w.active_pane().clone(), SplitDirection::Right, ctx);
 890            assert!(w.open_entry(file2.clone(), ctx).is_none());
 891            assert_eq!(
 892                w.active_pane()
 893                    .read(ctx)
 894                    .active_item()
 895                    .unwrap()
 896                    .entry_id(ctx.as_ref()),
 897                Some(file2.clone())
 898            );
 899        });
 900
 901        // Open the third entry twice concurrently. Two pane items
 902        // are added.
 903        let (t1, t2) = workspace.update(&mut app, |w, ctx| {
 904            (
 905                w.open_entry(file3.clone(), ctx).unwrap(),
 906                w.open_entry(file3.clone(), ctx).unwrap(),
 907            )
 908        });
 909        t1.await;
 910        t2.await;
 911        app.read(|ctx| {
 912            let pane = workspace.read(ctx).active_pane().read(ctx);
 913            assert_eq!(
 914                pane.active_item().unwrap().entry_id(ctx),
 915                Some(file3.clone())
 916            );
 917            let pane_entries = pane
 918                .items()
 919                .iter()
 920                .map(|i| i.entry_id(ctx).unwrap())
 921                .collect::<Vec<_>>();
 922            assert_eq!(pane_entries, &[file1, file2, file3.clone(), file3]);
 923        });
 924    }
 925
 926    #[gpui::test]
 927    async fn test_open_paths(mut app: gpui::TestAppContext) {
 928        let dir1 = temp_tree(json!({
 929            "a.txt": "",
 930        }));
 931        let dir2 = temp_tree(json!({
 932            "b.txt": "",
 933        }));
 934
 935        let settings = settings::channel(&app.font_cache()).unwrap().1;
 936        let (_, workspace) = app.add_window(|ctx| {
 937            let mut workspace = Workspace::new(0, settings, ctx);
 938            workspace.add_worktree(dir1.path(), ctx);
 939            workspace
 940        });
 941        app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx))
 942            .await;
 943
 944        // Open a file within an existing worktree.
 945        app.update(|ctx| {
 946            workspace.update(ctx, |view, ctx| {
 947                view.open_paths(&[dir1.path().join("a.txt")], ctx)
 948            })
 949        })
 950        .await;
 951        app.read(|ctx| {
 952            assert_eq!(
 953                workspace
 954                    .read(ctx)
 955                    .active_pane()
 956                    .read(ctx)
 957                    .active_item()
 958                    .unwrap()
 959                    .title(ctx),
 960                "a.txt"
 961            );
 962        });
 963
 964        // Open a file outside of any existing worktree.
 965        app.update(|ctx| {
 966            workspace.update(ctx, |view, ctx| {
 967                view.open_paths(&[dir2.path().join("b.txt")], ctx)
 968            })
 969        })
 970        .await;
 971        app.read(|ctx| {
 972            let worktree_roots = workspace
 973                .read(ctx)
 974                .worktrees()
 975                .iter()
 976                .map(|w| w.read(ctx).abs_path())
 977                .collect::<HashSet<_>>();
 978            assert_eq!(
 979                worktree_roots,
 980                vec![dir1.path(), &dir2.path().join("b.txt")]
 981                    .into_iter()
 982                    .collect(),
 983            );
 984            assert_eq!(
 985                workspace
 986                    .read(ctx)
 987                    .active_pane()
 988                    .read(ctx)
 989                    .active_item()
 990                    .unwrap()
 991                    .title(ctx),
 992                "b.txt"
 993            );
 994        });
 995    }
 996
 997    #[gpui::test]
 998    async fn test_save_conflicting_item(mut app: gpui::TestAppContext) {
 999        let dir = temp_tree(json!({
1000            "a.txt": "",
1001        }));
1002
1003        let settings = settings::channel(&app.font_cache()).unwrap().1;
1004        let (window_id, workspace) = app.add_window(|ctx| {
1005            let mut workspace = Workspace::new(0, settings, ctx);
1006            workspace.add_worktree(dir.path(), ctx);
1007            workspace
1008        });
1009        let tree = app.read(|ctx| {
1010            let mut trees = workspace.read(ctx).worktrees().iter();
1011            trees.next().unwrap().clone()
1012        });
1013        tree.flush_fs_events(&app).await;
1014
1015        // Open a file within an existing worktree.
1016        app.update(|ctx| {
1017            workspace.update(ctx, |view, ctx| {
1018                view.open_paths(&[dir.path().join("a.txt")], ctx)
1019            })
1020        })
1021        .await;
1022        let editor = app.read(|ctx| {
1023            let pane = workspace.read(ctx).active_pane().read(ctx);
1024            let item = pane.active_item().unwrap();
1025            item.to_any().downcast::<BufferView>().unwrap()
1026        });
1027
1028        app.update(|ctx| editor.update(ctx, |editor, ctx| editor.insert(&"x".to_string(), ctx)));
1029        fs::write(dir.path().join("a.txt"), "changed").unwrap();
1030        tree.flush_fs_events(&app).await;
1031        app.read(|ctx| {
1032            assert!(editor.is_dirty(ctx));
1033            assert!(editor.has_conflict(ctx));
1034        });
1035
1036        app.update(|ctx| workspace.update(ctx, |w, ctx| w.save_active_item(&(), ctx)));
1037        app.simulate_prompt_answer(window_id, 0);
1038        tree.update(&mut app, |tree, ctx| tree.next_scan_complete(ctx))
1039            .await;
1040        app.read(|ctx| {
1041            assert!(!editor.is_dirty(ctx));
1042            assert!(!editor.has_conflict(ctx));
1043        });
1044    }
1045
1046    #[gpui::test]
1047    async fn test_open_and_save_new_file(mut app: gpui::TestAppContext) {
1048        let dir = TempDir::new("test-new-file").unwrap();
1049        let settings = settings::channel(&app.font_cache()).unwrap().1;
1050        let (_, workspace) = app.add_window(|ctx| {
1051            let mut workspace = Workspace::new(0, settings, ctx);
1052            workspace.add_worktree(dir.path(), ctx);
1053            workspace
1054        });
1055        let tree = app.read(|ctx| {
1056            workspace
1057                .read(ctx)
1058                .worktrees()
1059                .iter()
1060                .next()
1061                .unwrap()
1062                .clone()
1063        });
1064        tree.flush_fs_events(&app).await;
1065
1066        // Create a new untitled buffer
1067        let editor = workspace.update(&mut app, |workspace, ctx| {
1068            workspace.open_new_file(&(), ctx);
1069            workspace
1070                .active_item(ctx)
1071                .unwrap()
1072                .to_any()
1073                .downcast::<BufferView>()
1074                .unwrap()
1075        });
1076        editor.update(&mut app, |editor, ctx| {
1077            assert!(!editor.is_dirty(ctx.as_ref()));
1078            assert_eq!(editor.title(ctx.as_ref()), "untitled");
1079            editor.insert(&"hi".to_string(), ctx);
1080            assert!(editor.is_dirty(ctx.as_ref()));
1081        });
1082
1083        // Save the buffer. This prompts for a filename.
1084        workspace.update(&mut app, |workspace, ctx| {
1085            workspace.save_active_item(&(), ctx)
1086        });
1087        app.simulate_new_path_selection(|parent_dir| {
1088            assert_eq!(parent_dir, dir.path());
1089            Some(parent_dir.join("the-new-name"))
1090        });
1091        app.read(|ctx| {
1092            assert!(editor.is_dirty(ctx));
1093            assert_eq!(editor.title(ctx), "untitled");
1094        });
1095
1096        // When the save completes, the buffer's title is updated.
1097        tree.update(&mut app, |tree, ctx| tree.next_scan_complete(ctx))
1098            .await;
1099        app.read(|ctx| {
1100            assert!(!editor.is_dirty(ctx));
1101            assert_eq!(editor.title(ctx), "the-new-name");
1102        });
1103
1104        // Edit the file and save it again. This time, there is no filename prompt.
1105        editor.update(&mut app, |editor, ctx| {
1106            editor.insert(&" there".to_string(), ctx);
1107            assert_eq!(editor.is_dirty(ctx.as_ref()), true);
1108        });
1109        workspace.update(&mut app, |workspace, ctx| {
1110            workspace.save_active_item(&(), ctx)
1111        });
1112        assert!(!app.did_prompt_for_new_path());
1113        editor
1114            .condition(&app, |editor, ctx| !editor.is_dirty(ctx))
1115            .await;
1116        app.read(|ctx| assert_eq!(editor.title(ctx), "the-new-name"));
1117
1118        // Open the same newly-created file in another pane item. The new editor should reuse
1119        // the same buffer.
1120        workspace.update(&mut app, |workspace, ctx| {
1121            workspace.open_new_file(&(), ctx);
1122            workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, ctx);
1123            assert!(workspace
1124                .open_entry((tree.id(), Path::new("the-new-name").into()), ctx)
1125                .is_none());
1126        });
1127        let editor2 = workspace.update(&mut app, |workspace, ctx| {
1128            workspace
1129                .active_item(ctx)
1130                .unwrap()
1131                .to_any()
1132                .downcast::<BufferView>()
1133                .unwrap()
1134        });
1135        app.read(|ctx| {
1136            assert_eq!(editor2.read(ctx).buffer(), editor.read(ctx).buffer());
1137        })
1138    }
1139
1140    #[gpui::test]
1141    async fn test_pane_actions(mut app: gpui::TestAppContext) {
1142        app.update(|ctx| pane::init(ctx));
1143
1144        let dir = temp_tree(json!({
1145            "a": {
1146                "file1": "contents 1",
1147                "file2": "contents 2",
1148                "file3": "contents 3",
1149            },
1150        }));
1151
1152        let settings = settings::channel(&app.font_cache()).unwrap().1;
1153        let (window_id, workspace) = app.add_window(|ctx| {
1154            let mut workspace = Workspace::new(0, settings, ctx);
1155            workspace.add_worktree(dir.path(), ctx);
1156            workspace
1157        });
1158        app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx))
1159            .await;
1160        let entries = app.read(|ctx| workspace.file_entries(ctx));
1161        let file1 = entries[0].clone();
1162
1163        let pane_1 = app.read(|ctx| workspace.read(ctx).active_pane().clone());
1164
1165        workspace
1166            .update(&mut app, |w, ctx| w.open_entry(file1.clone(), ctx))
1167            .unwrap()
1168            .await;
1169        app.read(|ctx| {
1170            assert_eq!(
1171                pane_1.read(ctx).active_item().unwrap().entry_id(ctx),
1172                Some(file1.clone())
1173            );
1174        });
1175
1176        app.dispatch_action(window_id, vec![pane_1.id()], "pane:split_right", ());
1177        app.update(|ctx| {
1178            let pane_2 = workspace.read(ctx).active_pane().clone();
1179            assert_ne!(pane_1, pane_2);
1180
1181            let pane2_item = pane_2.read(ctx).active_item().unwrap();
1182            assert_eq!(pane2_item.entry_id(ctx.as_ref()), Some(file1.clone()));
1183
1184            ctx.dispatch_action(window_id, vec![pane_2.id()], "pane:close_active_item", ());
1185            let workspace_view = workspace.read(ctx);
1186            assert_eq!(workspace_view.panes.len(), 1);
1187            assert_eq!(workspace_view.active_pane(), &pane_1);
1188        });
1189    }
1190}