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