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