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