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