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