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