1use crate::{
2 item::{
3 ClosePosition, Item, ItemHandle, ItemSettings, PreviewTabsSettings, TabContentParams,
4 WeakItemHandle,
5 },
6 toolbar::Toolbar,
7 workspace_settings::{AutosaveSetting, TabBarSettings, WorkspaceSettings},
8 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 return None;
952 }
953 let active_item_id = self.items[self.active_item_index].item_id();
954 Some(self.close_item_by_id(
955 active_item_id,
956 action.save_intent.unwrap_or(SaveIntent::Close),
957 cx,
958 ))
959 }
960
961 pub fn close_item_by_id(
962 &mut self,
963 item_id_to_close: EntityId,
964 save_intent: SaveIntent,
965 cx: &mut ViewContext<Self>,
966 ) -> Task<Result<()>> {
967 self.close_items(cx, save_intent, move |view_id| view_id == item_id_to_close)
968 }
969
970 pub fn close_inactive_items(
971 &mut self,
972 action: &CloseInactiveItems,
973 cx: &mut ViewContext<Self>,
974 ) -> Option<Task<Result<()>>> {
975 if self.items.is_empty() {
976 return None;
977 }
978
979 let active_item_id = self.items[self.active_item_index].item_id();
980 Some(self.close_items(
981 cx,
982 action.save_intent.unwrap_or(SaveIntent::Close),
983 move |item_id| item_id != active_item_id,
984 ))
985 }
986
987 pub fn close_clean_items(
988 &mut self,
989 _: &CloseCleanItems,
990 cx: &mut ViewContext<Self>,
991 ) -> Option<Task<Result<()>>> {
992 let item_ids: Vec<_> = self
993 .items()
994 .filter(|item| !item.is_dirty(cx))
995 .map(|item| item.item_id())
996 .collect();
997 Some(self.close_items(cx, SaveIntent::Close, move |item_id| {
998 item_ids.contains(&item_id)
999 }))
1000 }
1001
1002 pub fn close_items_to_the_left(
1003 &mut self,
1004 _: &CloseItemsToTheLeft,
1005 cx: &mut ViewContext<Self>,
1006 ) -> Option<Task<Result<()>>> {
1007 if self.items.is_empty() {
1008 return None;
1009 }
1010 let active_item_id = self.items[self.active_item_index].item_id();
1011 Some(self.close_items_to_the_left_by_id(active_item_id, cx))
1012 }
1013
1014 pub fn close_items_to_the_left_by_id(
1015 &mut self,
1016 item_id: EntityId,
1017 cx: &mut ViewContext<Self>,
1018 ) -> Task<Result<()>> {
1019 let item_ids: Vec<_> = self
1020 .items()
1021 .take_while(|item| item.item_id() != item_id)
1022 .map(|item| item.item_id())
1023 .collect();
1024 self.close_items(cx, SaveIntent::Close, move |item_id| {
1025 item_ids.contains(&item_id)
1026 })
1027 }
1028
1029 pub fn close_items_to_the_right(
1030 &mut self,
1031 _: &CloseItemsToTheRight,
1032 cx: &mut ViewContext<Self>,
1033 ) -> Option<Task<Result<()>>> {
1034 if self.items.is_empty() {
1035 return None;
1036 }
1037 let active_item_id = self.items[self.active_item_index].item_id();
1038 Some(self.close_items_to_the_right_by_id(active_item_id, cx))
1039 }
1040
1041 pub fn close_items_to_the_right_by_id(
1042 &mut self,
1043 item_id: EntityId,
1044 cx: &mut ViewContext<Self>,
1045 ) -> Task<Result<()>> {
1046 let item_ids: Vec<_> = self
1047 .items()
1048 .rev()
1049 .take_while(|item| item.item_id() != item_id)
1050 .map(|item| item.item_id())
1051 .collect();
1052 self.close_items(cx, SaveIntent::Close, move |item_id| {
1053 item_ids.contains(&item_id)
1054 })
1055 }
1056
1057 pub fn close_all_items(
1058 &mut self,
1059 action: &CloseAllItems,
1060 cx: &mut ViewContext<Self>,
1061 ) -> Option<Task<Result<()>>> {
1062 if self.items.is_empty() {
1063 return None;
1064 }
1065
1066 Some(
1067 self.close_items(cx, action.save_intent.unwrap_or(SaveIntent::Close), |_| {
1068 true
1069 }),
1070 )
1071 }
1072
1073 pub(super) fn file_names_for_prompt(
1074 items: &mut dyn Iterator<Item = &Box<dyn ItemHandle>>,
1075 all_dirty_items: usize,
1076 cx: &AppContext,
1077 ) -> (String, String) {
1078 /// Quantity of item paths displayed in prompt prior to cutoff..
1079 const FILE_NAMES_CUTOFF_POINT: usize = 10;
1080 let mut file_names: Vec<_> = items
1081 .filter_map(|item| {
1082 item.project_path(cx).and_then(|project_path| {
1083 project_path
1084 .path
1085 .file_name()
1086 .and_then(|name| name.to_str().map(ToOwned::to_owned))
1087 })
1088 })
1089 .take(FILE_NAMES_CUTOFF_POINT)
1090 .collect();
1091 let should_display_followup_text =
1092 all_dirty_items > FILE_NAMES_CUTOFF_POINT || file_names.len() != all_dirty_items;
1093 if should_display_followup_text {
1094 let not_shown_files = all_dirty_items - file_names.len();
1095 if not_shown_files == 1 {
1096 file_names.push(".. 1 file not shown".into());
1097 } else {
1098 file_names.push(format!(".. {} files not shown", not_shown_files));
1099 }
1100 }
1101 (
1102 format!(
1103 "Do you want to save changes to the following {} files?",
1104 all_dirty_items
1105 ),
1106 file_names.join("\n"),
1107 )
1108 }
1109
1110 pub fn close_items(
1111 &mut self,
1112 cx: &mut ViewContext<Pane>,
1113 mut save_intent: SaveIntent,
1114 should_close: impl Fn(EntityId) -> bool,
1115 ) -> Task<Result<()>> {
1116 // Find the items to close.
1117 let mut items_to_close = Vec::new();
1118 let mut dirty_items = Vec::new();
1119 for item in &self.items {
1120 if should_close(item.item_id()) {
1121 items_to_close.push(item.boxed_clone());
1122 if item.is_dirty(cx) {
1123 dirty_items.push(item.boxed_clone());
1124 }
1125 }
1126 }
1127
1128 // If a buffer is open both in a singleton editor and in a multibuffer, make sure
1129 // to focus the singleton buffer when prompting to save that buffer, as opposed
1130 // to focusing the multibuffer, because this gives the user a more clear idea
1131 // of what content they would be saving.
1132 items_to_close.sort_by_key(|item| !item.is_singleton(cx));
1133
1134 let workspace = self.workspace.clone();
1135 cx.spawn(|pane, mut cx| async move {
1136 if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
1137 let answer = pane.update(&mut cx, |_, cx| {
1138 let (prompt, detail) =
1139 Self::file_names_for_prompt(&mut dirty_items.iter(), dirty_items.len(), cx);
1140 cx.prompt(
1141 PromptLevel::Warning,
1142 &prompt,
1143 Some(&detail),
1144 &["Save all", "Discard all", "Cancel"],
1145 )
1146 })?;
1147 match answer.await {
1148 Ok(0) => save_intent = SaveIntent::SaveAll,
1149 Ok(1) => save_intent = SaveIntent::Skip,
1150 _ => {}
1151 }
1152 }
1153 let mut saved_project_items_ids = HashSet::default();
1154 for item in items_to_close.clone() {
1155 // Find the item's current index and its set of project item models. Avoid
1156 // storing these in advance, in case they have changed since this task
1157 // was started.
1158 let (item_ix, mut project_item_ids) = pane.update(&mut cx, |pane, cx| {
1159 (pane.index_for_item(&*item), item.project_item_model_ids(cx))
1160 })?;
1161 let item_ix = if let Some(ix) = item_ix {
1162 ix
1163 } else {
1164 continue;
1165 };
1166
1167 // Check if this view has any project items that are not open anywhere else
1168 // in the workspace, AND that the user has not already been prompted to save.
1169 // If there are any such project entries, prompt the user to save this item.
1170 let project = workspace.update(&mut cx, |workspace, cx| {
1171 for item in workspace.items(cx) {
1172 if !items_to_close
1173 .iter()
1174 .any(|item_to_close| item_to_close.item_id() == item.item_id())
1175 {
1176 let other_project_item_ids = item.project_item_model_ids(cx);
1177 project_item_ids.retain(|id| !other_project_item_ids.contains(id));
1178 }
1179 }
1180 workspace.project().clone()
1181 })?;
1182 let should_save = project_item_ids
1183 .iter()
1184 .any(|id| saved_project_items_ids.insert(*id));
1185
1186 if should_save
1187 && !Self::save_item(
1188 project.clone(),
1189 &pane,
1190 item_ix,
1191 &*item,
1192 save_intent,
1193 &mut cx,
1194 )
1195 .await?
1196 {
1197 break;
1198 }
1199
1200 // Remove the item from the pane.
1201 pane.update(&mut cx, |pane, cx| {
1202 if let Some(item_ix) = pane
1203 .items
1204 .iter()
1205 .position(|i| i.item_id() == item.item_id())
1206 {
1207 pane.remove_item(item_ix, false, true, cx);
1208 }
1209 })
1210 .ok();
1211 }
1212
1213 pane.update(&mut cx, |_, cx| cx.notify()).ok();
1214 Ok(())
1215 })
1216 }
1217
1218 pub fn remove_item(
1219 &mut self,
1220 item_index: usize,
1221 activate_pane: bool,
1222 close_pane_if_empty: bool,
1223 cx: &mut ViewContext<Self>,
1224 ) {
1225 self.activation_history
1226 .retain(|entry| entry.entity_id != self.items[item_index].item_id());
1227
1228 if item_index == self.active_item_index {
1229 let index_to_activate = self
1230 .activation_history
1231 .pop()
1232 .and_then(|last_activated_item| {
1233 self.items.iter().enumerate().find_map(|(index, item)| {
1234 (item.item_id() == last_activated_item.entity_id).then_some(index)
1235 })
1236 })
1237 // We didn't have a valid activation history entry, so fallback
1238 // to activating the item to the left
1239 .unwrap_or_else(|| item_index.min(self.items.len()).saturating_sub(1));
1240
1241 let should_activate = activate_pane || self.has_focus(cx);
1242 if self.items.len() == 1 && should_activate {
1243 self.focus_handle.focus(cx);
1244 } else {
1245 self.activate_item(index_to_activate, should_activate, should_activate, cx);
1246 }
1247 }
1248
1249 let item = self.items.remove(item_index);
1250
1251 cx.emit(Event::RemoveItem {
1252 item_id: item.item_id(),
1253 });
1254 if self.items.is_empty() {
1255 item.deactivated(cx);
1256 if close_pane_if_empty {
1257 self.update_toolbar(cx);
1258 cx.emit(Event::Remove);
1259 }
1260 }
1261
1262 if item_index < self.active_item_index {
1263 self.active_item_index -= 1;
1264 }
1265
1266 let mode = self.nav_history.mode();
1267 self.nav_history.set_mode(NavigationMode::ClosingItem);
1268 item.deactivated(cx);
1269 self.nav_history.set_mode(mode);
1270
1271 if self.is_active_preview_item(item.item_id()) {
1272 self.set_preview_item_id(None, cx);
1273 }
1274
1275 if let Some(path) = item.project_path(cx) {
1276 let abs_path = self
1277 .nav_history
1278 .0
1279 .lock()
1280 .paths_by_item
1281 .get(&item.item_id())
1282 .and_then(|(_, abs_path)| abs_path.clone());
1283
1284 self.nav_history
1285 .0
1286 .lock()
1287 .paths_by_item
1288 .insert(item.item_id(), (path, abs_path));
1289 } else {
1290 self.nav_history
1291 .0
1292 .lock()
1293 .paths_by_item
1294 .remove(&item.item_id());
1295 }
1296
1297 if self.items.is_empty() && close_pane_if_empty && self.zoomed {
1298 cx.emit(Event::ZoomOut);
1299 }
1300
1301 cx.notify();
1302 }
1303
1304 pub async fn save_item(
1305 project: Model<Project>,
1306 pane: &WeakView<Pane>,
1307 item_ix: usize,
1308 item: &dyn ItemHandle,
1309 save_intent: SaveIntent,
1310 cx: &mut AsyncWindowContext,
1311 ) -> Result<bool> {
1312 const CONFLICT_MESSAGE: &str =
1313 "This file has changed on disk since you started editing it. Do you want to overwrite it?";
1314
1315 if save_intent == SaveIntent::Skip {
1316 return Ok(true);
1317 }
1318
1319 let (mut has_conflict, mut is_dirty, mut can_save, can_save_as) = cx.update(|cx| {
1320 (
1321 item.has_conflict(cx),
1322 item.is_dirty(cx),
1323 item.can_save(cx),
1324 item.is_singleton(cx),
1325 )
1326 })?;
1327
1328 // when saving a single buffer, we ignore whether or not it's dirty.
1329 if save_intent == SaveIntent::Save || save_intent == SaveIntent::SaveWithoutFormat {
1330 is_dirty = true;
1331 }
1332
1333 if save_intent == SaveIntent::SaveAs {
1334 is_dirty = true;
1335 has_conflict = false;
1336 can_save = false;
1337 }
1338
1339 if save_intent == SaveIntent::Overwrite {
1340 has_conflict = false;
1341 }
1342
1343 let should_format = save_intent != SaveIntent::SaveWithoutFormat;
1344
1345 if has_conflict && can_save {
1346 let answer = pane.update(cx, |pane, cx| {
1347 pane.activate_item(item_ix, true, true, cx);
1348 cx.prompt(
1349 PromptLevel::Warning,
1350 CONFLICT_MESSAGE,
1351 None,
1352 &["Overwrite", "Discard", "Cancel"],
1353 )
1354 })?;
1355 match answer.await {
1356 Ok(0) => {
1357 pane.update(cx, |_, cx| item.save(should_format, project, cx))?
1358 .await?
1359 }
1360 Ok(1) => pane.update(cx, |_, cx| item.reload(project, cx))?.await?,
1361 _ => return Ok(false),
1362 }
1363 } else if is_dirty && (can_save || can_save_as) {
1364 if save_intent == SaveIntent::Close {
1365 let will_autosave = cx.update(|cx| {
1366 matches!(
1367 WorkspaceSettings::get_global(cx).autosave,
1368 AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange
1369 ) && Self::can_autosave_item(item, cx)
1370 })?;
1371 if !will_autosave {
1372 let item_id = item.item_id();
1373 let answer_task = pane.update(cx, |pane, cx| {
1374 if pane.save_modals_spawned.insert(item_id) {
1375 pane.activate_item(item_ix, true, true, cx);
1376 let prompt = dirty_message_for(item.project_path(cx));
1377 Some(cx.prompt(
1378 PromptLevel::Warning,
1379 &prompt,
1380 None,
1381 &["Save", "Don't Save", "Cancel"],
1382 ))
1383 } else {
1384 None
1385 }
1386 })?;
1387 if let Some(answer_task) = answer_task {
1388 let answer = answer_task.await;
1389 pane.update(cx, |pane, _| {
1390 if !pane.save_modals_spawned.remove(&item_id) {
1391 debug_panic!(
1392 "save modal was not present in spawned modals after awaiting for its answer"
1393 )
1394 }
1395 })?;
1396 match answer {
1397 Ok(0) => {}
1398 Ok(1) => return Ok(true), // Don't save this file
1399 _ => return Ok(false), // Cancel
1400 }
1401 } else {
1402 return Ok(false);
1403 }
1404 }
1405 }
1406
1407 if can_save {
1408 pane.update(cx, |_, cx| item.save(should_format, project, cx))?
1409 .await?;
1410 } else if can_save_as {
1411 let abs_path = pane.update(cx, |pane, cx| {
1412 pane.workspace
1413 .update(cx, |workspace, cx| workspace.prompt_for_new_path(cx))
1414 })??;
1415 if let Some(abs_path) = abs_path.await.ok().flatten() {
1416 pane.update(cx, |_, cx| item.save_as(project, abs_path, cx))?
1417 .await?;
1418 } else {
1419 return Ok(false);
1420 }
1421 }
1422 }
1423 Ok(true)
1424 }
1425
1426 fn can_autosave_item(item: &dyn ItemHandle, cx: &AppContext) -> bool {
1427 let is_deleted = item.project_entry_ids(cx).is_empty();
1428 item.is_dirty(cx) && !item.has_conflict(cx) && item.can_save(cx) && !is_deleted
1429 }
1430
1431 pub fn autosave_item(
1432 item: &dyn ItemHandle,
1433 project: Model<Project>,
1434 cx: &mut WindowContext,
1435 ) -> Task<Result<()>> {
1436 let format = if let AutosaveSetting::AfterDelay { .. } =
1437 WorkspaceSettings::get_global(cx).autosave
1438 {
1439 false
1440 } else {
1441 true
1442 };
1443 if Self::can_autosave_item(item, cx) {
1444 item.save(format, project, cx)
1445 } else {
1446 Task::ready(Ok(()))
1447 }
1448 }
1449
1450 pub fn focus(&mut self, cx: &mut ViewContext<Pane>) {
1451 cx.focus(&self.focus_handle);
1452 }
1453
1454 pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
1455 if let Some(active_item) = self.active_item() {
1456 let focus_handle = active_item.focus_handle(cx);
1457 cx.focus(&focus_handle);
1458 }
1459 }
1460
1461 pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
1462 cx.emit(Event::Split(direction));
1463 }
1464
1465 pub fn toolbar(&self) -> &View<Toolbar> {
1466 &self.toolbar
1467 }
1468
1469 pub fn handle_deleted_project_item(
1470 &mut self,
1471 entry_id: ProjectEntryId,
1472 cx: &mut ViewContext<Pane>,
1473 ) -> Option<()> {
1474 let (item_index_to_delete, item_id) = self.items().enumerate().find_map(|(i, item)| {
1475 if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
1476 Some((i, item.item_id()))
1477 } else {
1478 None
1479 }
1480 })?;
1481
1482 self.remove_item(item_index_to_delete, false, true, cx);
1483 self.nav_history.remove_item(item_id);
1484
1485 Some(())
1486 }
1487
1488 fn update_toolbar(&mut self, cx: &mut ViewContext<Self>) {
1489 let active_item = self
1490 .items
1491 .get(self.active_item_index)
1492 .map(|item| item.as_ref());
1493 self.toolbar.update(cx, |toolbar, cx| {
1494 toolbar.set_active_item(active_item, cx);
1495 });
1496 }
1497
1498 fn update_status_bar(&mut self, cx: &mut ViewContext<Self>) {
1499 let workspace = self.workspace.clone();
1500 let pane = cx.view().clone();
1501
1502 cx.window_context().defer(move |cx| {
1503 let Ok(status_bar) = workspace.update(cx, |workspace, _| workspace.status_bar.clone())
1504 else {
1505 return;
1506 };
1507
1508 status_bar.update(cx, move |status_bar, cx| {
1509 status_bar.set_active_pane(&pane, cx);
1510 });
1511 });
1512 }
1513
1514 fn render_tab(
1515 &self,
1516 ix: usize,
1517 item: &Box<dyn ItemHandle>,
1518 detail: usize,
1519 cx: &mut ViewContext<'_, Pane>,
1520 ) -> impl IntoElement {
1521 let is_active = ix == self.active_item_index;
1522 let is_preview = self
1523 .preview_item_id
1524 .map(|id| id == item.item_id())
1525 .unwrap_or(false);
1526
1527 let label = item.tab_content(
1528 TabContentParams {
1529 detail: Some(detail),
1530 selected: is_active,
1531 preview: is_preview,
1532 },
1533 cx,
1534 );
1535 let close_side = &ItemSettings::get_global(cx).close_position;
1536 let indicator = render_item_indicator(item.boxed_clone(), cx);
1537 let item_id = item.item_id();
1538 let is_first_item = ix == 0;
1539 let is_last_item = ix == self.items.len() - 1;
1540 let position_relative_to_active_item = ix.cmp(&self.active_item_index);
1541
1542 let tab = Tab::new(ix)
1543 .position(if is_first_item {
1544 TabPosition::First
1545 } else if is_last_item {
1546 TabPosition::Last
1547 } else {
1548 TabPosition::Middle(position_relative_to_active_item)
1549 })
1550 .close_side(match close_side {
1551 ClosePosition::Left => ui::TabCloseSide::Start,
1552 ClosePosition::Right => ui::TabCloseSide::End,
1553 })
1554 .selected(is_active)
1555 .on_click(
1556 cx.listener(move |pane: &mut Self, _, cx| pane.activate_item(ix, true, true, cx)),
1557 )
1558 // TODO: This should be a click listener with the middle mouse button instead of a mouse down listener.
1559 .on_mouse_down(
1560 MouseButton::Middle,
1561 cx.listener(move |pane, _event, cx| {
1562 pane.close_item_by_id(item_id, SaveIntent::Close, cx)
1563 .detach_and_log_err(cx);
1564 }),
1565 )
1566 .on_mouse_down(
1567 MouseButton::Left,
1568 cx.listener(move |pane, event: &MouseDownEvent, cx| {
1569 if let Some(id) = pane.preview_item_id {
1570 if id == item_id && event.click_count > 1 {
1571 pane.set_preview_item_id(None, cx);
1572 }
1573 }
1574 }),
1575 )
1576 .on_drag(
1577 DraggedTab {
1578 item: item.boxed_clone(),
1579 pane: cx.view().clone(),
1580 detail,
1581 is_active,
1582 ix,
1583 },
1584 |tab, cx| cx.new_view(|_| tab.clone()),
1585 )
1586 .drag_over::<DraggedTab>(|tab, _, cx| {
1587 tab.bg(cx.theme().colors().drop_target_background)
1588 })
1589 .drag_over::<ProjectEntryId>(|tab, _, cx| {
1590 tab.bg(cx.theme().colors().drop_target_background)
1591 })
1592 .when_some(self.can_drop_predicate.clone(), |this, p| {
1593 this.can_drop(move |a, cx| p(a, cx))
1594 })
1595 .on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {
1596 this.drag_split_direction = None;
1597 this.handle_tab_drop(dragged_tab, ix, cx)
1598 }))
1599 .on_drop(cx.listener(move |this, entry_id: &ProjectEntryId, cx| {
1600 this.drag_split_direction = None;
1601 this.handle_project_entry_drop(entry_id, cx)
1602 }))
1603 .on_drop(cx.listener(move |this, paths, cx| {
1604 this.drag_split_direction = None;
1605 this.handle_external_paths_drop(paths, cx)
1606 }))
1607 .when_some(item.tab_tooltip_text(cx), |tab, text| {
1608 tab.tooltip(move |cx| Tooltip::text(text.clone(), cx))
1609 })
1610 .start_slot::<Indicator>(indicator)
1611 .end_slot(
1612 IconButton::new("close tab", IconName::Close)
1613 .shape(IconButtonShape::Square)
1614 .icon_color(Color::Muted)
1615 .size(ButtonSize::None)
1616 .icon_size(IconSize::XSmall)
1617 .on_click(cx.listener(move |pane, _, cx| {
1618 pane.close_item_by_id(item_id, SaveIntent::Close, cx)
1619 .detach_and_log_err(cx);
1620 })),
1621 )
1622 .child(label);
1623
1624 let single_entry_to_resolve = {
1625 let item_entries = self.items[ix].project_entry_ids(cx);
1626 if item_entries.len() == 1 {
1627 Some(item_entries[0])
1628 } else {
1629 None
1630 }
1631 };
1632
1633 let pane = cx.view().downgrade();
1634 right_click_menu(ix).trigger(tab).menu(move |cx| {
1635 let pane = pane.clone();
1636 ContextMenu::build(cx, move |mut menu, cx| {
1637 if let Some(pane) = pane.upgrade() {
1638 menu = menu
1639 .entry(
1640 "Close",
1641 Some(Box::new(CloseActiveItem { save_intent: None })),
1642 cx.handler_for(&pane, move |pane, cx| {
1643 pane.close_item_by_id(item_id, SaveIntent::Close, cx)
1644 .detach_and_log_err(cx);
1645 }),
1646 )
1647 .entry(
1648 "Close Others",
1649 Some(Box::new(CloseInactiveItems { save_intent: None })),
1650 cx.handler_for(&pane, move |pane, cx| {
1651 pane.close_items(cx, SaveIntent::Close, |id| id != item_id)
1652 .detach_and_log_err(cx);
1653 }),
1654 )
1655 .separator()
1656 .entry(
1657 "Close Left",
1658 Some(Box::new(CloseItemsToTheLeft)),
1659 cx.handler_for(&pane, move |pane, cx| {
1660 pane.close_items_to_the_left_by_id(item_id, cx)
1661 .detach_and_log_err(cx);
1662 }),
1663 )
1664 .entry(
1665 "Close Right",
1666 Some(Box::new(CloseItemsToTheRight)),
1667 cx.handler_for(&pane, move |pane, cx| {
1668 pane.close_items_to_the_right_by_id(item_id, cx)
1669 .detach_and_log_err(cx);
1670 }),
1671 )
1672 .separator()
1673 .entry(
1674 "Close Clean",
1675 Some(Box::new(CloseCleanItems)),
1676 cx.handler_for(&pane, move |pane, cx| {
1677 if let Some(task) = pane.close_clean_items(&CloseCleanItems, cx) {
1678 task.detach_and_log_err(cx)
1679 }
1680 }),
1681 )
1682 .entry(
1683 "Close All",
1684 Some(Box::new(CloseAllItems { save_intent: None })),
1685 cx.handler_for(&pane, |pane, cx| {
1686 if let Some(task) =
1687 pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
1688 {
1689 task.detach_and_log_err(cx)
1690 }
1691 }),
1692 );
1693
1694 if let Some(entry) = single_entry_to_resolve {
1695 let parent_abs_path = pane
1696 .update(cx, |pane, cx| {
1697 pane.workspace.update(cx, |workspace, cx| {
1698 let project = workspace.project().read(cx);
1699 project.worktree_for_entry(entry, cx).and_then(|worktree| {
1700 let worktree = worktree.read(cx);
1701 let entry = worktree.entry_for_id(entry)?;
1702 let abs_path = worktree.absolutize(&entry.path).ok()?;
1703 let parent = if entry.is_symlink {
1704 abs_path.canonicalize().ok()?
1705 } else {
1706 abs_path
1707 }
1708 .parent()?
1709 .to_path_buf();
1710 Some(parent)
1711 })
1712 })
1713 })
1714 .ok()
1715 .flatten();
1716
1717 let entry_id = entry.to_proto();
1718 menu = menu
1719 .separator()
1720 .entry(
1721 "Reveal In Project Panel",
1722 Some(Box::new(RevealInProjectPanel {
1723 entry_id: Some(entry_id),
1724 })),
1725 cx.handler_for(&pane, move |pane, cx| {
1726 pane.project.update(cx, |_, cx| {
1727 cx.emit(project::Event::RevealInProjectPanel(
1728 ProjectEntryId::from_proto(entry_id),
1729 ))
1730 });
1731 }),
1732 )
1733 .when_some(parent_abs_path, |menu, abs_path| {
1734 menu.entry(
1735 "Open in Terminal",
1736 Some(Box::new(OpenInTerminal)),
1737 cx.handler_for(&pane, move |_, cx| {
1738 cx.dispatch_action(
1739 OpenTerminal {
1740 working_directory: abs_path.clone(),
1741 }
1742 .boxed_clone(),
1743 );
1744 }),
1745 )
1746 });
1747 }
1748 }
1749
1750 menu
1751 })
1752 })
1753 }
1754
1755 fn render_tab_bar(&mut self, cx: &mut ViewContext<'_, Pane>) -> impl IntoElement {
1756 let navigate_backward = IconButton::new("navigate_backward", IconName::ArrowLeft)
1757 .shape(IconButtonShape::Square)
1758 .icon_size(IconSize::Small)
1759 .on_click({
1760 let view = cx.view().clone();
1761 move |_, cx| view.update(cx, Self::navigate_backward)
1762 })
1763 .disabled(!self.can_navigate_backward())
1764 .tooltip(|cx| Tooltip::for_action("Go Back", &GoBack, cx));
1765
1766 let navigate_forward = IconButton::new("navigate_forward", IconName::ArrowRight)
1767 .shape(IconButtonShape::Square)
1768 .icon_size(IconSize::Small)
1769 .on_click({
1770 let view = cx.view().clone();
1771 move |_, cx| view.update(cx, Self::navigate_forward)
1772 })
1773 .disabled(!self.can_navigate_forward())
1774 .tooltip(|cx| Tooltip::for_action("Go Forward", &GoForward, cx));
1775
1776 TabBar::new("tab_bar")
1777 .track_scroll(self.tab_bar_scroll_handle.clone())
1778 .when(
1779 self.display_nav_history_buttons.unwrap_or_default(),
1780 |tab_bar| tab_bar.start_children(vec![navigate_backward, navigate_forward]),
1781 )
1782 .when(self.has_focus(cx), |tab_bar| {
1783 tab_bar.end_child({
1784 let render_tab_buttons = self.render_tab_bar_buttons.clone();
1785 render_tab_buttons(self, cx)
1786 })
1787 })
1788 .children(
1789 self.items
1790 .iter()
1791 .enumerate()
1792 .zip(tab_details(&self.items, cx))
1793 .map(|((ix, item), detail)| self.render_tab(ix, item, detail, cx)),
1794 )
1795 .child(
1796 div()
1797 .id("tab_bar_drop_target")
1798 .min_w_6()
1799 // HACK: This empty child is currently necessary to force the drop target to appear
1800 // despite us setting a min width above.
1801 .child("")
1802 .h_full()
1803 .flex_grow()
1804 .drag_over::<DraggedTab>(|bar, _, cx| {
1805 bar.bg(cx.theme().colors().drop_target_background)
1806 })
1807 .drag_over::<ProjectEntryId>(|bar, _, cx| {
1808 bar.bg(cx.theme().colors().drop_target_background)
1809 })
1810 .on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {
1811 this.drag_split_direction = None;
1812 this.handle_tab_drop(dragged_tab, this.items.len(), cx)
1813 }))
1814 .on_drop(cx.listener(move |this, entry_id: &ProjectEntryId, cx| {
1815 this.drag_split_direction = None;
1816 this.handle_project_entry_drop(entry_id, cx)
1817 }))
1818 .on_drop(cx.listener(move |this, paths, cx| {
1819 this.drag_split_direction = None;
1820 this.handle_external_paths_drop(paths, cx)
1821 }))
1822 .on_click(cx.listener(move |this, event: &ClickEvent, cx| {
1823 if event.up.click_count == 2 {
1824 cx.dispatch_action(this.double_click_dispatch_action.boxed_clone())
1825 }
1826 })),
1827 )
1828 }
1829
1830 pub fn render_menu_overlay(menu: &View<ContextMenu>) -> Div {
1831 div().absolute().bottom_0().right_0().size_0().child(
1832 deferred(
1833 anchored()
1834 .anchor(AnchorCorner::TopRight)
1835 .child(menu.clone()),
1836 )
1837 .with_priority(1),
1838 )
1839 }
1840
1841 pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
1842 self.zoomed = zoomed;
1843 cx.notify();
1844 }
1845
1846 pub fn is_zoomed(&self) -> bool {
1847 self.zoomed
1848 }
1849
1850 fn handle_drag_move<T>(&mut self, event: &DragMoveEvent<T>, cx: &mut ViewContext<Self>) {
1851 if !self.can_split {
1852 return;
1853 }
1854
1855 let rect = event.bounds.size;
1856
1857 let size = event.bounds.size.width.min(event.bounds.size.height)
1858 * WorkspaceSettings::get_global(cx).drop_target_size;
1859
1860 let relative_cursor = Point::new(
1861 event.event.position.x - event.bounds.left(),
1862 event.event.position.y - event.bounds.top(),
1863 );
1864
1865 let direction = if relative_cursor.x < size
1866 || relative_cursor.x > rect.width - size
1867 || relative_cursor.y < size
1868 || relative_cursor.y > rect.height - size
1869 {
1870 [
1871 SplitDirection::Up,
1872 SplitDirection::Right,
1873 SplitDirection::Down,
1874 SplitDirection::Left,
1875 ]
1876 .iter()
1877 .min_by_key(|side| match side {
1878 SplitDirection::Up => relative_cursor.y,
1879 SplitDirection::Right => rect.width - relative_cursor.x,
1880 SplitDirection::Down => rect.height - relative_cursor.y,
1881 SplitDirection::Left => relative_cursor.x,
1882 })
1883 .cloned()
1884 } else {
1885 None
1886 };
1887
1888 if direction != self.drag_split_direction {
1889 self.drag_split_direction = direction;
1890 }
1891 }
1892
1893 fn handle_tab_drop(
1894 &mut self,
1895 dragged_tab: &DraggedTab,
1896 ix: usize,
1897 cx: &mut ViewContext<'_, Self>,
1898 ) {
1899 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
1900 if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, cx) {
1901 return;
1902 }
1903 }
1904 let mut to_pane = cx.view().clone();
1905 let split_direction = self.drag_split_direction;
1906 let item_id = dragged_tab.item.item_id();
1907 if let Some(preview_item_id) = self.preview_item_id {
1908 if item_id == preview_item_id {
1909 self.set_preview_item_id(None, cx);
1910 }
1911 }
1912
1913 let from_pane = dragged_tab.pane.clone();
1914 self.workspace
1915 .update(cx, |_, cx| {
1916 cx.defer(move |workspace, cx| {
1917 if let Some(split_direction) = split_direction {
1918 to_pane = workspace.split_pane(to_pane, split_direction, cx);
1919 }
1920 workspace.move_item(from_pane, to_pane, item_id, ix, cx);
1921 });
1922 })
1923 .log_err();
1924 }
1925
1926 fn handle_project_entry_drop(
1927 &mut self,
1928 project_entry_id: &ProjectEntryId,
1929 cx: &mut ViewContext<'_, Self>,
1930 ) {
1931 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
1932 if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, cx) {
1933 return;
1934 }
1935 }
1936 let mut to_pane = cx.view().clone();
1937 let split_direction = self.drag_split_direction;
1938 let project_entry_id = *project_entry_id;
1939 self.workspace
1940 .update(cx, |_, cx| {
1941 cx.defer(move |workspace, cx| {
1942 if let Some(path) = workspace
1943 .project()
1944 .read(cx)
1945 .path_for_entry(project_entry_id, cx)
1946 {
1947 if let Some(split_direction) = split_direction {
1948 to_pane = workspace.split_pane(to_pane, split_direction, cx);
1949 }
1950 workspace
1951 .open_path(path, Some(to_pane.downgrade()), true, cx)
1952 .detach_and_log_err(cx);
1953 }
1954 });
1955 })
1956 .log_err();
1957 }
1958
1959 fn handle_external_paths_drop(
1960 &mut self,
1961 paths: &ExternalPaths,
1962 cx: &mut ViewContext<'_, Self>,
1963 ) {
1964 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
1965 if let ControlFlow::Break(()) = custom_drop_handle(self, paths, cx) {
1966 return;
1967 }
1968 }
1969 let mut to_pane = cx.view().clone();
1970 let mut split_direction = self.drag_split_direction;
1971 let paths = paths.paths().to_vec();
1972 let is_remote = self
1973 .workspace
1974 .update(cx, |workspace, cx| {
1975 if workspace.project().read(cx).is_remote() {
1976 workspace.show_error(
1977 &anyhow::anyhow!("Cannot drop files on a remote project"),
1978 cx,
1979 );
1980 true
1981 } else {
1982 false
1983 }
1984 })
1985 .unwrap_or(true);
1986 if is_remote {
1987 return;
1988 }
1989
1990 self.workspace
1991 .update(cx, |workspace, cx| {
1992 let fs = Arc::clone(workspace.project().read(cx).fs());
1993 cx.spawn(|workspace, mut cx| async move {
1994 let mut is_file_checks = FuturesUnordered::new();
1995 for path in &paths {
1996 is_file_checks.push(fs.is_file(path))
1997 }
1998 let mut has_files_to_open = false;
1999 while let Some(is_file) = is_file_checks.next().await {
2000 if is_file {
2001 has_files_to_open = true;
2002 break;
2003 }
2004 }
2005 drop(is_file_checks);
2006 if !has_files_to_open {
2007 split_direction = None;
2008 }
2009
2010 if let Some(open_task) = workspace
2011 .update(&mut cx, |workspace, cx| {
2012 if let Some(split_direction) = split_direction {
2013 to_pane = workspace.split_pane(to_pane, split_direction, cx);
2014 }
2015 workspace.open_paths(
2016 paths,
2017 OpenVisible::OnlyDirectories,
2018 Some(to_pane.downgrade()),
2019 cx,
2020 )
2021 })
2022 .ok()
2023 {
2024 let _opened_items: Vec<_> = open_task.await;
2025 }
2026 })
2027 .detach();
2028 })
2029 .log_err();
2030 }
2031
2032 pub fn display_nav_history_buttons(&mut self, display: Option<bool>) {
2033 self.display_nav_history_buttons = display;
2034 }
2035}
2036
2037impl FocusableView for Pane {
2038 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
2039 self.focus_handle.clone()
2040 }
2041}
2042
2043impl Render for Pane {
2044 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2045 let mut key_context = KeyContext::new_with_defaults();
2046 key_context.add("Pane");
2047 if self.active_item().is_none() {
2048 key_context.add("EmptyPane");
2049 }
2050
2051 let should_display_tab_bar = self.should_display_tab_bar.clone();
2052 let display_tab_bar = should_display_tab_bar(cx);
2053
2054 v_flex()
2055 .key_context(key_context)
2056 .track_focus(&self.focus_handle)
2057 .size_full()
2058 .flex_none()
2059 .overflow_hidden()
2060 .on_action(cx.listener(|pane, _: &AlternateFile, cx| {
2061 pane.alternate_file(cx);
2062 }))
2063 .on_action(cx.listener(|pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx)))
2064 .on_action(cx.listener(|pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx)))
2065 .on_action(
2066 cx.listener(|pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx)),
2067 )
2068 .on_action(cx.listener(|pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx)))
2069 .on_action(cx.listener(|pane, _: &GoBack, cx| pane.navigate_backward(cx)))
2070 .on_action(cx.listener(|pane, _: &GoForward, cx| pane.navigate_forward(cx)))
2071 .on_action(cx.listener(Pane::toggle_zoom))
2072 .on_action(cx.listener(|pane: &mut Pane, action: &ActivateItem, cx| {
2073 pane.activate_item(action.0, true, true, cx);
2074 }))
2075 .on_action(cx.listener(|pane: &mut Pane, _: &ActivateLastItem, cx| {
2076 pane.activate_item(pane.items.len() - 1, true, true, cx);
2077 }))
2078 .on_action(cx.listener(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
2079 pane.activate_prev_item(true, cx);
2080 }))
2081 .on_action(cx.listener(|pane: &mut Pane, _: &ActivateNextItem, cx| {
2082 pane.activate_next_item(true, cx);
2083 }))
2084 .when(PreviewTabsSettings::get_global(cx).enabled, |this| {
2085 this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, cx| {
2086 if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) {
2087 if pane.is_active_preview_item(active_item_id) {
2088 pane.set_preview_item_id(None, cx);
2089 } else {
2090 pane.set_preview_item_id(Some(active_item_id), cx);
2091 }
2092 }
2093 }))
2094 })
2095 .on_action(
2096 cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
2097 if let Some(task) = pane.close_active_item(action, cx) {
2098 task.detach_and_log_err(cx)
2099 }
2100 }),
2101 )
2102 .on_action(
2103 cx.listener(|pane: &mut Self, action: &CloseInactiveItems, cx| {
2104 if let Some(task) = pane.close_inactive_items(action, cx) {
2105 task.detach_and_log_err(cx)
2106 }
2107 }),
2108 )
2109 .on_action(
2110 cx.listener(|pane: &mut Self, action: &CloseCleanItems, cx| {
2111 if let Some(task) = pane.close_clean_items(action, cx) {
2112 task.detach_and_log_err(cx)
2113 }
2114 }),
2115 )
2116 .on_action(
2117 cx.listener(|pane: &mut Self, action: &CloseItemsToTheLeft, cx| {
2118 if let Some(task) = pane.close_items_to_the_left(action, cx) {
2119 task.detach_and_log_err(cx)
2120 }
2121 }),
2122 )
2123 .on_action(
2124 cx.listener(|pane: &mut Self, action: &CloseItemsToTheRight, cx| {
2125 if let Some(task) = pane.close_items_to_the_right(action, cx) {
2126 task.detach_and_log_err(cx)
2127 }
2128 }),
2129 )
2130 .on_action(cx.listener(|pane: &mut Self, action: &CloseAllItems, cx| {
2131 if let Some(task) = pane.close_all_items(action, cx) {
2132 task.detach_and_log_err(cx)
2133 }
2134 }))
2135 .on_action(
2136 cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
2137 if let Some(task) = pane.close_active_item(action, cx) {
2138 task.detach_and_log_err(cx)
2139 }
2140 }),
2141 )
2142 .on_action(
2143 cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, cx| {
2144 let entry_id = action
2145 .entry_id
2146 .map(ProjectEntryId::from_proto)
2147 .or_else(|| pane.active_item()?.project_entry_ids(cx).first().copied());
2148 if let Some(entry_id) = entry_id {
2149 pane.project.update(cx, |_, cx| {
2150 cx.emit(project::Event::RevealInProjectPanel(entry_id))
2151 });
2152 }
2153 }),
2154 )
2155 .when(self.active_item().is_some() && display_tab_bar, |pane| {
2156 pane.child(self.render_tab_bar(cx))
2157 })
2158 .child({
2159 let has_worktrees = self.project.read(cx).worktrees().next().is_some();
2160 // main content
2161 div()
2162 .flex_1()
2163 .relative()
2164 .group("")
2165 .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
2166 .on_drag_move::<ProjectEntryId>(cx.listener(Self::handle_drag_move))
2167 .on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
2168 .map(|div| {
2169 if let Some(item) = self.active_item() {
2170 div.v_flex()
2171 .child(self.toolbar.clone())
2172 .child(item.to_any())
2173 } else {
2174 let placeholder = div.h_flex().size_full().justify_center();
2175 if has_worktrees {
2176 placeholder
2177 } else {
2178 placeholder.child(
2179 Label::new("Open a file or project to get started.")
2180 .color(Color::Muted),
2181 )
2182 }
2183 }
2184 })
2185 .child(
2186 // drag target
2187 div()
2188 .invisible()
2189 .absolute()
2190 .bg(cx.theme().colors().drop_target_background)
2191 .group_drag_over::<DraggedTab>("", |style| style.visible())
2192 .group_drag_over::<ProjectEntryId>("", |style| style.visible())
2193 .group_drag_over::<ExternalPaths>("", |style| style.visible())
2194 .when_some(self.can_drop_predicate.clone(), |this, p| {
2195 this.can_drop(move |a, cx| p(a, cx))
2196 })
2197 .on_drop(cx.listener(move |this, dragged_tab, cx| {
2198 this.handle_tab_drop(dragged_tab, this.active_item_index(), cx)
2199 }))
2200 .on_drop(cx.listener(move |this, entry_id, cx| {
2201 this.handle_project_entry_drop(entry_id, cx)
2202 }))
2203 .on_drop(cx.listener(move |this, paths, cx| {
2204 this.handle_external_paths_drop(paths, cx)
2205 }))
2206 .map(|div| {
2207 let size = DefiniteLength::Fraction(0.5);
2208 match self.drag_split_direction {
2209 None => div.top_0().right_0().bottom_0().left_0(),
2210 Some(SplitDirection::Up) => {
2211 div.top_0().left_0().right_0().h(size)
2212 }
2213 Some(SplitDirection::Down) => {
2214 div.left_0().bottom_0().right_0().h(size)
2215 }
2216 Some(SplitDirection::Left) => {
2217 div.top_0().left_0().bottom_0().w(size)
2218 }
2219 Some(SplitDirection::Right) => {
2220 div.top_0().bottom_0().right_0().w(size)
2221 }
2222 }
2223 }),
2224 )
2225 })
2226 .on_mouse_down(
2227 MouseButton::Navigate(NavigationDirection::Back),
2228 cx.listener(|pane, _, cx| {
2229 if let Some(workspace) = pane.workspace.upgrade() {
2230 let pane = cx.view().downgrade();
2231 cx.window_context().defer(move |cx| {
2232 workspace.update(cx, |workspace, cx| {
2233 workspace.go_back(pane, cx).detach_and_log_err(cx)
2234 })
2235 })
2236 }
2237 }),
2238 )
2239 .on_mouse_down(
2240 MouseButton::Navigate(NavigationDirection::Forward),
2241 cx.listener(|pane, _, cx| {
2242 if let Some(workspace) = pane.workspace.upgrade() {
2243 let pane = cx.view().downgrade();
2244 cx.window_context().defer(move |cx| {
2245 workspace.update(cx, |workspace, cx| {
2246 workspace.go_forward(pane, cx).detach_and_log_err(cx)
2247 })
2248 })
2249 }
2250 }),
2251 )
2252 }
2253}
2254
2255impl ItemNavHistory {
2256 pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut WindowContext) {
2257 self.history
2258 .push(data, self.item.clone(), self.is_preview, cx);
2259 }
2260
2261 pub fn pop_backward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
2262 self.history.pop(NavigationMode::GoingBack, cx)
2263 }
2264
2265 pub fn pop_forward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
2266 self.history.pop(NavigationMode::GoingForward, cx)
2267 }
2268}
2269
2270impl NavHistory {
2271 pub fn for_each_entry(
2272 &self,
2273 cx: &AppContext,
2274 mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
2275 ) {
2276 let borrowed_history = self.0.lock();
2277 borrowed_history
2278 .forward_stack
2279 .iter()
2280 .chain(borrowed_history.backward_stack.iter())
2281 .chain(borrowed_history.closed_stack.iter())
2282 .for_each(|entry| {
2283 if let Some(project_and_abs_path) =
2284 borrowed_history.paths_by_item.get(&entry.item.id())
2285 {
2286 f(entry, project_and_abs_path.clone());
2287 } else if let Some(item) = entry.item.upgrade() {
2288 if let Some(path) = item.project_path(cx) {
2289 f(entry, (path, None));
2290 }
2291 }
2292 })
2293 }
2294
2295 pub fn set_mode(&mut self, mode: NavigationMode) {
2296 self.0.lock().mode = mode;
2297 }
2298
2299 pub fn mode(&self) -> NavigationMode {
2300 self.0.lock().mode
2301 }
2302
2303 pub fn disable(&mut self) {
2304 self.0.lock().mode = NavigationMode::Disabled;
2305 }
2306
2307 pub fn enable(&mut self) {
2308 self.0.lock().mode = NavigationMode::Normal;
2309 }
2310
2311 pub fn pop(&mut self, mode: NavigationMode, cx: &mut WindowContext) -> Option<NavigationEntry> {
2312 let mut state = self.0.lock();
2313 let entry = match mode {
2314 NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
2315 return None
2316 }
2317 NavigationMode::GoingBack => &mut state.backward_stack,
2318 NavigationMode::GoingForward => &mut state.forward_stack,
2319 NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
2320 }
2321 .pop_back();
2322 if entry.is_some() {
2323 state.did_update(cx);
2324 }
2325 entry
2326 }
2327
2328 pub fn push<D: 'static + Send + Any>(
2329 &mut self,
2330 data: Option<D>,
2331 item: Arc<dyn WeakItemHandle>,
2332 is_preview: bool,
2333 cx: &mut WindowContext,
2334 ) {
2335 let state = &mut *self.0.lock();
2336 match state.mode {
2337 NavigationMode::Disabled => {}
2338 NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
2339 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2340 state.backward_stack.pop_front();
2341 }
2342 state.backward_stack.push_back(NavigationEntry {
2343 item,
2344 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2345 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2346 is_preview,
2347 });
2348 state.forward_stack.clear();
2349 }
2350 NavigationMode::GoingBack => {
2351 if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2352 state.forward_stack.pop_front();
2353 }
2354 state.forward_stack.push_back(NavigationEntry {
2355 item,
2356 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2357 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2358 is_preview,
2359 });
2360 }
2361 NavigationMode::GoingForward => {
2362 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2363 state.backward_stack.pop_front();
2364 }
2365 state.backward_stack.push_back(NavigationEntry {
2366 item,
2367 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2368 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2369 is_preview,
2370 });
2371 }
2372 NavigationMode::ClosingItem => {
2373 if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2374 state.closed_stack.pop_front();
2375 }
2376 state.closed_stack.push_back(NavigationEntry {
2377 item,
2378 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2379 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2380 is_preview,
2381 });
2382 }
2383 }
2384 state.did_update(cx);
2385 }
2386
2387 pub fn remove_item(&mut self, item_id: EntityId) {
2388 let mut state = self.0.lock();
2389 state.paths_by_item.remove(&item_id);
2390 state
2391 .backward_stack
2392 .retain(|entry| entry.item.id() != item_id);
2393 state
2394 .forward_stack
2395 .retain(|entry| entry.item.id() != item_id);
2396 state
2397 .closed_stack
2398 .retain(|entry| entry.item.id() != item_id);
2399 }
2400
2401 pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
2402 self.0.lock().paths_by_item.get(&item_id).cloned()
2403 }
2404}
2405
2406impl NavHistoryState {
2407 pub fn did_update(&self, cx: &mut WindowContext) {
2408 if let Some(pane) = self.pane.upgrade() {
2409 cx.defer(move |cx| {
2410 pane.update(cx, |pane, cx| pane.history_updated(cx));
2411 });
2412 }
2413 }
2414}
2415
2416fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
2417 let path = buffer_path
2418 .as_ref()
2419 .and_then(|p| {
2420 p.path
2421 .to_str()
2422 .and_then(|s| if s == "" { None } else { Some(s) })
2423 })
2424 .unwrap_or("This buffer");
2425 let path = truncate_and_remove_front(path, 80);
2426 format!("{path} contains unsaved edits. Do you want to save it?")
2427}
2428
2429pub fn tab_details(items: &Vec<Box<dyn ItemHandle>>, cx: &AppContext) -> Vec<usize> {
2430 let mut tab_details = items.iter().map(|_| 0).collect::<Vec<_>>();
2431 let mut tab_descriptions = HashMap::default();
2432 let mut done = false;
2433 while !done {
2434 done = true;
2435
2436 // Store item indices by their tab description.
2437 for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() {
2438 if let Some(description) = item.tab_description(*detail, cx) {
2439 if *detail == 0
2440 || Some(&description) != item.tab_description(detail - 1, cx).as_ref()
2441 {
2442 tab_descriptions
2443 .entry(description)
2444 .or_insert(Vec::new())
2445 .push(ix);
2446 }
2447 }
2448 }
2449
2450 // If two or more items have the same tab description, increase their level
2451 // of detail and try again.
2452 for (_, item_ixs) in tab_descriptions.drain() {
2453 if item_ixs.len() > 1 {
2454 done = false;
2455 for ix in item_ixs {
2456 tab_details[ix] += 1;
2457 }
2458 }
2459 }
2460 }
2461
2462 tab_details
2463}
2464
2465pub fn render_item_indicator(item: Box<dyn ItemHandle>, cx: &WindowContext) -> Option<Indicator> {
2466 maybe!({
2467 let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
2468 (true, _) => Color::Warning,
2469 (_, true) => Color::Accent,
2470 (false, false) => return None,
2471 };
2472
2473 Some(Indicator::dot().color(indicator_color))
2474 })
2475}
2476
2477#[cfg(test)]
2478mod tests {
2479 use super::*;
2480 use crate::item::test::{TestItem, TestProjectItem};
2481 use gpui::{TestAppContext, VisualTestContext};
2482 use project::FakeFs;
2483 use settings::SettingsStore;
2484 use theme::LoadThemes;
2485
2486 #[gpui::test]
2487 async fn test_remove_active_empty(cx: &mut TestAppContext) {
2488 init_test(cx);
2489 let fs = FakeFs::new(cx.executor());
2490
2491 let project = Project::test(fs, None, cx).await;
2492 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2493 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2494
2495 pane.update(cx, |pane, cx| {
2496 assert!(pane
2497 .close_active_item(&CloseActiveItem { save_intent: None }, cx)
2498 .is_none())
2499 });
2500 }
2501
2502 #[gpui::test]
2503 async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
2504 init_test(cx);
2505 let fs = FakeFs::new(cx.executor());
2506
2507 let project = Project::test(fs, None, cx).await;
2508 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2509 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2510
2511 // 1. Add with a destination index
2512 // a. Add before the active item
2513 set_labeled_items(&pane, ["A", "B*", "C"], cx);
2514 pane.update(cx, |pane, cx| {
2515 pane.add_item(
2516 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2517 false,
2518 false,
2519 Some(0),
2520 cx,
2521 );
2522 });
2523 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
2524
2525 // b. Add after the active item
2526 set_labeled_items(&pane, ["A", "B*", "C"], cx);
2527 pane.update(cx, |pane, cx| {
2528 pane.add_item(
2529 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2530 false,
2531 false,
2532 Some(2),
2533 cx,
2534 );
2535 });
2536 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
2537
2538 // c. Add at the end of the item list (including off the length)
2539 set_labeled_items(&pane, ["A", "B*", "C"], cx);
2540 pane.update(cx, |pane, cx| {
2541 pane.add_item(
2542 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2543 false,
2544 false,
2545 Some(5),
2546 cx,
2547 );
2548 });
2549 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2550
2551 // 2. Add without a destination index
2552 // a. Add with active item at the start of the item list
2553 set_labeled_items(&pane, ["A*", "B", "C"], cx);
2554 pane.update(cx, |pane, cx| {
2555 pane.add_item(
2556 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2557 false,
2558 false,
2559 None,
2560 cx,
2561 );
2562 });
2563 set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
2564
2565 // b. Add with active item at the end of the item list
2566 set_labeled_items(&pane, ["A", "B", "C*"], cx);
2567 pane.update(cx, |pane, cx| {
2568 pane.add_item(
2569 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2570 false,
2571 false,
2572 None,
2573 cx,
2574 );
2575 });
2576 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2577 }
2578
2579 #[gpui::test]
2580 async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
2581 init_test(cx);
2582 let fs = FakeFs::new(cx.executor());
2583
2584 let project = Project::test(fs, None, cx).await;
2585 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2586 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2587
2588 // 1. Add with a destination index
2589 // 1a. Add before the active item
2590 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2591 pane.update(cx, |pane, cx| {
2592 pane.add_item(d, false, false, Some(0), cx);
2593 });
2594 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
2595
2596 // 1b. Add after the active item
2597 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2598 pane.update(cx, |pane, cx| {
2599 pane.add_item(d, false, false, Some(2), cx);
2600 });
2601 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
2602
2603 // 1c. Add at the end of the item list (including off the length)
2604 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2605 pane.update(cx, |pane, cx| {
2606 pane.add_item(a, false, false, Some(5), cx);
2607 });
2608 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
2609
2610 // 1d. Add same item to active index
2611 let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
2612 pane.update(cx, |pane, cx| {
2613 pane.add_item(b, false, false, Some(1), cx);
2614 });
2615 assert_item_labels(&pane, ["A", "B*", "C"], cx);
2616
2617 // 1e. Add item to index after same item in last position
2618 let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
2619 pane.update(cx, |pane, cx| {
2620 pane.add_item(c, false, false, Some(2), cx);
2621 });
2622 assert_item_labels(&pane, ["A", "B", "C*"], cx);
2623
2624 // 2. Add without a destination index
2625 // 2a. Add with active item at the start of the item list
2626 let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
2627 pane.update(cx, |pane, cx| {
2628 pane.add_item(d, false, false, None, cx);
2629 });
2630 assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
2631
2632 // 2b. Add with active item at the end of the item list
2633 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
2634 pane.update(cx, |pane, cx| {
2635 pane.add_item(a, false, false, None, cx);
2636 });
2637 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
2638
2639 // 2c. Add active item to active item at end of list
2640 let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
2641 pane.update(cx, |pane, cx| {
2642 pane.add_item(c, false, false, None, cx);
2643 });
2644 assert_item_labels(&pane, ["A", "B", "C*"], cx);
2645
2646 // 2d. Add active item to active item at start of list
2647 let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
2648 pane.update(cx, |pane, cx| {
2649 pane.add_item(a, false, false, None, cx);
2650 });
2651 assert_item_labels(&pane, ["A*", "B", "C"], cx);
2652 }
2653
2654 #[gpui::test]
2655 async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
2656 init_test(cx);
2657 let fs = FakeFs::new(cx.executor());
2658
2659 let project = Project::test(fs, None, cx).await;
2660 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2661 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2662
2663 // singleton view
2664 pane.update(cx, |pane, cx| {
2665 pane.add_item(
2666 Box::new(cx.new_view(|cx| {
2667 TestItem::new(cx)
2668 .with_singleton(true)
2669 .with_label("buffer 1")
2670 .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
2671 })),
2672 false,
2673 false,
2674 None,
2675 cx,
2676 );
2677 });
2678 assert_item_labels(&pane, ["buffer 1*"], cx);
2679
2680 // new singleton view with the same project entry
2681 pane.update(cx, |pane, cx| {
2682 pane.add_item(
2683 Box::new(cx.new_view(|cx| {
2684 TestItem::new(cx)
2685 .with_singleton(true)
2686 .with_label("buffer 1")
2687 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
2688 })),
2689 false,
2690 false,
2691 None,
2692 cx,
2693 );
2694 });
2695 assert_item_labels(&pane, ["buffer 1*"], cx);
2696
2697 // new singleton view with different project entry
2698 pane.update(cx, |pane, cx| {
2699 pane.add_item(
2700 Box::new(cx.new_view(|cx| {
2701 TestItem::new(cx)
2702 .with_singleton(true)
2703 .with_label("buffer 2")
2704 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
2705 })),
2706 false,
2707 false,
2708 None,
2709 cx,
2710 );
2711 });
2712 assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
2713
2714 // new multibuffer view with the same project entry
2715 pane.update(cx, |pane, cx| {
2716 pane.add_item(
2717 Box::new(cx.new_view(|cx| {
2718 TestItem::new(cx)
2719 .with_singleton(false)
2720 .with_label("multibuffer 1")
2721 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
2722 })),
2723 false,
2724 false,
2725 None,
2726 cx,
2727 );
2728 });
2729 assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
2730
2731 // another multibuffer view with the same project entry
2732 pane.update(cx, |pane, cx| {
2733 pane.add_item(
2734 Box::new(cx.new_view(|cx| {
2735 TestItem::new(cx)
2736 .with_singleton(false)
2737 .with_label("multibuffer 1b")
2738 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
2739 })),
2740 false,
2741 false,
2742 None,
2743 cx,
2744 );
2745 });
2746 assert_item_labels(
2747 &pane,
2748 ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
2749 cx,
2750 );
2751 }
2752
2753 #[gpui::test]
2754 async fn test_remove_item_ordering(cx: &mut TestAppContext) {
2755 init_test(cx);
2756 let fs = FakeFs::new(cx.executor());
2757
2758 let project = Project::test(fs, None, cx).await;
2759 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2760 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2761
2762 add_labeled_item(&pane, "A", false, cx);
2763 add_labeled_item(&pane, "B", false, cx);
2764 add_labeled_item(&pane, "C", false, cx);
2765 add_labeled_item(&pane, "D", false, cx);
2766 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2767
2768 pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
2769 add_labeled_item(&pane, "1", false, cx);
2770 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
2771
2772 pane.update(cx, |pane, cx| {
2773 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
2774 })
2775 .unwrap()
2776 .await
2777 .unwrap();
2778 assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
2779
2780 pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
2781 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2782
2783 pane.update(cx, |pane, cx| {
2784 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
2785 })
2786 .unwrap()
2787 .await
2788 .unwrap();
2789 assert_item_labels(&pane, ["A", "B*", "C"], 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", "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*"], cx);
2806 }
2807
2808 #[gpui::test]
2809 async fn test_close_inactive_items(cx: &mut TestAppContext) {
2810 init_test(cx);
2811 let fs = FakeFs::new(cx.executor());
2812
2813 let project = Project::test(fs, None, cx).await;
2814 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2815 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2816
2817 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
2818
2819 pane.update(cx, |pane, cx| {
2820 pane.close_inactive_items(&CloseInactiveItems { save_intent: None }, cx)
2821 })
2822 .unwrap()
2823 .await
2824 .unwrap();
2825 assert_item_labels(&pane, ["C*"], cx);
2826 }
2827
2828 #[gpui::test]
2829 async fn test_close_clean_items(cx: &mut TestAppContext) {
2830 init_test(cx);
2831 let fs = FakeFs::new(cx.executor());
2832
2833 let project = Project::test(fs, None, cx).await;
2834 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2835 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2836
2837 add_labeled_item(&pane, "A", true, cx);
2838 add_labeled_item(&pane, "B", false, cx);
2839 add_labeled_item(&pane, "C", true, cx);
2840 add_labeled_item(&pane, "D", false, cx);
2841 add_labeled_item(&pane, "E", false, cx);
2842 assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
2843
2844 pane.update(cx, |pane, cx| pane.close_clean_items(&CloseCleanItems, cx))
2845 .unwrap()
2846 .await
2847 .unwrap();
2848 assert_item_labels(&pane, ["A^", "C*^"], cx);
2849 }
2850
2851 #[gpui::test]
2852 async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
2853 init_test(cx);
2854 let fs = FakeFs::new(cx.executor());
2855
2856 let project = Project::test(fs, None, cx).await;
2857 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2858 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2859
2860 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
2861
2862 pane.update(cx, |pane, cx| {
2863 pane.close_items_to_the_left(&CloseItemsToTheLeft, cx)
2864 })
2865 .unwrap()
2866 .await
2867 .unwrap();
2868 assert_item_labels(&pane, ["C*", "D", "E"], cx);
2869 }
2870
2871 #[gpui::test]
2872 async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
2873 init_test(cx);
2874 let fs = FakeFs::new(cx.executor());
2875
2876 let project = Project::test(fs, None, cx).await;
2877 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2878 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2879
2880 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
2881
2882 pane.update(cx, |pane, cx| {
2883 pane.close_items_to_the_right(&CloseItemsToTheRight, cx)
2884 })
2885 .unwrap()
2886 .await
2887 .unwrap();
2888 assert_item_labels(&pane, ["A", "B", "C*"], cx);
2889 }
2890
2891 #[gpui::test]
2892 async fn test_close_all_items(cx: &mut TestAppContext) {
2893 init_test(cx);
2894 let fs = FakeFs::new(cx.executor());
2895
2896 let project = Project::test(fs, None, cx).await;
2897 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2898 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2899
2900 add_labeled_item(&pane, "A", false, cx);
2901 add_labeled_item(&pane, "B", false, cx);
2902 add_labeled_item(&pane, "C", false, cx);
2903 assert_item_labels(&pane, ["A", "B", "C*"], cx);
2904
2905 pane.update(cx, |pane, cx| {
2906 pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
2907 })
2908 .unwrap()
2909 .await
2910 .unwrap();
2911 assert_item_labels(&pane, [], cx);
2912
2913 add_labeled_item(&pane, "A", true, cx);
2914 add_labeled_item(&pane, "B", true, cx);
2915 add_labeled_item(&pane, "C", true, cx);
2916 assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
2917
2918 let save = pane
2919 .update(cx, |pane, cx| {
2920 pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
2921 })
2922 .unwrap();
2923
2924 cx.executor().run_until_parked();
2925 cx.simulate_prompt_answer(2);
2926 save.await.unwrap();
2927 assert_item_labels(&pane, [], cx);
2928 }
2929
2930 fn init_test(cx: &mut TestAppContext) {
2931 cx.update(|cx| {
2932 let settings_store = SettingsStore::test(cx);
2933 cx.set_global(settings_store);
2934 theme::init(LoadThemes::JustBase, cx);
2935 crate::init_settings(cx);
2936 Project::init_settings(cx);
2937 });
2938 }
2939
2940 fn add_labeled_item(
2941 pane: &View<Pane>,
2942 label: &str,
2943 is_dirty: bool,
2944 cx: &mut VisualTestContext,
2945 ) -> Box<View<TestItem>> {
2946 pane.update(cx, |pane, cx| {
2947 let labeled_item = Box::new(
2948 cx.new_view(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)),
2949 );
2950 pane.add_item(labeled_item.clone(), false, false, None, cx);
2951 labeled_item
2952 })
2953 }
2954
2955 fn set_labeled_items<const COUNT: usize>(
2956 pane: &View<Pane>,
2957 labels: [&str; COUNT],
2958 cx: &mut VisualTestContext,
2959 ) -> [Box<View<TestItem>>; COUNT] {
2960 pane.update(cx, |pane, cx| {
2961 pane.items.clear();
2962 let mut active_item_index = 0;
2963
2964 let mut index = 0;
2965 let items = labels.map(|mut label| {
2966 if label.ends_with('*') {
2967 label = label.trim_end_matches('*');
2968 active_item_index = index;
2969 }
2970
2971 let labeled_item = Box::new(cx.new_view(|cx| TestItem::new(cx).with_label(label)));
2972 pane.add_item(labeled_item.clone(), false, false, None, cx);
2973 index += 1;
2974 labeled_item
2975 });
2976
2977 pane.activate_item(active_item_index, false, false, cx);
2978
2979 items
2980 })
2981 }
2982
2983 // Assert the item label, with the active item label suffixed with a '*'
2984 fn assert_item_labels<const COUNT: usize>(
2985 pane: &View<Pane>,
2986 expected_states: [&str; COUNT],
2987 cx: &mut VisualTestContext,
2988 ) {
2989 pane.update(cx, |pane, cx| {
2990 let actual_states = pane
2991 .items
2992 .iter()
2993 .enumerate()
2994 .map(|(ix, item)| {
2995 let mut state = item
2996 .to_any()
2997 .downcast::<TestItem>()
2998 .unwrap()
2999 .read(cx)
3000 .label
3001 .clone();
3002 if ix == pane.active_item_index {
3003 state.push('*');
3004 }
3005 if item.is_dirty(cx) {
3006 state.push('^');
3007 }
3008 state
3009 })
3010 .collect::<Vec<_>>();
3011
3012 assert_eq!(
3013 actual_states, expected_states,
3014 "pane items do not match expectation"
3015 );
3016 })
3017 }
3018}
3019
3020impl Render for DraggedTab {
3021 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3022 let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
3023 let label = self.item.tab_content(
3024 TabContentParams {
3025 detail: Some(self.detail),
3026 selected: false,
3027 preview: false,
3028 },
3029 cx,
3030 );
3031 Tab::new("")
3032 .selected(self.is_active)
3033 .child(label)
3034 .render(cx)
3035 .font_family(ui_font)
3036 }
3037}