workspace.rs

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