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