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