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