lib.rs

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