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