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