workspace.rs

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