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