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 keymap_context(&self, _: &AppContext) -> KeymapContext {
1858 let mut keymap = Self::default_keymap_context();
1859 if self.docked.is_some() {
1860 keymap.add_identifier("docked");
1861 }
1862 keymap
1863 }
1864}
1865
1866fn render_tab_bar_button<A: Action + Clone>(
1867 index: usize,
1868 icon: &'static str,
1869 cx: &mut ViewContext<Pane>,
1870 action: A,
1871 context_menu: Option<ViewHandle<ContextMenu>>,
1872) -> AnyElement<Pane> {
1873 enum TabBarButton {}
1874
1875 Stack::new()
1876 .with_child(
1877 MouseEventHandler::<TabBarButton, _>::new(index, cx, |mouse_state, cx| {
1878 let theme = &cx.global::<Settings>().theme.workspace.tab_bar;
1879 let style = theme.pane_button.style_for(mouse_state, false);
1880 Svg::new(icon)
1881 .with_color(style.color)
1882 .constrained()
1883 .with_width(style.icon_width)
1884 .aligned()
1885 .constrained()
1886 .with_width(style.button_width)
1887 .with_height(style.button_width)
1888 })
1889 .with_cursor_style(CursorStyle::PointingHand)
1890 .on_click(MouseButton::Left, move |_, _, cx| {
1891 cx.dispatch_action(action.clone());
1892 }),
1893 )
1894 .with_children(
1895 context_menu.map(|menu| ChildView::new(&menu, cx).aligned().bottom().right()),
1896 )
1897 .flex(1., false)
1898 .into_any_named("tab bar button")
1899}
1900
1901impl ItemNavHistory {
1902 pub fn push<D: 'static + Any>(&self, data: Option<D>, cx: &mut WindowContext) {
1903 self.history.borrow_mut().push(data, self.item.clone(), cx);
1904 }
1905
1906 pub fn pop_backward(&self, cx: &mut WindowContext) -> Option<NavigationEntry> {
1907 self.history.borrow_mut().pop(NavigationMode::GoingBack, cx)
1908 }
1909
1910 pub fn pop_forward(&self, cx: &mut WindowContext) -> Option<NavigationEntry> {
1911 self.history
1912 .borrow_mut()
1913 .pop(NavigationMode::GoingForward, cx)
1914 }
1915}
1916
1917impl NavHistory {
1918 fn set_mode(&mut self, mode: NavigationMode) {
1919 self.mode = mode;
1920 }
1921
1922 fn disable(&mut self) {
1923 self.mode = NavigationMode::Disabled;
1924 }
1925
1926 fn enable(&mut self) {
1927 self.mode = NavigationMode::Normal;
1928 }
1929
1930 fn pop(&mut self, mode: NavigationMode, cx: &mut WindowContext) -> Option<NavigationEntry> {
1931 let entry = match mode {
1932 NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
1933 return None
1934 }
1935 NavigationMode::GoingBack => &mut self.backward_stack,
1936 NavigationMode::GoingForward => &mut self.forward_stack,
1937 NavigationMode::ReopeningClosedItem => &mut self.closed_stack,
1938 }
1939 .pop_back();
1940 if entry.is_some() {
1941 self.did_update(cx);
1942 }
1943 entry
1944 }
1945
1946 fn push<D: 'static + Any>(
1947 &mut self,
1948 data: Option<D>,
1949 item: Rc<dyn WeakItemHandle>,
1950 cx: &mut WindowContext,
1951 ) {
1952 match self.mode {
1953 NavigationMode::Disabled => {}
1954 NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
1955 if self.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
1956 self.backward_stack.pop_front();
1957 }
1958 self.backward_stack.push_back(NavigationEntry {
1959 item,
1960 data: data.map(|data| Box::new(data) as Box<dyn Any>),
1961 });
1962 self.forward_stack.clear();
1963 }
1964 NavigationMode::GoingBack => {
1965 if self.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
1966 self.forward_stack.pop_front();
1967 }
1968 self.forward_stack.push_back(NavigationEntry {
1969 item,
1970 data: data.map(|data| Box::new(data) as Box<dyn Any>),
1971 });
1972 }
1973 NavigationMode::GoingForward => {
1974 if self.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
1975 self.backward_stack.pop_front();
1976 }
1977 self.backward_stack.push_back(NavigationEntry {
1978 item,
1979 data: data.map(|data| Box::new(data) as Box<dyn Any>),
1980 });
1981 }
1982 NavigationMode::ClosingItem => {
1983 if self.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
1984 self.closed_stack.pop_front();
1985 }
1986 self.closed_stack.push_back(NavigationEntry {
1987 item,
1988 data: data.map(|data| Box::new(data) as Box<dyn Any>),
1989 });
1990 }
1991 }
1992 self.did_update(cx);
1993 }
1994
1995 fn did_update(&self, cx: &mut WindowContext) {
1996 if let Some(pane) = self.pane.upgrade(cx) {
1997 cx.defer(move |cx| {
1998 pane.update(cx, |pane, cx| pane.history_updated(cx));
1999 });
2000 }
2001 }
2002}
2003
2004pub struct PaneBackdrop<V: View> {
2005 child_view: usize,
2006 child: AnyElement<V>,
2007}
2008
2009impl<V: View> PaneBackdrop<V> {
2010 pub fn new(pane_item_view: usize, child: AnyElement<V>) -> Self {
2011 PaneBackdrop {
2012 child,
2013 child_view: pane_item_view,
2014 }
2015 }
2016}
2017
2018impl<V: View> Element<V> for PaneBackdrop<V> {
2019 type LayoutState = ();
2020
2021 type PaintState = ();
2022
2023 fn layout(
2024 &mut self,
2025 constraint: gpui::SizeConstraint,
2026 view: &mut V,
2027 cx: &mut ViewContext<V>,
2028 ) -> (Vector2F, Self::LayoutState) {
2029 let size = self.child.layout(constraint, view, cx);
2030 (size, ())
2031 }
2032
2033 fn paint(
2034 &mut self,
2035 scene: &mut gpui::SceneBuilder,
2036 bounds: RectF,
2037 visible_bounds: RectF,
2038 _: &mut Self::LayoutState,
2039 view: &mut V,
2040 cx: &mut ViewContext<V>,
2041 ) -> Self::PaintState {
2042 let background = cx.global::<Settings>().theme.editor.background;
2043
2044 let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
2045
2046 scene.push_quad(gpui::Quad {
2047 bounds: RectF::new(bounds.origin(), bounds.size()),
2048 background: Some(background),
2049 ..Default::default()
2050 });
2051
2052 let child_view_id = self.child_view;
2053 scene.push_mouse_region(
2054 MouseRegion::new::<Self>(child_view_id, 0, visible_bounds).on_down(
2055 gpui::platform::MouseButton::Left,
2056 move |_, _: &mut V, cx| {
2057 let window_id = cx.window_id();
2058 cx.app_context().focus(window_id, Some(child_view_id))
2059 },
2060 ),
2061 );
2062
2063 scene.paint_layer(Some(bounds), |scene| {
2064 self.child
2065 .paint(scene, bounds.origin(), visible_bounds, view, cx)
2066 })
2067 }
2068
2069 fn rect_for_text_range(
2070 &self,
2071 range_utf16: std::ops::Range<usize>,
2072 _bounds: RectF,
2073 _visible_bounds: RectF,
2074 _layout: &Self::LayoutState,
2075 _paint: &Self::PaintState,
2076 view: &V,
2077 cx: &gpui::ViewContext<V>,
2078 ) -> Option<RectF> {
2079 self.child.rect_for_text_range(range_utf16, view, cx)
2080 }
2081
2082 fn debug(
2083 &self,
2084 _bounds: RectF,
2085 _layout: &Self::LayoutState,
2086 _paint: &Self::PaintState,
2087 view: &V,
2088 cx: &gpui::ViewContext<V>,
2089 ) -> serde_json::Value {
2090 gpui::json::json!({
2091 "type": "Pane Back Drop",
2092 "view": self.child_view,
2093 "child": self.child.debug(view, cx),
2094 })
2095 }
2096}
2097
2098#[cfg(test)]
2099mod tests {
2100 use std::sync::Arc;
2101
2102 use super::*;
2103 use crate::item::test::{TestItem, TestProjectItem};
2104 use gpui::{executor::Deterministic, TestAppContext};
2105 use project::FakeFs;
2106
2107 #[gpui::test]
2108 async fn test_remove_active_empty(cx: &mut TestAppContext) {
2109 Settings::test_async(cx);
2110 let fs = FakeFs::new(cx.background());
2111
2112 let project = Project::test(fs, None, cx).await;
2113 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2114
2115 workspace.update(cx, |workspace, cx| {
2116 assert!(Pane::close_active_item(workspace, &CloseActiveItem, cx).is_none())
2117 });
2118 }
2119
2120 #[gpui::test]
2121 async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
2122 cx.foreground().forbid_parking();
2123 Settings::test_async(cx);
2124 let fs = FakeFs::new(cx.background());
2125
2126 let project = Project::test(fs, None, cx).await;
2127 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2128 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2129
2130 // 1. Add with a destination index
2131 // a. Add before the active item
2132 set_labeled_items(&workspace, &pane, ["A", "B*", "C"], cx);
2133 workspace.update(cx, |workspace, cx| {
2134 Pane::add_item(
2135 workspace,
2136 &pane,
2137 Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
2138 false,
2139 false,
2140 Some(0),
2141 cx,
2142 );
2143 });
2144 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
2145
2146 // b. Add after the active item
2147 set_labeled_items(&workspace, &pane, ["A", "B*", "C"], cx);
2148 workspace.update(cx, |workspace, cx| {
2149 Pane::add_item(
2150 workspace,
2151 &pane,
2152 Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
2153 false,
2154 false,
2155 Some(2),
2156 cx,
2157 );
2158 });
2159 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
2160
2161 // c. Add at the end of the item list (including off the length)
2162 set_labeled_items(&workspace, &pane, ["A", "B*", "C"], cx);
2163 workspace.update(cx, |workspace, cx| {
2164 Pane::add_item(
2165 workspace,
2166 &pane,
2167 Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
2168 false,
2169 false,
2170 Some(5),
2171 cx,
2172 );
2173 });
2174 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2175
2176 // 2. Add without a destination index
2177 // a. Add with active item at the start of the item list
2178 set_labeled_items(&workspace, &pane, ["A*", "B", "C"], cx);
2179 workspace.update(cx, |workspace, cx| {
2180 Pane::add_item(
2181 workspace,
2182 &pane,
2183 Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
2184 false,
2185 false,
2186 None,
2187 cx,
2188 );
2189 });
2190 set_labeled_items(&workspace, &pane, ["A", "D*", "B", "C"], cx);
2191
2192 // b. Add with active item at the end of the item list
2193 set_labeled_items(&workspace, &pane, ["A", "B", "C*"], cx);
2194 workspace.update(cx, |workspace, cx| {
2195 Pane::add_item(
2196 workspace,
2197 &pane,
2198 Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
2199 false,
2200 false,
2201 None,
2202 cx,
2203 );
2204 });
2205 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2206 }
2207
2208 #[gpui::test]
2209 async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
2210 cx.foreground().forbid_parking();
2211 Settings::test_async(cx);
2212 let fs = FakeFs::new(cx.background());
2213
2214 let project = Project::test(fs, None, cx).await;
2215 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2216 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2217
2218 // 1. Add with a destination index
2219 // 1a. Add before the active item
2220 let [_, _, _, d] = set_labeled_items(&workspace, &pane, ["A", "B*", "C", "D"], cx);
2221 workspace.update(cx, |workspace, cx| {
2222 Pane::add_item(workspace, &pane, d, false, false, Some(0), cx);
2223 });
2224 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
2225
2226 // 1b. Add after the active item
2227 let [_, _, _, d] = set_labeled_items(&workspace, &pane, ["A", "B*", "C", "D"], cx);
2228 workspace.update(cx, |workspace, cx| {
2229 Pane::add_item(workspace, &pane, d, false, false, Some(2), cx);
2230 });
2231 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
2232
2233 // 1c. Add at the end of the item list (including off the length)
2234 let [a, _, _, _] = set_labeled_items(&workspace, &pane, ["A", "B*", "C", "D"], cx);
2235 workspace.update(cx, |workspace, cx| {
2236 Pane::add_item(workspace, &pane, a, false, false, Some(5), cx);
2237 });
2238 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
2239
2240 // 1d. Add same item to active index
2241 let [_, b, _] = set_labeled_items(&workspace, &pane, ["A", "B*", "C"], cx);
2242 workspace.update(cx, |workspace, cx| {
2243 Pane::add_item(workspace, &pane, b, false, false, Some(1), cx);
2244 });
2245 assert_item_labels(&pane, ["A", "B*", "C"], cx);
2246
2247 // 1e. Add item to index after same item in last position
2248 let [_, _, c] = set_labeled_items(&workspace, &pane, ["A", "B*", "C"], cx);
2249 workspace.update(cx, |workspace, cx| {
2250 Pane::add_item(workspace, &pane, c, false, false, Some(2), cx);
2251 });
2252 assert_item_labels(&pane, ["A", "B", "C*"], cx);
2253
2254 // 2. Add without a destination index
2255 // 2a. Add with active item at the start of the item list
2256 let [_, _, _, d] = set_labeled_items(&workspace, &pane, ["A*", "B", "C", "D"], cx);
2257 workspace.update(cx, |workspace, cx| {
2258 Pane::add_item(workspace, &pane, d, false, false, None, cx);
2259 });
2260 assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
2261
2262 // 2b. Add with active item at the end of the item list
2263 let [a, _, _, _] = set_labeled_items(&workspace, &pane, ["A", "B", "C", "D*"], cx);
2264 workspace.update(cx, |workspace, cx| {
2265 Pane::add_item(workspace, &pane, a, false, false, None, cx);
2266 });
2267 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
2268
2269 // 2c. Add active item to active item at end of list
2270 let [_, _, c] = set_labeled_items(&workspace, &pane, ["A", "B", "C*"], cx);
2271 workspace.update(cx, |workspace, cx| {
2272 Pane::add_item(workspace, &pane, c, false, false, None, cx);
2273 });
2274 assert_item_labels(&pane, ["A", "B", "C*"], cx);
2275
2276 // 2d. Add active item to active item at start of list
2277 let [a, _, _] = set_labeled_items(&workspace, &pane, ["A*", "B", "C"], cx);
2278 workspace.update(cx, |workspace, cx| {
2279 Pane::add_item(workspace, &pane, a, false, false, None, cx);
2280 });
2281 assert_item_labels(&pane, ["A*", "B", "C"], cx);
2282 }
2283
2284 #[gpui::test]
2285 async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
2286 cx.foreground().forbid_parking();
2287 Settings::test_async(cx);
2288 let fs = FakeFs::new(cx.background());
2289
2290 let project = Project::test(fs, None, cx).await;
2291 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2292 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2293
2294 // singleton view
2295 workspace.update(cx, |workspace, cx| {
2296 let item = TestItem::new()
2297 .with_singleton(true)
2298 .with_label("buffer 1")
2299 .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)]);
2300
2301 Pane::add_item(
2302 workspace,
2303 &pane,
2304 Box::new(cx.add_view(|_| item)),
2305 false,
2306 false,
2307 None,
2308 cx,
2309 );
2310 });
2311 assert_item_labels(&pane, ["buffer 1*"], cx);
2312
2313 // new singleton view with the same project entry
2314 workspace.update(cx, |workspace, cx| {
2315 let item = TestItem::new()
2316 .with_singleton(true)
2317 .with_label("buffer 1")
2318 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]);
2319
2320 Pane::add_item(
2321 workspace,
2322 &pane,
2323 Box::new(cx.add_view(|_| item)),
2324 false,
2325 false,
2326 None,
2327 cx,
2328 );
2329 });
2330 assert_item_labels(&pane, ["buffer 1*"], cx);
2331
2332 // new singleton view with different project entry
2333 workspace.update(cx, |workspace, cx| {
2334 let item = TestItem::new()
2335 .with_singleton(true)
2336 .with_label("buffer 2")
2337 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)]);
2338
2339 Pane::add_item(
2340 workspace,
2341 &pane,
2342 Box::new(cx.add_view(|_| item)),
2343 false,
2344 false,
2345 None,
2346 cx,
2347 );
2348 });
2349 assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
2350
2351 // new multibuffer view with the same project entry
2352 workspace.update(cx, |workspace, cx| {
2353 let item = TestItem::new()
2354 .with_singleton(false)
2355 .with_label("multibuffer 1")
2356 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]);
2357
2358 Pane::add_item(
2359 workspace,
2360 &pane,
2361 Box::new(cx.add_view(|_| item)),
2362 false,
2363 false,
2364 None,
2365 cx,
2366 );
2367 });
2368 assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
2369
2370 // another multibuffer view with the same project entry
2371 workspace.update(cx, |workspace, cx| {
2372 let item = TestItem::new()
2373 .with_singleton(false)
2374 .with_label("multibuffer 1b")
2375 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]);
2376
2377 Pane::add_item(
2378 workspace,
2379 &pane,
2380 Box::new(cx.add_view(|_| item)),
2381 false,
2382 false,
2383 None,
2384 cx,
2385 );
2386 });
2387 assert_item_labels(
2388 &pane,
2389 ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
2390 cx,
2391 );
2392 }
2393
2394 #[gpui::test]
2395 async fn test_remove_item_ordering(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
2396 Settings::test_async(cx);
2397 let fs = FakeFs::new(cx.background());
2398
2399 let project = Project::test(fs, None, cx).await;
2400 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2401 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2402
2403 add_labeled_item(&workspace, &pane, "A", false, cx);
2404 add_labeled_item(&workspace, &pane, "B", false, cx);
2405 add_labeled_item(&workspace, &pane, "C", false, cx);
2406 add_labeled_item(&workspace, &pane, "D", false, cx);
2407 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2408
2409 pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
2410 add_labeled_item(&workspace, &pane, "1", false, cx);
2411 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
2412
2413 workspace.update(cx, |workspace, cx| {
2414 Pane::close_active_item(workspace, &CloseActiveItem, cx);
2415 });
2416 deterministic.run_until_parked();
2417 assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
2418
2419 pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
2420 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2421
2422 workspace.update(cx, |workspace, cx| {
2423 Pane::close_active_item(workspace, &CloseActiveItem, cx);
2424 });
2425 deterministic.run_until_parked();
2426 assert_item_labels(&pane, ["A", "B*", "C"], cx);
2427
2428 workspace.update(cx, |workspace, cx| {
2429 Pane::close_active_item(workspace, &CloseActiveItem, cx);
2430 });
2431 deterministic.run_until_parked();
2432 assert_item_labels(&pane, ["A", "C*"], cx);
2433
2434 workspace.update(cx, |workspace, cx| {
2435 Pane::close_active_item(workspace, &CloseActiveItem, cx);
2436 });
2437 deterministic.run_until_parked();
2438 assert_item_labels(&pane, ["A*"], cx);
2439 }
2440
2441 #[gpui::test]
2442 async fn test_close_inactive_items(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
2443 Settings::test_async(cx);
2444 let fs = FakeFs::new(cx.background());
2445
2446 let project = Project::test(fs, None, cx).await;
2447 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2448 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2449
2450 set_labeled_items(&workspace, &pane, ["A", "B", "C*", "D", "E"], cx);
2451
2452 workspace.update(cx, |workspace, cx| {
2453 Pane::close_inactive_items(workspace, &CloseInactiveItems, cx);
2454 });
2455
2456 deterministic.run_until_parked();
2457 assert_item_labels(&pane, ["C*"], cx);
2458 }
2459
2460 #[gpui::test]
2461 async fn test_close_clean_items(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
2462 Settings::test_async(cx);
2463 let fs = FakeFs::new(cx.background());
2464
2465 let project = Project::test(fs, None, cx).await;
2466 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2467 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2468
2469 add_labeled_item(&workspace, &pane, "A", true, cx);
2470 add_labeled_item(&workspace, &pane, "B", false, cx);
2471 add_labeled_item(&workspace, &pane, "C", true, cx);
2472 add_labeled_item(&workspace, &pane, "D", false, cx);
2473 add_labeled_item(&workspace, &pane, "E", false, cx);
2474 assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
2475
2476 workspace.update(cx, |workspace, cx| {
2477 Pane::close_clean_items(workspace, &CloseCleanItems, cx);
2478 });
2479
2480 deterministic.run_until_parked();
2481 assert_item_labels(&pane, ["A^", "C*^"], cx);
2482 }
2483
2484 #[gpui::test]
2485 async fn test_close_items_to_the_left(
2486 deterministic: Arc<Deterministic>,
2487 cx: &mut TestAppContext,
2488 ) {
2489 Settings::test_async(cx);
2490 let fs = FakeFs::new(cx.background());
2491
2492 let project = Project::test(fs, None, cx).await;
2493 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2494 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2495
2496 set_labeled_items(&workspace, &pane, ["A", "B", "C*", "D", "E"], cx);
2497
2498 workspace.update(cx, |workspace, cx| {
2499 Pane::close_items_to_the_left(workspace, &CloseItemsToTheLeft, cx);
2500 });
2501
2502 deterministic.run_until_parked();
2503 assert_item_labels(&pane, ["C*", "D", "E"], cx);
2504 }
2505
2506 #[gpui::test]
2507 async fn test_close_items_to_the_right(
2508 deterministic: Arc<Deterministic>,
2509 cx: &mut TestAppContext,
2510 ) {
2511 Settings::test_async(cx);
2512 let fs = FakeFs::new(cx.background());
2513
2514 let project = Project::test(fs, None, cx).await;
2515 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2516 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2517
2518 set_labeled_items(&workspace, &pane, ["A", "B", "C*", "D", "E"], cx);
2519
2520 workspace.update(cx, |workspace, cx| {
2521 Pane::close_items_to_the_right(workspace, &CloseItemsToTheRight, cx);
2522 });
2523
2524 deterministic.run_until_parked();
2525 assert_item_labels(&pane, ["A", "B", "C*"], cx);
2526 }
2527
2528 #[gpui::test]
2529 async fn test_close_all_items(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
2530 Settings::test_async(cx);
2531 let fs = FakeFs::new(cx.background());
2532
2533 let project = Project::test(fs, None, cx).await;
2534 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2535 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2536
2537 add_labeled_item(&workspace, &pane, "A", false, cx);
2538 add_labeled_item(&workspace, &pane, "B", false, cx);
2539 add_labeled_item(&workspace, &pane, "C", false, cx);
2540 assert_item_labels(&pane, ["A", "B", "C*"], cx);
2541
2542 workspace.update(cx, |workspace, cx| {
2543 Pane::close_all_items(workspace, &CloseAllItems, cx);
2544 });
2545
2546 deterministic.run_until_parked();
2547 assert_item_labels(&pane, [], cx);
2548 }
2549
2550 fn add_labeled_item(
2551 workspace: &ViewHandle<Workspace>,
2552 pane: &ViewHandle<Pane>,
2553 label: &str,
2554 is_dirty: bool,
2555 cx: &mut TestAppContext,
2556 ) -> Box<ViewHandle<TestItem>> {
2557 workspace.update(cx, |workspace, cx| {
2558 let labeled_item =
2559 Box::new(cx.add_view(|_| TestItem::new().with_label(label).with_dirty(is_dirty)));
2560
2561 Pane::add_item(
2562 workspace,
2563 pane,
2564 labeled_item.clone(),
2565 false,
2566 false,
2567 None,
2568 cx,
2569 );
2570
2571 labeled_item
2572 })
2573 }
2574
2575 fn set_labeled_items<const COUNT: usize>(
2576 workspace: &ViewHandle<Workspace>,
2577 pane: &ViewHandle<Pane>,
2578 labels: [&str; COUNT],
2579 cx: &mut TestAppContext,
2580 ) -> [Box<ViewHandle<TestItem>>; COUNT] {
2581 pane.update(cx, |pane, _| {
2582 pane.items.clear();
2583 });
2584
2585 workspace.update(cx, |workspace, cx| {
2586 let mut active_item_index = 0;
2587
2588 let mut index = 0;
2589 let items = labels.map(|mut label| {
2590 if label.ends_with("*") {
2591 label = label.trim_end_matches("*");
2592 active_item_index = index;
2593 }
2594
2595 let labeled_item = Box::new(cx.add_view(|_| TestItem::new().with_label(label)));
2596 Pane::add_item(
2597 workspace,
2598 pane,
2599 labeled_item.clone(),
2600 false,
2601 false,
2602 None,
2603 cx,
2604 );
2605 index += 1;
2606 labeled_item
2607 });
2608
2609 pane.update(cx, |pane, cx| {
2610 pane.activate_item(active_item_index, false, false, cx)
2611 });
2612
2613 items
2614 })
2615 }
2616
2617 // Assert the item label, with the active item label suffixed with a '*'
2618 fn assert_item_labels<const COUNT: usize>(
2619 pane: &ViewHandle<Pane>,
2620 expected_states: [&str; COUNT],
2621 cx: &mut TestAppContext,
2622 ) {
2623 pane.read_with(cx, |pane, cx| {
2624 let actual_states = pane
2625 .items
2626 .iter()
2627 .enumerate()
2628 .map(|(ix, item)| {
2629 let mut state = item
2630 .as_any()
2631 .downcast_ref::<TestItem>()
2632 .unwrap()
2633 .read(cx)
2634 .label
2635 .clone();
2636 if ix == pane.active_item_index {
2637 state.push('*');
2638 }
2639 if item.is_dirty(cx) {
2640 state.push('^');
2641 }
2642 state
2643 })
2644 .collect::<Vec<_>>();
2645
2646 assert_eq!(
2647 actual_states, expected_states,
2648 "pane items do not match expectation"
2649 );
2650 })
2651 }
2652}