pane.rs

   1use super::{ItemHandle, SplitDirection};
   2use crate::{toolbar::Toolbar, Item, WeakItemHandle, Workspace};
   3use anyhow::Result;
   4use collections::{HashMap, HashSet, VecDeque};
   5use context_menu::{ContextMenu, ContextMenuItem};
   6use futures::StreamExt;
   7use gpui::{
   8    actions,
   9    elements::*,
  10    geometry::{
  11        rect::RectF,
  12        vector::{vec2f, Vector2F},
  13    },
  14    impl_actions, impl_internal_actions,
  15    platform::{CursorStyle, NavigationDirection},
  16    AppContext, AsyncAppContext, Entity, ModelHandle, MouseButton, MouseButtonEvent,
  17    MutableAppContext, PromptLevel, Quad, RenderContext, Task, View, ViewContext, ViewHandle,
  18    WeakViewHandle,
  19};
  20use project::{Project, ProjectEntryId, ProjectPath};
  21use serde::Deserialize;
  22use settings::{Autosave, Settings};
  23use std::{any::Any, cell::RefCell, mem, path::Path, rc::Rc};
  24use util::ResultExt;
  25
  26#[derive(Clone, Deserialize, PartialEq)]
  27pub struct ActivateItem(pub usize);
  28
  29actions!(
  30    pane,
  31    [
  32        ActivatePrevItem,
  33        ActivateNextItem,
  34        ActivateLastItem,
  35        CloseActiveItem,
  36        CloseInactiveItems,
  37        ReopenClosedItem,
  38        SplitLeft,
  39        SplitUp,
  40        SplitRight,
  41        SplitDown,
  42    ]
  43);
  44
  45#[derive(Clone, PartialEq)]
  46pub struct CloseItem {
  47    pub item_id: usize,
  48    pub pane: WeakViewHandle<Pane>,
  49}
  50
  51#[derive(Clone, Deserialize, PartialEq)]
  52pub struct GoBack {
  53    #[serde(skip_deserializing)]
  54    pub pane: Option<WeakViewHandle<Pane>>,
  55}
  56
  57#[derive(Clone, Deserialize, PartialEq)]
  58pub struct GoForward {
  59    #[serde(skip_deserializing)]
  60    pub pane: Option<WeakViewHandle<Pane>>,
  61}
  62
  63#[derive(Clone, PartialEq)]
  64pub struct DeploySplitMenu {
  65    position: Vector2F,
  66}
  67
  68impl_actions!(pane, [GoBack, GoForward, ActivateItem]);
  69impl_internal_actions!(pane, [CloseItem, DeploySplitMenu]);
  70
  71const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
  72
  73pub fn init(cx: &mut MutableAppContext) {
  74    cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
  75        pane.activate_item(action.0, true, true, false, cx);
  76    });
  77    cx.add_action(|pane: &mut Pane, _: &ActivateLastItem, cx| {
  78        pane.activate_item(pane.items.len() - 1, true, true, false, cx);
  79    });
  80    cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
  81        pane.activate_prev_item(cx);
  82    });
  83    cx.add_action(|pane: &mut Pane, _: &ActivateNextItem, cx| {
  84        pane.activate_next_item(cx);
  85    });
  86    cx.add_async_action(Pane::close_active_item);
  87    cx.add_async_action(Pane::close_inactive_items);
  88    cx.add_async_action(|workspace: &mut Workspace, action: &CloseItem, cx| {
  89        let pane = action.pane.upgrade(cx)?;
  90        let task = Pane::close_item(workspace, pane, action.item_id, cx);
  91        Some(cx.foreground().spawn(async move {
  92            task.await?;
  93            Ok(())
  94        }))
  95    });
  96    cx.add_action(|pane: &mut Pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx));
  97    cx.add_action(|pane: &mut Pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx));
  98    cx.add_action(|pane: &mut Pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx));
  99    cx.add_action(|pane: &mut Pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx));
 100    cx.add_action(Pane::deploy_split_menu);
 101    cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
 102        Pane::reopen_closed_item(workspace, cx).detach();
 103    });
 104    cx.add_action(|workspace: &mut Workspace, action: &GoBack, cx| {
 105        Pane::go_back(
 106            workspace,
 107            action
 108                .pane
 109                .as_ref()
 110                .and_then(|weak_handle| weak_handle.upgrade(cx)),
 111            cx,
 112        )
 113        .detach();
 114    });
 115    cx.add_action(|workspace: &mut Workspace, action: &GoForward, cx| {
 116        Pane::go_forward(
 117            workspace,
 118            action
 119                .pane
 120                .as_ref()
 121                .and_then(|weak_handle| weak_handle.upgrade(cx)),
 122            cx,
 123        )
 124        .detach();
 125    });
 126}
 127
 128pub enum Event {
 129    Activate,
 130    ActivateItem { local: bool },
 131    Remove,
 132    RemoveItem,
 133    Split(SplitDirection),
 134    ChangeItemTitle,
 135}
 136
 137pub struct Pane {
 138    items: Vec<Box<dyn ItemHandle>>,
 139    active_item_index: usize,
 140    autoscroll: bool,
 141    nav_history: Rc<RefCell<NavHistory>>,
 142    toolbar: ViewHandle<Toolbar>,
 143    split_menu: ViewHandle<ContextMenu>,
 144}
 145
 146pub struct ItemNavHistory {
 147    history: Rc<RefCell<NavHistory>>,
 148    item: Rc<dyn WeakItemHandle>,
 149}
 150
 151struct NavHistory {
 152    mode: NavigationMode,
 153    backward_stack: VecDeque<NavigationEntry>,
 154    forward_stack: VecDeque<NavigationEntry>,
 155    closed_stack: VecDeque<NavigationEntry>,
 156    paths_by_item: HashMap<usize, ProjectPath>,
 157    pane: WeakViewHandle<Pane>,
 158}
 159
 160#[derive(Copy, Clone)]
 161enum NavigationMode {
 162    Normal,
 163    GoingBack,
 164    GoingForward,
 165    ClosingItem,
 166    ReopeningClosedItem,
 167    Disabled,
 168}
 169
 170impl Default for NavigationMode {
 171    fn default() -> Self {
 172        Self::Normal
 173    }
 174}
 175
 176pub struct NavigationEntry {
 177    pub item: Rc<dyn WeakItemHandle>,
 178    pub data: Option<Box<dyn Any>>,
 179}
 180
 181impl Pane {
 182    pub fn new(cx: &mut ViewContext<Self>) -> Self {
 183        let handle = cx.weak_handle();
 184        let split_menu = cx.add_view(|cx| ContextMenu::new(cx));
 185        Self {
 186            items: Vec::new(),
 187            active_item_index: 0,
 188            autoscroll: false,
 189            nav_history: Rc::new(RefCell::new(NavHistory {
 190                mode: NavigationMode::Normal,
 191                backward_stack: Default::default(),
 192                forward_stack: Default::default(),
 193                closed_stack: Default::default(),
 194                paths_by_item: Default::default(),
 195                pane: handle.clone(),
 196            })),
 197            toolbar: cx.add_view(|_| Toolbar::new(handle)),
 198            split_menu,
 199        }
 200    }
 201
 202    pub fn nav_history_for_item<T: Item>(&self, item: &ViewHandle<T>) -> ItemNavHistory {
 203        ItemNavHistory {
 204            history: self.nav_history.clone(),
 205            item: Rc::new(item.downgrade()),
 206        }
 207    }
 208
 209    pub fn activate(&self, cx: &mut ViewContext<Self>) {
 210        cx.emit(Event::Activate);
 211    }
 212
 213    pub fn go_back(
 214        workspace: &mut Workspace,
 215        pane: Option<ViewHandle<Pane>>,
 216        cx: &mut ViewContext<Workspace>,
 217    ) -> Task<()> {
 218        Self::navigate_history(
 219            workspace,
 220            pane.unwrap_or_else(|| workspace.active_pane().clone()),
 221            NavigationMode::GoingBack,
 222            cx,
 223        )
 224    }
 225
 226    pub fn go_forward(
 227        workspace: &mut Workspace,
 228        pane: Option<ViewHandle<Pane>>,
 229        cx: &mut ViewContext<Workspace>,
 230    ) -> Task<()> {
 231        Self::navigate_history(
 232            workspace,
 233            pane.unwrap_or_else(|| workspace.active_pane().clone()),
 234            NavigationMode::GoingForward,
 235            cx,
 236        )
 237    }
 238
 239    pub fn reopen_closed_item(
 240        workspace: &mut Workspace,
 241        cx: &mut ViewContext<Workspace>,
 242    ) -> Task<()> {
 243        Self::navigate_history(
 244            workspace,
 245            workspace.active_pane().clone(),
 246            NavigationMode::ReopeningClosedItem,
 247            cx,
 248        )
 249    }
 250
 251    pub fn disable_history(&mut self) {
 252        self.nav_history.borrow_mut().disable();
 253    }
 254
 255    pub fn enable_history(&mut self) {
 256        self.nav_history.borrow_mut().enable();
 257    }
 258
 259    pub fn can_navigate_backward(&self) -> bool {
 260        !self.nav_history.borrow().backward_stack.is_empty()
 261    }
 262
 263    pub fn can_navigate_forward(&self) -> bool {
 264        !self.nav_history.borrow().forward_stack.is_empty()
 265    }
 266
 267    fn history_updated(&mut self, cx: &mut ViewContext<Self>) {
 268        self.toolbar.update(cx, |_, cx| cx.notify());
 269    }
 270
 271    fn navigate_history(
 272        workspace: &mut Workspace,
 273        pane: ViewHandle<Pane>,
 274        mode: NavigationMode,
 275        cx: &mut ViewContext<Workspace>,
 276    ) -> Task<()> {
 277        workspace.activate_pane(pane.clone(), cx);
 278
 279        let to_load = pane.update(cx, |pane, cx| {
 280            loop {
 281                // Retrieve the weak item handle from the history.
 282                let entry = pane.nav_history.borrow_mut().pop(mode, cx)?;
 283
 284                // If the item is still present in this pane, then activate it.
 285                if let Some(index) = entry
 286                    .item
 287                    .upgrade(cx)
 288                    .and_then(|v| pane.index_for_item(v.as_ref()))
 289                {
 290                    let prev_active_item_index = pane.active_item_index;
 291                    pane.nav_history.borrow_mut().set_mode(mode);
 292                    pane.activate_item(index, true, true, false, cx);
 293                    pane.nav_history
 294                        .borrow_mut()
 295                        .set_mode(NavigationMode::Normal);
 296
 297                    let mut navigated = prev_active_item_index != pane.active_item_index;
 298                    if let Some(data) = entry.data {
 299                        navigated |= pane.active_item()?.navigate(data, cx);
 300                    }
 301
 302                    if navigated {
 303                        break None;
 304                    }
 305                }
 306                // If the item is no longer present in this pane, then retrieve its
 307                // project path in order to reopen it.
 308                else {
 309                    break pane
 310                        .nav_history
 311                        .borrow()
 312                        .paths_by_item
 313                        .get(&entry.item.id())
 314                        .cloned()
 315                        .map(|project_path| (project_path, entry));
 316                }
 317            }
 318        });
 319
 320        if let Some((project_path, entry)) = to_load {
 321            // If the item was no longer present, then load it again from its previous path.
 322            let pane = pane.downgrade();
 323            let task = workspace.load_path(project_path, cx);
 324            cx.spawn(|workspace, mut cx| async move {
 325                let task = task.await;
 326                if let Some(pane) = pane.upgrade(&cx) {
 327                    let mut navigated = false;
 328                    if let Some((project_entry_id, build_item)) = task.log_err() {
 329                        let prev_active_item_id = pane.update(&mut cx, |pane, _| {
 330                            pane.nav_history.borrow_mut().set_mode(mode);
 331                            pane.active_item().map(|p| p.id())
 332                        });
 333
 334                        let item = workspace.update(&mut cx, |workspace, cx| {
 335                            Self::open_item(
 336                                workspace,
 337                                pane.clone(),
 338                                project_entry_id,
 339                                true,
 340                                cx,
 341                                build_item,
 342                            )
 343                        });
 344
 345                        pane.update(&mut cx, |pane, cx| {
 346                            navigated |= Some(item.id()) != prev_active_item_id;
 347                            pane.nav_history
 348                                .borrow_mut()
 349                                .set_mode(NavigationMode::Normal);
 350                            if let Some(data) = entry.data {
 351                                navigated |= item.navigate(data, cx);
 352                            }
 353                        });
 354                    }
 355
 356                    if !navigated {
 357                        workspace
 358                            .update(&mut cx, |workspace, cx| {
 359                                Self::navigate_history(workspace, pane, mode, cx)
 360                            })
 361                            .await;
 362                    }
 363                }
 364            })
 365        } else {
 366            Task::ready(())
 367        }
 368    }
 369
 370    pub(crate) fn open_item(
 371        workspace: &mut Workspace,
 372        pane: ViewHandle<Pane>,
 373        project_entry_id: ProjectEntryId,
 374        focus_item: bool,
 375        cx: &mut ViewContext<Workspace>,
 376        build_item: impl FnOnce(&mut MutableAppContext) -> Box<dyn ItemHandle>,
 377    ) -> Box<dyn ItemHandle> {
 378        let existing_item = pane.update(cx, |pane, cx| {
 379            for (ix, item) in pane.items.iter().enumerate() {
 380                if item.project_path(cx).is_some()
 381                    && item.project_entry_ids(cx).as_slice() == &[project_entry_id]
 382                {
 383                    let item = item.boxed_clone();
 384                    pane.activate_item(ix, true, focus_item, true, cx);
 385                    return Some(item);
 386                }
 387            }
 388            None
 389        });
 390        if let Some(existing_item) = existing_item {
 391            existing_item
 392        } else {
 393            let item = build_item(cx);
 394            Self::add_item(workspace, pane, item.boxed_clone(), true, focus_item, cx);
 395            item
 396        }
 397    }
 398
 399    pub(crate) fn add_item(
 400        workspace: &mut Workspace,
 401        pane: ViewHandle<Pane>,
 402        item: Box<dyn ItemHandle>,
 403        activate_pane: bool,
 404        focus_item: bool,
 405        cx: &mut ViewContext<Workspace>,
 406    ) {
 407        // Prevent adding the same item to the pane more than once.
 408        // If there is already an active item, reorder the desired item to be after it
 409        // and activate it.
 410        if let Some(item_ix) = pane.read(cx).items.iter().position(|i| i.id() == item.id()) {
 411            pane.update(cx, |pane, cx| {
 412                pane.activate_item(item_ix, activate_pane, focus_item, true, cx)
 413            });
 414            return;
 415        }
 416
 417        item.added_to_pane(workspace, pane.clone(), cx);
 418        pane.update(cx, |pane, cx| {
 419            // If there is already an active item, then insert the new item
 420            // right after it. Otherwise, adjust the `active_item_index` field
 421            // before activating the new item, so that in the `activate_item`
 422            // method, we can detect that the active item is changing.
 423            let item_ix;
 424            if pane.active_item_index < pane.items.len() {
 425                item_ix = pane.active_item_index + 1
 426            } else {
 427                item_ix = pane.items.len();
 428                pane.active_item_index = usize::MAX;
 429            };
 430
 431            pane.items.insert(item_ix, item);
 432            pane.activate_item(item_ix, activate_pane, focus_item, false, cx);
 433            cx.notify();
 434        });
 435    }
 436
 437    pub fn items(&self) -> impl Iterator<Item = &Box<dyn ItemHandle>> {
 438        self.items.iter()
 439    }
 440
 441    pub fn items_of_type<'a, T: View>(&'a self) -> impl 'a + Iterator<Item = ViewHandle<T>> {
 442        self.items
 443            .iter()
 444            .filter_map(|item| item.to_any().downcast())
 445    }
 446
 447    pub fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
 448        self.items.get(self.active_item_index).cloned()
 449    }
 450
 451    pub fn item_for_entry(
 452        &self,
 453        entry_id: ProjectEntryId,
 454        cx: &AppContext,
 455    ) -> Option<Box<dyn ItemHandle>> {
 456        self.items.iter().find_map(|item| {
 457            if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == &[entry_id] {
 458                Some(item.boxed_clone())
 459            } else {
 460                None
 461            }
 462        })
 463    }
 464
 465    pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option<usize> {
 466        self.items.iter().position(|i| i.id() == item.id())
 467    }
 468
 469    pub fn activate_item(
 470        &mut self,
 471        mut index: usize,
 472        activate_pane: bool,
 473        focus_item: bool,
 474        move_after_current_active: bool,
 475        cx: &mut ViewContext<Self>,
 476    ) {
 477        use NavigationMode::{GoingBack, GoingForward};
 478        if index < self.items.len() {
 479            if move_after_current_active {
 480                // If there is already an active item, reorder the desired item to be after it
 481                // and activate it.
 482                if self.active_item_index != index && self.active_item_index < self.items.len() {
 483                    let pane_to_activate = self.items.remove(index);
 484                    if self.active_item_index < index {
 485                        index = self.active_item_index + 1;
 486                    } else if self.active_item_index < self.items.len() + 1 {
 487                        index = self.active_item_index;
 488                        // Index is less than active_item_index. Reordering will decrement the
 489                        // active_item_index, so adjust it accordingly
 490                        self.active_item_index = index - 1;
 491                    }
 492                    self.items.insert(index, pane_to_activate);
 493                }
 494            }
 495
 496            let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
 497            if prev_active_item_ix != self.active_item_index
 498                || matches!(self.nav_history.borrow().mode, GoingBack | GoingForward)
 499            {
 500                if let Some(prev_item) = self.items.get(prev_active_item_ix) {
 501                    prev_item.deactivated(cx);
 502                }
 503                cx.emit(Event::ActivateItem {
 504                    local: activate_pane,
 505                });
 506            }
 507            self.update_toolbar(cx);
 508            if focus_item {
 509                self.focus_active_item(cx);
 510            }
 511            if activate_pane {
 512                self.activate(cx);
 513            }
 514            self.autoscroll = true;
 515            cx.notify();
 516        }
 517    }
 518
 519    pub fn activate_prev_item(&mut self, cx: &mut ViewContext<Self>) {
 520        let mut index = self.active_item_index;
 521        if index > 0 {
 522            index -= 1;
 523        } else if self.items.len() > 0 {
 524            index = self.items.len() - 1;
 525        }
 526        self.activate_item(index, true, true, false, cx);
 527    }
 528
 529    pub fn activate_next_item(&mut self, cx: &mut ViewContext<Self>) {
 530        let mut index = self.active_item_index;
 531        if index + 1 < self.items.len() {
 532            index += 1;
 533        } else {
 534            index = 0;
 535        }
 536        self.activate_item(index, true, true, false, cx);
 537    }
 538
 539    pub fn close_active_item(
 540        workspace: &mut Workspace,
 541        _: &CloseActiveItem,
 542        cx: &mut ViewContext<Workspace>,
 543    ) -> Option<Task<Result<()>>> {
 544        let pane_handle = workspace.active_pane().clone();
 545        let pane = pane_handle.read(cx);
 546        if pane.items.is_empty() {
 547            None
 548        } else {
 549            let item_id_to_close = pane.items[pane.active_item_index].id();
 550            let task = Self::close_items(workspace, pane_handle, cx, move |item_id| {
 551                item_id == item_id_to_close
 552            });
 553            Some(cx.foreground().spawn(async move {
 554                task.await?;
 555                Ok(())
 556            }))
 557        }
 558    }
 559
 560    pub fn close_inactive_items(
 561        workspace: &mut Workspace,
 562        _: &CloseInactiveItems,
 563        cx: &mut ViewContext<Workspace>,
 564    ) -> Option<Task<Result<()>>> {
 565        let pane_handle = workspace.active_pane().clone();
 566        let pane = pane_handle.read(cx);
 567        if pane.items.is_empty() {
 568            None
 569        } else {
 570            let active_item_id = pane.items[pane.active_item_index].id();
 571            let task =
 572                Self::close_items(workspace, pane_handle, cx, move |id| id != active_item_id);
 573            Some(cx.foreground().spawn(async move {
 574                task.await?;
 575                Ok(())
 576            }))
 577        }
 578    }
 579
 580    pub fn close_item(
 581        workspace: &mut Workspace,
 582        pane: ViewHandle<Pane>,
 583        item_id_to_close: usize,
 584        cx: &mut ViewContext<Workspace>,
 585    ) -> Task<Result<bool>> {
 586        Self::close_items(workspace, pane, cx, move |view_id| {
 587            view_id == item_id_to_close
 588        })
 589    }
 590
 591    pub fn close_items(
 592        workspace: &mut Workspace,
 593        pane: ViewHandle<Pane>,
 594        cx: &mut ViewContext<Workspace>,
 595        should_close: impl 'static + Fn(usize) -> bool,
 596    ) -> Task<Result<bool>> {
 597        let project = workspace.project().clone();
 598
 599        // Find the items to close.
 600        let mut items_to_close = Vec::new();
 601        for item in &pane.read(cx).items {
 602            if should_close(item.id()) {
 603                items_to_close.push(item.boxed_clone());
 604            }
 605        }
 606
 607        // If a buffer is open both in a singleton editor and in a multibuffer, make sure
 608        // to focus the singleton buffer when prompting to save that buffer, as opposed
 609        // to focusing the multibuffer, because this gives the user a more clear idea
 610        // of what content they would be saving.
 611        items_to_close.sort_by_key(|item| !item.is_singleton(cx));
 612
 613        cx.spawn(|workspace, mut cx| async move {
 614            let mut saved_project_entry_ids = HashSet::default();
 615            for item in items_to_close.clone() {
 616                // Find the item's current index and its set of project entries. Avoid
 617                // storing these in advance, in case they have changed since this task
 618                // was started.
 619                let (item_ix, mut project_entry_ids) = pane.read_with(&cx, |pane, cx| {
 620                    (pane.index_for_item(&*item), item.project_entry_ids(cx))
 621                });
 622                let item_ix = if let Some(ix) = item_ix {
 623                    ix
 624                } else {
 625                    continue;
 626                };
 627
 628                // If an item hasn't yet been associated with a project entry, then always
 629                // prompt to save it before closing it. Otherwise, check if the item has
 630                // any project entries that are not open anywhere else in the workspace,
 631                // AND that the user has not already been prompted to save. If there are
 632                // any such project entries, prompt the user to save this item.
 633                let should_save = if project_entry_ids.is_empty() {
 634                    true
 635                } else {
 636                    workspace.read_with(&cx, |workspace, cx| {
 637                        for item in workspace.items(cx) {
 638                            if !items_to_close
 639                                .iter()
 640                                .any(|item_to_close| item_to_close.id() == item.id())
 641                            {
 642                                let other_project_entry_ids = item.project_entry_ids(cx);
 643                                project_entry_ids
 644                                    .retain(|id| !other_project_entry_ids.contains(&id));
 645                            }
 646                        }
 647                    });
 648                    project_entry_ids
 649                        .iter()
 650                        .any(|id| saved_project_entry_ids.insert(*id))
 651                };
 652
 653                if should_save {
 654                    if !Self::save_item(project.clone(), &pane, item_ix, &item, true, &mut cx)
 655                        .await?
 656                    {
 657                        break;
 658                    }
 659                }
 660
 661                // Remove the item from the pane.
 662                pane.update(&mut cx, |pane, cx| {
 663                    if let Some(item_ix) = pane.items.iter().position(|i| i.id() == item.id()) {
 664                        if item_ix == pane.active_item_index {
 665                            // Activate the previous item if possible.
 666                            // This returns the user to the previously opened tab if they closed
 667                            // a ne item they just navigated to.
 668                            if item_ix > 0 {
 669                                pane.activate_prev_item(cx);
 670                            } else if item_ix + 1 < pane.items.len() {
 671                                pane.activate_next_item(cx);
 672                            }
 673                        }
 674
 675                        let item = pane.items.remove(item_ix);
 676                        cx.emit(Event::RemoveItem);
 677                        if pane.items.is_empty() {
 678                            item.deactivated(cx);
 679                            pane.update_toolbar(cx);
 680                            cx.emit(Event::Remove);
 681                        }
 682
 683                        if item_ix < pane.active_item_index {
 684                            pane.active_item_index -= 1;
 685                        }
 686
 687                        pane.nav_history
 688                            .borrow_mut()
 689                            .set_mode(NavigationMode::ClosingItem);
 690                        item.deactivated(cx);
 691                        pane.nav_history
 692                            .borrow_mut()
 693                            .set_mode(NavigationMode::Normal);
 694
 695                        if let Some(path) = item.project_path(cx) {
 696                            pane.nav_history
 697                                .borrow_mut()
 698                                .paths_by_item
 699                                .insert(item.id(), path);
 700                        } else {
 701                            pane.nav_history
 702                                .borrow_mut()
 703                                .paths_by_item
 704                                .remove(&item.id());
 705                        }
 706                    }
 707                });
 708            }
 709
 710            pane.update(&mut cx, |_, cx| cx.notify());
 711            Ok(true)
 712        })
 713    }
 714
 715    pub async fn save_item(
 716        project: ModelHandle<Project>,
 717        pane: &ViewHandle<Pane>,
 718        item_ix: usize,
 719        item: &Box<dyn ItemHandle>,
 720        should_prompt_for_save: bool,
 721        cx: &mut AsyncAppContext,
 722    ) -> Result<bool> {
 723        const CONFLICT_MESSAGE: &'static str =
 724            "This file has changed on disk since you started editing it. Do you want to overwrite it?";
 725        const DIRTY_MESSAGE: &'static str =
 726            "This file contains unsaved edits. Do you want to save it?";
 727
 728        let (has_conflict, is_dirty, can_save, is_singleton) = cx.read(|cx| {
 729            (
 730                item.has_conflict(cx),
 731                item.is_dirty(cx),
 732                item.can_save(cx),
 733                item.is_singleton(cx),
 734            )
 735        });
 736
 737        if has_conflict && can_save {
 738            let mut answer = pane.update(cx, |pane, cx| {
 739                pane.activate_item(item_ix, true, true, false, cx);
 740                cx.prompt(
 741                    PromptLevel::Warning,
 742                    CONFLICT_MESSAGE,
 743                    &["Overwrite", "Discard", "Cancel"],
 744                )
 745            });
 746            match answer.next().await {
 747                Some(0) => cx.update(|cx| item.save(project, cx)).await?,
 748                Some(1) => cx.update(|cx| item.reload(project, cx)).await?,
 749                _ => return Ok(false),
 750            }
 751        } else if is_dirty && (can_save || is_singleton) {
 752            let will_autosave = cx.read(|cx| {
 753                matches!(
 754                    cx.global::<Settings>().autosave,
 755                    Autosave::OnFocusChange | Autosave::OnWindowChange
 756                ) && Self::can_autosave_item(item.as_ref(), cx)
 757            });
 758            let should_save = if should_prompt_for_save && !will_autosave {
 759                let mut answer = pane.update(cx, |pane, cx| {
 760                    pane.activate_item(item_ix, true, true, false, cx);
 761                    cx.prompt(
 762                        PromptLevel::Warning,
 763                        DIRTY_MESSAGE,
 764                        &["Save", "Don't Save", "Cancel"],
 765                    )
 766                });
 767                match answer.next().await {
 768                    Some(0) => true,
 769                    Some(1) => false,
 770                    _ => return Ok(false),
 771                }
 772            } else {
 773                true
 774            };
 775
 776            if should_save {
 777                if can_save {
 778                    cx.update(|cx| item.save(project, cx)).await?;
 779                } else if is_singleton {
 780                    let start_abs_path = project
 781                        .read_with(cx, |project, cx| {
 782                            let worktree = project.visible_worktrees(cx).next()?;
 783                            Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
 784                        })
 785                        .unwrap_or(Path::new("").into());
 786
 787                    let mut abs_path = cx.update(|cx| cx.prompt_for_new_path(&start_abs_path));
 788                    if let Some(abs_path) = abs_path.next().await.flatten() {
 789                        cx.update(|cx| item.save_as(project, abs_path, cx)).await?;
 790                    } else {
 791                        return Ok(false);
 792                    }
 793                }
 794            }
 795        }
 796        Ok(true)
 797    }
 798
 799    fn can_autosave_item(item: &dyn ItemHandle, cx: &AppContext) -> bool {
 800        let is_deleted = item.project_entry_ids(cx).is_empty();
 801        item.is_dirty(cx) && !item.has_conflict(cx) && item.can_save(cx) && !is_deleted
 802    }
 803
 804    pub fn autosave_item(
 805        item: &dyn ItemHandle,
 806        project: ModelHandle<Project>,
 807        cx: &mut MutableAppContext,
 808    ) -> Task<Result<()>> {
 809        if Self::can_autosave_item(item, cx) {
 810            item.save(project, cx)
 811        } else {
 812            Task::ready(Ok(()))
 813        }
 814    }
 815
 816    pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
 817        if let Some(active_item) = self.active_item() {
 818            cx.focus(active_item);
 819        }
 820    }
 821
 822    pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
 823        cx.emit(Event::Split(direction));
 824    }
 825
 826    fn deploy_split_menu(&mut self, action: &DeploySplitMenu, cx: &mut ViewContext<Self>) {
 827        self.split_menu.update(cx, |menu, cx| {
 828            menu.show(
 829                action.position,
 830                vec![
 831                    ContextMenuItem::item("Split Right", SplitRight),
 832                    ContextMenuItem::item("Split Left", SplitLeft),
 833                    ContextMenuItem::item("Split Up", SplitUp),
 834                    ContextMenuItem::item("Split Down", SplitDown),
 835                ],
 836                cx,
 837            );
 838        });
 839    }
 840
 841    pub fn toolbar(&self) -> &ViewHandle<Toolbar> {
 842        &self.toolbar
 843    }
 844
 845    fn update_toolbar(&mut self, cx: &mut ViewContext<Self>) {
 846        let active_item = self
 847            .items
 848            .get(self.active_item_index)
 849            .map(|item| item.as_ref());
 850        self.toolbar.update(cx, |toolbar, cx| {
 851            toolbar.set_active_pane_item(active_item, cx);
 852        });
 853    }
 854
 855    fn render_tabs(&mut self, cx: &mut RenderContext<Self>) -> impl Element {
 856        let theme = cx.global::<Settings>().theme.clone();
 857
 858        enum Tabs {}
 859        enum Tab {}
 860        let pane = cx.handle();
 861        MouseEventHandler::new::<Tabs, _, _>(0, cx, |mouse_state, cx| {
 862            let autoscroll = if mem::take(&mut self.autoscroll) {
 863                Some(self.active_item_index)
 864            } else {
 865                None
 866            };
 867
 868            let mut row = Flex::row().scrollable::<Tabs, _>(1, autoscroll, cx);
 869            for (ix, (item, detail)) in self.items.iter().zip(self.tab_details(cx)).enumerate() {
 870                let detail = if detail == 0 { None } else { Some(detail) };
 871                let is_active = ix == self.active_item_index;
 872
 873                row.add_child({
 874                    let tab_style = if is_active {
 875                        theme.workspace.active_tab.clone()
 876                    } else {
 877                        theme.workspace.tab.clone()
 878                    };
 879                    let title = item.tab_content(detail, &tab_style, cx);
 880
 881                    let mut style = if is_active {
 882                        theme.workspace.active_tab.clone()
 883                    } else {
 884                        theme.workspace.tab.clone()
 885                    };
 886                    if ix == 0 {
 887                        style.container.border.left = false;
 888                    }
 889
 890                    MouseEventHandler::new::<Tab, _, _>(ix, cx, |_, cx| {
 891                        Container::new(
 892                            Flex::row()
 893                                .with_child(
 894                                    Align::new({
 895                                        let diameter = 7.0;
 896                                        let icon_color = if item.has_conflict(cx) {
 897                                            Some(style.icon_conflict)
 898                                        } else if item.is_dirty(cx) {
 899                                            Some(style.icon_dirty)
 900                                        } else {
 901                                            None
 902                                        };
 903
 904                                        ConstrainedBox::new(
 905                                            Canvas::new(move |bounds, _, cx| {
 906                                                if let Some(color) = icon_color {
 907                                                    let square = RectF::new(
 908                                                        bounds.origin(),
 909                                                        vec2f(diameter, diameter),
 910                                                    );
 911                                                    cx.scene.push_quad(Quad {
 912                                                        bounds: square,
 913                                                        background: Some(color),
 914                                                        border: Default::default(),
 915                                                        corner_radius: diameter / 2.,
 916                                                    });
 917                                                }
 918                                            })
 919                                            .boxed(),
 920                                        )
 921                                        .with_width(diameter)
 922                                        .with_height(diameter)
 923                                        .boxed()
 924                                    })
 925                                    .boxed(),
 926                                )
 927                                .with_child(
 928                                    Container::new(Align::new(title).boxed())
 929                                        .with_style(ContainerStyle {
 930                                            margin: Margin {
 931                                                left: style.spacing,
 932                                                right: style.spacing,
 933                                                ..Default::default()
 934                                            },
 935                                            ..Default::default()
 936                                        })
 937                                        .boxed(),
 938                                )
 939                                .with_child(
 940                                    Align::new(
 941                                        ConstrainedBox::new(if mouse_state.hovered {
 942                                            let item_id = item.id();
 943                                            enum TabCloseButton {}
 944                                            let icon = Svg::new("icons/x_mark_thin_8.svg");
 945                                            MouseEventHandler::new::<TabCloseButton, _, _>(
 946                                                item_id,
 947                                                cx,
 948                                                |mouse_state, _| {
 949                                                    if mouse_state.hovered {
 950                                                        icon.with_color(style.icon_close_active)
 951                                                            .boxed()
 952                                                    } else {
 953                                                        icon.with_color(style.icon_close).boxed()
 954                                                    }
 955                                                },
 956                                            )
 957                                            .with_padding(Padding::uniform(4.))
 958                                            .with_cursor_style(CursorStyle::PointingHand)
 959                                            .on_click(MouseButton::Left, {
 960                                                let pane = pane.clone();
 961                                                move |_, cx| {
 962                                                    cx.dispatch_action(CloseItem {
 963                                                        item_id,
 964                                                        pane: pane.clone(),
 965                                                    })
 966                                                }
 967                                            })
 968                                            .named("close-tab-icon")
 969                                        } else {
 970                                            Empty::new().boxed()
 971                                        })
 972                                        .with_width(style.icon_width)
 973                                        .boxed(),
 974                                    )
 975                                    .boxed(),
 976                                )
 977                                .boxed(),
 978                        )
 979                        .with_style(style.container)
 980                        .boxed()
 981                    })
 982                    .on_mouse_down(MouseButton::Left, move |_, cx| {
 983                        cx.dispatch_action(ActivateItem(ix));
 984                    })
 985                    .boxed()
 986                })
 987            }
 988
 989            row.add_child(
 990                Empty::new()
 991                    .contained()
 992                    .with_border(theme.workspace.tab.container.border)
 993                    .flex(0., true)
 994                    .named("filler"),
 995            );
 996
 997            row.boxed()
 998        })
 999    }
