pane.rs

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