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