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