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