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