workspace.rs

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