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