1000
1001    fn tab_details(&self, cx: &AppContext) -> Vec<usize> {
1002        let mut tab_details = (0..self.items.len()).map(|_| 0).collect::<Vec<_>>();
1003
1004        let mut tab_descriptions = HashMap::default();
1005        let mut done = false;
1006        while !done {
1007            done = true;
1008
1009            // Store item indices by their tab description.
1010            for (ix, (item, detail)) in self.items.iter().zip(&tab_details).enumerate() {
1011                if let Some(description) = item.tab_description(*detail, cx) {
1012                    if *detail == 0
1013                        || Some(&description) != item.tab_description(detail - 1, cx).as_ref()
1014                    {
1015                        tab_descriptions
1016                            .entry(description)
1017                            .or_insert(Vec::new())
1018                            .push(ix);
1019                    }
1020                }
1021            }
1022
1023            // If two or more items have the same tab description, increase their level
1024            // of detail and try again.
1025            for (_, item_ixs) in tab_descriptions.drain() {
1026                if item_ixs.len() > 1 {
1027                    done = false;
1028                    for ix in item_ixs {
1029                        tab_details[ix] += 1;
1030                    }
1031                }
1032            }
1033        }
1034
1035        tab_details
1036    }
1037}
1038
1039impl Entity for Pane {
1040    type Event = Event;
1041}
1042
1043impl View for Pane {
1044    fn ui_name() -> &'static str {
1045        "Pane"
1046    }
1047
1048    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
1049        enum SplitIcon {}
1050
1051        let this = cx.handle();
1052
1053        Stack::new()
1054            .with_child(
1055                EventHandler::new(if let Some(active_item) = self.active_item() {
1056                    Flex::column()
1057                        .with_child(
1058                            Flex::row()
1059                                .with_child(self.render_tabs(cx).flex(1., true).named("tabs"))
1060                                .with_child(
1061                                    MouseEventHandler::new::<SplitIcon, _, _>(
1062                                        0,
1063                                        cx,
1064                                        |mouse_state, cx| {
1065                                            let theme = &cx.global::<Settings>().theme.workspace;
1066                                            let style =
1067                                                theme.pane_button.style_for(mouse_state, false);
1068                                            Svg::new("icons/split_12.svg")
1069                                                .with_color(style.color)
1070                                                .constrained()
1071                                                .with_width(style.icon_width)
1072                                                .aligned()
1073                                                .contained()
1074                                                .with_style(style.container)
1075                                                .constrained()
1076                                                .with_width(style.button_width)
1077                                                .with_height(style.button_width)
1078                                                .aligned()
1079                                                .boxed()
1080                                        },
1081                                    )
1082                                    .with_cursor_style(CursorStyle::PointingHand)
1083                                    .on_mouse_down(
1084                                        MouseButton::Left,
1085                                        |MouseButtonEvent { position, .. }, cx| {
1086                                            cx.dispatch_action(DeploySplitMenu { position });
1087                                        },
1088                                    )
1089                                    .boxed(),
1090                                )
1091                                .constrained()
1092                                .with_height(cx.global::<Settings>().theme.workspace.tab.height)
1093                                .boxed(),
1094                        )
1095                        .with_child(ChildView::new(&self.toolbar).boxed())
1096                        .with_child(ChildView::new(active_item).flex(1., true).boxed())
1097                        .boxed()
1098                } else {
1099                    Empty::new().boxed()
1100                })
1101                .on_navigate_mouse_down(move |direction, cx| {
1102                    let this = this.clone();
1103                    match direction {
1104                        NavigationDirection::Back => {
1105                            cx.dispatch_action(GoBack { pane: Some(this) })
1106                        }
1107                        NavigationDirection::Forward => {
1108                            cx.dispatch_action(GoForward { pane: Some(this) })
1109                        }
1110                    }
1111
1112                    true
1113                })
1114                .boxed(),
1115            )
1116            .with_child(ChildView::new(&self.split_menu).boxed())
1117            .named("pane")
1118    }
1119
1120    fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
1121        self.focus_active_item(cx);
1122    }
1123}
1124
1125impl ItemNavHistory {
1126    pub fn push<D: 'static + Any>(&self, data: Option<D>, cx: &mut MutableAppContext) {
1127        self.history.borrow_mut().push(data, self.item.clone(), cx);
1128    }
1129
1130    pub fn pop_backward(&self, cx: &mut MutableAppContext) -> Option<NavigationEntry> {
1131        self.history.borrow_mut().pop(NavigationMode::GoingBack, cx)
1132    }
1133
1134    pub fn pop_forward(&self, cx: &mut MutableAppContext) -> Option<NavigationEntry> {
1135        self.history
1136            .borrow_mut()
1137            .pop(NavigationMode::GoingForward, cx)
1138    }
1139}
1140
1141impl NavHistory {
1142    fn set_mode(&mut self, mode: NavigationMode) {
1143        self.mode = mode;
1144    }
1145
1146    fn disable(&mut self) {
1147        self.mode = NavigationMode::Disabled;
1148    }
1149
1150    fn enable(&mut self) {
1151        self.mode = NavigationMode::Normal;
1152    }
1153
1154    fn pop(&mut self, mode: NavigationMode, cx: &mut MutableAppContext) -> Option<NavigationEntry> {
1155        let entry = match mode {
1156            NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
1157                return None
1158            }
1159            NavigationMode::GoingBack => &mut self.backward_stack,
1160            NavigationMode::GoingForward => &mut self.forward_stack,
1161            NavigationMode::ReopeningClosedItem => &mut self.closed_stack,
1162        }
1163        .pop_back();
1164        if entry.is_some() {
1165            self.did_update(cx);
1166        }
1167        entry
1168    }
1169
1170    fn push<D: 'static + Any>(
1171        &mut self,
1172        data: Option<D>,
1173        item: Rc<dyn WeakItemHandle>,
1174        cx: &mut MutableAppContext,
1175    ) {
1176        match self.mode {
1177            NavigationMode::Disabled => {}
1178            NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
1179                if self.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
1180                    self.backward_stack.pop_front();
1181                }
1182                self.backward_stack.push_back(NavigationEntry {
1183                    item,
1184                    data: data.map(|data| Box::new(data) as Box<dyn Any>),
1185                });
1186                self.forward_stack.clear();
1187            }
1188            NavigationMode::GoingBack => {
1189                if self.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
1190                    self.forward_stack.pop_front();
1191                }
1192                self.forward_stack.push_back(NavigationEntry {
1193                    item,
1194                    data: data.map(|data| Box::new(data) as Box<dyn Any>),
1195                });
1196            }
1197            NavigationMode::GoingForward => {
1198                if self.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
1199                    self.backward_stack.pop_front();
1200                }
1201                self.backward_stack.push_back(NavigationEntry {
1202                    item,
1203                    data: data.map(|data| Box::new(data) as Box<dyn Any>),
1204                });
1205            }
1206            NavigationMode::ClosingItem => {
1207                if self.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
1208                    self.closed_stack.pop_front();
1209                }
1210                self.closed_stack.push_back(NavigationEntry {
1211                    item,
1212                    data: data.map(|data| Box::new(data) as Box<dyn Any>),
1213                });
1214            }
1215        }
1216        self.did_update(cx);
1217    }
1218
1219    fn did_update(&self, cx: &mut MutableAppContext) {
1220        if let Some(pane) = self.pane.upgrade(cx) {
1221            cx.defer(move |cx| pane.update(cx, |pane, cx| pane.history_updated(cx)));
1222        }
1223    }
1224}