1mod dragged_item_receiver;
2
3use super::{ItemHandle, SplitDirection};
4use crate::{
5 dock::{icon_for_dock_anchor, AnchorDockBottom, AnchorDockRight, ExpandDock, HideDock},
6 item::WeakItemHandle,
7 toolbar::Toolbar,
8 Item, NewFile, NewSearch, NewTerminal, Workspace,
9};
10use anyhow::Result;
11use collections::{HashMap, HashSet, VecDeque};
12use context_menu::{ContextMenu, ContextMenuItem};
13use drag_and_drop::Draggable;
14pub use dragged_item_receiver::{dragged_item_receiver, handle_dropped_item};
15use futures::StreamExt;
16use gpui::{
17 actions,
18 elements::*,
19 geometry::{
20 rect::RectF,
21 vector::{vec2f, Vector2F},
22 },
23 impl_actions, impl_internal_actions,
24 platform::{CursorStyle, NavigationDirection},
25 Action, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, EventContext,
26 ModelHandle, MouseButton, MutableAppContext, PromptLevel, Quad, RenderContext, Task, View,
27 ViewContext, ViewHandle, WeakViewHandle,
28};
29use project::{Project, ProjectEntryId, ProjectPath};
30use serde::Deserialize;
31use settings::{Autosave, DockAnchor, Settings};
32use std::{any::Any, cell::RefCell, cmp, mem, path::Path, rc::Rc};
33use theme::Theme;
34use util::ResultExt;
35
36#[derive(Clone, Deserialize, PartialEq)]
37pub struct ActivateItem(pub usize);
38
39actions!(
40 pane,
41 [
42 ActivatePrevItem,
43 ActivateNextItem,
44 ActivateLastItem,
45 CloseActiveItem,
46 CloseInactiveItems,
47 CloseCleanItems,
48 CloseAllItems,
49 ReopenClosedItem,
50 SplitLeft,
51 SplitUp,
52 SplitRight,
53 SplitDown,
54 ]
55);
56
57#[derive(Clone, PartialEq)]
58pub struct CloseItem {
59 pub item_id: usize,
60 pub pane: WeakViewHandle<Pane>,
61}
62
63#[derive(Clone, PartialEq)]
64pub struct MoveItem {
65 pub item_id: usize,
66 pub from: WeakViewHandle<Pane>,
67 pub to: WeakViewHandle<Pane>,
68 pub destination_index: usize,
69}
70
71#[derive(Clone, Deserialize, PartialEq)]
72pub struct GoBack {
73 #[serde(skip_deserializing)]
74 pub pane: Option<WeakViewHandle<Pane>>,
75}
76
77#[derive(Clone, Deserialize, PartialEq)]
78pub struct GoForward {
79 #[serde(skip_deserializing)]
80 pub pane: Option<WeakViewHandle<Pane>>,
81}
82
83#[derive(Clone, PartialEq)]
84pub struct DeploySplitMenu {
85 position: Vector2F,
86}
87
88#[derive(Clone, PartialEq)]
89pub struct DeployDockMenu {
90 position: Vector2F,
91}
92
93#[derive(Clone, PartialEq)]
94pub struct DeployNewMenu {
95 position: Vector2F,
96}
97
98#[derive(Clone, PartialEq)]
99pub struct DeployFeedbackModal {
100 position: Vector2F,
101}
102
103impl_actions!(pane, [GoBack, GoForward, ActivateItem]);
104impl_internal_actions!(
105 pane,
106 [
107 CloseItem,
108 DeploySplitMenu,
109 DeployNewMenu,
110 DeployDockMenu,
111 MoveItem,
112 DeployFeedbackModal
113 ]
114);
115
116const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
117
118pub fn init(cx: &mut MutableAppContext) {
119 cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
120 pane.activate_item(action.0, true, true, cx);
121 });
122 cx.add_action(|pane: &mut Pane, _: &ActivateLastItem, cx| {
123 pane.activate_item(pane.items.len() - 1, true, true, cx);
124 });
125 cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
126 pane.activate_prev_item(true, cx);
127 });
128 cx.add_action(|pane: &mut Pane, _: &ActivateNextItem, cx| {
129 pane.activate_next_item(true, cx);
130 });
131 cx.add_async_action(Pane::close_active_item);
132 cx.add_async_action(Pane::close_inactive_items);
133 cx.add_async_action(Pane::close_clean_items);
134 cx.add_async_action(Pane::close_all_items);
135 cx.add_async_action(|workspace: &mut Workspace, action: &CloseItem, cx| {
136 let pane = action.pane.upgrade(cx)?;
137 let task = Pane::close_item(workspace, pane, action.item_id, cx);
138 Some(cx.foreground().spawn(async move {
139 task.await?;
140 Ok(())
141 }))
142 });
143 cx.add_action(
144 |workspace,
145 MoveItem {
146 from,
147 to,
148 item_id,
149 destination_index,
150 },
151 cx| {
152 // Get item handle to move
153 let from = if let Some(from) = from.upgrade(cx) {
154 from
155 } else {
156 return;
157 };
158
159 // Add item to new pane at given index
160 let to = if let Some(to) = to.upgrade(cx) {
161 to
162 } else {
163 return;
164 };
165
166 Pane::move_item(workspace, from, to, *item_id, *destination_index, cx)
167 },
168 );
169 cx.add_action(|pane: &mut Pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx));
170 cx.add_action(|pane: &mut Pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx));
171 cx.add_action(|pane: &mut Pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx));
172 cx.add_action(|pane: &mut Pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx));
173 cx.add_action(Pane::deploy_split_menu);
174 cx.add_action(Pane::deploy_new_menu);
175 cx.add_action(Pane::deploy_dock_menu);
176 cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
177 Pane::reopen_closed_item(workspace, cx).detach();
178 });
179 cx.add_action(|workspace: &mut Workspace, action: &GoBack, cx| {
180 Pane::go_back(
181 workspace,
182 action
183 .pane
184 .as_ref()
185 .and_then(|weak_handle| weak_handle.upgrade(cx)),
186 cx,
187 )
188 .detach();
189 });
190 cx.add_action(|workspace: &mut Workspace, action: &GoForward, cx| {
191 Pane::go_forward(
192 workspace,
193 action
194 .pane
195 .as_ref()
196 .and_then(|weak_handle| weak_handle.upgrade(cx)),
197 cx,
198 )
199 .detach();
200 });
201}
202
203#[derive(Debug)]
204pub enum Event {
205 ActivateItem { local: bool },
206 Remove,
207 RemoveItem { item_id: usize },
208 Split(SplitDirection),
209 ChangeItemTitle,
210}
211
212pub struct Pane {
213 items: Vec<Box<dyn ItemHandle>>,
214 activation_history: Vec<usize>,
215 is_active: bool,
216 active_item_index: usize,
217 last_focused_view_by_item: HashMap<usize, AnyWeakViewHandle>,
218 autoscroll: bool,
219 nav_history: Rc<RefCell<NavHistory>>,
220 toolbar: ViewHandle<Toolbar>,
221 tab_bar_context_menu: ViewHandle<ContextMenu>,
222 docked: Option<DockAnchor>,
223}
224
225pub struct ItemNavHistory {
226 history: Rc<RefCell<NavHistory>>,
227 item: Rc<dyn WeakItemHandle>,
228}
229
230struct NavHistory {
231 mode: NavigationMode,
232 backward_stack: VecDeque<NavigationEntry>,
233 forward_stack: VecDeque<NavigationEntry>,
234 closed_stack: VecDeque<NavigationEntry>,
235 paths_by_item: HashMap<usize, ProjectPath>,
236 pane: WeakViewHandle<Pane>,
237}
238
239#[derive(Copy, Clone)]
240enum NavigationMode {
241 Normal,
242 GoingBack,
243 GoingForward,
244 ClosingItem,
245 ReopeningClosedItem,
246 Disabled,
247}
248
249impl Default for NavigationMode {
250 fn default() -> Self {
251 Self::Normal
252 }
253}
254
255pub struct NavigationEntry {
256 pub item: Rc<dyn WeakItemHandle>,
257 pub data: Option<Box<dyn Any>>,
258}
259
260struct DraggedItem {
261 item: Box<dyn ItemHandle>,
262 pane: WeakViewHandle<Pane>,
263}
264
265pub enum ReorderBehavior {
266 None,
267 MoveAfterActive,
268 MoveToIndex(usize),
269}
270
271enum ItemType {
272 Active,
273 Inactive,
274 Clean,
275 All,
276}
277
278impl Pane {
279 pub fn new(docked: Option<DockAnchor>, cx: &mut ViewContext<Self>) -> Self {
280 let handle = cx.weak_handle();
281 let context_menu = cx.add_view(ContextMenu::new);
282 Self {
283 items: Vec::new(),
284 activation_history: Vec::new(),
285 is_active: true,
286 active_item_index: 0,
287 last_focused_view_by_item: Default::default(),
288 autoscroll: false,
289 nav_history: Rc::new(RefCell::new(NavHistory {
290 mode: NavigationMode::Normal,
291 backward_stack: Default::default(),
292 forward_stack: Default::default(),
293 closed_stack: Default::default(),
294 paths_by_item: Default::default(),
295 pane: handle.clone(),
296 })),
297 toolbar: cx.add_view(|_| Toolbar::new(handle)),
298 tab_bar_context_menu: context_menu,
299 docked,
300 }
301 }
302
303 pub fn is_active(&self) -> bool {
304 self.is_active
305 }
306
307 pub fn set_active(&mut self, is_active: bool, cx: &mut ViewContext<Self>) {
308 self.is_active = is_active;
309 cx.notify();
310 }
311
312 pub fn set_docked(&mut self, docked: Option<DockAnchor>, cx: &mut ViewContext<Self>) {
313 self.docked = docked;
314 cx.notify();
315 }
316
317 pub fn nav_history_for_item<T: Item>(&self, item: &ViewHandle<T>) -> ItemNavHistory {
318 ItemNavHistory {
319 history: self.nav_history.clone(),
320 item: Rc::new(item.downgrade()),
321 }
322 }
323
324 pub fn go_back(
325 workspace: &mut Workspace,
326 pane: Option<ViewHandle<Pane>>,
327 cx: &mut ViewContext<Workspace>,
328 ) -> Task<()> {
329 Self::navigate_history(
330 workspace,
331 pane.unwrap_or_else(|| workspace.active_pane().clone()),
332 NavigationMode::GoingBack,
333 cx,
334 )
335 }
336
337 pub fn go_forward(
338 workspace: &mut Workspace,
339 pane: Option<ViewHandle<Pane>>,
340 cx: &mut ViewContext<Workspace>,
341 ) -> Task<()> {
342 Self::navigate_history(
343 workspace,
344 pane.unwrap_or_else(|| workspace.active_pane().clone()),
345 NavigationMode::GoingForward,
346 cx,
347 )
348 }
349
350 pub fn reopen_closed_item(
351 workspace: &mut Workspace,
352 cx: &mut ViewContext<Workspace>,
353 ) -> Task<()> {
354 Self::navigate_history(
355 workspace,
356 workspace.active_pane().clone(),
357 NavigationMode::ReopeningClosedItem,
358 cx,
359 )
360 }
361
362 pub fn disable_history(&mut self) {
363 self.nav_history.borrow_mut().disable();
364 }
365
366 pub fn enable_history(&mut self) {
367 self.nav_history.borrow_mut().enable();
368 }
369
370 pub fn can_navigate_backward(&self) -> bool {
371 !self.nav_history.borrow().backward_stack.is_empty()
372 }
373
374 pub fn can_navigate_forward(&self) -> bool {
375 !self.nav_history.borrow().forward_stack.is_empty()
376 }
377
378 fn history_updated(&mut self, cx: &mut ViewContext<Self>) {
379 self.toolbar.update(cx, |_, cx| cx.notify());
380 }
381
382 fn navigate_history(
383 workspace: &mut Workspace,
384 pane: ViewHandle<Pane>,
385 mode: NavigationMode,
386 cx: &mut ViewContext<Workspace>,
387 ) -> Task<()> {
388 cx.focus(pane.clone());
389
390 let to_load = pane.update(cx, |pane, cx| {
391 loop {
392 // Retrieve the weak item handle from the history.
393 let entry = pane.nav_history.borrow_mut().pop(mode, cx)?;
394
395 // If the item is still present in this pane, then activate it.
396 if let Some(index) = entry
397 .item
398 .upgrade(cx)
399 .and_then(|v| pane.index_for_item(v.as_ref()))
400 {
401 let prev_active_item_index = pane.active_item_index;
402 pane.nav_history.borrow_mut().set_mode(mode);
403 pane.activate_item(index, true, true, cx);
404 pane.nav_history
405 .borrow_mut()
406 .set_mode(NavigationMode::Normal);
407
408 let mut navigated = prev_active_item_index != pane.active_item_index;
409 if let Some(data) = entry.data {
410 navigated |= pane.active_item()?.navigate(data, cx);
411 }
412
413 if navigated {
414 break None;
415 }
416 }
417 // If the item is no longer present in this pane, then retrieve its
418 // project path in order to reopen it.
419 else {
420 break pane
421 .nav_history
422 .borrow()
423 .paths_by_item
424 .get(&entry.item.id())
425 .cloned()
426 .map(|project_path| (project_path, entry));
427 }
428 }
429 });
430
431 if let Some((project_path, entry)) = to_load {
432 // If the item was no longer present, then load it again from its previous path.
433 let pane = pane.downgrade();
434 let task = workspace.load_path(project_path, cx);
435 cx.spawn(|workspace, mut cx| async move {
436 let task = task.await;
437 if let Some(pane) = pane.upgrade(&cx) {
438 let mut navigated = false;
439 if let Some((project_entry_id, build_item)) = task.log_err() {
440 let prev_active_item_id = pane.update(&mut cx, |pane, _| {
441 pane.nav_history.borrow_mut().set_mode(mode);
442 pane.active_item().map(|p| p.id())
443 });
444
445 let item = workspace.update(&mut cx, |workspace, cx| {
446 Self::open_item(
447 workspace,
448 pane.clone(),
449 project_entry_id,
450 true,
451 cx,
452 build_item,
453 )
454 });
455
456 pane.update(&mut cx, |pane, cx| {
457 navigated |= Some(item.id()) != prev_active_item_id;
458 pane.nav_history
459 .borrow_mut()
460 .set_mode(NavigationMode::Normal);
461 if let Some(data) = entry.data {
462 navigated |= item.navigate(data, cx);
463 }
464 });
465 }
466
467 if !navigated {
468 workspace
469 .update(&mut cx, |workspace, cx| {
470 Self::navigate_history(workspace, pane, mode, cx)
471 })
472 .await;
473 }
474 }
475 })
476 } else {
477 Task::ready(())
478 }
479 }
480
481 pub(crate) fn open_item(
482 workspace: &mut Workspace,
483 pane: ViewHandle<Pane>,
484 project_entry_id: ProjectEntryId,
485 focus_item: bool,
486 cx: &mut ViewContext<Workspace>,
487 build_item: impl FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
488 ) -> Box<dyn ItemHandle> {
489 let existing_item = pane.update(cx, |pane, cx| {
490 for (index, item) in pane.items.iter().enumerate() {
491 if item.project_path(cx).is_some()
492 && item.project_entry_ids(cx).as_slice() == [project_entry_id]
493 {
494 let item = item.boxed_clone();
495 return Some((index, item));
496 }
497 }
498 None
499 });
500
501 if let Some((index, existing_item)) = existing_item {
502 pane.update(cx, |pane, cx| {
503 pane.activate_item(index, focus_item, focus_item, cx);
504 });
505 existing_item
506 } else {
507 let new_item = pane.update(cx, |_, cx| build_item(cx));
508 Pane::add_item(
509 workspace,
510 &pane,
511 new_item.clone(),
512 true,
513 focus_item,
514 None,
515 cx,
516 );
517 new_item
518 }
519 }
520
521 pub(crate) fn add_item(
522 workspace: &mut Workspace,
523 pane: &ViewHandle<Pane>,
524 item: Box<dyn ItemHandle>,
525 activate_pane: bool,
526 focus_item: bool,
527 destination_index: Option<usize>,
528 cx: &mut ViewContext<Workspace>,
529 ) {
530 // If no destination index is specified, add or move the item after the active item.
531 let mut insertion_index = {
532 let pane = pane.read(cx);
533 cmp::min(
534 if let Some(destination_index) = destination_index {
535 destination_index
536 } else {
537 pane.active_item_index + 1
538 },
539 pane.items.len(),
540 )
541 };
542
543 item.added_to_pane(workspace, pane.clone(), cx);
544
545 // Does the item already exist?
546 let project_entry_id = if item.is_singleton(cx) {
547 item.project_entry_ids(cx).get(0).copied()
548 } else {
549 None
550 };
551
552 let existing_item_index = pane.read(cx).items.iter().position(|existing_item| {
553 if existing_item.id() == item.id() {
554 true
555 } else if existing_item.is_singleton(cx) {
556 existing_item
557 .project_entry_ids(cx)
558 .get(0)
559 .map_or(false, |existing_entry_id| {
560 Some(existing_entry_id) == project_entry_id.as_ref()
561 })
562 } else {
563 false
564 }
565 });
566
567 if let Some(existing_item_index) = existing_item_index {
568 // If the item already exists, move it to the desired destination and activate it
569 pane.update(cx, |pane, cx| {
570 if existing_item_index != insertion_index {
571 cx.reparent(&item);
572 let existing_item_is_active = existing_item_index == pane.active_item_index;
573
574 // If the caller didn't specify a destination and the added item is already
575 // the active one, don't move it
576 if existing_item_is_active && destination_index.is_none() {
577 insertion_index = existing_item_index;
578 } else {
579 pane.items.remove(existing_item_index);
580 if existing_item_index < pane.active_item_index {
581 pane.active_item_index -= 1;
582 }
583 insertion_index = insertion_index.min(pane.items.len());
584
585 pane.items.insert(insertion_index, item.clone());
586
587 if existing_item_is_active {
588 pane.active_item_index = insertion_index;
589 } else if insertion_index <= pane.active_item_index {
590 pane.active_item_index += 1;
591 }
592 }
593
594 cx.notify();
595 }
596
597 pane.activate_item(insertion_index, activate_pane, focus_item, cx);
598 });
599 } else {
600 pane.update(cx, |pane, cx| {
601 cx.reparent(&item);
602 pane.items.insert(insertion_index, item);
603 if insertion_index <= pane.active_item_index {
604 pane.active_item_index += 1;
605 }
606
607 pane.activate_item(insertion_index, activate_pane, focus_item, cx);
608 cx.notify();
609 });
610 }
611 }
612
613 pub fn items_len(&self) -> usize {
614 self.items.len()
615 }
616
617 pub fn items(&self) -> impl Iterator<Item = &Box<dyn ItemHandle>> {
618 self.items.iter()
619 }
620
621 pub fn items_of_type<T: View>(&self) -> impl '_ + Iterator<Item = ViewHandle<T>> {
622 self.items
623 .iter()
624 .filter_map(|item| item.to_any().downcast())
625 }
626
627 pub fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
628 self.items.get(self.active_item_index).cloned()
629 }
630
631 pub fn item_for_entry(
632 &self,
633 entry_id: ProjectEntryId,
634 cx: &AppContext,
635 ) -> Option<Box<dyn ItemHandle>> {
636 self.items.iter().find_map(|item| {
637 if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
638 Some(item.boxed_clone())
639 } else {
640 None
641 }
642 })
643 }
644
645 pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option<usize> {
646 self.items.iter().position(|i| i.id() == item.id())
647 }
648
649 pub fn activate_item(
650 &mut self,
651 index: usize,
652 activate_pane: bool,
653 focus_item: bool,
654 cx: &mut ViewContext<Self>,
655 ) {
656 use NavigationMode::{GoingBack, GoingForward};
657
658 if index < self.items.len() {
659 let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
660 if prev_active_item_ix != self.active_item_index
661 || matches!(self.nav_history.borrow().mode, GoingBack | GoingForward)
662 {
663 if let Some(prev_item) = self.items.get(prev_active_item_ix) {
664 prev_item.deactivated(cx);
665 }
666
667 cx.emit(Event::ActivateItem {
668 local: activate_pane,
669 });
670 }
671
672 if let Some(newly_active_item) = self.items.get(index) {
673 self.activation_history
674 .retain(|&previously_active_item_id| {
675 previously_active_item_id != newly_active_item.id()
676 });
677 self.activation_history.push(newly_active_item.id());
678 }
679
680 self.update_toolbar(cx);
681
682 if focus_item {
683 self.focus_active_item(cx);
684 }
685
686 self.autoscroll = true;
687 cx.notify();
688 }
689 }
690
691 pub fn activate_prev_item(&mut self, activate_pane: bool, cx: &mut ViewContext<Self>) {
692 let mut index = self.active_item_index;
693 if index > 0 {
694 index -= 1;
695 } else if !self.items.is_empty() {
696 index = self.items.len() - 1;
697 }
698 self.activate_item(index, activate_pane, activate_pane, cx);
699 }
700
701 pub fn activate_next_item(&mut self, activate_pane: bool, cx: &mut ViewContext<Self>) {
702 let mut index = self.active_item_index;
703 if index + 1 < self.items.len() {
704 index += 1;
705 } else {
706 index = 0;
707 }
708 self.activate_item(index, activate_pane, activate_pane, cx);
709 }
710
711 pub fn close_active_item(
712 workspace: &mut Workspace,
713 _: &CloseActiveItem,
714 cx: &mut ViewContext<Workspace>,
715 ) -> Option<Task<Result<()>>> {
716 Self::close_main(workspace, ItemType::Active, cx)
717 }
718
719 pub fn close_inactive_items(
720 workspace: &mut Workspace,
721 _: &CloseInactiveItems,
722 cx: &mut ViewContext<Workspace>,
723 ) -> Option<Task<Result<()>>> {
724 Self::close_main(workspace, ItemType::Inactive, cx)
725 }
726
727 pub fn close_all_items(
728 workspace: &mut Workspace,
729 _: &CloseAllItems,
730 cx: &mut ViewContext<Workspace>,
731 ) -> Option<Task<Result<()>>> {
732 Self::close_main(workspace, ItemType::All, cx)
733 }
734
735 pub fn close_clean_items(
736 workspace: &mut Workspace,
737 _: &CloseCleanItems,
738 cx: &mut ViewContext<Workspace>,
739 ) -> Option<Task<Result<()>>> {
740 Self::close_main(workspace, ItemType::Clean, cx)
741 }
742
743 fn close_main(
744 workspace: &mut Workspace,
745 close_item_type: ItemType,
746 cx: &mut ViewContext<Workspace>,
747 ) -> Option<Task<Result<()>>> {
748 let pane_handle = workspace.active_pane().clone();
749 let pane = pane_handle.read(cx);
750 if pane.items.is_empty() {
751 return None;
752 }
753
754 let active_item_id = pane.items[pane.active_item_index].id();
755 let clean_item_ids: Vec<_> = pane
756 .items()
757 .filter(|item| !item.is_dirty(cx))
758 .map(|item| item.id())
759 .collect();
760 let task =
761 Self::close_items(
762 workspace,
763 pane_handle,
764 cx,
765 move |item_id| match close_item_type {
766 ItemType::Active => item_id == active_item_id,
767 ItemType::Inactive => item_id != active_item_id,
768 ItemType::Clean => clean_item_ids.contains(&item_id),
769 ItemType::All => true,
770 },
771 );
772
773 Some(cx.foreground().spawn(async move {
774 task.await?;
775 Ok(())
776 }))
777 }
778
779 pub fn close_item(
780 workspace: &mut Workspace,
781 pane: ViewHandle<Pane>,
782 item_id_to_close: usize,
783 cx: &mut ViewContext<Workspace>,
784 ) -> Task<Result<()>> {
785 Self::close_items(workspace, pane, cx, move |view_id| {
786 view_id == item_id_to_close
787 })
788 }
789
790 pub fn close_items(
791 workspace: &mut Workspace,
792 pane: ViewHandle<Pane>,
793 cx: &mut ViewContext<Workspace>,
794 should_close: impl 'static + Fn(usize) -> bool,
795 ) -> Task<Result<()>> {
796 let project = workspace.project().clone();
797
798 // Find the items to close.
799 let mut items_to_close = Vec::new();
800 for item in &pane.read(cx).items {
801 if should_close(item.id()) {
802 items_to_close.push(item.boxed_clone());
803 }
804 }
805
806 // If a buffer is open both in a singleton editor and in a multibuffer, make sure
807 // to focus the singleton buffer when prompting to save that buffer, as opposed
808 // to focusing the multibuffer, because this gives the user a more clear idea
809 // of what content they would be saving.
810 items_to_close.sort_by_key(|item| !item.is_singleton(cx));
811
812 cx.spawn(|workspace, mut cx| async move {
813 let mut saved_project_entry_ids = HashSet::default();
814 for item in items_to_close.clone() {
815 // Find the item's current index and its set of project entries. Avoid
816 // storing these in advance, in case they have changed since this task
817 // was started.
818 let (item_ix, mut project_entry_ids) = pane.read_with(&cx, |pane, cx| {
819 (pane.index_for_item(&*item), item.project_entry_ids(cx))
820 });
821 let item_ix = if let Some(ix) = item_ix {
822 ix
823 } else {
824 continue;
825 };
826
827 // If an item hasn't yet been associated with a project entry, then always
828 // prompt to save it before closing it. Otherwise, check if the item has
829 // any project entries that are not open anywhere else in the workspace,
830 // AND that the user has not already been prompted to save. If there are
831 // any such project entries, prompt the user to save this item.
832 let should_save = if project_entry_ids.is_empty() {
833 true
834 } else {
835 workspace.read_with(&cx, |workspace, cx| {
836 for item in workspace.items(cx) {
837 if !items_to_close
838 .iter()
839 .any(|item_to_close| item_to_close.id() == item.id())
840 {
841 let other_project_entry_ids = item.project_entry_ids(cx);
842 project_entry_ids
843 .retain(|id| !other_project_entry_ids.contains(id));
844 }
845 }
846 });
847 project_entry_ids
848 .iter()
849 .any(|id| saved_project_entry_ids.insert(*id))
850 };
851
852 if should_save
853 && !Self::save_item(project.clone(), &pane, item_ix, &*item, true, &mut cx)
854 .await?
855 {
856 break;
857 }
858
859 // Remove the item from the pane.
860 pane.update(&mut cx, |pane, cx| {
861 if let Some(item_ix) = pane.items.iter().position(|i| i.id() == item.id()) {
862 pane.remove_item(item_ix, false, cx);
863 }
864 });
865 }
866
867 pane.update(&mut cx, |_, cx| cx.notify());
868 Ok(())
869 })
870 }
871
872 fn remove_item(&mut self, item_index: usize, activate_pane: bool, cx: &mut ViewContext<Self>) {
873 self.activation_history
874 .retain(|&history_entry| history_entry != self.items[item_index].id());
875
876 if item_index == self.active_item_index {
877 let index_to_activate = self
878 .activation_history
879 .pop()
880 .and_then(|last_activated_item| {
881 self.items.iter().enumerate().find_map(|(index, item)| {
882 (item.id() == last_activated_item).then_some(index)
883 })
884 })
885 // We didn't have a valid activation history entry, so fallback
886 // to activating the item to the left
887 .unwrap_or_else(|| item_index.min(self.items.len()).saturating_sub(1));
888
889 self.activate_item(index_to_activate, activate_pane, activate_pane, cx);
890 }
891
892 let item = self.items.remove(item_index);
893
894 cx.emit(Event::RemoveItem { item_id: item.id() });
895 if self.items.is_empty() {
896 item.deactivated(cx);
897 self.update_toolbar(cx);
898 cx.emit(Event::Remove);
899 }
900
901 if item_index < self.active_item_index {
902 self.active_item_index -= 1;
903 }
904
905 self.nav_history
906 .borrow_mut()
907 .set_mode(NavigationMode::ClosingItem);
908 item.deactivated(cx);
909 self.nav_history
910 .borrow_mut()
911 .set_mode(NavigationMode::Normal);
912
913 if let Some(path) = item.project_path(cx) {
914 self.nav_history
915 .borrow_mut()
916 .paths_by_item
917 .insert(item.id(), path);
918 } else {
919 self.nav_history
920 .borrow_mut()
921 .paths_by_item
922 .remove(&item.id());
923 }
924
925 cx.notify();
926 }
927
928 pub async fn save_item(
929 project: ModelHandle<Project>,
930 pane: &ViewHandle<Pane>,
931 item_ix: usize,
932 item: &dyn ItemHandle,
933 should_prompt_for_save: bool,
934 cx: &mut AsyncAppContext,
935 ) -> Result<bool> {
936 const CONFLICT_MESSAGE: &str =
937 "This file has changed on disk since you started editing it. Do you want to overwrite it?";
938 const DIRTY_MESSAGE: &str = "This file contains unsaved edits. Do you want to save it?";
939
940 let (has_conflict, is_dirty, can_save, is_singleton) = cx.read(|cx| {
941 (
942 item.has_conflict(cx),
943 item.is_dirty(cx),
944 item.can_save(cx),
945 item.is_singleton(cx),
946 )
947 });
948
949 if has_conflict && can_save {
950 let mut answer = pane.update(cx, |pane, cx| {
951 pane.activate_item(item_ix, true, true, cx);
952 cx.prompt(
953 PromptLevel::Warning,
954 CONFLICT_MESSAGE,
955 &["Overwrite", "Discard", "Cancel"],
956 )
957 });
958 match answer.next().await {
959 Some(0) => cx.update(|cx| item.save(project, cx)).await?,
960 Some(1) => cx.update(|cx| item.reload(project, cx)).await?,
961 _ => return Ok(false),
962 }
963 } else if is_dirty && (can_save || is_singleton) {
964 let will_autosave = cx.read(|cx| {
965 matches!(
966 cx.global::<Settings>().autosave,
967 Autosave::OnFocusChange | Autosave::OnWindowChange
968 ) && Self::can_autosave_item(&*item, cx)
969 });
970 let should_save = if should_prompt_for_save && !will_autosave {
971 let mut answer = pane.update(cx, |pane, cx| {
972 pane.activate_item(item_ix, true, true, cx);
973 cx.prompt(
974 PromptLevel::Warning,
975 DIRTY_MESSAGE,
976 &["Save", "Don't Save", "Cancel"],
977 )
978 });
979 match answer.next().await {
980 Some(0) => true,
981 Some(1) => false,
982 _ => return Ok(false),
983 }
984 } else {
985 true
986 };
987
988 if should_save {
989 if can_save {
990 cx.update(|cx| item.save(project, cx)).await?;
991 } else if is_singleton {
992 let start_abs_path = project
993 .read_with(cx, |project, cx| {
994 let worktree = project.visible_worktrees(cx).next()?;
995 Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
996 })
997 .unwrap_or_else(|| Path::new("").into());
998
999 let mut abs_path = cx.update(|cx| cx.prompt_for_new_path(&start_abs_path));
1000 if let Some(abs_path) = abs_path.next().await.flatten() {
1001 cx.update(|cx| item.save_as(project, abs_path, cx)).await?;
1002 } else {
1003 return Ok(false);
1004 }
1005 }
1006 }
1007 }
1008 Ok(true)
1009 }
1010
1011 fn can_autosave_item(item: &dyn ItemHandle, cx: &AppContext) -> bool {
1012 let is_deleted = item.project_entry_ids(cx).is_empty();
1013 item.is_dirty(cx) && !item.has_conflict(cx) && item.can_save(cx) && !is_deleted
1014 }
1015
1016 pub fn autosave_item(
1017 item: &dyn ItemHandle,
1018 project: ModelHandle<Project>,
1019 cx: &mut MutableAppContext,
1020 ) -> Task<Result<()>> {
1021 if Self::can_autosave_item(item, cx) {
1022 item.save(project, cx)
1023 } else {
1024 Task::ready(Ok(()))
1025 }
1026 }
1027
1028 pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
1029 if let Some(active_item) = self.active_item() {
1030 cx.focus(active_item);
1031 }
1032 }
1033
1034 pub fn move_item(
1035 workspace: &mut Workspace,
1036 from: ViewHandle<Pane>,
1037 to: ViewHandle<Pane>,
1038 item_id_to_move: usize,
1039 destination_index: usize,
1040 cx: &mut ViewContext<Workspace>,
1041 ) {
1042 let item_to_move = from
1043 .read(cx)
1044 .items()
1045 .enumerate()
1046 .find(|(_, item_handle)| item_handle.id() == item_id_to_move);
1047
1048 if item_to_move.is_none() {
1049 log::warn!("Tried to move item handle which was not in `from` pane. Maybe tab was closed during drop");
1050 return;
1051 }
1052 let (item_ix, item_handle) = item_to_move.unwrap();
1053 let item_handle = item_handle.clone();
1054
1055 if from != to {
1056 // Close item from previous pane
1057 from.update(cx, |from, cx| {
1058 from.remove_item(item_ix, false, cx);
1059 });
1060 }
1061
1062 // This automatically removes duplicate items in the pane
1063 Pane::add_item(
1064 workspace,
1065 &to,
1066 item_handle,
1067 true,
1068 true,
1069 Some(destination_index),
1070 cx,
1071 );
1072
1073 cx.focus(to);
1074 }
1075
1076 pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
1077 cx.emit(Event::Split(direction));
1078 }
1079
1080 fn deploy_split_menu(&mut self, action: &DeploySplitMenu, cx: &mut ViewContext<Self>) {
1081 self.tab_bar_context_menu.update(cx, |menu, cx| {
1082 menu.show(
1083 action.position,
1084 AnchorCorner::TopRight,
1085 vec![
1086 ContextMenuItem::item("Split Right", SplitRight),
1087 ContextMenuItem::item("Split Left", SplitLeft),
1088 ContextMenuItem::item("Split Up", SplitUp),
1089 ContextMenuItem::item("Split Down", SplitDown),
1090 ],
1091 cx,
1092 );
1093 });
1094 }
1095
1096 fn deploy_dock_menu(&mut self, action: &DeployDockMenu, cx: &mut ViewContext<Self>) {
1097 self.tab_bar_context_menu.update(cx, |menu, cx| {
1098 menu.show(
1099 action.position,
1100 AnchorCorner::TopRight,
1101 vec![
1102 ContextMenuItem::item("Anchor Dock Right", AnchorDockRight),
1103 ContextMenuItem::item("Anchor Dock Bottom", AnchorDockBottom),
1104 ContextMenuItem::item("Expand Dock", ExpandDock),
1105 ],
1106 cx,
1107 );
1108 });
1109 }
1110
1111 fn deploy_new_menu(&mut self, action: &DeployNewMenu, cx: &mut ViewContext<Self>) {
1112 self.tab_bar_context_menu.update(cx, |menu, cx| {
1113 menu.show(
1114 action.position,
1115 AnchorCorner::TopRight,
1116 vec![
1117 ContextMenuItem::item("New File", NewFile),
1118 ContextMenuItem::item("New Terminal", NewTerminal),
1119 ContextMenuItem::item("New Search", NewSearch),
1120 ],
1121 cx,
1122 );
1123 });
1124 }
1125
1126 pub fn toolbar(&self) -> &ViewHandle<Toolbar> {
1127 &self.toolbar
1128 }
1129
1130 fn update_toolbar(&mut self, cx: &mut ViewContext<Self>) {
1131 let active_item = self
1132 .items
1133 .get(self.active_item_index)
1134 .map(|item| item.as_ref());
1135 self.toolbar.update(cx, |toolbar, cx| {
1136 toolbar.set_active_pane_item(active_item, cx);
1137 });
1138 }
1139
1140 fn render_tabs(&mut self, cx: &mut RenderContext<Self>) -> impl Element {
1141 let theme = cx.global::<Settings>().theme.clone();
1142
1143 let pane = cx.handle();
1144 let autoscroll = if mem::take(&mut self.autoscroll) {
1145 Some(self.active_item_index)
1146 } else {
1147 None
1148 };
1149
1150 let pane_active = self.is_active;
1151
1152 enum Tabs {}
1153 let mut row = Flex::row().scrollable::<Tabs, _>(1, autoscroll, cx);
1154 for (ix, (item, detail)) in self
1155 .items
1156 .iter()
1157 .cloned()
1158 .zip(self.tab_details(cx))
1159 .enumerate()
1160 {
1161 let detail = if detail == 0 { None } else { Some(detail) };
1162 let tab_active = ix == self.active_item_index;
1163
1164 row.add_child({
1165 enum Tab {}
1166 dragged_item_receiver::<Tab, _>(ix, ix, true, None, cx, {
1167 let item = item.clone();
1168 let pane = pane.clone();
1169 let detail = detail.clone();
1170
1171 let theme = cx.global::<Settings>().theme.clone();
1172
1173 move |mouse_state, cx| {
1174 let tab_style = theme.workspace.tab_bar.tab_style(pane_active, tab_active);
1175 let hovered = mouse_state.hovered();
1176 Self::render_tab(&item, pane, ix == 0, detail, hovered, tab_style, cx)
1177 }
1178 })
1179 .with_cursor_style(if pane_active && tab_active {
1180 CursorStyle::Arrow
1181 } else {
1182 CursorStyle::PointingHand
1183 })
1184 .on_down(MouseButton::Left, move |_, cx| {
1185 cx.dispatch_action(ActivateItem(ix));
1186 cx.propagate_event();
1187 })
1188 .on_click(MouseButton::Middle, {
1189 let item = item.clone();
1190 let pane = pane.clone();
1191 move |_, cx: &mut EventContext| {
1192 cx.dispatch_action(CloseItem {
1193 item_id: item.id(),
1194 pane: pane.clone(),
1195 })
1196 }
1197 })
1198 .as_draggable(
1199 DraggedItem {
1200 item,
1201 pane: pane.clone(),
1202 },
1203 {
1204 let theme = cx.global::<Settings>().theme.clone();
1205
1206 let detail = detail.clone();
1207 move |dragged_item, cx: &mut RenderContext<Workspace>| {
1208 let tab_style = &theme.workspace.tab_bar.dragged_tab;
1209 Self::render_tab(
1210 &dragged_item.item,
1211 dragged_item.pane.clone(),
1212 false,
1213 detail,
1214 false,
1215 &tab_style,
1216 cx,
1217 )
1218 }
1219 },
1220 )
1221 .boxed()
1222 })
1223 }
1224
1225 // Use the inactive tab style along with the current pane's active status to decide how to render
1226 // the filler
1227 let filler_index = self.items.len();
1228 let filler_style = theme.workspace.tab_bar.tab_style(pane_active, false);
1229 enum Filler {}
1230 row.add_child(
1231 dragged_item_receiver::<Filler, _>(0, filler_index, true, None, cx, |_, _| {
1232 Empty::new()
1233 .contained()
1234 .with_style(filler_style.container)
1235 .with_border(filler_style.container.border)
1236 .boxed()
1237 })
1238 .flex(1., true)
1239 .named("filler"),
1240 );
1241
1242 row
1243 }
1244
1245 fn tab_details(&self, cx: &AppContext) -> Vec<usize> {
1246 let mut tab_details = (0..self.items.len()).map(|_| 0).collect::<Vec<_>>();
1247
1248 let mut tab_descriptions = HashMap::default();
1249 let mut done = false;
1250 while !done {
1251 done = true;
1252
1253 // Store item indices by their tab description.
1254 for (ix, (item, detail)) in self.items.iter().zip(&tab_details).enumerate() {
1255 if let Some(description) = item.tab_description(*detail, cx) {
1256 if *detail == 0
1257 || Some(&description) != item.tab_description(detail - 1, cx).as_ref()
1258 {
1259 tab_descriptions
1260 .entry(description)
1261 .or_insert(Vec::new())
1262 .push(ix);
1263 }
1264 }
1265 }
1266
1267 // If two or more items have the same tab description, increase their level
1268 // of detail and try again.
1269 for (_, item_ixs) in tab_descriptions.drain() {
1270 if item_ixs.len() > 1 {
1271 done = false;
1272 for ix in item_ixs {
1273 tab_details[ix] += 1;
1274 }
1275 }
1276 }
1277 }
1278
1279 tab_details
1280 }
1281
1282 fn render_tab<V: View>(
1283 item: &Box<dyn ItemHandle>,
1284 pane: WeakViewHandle<Pane>,
1285 first: bool,
1286 detail: Option<usize>,
1287 hovered: bool,
1288 tab_style: &theme::Tab,
1289 cx: &mut RenderContext<V>,
1290 ) -> ElementBox {
1291 let title = item.tab_content(detail, &tab_style, cx);
1292 let mut container = tab_style.container.clone();
1293 if first {
1294 container.border.left = false;
1295 }
1296
1297 Flex::row()
1298 .with_child(
1299 Align::new({
1300 let diameter = 7.0;
1301 let icon_color = if item.has_conflict(cx) {
1302 Some(tab_style.icon_conflict)
1303 } else if item.is_dirty(cx) {
1304 Some(tab_style.icon_dirty)
1305 } else {
1306 None
1307 };
1308
1309 ConstrainedBox::new(
1310 Canvas::new(move |bounds, _, cx| {
1311 if let Some(color) = icon_color {
1312 let square = RectF::new(bounds.origin(), vec2f(diameter, diameter));
1313 cx.scene.push_quad(Quad {
1314 bounds: square,
1315 background: Some(color),
1316 border: Default::default(),
1317 corner_radius: diameter / 2.,
1318 });
1319 }
1320 })
1321 .boxed(),
1322 )
1323 .with_width(diameter)
1324 .with_height(diameter)
1325 .boxed()
1326 })
1327 .boxed(),
1328 )
1329 .with_child(
1330 Container::new(Align::new(title).boxed())
1331 .with_style(ContainerStyle {
1332 margin: Margin {
1333 left: tab_style.spacing,
1334 right: tab_style.spacing,
1335 ..Default::default()
1336 },
1337 ..Default::default()
1338 })
1339 .boxed(),
1340 )
1341 .with_child(
1342 Align::new(
1343 ConstrainedBox::new(if hovered {
1344 let item_id = item.id();
1345 enum TabCloseButton {}
1346 let icon = Svg::new("icons/x_mark_8.svg");
1347 MouseEventHandler::<TabCloseButton>::new(item_id, cx, |mouse_state, _| {
1348 if mouse_state.hovered() {
1349 icon.with_color(tab_style.icon_close_active).boxed()
1350 } else {
1351 icon.with_color(tab_style.icon_close).boxed()
1352 }
1353 })
1354 .with_padding(Padding::uniform(4.))
1355 .with_cursor_style(CursorStyle::PointingHand)
1356 .on_click(MouseButton::Left, {
1357 let pane = pane.clone();
1358 move |_, cx| {
1359 cx.dispatch_action(CloseItem {
1360 item_id,
1361 pane: pane.clone(),
1362 })
1363 }
1364 })
1365 .named("close-tab-icon")
1366 } else {
1367 Empty::new().boxed()
1368 })
1369 .with_width(tab_style.icon_width)
1370 .boxed(),
1371 )
1372 .boxed(),
1373 )
1374 .contained()
1375 .with_style(container)
1376 .constrained()
1377 .with_height(tab_style.height)
1378 .boxed()
1379 }
1380
1381 fn render_tab_bar_buttons(
1382 &mut self,
1383 theme: &Theme,
1384 cx: &mut RenderContext<Self>,
1385 ) -> ElementBox {
1386 Flex::row()
1387 // New menu
1388 .with_child(tab_bar_button(0, "icons/plus_12.svg", cx, |position| {
1389 DeployNewMenu { position }
1390 }))
1391 .with_child(
1392 self.docked
1393 .map(|anchor| {
1394 // Add the dock menu button if this pane is a dock
1395 let dock_icon = icon_for_dock_anchor(anchor);
1396
1397 tab_bar_button(1, dock_icon, cx, |position| DeployDockMenu { position })
1398 })
1399 .unwrap_or_else(|| {
1400 // Add the split menu if this pane is not a dock
1401 tab_bar_button(2, "icons/split_12.svg", cx, |position| DeploySplitMenu {
1402 position,
1403 })
1404 }),
1405 )
1406 // Add the close dock button if this pane is a dock
1407 .with_children(
1408 self.docked
1409 .map(|_| tab_bar_button(3, "icons/x_mark_8.svg", cx, |_| HideDock)),
1410 )
1411 .contained()
1412 .with_style(theme.workspace.tab_bar.pane_button_container)
1413 .flex(1., false)
1414 .boxed()
1415 }
1416}
1417
1418impl Entity for Pane {
1419 type Event = Event;
1420}
1421
1422impl View for Pane {
1423 fn ui_name() -> &'static str {
1424 "Pane"
1425 }
1426
1427 fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
1428 let this = cx.handle();
1429
1430 enum MouseNavigationHandler {}
1431
1432 Stack::new()
1433 .with_child(
1434 MouseEventHandler::<MouseNavigationHandler>::new(0, cx, |_, cx| {
1435 if let Some(active_item) = self.active_item() {
1436 Flex::column()
1437 .with_child({
1438 let mut tab_row = Flex::row()
1439 .with_child(self.render_tabs(cx).flex(1., true).named("tabs"));
1440
1441 // Render pane buttons
1442 let theme = cx.global::<Settings>().theme.clone();
1443 if self.is_active {
1444 tab_row.add_child(self.render_tab_bar_buttons(&theme, cx))
1445 }
1446
1447 tab_row
1448 .constrained()
1449 .with_height(theme.workspace.tab_bar.height)
1450 .contained()
1451 .with_style(theme.workspace.tab_bar.container)
1452 .flex(1., false)
1453 .named("tab bar")
1454 })
1455 .with_child({
1456 enum PaneContentTabDropTarget {}
1457 dragged_item_receiver::<PaneContentTabDropTarget, _>(
1458 0,
1459 self.active_item_index + 1,
1460 false,
1461 Some(100.),
1462 cx,
1463 {
1464 let toolbar = self.toolbar.clone();
1465 move |_, cx| {
1466 Flex::column()
1467 .with_child(
1468 ChildView::new(&toolbar, cx).expanded().boxed(),
1469 )
1470 .with_child(
1471 ChildView::new(active_item, cx)
1472 .flex(1., true)
1473 .boxed(),
1474 )
1475 .boxed()
1476 }
1477 },
1478 )
1479 .flex(1., true)
1480 .boxed()
1481 })
1482 .boxed()
1483 } else {
1484 enum EmptyPane {}
1485 let theme = cx.global::<Settings>().theme.clone();
1486
1487 dragged_item_receiver::<EmptyPane, _>(0, 0, false, None, cx, |_, _| {
1488 Empty::new()
1489 .contained()
1490 .with_background_color(theme.workspace.background)
1491 .boxed()
1492 })
1493 .on_down(MouseButton::Left, |_, cx| {
1494 cx.focus_parent_view();
1495 })
1496 .boxed()
1497 }
1498 })
1499 .on_down(MouseButton::Navigate(NavigationDirection::Back), {
1500 let this = this.clone();
1501 move |_, cx| {
1502 cx.dispatch_action(GoBack {
1503 pane: Some(this.clone()),
1504 });
1505 }
1506 })
1507 .on_down(MouseButton::Navigate(NavigationDirection::Forward), {
1508 let this = this.clone();
1509 move |_, cx| {
1510 cx.dispatch_action(GoForward {
1511 pane: Some(this.clone()),
1512 })
1513 }
1514 })
1515 .boxed(),
1516 )
1517 .with_child(ChildView::new(&self.tab_bar_context_menu, cx).boxed())
1518 .named("pane")
1519 }
1520
1521 fn focus_in(&mut self, focused: AnyViewHandle, cx: &mut ViewContext<Self>) {
1522 if let Some(active_item) = self.active_item() {
1523 if cx.is_self_focused() {
1524 // Pane was focused directly. We need to either focus a view inside the active item,
1525 // or focus the active item itself
1526 if let Some(weak_last_focused_view) =
1527 self.last_focused_view_by_item.get(&active_item.id())
1528 {
1529 if let Some(last_focused_view) = weak_last_focused_view.upgrade(cx) {
1530 cx.focus(last_focused_view);
1531 return;
1532 } else {
1533 self.last_focused_view_by_item.remove(&active_item.id());
1534 }
1535 }
1536
1537 cx.focus(active_item);
1538 } else {
1539 self.last_focused_view_by_item
1540 .insert(active_item.id(), focused.downgrade());
1541 }
1542 }
1543 }
1544}
1545
1546fn tab_bar_button<A: Action>(
1547 index: usize,
1548 icon: &'static str,
1549 cx: &mut RenderContext<Pane>,
1550 action_builder: impl 'static + Fn(Vector2F) -> A,
1551) -> ElementBox {
1552 enum TabBarButton {}
1553
1554 MouseEventHandler::<TabBarButton>::new(index, cx, |mouse_state, cx| {
1555 let theme = &cx.global::<Settings>().theme.workspace.tab_bar;
1556 let style = theme.pane_button.style_for(mouse_state, false);
1557 Svg::new(icon)
1558 .with_color(style.color)
1559 .constrained()
1560 .with_width(style.icon_width)
1561 .aligned()
1562 .constrained()
1563 .with_width(style.button_width)
1564 .with_height(style.button_width)
1565 // .aligned()
1566 .boxed()
1567 })
1568 .with_cursor_style(CursorStyle::PointingHand)
1569 .on_click(MouseButton::Left, move |e, cx| {
1570 cx.dispatch_action(action_builder(e.region.lower_right()));
1571 })
1572 .flex(1., false)
1573 .boxed()
1574}
1575
1576impl ItemNavHistory {
1577 pub fn push<D: 'static + Any>(&self, data: Option<D>, cx: &mut MutableAppContext) {
1578 self.history.borrow_mut().push(data, self.item.clone(), cx);
1579 }
1580
1581 pub fn pop_backward(&self, cx: &mut MutableAppContext) -> Option<NavigationEntry> {
1582 self.history.borrow_mut().pop(NavigationMode::GoingBack, cx)
1583 }
1584
1585 pub fn pop_forward(&self, cx: &mut MutableAppContext) -> Option<NavigationEntry> {
1586 self.history
1587 .borrow_mut()
1588 .pop(NavigationMode::GoingForward, cx)
1589 }
1590}
1591
1592impl NavHistory {
1593 fn set_mode(&mut self, mode: NavigationMode) {
1594 self.mode = mode;
1595 }
1596
1597 fn disable(&mut self) {
1598 self.mode = NavigationMode::Disabled;
1599 }
1600
1601 fn enable(&mut self) {
1602 self.mode = NavigationMode::Normal;
1603 }
1604
1605 fn pop(&mut self, mode: NavigationMode, cx: &mut MutableAppContext) -> Option<NavigationEntry> {
1606 let entry = match mode {
1607 NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
1608 return None
1609 }
1610 NavigationMode::GoingBack => &mut self.backward_stack,
1611 NavigationMode::GoingForward => &mut self.forward_stack,
1612 NavigationMode::ReopeningClosedItem => &mut self.closed_stack,
1613 }
1614 .pop_back();
1615 if entry.is_some() {
1616 self.did_update(cx);
1617 }
1618 entry
1619 }
1620
1621 fn push<D: 'static + Any>(
1622 &mut self,
1623 data: Option<D>,
1624 item: Rc<dyn WeakItemHandle>,
1625 cx: &mut MutableAppContext,
1626 ) {
1627 match self.mode {
1628 NavigationMode::Disabled => {}
1629 NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
1630 if self.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
1631 self.backward_stack.pop_front();
1632 }
1633 self.backward_stack.push_back(NavigationEntry {
1634 item,
1635 data: data.map(|data| Box::new(data) as Box<dyn Any>),
1636 });
1637 self.forward_stack.clear();
1638 }
1639 NavigationMode::GoingBack => {
1640 if self.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
1641 self.forward_stack.pop_front();
1642 }
1643 self.forward_stack.push_back(NavigationEntry {
1644 item,
1645 data: data.map(|data| Box::new(data) as Box<dyn Any>),
1646 });
1647 }
1648 NavigationMode::GoingForward => {
1649 if self.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
1650 self.backward_stack.pop_front();
1651 }
1652 self.backward_stack.push_back(NavigationEntry {
1653 item,
1654 data: data.map(|data| Box::new(data) as Box<dyn Any>),
1655 });
1656 }
1657 NavigationMode::ClosingItem => {
1658 if self.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
1659 self.closed_stack.pop_front();
1660 }
1661 self.closed_stack.push_back(NavigationEntry {
1662 item,
1663 data: data.map(|data| Box::new(data) as Box<dyn Any>),
1664 });
1665 }
1666 }
1667 self.did_update(cx);
1668 }
1669
1670 fn did_update(&self, cx: &mut MutableAppContext) {
1671 if let Some(pane) = self.pane.upgrade(cx) {
1672 cx.defer(move |cx| pane.update(cx, |pane, cx| pane.history_updated(cx)));
1673 }
1674 }
1675}
1676
1677#[cfg(test)]
1678mod tests {
1679 use std::sync::Arc;
1680
1681 use super::*;
1682 use crate::item::test::TestItem;
1683 use gpui::{executor::Deterministic, TestAppContext};
1684 use project::FakeFs;
1685
1686 #[gpui::test]
1687 async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
1688 cx.foreground().forbid_parking();
1689 Settings::test_async(cx);
1690 let fs = FakeFs::new(cx.background());
1691
1692 let project = Project::test(fs, None, cx).await;
1693 let (_, workspace) = cx.add_window(|cx| {
1694 Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
1695 });
1696 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
1697
1698 // 1. Add with a destination index
1699 // a. Add before the active item
1700 set_labeled_items(&workspace, &pane, ["A", "B*", "C"], cx);
1701 workspace.update(cx, |workspace, cx| {
1702 Pane::add_item(
1703 workspace,
1704 &pane,
1705 Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
1706 false,
1707 false,
1708 Some(0),
1709 cx,
1710 );
1711 });
1712 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
1713
1714 // b. Add after the active item
1715 set_labeled_items(&workspace, &pane, ["A", "B*", "C"], cx);
1716 workspace.update(cx, |workspace, cx| {
1717 Pane::add_item(
1718 workspace,
1719 &pane,
1720 Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
1721 false,
1722 false,
1723 Some(2),
1724 cx,
1725 );
1726 });
1727 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
1728
1729 // c. Add at the end of the item list (including off the length)
1730 set_labeled_items(&workspace, &pane, ["A", "B*", "C"], cx);
1731 workspace.update(cx, |workspace, cx| {
1732 Pane::add_item(
1733 workspace,
1734 &pane,
1735 Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
1736 false,
1737 false,
1738 Some(5),
1739 cx,
1740 );
1741 });
1742 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
1743
1744 // 2. Add without a destination index
1745 // a. Add with active item at the start of the item list
1746 set_labeled_items(&workspace, &pane, ["A*", "B", "C"], cx);
1747 workspace.update(cx, |workspace, cx| {
1748 Pane::add_item(
1749 workspace,
1750 &pane,
1751 Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
1752 false,
1753 false,
1754 None,
1755 cx,
1756 );
1757 });
1758 set_labeled_items(&workspace, &pane, ["A", "D*", "B", "C"], cx);
1759
1760 // b. Add with active item at the end of the item list
1761 set_labeled_items(&workspace, &pane, ["A", "B", "C*"], cx);
1762 workspace.update(cx, |workspace, cx| {
1763 Pane::add_item(
1764 workspace,
1765 &pane,
1766 Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
1767 false,
1768 false,
1769 None,
1770 cx,
1771 );
1772 });
1773 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
1774 }
1775
1776 #[gpui::test]
1777 async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
1778 cx.foreground().forbid_parking();
1779 Settings::test_async(cx);
1780 let fs = FakeFs::new(cx.background());
1781
1782 let project = Project::test(fs, None, cx).await;
1783 let (_, workspace) = cx.add_window(|cx| {
1784 Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
1785 });
1786 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
1787
1788 // 1. Add with a destination index
1789 // 1a. Add before the active item
1790 let [_, _, _, d] = set_labeled_items(&workspace, &pane, ["A", "B*", "C", "D"], cx);
1791 workspace.update(cx, |workspace, cx| {
1792 Pane::add_item(workspace, &pane, d, false, false, Some(0), cx);
1793 });
1794 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
1795
1796 // 1b. Add after the active item
1797 let [_, _, _, d] = set_labeled_items(&workspace, &pane, ["A", "B*", "C", "D"], cx);
1798 workspace.update(cx, |workspace, cx| {
1799 Pane::add_item(workspace, &pane, d, false, false, Some(2), cx);
1800 });
1801 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
1802
1803 // 1c. Add at the end of the item list (including off the length)
1804 let [a, _, _, _] = set_labeled_items(&workspace, &pane, ["A", "B*", "C", "D"], cx);
1805 workspace.update(cx, |workspace, cx| {
1806 Pane::add_item(workspace, &pane, a, false, false, Some(5), cx);
1807 });
1808 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
1809
1810 // 1d. Add same item to active index
1811 let [_, b, _] = set_labeled_items(&workspace, &pane, ["A", "B*", "C"], cx);
1812 workspace.update(cx, |workspace, cx| {
1813 Pane::add_item(workspace, &pane, b, false, false, Some(1), cx);
1814 });
1815 assert_item_labels(&pane, ["A", "B*", "C"], cx);
1816
1817 // 1e. Add item to index after same item in last position
1818 let [_, _, c] = set_labeled_items(&workspace, &pane, ["A", "B*", "C"], cx);
1819 workspace.update(cx, |workspace, cx| {
1820 Pane::add_item(workspace, &pane, c, false, false, Some(2), cx);
1821 });
1822 assert_item_labels(&pane, ["A", "B", "C*"], cx);
1823
1824 // 2. Add without a destination index
1825 // 2a. Add with active item at the start of the item list
1826 let [_, _, _, d] = set_labeled_items(&workspace, &pane, ["A*", "B", "C", "D"], cx);
1827 workspace.update(cx, |workspace, cx| {
1828 Pane::add_item(workspace, &pane, d, false, false, None, cx);
1829 });
1830 assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
1831
1832 // 2b. Add with active item at the end of the item list
1833 let [a, _, _, _] = set_labeled_items(&workspace, &pane, ["A", "B", "C", "D*"], cx);
1834 workspace.update(cx, |workspace, cx| {
1835 Pane::add_item(workspace, &pane, a, false, false, None, cx);
1836 });
1837 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
1838
1839 // 2c. Add active item to active item at end of list
1840 let [_, _, c] = set_labeled_items(&workspace, &pane, ["A", "B", "C*"], cx);
1841 workspace.update(cx, |workspace, cx| {
1842 Pane::add_item(workspace, &pane, c, false, false, None, cx);
1843 });
1844 assert_item_labels(&pane, ["A", "B", "C*"], cx);
1845
1846 // 2d. Add active item to active item at start of list
1847 let [a, _, _] = set_labeled_items(&workspace, &pane, ["A*", "B", "C"], cx);
1848 workspace.update(cx, |workspace, cx| {
1849 Pane::add_item(workspace, &pane, a, false, false, None, cx);
1850 });
1851 assert_item_labels(&pane, ["A*", "B", "C"], cx);
1852 }
1853
1854 #[gpui::test]
1855 async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
1856 cx.foreground().forbid_parking();
1857 Settings::test_async(cx);
1858 let fs = FakeFs::new(cx.background());
1859
1860 let project = Project::test(fs, None, cx).await;
1861 let (_, workspace) = cx.add_window(|cx| {
1862 Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
1863 });
1864 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
1865
1866 // singleton view
1867 workspace.update(cx, |workspace, cx| {
1868 let item = TestItem::new()
1869 .with_singleton(true)
1870 .with_label("buffer 1")
1871 .with_project_entry_ids(&[1]);
1872
1873 Pane::add_item(
1874 workspace,
1875 &pane,
1876 Box::new(cx.add_view(|_| item)),
1877 false,
1878 false,
1879 None,
1880 cx,
1881 );
1882 });
1883 assert_item_labels(&pane, ["buffer 1*"], cx);
1884
1885 // new singleton view with the same project entry
1886 workspace.update(cx, |workspace, cx| {
1887 let item = TestItem::new()
1888 .with_singleton(true)
1889 .with_label("buffer 1")
1890 .with_project_entry_ids(&[1]);
1891
1892 Pane::add_item(
1893 workspace,
1894 &pane,
1895 Box::new(cx.add_view(|_| item)),
1896 false,
1897 false,
1898 None,
1899 cx,
1900 );
1901 });
1902 assert_item_labels(&pane, ["buffer 1*"], cx);
1903
1904 // new singleton view with different project entry
1905 workspace.update(cx, |workspace, cx| {
1906 let item = TestItem::new()
1907 .with_singleton(true)
1908 .with_label("buffer 2")
1909 .with_project_entry_ids(&[2]);
1910
1911 Pane::add_item(
1912 workspace,
1913 &pane,
1914 Box::new(cx.add_view(|_| item)),
1915 false,
1916 false,
1917 None,
1918 cx,
1919 );
1920 });
1921 assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
1922
1923 // new multibuffer view with the same project entry
1924 workspace.update(cx, |workspace, cx| {
1925 let item = TestItem::new()
1926 .with_singleton(false)
1927 .with_label("multibuffer 1")
1928 .with_project_entry_ids(&[1]);
1929
1930 Pane::add_item(
1931 workspace,
1932 &pane,
1933 Box::new(cx.add_view(|_| item)),
1934 false,
1935 false,
1936 None,
1937 cx,
1938 );
1939 });
1940 assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
1941
1942 // another multibuffer view with the same project entry
1943 workspace.update(cx, |workspace, cx| {
1944 let item = TestItem::new()
1945 .with_singleton(false)
1946 .with_label("multibuffer 1b")
1947 .with_project_entry_ids(&[1]);
1948
1949 Pane::add_item(
1950 workspace,
1951 &pane,
1952 Box::new(cx.add_view(|_| item)),
1953 false,
1954 false,
1955 None,
1956 cx,
1957 );
1958 });
1959 assert_item_labels(
1960 &pane,
1961 ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
1962 cx,
1963 );
1964 }
1965
1966 #[gpui::test]
1967 async fn test_remove_item_ordering(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
1968 Settings::test_async(cx);
1969 let fs = FakeFs::new(cx.background());
1970
1971 let project = Project::test(fs, None, cx).await;
1972 let (_, workspace) =
1973 cx.add_window(|cx| Workspace::new(None, 0, project, |_, _| unimplemented!(), cx));
1974 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
1975
1976 add_labled_item(&workspace, &pane, "A", cx);
1977 add_labled_item(&workspace, &pane, "B", cx);
1978 add_labled_item(&workspace, &pane, "C", cx);
1979 add_labled_item(&workspace, &pane, "D", cx);
1980 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
1981
1982 pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
1983 add_labled_item(&workspace, &pane, "1", cx);
1984 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
1985
1986 workspace.update(cx, |workspace, cx| {
1987 Pane::close_active_item(workspace, &CloseActiveItem, cx);
1988 });
1989 deterministic.run_until_parked();
1990 assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
1991
1992 pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
1993 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
1994
1995 workspace.update(cx, |workspace, cx| {
1996 Pane::close_active_item(workspace, &CloseActiveItem, cx);
1997 });
1998 deterministic.run_until_parked();
1999 assert_item_labels(&pane, ["A", "B*", "C"], cx);
2000
2001 workspace.update(cx, |workspace, cx| {
2002 Pane::close_active_item(workspace, &CloseActiveItem, cx);
2003 });
2004 deterministic.run_until_parked();
2005 assert_item_labels(&pane, ["A", "C*"], cx);
2006
2007 workspace.update(cx, |workspace, cx| {
2008 Pane::close_active_item(workspace, &CloseActiveItem, cx);
2009 });
2010 deterministic.run_until_parked();
2011 assert_item_labels(&pane, ["A*"], cx);
2012 }
2013
2014 fn add_labled_item(
2015 workspace: &ViewHandle<Workspace>,
2016 pane: &ViewHandle<Pane>,
2017 label: &str,
2018 cx: &mut TestAppContext,
2019 ) -> Box<ViewHandle<TestItem>> {
2020 workspace.update(cx, |workspace, cx| {
2021 let labeled_item = Box::new(cx.add_view(|_| TestItem::new().with_label(label)));
2022
2023 Pane::add_item(
2024 workspace,
2025 pane,
2026 labeled_item.clone(),
2027 false,
2028 false,
2029 None,
2030 cx,
2031 );
2032
2033 labeled_item
2034 })
2035 }
2036
2037 fn set_labeled_items<const COUNT: usize>(
2038 workspace: &ViewHandle<Workspace>,
2039 pane: &ViewHandle<Pane>,
2040 labels: [&str; COUNT],
2041 cx: &mut TestAppContext,
2042 ) -> [Box<ViewHandle<TestItem>>; COUNT] {
2043 pane.update(cx, |pane, _| {
2044 pane.items.clear();
2045 });
2046
2047 workspace.update(cx, |workspace, cx| {
2048 let mut active_item_index = 0;
2049
2050 let mut index = 0;
2051 let items = labels.map(|mut label| {
2052 if label.ends_with("*") {
2053 label = label.trim_end_matches("*");
2054 active_item_index = index;
2055 }
2056
2057 let labeled_item = Box::new(cx.add_view(|_| TestItem::new().with_label(label)));
2058 Pane::add_item(
2059 workspace,
2060 pane,
2061 labeled_item.clone(),
2062 false,
2063 false,
2064 None,
2065 cx,
2066 );
2067 index += 1;
2068 labeled_item
2069 });
2070
2071 pane.update(cx, |pane, cx| {
2072 pane.activate_item(active_item_index, false, false, cx)
2073 });
2074
2075 items
2076 })
2077 }
2078
2079 // Assert the item label, with the active item label suffixed with a '*'
2080 fn assert_item_labels<const COUNT: usize>(
2081 pane: &ViewHandle<Pane>,
2082 expected_states: [&str; COUNT],
2083 cx: &mut TestAppContext,
2084 ) {
2085 pane.read_with(cx, |pane, cx| {
2086 let actual_states = pane
2087 .items
2088 .iter()
2089 .enumerate()
2090 .map(|(ix, item)| {
2091 let mut state = item
2092 .to_any()
2093 .downcast::<TestItem>()
2094 .unwrap()
2095 .read(cx)
2096 .label
2097 .clone();
2098 if ix == pane.active_item_index {
2099 state.push('*');
2100 }
2101 state
2102 })
2103 .collect::<Vec<_>>();
2104
2105 assert_eq!(
2106 actual_states, expected_states,
2107 "pane items do not match expectation"
2108 );
2109 })
2110 }
2111}