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