pane.rs

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