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