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