workspace.rs

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