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