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