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 file = file.await;
 373                    let is_file = is_file.await;
 374                    this.update(&mut ctx, |this, ctx| {
 375                        if is_file {
 376                            this.open_entry(file.entry_id(), ctx)
 377                        } else {
 378                            None
 379                        }
 380                    })
 381                })
 382            })
 383            .collect::<Vec<_>>();
 384        async move {
 385            for task in tasks {
 386                if let Some(task) = task.await {
 387                    task.await;
 388                }
 389            }
 390        }
 391    }
 392
 393    fn file_for_path(&mut self, abs_path: &Path, ctx: &mut ViewContext<Self>) -> Task<FileHandle> {
 394        for tree in self.worktrees.iter() {
 395            if let Ok(relative_path) = abs_path.strip_prefix(tree.read(ctx).abs_path()) {
 396                return tree.file(relative_path, ctx.as_mut());
 397            }
 398        }
 399        let worktree = self.add_worktree(&abs_path, ctx);
 400        worktree.file(Path::new(""), ctx.as_mut())
 401    }
 402
 403    pub fn add_worktree(
 404        &mut self,
 405        path: &Path,
 406        ctx: &mut ViewContext<Self>,
 407    ) -> ModelHandle<Worktree> {
 408        let worktree = ctx.add_model(|ctx| Worktree::new(path, ctx));
 409        ctx.observe_model(&worktree, |_, _, ctx| ctx.notify());
 410        self.worktrees.insert(worktree.clone());
 411        ctx.notify();
 412        worktree
 413    }
 414
 415    pub fn toggle_modal<V, F>(&mut self, ctx: &mut ViewContext<Self>, add_view: F)
 416    where
 417        V: 'static + View,
 418        F: FnOnce(&mut ViewContext<Self>, &mut Self) -> ViewHandle<V>,
 419    {
 420        if self.modal.as_ref().map_or(false, |modal| modal.is::<V>()) {
 421            self.modal.take();
 422            ctx.focus_self();
 423        } else {
 424            let modal = add_view(ctx, self);
 425            ctx.focus(&modal);
 426            self.modal = Some(modal.into());
 427        }
 428        ctx.notify();
 429    }
 430
 431    pub fn modal(&self) -> Option<&AnyViewHandle> {
 432        self.modal.as_ref()
 433    }
 434
 435    pub fn dismiss_modal(&mut self, ctx: &mut ViewContext<Self>) {
 436        if self.modal.take().is_some() {
 437            ctx.focus(&self.active_pane);
 438            ctx.notify();
 439        }
 440    }
 441
 442    pub fn open_new_file(&mut self, _: &(), ctx: &mut ViewContext<Self>) {
 443        let buffer = ctx.add_model(|ctx| Buffer::new(self.replica_id, "", ctx));
 444        let buffer_view =
 445            ctx.add_view(|ctx| BufferView::for_buffer(buffer.clone(), self.settings.clone(), ctx));
 446        self.items.push(ItemHandle::downgrade(&buffer));
 447        self.add_item_view(Box::new(buffer_view), ctx);
 448    }
 449
 450    #[must_use]
 451    pub fn open_entry(
 452        &mut self,
 453        entry: (usize, Arc<Path>),
 454        ctx: &mut ViewContext<Self>,
 455    ) -> Option<Task<()>> {
 456        // If the active pane contains a view for this file, then activate
 457        // that item view.
 458        if self
 459            .active_pane()
 460            .update(ctx, |pane, ctx| pane.activate_entry(entry.clone(), ctx))
 461        {
 462            return None;
 463        }
 464
 465        // Otherwise, if this file is already open somewhere in the workspace,
 466        // then add another view for it.
 467        let settings = self.settings.clone();
 468        let mut view_for_existing_item = None;
 469        self.items.retain(|item| {
 470            if item.alive(ctx.as_ref()) {
 471                if view_for_existing_item.is_none()
 472                    && item
 473                        .file(ctx.as_ref())
 474                        .map_or(false, |f| f.entry_id() == entry)
 475                {
 476                    view_for_existing_item = Some(
 477                        item.add_view(ctx.window_id(), settings.clone(), ctx.as_mut())
 478                            .unwrap(),
 479                    );
 480                }
 481                true
 482            } else {
 483                false
 484            }
 485        });
 486        if let Some(view) = view_for_existing_item {
 487            self.add_item_view(view, ctx);
 488            return None;
 489        }
 490
 491        let (worktree_id, path) = entry.clone();
 492
 493        let worktree = match self.worktrees.get(&worktree_id).cloned() {
 494            Some(worktree) => worktree,
 495            None => {
 496                log::error!("worktree {} does not exist", worktree_id);
 497                return None;
 498            }
 499        };
 500
 501        let file = worktree.file(path.clone(), ctx.as_mut());
 502        if let Entry::Vacant(entry) = self.loading_items.entry(entry.clone()) {
 503            let (mut tx, rx) = postage::watch::channel();
 504            entry.insert(rx);
 505            let replica_id = self.replica_id;
 506
 507            ctx.as_mut()
 508                .spawn(|mut ctx| async move {
 509                    let file = file.await;
 510                    let history = ctx.read(|ctx| file.load_history(ctx));
 511                    let history = ctx.background_executor().spawn(history).await;
 512
 513                    *tx.borrow_mut() = Some(match history {
 514                        Ok(history) => Ok(Box::new(ctx.add_model(|ctx| {
 515                            Buffer::from_history(replica_id, history, Some(file), ctx)
 516                        }))),
 517                        Err(error) => Err(Arc::new(error)),
 518                    })
 519                })
 520                .detach();
 521        }
 522
 523        let mut watch = self.loading_items.get(&entry).unwrap().clone();
 524
 525        Some(ctx.spawn(|this, mut ctx| async move {
 526            let load_result = loop {
 527                if let Some(load_result) = watch.borrow().as_ref() {
 528                    break load_result.clone();
 529                }
 530                watch.next().await;
 531            };
 532
 533            this.update(&mut ctx, |this, ctx| {
 534                this.loading_items.remove(&entry);
 535                match load_result {
 536                    Ok(item) => {
 537                        let weak_item = item.downgrade();
 538                        let view = weak_item
 539                            .add_view(ctx.window_id(), settings, ctx.as_mut())
 540                            .unwrap();
 541                        this.items.push(weak_item);
 542                        this.add_item_view(view, ctx);
 543                    }
 544                    Err(error) => {
 545                        log::error!("error opening item: {}", error);
 546                    }
 547                }
 548            })
 549        }))
 550    }
 551
 552    pub fn active_item(&self, ctx: &ViewContext<Self>) -> Option<Box<dyn ItemViewHandle>> {
 553        self.active_pane().read(ctx).active_item()
 554    }
 555
 556    pub fn save_active_item(&mut self, _: &(), ctx: &mut ViewContext<Self>) {
 557        if let Some(item) = self.active_item(ctx) {
 558            let handle = ctx.handle();
 559            if item.entry_id(ctx.as_ref()).is_none() {
 560                let start_path = self
 561                    .worktrees
 562                    .iter()
 563                    .next()
 564                    .map_or(Path::new(""), |h| h.read(ctx).abs_path())
 565                    .to_path_buf();
 566                ctx.prompt_for_new_path(&start_path, move |path, ctx| {
 567                    if let Some(path) = path {
 568                        ctx.spawn(|mut ctx| async move {
 569                            let file = handle
 570                                .update(&mut ctx, |me, ctx| me.file_for_path(&path, ctx))
 571                                .await;
 572                            if let Err(error) = ctx.update(|ctx| item.save(Some(file), ctx)).await {
 573                                error!("failed to save item: {:?}, ", error);
 574                            }
 575                        })
 576                        .detach()
 577                    }
 578                });
 579                return;
 580            } else if item.has_conflict(ctx.as_ref()) {
 581                const CONFLICT_MESSAGE: &'static str = "This file has changed on disk since you started editing it. Do you want to overwrite it?";
 582
 583                ctx.prompt(
 584                    PromptLevel::Warning,
 585                    CONFLICT_MESSAGE,
 586                    &["Overwrite", "Cancel"],
 587                    move |answer, ctx| {
 588                        if answer == 0 {
 589                            ctx.spawn(|mut ctx| async move {
 590                                if let Err(error) = ctx.update(|ctx| item.save(None, ctx)).await {
 591                                    error!("failed to save item: {:?}, ", error);
 592                                }
 593                            })
 594                            .detach();
 595                        }
 596                    },
 597                );
 598            } else {
 599                ctx.spawn(|_, mut ctx| async move {
 600                    if let Err(error) = ctx.update(|ctx| item.save(None, ctx)).await {
 601                        error!("failed to save item: {:?}, ", error);
 602                    }
 603                })
 604                .detach();
 605            }
 606        }
 607    }
 608
 609    pub fn debug_elements(&mut self, _: &(), ctx: &mut ViewContext<Self>) {
 610        match to_string_pretty(&ctx.debug_elements()) {
 611            Ok(json) => {
 612                let kib = json.len() as f32 / 1024.;
 613                ctx.as_mut().write_to_clipboard(ClipboardItem::new(json));
 614                log::info!(
 615                    "copied {:.1} KiB of element debug JSON to the clipboard",
 616                    kib
 617                );
 618            }
 619            Err(error) => {
 620                log::error!("error debugging elements: {}", error);
 621            }
 622        };
 623    }
 624
 625    fn add_pane(&mut self, ctx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
 626        let pane = ctx.add_view(|_| Pane::new(self.settings.clone()));
 627        let pane_id = pane.id();
 628        ctx.subscribe_to_view(&pane, move |me, _, event, ctx| {
 629            me.handle_pane_event(pane_id, event, ctx)
 630        });
 631        self.panes.push(pane.clone());
 632        self.activate_pane(pane.clone(), ctx);
 633        pane
 634    }
 635
 636    fn activate_pane(&mut self, pane: ViewHandle<Pane>, ctx: &mut ViewContext<Self>) {
 637        self.active_pane = pane;
 638        ctx.focus(&self.active_pane);
 639        ctx.notify();
 640    }
 641
 642    fn handle_pane_event(
 643        &mut self,
 644        pane_id: usize,
 645        event: &pane::Event,
 646        ctx: &mut ViewContext<Self>,
 647    ) {
 648        if let Some(pane) = self.pane(pane_id) {
 649            match event {
 650                pane::Event::Split(direction) => {
 651                    self.split_pane(pane, *direction, ctx);
 652                }
 653                pane::Event::Remove => {
 654                    self.remove_pane(pane, ctx);
 655                }
 656                pane::Event::Activate => {
 657                    self.activate_pane(pane, ctx);
 658                }
 659            }
 660        } else {
 661            error!("pane {} not found", pane_id);
 662        }
 663    }
 664
 665    fn split_pane(
 666        &mut self,
 667        pane: ViewHandle<Pane>,
 668        direction: SplitDirection,
 669        ctx: &mut ViewContext<Self>,
 670    ) -> ViewHandle<Pane> {
 671        let new_pane = self.add_pane(ctx);
 672        self.activate_pane(new_pane.clone(), ctx);
 673        if let Some(item) = pane.read(ctx).active_item() {
 674            if let Some(clone) = item.clone_on_split(ctx.as_mut()) {
 675                self.add_item_view(clone, ctx);
 676            }
 677        }
 678        self.center
 679            .split(pane.id(), new_pane.id(), direction)
 680            .unwrap();
 681        ctx.notify();
 682        new_pane
 683    }
 684
 685    fn remove_pane(&mut self, pane: ViewHandle<Pane>, ctx: &mut ViewContext<Self>) {
 686        if self.center.remove(pane.id()).unwrap() {
 687            self.panes.retain(|p| p != &pane);
 688            self.activate_pane(self.panes.last().unwrap().clone(), ctx);
 689        }
 690    }
 691
 692    fn pane(&self, pane_id: usize) -> Option<ViewHandle<Pane>> {
 693        self.panes.iter().find(|pane| pane.id() == pane_id).cloned()
 694    }
 695
 696    pub fn active_pane(&self) -> &ViewHandle<Pane> {
 697        &self.active_pane
 698    }
 699
 700    fn add_item_view(&self, item: Box<dyn ItemViewHandle>, ctx: &mut ViewContext<Self>) {
 701        let active_pane = self.active_pane();
 702        item.set_parent_pane(&active_pane, ctx.as_mut());
 703        active_pane.update(ctx, |pane, ctx| {
 704            let item_idx = pane.add_item(item, ctx);
 705            pane.activate_item(item_idx, ctx);
 706        });
 707    }
 708}
 709
 710impl Entity for Workspace {
 711    type Event = ();
 712}
 713
 714impl View for Workspace {
 715    fn ui_name() -> &'static str {
 716        "Workspace"
 717    }
 718
 719    fn render(&self, _: &AppContext) -> ElementBox {
 720        Container::new(
 721            // self.center.render(bump)
 722            Stack::new()
 723                .with_child(self.center.render())
 724                .with_children(self.modal.as_ref().map(|m| ChildView::new(m.id()).boxed()))
 725                .boxed(),
 726        )
 727        .with_background_color(rgbu(0xea, 0xea, 0xeb))
 728        .named("workspace")
 729    }
 730
 731    fn on_focus(&mut self, ctx: &mut ViewContext<Self>) {
 732        ctx.focus(&self.active_pane);
 733    }
 734}
 735
 736#[cfg(test)]
 737pub trait WorkspaceHandle {
 738    fn file_entries(&self, app: &AppContext) -> Vec<(usize, Arc<Path>)>;
 739}
 740
 741#[cfg(test)]
 742impl WorkspaceHandle for ViewHandle<Workspace> {
 743    fn file_entries(&self, app: &AppContext) -> Vec<(usize, Arc<Path>)> {
 744        self.read(app)
 745            .worktrees()
 746            .iter()
 747            .flat_map(|tree| {
 748                let tree_id = tree.id();
 749                tree.read(app)
 750                    .files(0)
 751                    .map(move |f| (tree_id, f.path().clone()))
 752            })
 753            .collect::<Vec<_>>()
 754    }
 755}
 756
 757#[cfg(test)]
 758mod tests {
 759    use super::*;
 760    use crate::{editor::BufferView, settings, test::temp_tree};
 761    use serde_json::json;
 762    use std::{collections::HashSet, fs};
 763    use tempdir::TempDir;
 764
 765    #[gpui::test]
 766    fn test_open_paths_action(app: &mut gpui::MutableAppContext) {
 767        let settings = settings::channel(&app.font_cache()).unwrap().1;
 768
 769        init(app);
 770
 771        let dir = temp_tree(json!({
 772            "a": {
 773                "aa": null,
 774                "ab": null,
 775            },
 776            "b": {
 777                "ba": null,
 778                "bb": null,
 779            },
 780            "c": {
 781                "ca": null,
 782                "cb": null,
 783            },
 784        }));
 785
 786        app.dispatch_global_action(
 787            "workspace:open_paths",
 788            OpenParams {
 789                paths: vec![
 790                    dir.path().join("a").to_path_buf(),
 791                    dir.path().join("b").to_path_buf(),
 792                ],
 793                settings: settings.clone(),
 794            },
 795        );
 796        assert_eq!(app.window_ids().count(), 1);
 797
 798        app.dispatch_global_action(
 799            "workspace:open_paths",
 800            OpenParams {
 801                paths: vec![dir.path().join("a").to_path_buf()],
 802                settings: settings.clone(),
 803            },
 804        );
 805        assert_eq!(app.window_ids().count(), 1);
 806        let workspace_view_1 = app
 807            .root_view::<Workspace>(app.window_ids().next().unwrap())
 808            .unwrap();
 809        assert_eq!(workspace_view_1.read(app).worktrees().len(), 2);
 810
 811        app.dispatch_global_action(
 812            "workspace:open_paths",
 813            OpenParams {
 814                paths: vec![
 815                    dir.path().join("b").to_path_buf(),
 816                    dir.path().join("c").to_path_buf(),
 817                ],
 818                settings: settings.clone(),
 819            },
 820        );
 821        assert_eq!(app.window_ids().count(), 2);
 822    }
 823
 824    #[gpui::test]
 825    async fn test_open_entry(mut app: gpui::TestAppContext) {
 826        let dir = temp_tree(json!({
 827            "a": {
 828                "file1": "contents 1",
 829                "file2": "contents 2",
 830                "file3": "contents 3",
 831            },
 832        }));
 833
 834        let settings = settings::channel(&app.font_cache()).unwrap().1;
 835
 836        let (_, workspace) = app.add_window(|ctx| {
 837            let mut workspace = Workspace::new(0, settings, ctx);
 838            workspace.add_worktree(dir.path(), ctx);
 839            workspace
 840        });
 841
 842        app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx))
 843            .await;
 844        let entries = app.read(|ctx| workspace.file_entries(ctx));
 845        let file1 = entries[0].clone();
 846        let file2 = entries[1].clone();
 847        let file3 = entries[2].clone();
 848
 849        // Open the first entry
 850        workspace
 851            .update(&mut app, |w, ctx| w.open_entry(file1.clone(), ctx))
 852            .unwrap()
 853            .await;
 854        app.read(|ctx| {
 855            let pane = workspace.read(ctx).active_pane().read(ctx);
 856            assert_eq!(
 857                pane.active_item().unwrap().entry_id(ctx),
 858                Some(file1.clone())
 859            );
 860            assert_eq!(pane.items().len(), 1);
 861        });
 862
 863        // Open the second entry
 864        workspace
 865            .update(&mut app, |w, ctx| w.open_entry(file2.clone(), ctx))
 866            .unwrap()
 867            .await;
 868        app.read(|ctx| {
 869            let pane = workspace.read(ctx).active_pane().read(ctx);
 870            assert_eq!(
 871                pane.active_item().unwrap().entry_id(ctx),
 872                Some(file2.clone())
 873            );
 874            assert_eq!(pane.items().len(), 2);
 875        });
 876
 877        // Open the first entry again. The existing pane item is activated.
 878        workspace.update(&mut app, |w, ctx| {
 879            assert!(w.open_entry(file1.clone(), ctx).is_none())
 880        });
 881        app.read(|ctx| {
 882            let pane = workspace.read(ctx).active_pane().read(ctx);
 883            assert_eq!(
 884                pane.active_item().unwrap().entry_id(ctx),
 885                Some(file1.clone())
 886            );
 887            assert_eq!(pane.items().len(), 2);
 888        });
 889
 890        // Split the pane with the first entry, then open the second entry again.
 891        workspace.update(&mut app, |w, ctx| {
 892            w.split_pane(w.active_pane().clone(), SplitDirection::Right, ctx);
 893            assert!(w.open_entry(file2.clone(), ctx).is_none());
 894            assert_eq!(
 895                w.active_pane()
 896                    .read(ctx)
 897                    .active_item()
 898                    .unwrap()
 899                    .entry_id(ctx.as_ref()),
 900                Some(file2.clone())
 901            );
 902        });
 903
 904        // Open the third entry twice concurrently. Two pane items
 905        // are added.
 906        let (t1, t2) = workspace.update(&mut app, |w, ctx| {
 907            (
 908                w.open_entry(file3.clone(), ctx).unwrap(),
 909                w.open_entry(file3.clone(), ctx).unwrap(),
 910            )
 911        });
 912        t1.await;
 913        t2.await;
 914        app.read(|ctx| {
 915            let pane = workspace.read(ctx).active_pane().read(ctx);
 916            assert_eq!(
 917                pane.active_item().unwrap().entry_id(ctx),
 918                Some(file3.clone())
 919            );
 920            let pane_entries = pane
 921                .items()
 922                .iter()
 923                .map(|i| i.entry_id(ctx).unwrap())
 924                .collect::<Vec<_>>();
 925            assert_eq!(pane_entries, &[file1, file2, file3.clone(), file3]);
 926        });
 927    }
 928
 929    #[gpui::test]
 930    async fn test_open_paths(mut app: gpui::TestAppContext) {
 931        let dir1 = temp_tree(json!({
 932            "a.txt": "",
 933        }));
 934        let dir2 = temp_tree(json!({
 935            "b.txt": "",
 936        }));
 937
 938        let settings = settings::channel(&app.font_cache()).unwrap().1;
 939        let (_, workspace) = app.add_window(|ctx| {
 940            let mut workspace = Workspace::new(0, settings, ctx);
 941            workspace.add_worktree(dir1.path(), ctx);
 942            workspace
 943        });
 944        app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx))
 945            .await;
 946
 947        // Open a file within an existing worktree.
 948        app.update(|ctx| {
 949            workspace.update(ctx, |view, ctx| {
 950                view.open_paths(&[dir1.path().join("a.txt")], ctx)
 951            })
 952        })
 953        .await;
 954        app.read(|ctx| {
 955            assert_eq!(
 956                workspace
 957                    .read(ctx)
 958                    .active_pane()
 959                    .read(ctx)
 960                    .active_item()
 961                    .unwrap()
 962                    .title(ctx),
 963                "a.txt"
 964            );
 965        });
 966
 967        // Open a file outside of any existing worktree.
 968        app.update(|ctx| {
 969            workspace.update(ctx, |view, ctx| {
 970                view.open_paths(&[dir2.path().join("b.txt")], ctx)
 971            })
 972        })
 973        .await;
 974        app.read(|ctx| {
 975            let worktree_roots = workspace
 976                .read(ctx)
 977                .worktrees()
 978                .iter()
 979                .map(|w| w.read(ctx).abs_path())
 980                .collect::<HashSet<_>>();
 981            assert_eq!(
 982                worktree_roots,
 983                vec![dir1.path(), &dir2.path().join("b.txt")]
 984                    .into_iter()
 985                    .collect(),
 986            );
 987            assert_eq!(
 988                workspace
 989                    .read(ctx)
 990                    .active_pane()
 991                    .read(ctx)
 992                    .active_item()
 993                    .unwrap()
 994                    .title(ctx),
 995                "b.txt"
 996            );
 997        });
 998    }
 999
