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