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