1mod dragged_item_receiver;
2
3use super::{ItemHandle, SplitDirection};
4use crate::{
5 item::WeakItemHandle, toolbar::Toolbar, AutosaveSetting, Item, NewFile, NewSearch, NewTerminal,
6 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 is_active: bool,
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 is_active: true,
242 zoomed: false,
243 active_item_index: 0,
244 last_focused_view_by_item: Default::default(),
245 autoscroll: false,
246 nav_history: NavHistory(Rc::new(RefCell::new(NavHistoryState {
247 mode: NavigationMode::Normal,
248 backward_stack: Default::default(),
249 forward_stack: Default::default(),
250 closed_stack: Default::default(),
251 paths_by_item: Default::default(),
252 pane: handle.clone(),
253 next_timestamp,
254 }))),
255 toolbar: cx.add_view(|_| Toolbar::new(handle)),
256 tab_bar_context_menu: TabBarContextMenu {
257 kind: TabBarContextMenuKind::New,
258 handle: context_menu,
259 },
260 tab_context_menu: cx.add_view(|cx| ContextMenu::new(pane_view_id, cx)),
261 _background_actions: background_actions,
262 workspace,
263 project,
264 has_focus: false,
265 can_drop: Rc::new(|_, _| true),
266 can_split: true,
267 render_tab_bar_buttons: Rc::new(|pane, cx| {
268 Flex::row()
269 // New menu
270 .with_child(Self::render_tab_bar_button(
271 0,
272 "icons/plus_12.svg",
273 Some(("New...".into(), None)),
274 cx,
275 |pane, cx| pane.deploy_new_menu(cx),
276 pane.tab_bar_context_menu
277 .handle_if_kind(TabBarContextMenuKind::New),
278 ))
279 .with_child(Self::render_tab_bar_button(
280 1,
281 "icons/split_12.svg",
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 Some(("Toggle Zoom".into(), Some(Box::new(ToggleZoom)))),
296 cx,
297 move |pane, cx| pane.toggle_zoom(&Default::default(), cx),
298 None,
299 ))
300 .into_any()
301 }),
302 }
303 }
304
305 pub(crate) fn workspace(&self) -> &WeakViewHandle<Workspace> {
306 &self.workspace
307 }
308
309 pub fn is_active(&self) -> bool {
310 self.is_active
311 }
312
313 pub fn set_active(&mut self, is_active: bool, cx: &mut ViewContext<Self>) {
314 self.is_active = is_active;
315 cx.notify();
316 }
317
318 pub fn has_focus(&self) -> bool {
319 self.has_focus
320 }
321
322 pub fn active_item_index(&self) -> usize {
323 self.active_item_index
324 }
325
326 pub fn on_can_drop<F>(&mut self, can_drop: F)
327 where
328 F: 'static + Fn(&DragAndDrop<Workspace>, &WindowContext) -> bool,
329 {
330 self.can_drop = Rc::new(can_drop);
331 }
332
333 pub fn set_can_split(&mut self, can_split: bool, cx: &mut ViewContext<Self>) {
334 self.can_split = can_split;
335 cx.notify();
336 }
337
338 pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut ViewContext<Self>) {
339 self.toolbar.update(cx, |toolbar, cx| {
340 toolbar.set_can_navigate(can_navigate, cx);
341 });
342 cx.notify();
343 }
344
345 pub fn set_render_tab_bar_buttons<F>(&mut self, cx: &mut ViewContext<Self>, render: F)
346 where
347 F: 'static + Fn(&mut Pane, &mut ViewContext<Pane>) -> AnyElement<Pane>,
348 {
349 self.render_tab_bar_buttons = Rc::new(render);
350 cx.notify();
351 }
352
353 pub fn nav_history_for_item<T: Item>(&self, item: &ViewHandle<T>) -> ItemNavHistory {
354 ItemNavHistory {
355 history: self.nav_history.clone(),
356 item: Rc::new(item.downgrade()),
357 }
358 }
359
360 pub fn nav_history(&self) -> &NavHistory {
361 &self.nav_history
362 }
363
364 pub fn nav_history_mut(&mut self) -> &mut NavHistory {
365 &mut self.nav_history
366 }
367
368 pub fn disable_history(&mut self) {
369 self.nav_history.disable();
370 }
371
372 pub fn enable_history(&mut self) {
373 self.nav_history.enable();
374 }
375
376 pub fn can_navigate_backward(&self) -> bool {
377 !self.nav_history.0.borrow().backward_stack.is_empty()
378 }
379
380 pub fn can_navigate_forward(&self) -> bool {
381 !self.nav_history.0.borrow().forward_stack.is_empty()
382 }
383
384 fn history_updated(&mut self, cx: &mut ViewContext<Self>) {
385 self.toolbar.update(cx, |_, cx| cx.notify());
386 }
387
388 pub(crate) fn open_item(
389 &mut self,
390 project_entry_id: ProjectEntryId,
391 focus_item: bool,
392 cx: &mut ViewContext<Self>,
393 build_item: impl FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
394 ) -> Box<dyn ItemHandle> {
395 let mut existing_item = None;
396 for (index, item) in self.items.iter().enumerate() {
397 if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [project_entry_id]
398 {
399 let item = item.boxed_clone();
400 existing_item = Some((index, item));
401 break;
402 }
403 }
404
405 if let Some((index, existing_item)) = existing_item {
406 self.activate_item(index, focus_item, focus_item, cx);
407 existing_item
408 } else {
409 let new_item = build_item(cx);
410 self.add_item(new_item.clone(), true, focus_item, None, cx);
411 new_item
412 }
413 }
414
415 pub fn add_item(
416 &mut self,
417 item: Box<dyn ItemHandle>,
418 activate_pane: bool,
419 focus_item: bool,
420 destination_index: Option<usize>,
421 cx: &mut ViewContext<Self>,
422 ) {
423 if item.is_singleton(cx) {
424 if let Some(&entry_id) = item.project_entry_ids(cx).get(0) {
425 let project = self.project.read(cx);
426 if let Some(project_path) = project.path_for_entry(entry_id, cx) {
427 let abs_path = project.absolute_path(&project_path, cx);
428 self.nav_history
429 .0
430 .borrow_mut()
431 .paths_by_item
432 .insert(item.id(), (project_path, abs_path));
433 }
434 }
435 }
436 // If no destination index is specified, add or move the item after the active item.
437 let mut insertion_index = {
438 cmp::min(
439 if let Some(destination_index) = destination_index {
440 destination_index
441 } else {
442 self.active_item_index + 1
443 },
444 self.items.len(),
445 )
446 };
447
448 // Does the item already exist?
449 let project_entry_id = if item.is_singleton(cx) {
450 item.project_entry_ids(cx).get(0).copied()
451 } else {
452 None
453 };
454
455 let existing_item_index = self.items.iter().position(|existing_item| {
456 if existing_item.id() == item.id() {
457 true
458 } else if existing_item.is_singleton(cx) {
459 existing_item
460 .project_entry_ids(cx)
461 .get(0)
462 .map_or(false, |existing_entry_id| {
463 Some(existing_entry_id) == project_entry_id.as_ref()
464 })
465 } else {
466 false
467 }
468 });
469
470 if let Some(existing_item_index) = existing_item_index {
471 // If the item already exists, move it to the desired destination and activate it
472
473 if existing_item_index != insertion_index {
474 let existing_item_is_active = existing_item_index == self.active_item_index;
475
476 // If the caller didn't specify a destination and the added item is already
477 // the active one, don't move it
478 if existing_item_is_active && destination_index.is_none() {
479 insertion_index = existing_item_index;
480 } else {
481 self.items.remove(existing_item_index);
482 if existing_item_index < self.active_item_index {
483 self.active_item_index -= 1;
484 }
485 insertion_index = insertion_index.min(self.items.len());
486
487 self.items.insert(insertion_index, item.clone());
488
489 if existing_item_is_active {
490 self.active_item_index = insertion_index;
491 } else if insertion_index <= self.active_item_index {
492 self.active_item_index += 1;
493 }
494 }
495
496 cx.notify();
497 }
498
499 self.activate_item(insertion_index, activate_pane, focus_item, cx);
500 } else {
501 self.items.insert(insertion_index, item.clone());
502 if insertion_index <= self.active_item_index {
503 self.active_item_index += 1;
504 }
505
506 self.activate_item(insertion_index, activate_pane, focus_item, cx);
507 cx.notify();
508 }
509
510 cx.emit(Event::AddItem { item });
511 }
512
513 pub fn items_len(&self) -> usize {
514 self.items.len()
515 }
516
517 pub fn items(&self) -> impl Iterator<Item = &Box<dyn ItemHandle>> + DoubleEndedIterator {
518 self.items.iter()
519 }
520
521 pub fn items_of_type<T: View>(&self) -> impl '_ + Iterator<Item = ViewHandle<T>> {
522 self.items
523 .iter()
524 .filter_map(|item| item.as_any().clone().downcast())
525 }
526
527 pub fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
528 self.items.get(self.active_item_index).cloned()
529 }
530
531 pub fn item_for_entry(
532 &self,
533 entry_id: ProjectEntryId,
534 cx: &AppContext,
535 ) -> Option<Box<dyn ItemHandle>> {
536 self.items.iter().find_map(|item| {
537 if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
538 Some(item.boxed_clone())
539 } else {
540 None
541 }
542 })
543 }
544
545 pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option<usize> {
546 self.items.iter().position(|i| i.id() == item.id())
547 }
548
549 pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) {
550 if self.zoomed {
551 cx.emit(Event::ZoomOut);
552 } else if !self.items.is_empty() {
553 if !self.has_focus {
554 cx.focus_self();
555 }
556 cx.emit(Event::ZoomIn);
557 }
558 }
559
560 pub fn activate_item(
561 &mut self,
562 index: usize,
563 activate_pane: bool,
564 focus_item: bool,
565 cx: &mut ViewContext<Self>,
566 ) {
567 use NavigationMode::{GoingBack, GoingForward};
568
569 if index < self.items.len() {
570 let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
571 if prev_active_item_ix != self.active_item_index
572 || matches!(self.nav_history.mode(), GoingBack | GoingForward)
573 {
574 if let Some(prev_item) = self.items.get(prev_active_item_ix) {
575 prev_item.deactivated(cx);
576 }
577
578 cx.emit(Event::ActivateItem {
579 local: activate_pane,
580 });
581 }
582
583 if let Some(newly_active_item) = self.items.get(index) {
584 self.activation_history
585 .retain(|&previously_active_item_id| {
586 previously_active_item_id != newly_active_item.id()
587 });
588 self.activation_history.push(newly_active_item.id());
589 }
590
591 self.update_toolbar(cx);
592
593 if focus_item {
594 self.focus_active_item(cx);
595 }
596
597 self.autoscroll = true;
598 cx.notify();
599 }
600 }
601
602 pub fn activate_prev_item(&mut self, activate_pane: bool, cx: &mut ViewContext<Self>) {
603 let mut index = self.active_item_index;
604 if index > 0 {
605 index -= 1;
606 } else if !self.items.is_empty() {
607 index = self.items.len() - 1;
608 }
609 self.activate_item(index, activate_pane, activate_pane, cx);
610 }
611
612 pub fn activate_next_item(&mut self, activate_pane: bool, cx: &mut ViewContext<Self>) {
613 let mut index = self.active_item_index;
614 if index + 1 < self.items.len() {
615 index += 1;
616 } else {
617 index = 0;
618 }
619 self.activate_item(index, activate_pane, activate_pane, cx);
620 }
621
622 pub fn close_active_item(
623 &mut self,
624 _: &CloseActiveItem,
625 cx: &mut ViewContext<Self>,
626 ) -> Option<Task<Result<()>>> {
627 if self.items.is_empty() {
628 return None;
629 }
630 let active_item_id = self.items[self.active_item_index].id();
631 Some(self.close_item_by_id(active_item_id, cx))
632 }
633
634 pub fn close_item_by_id(
635 &mut self,
636 item_id_to_close: usize,
637 cx: &mut ViewContext<Self>,
638 ) -> Task<Result<()>> {
639 self.close_items(cx, move |view_id| view_id == item_id_to_close)
640 }
641
642 pub fn close_inactive_items(
643 &mut self,
644 _: &CloseInactiveItems,
645 cx: &mut ViewContext<Self>,
646 ) -> Option<Task<Result<()>>> {
647 if self.items.is_empty() {
648 return None;
649 }
650
651 let active_item_id = self.items[self.active_item_index].id();
652 Some(self.close_items(cx, move |item_id| item_id != active_item_id))
653 }
654
655 pub fn close_clean_items(
656 &mut self,
657 _: &CloseCleanItems,
658 cx: &mut ViewContext<Self>,
659 ) -> Option<Task<Result<()>>> {
660 let item_ids: Vec<_> = self
661 .items()
662 .filter(|item| !item.is_dirty(cx))
663 .map(|item| item.id())
664 .collect();
665 Some(self.close_items(cx, move |item_id| item_ids.contains(&item_id)))
666 }
667
668 pub fn close_items_to_the_left(
669 &mut self,
670 _: &CloseItemsToTheLeft,
671 cx: &mut ViewContext<Self>,
672 ) -> Option<Task<Result<()>>> {
673 if self.items.is_empty() {
674 return None;
675 }
676 let active_item_id = self.items[self.active_item_index].id();
677 Some(self.close_items_to_the_left_by_id(active_item_id, cx))
678 }
679
680 pub fn close_items_to_the_left_by_id(
681 &mut self,
682 item_id: usize,
683 cx: &mut ViewContext<Self>,
684 ) -> Task<Result<()>> {
685 let item_ids: Vec<_> = self
686 .items()
687 .take_while(|item| item.id() != item_id)
688 .map(|item| item.id())
689 .collect();
690 self.close_items(cx, move |item_id| item_ids.contains(&item_id))
691 }
692
693 pub fn close_items_to_the_right(
694 &mut self,
695 _: &CloseItemsToTheRight,
696 cx: &mut ViewContext<Self>,
697 ) -> Option<Task<Result<()>>> {
698 if self.items.is_empty() {
699 return None;
700 }
701 let active_item_id = self.items[self.active_item_index].id();
702 Some(self.close_items_to_the_right_by_id(active_item_id, cx))
703 }
704
705 pub fn close_items_to_the_right_by_id(
706 &mut self,
707 item_id: usize,
708 cx: &mut ViewContext<Self>,
709 ) -> Task<Result<()>> {
710 let item_ids: Vec<_> = self
711 .items()
712 .rev()
713 .take_while(|item| item.id() != item_id)
714 .map(|item| item.id())
715 .collect();
716 self.close_items(cx, move |item_id| item_ids.contains(&item_id))
717 }
718
719 pub fn close_all_items(
720 &mut self,
721 _: &CloseAllItems,
722 cx: &mut ViewContext<Self>,
723 ) -> Option<Task<Result<()>>> {
724 Some(self.close_items(cx, move |_| true))
725 }
726
727 pub fn close_items(
728 &mut self,
729 cx: &mut ViewContext<Pane>,
730 should_close: impl 'static + Fn(usize) -> bool,
731 ) -> Task<Result<()>> {
732 // Find the items to close.
733 let mut items_to_close = Vec::new();
734 for item in &self.items {
735 if should_close(item.id()) {
736 items_to_close.push(item.boxed_clone());
737 }
738 }
739
740 // If a buffer is open both in a singleton editor and in a multibuffer, make sure
741 // to focus the singleton buffer when prompting to save that buffer, as opposed
742 // to focusing the multibuffer, because this gives the user a more clear idea
743 // of what content they would be saving.
744 items_to_close.sort_by_key(|item| !item.is_singleton(cx));
745
746 let workspace = self.workspace.clone();
747 cx.spawn(|pane, mut cx| async move {
748 let mut saved_project_items_ids = HashSet::default();
749 for item in items_to_close.clone() {
750 // Find the item's current index and its set of project item models. Avoid
751 // storing these in advance, in case they have changed since this task
752 // was started.
753 let (item_ix, mut project_item_ids) = pane.read_with(&cx, |pane, cx| {
754 (pane.index_for_item(&*item), item.project_item_model_ids(cx))
755 })?;
756 let item_ix = if let Some(ix) = item_ix {
757 ix
758 } else {
759 continue;
760 };
761
762 // Check if this view has any project items that are not open anywhere else
763 // in the workspace, AND that the user has not already been prompted to save.
764 // If there are any such project entries, prompt the user to save this item.
765 let project = workspace.read_with(&cx, |workspace, cx| {
766 for item in workspace.items(cx) {
767 if !items_to_close
768 .iter()
769 .any(|item_to_close| item_to_close.id() == item.id())
770 {
771 let other_project_item_ids = item.project_item_model_ids(cx);
772 project_item_ids.retain(|id| !other_project_item_ids.contains(id));
773 }
774 }
775 workspace.project().clone()
776 })?;
777 let should_save = project_item_ids
778 .iter()
779 .any(|id| saved_project_items_ids.insert(*id));
780
781 if should_save
782 && !Self::save_item(project.clone(), &pane, item_ix, &*item, true, &mut cx)
783 .await?
784 {
785 break;
786 }
787
788 // Remove the item from the pane.
789 pane.update(&mut cx, |pane, cx| {
790 if let Some(item_ix) = pane.items.iter().position(|i| i.id() == item.id()) {
791 pane.remove_item(item_ix, false, cx);
792 }
793 })?;
794 }
795
796 pane.update(&mut cx, |_, cx| cx.notify())?;
797 Ok(())
798 })
799 }
800
801 pub fn remove_item(
802 &mut self,
803 item_index: usize,
804 activate_pane: bool,
805 cx: &mut ViewContext<Self>,
806 ) {
807 self.activation_history
808 .retain(|&history_entry| history_entry != self.items[item_index].id());
809
810 if item_index == self.active_item_index {
811 let index_to_activate = self
812 .activation_history
813 .pop()
814 .and_then(|last_activated_item| {
815 self.items.iter().enumerate().find_map(|(index, item)| {
816 (item.id() == last_activated_item).then_some(index)
817 })
818 })
819 // We didn't have a valid activation history entry, so fallback
820 // to activating the item to the left
821 .unwrap_or_else(|| item_index.min(self.items.len()).saturating_sub(1));
822
823 let should_activate = activate_pane || self.has_focus;
824 self.activate_item(index_to_activate, should_activate, should_activate, cx);
825 }
826
827 let item = self.items.remove(item_index);
828
829 cx.emit(Event::RemoveItem { item_id: item.id() });
830 if self.items.is_empty() {
831 item.deactivated(cx);
832 self.update_toolbar(cx);
833 cx.emit(Event::Remove);
834 }
835
836 if item_index < self.active_item_index {
837 self.active_item_index -= 1;
838 }
839
840 self.nav_history.set_mode(NavigationMode::ClosingItem);
841 item.deactivated(cx);
842 self.nav_history.set_mode(NavigationMode::Normal);
843
844 if let Some(path) = item.project_path(cx) {
845 let abs_path = self
846 .nav_history
847 .0
848 .borrow()
849 .paths_by_item
850 .get(&item.id())
851 .and_then(|(_, abs_path)| abs_path.clone());
852 self.nav_history
853 .0
854 .borrow_mut()
855 .paths_by_item
856 .insert(item.id(), (path, abs_path));
857 } else {
858 self.nav_history
859 .0
860 .borrow_mut()
861 .paths_by_item
862 .remove(&item.id());
863 }
864
865 if self.items.is_empty() && self.zoomed {
866 cx.emit(Event::ZoomOut);
867 }
868
869 cx.notify();
870 }
871
872 pub async fn save_item(
873 project: ModelHandle<Project>,
874 pane: &WeakViewHandle<Pane>,
875 item_ix: usize,
876 item: &dyn ItemHandle,
877 should_prompt_for_save: bool,
878 cx: &mut AsyncAppContext,
879 ) -> Result<bool> {
880 const CONFLICT_MESSAGE: &str =
881 "This file has changed on disk since you started editing it. Do you want to overwrite it?";
882 const DIRTY_MESSAGE: &str = "This file contains unsaved edits. Do you want to save it?";
883
884 let (has_conflict, is_dirty, can_save, is_singleton) = cx.read(|cx| {
885 (
886 item.has_conflict(cx),
887 item.is_dirty(cx),
888 item.can_save(cx),
889 item.is_singleton(cx),
890 )
891 });
892
893 if has_conflict && can_save {
894 let mut answer = pane.update(cx, |pane, cx| {
895 pane.activate_item(item_ix, true, true, cx);
896 cx.prompt(
897 PromptLevel::Warning,
898 CONFLICT_MESSAGE,
899 &["Overwrite", "Discard", "Cancel"],
900 )
901 })?;
902 match answer.next().await {
903 Some(0) => pane.update(cx, |_, cx| item.save(project, cx))?.await?,
904 Some(1) => pane.update(cx, |_, cx| item.reload(project, cx))?.await?,
905 _ => return Ok(false),
906 }
907 } else if is_dirty && (can_save || is_singleton) {
908 let will_autosave = cx.read(|cx| {
909 matches!(
910 settings::get::<WorkspaceSettings>(cx).autosave,
911 AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange
912 ) && Self::can_autosave_item(&*item, cx)
913 });
914 let should_save = if should_prompt_for_save && !will_autosave {
915 let mut answer = pane.update(cx, |pane, cx| {
916 pane.activate_item(item_ix, true, true, cx);
917 cx.prompt(
918 PromptLevel::Warning,
919 DIRTY_MESSAGE,
920 &["Save", "Don't Save", "Cancel"],
921 )
922 })?;
923 match answer.next().await {
924 Some(0) => true,
925 Some(1) => false,
926 _ => return Ok(false),
927 }
928 } else {
929 true
930 };
931
932 if should_save {
933 if can_save {
934 pane.update(cx, |_, cx| item.save(project, cx))?.await?;
935 } else if is_singleton {
936 let start_abs_path = project
937 .read_with(cx, |project, cx| {
938 let worktree = project.visible_worktrees(cx).next()?;
939 Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
940 })
941 .unwrap_or_else(|| Path::new("").into());
942
943 let mut abs_path = cx.update(|cx| cx.prompt_for_new_path(&start_abs_path));
944 if let Some(abs_path) = abs_path.next().await.flatten() {
945 pane.update(cx, |_, cx| item.save_as(project, abs_path, cx))?
946 .await?;
947 } else {
948 return Ok(false);
949 }
950 }
951 }
952 }
953 Ok(true)
954 }
955
956 fn can_autosave_item(item: &dyn ItemHandle, cx: &AppContext) -> bool {
957 let is_deleted = item.project_entry_ids(cx).is_empty();
958 item.is_dirty(cx) && !item.has_conflict(cx) && item.can_save(cx) && !is_deleted
959 }
960
961 pub fn autosave_item(
962 item: &dyn ItemHandle,
963 project: ModelHandle<Project>,
964 cx: &mut WindowContext,
965 ) -> Task<Result<()>> {
966 if Self::can_autosave_item(item, cx) {
967 item.save(project, cx)
968 } else {
969 Task::ready(Ok(()))
970 }
971 }
972
973 pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
974 if let Some(active_item) = self.active_item() {
975 cx.focus(active_item.as_any());
976 }
977 }
978
979 pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
980 cx.emit(Event::Split(direction));
981 }
982
983 fn deploy_split_menu(&mut self, cx: &mut ViewContext<Self>) {
984 self.tab_bar_context_menu.handle.update(cx, |menu, cx| {
985 menu.show(
986 Default::default(),
987 AnchorCorner::TopRight,
988 vec![
989 ContextMenuItem::action("Split Right", SplitRight),
990 ContextMenuItem::action("Split Left", SplitLeft),
991 ContextMenuItem::action("Split Up", SplitUp),
992 ContextMenuItem::action("Split Down", SplitDown),
993 ],
994 cx,
995 );
996 });
997
998 self.tab_bar_context_menu.kind = TabBarContextMenuKind::Split;
999 }
1000
1001 fn deploy_new_menu(&mut self, cx: &mut ViewContext<Self>) {
1002 self.tab_bar_context_menu.handle.update(cx, |menu, cx| {
1003 menu.show(
1004 Default::default(),
1005 AnchorCorner::TopRight,
1006 vec![
1007 ContextMenuItem::action("New File", NewFile),
1008 ContextMenuItem::action("New Terminal", NewTerminal),
1009 ContextMenuItem::action("New Search", NewSearch),
1010 ],
1011 cx,
1012 );
1013 });
1014
1015 self.tab_bar_context_menu.kind = TabBarContextMenuKind::New;
1016 }
1017
1018 fn deploy_tab_context_menu(
1019 &mut self,
1020 position: Vector2F,
1021 target_item_id: usize,
1022 cx: &mut ViewContext<Self>,
1023 ) {
1024 let active_item_id = self.items[self.active_item_index].id();
1025 let is_active_item = target_item_id == active_item_id;
1026 let target_pane = cx.weak_handle();
1027
1028 // 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
1029
1030 self.tab_context_menu.update(cx, |menu, cx| {
1031 menu.show(
1032 position,
1033 AnchorCorner::TopLeft,
1034 if is_active_item {
1035 vec![
1036 ContextMenuItem::action("Close Active Item", CloseActiveItem),
1037 ContextMenuItem::action("Close Inactive Items", CloseInactiveItems),
1038 ContextMenuItem::action("Close Clean Items", CloseCleanItems),
1039 ContextMenuItem::action("Close Items To The Left", CloseItemsToTheLeft),
1040 ContextMenuItem::action("Close Items To The Right", CloseItemsToTheRight),
1041 ContextMenuItem::action("Close All Items", CloseAllItems),
1042 ]
1043 } else {
1044 // 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.
1045 vec![
1046 ContextMenuItem::handler("Close Inactive Item", {
1047 let pane = target_pane.clone();
1048 move |cx| {
1049 if let Some(pane) = pane.upgrade(cx) {
1050 pane.update(cx, |pane, cx| {
1051 pane.close_item_by_id(target_item_id, cx)
1052 .detach_and_log_err(cx);
1053 })
1054 }
1055 }
1056 }),
1057 ContextMenuItem::action("Close Inactive Items", CloseInactiveItems),
1058 ContextMenuItem::action("Close Clean Items", CloseCleanItems),
1059 ContextMenuItem::handler("Close Items To The Left", {
1060 let pane = target_pane.clone();
1061 move |cx| {
1062 if let Some(pane) = pane.upgrade(cx) {
1063 pane.update(cx, |pane, cx| {
1064 pane.close_items_to_the_left_by_id(target_item_id, cx)
1065 .detach_and_log_err(cx);
1066 })
1067 }
1068 }
1069 }),
1070 ContextMenuItem::handler("Close Items To The Right", {
1071 let pane = target_pane.clone();
1072 move |cx| {
1073 if let Some(pane) = pane.upgrade(cx) {
1074 pane.update(cx, |pane, cx| {
1075 pane.close_items_to_the_right_by_id(target_item_id, cx)
1076 .detach_and_log_err(cx);
1077 })
1078 }
1079 }
1080 }),
1081 ContextMenuItem::action("Close All Items", CloseAllItems),
1082 ]
1083 },
1084 cx,
1085 );
1086 });
1087 }
1088
1089 pub fn toolbar(&self) -> &ViewHandle<Toolbar> {
1090 &self.toolbar
1091 }
1092
1093 pub fn handle_deleted_project_item(
1094 &mut self,
1095 entry_id: ProjectEntryId,
1096 cx: &mut ViewContext<Pane>,
1097 ) -> Option<()> {
1098 let (item_index_to_delete, item_id) = self.items().enumerate().find_map(|(i, item)| {
1099 if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
1100 Some((i, item.id()))
1101 } else {
1102 None
1103 }
1104 })?;
1105
1106 self.remove_item(item_index_to_delete, false, cx);
1107 self.nav_history.remove_item(item_id);
1108
1109 Some(())
1110 }
1111
1112 fn update_toolbar(&mut self, cx: &mut ViewContext<Self>) {
1113 let active_item = self
1114 .items
1115 .get(self.active_item_index)
1116 .map(|item| item.as_ref());
1117 self.toolbar.update(cx, |toolbar, cx| {
1118 toolbar.set_active_pane_item(active_item, cx);
1119 });
1120 }
1121
1122 fn render_tabs(&mut self, cx: &mut ViewContext<Self>) -> impl Element<Self> {
1123 let theme = theme::current(cx).clone();
1124
1125 let pane = cx.handle().downgrade();
1126 let autoscroll = if mem::take(&mut self.autoscroll) {
1127 Some(self.active_item_index)
1128 } else {
1129 None
1130 };
1131
1132 let pane_active = self.is_active;
1133
1134 enum Tabs {}
1135 let mut row = Flex::row().scrollable::<Tabs>(1, autoscroll, cx);
1136 for (ix, (item, detail)) in self
1137 .items
1138 .iter()
1139 .cloned()
1140 .zip(self.tab_details(cx))
1141 .enumerate()
1142 {
1143 let detail = if detail == 0 { None } else { Some(detail) };
1144 let tab_active = ix == self.active_item_index;
1145
1146 row.add_child({
1147 enum TabDragReceiver {}
1148 let mut receiver =
1149 dragged_item_receiver::<TabDragReceiver, _, _>(self, ix, ix, true, None, cx, {
1150 let item = item.clone();
1151 let pane = pane.clone();
1152 let detail = detail.clone();
1153
1154 let theme = theme::current(cx).clone();
1155 let mut tooltip_theme = theme.tooltip.clone();
1156 tooltip_theme.max_text_width = None;
1157 let tab_tooltip_text = item.tab_tooltip_text(cx).map(|a| a.to_string());
1158
1159 move |mouse_state, cx| {
1160 let tab_style =
1161 theme.workspace.tab_bar.tab_style(pane_active, tab_active);
1162 let hovered = mouse_state.hovered();
1163
1164 enum Tab {}
1165 let mouse_event_handler =
1166 MouseEventHandler::<Tab, Pane>::new(ix, cx, |_, cx| {
1167 Self::render_tab(
1168 &item,
1169 pane.clone(),
1170 ix == 0,
1171 detail,
1172 hovered,
1173 tab_style,
1174 cx,
1175 )
1176 })
1177 .on_down(MouseButton::Left, move |_, this, cx| {
1178 this.activate_item(ix, true, true, cx);
1179 })
1180 .on_click(MouseButton::Middle, {
1181 let item_id = item.id();
1182 move |_, pane, cx| {
1183 pane.close_item_by_id(item_id, cx).detach_and_log_err(cx);
1184 }
1185 })
1186 .on_down(
1187 MouseButton::Right,
1188 move |event, pane, cx| {
1189 pane.deploy_tab_context_menu(event.position, item.id(), cx);
1190 },
1191 );
1192
1193 if let Some(tab_tooltip_text) = tab_tooltip_text {
1194 mouse_event_handler
1195 .with_tooltip::<Self>(
1196 ix,
1197 tab_tooltip_text,
1198 None,
1199 tooltip_theme,
1200 cx,
1201 )
1202 .into_any()
1203 } else {
1204 mouse_event_handler.into_any()
1205 }
1206 }
1207 });
1208
1209 if !pane_active || !tab_active {
1210 receiver = receiver.with_cursor_style(CursorStyle::PointingHand);
1211 }
1212
1213 receiver.as_draggable(
1214 DraggedItem {
1215 handle: item,
1216 pane: pane.clone(),
1217 },
1218 {
1219 let theme = theme::current(cx).clone();
1220
1221 let detail = detail.clone();
1222 move |dragged_item: &DraggedItem, cx: &mut ViewContext<Workspace>| {
1223 let tab_style = &theme.workspace.tab_bar.dragged_tab;
1224 Self::render_dragged_tab(
1225 &dragged_item.handle,
1226 dragged_item.pane.clone(),
1227 false,
1228 detail,
1229 false,
1230 &tab_style,
1231 cx,
1232 )
1233 }
1234 },
1235 )
1236 })
1237 }
1238
1239 // Use the inactive tab style along with the current pane's active status to decide how to render
1240 // the filler
1241 let filler_index = self.items.len();
1242 let filler_style = theme.workspace.tab_bar.tab_style(pane_active, false);
1243 enum Filler {}
1244 row.add_child(
1245 dragged_item_receiver::<Filler, _, _>(self, 0, filler_index, true, None, cx, |_, _| {
1246 Empty::new()
1247 .contained()
1248 .with_style(filler_style.container)
1249 .with_border(filler_style.container.border)
1250 })
1251 .flex(1., true)
1252 .into_any_named("filler"),
1253 );
1254
1255 row
1256 }
1257
1258 fn tab_details(&self, cx: &AppContext) -> Vec<usize> {
1259 let mut tab_details = (0..self.items.len()).map(|_| 0).collect::<Vec<_>>();
1260
1261 let mut tab_descriptions = HashMap::default();
1262 let mut done = false;
1263 while !done {
1264 done = true;
1265
1266 // Store item indices by their tab description.
1267 for (ix, (item, detail)) in self.items.iter().zip(&tab_details).enumerate() {
1268 if let Some(description) = item.tab_description(*detail, cx) {
1269 if *detail == 0
1270 || Some(&description) != item.tab_description(detail - 1, cx).as_ref()
1271 {
1272 tab_descriptions
1273 .entry(description)
1274 .or_insert(Vec::new())
1275 .push(ix);
1276 }
1277 }
1278 }
1279
1280 // If two or more items have the same tab description, increase their level
1281 // of detail and try again.
1282 for (_, item_ixs) in tab_descriptions.drain() {
1283 if item_ixs.len() > 1 {
1284 done = false;
1285 for ix in item_ixs {
1286 tab_details[ix] += 1;
1287 }
1288 }
1289 }
1290 }
1291
1292 tab_details
1293 }
1294
1295 fn render_tab(
1296 item: &Box<dyn ItemHandle>,
1297 pane: WeakViewHandle<Pane>,
1298 first: bool,
1299 detail: Option<usize>,
1300 hovered: bool,
1301 tab_style: &theme::Tab,
1302 cx: &mut ViewContext<Self>,
1303 ) -> AnyElement<Self> {
1304 let title = item.tab_content(detail, &tab_style, cx);
1305 Self::render_tab_with_title(title, item, pane, first, hovered, tab_style, cx)
1306 }
1307
1308 fn render_dragged_tab(
1309 item: &Box<dyn ItemHandle>,
1310 pane: WeakViewHandle<Pane>,
1311 first: bool,
1312 detail: Option<usize>,
1313 hovered: bool,
1314 tab_style: &theme::Tab,
1315 cx: &mut ViewContext<Workspace>,
1316 ) -> AnyElement<Workspace> {
1317 let title = item.dragged_tab_content(detail, &tab_style, cx);
1318 Self::render_tab_with_title(title, item, pane, first, hovered, tab_style, cx)
1319 }
1320
1321 fn render_tab_with_title<T: View>(
1322 title: AnyElement<T>,
1323 item: &Box<dyn ItemHandle>,
1324 pane: WeakViewHandle<Pane>,
1325 first: bool,
1326 hovered: bool,
1327 tab_style: &theme::Tab,
1328 cx: &mut ViewContext<T>,
1329 ) -> AnyElement<T> {
1330 let mut container = tab_style.container.clone();
1331 if first {
1332 container.border.left = false;
1333 }
1334
1335 Flex::row()
1336 .with_child({
1337 let diameter = 7.0;
1338 let icon_color = if item.has_conflict(cx) {
1339 Some(tab_style.icon_conflict)
1340 } else if item.is_dirty(cx) {
1341 Some(tab_style.icon_dirty)
1342 } else {
1343 None
1344 };
1345
1346 Canvas::new(move |scene, bounds, _, _, _| {
1347 if let Some(color) = icon_color {
1348 let square = RectF::new(bounds.origin(), vec2f(diameter, diameter));
1349 scene.push_quad(Quad {
1350 bounds: square,
1351 background: Some(color),
1352 border: Default::default(),
1353 corner_radius: diameter / 2.,
1354 });
1355 }
1356 })
1357 .constrained()
1358 .with_width(diameter)
1359 .with_height(diameter)
1360 .aligned()
1361 })
1362 .with_child(title.aligned().contained().with_style(ContainerStyle {
1363 margin: Margin {
1364 left: tab_style.spacing,
1365 right: tab_style.spacing,
1366 ..Default::default()
1367 },
1368 ..Default::default()
1369 }))
1370 .with_child(
1371 if hovered {
1372 let item_id = item.id();
1373 enum TabCloseButton {}
1374 let icon = Svg::new("icons/x_mark_8.svg");
1375 MouseEventHandler::<TabCloseButton, _>::new(item_id, cx, |mouse_state, _| {
1376 if mouse_state.hovered() {
1377 icon.with_color(tab_style.icon_close_active)
1378 } else {
1379 icon.with_color(tab_style.icon_close)
1380 }
1381 })
1382 .with_padding(Padding::uniform(4.))
1383 .with_cursor_style(CursorStyle::PointingHand)
1384 .on_click(MouseButton::Left, {
1385 let pane = pane.clone();
1386 move |_, _, cx| {
1387 let pane = pane.clone();
1388 cx.window_context().defer(move |cx| {
1389 if let Some(pane) = pane.upgrade(cx) {
1390 pane.update(cx, |pane, cx| {
1391 pane.close_item_by_id(item_id, cx).detach_and_log_err(cx);
1392 });
1393 }
1394 });
1395 }
1396 })
1397 .into_any_named("close-tab-icon")
1398 .constrained()
1399 } else {
1400 Empty::new().constrained()
1401 }
1402 .with_width(tab_style.close_icon_width)
1403 .aligned(),
1404 )
1405 .contained()
1406 .with_style(container)
1407 .constrained()
1408 .with_height(tab_style.height)
1409 .into_any()
1410 }
1411
1412 pub fn render_tab_bar_button<F: 'static + Fn(&mut Pane, &mut EventContext<Pane>)>(
1413 index: usize,
1414 icon: &'static str,
1415 tooltip: Option<(String, Option<Box<dyn Action>>)>,
1416 cx: &mut ViewContext<Pane>,
1417 on_click: F,
1418 context_menu: Option<ViewHandle<ContextMenu>>,
1419 ) -> AnyElement<Pane> {
1420 enum TabBarButton {}
1421
1422 let mut button = MouseEventHandler::<TabBarButton, _>::new(index, cx, |mouse_state, cx| {
1423 let theme = &settings::get::<ThemeSettings>(cx).theme.workspace.tab_bar;
1424 let style = theme.pane_button.style_for(mouse_state, false);
1425 Svg::new(icon)
1426 .with_color(style.color)
1427 .constrained()
1428 .with_width(style.icon_width)
1429 .aligned()
1430 .constrained()
1431 .with_width(style.button_width)
1432 .with_height(style.button_width)
1433 })
1434 .with_cursor_style(CursorStyle::PointingHand)
1435 .on_click(MouseButton::Left, move |_, pane, cx| on_click(pane, cx))
1436 .into_any();
1437 if let Some((tooltip, action)) = tooltip {
1438 let tooltip_style = settings::get::<ThemeSettings>(cx).theme.tooltip.clone();
1439 button = button
1440 .with_tooltip::<TabBarButton>(index, tooltip, action, tooltip_style, cx)
1441 .into_any();
1442 }
1443
1444 Stack::new()
1445 .with_child(button)
1446 .with_children(
1447 context_menu.map(|menu| ChildView::new(&menu, cx).aligned().bottom().right()),
1448 )
1449 .flex(1., false)
1450 .into_any_named("tab bar button")
1451 }
1452
1453 fn render_blank_pane(&self, theme: &Theme, _cx: &mut ViewContext<Self>) -> AnyElement<Self> {
1454 let background = theme.workspace.background;
1455 Empty::new()
1456 .contained()
1457 .with_background_color(background)
1458 .into_any()
1459 }
1460
1461 pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
1462 self.zoomed = zoomed;
1463 cx.notify();
1464 }
1465
1466 pub fn is_zoomed(&self) -> bool {
1467 self.zoomed
1468 }
1469}
1470
1471impl Entity for Pane {
1472 type Event = Event;
1473}
1474
1475impl View for Pane {
1476 fn ui_name() -> &'static str {
1477 "Pane"
1478 }
1479
1480 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
1481 enum MouseNavigationHandler {}
1482
1483 MouseEventHandler::<MouseNavigationHandler, _>::new(0, cx, |_, cx| {
1484 let active_item_index = self.active_item_index;
1485
1486 if let Some(active_item) = self.active_item() {
1487 Flex::column()
1488 .with_child({
1489 let theme = theme::current(cx).clone();
1490
1491 let mut stack = Stack::new();
1492
1493 enum TabBarEventHandler {}
1494 stack.add_child(
1495 MouseEventHandler::<TabBarEventHandler, _>::new(0, cx, |_, _| {
1496 Empty::new()
1497 .contained()
1498 .with_style(theme.workspace.tab_bar.container)
1499 })
1500 .on_down(
1501 MouseButton::Left,
1502 move |_, this, cx| {
1503 this.activate_item(active_item_index, true, true, cx);
1504 },
1505 ),
1506 );
1507
1508 let mut tab_row = Flex::row()
1509 .with_child(self.render_tabs(cx).flex(1., true).into_any_named("tabs"));
1510
1511 if self.is_active {
1512 let render_tab_bar_buttons = self.render_tab_bar_buttons.clone();
1513 tab_row.add_child(
1514 (render_tab_bar_buttons)(self, cx)
1515 .contained()
1516 .with_style(theme.workspace.tab_bar.pane_button_container)
1517 .flex(1., false)
1518 .into_any(),
1519 )
1520 }
1521
1522 stack.add_child(tab_row);
1523 stack
1524 .constrained()
1525 .with_height(theme.workspace.tab_bar.height)
1526 .flex(1., false)
1527 .into_any_named("tab bar")
1528 })
1529 .with_child({
1530 enum PaneContentTabDropTarget {}
1531 dragged_item_receiver::<PaneContentTabDropTarget, _, _>(
1532 self,
1533 0,
1534 self.active_item_index + 1,
1535 !self.can_split,
1536 if self.can_split { Some(100.) } else { None },
1537 cx,
1538 {
1539 let toolbar = self.toolbar.clone();
1540 let toolbar_hidden = toolbar.read(cx).hidden();
1541 move |_, cx| {
1542 Flex::column()
1543 .with_children(
1544 (!toolbar_hidden)
1545 .then(|| ChildView::new(&toolbar, cx).expanded()),
1546 )
1547 .with_child(
1548 ChildView::new(active_item.as_any(), cx).flex(1., true),
1549 )
1550 }
1551 },
1552 )
1553 .flex(1., true)
1554 })
1555 .with_child(ChildView::new(&self.tab_context_menu, cx))
1556 .into_any()
1557 } else {
1558 enum EmptyPane {}
1559 let theme = theme::current(cx).clone();
1560
1561 dragged_item_receiver::<EmptyPane, _, _>(self, 0, 0, false, None, cx, |_, cx| {
1562 self.render_blank_pane(&theme, cx)
1563 })
1564 .on_down(MouseButton::Left, |_, _, cx| {
1565 cx.focus_parent();
1566 })
1567 .into_any()
1568 }
1569 })
1570 .on_down(
1571 MouseButton::Navigate(NavigationDirection::Back),
1572 move |_, pane, cx| {
1573 if let Some(workspace) = pane.workspace.upgrade(cx) {
1574 let pane = cx.weak_handle();
1575 cx.window_context().defer(move |cx| {
1576 workspace.update(cx, |workspace, cx| {
1577 workspace.go_back(pane, cx).detach_and_log_err(cx)
1578 })
1579 })
1580 }
1581 },
1582 )
1583 .on_down(MouseButton::Navigate(NavigationDirection::Forward), {
1584 move |_, pane, cx| {
1585 if let Some(workspace) = pane.workspace.upgrade(cx) {
1586 let pane = cx.weak_handle();
1587 cx.window_context().defer(move |cx| {
1588 workspace.update(cx, |workspace, cx| {
1589 workspace.go_forward(pane, cx).detach_and_log_err(cx)
1590 })
1591 })
1592 }
1593 }
1594 })
1595 .into_any_named("pane")
1596 }
1597
1598 fn focus_in(&mut self, focused: AnyViewHandle, cx: &mut ViewContext<Self>) {
1599 if !self.has_focus {
1600 self.has_focus = true;
1601 cx.emit(Event::Focus);
1602 }
1603
1604 self.toolbar.update(cx, |toolbar, cx| {
1605 toolbar.pane_focus_update(true, cx);
1606 });
1607
1608 if let Some(active_item) = self.active_item() {
1609 if cx.is_self_focused() {
1610 // Pane was focused directly. We need to either focus a view inside the active item,
1611 // or focus the active item itself
1612 if let Some(weak_last_focused_view) =
1613 self.last_focused_view_by_item.get(&active_item.id())
1614 {
1615 if let Some(last_focused_view) = weak_last_focused_view.upgrade(cx) {
1616 cx.focus(&last_focused_view);
1617 return;
1618 } else {
1619 self.last_focused_view_by_item.remove(&active_item.id());
1620 }
1621 }
1622
1623 cx.focus(active_item.as_any());
1624 } else if focused != self.tab_bar_context_menu.handle {
1625 self.last_focused_view_by_item
1626 .insert(active_item.id(), focused.downgrade());
1627 }
1628 }
1629 }
1630
1631 fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
1632 self.has_focus = false;
1633 self.toolbar.update(cx, |toolbar, cx| {
1634 toolbar.pane_focus_update(false, cx);
1635 });
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}