workspace.rs

   1pub mod pane;
   2pub mod pane_group;
   3pub mod sidebar;
   4
   5use crate::{
   6    chat_panel::ChatPanel,
   7    editor::Buffer,
   8    fs::Fs,
   9    language::LanguageRegistry,
  10    people_panel::{JoinWorktree, LeaveWorktree, PeoplePanel, ShareWorktree, UnshareWorktree},
  11    project_browser::ProjectBrowser,
  12    rpc,
  13    settings::Settings,
  14    user,
  15    util::TryFutureExt as _,
  16    worktree::{self, File, Worktree},
  17    AppState, Authenticate,
  18};
  19use anyhow::Result;
  20use gpui::{
  21    action,
  22    elements::*,
  23    geometry::{rect::RectF, vector::vec2f},
  24    json::to_string_pretty,
  25    keymap::Binding,
  26    platform::{CursorStyle, WindowOptions},
  27    AnyViewHandle, AppContext, ClipboardItem, Entity, ModelHandle, MutableAppContext,
  28    PathPromptOptions, PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle,
  29    WeakModelHandle,
  30};
  31use log::error;
  32pub use pane::*;
  33pub use pane_group::*;
  34use postage::{prelude::Stream, watch};
  35use sidebar::{Side, Sidebar, ToggleSidebarItem};
  36use std::{
  37    collections::{hash_map::Entry, HashMap, HashSet},
  38    future::Future,
  39    path::{Path, PathBuf},
  40    sync::Arc,
  41};
  42
  43action!(Open, Arc<AppState>);
  44action!(OpenPaths, OpenParams);
  45action!(OpenNew, Arc<AppState>);
  46action!(Save);
  47action!(DebugElements);
  48
  49pub fn init(cx: &mut MutableAppContext) {
  50    cx.add_global_action(open);
  51    cx.add_global_action(|action: &OpenPaths, cx: &mut MutableAppContext| {
  52        open_paths(action, cx).detach()
  53    });
  54    cx.add_global_action(open_new);
  55    cx.add_action(Workspace::save_active_item);
  56    cx.add_action(Workspace::debug_elements);
  57    cx.add_action(Workspace::open_new_file);
  58    cx.add_action(Workspace::toggle_sidebar_item);
  59    cx.add_action(Workspace::share_worktree);
  60    cx.add_action(Workspace::unshare_worktree);
  61    cx.add_action(Workspace::join_worktree);
  62    cx.add_action(Workspace::leave_worktree);
  63    cx.add_bindings(vec![
  64        Binding::new("cmd-s", Save, None),
  65        Binding::new("cmd-alt-i", DebugElements, None),
  66    ]);
  67    pane::init(cx);
  68}
  69
  70#[derive(Clone)]
  71pub struct OpenParams {
  72    pub paths: Vec<PathBuf>,
  73    pub app_state: Arc<AppState>,
  74}
  75
  76fn open(action: &Open, cx: &mut MutableAppContext) {
  77    let app_state = action.0.clone();
  78    cx.prompt_for_paths(
  79        PathPromptOptions {
  80            files: true,
  81            directories: true,
  82            multiple: true,
  83        },
  84        move |paths, cx| {
  85            if let Some(paths) = paths {
  86                cx.dispatch_global_action(OpenPaths(OpenParams { paths, app_state }));
  87            }
  88        },
  89    );
  90}
  91
  92fn open_paths(action: &OpenPaths, cx: &mut MutableAppContext) -> Task<()> {
  93    log::info!("open paths {:?}", action.0.paths);
  94
  95    // Open paths in existing workspace if possible
  96    for window_id in cx.window_ids().collect::<Vec<_>>() {
  97        if let Some(handle) = cx.root_view::<Workspace>(window_id) {
  98            let task = handle.update(cx, |view, cx| {
  99                if view.contains_paths(&action.0.paths, cx.as_ref()) {
 100                    log::info!("open paths on existing workspace");
 101                    Some(view.open_paths(&action.0.paths, cx))
 102                } else {
 103                    None
 104                }
 105            });
 106
 107            if let Some(task) = task {
 108                return task;
 109            }
 110        }
 111    }
 112
 113    log::info!("open new workspace");
 114
 115    // Add a new workspace if necessary
 116
 117    let (_, workspace) = cx.add_window(window_options(), |cx| {
 118        Workspace::new(&action.0.app_state, cx)
 119    });
 120    workspace.update(cx, |workspace, cx| {
 121        workspace.open_paths(&action.0.paths, cx)
 122    })
 123}
 124
 125fn open_new(action: &OpenNew, cx: &mut MutableAppContext) {
 126    cx.add_window(window_options(), |cx| {
 127        let mut view = Workspace::new(action.0.as_ref(), cx);
 128        view.open_new_file(&action, cx);
 129        view
 130    });
 131}
 132
 133fn window_options() -> WindowOptions<'static> {
 134    WindowOptions {
 135        bounds: RectF::new(vec2f(0., 0.), vec2f(1024., 768.)),
 136        title: None,
 137        titlebar_appears_transparent: true,
 138        traffic_light_position: Some(vec2f(8., 8.)),
 139    }
 140}
 141
 142pub trait Item: Entity + Sized {
 143    type View: ItemView;
 144
 145    fn build_view(
 146        handle: ModelHandle<Self>,
 147        settings: watch::Receiver<Settings>,
 148        cx: &mut ViewContext<Self::View>,
 149    ) -> Self::View;
 150
 151    fn file(&self) -> Option<&File>;
 152}
 153
 154pub trait ItemView: View {
 155    fn title(&self, cx: &AppContext) -> String;
 156    fn entry_id(&self, cx: &AppContext) -> Option<(usize, Arc<Path>)>;
 157    fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
 158    where
 159        Self: Sized,
 160    {
 161        None
 162    }
 163    fn is_dirty(&self, _: &AppContext) -> bool {
 164        false
 165    }
 166    fn has_conflict(&self, _: &AppContext) -> bool {
 167        false
 168    }
 169    fn save(&mut self, cx: &mut ViewContext<Self>) -> Result<Task<Result<()>>>;
 170    fn save_as(
 171        &mut self,
 172        worktree: &ModelHandle<Worktree>,
 173        path: &Path,
 174        cx: &mut ViewContext<Self>,
 175    ) -> Task<anyhow::Result<()>>;
 176    fn should_activate_item_on_event(_: &Self::Event) -> bool {
 177        false
 178    }
 179    fn should_close_item_on_event(_: &Self::Event) -> bool {
 180        false
 181    }
 182    fn should_update_tab_on_event(_: &Self::Event) -> bool {
 183        false
 184    }
 185}
 186
 187pub trait ItemHandle: Send + Sync {
 188    fn boxed_clone(&self) -> Box<dyn ItemHandle>;
 189    fn downgrade(&self) -> Box<dyn WeakItemHandle>;
 190}
 191
 192pub trait WeakItemHandle {
 193    fn file<'a>(&'a self, cx: &'a AppContext) -> Option<&'a File>;
 194    fn add_view(
 195        &self,
 196        window_id: usize,
 197        settings: watch::Receiver<Settings>,
 198        cx: &mut MutableAppContext,
 199    ) -> Option<Box<dyn ItemViewHandle>>;
 200    fn alive(&self, cx: &AppContext) -> bool;
 201}
 202
 203pub trait ItemViewHandle {
 204    fn title(&self, cx: &AppContext) -> String;
 205    fn entry_id(&self, cx: &AppContext) -> Option<(usize, Arc<Path>)>;
 206    fn boxed_clone(&self) -> Box<dyn ItemViewHandle>;
 207    fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemViewHandle>>;
 208    fn set_parent_pane(&self, pane: &ViewHandle<Pane>, cx: &mut MutableAppContext);
 209    fn id(&self) -> usize;
 210    fn to_any(&self) -> AnyViewHandle;
 211    fn is_dirty(&self, cx: &AppContext) -> bool;
 212    fn has_conflict(&self, cx: &AppContext) -> bool;
 213    fn save(&self, cx: &mut MutableAppContext) -> Result<Task<Result<()>>>;
 214    fn save_as(
 215        &self,
 216        worktree: &ModelHandle<Worktree>,
 217        path: &Path,
 218        cx: &mut MutableAppContext,
 219    ) -> Task<anyhow::Result<()>>;
 220}
 221
 222impl<T: Item> ItemHandle for ModelHandle<T> {
 223    fn boxed_clone(&self) -> Box<dyn ItemHandle> {
 224        Box::new(self.clone())
 225    }
 226
 227    fn downgrade(&self) -> Box<dyn WeakItemHandle> {
 228        Box::new(self.downgrade())
 229    }
 230}
 231
 232impl<T: Item> WeakItemHandle for WeakModelHandle<T> {
 233    fn file<'a>(&'a self, cx: &'a AppContext) -> Option<&'a File> {
 234        self.upgrade(cx).and_then(|h| h.read(cx).file())
 235    }
 236
 237    fn add_view(
 238        &self,
 239        window_id: usize,
 240        settings: watch::Receiver<Settings>,
 241        cx: &mut MutableAppContext,
 242    ) -> Option<Box<dyn ItemViewHandle>> {
 243        if let Some(handle) = self.upgrade(cx.as_ref()) {
 244            Some(Box::new(cx.add_view(window_id, |cx| {
 245                T::build_view(handle, settings, cx)
 246            })))
 247        } else {
 248            None
 249        }
 250    }
 251
 252    fn alive(&self, cx: &AppContext) -> bool {
 253        self.upgrade(cx).is_some()
 254    }
 255}
 256
 257impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
 258    fn title(&self, cx: &AppContext) -> String {
 259        self.read(cx).title(cx)
 260    }
 261
 262    fn entry_id(&self, cx: &AppContext) -> Option<(usize, Arc<Path>)> {
 263        self.read(cx).entry_id(cx)
 264    }
 265
 266    fn boxed_clone(&self) -> Box<dyn ItemViewHandle> {
 267        Box::new(self.clone())
 268    }
 269
 270    fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemViewHandle>> {
 271        self.update(cx, |item, cx| {
 272            cx.add_option_view(|cx| item.clone_on_split(cx))
 273        })
 274        .map(|handle| Box::new(handle) as Box<dyn ItemViewHandle>)
 275    }
 276
 277    fn set_parent_pane(&self, pane: &ViewHandle<Pane>, cx: &mut MutableAppContext) {
 278        pane.update(cx, |_, cx| {
 279            cx.subscribe(self, |pane, item, event, cx| {
 280                if T::should_close_item_on_event(event) {
 281                    pane.close_item(item.id(), cx);
 282                    return;
 283                }
 284                if T::should_activate_item_on_event(event) {
 285                    if let Some(ix) = pane.item_index(&item) {
 286                        pane.activate_item(ix, cx);
 287                        pane.activate(cx);
 288                    }
 289                }
 290                if T::should_update_tab_on_event(event) {
 291                    cx.notify()
 292                }
 293            })
 294            .detach();
 295        });
 296    }
 297
 298    fn save(&self, cx: &mut MutableAppContext) -> Result<Task<Result<()>>> {
 299        self.update(cx, |item, cx| item.save(cx))
 300    }
 301
 302    fn save_as(
 303        &self,
 304        worktree: &ModelHandle<Worktree>,
 305        path: &Path,
 306        cx: &mut MutableAppContext,
 307    ) -> Task<anyhow::Result<()>> {
 308        self.update(cx, |item, cx| item.save_as(worktree, path, cx))
 309    }
 310
 311    fn is_dirty(&self, cx: &AppContext) -> bool {
 312        self.read(cx).is_dirty(cx)
 313    }
 314
 315    fn has_conflict(&self, cx: &AppContext) -> bool {
 316        self.read(cx).has_conflict(cx)
 317    }
 318
 319    fn id(&self) -> usize {
 320        self.id()
 321    }
 322
 323    fn to_any(&self) -> AnyViewHandle {
 324        self.into()
 325    }
 326}
 327
 328impl Clone for Box<dyn ItemViewHandle> {
 329    fn clone(&self) -> Box<dyn ItemViewHandle> {
 330        self.boxed_clone()
 331    }
 332}
 333
 334impl Clone for Box<dyn ItemHandle> {
 335    fn clone(&self) -> Box<dyn ItemHandle> {
 336        self.boxed_clone()
 337    }
 338}
 339
 340pub struct Workspace {
 341    pub settings: watch::Receiver<Settings>,
 342    languages: Arc<LanguageRegistry>,
 343    rpc: Arc<rpc::Client>,
 344    user_store: ModelHandle<user::UserStore>,
 345    fs: Arc<dyn Fs>,
 346    modal: Option<AnyViewHandle>,
 347    center: PaneGroup,
 348    left_sidebar: Sidebar,
 349    right_sidebar: Sidebar,
 350    panes: Vec<ViewHandle<Pane>>,
 351    active_pane: ViewHandle<Pane>,
 352    worktrees: HashSet<ModelHandle<Worktree>>,
 353    items: Vec<Box<dyn WeakItemHandle>>,
 354    loading_items: HashMap<
 355        (usize, Arc<Path>),
 356        postage::watch::Receiver<Option<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>>>,
 357    >,
 358    _observe_current_user: Task<()>,
 359}
 360
 361impl Workspace {
 362    pub fn new(app_state: &AppState, cx: &mut ViewContext<Self>) -> Self {
 363        let pane = cx.add_view(|_| Pane::new(app_state.settings.clone()));
 364        let pane_id = pane.id();
 365        cx.subscribe(&pane, move |me, _, event, cx| {
 366            me.handle_pane_event(pane_id, event, cx)
 367        })
 368        .detach();
 369        cx.focus(&pane);
 370
 371        let mut left_sidebar = Sidebar::new(Side::Left);
 372        left_sidebar.add_item(
 373            "icons/folder-tree-16.svg",
 374            cx.add_view(|_| ProjectBrowser).into(),
 375        );
 376
 377        let mut right_sidebar = Sidebar::new(Side::Right);
 378        right_sidebar.add_item(
 379            "icons/user-16.svg",
 380            cx.add_view(|cx| {
 381                PeoplePanel::new(app_state.user_store.clone(), app_state.settings.clone(), cx)
 382            })
 383            .into(),
 384        );
 385        right_sidebar.add_item(
 386            "icons/comment-16.svg",
 387            cx.add_view(|cx| {
 388                ChatPanel::new(
 389                    app_state.rpc.clone(),
 390                    app_state.channel_list.clone(),
 391                    app_state.settings.clone(),
 392                    cx,
 393                )
 394            })
 395            .into(),
 396        );
 397
 398        let mut current_user = app_state.user_store.read(cx).watch_current_user().clone();
 399        let mut connection_status = app_state.rpc.status().clone();
 400        let _observe_current_user = cx.spawn_weak(|this, mut cx| async move {
 401            current_user.recv().await;
 402            connection_status.recv().await;
 403            let mut stream =
 404                Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
 405
 406            while stream.recv().await.is_some() {
 407                cx.update(|cx| {
 408                    if let Some(this) = this.upgrade(&cx) {
 409                        this.update(cx, |_, cx| cx.notify());
 410                    }
 411                })
 412            }
 413        });
 414
 415        Workspace {
 416            modal: None,
 417            center: PaneGroup::new(pane.id()),
 418            panes: vec![pane.clone()],
 419            active_pane: pane.clone(),
 420            settings: app_state.settings.clone(),
 421            languages: app_state.languages.clone(),
 422            rpc: app_state.rpc.clone(),
 423            user_store: app_state.user_store.clone(),
 424            fs: app_state.fs.clone(),
 425            left_sidebar,
 426            right_sidebar,
 427            worktrees: Default::default(),
 428            items: Default::default(),
 429            loading_items: Default::default(),
 430            _observe_current_user,
 431        }
 432    }
 433
 434    pub fn worktrees(&self) -> &HashSet<ModelHandle<Worktree>> {
 435        &self.worktrees
 436    }
 437
 438    pub fn contains_paths(&self, paths: &[PathBuf], cx: &AppContext) -> bool {
 439        paths.iter().all(|path| self.contains_path(&path, cx))
 440    }
 441
 442    pub fn contains_path(&self, path: &Path, cx: &AppContext) -> bool {
 443        for worktree in &self.worktrees {
 444            let worktree = worktree.read(cx).as_local();
 445            if worktree.map_or(false, |w| w.contains_abs_path(path)) {
 446                return true;
 447            }
 448        }
 449        false
 450    }
 451
 452    pub fn worktree_scans_complete(&self, cx: &AppContext) -> impl Future<Output = ()> + 'static {
 453        let futures = self
 454            .worktrees
 455            .iter()
 456            .filter_map(|worktree| worktree.read(cx).as_local())
 457            .map(|worktree| worktree.scan_complete())
 458            .collect::<Vec<_>>();
 459        async move {
 460            for future in futures {
 461                future.await;
 462            }
 463        }
 464    }
 465
 466    pub fn open_paths(&mut self, abs_paths: &[PathBuf], cx: &mut ViewContext<Self>) -> Task<()> {
 467        let entries = abs_paths
 468            .iter()
 469            .cloned()
 470            .map(|path| self.entry_id_for_path(&path, cx))
 471            .collect::<Vec<_>>();
 472
 473        let fs = self.fs.clone();
 474        let tasks = abs_paths
 475            .iter()
 476            .cloned()
 477            .zip(entries.into_iter())
 478            .map(|(abs_path, entry_id)| {
 479                cx.spawn(|this, mut cx| {
 480                    let fs = fs.clone();
 481                    async move {
 482                        let entry_id = entry_id.await?;
 483                        if fs.is_file(&abs_path).await {
 484                            if let Some(entry) =
 485                                this.update(&mut cx, |this, cx| this.open_entry(entry_id, cx))
 486                            {
 487                                entry.await;
 488                            }
 489                        }
 490                        Ok(())
 491                    }
 492                })
 493            })
 494            .collect::<Vec<Task<Result<()>>>>();
 495
 496        cx.foreground().spawn(async move {
 497            for task in tasks {
 498                if let Err(error) = task.await {
 499                    log::error!("error opening paths {}", error);
 500                }
 501            }
 502        })
 503    }
 504
 505    fn worktree_for_abs_path(
 506        &self,
 507        abs_path: &Path,
 508        cx: &mut ViewContext<Self>,
 509    ) -> Task<Result<(ModelHandle<Worktree>, PathBuf)>> {
 510        let abs_path: Arc<Path> = Arc::from(abs_path);
 511        cx.spawn(|this, mut cx| async move {
 512            let mut entry_id = None;
 513            this.read_with(&cx, |this, cx| {
 514                for tree in this.worktrees.iter() {
 515                    if let Some(relative_path) = tree
 516                        .read(cx)
 517                        .as_local()
 518                        .and_then(|t| abs_path.strip_prefix(t.abs_path()).ok())
 519                    {
 520                        entry_id = Some((tree.clone(), relative_path.into()));
 521                        break;
 522                    }
 523                }
 524            });
 525
 526            if let Some(entry_id) = entry_id {
 527                Ok(entry_id)
 528            } else {
 529                let worktree = this
 530                    .update(&mut cx, |this, cx| this.add_worktree(&abs_path, cx))
 531                    .await?;
 532                Ok((worktree, PathBuf::new()))
 533            }
 534        })
 535    }
 536
 537    fn entry_id_for_path(
 538        &self,
 539        abs_path: &Path,
 540        cx: &mut ViewContext<Self>,
 541    ) -> Task<Result<(usize, Arc<Path>)>> {
 542        let entry = self.worktree_for_abs_path(abs_path, cx);
 543        cx.spawn(|_, _| async move {
 544            let (worktree, path) = entry.await?;
 545            Ok((worktree.id(), path.into()))
 546        })
 547    }
 548
 549    pub fn add_worktree(
 550        &self,
 551        path: &Path,
 552        cx: &mut ViewContext<Self>,
 553    ) -> Task<Result<ModelHandle<Worktree>>> {
 554        let languages = self.languages.clone();
 555        let rpc = self.rpc.clone();
 556        let fs = self.fs.clone();
 557        let path = Arc::from(path);
 558        cx.spawn(|this, mut cx| async move {
 559            let worktree = Worktree::open_local(rpc, path, fs, languages, &mut cx).await?;
 560            this.update(&mut cx, |this, cx| {
 561                cx.observe(&worktree, |_, _, cx| cx.notify()).detach();
 562                this.worktrees.insert(worktree.clone());
 563                cx.notify();
 564            });
 565            Ok(worktree)
 566        })
 567    }
 568
 569    pub fn toggle_modal<V, F>(&mut self, cx: &mut ViewContext<Self>, add_view: F)
 570    where
 571        V: 'static + View,
 572        F: FnOnce(&mut ViewContext<Self>, &mut Self) -> ViewHandle<V>,
 573    {
 574        if self.modal.as_ref().map_or(false, |modal| modal.is::<V>()) {
 575            self.modal.take();
 576            cx.focus_self();
 577        } else {
 578            let modal = add_view(cx, self);
 579            cx.focus(&modal);
 580            self.modal = Some(modal.into());
 581        }
 582        cx.notify();
 583    }
 584
 585    pub fn modal(&self) -> Option<&AnyViewHandle> {
 586        self.modal.as_ref()
 587    }
 588
 589    pub fn dismiss_modal(&mut self, cx: &mut ViewContext<Self>) {
 590        if self.modal.take().is_some() {
 591            cx.focus(&self.active_pane);
 592            cx.notify();
 593        }
 594    }
 595
 596    pub fn open_new_file(&mut self, _: &OpenNew, cx: &mut ViewContext<Self>) {
 597        let buffer = cx.add_model(|cx| Buffer::new(0, "", cx));
 598        let item_handle = ItemHandle::downgrade(&buffer);
 599        let view = item_handle
 600            .add_view(cx.window_id(), self.settings.clone(), cx)
 601            .unwrap();
 602        self.items.push(item_handle);
 603        self.active_pane().add_item_view(view, cx.as_mut());
 604    }
 605
 606    #[must_use]
 607    pub fn open_entry(
 608        &mut self,
 609        entry: (usize, Arc<Path>),
 610        cx: &mut ViewContext<Self>,
 611    ) -> Option<Task<()>> {
 612        let pane = self.active_pane().clone();
 613        if self.activate_or_open_existing_entry(entry.clone(), &pane, cx) {
 614            return None;
 615        }
 616
 617        let (worktree_id, path) = entry.clone();
 618
 619        let worktree = match self.worktrees.get(&worktree_id).cloned() {
 620            Some(worktree) => worktree,
 621            None => {
 622                log::error!("worktree {} does not exist", worktree_id);
 623                return None;
 624            }
 625        };
 626
 627        if let Entry::Vacant(entry) = self.loading_items.entry(entry.clone()) {
 628            let (mut tx, rx) = postage::watch::channel();
 629            entry.insert(rx);
 630
 631            cx.as_mut()
 632                .spawn(|mut cx| async move {
 633                    let buffer = worktree
 634                        .update(&mut cx, |worktree, cx| {
 635                            worktree.open_buffer(path.as_ref(), cx)
 636                        })
 637                        .await;
 638                    *tx.borrow_mut() = Some(
 639                        buffer
 640                            .map(|buffer| Box::new(buffer) as Box<dyn ItemHandle>)
 641                            .map_err(Arc::new),
 642                    );
 643                })
 644                .detach();
 645        }
 646
 647        let pane = pane.downgrade();
 648        let settings = self.settings.clone();
 649        let mut watch = self.loading_items.get(&entry).unwrap().clone();
 650
 651        Some(cx.spawn(|this, mut cx| async move {
 652            let load_result = loop {
 653                if let Some(load_result) = watch.borrow().as_ref() {
 654                    break load_result.clone();
 655                }
 656                watch.recv().await;
 657            };
 658
 659            this.update(&mut cx, |this, cx| {
 660                this.loading_items.remove(&entry);
 661                if let Some(pane) = pane.upgrade(&cx) {
 662                    match load_result {
 663                        Ok(item) => {
 664                            // By the time loading finishes, the entry could have been already added
 665                            // to the pane. If it was, we activate it, otherwise we'll store the
 666                            // item and add a new view for it.
 667                            if !this.activate_or_open_existing_entry(entry, &pane, cx) {
 668                                let weak_item = item.downgrade();
 669                                let view = weak_item
 670                                    .add_view(cx.window_id(), settings, cx.as_mut())
 671                                    .unwrap();
 672                                this.items.push(weak_item);
 673                                pane.add_item_view(view, cx.as_mut());
 674                            }
 675                        }
 676                        Err(error) => {
 677                            log::error!("error opening item: {}", error);
 678                        }
 679                    }
 680                }
 681            })
 682        }))
 683    }
 684
 685    fn activate_or_open_existing_entry(
 686        &mut self,
 687        entry: (usize, Arc<Path>),
 688        pane: &ViewHandle<Pane>,
 689        cx: &mut ViewContext<Self>,
 690    ) -> bool {
 691        // If the pane contains a view for this file, then activate
 692        // that item view.
 693        if pane.update(cx, |pane, cx| pane.activate_entry(entry.clone(), cx)) {
 694            return true;
 695        }
 696
 697        // Otherwise, if this file is already open somewhere in the workspace,
 698        // then add another view for it.
 699        let settings = self.settings.clone();
 700        let mut view_for_existing_item = None;
 701        self.items.retain(|item| {
 702            if item.alive(cx.as_ref()) {
 703                if view_for_existing_item.is_none()
 704                    && item
 705                        .file(cx.as_ref())
 706                        .map_or(false, |file| file.entry_id() == entry)
 707                {
 708                    view_for_existing_item = Some(
 709                        item.add_view(cx.window_id(), settings.clone(), cx.as_mut())
 710                            .unwrap(),
 711                    );
 712                }
 713                true
 714            } else {
 715                false
 716            }
 717        });
 718        if let Some(view) = view_for_existing_item {
 719            pane.add_item_view(view, cx.as_mut());
 720            true
 721        } else {
 722            false
 723        }
 724    }
 725
 726    pub fn active_item(&self, cx: &ViewContext<Self>) -> Option<Box<dyn ItemViewHandle>> {
 727        self.active_pane().read(cx).active_item()
 728    }
 729
 730    pub fn save_active_item(&mut self, _: &Save, cx: &mut ViewContext<Self>) {
 731        if let Some(item) = self.active_item(cx) {
 732            let handle = cx.handle();
 733            if item.entry_id(cx.as_ref()).is_none() {
 734                let worktree = self.worktrees.iter().next();
 735                let start_abs_path = worktree
 736                    .and_then(|w| w.read(cx).as_local())
 737                    .map_or(Path::new(""), |w| w.abs_path())
 738                    .to_path_buf();
 739                cx.prompt_for_new_path(&start_abs_path, move |abs_path, cx| {
 740                    if let Some(abs_path) = abs_path {
 741                        cx.spawn(|mut cx| async move {
 742                            let result = match handle
 743                                .update(&mut cx, |this, cx| {
 744                                    this.worktree_for_abs_path(&abs_path, cx)
 745                                })
 746                                .await
 747                            {
 748                                Ok((worktree, path)) => {
 749                                    handle
 750                                        .update(&mut cx, |_, cx| {
 751                                            item.save_as(&worktree, &path, cx.as_mut())
 752                                        })
 753                                        .await
 754                                }
 755                                Err(error) => Err(error),
 756                            };
 757
 758                            if let Err(error) = result {
 759                                error!("failed to save item: {:?}, ", error);
 760                            }
 761                        })
 762                        .detach()
 763                    }
 764                });
 765                return;
 766            } else if item.has_conflict(cx.as_ref()) {
 767                const CONFLICT_MESSAGE: &'static str = "This file has changed on disk since you started editing it. Do you want to overwrite it?";
 768
 769                cx.prompt(
 770                    PromptLevel::Warning,
 771                    CONFLICT_MESSAGE,
 772                    &["Overwrite", "Cancel"],
 773                    move |answer, cx| {
 774                        if answer == 0 {
 775                            cx.spawn(|mut cx| async move {
 776                                if let Err(error) = cx.update(|cx| item.save(cx)).unwrap().await {
 777                                    error!("failed to save item: {:?}, ", error);
 778                                }
 779                            })
 780                            .detach();
 781                        }
 782                    },
 783                );
 784            } else {
 785                cx.spawn(|_, mut cx| async move {
 786                    if let Err(error) = cx.update(|cx| item.save(cx)).unwrap().await {
 787                        error!("failed to save item: {:?}, ", error);
 788                    }
 789                })
 790                .detach();
 791            }
 792        }
 793    }
 794
 795    pub fn toggle_sidebar_item(&mut self, action: &ToggleSidebarItem, cx: &mut ViewContext<Self>) {
 796        let sidebar = match action.0.side {
 797            Side::Left => &mut self.left_sidebar,
 798            Side::Right => &mut self.right_sidebar,
 799        };
 800        sidebar.toggle_item(action.0.item_index);
 801        if let Some(active_item) = sidebar.active_item() {
 802            cx.focus(active_item);
 803        } else {
 804            cx.focus_self();
 805        }
 806        cx.notify();
 807    }
 808
 809    pub fn debug_elements(&mut self, _: &DebugElements, cx: &mut ViewContext<Self>) {
 810        match to_string_pretty(&cx.debug_elements()) {
 811            Ok(json) => {
 812                let kib = json.len() as f32 / 1024.;
 813                cx.as_mut().write_to_clipboard(ClipboardItem::new(json));
 814                log::info!(
 815                    "copied {:.1} KiB of element debug JSON to the clipboard",
 816                    kib
 817                );
 818            }
 819            Err(error) => {
 820                log::error!("error debugging elements: {}", error);
 821            }
 822        };
 823    }
 824
 825    fn share_worktree(&mut self, action: &ShareWorktree, cx: &mut ViewContext<Self>) {
 826        let rpc = self.rpc.clone();
 827        let remote_id = action.0;
 828        cx.spawn(|this, mut cx| {
 829            async move {
 830                rpc.authenticate_and_connect(&cx).await?;
 831
 832                let task = this.update(&mut cx, |this, cx| {
 833                    for worktree in &this.worktrees {
 834                        let task = worktree.update(cx, |worktree, cx| {
 835                            worktree.as_local_mut().and_then(|worktree| {
 836                                if worktree.remote_id() == Some(remote_id) {
 837                                    Some(worktree.share(cx))
 838                                } else {
 839                                    None
 840                                }
 841                            })
 842                        });
 843
 844                        if task.is_some() {
 845                            return task;
 846                        }
 847                    }
 848                    None
 849                });
 850
 851                if let Some(share_task) = task {
 852                    share_task.await?;
 853                }
 854
 855                Ok(())
 856            }
 857            .log_err()
 858        })
 859        .detach();
 860    }
 861
 862    fn unshare_worktree(&mut self, action: &UnshareWorktree, cx: &mut ViewContext<Self>) {
 863        let remote_id = action.0;
 864        for worktree in &self.worktrees {
 865            if worktree.update(cx, |worktree, cx| {
 866                if let Some(worktree) = worktree.as_local_mut() {
 867                    if worktree.remote_id() == Some(remote_id) {
 868                        worktree.unshare(cx);
 869                        return true;
 870                    }
 871                }
 872                false
 873            }) {
 874                break;
 875            }
 876        }
 877    }
 878
 879    fn join_worktree(&mut self, action: &JoinWorktree, cx: &mut ViewContext<Self>) {
 880        let rpc = self.rpc.clone();
 881        let languages = self.languages.clone();
 882        let worktree_id = action.0;
 883
 884        cx.spawn(|this, mut cx| {
 885            async move {
 886                rpc.authenticate_and_connect(&cx).await?;
 887                let worktree =
 888                    Worktree::open_remote(rpc.clone(), worktree_id, languages, &mut cx).await?;
 889                this.update(&mut cx, |this, cx| {
 890                    cx.observe(&worktree, |_, _, cx| cx.notify()).detach();
 891                    cx.subscribe(&worktree, move |this, _, event, cx| match event {
 892                        worktree::Event::Closed => {
 893                            this.worktrees.retain(|worktree| {
 894                                worktree.update(cx, |worktree, cx| {
 895                                    if let Some(worktree) = worktree.as_remote_mut() {
 896                                        if worktree.remote_id() == worktree_id {
 897                                            worktree.close_all_buffers(cx);
 898                                            return false;
 899                                        }
 900                                    }
 901                                    true
 902                                })
 903                            });
 904
 905                            cx.notify();
 906                        }
 907                    })
 908                    .detach();
 909                    this.worktrees.insert(worktree);
 910                    cx.notify();
 911                });
 912
 913                Ok(())
 914            }
 915            .log_err()
 916        })
 917        .detach();
 918    }
 919
 920    fn leave_worktree(&mut self, action: &LeaveWorktree, cx: &mut ViewContext<Self>) {
 921        let remote_id = action.0;
 922        cx.spawn(|this, mut cx| {
 923            async move {
 924                this.update(&mut cx, |this, cx| {
 925                    this.worktrees.retain(|worktree| {
 926                        worktree.update(cx, |worktree, cx| {
 927                            if let Some(worktree) = worktree.as_remote_mut() {
 928                                if worktree.remote_id() == remote_id {
 929                                    worktree.close_all_buffers(cx);
 930                                    return false;
 931                                }
 932                            }
 933                            true
 934                        })
 935                    })
 936                });
 937
 938                Ok(())
 939            }
 940            .log_err()
 941        })
 942        .detach();
 943    }
 944
 945    fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
 946        let pane = cx.add_view(|_| Pane::new(self.settings.clone()));
 947        let pane_id = pane.id();
 948        cx.subscribe(&pane, move |me, _, event, cx| {
 949            me.handle_pane_event(pane_id, event, cx)
 950        })
 951        .detach();
 952        self.panes.push(pane.clone());
 953        self.activate_pane(pane.clone(), cx);
 954        pane
 955    }
 956
 957    fn activate_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
 958        self.active_pane = pane;
 959        cx.focus(&self.active_pane);
 960        cx.notify();
 961    }
 962
 963    fn handle_pane_event(
 964        &mut self,
 965        pane_id: usize,
 966        event: &pane::Event,
 967        cx: &mut ViewContext<Self>,
 968    ) {
 969        if let Some(pane) = self.pane(pane_id) {
 970            match event {
 971                pane::Event::Split(direction) => {
 972                    self.split_pane(pane, *direction, cx);
 973                }
 974                pane::Event::Remove => {
 975                    self.remove_pane(pane, cx);
 976                }
 977                pane::Event::Activate => {
 978                    self.activate_pane(pane, cx);
 979                }
 980            }
 981        } else {
 982            error!("pane {} not found", pane_id);
 983        }
 984    }
 985
 986    fn split_pane(
 987        &mut self,
 988        pane: ViewHandle<Pane>,
 989        direction: SplitDirection,
 990        cx: &mut ViewContext<Self>,
 991    ) -> ViewHandle<Pane> {
 992        let new_pane = self.add_pane(cx);
 993        self.activate_pane(new_pane.clone(), cx);
 994        if let Some(item) = pane.read(cx).active_item() {
 995            if let Some(clone) = item.clone_on_split(cx.as_mut()) {
 996                new_pane.add_item_view(clone, cx.as_mut());
 997            }
 998        }
 999        self.center
1000            .split(pane.id(), new_pane.id(), direction)
1001            .unwrap();
1002        cx.notify();
1003        new_pane
1004    }
1005
1006    fn remove_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
1007        if self.center.remove(pane.id()).unwrap() {
1008            self.panes.retain(|p| p != &pane);
1009            self.activate_pane(self.panes.last().unwrap().clone(), cx);
1010        }
1011    }
1012
1013    fn pane(&self, pane_id: usize) -> Option<ViewHandle<Pane>> {
1014        self.panes.iter().find(|pane| pane.id() == pane_id).cloned()
1015    }
1016
1017    pub fn active_pane(&self) -> &ViewHandle<Pane> {
1018        &self.active_pane
1019    }
1020
1021    fn render_connection_status(&self) -> Option<ElementBox> {
1022        let theme = &self.settings.borrow().theme;
1023        match &*self.rpc.status().borrow() {
1024            rpc::Status::ConnectionError
1025            | rpc::Status::ConnectionLost
1026            | rpc::Status::Reauthenticating
1027            | rpc::Status::Reconnecting { .. }
1028            | rpc::Status::ReconnectionError { .. } => Some(
1029                Container::new(
1030                    Align::new(
1031                        ConstrainedBox::new(
1032                            Svg::new("icons/offline-14.svg")
1033                                .with_color(theme.workspace.titlebar.icon_color)
1034                                .boxed(),
1035                        )
1036                        .with_width(theme.workspace.titlebar.offline_icon.width)
1037                        .boxed(),
1038                    )
1039                    .boxed(),
1040                )
1041                .with_style(theme.workspace.titlebar.offline_icon.container)
1042                .boxed(),
1043            ),
1044            rpc::Status::UpgradeRequired => Some(
1045                Label::new(
1046                    "Please update Zed to collaborate".to_string(),
1047                    theme.workspace.titlebar.outdated_warning.text.clone(),
1048                )
1049                .contained()
1050                .with_style(theme.workspace.titlebar.outdated_warning.container)
1051                .aligned()
1052                .boxed(),
1053            ),
1054            _ => None,
1055        }
1056    }
1057
1058    fn render_avatar(&self, cx: &mut RenderContext<Self>) -> ElementBox {
1059        let theme = &self.settings.borrow().theme;
1060        let avatar = if let Some(avatar) = self
1061            .user_store
1062            .read(cx)
1063            .current_user()
1064            .and_then(|user| user.avatar.clone())
1065        {
1066            Image::new(avatar)
1067                .with_style(theme.workspace.titlebar.avatar)
1068                .boxed()
1069        } else {
1070            MouseEventHandler::new::<Authenticate, _, _, _>(0, cx, |_, _| {
1071                Svg::new("icons/signed-out-12.svg")
1072                    .with_color(theme.workspace.titlebar.icon_color)
1073                    .boxed()
1074            })
1075            .on_click(|cx| cx.dispatch_action(Authenticate))
1076            .with_cursor_style(CursorStyle::PointingHand)
1077            .boxed()
1078        };
1079
1080        ConstrainedBox::new(
1081            Align::new(
1082                ConstrainedBox::new(avatar)
1083                    .with_width(theme.workspace.titlebar.avatar_width)
1084                    .boxed(),
1085            )
1086            .boxed(),
1087        )
1088        .with_width(theme.workspace.right_sidebar.width)
1089        .boxed()
1090    }
1091}
1092
1093impl Entity for Workspace {
1094    type Event = ();
1095}
1096
1097impl View for Workspace {
1098    fn ui_name() -> &'static str {
1099        "Workspace"
1100    }
1101
1102    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
1103        let settings = self.settings.borrow();
1104        let theme = &settings.theme;
1105        Container::new(
1106            Flex::column()
1107                .with_child(
1108                    ConstrainedBox::new(
1109                        Container::new(
1110                            Stack::new()
1111                                .with_child(
1112                                    Align::new(
1113                                        Label::new(
1114                                            "zed".into(),
1115                                            theme.workspace.titlebar.title.clone(),
1116                                        )
1117                                        .boxed(),
1118                                    )
1119                                    .boxed(),
1120                                )
1121                                .with_child(
1122                                    Align::new(
1123                                        Flex::row()
1124                                            .with_children(self.render_connection_status())
1125                                            .with_child(self.render_avatar(cx))
1126                                            .boxed(),
1127                                    )
1128                                    .right()
1129                                    .boxed(),
1130                                )
1131                                .boxed(),
1132                        )
1133                        .with_style(theme.workspace.titlebar.container)
1134                        .boxed(),
1135                    )
1136                    .with_height(32.)
1137                    .named("titlebar"),
1138                )
1139                .with_child(
1140                    Expanded::new(
1141                        1.0,
1142                        Stack::new()
1143                            .with_child({
1144                                let mut content = Flex::row();
1145                                content.add_child(self.left_sidebar.render(&settings, cx));
1146                                if let Some(element) =
1147                                    self.left_sidebar.render_active_item(&settings, cx)
1148                                {
1149                                    content.add_child(Flexible::new(0.8, element).boxed());
1150                                }
1151                                content.add_child(
1152                                    Expanded::new(1.0, self.center.render(&settings.theme)).boxed(),
1153                                );
1154                                if let Some(element) =
1155                                    self.right_sidebar.render_active_item(&settings, cx)
1156                                {
1157                                    content.add_child(Flexible::new(0.8, element).boxed());
1158                                }
1159                                content.add_child(self.right_sidebar.render(&settings, cx));
1160                                content.boxed()
1161                            })
1162                            .with_children(
1163                                self.modal.as_ref().map(|m| ChildView::new(m.id()).boxed()),
1164                            )
1165                            .boxed(),
1166                    )
1167                    .boxed(),
1168                )
1169                .boxed(),
1170        )
1171        .with_background_color(settings.theme.workspace.background)
1172        .named("workspace")
1173    }
1174
1175    fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
1176        cx.focus(&self.active_pane);
1177    }
1178}
1179
1180#[cfg(test)]
1181pub trait WorkspaceHandle {
1182    fn file_entries(&self, cx: &AppContext) -> Vec<(usize, Arc<Path>)>;
1183}
1184
1185#[cfg(test)]
1186impl WorkspaceHandle for ViewHandle<Workspace> {
1187    fn file_entries(&self, cx: &AppContext) -> Vec<(usize, Arc<Path>)> {
1188        self.read(cx)
1189            .worktrees()
1190            .iter()
1191            .flat_map(|tree| {
1192                let tree_id = tree.id();
1193                tree.read(cx)
1194                    .files(0)
1195                    .map(move |f| (tree_id, f.path.clone()))
1196            })
1197            .collect::<Vec<_>>()
1198    }
1199}
1200
1201#[cfg(test)]
1202mod tests {
1203    use super::*;
1204    use crate::{
1205        editor::{Editor, Insert},
1206        fs::FakeFs,
1207        test::{temp_tree, test_app_state},
1208    };
1209    use serde_json::json;
1210    use std::collections::HashSet;
1211
1212    #[gpui::test]
1213    async fn test_open_paths_action(mut cx: gpui::TestAppContext) {
1214        let app_state = cx.update(test_app_state);
1215        let dir = temp_tree(json!({
1216            "a": {
1217                "aa": null,
1218                "ab": null,
1219            },
1220            "b": {
1221                "ba": null,
1222                "bb": null,
1223            },
1224            "c": {
1225                "ca": null,
1226                "cb": null,
1227            },
1228        }));
1229
1230        cx.update(|cx| {
1231            open_paths(
1232                &OpenPaths(OpenParams {
1233                    paths: vec![
1234                        dir.path().join("a").to_path_buf(),
1235                        dir.path().join("b").to_path_buf(),
1236                    ],
1237                    app_state: app_state.clone(),
1238                }),
1239                cx,
1240            )
1241        })
1242        .await;
1243        assert_eq!(cx.window_ids().len(), 1);
1244
1245        cx.update(|cx| {
1246            open_paths(
1247                &OpenPaths(OpenParams {
1248                    paths: vec![dir.path().join("a").to_path_buf()],
1249                    app_state: app_state.clone(),
1250                }),
1251                cx,
1252            )
1253        })
1254        .await;
1255        assert_eq!(cx.window_ids().len(), 1);
1256        let workspace_1 = cx.root_view::<Workspace>(cx.window_ids()[0]).unwrap();
1257        workspace_1.read_with(&cx, |workspace, _| {
1258            assert_eq!(workspace.worktrees().len(), 2)
1259        });
1260
1261        cx.update(|cx| {
1262            open_paths(
1263                &OpenPaths(OpenParams {
1264                    paths: vec![
1265                        dir.path().join("b").to_path_buf(),
1266                        dir.path().join("c").to_path_buf(),
1267                    ],
1268                    app_state: app_state.clone(),
1269                }),
1270                cx,
1271            )
1272        })
1273        .await;
1274        assert_eq!(cx.window_ids().len(), 2);
1275    }
1276
1277    #[gpui::test]
1278    async fn test_open_entry(mut cx: gpui::TestAppContext) {
1279        let app_state = cx.update(test_app_state);
1280        app_state
1281            .fs
1282            .as_fake()
1283            .insert_tree(
1284                "/root",
1285                json!({
1286                    "a": {
1287                        "file1": "contents 1",
1288                        "file2": "contents 2",
1289                        "file3": "contents 3",
1290                    },
1291                }),
1292            )
1293            .await;
1294
1295        let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx));
1296        workspace
1297            .update(&mut cx, |workspace, cx| {
1298                workspace.add_worktree(Path::new("/root"), cx)
1299            })
1300            .await
1301            .unwrap();
1302
1303        cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
1304            .await;
1305        let entries = cx.read(|cx| workspace.file_entries(cx));
1306        let file1 = entries[0].clone();
1307        let file2 = entries[1].clone();
1308        let file3 = entries[2].clone();
1309
1310        // Open the first entry
1311        workspace
1312            .update(&mut cx, |w, cx| w.open_entry(file1.clone(), cx))
1313            .unwrap()
1314            .await;
1315        cx.read(|cx| {
1316            let pane = workspace.read(cx).active_pane().read(cx);
1317            assert_eq!(
1318                pane.active_item().unwrap().entry_id(cx),
1319                Some(file1.clone())
1320            );
1321            assert_eq!(pane.items().len(), 1);
1322        });
1323
1324        // Open the second entry
1325        workspace
1326            .update(&mut cx, |w, cx| w.open_entry(file2.clone(), cx))
1327            .unwrap()
1328            .await;
1329        cx.read(|cx| {
1330            let pane = workspace.read(cx).active_pane().read(cx);
1331            assert_eq!(
1332                pane.active_item().unwrap().entry_id(cx),
1333                Some(file2.clone())
1334            );
1335            assert_eq!(pane.items().len(), 2);
1336        });
1337
1338        // Open the first entry again. The existing pane item is activated.
1339        workspace.update(&mut cx, |w, cx| {
1340            assert!(w.open_entry(file1.clone(), cx).is_none())
1341        });
1342        cx.read(|cx| {
1343            let pane = workspace.read(cx).active_pane().read(cx);
1344            assert_eq!(
1345                pane.active_item().unwrap().entry_id(cx),
1346                Some(file1.clone())
1347            );
1348            assert_eq!(pane.items().len(), 2);
1349        });
1350
1351        // Split the pane with the first entry, then open the second entry again.
1352        workspace.update(&mut cx, |w, cx| {
1353            w.split_pane(w.active_pane().clone(), SplitDirection::Right, cx);
1354            assert!(w.open_entry(file2.clone(), cx).is_none());
1355            assert_eq!(
1356                w.active_pane()
1357                    .read(cx)
1358                    .active_item()
1359                    .unwrap()
1360                    .entry_id(cx.as_ref()),
1361                Some(file2.clone())
1362            );
1363        });
1364
1365        // Open the third entry twice concurrently. Only one pane item is added.
1366        let (t1, t2) = workspace.update(&mut cx, |w, cx| {
1367            (
1368                w.open_entry(file3.clone(), cx).unwrap(),
1369                w.open_entry(file3.clone(), cx).unwrap(),
1370            )
1371        });
1372        t1.await;
1373        t2.await;
1374        cx.read(|cx| {
1375            let pane = workspace.read(cx).active_pane().read(cx);
1376            assert_eq!(
1377                pane.active_item().unwrap().entry_id(cx),
1378                Some(file3.clone())
1379            );
1380            let pane_entries = pane
1381                .items()
1382                .iter()
1383                .map(|i| i.entry_id(cx).unwrap())
1384                .collect::<Vec<_>>();
1385            assert_eq!(pane_entries, &[file1, file2, file3]);
1386        });
1387    }
1388
1389    #[gpui::test]
1390    async fn test_open_paths(mut cx: gpui::TestAppContext) {
1391        let fs = FakeFs::new();
1392        fs.insert_dir("/dir1").await.unwrap();
1393        fs.insert_dir("/dir2").await.unwrap();
1394        fs.insert_file("/dir1/a.txt", "".into()).await.unwrap();
1395        fs.insert_file("/dir2/b.txt", "".into()).await.unwrap();
1396
1397        let mut app_state = cx.update(test_app_state);
1398        Arc::get_mut(&mut app_state).unwrap().fs = Arc::new(fs);
1399
1400        let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx));
1401        workspace
1402            .update(&mut cx, |workspace, cx| {
1403                workspace.add_worktree("/dir1".as_ref(), cx)
1404            })
1405            .await
1406            .unwrap();
1407        cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
1408            .await;
1409
1410        // Open a file within an existing worktree.
1411        cx.update(|cx| {
1412            workspace.update(cx, |view, cx| view.open_paths(&["/dir1/a.txt".into()], cx))
1413        })
1414        .await;
1415        cx.read(|cx| {
1416            assert_eq!(
1417                workspace
1418                    .read(cx)
1419                    .active_pane()
1420                    .read(cx)
1421                    .active_item()
1422                    .unwrap()
1423                    .title(cx),
1424                "a.txt"
1425            );
1426        });
1427
1428        // Open a file outside of any existing worktree.
1429        cx.update(|cx| {
1430            workspace.update(cx, |view, cx| view.open_paths(&["/dir2/b.txt".into()], cx))
1431        })
1432        .await;
1433        cx.read(|cx| {
1434            let worktree_roots = workspace
1435                .read(cx)
1436                .worktrees()
1437                .iter()
1438                .map(|w| w.read(cx).as_local().unwrap().abs_path())
1439                .collect::<HashSet<_>>();
1440            assert_eq!(
1441                worktree_roots,
1442                vec!["/dir1", "/dir2/b.txt"]
1443                    .into_iter()
1444                    .map(Path::new)
1445                    .collect(),
1446            );
1447            assert_eq!(
1448                workspace
1449                    .read(cx)
1450                    .active_pane()
1451                    .read(cx)
1452                    .active_item()
1453                    .unwrap()
1454                    .title(cx),
1455                "b.txt"
1456            );
1457        });
1458    }
1459
1460    #[gpui::test]
1461    async fn test_save_conflicting_item(mut cx: gpui::TestAppContext) {
1462        let app_state = cx.update(test_app_state);
1463        app_state
1464            .fs
1465            .as_fake()
1466            .insert_tree(
1467                "/root",
1468                json!({
1469                    "a.txt": "",
1470                }),
1471            )
1472            .await;
1473
1474        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx));
1475        workspace
1476            .update(&mut cx, |workspace, cx| {
1477                workspace.add_worktree(Path::new("/root"), cx)
1478            })
1479            .await
1480            .unwrap();
1481
1482        // Open a file within an existing worktree.
1483        cx.update(|cx| {
1484            workspace.update(cx, |view, cx| {
1485                view.open_paths(&[PathBuf::from("/root/a.txt")], cx)
1486            })
1487        })
1488        .await;
1489        let editor = cx.read(|cx| {
1490            let pane = workspace.read(cx).active_pane().read(cx);
1491            let item = pane.active_item().unwrap();
1492            item.to_any().downcast::<Editor>().unwrap()
1493        });
1494
1495        cx.update(|cx| editor.update(cx, |editor, cx| editor.insert(&Insert("x".into()), cx)));
1496        app_state
1497            .fs
1498            .as_fake()
1499            .insert_file("/root/a.txt", "changed".to_string())
1500            .await
1501            .unwrap();
1502        editor
1503            .condition(&cx, |editor, cx| editor.has_conflict(cx))
1504            .await;
1505        cx.read(|cx| assert!(editor.is_dirty(cx)));
1506
1507        cx.update(|cx| workspace.update(cx, |w, cx| w.save_active_item(&Save, cx)));
1508        cx.simulate_prompt_answer(window_id, 0);
1509        editor
1510            .condition(&cx, |editor, cx| !editor.is_dirty(cx))
1511            .await;
1512        cx.read(|cx| assert!(!editor.has_conflict(cx)));
1513    }
1514
1515    #[gpui::test]
1516    async fn test_open_and_save_new_file(mut cx: gpui::TestAppContext) {
1517        let app_state = cx.update(test_app_state);
1518        app_state.fs.as_fake().insert_dir("/root").await.unwrap();
1519        let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx));
1520        workspace
1521            .update(&mut cx, |workspace, cx| {
1522                workspace.add_worktree(Path::new("/root"), cx)
1523            })
1524            .await
1525            .unwrap();
1526        let tree = cx.read(|cx| {
1527            workspace
1528                .read(cx)
1529                .worktrees()
1530                .iter()
1531                .next()
1532                .unwrap()
1533                .clone()
1534        });
1535
1536        // Create a new untitled buffer
1537        let editor = workspace.update(&mut cx, |workspace, cx| {
1538            workspace.open_new_file(&OpenNew(app_state.clone()), cx);
1539            workspace
1540                .active_item(cx)
1541                .unwrap()
1542                .to_any()
1543                .downcast::<Editor>()
1544                .unwrap()
1545        });
1546
1547        editor.update(&mut cx, |editor, cx| {
1548            assert!(!editor.is_dirty(cx.as_ref()));
1549            assert_eq!(editor.title(cx.as_ref()), "untitled");
1550            assert!(editor.language(cx).is_none());
1551            editor.insert(&Insert("hi".into()), cx);
1552            assert!(editor.is_dirty(cx.as_ref()));
1553        });
1554
1555        // Save the buffer. This prompts for a filename.
1556        workspace.update(&mut cx, |workspace, cx| {
1557            workspace.save_active_item(&Save, cx)
1558        });
1559        cx.simulate_new_path_selection(|parent_dir| {
1560            assert_eq!(parent_dir, Path::new("/root"));
1561            Some(parent_dir.join("the-new-name.rs"))
1562        });
1563        cx.read(|cx| {
1564            assert!(editor.is_dirty(cx));
1565            assert_eq!(editor.title(cx), "untitled");
1566        });
1567
1568        // When the save completes, the buffer's title is updated.
1569        editor
1570            .condition(&cx, |editor, cx| !editor.is_dirty(cx))
1571            .await;
1572        cx.read(|cx| {
1573            assert!(!editor.is_dirty(cx));
1574            assert_eq!(editor.title(cx), "the-new-name.rs");
1575        });
1576        // The language is assigned based on the path
1577        editor.read_with(&cx, |editor, cx| {
1578            assert_eq!(editor.language(cx).unwrap().name(), "Rust")
1579        });
1580
1581        // Edit the file and save it again. This time, there is no filename prompt.
1582        editor.update(&mut cx, |editor, cx| {
1583            editor.insert(&Insert(" there".into()), cx);
1584            assert_eq!(editor.is_dirty(cx.as_ref()), true);
1585        });
1586        workspace.update(&mut cx, |workspace, cx| {
1587            workspace.save_active_item(&Save, cx)
1588        });
1589        assert!(!cx.did_prompt_for_new_path());
1590        editor
1591            .condition(&cx, |editor, cx| !editor.is_dirty(cx))
1592            .await;
1593        cx.read(|cx| assert_eq!(editor.title(cx), "the-new-name.rs"));
1594
1595        // Open the same newly-created file in another pane item. The new editor should reuse
1596        // the same buffer.
1597        workspace.update(&mut cx, |workspace, cx| {
1598            workspace.open_new_file(&OpenNew(app_state.clone()), cx);
1599            workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
1600            assert!(workspace
1601                .open_entry((tree.id(), Path::new("the-new-name.rs").into()), cx)
1602                .is_none());
1603        });
1604        let editor2 = workspace.update(&mut cx, |workspace, cx| {
1605            workspace
1606                .active_item(cx)
1607                .unwrap()
1608                .to_any()
1609                .downcast::<Editor>()
1610                .unwrap()
1611        });
1612        cx.read(|cx| {
1613            assert_eq!(editor2.read(cx).buffer(), editor.read(cx).buffer());
1614        })
1615    }
1616
1617    #[gpui::test]
1618    async fn test_setting_language_when_saving_as_single_file_worktree(
1619        mut cx: gpui::TestAppContext,
1620    ) {
1621        let app_state = cx.update(test_app_state);
1622        app_state.fs.as_fake().insert_dir("/root").await.unwrap();
1623        let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx));
1624
1625        // Create a new untitled buffer
1626        let editor = workspace.update(&mut cx, |workspace, cx| {
1627            workspace.open_new_file(&OpenNew(app_state.clone()), cx);
1628            workspace
1629                .active_item(cx)
1630                .unwrap()
1631                .to_any()
1632                .downcast::<Editor>()
1633                .unwrap()
1634        });
1635
1636        editor.update(&mut cx, |editor, cx| {
1637            assert!(editor.language(cx).is_none());
1638            editor.insert(&Insert("hi".into()), cx);
1639            assert!(editor.is_dirty(cx.as_ref()));
1640        });
1641
1642        // Save the buffer. This prompts for a filename.
1643        workspace.update(&mut cx, |workspace, cx| {
1644            workspace.save_active_item(&Save, cx)
1645        });
1646        cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs")));
1647
1648        editor
1649            .condition(&cx, |editor, cx| !editor.is_dirty(cx))
1650            .await;
1651
1652        // The language is assigned based on the path
1653        editor.read_with(&cx, |editor, cx| {
1654            assert_eq!(editor.language(cx).unwrap().name(), "Rust")
1655        });
1656    }
1657
1658    #[gpui::test]
1659    async fn test_new_empty_workspace(mut cx: gpui::TestAppContext) {
1660        cx.update(init);
1661
1662        let app_state = cx.update(test_app_state);
1663        cx.dispatch_global_action(OpenNew(app_state.clone()));
1664        let window_id = *cx.window_ids().first().unwrap();
1665        let workspace = cx.root_view::<Workspace>(window_id).unwrap();
1666        let editor = workspace.update(&mut cx, |workspace, cx| {
1667            workspace
1668                .active_item(cx)
1669                .unwrap()
1670                .to_any()
1671                .downcast::<Editor>()
1672                .unwrap()
1673        });
1674
1675        editor.update(&mut cx, |editor, cx| {
1676            assert!(editor.text(cx).is_empty());
1677        });
1678
1679        workspace.update(&mut cx, |workspace, cx| {
1680            workspace.save_active_item(&Save, cx)
1681        });
1682
1683        app_state.fs.as_fake().insert_dir("/root").await.unwrap();
1684        cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name")));
1685
1686        editor
1687            .condition(&cx, |editor, cx| editor.title(cx) == "the-new-name")
1688            .await;
1689        editor.update(&mut cx, |editor, cx| {
1690            assert!(!editor.is_dirty(cx));
1691        });
1692    }
1693
1694    #[gpui::test]
1695    async fn test_pane_actions(mut cx: gpui::TestAppContext) {
1696        cx.update(|cx| pane::init(cx));
1697        let app_state = cx.update(test_app_state);
1698        app_state
1699            .fs
1700            .as_fake()
1701            .insert_tree(
1702                "/root",
1703                json!({
1704                    "a": {
1705                        "file1": "contents 1",
1706                        "file2": "contents 2",
1707                        "file3": "contents 3",
1708                    },
1709                }),
1710            )
1711            .await;
1712
1713        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx));
1714        workspace
1715            .update(&mut cx, |workspace, cx| {
1716                workspace.add_worktree(Path::new("/root"), cx)
1717            })
1718            .await
1719            .unwrap();
1720        cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
1721            .await;
1722        let entries = cx.read(|cx| workspace.file_entries(cx));
1723        let file1 = entries[0].clone();
1724
1725        let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone());
1726
1727        workspace
1728            .update(&mut cx, |w, cx| w.open_entry(file1.clone(), cx))
1729            .unwrap()
1730            .await;
1731        cx.read(|cx| {
1732            assert_eq!(
1733                pane_1.read(cx).active_item().unwrap().entry_id(cx),
1734                Some(file1.clone())
1735            );
1736        });
1737
1738        cx.dispatch_action(
1739            window_id,
1740            vec![pane_1.id()],
1741            pane::Split(SplitDirection::Right),
1742        );
1743        cx.update(|cx| {
1744            let pane_2 = workspace.read(cx).active_pane().clone();
1745            assert_ne!(pane_1, pane_2);
1746
1747            let pane2_item = pane_2.read(cx).active_item().unwrap();
1748            assert_eq!(pane2_item.entry_id(cx.as_ref()), Some(file1.clone()));
1749
1750            cx.dispatch_action(window_id, vec![pane_2.id()], &CloseActiveItem);
1751            let workspace = workspace.read(cx);
1752            assert_eq!(workspace.panes.len(), 1);
1753            assert_eq!(workspace.active_pane(), &pane_1);
1754        });
1755    }
1756}