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