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