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 dirty_items = Vec::new();
1299 for item in &self.items {
1300 if should_close(item.item_id()) {
1301 items_to_close.push(item.boxed_clone());
1302 if item.is_dirty(cx) {
1303 dirty_items.push(item.boxed_clone());
1304 }
1305 }
1306 }
1307
1308 let active_item_id = self.active_item().map(|item| item.item_id());
1309
1310 items_to_close.sort_by_key(|item| {
1311 // Put the currently active item at the end, because if the currently active item is not closed last
1312 // closing the currently active item will cause the focus to switch to another item
1313 // This will cause Zed to expand the content of the currently active item
1314 active_item_id.filter(|&id| id == item.item_id()).is_some()
1315 // If a buffer is open both in a singleton editor and in a multibuffer, make sure
1316 // to focus the singleton buffer when prompting to save that buffer, as opposed
1317 // to focusing the multibuffer, because this gives the user a more clear idea
1318 // of what content they would be saving.
1319 || !item.is_singleton(cx)
1320 });
1321
1322 let workspace = self.workspace.clone();
1323 cx.spawn(|pane, mut cx| async move {
1324 if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
1325 let answer = pane.update(&mut cx, |_, cx| {
1326 let (prompt, detail) =
1327 Self::file_names_for_prompt(&mut dirty_items.iter(), dirty_items.len(), cx);
1328 cx.prompt(
1329 PromptLevel::Warning,
1330 &prompt,
1331 Some(&detail),
1332 &["Save all", "Discard all", "Cancel"],
1333 )
1334 })?;
1335 match answer.await {
1336 Ok(0) => save_intent = SaveIntent::SaveAll,
1337 Ok(1) => save_intent = SaveIntent::Skip,
1338 _ => {}
1339 }
1340 }
1341 let mut saved_project_items_ids = HashSet::default();
1342 for item in items_to_close.clone() {
1343 // Find the item's current index and its set of project item models. Avoid
1344 // storing these in advance, in case they have changed since this task
1345 // was started.
1346 let (item_ix, mut project_item_ids) = pane.update(&mut cx, |pane, cx| {
1347 (pane.index_for_item(&*item), item.project_item_model_ids(cx))
1348 })?;
1349 let item_ix = if let Some(ix) = item_ix {
1350 ix
1351 } else {
1352 continue;
1353 };
1354
1355 // Check if this view has any project items that are not open anywhere else
1356 // in the workspace, AND that the user has not already been prompted to save.
1357 // If there are any such project entries, prompt the user to save this item.
1358 let project = workspace.update(&mut cx, |workspace, cx| {
1359 for item in workspace.items(cx) {
1360 if !items_to_close
1361 .iter()
1362 .any(|item_to_close| item_to_close.item_id() == item.item_id())
1363 {
1364 let other_project_item_ids = item.project_item_model_ids(cx);
1365 project_item_ids.retain(|id| !other_project_item_ids.contains(id));
1366 }
1367 }
1368 workspace.project().clone()
1369 })?;
1370 let should_save = project_item_ids
1371 .iter()
1372 .any(|id| saved_project_items_ids.insert(*id));
1373
1374 if should_save
1375 && !Self::save_item(
1376 project.clone(),
1377 &pane,
1378 item_ix,
1379 &*item,
1380 save_intent,
1381 &mut cx,
1382 )
1383 .await?
1384 {
1385 break;
1386 }
1387
1388 // Remove the item from the pane.
1389 pane.update(&mut cx, |pane, cx| {
1390 if let Some(item_ix) = pane
1391 .items
1392 .iter()
1393 .position(|i| i.item_id() == item.item_id())
1394 {
1395 pane.remove_item(item_ix, false, true, cx);
1396 }
1397 })
1398 .ok();
1399 }
1400
1401 pane.update(&mut cx, |_, cx| cx.notify()).ok();
1402 Ok(())
1403 })
1404 }
1405
1406 pub fn remove_item(
1407 &mut self,
1408 item_index: usize,
1409 activate_pane: bool,
1410 close_pane_if_empty: bool,
1411 cx: &mut ViewContext<Self>,
1412 ) {
1413 self._remove_item(item_index, activate_pane, close_pane_if_empty, None, cx)
1414 }
1415
1416 pub fn remove_item_and_focus_on_pane(
1417 &mut self,
1418 item_index: usize,
1419 activate_pane: bool,
1420 focus_on_pane_if_closed: View<Pane>,
1421 cx: &mut ViewContext<Self>,
1422 ) {
1423 self._remove_item(
1424 item_index,
1425 activate_pane,
1426 true,
1427 Some(focus_on_pane_if_closed),
1428 cx,
1429 )
1430 }
1431
1432 fn _remove_item(
1433 &mut self,
1434 item_index: usize,
1435 activate_pane: bool,
1436 close_pane_if_empty: bool,
1437 focus_on_pane_if_closed: Option<View<Pane>>,
1438 cx: &mut ViewContext<Self>,
1439 ) {
1440 let activate_on_close = &ItemSettings::get_global(cx).activate_on_close;
1441 self.activation_history
1442 .retain(|entry| entry.entity_id != self.items[item_index].item_id());
1443
1444 if self.is_tab_pinned(item_index) {
1445 self.pinned_tab_count -= 1;
1446 }
1447 if item_index == self.active_item_index {
1448 let index_to_activate = match activate_on_close {
1449 ActivateOnClose::History => self
1450 .activation_history
1451 .pop()
1452 .and_then(|last_activated_item| {
1453 self.items.iter().enumerate().find_map(|(index, item)| {
1454 (item.item_id() == last_activated_item.entity_id).then_some(index)
1455 })
1456 })
1457 // We didn't have a valid activation history entry, so fallback
1458 // to activating the item to the left
1459 .unwrap_or_else(|| item_index.min(self.items.len()).saturating_sub(1)),
1460 ActivateOnClose::Neighbour => {
1461 self.activation_history.pop();
1462 if item_index + 1 < self.items.len() {
1463 item_index + 1
1464 } else {
1465 item_index.saturating_sub(1)
1466 }
1467 }
1468 };
1469
1470 let should_activate = activate_pane || self.has_focus(cx);
1471 if self.items.len() == 1 && should_activate {
1472 self.focus_handle.focus(cx);
1473 } else {
1474 self.activate_item(index_to_activate, should_activate, should_activate, cx);
1475 }
1476 }
1477
1478 cx.emit(Event::RemoveItem { idx: item_index });
1479
1480 let item = self.items.remove(item_index);
1481
1482 cx.emit(Event::RemovedItem {
1483 item_id: item.item_id(),
1484 });
1485 if self.items.is_empty() {
1486 item.deactivated(cx);
1487 if close_pane_if_empty {
1488 self.update_toolbar(cx);
1489 cx.emit(Event::Remove {
1490 focus_on_pane: focus_on_pane_if_closed,
1491 });
1492 }
1493 }
1494
1495 if item_index < self.active_item_index {
1496 self.active_item_index -= 1;
1497 }
1498
1499 let mode = self.nav_history.mode();
1500 self.nav_history.set_mode(NavigationMode::ClosingItem);
1501 item.deactivated(cx);
1502 self.nav_history.set_mode(mode);
1503
1504 if self.is_active_preview_item(item.item_id()) {
1505 self.set_preview_item_id(None, cx);
1506 }
1507
1508 if let Some(path) = item.project_path(cx) {
1509 let abs_path = self
1510 .nav_history
1511 .0
1512 .lock()
1513 .paths_by_item
1514 .get(&item.item_id())
1515 .and_then(|(_, abs_path)| abs_path.clone());
1516
1517 self.nav_history
1518 .0
1519 .lock()
1520 .paths_by_item
1521 .insert(item.item_id(), (path, abs_path));
1522 } else {
1523 self.nav_history
1524 .0
1525 .lock()
1526 .paths_by_item
1527 .remove(&item.item_id());
1528 }
1529
1530 if self.items.is_empty() && close_pane_if_empty && self.zoomed {
1531 cx.emit(Event::ZoomOut);
1532 }
1533
1534 cx.notify();
1535 }
1536
1537 pub async fn save_item(
1538 project: Model<Project>,
1539 pane: &WeakView<Pane>,
1540 item_ix: usize,
1541 item: &dyn ItemHandle,
1542 save_intent: SaveIntent,
1543 cx: &mut AsyncWindowContext,
1544 ) -> Result<bool> {
1545 const CONFLICT_MESSAGE: &str =
1546 "This file has changed on disk since you started editing it. Do you want to overwrite it?";
1547
1548 const DELETED_MESSAGE: &str =
1549 "This file has been deleted on disk since you started editing it. Do you want to recreate it?";
1550
1551 if save_intent == SaveIntent::Skip {
1552 return Ok(true);
1553 }
1554
1555 let (mut has_conflict, mut is_dirty, mut can_save, is_singleton, has_deleted_file) = cx
1556 .update(|cx| {
1557 (
1558 item.has_conflict(cx),
1559 item.is_dirty(cx),
1560 item.can_save(cx),
1561 item.is_singleton(cx),
1562 item.has_deleted_file(cx),
1563 )
1564 })?;
1565
1566 let can_save_as = is_singleton;
1567
1568 // when saving a single buffer, we ignore whether or not it's dirty.
1569 if save_intent == SaveIntent::Save || save_intent == SaveIntent::SaveWithoutFormat {
1570 is_dirty = true;
1571 }
1572
1573 if save_intent == SaveIntent::SaveAs {
1574 is_dirty = true;
1575 has_conflict = false;
1576 can_save = false;
1577 }
1578
1579 if save_intent == SaveIntent::Overwrite {
1580 has_conflict = false;
1581 }
1582
1583 let should_format = save_intent != SaveIntent::SaveWithoutFormat;
1584
1585 if has_conflict && can_save {
1586 if has_deleted_file && is_singleton {
1587 let answer = pane.update(cx, |pane, cx| {
1588 pane.activate_item(item_ix, true, true, cx);
1589 cx.prompt(
1590 PromptLevel::Warning,
1591 DELETED_MESSAGE,
1592 None,
1593 &["Save", "Close", "Cancel"],
1594 )
1595 })?;
1596 match answer.await {
1597 Ok(0) => {
1598 pane.update(cx, |_, cx| item.save(should_format, project, cx))?
1599 .await?
1600 }
1601 Ok(1) => {
1602 pane.update(cx, |pane, cx| pane.remove_item(item_ix, false, false, cx))?;
1603 }
1604 _ => return Ok(false),
1605 }
1606 return Ok(true);
1607 } else {
1608 let answer = pane.update(cx, |pane, cx| {
1609 pane.activate_item(item_ix, true, true, cx);
1610 cx.prompt(
1611 PromptLevel::Warning,
1612 CONFLICT_MESSAGE,
1613 None,
1614 &["Overwrite", "Discard", "Cancel"],
1615 )
1616 })?;
1617 match answer.await {
1618 Ok(0) => {
1619 pane.update(cx, |_, cx| item.save(should_format, project, cx))?
1620 .await?
1621 }
1622 Ok(1) => pane.update(cx, |_, cx| item.reload(project, cx))?.await?,
1623 _ => return Ok(false),
1624 }
1625 }
1626 } else if is_dirty && (can_save || can_save_as) {
1627 if save_intent == SaveIntent::Close {
1628 let will_autosave = cx.update(|cx| {
1629 matches!(
1630 item.workspace_settings(cx).autosave,
1631 AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange
1632 ) && Self::can_autosave_item(item, cx)
1633 })?;
1634 if !will_autosave {
1635 let item_id = item.item_id();
1636 let answer_task = pane.update(cx, |pane, cx| {
1637 if pane.save_modals_spawned.insert(item_id) {
1638 pane.activate_item(item_ix, true, true, cx);
1639 let prompt = dirty_message_for(item.project_path(cx));
1640 Some(cx.prompt(
1641 PromptLevel::Warning,
1642 &prompt,
1643 None,
1644 &["Save", "Don't Save", "Cancel"],
1645 ))
1646 } else {
1647 None
1648 }
1649 })?;
1650 if let Some(answer_task) = answer_task {
1651 let answer = answer_task.await;
1652 pane.update(cx, |pane, _| {
1653 if !pane.save_modals_spawned.remove(&item_id) {
1654 debug_panic!(
1655 "save modal was not present in spawned modals after awaiting for its answer"
1656 )
1657 }
1658 })?;
1659 match answer {
1660 Ok(0) => {}
1661 Ok(1) => {
1662 // Don't save this file
1663 pane.update(cx, |pane, cx| {
1664 if pane.is_tab_pinned(item_ix) && !item.can_save(cx) {
1665 pane.pinned_tab_count -= 1;
1666 }
1667 item.discarded(project, cx)
1668 })
1669 .log_err();
1670 return Ok(true);
1671 }
1672 _ => return Ok(false), // Cancel
1673 }
1674 } else {
1675 return Ok(false);
1676 }
1677 }
1678 }
1679
1680 if can_save {
1681 pane.update(cx, |pane, cx| {
1682 if pane.is_active_preview_item(item.item_id()) {
1683 pane.set_preview_item_id(None, cx);
1684 }
1685 item.save(should_format, project, cx)
1686 })?
1687 .await?;
1688 } else if can_save_as {
1689 let abs_path = pane.update(cx, |pane, cx| {
1690 pane.workspace
1691 .update(cx, |workspace, cx| workspace.prompt_for_new_path(cx))
1692 })??;
1693 if let Some(abs_path) = abs_path.await.ok().flatten() {
1694 pane.update(cx, |pane, cx| {
1695 if let Some(item) = pane.item_for_path(abs_path.clone(), cx) {
1696 if let Some(idx) = pane.index_for_item(&*item) {
1697 pane.remove_item(idx, false, false, cx);
1698 }
1699 }
1700
1701 item.save_as(project, abs_path, cx)
1702 })?
1703 .await?;
1704 } else {
1705 return Ok(false);
1706 }
1707 }
1708 }
1709
1710 pane.update(cx, |_, cx| {
1711 cx.emit(Event::UserSavedItem {
1712 item: item.downgrade_item(),
1713 save_intent,
1714 });
1715 true
1716 })
1717 }
1718
1719 fn can_autosave_item(item: &dyn ItemHandle, cx: &AppContext) -> bool {
1720 let is_deleted = item.project_entry_ids(cx).is_empty();
1721 item.is_dirty(cx) && !item.has_conflict(cx) && item.can_save(cx) && !is_deleted
1722 }
1723
1724 pub fn autosave_item(
1725 item: &dyn ItemHandle,
1726 project: Model<Project>,
1727 cx: &mut WindowContext,
1728 ) -> Task<Result<()>> {
1729 let format = !matches!(
1730 item.workspace_settings(cx).autosave,
1731 AutosaveSetting::AfterDelay { .. }
1732 );
1733 if Self::can_autosave_item(item, cx) {
1734 item.save(format, project, cx)
1735 } else {
1736 Task::ready(Ok(()))
1737 }
1738 }
1739
1740 pub fn focus(&mut self, cx: &mut ViewContext<Pane>) {
1741 cx.focus(&self.focus_handle);
1742 }
1743
1744 pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
1745 if let Some(active_item) = self.active_item() {
1746 let focus_handle = active_item.focus_handle(cx);
1747 cx.focus(&focus_handle);
1748 }
1749 }
1750
1751 pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
1752 cx.emit(Event::Split(direction));
1753 }
1754
1755 pub fn toolbar(&self) -> &View<Toolbar> {
1756 &self.toolbar
1757 }
1758
1759 pub fn handle_deleted_project_item(
1760 &mut self,
1761 entry_id: ProjectEntryId,
1762 cx: &mut ViewContext<Pane>,
1763 ) -> Option<()> {
1764 let (item_index_to_delete, item_id) = self.items().enumerate().find_map(|(i, item)| {
1765 if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
1766 Some((i, item.item_id()))
1767 } else {
1768 None
1769 }
1770 })?;
1771
1772 self.remove_item(item_index_to_delete, false, true, cx);
1773 self.nav_history.remove_item(item_id);
1774
1775 Some(())
1776 }
1777
1778 fn update_toolbar(&mut self, cx: &mut ViewContext<Self>) {
1779 let active_item = self
1780 .items
1781 .get(self.active_item_index)
1782 .map(|item| item.as_ref());
1783 self.toolbar.update(cx, |toolbar, cx| {
1784 toolbar.set_active_item(active_item, cx);
1785 });
1786 }
1787
1788 fn update_status_bar(&mut self, cx: &mut ViewContext<Self>) {
1789 let workspace = self.workspace.clone();
1790 let pane = cx.view().clone();
1791
1792 cx.window_context().defer(move |cx| {
1793 let Ok(status_bar) = workspace.update(cx, |workspace, _| workspace.status_bar.clone())
1794 else {
1795 return;
1796 };
1797
1798 status_bar.update(cx, move |status_bar, cx| {
1799 status_bar.set_active_pane(&pane, cx);
1800 });
1801 });
1802 }
1803
1804 fn entry_abs_path(&self, entry: ProjectEntryId, cx: &WindowContext) -> Option<PathBuf> {
1805 let worktree = self
1806 .workspace
1807 .upgrade()?
1808 .read(cx)
1809 .project()
1810 .read(cx)
1811 .worktree_for_entry(entry, cx)?
1812 .read(cx);
1813 let entry = worktree.entry_for_id(entry)?;
1814 match &entry.canonical_path {
1815 Some(canonical_path) => Some(canonical_path.to_path_buf()),
1816 None => worktree.absolutize(&entry.path).ok(),
1817 }
1818 }
1819
1820 pub fn icon_color(selected: bool) -> Color {
1821 if selected {
1822 Color::Default
1823 } else {
1824 Color::Muted
1825 }
1826 }
1827
1828 pub fn git_aware_icon_color(
1829 git_status: Option<GitFileStatus>,
1830 ignored: bool,
1831 selected: bool,
1832 ) -> Color {
1833 if ignored {
1834 Color::Ignored
1835 } else {
1836 match git_status {
1837 Some(GitFileStatus::Added) => Color::Created,
1838 Some(GitFileStatus::Modified) => Color::Modified,
1839 Some(GitFileStatus::Conflict) => Color::Conflict,
1840 None => Self::icon_color(selected),
1841 }
1842 }
1843 }
1844
1845 fn toggle_pin_tab(&mut self, _: &TogglePinTab, cx: &mut ViewContext<'_, Self>) {
1846 if self.items.is_empty() {
1847 return;
1848 }
1849 let active_tab_ix = self.active_item_index();
1850 if self.is_tab_pinned(active_tab_ix) {
1851 self.unpin_tab_at(active_tab_ix, cx);
1852 } else {
1853 self.pin_tab_at(active_tab_ix, cx);
1854 }
1855 }
1856
1857 fn pin_tab_at(&mut self, ix: usize, cx: &mut ViewContext<'_, Self>) {
1858 maybe!({
1859 let pane = cx.view().clone();
1860 let destination_index = self.pinned_tab_count.min(ix);
1861 self.pinned_tab_count += 1;
1862 let id = self.item_for_index(ix)?.item_id();
1863
1864 self.workspace
1865 .update(cx, |_, cx| {
1866 cx.defer(move |_, cx| move_item(&pane, &pane, id, destination_index, cx));
1867 })
1868 .ok()?;
1869
1870 Some(())
1871 });
1872 }
1873
1874 fn unpin_tab_at(&mut self, ix: usize, cx: &mut ViewContext<'_, Self>) {
1875 maybe!({
1876 let pane = cx.view().clone();
1877 self.pinned_tab_count = self.pinned_tab_count.checked_sub(1).unwrap();
1878 let destination_index = self.pinned_tab_count;
1879
1880 let id = self.item_for_index(ix)?.item_id();
1881
1882 self.workspace
1883 .update(cx, |_, cx| {
1884 cx.defer(move |_, cx| move_item(&pane, &pane, id, destination_index, cx));
1885 })
1886 .ok()?;
1887
1888 Some(())
1889 });
1890 }
1891
1892 fn is_tab_pinned(&self, ix: usize) -> bool {
1893 self.pinned_tab_count > ix
1894 }
1895
1896 fn has_pinned_tabs(&self) -> bool {
1897 self.pinned_tab_count != 0
1898 }
1899
1900 fn render_tab(
1901 &self,
1902 ix: usize,
1903 item: &dyn ItemHandle,
1904 detail: usize,
1905 focus_handle: &FocusHandle,
1906 cx: &mut ViewContext<'_, Pane>,
1907 ) -> impl IntoElement {
1908 let project_path = item.project_path(cx);
1909
1910 let is_active = ix == self.active_item_index;
1911 let is_preview = self
1912 .preview_item_id
1913 .map(|id| id == item.item_id())
1914 .unwrap_or(false);
1915
1916 let label = item.tab_content(
1917 TabContentParams {
1918 detail: Some(detail),
1919 selected: is_active,
1920 preview: is_preview,
1921 },
1922 cx,
1923 );
1924
1925 let icon_color = if ItemSettings::get_global(cx).git_status {
1926 project_path
1927 .as_ref()
1928 .and_then(|path| self.project.read(cx).entry_for_path(path, cx))
1929 .map(|entry| {
1930 Self::git_aware_icon_color(entry.git_status, entry.is_ignored, is_active)
1931 })
1932 .unwrap_or_else(|| Self::icon_color(is_active))
1933 } else {
1934 Self::icon_color(is_active)
1935 };
1936
1937 let icon = item.tab_icon(cx);
1938 let close_side = &ItemSettings::get_global(cx).close_position;
1939 let indicator = render_item_indicator(item.boxed_clone(), cx);
1940 let item_id = item.item_id();
1941 let is_first_item = ix == 0;
1942 let is_last_item = ix == self.items.len() - 1;
1943 let is_pinned = self.is_tab_pinned(ix);
1944 let position_relative_to_active_item = ix.cmp(&self.active_item_index);
1945
1946 let tab = Tab::new(ix)
1947 .position(if is_first_item {
1948 TabPosition::First
1949 } else if is_last_item {
1950 TabPosition::Last
1951 } else {
1952 TabPosition::Middle(position_relative_to_active_item)
1953 })
1954 .close_side(match close_side {
1955 ClosePosition::Left => ui::TabCloseSide::Start,
1956 ClosePosition::Right => ui::TabCloseSide::End,
1957 })
1958 .selected(is_active)
1959 .on_click(
1960 cx.listener(move |pane: &mut Self, _, cx| pane.activate_item(ix, true, true, cx)),
1961 )
1962 // TODO: This should be a click listener with the middle mouse button instead of a mouse down listener.
1963 .on_mouse_down(
1964 MouseButton::Middle,
1965 cx.listener(move |pane, _event, cx| {
1966 pane.close_item_by_id(item_id, SaveIntent::Close, cx)
1967 .detach_and_log_err(cx);
1968 }),
1969 )
1970 .on_mouse_down(
1971 MouseButton::Left,
1972 cx.listener(move |pane, event: &MouseDownEvent, cx| {
1973 if let Some(id) = pane.preview_item_id {
1974 if id == item_id && event.click_count > 1 {
1975 pane.set_preview_item_id(None, cx);
1976 }
1977 }
1978 }),
1979 )
1980 .on_drag(
1981 DraggedTab {
1982 item: item.boxed_clone(),
1983 pane: cx.view().clone(),
1984 detail,
1985 is_active,
1986 ix,
1987 },
1988 |tab, _, cx| cx.new_view(|_| tab.clone()),
1989 )
1990 .drag_over::<DraggedTab>(|tab, _, cx| {
1991 tab.bg(cx.theme().colors().drop_target_background)
1992 })
1993 .drag_over::<DraggedSelection>(|tab, _, cx| {
1994 tab.bg(cx.theme().colors().drop_target_background)
1995 })
1996 .when_some(self.can_drop_predicate.clone(), |this, p| {
1997 this.can_drop(move |a, cx| p(a, cx))
1998 })
1999 .on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {
2000 this.drag_split_direction = None;
2001 this.handle_tab_drop(dragged_tab, ix, cx)
2002 }))
2003 .on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
2004 this.drag_split_direction = None;
2005 this.handle_dragged_selection_drop(selection, Some(ix), cx)
2006 }))
2007 .on_drop(cx.listener(move |this, paths, cx| {
2008 this.drag_split_direction = None;
2009 this.handle_external_paths_drop(paths, cx)
2010 }))
2011 .when_some(item.tab_tooltip_text(cx), |tab, text| {
2012 tab.tooltip(move |cx| Tooltip::text(text.clone(), cx))
2013 })
2014 .start_slot::<Indicator>(indicator)
2015 .map(|this| {
2016 let end_slot_action: &'static dyn Action;
2017 let end_slot_tooltip_text: &'static str;
2018 let end_slot = if is_pinned {
2019 end_slot_action = &TogglePinTab;
2020 end_slot_tooltip_text = "Unpin Tab";
2021 IconButton::new("unpin tab", IconName::Pin)
2022 .shape(IconButtonShape::Square)
2023 .icon_color(Color::Muted)
2024 .size(ButtonSize::None)
2025 .icon_size(IconSize::XSmall)
2026 .on_click(cx.listener(move |pane, _, cx| {
2027 pane.unpin_tab_at(ix, cx);
2028 }))
2029 } else {
2030 end_slot_action = &CloseActiveItem { save_intent: None };
2031 end_slot_tooltip_text = "Close Tab";
2032 IconButton::new("close tab", IconName::Close)
2033 .visible_on_hover("")
2034 .shape(IconButtonShape::Square)
2035 .icon_color(Color::Muted)
2036 .size(ButtonSize::None)
2037 .icon_size(IconSize::XSmall)
2038 .on_click(cx.listener(move |pane, _, cx| {
2039 pane.close_item_by_id(item_id, SaveIntent::Close, cx)
2040 .detach_and_log_err(cx);
2041 }))
2042 }
2043 .map(|this| {
2044 if is_active {
2045 let focus_handle = focus_handle.clone();
2046 this.tooltip(move |cx| {
2047 Tooltip::for_action_in(
2048 end_slot_tooltip_text,
2049 end_slot_action,
2050 &focus_handle,
2051 cx,
2052 )
2053 })
2054 } else {
2055 this.tooltip(move |cx| Tooltip::text(end_slot_tooltip_text, cx))
2056 }
2057 });
2058 this.end_slot(end_slot)
2059 })
2060 .child(
2061 h_flex()
2062 .gap_1()
2063 .children(icon.map(|icon| icon.size(IconSize::Small).color(icon_color)))
2064 .child(label),
2065 );
2066
2067 let single_entry_to_resolve = {
2068 let item_entries = self.items[ix].project_entry_ids(cx);
2069 if item_entries.len() == 1 {
2070 Some(item_entries[0])
2071 } else {
2072 None
2073 }
2074 };
2075
2076 let is_pinned = self.is_tab_pinned(ix);
2077 let pane = cx.view().downgrade();
2078 let menu_context = item.focus_handle(cx);
2079 right_click_menu(ix).trigger(tab).menu(move |cx| {
2080 let pane = pane.clone();
2081 let menu_context = menu_context.clone();
2082 ContextMenu::build(cx, move |mut menu, cx| {
2083 if let Some(pane) = pane.upgrade() {
2084 menu = menu
2085 .entry(
2086 "Close",
2087 Some(Box::new(CloseActiveItem { save_intent: None })),
2088 cx.handler_for(&pane, move |pane, cx| {
2089 pane.close_item_by_id(item_id, SaveIntent::Close, cx)
2090 .detach_and_log_err(cx);
2091 }),
2092 )
2093 .entry(
2094 "Close Others",
2095 Some(Box::new(CloseInactiveItems {
2096 save_intent: None,
2097 close_pinned: false,
2098 })),
2099 cx.handler_for(&pane, move |pane, cx| {
2100 pane.close_items(cx, SaveIntent::Close, |id| id != item_id)
2101 .detach_and_log_err(cx);
2102 }),
2103 )
2104 .separator()
2105 .entry(
2106 "Close Left",
2107 Some(Box::new(CloseItemsToTheLeft {
2108 close_pinned: false,
2109 })),
2110 cx.handler_for(&pane, move |pane, cx| {
2111 pane.close_items_to_the_left_by_id(
2112 item_id,
2113 pane.get_non_closeable_item_ids(false),
2114 cx,
2115 )
2116 .detach_and_log_err(cx);
2117 }),
2118 )
2119 .entry(
2120 "Close Right",
2121 Some(Box::new(CloseItemsToTheRight {
2122 close_pinned: false,
2123 })),
2124 cx.handler_for(&pane, move |pane, cx| {
2125 pane.close_items_to_the_right_by_id(
2126 item_id,
2127 pane.get_non_closeable_item_ids(false),
2128 cx,
2129 )
2130 .detach_and_log_err(cx);
2131 }),
2132 )
2133 .separator()
2134 .entry(
2135 "Close Clean",
2136 Some(Box::new(CloseCleanItems {
2137 close_pinned: false,
2138 })),
2139 cx.handler_for(&pane, move |pane, cx| {
2140 if let Some(task) = pane.close_clean_items(
2141 &CloseCleanItems {
2142 close_pinned: false,
2143 },
2144 cx,
2145 ) {
2146 task.detach_and_log_err(cx)
2147 }
2148 }),
2149 )
2150 .entry(
2151 "Close All",
2152 Some(Box::new(CloseAllItems {
2153 save_intent: None,
2154 close_pinned: false,
2155 })),
2156 cx.handler_for(&pane, |pane, cx| {
2157 if let Some(task) = pane.close_all_items(
2158 &CloseAllItems {
2159 save_intent: None,
2160 close_pinned: false,
2161 },
2162 cx,
2163 ) {
2164 task.detach_and_log_err(cx)
2165 }
2166 }),
2167 );
2168
2169 let pin_tab_entries = |menu: ContextMenu| {
2170 menu.separator().map(|this| {
2171 if is_pinned {
2172 this.entry(
2173 "Unpin Tab",
2174 Some(TogglePinTab.boxed_clone()),
2175 cx.handler_for(&pane, move |pane, cx| {
2176 pane.unpin_tab_at(ix, cx);
2177 }),
2178 )
2179 } else {
2180 this.entry(
2181 "Pin Tab",
2182 Some(TogglePinTab.boxed_clone()),
2183 cx.handler_for(&pane, move |pane, cx| {
2184 pane.pin_tab_at(ix, cx);
2185 }),
2186 )
2187 }
2188 })
2189 };
2190 if let Some(entry) = single_entry_to_resolve {
2191 let entry_abs_path = pane.read(cx).entry_abs_path(entry, cx);
2192 let parent_abs_path = entry_abs_path
2193 .as_deref()
2194 .and_then(|abs_path| Some(abs_path.parent()?.to_path_buf()));
2195 let relative_path = pane
2196 .read(cx)
2197 .item_for_entry(entry, cx)
2198 .and_then(|item| item.project_path(cx))
2199 .map(|project_path| project_path.path);
2200
2201 let entry_id = entry.to_proto();
2202 menu = menu
2203 .separator()
2204 .when_some(entry_abs_path, |menu, abs_path| {
2205 menu.entry(
2206 "Copy Path",
2207 Some(Box::new(CopyPath)),
2208 cx.handler_for(&pane, move |_, cx| {
2209 cx.write_to_clipboard(ClipboardItem::new_string(
2210 abs_path.to_string_lossy().to_string(),
2211 ));
2212 }),
2213 )
2214 })
2215 .when_some(relative_path, |menu, relative_path| {
2216 menu.entry(
2217 "Copy Relative Path",
2218 Some(Box::new(CopyRelativePath)),
2219 cx.handler_for(&pane, move |_, cx| {
2220 cx.write_to_clipboard(ClipboardItem::new_string(
2221 relative_path.to_string_lossy().to_string(),
2222 ));
2223 }),
2224 )
2225 })
2226 .map(pin_tab_entries)
2227 .separator()
2228 .entry(
2229 "Reveal In Project Panel",
2230 Some(Box::new(RevealInProjectPanel {
2231 entry_id: Some(entry_id),
2232 })),
2233 cx.handler_for(&pane, move |pane, cx| {
2234 pane.project.update(cx, |_, cx| {
2235 cx.emit(project::Event::RevealInProjectPanel(
2236 ProjectEntryId::from_proto(entry_id),
2237 ))
2238 });
2239 }),
2240 )
2241 .when_some(parent_abs_path, |menu, parent_abs_path| {
2242 menu.entry(
2243 "Open in Terminal",
2244 Some(Box::new(OpenInTerminal)),
2245 cx.handler_for(&pane, move |_, cx| {
2246 cx.dispatch_action(
2247 OpenTerminal {
2248 working_directory: parent_abs_path.clone(),
2249 }
2250 .boxed_clone(),
2251 );
2252 }),
2253 )
2254 });
2255 } else {
2256 menu = menu.map(pin_tab_entries);
2257 }
2258 }
2259
2260 menu.context(menu_context)
2261 })
2262 })
2263 }
2264
2265 fn render_tab_bar(&mut self, cx: &mut ViewContext<'_, Pane>) -> impl IntoElement {
2266 let focus_handle = self.focus_handle.clone();
2267 let navigate_backward = IconButton::new("navigate_backward", IconName::ArrowLeft)
2268 .icon_size(IconSize::Small)
2269 .on_click({
2270 let view = cx.view().clone();
2271 move |_, cx| view.update(cx, Self::navigate_backward)
2272 })
2273 .disabled(!self.can_navigate_backward())
2274 .tooltip({
2275 let focus_handle = focus_handle.clone();
2276 move |cx| Tooltip::for_action_in("Go Back", &GoBack, &focus_handle, cx)
2277 });
2278
2279 let navigate_forward = IconButton::new("navigate_forward", IconName::ArrowRight)
2280 .icon_size(IconSize::Small)
2281 .on_click({
2282 let view = cx.view().clone();
2283 move |_, cx| view.update(cx, Self::navigate_forward)
2284 })
2285 .disabled(!self.can_navigate_forward())
2286 .tooltip({
2287 let focus_handle = focus_handle.clone();
2288 move |cx| Tooltip::for_action_in("Go Forward", &GoForward, &focus_handle, cx)
2289 });
2290
2291 let mut tab_items = self
2292 .items
2293 .iter()
2294 .enumerate()
2295 .zip(tab_details(&self.items, cx))
2296 .map(|((ix, item), detail)| self.render_tab(ix, &**item, detail, &focus_handle, cx))
2297 .collect::<Vec<_>>();
2298 let tab_count = tab_items.len();
2299 let unpinned_tabs = tab_items.split_off(self.pinned_tab_count);
2300 let pinned_tabs = tab_items;
2301 TabBar::new("tab_bar")
2302 .when(
2303 self.display_nav_history_buttons.unwrap_or_default(),
2304 |tab_bar| {
2305 tab_bar
2306 .start_child(navigate_backward)
2307 .start_child(navigate_forward)
2308 },
2309 )
2310 .map(|tab_bar| {
2311 let render_tab_buttons = self.render_tab_bar_buttons.clone();
2312 let (left_children, right_children) = render_tab_buttons(self, cx);
2313
2314 tab_bar
2315 .start_children(left_children)
2316 .end_children(right_children)
2317 })
2318 .children(pinned_tabs.len().ne(&0).then(|| {
2319 h_flex()
2320 .children(pinned_tabs)
2321 .border_r_2()
2322 .border_color(cx.theme().colors().border)
2323 }))
2324 .child(
2325 h_flex()
2326 .id("unpinned tabs")
2327 .overflow_x_scroll()
2328 .w_full()
2329 .track_scroll(&self.tab_bar_scroll_handle)
2330 .children(unpinned_tabs)
2331 .child(
2332 div()
2333 .id("tab_bar_drop_target")
2334 .min_w_6()
2335 // HACK: This empty child is currently necessary to force the drop target to appear
2336 // despite us setting a min width above.
2337 .child("")
2338 .h_full()
2339 .flex_grow()
2340 .drag_over::<DraggedTab>(|bar, _, cx| {
2341 bar.bg(cx.theme().colors().drop_target_background)
2342 })
2343 .drag_over::<DraggedSelection>(|bar, _, cx| {
2344 bar.bg(cx.theme().colors().drop_target_background)
2345 })
2346 .on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {
2347 this.drag_split_direction = None;
2348 this.handle_tab_drop(dragged_tab, this.items.len(), cx)
2349 }))
2350 .on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
2351 this.drag_split_direction = None;
2352 this.handle_project_entry_drop(
2353 &selection.active_selection.entry_id,
2354 Some(tab_count),
2355 cx,
2356 )
2357 }))
2358 .on_drop(cx.listener(move |this, paths, cx| {
2359 this.drag_split_direction = None;
2360 this.handle_external_paths_drop(paths, cx)
2361 }))
2362 .on_click(cx.listener(move |this, event: &ClickEvent, cx| {
2363 if event.up.click_count == 2 {
2364 cx.dispatch_action(
2365 this.double_click_dispatch_action.boxed_clone(),
2366 )
2367 }
2368 })),
2369 ),
2370 )
2371 }
2372
2373 pub fn render_menu_overlay(menu: &View<ContextMenu>) -> Div {
2374 div().absolute().bottom_0().right_0().size_0().child(
2375 deferred(
2376 anchored()
2377 .anchor(AnchorCorner::TopRight)
2378 .child(menu.clone()),
2379 )
2380 .with_priority(1),
2381 )
2382 }
2383
2384 pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
2385 self.zoomed = zoomed;
2386 cx.notify();
2387 }
2388
2389 pub fn is_zoomed(&self) -> bool {
2390 self.zoomed
2391 }
2392
2393 fn handle_drag_move<T: 'static>(
2394 &mut self,
2395 event: &DragMoveEvent<T>,
2396 cx: &mut ViewContext<Self>,
2397 ) {
2398 let can_split_predicate = self.can_split_predicate.take();
2399 let can_split = match &can_split_predicate {
2400 Some(can_split_predicate) => can_split_predicate(self, event.dragged_item(), cx),
2401 None => false,
2402 };
2403 self.can_split_predicate = can_split_predicate;
2404 if !can_split {
2405 return;
2406 }
2407
2408 let rect = event.bounds.size;
2409
2410 let size = event.bounds.size.width.min(event.bounds.size.height)
2411 * WorkspaceSettings::get_global(cx).drop_target_size;
2412
2413 let relative_cursor = Point::new(
2414 event.event.position.x - event.bounds.left(),
2415 event.event.position.y - event.bounds.top(),
2416 );
2417
2418 let direction = if relative_cursor.x < size
2419 || relative_cursor.x > rect.width - size
2420 || relative_cursor.y < size
2421 || relative_cursor.y > rect.height - size
2422 {
2423 [
2424 SplitDirection::Up,
2425 SplitDirection::Right,
2426 SplitDirection::Down,
2427 SplitDirection::Left,
2428 ]
2429 .iter()
2430 .min_by_key(|side| match side {
2431 SplitDirection::Up => relative_cursor.y,
2432 SplitDirection::Right => rect.width - relative_cursor.x,
2433 SplitDirection::Down => rect.height - relative_cursor.y,
2434 SplitDirection::Left => relative_cursor.x,
2435 })
2436 .cloned()
2437 } else {
2438 None
2439 };
2440
2441 if direction != self.drag_split_direction {
2442 self.drag_split_direction = direction;
2443 }
2444 }
2445
2446 fn handle_tab_drop(
2447 &mut self,
2448 dragged_tab: &DraggedTab,
2449 ix: usize,
2450 cx: &mut ViewContext<'_, Self>,
2451 ) {
2452 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2453 if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, cx) {
2454 return;
2455 }
2456 }
2457 let mut to_pane = cx.view().clone();
2458 let split_direction = self.drag_split_direction;
2459 let item_id = dragged_tab.item.item_id();
2460 if let Some(preview_item_id) = self.preview_item_id {
2461 if item_id == preview_item_id {
2462 self.set_preview_item_id(None, cx);
2463 }
2464 }
2465
2466 let from_pane = dragged_tab.pane.clone();
2467 self.workspace
2468 .update(cx, |_, cx| {
2469 cx.defer(move |workspace, cx| {
2470 if let Some(split_direction) = split_direction {
2471 to_pane = workspace.split_pane(to_pane, split_direction, cx);
2472 }
2473 let old_ix = from_pane.read(cx).index_for_item_id(item_id);
2474 let old_len = to_pane.read(cx).items.len();
2475 move_item(&from_pane, &to_pane, item_id, ix, cx);
2476 if to_pane == from_pane {
2477 if let Some(old_index) = old_ix {
2478 to_pane.update(cx, |this, _| {
2479 if old_index < this.pinned_tab_count
2480 && (ix == this.items.len() || ix > this.pinned_tab_count)
2481 {
2482 this.pinned_tab_count -= 1;
2483 } else if this.has_pinned_tabs()
2484 && old_index >= this.pinned_tab_count
2485 && ix < this.pinned_tab_count
2486 {
2487 this.pinned_tab_count += 1;
2488 }
2489 });
2490 }
2491 } else {
2492 to_pane.update(cx, |this, _| {
2493 if this.items.len() > old_len // Did we not deduplicate on drag?
2494 && this.has_pinned_tabs()
2495 && ix < this.pinned_tab_count
2496 {
2497 this.pinned_tab_count += 1;
2498 }
2499 });
2500 from_pane.update(cx, |this, _| {
2501 if let Some(index) = old_ix {
2502 if this.pinned_tab_count > index {
2503 this.pinned_tab_count -= 1;
2504 }
2505 }
2506 })
2507 }
2508 });
2509 })
2510 .log_err();
2511 }
2512
2513 fn handle_dragged_selection_drop(
2514 &mut self,
2515 dragged_selection: &DraggedSelection,
2516 dragged_onto: Option<usize>,
2517 cx: &mut ViewContext<'_, Self>,
2518 ) {
2519 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2520 if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_selection, cx) {
2521 return;
2522 }
2523 }
2524 self.handle_project_entry_drop(
2525 &dragged_selection.active_selection.entry_id,
2526 dragged_onto,
2527 cx,
2528 );
2529 }
2530
2531 fn handle_project_entry_drop(
2532 &mut self,
2533 project_entry_id: &ProjectEntryId,
2534 target: Option<usize>,
2535 cx: &mut ViewContext<'_, Self>,
2536 ) {
2537 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2538 if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, cx) {
2539 return;
2540 }
2541 }
2542 let mut to_pane = cx.view().clone();
2543 let split_direction = self.drag_split_direction;
2544 let project_entry_id = *project_entry_id;
2545 self.workspace
2546 .update(cx, |_, cx| {
2547 cx.defer(move |workspace, cx| {
2548 if let Some(path) = workspace
2549 .project()
2550 .read(cx)
2551 .path_for_entry(project_entry_id, cx)
2552 {
2553 let load_path_task = workspace.load_path(path, cx);
2554 cx.spawn(|workspace, mut cx| async move {
2555 if let Some((project_entry_id, build_item)) =
2556 load_path_task.await.notify_async_err(&mut cx)
2557 {
2558 let (to_pane, new_item_handle) = workspace
2559 .update(&mut cx, |workspace, cx| {
2560 if let Some(split_direction) = split_direction {
2561 to_pane =
2562 workspace.split_pane(to_pane, split_direction, cx);
2563 }
2564 let new_item_handle = to_pane.update(cx, |pane, cx| {
2565 pane.open_item(
2566 project_entry_id,
2567 true,
2568 false,
2569 target,
2570 cx,
2571 build_item,
2572 )
2573 });
2574 (to_pane, new_item_handle)
2575 })
2576 .log_err()?;
2577 to_pane
2578 .update(&mut cx, |this, cx| {
2579 let Some(index) = this.index_for_item(&*new_item_handle)
2580 else {
2581 return;
2582 };
2583
2584 if target.map_or(false, |target| this.is_tab_pinned(target))
2585 {
2586 this.pin_tab_at(index, cx);
2587 }
2588 })
2589 .ok()?
2590 }
2591 Some(())
2592 })
2593 .detach();
2594 };
2595 });
2596 })
2597 .log_err();
2598 }
2599
2600 fn handle_external_paths_drop(
2601 &mut self,
2602 paths: &ExternalPaths,
2603 cx: &mut ViewContext<'_, Self>,
2604 ) {
2605 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2606 if let ControlFlow::Break(()) = custom_drop_handle(self, paths, cx) {
2607 return;
2608 }
2609 }
2610 let mut to_pane = cx.view().clone();
2611 let mut split_direction = self.drag_split_direction;
2612 let paths = paths.paths().to_vec();
2613 let is_remote = self
2614 .workspace
2615 .update(cx, |workspace, cx| {
2616 if workspace.project().read(cx).is_via_collab() {
2617 workspace.show_error(
2618 &anyhow::anyhow!("Cannot drop files on a remote project"),
2619 cx,
2620 );
2621 true
2622 } else {
2623 false
2624 }
2625 })
2626 .unwrap_or(true);
2627 if is_remote {
2628 return;
2629 }
2630
2631 self.workspace
2632 .update(cx, |workspace, cx| {
2633 let fs = Arc::clone(workspace.project().read(cx).fs());
2634 cx.spawn(|workspace, mut cx| async move {
2635 let mut is_file_checks = FuturesUnordered::new();
2636 for path in &paths {
2637 is_file_checks.push(fs.is_file(path))
2638 }
2639 let mut has_files_to_open = false;
2640 while let Some(is_file) = is_file_checks.next().await {
2641 if is_file {
2642 has_files_to_open = true;
2643 break;
2644 }
2645 }
2646 drop(is_file_checks);
2647 if !has_files_to_open {
2648 split_direction = None;
2649 }
2650
2651 if let Ok(open_task) = workspace.update(&mut cx, |workspace, cx| {
2652 if let Some(split_direction) = split_direction {
2653 to_pane = workspace.split_pane(to_pane, split_direction, cx);
2654 }
2655 workspace.open_paths(
2656 paths,
2657 OpenVisible::OnlyDirectories,
2658 Some(to_pane.downgrade()),
2659 cx,
2660 )
2661 }) {
2662 let opened_items: Vec<_> = open_task.await;
2663 _ = workspace.update(&mut cx, |workspace, cx| {
2664 for item in opened_items.into_iter().flatten() {
2665 if let Err(e) = item {
2666 workspace.show_error(&e, cx);
2667 }
2668 }
2669 });
2670 }
2671 })
2672 .detach();
2673 })
2674 .log_err();
2675 }
2676
2677 pub fn display_nav_history_buttons(&mut self, display: Option<bool>) {
2678 self.display_nav_history_buttons = display;
2679 }
2680
2681 fn get_non_closeable_item_ids(&self, close_pinned: bool) -> Vec<EntityId> {
2682 if close_pinned {
2683 return vec![];
2684 }
2685
2686 self.items
2687 .iter()
2688 .map(|item| item.item_id())
2689 .filter(|item_id| {
2690 if let Some(ix) = self.index_for_item_id(*item_id) {
2691 self.is_tab_pinned(ix)
2692 } else {
2693 true
2694 }
2695 })
2696 .collect()
2697 }
2698
2699 pub fn drag_split_direction(&self) -> Option<SplitDirection> {
2700 self.drag_split_direction
2701 }
2702}
2703
2704impl FocusableView for Pane {
2705 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
2706 self.focus_handle.clone()
2707 }
2708}
2709
2710impl Render for Pane {
2711 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2712 let mut key_context = KeyContext::new_with_defaults();
2713 key_context.add("Pane");
2714 if self.active_item().is_none() {
2715 key_context.add("EmptyPane");
2716 }
2717
2718 let should_display_tab_bar = self.should_display_tab_bar.clone();
2719 let display_tab_bar = should_display_tab_bar(cx);
2720 let is_local = self.project.read(cx).is_local();
2721
2722 v_flex()
2723 .key_context(key_context)
2724 .track_focus(&self.focus_handle(cx))
2725 .size_full()
2726 .flex_none()
2727 .overflow_hidden()
2728 .on_action(cx.listener(|pane, _: &AlternateFile, cx| {
2729 pane.alternate_file(cx);
2730 }))
2731 .on_action(cx.listener(|pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx)))
2732 .on_action(cx.listener(|pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx)))
2733 .on_action(cx.listener(|pane, _: &SplitHorizontal, cx| {
2734 pane.split(SplitDirection::horizontal(cx), cx)
2735 }))
2736 .on_action(cx.listener(|pane, _: &SplitVertical, cx| {
2737 pane.split(SplitDirection::vertical(cx), cx)
2738 }))
2739 .on_action(
2740 cx.listener(|pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx)),
2741 )
2742 .on_action(cx.listener(|pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx)))
2743 .on_action(cx.listener(|pane, _: &GoBack, cx| pane.navigate_backward(cx)))
2744 .on_action(cx.listener(|pane, _: &GoForward, cx| pane.navigate_forward(cx)))
2745 .on_action(cx.listener(|pane, _: &JoinIntoNext, cx| pane.join_into_next(cx)))
2746 .on_action(cx.listener(|pane, _: &JoinAll, cx| pane.join_all(cx)))
2747 .on_action(cx.listener(Pane::toggle_zoom))
2748 .on_action(cx.listener(|pane: &mut Pane, action: &ActivateItem, cx| {
2749 pane.activate_item(action.0, true, true, cx);
2750 }))
2751 .on_action(cx.listener(|pane: &mut Pane, _: &ActivateLastItem, cx| {
2752 pane.activate_item(pane.items.len() - 1, true, true, cx);
2753 }))
2754 .on_action(cx.listener(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
2755 pane.activate_prev_item(true, cx);
2756 }))
2757 .on_action(cx.listener(|pane: &mut Pane, _: &ActivateNextItem, cx| {
2758 pane.activate_next_item(true, cx);
2759 }))
2760 .on_action(cx.listener(|pane, _: &SwapItemLeft, cx| pane.swap_item_left(cx)))
2761 .on_action(cx.listener(|pane, _: &SwapItemRight, cx| pane.swap_item_right(cx)))
2762 .on_action(cx.listener(|pane, action, cx| {
2763 pane.toggle_pin_tab(action, cx);
2764 }))
2765 .when(PreviewTabsSettings::get_global(cx).enabled, |this| {
2766 this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, cx| {
2767 if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) {
2768 if pane.is_active_preview_item(active_item_id) {
2769 pane.set_preview_item_id(None, cx);
2770 } else {
2771 pane.set_preview_item_id(Some(active_item_id), cx);
2772 }
2773 }
2774 }))
2775 })
2776 .on_action(
2777 cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
2778 if let Some(task) = pane.close_active_item(action, cx) {
2779 task.detach_and_log_err(cx)
2780 }
2781 }),
2782 )
2783 .on_action(
2784 cx.listener(|pane: &mut Self, action: &CloseInactiveItems, cx| {
2785 if let Some(task) = pane.close_inactive_items(action, cx) {
2786 task.detach_and_log_err(cx)
2787 }
2788 }),
2789 )
2790 .on_action(
2791 cx.listener(|pane: &mut Self, action: &CloseCleanItems, cx| {
2792 if let Some(task) = pane.close_clean_items(action, cx) {
2793 task.detach_and_log_err(cx)
2794 }
2795 }),
2796 )
2797 .on_action(
2798 cx.listener(|pane: &mut Self, action: &CloseItemsToTheLeft, cx| {
2799 if let Some(task) = pane.close_items_to_the_left(action, cx) {
2800 task.detach_and_log_err(cx)
2801 }
2802 }),
2803 )
2804 .on_action(
2805 cx.listener(|pane: &mut Self, action: &CloseItemsToTheRight, cx| {
2806 if let Some(task) = pane.close_items_to_the_right(action, cx) {
2807 task.detach_and_log_err(cx)
2808 }
2809 }),
2810 )
2811 .on_action(cx.listener(|pane: &mut Self, action: &CloseAllItems, cx| {
2812 if let Some(task) = pane.close_all_items(action, cx) {
2813 task.detach_and_log_err(cx)
2814 }
2815 }))
2816 .on_action(
2817 cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
2818 if let Some(task) = pane.close_active_item(action, cx) {
2819 task.detach_and_log_err(cx)
2820 }
2821 }),
2822 )
2823 .on_action(
2824 cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, cx| {
2825 let entry_id = action
2826 .entry_id
2827 .map(ProjectEntryId::from_proto)
2828 .or_else(|| pane.active_item()?.project_entry_ids(cx).first().copied());
2829 if let Some(entry_id) = entry_id {
2830 pane.project.update(cx, |_, cx| {
2831 cx.emit(project::Event::RevealInProjectPanel(entry_id))
2832 });
2833 }
2834 }),
2835 )
2836 .when(self.active_item().is_some() && display_tab_bar, |pane| {
2837 pane.child(self.render_tab_bar(cx))
2838 })
2839 .child({
2840 let has_worktrees = self.project.read(cx).worktrees(cx).next().is_some();
2841 // main content
2842 div()
2843 .flex_1()
2844 .relative()
2845 .group("")
2846 .overflow_hidden()
2847 .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
2848 .on_drag_move::<DraggedSelection>(cx.listener(Self::handle_drag_move))
2849 .when(is_local, |div| {
2850 div.on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
2851 })
2852 .map(|div| {
2853 if let Some(item) = self.active_item() {
2854 div.v_flex()
2855 .size_full()
2856 .overflow_hidden()
2857 .child(self.toolbar.clone())
2858 .child(item.to_any())
2859 } else {
2860 let placeholder = div.h_flex().size_full().justify_center();
2861 if has_worktrees {
2862 placeholder
2863 } else {
2864 placeholder.child(
2865 Label::new("Open a file or project to get started.")
2866 .color(Color::Muted),
2867 )
2868 }
2869 }
2870 })
2871 .child(
2872 // drag target
2873 div()
2874 .invisible()
2875 .absolute()
2876 .bg(cx.theme().colors().drop_target_background)
2877 .group_drag_over::<DraggedTab>("", |style| style.visible())
2878 .group_drag_over::<DraggedSelection>("", |style| style.visible())
2879 .when(is_local, |div| {
2880 div.group_drag_over::<ExternalPaths>("", |style| style.visible())
2881 })
2882 .when_some(self.can_drop_predicate.clone(), |this, p| {
2883 this.can_drop(move |a, cx| p(a, cx))
2884 })
2885 .on_drop(cx.listener(move |this, dragged_tab, cx| {
2886 this.handle_tab_drop(dragged_tab, this.active_item_index(), cx)
2887 }))
2888 .on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
2889 this.handle_dragged_selection_drop(selection, None, cx)
2890 }))
2891 .on_drop(cx.listener(move |this, paths, cx| {
2892 this.handle_external_paths_drop(paths, cx)
2893 }))
2894 .map(|div| {
2895 let size = DefiniteLength::Fraction(0.5);
2896 match self.drag_split_direction {
2897 None => div.top_0().right_0().bottom_0().left_0(),
2898 Some(SplitDirection::Up) => {
2899 div.top_0().left_0().right_0().h(size)
2900 }
2901 Some(SplitDirection::Down) => {
2902 div.left_0().bottom_0().right_0().h(size)
2903 }
2904 Some(SplitDirection::Left) => {
2905 div.top_0().left_0().bottom_0().w(size)
2906 }
2907 Some(SplitDirection::Right) => {
2908 div.top_0().bottom_0().right_0().w(size)
2909 }
2910 }
2911 }),
2912 )
2913 })
2914 .on_mouse_down(
2915 MouseButton::Navigate(NavigationDirection::Back),
2916 cx.listener(|pane, _, cx| {
2917 if let Some(workspace) = pane.workspace.upgrade() {
2918 let pane = cx.view().downgrade();
2919 cx.window_context().defer(move |cx| {
2920 workspace.update(cx, |workspace, cx| {
2921 workspace.go_back(pane, cx).detach_and_log_err(cx)
2922 })
2923 })
2924 }
2925 }),
2926 )
2927 .on_mouse_down(
2928 MouseButton::Navigate(NavigationDirection::Forward),
2929 cx.listener(|pane, _, cx| {
2930 if let Some(workspace) = pane.workspace.upgrade() {
2931 let pane = cx.view().downgrade();
2932 cx.window_context().defer(move |cx| {
2933 workspace.update(cx, |workspace, cx| {
2934 workspace.go_forward(pane, cx).detach_and_log_err(cx)
2935 })
2936 })
2937 }
2938 }),
2939 )
2940 }
2941}
2942
2943impl ItemNavHistory {
2944 pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut WindowContext) {
2945 self.history
2946 .push(data, self.item.clone(), self.is_preview, cx);
2947 }
2948
2949 pub fn pop_backward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
2950 self.history.pop(NavigationMode::GoingBack, cx)
2951 }
2952
2953 pub fn pop_forward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
2954 self.history.pop(NavigationMode::GoingForward, cx)
2955 }
2956}
2957
2958impl NavHistory {
2959 pub fn for_each_entry(
2960 &self,
2961 cx: &AppContext,
2962 mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
2963 ) {
2964 let borrowed_history = self.0.lock();
2965 borrowed_history
2966 .forward_stack
2967 .iter()
2968 .chain(borrowed_history.backward_stack.iter())
2969 .chain(borrowed_history.closed_stack.iter())
2970 .for_each(|entry| {
2971 if let Some(project_and_abs_path) =
2972 borrowed_history.paths_by_item.get(&entry.item.id())
2973 {
2974 f(entry, project_and_abs_path.clone());
2975 } else if let Some(item) = entry.item.upgrade() {
2976 if let Some(path) = item.project_path(cx) {
2977 f(entry, (path, None));
2978 }
2979 }
2980 })
2981 }
2982
2983 pub fn set_mode(&mut self, mode: NavigationMode) {
2984 self.0.lock().mode = mode;
2985 }
2986
2987 pub fn mode(&self) -> NavigationMode {
2988 self.0.lock().mode
2989 }
2990
2991 pub fn disable(&mut self) {
2992 self.0.lock().mode = NavigationMode::Disabled;
2993 }
2994
2995 pub fn enable(&mut self) {
2996 self.0.lock().mode = NavigationMode::Normal;
2997 }
2998
2999 pub fn pop(&mut self, mode: NavigationMode, cx: &mut WindowContext) -> Option<NavigationEntry> {
3000 let mut state = self.0.lock();
3001 let entry = match mode {
3002 NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
3003 return None
3004 }
3005 NavigationMode::GoingBack => &mut state.backward_stack,
3006 NavigationMode::GoingForward => &mut state.forward_stack,
3007 NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
3008 }
3009 .pop_back();
3010 if entry.is_some() {
3011 state.did_update(cx);
3012 }
3013 entry
3014 }
3015
3016 pub fn push<D: 'static + Send + Any>(
3017 &mut self,
3018 data: Option<D>,
3019 item: Arc<dyn WeakItemHandle>,
3020 is_preview: bool,
3021 cx: &mut WindowContext,
3022 ) {
3023 let state = &mut *self.0.lock();
3024 match state.mode {
3025 NavigationMode::Disabled => {}
3026 NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
3027 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3028 state.backward_stack.pop_front();
3029 }
3030 state.backward_stack.push_back(NavigationEntry {
3031 item,
3032 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3033 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3034 is_preview,
3035 });
3036 state.forward_stack.clear();
3037 }
3038 NavigationMode::GoingBack => {
3039 if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3040 state.forward_stack.pop_front();
3041 }
3042 state.forward_stack.push_back(NavigationEntry {
3043 item,
3044 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3045 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3046 is_preview,
3047 });
3048 }
3049 NavigationMode::GoingForward => {
3050 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3051 state.backward_stack.pop_front();
3052 }
3053 state.backward_stack.push_back(NavigationEntry {
3054 item,
3055 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3056 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3057 is_preview,
3058 });
3059 }
3060 NavigationMode::ClosingItem => {
3061 if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3062 state.closed_stack.pop_front();
3063 }
3064 state.closed_stack.push_back(NavigationEntry {
3065 item,
3066 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3067 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3068 is_preview,
3069 });
3070 }
3071 }
3072 state.did_update(cx);
3073 }
3074
3075 pub fn remove_item(&mut self, item_id: EntityId) {
3076 let mut state = self.0.lock();
3077 state.paths_by_item.remove(&item_id);
3078 state
3079 .backward_stack
3080 .retain(|entry| entry.item.id() != item_id);
3081 state
3082 .forward_stack
3083 .retain(|entry| entry.item.id() != item_id);
3084 state
3085 .closed_stack
3086 .retain(|entry| entry.item.id() != item_id);
3087 }
3088
3089 pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
3090 self.0.lock().paths_by_item.get(&item_id).cloned()
3091 }
3092}
3093
3094impl NavHistoryState {
3095 pub fn did_update(&self, cx: &mut WindowContext) {
3096 if let Some(pane) = self.pane.upgrade() {
3097 cx.defer(move |cx| {
3098 pane.update(cx, |pane, cx| pane.history_updated(cx));
3099 });
3100 }
3101 }
3102}
3103
3104fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
3105 let path = buffer_path
3106 .as_ref()
3107 .and_then(|p| {
3108 p.path
3109 .to_str()
3110 .and_then(|s| if s.is_empty() { None } else { Some(s) })
3111 })
3112 .unwrap_or("This buffer");
3113 let path = truncate_and_remove_front(path, 80);
3114 format!("{path} contains unsaved edits. Do you want to save it?")
3115}
3116
3117pub fn tab_details(items: &[Box<dyn ItemHandle>], cx: &AppContext) -> Vec<usize> {
3118 let mut tab_details = items.iter().map(|_| 0).collect::<Vec<_>>();
3119 let mut tab_descriptions = HashMap::default();
3120 let mut done = false;
3121 while !done {
3122 done = true;
3123
3124 // Store item indices by their tab description.
3125 for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() {
3126 if let Some(description) = item.tab_description(*detail, cx) {
3127 if *detail == 0
3128 || Some(&description) != item.tab_description(detail - 1, cx).as_ref()
3129 {
3130 tab_descriptions
3131 .entry(description)
3132 .or_insert(Vec::new())
3133 .push(ix);
3134 }
3135 }
3136 }
3137
3138 // If two or more items have the same tab description, increase their level
3139 // of detail and try again.
3140 for (_, item_ixs) in tab_descriptions.drain() {
3141 if item_ixs.len() > 1 {
3142 done = false;
3143 for ix in item_ixs {
3144 tab_details[ix] += 1;
3145 }
3146 }
3147 }
3148 }
3149
3150 tab_details
3151}
3152
3153pub fn render_item_indicator(item: Box<dyn ItemHandle>, cx: &WindowContext) -> Option<Indicator> {
3154 maybe!({
3155 let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
3156 (true, _) => Color::Warning,
3157 (_, true) => Color::Accent,
3158 (false, false) => return None,
3159 };
3160
3161 Some(Indicator::dot().color(indicator_color))
3162 })
3163}
3164
3165impl Render for DraggedTab {
3166 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3167 let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
3168 let label = self.item.tab_content(
3169 TabContentParams {
3170 detail: Some(self.detail),
3171 selected: false,
3172 preview: false,
3173 },
3174 cx,
3175 );
3176 Tab::new("")
3177 .selected(self.is_active)
3178 .child(label)
3179 .render(cx)
3180 .font(ui_font)
3181 }
3182}
3183
3184#[cfg(test)]
3185mod tests {
3186 use super::*;
3187 use crate::item::test::{TestItem, TestProjectItem};
3188 use gpui::{TestAppContext, VisualTestContext};
3189 use project::FakeFs;
3190 use settings::SettingsStore;
3191 use theme::LoadThemes;
3192
3193 #[gpui::test]
3194 async fn test_remove_active_empty(cx: &mut TestAppContext) {
3195 init_test(cx);
3196 let fs = FakeFs::new(cx.executor());
3197
3198 let project = Project::test(fs, None, cx).await;
3199 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3200 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3201
3202 pane.update(cx, |pane, cx| {
3203 assert!(pane
3204 .close_active_item(&CloseActiveItem { save_intent: None }, cx)
3205 .is_none())
3206 });
3207 }
3208
3209 #[gpui::test]
3210 async fn test_add_item_with_new_item(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 // 1. Add with a destination index
3219 // a. Add before the active item
3220 set_labeled_items(&pane, ["A", "B*", "C"], cx);
3221 pane.update(cx, |pane, cx| {
3222 pane.add_item(
3223 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3224 false,
3225 false,
3226 Some(0),
3227 cx,
3228 );
3229 });
3230 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3231
3232 // b. Add after the active item
3233 set_labeled_items(&pane, ["A", "B*", "C"], cx);
3234 pane.update(cx, |pane, cx| {
3235 pane.add_item(
3236 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3237 false,
3238 false,
3239 Some(2),
3240 cx,
3241 );
3242 });
3243 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3244
3245 // c. Add at the end of the item list (including off the length)
3246 set_labeled_items(&pane, ["A", "B*", "C"], cx);
3247 pane.update(cx, |pane, cx| {
3248 pane.add_item(
3249 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3250 false,
3251 false,
3252 Some(5),
3253 cx,
3254 );
3255 });
3256 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3257
3258 // 2. Add without a destination index
3259 // a. Add with active item at the start of the item list
3260 set_labeled_items(&pane, ["A*", "B", "C"], cx);
3261 pane.update(cx, |pane, cx| {
3262 pane.add_item(
3263 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3264 false,
3265 false,
3266 None,
3267 cx,
3268 );
3269 });
3270 set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
3271
3272 // b. Add with active item at the end of the item list
3273 set_labeled_items(&pane, ["A", "B", "C*"], cx);
3274 pane.update(cx, |pane, cx| {
3275 pane.add_item(
3276 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
3277 false,
3278 false,
3279 None,
3280 cx,
3281 );
3282 });
3283 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3284 }
3285
3286 #[gpui::test]
3287 async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
3288 init_test(cx);
3289 let fs = FakeFs::new(cx.executor());
3290
3291 let project = Project::test(fs, None, cx).await;
3292 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3293 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3294
3295 // 1. Add with a destination index
3296 // 1a. Add before the active item
3297 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3298 pane.update(cx, |pane, cx| {
3299 pane.add_item(d, false, false, Some(0), cx);
3300 });
3301 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3302
3303 // 1b. Add after the active item
3304 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3305 pane.update(cx, |pane, cx| {
3306 pane.add_item(d, false, false, Some(2), cx);
3307 });
3308 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3309
3310 // 1c. Add at the end of the item list (including off the length)
3311 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3312 pane.update(cx, |pane, cx| {
3313 pane.add_item(a, false, false, Some(5), cx);
3314 });
3315 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3316
3317 // 1d. Add same item to active index
3318 let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3319 pane.update(cx, |pane, cx| {
3320 pane.add_item(b, false, false, Some(1), cx);
3321 });
3322 assert_item_labels(&pane, ["A", "B*", "C"], cx);
3323
3324 // 1e. Add item to index after same item in last position
3325 let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3326 pane.update(cx, |pane, cx| {
3327 pane.add_item(c, false, false, Some(2), cx);
3328 });
3329 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3330
3331 // 2. Add without a destination index
3332 // 2a. Add with active item at the start of the item list
3333 let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
3334 pane.update(cx, |pane, cx| {
3335 pane.add_item(d, false, false, None, cx);
3336 });
3337 assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
3338
3339 // 2b. Add with active item at the end of the item list
3340 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
3341 pane.update(cx, |pane, cx| {
3342 pane.add_item(a, false, false, None, cx);
3343 });
3344 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3345
3346 // 2c. Add active item to active item at end of list
3347 let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
3348 pane.update(cx, |pane, cx| {
3349 pane.add_item(c, false, false, None, cx);
3350 });
3351 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3352
3353 // 2d. Add active item to active item at start of list
3354 let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
3355 pane.update(cx, |pane, cx| {
3356 pane.add_item(a, false, false, None, cx);
3357 });
3358 assert_item_labels(&pane, ["A*", "B", "C"], cx);
3359 }
3360
3361 #[gpui::test]
3362 async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
3363 init_test(cx);
3364 let fs = FakeFs::new(cx.executor());
3365
3366 let project = Project::test(fs, None, cx).await;
3367 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3368 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3369
3370 // singleton view
3371 pane.update(cx, |pane, cx| {
3372 pane.add_item(
3373 Box::new(cx.new_view(|cx| {
3374 TestItem::new(cx)
3375 .with_singleton(true)
3376 .with_label("buffer 1")
3377 .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
3378 })),
3379 false,
3380 false,
3381 None,
3382 cx,
3383 );
3384 });
3385 assert_item_labels(&pane, ["buffer 1*"], cx);
3386
3387 // new singleton view with the same project entry
3388 pane.update(cx, |pane, cx| {
3389 pane.add_item(
3390 Box::new(cx.new_view(|cx| {
3391 TestItem::new(cx)
3392 .with_singleton(true)
3393 .with_label("buffer 1")
3394 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3395 })),
3396 false,
3397 false,
3398 None,
3399 cx,
3400 );
3401 });
3402 assert_item_labels(&pane, ["buffer 1*"], cx);
3403
3404 // new singleton view with different project entry
3405 pane.update(cx, |pane, cx| {
3406 pane.add_item(
3407 Box::new(cx.new_view(|cx| {
3408 TestItem::new(cx)
3409 .with_singleton(true)
3410 .with_label("buffer 2")
3411 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
3412 })),
3413 false,
3414 false,
3415 None,
3416 cx,
3417 );
3418 });
3419 assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
3420
3421 // new multibuffer view with the same project entry
3422 pane.update(cx, |pane, cx| {
3423 pane.add_item(
3424 Box::new(cx.new_view(|cx| {
3425 TestItem::new(cx)
3426 .with_singleton(false)
3427 .with_label("multibuffer 1")
3428 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3429 })),
3430 false,
3431 false,
3432 None,
3433 cx,
3434 );
3435 });
3436 assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
3437
3438 // another multibuffer view with the same project entry
3439 pane.update(cx, |pane, cx| {
3440 pane.add_item(
3441 Box::new(cx.new_view(|cx| {
3442 TestItem::new(cx)
3443 .with_singleton(false)
3444 .with_label("multibuffer 1b")
3445 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3446 })),
3447 false,
3448 false,
3449 None,
3450 cx,
3451 );
3452 });
3453 assert_item_labels(
3454 &pane,
3455 ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
3456 cx,
3457 );
3458 }
3459
3460 #[gpui::test]
3461 async fn test_remove_item_ordering_history(cx: &mut TestAppContext) {
3462 init_test(cx);
3463 let fs = FakeFs::new(cx.executor());
3464
3465 let project = Project::test(fs, None, cx).await;
3466 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3467 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3468
3469 add_labeled_item(&pane, "A", false, cx);
3470 add_labeled_item(&pane, "B", false, cx);
3471 add_labeled_item(&pane, "C", false, cx);
3472 add_labeled_item(&pane, "D", false, cx);
3473 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3474
3475 pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
3476 add_labeled_item(&pane, "1", false, cx);
3477 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
3478
3479 pane.update(cx, |pane, cx| {
3480 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3481 })
3482 .unwrap()
3483 .await
3484 .unwrap();
3485 assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
3486
3487 pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
3488 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3489
3490 pane.update(cx, |pane, cx| {
3491 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3492 })
3493 .unwrap()
3494 .await
3495 .unwrap();
3496 assert_item_labels(&pane, ["A", "B*", "C"], cx);
3497
3498 pane.update(cx, |pane, cx| {
3499 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3500 })
3501 .unwrap()
3502 .await
3503 .unwrap();
3504 assert_item_labels(&pane, ["A", "C*"], 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*"], cx);
3513 }
3514
3515 #[gpui::test]
3516 async fn test_remove_item_ordering_neighbour(cx: &mut TestAppContext) {
3517 init_test(cx);
3518 cx.update_global::<SettingsStore, ()>(|s, cx| {
3519 s.update_user_settings::<ItemSettings>(cx, |s| {
3520 s.activate_on_close = Some(ActivateOnClose::Neighbour);
3521 });
3522 });
3523 let fs = FakeFs::new(cx.executor());
3524
3525 let project = Project::test(fs, None, cx).await;
3526 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3527 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3528
3529 add_labeled_item(&pane, "A", false, cx);
3530 add_labeled_item(&pane, "B", false, cx);
3531 add_labeled_item(&pane, "C", false, cx);
3532 add_labeled_item(&pane, "D", false, cx);
3533 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3534
3535 pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
3536 add_labeled_item(&pane, "1", false, cx);
3537 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
3538
3539 pane.update(cx, |pane, cx| {
3540 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3541 })
3542 .unwrap()
3543 .await
3544 .unwrap();
3545 assert_item_labels(&pane, ["A", "B", "C*", "D"], cx);
3546
3547 pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
3548 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3549
3550 pane.update(cx, |pane, cx| {
3551 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3552 })
3553 .unwrap()
3554 .await
3555 .unwrap();
3556 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3557
3558 pane.update(cx, |pane, cx| {
3559 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3560 })
3561 .unwrap()
3562 .await
3563 .unwrap();
3564 assert_item_labels(&pane, ["A", "B*"], 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*"], cx);
3573 }
3574
3575 #[gpui::test]
3576 async fn test_close_inactive_items(cx: &mut TestAppContext) {
3577 init_test(cx);
3578 let fs = FakeFs::new(cx.executor());
3579
3580 let project = Project::test(fs, None, cx).await;
3581 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3582 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3583
3584 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3585
3586 pane.update(cx, |pane, cx| {
3587 pane.close_inactive_items(
3588 &CloseInactiveItems {
3589 save_intent: None,
3590 close_pinned: false,
3591 },
3592 cx,
3593 )
3594 })
3595 .unwrap()
3596 .await
3597 .unwrap();
3598 assert_item_labels(&pane, ["C*"], cx);
3599 }
3600
3601 #[gpui::test]
3602 async fn test_close_clean_items(cx: &mut TestAppContext) {
3603 init_test(cx);
3604 let fs = FakeFs::new(cx.executor());
3605
3606 let project = Project::test(fs, None, cx).await;
3607 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3608 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3609
3610 add_labeled_item(&pane, "A", true, cx);
3611 add_labeled_item(&pane, "B", false, cx);
3612 add_labeled_item(&pane, "C", true, cx);
3613 add_labeled_item(&pane, "D", false, cx);
3614 add_labeled_item(&pane, "E", false, cx);
3615 assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
3616
3617 pane.update(cx, |pane, cx| {
3618 pane.close_clean_items(
3619 &CloseCleanItems {
3620 close_pinned: false,
3621 },
3622 cx,
3623 )
3624 })
3625 .unwrap()
3626 .await
3627 .unwrap();
3628 assert_item_labels(&pane, ["A^", "C*^"], cx);
3629 }
3630
3631 #[gpui::test]
3632 async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
3633 init_test(cx);
3634 let fs = FakeFs::new(cx.executor());
3635
3636 let project = Project::test(fs, None, cx).await;
3637 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3638 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3639
3640 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3641
3642 pane.update(cx, |pane, cx| {
3643 pane.close_items_to_the_left(
3644 &CloseItemsToTheLeft {
3645 close_pinned: false,
3646 },
3647 cx,
3648 )
3649 })
3650 .unwrap()
3651 .await
3652 .unwrap();
3653 assert_item_labels(&pane, ["C*", "D", "E"], cx);
3654 }
3655
3656 #[gpui::test]
3657 async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
3658 init_test(cx);
3659 let fs = FakeFs::new(cx.executor());
3660
3661 let project = Project::test(fs, None, cx).await;
3662 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3663 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3664
3665 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3666
3667 pane.update(cx, |pane, cx| {
3668 pane.close_items_to_the_right(
3669 &CloseItemsToTheRight {
3670 close_pinned: false,
3671 },
3672 cx,
3673 )
3674 })
3675 .unwrap()
3676 .await
3677 .unwrap();
3678 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3679 }
3680
3681 #[gpui::test]
3682 async fn test_close_all_items(cx: &mut TestAppContext) {
3683 init_test(cx);
3684 let fs = FakeFs::new(cx.executor());
3685
3686 let project = Project::test(fs, None, cx).await;
3687 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3688 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3689
3690 let item_a = add_labeled_item(&pane, "A", false, cx);
3691 add_labeled_item(&pane, "B", false, cx);
3692 add_labeled_item(&pane, "C", false, cx);
3693 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3694
3695 pane.update(cx, |pane, cx| {
3696 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
3697 pane.pin_tab_at(ix, cx);
3698 pane.close_all_items(
3699 &CloseAllItems {
3700 save_intent: None,
3701 close_pinned: false,
3702 },
3703 cx,
3704 )
3705 })
3706 .unwrap()
3707 .await
3708 .unwrap();
3709 assert_item_labels(&pane, ["A*"], cx);
3710
3711 pane.update(cx, |pane, cx| {
3712 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
3713 pane.unpin_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
3726 assert_item_labels(&pane, [], cx);
3727
3728 add_labeled_item(&pane, "A", true, cx);
3729 add_labeled_item(&pane, "B", true, cx);
3730 add_labeled_item(&pane, "C", true, cx);
3731 assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
3732
3733 let save = pane
3734 .update(cx, |pane, cx| {
3735 pane.close_all_items(
3736 &CloseAllItems {
3737 save_intent: None,
3738 close_pinned: false,
3739 },
3740 cx,
3741 )
3742 })
3743 .unwrap();
3744
3745 cx.executor().run_until_parked();
3746 cx.simulate_prompt_answer(2);
3747 save.await.unwrap();
3748 assert_item_labels(&pane, [], cx);
3749 }
3750
3751 #[gpui::test]
3752 async fn test_close_all_items_including_pinned(cx: &mut TestAppContext) {
3753 init_test(cx);
3754 let fs = FakeFs::new(cx.executor());
3755
3756 let project = Project::test(fs, None, cx).await;
3757 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3758 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3759
3760 let item_a = add_labeled_item(&pane, "A", false, cx);
3761 add_labeled_item(&pane, "B", false, cx);
3762 add_labeled_item(&pane, "C", false, cx);
3763 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3764
3765 pane.update(cx, |pane, cx| {
3766 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
3767 pane.pin_tab_at(ix, cx);
3768 pane.close_all_items(
3769 &CloseAllItems {
3770 save_intent: None,
3771 close_pinned: true,
3772 },
3773 cx,
3774 )
3775 })
3776 .unwrap()
3777 .await
3778 .unwrap();
3779 assert_item_labels(&pane, [], cx);
3780 }
3781
3782 fn init_test(cx: &mut TestAppContext) {
3783 cx.update(|cx| {
3784 let settings_store = SettingsStore::test(cx);
3785 cx.set_global(settings_store);
3786 theme::init(LoadThemes::JustBase, cx);
3787 crate::init_settings(cx);
3788 Project::init_settings(cx);
3789 });
3790 }
3791
3792 fn add_labeled_item(
3793 pane: &View<Pane>,
3794 label: &str,
3795 is_dirty: bool,
3796 cx: &mut VisualTestContext,
3797 ) -> Box<View<TestItem>> {
3798 pane.update(cx, |pane, cx| {
3799 let labeled_item = Box::new(
3800 cx.new_view(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)),
3801 );
3802 pane.add_item(labeled_item.clone(), false, false, None, cx);
3803 labeled_item
3804 })
3805 }
3806
3807 fn set_labeled_items<const COUNT: usize>(
3808 pane: &View<Pane>,
3809 labels: [&str; COUNT],
3810 cx: &mut VisualTestContext,
3811 ) -> [Box<View<TestItem>>; COUNT] {
3812 pane.update(cx, |pane, cx| {
3813 pane.items.clear();
3814 let mut active_item_index = 0;
3815
3816 let mut index = 0;
3817 let items = labels.map(|mut label| {
3818 if label.ends_with('*') {
3819 label = label.trim_end_matches('*');
3820 active_item_index = index;
3821 }
3822
3823 let labeled_item = Box::new(cx.new_view(|cx| TestItem::new(cx).with_label(label)));
3824 pane.add_item(labeled_item.clone(), false, false, None, cx);
3825 index += 1;
3826 labeled_item
3827 });
3828
3829 pane.activate_item(active_item_index, false, false, cx);
3830
3831 items
3832 })
3833 }
3834
3835 // Assert the item label, with the active item label suffixed with a '*'
3836 fn assert_item_labels<const COUNT: usize>(
3837 pane: &View<Pane>,
3838 expected_states: [&str; COUNT],
3839 cx: &mut VisualTestContext,
3840 ) {
3841 pane.update(cx, |pane, cx| {
3842 let actual_states = pane
3843 .items
3844 .iter()
3845 .enumerate()
3846 .map(|(ix, item)| {
3847 let mut state = item
3848 .to_any()
3849 .downcast::<TestItem>()
3850 .unwrap()
3851 .read(cx)
3852 .label
3853 .clone();
3854 if ix == pane.active_item_index {
3855 state.push('*');
3856 }
3857 if item.is_dirty(cx) {
3858 state.push('^');
3859 }
3860 state
3861 })
3862 .collect::<Vec<_>>();
3863
3864 assert_eq!(
3865 actual_states, expected_states,
3866 "pane items do not match expectation"
3867 );
3868 })
3869 }
3870}