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_project_entry_drop(&selection.active_selection.entry_id, 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_project_entry_drop(
2174 &mut self,
2175 project_entry_id: &ProjectEntryId,
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, project_entry_id, cx) {
2180 return;
2181 }
2182 }
2183 let mut to_pane = cx.view().clone();
2184 let split_direction = self.drag_split_direction;
2185 let project_entry_id = *project_entry_id;
2186 self.workspace
2187 .update(cx, |_, cx| {
2188 cx.defer(move |workspace, cx| {
2189 if let Some(path) = workspace
2190 .project()
2191 .read(cx)
2192 .path_for_entry(project_entry_id, cx)
2193 {
2194 let load_path_task = workspace.load_path(path, cx);
2195 cx.spawn(|workspace, mut cx| async move {
2196 if let Some((project_entry_id, build_item)) =
2197 load_path_task.await.notify_async_err(&mut cx)
2198 {
2199 workspace
2200 .update(&mut cx, |workspace, cx| {
2201 if let Some(split_direction) = split_direction {
2202 to_pane =
2203 workspace.split_pane(to_pane, split_direction, cx);
2204 }
2205 to_pane.update(cx, |pane, cx| {
2206 pane.open_item(
2207 project_entry_id,
2208 true,
2209 false,
2210 cx,
2211 build_item,
2212 )
2213 })
2214 })
2215 .log_err();
2216 }
2217 })
2218 .detach();
2219 };
2220 });
2221 })
2222 .log_err();
2223 }
2224
2225 fn handle_external_paths_drop(
2226 &mut self,
2227 paths: &ExternalPaths,
2228 cx: &mut ViewContext<'_, Self>,
2229 ) {
2230 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2231 if let ControlFlow::Break(()) = custom_drop_handle(self, paths, cx) {
2232 return;
2233 }
2234 }
2235 let mut to_pane = cx.view().clone();
2236 let mut split_direction = self.drag_split_direction;
2237 let paths = paths.paths().to_vec();
2238 let is_remote = self
2239 .workspace
2240 .update(cx, |workspace, cx| {
2241 if workspace.project().read(cx).is_via_collab() {
2242 workspace.show_error(
2243 &anyhow::anyhow!("Cannot drop files on a remote project"),
2244 cx,
2245 );
2246 true
2247 } else {
2248 false
2249 }
2250 })
2251 .unwrap_or(true);
2252 if is_remote {
2253 return;
2254 }
2255
2256 self.workspace
2257 .update(cx, |workspace, cx| {
2258 let fs = Arc::clone(workspace.project().read(cx).fs());
2259 cx.spawn(|workspace, mut cx| async move {
2260 let mut is_file_checks = FuturesUnordered::new();
2261 for path in &paths {
2262 is_file_checks.push(fs.is_file(path))
2263 }
2264 let mut has_files_to_open = false;
2265 while let Some(is_file) = is_file_checks.next().await {
2266 if is_file {
2267 has_files_to_open = true;
2268 break;
2269 }
2270 }
2271 drop(is_file_checks);
2272 if !has_files_to_open {
2273 split_direction = None;
2274 }
2275
2276 if let Some(open_task) = workspace
2277 .update(&mut cx, |workspace, cx| {
2278 if let Some(split_direction) = split_direction {
2279 to_pane = workspace.split_pane(to_pane, split_direction, cx);
2280 }
2281 workspace.open_paths(
2282 paths,
2283 OpenVisible::OnlyDirectories,
2284 Some(to_pane.downgrade()),
2285 cx,
2286 )
2287 })
2288 .ok()
2289 {
2290 let opened_items: Vec<_> = open_task.await;
2291 _ = workspace.update(&mut cx, |workspace, cx| {
2292 for item in opened_items.into_iter().flatten() {
2293 if let Err(e) = item {
2294 workspace.show_error(&e, cx);
2295 }
2296 }
2297 });
2298 }
2299 })
2300 .detach();
2301 })
2302 .log_err();
2303 }
2304
2305 pub fn display_nav_history_buttons(&mut self, display: Option<bool>) {
2306 self.display_nav_history_buttons = display;
2307 }
2308}
2309
2310impl FocusableView for Pane {
2311 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
2312 self.focus_handle.clone()
2313 }
2314}
2315
2316impl Render for Pane {
2317 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2318 let mut key_context = KeyContext::new_with_defaults();
2319 key_context.add("Pane");
2320 if self.active_item().is_none() {
2321 key_context.add("EmptyPane");
2322 }
2323
2324 let should_display_tab_bar = self.should_display_tab_bar.clone();
2325 let display_tab_bar = should_display_tab_bar(cx);
2326
2327 v_flex()
2328 .key_context(key_context)
2329 .track_focus(&self.focus_handle)
2330 .size_full()
2331 .flex_none()
2332 .overflow_hidden()
2333 .on_action(cx.listener(|pane, _: &AlternateFile, cx| {
2334 pane.alternate_file(cx);
2335 }))
2336 .on_action(cx.listener(|pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx)))
2337 .on_action(cx.listener(|pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx)))
2338 .on_action(cx.listener(|pane, _: &SplitHorizontal, cx| {
2339 pane.split(SplitDirection::horizontal(cx), cx)
2340 }))
2341 .on_action(cx.listener(|pane, _: &SplitVertical, cx| {
2342 pane.split(SplitDirection::vertical(cx), cx)
2343 }))
2344 .on_action(
2345 cx.listener(|pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx)),
2346 )
2347 .on_action(cx.listener(|pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx)))
2348 .on_action(cx.listener(|pane, _: &GoBack, cx| pane.navigate_backward(cx)))
2349 .on_action(cx.listener(|pane, _: &GoForward, cx| pane.navigate_forward(cx)))
2350 .on_action(cx.listener(|pane, _: &JoinIntoNext, cx| pane.join_into_next(cx)))
2351 .on_action(cx.listener(Pane::toggle_zoom))
2352 .on_action(cx.listener(|pane: &mut Pane, action: &ActivateItem, cx| {
2353 pane.activate_item(action.0, true, true, cx);
2354 }))
2355 .on_action(cx.listener(|pane: &mut Pane, _: &ActivateLastItem, cx| {
2356 pane.activate_item(pane.items.len() - 1, true, true, cx);
2357 }))
2358 .on_action(cx.listener(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
2359 pane.activate_prev_item(true, cx);
2360 }))
2361 .on_action(cx.listener(|pane: &mut Pane, _: &ActivateNextItem, cx| {
2362 pane.activate_next_item(true, cx);
2363 }))
2364 .when(PreviewTabsSettings::get_global(cx).enabled, |this| {
2365 this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, cx| {
2366 if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) {
2367 if pane.is_active_preview_item(active_item_id) {
2368 pane.set_preview_item_id(None, cx);
2369 } else {
2370 pane.set_preview_item_id(Some(active_item_id), cx);
2371 }
2372 }
2373 }))
2374 })
2375 .on_action(
2376 cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
2377 if let Some(task) = pane.close_active_item(action, cx) {
2378 task.detach_and_log_err(cx)
2379 }
2380 }),
2381 )
2382 .on_action(
2383 cx.listener(|pane: &mut Self, action: &CloseInactiveItems, cx| {
2384 if let Some(task) = pane.close_inactive_items(action, cx) {
2385 task.detach_and_log_err(cx)
2386 }
2387 }),
2388 )
2389 .on_action(
2390 cx.listener(|pane: &mut Self, action: &CloseCleanItems, cx| {
2391 if let Some(task) = pane.close_clean_items(action, cx) {
2392 task.detach_and_log_err(cx)
2393 }
2394 }),
2395 )
2396 .on_action(
2397 cx.listener(|pane: &mut Self, action: &CloseItemsToTheLeft, cx| {
2398 if let Some(task) = pane.close_items_to_the_left(action, cx) {
2399 task.detach_and_log_err(cx)
2400 }
2401 }),
2402 )
2403 .on_action(
2404 cx.listener(|pane: &mut Self, action: &CloseItemsToTheRight, cx| {
2405 if let Some(task) = pane.close_items_to_the_right(action, cx) {
2406 task.detach_and_log_err(cx)
2407 }
2408 }),
2409 )
2410 .on_action(cx.listener(|pane: &mut Self, action: &CloseAllItems, cx| {
2411 if let Some(task) = pane.close_all_items(action, cx) {
2412 task.detach_and_log_err(cx)
2413 }
2414 }))
2415 .on_action(
2416 cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
2417 if let Some(task) = pane.close_active_item(action, cx) {
2418 task.detach_and_log_err(cx)
2419 }
2420 }),
2421 )
2422 .on_action(
2423 cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, cx| {
2424 let entry_id = action
2425 .entry_id
2426 .map(ProjectEntryId::from_proto)
2427 .or_else(|| pane.active_item()?.project_entry_ids(cx).first().copied());
2428 if let Some(entry_id) = entry_id {
2429 pane.project.update(cx, |_, cx| {
2430 cx.emit(project::Event::RevealInProjectPanel(entry_id))
2431 });
2432 }
2433 }),
2434 )
2435 .when(self.active_item().is_some() && display_tab_bar, |pane| {
2436 pane.child(self.render_tab_bar(cx))
2437 })
2438 .child({
2439 let has_worktrees = self.project.read(cx).worktrees(cx).next().is_some();
2440 // main content
2441 div()
2442 .flex_1()
2443 .relative()
2444 .group("")
2445 .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
2446 .on_drag_move::<DraggedSelection>(cx.listener(Self::handle_drag_move))
2447 .on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
2448 .map(|div| {
2449 if let Some(item) = self.active_item() {
2450 div.v_flex()
2451 .child(self.toolbar.clone())
2452 .child(item.to_any())
2453 } else {
2454 let placeholder = div.h_flex().size_full().justify_center();
2455 if has_worktrees {
2456 placeholder
2457 } else {
2458 placeholder.child(
2459 Label::new("Open a file or project to get started.")
2460 .color(Color::Muted),
2461 )
2462 }
2463 }
2464 })
2465 .child(
2466 // drag target
2467 div()
2468 .invisible()
2469 .absolute()
2470 .bg(cx.theme().colors().drop_target_background)
2471 .group_drag_over::<DraggedTab>("", |style| style.visible())
2472 .group_drag_over::<DraggedSelection>("", |style| style.visible())
2473 .group_drag_over::<ExternalPaths>("", |style| style.visible())
2474 .when_some(self.can_drop_predicate.clone(), |this, p| {
2475 this.can_drop(move |a, cx| p(a, cx))
2476 })
2477 .on_drop(cx.listener(move |this, dragged_tab, cx| {
2478 this.handle_tab_drop(dragged_tab, this.active_item_index(), cx)
2479 }))
2480 .on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
2481 this.handle_project_entry_drop(
2482 &selection.active_selection.entry_id,
2483 cx,
2484 )
2485 }))
2486 .on_drop(cx.listener(move |this, paths, cx| {
2487 this.handle_external_paths_drop(paths, cx)
2488 }))
2489 .map(|div| {
2490 let size = DefiniteLength::Fraction(0.5);
2491 match self.drag_split_direction {
2492 None => div.top_0().right_0().bottom_0().left_0(),
2493 Some(SplitDirection::Up) => {
2494 div.top_0().left_0().right_0().h(size)
2495 }
2496 Some(SplitDirection::Down) => {
2497 div.left_0().bottom_0().right_0().h(size)
2498 }
2499 Some(SplitDirection::Left) => {
2500 div.top_0().left_0().bottom_0().w(size)
2501 }
2502 Some(SplitDirection::Right) => {
2503 div.top_0().bottom_0().right_0().w(size)
2504 }
2505 }
2506 }),
2507 )
2508 })
2509 .on_mouse_down(
2510 MouseButton::Navigate(NavigationDirection::Back),
2511 cx.listener(|pane, _, cx| {
2512 if let Some(workspace) = pane.workspace.upgrade() {
2513 let pane = cx.view().downgrade();
2514 cx.window_context().defer(move |cx| {
2515 workspace.update(cx, |workspace, cx| {
2516 workspace.go_back(pane, cx).detach_and_log_err(cx)
2517 })
2518 })
2519 }
2520 }),
2521 )
2522 .on_mouse_down(
2523 MouseButton::Navigate(NavigationDirection::Forward),
2524 cx.listener(|pane, _, cx| {
2525 if let Some(workspace) = pane.workspace.upgrade() {
2526 let pane = cx.view().downgrade();
2527 cx.window_context().defer(move |cx| {
2528 workspace.update(cx, |workspace, cx| {
2529 workspace.go_forward(pane, cx).detach_and_log_err(cx)
2530 })
2531 })
2532 }
2533 }),
2534 )
2535 }
2536}
2537
2538impl ItemNavHistory {
2539 pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut WindowContext) {
2540 self.history
2541 .push(data, self.item.clone(), self.is_preview, cx);
2542 }
2543
2544 pub fn pop_backward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
2545 self.history.pop(NavigationMode::GoingBack, cx)
2546 }
2547
2548 pub fn pop_forward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
2549 self.history.pop(NavigationMode::GoingForward, cx)
2550 }
2551}
2552
2553impl NavHistory {
2554 pub fn for_each_entry(
2555 &self,
2556 cx: &AppContext,
2557 mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
2558 ) {
2559 let borrowed_history = self.0.lock();
2560 borrowed_history
2561 .forward_stack
2562 .iter()
2563 .chain(borrowed_history.backward_stack.iter())
2564 .chain(borrowed_history.closed_stack.iter())
2565 .for_each(|entry| {
2566 if let Some(project_and_abs_path) =
2567 borrowed_history.paths_by_item.get(&entry.item.id())
2568 {
2569 f(entry, project_and_abs_path.clone());
2570 } else if let Some(item) = entry.item.upgrade() {
2571 if let Some(path) = item.project_path(cx) {
2572 f(entry, (path, None));
2573 }
2574 }
2575 })
2576 }
2577
2578 pub fn set_mode(&mut self, mode: NavigationMode) {
2579 self.0.lock().mode = mode;
2580 }
2581
2582 pub fn mode(&self) -> NavigationMode {
2583 self.0.lock().mode
2584 }
2585
2586 pub fn disable(&mut self) {
2587 self.0.lock().mode = NavigationMode::Disabled;
2588 }
2589
2590 pub fn enable(&mut self) {
2591 self.0.lock().mode = NavigationMode::Normal;
2592 }
2593
2594 pub fn pop(&mut self, mode: NavigationMode, cx: &mut WindowContext) -> Option<NavigationEntry> {
2595 let mut state = self.0.lock();
2596 let entry = match mode {
2597 NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
2598 return None
2599 }
2600 NavigationMode::GoingBack => &mut state.backward_stack,
2601 NavigationMode::GoingForward => &mut state.forward_stack,
2602 NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
2603 }
2604 .pop_back();
2605 if entry.is_some() {
2606 state.did_update(cx);
2607 }
2608 entry
2609 }
2610
2611 pub fn push<D: 'static + Send + Any>(
2612 &mut self,
2613 data: Option<D>,
2614 item: Arc<dyn WeakItemHandle>,
2615 is_preview: bool,
2616 cx: &mut WindowContext,
2617 ) {
2618 let state = &mut *self.0.lock();
2619 match state.mode {
2620 NavigationMode::Disabled => {}
2621 NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
2622 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2623 state.backward_stack.pop_front();
2624 }
2625 state.backward_stack.push_back(NavigationEntry {
2626 item,
2627 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2628 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2629 is_preview,
2630 });
2631 state.forward_stack.clear();
2632 }
2633 NavigationMode::GoingBack => {
2634 if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2635 state.forward_stack.pop_front();
2636 }
2637 state.forward_stack.push_back(NavigationEntry {
2638 item,
2639 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2640 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2641 is_preview,
2642 });
2643 }
2644 NavigationMode::GoingForward => {
2645 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2646 state.backward_stack.pop_front();
2647 }
2648 state.backward_stack.push_back(NavigationEntry {
2649 item,
2650 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2651 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2652 is_preview,
2653 });
2654 }
2655 NavigationMode::ClosingItem => {
2656 if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2657 state.closed_stack.pop_front();
2658 }
2659 state.closed_stack.push_back(NavigationEntry {
2660 item,
2661 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2662 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2663 is_preview,
2664 });
2665 }
2666 }
2667 state.did_update(cx);
2668 }
2669
2670 pub fn remove_item(&mut self, item_id: EntityId) {
2671 let mut state = self.0.lock();
2672 state.paths_by_item.remove(&item_id);
2673 state
2674 .backward_stack
2675 .retain(|entry| entry.item.id() != item_id);
2676 state
2677 .forward_stack
2678 .retain(|entry| entry.item.id() != item_id);
2679 state
2680 .closed_stack
2681 .retain(|entry| entry.item.id() != item_id);
2682 }
2683
2684 pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
2685 self.0.lock().paths_by_item.get(&item_id).cloned()
2686 }
2687}
2688
2689impl NavHistoryState {
2690 pub fn did_update(&self, cx: &mut WindowContext) {
2691 if let Some(pane) = self.pane.upgrade() {
2692 cx.defer(move |cx| {
2693 pane.update(cx, |pane, cx| pane.history_updated(cx));
2694 });
2695 }
2696 }
2697}
2698
2699fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
2700 let path = buffer_path
2701 .as_ref()
2702 .and_then(|p| {
2703 p.path
2704 .to_str()
2705 .and_then(|s| if s == "" { None } else { Some(s) })
2706 })
2707 .unwrap_or("This buffer");
2708 let path = truncate_and_remove_front(path, 80);
2709 format!("{path} contains unsaved edits. Do you want to save it?")
2710}
2711
2712pub fn tab_details(items: &Vec<Box<dyn ItemHandle>>, cx: &AppContext) -> Vec<usize> {
2713 let mut tab_details = items.iter().map(|_| 0).collect::<Vec<_>>();
2714 let mut tab_descriptions = HashMap::default();
2715 let mut done = false;
2716 while !done {
2717 done = true;
2718
2719 // Store item indices by their tab description.
2720 for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() {
2721 if let Some(description) = item.tab_description(*detail, cx) {
2722 if *detail == 0
2723 || Some(&description) != item.tab_description(detail - 1, cx).as_ref()
2724 {
2725 tab_descriptions
2726 .entry(description)
2727 .or_insert(Vec::new())
2728 .push(ix);
2729 }
2730 }
2731 }
2732
2733 // If two or more items have the same tab description, increase their level
2734 // of detail and try again.
2735 for (_, item_ixs) in tab_descriptions.drain() {
2736 if item_ixs.len() > 1 {
2737 done = false;
2738 for ix in item_ixs {
2739 tab_details[ix] += 1;
2740 }
2741 }
2742 }
2743 }
2744
2745 tab_details
2746}
2747
2748pub fn render_item_indicator(item: Box<dyn ItemHandle>, cx: &WindowContext) -> Option<Indicator> {
2749 maybe!({
2750 let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
2751 (true, _) => Color::Warning,
2752 (_, true) => Color::Accent,
2753 (false, false) => return None,
2754 };
2755
2756 Some(Indicator::dot().color(indicator_color))
2757 })
2758}
2759
2760#[cfg(test)]
2761mod tests {
2762 use super::*;
2763 use crate::item::test::{TestItem, TestProjectItem};
2764 use gpui::{TestAppContext, VisualTestContext};
2765 use project::FakeFs;
2766 use settings::SettingsStore;
2767 use theme::LoadThemes;
2768
2769 #[gpui::test]
2770 async fn test_remove_active_empty(cx: &mut TestAppContext) {
2771 init_test(cx);
2772 let fs = FakeFs::new(cx.executor());
2773
2774 let project = Project::test(fs, None, cx).await;
2775 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2776 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2777
2778 pane.update(cx, |pane, cx| {
2779 assert!(pane
2780 .close_active_item(&CloseActiveItem { save_intent: None }, cx)
2781 .is_none())
2782 });
2783 }
2784
2785 #[gpui::test]
2786 async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
2787 init_test(cx);
2788 let fs = FakeFs::new(cx.executor());
2789
2790 let project = Project::test(fs, None, cx).await;
2791 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2792 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2793
2794 // 1. Add with a destination index
2795 // a. Add before the active item
2796 set_labeled_items(&pane, ["A", "B*", "C"], cx);
2797 pane.update(cx, |pane, cx| {
2798 pane.add_item(
2799 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2800 false,
2801 false,
2802 Some(0),
2803 cx,
2804 );
2805 });
2806 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
2807
2808 // b. Add after the active item
2809 set_labeled_items(&pane, ["A", "B*", "C"], cx);
2810 pane.update(cx, |pane, cx| {
2811 pane.add_item(
2812 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2813 false,
2814 false,
2815 Some(2),
2816 cx,
2817 );
2818 });
2819 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
2820
2821 // c. Add at the end of the item list (including off the length)
2822 set_labeled_items(&pane, ["A", "B*", "C"], cx);
2823 pane.update(cx, |pane, cx| {
2824 pane.add_item(
2825 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2826 false,
2827 false,
2828 Some(5),
2829 cx,
2830 );
2831 });
2832 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2833
2834 // 2. Add without a destination index
2835 // a. Add with active item at the start of the item list
2836 set_labeled_items(&pane, ["A*", "B", "C"], cx);
2837 pane.update(cx, |pane, cx| {
2838 pane.add_item(
2839 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2840 false,
2841 false,
2842 None,
2843 cx,
2844 );
2845 });
2846 set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
2847
2848 // b. Add with active item at the end of the item list
2849 set_labeled_items(&pane, ["A", "B", "C*"], cx);
2850 pane.update(cx, |pane, cx| {
2851 pane.add_item(
2852 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2853 false,
2854 false,
2855 None,
2856 cx,
2857 );
2858 });
2859 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2860 }
2861
2862 #[gpui::test]
2863 async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
2864 init_test(cx);
2865 let fs = FakeFs::new(cx.executor());
2866
2867 let project = Project::test(fs, None, cx).await;
2868 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2869 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2870
2871 // 1. Add with a destination index
2872 // 1a. Add before the active item
2873 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2874 pane.update(cx, |pane, cx| {
2875 pane.add_item(d, false, false, Some(0), cx);
2876 });
2877 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
2878
2879 // 1b. Add after the active item
2880 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2881 pane.update(cx, |pane, cx| {
2882 pane.add_item(d, false, false, Some(2), cx);
2883 });
2884 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
2885
2886 // 1c. Add at the end of the item list (including off the length)
2887 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2888 pane.update(cx, |pane, cx| {
2889 pane.add_item(a, false, false, Some(5), cx);
2890 });
2891 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
2892
2893 // 1d. Add same item to active index
2894 let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
2895 pane.update(cx, |pane, cx| {
2896 pane.add_item(b, false, false, Some(1), cx);
2897 });
2898 assert_item_labels(&pane, ["A", "B*", "C"], cx);
2899
2900 // 1e. Add item to index after same item in last position
2901 let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
2902 pane.update(cx, |pane, cx| {
2903 pane.add_item(c, false, false, Some(2), cx);
2904 });
2905 assert_item_labels(&pane, ["A", "B", "C*"], cx);
2906
2907 // 2. Add without a destination index
2908 // 2a. Add with active item at the start of the item list
2909 let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
2910 pane.update(cx, |pane, cx| {
2911 pane.add_item(d, false, false, None, cx);
2912 });
2913 assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
2914
2915 // 2b. Add with active item at the end of the item list
2916 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
2917 pane.update(cx, |pane, cx| {
2918 pane.add_item(a, false, false, None, cx);
2919 });
2920 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
2921
2922 // 2c. Add active item to active item at end of list
2923 let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
2924 pane.update(cx, |pane, cx| {
2925 pane.add_item(c, false, false, None, cx);
2926 });
2927 assert_item_labels(&pane, ["A", "B", "C*"], cx);
2928
2929 // 2d. Add active item to active item at start of list
2930 let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
2931 pane.update(cx, |pane, cx| {
2932 pane.add_item(a, false, false, None, cx);
2933 });
2934 assert_item_labels(&pane, ["A*", "B", "C"], cx);
2935 }
2936
2937 #[gpui::test]
2938 async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
2939 init_test(cx);
2940 let fs = FakeFs::new(cx.executor());
2941
2942 let project = Project::test(fs, None, cx).await;
2943 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2944 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2945
2946 // singleton view
2947 pane.update(cx, |pane, cx| {
2948 pane.add_item(
2949 Box::new(cx.new_view(|cx| {
2950 TestItem::new(cx)
2951 .with_singleton(true)
2952 .with_label("buffer 1")
2953 .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
2954 })),
2955 false,
2956 false,
2957 None,
2958 cx,
2959 );
2960 });
2961 assert_item_labels(&pane, ["buffer 1*"], cx);
2962
2963 // new singleton view with the same project entry
2964 pane.update(cx, |pane, cx| {
2965 pane.add_item(
2966 Box::new(cx.new_view(|cx| {
2967 TestItem::new(cx)
2968 .with_singleton(true)
2969 .with_label("buffer 1")
2970 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
2971 })),
2972 false,
2973 false,
2974 None,
2975 cx,
2976 );
2977 });
2978 assert_item_labels(&pane, ["buffer 1*"], cx);
2979
2980 // new singleton view with different project entry
2981 pane.update(cx, |pane, cx| {
2982 pane.add_item(
2983 Box::new(cx.new_view(|cx| {
2984 TestItem::new(cx)
2985 .with_singleton(true)
2986 .with_label("buffer 2")
2987 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
2988 })),
2989 false,
2990 false,
2991 None,
2992 cx,
2993 );
2994 });
2995 assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
2996
2997 // new multibuffer view with the same project entry
2998 pane.update(cx, |pane, cx| {
2999 pane.add_item(
3000 Box::new(cx.new_view(|cx| {
3001 TestItem::new(cx)
3002 .with_singleton(false)
3003 .with_label("multibuffer 1")
3004 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3005 })),
3006 false,
3007 false,
3008 None,
3009 cx,
3010 );
3011 });
3012 assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
3013
3014 // another multibuffer view with the same project entry
3015 pane.update(cx, |pane, cx| {
3016 pane.add_item(
3017 Box::new(cx.new_view(|cx| {
3018 TestItem::new(cx)
3019 .with_singleton(false)
3020 .with_label("multibuffer 1b")
3021 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3022 })),
3023 false,
3024 false,
3025 None,
3026 cx,
3027 );
3028 });
3029 assert_item_labels(
3030 &pane,
3031 ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
3032 cx,
3033 );
3034 }
3035
3036 #[gpui::test]
3037 async fn test_remove_item_ordering(cx: &mut TestAppContext) {
3038 init_test(cx);
3039 let fs = FakeFs::new(cx.executor());
3040
3041 let project = Project::test(fs, None, cx).await;
3042 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3043 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3044
3045 add_labeled_item(&pane, "A", false, cx);
3046 add_labeled_item(&pane, "B", false, cx);
3047 add_labeled_item(&pane, "C", false, cx);
3048 add_labeled_item(&pane, "D", false, cx);
3049 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3050
3051 pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
3052 add_labeled_item(&pane, "1", false, cx);
3053 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
3054
3055 pane.update(cx, |pane, cx| {
3056 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3057 })
3058 .unwrap()
3059 .await
3060 .unwrap();
3061 assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
3062
3063 pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
3064 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3065
3066 pane.update(cx, |pane, cx| {
3067 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3068 })
3069 .unwrap()
3070 .await
3071 .unwrap();
3072 assert_item_labels(&pane, ["A", "B*", "C"], cx);
3073
3074 pane.update(cx, |pane, cx| {
3075 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3076 })
3077 .unwrap()
3078 .await
3079 .unwrap();
3080 assert_item_labels(&pane, ["A", "C*"], cx);
3081
3082 pane.update(cx, |pane, cx| {
3083 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
3084 })
3085 .unwrap()
3086 .await
3087 .unwrap();
3088 assert_item_labels(&pane, ["A*"], cx);
3089 }
3090
3091 #[gpui::test]
3092 async fn test_close_inactive_items(cx: &mut TestAppContext) {
3093 init_test(cx);
3094 let fs = FakeFs::new(cx.executor());
3095
3096 let project = Project::test(fs, None, cx).await;
3097 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3098 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3099
3100 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3101
3102 pane.update(cx, |pane, cx| {
3103 pane.close_inactive_items(&CloseInactiveItems { save_intent: None }, cx)
3104 })
3105 .unwrap()
3106 .await
3107 .unwrap();
3108 assert_item_labels(&pane, ["C*"], cx);
3109 }
3110
3111 #[gpui::test]
3112 async fn test_close_clean_items(cx: &mut TestAppContext) {
3113 init_test(cx);
3114 let fs = FakeFs::new(cx.executor());
3115
3116 let project = Project::test(fs, None, cx).await;
3117 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3118 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3119
3120 add_labeled_item(&pane, "A", true, cx);
3121 add_labeled_item(&pane, "B", false, cx);
3122 add_labeled_item(&pane, "C", true, cx);
3123 add_labeled_item(&pane, "D", false, cx);
3124 add_labeled_item(&pane, "E", false, cx);
3125 assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
3126
3127 pane.update(cx, |pane, cx| pane.close_clean_items(&CloseCleanItems, cx))
3128 .unwrap()
3129 .await
3130 .unwrap();
3131 assert_item_labels(&pane, ["A^", "C*^"], cx);
3132 }
3133
3134 #[gpui::test]
3135 async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
3136 init_test(cx);
3137 let fs = FakeFs::new(cx.executor());
3138
3139 let project = Project::test(fs, None, cx).await;
3140 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3141 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3142
3143 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3144
3145 pane.update(cx, |pane, cx| {
3146 pane.close_items_to_the_left(&CloseItemsToTheLeft, cx)
3147 })
3148 .unwrap()
3149 .await
3150 .unwrap();
3151 assert_item_labels(&pane, ["C*", "D", "E"], cx);
3152 }
3153
3154 #[gpui::test]
3155 async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
3156 init_test(cx);
3157 let fs = FakeFs::new(cx.executor());
3158
3159 let project = Project::test(fs, None, cx).await;
3160 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3161 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3162
3163 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
3164
3165 pane.update(cx, |pane, cx| {
3166 pane.close_items_to_the_right(&CloseItemsToTheRight, cx)
3167 })
3168 .unwrap()
3169 .await
3170 .unwrap();
3171 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3172 }
3173
3174 #[gpui::test]
3175 async fn test_close_all_items(cx: &mut TestAppContext) {
3176 init_test(cx);
3177 let fs = FakeFs::new(cx.executor());
3178
3179 let project = Project::test(fs, None, cx).await;
3180 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
3181 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
3182
3183 add_labeled_item(&pane, "A", false, cx);
3184 add_labeled_item(&pane, "B", false, cx);
3185 add_labeled_item(&pane, "C", false, cx);
3186 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3187
3188 pane.update(cx, |pane, cx| {
3189 pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
3190 })
3191 .unwrap()
3192 .await
3193 .unwrap();
3194 assert_item_labels(&pane, [], cx);
3195
3196 add_labeled_item(&pane, "A", true, cx);
3197 add_labeled_item(&pane, "B", true, cx);
3198 add_labeled_item(&pane, "C", true, cx);
3199 assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
3200
3201 let save = pane
3202 .update(cx, |pane, cx| {
3203 pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
3204 })
3205 .unwrap();
3206
3207 cx.executor().run_until_parked();
3208 cx.simulate_prompt_answer(2);
3209 save.await.unwrap();
3210 assert_item_labels(&pane, [], cx);
3211 }
3212
3213 fn init_test(cx: &mut TestAppContext) {
3214 cx.update(|cx| {
3215 let settings_store = SettingsStore::test(cx);
3216 cx.set_global(settings_store);
3217 theme::init(LoadThemes::JustBase, cx);
3218 crate::init_settings(cx);
3219 Project::init_settings(cx);
3220 });
3221 }
3222
3223 fn add_labeled_item(
3224 pane: &View<Pane>,
3225 label: &str,
3226 is_dirty: bool,
3227 cx: &mut VisualTestContext,
3228 ) -> Box<View<TestItem>> {
3229 pane.update(cx, |pane, cx| {
3230 let labeled_item = Box::new(
3231 cx.new_view(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)),
3232 );
3233 pane.add_item(labeled_item.clone(), false, false, None, cx);
3234 labeled_item
3235 })
3236 }
3237
3238 fn set_labeled_items<const COUNT: usize>(
3239 pane: &View<Pane>,
3240 labels: [&str; COUNT],
3241 cx: &mut VisualTestContext,
3242 ) -> [Box<View<TestItem>>; COUNT] {
3243 pane.update(cx, |pane, cx| {
3244 pane.items.clear();
3245 let mut active_item_index = 0;
3246
3247 let mut index = 0;
3248 let items = labels.map(|mut label| {
3249 if label.ends_with('*') {
3250 label = label.trim_end_matches('*');
3251 active_item_index = index;
3252 }
3253
3254 let labeled_item = Box::new(cx.new_view(|cx| TestItem::new(cx).with_label(label)));
3255 pane.add_item(labeled_item.clone(), false, false, None, cx);
3256 index += 1;
3257 labeled_item
3258 });
3259
3260 pane.activate_item(active_item_index, false, false, cx);
3261
3262 items
3263 })
3264 }
3265
3266 // Assert the item label, with the active item label suffixed with a '*'
3267 fn assert_item_labels<const COUNT: usize>(
3268 pane: &View<Pane>,
3269 expected_states: [&str; COUNT],
3270 cx: &mut VisualTestContext,
3271 ) {
3272 pane.update(cx, |pane, cx| {
3273 let actual_states = pane
3274 .items
3275 .iter()
3276 .enumerate()
3277 .map(|(ix, item)| {
3278 let mut state = item
3279 .to_any()
3280 .downcast::<TestItem>()
3281 .unwrap()
3282 .read(cx)
3283 .label
3284 .clone();
3285 if ix == pane.active_item_index {
3286 state.push('*');
3287 }
3288 if item.is_dirty(cx) {
3289 state.push('^');
3290 }
3291 state
3292 })
3293 .collect::<Vec<_>>();
3294
3295 assert_eq!(
3296 actual_states, expected_states,
3297 "pane items do not match expectation"
3298 );
3299 })
3300 }
3301}
3302
3303impl Render for DraggedTab {
3304 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3305 let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
3306 let label = self.item.tab_content(
3307 TabContentParams {
3308 detail: Some(self.detail),
3309 selected: false,
3310 preview: false,
3311 },
3312 cx,
3313 );
3314 Tab::new("")
3315 .selected(self.is_active)
3316 .child(label)
3317 .render(cx)
3318 .font(ui_font)
3319 }
3320}