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