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