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