1use crate::{
2 item::{
3 ClosePosition, Item, ItemHandle, ItemSettings, PreviewTabsSettings, TabContentParams,
4 WeakItemHandle,
5 },
6 toolbar::Toolbar,
7 workspace_settings::{AutosaveSetting, TabBarSettings, WorkspaceSettings},
8 CloseWindow, NewCenterTerminal, NewFile, NewSearch, OpenInTerminal, OpenTerminal, OpenVisible,
9 SplitDirection, ToggleZoom, Workspace,
10};
11use anyhow::Result;
12use collections::{HashMap, HashSet, VecDeque};
13use futures::{stream::FuturesUnordered, StreamExt};
14use gpui::{
15 actions, anchored, deferred, impl_actions, prelude::*, Action, AnchorCorner, AnyElement,
16 AppContext, AsyncWindowContext, ClickEvent, DismissEvent, Div, DragMoveEvent, EntityId,
17 EventEmitter, ExternalPaths, FocusHandle, FocusableView, KeyContext, Model, MouseButton,
18 MouseDownEvent, NavigationDirection, Pixels, Point, PromptLevel, Render, ScrollHandle,
19 Subscription, Task, View, ViewContext, VisualContext, WeakFocusHandle, WeakView, WindowContext,
20};
21use itertools::Itertools;
22use parking_lot::Mutex;
23use project::{Project, ProjectEntryId, ProjectPath};
24use serde::Deserialize;
25use settings::{Settings, SettingsStore};
26use std::{
27 any::Any,
28 cmp, fmt, mem,
29 ops::ControlFlow,
30 path::PathBuf,
31 rc::Rc,
32 sync::{
33 atomic::{AtomicUsize, Ordering},
34 Arc,
35 },
36};
37use theme::ThemeSettings;
38
39use ui::{
40 prelude::*, right_click_menu, ButtonSize, Color, IconButton, IconButtonShape, IconName,
41 IconSize, Indicator, Label, Tab, TabBar, TabPosition, Tooltip,
42};
43use ui::{v_flex, ContextMenu};
44use util::{debug_panic, maybe, truncate_and_remove_front, ResultExt};
45
46#[derive(PartialEq, Clone, Copy, Deserialize, Debug)]
47#[serde(rename_all = "camelCase")]
48pub enum SaveIntent {
49 /// write all files (even if unchanged)
50 /// prompt before overwriting on-disk changes
51 Save,
52 /// same as Save, but without auto formatting
53 SaveWithoutFormat,
54 /// write any files that have local changes
55 /// prompt before overwriting on-disk changes
56 SaveAll,
57 /// always prompt for a new path
58 SaveAs,
59 /// prompt "you have unsaved changes" before writing
60 Close,
61 /// write all dirty files, don't prompt on conflict
62 Overwrite,
63 /// skip all save-related behavior
64 Skip,
65}
66
67#[derive(Clone, Deserialize, PartialEq, Debug)]
68pub struct ActivateItem(pub usize);
69
70#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
71#[serde(rename_all = "camelCase")]
72pub struct CloseActiveItem {
73 pub save_intent: Option<SaveIntent>,
74}
75
76#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
77#[serde(rename_all = "camelCase")]
78pub struct CloseInactiveItems {
79 pub save_intent: Option<SaveIntent>,
80}
81
82#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
83#[serde(rename_all = "camelCase")]
84pub struct CloseAllItems {
85 pub save_intent: Option<SaveIntent>,
86}
87
88#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
89#[serde(rename_all = "camelCase")]
90pub struct RevealInProjectPanel {
91 pub entry_id: Option<u64>,
92}
93
94#[derive(PartialEq, Clone, Deserialize)]
95pub struct DeploySearch {
96 #[serde(default)]
97 pub replace_enabled: bool,
98}
99
100impl_actions!(
101 pane,
102 [
103 CloseAllItems,
104 CloseActiveItem,
105 CloseInactiveItems,
106 ActivateItem,
107 RevealInProjectPanel,
108 DeploySearch,
109 ]
110);
111
112actions!(
113 pane,
114 [
115 ActivatePrevItem,
116 ActivateNextItem,
117 ActivateLastItem,
118 AlternateFile,
119 CloseCleanItems,
120 CloseItemsToTheLeft,
121 CloseItemsToTheRight,
122 GoBack,
123 GoForward,
124 ReopenClosedItem,
125 SplitLeft,
126 SplitUp,
127 SplitRight,
128 SplitDown,
129 TogglePreviewTab,
130 ]
131);
132
133impl DeploySearch {
134 pub fn find() -> Self {
135 Self {
136 replace_enabled: false,
137 }
138 }
139}
140
141const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
142
143pub enum Event {
144 AddItem { item: Box<dyn ItemHandle> },
145 ActivateItem { local: bool },
146 Remove,
147 RemoveItem { item_id: EntityId },
148 Split(SplitDirection),
149 ChangeItemTitle,
150 Focus,
151 ZoomIn,
152 ZoomOut,
153}
154
155impl fmt::Debug for Event {
156 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157 match self {
158 Event::AddItem { item } => f
159 .debug_struct("AddItem")
160 .field("item", &item.item_id())
161 .finish(),
162 Event::ActivateItem { local } => f
163 .debug_struct("ActivateItem")
164 .field("local", local)
165 .finish(),
166 Event::Remove => f.write_str("Remove"),
167 Event::RemoveItem { item_id } => f
168 .debug_struct("RemoveItem")
169 .field("item_id", item_id)
170 .finish(),
171 Event::Split(direction) => f
172 .debug_struct("Split")
173 .field("direction", direction)
174 .finish(),
175 Event::ChangeItemTitle => f.write_str("ChangeItemTitle"),
176 Event::Focus => f.write_str("Focus"),
177 Event::ZoomIn => f.write_str("ZoomIn"),
178 Event::ZoomOut => f.write_str("ZoomOut"),
179 }
180 }
181}
182
183/// A container for 0 to many items that are open in the workspace.
184/// Treats all items uniformly via the [`ItemHandle`] trait, whether it's an editor, search results multibuffer, terminal or something else,
185/// responsible for managing item tabs, focus and zoom states and drag and drop features.
186/// Can be split, see `PaneGroup` for more details.
187pub struct Pane {
188 alternate_file_items: (
189 Option<Box<dyn WeakItemHandle>>,
190 Option<Box<dyn WeakItemHandle>>,
191 ),
192 focus_handle: FocusHandle,
193 items: Vec<Box<dyn ItemHandle>>,
194 activation_history: Vec<ActivationHistoryEntry>,
195 next_activation_timestamp: Arc<AtomicUsize>,
196 zoomed: bool,
197 was_focused: bool,
198 active_item_index: usize,
199 preview_item_id: Option<EntityId>,
200 last_focus_handle_by_item: HashMap<EntityId, WeakFocusHandle>,
201 nav_history: NavHistory,
202 toolbar: View<Toolbar>,
203 pub new_item_menu: Option<View<ContextMenu>>,
204 split_item_menu: Option<View<ContextMenu>>,
205 // tab_context_menu: View<ContextMenu>,
206 pub(crate) workspace: WeakView<Workspace>,
207 project: Model<Project>,
208 drag_split_direction: Option<SplitDirection>,
209 can_drop_predicate: Option<Arc<dyn Fn(&dyn Any, &mut WindowContext) -> bool>>,
210 custom_drop_handle:
211 Option<Arc<dyn Fn(&mut Pane, &dyn Any, &mut ViewContext<Pane>) -> ControlFlow<(), ()>>>,
212 can_split: bool,
213 should_display_tab_bar: Rc<dyn Fn(&ViewContext<Pane>) -> bool>,
214 render_tab_bar_buttons: Rc<dyn Fn(&mut Pane, &mut ViewContext<Pane>) -> AnyElement>,
215 _subscriptions: Vec<Subscription>,
216 tab_bar_scroll_handle: ScrollHandle,
217 /// Is None if navigation buttons are permanently turned off (and should not react to setting changes).
218 /// Otherwise, when `display_nav_history_buttons` is Some, it determines whether nav buttons should be displayed.
219 display_nav_history_buttons: Option<bool>,
220 double_click_dispatch_action: Box<dyn Action>,
221 save_modals_spawned: HashSet<EntityId>,
222}
223
224pub struct ActivationHistoryEntry {
225 pub entity_id: EntityId,
226 pub timestamp: usize,
227}
228
229pub struct ItemNavHistory {
230 history: NavHistory,
231 item: Arc<dyn WeakItemHandle>,
232 is_preview: bool,
233}
234
235#[derive(Clone)]
236pub struct NavHistory(Arc<Mutex<NavHistoryState>>);
237
238struct NavHistoryState {
239 mode: NavigationMode,
240 backward_stack: VecDeque<NavigationEntry>,
241 forward_stack: VecDeque<NavigationEntry>,
242 closed_stack: VecDeque<NavigationEntry>,
243 paths_by_item: HashMap<EntityId, (ProjectPath, Option<PathBuf>)>,
244 pane: WeakView<Pane>,
245 next_timestamp: Arc<AtomicUsize>,
246}
247
248#[derive(Debug, Copy, Clone)]
249pub enum NavigationMode {
250 Normal,
251 GoingBack,
252 GoingForward,
253 ClosingItem,
254 ReopeningClosedItem,
255 Disabled,
256}
257
258impl Default for NavigationMode {
259 fn default() -> Self {
260 Self::Normal
261 }
262}
263
264pub struct NavigationEntry {
265 pub item: Arc<dyn WeakItemHandle>,
266 pub data: Option<Box<dyn Any + Send>>,
267 pub timestamp: usize,
268 pub is_preview: bool,
269}
270
271#[derive(Clone)]
272pub struct DraggedTab {
273 pub pane: View<Pane>,
274 pub item: Box<dyn ItemHandle>,
275 pub ix: usize,
276 pub detail: usize,
277 pub is_active: bool,
278}
279
280impl EventEmitter<Event> for Pane {}
281
282impl Pane {
283 pub fn new(
284 workspace: WeakView<Workspace>,
285 project: Model<Project>,
286 next_timestamp: Arc<AtomicUsize>,
287 can_drop_predicate: Option<Arc<dyn Fn(&dyn Any, &mut WindowContext) -> bool + 'static>>,
288 double_click_dispatch_action: Box<dyn Action>,
289 cx: &mut ViewContext<Self>,
290 ) -> Self {
291 let focus_handle = cx.focus_handle();
292
293 let subscriptions = vec![
294 cx.on_focus(&focus_handle, Pane::focus_in),
295 cx.on_focus_in(&focus_handle, Pane::focus_in),
296 cx.on_focus_out(&focus_handle, Pane::focus_out),
297 cx.observe_global::<SettingsStore>(Self::settings_changed),
298 ];
299
300 let handle = cx.view().downgrade();
301 Self {
302 alternate_file_items: (None, None),
303 focus_handle,
304 items: Vec::new(),
305 activation_history: Vec::new(),
306 next_activation_timestamp: next_timestamp.clone(),
307 was_focused: false,
308 zoomed: false,
309 active_item_index: 0,
310 preview_item_id: None,
311 last_focus_handle_by_item: Default::default(),
312 nav_history: NavHistory(Arc::new(Mutex::new(NavHistoryState {
313 mode: NavigationMode::Normal,
314 backward_stack: Default::default(),
315 forward_stack: Default::default(),
316 closed_stack: Default::default(),
317 paths_by_item: Default::default(),
318 pane: handle.clone(),
319 next_timestamp,
320 }))),
321 toolbar: cx.new_view(|_| Toolbar::new()),
322 new_item_menu: None,
323 split_item_menu: None,
324 tab_bar_scroll_handle: ScrollHandle::new(),
325 drag_split_direction: None,
326 workspace,
327 project,
328 can_drop_predicate,
329 custom_drop_handle: None,
330 can_split: true,
331 should_display_tab_bar: Rc::new(|cx| TabBarSettings::get_global(cx).show),
332 render_tab_bar_buttons: Rc::new(move |pane, cx| {
333 // Ideally we would return a vec of elements here to pass directly to the [TabBar]'s
334 // `end_slot`, but due to needing a view here that isn't possible.
335 h_flex()
336 // Instead we need to replicate the spacing from the [TabBar]'s `end_slot` here.
337 .gap(Spacing::Small.rems(cx))
338 .child(
339 IconButton::new("plus", IconName::Plus)
340 .icon_size(IconSize::Small)
341 .on_click(cx.listener(|pane, _, cx| {
342 let menu = ContextMenu::build(cx, |menu, _| {
343 menu.action("New File", NewFile.boxed_clone())
344 .action("New Terminal", NewCenterTerminal.boxed_clone())
345 .action("New Search", NewSearch.boxed_clone())
346 });
347 cx.subscribe(&menu, |pane, _, _: &DismissEvent, cx| {
348 pane.focus(cx);
349 pane.new_item_menu = None;
350 })
351 .detach();
352 pane.new_item_menu = Some(menu);
353 }))
354 .tooltip(|cx| Tooltip::text("New...", cx)),
355 )
356 .when_some(pane.new_item_menu.as_ref(), |el, new_item_menu| {
357 el.child(Self::render_menu_overlay(new_item_menu))
358 })
359 .child(
360 IconButton::new("split", IconName::Split)
361 .icon_size(IconSize::Small)
362 .on_click(cx.listener(|pane, _, cx| {
363 let menu = ContextMenu::build(cx, |menu, _| {
364 menu.action("Split Right", SplitRight.boxed_clone())
365 .action("Split Left", SplitLeft.boxed_clone())
366 .action("Split Up", SplitUp.boxed_clone())
367 .action("Split Down", SplitDown.boxed_clone())
368 });
369 cx.subscribe(&menu, |pane, _, _: &DismissEvent, cx| {
370 pane.focus(cx);
371 pane.split_item_menu = None;
372 })
373 .detach();
374 pane.split_item_menu = Some(menu);
375 }))
376 .tooltip(|cx| Tooltip::text("Split Pane", cx)),
377 )
378 .child({
379 let zoomed = pane.is_zoomed();
380 IconButton::new("toggle_zoom", IconName::Maximize)
381 .icon_size(IconSize::Small)
382 .selected(zoomed)
383 .selected_icon(IconName::Minimize)
384 .on_click(cx.listener(|pane, _, cx| {
385 pane.toggle_zoom(&crate::ToggleZoom, cx);
386 }))
387 .tooltip(move |cx| {
388 Tooltip::for_action(
389 if zoomed { "Zoom Out" } else { "Zoom In" },
390 &ToggleZoom,
391 cx,
392 )
393 })
394 })
395 .when_some(pane.split_item_menu.as_ref(), |el, split_item_menu| {
396 el.child(Self::render_menu_overlay(split_item_menu))
397 })
398 .into_any_element()
399 }),
400 display_nav_history_buttons: Some(
401 TabBarSettings::get_global(cx).show_nav_history_buttons,
402 ),
403 _subscriptions: subscriptions,
404 double_click_dispatch_action,
405 save_modals_spawned: HashSet::default(),
406 }
407 }
408
409 fn alternate_file(&mut self, cx: &mut ViewContext<Pane>) {
410 let (_, alternative) = &self.alternate_file_items;
411 if let Some(alternative) = alternative {
412 let existing = self
413 .items()
414 .find_position(|item| item.item_id() == alternative.id());
415 if let Some((ix, _)) = existing {
416 self.activate_item(ix, true, true, cx);
417 } else {
418 if let Some(upgraded) = alternative.upgrade() {
419 self.add_item(upgraded, true, true, None, cx);
420 }
421 }
422 }
423 }
424
425 pub fn track_alternate_file_items(&mut self) {
426 if let Some(item) = self.active_item().map(|item| item.downgrade_item()) {
427 let (current, _) = &self.alternate_file_items;
428 match current {
429 Some(current) => {
430 if current.id() != item.id() {
431 self.alternate_file_items =
432 (Some(item), self.alternate_file_items.0.take());
433 }
434 }
435 None => {
436 self.alternate_file_items = (Some(item), None);
437 }
438 }
439 }
440 }
441
442 pub fn has_focus(&self, cx: &WindowContext) -> bool {
443 // We not only check whether our focus handle contains focus, but also
444 // whether the active_item might have focus, because we might have just activated an item
445 // but that hasn't rendered yet.
446 // So before the next render, we might have transferred focus
447 // to the item and `focus_handle.contains_focus` returns false because the `active_item`
448 // is not hooked up to us in the dispatch tree.
449 self.focus_handle.contains_focused(cx)
450 || self
451 .active_item()
452 .map_or(false, |item| item.focus_handle(cx).contains_focused(cx))
453 }
454
455 fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
456 if !self.was_focused {
457 self.was_focused = true;
458 cx.emit(Event::Focus);
459 cx.notify();
460 }
461
462 self.toolbar.update(cx, |toolbar, cx| {
463 toolbar.focus_changed(true, cx);
464 });
465
466 if let Some(active_item) = self.active_item() {
467 if self.focus_handle.is_focused(cx) {
468 // Pane was focused directly. We need to either focus a view inside the active item,
469 // or focus the active item itself
470 if let Some(weak_last_focus_handle) =
471 self.last_focus_handle_by_item.get(&active_item.item_id())
472 {
473 if let Some(focus_handle) = weak_last_focus_handle.upgrade() {
474 focus_handle.focus(cx);
475 return;
476 }
477 }
478
479 active_item.focus_handle(cx).focus(cx);
480 } else if let Some(focused) = cx.focused() {
481 if !self.context_menu_focused(cx) {
482 self.last_focus_handle_by_item
483 .insert(active_item.item_id(), focused.downgrade());
484 }
485 }
486 }
487 }
488
489 fn context_menu_focused(&self, cx: &mut ViewContext<Self>) -> bool {
490 self.new_item_menu
491 .as_ref()
492 .or(self.split_item_menu.as_ref())
493 .map_or(false, |menu| menu.focus_handle(cx).is_focused(cx))
494 }
495
496 fn focus_out(&mut self, cx: &mut ViewContext<Self>) {
497 self.was_focused = false;
498 self.toolbar.update(cx, |toolbar, cx| {
499 toolbar.focus_changed(false, cx);
500 });
501 cx.notify();
502 }
503
504 fn settings_changed(&mut self, cx: &mut ViewContext<Self>) {
505 if let Some(display_nav_history_buttons) = self.display_nav_history_buttons.as_mut() {
506 *display_nav_history_buttons = TabBarSettings::get_global(cx).show_nav_history_buttons;
507 }
508 if !PreviewTabsSettings::get_global(cx).enabled {
509 self.preview_item_id = None;
510 }
511 cx.notify();
512 }
513
514 pub fn active_item_index(&self) -> usize {
515 self.active_item_index
516 }
517
518 pub fn activation_history(&self) -> &[ActivationHistoryEntry] {
519 &self.activation_history
520 }
521
522 pub fn set_should_display_tab_bar<F>(&mut self, should_display_tab_bar: F)
523 where
524 F: 'static + Fn(&ViewContext<Pane>) -> bool,
525 {
526 self.should_display_tab_bar = Rc::new(should_display_tab_bar);
527 }
528
529 pub fn set_can_split(&mut self, can_split: bool, cx: &mut ViewContext<Self>) {
530 self.can_split = can_split;
531 cx.notify();
532 }
533
534 pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut ViewContext<Self>) {
535 self.toolbar.update(cx, |toolbar, cx| {
536 toolbar.set_can_navigate(can_navigate, cx);
537 });
538 cx.notify();
539 }
540
541 pub fn set_render_tab_bar_buttons<F>(&mut self, cx: &mut ViewContext<Self>, render: F)
542 where
543 F: 'static + Fn(&mut Pane, &mut ViewContext<Pane>) -> AnyElement,
544 {
545 self.render_tab_bar_buttons = Rc::new(render);
546 cx.notify();
547 }
548
549 pub fn set_custom_drop_handle<F>(&mut self, cx: &mut ViewContext<Self>, handle: F)
550 where
551 F: 'static + Fn(&mut Pane, &dyn Any, &mut ViewContext<Pane>) -> ControlFlow<(), ()>,
552 {
553 self.custom_drop_handle = Some(Arc::new(handle));
554 cx.notify();
555 }
556
557 pub fn nav_history_for_item<T: Item>(&self, item: &View<T>) -> ItemNavHistory {
558 ItemNavHistory {
559 history: self.nav_history.clone(),
560 item: Arc::new(item.downgrade()),
561 is_preview: self.preview_item_id == Some(item.item_id()),
562 }
563 }
564
565 pub fn nav_history(&self) -> &NavHistory {
566 &self.nav_history
567 }
568
569 pub fn nav_history_mut(&mut self) -> &mut NavHistory {
570 &mut self.nav_history
571 }
572
573 pub fn disable_history(&mut self) {
574 self.nav_history.disable();
575 }
576
577 pub fn enable_history(&mut self) {
578 self.nav_history.enable();
579 }
580
581 pub fn can_navigate_backward(&self) -> bool {
582 !self.nav_history.0.lock().backward_stack.is_empty()
583 }
584
585 pub fn can_navigate_forward(&self) -> bool {
586 !self.nav_history.0.lock().forward_stack.is_empty()
587 }
588
589 fn navigate_backward(&mut self, cx: &mut ViewContext<Self>) {
590 if let Some(workspace) = self.workspace.upgrade() {
591 let pane = cx.view().downgrade();
592 cx.window_context().defer(move |cx| {
593 workspace.update(cx, |workspace, cx| {
594 workspace.go_back(pane, cx).detach_and_log_err(cx)
595 })
596 })
597 }
598 }
599
600 fn navigate_forward(&mut self, cx: &mut ViewContext<Self>) {
601 if let Some(workspace) = self.workspace.upgrade() {
602 let pane = cx.view().downgrade();
603 cx.window_context().defer(move |cx| {
604 workspace.update(cx, |workspace, cx| {
605 workspace.go_forward(pane, cx).detach_and_log_err(cx)
606 })
607 })
608 }
609 }
610
611 fn history_updated(&mut self, cx: &mut ViewContext<Self>) {
612 self.toolbar.update(cx, |_, cx| cx.notify());
613 }
614
615 pub fn preview_item_id(&self) -> Option<EntityId> {
616 self.preview_item_id
617 }
618
619 fn preview_item_idx(&self) -> Option<usize> {
620 if let Some(preview_item_id) = self.preview_item_id {
621 self.items
622 .iter()
623 .position(|item| item.item_id() == preview_item_id)
624 } else {
625 None
626 }
627 }
628
629 pub fn is_active_preview_item(&self, item_id: EntityId) -> bool {
630 self.preview_item_id == Some(item_id)
631 }
632
633 /// Marks the item with the given ID as the preview item.
634 /// This will be ignored if the global setting `preview_tabs` is disabled.
635 pub fn set_preview_item_id(&mut self, item_id: Option<EntityId>, cx: &AppContext) {
636 if PreviewTabsSettings::get_global(cx).enabled {
637 self.preview_item_id = item_id;
638 }
639 }
640
641 pub fn handle_item_edit(&mut self, item_id: EntityId, cx: &AppContext) {
642 if let Some(preview_item_id) = self.preview_item_id {
643 if preview_item_id == item_id {
644 self.set_preview_item_id(None, cx)
645 }
646 }
647 }
648
649 pub(crate) fn open_item(
650 &mut self,
651 project_entry_id: Option<ProjectEntryId>,
652 focus_item: bool,
653 allow_preview: bool,
654 cx: &mut ViewContext<Self>,
655 build_item: impl FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
656 ) -> Box<dyn ItemHandle> {
657 let mut existing_item = None;
658 if let Some(project_entry_id) = project_entry_id {
659 for (index, item) in self.items.iter().enumerate() {
660 if item.is_singleton(cx)
661 && item.project_entry_ids(cx).as_slice() == [project_entry_id]
662 {
663 let item = item.boxed_clone();
664 existing_item = Some((index, item));
665 break;
666 }
667 }
668 }
669
670 if let Some((index, existing_item)) = existing_item {
671 // If the item is already open, and the item is a preview item
672 // and we are not allowing items to open as preview, mark the item as persistent.
673 if let Some(preview_item_id) = self.preview_item_id {
674 if let Some(tab) = self.items.get(index) {
675 if tab.item_id() == preview_item_id && !allow_preview {
676 self.set_preview_item_id(None, cx);
677 }
678 }
679 }
680
681 self.activate_item(index, focus_item, focus_item, cx);
682 existing_item
683 } else {
684 // If the item is being opened as preview and we have an existing preview tab,
685 // open the new item in the position of the existing preview tab.
686 let destination_index = if allow_preview {
687 self.close_current_preview_item(cx)
688 } else {
689 None
690 };
691
692 let new_item = build_item(cx);
693
694 if allow_preview {
695 self.set_preview_item_id(Some(new_item.item_id()), cx);
696 }
697
698 self.add_item(new_item.clone(), true, focus_item, destination_index, cx);
699
700 new_item
701 }
702 }
703
704 pub fn close_current_preview_item(&mut self, cx: &mut ViewContext<Self>) -> Option<usize> {
705 let Some(item_idx) = self.preview_item_idx() else {
706 return None;
707 };
708
709 let prev_active_item_index = self.active_item_index;
710 self.remove_item(item_idx, false, false, cx);
711 self.active_item_index = prev_active_item_index;
712
713 if item_idx < self.items.len() {
714 Some(item_idx)
715 } else {
716 None
717 }
718 }
719
720 pub fn add_item(
721 &mut self,
722 item: Box<dyn ItemHandle>,
723 activate_pane: bool,
724 focus_item: bool,
725 destination_index: Option<usize>,
726 cx: &mut ViewContext<Self>,
727 ) {
728 if item.is_singleton(cx) {
729 if let Some(&entry_id) = item.project_entry_ids(cx).get(0) {
730 let project = self.project.read(cx);
731 if let Some(project_path) = project.path_for_entry(entry_id, cx) {
732 let abs_path = project.absolute_path(&project_path, cx);
733 self.nav_history
734 .0
735 .lock()
736 .paths_by_item
737 .insert(item.item_id(), (project_path, abs_path));
738 }
739 }
740 }
741 // If no destination index is specified, add or move the item after the active item.
742 let mut insertion_index = {
743 cmp::min(
744 if let Some(destination_index) = destination_index {
745 destination_index
746 } else {
747 self.active_item_index + 1
748 },
749 self.items.len(),
750 )
751 };
752
753 // Does the item already exist?
754 let project_entry_id = if item.is_singleton(cx) {
755 item.project_entry_ids(cx).get(0).copied()
756 } else {
757 None
758 };
759
760 let existing_item_index = self.items.iter().position(|existing_item| {
761 if existing_item.item_id() == item.item_id() {
762 true
763 } else if existing_item.is_singleton(cx) {
764 existing_item
765 .project_entry_ids(cx)
766 .get(0)
767 .map_or(false, |existing_entry_id| {
768 Some(existing_entry_id) == project_entry_id.as_ref()
769 })
770 } else {
771 false
772 }
773 });
774
775 if let Some(existing_item_index) = existing_item_index {
776 // If the item already exists, move it to the desired destination and activate it
777
778 if existing_item_index != insertion_index {
779 let existing_item_is_active = existing_item_index == self.active_item_index;
780
781 // If the caller didn't specify a destination and the added item is already
782 // the active one, don't move it
783 if existing_item_is_active && destination_index.is_none() {
784 insertion_index = existing_item_index;
785 } else {
786 self.items.remove(existing_item_index);
787 if existing_item_index < self.active_item_index {
788 self.active_item_index -= 1;
789 }
790 insertion_index = insertion_index.min(self.items.len());
791
792 self.items.insert(insertion_index, item.clone());
793
794 if existing_item_is_active {
795 self.active_item_index = insertion_index;
796 } else if insertion_index <= self.active_item_index {
797 self.active_item_index += 1;
798 }
799 }
800
801 cx.notify();
802 }
803
804 self.activate_item(insertion_index, activate_pane, focus_item, cx);
805 } else {
806 self.items.insert(insertion_index, item.clone());
807
808 if insertion_index <= self.active_item_index
809 && self.preview_item_idx() != Some(self.active_item_index)
810 {
811 self.active_item_index += 1;
812 }
813
814 self.activate_item(insertion_index, activate_pane, focus_item, cx);
815 cx.notify();
816 }
817
818 cx.emit(Event::AddItem { item });
819 }
820
821 pub fn items_len(&self) -> usize {
822 self.items.len()
823 }
824
825 pub fn items(&self) -> impl DoubleEndedIterator<Item = &Box<dyn ItemHandle>> {
826 self.items.iter()
827 }
828
829 pub fn items_of_type<T: Render>(&self) -> impl '_ + Iterator<Item = View<T>> {
830 self.items
831 .iter()
832 .filter_map(|item| item.to_any().downcast().ok())
833 }
834
835 pub fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
836 self.items.get(self.active_item_index).cloned()
837 }
838
839 pub fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Point<Pixels>> {
840 self.items
841 .get(self.active_item_index)?
842 .pixel_position_of_cursor(cx)
843 }
844
845 pub fn item_for_entry(
846 &self,
847 entry_id: ProjectEntryId,
848 cx: &AppContext,
849 ) -> Option<Box<dyn ItemHandle>> {
850 self.items.iter().find_map(|item| {
851 if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
852 Some(item.boxed_clone())
853 } else {
854 None
855 }
856 })
857 }
858
859 pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option<usize> {
860 self.items
861 .iter()
862 .position(|i| i.item_id() == item.item_id())
863 }
864
865 pub fn item_for_index(&self, ix: usize) -> Option<&dyn ItemHandle> {
866 self.items.get(ix).map(|i| i.as_ref())
867 }
868
869 pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) {
870 if self.zoomed {
871 cx.emit(Event::ZoomOut);
872 } else if !self.items.is_empty() {
873 if !self.focus_handle.contains_focused(cx) {
874 cx.focus_self();
875 }
876 cx.emit(Event::ZoomIn);
877 }
878 }
879
880 pub fn activate_item(
881 &mut self,
882 index: usize,
883 activate_pane: bool,
884 focus_item: bool,
885 cx: &mut ViewContext<Self>,
886 ) {
887 use NavigationMode::{GoingBack, GoingForward};
888
889 if index < self.items.len() {
890 let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
891 if prev_active_item_ix != self.active_item_index
892 || matches!(self.nav_history.mode(), GoingBack | GoingForward)
893 {
894 if let Some(prev_item) = self.items.get(prev_active_item_ix) {
895 prev_item.deactivated(cx);
896 }
897 }
898 cx.emit(Event::ActivateItem {
899 local: activate_pane,
900 });
901
902 if let Some(newly_active_item) = self.items.get(index) {
903 self.activation_history
904 .retain(|entry| entry.entity_id != newly_active_item.item_id());
905 self.activation_history.push(ActivationHistoryEntry {
906 entity_id: newly_active_item.item_id(),
907 timestamp: self
908 .next_activation_timestamp
909 .fetch_add(1, Ordering::SeqCst),
910 });
911 }
912
913 self.update_toolbar(cx);
914 self.update_status_bar(cx);
915
916 if focus_item {
917 self.focus_active_item(cx);
918 }
919
920 self.tab_bar_scroll_handle.scroll_to_item(index);
921 cx.notify();
922 }
923 }
924
925 pub fn activate_prev_item(&mut self, activate_pane: bool, cx: &mut ViewContext<Self>) {
926 let mut index = self.active_item_index;
927 if index > 0 {
928 index -= 1;
929 } else if !self.items.is_empty() {
930 index = self.items.len() - 1;
931 }
932 self.activate_item(index, activate_pane, activate_pane, cx);
933 }
934
935 pub fn activate_next_item(&mut self, activate_pane: bool, cx: &mut ViewContext<Self>) {
936 let mut index = self.active_item_index;
937 if index + 1 < self.items.len() {
938 index += 1;
939 } else {
940 index = 0;
941 }
942 self.activate_item(index, activate_pane, activate_pane, cx);
943 }
944
945 pub fn close_active_item(
946 &mut self,
947 action: &CloseActiveItem,
948 cx: &mut ViewContext<Self>,
949 ) -> Option<Task<Result<()>>> {
950 if self.items.is_empty() {
951 // Close the window when there's no active items to close, if configured
952 if WorkspaceSettings::get_global(cx)
953 .when_closing_with_no_tabs
954 .should_close()
955 {
956 cx.dispatch_action(Box::new(CloseWindow));
957 }
958
959 return None;
960 }
961 let active_item_id = self.items[self.active_item_index].item_id();
962 Some(self.close_item_by_id(
963 active_item_id,
964 action.save_intent.unwrap_or(SaveIntent::Close),
965 cx,
966 ))
967 }
968
969 pub fn close_item_by_id(
970 &mut self,
971 item_id_to_close: EntityId,
972 save_intent: SaveIntent,
973 cx: &mut ViewContext<Self>,
974 ) -> Task<Result<()>> {
975 self.close_items(cx, save_intent, move |view_id| view_id == item_id_to_close)
976 }
977
978 pub fn close_inactive_items(
979 &mut self,
980 action: &CloseInactiveItems,
981 cx: &mut ViewContext<Self>,
982 ) -> Option<Task<Result<()>>> {
983 if self.items.is_empty() {
984 return None;
985 }
986
987 let active_item_id = self.items[self.active_item_index].item_id();
988 Some(self.close_items(
989 cx,
990 action.save_intent.unwrap_or(SaveIntent::Close),
991 move |item_id| item_id != active_item_id,
992 ))
993 }
994
995 pub fn close_clean_items(
996 &mut self,
997 _: &CloseCleanItems,
998 cx: &mut ViewContext<Self>,
999 ) -> Option<Task<Result<()>>> {
1000 let item_ids: Vec<_> = self
1001 .items()
1002 .filter(|item| !item.is_dirty(cx))
1003 .map(|item| item.item_id())
1004 .collect();
1005 Some(self.close_items(cx, SaveIntent::Close, move |item_id| {
1006 item_ids.contains(&item_id)
1007 }))
1008 }
1009
1010 pub fn close_items_to_the_left(
1011 &mut self,
1012 _: &CloseItemsToTheLeft,
1013 cx: &mut ViewContext<Self>,
1014 ) -> Option<Task<Result<()>>> {
1015 if self.items.is_empty() {
1016 return None;
1017 }
1018 let active_item_id = self.items[self.active_item_index].item_id();
1019 Some(self.close_items_to_the_left_by_id(active_item_id, cx))
1020 }
1021
1022 pub fn close_items_to_the_left_by_id(
1023 &mut self,
1024 item_id: EntityId,
1025 cx: &mut ViewContext<Self>,
1026 ) -> Task<Result<()>> {
1027 let item_ids: Vec<_> = self
1028 .items()
1029 .take_while(|item| item.item_id() != item_id)
1030 .map(|item| item.item_id())
1031 .collect();
1032 self.close_items(cx, SaveIntent::Close, move |item_id| {
1033 item_ids.contains(&item_id)
1034 })
1035 }
1036
1037 pub fn close_items_to_the_right(
1038 &mut self,
1039 _: &CloseItemsToTheRight,
1040 cx: &mut ViewContext<Self>,
1041 ) -> Option<Task<Result<()>>> {
1042 if self.items.is_empty() {
1043 return None;
1044 }
1045 let active_item_id = self.items[self.active_item_index].item_id();
1046 Some(self.close_items_to_the_right_by_id(active_item_id, cx))
1047 }
1048
1049 pub fn close_items_to_the_right_by_id(
1050 &mut self,
1051 item_id: EntityId,
1052 cx: &mut ViewContext<Self>,
1053 ) -> Task<Result<()>> {
1054 let item_ids: Vec<_> = self
1055 .items()
1056 .rev()
1057 .take_while(|item| item.item_id() != item_id)
1058 .map(|item| item.item_id())
1059 .collect();
1060 self.close_items(cx, SaveIntent::Close, move |item_id| {
1061 item_ids.contains(&item_id)
1062 })
1063 }
1064
1065 pub fn close_all_items(
1066 &mut self,
1067 action: &CloseAllItems,
1068 cx: &mut ViewContext<Self>,
1069 ) -> Option<Task<Result<()>>> {
1070 if self.items.is_empty() {
1071 return None;
1072 }
1073
1074 Some(
1075 self.close_items(cx, action.save_intent.unwrap_or(SaveIntent::Close), |_| {
1076 true
1077 }),
1078 )
1079 }
1080
1081 pub(super) fn file_names_for_prompt(
1082 items: &mut dyn Iterator<Item = &Box<dyn ItemHandle>>,
1083 all_dirty_items: usize,
1084 cx: &AppContext,
1085 ) -> (String, String) {
1086 /// Quantity of item paths displayed in prompt prior to cutoff..
1087 const FILE_NAMES_CUTOFF_POINT: usize = 10;
1088 let mut file_names: Vec<_> = items
1089 .filter_map(|item| {
1090 item.project_path(cx).and_then(|project_path| {
1091 project_path
1092 .path
1093 .file_name()
1094 .and_then(|name| name.to_str().map(ToOwned::to_owned))
1095 })
1096 })
1097 .take(FILE_NAMES_CUTOFF_POINT)
1098 .collect();
1099 let should_display_followup_text =
1100 all_dirty_items > FILE_NAMES_CUTOFF_POINT || file_names.len() != all_dirty_items;
1101 if should_display_followup_text {
1102 let not_shown_files = all_dirty_items - file_names.len();
1103 if not_shown_files == 1 {
1104 file_names.push(".. 1 file not shown".into());
1105 } else {
1106 file_names.push(format!(".. {} files not shown", not_shown_files));
1107 }
1108 }
1109 (
1110 format!(
1111 "Do you want to save changes to the following {} files?",
1112 all_dirty_items
1113 ),
1114 file_names.join("\n"),
1115 )
1116 }
1117
1118 pub fn close_items(
1119 &mut self,
1120 cx: &mut ViewContext<Pane>,
1121 mut save_intent: SaveIntent,
1122 should_close: impl Fn(EntityId) -> bool,
1123 ) -> Task<Result<()>> {
1124 // Find the items to close.
1125 let mut items_to_close = Vec::new();
1126 let mut dirty_items = Vec::new();
1127 for item in &self.items {
1128 if should_close(item.item_id()) {
1129 items_to_close.push(item.boxed_clone());
1130 if item.is_dirty(cx) {
1131 dirty_items.push(item.boxed_clone());
1132 }
1133 }
1134 }
1135
1136 // If a buffer is open both in a singleton editor and in a multibuffer, make sure
1137 // to focus the singleton buffer when prompting to save that buffer, as opposed
1138 // to focusing the multibuffer, because this gives the user a more clear idea
1139 // of what content they would be saving.
1140 items_to_close.sort_by_key(|item| !item.is_singleton(cx));
1141
1142 let workspace = self.workspace.clone();
1143 cx.spawn(|pane, mut cx| async move {
1144 if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
1145 let answer = pane.update(&mut cx, |_, cx| {
1146 let (prompt, detail) =
1147 Self::file_names_for_prompt(&mut dirty_items.iter(), dirty_items.len(), cx);
1148 cx.prompt(
1149 PromptLevel::Warning,
1150 &prompt,
1151 Some(&detail),
1152 &["Save all", "Discard all", "Cancel"],
1153 )
1154 })?;
1155 match answer.await {
1156 Ok(0) => save_intent = SaveIntent::SaveAll,
1157 Ok(1) => save_intent = SaveIntent::Skip,
1158 _ => {}
1159 }
1160 }
1161 let mut saved_project_items_ids = HashSet::default();
1162 for item in items_to_close.clone() {
1163 // Find the item's current index and its set of project item models. Avoid
1164 // storing these in advance, in case they have changed since this task
1165 // was started.
1166 let (item_ix, mut project_item_ids) = pane.update(&mut cx, |pane, cx| {
1167 (pane.index_for_item(&*item), item.project_item_model_ids(cx))
1168 })?;
1169 let item_ix = if let Some(ix) = item_ix {
1170 ix
1171 } else {
1172 continue;
1173 };
1174
1175 // Check if this view has any project items that are not open anywhere else
1176 // in the workspace, AND that the user has not already been prompted to save.
1177 // If there are any such project entries, prompt the user to save this item.
1178 let project = workspace.update(&mut cx, |workspace, cx| {
1179 for item in workspace.items(cx) {
1180 if !items_to_close
1181 .iter()
1182 .any(|item_to_close| item_to_close.item_id() == item.item_id())
1183 {
1184 let other_project_item_ids = item.project_item_model_ids(cx);
1185 project_item_ids.retain(|id| !other_project_item_ids.contains(id));
1186 }
1187 }
1188 workspace.project().clone()
1189 })?;
1190 let should_save = project_item_ids
1191 .iter()
1192 .any(|id| saved_project_items_ids.insert(*id));
1193
1194 if should_save
1195 && !Self::save_item(
1196 project.clone(),
1197 &pane,
1198 item_ix,
1199 &*item,
1200 save_intent,
1201 &mut cx,
1202 )
1203 .await?
1204 {
1205 break;
1206 }
1207
1208 // Remove the item from the pane.
1209 pane.update(&mut cx, |pane, cx| {
1210 if let Some(item_ix) = pane
1211 .items
1212 .iter()
1213 .position(|i| i.item_id() == item.item_id())
1214 {
1215 pane.remove_item(item_ix, false, true, cx);
1216 }
1217 })
1218 .ok();
1219 }
1220
1221 pane.update(&mut cx, |_, cx| cx.notify()).ok();
1222 Ok(())
1223 })
1224 }
1225
1226 pub fn remove_item(
1227 &mut self,
1228 item_index: usize,
1229 activate_pane: bool,
1230 close_pane_if_empty: bool,
1231 cx: &mut ViewContext<Self>,
1232 ) {
1233 self.activation_history
1234 .retain(|entry| entry.entity_id != self.items[item_index].item_id());
1235
1236 if item_index == self.active_item_index {
1237 let index_to_activate = self
1238 .activation_history
1239 .pop()
1240 .and_then(|last_activated_item| {
1241 self.items.iter().enumerate().find_map(|(index, item)| {
1242 (item.item_id() == last_activated_item.entity_id).then_some(index)
1243 })
1244 })
1245 // We didn't have a valid activation history entry, so fallback
1246 // to activating the item to the left
1247 .unwrap_or_else(|| item_index.min(self.items.len()).saturating_sub(1));
1248
1249 let should_activate = activate_pane || self.has_focus(cx);
1250 if self.items.len() == 1 && should_activate {
1251 self.focus_handle.focus(cx);
1252 } else {
1253 self.activate_item(index_to_activate, should_activate, should_activate, cx);
1254 }
1255 }
1256
1257 let item = self.items.remove(item_index);
1258
1259 cx.emit(Event::RemoveItem {
1260 item_id: item.item_id(),
1261 });
1262 if self.items.is_empty() {
1263 item.deactivated(cx);
1264 if close_pane_if_empty {
1265 self.update_toolbar(cx);
1266 cx.emit(Event::Remove);
1267 }
1268 }
1269
1270 if item_index < self.active_item_index {
1271 self.active_item_index -= 1;
1272 }
1273
1274 let mode = self.nav_history.mode();
1275 self.nav_history.set_mode(NavigationMode::ClosingItem);
1276 item.deactivated(cx);
1277 self.nav_history.set_mode(mode);
1278
1279 if self.is_active_preview_item(item.item_id()) {
1280 self.set_preview_item_id(None, cx);
1281 }
1282
1283 if let Some(path) = item.project_path(cx) {
1284 let abs_path = self
1285 .nav_history
1286 .0
1287 .lock()
1288 .paths_by_item
1289 .get(&item.item_id())
1290 .and_then(|(_, abs_path)| abs_path.clone());
1291
1292 self.nav_history
1293 .0
1294 .lock()
1295 .paths_by_item
1296 .insert(item.item_id(), (path, abs_path));
1297 } else {
1298 self.nav_history
1299 .0
1300 .lock()
1301 .paths_by_item
1302 .remove(&item.item_id());
1303 }
1304
1305 if self.items.is_empty() && close_pane_if_empty && self.zoomed {
1306 cx.emit(Event::ZoomOut);
1307 }
1308
1309 cx.notify();
1310 }
1311
1312 pub async fn save_item(
1313 project: Model<Project>,
1314 pane: &WeakView<Pane>,
1315 item_ix: usize,
1316 item: &dyn ItemHandle,
1317 save_intent: SaveIntent,
1318 cx: &mut AsyncWindowContext,
1319 ) -> Result<bool> {
1320 const CONFLICT_MESSAGE: &str =
1321 "This file has changed on disk since you started editing it. Do you want to overwrite it?";
1322
1323 if save_intent == SaveIntent::Skip {
1324 return Ok(true);
1325 }
1326
1327 let (mut has_conflict, mut is_dirty, mut can_save, can_save_as) = cx.update(|cx| {
1328 (
1329 item.has_conflict(cx),
1330 item.is_dirty(cx),
1331 item.can_save(cx),
1332 item.is_singleton(cx),
1333 )
1334 })?;
1335
1336 // when saving a single buffer, we ignore whether or not it's dirty.
1337 if save_intent == SaveIntent::Save || save_intent == SaveIntent::SaveWithoutFormat {
1338 is_dirty = true;
1339 }
1340
1341 if save_intent == SaveIntent::SaveAs {
1342 is_dirty = true;
1343 has_conflict = false;
1344 can_save = false;
1345 }
1346
1347 if save_intent == SaveIntent::Overwrite {
1348 has_conflict = false;
1349 }
1350
1351 let should_format = save_intent != SaveIntent::SaveWithoutFormat;
1352
1353 if has_conflict && can_save {
1354 let answer = pane.update(cx, |pane, cx| {
1355 pane.activate_item(item_ix, true, true, cx);
1356 cx.prompt(
1357 PromptLevel::Warning,
1358 CONFLICT_MESSAGE,
1359 None,
1360 &["Overwrite", "Discard", "Cancel"],
1361 )
1362 })?;
1363 match answer.await {
1364 Ok(0) => {
1365 pane.update(cx, |_, cx| item.save(should_format, project, cx))?
1366 .await?
1367 }
1368 Ok(1) => pane.update(cx, |_, cx| item.reload(project, cx))?.await?,
1369 _ => return Ok(false),
1370 }
1371 } else if is_dirty && (can_save || can_save_as) {
1372 if save_intent == SaveIntent::Close {
1373 let will_autosave = cx.update(|cx| {
1374 matches!(
1375 WorkspaceSettings::get_global(cx).autosave,
1376 AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange
1377 ) && Self::can_autosave_item(item, cx)
1378 })?;
1379 if !will_autosave {
1380 let item_id = item.item_id();
1381 let answer_task = pane.update(cx, |pane, cx| {
1382 if pane.save_modals_spawned.insert(item_id) {
1383 pane.activate_item(item_ix, true, true, cx);
1384 let prompt = dirty_message_for(item.project_path(cx));
1385 Some(cx.prompt(
1386 PromptLevel::Warning,
1387 &prompt,
1388 None,
1389 &["Save", "Don't Save", "Cancel"],
1390 ))
1391 } else {
1392 None
1393 }
1394 })?;
1395 if let Some(answer_task) = answer_task {
1396 let answer = answer_task.await;
1397 pane.update(cx, |pane, _| {
1398 if !pane.save_modals_spawned.remove(&item_id) {
1399 debug_panic!(
1400 "save modal was not present in spawned modals after awaiting for its answer"
1401 )
1402 }
1403 })?;
1404 match answer {
1405 Ok(0) => {}
1406 Ok(1) => return Ok(true), // Don't save this file
1407 _ => return Ok(false), // Cancel
1408 }
1409 } else {
1410 return Ok(false);
1411 }
1412 }
1413 }
1414
1415 if can_save {
1416 pane.update(cx, |_, cx| item.save(should_format, project, cx))?
1417 .await?;
1418 } else if can_save_as {
1419 let abs_path = pane.update(cx, |pane, cx| {
1420 pane.workspace
1421 .update(cx, |workspace, cx| workspace.prompt_for_new_path(cx))
1422 })??;
1423 if let Some(abs_path) = abs_path.await.ok().flatten() {
1424 pane.update(cx, |_, cx| item.save_as(project, abs_path, cx))?
1425 .await?;
1426 } else {
1427 return Ok(false);
1428 }
1429 }
1430 }
1431 Ok(true)
1432 }
1433
1434 fn can_autosave_item(item: &dyn ItemHandle, cx: &AppContext) -> bool {
1435 let is_deleted = item.project_entry_ids(cx).is_empty();
1436 item.is_dirty(cx) && !item.has_conflict(cx) && item.can_save(cx) && !is_deleted
1437 }
1438
1439 pub fn autosave_item(
1440 item: &dyn ItemHandle,
1441 project: Model<Project>,
1442 cx: &mut WindowContext,
1443 ) -> Task<Result<()>> {
1444 let format = if let AutosaveSetting::AfterDelay { .. } =
1445 WorkspaceSettings::get_global(cx).autosave
1446 {
1447 false
1448 } else {
1449 true
1450 };
1451 if Self::can_autosave_item(item, cx) {
1452 item.save(format, project, cx)
1453 } else {
1454 Task::ready(Ok(()))
1455 }
1456 }
1457
1458 pub fn focus(&mut self, cx: &mut ViewContext<Pane>) {
1459 cx.focus(&self.focus_handle);
1460 }
1461
1462 pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
1463 if let Some(active_item) = self.active_item() {
1464 let focus_handle = active_item.focus_handle(cx);
1465 cx.focus(&focus_handle);
1466 }
1467 }
1468
1469 pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
1470 cx.emit(Event::Split(direction));
1471 }
1472
1473 pub fn toolbar(&self) -> &View<Toolbar> {
1474 &self.toolbar
1475 }
1476
1477 pub fn handle_deleted_project_item(
1478 &mut self,
1479 entry_id: ProjectEntryId,
1480 cx: &mut ViewContext<Pane>,
1481 ) -> Option<()> {
1482 let (item_index_to_delete, item_id) = self.items().enumerate().find_map(|(i, item)| {
1483 if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
1484 Some((i, item.item_id()))
1485 } else {
1486 None
1487 }
1488 })?;
1489
1490 self.remove_item(item_index_to_delete, false, true, cx);
1491 self.nav_history.remove_item(item_id);
1492
1493 Some(())
1494 }
1495
1496 fn update_toolbar(&mut self, cx: &mut ViewContext<Self>) {
1497 let active_item = self
1498 .items
1499 .get(self.active_item_index)
1500 .map(|item| item.as_ref());
1501 self.toolbar.update(cx, |toolbar, cx| {
1502 toolbar.set_active_item(active_item, cx);
1503 });
1504 }
1505
1506 fn update_status_bar(&mut self, cx: &mut ViewContext<Self>) {
1507 let workspace = self.workspace.clone();
1508 let pane = cx.view().clone();
1509
1510 cx.window_context().defer(move |cx| {
1511 let Ok(status_bar) = workspace.update(cx, |workspace, _| workspace.status_bar.clone())
1512 else {
1513 return;
1514 };
1515
1516 status_bar.update(cx, move |status_bar, cx| {
1517 status_bar.set_active_pane(&pane, cx);
1518 });
1519 });
1520 }
1521
1522 fn render_tab(
1523 &self,
1524 ix: usize,
1525 item: &Box<dyn ItemHandle>,
1526 detail: usize,
1527 cx: &mut ViewContext<'_, Pane>,
1528 ) -> impl IntoElement {
1529 let is_active = ix == self.active_item_index;
1530 let is_preview = self
1531 .preview_item_id
1532 .map(|id| id == item.item_id())
1533 .unwrap_or(false);
1534
1535 let label = item.tab_content(
1536 TabContentParams {
1537 detail: Some(detail),
1538 selected: is_active,
1539 preview: is_preview,
1540 },
1541 cx,
1542 );
1543 let close_side = &ItemSettings::get_global(cx).close_position;
1544 let indicator = render_item_indicator(item.boxed_clone(), cx);
1545 let item_id = item.item_id();
1546 let is_first_item = ix == 0;
1547 let is_last_item = ix == self.items.len() - 1;
1548 let position_relative_to_active_item = ix.cmp(&self.active_item_index);
1549
1550 let tab = Tab::new(ix)
1551 .position(if is_first_item {
1552 TabPosition::First
1553 } else if is_last_item {
1554 TabPosition::Last
1555 } else {
1556 TabPosition::Middle(position_relative_to_active_item)
1557 })
1558 .close_side(match close_side {
1559 ClosePosition::Left => ui::TabCloseSide::Start,
1560 ClosePosition::Right => ui::TabCloseSide::End,
1561 })
1562 .selected(is_active)
1563 .on_click(
1564 cx.listener(move |pane: &mut Self, _, cx| pane.activate_item(ix, true, true, cx)),
1565 )
1566 // TODO: This should be a click listener with the middle mouse button instead of a mouse down listener.
1567 .on_mouse_down(
1568 MouseButton::Middle,
1569 cx.listener(move |pane, _event, cx| {
1570 pane.close_item_by_id(item_id, SaveIntent::Close, cx)
1571 .detach_and_log_err(cx);
1572 }),
1573 )
1574 .on_mouse_down(
1575 MouseButton::Left,
1576 cx.listener(move |pane, event: &MouseDownEvent, cx| {
1577 if let Some(id) = pane.preview_item_id {
1578 if id == item_id && event.click_count > 1 {
1579 pane.set_preview_item_id(None, cx);
1580 }
1581 }
1582 }),
1583 )
1584 .on_drag(
1585 DraggedTab {
1586 item: item.boxed_clone(),
1587 pane: cx.view().clone(),
1588 detail,
1589 is_active,
1590 ix,
1591 },
1592 |tab, cx| cx.new_view(|_| tab.clone()),
1593 )
1594 .drag_over::<DraggedTab>(|tab, _, cx| {
1595 tab.bg(cx.theme().colors().drop_target_background)
1596 })
1597 .drag_over::<ProjectEntryId>(|tab, _, cx| {
1598 tab.bg(cx.theme().colors().drop_target_background)
1599 })
1600 .when_some(self.can_drop_predicate.clone(), |this, p| {
1601 this.can_drop(move |a, cx| p(a, cx))
1602 })
1603 .on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {
1604 this.drag_split_direction = None;
1605 this.handle_tab_drop(dragged_tab, ix, cx)
1606 }))
1607 .on_drop(cx.listener(move |this, entry_id: &ProjectEntryId, cx| {
1608 this.drag_split_direction = None;
1609 this.handle_project_entry_drop(entry_id, cx)
1610 }))
1611 .on_drop(cx.listener(move |this, paths, cx| {
1612 this.drag_split_direction = None;
1613 this.handle_external_paths_drop(paths, cx)
1614 }))
1615 .when_some(item.tab_tooltip_text(cx), |tab, text| {
1616 tab.tooltip(move |cx| Tooltip::text(text.clone(), cx))
1617 })
1618 .start_slot::<Indicator>(indicator)
1619 .end_slot(
1620 IconButton::new("close tab", IconName::Close)
1621 .shape(IconButtonShape::Square)
1622 .icon_color(Color::Muted)
1623 .size(ButtonSize::None)
1624 .icon_size(IconSize::XSmall)
1625 .on_click(cx.listener(move |pane, _, cx| {
1626 pane.close_item_by_id(item_id, SaveIntent::Close, cx)
1627 .detach_and_log_err(cx);
1628 })),
1629 )
1630 .child(label);
1631
1632 let single_entry_to_resolve = {
1633 let item_entries = self.items[ix].project_entry_ids(cx);
1634 if item_entries.len() == 1 {
1635 Some(item_entries[0])
1636 } else {
1637 None
1638 }
1639 };
1640
1641 let pane = cx.view().downgrade();
1642 right_click_menu(ix).trigger(tab).menu(move |cx| {
1643 let pane = pane.clone();
1644 ContextMenu::build(cx, move |mut menu, cx| {
1645 if let Some(pane) = pane.upgrade() {
1646 menu = menu
1647 .entry(
1648 "Close",
1649 Some(Box::new(CloseActiveItem { save_intent: None })),
1650 cx.handler_for(&pane, move |pane, cx| {
1651 pane.close_item_by_id(item_id, SaveIntent::Close, cx)
1652 .detach_and_log_err(cx);
1653 }),
1654 )
1655 .entry(
1656 "Close Others",
1657 Some(Box::new(CloseInactiveItems { save_intent: None })),
1658 cx.handler_for(&pane, move |pane, cx| {
1659 pane.close_items(cx, SaveIntent::Close, |id| id != item_id)
1660 .detach_and_log_err(cx);
1661 }),
1662 )
1663 .separator()
1664 .entry(
1665 "Close Left",
1666 Some(Box::new(CloseItemsToTheLeft)),
1667 cx.handler_for(&pane, move |pane, cx| {
1668 pane.close_items_to_the_left_by_id(item_id, cx)
1669 .detach_and_log_err(cx);
1670 }),
1671 )
1672 .entry(
1673 "Close Right",
1674 Some(Box::new(CloseItemsToTheRight)),
1675 cx.handler_for(&pane, move |pane, cx| {
1676 pane.close_items_to_the_right_by_id(item_id, cx)
1677 .detach_and_log_err(cx);
1678 }),
1679 )
1680 .separator()
1681 .entry(
1682 "Close Clean",
1683 Some(Box::new(CloseCleanItems)),
1684 cx.handler_for(&pane, move |pane, cx| {
1685 if let Some(task) = pane.close_clean_items(&CloseCleanItems, cx) {
1686 task.detach_and_log_err(cx)
1687 }
1688 }),
1689 )
1690 .entry(
1691 "Close All",
1692 Some(Box::new(CloseAllItems { save_intent: None })),
1693 cx.handler_for(&pane, |pane, cx| {
1694 if let Some(task) =
1695 pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
1696 {
1697 task.detach_and_log_err(cx)
1698 }
1699 }),
1700 );
1701
1702 if let Some(entry) = single_entry_to_resolve {
1703 let parent_abs_path = pane
1704 .update(cx, |pane, cx| {
1705 pane.workspace.update(cx, |workspace, cx| {
1706 let project = workspace.project().read(cx);
1707 project.worktree_for_entry(entry, cx).and_then(|worktree| {
1708 let worktree = worktree.read(cx);
1709 let entry = worktree.entry_for_id(entry)?;
1710 let abs_path = worktree.absolutize(&entry.path).ok()?;
1711 let parent = if entry.is_symlink {
1712 abs_path.canonicalize().ok()?
1713 } else {
1714 abs_path
1715 }
1716 .parent()?
1717 .to_path_buf();
1718 Some(parent)
1719 })
1720 })
1721 })
1722 .ok()
1723 .flatten();
1724
1725 let entry_id = entry.to_proto();
1726 menu = menu
1727 .separator()
1728 .entry(
1729 "Reveal In Project Panel",
1730 Some(Box::new(RevealInProjectPanel {
1731 entry_id: Some(entry_id),
1732 })),
1733 cx.handler_for(&pane, move |pane, cx| {
1734 pane.project.update(cx, |_, cx| {
1735 cx.emit(project::Event::RevealInProjectPanel(
1736 ProjectEntryId::from_proto(entry_id),
1737 ))
1738 });
1739 }),
1740 )
1741 .when_some(parent_abs_path, |menu, abs_path| {
1742 menu.entry(
1743 "Open in Terminal",
1744 Some(Box::new(OpenInTerminal)),
1745 cx.handler_for(&pane, move |_, cx| {
1746 cx.dispatch_action(
1747 OpenTerminal {
1748 working_directory: abs_path.clone(),
1749 }
1750 .boxed_clone(),
1751 );
1752 }),
1753 )
1754 });
1755 }
1756 }
1757
1758 menu
1759 })
1760 })
1761 }
1762
1763 fn render_tab_bar(&mut self, cx: &mut ViewContext<'_, Pane>) -> impl IntoElement {
1764 let navigate_backward = IconButton::new("navigate_backward", IconName::ArrowLeft)
1765 .shape(IconButtonShape::Square)
1766 .icon_size(IconSize::Small)
1767 .on_click({
1768 let view = cx.view().clone();
1769 move |_, cx| view.update(cx, Self::navigate_backward)
1770 })
1771 .disabled(!self.can_navigate_backward())
1772 .tooltip(|cx| Tooltip::for_action("Go Back", &GoBack, cx));
1773
1774 let navigate_forward = IconButton::new("navigate_forward", IconName::ArrowRight)
1775 .shape(IconButtonShape::Square)
1776 .icon_size(IconSize::Small)
1777 .on_click({
1778 let view = cx.view().clone();
1779 move |_, cx| view.update(cx, Self::navigate_forward)
1780 })
1781 .disabled(!self.can_navigate_forward())
1782 .tooltip(|cx| Tooltip::for_action("Go Forward", &GoForward, cx));
1783
1784 TabBar::new("tab_bar")
1785 .track_scroll(self.tab_bar_scroll_handle.clone())
1786 .when(
1787 self.display_nav_history_buttons.unwrap_or_default(),
1788 |tab_bar| tab_bar.start_children(vec![navigate_backward, navigate_forward]),
1789 )
1790 .when(self.has_focus(cx), |tab_bar| {
1791 tab_bar.end_child({
1792 let render_tab_buttons = self.render_tab_bar_buttons.clone();
1793 render_tab_buttons(self, cx)
1794 })
1795 })
1796 .children(
1797 self.items
1798 .iter()
1799 .enumerate()
1800 .zip(tab_details(&self.items, cx))
1801 .map(|((ix, item), detail)| self.render_tab(ix, item, detail, cx)),
1802 )
1803 .child(
1804 div()
1805 .id("tab_bar_drop_target")
1806 .min_w_6()
1807 // HACK: This empty child is currently necessary to force the drop target to appear
1808 // despite us setting a min width above.
1809 .child("")
1810 .h_full()
1811 .flex_grow()
1812 .drag_over::<DraggedTab>(|bar, _, cx| {
1813 bar.bg(cx.theme().colors().drop_target_background)
1814 })
1815 .drag_over::<ProjectEntryId>(|bar, _, cx| {
1816 bar.bg(cx.theme().colors().drop_target_background)
1817 })
1818 .on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {
1819 this.drag_split_direction = None;
1820 this.handle_tab_drop(dragged_tab, this.items.len(), cx)
1821 }))
1822 .on_drop(cx.listener(move |this, entry_id: &ProjectEntryId, cx| {
1823 this.drag_split_direction = None;
1824 this.handle_project_entry_drop(entry_id, cx)
1825 }))
1826 .on_drop(cx.listener(move |this, paths, cx| {
1827 this.drag_split_direction = None;
1828 this.handle_external_paths_drop(paths, cx)
1829 }))
1830 .on_click(cx.listener(move |this, event: &ClickEvent, cx| {
1831 if event.up.click_count == 2 {
1832 cx.dispatch_action(this.double_click_dispatch_action.boxed_clone())
1833 }
1834 })),
1835 )
1836 }
1837
1838 pub fn render_menu_overlay(menu: &View<ContextMenu>) -> Div {
1839 div().absolute().bottom_0().right_0().size_0().child(
1840 deferred(
1841 anchored()
1842 .anchor(AnchorCorner::TopRight)
1843 .child(menu.clone()),
1844 )
1845 .with_priority(1),
1846 )
1847 }
1848
1849 pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
1850 self.zoomed = zoomed;
1851 cx.notify();
1852 }
1853
1854 pub fn is_zoomed(&self) -> bool {
1855 self.zoomed
1856 }
1857
1858 fn handle_drag_move<T>(&mut self, event: &DragMoveEvent<T>, cx: &mut ViewContext<Self>) {
1859 if !self.can_split {
1860 return;
1861 }
1862
1863 let rect = event.bounds.size;
1864
1865 let size = event.bounds.size.width.min(event.bounds.size.height)
1866 * WorkspaceSettings::get_global(cx).drop_target_size;
1867
1868 let relative_cursor = Point::new(
1869 event.event.position.x - event.bounds.left(),
1870 event.event.position.y - event.bounds.top(),
1871 );
1872
1873 let direction = if relative_cursor.x < size
1874 || relative_cursor.x > rect.width - size
1875 || relative_cursor.y < size
1876 || relative_cursor.y > rect.height - size
1877 {
1878 [
1879 SplitDirection::Up,
1880 SplitDirection::Right,
1881 SplitDirection::Down,
1882 SplitDirection::Left,
1883 ]
1884 .iter()
1885 .min_by_key(|side| match side {
1886 SplitDirection::Up => relative_cursor.y,
1887 SplitDirection::Right => rect.width - relative_cursor.x,
1888 SplitDirection::Down => rect.height - relative_cursor.y,
1889 SplitDirection::Left => relative_cursor.x,
1890 })
1891 .cloned()
1892 } else {
1893 None
1894 };
1895
1896 if direction != self.drag_split_direction {
1897 self.drag_split_direction = direction;
1898 }
1899 }
1900
1901 fn handle_tab_drop(
1902 &mut self,
1903 dragged_tab: &DraggedTab,
1904 ix: usize,
1905 cx: &mut ViewContext<'_, Self>,
1906 ) {
1907 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
1908 if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, cx) {
1909 return;
1910 }
1911 }
1912 let mut to_pane = cx.view().clone();
1913 let split_direction = self.drag_split_direction;
1914 let item_id = dragged_tab.item.item_id();
1915 if let Some(preview_item_id) = self.preview_item_id {
1916 if item_id == preview_item_id {
1917 self.set_preview_item_id(None, cx);
1918 }
1919 }
1920
1921 let from_pane = dragged_tab.pane.clone();
1922 self.workspace
1923 .update(cx, |_, cx| {
1924 cx.defer(move |workspace, cx| {
1925 if let Some(split_direction) = split_direction {
1926 to_pane = workspace.split_pane(to_pane, split_direction, cx);
1927 }
1928 workspace.move_item(from_pane, to_pane, item_id, ix, cx);
1929 });
1930 })
1931 .log_err();
1932 }
1933
1934 fn handle_project_entry_drop(
1935 &mut self,
1936 project_entry_id: &ProjectEntryId,
1937 cx: &mut ViewContext<'_, Self>,
1938 ) {
1939 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
1940 if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, cx) {
1941 return;
1942 }
1943 }
1944 let mut to_pane = cx.view().clone();
1945 let split_direction = self.drag_split_direction;
1946 let project_entry_id = *project_entry_id;
1947 self.workspace
1948 .update(cx, |_, cx| {
1949 cx.defer(move |workspace, cx| {
1950 if let Some(path) = workspace
1951 .project()
1952 .read(cx)
1953 .path_for_entry(project_entry_id, cx)
1954 {
1955 if let Some(split_direction) = split_direction {
1956 to_pane = workspace.split_pane(to_pane, split_direction, cx);
1957 }
1958 workspace
1959 .open_path(path, Some(to_pane.downgrade()), true, cx)
1960 .detach_and_log_err(cx);
1961 }
1962 });
1963 })
1964 .log_err();
1965 }
1966
1967 fn handle_external_paths_drop(
1968 &mut self,
1969 paths: &ExternalPaths,
1970 cx: &mut ViewContext<'_, Self>,
1971 ) {
1972 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
1973 if let ControlFlow::Break(()) = custom_drop_handle(self, paths, cx) {
1974 return;
1975 }
1976 }
1977 let mut to_pane = cx.view().clone();
1978 let mut split_direction = self.drag_split_direction;
1979 let paths = paths.paths().to_vec();
1980 let is_remote = self
1981 .workspace
1982 .update(cx, |workspace, cx| {
1983 if workspace.project().read(cx).is_remote() {
1984 workspace.show_error(
1985 &anyhow::anyhow!("Cannot drop files on a remote project"),
1986 cx,
1987 );
1988 true
1989 } else {
1990 false
1991 }
1992 })
1993 .unwrap_or(true);
1994 if is_remote {
1995 return;
1996 }
1997
1998 self.workspace
1999 .update(cx, |workspace, cx| {
2000 let fs = Arc::clone(workspace.project().read(cx).fs());
2001 cx.spawn(|workspace, mut cx| async move {
2002 let mut is_file_checks = FuturesUnordered::new();
2003 for path in &paths {
2004 is_file_checks.push(fs.is_file(path))
2005 }
2006 let mut has_files_to_open = false;
2007 while let Some(is_file) = is_file_checks.next().await {
2008 if is_file {
2009 has_files_to_open = true;
2010 break;
2011 }
2012 }
2013 drop(is_file_checks);
2014 if !has_files_to_open {
2015 split_direction = None;
2016 }
2017
2018 if let Some(open_task) = workspace
2019 .update(&mut cx, |workspace, cx| {
2020 if let Some(split_direction) = split_direction {
2021 to_pane = workspace.split_pane(to_pane, split_direction, cx);
2022 }
2023 workspace.open_paths(
2024 paths,
2025 OpenVisible::OnlyDirectories,
2026 Some(to_pane.downgrade()),
2027 cx,
2028 )
2029 })
2030 .ok()
2031 {
2032 let _opened_items: Vec<_> = open_task.await;
2033 }
2034 })
2035 .detach();
2036 })
2037 .log_err();
2038 }
2039
2040 pub fn display_nav_history_buttons(&mut self, display: Option<bool>) {
2041 self.display_nav_history_buttons = display;
2042 }
2043}
2044
2045impl FocusableView for Pane {
2046 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
2047 self.focus_handle.clone()
2048 }
2049}
2050
2051impl Render for Pane {
2052 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2053 let mut key_context = KeyContext::new_with_defaults();
2054 key_context.add("Pane");
2055 if self.active_item().is_none() {
2056 key_context.add("EmptyPane");
2057 }
2058
2059 let should_display_tab_bar = self.should_display_tab_bar.clone();
2060 let display_tab_bar = should_display_tab_bar(cx);
2061
2062 v_flex()
2063 .key_context(key_context)
2064 .track_focus(&self.focus_handle)
2065 .size_full()
2066 .flex_none()
2067 .overflow_hidden()
2068 .on_action(cx.listener(|pane, _: &AlternateFile, cx| {
2069 pane.alternate_file(cx);
2070 }))
2071 .on_action(cx.listener(|pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx)))
2072 .on_action(cx.listener(|pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx)))
2073 .on_action(
2074 cx.listener(|pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx)),
2075 )
2076 .on_action(cx.listener(|pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx)))
2077 .on_action(cx.listener(|pane, _: &GoBack, cx| pane.navigate_backward(cx)))
2078 .on_action(cx.listener(|pane, _: &GoForward, cx| pane.navigate_forward(cx)))
2079 .on_action(cx.listener(Pane::toggle_zoom))
2080 .on_action(cx.listener(|pane: &mut Pane, action: &ActivateItem, cx| {
2081 pane.activate_item(action.0, true, true, cx);
2082 }))
2083 .on_action(cx.listener(|pane: &mut Pane, _: &ActivateLastItem, cx| {
2084 pane.activate_item(pane.items.len() - 1, true, true, cx);
2085 }))
2086 .on_action(cx.listener(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
2087 pane.activate_prev_item(true, cx);
2088 }))
2089 .on_action(cx.listener(|pane: &mut Pane, _: &ActivateNextItem, cx| {
2090 pane.activate_next_item(true, cx);
2091 }))
2092 .when(PreviewTabsSettings::get_global(cx).enabled, |this| {
2093 this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, cx| {
2094 if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) {
2095 if pane.is_active_preview_item(active_item_id) {
2096 pane.set_preview_item_id(None, cx);
2097 } else {
2098 pane.set_preview_item_id(Some(active_item_id), cx);
2099 }
2100 }
2101 }))
2102 })
2103 .on_action(
2104 cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
2105 if let Some(task) = pane.close_active_item(action, cx) {
2106 task.detach_and_log_err(cx)
2107 }
2108 }),
2109 )
2110 .on_action(
2111 cx.listener(|pane: &mut Self, action: &CloseInactiveItems, cx| {
2112 if let Some(task) = pane.close_inactive_items(action, cx) {
2113 task.detach_and_log_err(cx)
2114 }
2115 }),
2116 )
2117 .on_action(
2118 cx.listener(|pane: &mut Self, action: &CloseCleanItems, cx| {
2119 if let Some(task) = pane.close_clean_items(action, cx) {
2120 task.detach_and_log_err(cx)
2121 }
2122 }),
2123 )
2124 .on_action(
2125 cx.listener(|pane: &mut Self, action: &CloseItemsToTheLeft, cx| {
2126 if let Some(task) = pane.close_items_to_the_left(action, cx) {
2127 task.detach_and_log_err(cx)
2128 }
2129 }),
2130 )
2131 .on_action(
2132 cx.listener(|pane: &mut Self, action: &CloseItemsToTheRight, cx| {
2133 if let Some(task) = pane.close_items_to_the_right(action, cx) {
2134 task.detach_and_log_err(cx)
2135 }
2136 }),
2137 )
2138 .on_action(cx.listener(|pane: &mut Self, action: &CloseAllItems, cx| {
2139 if let Some(task) = pane.close_all_items(action, cx) {
2140 task.detach_and_log_err(cx)
2141 }
2142 }))
2143 .on_action(
2144 cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
2145 if let Some(task) = pane.close_active_item(action, cx) {
2146 task.detach_and_log_err(cx)
2147 }
2148 }),
2149 )
2150 .on_action(
2151 cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, cx| {
2152 let entry_id = action
2153 .entry_id
2154 .map(ProjectEntryId::from_proto)
2155 .or_else(|| pane.active_item()?.project_entry_ids(cx).first().copied());
2156 if let Some(entry_id) = entry_id {
2157 pane.project.update(cx, |_, cx| {
2158 cx.emit(project::Event::RevealInProjectPanel(entry_id))
2159 });
2160 }
2161 }),
2162 )
2163 .when(self.active_item().is_some() && display_tab_bar, |pane| {
2164 pane.child(self.render_tab_bar(cx))
2165 })
2166 .child({
2167 let has_worktrees = self.project.read(cx).worktrees().next().is_some();
2168 // main content
2169 div()
2170 .flex_1()
2171 .relative()
2172 .group("")
2173 .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
2174 .on_drag_move::<ProjectEntryId>(cx.listener(Self::handle_drag_move))
2175 .on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
2176 .map(|div| {
2177 if let Some(item) = self.active_item() {
2178 div.v_flex()
2179 .child(self.toolbar.clone())
2180 .child(item.to_any())
2181 } else {
2182 let placeholder = div.h_flex().size_full().justify_center();
2183 if has_worktrees {
2184 placeholder
2185 } else {
2186 placeholder.child(
2187 Label::new("Open a file or project to get started.")
2188 .color(Color::Muted),
2189 )
2190 }
2191 }
2192 })
2193 .child(
2194 // drag target
2195 div()
2196 .invisible()
2197 .absolute()
2198 .bg(cx.theme().colors().drop_target_background)
2199 .group_drag_over::<DraggedTab>("", |style| style.visible())
2200 .group_drag_over::<ProjectEntryId>("", |style| style.visible())
2201 .group_drag_over::<ExternalPaths>("", |style| style.visible())
2202 .when_some(self.can_drop_predicate.clone(), |this, p| {
2203 this.can_drop(move |a, cx| p(a, cx))
2204 })
2205 .on_drop(cx.listener(move |this, dragged_tab, cx| {
2206 this.handle_tab_drop(dragged_tab, this.active_item_index(), cx)
2207 }))
2208 .on_drop(cx.listener(move |this, entry_id, cx| {
2209 this.handle_project_entry_drop(entry_id, cx)
2210 }))
2211 .on_drop(cx.listener(move |this, paths, cx| {
2212 this.handle_external_paths_drop(paths, cx)
2213 }))
2214 .map(|div| {
2215 let size = DefiniteLength::Fraction(0.5);
2216 match self.drag_split_direction {
2217 None => div.top_0().right_0().bottom_0().left_0(),
2218 Some(SplitDirection::Up) => {
2219 div.top_0().left_0().right_0().h(size)
2220 }
2221 Some(SplitDirection::Down) => {
2222 div.left_0().bottom_0().right_0().h(size)
2223 }
2224 Some(SplitDirection::Left) => {
2225 div.top_0().left_0().bottom_0().w(size)
2226 }
2227 Some(SplitDirection::Right) => {
2228 div.top_0().bottom_0().right_0().w(size)
2229 }
2230 }
2231 }),
2232 )
2233 })
2234 .on_mouse_down(
2235 MouseButton::Navigate(NavigationDirection::Back),
2236 cx.listener(|pane, _, cx| {
2237 if let Some(workspace) = pane.workspace.upgrade() {
2238 let pane = cx.view().downgrade();
2239 cx.window_context().defer(move |cx| {
2240 workspace.update(cx, |workspace, cx| {
2241 workspace.go_back(pane, cx).detach_and_log_err(cx)
2242 })
2243 })
2244 }
2245 }),
2246 )
2247 .on_mouse_down(
2248 MouseButton::Navigate(NavigationDirection::Forward),
2249 cx.listener(|pane, _, cx| {
2250 if let Some(workspace) = pane.workspace.upgrade() {
2251 let pane = cx.view().downgrade();
2252 cx.window_context().defer(move |cx| {
2253 workspace.update(cx, |workspace, cx| {
2254 workspace.go_forward(pane, cx).detach_and_log_err(cx)
2255 })
2256 })
2257 }
2258 }),
2259 )
2260 }
2261}
2262
2263impl ItemNavHistory {
2264 pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut WindowContext) {
2265 self.history
2266 .push(data, self.item.clone(), self.is_preview, cx);
2267 }
2268
2269 pub fn pop_backward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
2270 self.history.pop(NavigationMode::GoingBack, cx)
2271 }
2272
2273 pub fn pop_forward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
2274 self.history.pop(NavigationMode::GoingForward, cx)
2275 }
2276}
2277
2278impl NavHistory {
2279 pub fn for_each_entry(
2280 &self,
2281 cx: &AppContext,
2282 mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
2283 ) {
2284 let borrowed_history = self.0.lock();
2285 borrowed_history
2286 .forward_stack
2287 .iter()
2288 .chain(borrowed_history.backward_stack.iter())
2289 .chain(borrowed_history.closed_stack.iter())
2290 .for_each(|entry| {
2291 if let Some(project_and_abs_path) =
2292 borrowed_history.paths_by_item.get(&entry.item.id())
2293 {
2294 f(entry, project_and_abs_path.clone());
2295 } else if let Some(item) = entry.item.upgrade() {
2296 if let Some(path) = item.project_path(cx) {
2297 f(entry, (path, None));
2298 }
2299 }
2300 })
2301 }
2302
2303 pub fn set_mode(&mut self, mode: NavigationMode) {
2304 self.0.lock().mode = mode;
2305 }
2306
2307 pub fn mode(&self) -> NavigationMode {
2308 self.0.lock().mode
2309 }
2310
2311 pub fn disable(&mut self) {
2312 self.0.lock().mode = NavigationMode::Disabled;
2313 }
2314
2315 pub fn enable(&mut self) {
2316 self.0.lock().mode = NavigationMode::Normal;
2317 }
2318
2319 pub fn pop(&mut self, mode: NavigationMode, cx: &mut WindowContext) -> Option<NavigationEntry> {
2320 let mut state = self.0.lock();
2321 let entry = match mode {
2322 NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
2323 return None
2324 }
2325 NavigationMode::GoingBack => &mut state.backward_stack,
2326 NavigationMode::GoingForward => &mut state.forward_stack,
2327 NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
2328 }
2329 .pop_back();
2330 if entry.is_some() {
2331 state.did_update(cx);
2332 }
2333 entry
2334 }
2335
2336 pub fn push<D: 'static + Send + Any>(
2337 &mut self,
2338 data: Option<D>,
2339 item: Arc<dyn WeakItemHandle>,
2340 is_preview: bool,
2341 cx: &mut WindowContext,
2342 ) {
2343 let state = &mut *self.0.lock();
2344 match state.mode {
2345 NavigationMode::Disabled => {}
2346 NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
2347 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2348 state.backward_stack.pop_front();
2349 }
2350 state.backward_stack.push_back(NavigationEntry {
2351 item,
2352 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2353 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2354 is_preview,
2355 });
2356 state.forward_stack.clear();
2357 }
2358 NavigationMode::GoingBack => {
2359 if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2360 state.forward_stack.pop_front();
2361 }
2362 state.forward_stack.push_back(NavigationEntry {
2363 item,
2364 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2365 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2366 is_preview,
2367 });
2368 }
2369 NavigationMode::GoingForward => {
2370 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2371 state.backward_stack.pop_front();
2372 }
2373 state.backward_stack.push_back(NavigationEntry {
2374 item,
2375 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2376 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2377 is_preview,
2378 });
2379 }
2380 NavigationMode::ClosingItem => {
2381 if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2382 state.closed_stack.pop_front();
2383 }
2384 state.closed_stack.push_back(NavigationEntry {
2385 item,
2386 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2387 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2388 is_preview,
2389 });
2390 }
2391 }
2392 state.did_update(cx);
2393 }
2394
2395 pub fn remove_item(&mut self, item_id: EntityId) {
2396 let mut state = self.0.lock();
2397 state.paths_by_item.remove(&item_id);
2398 state
2399 .backward_stack
2400 .retain(|entry| entry.item.id() != item_id);
2401 state
2402 .forward_stack
2403 .retain(|entry| entry.item.id() != item_id);
2404 state
2405 .closed_stack
2406 .retain(|entry| entry.item.id() != item_id);
2407 }
2408
2409 pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
2410 self.0.lock().paths_by_item.get(&item_id).cloned()
2411 }
2412}
2413
2414impl NavHistoryState {
2415 pub fn did_update(&self, cx: &mut WindowContext) {
2416 if let Some(pane) = self.pane.upgrade() {
2417 cx.defer(move |cx| {
2418 pane.update(cx, |pane, cx| pane.history_updated(cx));
2419 });
2420 }
2421 }
2422}
2423
2424fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
2425 let path = buffer_path
2426 .as_ref()
2427 .and_then(|p| {
2428 p.path
2429 .to_str()
2430 .and_then(|s| if s == "" { None } else { Some(s) })
2431 })
2432 .unwrap_or("This buffer");
2433 let path = truncate_and_remove_front(path, 80);
2434 format!("{path} contains unsaved edits. Do you want to save it?")
2435}
2436
2437pub fn tab_details(items: &Vec<Box<dyn ItemHandle>>, cx: &AppContext) -> Vec<usize> {
2438 let mut tab_details = items.iter().map(|_| 0).collect::<Vec<_>>();
2439 let mut tab_descriptions = HashMap::default();
2440 let mut done = false;
2441 while !done {
2442 done = true;
2443
2444 // Store item indices by their tab description.
2445 for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() {
2446 if let Some(description) = item.tab_description(*detail, cx) {
2447 if *detail == 0
2448 || Some(&description) != item.tab_description(detail - 1, cx).as_ref()
2449 {
2450 tab_descriptions
2451 .entry(description)
2452 .or_insert(Vec::new())
2453 .push(ix);
2454 }
2455 }
2456 }
2457
2458 // If two or more items have the same tab description, increase their level
2459 // of detail and try again.
2460 for (_, item_ixs) in tab_descriptions.drain() {
2461 if item_ixs.len() > 1 {
2462 done = false;
2463 for ix in item_ixs {
2464 tab_details[ix] += 1;
2465 }
2466 }
2467 }
2468 }
2469
2470 tab_details
2471}
2472
2473pub fn render_item_indicator(item: Box<dyn ItemHandle>, cx: &WindowContext) -> Option<Indicator> {
2474 maybe!({
2475 let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
2476 (true, _) => Color::Warning,
2477 (_, true) => Color::Accent,
2478 (false, false) => return None,
2479 };
2480
2481 Some(Indicator::dot().color(indicator_color))
2482 })
2483}
2484
2485#[cfg(test)]
2486mod tests {
2487 use super::*;
2488 use crate::item::test::{TestItem, TestProjectItem};
2489 use gpui::{TestAppContext, VisualTestContext};
2490 use project::FakeFs;
2491 use settings::SettingsStore;
2492 use theme::LoadThemes;
2493
2494 #[gpui::test]
2495 async fn test_remove_active_empty(cx: &mut TestAppContext) {
2496 init_test(cx);
2497 let fs = FakeFs::new(cx.executor());
2498
2499 let project = Project::test(fs, None, cx).await;
2500 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2501 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2502
2503 pane.update(cx, |pane, cx| {
2504 assert!(pane
2505 .close_active_item(&CloseActiveItem { save_intent: None }, cx)
2506 .is_none())
2507 });
2508 }
2509
2510 #[gpui::test]
2511 async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
2512 init_test(cx);
2513 let fs = FakeFs::new(cx.executor());
2514
2515 let project = Project::test(fs, None, cx).await;
2516 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2517 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2518
2519 // 1. Add with a destination index
2520 // a. Add before the active item
2521 set_labeled_items(&pane, ["A", "B*", "C"], cx);
2522 pane.update(cx, |pane, cx| {
2523 pane.add_item(
2524 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2525 false,
2526 false,
2527 Some(0),
2528 cx,
2529 );
2530 });
2531 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
2532
2533 // b. Add after the active item
2534 set_labeled_items(&pane, ["A", "B*", "C"], cx);
2535 pane.update(cx, |pane, cx| {
2536 pane.add_item(
2537 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2538 false,
2539 false,
2540 Some(2),
2541 cx,
2542 );
2543 });
2544 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
2545
2546 // c. Add at the end of the item list (including off the length)
2547 set_labeled_items(&pane, ["A", "B*", "C"], cx);
2548 pane.update(cx, |pane, cx| {
2549 pane.add_item(
2550 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2551 false,
2552 false,
2553 Some(5),
2554 cx,
2555 );
2556 });
2557 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2558
2559 // 2. Add without a destination index
2560 // a. Add with active item at the start of the item list
2561 set_labeled_items(&pane, ["A*", "B", "C"], cx);
2562 pane.update(cx, |pane, cx| {
2563 pane.add_item(
2564 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2565 false,
2566 false,
2567 None,
2568 cx,
2569 );
2570 });
2571 set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
2572
2573 // b. Add with active item at the end of the item list
2574 set_labeled_items(&pane, ["A", "B", "C*"], cx);
2575 pane.update(cx, |pane, cx| {
2576 pane.add_item(
2577 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2578 false,
2579 false,
2580 None,
2581 cx,
2582 );
2583 });
2584 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2585 }
2586
2587 #[gpui::test]
2588 async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
2589 init_test(cx);
2590 let fs = FakeFs::new(cx.executor());
2591
2592 let project = Project::test(fs, None, cx).await;
2593 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2594 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2595
2596 // 1. Add with a destination index
2597 // 1a. Add before the active item
2598 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2599 pane.update(cx, |pane, cx| {
2600 pane.add_item(d, false, false, Some(0), cx);
2601 });
2602 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
2603
2604 // 1b. Add after the active item
2605 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2606 pane.update(cx, |pane, cx| {
2607 pane.add_item(d, false, false, Some(2), cx);
2608 });
2609 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
2610
2611 // 1c. Add at the end of the item list (including off the length)
2612 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2613 pane.update(cx, |pane, cx| {
2614 pane.add_item(a, false, false, Some(5), cx);
2615 });
2616 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
2617
2618 // 1d. Add same item to active index
2619 let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
2620 pane.update(cx, |pane, cx| {
2621 pane.add_item(b, false, false, Some(1), cx);
2622 });
2623 assert_item_labels(&pane, ["A", "B*", "C"], cx);
2624
2625 // 1e. Add item to index after same item in last position
2626 let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
2627 pane.update(cx, |pane, cx| {
2628 pane.add_item(c, false, false, Some(2), cx);
2629 });
2630 assert_item_labels(&pane, ["A", "B", "C*"], cx);
2631
2632 // 2. Add without a destination index
2633 // 2a. Add with active item at the start of the item list
2634 let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
2635 pane.update(cx, |pane, cx| {
2636 pane.add_item(d, false, false, None, cx);
2637 });
2638 assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
2639
2640 // 2b. Add with active item at the end of the item list
2641 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
2642 pane.update(cx, |pane, cx| {
2643 pane.add_item(a, false, false, None, cx);
2644 });
2645 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
2646
2647 // 2c. Add active item to active item at end of list
2648 let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
2649 pane.update(cx, |pane, cx| {
2650 pane.add_item(c, false, false, None, cx);
2651 });
2652 assert_item_labels(&pane, ["A", "B", "C*"], cx);
2653
2654 // 2d. Add active item to active item at start of list
2655 let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
2656 pane.update(cx, |pane, cx| {
2657 pane.add_item(a, false, false, None, cx);
2658 });
2659 assert_item_labels(&pane, ["A*", "B", "C"], cx);
2660 }
2661
2662 #[gpui::test]
2663 async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
2664 init_test(cx);
2665 let fs = FakeFs::new(cx.executor());
2666
2667 let project = Project::test(fs, None, cx).await;
2668 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2669 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2670
2671 // singleton view
2672 pane.update(cx, |pane, cx| {
2673 pane.add_item(
2674 Box::new(cx.new_view(|cx| {
2675 TestItem::new(cx)
2676 .with_singleton(true)
2677 .with_label("buffer 1")
2678 .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
2679 })),
2680 false,
2681 false,
2682 None,
2683 cx,
2684 );
2685 });
2686 assert_item_labels(&pane, ["buffer 1*"], cx);
2687
2688 // new singleton view with the same project entry
2689 pane.update(cx, |pane, cx| {
2690 pane.add_item(
2691 Box::new(cx.new_view(|cx| {
2692 TestItem::new(cx)
2693 .with_singleton(true)
2694 .with_label("buffer 1")
2695 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
2696 })),
2697 false,
2698 false,
2699 None,
2700 cx,
2701 );
2702 });
2703 assert_item_labels(&pane, ["buffer 1*"], cx);
2704
2705 // new singleton view with different project entry
2706 pane.update(cx, |pane, cx| {
2707 pane.add_item(
2708 Box::new(cx.new_view(|cx| {
2709 TestItem::new(cx)
2710 .with_singleton(true)
2711 .with_label("buffer 2")
2712 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
2713 })),
2714 false,
2715 false,
2716 None,
2717 cx,
2718 );
2719 });
2720 assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
2721
2722 // new multibuffer view with the same project entry
2723 pane.update(cx, |pane, cx| {
2724 pane.add_item(
2725 Box::new(cx.new_view(|cx| {
2726 TestItem::new(cx)
2727 .with_singleton(false)
2728 .with_label("multibuffer 1")
2729 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
2730 })),
2731 false,
2732 false,
2733 None,
2734 cx,
2735 );
2736 });
2737 assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
2738
2739 // another multibuffer view with the same project entry
2740 pane.update(cx, |pane, cx| {
2741 pane.add_item(
2742 Box::new(cx.new_view(|cx| {
2743 TestItem::new(cx)
2744 .with_singleton(false)
2745 .with_label("multibuffer 1b")
2746 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
2747 })),
2748 false,
2749 false,
2750 None,
2751 cx,
2752 );
2753 });
2754 assert_item_labels(
2755 &pane,
2756 ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
2757 cx,
2758 );
2759 }
2760
2761 #[gpui::test]
2762 async fn test_remove_item_ordering(cx: &mut TestAppContext) {
2763 init_test(cx);
2764 let fs = FakeFs::new(cx.executor());
2765
2766 let project = Project::test(fs, None, cx).await;
2767 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2768 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2769
2770 add_labeled_item(&pane, "A", false, cx);
2771 add_labeled_item(&pane, "B", false, cx);
2772 add_labeled_item(&pane, "C", false, cx);
2773 add_labeled_item(&pane, "D", false, cx);
2774 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2775
2776 pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
2777 add_labeled_item(&pane, "1", false, cx);
2778 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
2779
2780 pane.update(cx, |pane, cx| {
2781 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
2782 })
2783 .unwrap()
2784 .await
2785 .unwrap();
2786 assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
2787
2788 pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
2789 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2790
2791 pane.update(cx, |pane, cx| {
2792 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
2793 })
2794 .unwrap()
2795 .await
2796 .unwrap();
2797 assert_item_labels(&pane, ["A", "B*", "C"], cx);
2798
2799 pane.update(cx, |pane, cx| {
2800 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
2801 })
2802 .unwrap()
2803 .await
2804 .unwrap();
2805 assert_item_labels(&pane, ["A", "C*"], cx);
2806
2807 pane.update(cx, |pane, cx| {
2808 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
2809 })
2810 .unwrap()
2811 .await
2812 .unwrap();
2813 assert_item_labels(&pane, ["A*"], cx);
2814 }
2815
2816 #[gpui::test]
2817 async fn test_close_inactive_items(cx: &mut TestAppContext) {
2818 init_test(cx);
2819 let fs = FakeFs::new(cx.executor());
2820
2821 let project = Project::test(fs, None, cx).await;
2822 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2823 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2824
2825 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
2826
2827 pane.update(cx, |pane, cx| {
2828 pane.close_inactive_items(&CloseInactiveItems { save_intent: None }, cx)
2829 })
2830 .unwrap()
2831 .await
2832 .unwrap();
2833 assert_item_labels(&pane, ["C*"], cx);
2834 }
2835
2836 #[gpui::test]
2837 async fn test_close_clean_items(cx: &mut TestAppContext) {
2838 init_test(cx);
2839 let fs = FakeFs::new(cx.executor());
2840
2841 let project = Project::test(fs, None, cx).await;
2842 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2843 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2844
2845 add_labeled_item(&pane, "A", true, cx);
2846 add_labeled_item(&pane, "B", false, cx);
2847 add_labeled_item(&pane, "C", true, cx);
2848 add_labeled_item(&pane, "D", false, cx);
2849 add_labeled_item(&pane, "E", false, cx);
2850 assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
2851
2852 pane.update(cx, |pane, cx| pane.close_clean_items(&CloseCleanItems, cx))
2853 .unwrap()
2854 .await
2855 .unwrap();
2856 assert_item_labels(&pane, ["A^", "C*^"], cx);
2857 }
2858
2859 #[gpui::test]
2860 async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
2861 init_test(cx);
2862 let fs = FakeFs::new(cx.executor());
2863
2864 let project = Project::test(fs, None, cx).await;
2865 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2866 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2867
2868 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
2869
2870 pane.update(cx, |pane, cx| {
2871 pane.close_items_to_the_left(&CloseItemsToTheLeft, cx)
2872 })
2873 .unwrap()
2874 .await
2875 .unwrap();
2876 assert_item_labels(&pane, ["C*", "D", "E"], cx);
2877 }
2878
2879 #[gpui::test]
2880 async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
2881 init_test(cx);
2882 let fs = FakeFs::new(cx.executor());
2883
2884 let project = Project::test(fs, None, cx).await;
2885 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2886 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2887
2888 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
2889
2890 pane.update(cx, |pane, cx| {
2891 pane.close_items_to_the_right(&CloseItemsToTheRight, cx)
2892 })
2893 .unwrap()
2894 .await
2895 .unwrap();
2896 assert_item_labels(&pane, ["A", "B", "C*"], cx);
2897 }
2898
2899 #[gpui::test]
2900 async fn test_close_all_items(cx: &mut TestAppContext) {
2901 init_test(cx);
2902 let fs = FakeFs::new(cx.executor());
2903
2904 let project = Project::test(fs, None, cx).await;
2905 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2906 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2907
2908 add_labeled_item(&pane, "A", false, cx);
2909 add_labeled_item(&pane, "B", false, cx);
2910 add_labeled_item(&pane, "C", false, cx);
2911 assert_item_labels(&pane, ["A", "B", "C*"], cx);
2912
2913 pane.update(cx, |pane, cx| {
2914 pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
2915 })
2916 .unwrap()
2917 .await
2918 .unwrap();
2919 assert_item_labels(&pane, [], cx);
2920
2921 add_labeled_item(&pane, "A", true, cx);
2922 add_labeled_item(&pane, "B", true, cx);
2923 add_labeled_item(&pane, "C", true, cx);
2924 assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
2925
2926 let save = pane
2927 .update(cx, |pane, cx| {
2928 pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
2929 })
2930 .unwrap();
2931
2932 cx.executor().run_until_parked();
2933 cx.simulate_prompt_answer(2);
2934 save.await.unwrap();
2935 assert_item_labels(&pane, [], cx);
2936 }
2937
2938 fn init_test(cx: &mut TestAppContext) {
2939 cx.update(|cx| {
2940 let settings_store = SettingsStore::test(cx);
2941 cx.set_global(settings_store);
2942 theme::init(LoadThemes::JustBase, cx);
2943 crate::init_settings(cx);
2944 Project::init_settings(cx);
2945 });
2946 }
2947
2948 fn add_labeled_item(
2949 pane: &View<Pane>,
2950 label: &str,
2951 is_dirty: bool,
2952 cx: &mut VisualTestContext,
2953 ) -> Box<View<TestItem>> {
2954 pane.update(cx, |pane, cx| {
2955 let labeled_item = Box::new(
2956 cx.new_view(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)),
2957 );
2958 pane.add_item(labeled_item.clone(), false, false, None, cx);
2959 labeled_item
2960 })
2961 }
2962
2963 fn set_labeled_items<const COUNT: usize>(
2964 pane: &View<Pane>,
2965 labels: [&str; COUNT],
2966 cx: &mut VisualTestContext,
2967 ) -> [Box<View<TestItem>>; COUNT] {
2968 pane.update(cx, |pane, cx| {
2969 pane.items.clear();
2970 let mut active_item_index = 0;
2971
2972 let mut index = 0;
2973 let items = labels.map(|mut label| {
2974 if label.ends_with('*') {
2975 label = label.trim_end_matches('*');
2976 active_item_index = index;
2977 }
2978
2979 let labeled_item = Box::new(cx.new_view(|cx| TestItem::new(cx).with_label(label)));
2980 pane.add_item(labeled_item.clone(), false, false, None, cx);
2981 index += 1;
2982 labeled_item
2983 });
2984
2985 pane.activate_item(active_item_index, false, false, cx);
2986
2987 items
2988 })
2989 }
2990
2991 // Assert the item label, with the active item label suffixed with a '*'
2992 fn assert_item_labels<const COUNT: usize>(
2993 pane: &View<Pane>,
2994 expected_states: [&str; COUNT],
2995 cx: &mut VisualTestContext,
2996 ) {
2997 pane.update(cx, |pane, cx| {
2998 let actual_states = pane
2999 .items
3000 .iter()
3001 .enumerate()
3002 .map(|(ix, item)| {
3003 let mut state = item
3004 .to_any()
3005 .downcast::<TestItem>()
3006 .unwrap()
3007 .read(cx)
3008 .label
3009 .clone();
3010 if ix == pane.active_item_index {
3011 state.push('*');
3012 }
3013 if item.is_dirty(cx) {
3014 state.push('^');
3015 }
3016 state
3017 })
3018 .collect::<Vec<_>>();
3019
3020 assert_eq!(
3021 actual_states, expected_states,
3022 "pane items do not match expectation"
3023 );
3024 })
3025 }
3026}
3027
3028impl Render for DraggedTab {
3029 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3030 let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
3031 let label = self.item.tab_content(
3032 TabContentParams {
3033 detail: Some(self.detail),
3034 selected: false,
3035 preview: false,
3036 },
3037 cx,
3038 );
3039 Tab::new("")
3040 .selected(self.is_active)
3041 .child(label)
3042 .render(cx)
3043 .font(ui_font)
3044 }
3045}