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