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