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