lib.rs

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