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