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 {
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 cx.notify();
987 }
988
989 pub async fn save_item(
990 project: ModelHandle<Project>,
991 pane: &WeakViewHandle<Pane>,
992 item_ix: usize,
993 item: &dyn ItemHandle,
994 should_prompt_for_save: bool,
995 cx: &mut AsyncAppContext,
996 ) -> Result<bool> {
997 const CONFLICT_MESSAGE: &str =
998 "This file has changed on disk since you started editing it. Do you want to overwrite it?";
999 const DIRTY_MESSAGE: &str = "This file contains unsaved edits. Do you want to save it?";
1000
1001 let (has_conflict, is_dirty, can_save, is_singleton) = cx.read(|cx| {
1002 (
1003 item.has_conflict(cx),
1004 item.is_dirty(cx),
1005 item.can_save(cx),
1006 item.is_singleton(cx),
1007 )
1008 });
1009
1010 if has_conflict && can_save {
1011 let mut answer = pane.update(cx, |pane, cx| {
1012 pane.activate_item(item_ix, true, true, cx);
1013 cx.prompt(
1014 PromptLevel::Warning,
1015 CONFLICT_MESSAGE,
1016 &["Overwrite", "Discard", "Cancel"],
1017 )
1018 })?;
1019 match answer.next().await {
1020 Some(0) => pane.update(cx, |_, cx| item.save(project, cx))?.await?,
1021 Some(1) => pane.update(cx, |_, cx| item.reload(project, cx))?.await?,
1022 _ => return Ok(false),
1023 }
1024 } else if is_dirty && (can_save || is_singleton) {
1025 let will_autosave = cx.read(|cx| {
1026 matches!(
1027 settings::get::<WorkspaceSettings>(cx).autosave,
1028 AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange
1029 ) && Self::can_autosave_item(&*item, cx)
1030 });
1031 let should_save = if should_prompt_for_save && !will_autosave {
1032 let mut answer = pane.update(cx, |pane, cx| {
1033 pane.activate_item(item_ix, true, true, cx);
1034 cx.prompt(
1035 PromptLevel::Warning,
1036 DIRTY_MESSAGE,
1037 &["Save", "Don't Save", "Cancel"],
1038 )
1039 })?;
1040 match answer.next().await {
1041 Some(0) => true,
1042 Some(1) => false,
1043 _ => return Ok(false),
1044 }
1045 } else {
1046 true
1047 };
1048
1049 if should_save {
1050 if can_save {
1051 pane.update(cx, |_, cx| item.save(project, cx))?.await?;
1052 } else if is_singleton {
1053 let start_abs_path = project
1054 .read_with(cx, |project, cx| {
1055 let worktree = project.visible_worktrees(cx).next()?;
1056 Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
1057 })
1058 .unwrap_or_else(|| Path::new("").into());
1059
1060 let mut abs_path = cx.update(|cx| cx.prompt_for_new_path(&start_abs_path));
1061 if let Some(abs_path) = abs_path.next().await.flatten() {
1062 pane.update(cx, |_, cx| item.save_as(project, abs_path, cx))?
1063 .await?;
1064 } else {
1065 return Ok(false);
1066 }
1067 }
1068 }
1069 }
1070 Ok(true)
1071 }
1072
1073 fn can_autosave_item(item: &dyn ItemHandle, cx: &AppContext) -> bool {
1074 let is_deleted = item.project_entry_ids(cx).is_empty();
1075 item.is_dirty(cx) && !item.has_conflict(cx) && item.can_save(cx) && !is_deleted
1076 }
1077
1078 pub fn autosave_item(
1079 item: &dyn ItemHandle,
1080 project: ModelHandle<Project>,
1081 cx: &mut WindowContext,
1082 ) -> Task<Result<()>> {
1083 if Self::can_autosave_item(item, cx) {
1084 item.save(project, cx)
1085 } else {
1086 Task::ready(Ok(()))
1087 }
1088 }
1089
1090 pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
1091 if let Some(active_item) = self.active_item() {
1092 cx.focus(active_item.as_any());
1093 }
1094 }
1095
1096 pub fn move_item(
1097 workspace: &mut Workspace,
1098 from: ViewHandle<Pane>,
1099 to: ViewHandle<Pane>,
1100 item_id_to_move: usize,
1101 destination_index: usize,
1102 cx: &mut ViewContext<Workspace>,
1103 ) {
1104 let item_to_move = from
1105 .read(cx)
1106 .items()
1107 .enumerate()
1108 .find(|(_, item_handle)| item_handle.id() == item_id_to_move);
1109
1110 if item_to_move.is_none() {
1111 log::warn!("Tried to move item handle which was not in `from` pane. Maybe tab was closed during drop");
1112 return;
1113 }
1114 let (item_ix, item_handle) = item_to_move.unwrap();
1115 let item_handle = item_handle.clone();
1116
1117 if from != to {
1118 // Close item from previous pane
1119 from.update(cx, |from, cx| {
1120 from.remove_item(item_ix, false, cx);
1121 });
1122 }
1123
1124 // This automatically removes duplicate items in the pane
1125 Pane::add_item(
1126 workspace,
1127 &to,
1128 item_handle,
1129 true,
1130 true,
1131 Some(destination_index),
1132 cx,
1133 );
1134
1135 cx.focus(&to);
1136 }
1137
1138 pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
1139 cx.emit(Event::Split(direction));
1140 }
1141
1142 fn deploy_split_menu(&mut self, cx: &mut ViewContext<Self>) {
1143 self.tab_bar_context_menu.handle.update(cx, |menu, cx| {
1144 menu.show(
1145 Default::default(),
1146 AnchorCorner::TopRight,
1147 vec![
1148 ContextMenuItem::action("Split Right", SplitRight),
1149 ContextMenuItem::action("Split Left", SplitLeft),
1150 ContextMenuItem::action("Split Up", SplitUp),
1151 ContextMenuItem::action("Split Down", SplitDown),
1152 ],
1153 cx,
1154 );
1155 });
1156
1157 self.tab_bar_context_menu.kind = TabBarContextMenuKind::Split;
1158 }
1159
1160 fn deploy_new_menu(&mut self, cx: &mut ViewContext<Self>) {
1161 self.tab_bar_context_menu.handle.update(cx, |menu, cx| {
1162 menu.show(
1163 Default::default(),
1164 AnchorCorner::TopRight,
1165 vec![
1166 ContextMenuItem::action("New File", NewFile),
1167 ContextMenuItem::action("New Terminal", NewTerminal),
1168 ContextMenuItem::action("New Search", NewSearch),
1169 ],
1170 cx,
1171 );
1172 });
1173
1174 self.tab_bar_context_menu.kind = TabBarContextMenuKind::New;
1175 }
1176
1177 fn deploy_tab_context_menu(
1178 &mut self,
1179 position: Vector2F,
1180 target_item_id: usize,
1181 cx: &mut ViewContext<Self>,
1182 ) {
1183 let active_item_id = self.items[self.active_item_index].id();
1184 let is_active_item = target_item_id == active_item_id;
1185 let target_pane = cx.weak_handle();
1186
1187 // 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
1188
1189 self.tab_context_menu.update(cx, |menu, cx| {
1190 menu.show(
1191 position,
1192 AnchorCorner::TopLeft,
1193 if is_active_item {
1194 vec![
1195 ContextMenuItem::action("Close Active Item", CloseActiveItem),
1196 ContextMenuItem::action("Close Inactive Items", CloseInactiveItems),
1197 ContextMenuItem::action("Close Clean Items", CloseCleanItems),
1198 ContextMenuItem::action("Close Items To The Left", CloseItemsToTheLeft),
1199 ContextMenuItem::action("Close Items To The Right", CloseItemsToTheRight),
1200 ContextMenuItem::action("Close All Items", CloseAllItems),
1201 ]
1202 } else {
1203 // 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.
1204 vec![
1205 ContextMenuItem::handler("Close Inactive Item", {
1206 let pane = target_pane.clone();
1207 move |cx| {
1208 if let Some(pane) = pane.upgrade(cx) {
1209 pane.update(cx, |pane, cx| {
1210 pane.close_item_by_id(target_item_id, cx)
1211 .detach_and_log_err(cx);
1212 })
1213 }
1214 }
1215 }),
1216 ContextMenuItem::action("Close Inactive Items", CloseInactiveItems),
1217 ContextMenuItem::action("Close Clean Items", CloseCleanItems),
1218 ContextMenuItem::handler("Close Items To The Left", {
1219 let pane = target_pane.clone();
1220 move |cx| {
1221 if let Some(pane) = pane.upgrade(cx) {
1222 pane.update(cx, |pane, cx| {
1223 pane.close_items_to_the_left_by_id(target_item_id, cx)
1224 .detach_and_log_err(cx);
1225 })
1226 }
1227 }
1228 }),
1229 ContextMenuItem::handler("Close Items To The Right", {
1230 let pane = target_pane.clone();
1231 move |cx| {
1232 if let Some(pane) = pane.upgrade(cx) {
1233 pane.update(cx, |pane, cx| {
1234 pane.close_items_to_the_right_by_id(target_item_id, cx)
1235 .detach_and_log_err(cx);
1236 })
1237 }
1238 }
1239 }),
1240 ContextMenuItem::action("Close All Items", CloseAllItems),
1241 ]
1242 },
1243 cx,
1244 );
1245 });
1246 }
1247
1248 pub fn toolbar(&self) -> &ViewHandle<Toolbar> {
1249 &self.toolbar
1250 }
1251
1252 pub fn handle_deleted_project_item(
1253 &mut self,
1254 entry_id: ProjectEntryId,
1255 cx: &mut ViewContext<Pane>,
1256 ) -> Option<()> {
1257 let (item_index_to_delete, item_id) = self.items().enumerate().find_map(|(i, item)| {
1258 if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
1259 Some((i, item.id()))
1260 } else {
1261 None
1262 }
1263 })?;
1264
1265 self.remove_item(item_index_to_delete, false, cx);
1266 self.nav_history.borrow_mut().remove_item(item_id);
1267
1268 Some(())
1269 }
1270
1271 fn update_toolbar(&mut self, cx: &mut ViewContext<Self>) {
1272 let active_item = self
1273 .items
1274 .get(self.active_item_index)
1275 .map(|item| item.as_ref());
1276 self.toolbar.update(cx, |toolbar, cx| {
1277 toolbar.set_active_pane_item(active_item, cx);
1278 });
1279 }
1280
1281 fn render_tabs(&mut self, cx: &mut ViewContext<Self>) -> impl Element<Self> {
1282 let theme = theme::current(cx).clone();
1283
1284 let pane = cx.handle().downgrade();
1285 let autoscroll = if mem::take(&mut self.autoscroll) {
1286 Some(self.active_item_index)
1287 } else {
1288 None
1289 };
1290
1291 let pane_active = self.is_active;
1292
1293 enum Tabs {}
1294 let mut row = Flex::row().scrollable::<Tabs>(1, autoscroll, cx);
1295 for (ix, (item, detail)) in self
1296 .items
1297 .iter()
1298 .cloned()
1299 .zip(self.tab_details(cx))
1300 .enumerate()
1301 {
1302 let detail = if detail == 0 { None } else { Some(detail) };
1303 let tab_active = ix == self.active_item_index;
1304
1305 row.add_child({
1306 enum TabDragReceiver {}
1307 let mut receiver =
1308 dragged_item_receiver::<TabDragReceiver, _, _>(self, ix, ix, true, None, cx, {
1309 let item = item.clone();
1310 let pane = pane.clone();
1311 let detail = detail.clone();
1312
1313 let theme = theme::current(cx).clone();
1314 let mut tooltip_theme = theme.tooltip.clone();
1315 tooltip_theme.max_text_width = None;
1316 let tab_tooltip_text = item.tab_tooltip_text(cx).map(|a| a.to_string());
1317
1318 move |mouse_state, cx| {
1319 let tab_style =
1320 theme.workspace.tab_bar.tab_style(pane_active, tab_active);
1321 let hovered = mouse_state.hovered();
1322
1323 enum Tab {}
1324 let mouse_event_handler =
1325 MouseEventHandler::<Tab, Pane>::new(ix, cx, |_, cx| {
1326 Self::render_tab(
1327 &item,
1328 pane.clone(),
1329 ix == 0,
1330 detail,
1331 hovered,
1332 tab_style,
1333 cx,
1334 )
1335 })
1336 .on_down(MouseButton::Left, move |_, this, cx| {
1337 this.activate_item(ix, true, true, cx);
1338 })
1339 .on_click(MouseButton::Middle, {
1340 let item_id = item.id();
1341 move |_, pane, cx| {
1342 pane.close_item_by_id(item_id, cx).detach_and_log_err(cx);
1343 }
1344 })
1345 .on_down(
1346 MouseButton::Right,
1347 move |event, pane, cx| {
1348 pane.deploy_tab_context_menu(event.position, item.id(), cx);
1349 },
1350 );
1351
1352 if let Some(tab_tooltip_text) = tab_tooltip_text {
1353 mouse_event_handler
1354 .with_tooltip::<Self>(
1355 ix,
1356 tab_tooltip_text,
1357 None,
1358 tooltip_theme,
1359 cx,
1360 )
1361 .into_any()
1362 } else {
1363 mouse_event_handler.into_any()
1364 }
1365 }
1366 });
1367
1368 if !pane_active || !tab_active {
1369 receiver = receiver.with_cursor_style(CursorStyle::PointingHand);
1370 }
1371
1372 receiver.as_draggable(
1373 DraggedItem {
1374 handle: item,
1375 pane: pane.clone(),
1376 },
1377 {
1378 let theme = theme::current(cx).clone();
1379
1380 let detail = detail.clone();
1381 move |dragged_item: &DraggedItem, cx: &mut ViewContext<Workspace>| {
1382 let tab_style = &theme.workspace.tab_bar.dragged_tab;
1383 Self::render_dragged_tab(
1384 &dragged_item.handle,
1385 dragged_item.pane.clone(),
1386 false,
1387 detail,
1388 false,
1389 &tab_style,
1390 cx,
1391 )
1392 }
1393 },
1394 )
1395 })
1396 }
1397
1398 // Use the inactive tab style along with the current pane's active status to decide how to render
1399 // the filler
1400 let filler_index = self.items.len();
1401 let filler_style = theme.workspace.tab_bar.tab_style(pane_active, false);
1402 enum Filler {}
1403 row.add_child(
1404 dragged_item_receiver::<Filler, _, _>(self, 0, filler_index, true, None, cx, |_, _| {
1405 Empty::new()
1406 .contained()
1407 .with_style(filler_style.container)
1408 .with_border(filler_style.container.border)
1409 })
1410 .flex(1., true)
1411 .into_any_named("filler"),
1412 );
1413
1414 row
1415 }
1416
1417 fn tab_details(&self, cx: &AppContext) -> Vec<usize> {
1418 let mut tab_details = (0..self.items.len()).map(|_| 0).collect::<Vec<_>>();
1419
1420 let mut tab_descriptions = HashMap::default();
1421 let mut done = false;
1422 while !done {
1423 done = true;
1424
1425 // Store item indices by their tab description.
1426 for (ix, (item, detail)) in self.items.iter().zip(&tab_details).enumerate() {
1427 if let Some(description) = item.tab_description(*detail, cx) {
1428 if *detail == 0
1429 || Some(&description) != item.tab_description(detail - 1, cx).as_ref()
1430 {
1431 tab_descriptions
1432 .entry(description)
1433 .or_insert(Vec::new())
1434 .push(ix);
1435 }
1436 }
1437 }
1438
1439 // If two or more items have the same tab description, increase their level
1440 // of detail and try again.
1441 for (_, item_ixs) in tab_descriptions.drain() {
1442 if item_ixs.len() > 1 {
1443 done = false;
1444 for ix in item_ixs {
1445 tab_details[ix] += 1;
1446 }
1447 }
1448 }
1449 }
1450
1451 tab_details
1452 }
1453
1454 fn render_tab(
1455 item: &Box<dyn ItemHandle>,
1456 pane: WeakViewHandle<Pane>,
1457 first: bool,
1458 detail: Option<usize>,
1459 hovered: bool,
1460 tab_style: &theme::Tab,
1461 cx: &mut ViewContext<Self>,
1462 ) -> AnyElement<Self> {
1463 let title = item.tab_content(detail, &tab_style, cx);
1464 Self::render_tab_with_title(title, item, pane, first, hovered, tab_style, cx)
1465 }
1466
1467 fn render_dragged_tab(
1468 item: &Box<dyn ItemHandle>,
1469 pane: WeakViewHandle<Pane>,
1470 first: bool,
1471 detail: Option<usize>,
1472 hovered: bool,
1473 tab_style: &theme::Tab,
1474 cx: &mut ViewContext<Workspace>,
1475 ) -> AnyElement<Workspace> {
1476 let title = item.dragged_tab_content(detail, &tab_style, cx);
1477 Self::render_tab_with_title(title, item, pane, first, hovered, tab_style, cx)
1478 }
1479
1480 fn render_tab_with_title<T: View>(
1481 title: AnyElement<T>,
1482 item: &Box<dyn ItemHandle>,
1483 pane: WeakViewHandle<Pane>,
1484 first: bool,
1485 hovered: bool,
1486 tab_style: &theme::Tab,
1487 cx: &mut ViewContext<T>,
1488 ) -> AnyElement<T> {
1489 let mut container = tab_style.container.clone();
1490 if first {
1491 container.border.left = false;
1492 }
1493
1494 Flex::row()
1495 .with_child({
1496 let diameter = 7.0;
1497 let icon_color = if item.has_conflict(cx) {
1498 Some(tab_style.icon_conflict)
1499 } else if item.is_dirty(cx) {
1500 Some(tab_style.icon_dirty)
1501 } else {
1502 None
1503 };
1504
1505 Canvas::new(move |scene, bounds, _, _, _| {
1506 if let Some(color) = icon_color {
1507 let square = RectF::new(bounds.origin(), vec2f(diameter, diameter));
1508 scene.push_quad(Quad {
1509 bounds: square,
1510 background: Some(color),
1511 border: Default::default(),
1512 corner_radius: diameter / 2.,
1513 });
1514 }
1515 })
1516 .constrained()
1517 .with_width(diameter)
1518 .with_height(diameter)
1519 .aligned()
1520 })
1521 .with_child(title.aligned().contained().with_style(ContainerStyle {
1522 margin: Margin {
1523 left: tab_style.spacing,
1524 right: tab_style.spacing,
1525 ..Default::default()
1526 },
1527 ..Default::default()
1528 }))
1529 .with_child(
1530 if hovered {
1531 let item_id = item.id();
1532 enum TabCloseButton {}
1533 let icon = Svg::new("icons/x_mark_8.svg");
1534 MouseEventHandler::<TabCloseButton, _>::new(item_id, cx, |mouse_state, _| {
1535 if mouse_state.hovered() {
1536 icon.with_color(tab_style.icon_close_active)
1537 } else {
1538 icon.with_color(tab_style.icon_close)
1539 }
1540 })
1541 .with_padding(Padding::uniform(4.))
1542 .with_cursor_style(CursorStyle::PointingHand)
1543 .on_click(MouseButton::Left, {
1544 let pane = pane.clone();
1545 move |_, _, cx| {
1546 let pane = pane.clone();
1547 cx.window_context().defer(move |cx| {
1548 if let Some(pane) = pane.upgrade(cx) {
1549 pane.update(cx, |pane, cx| {
1550 pane.close_item_by_id(item_id, cx).detach_and_log_err(cx);
1551 });
1552 }
1553 });
1554 }
1555 })
1556 .into_any_named("close-tab-icon")
1557 .constrained()
1558 } else {
1559 Empty::new().constrained()
1560 }
1561 .with_width(tab_style.close_icon_width)
1562 .aligned(),
1563 )
1564 .contained()
1565 .with_style(container)
1566 .constrained()
1567 .with_height(tab_style.height)
1568 .into_any()
1569 }
1570
1571 pub fn render_tab_bar_button<F: 'static + Fn(&mut Pane, &mut EventContext<Pane>)>(
1572 index: usize,
1573 icon: &'static str,
1574 cx: &mut ViewContext<Pane>,
1575 on_click: F,
1576 context_menu: Option<ViewHandle<ContextMenu>>,
1577 ) -> AnyElement<Pane> {
1578 enum TabBarButton {}
1579
1580 Stack::new()
1581 .with_child(
1582 MouseEventHandler::<TabBarButton, _>::new(index, cx, |mouse_state, cx| {
1583 let theme = &settings::get::<ThemeSettings>(cx).theme.workspace.tab_bar;
1584 let style = theme.pane_button.style_for(mouse_state, false);
1585 Svg::new(icon)
1586 .with_color(style.color)
1587 .constrained()
1588 .with_width(style.icon_width)
1589 .aligned()
1590 .constrained()
1591 .with_width(style.button_width)
1592 .with_height(style.button_width)
1593 })
1594 .with_cursor_style(CursorStyle::PointingHand)
1595 .on_click(MouseButton::Left, move |_, pane, cx| on_click(pane, cx)),
1596 )
1597 .with_children(
1598 context_menu.map(|menu| ChildView::new(&menu, cx).aligned().bottom().right()),
1599 )
1600 .flex(1., false)
1601 .into_any_named("tab bar button")
1602 }
1603
1604 fn render_blank_pane(&self, theme: &Theme, _cx: &mut ViewContext<Self>) -> AnyElement<Self> {
1605 let background = theme.workspace.background;
1606 Empty::new()
1607 .contained()
1608 .with_background_color(background)
1609 .into_any()
1610 }
1611
1612 pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
1613 self.zoomed = zoomed;
1614 cx.notify();
1615 }
1616
1617 pub fn is_zoomed(&self) -> bool {
1618 self.zoomed
1619 }
1620}
1621
1622impl Entity for Pane {
1623 type Event = Event;
1624}
1625
1626impl View for Pane {
1627 fn ui_name() -> &'static str {
1628 "Pane"
1629 }
1630
1631 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
1632 enum MouseNavigationHandler {}
1633
1634 MouseEventHandler::<MouseNavigationHandler, _>::new(0, cx, |_, cx| {
1635 let active_item_index = self.active_item_index;
1636
1637 if let Some(active_item) = self.active_item() {
1638 Flex::column()
1639 .with_child({
1640 let theme = theme::current(cx).clone();
1641
1642 let mut stack = Stack::new();
1643
1644 enum TabBarEventHandler {}
1645 stack.add_child(
1646 MouseEventHandler::<TabBarEventHandler, _>::new(0, cx, |_, _| {
1647 Empty::new()
1648 .contained()
1649 .with_style(theme.workspace.tab_bar.container)
1650 })
1651 .on_down(
1652 MouseButton::Left,
1653 move |_, this, cx| {
1654 this.activate_item(active_item_index, true, true, cx);
1655 },
1656 ),
1657 );
1658
1659 let mut tab_row = Flex::row()
1660 .with_child(self.render_tabs(cx).flex(1., true).into_any_named("tabs"));
1661
1662 if self.is_active {
1663 let render_tab_bar_buttons = self.render_tab_bar_buttons.clone();
1664 tab_row.add_child(
1665 (render_tab_bar_buttons)(self, cx)
1666 .contained()
1667 .with_style(theme.workspace.tab_bar.pane_button_container)
1668 .flex(1., false)
1669 .into_any(),
1670 )
1671 }
1672
1673 stack.add_child(tab_row);
1674 stack
1675 .constrained()
1676 .with_height(theme.workspace.tab_bar.height)
1677 .flex(1., false)
1678 .into_any_named("tab bar")
1679 })
1680 .with_child({
1681 enum PaneContentTabDropTarget {}
1682 dragged_item_receiver::<PaneContentTabDropTarget, _, _>(
1683 self,
1684 0,
1685 self.active_item_index + 1,
1686 !self.can_split,
1687 if self.can_split { Some(100.) } else { None },
1688 cx,
1689 {
1690 let toolbar = self.toolbar.clone();
1691 let toolbar_hidden = toolbar.read(cx).hidden();
1692 move |_, cx| {
1693 Flex::column()
1694 .with_children(
1695 (!toolbar_hidden)
1696 .then(|| ChildView::new(&toolbar, cx).expanded()),
1697 )
1698 .with_child(
1699 ChildView::new(active_item.as_any(), cx).flex(1., true),
1700 )
1701 }
1702 },
1703 )
1704 .flex(1., true)
1705 })
1706 .with_child(ChildView::new(&self.tab_context_menu, cx))
1707 .into_any()
1708 } else {
1709 enum EmptyPane {}
1710 let theme = theme::current(cx).clone();
1711
1712 dragged_item_receiver::<EmptyPane, _, _>(self, 0, 0, false, None, cx, |_, cx| {
1713 self.render_blank_pane(&theme, cx)
1714 })
1715 .on_down(MouseButton::Left, |_, _, cx| {
1716 cx.focus_parent();
1717 })
1718 .into_any()
1719 }
1720 })
1721 .on_down(
1722 MouseButton::Navigate(NavigationDirection::Back),
1723 move |_, pane, cx| {
1724 if let Some(workspace) = pane.workspace.upgrade(cx) {
1725 let pane = cx.weak_handle();
1726 cx.window_context().defer(move |cx| {
1727 workspace.update(cx, |workspace, cx| {
1728 Pane::go_back(workspace, Some(pane), cx).detach_and_log_err(cx)
1729 })
1730 })
1731 }
1732 },
1733 )
1734 .on_down(MouseButton::Navigate(NavigationDirection::Forward), {
1735 move |_, pane, cx| {
1736 if let Some(workspace) = pane.workspace.upgrade(cx) {
1737 let pane = cx.weak_handle();
1738 cx.window_context().defer(move |cx| {
1739 workspace.update(cx, |workspace, cx| {
1740 Pane::go_forward(workspace, Some(pane), cx).detach_and_log_err(cx)
1741 })
1742 })
1743 }
1744 }
1745 })
1746 .into_any_named("pane")
1747 }
1748
1749 fn focus_in(&mut self, focused: AnyViewHandle, cx: &mut ViewContext<Self>) {
1750 if !self.has_focus {
1751 self.has_focus = true;
1752 cx.emit(Event::Focus);
1753 }
1754
1755 self.toolbar.update(cx, |toolbar, cx| {
1756 toolbar.pane_focus_update(true, cx);
1757 });
1758
1759 if let Some(active_item) = self.active_item() {
1760 if cx.is_self_focused() {
1761 // Pane was focused directly. We need to either focus a view inside the active item,
1762 // or focus the active item itself
1763 if let Some(weak_last_focused_view) =
1764 self.last_focused_view_by_item.get(&active_item.id())
1765 {
1766 if let Some(last_focused_view) = weak_last_focused_view.upgrade(cx) {
1767 cx.focus(&last_focused_view);
1768 return;
1769 } else {
1770 self.last_focused_view_by_item.remove(&active_item.id());
1771 }
1772 }
1773
1774 cx.focus(active_item.as_any());
1775 } else if focused != self.tab_bar_context_menu.handle {
1776 self.last_focused_view_by_item
1777 .insert(active_item.id(), focused.downgrade());
1778 }
1779 }
1780 }
1781
1782 fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
1783 self.has_focus = false;
1784 self.toolbar.update(cx, |toolbar, cx| {
1785 toolbar.pane_focus_update(false, cx);
1786 });
1787 }
1788
1789 fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) {
1790 Self::reset_to_default_keymap_context(keymap);
1791 }
1792}
1793
1794impl ItemNavHistory {
1795 pub fn push<D: 'static + Any>(&self, data: Option<D>, cx: &mut WindowContext) {
1796 self.history.borrow_mut().push(data, self.item.clone(), cx);
1797 }
1798
1799 pub fn pop_backward(&self, cx: &mut WindowContext) -> Option<NavigationEntry> {
1800 self.history.borrow_mut().pop(NavigationMode::GoingBack, cx)
1801 }
1802
1803 pub fn pop_forward(&self, cx: &mut WindowContext) -> Option<NavigationEntry> {
1804 self.history
1805 .borrow_mut()
1806 .pop(NavigationMode::GoingForward, cx)
1807 }
1808}
1809
1810impl NavHistory {
1811 fn set_mode(&mut self, mode: NavigationMode) {
1812 self.mode = mode;
1813 }
1814
1815 fn disable(&mut self) {
1816 self.mode = NavigationMode::Disabled;
1817 }
1818
1819 fn enable(&mut self) {
1820 self.mode = NavigationMode::Normal;
1821 }
1822
1823 fn pop(&mut self, mode: NavigationMode, cx: &mut WindowContext) -> Option<NavigationEntry> {
1824 let entry = match mode {
1825 NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
1826 return None
1827 }
1828 NavigationMode::GoingBack => &mut self.backward_stack,
1829 NavigationMode::GoingForward => &mut self.forward_stack,
1830 NavigationMode::ReopeningClosedItem => &mut self.closed_stack,
1831 }
1832 .pop_back();
1833 if entry.is_some() {
1834 self.did_update(cx);
1835 }
1836 entry
1837 }
1838
1839 fn push<D: 'static + Any>(
1840 &mut self,
1841 data: Option<D>,
1842 item: Rc<dyn WeakItemHandle>,
1843 cx: &mut WindowContext,
1844 ) {
1845 match self.mode {
1846 NavigationMode::Disabled => {}
1847 NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
1848 if self.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
1849 self.backward_stack.pop_front();
1850 }
1851 self.backward_stack.push_back(NavigationEntry {
1852 item,
1853 data: data.map(|data| Box::new(data) as Box<dyn Any>),
1854 timestamp: self.next_timestamp.fetch_add(1, Ordering::SeqCst),
1855 });
1856 self.forward_stack.clear();
1857 }
1858 NavigationMode::GoingBack => {
1859 if self.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
1860 self.forward_stack.pop_front();
1861 }
1862 self.forward_stack.push_back(NavigationEntry {
1863 item,
1864 data: data.map(|data| Box::new(data) as Box<dyn Any>),
1865 timestamp: self.next_timestamp.fetch_add(1, Ordering::SeqCst),
1866 });
1867 }
1868 NavigationMode::GoingForward => {
1869 if self.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
1870 self.backward_stack.pop_front();
1871 }
1872 self.backward_stack.push_back(NavigationEntry {
1873 item,
1874 data: data.map(|data| Box::new(data) as Box<dyn Any>),
1875 timestamp: self.next_timestamp.fetch_add(1, Ordering::SeqCst),
1876 });
1877 }
1878 NavigationMode::ClosingItem => {
1879 if self.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
1880 self.closed_stack.pop_front();
1881 }
1882 self.closed_stack.push_back(NavigationEntry {
1883 item,
1884 data: data.map(|data| Box::new(data) as Box<dyn Any>),
1885 timestamp: self.next_timestamp.fetch_add(1, Ordering::SeqCst),
1886 });
1887 }
1888 }
1889 self.did_update(cx);
1890 }
1891
1892 fn did_update(&self, cx: &mut WindowContext) {
1893 if let Some(pane) = self.pane.upgrade(cx) {
1894 cx.defer(move |cx| {
1895 pane.update(cx, |pane, cx| pane.history_updated(cx));
1896 });
1897 }
1898 }
1899
1900 fn remove_item(&mut self, item_id: usize) {
1901 self.paths_by_item.remove(&item_id);
1902 self.backward_stack
1903 .retain(|entry| entry.item.id() != item_id);
1904 self.forward_stack
1905 .retain(|entry| entry.item.id() != item_id);
1906 self.closed_stack.retain(|entry| entry.item.id() != item_id);
1907 }
1908}
1909
1910impl PaneNavHistory {
1911 pub fn for_each_entry(
1912 &self,
1913 cx: &AppContext,
1914 mut f: impl FnMut(&NavigationEntry, ProjectPath),
1915 ) {
1916 let borrowed_history = self.0.borrow();
1917 borrowed_history
1918 .forward_stack
1919 .iter()
1920 .chain(borrowed_history.backward_stack.iter())
1921 .chain(borrowed_history.closed_stack.iter())
1922 .for_each(|entry| {
1923 if let Some(path) = borrowed_history.paths_by_item.get(&entry.item.id()) {
1924 f(entry, path.clone());
1925 } else if let Some(item) = entry.item.upgrade(cx) {
1926 let path = item.project_path(cx);
1927 if let Some(path) = path {
1928 f(entry, path);
1929 }
1930 }
1931 })
1932 }
1933}
1934
1935pub struct PaneBackdrop<V: View> {
1936 child_view: usize,
1937 child: AnyElement<V>,
1938}
1939
1940impl<V: View> PaneBackdrop<V> {
1941 pub fn new(pane_item_view: usize, child: AnyElement<V>) -> Self {
1942 PaneBackdrop {
1943 child,
1944 child_view: pane_item_view,
1945 }
1946 }
1947}
1948
1949impl<V: View> Element<V> for PaneBackdrop<V> {
1950 type LayoutState = ();
1951
1952 type PaintState = ();
1953
1954 fn layout(
1955 &mut self,
1956 constraint: gpui::SizeConstraint,
1957 view: &mut V,
1958 cx: &mut LayoutContext<V>,
1959 ) -> (Vector2F, Self::LayoutState) {
1960 let size = self.child.layout(constraint, view, cx);
1961 (size, ())
1962 }
1963
1964 fn paint(
1965 &mut self,
1966 scene: &mut gpui::SceneBuilder,
1967 bounds: RectF,
1968 visible_bounds: RectF,
1969 _: &mut Self::LayoutState,
1970 view: &mut V,
1971 cx: &mut ViewContext<V>,
1972 ) -> Self::PaintState {
1973 let background = theme::current(cx).editor.background;
1974
1975 let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
1976
1977 scene.push_quad(gpui::Quad {
1978 bounds: RectF::new(bounds.origin(), bounds.size()),
1979 background: Some(background),
1980 ..Default::default()
1981 });
1982
1983 let child_view_id = self.child_view;
1984 scene.push_mouse_region(
1985 MouseRegion::new::<Self>(child_view_id, 0, visible_bounds).on_down(
1986 gpui::platform::MouseButton::Left,
1987 move |_, _: &mut V, cx| {
1988 let window_id = cx.window_id();
1989 cx.app_context().focus(window_id, Some(child_view_id))
1990 },
1991 ),
1992 );
1993
1994 scene.paint_layer(Some(bounds), |scene| {
1995 self.child
1996 .paint(scene, bounds.origin(), visible_bounds, view, cx)
1997 })
1998 }
1999
2000 fn rect_for_text_range(
2001 &self,
2002 range_utf16: std::ops::Range<usize>,
2003 _bounds: RectF,
2004 _visible_bounds: RectF,
2005 _layout: &Self::LayoutState,
2006 _paint: &Self::PaintState,
2007 view: &V,
2008 cx: &gpui::ViewContext<V>,
2009 ) -> Option<RectF> {
2010 self.child.rect_for_text_range(range_utf16, view, cx)
2011 }
2012
2013 fn debug(
2014 &self,
2015 _bounds: RectF,
2016 _layout: &Self::LayoutState,
2017 _paint: &Self::PaintState,
2018 view: &V,
2019 cx: &gpui::ViewContext<V>,
2020 ) -> serde_json::Value {
2021 gpui::json::json!({
2022 "type": "Pane Back Drop",
2023 "view": self.child_view,
2024 "child": self.child.debug(view, cx),
2025 })
2026 }
2027}
2028
2029#[cfg(test)]
2030mod tests {
2031 use super::*;
2032 use crate::item::test::{TestItem, TestProjectItem};
2033 use gpui::TestAppContext;
2034 use project::FakeFs;
2035 use settings::SettingsStore;
2036
2037 #[gpui::test]
2038 async fn test_remove_active_empty(cx: &mut TestAppContext) {
2039 init_test(cx);
2040 let fs = FakeFs::new(cx.background());
2041
2042 let project = Project::test(fs, None, cx).await;
2043 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2044 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2045
2046 pane.update(cx, |pane, cx| {
2047 assert!(pane.close_active_item(&CloseActiveItem, cx).is_none())
2048 });
2049 }
2050
2051 #[gpui::test]
2052 async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
2053 cx.foreground().forbid_parking();
2054 init_test(cx);
2055 let fs = FakeFs::new(cx.background());
2056
2057 let project = Project::test(fs, None, cx).await;
2058 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2059 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2060
2061 // 1. Add with a destination index
2062 // a. Add before the active item
2063 set_labeled_items(&workspace, &pane, ["A", "B*", "C"], cx);
2064 workspace.update(cx, |workspace, cx| {
2065 Pane::add_item(
2066 workspace,
2067 &pane,
2068 Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
2069 false,
2070 false,
2071 Some(0),
2072 cx,
2073 );
2074 });
2075 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
2076
2077 // b. Add after the active item
2078 set_labeled_items(&workspace, &pane, ["A", "B*", "C"], cx);
2079 workspace.update(cx, |workspace, cx| {
2080 Pane::add_item(
2081 workspace,
2082 &pane,
2083 Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
2084 false,
2085 false,
2086 Some(2),
2087 cx,
2088 );
2089 });
2090 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
2091
2092 // c. Add at the end of the item list (including off the length)
2093 set_labeled_items(&workspace, &pane, ["A", "B*", "C"], cx);
2094 workspace.update(cx, |workspace, cx| {
2095 Pane::add_item(
2096 workspace,
2097 &pane,
2098 Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
2099 false,
2100 false,
2101 Some(5),
2102 cx,
2103 );
2104 });
2105 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2106
2107 // 2. Add without a destination index
2108 // a. Add with active item at the start of the item list
2109 set_labeled_items(&workspace, &pane, ["A*", "B", "C"], cx);
2110 workspace.update(cx, |workspace, cx| {
2111 Pane::add_item(
2112 workspace,
2113 &pane,
2114 Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
2115 false,
2116 false,
2117 None,
2118 cx,
2119 );
2120 });
2121 set_labeled_items(&workspace, &pane, ["A", "D*", "B", "C"], cx);
2122
2123 // b. Add with active item at the end of the item list
2124 set_labeled_items(&workspace, &pane, ["A", "B", "C*"], cx);
2125 workspace.update(cx, |workspace, cx| {
2126 Pane::add_item(
2127 workspace,
2128 &pane,
2129 Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
2130 false,
2131 false,
2132 None,
2133 cx,
2134 );
2135 });
2136 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2137 }
2138
2139 #[gpui::test]
2140 async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
2141 cx.foreground().forbid_parking();
2142 init_test(cx);
2143 let fs = FakeFs::new(cx.background());
2144
2145 let project = Project::test(fs, None, cx).await;
2146 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2147 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2148
2149 // 1. Add with a destination index
2150 // 1a. Add before the active item
2151 let [_, _, _, d] = set_labeled_items(&workspace, &pane, ["A", "B*", "C", "D"], cx);
2152 workspace.update(cx, |workspace, cx| {
2153 Pane::add_item(workspace, &pane, d, false, false, Some(0), cx);
2154 });
2155 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
2156
2157 // 1b. Add after the active item
2158 let [_, _, _, d] = set_labeled_items(&workspace, &pane, ["A", "B*", "C", "D"], cx);
2159 workspace.update(cx, |workspace, cx| {
2160 Pane::add_item(workspace, &pane, d, false, false, Some(2), cx);
2161 });
2162 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
2163
2164 // 1c. Add at the end of the item list (including off the length)
2165 let [a, _, _, _] = set_labeled_items(&workspace, &pane, ["A", "B*", "C", "D"], cx);
2166 workspace.update(cx, |workspace, cx| {
2167 Pane::add_item(workspace, &pane, a, false, false, Some(5), cx);
2168 });
2169 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
2170
2171 // 1d. Add same item to active index
2172 let [_, b, _] = set_labeled_items(&workspace, &pane, ["A", "B*", "C"], cx);
2173 workspace.update(cx, |workspace, cx| {
2174 Pane::add_item(workspace, &pane, b, false, false, Some(1), cx);
2175 });
2176 assert_item_labels(&pane, ["A", "B*", "C"], cx);
2177
2178 // 1e. Add item to index after same item in last position
2179 let [_, _, c] = set_labeled_items(&workspace, &pane, ["A", "B*", "C"], cx);
2180 workspace.update(cx, |workspace, cx| {
2181 Pane::add_item(workspace, &pane, c, false, false, Some(2), cx);
2182 });
2183 assert_item_labels(&pane, ["A", "B", "C*"], cx);
2184
2185 // 2. Add without a destination index
2186 // 2a. Add with active item at the start of the item list
2187 let [_, _, _, d] = set_labeled_items(&workspace, &pane, ["A*", "B", "C", "D"], cx);
2188 workspace.update(cx, |workspace, cx| {
2189 Pane::add_item(workspace, &pane, d, false, false, None, cx);
2190 });
2191 assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
2192
2193 // 2b. Add with active item at the end of the item list
2194 let [a, _, _, _] = set_labeled_items(&workspace, &pane, ["A", "B", "C", "D*"], cx);
2195 workspace.update(cx, |workspace, cx| {
2196 Pane::add_item(workspace, &pane, a, false, false, None, cx);
2197 });
2198 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
2199
2200 // 2c. Add active item to active item at end of list
2201 let [_, _, c] = set_labeled_items(&workspace, &pane, ["A", "B", "C*"], cx);
2202 workspace.update(cx, |workspace, cx| {
2203 Pane::add_item(workspace, &pane, c, false, false, None, cx);
2204 });
2205 assert_item_labels(&pane, ["A", "B", "C*"], cx);
2206
2207 // 2d. Add active item to active item at start of list
2208 let [a, _, _] = set_labeled_items(&workspace, &pane, ["A*", "B", "C"], cx);
2209 workspace.update(cx, |workspace, cx| {
2210 Pane::add_item(workspace, &pane, a, false, false, None, cx);
2211 });
2212 assert_item_labels(&pane, ["A*", "B", "C"], cx);
2213 }
2214
2215 #[gpui::test]
2216 async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
2217 cx.foreground().forbid_parking();
2218 init_test(cx);
2219 let fs = FakeFs::new(cx.background());
2220
2221 let project = Project::test(fs, None, cx).await;
2222 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2223 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2224
2225 // singleton view
2226 workspace.update(cx, |workspace, cx| {
2227 let item = TestItem::new()
2228 .with_singleton(true)
2229 .with_label("buffer 1")
2230 .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)]);
2231
2232 Pane::add_item(
2233 workspace,
2234 &pane,
2235 Box::new(cx.add_view(|_| item)),
2236 false,
2237 false,
2238 None,
2239 cx,
2240 );
2241 });
2242 assert_item_labels(&pane, ["buffer 1*"], cx);
2243
2244 // new singleton view with the same project entry
2245 workspace.update(cx, |workspace, cx| {
2246 let item = TestItem::new()
2247 .with_singleton(true)
2248 .with_label("buffer 1")
2249 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]);
2250
2251 Pane::add_item(
2252 workspace,
2253 &pane,
2254 Box::new(cx.add_view(|_| item)),
2255 false,
2256 false,
2257 None,
2258 cx,
2259 );
2260 });
2261 assert_item_labels(&pane, ["buffer 1*"], cx);
2262
2263 // new singleton view with different project entry
2264 workspace.update(cx, |workspace, cx| {
2265 let item = TestItem::new()
2266 .with_singleton(true)
2267 .with_label("buffer 2")
2268 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)]);
2269
2270 Pane::add_item(
2271 workspace,
2272 &pane,
2273 Box::new(cx.add_view(|_| item)),
2274 false,
2275 false,
2276 None,
2277 cx,
2278 );
2279 });
2280 assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
2281
2282 // new multibuffer view with the same project entry
2283 workspace.update(cx, |workspace, cx| {
2284 let item = TestItem::new()
2285 .with_singleton(false)
2286 .with_label("multibuffer 1")
2287 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]);
2288
2289 Pane::add_item(
2290 workspace,
2291 &pane,
2292 Box::new(cx.add_view(|_| item)),
2293 false,
2294 false,
2295 None,
2296 cx,
2297 );
2298 });
2299 assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
2300
2301 // another multibuffer view with the same project entry
2302 workspace.update(cx, |workspace, cx| {
2303 let item = TestItem::new()
2304 .with_singleton(false)
2305 .with_label("multibuffer 1b")
2306 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]);
2307
2308 Pane::add_item(
2309 workspace,
2310 &pane,
2311 Box::new(cx.add_view(|_| item)),
2312 false,
2313 false,
2314 None,
2315 cx,
2316 );
2317 });
2318 assert_item_labels(
2319 &pane,
2320 ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
2321 cx,
2322 );
2323 }
2324
2325 #[gpui::test]
2326 async fn test_remove_item_ordering(cx: &mut TestAppContext) {
2327 init_test(cx);
2328 let fs = FakeFs::new(cx.background());
2329
2330 let project = Project::test(fs, None, cx).await;
2331 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2332 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2333
2334 add_labeled_item(&workspace, &pane, "A", false, cx);
2335 add_labeled_item(&workspace, &pane, "B", false, cx);
2336 add_labeled_item(&workspace, &pane, "C", false, cx);
2337 add_labeled_item(&workspace, &pane, "D", false, cx);
2338 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2339
2340 pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
2341 add_labeled_item(&workspace, &pane, "1", false, cx);
2342 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
2343
2344 pane.update(cx, |pane, cx| pane.close_active_item(&CloseActiveItem, cx))
2345 .unwrap()
2346 .await
2347 .unwrap();
2348 assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
2349
2350 pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
2351 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2352
2353 pane.update(cx, |pane, cx| pane.close_active_item(&CloseActiveItem, cx))
2354 .unwrap()
2355 .await
2356 .unwrap();
2357 assert_item_labels(&pane, ["A", "B*", "C"], cx);
2358
2359 pane.update(cx, |pane, cx| pane.close_active_item(&CloseActiveItem, cx))
2360 .unwrap()
2361 .await
2362 .unwrap();
2363 assert_item_labels(&pane, ["A", "C*"], cx);
2364
2365 pane.update(cx, |pane, cx| pane.close_active_item(&CloseActiveItem, cx))
2366 .unwrap()
2367 .await
2368 .unwrap();
2369 assert_item_labels(&pane, ["A*"], cx);
2370 }
2371
2372 #[gpui::test]
2373 async fn test_close_inactive_items(cx: &mut TestAppContext) {
2374 init_test(cx);
2375 let fs = FakeFs::new(cx.background());
2376
2377 let project = Project::test(fs, None, cx).await;
2378 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2379 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2380
2381 set_labeled_items(&workspace, &pane, ["A", "B", "C*", "D", "E"], cx);
2382
2383 pane.update(cx, |pane, cx| {
2384 pane.close_inactive_items(&CloseInactiveItems, cx)
2385 })
2386 .unwrap()
2387 .await
2388 .unwrap();
2389 assert_item_labels(&pane, ["C*"], cx);
2390 }
2391
2392 #[gpui::test]
2393 async fn test_close_clean_items(cx: &mut TestAppContext) {
2394 init_test(cx);
2395 let fs = FakeFs::new(cx.background());
2396
2397 let project = Project::test(fs, None, cx).await;
2398 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2399 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2400
2401 add_labeled_item(&workspace, &pane, "A", true, cx);
2402 add_labeled_item(&workspace, &pane, "B", false, cx);
2403 add_labeled_item(&workspace, &pane, "C", true, cx);
2404 add_labeled_item(&workspace, &pane, "D", false, cx);
2405 add_labeled_item(&workspace, &pane, "E", false, cx);
2406 assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
2407
2408 pane.update(cx, |pane, cx| pane.close_clean_items(&CloseCleanItems, cx))
2409 .unwrap()
2410 .await
2411 .unwrap();
2412 assert_item_labels(&pane, ["A^", "C*^"], cx);
2413 }
2414
2415 #[gpui::test]
2416 async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
2417 init_test(cx);
2418 let fs = FakeFs::new(cx.background());
2419
2420 let project = Project::test(fs, None, cx).await;
2421 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2422 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2423
2424 set_labeled_items(&workspace, &pane, ["A", "B", "C*", "D", "E"], cx);
2425
2426 pane.update(cx, |pane, cx| {
2427 pane.close_items_to_the_left(&CloseItemsToTheLeft, cx)
2428 })
2429 .unwrap()
2430 .await
2431 .unwrap();
2432 assert_item_labels(&pane, ["C*", "D", "E"], cx);
2433 }
2434
2435 #[gpui::test]
2436 async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
2437 init_test(cx);
2438 let fs = FakeFs::new(cx.background());
2439
2440 let project = Project::test(fs, None, cx).await;
2441 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2442 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2443
2444 set_labeled_items(&workspace, &pane, ["A", "B", "C*", "D", "E"], cx);
2445
2446 pane.update(cx, |pane, cx| {
2447 pane.close_items_to_the_right(&CloseItemsToTheRight, cx)
2448 })
2449 .unwrap()
2450 .await
2451 .unwrap();
2452 assert_item_labels(&pane, ["A", "B", "C*"], cx);
2453 }
2454
2455 #[gpui::test]
2456 async fn test_close_all_items(cx: &mut TestAppContext) {
2457 init_test(cx);
2458 let fs = FakeFs::new(cx.background());
2459
2460 let project = Project::test(fs, None, cx).await;
2461 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2462 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2463
2464 add_labeled_item(&workspace, &pane, "A", false, cx);
2465 add_labeled_item(&workspace, &pane, "B", false, cx);
2466 add_labeled_item(&workspace, &pane, "C", false, cx);
2467 assert_item_labels(&pane, ["A", "B", "C*"], cx);
2468
2469 pane.update(cx, |pane, cx| pane.close_all_items(&CloseAllItems, cx))
2470 .unwrap()
2471 .await
2472 .unwrap();
2473 assert_item_labels(&pane, [], cx);
2474 }
2475
2476 fn init_test(cx: &mut TestAppContext) {
2477 cx.update(|cx| {
2478 cx.set_global(SettingsStore::test(cx));
2479 theme::init((), cx);
2480 crate::init_settings(cx);
2481 });
2482 }
2483
2484 fn add_labeled_item(
2485 workspace: &ViewHandle<Workspace>,
2486 pane: &ViewHandle<Pane>,
2487 label: &str,
2488 is_dirty: bool,
2489 cx: &mut TestAppContext,
2490 ) -> Box<ViewHandle<TestItem>> {
2491 workspace.update(cx, |workspace, cx| {
2492 let labeled_item =
2493 Box::new(cx.add_view(|_| TestItem::new().with_label(label).with_dirty(is_dirty)));
2494
2495 Pane::add_item(
2496 workspace,
2497 pane,
2498 labeled_item.clone(),
2499 false,
2500 false,
2501 None,
2502 cx,
2503 );
2504
2505 labeled_item
2506 })
2507 }
2508
2509 fn set_labeled_items<const COUNT: usize>(
2510 workspace: &ViewHandle<Workspace>,
2511 pane: &ViewHandle<Pane>,
2512 labels: [&str; COUNT],
2513 cx: &mut TestAppContext,
2514 ) -> [Box<ViewHandle<TestItem>>; COUNT] {
2515 pane.update(cx, |pane, _| {
2516 pane.items.clear();
2517 });
2518
2519 workspace.update(cx, |workspace, cx| {
2520 let mut active_item_index = 0;
2521
2522 let mut index = 0;
2523 let items = labels.map(|mut label| {
2524 if label.ends_with("*") {
2525 label = label.trim_end_matches("*");
2526 active_item_index = index;
2527 }
2528
2529 let labeled_item = Box::new(cx.add_view(|_| TestItem::new().with_label(label)));
2530 Pane::add_item(
2531 workspace,
2532 pane,
2533 labeled_item.clone(),
2534 false,
2535 false,
2536 None,
2537 cx,
2538 );
2539 index += 1;
2540 labeled_item
2541 });
2542
2543 pane.update(cx, |pane, cx| {
2544 pane.activate_item(active_item_index, false, false, cx)
2545 });
2546
2547 items
2548 })
2549 }
2550
2551 // Assert the item label, with the active item label suffixed with a '*'
2552 fn assert_item_labels<const COUNT: usize>(
2553 pane: &ViewHandle<Pane>,
2554 expected_states: [&str; COUNT],
2555 cx: &mut TestAppContext,
2556 ) {
2557 pane.read_with(cx, |pane, cx| {
2558 let actual_states = pane
2559 .items
2560 .iter()
2561 .enumerate()
2562 .map(|(ix, item)| {
2563 let mut state = item
2564 .as_any()
2565 .downcast_ref::<TestItem>()
2566 .unwrap()
2567 .read(cx)
2568 .label
2569 .clone();
2570 if ix == pane.active_item_index {
2571 state.push('*');
2572 }
2573 if item.is_dirty(cx) {
2574 state.push('^');
2575 }
2576 state
2577 })
2578 .collect::<Vec<_>>();
2579
2580 assert_eq!(
2581 actual_states, expected_states,
2582 "pane items do not match expectation"
2583 );
2584 })
2585 }
2586}