1mod dragged_item_receiver;
2
3use super::{ItemHandle, SplitDirection};
4use crate::{
5 item::WeakItemHandle, notify_of_new_dock, toolbar::Toolbar, AutosaveSetting, Item,
6 NewCenterTerminal, NewFile, 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 // Potentially warn the user of the new keybinding
540 let workspace_handle = self.workspace().clone();
541 cx.spawn(|_, mut cx| async move { notify_of_new_dock(&workspace_handle, &mut cx) })
542 .detach();
543
544 if self.zoomed {
545 cx.emit(Event::ZoomOut);
546 } else if !self.items.is_empty() {
547 if !self.has_focus {
548 cx.focus_self();
549 }
550 cx.emit(Event::ZoomIn);
551 }
552 }
553
554 pub fn activate_item(
555 &mut self,
556 index: usize,
557 activate_pane: bool,
558 focus_item: bool,
559 cx: &mut ViewContext<Self>,
560 ) {
561 use NavigationMode::{GoingBack, GoingForward};
562
563 if index < self.items.len() {
564 let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
565 if prev_active_item_ix != self.active_item_index
566 || matches!(self.nav_history.mode(), GoingBack | GoingForward)
567 {
568 if let Some(prev_item) = self.items.get(prev_active_item_ix) {
569 prev_item.deactivated(cx);
570 }
571
572 cx.emit(Event::ActivateItem {
573 local: activate_pane,
574 });
575 }
576
577 if let Some(newly_active_item) = self.items.get(index) {
578 self.activation_history
579 .retain(|&previously_active_item_id| {
580 previously_active_item_id != newly_active_item.id()
581 });
582 self.activation_history.push(newly_active_item.id());
583 }
584
585 self.update_toolbar(cx);
586
587 if focus_item {
588 self.focus_active_item(cx);
589 }
590
591 self.autoscroll = true;
592 cx.notify();
593 }
594 }
595
596 pub fn activate_prev_item(&mut self, activate_pane: bool, cx: &mut ViewContext<Self>) {
597 let mut index = self.active_item_index;
598 if index > 0 {
599 index -= 1;
600 } else if !self.items.is_empty() {
601 index = self.items.len() - 1;
602 }
603 self.activate_item(index, activate_pane, activate_pane, cx);
604 }
605
606 pub fn activate_next_item(&mut self, activate_pane: bool, cx: &mut ViewContext<Self>) {
607 let mut index = self.active_item_index;
608 if index + 1 < self.items.len() {
609 index += 1;
610 } else {
611 index = 0;
612 }
613 self.activate_item(index, activate_pane, activate_pane, cx);
614 }
615
616 pub fn close_active_item(
617 &mut self,
618 _: &CloseActiveItem,
619 cx: &mut ViewContext<Self>,
620 ) -> Option<Task<Result<()>>> {
621 if self.items.is_empty() {
622 return None;
623 }
624 let active_item_id = self.items[self.active_item_index].id();
625 Some(self.close_item_by_id(active_item_id, cx))
626 }
627
628 pub fn close_item_by_id(
629 &mut self,
630 item_id_to_close: usize,
631 cx: &mut ViewContext<Self>,
632 ) -> Task<Result<()>> {
633 self.close_items(cx, move |view_id| view_id == item_id_to_close)
634 }
635
636 pub fn close_inactive_items(
637 &mut self,
638 _: &CloseInactiveItems,
639 cx: &mut ViewContext<Self>,
640 ) -> Option<Task<Result<()>>> {
641 if self.items.is_empty() {
642 return None;
643 }
644
645 let active_item_id = self.items[self.active_item_index].id();
646 Some(self.close_items(cx, move |item_id| item_id != active_item_id))
647 }
648
649 pub fn close_clean_items(
650 &mut self,
651 _: &CloseCleanItems,
652 cx: &mut ViewContext<Self>,
653 ) -> Option<Task<Result<()>>> {
654 let item_ids: Vec<_> = self
655 .items()
656 .filter(|item| !item.is_dirty(cx))
657 .map(|item| item.id())
658 .collect();
659 Some(self.close_items(cx, move |item_id| item_ids.contains(&item_id)))
660 }
661
662 pub fn close_items_to_the_left(
663 &mut self,
664 _: &CloseItemsToTheLeft,
665 cx: &mut ViewContext<Self>,
666 ) -> Option<Task<Result<()>>> {
667 if self.items.is_empty() {
668 return None;
669 }
670 let active_item_id = self.items[self.active_item_index].id();
671 Some(self.close_items_to_the_left_by_id(active_item_id, cx))
672 }
673
674 pub fn close_items_to_the_left_by_id(
675 &mut self,
676 item_id: usize,
677 cx: &mut ViewContext<Self>,
678 ) -> Task<Result<()>> {
679 let item_ids: Vec<_> = self
680 .items()
681 .take_while(|item| item.id() != item_id)
682 .map(|item| item.id())
683 .collect();
684 self.close_items(cx, move |item_id| item_ids.contains(&item_id))
685 }
686
687 pub fn close_items_to_the_right(
688 &mut self,
689 _: &CloseItemsToTheRight,
690 cx: &mut ViewContext<Self>,
691 ) -> Option<Task<Result<()>>> {
692 if self.items.is_empty() {
693 return None;
694 }
695 let active_item_id = self.items[self.active_item_index].id();
696 Some(self.close_items_to_the_right_by_id(active_item_id, cx))
697 }
698
699 pub fn close_items_to_the_right_by_id(
700 &mut self,
701 item_id: usize,
702 cx: &mut ViewContext<Self>,
703 ) -> Task<Result<()>> {
704 let item_ids: Vec<_> = self
705 .items()
706 .rev()
707 .take_while(|item| item.id() != item_id)
708 .map(|item| item.id())
709 .collect();
710 self.close_items(cx, move |item_id| item_ids.contains(&item_id))
711 }
712
713 pub fn close_all_items(
714 &mut self,
715 _: &CloseAllItems,
716 cx: &mut ViewContext<Self>,
717 ) -> Option<Task<Result<()>>> {
718 Some(self.close_items(cx, move |_| true))
719 }
720
721 pub fn close_items(
722 &mut self,
723 cx: &mut ViewContext<Pane>,
724 should_close: impl 'static + Fn(usize) -> bool,
725 ) -> Task<Result<()>> {
726 // Find the items to close.
727 let mut items_to_close = Vec::new();
728 for item in &self.items {
729 if should_close(item.id()) {
730 items_to_close.push(item.boxed_clone());
731 }
732 }
733
734 // If a buffer is open both in a singleton editor and in a multibuffer, make sure
735 // to focus the singleton buffer when prompting to save that buffer, as opposed
736 // to focusing the multibuffer, because this gives the user a more clear idea
737 // of what content they would be saving.
738 items_to_close.sort_by_key(|item| !item.is_singleton(cx));
739
740 let workspace = self.workspace.clone();
741 cx.spawn(|pane, mut cx| async move {
742 let mut saved_project_items_ids = HashSet::default();
743 for item in items_to_close.clone() {
744 // Find the item's current index and its set of project item models. Avoid
745 // storing these in advance, in case they have changed since this task
746 // was started.
747 let (item_ix, mut project_item_ids) = pane.read_with(&cx, |pane, cx| {
748 (pane.index_for_item(&*item), item.project_item_model_ids(cx))
749 })?;
750 let item_ix = if let Some(ix) = item_ix {
751 ix
752 } else {
753 continue;
754 };
755
756 // Check if this view has any project items that are not open anywhere else
757 // in the workspace, AND that the user has not already been prompted to save.
758 // If there are any such project entries, prompt the user to save this item.
759 let project = workspace.read_with(&cx, |workspace, cx| {
760 for item in workspace.items(cx) {
761 if !items_to_close
762 .iter()
763 .any(|item_to_close| item_to_close.id() == item.id())
764 {
765 let other_project_item_ids = item.project_item_model_ids(cx);
766 project_item_ids.retain(|id| !other_project_item_ids.contains(id));
767 }
768 }
769 workspace.project().clone()
770 })?;
771 let should_save = project_item_ids
772 .iter()
773 .any(|id| saved_project_items_ids.insert(*id));
774
775 if should_save
776 && !Self::save_item(project.clone(), &pane, item_ix, &*item, true, &mut cx)
777 .await?
778 {
779 break;
780 }
781
782 // Remove the item from the pane.
783 pane.update(&mut cx, |pane, cx| {
784 if let Some(item_ix) = pane.items.iter().position(|i| i.id() == item.id()) {
785 pane.remove_item(item_ix, false, cx);
786 }
787 })?;
788 }
789
790 pane.update(&mut cx, |_, cx| cx.notify())?;
791 Ok(())
792 })
793 }
794
795 pub fn remove_item(
796 &mut self,
797 item_index: usize,
798 activate_pane: bool,
799 cx: &mut ViewContext<Self>,
800 ) {
801 self.activation_history
802 .retain(|&history_entry| history_entry != self.items[item_index].id());
803
804 if item_index == self.active_item_index {
805 let index_to_activate = self
806 .activation_history
807 .pop()
808 .and_then(|last_activated_item| {
809 self.items.iter().enumerate().find_map(|(index, item)| {
810 (item.id() == last_activated_item).then_some(index)
811 })
812 })
813 // We didn't have a valid activation history entry, so fallback
814 // to activating the item to the left
815 .unwrap_or_else(|| item_index.min(self.items.len()).saturating_sub(1));
816
817 let should_activate = activate_pane || self.has_focus;
818 self.activate_item(index_to_activate, should_activate, should_activate, cx);
819 }
820
821 let item = self.items.remove(item_index);
822
823 cx.emit(Event::RemoveItem { item_id: item.id() });
824 if self.items.is_empty() {
825 item.deactivated(cx);
826 self.update_toolbar(cx);
827 cx.emit(Event::Remove);
828 }
829
830 if item_index < self.active_item_index {
831 self.active_item_index -= 1;
832 }
833
834 self.nav_history.set_mode(NavigationMode::ClosingItem);
835 item.deactivated(cx);
836 self.nav_history.set_mode(NavigationMode::Normal);
837
838 if let Some(path) = item.project_path(cx) {
839 let abs_path = self
840 .nav_history
841 .0
842 .borrow()
843 .paths_by_item
844 .get(&item.id())
845 .and_then(|(_, abs_path)| abs_path.clone());
846 self.nav_history
847 .0
848 .borrow_mut()
849 .paths_by_item
850 .insert(item.id(), (path, abs_path));
851 } else {
852 self.nav_history
853 .0
854 .borrow_mut()
855 .paths_by_item
856 .remove(&item.id());
857 }
858
859 if self.items.is_empty() && self.zoomed {
860 cx.emit(Event::ZoomOut);
861 }
862
863 cx.notify();
864 }
865
866 pub async fn save_item(
867 project: ModelHandle<Project>,
868 pane: &WeakViewHandle<Pane>,
869 item_ix: usize,
870 item: &dyn ItemHandle,
871 should_prompt_for_save: bool,
872 cx: &mut AsyncAppContext,
873 ) -> Result<bool> {
874 const CONFLICT_MESSAGE: &str =
875 "This file has changed on disk since you started editing it. Do you want to overwrite it?";
876 const DIRTY_MESSAGE: &str = "This file contains unsaved edits. Do you want to save it?";
877
878 let (has_conflict, is_dirty, can_save, is_singleton) = cx.read(|cx| {
879 (
880 item.has_conflict(cx),
881 item.is_dirty(cx),
882 item.can_save(cx),
883 item.is_singleton(cx),
884 )
885 });
886
887 if has_conflict && can_save {
888 let mut answer = pane.update(cx, |pane, cx| {
889 pane.activate_item(item_ix, true, true, cx);
890 cx.prompt(
891 PromptLevel::Warning,
892 CONFLICT_MESSAGE,
893 &["Overwrite", "Discard", "Cancel"],
894 )
895 })?;
896 match answer.next().await {
897 Some(0) => pane.update(cx, |_, cx| item.save(project, cx))?.await?,
898 Some(1) => pane.update(cx, |_, cx| item.reload(project, cx))?.await?,
899 _ => return Ok(false),
900 }
901 } else if is_dirty && (can_save || is_singleton) {
902 let will_autosave = cx.read(|cx| {
903 matches!(
904 settings::get::<WorkspaceSettings>(cx).autosave,
905 AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange
906 ) && Self::can_autosave_item(&*item, cx)
907 });
908 let should_save = if should_prompt_for_save && !will_autosave {
909 let mut answer = pane.update(cx, |pane, cx| {
910 pane.activate_item(item_ix, true, true, cx);
911 cx.prompt(
912 PromptLevel::Warning,
913 DIRTY_MESSAGE,
914 &["Save", "Don't Save", "Cancel"],
915 )
916 })?;
917 match answer.next().await {
918 Some(0) => true,
919 Some(1) => false,
920 _ => return Ok(false),
921 }
922 } else {
923 true
924 };
925
926 if should_save {
927 if can_save {
928 pane.update(cx, |_, cx| item.save(project, cx))?.await?;
929 } else if is_singleton {
930 let start_abs_path = project
931 .read_with(cx, |project, cx| {
932 let worktree = project.visible_worktrees(cx).next()?;
933 Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
934 })
935 .unwrap_or_else(|| Path::new("").into());
936
937 let mut abs_path = cx.update(|cx| cx.prompt_for_new_path(&start_abs_path));
938 if let Some(abs_path) = abs_path.next().await.flatten() {
939 pane.update(cx, |_, cx| item.save_as(project, abs_path, cx))?
940 .await?;
941 } else {
942 return Ok(false);
943 }
944 }
945 }
946 }
947 Ok(true)
948 }
949
950 fn can_autosave_item(item: &dyn ItemHandle, cx: &AppContext) -> bool {
951 let is_deleted = item.project_entry_ids(cx).is_empty();
952 item.is_dirty(cx) && !item.has_conflict(cx) && item.can_save(cx) && !is_deleted
953 }
954
955 pub fn autosave_item(
956 item: &dyn ItemHandle,
957 project: ModelHandle<Project>,
958 cx: &mut WindowContext,
959 ) -> Task<Result<()>> {
960 if Self::can_autosave_item(item, cx) {
961 item.save(project, cx)
962 } else {
963 Task::ready(Ok(()))
964 }
965 }
966
967 pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
968 if let Some(active_item) = self.active_item() {
969 cx.focus(active_item.as_any());
970 }
971 }
972
973 pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
974 cx.emit(Event::Split(direction));
975 }
976
977 fn deploy_split_menu(&mut self, cx: &mut ViewContext<Self>) {
978 self.tab_bar_context_menu.handle.update(cx, |menu, cx| {
979 menu.show(
980 Default::default(),
981 AnchorCorner::TopRight,
982 vec![
983 ContextMenuItem::action("Split Right", SplitRight),
984 ContextMenuItem::action("Split Left", SplitLeft),
985 ContextMenuItem::action("Split Up", SplitUp),
986 ContextMenuItem::action("Split Down", SplitDown),
987 ],
988 cx,
989 );
990 });
991
992 self.tab_bar_context_menu.kind = TabBarContextMenuKind::Split;
993 }
994
995 fn deploy_new_menu(&mut self, cx: &mut ViewContext<Self>) {
996 self.tab_bar_context_menu.handle.update(cx, |menu, cx| {
997 menu.show(
998 Default::default(),
999 AnchorCorner::TopRight,
1000 vec![
1001 ContextMenuItem::action("New File", NewFile),
1002 ContextMenuItem::action("New Terminal", NewCenterTerminal),
1003 ContextMenuItem::action("New Search", NewSearch),
1004 ],
1005 cx,
1006 );
1007 });
1008
1009 self.tab_bar_context_menu.kind = TabBarContextMenuKind::New;
1010 }
1011
1012 fn deploy_tab_context_menu(
1013 &mut self,
1014 position: Vector2F,
1015 target_item_id: usize,
1016 cx: &mut ViewContext<Self>,
1017 ) {
1018 let active_item_id = self.items[self.active_item_index].id();
1019 let is_active_item = target_item_id == active_item_id;
1020 let target_pane = cx.weak_handle();
1021
1022 // 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
1023
1024 self.tab_context_menu.update(cx, |menu, cx| {
1025 menu.show(
1026 position,
1027 AnchorCorner::TopLeft,
1028 if is_active_item {
1029 vec![
1030 ContextMenuItem::action("Close Active Item", CloseActiveItem),
1031 ContextMenuItem::action("Close Inactive Items", CloseInactiveItems),
1032 ContextMenuItem::action("Close Clean Items", CloseCleanItems),
1033 ContextMenuItem::action("Close Items To The Left", CloseItemsToTheLeft),
1034 ContextMenuItem::action("Close Items To The Right", CloseItemsToTheRight),
1035 ContextMenuItem::action("Close All Items", CloseAllItems),
1036 ]
1037 } else {
1038 // 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.
1039 vec![
1040 ContextMenuItem::handler("Close Inactive Item", {
1041 let pane = target_pane.clone();
1042 move |cx| {
1043 if let Some(pane) = pane.upgrade(cx) {
1044 pane.update(cx, |pane, cx| {
1045 pane.close_item_by_id(target_item_id, cx)
1046 .detach_and_log_err(cx);
1047 })
1048 }
1049 }
1050 }),
1051 ContextMenuItem::action("Close Inactive Items", CloseInactiveItems),
1052 ContextMenuItem::action("Close Clean Items", CloseCleanItems),
1053 ContextMenuItem::handler("Close Items To The Left", {
1054 let pane = target_pane.clone();
1055 move |cx| {
1056 if let Some(pane) = pane.upgrade(cx) {
1057 pane.update(cx, |pane, cx| {
1058 pane.close_items_to_the_left_by_id(target_item_id, cx)
1059 .detach_and_log_err(cx);
1060 })
1061 }
1062 }
1063 }),
1064 ContextMenuItem::handler("Close Items To The Right", {
1065 let pane = target_pane.clone();
1066 move |cx| {
1067 if let Some(pane) = pane.upgrade(cx) {
1068 pane.update(cx, |pane, cx| {
1069 pane.close_items_to_the_right_by_id(target_item_id, cx)
1070 .detach_and_log_err(cx);
1071 })
1072 }
1073 }
1074 }),
1075 ContextMenuItem::action("Close All Items", CloseAllItems),
1076 ]
1077 },
1078 cx,
1079 );
1080 });
1081 }
1082
1083 pub fn toolbar(&self) -> &ViewHandle<Toolbar> {
1084 &self.toolbar
1085 }
1086
1087 pub fn handle_deleted_project_item(
1088 &mut self,
1089 entry_id: ProjectEntryId,
1090 cx: &mut ViewContext<Pane>,
1091 ) -> Option<()> {
1092 let (item_index_to_delete, item_id) = self.items().enumerate().find_map(|(i, item)| {
1093 if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
1094 Some((i, item.id()))
1095 } else {
1096 None
1097 }
1098 })?;
1099
1100 self.remove_item(item_index_to_delete, false, cx);
1101 self.nav_history.remove_item(item_id);
1102
1103 Some(())
1104 }
1105
1106 fn update_toolbar(&mut self, cx: &mut ViewContext<Self>) {
1107 let active_item = self
1108 .items
1109 .get(self.active_item_index)
1110 .map(|item| item.as_ref());
1111 self.toolbar.update(cx, |toolbar, cx| {
1112 toolbar.set_active_pane_item(active_item, cx);
1113 });
1114 }
1115
1116 fn render_tabs(&mut self, cx: &mut ViewContext<Self>) -> impl Element<Self> {
1117 let theme = theme::current(cx).clone();
1118
1119 let pane = cx.handle().downgrade();
1120 let autoscroll = if mem::take(&mut self.autoscroll) {
1121 Some(self.active_item_index)
1122 } else {
1123 None
1124 };
1125
1126 let pane_active = self.has_focus;
1127
1128 enum Tabs {}
1129 let mut row = Flex::row().scrollable::<Tabs>(1, autoscroll, cx);
1130 for (ix, (item, detail)) in self
1131 .items
1132 .iter()
1133 .cloned()
1134 .zip(self.tab_details(cx))
1135 .enumerate()
1136 {
1137 let detail = if detail == 0 { None } else { Some(detail) };
1138 let tab_active = ix == self.active_item_index;
1139
1140 row.add_child({
1141 enum TabDragReceiver {}
1142 let mut receiver =
1143 dragged_item_receiver::<TabDragReceiver, _, _>(self, ix, ix, true, None, cx, {
1144 let item = item.clone();
1145 let pane = pane.clone();
1146 let detail = detail.clone();
1147
1148 let theme = theme::current(cx).clone();
1149 let mut tooltip_theme = theme.tooltip.clone();
1150 tooltip_theme.max_text_width = None;
1151 let tab_tooltip_text = item.tab_tooltip_text(cx).map(|a| a.to_string());
1152
1153 move |mouse_state, cx| {
1154 let tab_style =
1155 theme.workspace.tab_bar.tab_style(pane_active, tab_active);
1156 let hovered = mouse_state.hovered();
1157
1158 enum Tab {}
1159 let mouse_event_handler =
1160 MouseEventHandler::<Tab, Pane>::new(ix, cx, |_, cx| {
1161 Self::render_tab(
1162 &item,
1163 pane.clone(),
1164 ix == 0,
1165 detail,
1166 hovered,
1167 tab_style,
1168 cx,
1169 )
1170 })
1171 .on_down(MouseButton::Left, move |_, this, cx| {
1172 this.activate_item(ix, true, true, cx);
1173 })
1174 .on_click(MouseButton::Middle, {
1175 let item_id = item.id();
1176 move |_, pane, cx| {
1177 pane.close_item_by_id(item_id, cx).detach_and_log_err(cx);
1178 }
1179 })
1180 .on_down(
1181 MouseButton::Right,
1182 move |event, pane, cx| {
1183 pane.deploy_tab_context_menu(event.position, item.id(), cx);
1184 },
1185 );
1186
1187 if let Some(tab_tooltip_text) = tab_tooltip_text {
1188 mouse_event_handler
1189 .with_tooltip::<Self>(
1190 ix,
1191 tab_tooltip_text,
1192 None,
1193 tooltip_theme,
1194 cx,
1195 )
1196 .into_any()
1197 } else {
1198 mouse_event_handler.into_any()
1199 }
1200 }
1201 });
1202
1203 if !pane_active || !tab_active {
1204 receiver = receiver.with_cursor_style(CursorStyle::PointingHand);
1205 }
1206
1207 receiver.as_draggable(
1208 DraggedItem {
1209 handle: item,
1210 pane: pane.clone(),
1211 },
1212 {
1213 let theme = theme::current(cx).clone();
1214
1215 let detail = detail.clone();
1216 move |dragged_item: &DraggedItem, cx: &mut ViewContext<Workspace>| {
1217 let tab_style = &theme.workspace.tab_bar.dragged_tab;
1218 Self::render_dragged_tab(
1219 &dragged_item.handle,
1220 dragged_item.pane.clone(),
1221 false,
1222 detail,
1223 false,
1224 &tab_style,
1225 cx,
1226 )
1227 }
1228 },
1229 )
1230 })
1231 }
1232
1233 // Use the inactive tab style along with the current pane's active status to decide how to render
1234 // the filler
1235 let filler_index = self.items.len();
1236 let filler_style = theme.workspace.tab_bar.tab_style(pane_active, false);
1237 enum Filler {}
1238 row.add_child(
1239 dragged_item_receiver::<Filler, _, _>(self, 0, filler_index, true, None, cx, |_, _| {
1240 Empty::new()
1241 .contained()
1242 .with_style(filler_style.container)
1243 .with_border(filler_style.container.border)
1244 })
1245 .flex(1., true)
1246 .into_any_named("filler"),
1247 );
1248
1249 row
1250 }
1251
1252 fn tab_details(&self, cx: &AppContext) -> Vec<usize> {
1253 let mut tab_details = (0..self.items.len()).map(|_| 0).collect::<Vec<_>>();
1254
1255 let mut tab_descriptions = HashMap::default();
1256 let mut done = false;
1257 while !done {
1258 done = true;
1259
1260 // Store item indices by their tab description.
1261 for (ix, (item, detail)) in self.items.iter().zip(&tab_details).enumerate() {
1262 if let Some(description) = item.tab_description(*detail, cx) {
1263 if *detail == 0
1264 || Some(&description) != item.tab_description(detail - 1, cx).as_ref()
1265 {
1266 tab_descriptions
1267 .entry(description)
1268 .or_insert(Vec::new())
1269 .push(ix);
1270 }
1271 }
1272 }
1273
1274 // If two or more items have the same tab description, increase their level
1275 // of detail and try again.
1276 for (_, item_ixs) in tab_descriptions.drain() {
1277 if item_ixs.len() > 1 {
1278 done = false;
1279 for ix in item_ixs {
1280 tab_details[ix] += 1;
1281 }
1282 }
1283 }
1284 }
1285
1286 tab_details
1287 }
1288
1289 fn render_tab(
1290 item: &Box<dyn ItemHandle>,
1291 pane: WeakViewHandle<Pane>,
1292 first: bool,
1293 detail: Option<usize>,
1294 hovered: bool,
1295 tab_style: &theme::Tab,
1296 cx: &mut ViewContext<Self>,
1297 ) -> AnyElement<Self> {
1298 let title = item.tab_content(detail, &tab_style, cx);
1299 Self::render_tab_with_title(title, item, pane, first, hovered, tab_style, cx)
1300 }
1301
1302 fn render_dragged_tab(
1303 item: &Box<dyn ItemHandle>,
1304 pane: WeakViewHandle<Pane>,
1305 first: bool,
1306 detail: Option<usize>,
1307 hovered: bool,
1308 tab_style: &theme::Tab,
1309 cx: &mut ViewContext<Workspace>,
1310 ) -> AnyElement<Workspace> {
1311 let title = item.dragged_tab_content(detail, &tab_style, cx);
1312 Self::render_tab_with_title(title, item, pane, first, hovered, tab_style, cx)
1313 }
1314
1315 fn render_tab_with_title<T: View>(
1316 title: AnyElement<T>,
1317 item: &Box<dyn ItemHandle>,
1318 pane: WeakViewHandle<Pane>,
1319 first: bool,
1320 hovered: bool,
1321 tab_style: &theme::Tab,
1322 cx: &mut ViewContext<T>,
1323 ) -> AnyElement<T> {
1324 let mut container = tab_style.container.clone();
1325 if first {
1326 container.border.left = false;
1327 }
1328
1329 Flex::row()
1330 .with_child({
1331 let diameter = 7.0;
1332 let icon_color = if item.has_conflict(cx) {
1333 Some(tab_style.icon_conflict)
1334 } else if item.is_dirty(cx) {
1335 Some(tab_style.icon_dirty)
1336 } else {
1337 None
1338 };
1339
1340 Canvas::new(move |scene, bounds, _, _, _| {
1341 if let Some(color) = icon_color {
1342 let square = RectF::new(bounds.origin(), vec2f(diameter, diameter));
1343 scene.push_quad(Quad {
1344 bounds: square,
1345 background: Some(color),
1346 border: Default::default(),
1347 corner_radius: diameter / 2.,
1348 });
1349 }
1350 })
1351 .constrained()
1352 .with_width(diameter)
1353 .with_height(diameter)
1354 .aligned()
1355 })
1356 .with_child(title.aligned().contained().with_style(ContainerStyle {
1357 margin: Margin {
1358 left: tab_style.spacing,
1359 right: tab_style.spacing,
1360 ..Default::default()
1361 },
1362 ..Default::default()
1363 }))
1364 .with_child(
1365 if hovered {
1366 let item_id = item.id();
1367 enum TabCloseButton {}
1368 let icon = Svg::new("icons/x_mark_8.svg");
1369 MouseEventHandler::<TabCloseButton, _>::new(item_id, cx, |mouse_state, _| {
1370 if mouse_state.hovered() {
1371 icon.with_color(tab_style.icon_close_active)
1372 } else {
1373 icon.with_color(tab_style.icon_close)
1374 }
1375 })
1376 .with_padding(Padding::uniform(4.))
1377 .with_cursor_style(CursorStyle::PointingHand)
1378 .on_click(MouseButton::Left, {
1379 let pane = pane.clone();
1380 move |_, _, cx| {
1381 let pane = pane.clone();
1382 cx.window_context().defer(move |cx| {
1383 if let Some(pane) = pane.upgrade(cx) {
1384 pane.update(cx, |pane, cx| {
1385 pane.close_item_by_id(item_id, cx).detach_and_log_err(cx);
1386 });
1387 }
1388 });
1389 }
1390 })
1391 .into_any_named("close-tab-icon")
1392 .constrained()
1393 } else {
1394 Empty::new().constrained()
1395 }
1396 .with_width(tab_style.close_icon_width)
1397 .aligned(),
1398 )
1399 .contained()
1400 .with_style(container)
1401 .constrained()
1402 .with_height(tab_style.height)
1403 .into_any()
1404 }
1405
1406 pub fn render_tab_bar_button<F: 'static + Fn(&mut Pane, &mut EventContext<Pane>)>(
1407 index: usize,
1408 icon: &'static str,
1409 tooltip: Option<(String, Option<Box<dyn Action>>)>,
1410 cx: &mut ViewContext<Pane>,
1411 on_click: F,
1412 context_menu: Option<ViewHandle<ContextMenu>>,
1413 ) -> AnyElement<Pane> {
1414 enum TabBarButton {}
1415
1416 let mut button = MouseEventHandler::<TabBarButton, _>::new(index, cx, |mouse_state, cx| {
1417 let theme = &settings::get::<ThemeSettings>(cx).theme.workspace.tab_bar;
1418 let style = theme.pane_button.style_for(mouse_state, false);
1419 Svg::new(icon)
1420 .with_color(style.color)
1421 .constrained()
1422 .with_width(style.icon_width)
1423 .aligned()
1424 .constrained()
1425 .with_width(style.button_width)
1426 .with_height(style.button_width)
1427 })
1428 .with_cursor_style(CursorStyle::PointingHand)
1429 .on_click(MouseButton::Left, move |_, pane, cx| on_click(pane, cx))
1430 .into_any();
1431 if let Some((tooltip, action)) = tooltip {
1432 let tooltip_style = settings::get::<ThemeSettings>(cx).theme.tooltip.clone();
1433 button = button
1434 .with_tooltip::<TabBarButton>(index, tooltip, action, tooltip_style, cx)
1435 .into_any();
1436 }
1437
1438 Stack::new()
1439 .with_child(button)
1440 .with_children(
1441 context_menu.map(|menu| ChildView::new(&menu, cx).aligned().bottom().right()),
1442 )
1443 .flex(1., false)
1444 .into_any_named("tab bar button")
1445 }
1446
1447 fn render_blank_pane(&self, theme: &Theme, _cx: &mut ViewContext<Self>) -> AnyElement<Self> {
1448 let background = theme.workspace.background;
1449 Empty::new()
1450 .contained()
1451 .with_background_color(background)
1452 .into_any()
1453 }
1454
1455 pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
1456 self.zoomed = zoomed;
1457 cx.notify();
1458 }
1459
1460 pub fn is_zoomed(&self) -> bool {
1461 self.zoomed
1462 }
1463}
1464
1465impl Entity for Pane {
1466 type Event = Event;
1467}
1468
1469impl View for Pane {
1470 fn ui_name() -> &'static str {
1471 "Pane"
1472 }
1473
1474 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
1475 enum MouseNavigationHandler {}
1476
1477 MouseEventHandler::<MouseNavigationHandler, _>::new(0, cx, |_, cx| {
1478 let active_item_index = self.active_item_index;
1479
1480 if let Some(active_item) = self.active_item() {
1481 Flex::column()
1482 .with_child({
1483 let theme = theme::current(cx).clone();
1484
1485 let mut stack = Stack::new();
1486
1487 enum TabBarEventHandler {}
1488 stack.add_child(
1489 MouseEventHandler::<TabBarEventHandler, _>::new(0, cx, |_, _| {
1490 Empty::new()
1491 .contained()
1492 .with_style(theme.workspace.tab_bar.container)
1493 })
1494 .on_down(
1495 MouseButton::Left,
1496 move |_, this, cx| {
1497 this.activate_item(active_item_index, true, true, cx);
1498 },
1499 ),
1500 );
1501
1502 let mut tab_row = Flex::row()
1503 .with_child(self.render_tabs(cx).flex(1., true).into_any_named("tabs"));
1504
1505 if self.has_focus {
1506 let render_tab_bar_buttons = self.render_tab_bar_buttons.clone();
1507 tab_row.add_child(
1508 (render_tab_bar_buttons)(self, cx)
1509 .contained()
1510 .with_style(theme.workspace.tab_bar.pane_button_container)
1511 .flex(1., false)
1512 .into_any(),
1513 )
1514 }
1515
1516 stack.add_child(tab_row);
1517 stack
1518 .constrained()
1519 .with_height(theme.workspace.tab_bar.height)
1520 .flex(1., false)
1521 .into_any_named("tab bar")
1522 })
1523 .with_child({
1524 enum PaneContentTabDropTarget {}
1525 dragged_item_receiver::<PaneContentTabDropTarget, _, _>(
1526 self,
1527 0,
1528 self.active_item_index + 1,
1529 !self.can_split,
1530 if self.can_split { Some(100.) } else { None },
1531 cx,
1532 {
1533 let toolbar = self.toolbar.clone();
1534 let toolbar_hidden = toolbar.read(cx).hidden();
1535 move |_, cx| {
1536 Flex::column()
1537 .with_children(
1538 (!toolbar_hidden)
1539 .then(|| ChildView::new(&toolbar, cx).expanded()),
1540 )
1541 .with_child(
1542 ChildView::new(active_item.as_any(), cx).flex(1., true),
1543 )
1544 }
1545 },
1546 )
1547 .flex(1., true)
1548 })
1549 .with_child(ChildView::new(&self.tab_context_menu, cx))
1550 .into_any()
1551 } else {
1552 enum EmptyPane {}
1553 let theme = theme::current(cx).clone();
1554
1555 dragged_item_receiver::<EmptyPane, _, _>(self, 0, 0, false, None, cx, |_, cx| {
1556 self.render_blank_pane(&theme, cx)
1557 })
1558 .on_down(MouseButton::Left, |_, _, cx| {
1559 cx.focus_parent();
1560 })
1561 .into_any()
1562 }
1563 })
1564 .on_down(
1565 MouseButton::Navigate(NavigationDirection::Back),
1566 move |_, pane, cx| {
1567 if let Some(workspace) = pane.workspace.upgrade(cx) {
1568 let pane = cx.weak_handle();
1569 cx.window_context().defer(move |cx| {
1570 workspace.update(cx, |workspace, cx| {
1571 workspace.go_back(pane, cx).detach_and_log_err(cx)
1572 })
1573 })
1574 }
1575 },
1576 )
1577 .on_down(MouseButton::Navigate(NavigationDirection::Forward), {
1578 move |_, pane, cx| {
1579 if let Some(workspace) = pane.workspace.upgrade(cx) {
1580 let pane = cx.weak_handle();
1581 cx.window_context().defer(move |cx| {
1582 workspace.update(cx, |workspace, cx| {
1583 workspace.go_forward(pane, cx).detach_and_log_err(cx)
1584 })
1585 })
1586 }
1587 }
1588 })
1589 .into_any_named("pane")
1590 }
1591
1592 fn focus_in(&mut self, focused: AnyViewHandle, cx: &mut ViewContext<Self>) {
1593 if !self.has_focus {
1594 self.has_focus = true;
1595 cx.emit(Event::Focus);
1596 cx.notify();
1597 }
1598
1599 self.toolbar.update(cx, |toolbar, cx| {
1600 toolbar.pane_focus_update(true, cx);
1601 });
1602
1603 if let Some(active_item) = self.active_item() {
1604 if cx.is_self_focused() {
1605 // Pane was focused directly. We need to either focus a view inside the active item,
1606 // or focus the active item itself
1607 if let Some(weak_last_focused_view) =
1608 self.last_focused_view_by_item.get(&active_item.id())
1609 {
1610 if let Some(last_focused_view) = weak_last_focused_view.upgrade(cx) {
1611 cx.focus(&last_focused_view);
1612 return;
1613 } else {
1614 self.last_focused_view_by_item.remove(&active_item.id());
1615 }
1616 }
1617
1618 cx.focus(active_item.as_any());
1619 } else if focused != self.tab_bar_context_menu.handle {
1620 self.last_focused_view_by_item
1621 .insert(active_item.id(), focused.downgrade());
1622 }
1623 }
1624 }
1625
1626 fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
1627 self.has_focus = false;
1628 self.toolbar.update(cx, |toolbar, cx| {
1629 toolbar.pane_focus_update(false, cx);
1630 });
1631 cx.notify();
1632 }
1633
1634 fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) {
1635 Self::reset_to_default_keymap_context(keymap);
1636 }
1637}
1638
1639impl ItemNavHistory {
1640 pub fn push<D: 'static + Any>(&mut self, data: Option<D>, cx: &mut WindowContext) {
1641 self.history.push(data, self.item.clone(), cx);
1642 }
1643
1644 pub fn pop_backward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
1645 self.history.pop(NavigationMode::GoingBack, cx)
1646 }
1647
1648 pub fn pop_forward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
1649 self.history.pop(NavigationMode::GoingForward, cx)
1650 }
1651}
1652
1653impl NavHistory {
1654 pub fn for_each_entry(
1655 &self,
1656 cx: &AppContext,
1657 mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
1658 ) {
1659 let borrowed_history = self.0.borrow();
1660 borrowed_history
1661 .forward_stack
1662 .iter()
1663 .chain(borrowed_history.backward_stack.iter())
1664 .chain(borrowed_history.closed_stack.iter())
1665 .for_each(|entry| {
1666 if let Some(project_and_abs_path) =
1667 borrowed_history.paths_by_item.get(&entry.item.id())
1668 {
1669 f(entry, project_and_abs_path.clone());
1670 } else if let Some(item) = entry.item.upgrade(cx) {
1671 if let Some(path) = item.project_path(cx) {
1672 f(entry, (path, None));
1673 }
1674 }
1675 })
1676 }
1677
1678 pub fn set_mode(&mut self, mode: NavigationMode) {
1679 self.0.borrow_mut().mode = mode;
1680 }
1681
1682 pub fn mode(&self) -> NavigationMode {
1683 self.0.borrow().mode
1684 }
1685
1686 pub fn disable(&mut self) {
1687 self.0.borrow_mut().mode = NavigationMode::Disabled;
1688 }
1689
1690 pub fn enable(&mut self) {
1691 self.0.borrow_mut().mode = NavigationMode::Normal;
1692 }
1693
1694 pub fn pop(&mut self, mode: NavigationMode, cx: &mut WindowContext) -> Option<NavigationEntry> {
1695 let mut state = self.0.borrow_mut();
1696 let entry = match mode {
1697 NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
1698 return None
1699 }
1700 NavigationMode::GoingBack => &mut state.backward_stack,
1701 NavigationMode::GoingForward => &mut state.forward_stack,
1702 NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
1703 }
1704 .pop_back();
1705 if entry.is_some() {
1706 state.did_update(cx);
1707 }
1708 entry
1709 }
1710
1711 pub fn push<D: 'static + Any>(
1712 &mut self,
1713 data: Option<D>,
1714 item: Rc<dyn WeakItemHandle>,
1715 cx: &mut WindowContext,
1716 ) {
1717 let state = &mut *self.0.borrow_mut();
1718 match state.mode {
1719 NavigationMode::Disabled => {}
1720 NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
1721 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
1722 state.backward_stack.pop_front();
1723 }
1724 state.backward_stack.push_back(NavigationEntry {
1725 item,
1726 data: data.map(|data| Box::new(data) as Box<dyn Any>),
1727 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
1728 });
1729 state.forward_stack.clear();
1730 }
1731 NavigationMode::GoingBack => {
1732 if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
1733 state.forward_stack.pop_front();
1734 }
1735 state.forward_stack.push_back(NavigationEntry {
1736 item,
1737 data: data.map(|data| Box::new(data) as Box<dyn Any>),
1738 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
1739 });
1740 }
1741 NavigationMode::GoingForward => {
1742 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
1743 state.backward_stack.pop_front();
1744 }
1745 state.backward_stack.push_back(NavigationEntry {
1746 item,
1747 data: data.map(|data| Box::new(data) as Box<dyn Any>),
1748 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
1749 });
1750 }
1751 NavigationMode::ClosingItem => {
1752 if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
1753 state.closed_stack.pop_front();
1754 }
1755 state.closed_stack.push_back(NavigationEntry {
1756 item,
1757 data: data.map(|data| Box::new(data) as Box<dyn Any>),
1758 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
1759 });
1760 }
1761 }
1762 state.did_update(cx);
1763 }
1764
1765 pub fn remove_item(&mut self, item_id: usize) {
1766 let mut state = self.0.borrow_mut();
1767 state.paths_by_item.remove(&item_id);
1768 state
1769 .backward_stack
1770 .retain(|entry| entry.item.id() != item_id);
1771 state
1772 .forward_stack
1773 .retain(|entry| entry.item.id() != item_id);
1774 state
1775 .closed_stack
1776 .retain(|entry| entry.item.id() != item_id);
1777 }
1778
1779 pub fn path_for_item(&self, item_id: usize) -> Option<(ProjectPath, Option<PathBuf>)> {
1780 self.0.borrow().paths_by_item.get(&item_id).cloned()
1781 }
1782}
1783
1784impl NavHistoryState {
1785 pub fn did_update(&self, cx: &mut WindowContext) {
1786 if let Some(pane) = self.pane.upgrade(cx) {
1787 cx.defer(move |cx| {
1788 pane.update(cx, |pane, cx| pane.history_updated(cx));
1789 });
1790 }
1791 }
1792}
1793
1794pub struct PaneBackdrop<V: View> {
1795 child_view: usize,
1796 child: AnyElement<V>,
1797}
1798
1799impl<V: View> PaneBackdrop<V> {
1800 pub fn new(pane_item_view: usize, child: AnyElement<V>) -> Self {
1801 PaneBackdrop {
1802 child,
1803 child_view: pane_item_view,
1804 }
1805 }
1806}
1807
1808impl<V: View> Element<V> for PaneBackdrop<V> {
1809 type LayoutState = ();
1810
1811 type PaintState = ();
1812
1813 fn layout(
1814 &mut self,
1815 constraint: gpui::SizeConstraint,
1816 view: &mut V,
1817 cx: &mut LayoutContext<V>,
1818 ) -> (Vector2F, Self::LayoutState) {
1819 let size = self.child.layout(constraint, view, cx);
1820 (size, ())
1821 }
1822
1823 fn paint(
1824 &mut self,
1825 scene: &mut gpui::SceneBuilder,
1826 bounds: RectF,
1827 visible_bounds: RectF,
1828 _: &mut Self::LayoutState,
1829 view: &mut V,
1830 cx: &mut ViewContext<V>,
1831 ) -> Self::PaintState {
1832 let background = theme::current(cx).editor.background;
1833
1834 let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
1835
1836 scene.push_quad(gpui::Quad {
1837 bounds: RectF::new(bounds.origin(), bounds.size()),
1838 background: Some(background),
1839 ..Default::default()
1840 });
1841
1842 let child_view_id = self.child_view;
1843 scene.push_mouse_region(
1844 MouseRegion::new::<Self>(child_view_id, 0, visible_bounds).on_down(
1845 gpui::platform::MouseButton::Left,
1846 move |_, _: &mut V, cx| {
1847 let window_id = cx.window_id();
1848 cx.app_context().focus(window_id, Some(child_view_id))
1849 },
1850 ),
1851 );
1852
1853 scene.paint_layer(Some(bounds), |scene| {
1854 self.child
1855 .paint(scene, bounds.origin(), visible_bounds, view, cx)
1856 })
1857 }
1858
1859 fn rect_for_text_range(
1860 &self,
1861 range_utf16: std::ops::Range<usize>,
1862 _bounds: RectF,
1863 _visible_bounds: RectF,
1864 _layout: &Self::LayoutState,
1865 _paint: &Self::PaintState,
1866 view: &V,
1867 cx: &gpui::ViewContext<V>,
1868 ) -> Option<RectF> {
1869 self.child.rect_for_text_range(range_utf16, view, cx)
1870 }
1871
1872 fn debug(
1873 &self,
1874 _bounds: RectF,
1875 _layout: &Self::LayoutState,
1876 _paint: &Self::PaintState,
1877 view: &V,
1878 cx: &gpui::ViewContext<V>,
1879 ) -> serde_json::Value {
1880 gpui::json::json!({
1881 "type": "Pane Back Drop",
1882 "view": self.child_view,
1883 "child": self.child.debug(view, cx),
1884 })
1885 }
1886}
1887
1888#[cfg(test)]
1889mod tests {
1890 use super::*;
1891 use crate::item::test::{TestItem, TestProjectItem};
1892 use gpui::TestAppContext;
1893 use project::FakeFs;
1894 use settings::SettingsStore;
1895
1896 #[gpui::test]
1897 async fn test_remove_active_empty(cx: &mut TestAppContext) {
1898 init_test(cx);
1899 let fs = FakeFs::new(cx.background());
1900
1901 let project = Project::test(fs, None, cx).await;
1902 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
1903 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
1904
1905 pane.update(cx, |pane, cx| {
1906 assert!(pane.close_active_item(&CloseActiveItem, cx).is_none())
1907 });
1908 }
1909
1910 #[gpui::test]
1911 async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
1912 cx.foreground().forbid_parking();
1913 init_test(cx);
1914 let fs = FakeFs::new(cx.background());
1915
1916 let project = Project::test(fs, None, cx).await;
1917 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
1918 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
1919
1920 // 1. Add with a destination index
1921 // a. Add before the active item
1922 set_labeled_items(&pane, ["A", "B*", "C"], cx);
1923 pane.update(cx, |pane, cx| {
1924 pane.add_item(
1925 Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
1926 false,
1927 false,
1928 Some(0),
1929 cx,
1930 );
1931 });
1932 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
1933
1934 // b. Add after the active item
1935 set_labeled_items(&pane, ["A", "B*", "C"], cx);
1936 pane.update(cx, |pane, cx| {
1937 pane.add_item(
1938 Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
1939 false,
1940 false,
1941 Some(2),
1942 cx,
1943 );
1944 });
1945 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
1946
1947 // c. Add at the end of the item list (including off the length)
1948 set_labeled_items(&pane, ["A", "B*", "C"], cx);
1949 pane.update(cx, |pane, cx| {
1950 pane.add_item(
1951 Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
1952 false,
1953 false,
1954 Some(5),
1955 cx,
1956 );
1957 });
1958 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
1959
1960 // 2. Add without a destination index
1961 // a. Add with active item at the start of the item list
1962 set_labeled_items(&pane, ["A*", "B", "C"], cx);
1963 pane.update(cx, |pane, cx| {
1964 pane.add_item(
1965 Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
1966 false,
1967 false,
1968 None,
1969 cx,
1970 );
1971 });
1972 set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
1973
1974 // b. Add with active item at the end of the item list
1975 set_labeled_items(&pane, ["A", "B", "C*"], cx);
1976 pane.update(cx, |pane, cx| {
1977 pane.add_item(
1978 Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
1979 false,
1980 false,
1981 None,
1982 cx,
1983 );
1984 });
1985 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
1986 }
1987
1988 #[gpui::test]
1989 async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
1990 cx.foreground().forbid_parking();
1991 init_test(cx);
1992 let fs = FakeFs::new(cx.background());
1993
1994 let project = Project::test(fs, None, cx).await;
1995 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
1996 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
1997
1998 // 1. Add with a destination index
1999 // 1a. Add before the active item
2000 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2001 pane.update(cx, |pane, cx| {
2002 pane.add_item(d, false, false, Some(0), cx);
2003 });
2004 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
2005
2006 // 1b. Add after the active item
2007 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2008 pane.update(cx, |pane, cx| {
2009 pane.add_item(d, false, false, Some(2), cx);
2010 });
2011 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
2012
2013 // 1c. Add at the end of the item list (including off the length)
2014 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2015 pane.update(cx, |pane, cx| {
2016 pane.add_item(a, false, false, Some(5), cx);
2017 });
2018 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
2019
2020 // 1d. Add same item to active index
2021 let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
2022 pane.update(cx, |pane, cx| {
2023 pane.add_item(b, false, false, Some(1), cx);
2024 });
2025 assert_item_labels(&pane, ["A", "B*", "C"], cx);
2026
2027 // 1e. Add item to index after same item in last position
2028 let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
2029 pane.update(cx, |pane, cx| {
2030 pane.add_item(c, false, false, Some(2), cx);
2031 });
2032 assert_item_labels(&pane, ["A", "B", "C*"], cx);
2033
2034 // 2. Add without a destination index
2035 // 2a. Add with active item at the start of the item list
2036 let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
2037 pane.update(cx, |pane, cx| {
2038 pane.add_item(d, false, false, None, cx);
2039 });
2040 assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
2041
2042 // 2b. Add with active item at the end of the item list
2043 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
2044 pane.update(cx, |pane, cx| {
2045 pane.add_item(a, false, false, None, cx);
2046 });
2047 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
2048
2049 // 2c. Add active item to active item at end of list
2050 let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
2051 pane.update(cx, |pane, cx| {
2052 pane.add_item(c, false, false, None, cx);
2053 });
2054 assert_item_labels(&pane, ["A", "B", "C*"], cx);
2055
2056 // 2d. Add active item to active item at start of list
2057 let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
2058 pane.update(cx, |pane, cx| {
2059 pane.add_item(a, false, false, None, cx);
2060 });
2061 assert_item_labels(&pane, ["A*", "B", "C"], cx);
2062 }
2063
2064 #[gpui::test]
2065 async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
2066 cx.foreground().forbid_parking();
2067 init_test(cx);
2068 let fs = FakeFs::new(cx.background());
2069
2070 let project = Project::test(fs, None, cx).await;
2071 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2072 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2073
2074 // singleton view
2075 pane.update(cx, |pane, cx| {
2076 let item = TestItem::new()
2077 .with_singleton(true)
2078 .with_label("buffer 1")
2079 .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)]);
2080
2081 pane.add_item(Box::new(cx.add_view(|_| item)), false, false, None, cx);
2082 });
2083 assert_item_labels(&pane, ["buffer 1*"], cx);
2084
2085 // new singleton view with the same project entry
2086 pane.update(cx, |pane, cx| {
2087 let item = TestItem::new()
2088 .with_singleton(true)
2089 .with_label("buffer 1")
2090 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]);
2091
2092 pane.add_item(Box::new(cx.add_view(|_| item)), false, false, None, cx);
2093 });
2094 assert_item_labels(&pane, ["buffer 1*"], cx);
2095
2096 // new singleton view with different project entry
2097 pane.update(cx, |pane, cx| {
2098 let item = TestItem::new()
2099 .with_singleton(true)
2100 .with_label("buffer 2")
2101 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)]);
2102 pane.add_item(Box::new(cx.add_view(|_| item)), false, false, None, cx);
2103 });
2104 assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
2105
2106 // new multibuffer view with the same project entry
2107 pane.update(cx, |pane, cx| {
2108 let item = TestItem::new()
2109 .with_singleton(false)
2110 .with_label("multibuffer 1")
2111 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]);
2112
2113 pane.add_item(Box::new(cx.add_view(|_| item)), false, false, None, cx);
2114 });
2115 assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
2116
2117 // another multibuffer view with the same project entry
2118 pane.update(cx, |pane, cx| {
2119 let item = TestItem::new()
2120 .with_singleton(false)
2121 .with_label("multibuffer 1b")
2122 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]);
2123
2124 pane.add_item(Box::new(cx.add_view(|_| item)), false, false, None, cx);
2125 });
2126 assert_item_labels(
2127 &pane,
2128 ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
2129 cx,
2130 );
2131 }
2132
2133 #[gpui::test]
2134 async fn test_remove_item_ordering(cx: &mut TestAppContext) {
2135 init_test(cx);
2136 let fs = FakeFs::new(cx.background());
2137
2138 let project = Project::test(fs, None, cx).await;
2139 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2140 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2141
2142 add_labeled_item(&pane, "A", false, cx);
2143 add_labeled_item(&pane, "B", false, cx);
2144 add_labeled_item(&pane, "C", false, cx);
2145 add_labeled_item(&pane, "D", false, cx);
2146 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2147
2148 pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
2149 add_labeled_item(&pane, "1", false, cx);
2150 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
2151
2152 pane.update(cx, |pane, cx| pane.close_active_item(&CloseActiveItem, cx))
2153 .unwrap()
2154 .await
2155 .unwrap();
2156 assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
2157
2158 pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
2159 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2160
2161 pane.update(cx, |pane, cx| pane.close_active_item(&CloseActiveItem, cx))
2162 .unwrap()
2163 .await
2164 .unwrap();
2165 assert_item_labels(&pane, ["A", "B*", "C"], cx);
2166
2167 pane.update(cx, |pane, cx| pane.close_active_item(&CloseActiveItem, cx))
2168 .unwrap()
2169 .await
2170 .unwrap();
2171 assert_item_labels(&pane, ["A", "C*"], cx);
2172
2173 pane.update(cx, |pane, cx| pane.close_active_item(&CloseActiveItem, cx))
2174 .unwrap()
2175 .await
2176 .unwrap();
2177 assert_item_labels(&pane, ["A*"], cx);
2178 }
2179
2180 #[gpui::test]
2181 async fn test_close_inactive_items(cx: &mut TestAppContext) {
2182 init_test(cx);
2183 let fs = FakeFs::new(cx.background());
2184
2185 let project = Project::test(fs, None, cx).await;
2186 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2187 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2188
2189 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
2190
2191 pane.update(cx, |pane, cx| {
2192 pane.close_inactive_items(&CloseInactiveItems, cx)
2193 })
2194 .unwrap()
2195 .await
2196 .unwrap();
2197 assert_item_labels(&pane, ["C*"], cx);
2198 }
2199
2200 #[gpui::test]
2201 async fn test_close_clean_items(cx: &mut TestAppContext) {
2202 init_test(cx);
2203 let fs = FakeFs::new(cx.background());
2204
2205 let project = Project::test(fs, None, cx).await;
2206 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2207 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2208
2209 add_labeled_item(&pane, "A", true, cx);
2210 add_labeled_item(&pane, "B", false, cx);
2211 add_labeled_item(&pane, "C", true, cx);
2212 add_labeled_item(&pane, "D", false, cx);
2213 add_labeled_item(&pane, "E", false, cx);
2214 assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
2215
2216 pane.update(cx, |pane, cx| pane.close_clean_items(&CloseCleanItems, cx))
2217 .unwrap()
2218 .await
2219 .unwrap();
2220 assert_item_labels(&pane, ["A^", "C*^"], cx);
2221 }
2222
2223 #[gpui::test]
2224 async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
2225 init_test(cx);
2226 let fs = FakeFs::new(cx.background());
2227
2228 let project = Project::test(fs, None, cx).await;
2229 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2230 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2231
2232 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
2233
2234 pane.update(cx, |pane, cx| {
2235 pane.close_items_to_the_left(&CloseItemsToTheLeft, cx)
2236 })
2237 .unwrap()
2238 .await
2239 .unwrap();
2240 assert_item_labels(&pane, ["C*", "D", "E"], cx);
2241 }
2242
2243 #[gpui::test]
2244 async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
2245 init_test(cx);
2246 let fs = FakeFs::new(cx.background());
2247
2248 let project = Project::test(fs, None, cx).await;
2249 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2250 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2251
2252 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
2253
2254 pane.update(cx, |pane, cx| {
2255 pane.close_items_to_the_right(&CloseItemsToTheRight, cx)
2256 })
2257 .unwrap()
2258 .await
2259 .unwrap();
2260 assert_item_labels(&pane, ["A", "B", "C*"], cx);
2261 }
2262
2263 #[gpui::test]
2264 async fn test_close_all_items(cx: &mut TestAppContext) {
2265 init_test(cx);
2266 let fs = FakeFs::new(cx.background());
2267
2268 let project = Project::test(fs, None, cx).await;
2269 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2270 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2271
2272 add_labeled_item(&pane, "A", false, cx);
2273 add_labeled_item(&pane, "B", false, cx);
2274 add_labeled_item(&pane, "C", false, cx);
2275 assert_item_labels(&pane, ["A", "B", "C*"], cx);
2276
2277 pane.update(cx, |pane, cx| pane.close_all_items(&CloseAllItems, cx))
2278 .unwrap()
2279 .await
2280 .unwrap();
2281 assert_item_labels(&pane, [], cx);
2282 }
2283
2284 fn init_test(cx: &mut TestAppContext) {
2285 cx.update(|cx| {
2286 cx.set_global(SettingsStore::test(cx));
2287 theme::init((), cx);
2288 crate::init_settings(cx);
2289 });
2290 }
2291
2292 fn add_labeled_item(
2293 pane: &ViewHandle<Pane>,
2294 label: &str,
2295 is_dirty: bool,
2296 cx: &mut TestAppContext,
2297 ) -> Box<ViewHandle<TestItem>> {
2298 pane.update(cx, |pane, cx| {
2299 let labeled_item =
2300 Box::new(cx.add_view(|_| TestItem::new().with_label(label).with_dirty(is_dirty)));
2301 pane.add_item(labeled_item.clone(), false, false, None, cx);
2302 labeled_item
2303 })
2304 }
2305
2306 fn set_labeled_items<const COUNT: usize>(
2307 pane: &ViewHandle<Pane>,
2308 labels: [&str; COUNT],
2309 cx: &mut TestAppContext,
2310 ) -> [Box<ViewHandle<TestItem>>; COUNT] {
2311 pane.update(cx, |pane, cx| {
2312 pane.items.clear();
2313 let mut active_item_index = 0;
2314
2315 let mut index = 0;
2316 let items = labels.map(|mut label| {
2317 if label.ends_with("*") {
2318 label = label.trim_end_matches("*");
2319 active_item_index = index;
2320 }
2321
2322 let labeled_item = Box::new(cx.add_view(|_| TestItem::new().with_label(label)));
2323 pane.add_item(labeled_item.clone(), false, false, None, cx);
2324 index += 1;
2325 labeled_item
2326 });
2327
2328 pane.activate_item(active_item_index, false, false, cx);
2329
2330 items
2331 })
2332 }
2333
2334 // Assert the item label, with the active item label suffixed with a '*'
2335 fn assert_item_labels<const COUNT: usize>(
2336 pane: &ViewHandle<Pane>,
2337 expected_states: [&str; COUNT],
2338 cx: &mut TestAppContext,
2339 ) {
2340 pane.read_with(cx, |pane, cx| {
2341 let actual_states = pane
2342 .items
2343 .iter()
2344 .enumerate()
2345 .map(|(ix, item)| {
2346 let mut state = item
2347 .as_any()
2348 .downcast_ref::<TestItem>()
2349 .unwrap()
2350 .read(cx)
2351 .label
2352 .clone();
2353 if ix == pane.active_item_index {
2354 state.push('*');
2355 }
2356 if item.is_dirty(cx) {
2357 state.push('^');
2358 }
2359 state
2360 })
2361 .collect::<Vec<_>>();
2362
2363 assert_eq!(
2364 actual_states, expected_states,
2365 "pane items do not match expectation"
2366 );
2367 })
2368 }
2369}