1000    #[gpui::test]
1001    async fn test_save_conflicting_item(mut app: gpui::TestAppContext) {
1002        let dir = temp_tree(json!({
1003            "a.txt": "",
1004        }));
1005
1006        let settings = settings::channel(&app.font_cache()).unwrap().1;
1007        let (window_id, workspace) = app.add_window(|ctx| {
1008            let mut workspace = Workspace::new(0, settings, ctx);
1009            workspace.add_worktree(dir.path(), ctx);
1010            workspace
1011        });
1012        let tree = app.read(|ctx| {
1013            let mut trees = workspace.read(ctx).worktrees().iter();
1014            trees.next().unwrap().clone()
1015        });
1016        tree.flush_fs_events(&app).await;
1017
1018        // Open a file within an existing worktree.
1019        app.update(|ctx| {
1020            workspace.update(ctx, |view, ctx| {
1021                view.open_paths(&[dir.path().join("a.txt")], ctx)
1022            })
1023        })
1024        .await;
1025        let editor = app.read(|ctx| {
1026            let pane = workspace.read(ctx).active_pane().read(ctx);
1027            let item = pane.active_item().unwrap();
1028            item.to_any().downcast::<BufferView>().unwrap()
1029        });
1030
1031        app.update(|ctx| editor.update(ctx, |editor, ctx| editor.insert(&"x".to_string(), ctx)));
1032        fs::write(dir.path().join("a.txt"), "changed").unwrap();
1033        tree.flush_fs_events(&app).await;
1034        app.read(|ctx| {
1035            assert!(editor.is_dirty(ctx));
1036            assert!(editor.has_conflict(ctx));
1037        });
1038
1039        app.update(|ctx| workspace.update(ctx, |w, ctx| w.save_active_item(&(), ctx)));
1040        app.simulate_prompt_answer(window_id, 0);
1041        tree.update(&mut app, |tree, ctx| tree.next_scan_complete(ctx))
1042            .await;
1043        app.read(|ctx| {
1044            assert!(!editor.is_dirty(ctx));
1045            assert!(!editor.has_conflict(ctx));
1046        });
1047    }
1048
1049    #[gpui::test]
1050    async fn test_open_and_save_new_file(mut app: gpui::TestAppContext) {
1051        let dir = TempDir::new("test-new-file").unwrap();
1052        let settings = settings::channel(&app.font_cache()).unwrap().1;
1053        let (_, workspace) = app.add_window(|ctx| {
1054            let mut workspace = Workspace::new(0, settings, ctx);
1055            workspace.add_worktree(dir.path(), ctx);
1056            workspace
1057        });
1058        let tree = app.read(|ctx| {
1059            workspace
1060                .read(ctx)
1061                .worktrees()
1062                .iter()
1063                .next()
1064                .unwrap()
1065                .clone()
1066        });
1067        tree.flush_fs_events(&app).await;
1068
1069        // Create a new untitled buffer
1070        let editor = workspace.update(&mut app, |workspace, ctx| {
1071            workspace.open_new_file(&(), ctx);
1072            workspace
1073                .active_item(ctx)
1074                .unwrap()
1075                .to_any()
1076                .downcast::<BufferView>()
1077                .unwrap()
1078        });
1079        editor.update(&mut app, |editor, ctx| {
1080            assert!(!editor.is_dirty(ctx.as_ref()));
1081            assert_eq!(editor.title(ctx.as_ref()), "untitled");
1082            editor.insert(&"hi".to_string(), ctx);
1083            assert!(editor.is_dirty(ctx.as_ref()));
1084        });
1085
1086        // Save the buffer. This prompts for a filename.
1087        workspace.update(&mut app, |workspace, ctx| {
1088            workspace.save_active_item(&(), ctx)
1089        });
1090        app.simulate_new_path_selection(|parent_dir| {
1091            assert_eq!(parent_dir, dir.path());
1092            Some(parent_dir.join("the-new-name"))
1093        });
1094        app.read(|ctx| {
1095            assert!(editor.is_dirty(ctx));
1096            assert_eq!(editor.title(ctx), "untitled");
1097        });
1098
1099        // When the save completes, the buffer's title is updated.
1100        tree.update(&mut app, |tree, ctx| tree.next_scan_complete(ctx))
1101            .await;
1102        app.read(|ctx| {
1103            assert!(!editor.is_dirty(ctx));
1104            assert_eq!(editor.title(ctx), "the-new-name");
1105        });
1106
1107        // Edit the file and save it again. This time, there is no filename prompt.
1108        editor.update(&mut app, |editor, ctx| {
1109            editor.insert(&" there".to_string(), ctx);
1110            assert_eq!(editor.is_dirty(ctx.as_ref()), true);
1111        });
1112        workspace.update(&mut app, |workspace, ctx| {
1113            workspace.save_active_item(&(), ctx)
1114        });
1115        assert!(!app.did_prompt_for_new_path());
1116        editor
1117            .condition(&app, |editor, ctx| !editor.is_dirty(ctx))
1118            .await;
1119        app.read(|ctx| assert_eq!(editor.title(ctx), "the-new-name"));
1120
1121        // Open the same newly-created file in another pane item. The new editor should reuse
1122        // the same buffer.
1123        workspace.update(&mut app, |workspace, ctx| {
1124            workspace.open_new_file(&(), ctx);
1125            workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, ctx);
1126            assert!(workspace
1127                .open_entry((tree.id(), Path::new("the-new-name").into()), ctx)
1128                .is_none());
1129        });
1130        let editor2 = workspace.update(&mut app, |workspace, ctx| {
1131            workspace
1132                .active_item(ctx)
1133                .unwrap()
1134                .to_any()
1135                .downcast::<BufferView>()
1136                .unwrap()
1137        });
1138        app.read(|ctx| {
1139            assert_eq!(editor2.read(ctx).buffer(), editor.read(ctx).buffer());
1140        })
1141    }
1142
1143    #[gpui::test]
1144    async fn test_pane_actions(mut app: gpui::TestAppContext) {
1145        app.update(|ctx| pane::init(ctx));
1146
1147        let dir = temp_tree(json!({
1148            "a": {
1149                "file1": "contents 1",
1150                "file2": "contents 2",
1151                "file3": "contents 3",
1152            },
1153        }));
1154
1155        let settings = settings::channel(&app.font_cache()).unwrap().1;
1156        let (window_id, workspace) = app.add_window(|ctx| {
1157            let mut workspace = Workspace::new(0, settings, ctx);
1158            workspace.add_worktree(dir.path(), ctx);
1159            workspace
1160        });
1161        app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx))
1162            .await;
1163        let entries = app.read(|ctx| workspace.file_entries(ctx));
1164        let file1 = entries[0].clone();
1165
1166        let pane_1 = app.read(|ctx| workspace.read(ctx).active_pane().clone());
1167
1168        workspace
1169            .update(&mut app, |w, ctx| w.open_entry(file1.clone(), ctx))
1170            .unwrap()
1171            .await;
1172        app.read(|ctx| {
1173            assert_eq!(
1174                pane_1.read(ctx).active_item().unwrap().entry_id(ctx),
1175                Some(file1.clone())
1176            );
1177        });
1178
1179        app.dispatch_action(window_id, vec![pane_1.id()], "pane:split_right", ());
1180        app.update(|ctx| {
1181            let pane_2 = workspace.read(ctx).active_pane().clone();
1182            assert_ne!(pane_1, pane_2);
1183
1184            let pane2_item = pane_2.read(ctx).active_item().unwrap();
1185            assert_eq!(pane2_item.entry_id(ctx.as_ref()), Some(file1.clone()));
1186
1187            ctx.dispatch_action(window_id, vec![pane_2.id()], "pane:close_active_item", ());
1188            let workspace_view = workspace.read(ctx);
1189            assert_eq!(workspace_view.panes.len(), 1);
1190            assert_eq!(workspace_view.active_pane(), &pane_1);
1191        });
1192    }
1193}