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