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