workspace.rs

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