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