1use crate::{
2 CloseWindow, NewFile, NewTerminal, OpenInTerminal, OpenOptions, OpenTerminal, OpenVisible,
3 SplitDirection, ToggleFileFinder, ToggleProjectSymbols, ToggleZoom, Workspace,
4 WorkspaceItemBuilder,
5 item::{
6 ActivateOnClose, ClosePosition, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
7 ProjectItemKind, ShowCloseButton, ShowDiagnostics, TabContentParams, TabTooltipContent,
8 WeakItemHandle,
9 },
10 move_item,
11 notifications::NotifyResultExt,
12 toolbar::Toolbar,
13 workspace_settings::{AutosaveSetting, TabBarSettings, WorkspaceSettings},
14};
15use anyhow::Result;
16use collections::{BTreeSet, HashMap, HashSet, VecDeque};
17use futures::{StreamExt, stream::FuturesUnordered};
18use gpui::{
19 Action, AnyElement, App, AsyncWindowContext, ClickEvent, ClipboardItem, Context, Corner, Div,
20 DragMoveEvent, Entity, EntityId, EventEmitter, ExternalPaths, FocusHandle, FocusOutEvent,
21 Focusable, KeyContext, MouseButton, MouseDownEvent, NavigationDirection, Pixels, Point,
22 PromptLevel, Render, ScrollHandle, Subscription, Task, WeakEntity, WeakFocusHandle, Window,
23 actions, anchored, deferred, impl_actions, prelude::*,
24};
25use itertools::Itertools;
26use language::DiagnosticSeverity;
27use parking_lot::Mutex;
28use project::{Project, ProjectEntryId, ProjectPath, WorktreeId};
29use schemars::JsonSchema;
30use serde::Deserialize;
31use settings::{Settings, SettingsStore};
32use std::{
33 any::Any,
34 cmp, fmt, mem,
35 ops::ControlFlow,
36 path::PathBuf,
37 rc::Rc,
38 sync::{
39 Arc,
40 atomic::{AtomicUsize, Ordering},
41 },
42};
43use theme::ThemeSettings;
44use ui::{
45 ButtonSize, Color, ContextMenu, ContextMenuEntry, ContextMenuItem, DecoratedIcon, IconButton,
46 IconButtonShape, IconDecoration, IconDecorationKind, IconName, IconSize, Indicator, Label,
47 PopoverMenu, PopoverMenuHandle, ScrollableHandle, Tab, TabBar, TabPosition, Tooltip,
48 prelude::*, right_click_menu,
49};
50use util::{ResultExt, debug_panic, maybe, truncate_and_remove_front};
51
52/// A selected entry in e.g. project panel.
53#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
54pub struct SelectedEntry {
55 pub worktree_id: WorktreeId,
56 pub entry_id: ProjectEntryId,
57}
58
59/// A group of selected entries from project panel.
60#[derive(Debug)]
61pub struct DraggedSelection {
62 pub active_selection: SelectedEntry,
63 pub marked_selections: Arc<BTreeSet<SelectedEntry>>,
64}
65
66impl DraggedSelection {
67 pub fn items<'a>(&'a self) -> Box<dyn Iterator<Item = &'a SelectedEntry> + 'a> {
68 if self.marked_selections.contains(&self.active_selection) {
69 Box::new(self.marked_selections.iter())
70 } else {
71 Box::new(std::iter::once(&self.active_selection))
72 }
73 }
74}
75
76#[derive(Clone, Copy, PartialEq, Debug, Deserialize, JsonSchema)]
77#[serde(rename_all = "snake_case")]
78pub enum SaveIntent {
79 /// write all files (even if unchanged)
80 /// prompt before overwriting on-disk changes
81 Save,
82 /// same as Save, but without auto formatting
83 SaveWithoutFormat,
84 /// write any files that have local changes
85 /// prompt before overwriting on-disk changes
86 SaveAll,
87 /// always prompt for a new path
88 SaveAs,
89 /// prompt "you have unsaved changes" before writing
90 Close,
91 /// write all dirty files, don't prompt on conflict
92 Overwrite,
93 /// skip all save-related behavior
94 Skip,
95}
96
97#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
98pub struct ActivateItem(pub usize);
99
100#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
101#[serde(deny_unknown_fields)]
102pub struct CloseActiveItem {
103 pub save_intent: Option<SaveIntent>,
104 #[serde(default)]
105 pub close_pinned: bool,
106}
107
108#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
109#[serde(deny_unknown_fields)]
110pub struct CloseInactiveItems {
111 pub save_intent: Option<SaveIntent>,
112 #[serde(default)]
113 pub close_pinned: bool,
114}
115
116#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
117#[serde(deny_unknown_fields)]
118pub struct CloseAllItems {
119 pub save_intent: Option<SaveIntent>,
120 #[serde(default)]
121 pub close_pinned: bool,
122}
123
124#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
125#[serde(deny_unknown_fields)]
126pub struct CloseCleanItems {
127 #[serde(default)]
128 pub close_pinned: bool,
129}
130
131#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
132#[serde(deny_unknown_fields)]
133pub struct CloseItemsToTheRight {
134 #[serde(default)]
135 pub close_pinned: bool,
136}
137
138#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
139#[serde(deny_unknown_fields)]
140pub struct CloseItemsToTheLeft {
141 #[serde(default)]
142 pub close_pinned: bool,
143}
144
145#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
146#[serde(deny_unknown_fields)]
147pub struct RevealInProjectPanel {
148 #[serde(skip)]
149 pub entry_id: Option<u64>,
150}
151
152#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
153#[serde(deny_unknown_fields)]
154pub struct DeploySearch {
155 #[serde(default)]
156 pub replace_enabled: bool,
157 #[serde(default)]
158 pub included_files: Option<String>,
159 #[serde(default)]
160 pub excluded_files: Option<String>,
161}
162
163impl_actions!(
164 pane,
165 [
166 CloseAllItems,
167 CloseActiveItem,
168 CloseCleanItems,
169 CloseItemsToTheLeft,
170 CloseItemsToTheRight,
171 CloseInactiveItems,
172 ActivateItem,
173 RevealInProjectPanel,
174 DeploySearch,
175 ]
176);
177
178actions!(
179 pane,
180 [
181 ActivatePreviousItem,
182 ActivateNextItem,
183 ActivateLastItem,
184 AlternateFile,
185 GoBack,
186 GoForward,
187 JoinIntoNext,
188 JoinAll,
189 ReopenClosedItem,
190 SplitLeft,
191 SplitUp,
192 SplitRight,
193 SplitDown,
194 SplitHorizontal,
195 SplitVertical,
196 SwapItemLeft,
197 SwapItemRight,
198 TogglePreviewTab,
199 TogglePinTab,
200 ]
201);
202
203impl DeploySearch {
204 pub fn find() -> Self {
205 Self {
206 replace_enabled: false,
207 included_files: None,
208 excluded_files: None,
209 }
210 }
211}
212
213const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
214
215pub enum Event {
216 AddItem {
217 item: Box<dyn ItemHandle>,
218 },
219 ActivateItem {
220 local: bool,
221 focus_changed: bool,
222 },
223 Remove {
224 focus_on_pane: Option<Entity<Pane>>,
225 },
226 RemoveItem {
227 idx: usize,
228 },
229 RemovedItem {
230 item: Box<dyn ItemHandle>,
231 },
232 Split(SplitDirection),
233 JoinAll,
234 JoinIntoNext,
235 ChangeItemTitle,
236 Focus,
237 ZoomIn,
238 ZoomOut,
239 UserSavedItem {
240 item: Box<dyn WeakItemHandle>,
241 save_intent: SaveIntent,
242 },
243}
244
245impl fmt::Debug for Event {
246 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247 match self {
248 Event::AddItem { item } => f
249 .debug_struct("AddItem")
250 .field("item", &item.item_id())
251 .finish(),
252 Event::ActivateItem { local, .. } => f
253 .debug_struct("ActivateItem")
254 .field("local", local)
255 .finish(),
256 Event::Remove { .. } => f.write_str("Remove"),
257 Event::RemoveItem { idx } => f.debug_struct("RemoveItem").field("idx", idx).finish(),
258 Event::RemovedItem { item } => f
259 .debug_struct("RemovedItem")
260 .field("item", &item.item_id())
261 .finish(),
262 Event::Split(direction) => f
263 .debug_struct("Split")
264 .field("direction", direction)
265 .finish(),
266 Event::JoinAll => f.write_str("JoinAll"),
267 Event::JoinIntoNext => f.write_str("JoinIntoNext"),
268 Event::ChangeItemTitle => f.write_str("ChangeItemTitle"),
269 Event::Focus => f.write_str("Focus"),
270 Event::ZoomIn => f.write_str("ZoomIn"),
271 Event::ZoomOut => f.write_str("ZoomOut"),
272 Event::UserSavedItem { item, save_intent } => f
273 .debug_struct("UserSavedItem")
274 .field("item", &item.id())
275 .field("save_intent", save_intent)
276 .finish(),
277 }
278 }
279}
280
281/// A container for 0 to many items that are open in the workspace.
282/// Treats all items uniformly via the [`ItemHandle`] trait, whether it's an editor, search results multibuffer, terminal or something else,
283/// responsible for managing item tabs, focus and zoom states and drag and drop features.
284/// Can be split, see `PaneGroup` for more details.
285pub struct Pane {
286 alternate_file_items: (
287 Option<Box<dyn WeakItemHandle>>,
288 Option<Box<dyn WeakItemHandle>>,
289 ),
290 focus_handle: FocusHandle,
291 items: Vec<Box<dyn ItemHandle>>,
292 activation_history: Vec<ActivationHistoryEntry>,
293 next_activation_timestamp: Arc<AtomicUsize>,
294 zoomed: bool,
295 was_focused: bool,
296 active_item_index: usize,
297 preview_item_id: Option<EntityId>,
298 last_focus_handle_by_item: HashMap<EntityId, WeakFocusHandle>,
299 nav_history: NavHistory,
300 toolbar: Entity<Toolbar>,
301 pub(crate) workspace: WeakEntity<Workspace>,
302 project: WeakEntity<Project>,
303 pub drag_split_direction: Option<SplitDirection>,
304 can_drop_predicate: Option<Arc<dyn Fn(&dyn Any, &mut Window, &mut App) -> bool>>,
305 custom_drop_handle: Option<
306 Arc<dyn Fn(&mut Pane, &dyn Any, &mut Window, &mut Context<Pane>) -> ControlFlow<(), ()>>,
307 >,
308 can_split_predicate:
309 Option<Arc<dyn Fn(&mut Self, &dyn Any, &mut Window, &mut Context<Self>) -> bool>>,
310 should_display_tab_bar: Rc<dyn Fn(&Window, &mut Context<Pane>) -> bool>,
311 render_tab_bar_buttons: Rc<
312 dyn Fn(
313 &mut Pane,
314 &mut Window,
315 &mut Context<Pane>,
316 ) -> (Option<AnyElement>, Option<AnyElement>),
317 >,
318 render_tab_bar: Rc<dyn Fn(&mut Pane, &mut Window, &mut Context<Pane>) -> AnyElement>,
319 show_tab_bar_buttons: bool,
320 _subscriptions: Vec<Subscription>,
321 tab_bar_scroll_handle: ScrollHandle,
322 /// Is None if navigation buttons are permanently turned off (and should not react to setting changes).
323 /// Otherwise, when `display_nav_history_buttons` is Some, it determines whether nav buttons should be displayed.
324 display_nav_history_buttons: Option<bool>,
325 double_click_dispatch_action: Box<dyn Action>,
326 save_modals_spawned: HashSet<EntityId>,
327 close_pane_if_empty: bool,
328 pub new_item_context_menu_handle: PopoverMenuHandle<ContextMenu>,
329 pub split_item_context_menu_handle: PopoverMenuHandle<ContextMenu>,
330 pinned_tab_count: usize,
331 diagnostics: HashMap<ProjectPath, DiagnosticSeverity>,
332 zoom_out_on_close: bool,
333 /// If a certain project item wants to get recreated with specific data, it can persist its data before the recreation here.
334 pub project_item_restoration_data: HashMap<ProjectItemKind, Box<dyn Any + Send>>,
335}
336
337pub struct ActivationHistoryEntry {
338 pub entity_id: EntityId,
339 pub timestamp: usize,
340}
341
342pub struct ItemNavHistory {
343 history: NavHistory,
344 item: Arc<dyn WeakItemHandle>,
345 is_preview: bool,
346}
347
348#[derive(Clone)]
349pub struct NavHistory(Arc<Mutex<NavHistoryState>>);
350
351struct NavHistoryState {
352 mode: NavigationMode,
353 backward_stack: VecDeque<NavigationEntry>,
354 forward_stack: VecDeque<NavigationEntry>,
355 closed_stack: VecDeque<NavigationEntry>,
356 paths_by_item: HashMap<EntityId, (ProjectPath, Option<PathBuf>)>,
357 pane: WeakEntity<Pane>,
358 next_timestamp: Arc<AtomicUsize>,
359}
360
361#[derive(Debug, Copy, Clone)]
362pub enum NavigationMode {
363 Normal,
364 GoingBack,
365 GoingForward,
366 ClosingItem,
367 ReopeningClosedItem,
368 Disabled,
369}
370
371impl Default for NavigationMode {
372 fn default() -> Self {
373 Self::Normal
374 }
375}
376
377pub struct NavigationEntry {
378 pub item: Arc<dyn WeakItemHandle>,
379 pub data: Option<Box<dyn Any + Send>>,
380 pub timestamp: usize,
381 pub is_preview: bool,
382}
383
384#[derive(Clone)]
385pub struct DraggedTab {
386 pub pane: Entity<Pane>,
387 pub item: Box<dyn ItemHandle>,
388 pub ix: usize,
389 pub detail: usize,
390 pub is_active: bool,
391}
392
393impl EventEmitter<Event> for Pane {}
394
395pub enum Side {
396 Left,
397 Right,
398}
399
400impl Pane {
401 pub fn new(
402 workspace: WeakEntity<Workspace>,
403 project: Entity<Project>,
404 next_timestamp: Arc<AtomicUsize>,
405 can_drop_predicate: Option<Arc<dyn Fn(&dyn Any, &mut Window, &mut App) -> bool + 'static>>,
406 double_click_dispatch_action: Box<dyn Action>,
407 window: &mut Window,
408 cx: &mut Context<Self>,
409 ) -> Self {
410 let focus_handle = cx.focus_handle();
411
412 let subscriptions = vec![
413 cx.on_focus(&focus_handle, window, Pane::focus_in),
414 cx.on_focus_in(&focus_handle, window, Pane::focus_in),
415 cx.on_focus_out(&focus_handle, window, Pane::focus_out),
416 cx.observe_global::<SettingsStore>(Self::settings_changed),
417 cx.subscribe(&project, Self::project_events),
418 ];
419
420 let handle = cx.entity().downgrade();
421 Self {
422 alternate_file_items: (None, None),
423 focus_handle,
424 items: Vec::new(),
425 activation_history: Vec::new(),
426 next_activation_timestamp: next_timestamp.clone(),
427 was_focused: false,
428 zoomed: false,
429 active_item_index: 0,
430 preview_item_id: None,
431 last_focus_handle_by_item: Default::default(),
432 nav_history: NavHistory(Arc::new(Mutex::new(NavHistoryState {
433 mode: NavigationMode::Normal,
434 backward_stack: Default::default(),
435 forward_stack: Default::default(),
436 closed_stack: Default::default(),
437 paths_by_item: Default::default(),
438 pane: handle.clone(),
439 next_timestamp,
440 }))),
441 toolbar: cx.new(|_| Toolbar::new()),
442 tab_bar_scroll_handle: ScrollHandle::new(),
443 drag_split_direction: None,
444 workspace,
445 project: project.downgrade(),
446 can_drop_predicate,
447 custom_drop_handle: None,
448 can_split_predicate: None,
449 should_display_tab_bar: Rc::new(|_, cx| TabBarSettings::get_global(cx).show),
450 render_tab_bar_buttons: Rc::new(default_render_tab_bar_buttons),
451 render_tab_bar: Rc::new(Self::render_tab_bar),
452 show_tab_bar_buttons: TabBarSettings::get_global(cx).show_tab_bar_buttons,
453 display_nav_history_buttons: Some(
454 TabBarSettings::get_global(cx).show_nav_history_buttons,
455 ),
456 _subscriptions: subscriptions,
457 double_click_dispatch_action,
458 save_modals_spawned: HashSet::default(),
459 close_pane_if_empty: true,
460 split_item_context_menu_handle: Default::default(),
461 new_item_context_menu_handle: Default::default(),
462 pinned_tab_count: 0,
463 diagnostics: Default::default(),
464 zoom_out_on_close: true,
465 project_item_restoration_data: HashMap::default(),
466 }
467 }
468
469 fn alternate_file(&mut self, window: &mut Window, cx: &mut Context<Pane>) {
470 let (_, alternative) = &self.alternate_file_items;
471 if let Some(alternative) = alternative {
472 let existing = self
473 .items()
474 .find_position(|item| item.item_id() == alternative.id());
475 if let Some((ix, _)) = existing {
476 self.activate_item(ix, true, true, window, cx);
477 } else if let Some(upgraded) = alternative.upgrade() {
478 self.add_item(upgraded, true, true, None, window, cx);
479 }
480 }
481 }
482
483 pub fn track_alternate_file_items(&mut self) {
484 if let Some(item) = self.active_item().map(|item| item.downgrade_item()) {
485 let (current, _) = &self.alternate_file_items;
486 match current {
487 Some(current) => {
488 if current.id() != item.id() {
489 self.alternate_file_items =
490 (Some(item), self.alternate_file_items.0.take());
491 }
492 }
493 None => {
494 self.alternate_file_items = (Some(item), None);
495 }
496 }
497 }
498 }
499
500 pub fn has_focus(&self, window: &Window, cx: &App) -> bool {
501 // We not only check whether our focus handle contains focus, but also
502 // whether the active item might have focus, because we might have just activated an item
503 // that hasn't rendered yet.
504 // Before the next render, we might transfer focus
505 // to the item, and `focus_handle.contains_focus` returns false because the `active_item`
506 // is not hooked up to us in the dispatch tree.
507 self.focus_handle.contains_focused(window, cx)
508 || self.active_item().map_or(false, |item| {
509 item.item_focus_handle(cx).contains_focused(window, cx)
510 })
511 }
512
513 fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
514 if !self.was_focused {
515 self.was_focused = true;
516 self.update_history(self.active_item_index);
517 cx.emit(Event::Focus);
518 cx.notify();
519 }
520
521 self.toolbar.update(cx, |toolbar, cx| {
522 toolbar.focus_changed(true, window, cx);
523 });
524
525 if let Some(active_item) = self.active_item() {
526 if self.focus_handle.is_focused(window) {
527 // Schedule a redraw next frame, so that the focus changes below take effect
528 cx.on_next_frame(window, |_, _, cx| {
529 cx.notify();
530 });
531
532 // Pane was focused directly. We need to either focus a view inside the active item,
533 // or focus the active item itself
534 if let Some(weak_last_focus_handle) =
535 self.last_focus_handle_by_item.get(&active_item.item_id())
536 {
537 if let Some(focus_handle) = weak_last_focus_handle.upgrade() {
538 focus_handle.focus(window);
539 return;
540 }
541 }
542
543 active_item.item_focus_handle(cx).focus(window);
544 } else if let Some(focused) = window.focused(cx) {
545 if !self.context_menu_focused(window, cx) {
546 self.last_focus_handle_by_item
547 .insert(active_item.item_id(), focused.downgrade());
548 }
549 }
550 }
551 }
552
553 pub fn context_menu_focused(&self, window: &mut Window, cx: &mut Context<Self>) -> bool {
554 self.new_item_context_menu_handle.is_focused(window, cx)
555 || self.split_item_context_menu_handle.is_focused(window, cx)
556 }
557
558 fn focus_out(&mut self, _event: FocusOutEvent, window: &mut Window, cx: &mut Context<Self>) {
559 self.was_focused = false;
560 self.toolbar.update(cx, |toolbar, cx| {
561 toolbar.focus_changed(false, window, cx);
562 });
563 cx.notify();
564 }
565
566 fn project_events(
567 &mut self,
568 _project: Entity<Project>,
569 event: &project::Event,
570 cx: &mut Context<Self>,
571 ) {
572 match event {
573 project::Event::DiskBasedDiagnosticsFinished { .. }
574 | project::Event::DiagnosticsUpdated { .. } => {
575 if ItemSettings::get_global(cx).show_diagnostics != ShowDiagnostics::Off {
576 self.update_diagnostics(cx);
577 cx.notify();
578 }
579 }
580 _ => {}
581 }
582 }
583
584 fn update_diagnostics(&mut self, cx: &mut Context<Self>) {
585 let Some(project) = self.project.upgrade() else {
586 return;
587 };
588 let show_diagnostics = ItemSettings::get_global(cx).show_diagnostics;
589 self.diagnostics = if show_diagnostics != ShowDiagnostics::Off {
590 project
591 .read(cx)
592 .diagnostic_summaries(false, cx)
593 .filter_map(|(project_path, _, diagnostic_summary)| {
594 if diagnostic_summary.error_count > 0 {
595 Some((project_path, DiagnosticSeverity::ERROR))
596 } else if diagnostic_summary.warning_count > 0
597 && show_diagnostics != ShowDiagnostics::Errors
598 {
599 Some((project_path, DiagnosticSeverity::WARNING))
600 } else {
601 None
602 }
603 })
604 .collect()
605 } else {
606 HashMap::default()
607 }
608 }
609
610 fn settings_changed(&mut self, cx: &mut Context<Self>) {
611 let tab_bar_settings = TabBarSettings::get_global(cx);
612
613 if let Some(display_nav_history_buttons) = self.display_nav_history_buttons.as_mut() {
614 *display_nav_history_buttons = tab_bar_settings.show_nav_history_buttons;
615 }
616 self.show_tab_bar_buttons = tab_bar_settings.show_tab_bar_buttons;
617
618 if !PreviewTabsSettings::get_global(cx).enabled {
619 self.preview_item_id = None;
620 }
621 self.update_diagnostics(cx);
622 cx.notify();
623 }
624
625 pub fn active_item_index(&self) -> usize {
626 self.active_item_index
627 }
628
629 pub fn activation_history(&self) -> &[ActivationHistoryEntry] {
630 &self.activation_history
631 }
632
633 pub fn set_should_display_tab_bar<F>(&mut self, should_display_tab_bar: F)
634 where
635 F: 'static + Fn(&Window, &mut Context<Pane>) -> bool,
636 {
637 self.should_display_tab_bar = Rc::new(should_display_tab_bar);
638 }
639
640 pub fn set_can_split(
641 &mut self,
642 can_split_predicate: Option<
643 Arc<dyn Fn(&mut Self, &dyn Any, &mut Window, &mut Context<Self>) -> bool + 'static>,
644 >,
645 ) {
646 self.can_split_predicate = can_split_predicate;
647 }
648
649 pub fn set_close_pane_if_empty(&mut self, close_pane_if_empty: bool, cx: &mut Context<Self>) {
650 self.close_pane_if_empty = close_pane_if_empty;
651 cx.notify();
652 }
653
654 pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut Context<Self>) {
655 self.toolbar.update(cx, |toolbar, cx| {
656 toolbar.set_can_navigate(can_navigate, cx);
657 });
658 cx.notify();
659 }
660
661 pub fn set_render_tab_bar<F>(&mut self, cx: &mut Context<Self>, render: F)
662 where
663 F: 'static + Fn(&mut Pane, &mut Window, &mut Context<Pane>) -> AnyElement,
664 {
665 self.render_tab_bar = Rc::new(render);
666 cx.notify();
667 }
668
669 pub fn set_render_tab_bar_buttons<F>(&mut self, cx: &mut Context<Self>, render: F)
670 where
671 F: 'static
672 + Fn(
673 &mut Pane,
674 &mut Window,
675 &mut Context<Pane>,
676 ) -> (Option<AnyElement>, Option<AnyElement>),
677 {
678 self.render_tab_bar_buttons = Rc::new(render);
679 cx.notify();
680 }
681
682 pub fn set_custom_drop_handle<F>(&mut self, cx: &mut Context<Self>, handle: F)
683 where
684 F: 'static
685 + Fn(&mut Pane, &dyn Any, &mut Window, &mut Context<Pane>) -> ControlFlow<(), ()>,
686 {
687 self.custom_drop_handle = Some(Arc::new(handle));
688 cx.notify();
689 }
690
691 pub fn nav_history_for_item<T: Item>(&self, item: &Entity<T>) -> ItemNavHistory {
692 ItemNavHistory {
693 history: self.nav_history.clone(),
694 item: Arc::new(item.downgrade()),
695 is_preview: self.preview_item_id == Some(item.item_id()),
696 }
697 }
698
699 pub fn nav_history(&self) -> &NavHistory {
700 &self.nav_history
701 }
702
703 pub fn nav_history_mut(&mut self) -> &mut NavHistory {
704 &mut self.nav_history
705 }
706
707 pub fn disable_history(&mut self) {
708 self.nav_history.disable();
709 }
710
711 pub fn enable_history(&mut self) {
712 self.nav_history.enable();
713 }
714
715 pub fn can_navigate_backward(&self) -> bool {
716 !self.nav_history.0.lock().backward_stack.is_empty()
717 }
718
719 pub fn can_navigate_forward(&self) -> bool {
720 !self.nav_history.0.lock().forward_stack.is_empty()
721 }
722
723 pub fn navigate_backward(&mut self, window: &mut Window, cx: &mut Context<Self>) {
724 if let Some(workspace) = self.workspace.upgrade() {
725 let pane = cx.entity().downgrade();
726 window.defer(cx, move |window, cx| {
727 workspace.update(cx, |workspace, cx| {
728 workspace.go_back(pane, window, cx).detach_and_log_err(cx)
729 })
730 })
731 }
732 }
733
734 fn navigate_forward(&mut self, window: &mut Window, cx: &mut Context<Self>) {
735 if let Some(workspace) = self.workspace.upgrade() {
736 let pane = cx.entity().downgrade();
737 window.defer(cx, move |window, cx| {
738 workspace.update(cx, |workspace, cx| {
739 workspace
740 .go_forward(pane, window, cx)
741 .detach_and_log_err(cx)
742 })
743 })
744 }
745 }
746
747 fn history_updated(&mut self, cx: &mut Context<Self>) {
748 self.toolbar.update(cx, |_, cx| cx.notify());
749 }
750
751 pub fn preview_item_id(&self) -> Option<EntityId> {
752 self.preview_item_id
753 }
754
755 pub fn preview_item(&self) -> Option<Box<dyn ItemHandle>> {
756 self.preview_item_id
757 .and_then(|id| self.items.iter().find(|item| item.item_id() == id))
758 .cloned()
759 }
760
761 pub fn preview_item_idx(&self) -> Option<usize> {
762 if let Some(preview_item_id) = self.preview_item_id {
763 self.items
764 .iter()
765 .position(|item| item.item_id() == preview_item_id)
766 } else {
767 None
768 }
769 }
770
771 pub fn is_active_preview_item(&self, item_id: EntityId) -> bool {
772 self.preview_item_id == Some(item_id)
773 }
774
775 /// Marks the item with the given ID as the preview item.
776 /// This will be ignored if the global setting `preview_tabs` is disabled.
777 pub fn set_preview_item_id(&mut self, item_id: Option<EntityId>, cx: &App) {
778 if PreviewTabsSettings::get_global(cx).enabled {
779 self.preview_item_id = item_id;
780 }
781 }
782
783 pub(crate) fn set_pinned_count(&mut self, count: usize) {
784 self.pinned_tab_count = count;
785 }
786
787 pub(crate) fn pinned_count(&self) -> usize {
788 self.pinned_tab_count
789 }
790
791 pub fn handle_item_edit(&mut self, item_id: EntityId, cx: &App) {
792 if let Some(preview_item) = self.preview_item() {
793 if preview_item.item_id() == item_id && !preview_item.preserve_preview(cx) {
794 self.set_preview_item_id(None, cx);
795 }
796 }
797 }
798
799 pub(crate) fn open_item(
800 &mut self,
801 project_entry_id: Option<ProjectEntryId>,
802 project_path: ProjectPath,
803 focus_item: bool,
804 allow_preview: bool,
805 activate: bool,
806 suggested_position: Option<usize>,
807 window: &mut Window,
808 cx: &mut Context<Self>,
809 build_item: WorkspaceItemBuilder,
810 ) -> Box<dyn ItemHandle> {
811 let mut existing_item = None;
812 if let Some(project_entry_id) = project_entry_id {
813 for (index, item) in self.items.iter().enumerate() {
814 if item.is_singleton(cx)
815 && item.project_entry_ids(cx).as_slice() == [project_entry_id]
816 {
817 let item = item.boxed_clone();
818 existing_item = Some((index, item));
819 break;
820 }
821 }
822 } else {
823 for (index, item) in self.items.iter().enumerate() {
824 if item.is_singleton(cx) && item.project_path(cx).as_ref() == Some(&project_path) {
825 let item = item.boxed_clone();
826 existing_item = Some((index, item));
827 break;
828 }
829 }
830 }
831 if let Some((index, existing_item)) = existing_item {
832 // If the item is already open, and the item is a preview item
833 // and we are not allowing items to open as preview, mark the item as persistent.
834 if let Some(preview_item_id) = self.preview_item_id {
835 if let Some(tab) = self.items.get(index) {
836 if tab.item_id() == preview_item_id && !allow_preview {
837 self.set_preview_item_id(None, cx);
838 }
839 }
840 }
841 if activate {
842 self.activate_item(index, focus_item, focus_item, window, cx);
843 }
844 existing_item
845 } else {
846 // If the item is being opened as preview and we have an existing preview tab,
847 // open the new item in the position of the existing preview tab.
848 let destination_index = if allow_preview {
849 self.close_current_preview_item(window, cx)
850 } else {
851 suggested_position
852 };
853
854 let new_item = build_item(self, window, cx);
855
856 if allow_preview {
857 self.set_preview_item_id(Some(new_item.item_id()), cx);
858 }
859 self.add_item_inner(
860 new_item.clone(),
861 true,
862 focus_item,
863 activate,
864 destination_index,
865 window,
866 cx,
867 );
868
869 new_item
870 }
871 }
872
873 pub fn close_current_preview_item(
874 &mut self,
875 window: &mut Window,
876 cx: &mut Context<Self>,
877 ) -> Option<usize> {
878 let item_idx = self.preview_item_idx()?;
879 let id = self.preview_item_id()?;
880
881 let prev_active_item_index = self.active_item_index;
882 self.remove_item(id, false, false, window, cx);
883 self.active_item_index = prev_active_item_index;
884
885 if item_idx < self.items.len() {
886 Some(item_idx)
887 } else {
888 None
889 }
890 }
891
892 pub fn add_item_inner(
893 &mut self,
894 item: Box<dyn ItemHandle>,
895 activate_pane: bool,
896 focus_item: bool,
897 activate: bool,
898 destination_index: Option<usize>,
899 window: &mut Window,
900 cx: &mut Context<Self>,
901 ) {
902 self.close_items_over_max_tabs(window, cx);
903
904 if item.is_singleton(cx) {
905 if let Some(&entry_id) = item.project_entry_ids(cx).first() {
906 let Some(project) = self.project.upgrade() else {
907 return;
908 };
909 let project = project.read(cx);
910 if let Some(project_path) = project.path_for_entry(entry_id, cx) {
911 let abs_path = project.absolute_path(&project_path, cx);
912 self.nav_history
913 .0
914 .lock()
915 .paths_by_item
916 .insert(item.item_id(), (project_path, abs_path));
917 }
918 }
919 }
920 // If no destination index is specified, add or move the item after the
921 // active item (or at the start of tab bar, if the active item is pinned)
922 let mut insertion_index = {
923 cmp::min(
924 if let Some(destination_index) = destination_index {
925 destination_index
926 } else {
927 cmp::max(self.active_item_index + 1, self.pinned_count())
928 },
929 self.items.len(),
930 )
931 };
932
933 // Does the item already exist?
934 let project_entry_id = if item.is_singleton(cx) {
935 item.project_entry_ids(cx).first().copied()
936 } else {
937 None
938 };
939
940 let existing_item_index = self.items.iter().position(|existing_item| {
941 if existing_item.item_id() == item.item_id() {
942 true
943 } else if existing_item.is_singleton(cx) {
944 existing_item
945 .project_entry_ids(cx)
946 .first()
947 .map_or(false, |existing_entry_id| {
948 Some(existing_entry_id) == project_entry_id.as_ref()
949 })
950 } else {
951 false
952 }
953 });
954
955 if let Some(existing_item_index) = existing_item_index {
956 // If the item already exists, move it to the desired destination and activate it
957
958 if existing_item_index != insertion_index {
959 let existing_item_is_active = existing_item_index == self.active_item_index;
960
961 // If the caller didn't specify a destination and the added item is already
962 // the active one, don't move it
963 if existing_item_is_active && destination_index.is_none() {
964 insertion_index = existing_item_index;
965 } else {
966 self.items.remove(existing_item_index);
967 if existing_item_index < self.active_item_index {
968 self.active_item_index -= 1;
969 }
970 insertion_index = insertion_index.min(self.items.len());
971
972 self.items.insert(insertion_index, item.clone());
973
974 if existing_item_is_active {
975 self.active_item_index = insertion_index;
976 } else if insertion_index <= self.active_item_index {
977 self.active_item_index += 1;
978 }
979 }
980
981 cx.notify();
982 }
983
984 if activate {
985 self.activate_item(insertion_index, activate_pane, focus_item, window, cx);
986 }
987 } else {
988 self.items.insert(insertion_index, item.clone());
989
990 if activate {
991 if insertion_index <= self.active_item_index
992 && self.preview_item_idx() != Some(self.active_item_index)
993 {
994 self.active_item_index += 1;
995 }
996
997 self.activate_item(insertion_index, activate_pane, focus_item, window, cx);
998 }
999 cx.notify();
1000 }
1001
1002 cx.emit(Event::AddItem { item });
1003 }
1004
1005 pub fn add_item(
1006 &mut self,
1007 item: Box<dyn ItemHandle>,
1008 activate_pane: bool,
1009 focus_item: bool,
1010 destination_index: Option<usize>,
1011 window: &mut Window,
1012 cx: &mut Context<Self>,
1013 ) {
1014 self.add_item_inner(
1015 item,
1016 activate_pane,
1017 focus_item,
1018 true,
1019 destination_index,
1020 window,
1021 cx,
1022 )
1023 }
1024
1025 pub fn items_len(&self) -> usize {
1026 self.items.len()
1027 }
1028
1029 pub fn items(&self) -> impl DoubleEndedIterator<Item = &Box<dyn ItemHandle>> {
1030 self.items.iter()
1031 }
1032
1033 pub fn items_of_type<T: Render>(&self) -> impl '_ + Iterator<Item = Entity<T>> {
1034 self.items
1035 .iter()
1036 .filter_map(|item| item.to_any().downcast().ok())
1037 }
1038
1039 pub fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
1040 self.items.get(self.active_item_index).cloned()
1041 }
1042
1043 fn active_item_id(&self) -> EntityId {
1044 self.items[self.active_item_index].item_id()
1045 }
1046
1047 pub fn pixel_position_of_cursor(&self, cx: &App) -> Option<Point<Pixels>> {
1048 self.items
1049 .get(self.active_item_index)?
1050 .pixel_position_of_cursor(cx)
1051 }
1052
1053 pub fn item_for_entry(
1054 &self,
1055 entry_id: ProjectEntryId,
1056 cx: &App,
1057 ) -> Option<Box<dyn ItemHandle>> {
1058 self.items.iter().find_map(|item| {
1059 if item.is_singleton(cx) && (item.project_entry_ids(cx).as_slice() == [entry_id]) {
1060 Some(item.boxed_clone())
1061 } else {
1062 None
1063 }
1064 })
1065 }
1066
1067 pub fn item_for_path(
1068 &self,
1069 project_path: ProjectPath,
1070 cx: &App,
1071 ) -> Option<Box<dyn ItemHandle>> {
1072 self.items.iter().find_map(move |item| {
1073 if item.is_singleton(cx) && (item.project_path(cx).as_slice() == [project_path.clone()])
1074 {
1075 Some(item.boxed_clone())
1076 } else {
1077 None
1078 }
1079 })
1080 }
1081
1082 pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option<usize> {
1083 self.index_for_item_id(item.item_id())
1084 }
1085
1086 fn index_for_item_id(&self, item_id: EntityId) -> Option<usize> {
1087 self.items.iter().position(|i| i.item_id() == item_id)
1088 }
1089
1090 pub fn item_for_index(&self, ix: usize) -> Option<&dyn ItemHandle> {
1091 self.items.get(ix).map(|i| i.as_ref())
1092 }
1093
1094 pub fn toggle_zoom(&mut self, _: &ToggleZoom, window: &mut Window, cx: &mut Context<Self>) {
1095 if self.zoomed {
1096 cx.emit(Event::ZoomOut);
1097 } else if !self.items.is_empty() {
1098 if !self.focus_handle.contains_focused(window, cx) {
1099 cx.focus_self(window);
1100 }
1101 cx.emit(Event::ZoomIn);
1102 }
1103 }
1104
1105 pub fn activate_item(
1106 &mut self,
1107 index: usize,
1108 activate_pane: bool,
1109 focus_item: bool,
1110 window: &mut Window,
1111 cx: &mut Context<Self>,
1112 ) {
1113 use NavigationMode::{GoingBack, GoingForward};
1114 if index < self.items.len() {
1115 let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
1116 if prev_active_item_ix != self.active_item_index
1117 || matches!(self.nav_history.mode(), GoingBack | GoingForward)
1118 {
1119 if let Some(prev_item) = self.items.get(prev_active_item_ix) {
1120 prev_item.deactivated(window, cx);
1121 }
1122 }
1123 self.update_history(index);
1124 self.update_toolbar(window, cx);
1125 self.update_status_bar(window, cx);
1126
1127 if focus_item {
1128 self.focus_active_item(window, cx);
1129 }
1130
1131 cx.emit(Event::ActivateItem {
1132 local: activate_pane,
1133 focus_changed: focus_item,
1134 });
1135
1136 if !self.is_tab_pinned(index) {
1137 self.tab_bar_scroll_handle
1138 .scroll_to_item(index - self.pinned_tab_count);
1139 }
1140
1141 cx.notify();
1142 }
1143 }
1144
1145 fn update_history(&mut self, index: usize) {
1146 if let Some(newly_active_item) = self.items.get(index) {
1147 self.activation_history
1148 .retain(|entry| entry.entity_id != newly_active_item.item_id());
1149 self.activation_history.push(ActivationHistoryEntry {
1150 entity_id: newly_active_item.item_id(),
1151 timestamp: self
1152 .next_activation_timestamp
1153 .fetch_add(1, Ordering::SeqCst),
1154 });
1155 }
1156 }
1157
1158 pub fn activate_prev_item(
1159 &mut self,
1160 activate_pane: bool,
1161 window: &mut Window,
1162 cx: &mut Context<Self>,
1163 ) {
1164 let mut index = self.active_item_index;
1165 if index > 0 {
1166 index -= 1;
1167 } else if !self.items.is_empty() {
1168 index = self.items.len() - 1;
1169 }
1170 self.activate_item(index, activate_pane, activate_pane, window, cx);
1171 }
1172
1173 pub fn activate_next_item(
1174 &mut self,
1175 activate_pane: bool,
1176 window: &mut Window,
1177 cx: &mut Context<Self>,
1178 ) {
1179 let mut index = self.active_item_index;
1180 if index + 1 < self.items.len() {
1181 index += 1;
1182 } else {
1183 index = 0;
1184 }
1185 self.activate_item(index, activate_pane, activate_pane, window, cx);
1186 }
1187
1188 pub fn swap_item_left(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1189 let index = self.active_item_index;
1190 if index == 0 {
1191 return;
1192 }
1193
1194 self.items.swap(index, index - 1);
1195 self.activate_item(index - 1, true, true, window, cx);
1196 }
1197
1198 pub fn swap_item_right(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1199 let index = self.active_item_index;
1200 if index + 1 == self.items.len() {
1201 return;
1202 }
1203
1204 self.items.swap(index, index + 1);
1205 self.activate_item(index + 1, true, true, window, cx);
1206 }
1207
1208 pub fn close_active_item(
1209 &mut self,
1210 action: &CloseActiveItem,
1211 window: &mut Window,
1212 cx: &mut Context<Self>,
1213 ) -> Task<Result<()>> {
1214 if self.items.is_empty() {
1215 // Close the window when there's no active items to close, if configured
1216 if WorkspaceSettings::get_global(cx)
1217 .when_closing_with_no_tabs
1218 .should_close()
1219 {
1220 window.dispatch_action(Box::new(CloseWindow), cx);
1221 }
1222
1223 return Task::ready(Ok(()));
1224 }
1225 if self.is_tab_pinned(self.active_item_index) && !action.close_pinned {
1226 // Activate any non-pinned tab in same pane
1227 let non_pinned_tab_index = self
1228 .items()
1229 .enumerate()
1230 .find(|(index, _item)| !self.is_tab_pinned(*index))
1231 .map(|(index, _item)| index);
1232 if let Some(index) = non_pinned_tab_index {
1233 self.activate_item(index, false, false, window, cx);
1234 return Task::ready(Ok(()));
1235 }
1236
1237 // Activate any non-pinned tab in different pane
1238 let current_pane = cx.entity();
1239 self.workspace
1240 .update(cx, |workspace, cx| {
1241 let panes = workspace.center.panes();
1242 let pane_with_unpinned_tab = panes.iter().find(|pane| {
1243 if **pane == ¤t_pane {
1244 return false;
1245 }
1246 pane.read(cx).has_unpinned_tabs()
1247 });
1248 if let Some(pane) = pane_with_unpinned_tab {
1249 pane.update(cx, |pane, cx| pane.activate_unpinned_tab(window, cx));
1250 }
1251 })
1252 .ok();
1253
1254 return Task::ready(Ok(()));
1255 };
1256
1257 let active_item_id = self.active_item_id();
1258
1259 self.close_item_by_id(
1260 active_item_id,
1261 action.save_intent.unwrap_or(SaveIntent::Close),
1262 window,
1263 cx,
1264 )
1265 }
1266
1267 pub fn close_item_by_id(
1268 &mut self,
1269 item_id_to_close: EntityId,
1270 save_intent: SaveIntent,
1271 window: &mut Window,
1272 cx: &mut Context<Self>,
1273 ) -> Task<Result<()>> {
1274 self.close_items(window, cx, save_intent, move |view_id| {
1275 view_id == item_id_to_close
1276 })
1277 }
1278
1279 pub fn close_inactive_items(
1280 &mut self,
1281 action: &CloseInactiveItems,
1282 window: &mut Window,
1283 cx: &mut Context<Self>,
1284 ) -> Task<Result<()>> {
1285 if self.items.is_empty() {
1286 return Task::ready(Ok(()));
1287 }
1288
1289 let active_item_id = self.active_item_id();
1290 let pinned_item_ids = self.pinned_item_ids();
1291
1292 self.close_items(
1293 window,
1294 cx,
1295 action.save_intent.unwrap_or(SaveIntent::Close),
1296 move |item_id| {
1297 item_id != active_item_id
1298 && (action.close_pinned || !pinned_item_ids.contains(&item_id))
1299 },
1300 )
1301 }
1302
1303 pub fn close_clean_items(
1304 &mut self,
1305 action: &CloseCleanItems,
1306 window: &mut Window,
1307 cx: &mut Context<Self>,
1308 ) -> Task<Result<()>> {
1309 if self.items.is_empty() {
1310 return Task::ready(Ok(()));
1311 }
1312
1313 let clean_item_ids = self.clean_item_ids(cx);
1314 let pinned_item_ids = self.pinned_item_ids();
1315
1316 self.close_items(window, cx, SaveIntent::Close, move |item_id| {
1317 clean_item_ids.contains(&item_id)
1318 && (action.close_pinned || !pinned_item_ids.contains(&item_id))
1319 })
1320 }
1321
1322 pub fn close_items_to_the_left_by_id(
1323 &mut self,
1324 item_id: Option<EntityId>,
1325 action: &CloseItemsToTheLeft,
1326 window: &mut Window,
1327 cx: &mut Context<Self>,
1328 ) -> Task<Result<()>> {
1329 self.close_items_to_the_side_by_id(item_id, Side::Left, action.close_pinned, window, cx)
1330 }
1331
1332 pub fn close_items_to_the_right_by_id(
1333 &mut self,
1334 item_id: Option<EntityId>,
1335 action: &CloseItemsToTheRight,
1336 window: &mut Window,
1337 cx: &mut Context<Self>,
1338 ) -> Task<Result<()>> {
1339 self.close_items_to_the_side_by_id(item_id, Side::Right, action.close_pinned, window, cx)
1340 }
1341
1342 pub fn close_items_to_the_side_by_id(
1343 &mut self,
1344 item_id: Option<EntityId>,
1345 side: Side,
1346 close_pinned: bool,
1347 window: &mut Window,
1348 cx: &mut Context<Self>,
1349 ) -> Task<Result<()>> {
1350 if self.items.is_empty() {
1351 return Task::ready(Ok(()));
1352 }
1353
1354 let item_id = item_id.unwrap_or_else(|| self.active_item_id());
1355 let to_the_side_item_ids = self.to_the_side_item_ids(item_id, side);
1356 let pinned_item_ids = self.pinned_item_ids();
1357
1358 self.close_items(window, cx, SaveIntent::Close, move |item_id| {
1359 to_the_side_item_ids.contains(&item_id)
1360 && (close_pinned || !pinned_item_ids.contains(&item_id))
1361 })
1362 }
1363
1364 pub fn close_all_items(
1365 &mut self,
1366 action: &CloseAllItems,
1367 window: &mut Window,
1368 cx: &mut Context<Self>,
1369 ) -> Task<Result<()>> {
1370 if self.items.is_empty() {
1371 return Task::ready(Ok(()));
1372 }
1373
1374 let pinned_item_ids = self.pinned_item_ids();
1375
1376 self.close_items(
1377 window,
1378 cx,
1379 action.save_intent.unwrap_or(SaveIntent::Close),
1380 |item_id| action.close_pinned || !pinned_item_ids.contains(&item_id),
1381 )
1382 }
1383
1384 pub fn close_items_over_max_tabs(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1385 let Some(max_tabs) = WorkspaceSettings::get_global(cx).max_tabs.map(|i| i.get()) else {
1386 return;
1387 };
1388
1389 // Reduce over the activation history to get every dirty items up to max_tabs
1390 // count.
1391 let mut index_list = Vec::new();
1392 let mut items_len = self.items_len();
1393 let mut indexes: HashMap<EntityId, usize> = HashMap::default();
1394 for (index, item) in self.items.iter().enumerate() {
1395 indexes.insert(item.item_id(), index);
1396 }
1397 for entry in self.activation_history.iter() {
1398 if items_len < max_tabs {
1399 break;
1400 }
1401 let Some(&index) = indexes.get(&entry.entity_id) else {
1402 continue;
1403 };
1404 if let Some(true) = self.items.get(index).map(|item| item.is_dirty(cx)) {
1405 continue;
1406 }
1407
1408 index_list.push(index);
1409 items_len -= 1;
1410 }
1411 // The sort and reverse is necessary since we remove items
1412 // using their index position, hence removing from the end
1413 // of the list first to avoid changing indexes.
1414 index_list.sort_unstable();
1415 index_list
1416 .iter()
1417 .rev()
1418 .for_each(|&index| self._remove_item(index, false, false, None, window, cx));
1419 }
1420
1421 // Usually when you close an item that has unsaved changes, we prompt you to
1422 // save it. That said, if you still have the buffer open in a different pane
1423 // we can close this one without fear of losing data.
1424 pub fn skip_save_on_close(item: &dyn ItemHandle, workspace: &Workspace, cx: &App) -> bool {
1425 let mut dirty_project_item_ids = Vec::new();
1426 item.for_each_project_item(cx, &mut |project_item_id, project_item| {
1427 if project_item.is_dirty() {
1428 dirty_project_item_ids.push(project_item_id);
1429 }
1430 });
1431 if dirty_project_item_ids.is_empty() {
1432 return !(item.is_singleton(cx) && item.is_dirty(cx));
1433 }
1434
1435 for open_item in workspace.items(cx) {
1436 if open_item.item_id() == item.item_id() {
1437 continue;
1438 }
1439 if !open_item.is_singleton(cx) {
1440 continue;
1441 }
1442 let other_project_item_ids = open_item.project_item_model_ids(cx);
1443 dirty_project_item_ids.retain(|id| !other_project_item_ids.contains(id));
1444 }
1445 return dirty_project_item_ids.is_empty();
1446 }
1447
1448 pub(super) fn file_names_for_prompt(
1449 items: &mut dyn Iterator<Item = &Box<dyn ItemHandle>>,
1450 cx: &App,
1451 ) -> String {
1452 let mut file_names = BTreeSet::default();
1453 for item in items {
1454 item.for_each_project_item(cx, &mut |_, project_item| {
1455 if !project_item.is_dirty() {
1456 return;
1457 }
1458 let filename = project_item.project_path(cx).and_then(|path| {
1459 path.path
1460 .file_name()
1461 .and_then(|name| name.to_str().map(ToOwned::to_owned))
1462 });
1463 file_names.insert(filename.unwrap_or("untitled".to_string()));
1464 });
1465 }
1466 if file_names.len() > 6 {
1467 format!(
1468 "{}\n.. and {} more",
1469 file_names.iter().take(5).join("\n"),
1470 file_names.len() - 5
1471 )
1472 } else {
1473 file_names.into_iter().join("\n")
1474 }
1475 }
1476
1477 pub fn close_items(
1478 &self,
1479 window: &mut Window,
1480 cx: &mut Context<Pane>,
1481 mut save_intent: SaveIntent,
1482 should_close: impl Fn(EntityId) -> bool,
1483 ) -> Task<Result<()>> {
1484 // Find the items to close.
1485 let mut items_to_close = Vec::new();
1486 for item in &self.items {
1487 if should_close(item.item_id()) {
1488 items_to_close.push(item.boxed_clone());
1489 }
1490 }
1491
1492 let active_item_id = self.active_item().map(|item| item.item_id());
1493
1494 items_to_close.sort_by_key(|item| {
1495 let path = item.project_path(cx);
1496 // Put the currently active item at the end, because if the currently active item is not closed last
1497 // closing the currently active item will cause the focus to switch to another item
1498 // This will cause Zed to expand the content of the currently active item
1499 //
1500 // Beyond that sort in order of project path, with untitled files and multibuffers coming last.
1501 (active_item_id == Some(item.item_id()), path.is_none(), path)
1502 });
1503
1504 let workspace = self.workspace.clone();
1505 let Some(project) = self.project.upgrade() else {
1506 return Task::ready(Ok(()));
1507 };
1508 cx.spawn_in(window, async move |pane, cx| {
1509 let dirty_items = workspace.update(cx, |workspace, cx| {
1510 items_to_close
1511 .iter()
1512 .filter(|item| {
1513 item.is_dirty(cx)
1514 && !Self::skip_save_on_close(item.as_ref(), &workspace, cx)
1515 })
1516 .map(|item| item.boxed_clone())
1517 .collect::<Vec<_>>()
1518 })?;
1519
1520 if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
1521 let answer = pane.update_in(cx, |_, window, cx| {
1522 let detail = Self::file_names_for_prompt(&mut dirty_items.iter(), cx);
1523 window.prompt(
1524 PromptLevel::Warning,
1525 "Do you want to save changes to the following files?",
1526 Some(&detail),
1527 &["Save all", "Discard all", "Cancel"],
1528 cx,
1529 )
1530 })?;
1531 match answer.await {
1532 Ok(0) => save_intent = SaveIntent::SaveAll,
1533 Ok(1) => save_intent = SaveIntent::Skip,
1534 Ok(2) => return Ok(()),
1535 _ => {}
1536 }
1537 }
1538
1539 for item_to_close in items_to_close {
1540 let mut should_save = true;
1541 if save_intent == SaveIntent::Close {
1542 workspace.update(cx, |workspace, cx| {
1543 if Self::skip_save_on_close(item_to_close.as_ref(), &workspace, cx) {
1544 should_save = false;
1545 }
1546 })?;
1547 }
1548
1549 if should_save {
1550 if !Self::save_item(project.clone(), &pane, &*item_to_close, save_intent, cx)
1551 .await?
1552 {
1553 break;
1554 }
1555 }
1556
1557 // Remove the item from the pane.
1558 pane.update_in(cx, |pane, window, cx| {
1559 pane.remove_item(
1560 item_to_close.item_id(),
1561 false,
1562 pane.close_pane_if_empty,
1563 window,
1564 cx,
1565 );
1566 })
1567 .ok();
1568 }
1569
1570 pane.update(cx, |_, cx| cx.notify()).ok();
1571 Ok(())
1572 })
1573 }
1574
1575 pub fn remove_item(
1576 &mut self,
1577 item_id: EntityId,
1578 activate_pane: bool,
1579 close_pane_if_empty: bool,
1580 window: &mut Window,
1581 cx: &mut Context<Self>,
1582 ) {
1583 let Some(item_index) = self.index_for_item_id(item_id) else {
1584 return;
1585 };
1586 self._remove_item(
1587 item_index,
1588 activate_pane,
1589 close_pane_if_empty,
1590 None,
1591 window,
1592 cx,
1593 )
1594 }
1595
1596 pub fn remove_item_and_focus_on_pane(
1597 &mut self,
1598 item_index: usize,
1599 activate_pane: bool,
1600 focus_on_pane_if_closed: Entity<Pane>,
1601 window: &mut Window,
1602 cx: &mut Context<Self>,
1603 ) {
1604 self._remove_item(
1605 item_index,
1606 activate_pane,
1607 true,
1608 Some(focus_on_pane_if_closed),
1609 window,
1610 cx,
1611 )
1612 }
1613
1614 fn _remove_item(
1615 &mut self,
1616 item_index: usize,
1617 activate_pane: bool,
1618 close_pane_if_empty: bool,
1619 focus_on_pane_if_closed: Option<Entity<Pane>>,
1620 window: &mut Window,
1621 cx: &mut Context<Self>,
1622 ) {
1623 let activate_on_close = &ItemSettings::get_global(cx).activate_on_close;
1624 self.activation_history
1625 .retain(|entry| entry.entity_id != self.items[item_index].item_id());
1626
1627 if self.is_tab_pinned(item_index) {
1628 self.pinned_tab_count -= 1;
1629 }
1630 if item_index == self.active_item_index {
1631 let left_neighbour_index = || item_index.min(self.items.len()).saturating_sub(1);
1632 let index_to_activate = match activate_on_close {
1633 ActivateOnClose::History => self
1634 .activation_history
1635 .pop()
1636 .and_then(|last_activated_item| {
1637 self.items.iter().enumerate().find_map(|(index, item)| {
1638 (item.item_id() == last_activated_item.entity_id).then_some(index)
1639 })
1640 })
1641 // We didn't have a valid activation history entry, so fallback
1642 // to activating the item to the left
1643 .unwrap_or_else(left_neighbour_index),
1644 ActivateOnClose::Neighbour => {
1645 self.activation_history.pop();
1646 if item_index + 1 < self.items.len() {
1647 item_index + 1
1648 } else {
1649 item_index.saturating_sub(1)
1650 }
1651 }
1652 ActivateOnClose::LeftNeighbour => {
1653 self.activation_history.pop();
1654 left_neighbour_index()
1655 }
1656 };
1657
1658 let should_activate = activate_pane || self.has_focus(window, cx);
1659 if self.items.len() == 1 && should_activate {
1660 self.focus_handle.focus(window);
1661 } else {
1662 self.activate_item(
1663 index_to_activate,
1664 should_activate,
1665 should_activate,
1666 window,
1667 cx,
1668 );
1669 }
1670 }
1671
1672 let item = self.items.remove(item_index);
1673
1674 cx.emit(Event::RemovedItem { item: item.clone() });
1675 if self.items.is_empty() {
1676 item.deactivated(window, cx);
1677 if close_pane_if_empty {
1678 self.update_toolbar(window, cx);
1679 cx.emit(Event::Remove {
1680 focus_on_pane: focus_on_pane_if_closed,
1681 });
1682 }
1683 }
1684
1685 if item_index < self.active_item_index {
1686 self.active_item_index -= 1;
1687 }
1688
1689 let mode = self.nav_history.mode();
1690 self.nav_history.set_mode(NavigationMode::ClosingItem);
1691 item.deactivated(window, cx);
1692 self.nav_history.set_mode(mode);
1693
1694 if self.is_active_preview_item(item.item_id()) {
1695 self.set_preview_item_id(None, cx);
1696 }
1697
1698 if let Some(path) = item.project_path(cx) {
1699 let abs_path = self
1700 .nav_history
1701 .0
1702 .lock()
1703 .paths_by_item
1704 .get(&item.item_id())
1705 .and_then(|(_, abs_path)| abs_path.clone());
1706
1707 self.nav_history
1708 .0
1709 .lock()
1710 .paths_by_item
1711 .insert(item.item_id(), (path, abs_path));
1712 } else {
1713 self.nav_history
1714 .0
1715 .lock()
1716 .paths_by_item
1717 .remove(&item.item_id());
1718 }
1719
1720 if self.zoom_out_on_close && self.items.is_empty() && close_pane_if_empty && self.zoomed {
1721 cx.emit(Event::ZoomOut);
1722 }
1723
1724 cx.notify();
1725 }
1726
1727 pub async fn save_item(
1728 project: Entity<Project>,
1729 pane: &WeakEntity<Pane>,
1730 item: &dyn ItemHandle,
1731 save_intent: SaveIntent,
1732 cx: &mut AsyncWindowContext,
1733 ) -> Result<bool> {
1734 const CONFLICT_MESSAGE: &str = "This file has changed on disk since you started editing it. Do you want to overwrite it?";
1735
1736 const DELETED_MESSAGE: &str = "This file has been deleted on disk since you started editing it. Do you want to recreate it?";
1737
1738 if save_intent == SaveIntent::Skip {
1739 return Ok(true);
1740 }
1741 let Some(item_ix) = pane
1742 .read_with(cx, |pane, _| pane.index_for_item(item))
1743 .ok()
1744 .flatten()
1745 else {
1746 return Ok(true);
1747 };
1748
1749 let (
1750 mut has_conflict,
1751 mut is_dirty,
1752 mut can_save,
1753 can_save_as,
1754 is_singleton,
1755 has_deleted_file,
1756 ) = cx.update(|_window, cx| {
1757 (
1758 item.has_conflict(cx),
1759 item.is_dirty(cx),
1760 item.can_save(cx),
1761 item.can_save_as(cx),
1762 item.is_singleton(cx),
1763 item.has_deleted_file(cx),
1764 )
1765 })?;
1766
1767 // when saving a single buffer, we ignore whether or not it's dirty.
1768 if save_intent == SaveIntent::Save || save_intent == SaveIntent::SaveWithoutFormat {
1769 is_dirty = true;
1770 }
1771
1772 if save_intent == SaveIntent::SaveAs {
1773 is_dirty = true;
1774 has_conflict = false;
1775 can_save = false;
1776 }
1777
1778 if save_intent == SaveIntent::Overwrite {
1779 has_conflict = false;
1780 }
1781
1782 let should_format = save_intent != SaveIntent::SaveWithoutFormat;
1783
1784 if has_conflict && can_save {
1785 if has_deleted_file && is_singleton {
1786 let answer = pane.update_in(cx, |pane, window, cx| {
1787 pane.activate_item(item_ix, true, true, window, cx);
1788 window.prompt(
1789 PromptLevel::Warning,
1790 DELETED_MESSAGE,
1791 None,
1792 &["Save", "Close", "Cancel"],
1793 cx,
1794 )
1795 })?;
1796 match answer.await {
1797 Ok(0) => {
1798 pane.update_in(cx, |_, window, cx| {
1799 item.save(should_format, project, window, cx)
1800 })?
1801 .await?
1802 }
1803 Ok(1) => {
1804 pane.update_in(cx, |pane, window, cx| {
1805 pane.remove_item(item.item_id(), false, true, window, cx)
1806 })?;
1807 }
1808 _ => return Ok(false),
1809 }
1810 return Ok(true);
1811 } else {
1812 let answer = pane.update_in(cx, |pane, window, cx| {
1813 pane.activate_item(item_ix, true, true, window, cx);
1814 window.prompt(
1815 PromptLevel::Warning,
1816 CONFLICT_MESSAGE,
1817 None,
1818 &["Overwrite", "Discard", "Cancel"],
1819 cx,
1820 )
1821 })?;
1822 match answer.await {
1823 Ok(0) => {
1824 pane.update_in(cx, |_, window, cx| {
1825 item.save(should_format, project, window, cx)
1826 })?
1827 .await?
1828 }
1829 Ok(1) => {
1830 pane.update_in(cx, |_, window, cx| item.reload(project, window, cx))?
1831 .await?
1832 }
1833 _ => return Ok(false),
1834 }
1835 }
1836 } else if is_dirty && (can_save || can_save_as) {
1837 if save_intent == SaveIntent::Close {
1838 let will_autosave = cx.update(|_window, cx| {
1839 matches!(
1840 item.workspace_settings(cx).autosave,
1841 AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange
1842 ) && item.can_autosave(cx)
1843 })?;
1844 if !will_autosave {
1845 let item_id = item.item_id();
1846 let answer_task = pane.update_in(cx, |pane, window, cx| {
1847 if pane.save_modals_spawned.insert(item_id) {
1848 pane.activate_item(item_ix, true, true, window, cx);
1849 let prompt = dirty_message_for(item.project_path(cx));
1850 Some(window.prompt(
1851 PromptLevel::Warning,
1852 &prompt,
1853 None,
1854 &["Save", "Don't Save", "Cancel"],
1855 cx,
1856 ))
1857 } else {
1858 None
1859 }
1860 })?;
1861 if let Some(answer_task) = answer_task {
1862 let answer = answer_task.await;
1863 pane.update(cx, |pane, _| {
1864 if !pane.save_modals_spawned.remove(&item_id) {
1865 debug_panic!(
1866 "save modal was not present in spawned modals after awaiting for its answer"
1867 )
1868 }
1869 })?;
1870 match answer {
1871 Ok(0) => {}
1872 Ok(1) => {
1873 // Don't save this file
1874 pane.update_in(cx, |pane, window, cx| {
1875 if pane.is_tab_pinned(item_ix) && !item.can_save(cx) {
1876 pane.pinned_tab_count -= 1;
1877 }
1878 item.discarded(project, window, cx)
1879 })
1880 .log_err();
1881 return Ok(true);
1882 }
1883 _ => return Ok(false), // Cancel
1884 }
1885 } else {
1886 return Ok(false);
1887 }
1888 }
1889 }
1890
1891 if can_save {
1892 pane.update_in(cx, |pane, window, cx| {
1893 if pane.is_active_preview_item(item.item_id()) {
1894 pane.set_preview_item_id(None, cx);
1895 }
1896 item.save(should_format, project, window, cx)
1897 })?
1898 .await?;
1899 } else if can_save_as && is_singleton {
1900 let abs_path = pane.update_in(cx, |pane, window, cx| {
1901 pane.activate_item(item_ix, true, true, window, cx);
1902 pane.workspace.update(cx, |workspace, cx| {
1903 workspace.prompt_for_new_path(window, cx)
1904 })
1905 })??;
1906 if let Some(abs_path) = abs_path.await.ok().flatten() {
1907 pane.update_in(cx, |pane, window, cx| {
1908 if let Some(item) = pane.item_for_path(abs_path.clone(), cx) {
1909 pane.remove_item(item.item_id(), false, false, window, cx);
1910 }
1911
1912 item.save_as(project, abs_path, window, cx)
1913 })?
1914 .await?;
1915 } else {
1916 return Ok(false);
1917 }
1918 }
1919 }
1920
1921 pane.update(cx, |_, cx| {
1922 cx.emit(Event::UserSavedItem {
1923 item: item.downgrade_item(),
1924 save_intent,
1925 });
1926 true
1927 })
1928 }
1929
1930 pub fn autosave_item(
1931 item: &dyn ItemHandle,
1932 project: Entity<Project>,
1933 window: &mut Window,
1934 cx: &mut App,
1935 ) -> Task<Result<()>> {
1936 let format = !matches!(
1937 item.workspace_settings(cx).autosave,
1938 AutosaveSetting::AfterDelay { .. }
1939 );
1940 if item.can_autosave(cx) {
1941 item.save(format, project, window, cx)
1942 } else {
1943 Task::ready(Ok(()))
1944 }
1945 }
1946
1947 pub fn focus_active_item(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1948 if let Some(active_item) = self.active_item() {
1949 let focus_handle = active_item.item_focus_handle(cx);
1950 window.focus(&focus_handle);
1951 }
1952 }
1953
1954 pub fn split(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
1955 cx.emit(Event::Split(direction));
1956 }
1957
1958 pub fn toolbar(&self) -> &Entity<Toolbar> {
1959 &self.toolbar
1960 }
1961
1962 pub fn handle_deleted_project_item(
1963 &mut self,
1964 entry_id: ProjectEntryId,
1965 window: &mut Window,
1966 cx: &mut Context<Pane>,
1967 ) -> Option<()> {
1968 let item_id = self.items().find_map(|item| {
1969 if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
1970 Some(item.item_id())
1971 } else {
1972 None
1973 }
1974 })?;
1975
1976 self.remove_item(item_id, false, true, window, cx);
1977 self.nav_history.remove_item(item_id);
1978
1979 Some(())
1980 }
1981
1982 fn update_toolbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1983 let active_item = self
1984 .items
1985 .get(self.active_item_index)
1986 .map(|item| item.as_ref());
1987 self.toolbar.update(cx, |toolbar, cx| {
1988 toolbar.set_active_item(active_item, window, cx);
1989 });
1990 }
1991
1992 fn update_status_bar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1993 let workspace = self.workspace.clone();
1994 let pane = cx.entity().clone();
1995
1996 window.defer(cx, move |window, cx| {
1997 let Ok(status_bar) =
1998 workspace.read_with(cx, |workspace, _| workspace.status_bar.clone())
1999 else {
2000 return;
2001 };
2002
2003 status_bar.update(cx, move |status_bar, cx| {
2004 status_bar.set_active_pane(&pane, window, cx);
2005 });
2006 });
2007 }
2008
2009 fn entry_abs_path(&self, entry: ProjectEntryId, cx: &App) -> Option<PathBuf> {
2010 let worktree = self
2011 .workspace
2012 .upgrade()?
2013 .read(cx)
2014 .project()
2015 .read(cx)
2016 .worktree_for_entry(entry, cx)?
2017 .read(cx);
2018 let entry = worktree.entry_for_id(entry)?;
2019 match &entry.canonical_path {
2020 Some(canonical_path) => Some(canonical_path.to_path_buf()),
2021 None => worktree.absolutize(&entry.path).ok(),
2022 }
2023 }
2024
2025 pub fn icon_color(selected: bool) -> Color {
2026 if selected {
2027 Color::Default
2028 } else {
2029 Color::Muted
2030 }
2031 }
2032
2033 fn toggle_pin_tab(&mut self, _: &TogglePinTab, window: &mut Window, cx: &mut Context<Self>) {
2034 if self.items.is_empty() {
2035 return;
2036 }
2037 let active_tab_ix = self.active_item_index();
2038 if self.is_tab_pinned(active_tab_ix) {
2039 self.unpin_tab_at(active_tab_ix, window, cx);
2040 } else {
2041 self.pin_tab_at(active_tab_ix, window, cx);
2042 }
2043 }
2044
2045 fn pin_tab_at(&mut self, ix: usize, window: &mut Window, cx: &mut Context<Self>) {
2046 maybe!({
2047 let pane = cx.entity().clone();
2048 let destination_index = self.pinned_tab_count.min(ix);
2049 self.pinned_tab_count += 1;
2050 let id = self.item_for_index(ix)?.item_id();
2051
2052 if self.is_active_preview_item(id) {
2053 self.set_preview_item_id(None, cx);
2054 }
2055
2056 self.workspace
2057 .update(cx, |_, cx| {
2058 cx.defer_in(window, move |_, window, cx| {
2059 move_item(&pane, &pane, id, destination_index, window, cx)
2060 });
2061 })
2062 .ok()?;
2063
2064 Some(())
2065 });
2066 }
2067
2068 fn unpin_tab_at(&mut self, ix: usize, window: &mut Window, cx: &mut Context<Self>) {
2069 maybe!({
2070 let pane = cx.entity().clone();
2071 self.pinned_tab_count = self.pinned_tab_count.checked_sub(1)?;
2072 let destination_index = self.pinned_tab_count;
2073
2074 let id = self.item_for_index(ix)?.item_id();
2075
2076 self.workspace
2077 .update(cx, |_, cx| {
2078 cx.defer_in(window, move |_, window, cx| {
2079 move_item(&pane, &pane, id, destination_index, window, cx)
2080 });
2081 })
2082 .ok()?;
2083
2084 Some(())
2085 });
2086 }
2087
2088 fn is_tab_pinned(&self, ix: usize) -> bool {
2089 self.pinned_tab_count > ix
2090 }
2091
2092 fn has_pinned_tabs(&self) -> bool {
2093 self.pinned_tab_count != 0
2094 }
2095
2096 fn has_unpinned_tabs(&self) -> bool {
2097 self.pinned_tab_count < self.items.len()
2098 }
2099
2100 fn activate_unpinned_tab(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2101 if self.items.is_empty() {
2102 return;
2103 }
2104 let Some(index) = self
2105 .items()
2106 .enumerate()
2107 .find_map(|(index, _item)| (!self.is_tab_pinned(index)).then_some(index))
2108 else {
2109 return;
2110 };
2111 self.activate_item(index, true, true, window, cx);
2112 }
2113
2114 fn render_tab(
2115 &self,
2116 ix: usize,
2117 item: &dyn ItemHandle,
2118 detail: usize,
2119 focus_handle: &FocusHandle,
2120 window: &mut Window,
2121 cx: &mut Context<Pane>,
2122 ) -> impl IntoElement + use<> {
2123 let is_active = ix == self.active_item_index;
2124 let is_preview = self
2125 .preview_item_id
2126 .map(|id| id == item.item_id())
2127 .unwrap_or(false);
2128
2129 let label = item.tab_content(
2130 TabContentParams {
2131 detail: Some(detail),
2132 selected: is_active,
2133 preview: is_preview,
2134 deemphasized: !self.has_focus(window, cx),
2135 },
2136 window,
2137 cx,
2138 );
2139
2140 let item_diagnostic = item
2141 .project_path(cx)
2142 .map_or(None, |project_path| self.diagnostics.get(&project_path));
2143
2144 let decorated_icon = item_diagnostic.map_or(None, |diagnostic| {
2145 let icon = match item.tab_icon(window, cx) {
2146 Some(icon) => icon,
2147 None => return None,
2148 };
2149
2150 let knockout_item_color = if is_active {
2151 cx.theme().colors().tab_active_background
2152 } else {
2153 cx.theme().colors().tab_bar_background
2154 };
2155
2156 let (icon_decoration, icon_color) = if matches!(diagnostic, &DiagnosticSeverity::ERROR)
2157 {
2158 (IconDecorationKind::X, Color::Error)
2159 } else {
2160 (IconDecorationKind::Triangle, Color::Warning)
2161 };
2162
2163 Some(DecoratedIcon::new(
2164 icon.size(IconSize::Small).color(Color::Muted),
2165 Some(
2166 IconDecoration::new(icon_decoration, knockout_item_color, cx)
2167 .color(icon_color.color(cx))
2168 .position(Point {
2169 x: px(-2.),
2170 y: px(-2.),
2171 }),
2172 ),
2173 ))
2174 });
2175
2176 let icon = if decorated_icon.is_none() {
2177 match item_diagnostic {
2178 Some(&DiagnosticSeverity::ERROR) => None,
2179 Some(&DiagnosticSeverity::WARNING) => None,
2180 _ => item
2181 .tab_icon(window, cx)
2182 .map(|icon| icon.color(Color::Muted)),
2183 }
2184 .map(|icon| icon.size(IconSize::Small))
2185 } else {
2186 None
2187 };
2188
2189 let settings = ItemSettings::get_global(cx);
2190 let close_side = &settings.close_position;
2191 let show_close_button = &settings.show_close_button;
2192 let indicator = render_item_indicator(item.boxed_clone(), cx);
2193 let item_id = item.item_id();
2194 let is_first_item = ix == 0;
2195 let is_last_item = ix == self.items.len() - 1;
2196 let is_pinned = self.is_tab_pinned(ix);
2197 let position_relative_to_active_item = ix.cmp(&self.active_item_index);
2198
2199 let tab = Tab::new(ix)
2200 .position(if is_first_item {
2201 TabPosition::First
2202 } else if is_last_item {
2203 TabPosition::Last
2204 } else {
2205 TabPosition::Middle(position_relative_to_active_item)
2206 })
2207 .close_side(match close_side {
2208 ClosePosition::Left => ui::TabCloseSide::Start,
2209 ClosePosition::Right => ui::TabCloseSide::End,
2210 })
2211 .toggle_state(is_active)
2212 .on_click(cx.listener(move |pane: &mut Self, _, window, cx| {
2213 pane.activate_item(ix, true, true, window, cx)
2214 }))
2215 // TODO: This should be a click listener with the middle mouse button instead of a mouse down listener.
2216 .on_mouse_down(
2217 MouseButton::Middle,
2218 cx.listener(move |pane, _event, window, cx| {
2219 pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
2220 .detach_and_log_err(cx);
2221 }),
2222 )
2223 .on_mouse_down(
2224 MouseButton::Left,
2225 cx.listener(move |pane, event: &MouseDownEvent, _, cx| {
2226 if let Some(id) = pane.preview_item_id {
2227 if id == item_id && event.click_count > 1 {
2228 pane.set_preview_item_id(None, cx);
2229 }
2230 }
2231 }),
2232 )
2233 .on_drag(
2234 DraggedTab {
2235 item: item.boxed_clone(),
2236 pane: cx.entity().clone(),
2237 detail,
2238 is_active,
2239 ix,
2240 },
2241 |tab, _, _, cx| cx.new(|_| tab.clone()),
2242 )
2243 .drag_over::<DraggedTab>(|tab, _, _, cx| {
2244 tab.bg(cx.theme().colors().drop_target_background)
2245 })
2246 .drag_over::<DraggedSelection>(|tab, _, _, cx| {
2247 tab.bg(cx.theme().colors().drop_target_background)
2248 })
2249 .when_some(self.can_drop_predicate.clone(), |this, p| {
2250 this.can_drop(move |a, window, cx| p(a, window, cx))
2251 })
2252 .on_drop(
2253 cx.listener(move |this, dragged_tab: &DraggedTab, window, cx| {
2254 this.drag_split_direction = None;
2255 this.handle_tab_drop(dragged_tab, ix, window, cx)
2256 }),
2257 )
2258 .on_drop(
2259 cx.listener(move |this, selection: &DraggedSelection, window, cx| {
2260 this.drag_split_direction = None;
2261 this.handle_dragged_selection_drop(selection, Some(ix), window, cx)
2262 }),
2263 )
2264 .on_drop(cx.listener(move |this, paths, window, cx| {
2265 this.drag_split_direction = None;
2266 this.handle_external_paths_drop(paths, window, cx)
2267 }))
2268 .when_some(item.tab_tooltip_content(cx), |tab, content| match content {
2269 TabTooltipContent::Text(text) => tab.tooltip(Tooltip::text(text.clone())),
2270 TabTooltipContent::Custom(element_fn) => {
2271 tab.tooltip(move |window, cx| element_fn(window, cx))
2272 }
2273 })
2274 .start_slot::<Indicator>(indicator)
2275 .map(|this| {
2276 let end_slot_action: &'static dyn Action;
2277 let end_slot_tooltip_text: &'static str;
2278 let end_slot = if is_pinned {
2279 end_slot_action = &TogglePinTab;
2280 end_slot_tooltip_text = "Unpin Tab";
2281 IconButton::new("unpin tab", IconName::Pin)
2282 .shape(IconButtonShape::Square)
2283 .icon_color(Color::Muted)
2284 .size(ButtonSize::None)
2285 .icon_size(IconSize::XSmall)
2286 .on_click(cx.listener(move |pane, _, window, cx| {
2287 pane.unpin_tab_at(ix, window, cx);
2288 }))
2289 } else {
2290 end_slot_action = &CloseActiveItem {
2291 save_intent: None,
2292 close_pinned: false,
2293 };
2294 end_slot_tooltip_text = "Close Tab";
2295 match show_close_button {
2296 ShowCloseButton::Always => IconButton::new("close tab", IconName::Close),
2297 ShowCloseButton::Hover => {
2298 IconButton::new("close tab", IconName::Close).visible_on_hover("")
2299 }
2300 ShowCloseButton::Hidden => return this,
2301 }
2302 .shape(IconButtonShape::Square)
2303 .icon_color(Color::Muted)
2304 .size(ButtonSize::None)
2305 .icon_size(IconSize::XSmall)
2306 .on_click(cx.listener(move |pane, _, window, cx| {
2307 pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
2308 .detach_and_log_err(cx);
2309 }))
2310 }
2311 .map(|this| {
2312 if is_active {
2313 let focus_handle = focus_handle.clone();
2314 this.tooltip(move |window, cx| {
2315 Tooltip::for_action_in(
2316 end_slot_tooltip_text,
2317 end_slot_action,
2318 &focus_handle,
2319 window,
2320 cx,
2321 )
2322 })
2323 } else {
2324 this.tooltip(Tooltip::text(end_slot_tooltip_text))
2325 }
2326 });
2327 this.end_slot(end_slot)
2328 })
2329 .child(
2330 h_flex()
2331 .gap_1()
2332 .items_center()
2333 .children(
2334 std::iter::once(if let Some(decorated_icon) = decorated_icon {
2335 Some(div().child(decorated_icon.into_any_element()))
2336 } else if let Some(icon) = icon {
2337 Some(div().child(icon.into_any_element()))
2338 } else {
2339 None
2340 })
2341 .flatten(),
2342 )
2343 .child(label),
2344 );
2345
2346 let single_entry_to_resolve = self.items[ix]
2347 .is_singleton(cx)
2348 .then(|| self.items[ix].project_entry_ids(cx).get(0).copied())
2349 .flatten();
2350
2351 let total_items = self.items.len();
2352 let has_items_to_left = ix > 0;
2353 let has_items_to_right = ix < total_items - 1;
2354 let has_clean_items = self.items.iter().any(|item| !item.is_dirty(cx));
2355 let is_pinned = self.is_tab_pinned(ix);
2356 let pane = cx.entity().downgrade();
2357 let menu_context = item.item_focus_handle(cx);
2358 right_click_menu(ix)
2359 .trigger(|_| tab)
2360 .menu(move |window, cx| {
2361 let pane = pane.clone();
2362 let menu_context = menu_context.clone();
2363 ContextMenu::build(window, cx, move |mut menu, window, cx| {
2364 let close_active_item_action = CloseActiveItem {
2365 save_intent: None,
2366 close_pinned: true,
2367 };
2368 let close_inactive_items_action = CloseInactiveItems {
2369 save_intent: None,
2370 close_pinned: false,
2371 };
2372 let close_items_to_the_left_action = CloseItemsToTheLeft {
2373 close_pinned: false,
2374 };
2375 let close_items_to_the_right_action = CloseItemsToTheRight {
2376 close_pinned: false,
2377 };
2378 let close_clean_items_action = CloseCleanItems {
2379 close_pinned: false,
2380 };
2381 let close_all_items_action = CloseAllItems {
2382 save_intent: None,
2383 close_pinned: false,
2384 };
2385 if let Some(pane) = pane.upgrade() {
2386 menu = menu
2387 .entry(
2388 "Close",
2389 Some(Box::new(close_active_item_action)),
2390 window.handler_for(&pane, move |pane, window, cx| {
2391 pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
2392 .detach_and_log_err(cx);
2393 }),
2394 )
2395 .item(ContextMenuItem::Entry(
2396 ContextMenuEntry::new("Close Others")
2397 .action(Box::new(close_inactive_items_action.clone()))
2398 .disabled(total_items == 1)
2399 .handler(window.handler_for(&pane, move |pane, window, cx| {
2400 pane.close_inactive_items(
2401 &close_inactive_items_action,
2402 window,
2403 cx,
2404 )
2405 .detach_and_log_err(cx);
2406 })),
2407 ))
2408 .separator()
2409 .item(ContextMenuItem::Entry(
2410 ContextMenuEntry::new("Close Left")
2411 .action(Box::new(close_items_to_the_left_action.clone()))
2412 .disabled(!has_items_to_left)
2413 .handler(window.handler_for(&pane, move |pane, window, cx| {
2414 pane.close_items_to_the_left_by_id(
2415 Some(item_id),
2416 &close_items_to_the_left_action,
2417 window,
2418 cx,
2419 )
2420 .detach_and_log_err(cx);
2421 })),
2422 ))
2423 .item(ContextMenuItem::Entry(
2424 ContextMenuEntry::new("Close Right")
2425 .action(Box::new(close_items_to_the_right_action.clone()))
2426 .disabled(!has_items_to_right)
2427 .handler(window.handler_for(&pane, move |pane, window, cx| {
2428 pane.close_items_to_the_right_by_id(
2429 Some(item_id),
2430 &close_items_to_the_right_action,
2431 window,
2432 cx,
2433 )
2434 .detach_and_log_err(cx);
2435 })),
2436 ))
2437 .separator()
2438 .item(ContextMenuItem::Entry(
2439 ContextMenuEntry::new("Close Clean")
2440 .action(Box::new(close_clean_items_action.clone()))
2441 .disabled(!has_clean_items)
2442 .handler(window.handler_for(&pane, move |pane, window, cx| {
2443 pane.close_clean_items(
2444 &close_clean_items_action,
2445 window,
2446 cx,
2447 )
2448 .detach_and_log_err(cx)
2449 })),
2450 ))
2451 .entry(
2452 "Close All",
2453 Some(Box::new(close_all_items_action.clone())),
2454 window.handler_for(&pane, move |pane, window, cx| {
2455 pane.close_all_items(&close_all_items_action, window, cx)
2456 .detach_and_log_err(cx)
2457 }),
2458 );
2459
2460 let pin_tab_entries = |menu: ContextMenu| {
2461 menu.separator().map(|this| {
2462 if is_pinned {
2463 this.entry(
2464 "Unpin Tab",
2465 Some(TogglePinTab.boxed_clone()),
2466 window.handler_for(&pane, move |pane, window, cx| {
2467 pane.unpin_tab_at(ix, window, cx);
2468 }),
2469 )
2470 } else {
2471 this.entry(
2472 "Pin Tab",
2473 Some(TogglePinTab.boxed_clone()),
2474 window.handler_for(&pane, move |pane, window, cx| {
2475 pane.pin_tab_at(ix, window, cx);
2476 }),
2477 )
2478 }
2479 })
2480 };
2481 if let Some(entry) = single_entry_to_resolve {
2482 let project_path = pane
2483 .read(cx)
2484 .item_for_entry(entry, cx)
2485 .and_then(|item| item.project_path(cx));
2486 let worktree = project_path.as_ref().and_then(|project_path| {
2487 pane.read(cx)
2488 .project
2489 .upgrade()?
2490 .read(cx)
2491 .worktree_for_id(project_path.worktree_id, cx)
2492 });
2493 let has_relative_path = worktree.as_ref().is_some_and(|worktree| {
2494 worktree
2495 .read(cx)
2496 .root_entry()
2497 .map_or(false, |entry| entry.is_dir())
2498 });
2499
2500 let entry_abs_path = pane.read(cx).entry_abs_path(entry, cx);
2501 let parent_abs_path = entry_abs_path
2502 .as_deref()
2503 .and_then(|abs_path| Some(abs_path.parent()?.to_path_buf()));
2504 let relative_path = project_path
2505 .map(|project_path| project_path.path)
2506 .filter(|_| has_relative_path);
2507
2508 let visible_in_project_panel = relative_path.is_some()
2509 && worktree.is_some_and(|worktree| worktree.read(cx).is_visible());
2510
2511 let entry_id = entry.to_proto();
2512 menu = menu
2513 .separator()
2514 .when_some(entry_abs_path, |menu, abs_path| {
2515 menu.entry(
2516 "Copy Path",
2517 Some(Box::new(zed_actions::workspace::CopyPath)),
2518 window.handler_for(&pane, move |_, _, cx| {
2519 cx.write_to_clipboard(ClipboardItem::new_string(
2520 abs_path.to_string_lossy().to_string(),
2521 ));
2522 }),
2523 )
2524 })
2525 .when_some(relative_path, |menu, relative_path| {
2526 menu.entry(
2527 "Copy Relative Path",
2528 Some(Box::new(zed_actions::workspace::CopyRelativePath)),
2529 window.handler_for(&pane, move |_, _, cx| {
2530 cx.write_to_clipboard(ClipboardItem::new_string(
2531 relative_path.to_string_lossy().to_string(),
2532 ));
2533 }),
2534 )
2535 })
2536 .map(pin_tab_entries)
2537 .separator()
2538 .when(visible_in_project_panel, |menu| {
2539 menu.entry(
2540 "Reveal In Project Panel",
2541 Some(Box::new(RevealInProjectPanel {
2542 entry_id: Some(entry_id),
2543 })),
2544 window.handler_for(&pane, move |pane, _, cx| {
2545 pane.project
2546 .update(cx, |_, cx| {
2547 cx.emit(project::Event::RevealInProjectPanel(
2548 ProjectEntryId::from_proto(entry_id),
2549 ))
2550 })
2551 .ok();
2552 }),
2553 )
2554 })
2555 .when_some(parent_abs_path, |menu, parent_abs_path| {
2556 menu.entry(
2557 "Open in Terminal",
2558 Some(Box::new(OpenInTerminal)),
2559 window.handler_for(&pane, move |_, window, cx| {
2560 window.dispatch_action(
2561 OpenTerminal {
2562 working_directory: parent_abs_path.clone(),
2563 }
2564 .boxed_clone(),
2565 cx,
2566 );
2567 }),
2568 )
2569 });
2570 } else {
2571 menu = menu.map(pin_tab_entries);
2572 }
2573 }
2574
2575 menu.context(menu_context)
2576 })
2577 })
2578 }
2579
2580 fn render_tab_bar(&mut self, window: &mut Window, cx: &mut Context<Pane>) -> AnyElement {
2581 let focus_handle = self.focus_handle.clone();
2582 let navigate_backward = IconButton::new("navigate_backward", IconName::ArrowLeft)
2583 .icon_size(IconSize::Small)
2584 .on_click({
2585 let entity = cx.entity().clone();
2586 move |_, window, cx| {
2587 entity.update(cx, |pane, cx| pane.navigate_backward(window, cx))
2588 }
2589 })
2590 .disabled(!self.can_navigate_backward())
2591 .tooltip({
2592 let focus_handle = focus_handle.clone();
2593 move |window, cx| {
2594 Tooltip::for_action_in("Go Back", &GoBack, &focus_handle, window, cx)
2595 }
2596 });
2597
2598 let navigate_forward = IconButton::new("navigate_forward", IconName::ArrowRight)
2599 .icon_size(IconSize::Small)
2600 .on_click({
2601 let entity = cx.entity().clone();
2602 move |_, window, cx| entity.update(cx, |pane, cx| pane.navigate_forward(window, cx))
2603 })
2604 .disabled(!self.can_navigate_forward())
2605 .tooltip({
2606 let focus_handle = focus_handle.clone();
2607 move |window, cx| {
2608 Tooltip::for_action_in("Go Forward", &GoForward, &focus_handle, window, cx)
2609 }
2610 });
2611
2612 let mut tab_items = self
2613 .items
2614 .iter()
2615 .enumerate()
2616 .zip(tab_details(&self.items, window, cx))
2617 .map(|((ix, item), detail)| {
2618 self.render_tab(ix, &**item, detail, &focus_handle, window, cx)
2619 })
2620 .collect::<Vec<_>>();
2621 let tab_count = tab_items.len();
2622 let unpinned_tabs = tab_items.split_off(self.pinned_tab_count);
2623 let pinned_tabs = tab_items;
2624 TabBar::new("tab_bar")
2625 .when(
2626 self.display_nav_history_buttons.unwrap_or_default(),
2627 |tab_bar| {
2628 tab_bar
2629 .start_child(navigate_backward)
2630 .start_child(navigate_forward)
2631 },
2632 )
2633 .map(|tab_bar| {
2634 if self.show_tab_bar_buttons {
2635 let render_tab_buttons = self.render_tab_bar_buttons.clone();
2636 let (left_children, right_children) = render_tab_buttons(self, window, cx);
2637 tab_bar
2638 .start_children(left_children)
2639 .end_children(right_children)
2640 } else {
2641 tab_bar
2642 }
2643 })
2644 .children(pinned_tabs.len().ne(&0).then(|| {
2645 let content_width = self.tab_bar_scroll_handle.content_size().width;
2646 let viewport_width = self.tab_bar_scroll_handle.viewport().size.width;
2647 // We need to check both because offset returns delta values even when the scroll handle is not scrollable
2648 let is_scrollable = content_width > viewport_width;
2649 let is_scrolled = self.tab_bar_scroll_handle.offset().x < px(0.);
2650 let has_active_unpinned_tab = self.active_item_index >= self.pinned_tab_count;
2651 h_flex()
2652 .children(pinned_tabs)
2653 .when(is_scrollable && is_scrolled, |this| {
2654 this.when(has_active_unpinned_tab, |this| this.border_r_2())
2655 .when(!has_active_unpinned_tab, |this| this.border_r_1())
2656 .border_color(cx.theme().colors().border)
2657 })
2658 }))
2659 .child(
2660 h_flex()
2661 .id("unpinned tabs")
2662 .overflow_x_scroll()
2663 .w_full()
2664 .track_scroll(&self.tab_bar_scroll_handle)
2665 .children(unpinned_tabs)
2666 .child(
2667 div()
2668 .id("tab_bar_drop_target")
2669 .min_w_6()
2670 // HACK: This empty child is currently necessary to force the drop target to appear
2671 // despite us setting a min width above.
2672 .child("")
2673 .h_full()
2674 .flex_grow()
2675 .drag_over::<DraggedTab>(|bar, _, _, cx| {
2676 bar.bg(cx.theme().colors().drop_target_background)
2677 })
2678 .drag_over::<DraggedSelection>(|bar, _, _, cx| {
2679 bar.bg(cx.theme().colors().drop_target_background)
2680 })
2681 .on_drop(cx.listener(
2682 move |this, dragged_tab: &DraggedTab, window, cx| {
2683 this.drag_split_direction = None;
2684 this.handle_tab_drop(dragged_tab, this.items.len(), window, cx)
2685 },
2686 ))
2687 .on_drop(cx.listener(
2688 move |this, selection: &DraggedSelection, window, cx| {
2689 this.drag_split_direction = None;
2690 this.handle_project_entry_drop(
2691 &selection.active_selection.entry_id,
2692 Some(tab_count),
2693 window,
2694 cx,
2695 )
2696 },
2697 ))
2698 .on_drop(cx.listener(move |this, paths, window, cx| {
2699 this.drag_split_direction = None;
2700 this.handle_external_paths_drop(paths, window, cx)
2701 }))
2702 .on_click(cx.listener(move |this, event: &ClickEvent, window, cx| {
2703 if event.up.click_count == 2 {
2704 window.dispatch_action(
2705 this.double_click_dispatch_action.boxed_clone(),
2706 cx,
2707 );
2708 }
2709 })),
2710 ),
2711 )
2712 .into_any_element()
2713 }
2714
2715 pub fn render_menu_overlay(menu: &Entity<ContextMenu>) -> Div {
2716 div().absolute().bottom_0().right_0().size_0().child(
2717 deferred(anchored().anchor(Corner::TopRight).child(menu.clone())).with_priority(1),
2718 )
2719 }
2720
2721 pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut Context<Self>) {
2722 self.zoomed = zoomed;
2723 cx.notify();
2724 }
2725
2726 pub fn is_zoomed(&self) -> bool {
2727 self.zoomed
2728 }
2729
2730 fn handle_drag_move<T: 'static>(
2731 &mut self,
2732 event: &DragMoveEvent<T>,
2733 window: &mut Window,
2734 cx: &mut Context<Self>,
2735 ) {
2736 let can_split_predicate = self.can_split_predicate.take();
2737 let can_split = match &can_split_predicate {
2738 Some(can_split_predicate) => {
2739 can_split_predicate(self, event.dragged_item(), window, cx)
2740 }
2741 None => false,
2742 };
2743 self.can_split_predicate = can_split_predicate;
2744 if !can_split {
2745 return;
2746 }
2747
2748 let rect = event.bounds.size;
2749
2750 let size = event.bounds.size.width.min(event.bounds.size.height)
2751 * WorkspaceSettings::get_global(cx).drop_target_size;
2752
2753 let relative_cursor = Point::new(
2754 event.event.position.x - event.bounds.left(),
2755 event.event.position.y - event.bounds.top(),
2756 );
2757
2758 let direction = if relative_cursor.x < size
2759 || relative_cursor.x > rect.width - size
2760 || relative_cursor.y < size
2761 || relative_cursor.y > rect.height - size
2762 {
2763 [
2764 SplitDirection::Up,
2765 SplitDirection::Right,
2766 SplitDirection::Down,
2767 SplitDirection::Left,
2768 ]
2769 .iter()
2770 .min_by_key(|side| match side {
2771 SplitDirection::Up => relative_cursor.y,
2772 SplitDirection::Right => rect.width - relative_cursor.x,
2773 SplitDirection::Down => rect.height - relative_cursor.y,
2774 SplitDirection::Left => relative_cursor.x,
2775 })
2776 .cloned()
2777 } else {
2778 None
2779 };
2780
2781 if direction != self.drag_split_direction {
2782 self.drag_split_direction = direction;
2783 }
2784 }
2785
2786 pub fn handle_tab_drop(
2787 &mut self,
2788 dragged_tab: &DraggedTab,
2789 ix: usize,
2790 window: &mut Window,
2791 cx: &mut Context<Self>,
2792 ) {
2793 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2794 if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, window, cx) {
2795 return;
2796 }
2797 }
2798 let mut to_pane = cx.entity().clone();
2799 let split_direction = self.drag_split_direction;
2800 let item_id = dragged_tab.item.item_id();
2801 if let Some(preview_item_id) = self.preview_item_id {
2802 if item_id == preview_item_id {
2803 self.set_preview_item_id(None, cx);
2804 }
2805 }
2806
2807 let is_clone = cfg!(target_os = "macos") && window.modifiers().alt
2808 || cfg!(not(target_os = "macos")) && window.modifiers().control;
2809
2810 let from_pane = dragged_tab.pane.clone();
2811 self.workspace
2812 .update(cx, |_, cx| {
2813 cx.defer_in(window, move |workspace, window, cx| {
2814 if let Some(split_direction) = split_direction {
2815 to_pane = workspace.split_pane(to_pane, split_direction, window, cx);
2816 }
2817 let database_id = workspace.database_id();
2818 let old_ix = from_pane.read(cx).index_for_item_id(item_id);
2819 let old_len = to_pane.read(cx).items.len();
2820 if is_clone {
2821 let Some(item) = from_pane
2822 .read(cx)
2823 .items()
2824 .find(|item| item.item_id() == item_id)
2825 .map(|item| item.clone())
2826 else {
2827 return;
2828 };
2829 if let Some(item) = item.clone_on_split(database_id, window, cx) {
2830 to_pane.update(cx, |pane, cx| {
2831 pane.add_item(item, true, true, None, window, cx);
2832 })
2833 }
2834 } else {
2835 move_item(&from_pane, &to_pane, item_id, ix, window, cx);
2836 }
2837 if to_pane == from_pane {
2838 if let Some(old_index) = old_ix {
2839 to_pane.update(cx, |this, _| {
2840 if old_index < this.pinned_tab_count
2841 && (ix == this.items.len() || ix > this.pinned_tab_count)
2842 {
2843 this.pinned_tab_count -= 1;
2844 } else if this.has_pinned_tabs()
2845 && old_index >= this.pinned_tab_count
2846 && ix < this.pinned_tab_count
2847 {
2848 this.pinned_tab_count += 1;
2849 }
2850 });
2851 }
2852 } else {
2853 to_pane.update(cx, |this, _| {
2854 if this.items.len() > old_len // Did we not deduplicate on drag?
2855 && this.has_pinned_tabs()
2856 && ix < this.pinned_tab_count
2857 {
2858 this.pinned_tab_count += 1;
2859 }
2860 });
2861 from_pane.update(cx, |this, _| {
2862 if let Some(index) = old_ix {
2863 if this.pinned_tab_count > index {
2864 this.pinned_tab_count -= 1;
2865 }
2866 }
2867 })
2868 }
2869 });
2870 })
2871 .log_err();
2872 }
2873
2874 fn handle_dragged_selection_drop(
2875 &mut self,
2876 dragged_selection: &DraggedSelection,
2877 dragged_onto: Option<usize>,
2878 window: &mut Window,
2879 cx: &mut Context<Self>,
2880 ) {
2881 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2882 if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_selection, window, cx)
2883 {
2884 return;
2885 }
2886 }
2887 self.handle_project_entry_drop(
2888 &dragged_selection.active_selection.entry_id,
2889 dragged_onto,
2890 window,
2891 cx,
2892 );
2893 }
2894
2895 fn handle_project_entry_drop(
2896 &mut self,
2897 project_entry_id: &ProjectEntryId,
2898 target: Option<usize>,
2899 window: &mut Window,
2900 cx: &mut Context<Self>,
2901 ) {
2902 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2903 if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, window, cx) {
2904 return;
2905 }
2906 }
2907 let mut to_pane = cx.entity().clone();
2908 let split_direction = self.drag_split_direction;
2909 let project_entry_id = *project_entry_id;
2910 self.workspace
2911 .update(cx, |_, cx| {
2912 cx.defer_in(window, move |workspace, window, cx| {
2913 if let Some(project_path) = workspace
2914 .project()
2915 .read(cx)
2916 .path_for_entry(project_entry_id, cx)
2917 {
2918 let load_path_task = workspace.load_path(project_path.clone(), window, cx);
2919 cx.spawn_in(window, async move |workspace, cx| {
2920 if let Some((project_entry_id, build_item)) =
2921 load_path_task.await.notify_async_err(cx)
2922 {
2923 let (to_pane, new_item_handle) = workspace
2924 .update_in(cx, |workspace, window, cx| {
2925 if let Some(split_direction) = split_direction {
2926 to_pane = workspace.split_pane(
2927 to_pane,
2928 split_direction,
2929 window,
2930 cx,
2931 );
2932 }
2933 let new_item_handle = to_pane.update(cx, |pane, cx| {
2934 pane.open_item(
2935 project_entry_id,
2936 project_path,
2937 true,
2938 false,
2939 true,
2940 target,
2941 window,
2942 cx,
2943 build_item,
2944 )
2945 });
2946 (to_pane, new_item_handle)
2947 })
2948 .log_err()?;
2949 to_pane
2950 .update_in(cx, |this, window, cx| {
2951 let Some(index) = this.index_for_item(&*new_item_handle)
2952 else {
2953 return;
2954 };
2955
2956 if target.map_or(false, |target| this.is_tab_pinned(target))
2957 {
2958 this.pin_tab_at(index, window, cx);
2959 }
2960 })
2961 .ok()?
2962 }
2963 Some(())
2964 })
2965 .detach();
2966 };
2967 });
2968 })
2969 .log_err();
2970 }
2971
2972 fn handle_external_paths_drop(
2973 &mut self,
2974 paths: &ExternalPaths,
2975 window: &mut Window,
2976 cx: &mut Context<Self>,
2977 ) {
2978 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2979 if let ControlFlow::Break(()) = custom_drop_handle(self, paths, window, cx) {
2980 return;
2981 }
2982 }
2983 let mut to_pane = cx.entity().clone();
2984 let mut split_direction = self.drag_split_direction;
2985 let paths = paths.paths().to_vec();
2986 let is_remote = self
2987 .workspace
2988 .update(cx, |workspace, cx| {
2989 if workspace.project().read(cx).is_via_collab() {
2990 workspace.show_error(
2991 &anyhow::anyhow!("Cannot drop files on a remote project"),
2992 cx,
2993 );
2994 true
2995 } else {
2996 false
2997 }
2998 })
2999 .unwrap_or(true);
3000 if is_remote {
3001 return;
3002 }
3003
3004 self.workspace
3005 .update(cx, |workspace, cx| {
3006 let fs = Arc::clone(workspace.project().read(cx).fs());
3007 cx.spawn_in(window, async move |workspace, cx| {
3008 let mut is_file_checks = FuturesUnordered::new();
3009 for path in &paths {
3010 is_file_checks.push(fs.is_file(path))
3011 }
3012 let mut has_files_to_open = false;
3013 while let Some(is_file) = is_file_checks.next().await {
3014 if is_file {
3015 has_files_to_open = true;
3016 break;
3017 }
3018 }
3019 drop(is_file_checks);
3020 if !has_files_to_open {
3021 split_direction = None;
3022 }
3023
3024 if let Ok(open_task) = workspace.update_in(cx, |workspace, window, cx| {
3025 if let Some(split_direction) = split_direction {
3026 to_pane = workspace.split_pane(to_pane, split_direction, window, cx);
3027 }
3028 workspace.open_paths(
3029 paths,
3030 OpenOptions {
3031 visible: Some(OpenVisible::OnlyDirectories),
3032 ..Default::default()
3033 },
3034 Some(to_pane.downgrade()),
3035 window,
3036 cx,
3037 )
3038 }) {
3039 let opened_items: Vec<_> = open_task.await;
3040 _ = workspace.update(cx, |workspace, cx| {
3041 for item in opened_items.into_iter().flatten() {
3042 if let Err(e) = item {
3043 workspace.show_error(&e, cx);
3044 }
3045 }
3046 });
3047 }
3048 })
3049 .detach();
3050 })
3051 .log_err();
3052 }
3053
3054 pub fn display_nav_history_buttons(&mut self, display: Option<bool>) {
3055 self.display_nav_history_buttons = display;
3056 }
3057
3058 fn pinned_item_ids(&self) -> HashSet<EntityId> {
3059 self.items
3060 .iter()
3061 .enumerate()
3062 .filter_map(|(index, item)| {
3063 if self.is_tab_pinned(index) {
3064 return Some(item.item_id());
3065 }
3066
3067 None
3068 })
3069 .collect()
3070 }
3071
3072 fn clean_item_ids(&self, cx: &mut Context<Pane>) -> HashSet<EntityId> {
3073 self.items()
3074 .filter_map(|item| {
3075 if !item.is_dirty(cx) {
3076 return Some(item.item_id());
3077 }
3078
3079 None
3080 })
3081 .collect()
3082 }
3083
3084 fn to_the_side_item_ids(&self, item_id: EntityId, side: Side) -> HashSet<EntityId> {
3085 match side {
3086 Side::Left => self
3087 .items()
3088 .take_while(|item| item.item_id() != item_id)
3089 .map(|item| item.item_id())
3090 .collect(),
3091 Side::Right => self
3092 .items()
3093 .rev()
3094 .take_while(|item| item.item_id() != item_id)
3095 .map(|item| item.item_id())
3096 .collect(),
3097 }
3098 }
3099
3100 pub fn drag_split_direction(&self) -> Option<SplitDirection> {
3101 self.drag_split_direction
3102 }
3103
3104 pub fn set_zoom_out_on_close(&mut self, zoom_out_on_close: bool) {
3105 self.zoom_out_on_close = zoom_out_on_close;
3106 }
3107}
3108
3109fn default_render_tab_bar_buttons(
3110 pane: &mut Pane,
3111 window: &mut Window,
3112 cx: &mut Context<Pane>,
3113) -> (Option<AnyElement>, Option<AnyElement>) {
3114 if !pane.has_focus(window, cx) && !pane.context_menu_focused(window, cx) {
3115 return (None, None);
3116 }
3117 // Ideally we would return a vec of elements here to pass directly to the [TabBar]'s
3118 // `end_slot`, but due to needing a view here that isn't possible.
3119 let right_children = h_flex()
3120 // Instead we need to replicate the spacing from the [TabBar]'s `end_slot` here.
3121 .gap(DynamicSpacing::Base04.rems(cx))
3122 .child(
3123 PopoverMenu::new("pane-tab-bar-popover-menu")
3124 .trigger_with_tooltip(
3125 IconButton::new("plus", IconName::Plus).icon_size(IconSize::Small),
3126 Tooltip::text("New..."),
3127 )
3128 .anchor(Corner::TopRight)
3129 .with_handle(pane.new_item_context_menu_handle.clone())
3130 .menu(move |window, cx| {
3131 Some(ContextMenu::build(window, cx, |menu, _, _| {
3132 menu.action("New File", NewFile.boxed_clone())
3133 .action("Open File", ToggleFileFinder::default().boxed_clone())
3134 .separator()
3135 .action(
3136 "Search Project",
3137 DeploySearch {
3138 replace_enabled: false,
3139 included_files: None,
3140 excluded_files: None,
3141 }
3142 .boxed_clone(),
3143 )
3144 .action("Search Symbols", ToggleProjectSymbols.boxed_clone())
3145 .separator()
3146 .action("New Terminal", NewTerminal.boxed_clone())
3147 }))
3148 }),
3149 )
3150 .child(
3151 PopoverMenu::new("pane-tab-bar-split")
3152 .trigger_with_tooltip(
3153 IconButton::new("split", IconName::Split).icon_size(IconSize::Small),
3154 Tooltip::text("Split Pane"),
3155 )
3156 .anchor(Corner::TopRight)
3157 .with_handle(pane.split_item_context_menu_handle.clone())
3158 .menu(move |window, cx| {
3159 ContextMenu::build(window, cx, |menu, _, _| {
3160 menu.action("Split Right", SplitRight.boxed_clone())
3161 .action("Split Left", SplitLeft.boxed_clone())
3162 .action("Split Up", SplitUp.boxed_clone())
3163 .action("Split Down", SplitDown.boxed_clone())
3164 })
3165 .into()
3166 }),
3167 )
3168 .child({
3169 let zoomed = pane.is_zoomed();
3170 IconButton::new("toggle_zoom", IconName::Maximize)
3171 .icon_size(IconSize::Small)
3172 .toggle_state(zoomed)
3173 .selected_icon(IconName::Minimize)
3174 .on_click(cx.listener(|pane, _, window, cx| {
3175 pane.toggle_zoom(&crate::ToggleZoom, window, cx);
3176 }))
3177 .tooltip(move |window, cx| {
3178 Tooltip::for_action(
3179 if zoomed { "Zoom Out" } else { "Zoom In" },
3180 &ToggleZoom,
3181 window,
3182 cx,
3183 )
3184 })
3185 })
3186 .into_any_element()
3187 .into();
3188 (None, right_children)
3189}
3190
3191impl Focusable for Pane {
3192 fn focus_handle(&self, _cx: &App) -> FocusHandle {
3193 self.focus_handle.clone()
3194 }
3195}
3196
3197impl Render for Pane {
3198 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3199 let mut key_context = KeyContext::new_with_defaults();
3200 key_context.add("Pane");
3201 if self.active_item().is_none() {
3202 key_context.add("EmptyPane");
3203 }
3204
3205 let should_display_tab_bar = self.should_display_tab_bar.clone();
3206 let display_tab_bar = should_display_tab_bar(window, cx);
3207 let Some(project) = self.project.upgrade() else {
3208 return div().track_focus(&self.focus_handle(cx));
3209 };
3210 let is_local = project.read(cx).is_local();
3211
3212 v_flex()
3213 .key_context(key_context)
3214 .track_focus(&self.focus_handle(cx))
3215 .size_full()
3216 .flex_none()
3217 .overflow_hidden()
3218 .on_action(cx.listener(|pane, _: &AlternateFile, window, cx| {
3219 pane.alternate_file(window, cx);
3220 }))
3221 .on_action(
3222 cx.listener(|pane, _: &SplitLeft, _, cx| pane.split(SplitDirection::Left, cx)),
3223 )
3224 .on_action(cx.listener(|pane, _: &SplitUp, _, cx| pane.split(SplitDirection::Up, cx)))
3225 .on_action(cx.listener(|pane, _: &SplitHorizontal, _, cx| {
3226 pane.split(SplitDirection::horizontal(cx), cx)
3227 }))
3228 .on_action(cx.listener(|pane, _: &SplitVertical, _, cx| {
3229 pane.split(SplitDirection::vertical(cx), cx)
3230 }))
3231 .on_action(
3232 cx.listener(|pane, _: &SplitRight, _, cx| pane.split(SplitDirection::Right, cx)),
3233 )
3234 .on_action(
3235 cx.listener(|pane, _: &SplitDown, _, cx| pane.split(SplitDirection::Down, cx)),
3236 )
3237 .on_action(
3238 cx.listener(|pane, _: &GoBack, window, cx| pane.navigate_backward(window, cx)),
3239 )
3240 .on_action(
3241 cx.listener(|pane, _: &GoForward, window, cx| pane.navigate_forward(window, cx)),
3242 )
3243 .on_action(cx.listener(|_, _: &JoinIntoNext, _, cx| {
3244 cx.emit(Event::JoinIntoNext);
3245 }))
3246 .on_action(cx.listener(|_, _: &JoinAll, _, cx| {
3247 cx.emit(Event::JoinAll);
3248 }))
3249 .on_action(cx.listener(Pane::toggle_zoom))
3250 .on_action(
3251 cx.listener(|pane: &mut Pane, action: &ActivateItem, window, cx| {
3252 pane.activate_item(
3253 action.0.min(pane.items.len().saturating_sub(1)),
3254 true,
3255 true,
3256 window,
3257 cx,
3258 );
3259 }),
3260 )
3261 .on_action(
3262 cx.listener(|pane: &mut Pane, _: &ActivateLastItem, window, cx| {
3263 pane.activate_item(pane.items.len().saturating_sub(1), true, true, window, cx);
3264 }),
3265 )
3266 .on_action(
3267 cx.listener(|pane: &mut Pane, _: &ActivatePreviousItem, window, cx| {
3268 pane.activate_prev_item(true, window, cx);
3269 }),
3270 )
3271 .on_action(
3272 cx.listener(|pane: &mut Pane, _: &ActivateNextItem, window, cx| {
3273 pane.activate_next_item(true, window, cx);
3274 }),
3275 )
3276 .on_action(
3277 cx.listener(|pane, _: &SwapItemLeft, window, cx| pane.swap_item_left(window, cx)),
3278 )
3279 .on_action(
3280 cx.listener(|pane, _: &SwapItemRight, window, cx| pane.swap_item_right(window, cx)),
3281 )
3282 .on_action(cx.listener(|pane, action, window, cx| {
3283 pane.toggle_pin_tab(action, window, cx);
3284 }))
3285 .when(PreviewTabsSettings::get_global(cx).enabled, |this| {
3286 this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, _, cx| {
3287 if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) {
3288 if pane.is_active_preview_item(active_item_id) {
3289 pane.set_preview_item_id(None, cx);
3290 } else {
3291 pane.set_preview_item_id(Some(active_item_id), cx);
3292 }
3293 }
3294 }))
3295 })
3296 .on_action(
3297 cx.listener(|pane: &mut Self, action: &CloseActiveItem, window, cx| {
3298 pane.close_active_item(action, window, cx)
3299 .detach_and_log_err(cx)
3300 }),
3301 )
3302 .on_action(
3303 cx.listener(|pane: &mut Self, action: &CloseInactiveItems, window, cx| {
3304 pane.close_inactive_items(action, window, cx)
3305 .detach_and_log_err(cx);
3306 }),
3307 )
3308 .on_action(
3309 cx.listener(|pane: &mut Self, action: &CloseCleanItems, window, cx| {
3310 pane.close_clean_items(action, window, cx)
3311 .detach_and_log_err(cx)
3312 }),
3313 )
3314 .on_action(cx.listener(
3315 |pane: &mut Self, action: &CloseItemsToTheLeft, window, cx| {
3316 pane.close_items_to_the_left_by_id(None, action, window, cx)
3317 .detach_and_log_err(cx)
3318 },
3319 ))
3320 .on_action(cx.listener(
3321 |pane: &mut Self, action: &CloseItemsToTheRight, window, cx| {
3322 pane.close_items_to_the_right_by_id(None, action, window, cx)
3323 .detach_and_log_err(cx)
3324 },
3325 ))
3326 .on_action(
3327 cx.listener(|pane: &mut Self, action: &CloseAllItems, window, cx| {
3328 pane.close_all_items(action, window, cx)
3329 .detach_and_log_err(cx)
3330 }),
3331 )
3332 .on_action(
3333 cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, _, cx| {
3334 let entry_id = action
3335 .entry_id
3336 .map(ProjectEntryId::from_proto)
3337 .or_else(|| pane.active_item()?.project_entry_ids(cx).first().copied());
3338 if let Some(entry_id) = entry_id {
3339 pane.project
3340 .update(cx, |_, cx| {
3341 cx.emit(project::Event::RevealInProjectPanel(entry_id))
3342 })
3343 .ok();
3344 }
3345 }),
3346 )
3347 .on_action(cx.listener(|_, _: &menu::Cancel, window, cx| {
3348 if cx.stop_active_drag(window) {
3349 return;
3350 } else {
3351 cx.propagate();
3352 }
3353 }))
3354 .when(self.active_item().is_some() && display_tab_bar, |pane| {
3355 pane.child((self.render_tab_bar.clone())(self, window, cx))
3356 })
3357 .child({
3358 let has_worktrees = project.read(cx).visible_worktrees(cx).next().is_some();
3359 // main content
3360 div()
3361 .flex_1()
3362 .relative()
3363 .group("")
3364 .overflow_hidden()
3365 .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
3366 .on_drag_move::<DraggedSelection>(cx.listener(Self::handle_drag_move))
3367 .when(is_local, |div| {
3368 div.on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
3369 })
3370 .map(|div| {
3371 if let Some(item) = self.active_item() {
3372 div.id("pane_placeholder")
3373 .v_flex()
3374 .size_full()
3375 .overflow_hidden()
3376 .child(self.toolbar.clone())
3377 .child(item.to_any())
3378 } else {
3379 let placeholder = div
3380 .id("pane_placeholder")
3381 .h_flex()
3382 .size_full()
3383 .justify_center()
3384 .on_click(cx.listener(
3385 move |this, event: &ClickEvent, window, cx| {
3386 if event.up.click_count == 2 {
3387 window.dispatch_action(
3388 this.double_click_dispatch_action.boxed_clone(),
3389 cx,
3390 );
3391 }
3392 },
3393 ));
3394 if has_worktrees {
3395 placeholder
3396 } else {
3397 placeholder.child(
3398 Label::new("Open a file or project to get started.")
3399 .color(Color::Muted),
3400 )
3401 }
3402 }
3403 })
3404 .child(
3405 // drag target
3406 div()
3407 .invisible()
3408 .absolute()
3409 .bg(cx.theme().colors().drop_target_background)
3410 .group_drag_over::<DraggedTab>("", |style| style.visible())
3411 .group_drag_over::<DraggedSelection>("", |style| style.visible())
3412 .when(is_local, |div| {
3413 div.group_drag_over::<ExternalPaths>("", |style| style.visible())
3414 })
3415 .when_some(self.can_drop_predicate.clone(), |this, p| {
3416 this.can_drop(move |a, window, cx| p(a, window, cx))
3417 })
3418 .on_drop(cx.listener(move |this, dragged_tab, window, cx| {
3419 this.handle_tab_drop(
3420 dragged_tab,
3421 this.active_item_index(),
3422 window,
3423 cx,
3424 )
3425 }))
3426 .on_drop(cx.listener(
3427 move |this, selection: &DraggedSelection, window, cx| {
3428 this.handle_dragged_selection_drop(selection, None, window, cx)
3429 },
3430 ))
3431 .on_drop(cx.listener(move |this, paths, window, cx| {
3432 this.handle_external_paths_drop(paths, window, cx)
3433 }))
3434 .map(|div| {
3435 let size = DefiniteLength::Fraction(0.5);
3436 match self.drag_split_direction {
3437 None => div.top_0().right_0().bottom_0().left_0(),
3438 Some(SplitDirection::Up) => {
3439 div.top_0().left_0().right_0().h(size)
3440 }
3441 Some(SplitDirection::Down) => {
3442 div.left_0().bottom_0().right_0().h(size)
3443 }
3444 Some(SplitDirection::Left) => {
3445 div.top_0().left_0().bottom_0().w(size)
3446 }
3447 Some(SplitDirection::Right) => {
3448 div.top_0().bottom_0().right_0().w(size)
3449 }
3450 }
3451 }),
3452 )
3453 })
3454 .on_mouse_down(
3455 MouseButton::Navigate(NavigationDirection::Back),
3456 cx.listener(|pane, _, window, cx| {
3457 if let Some(workspace) = pane.workspace.upgrade() {
3458 let pane = cx.entity().downgrade();
3459 window.defer(cx, move |window, cx| {
3460 workspace.update(cx, |workspace, cx| {
3461 workspace.go_back(pane, window, cx).detach_and_log_err(cx)
3462 })
3463 })
3464 }
3465 }),
3466 )
3467 .on_mouse_down(
3468 MouseButton::Navigate(NavigationDirection::Forward),
3469 cx.listener(|pane, _, window, cx| {
3470 if let Some(workspace) = pane.workspace.upgrade() {
3471 let pane = cx.entity().downgrade();
3472 window.defer(cx, move |window, cx| {
3473 workspace.update(cx, |workspace, cx| {
3474 workspace
3475 .go_forward(pane, window, cx)
3476 .detach_and_log_err(cx)
3477 })
3478 })
3479 }
3480 }),
3481 )
3482 }
3483}
3484
3485impl ItemNavHistory {
3486 pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut App) {
3487 if self
3488 .item
3489 .upgrade()
3490 .is_some_and(|item| item.include_in_nav_history())
3491 {
3492 self.history
3493 .push(data, self.item.clone(), self.is_preview, cx);
3494 }
3495 }
3496
3497 pub fn pop_backward(&mut self, cx: &mut App) -> Option<NavigationEntry> {
3498 self.history.pop(NavigationMode::GoingBack, cx)
3499 }
3500
3501 pub fn pop_forward(&mut self, cx: &mut App) -> Option<NavigationEntry> {
3502 self.history.pop(NavigationMode::GoingForward, cx)
3503 }
3504}
3505
3506impl NavHistory {
3507 pub fn for_each_entry(
3508 &self,
3509 cx: &App,
3510 mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
3511 ) {
3512 let borrowed_history = self.0.lock();
3513 borrowed_history
3514 .forward_stack
3515 .iter()
3516 .chain(borrowed_history.backward_stack.iter())
3517 .chain(borrowed_history.closed_stack.iter())
3518 .for_each(|entry| {
3519 if let Some(project_and_abs_path) =
3520 borrowed_history.paths_by_item.get(&entry.item.id())
3521 {
3522 f(entry, project_and_abs_path.clone());
3523 } else if let Some(item) = entry.item.upgrade() {
3524 if let Some(path) = item.project_path(cx) {
3525 f(entry, (path, None));
3526 }
3527 }
3528 })
3529 }
3530
3531 pub fn set_mode(&mut self, mode: NavigationMode) {
3532 self.0.lock().mode = mode;
3533 }
3534
3535 pub fn mode(&self) -> NavigationMode {
3536 self.0.lock().mode
3537 }
3538
3539 pub fn disable(&mut self) {
3540 self.0.lock().mode = NavigationMode::Disabled;
3541 }
3542
3543 pub fn enable(&mut self) {
3544 self.0.lock().mode = NavigationMode::Normal;
3545 }
3546
3547 pub fn pop(&mut self, mode: NavigationMode, cx: &mut App) -> Option<NavigationEntry> {
3548 let mut state = self.0.lock();
3549 let entry = match mode {
3550 NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
3551 return None;
3552 }
3553 NavigationMode::GoingBack => &mut state.backward_stack,
3554 NavigationMode::GoingForward => &mut state.forward_stack,
3555 NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
3556 }
3557 .pop_back();
3558 if entry.is_some() {
3559 state.did_update(cx);
3560 }
3561 entry
3562 }
3563
3564 pub fn push<D: 'static + Send + Any>(
3565 &mut self,
3566 data: Option<D>,
3567 item: Arc<dyn WeakItemHandle>,
3568 is_preview: bool,
3569 cx: &mut App,
3570 ) {
3571 let state = &mut *self.0.lock();
3572 match state.mode {
3573 NavigationMode::Disabled => {}
3574 NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
3575 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3576 state.backward_stack.pop_front();
3577 }
3578 state.backward_stack.push_back(NavigationEntry {
3579 item,
3580 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3581 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3582 is_preview,
3583 });
3584 state.forward_stack.clear();
3585 }
3586 NavigationMode::GoingBack => {
3587 if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3588 state.forward_stack.pop_front();
3589 }
3590 state.forward_stack.push_back(NavigationEntry {
3591 item,
3592 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3593 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3594 is_preview,
3595 });
3596 }
3597 NavigationMode::GoingForward => {
3598 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3599 state.backward_stack.pop_front();
3600 }
3601 state.backward_stack.push_back(NavigationEntry {
3602 item,
3603 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3604 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3605 is_preview,
3606 });
3607 }
3608 NavigationMode::ClosingItem => {
3609 if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3610 state.closed_stack.pop_front();
3611 }
3612 state.closed_stack.push_back(NavigationEntry {
3613 item,
3614 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3615 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3616 is_preview,
3617 });
3618 }
3619 }
3620 state.did_update(cx);
3621 }
3622
3623 pub fn remove_item(&mut self, item_id: EntityId) {
3624 let mut state = self.0.lock();
3625 state.paths_by_item.remove(&item_id);
3626 state
3627 .backward_stack
3628 .retain(|entry| entry.item.id() != item_id);
3629 state
3630 .forward_stack
3631 .retain(|entry| entry.item.id() != item_id);
3632 state
3633 .closed_stack
3634 .retain(|entry| entry.item.id() != item_id);
3635 }
3636
3637 pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
3638 self.0.lock().paths_by_item.get(&item_id).cloned()
3639 }
3640}
3641
3642impl NavHistoryState {
3643 pub fn did_update(&self, cx: &mut App) {
3644 if let Some(pane) = self.pane.upgrade() {
3645 cx.defer(move |cx| {
3646 pane.update(cx, |pane, cx| pane.history_updated(cx));
3647 });
3648 }
3649 }
3650}
3651
3652fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
3653 let path = buffer_path
3654 .as_ref()
3655 .and_then(|p| {
3656 p.path
3657 .to_str()
3658 .and_then(|s| if s.is_empty() { None } else { Some(s) })
3659 })
3660 .unwrap_or("This buffer");
3661 let path = truncate_and_remove_front(path, 80);
3662 format!("{path} contains unsaved edits. Do you want to save it?")
3663}
3664
3665pub fn tab_details(items: &[Box<dyn ItemHandle>], _window: &Window, cx: &App) -> Vec<usize> {
3666 let mut tab_details = items.iter().map(|_| 0).collect::<Vec<_>>();
3667 let mut tab_descriptions = HashMap::default();
3668 let mut done = false;
3669 while !done {
3670 done = true;
3671
3672 // Store item indices by their tab description.
3673 for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() {
3674 let description = item.tab_content_text(*detail, cx);
3675 if *detail == 0 || description != item.tab_content_text(detail - 1, cx) {
3676 tab_descriptions
3677 .entry(description)
3678 .or_insert(Vec::new())
3679 .push(ix);
3680 }
3681 }
3682
3683 // If two or more items have the same tab description, increase their level
3684 // of detail and try again.
3685 for (_, item_ixs) in tab_descriptions.drain() {
3686 if item_ixs.len() > 1 {
3687 done = false;
3688 for ix in item_ixs {
3689 tab_details[ix] += 1;
3690 }
3691 }
3692 }
3693 }
3694
3695 tab_details
3696}
3697
3698pub fn render_item_indicator(item: Box<dyn ItemHandle>, cx: &App) -> Option<Indicator> {
3699 maybe!({
3700 let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
3701 (true, _) => Color::Warning,
3702 (_, true) => Color::Accent,
3703 (false, false) => return None,
3704 };
3705
3706 Some(Indicator::dot().color(indicator_color))
3707 })
3708}
3709
3710impl Render for DraggedTab {
3711 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3712 let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
3713 let label = self.item.tab_content(
3714 TabContentParams {
3715 detail: Some(self.detail),
3716 selected: false,
3717 preview: false,
3718 deemphasized: false,
3719 },
3720 window,
3721 cx,
3722 );
3723 Tab::new("")
3724 .toggle_state(self.is_active)
3725 .child(label)
3726 .render(window, cx)
3727 .font(ui_font)
3728 }
3729}
3730
3731#[cfg(test)]
3732mod tests {
3733 use std::num::NonZero;
3734
3735 use super::*;
3736 use crate::item::test::{TestItem, TestProjectItem};
3737 use gpui::{TestAppContext, VisualTestContext};
3738 use project::FakeFs;
3739 use settings::SettingsStore;
3740 use theme::LoadThemes;
3741 use util::TryFutureExt;
3742
3743 #[gpui::test]
3744 async fn test_add_item_capped_to_max_tabs(cx: &mut TestAppContext) {
3745 init_test(cx);
3746 let fs = FakeFs::new(cx.executor());
3747
3748 let project = Project::test(fs, None, cx).await;
3749 let (workspace, cx) =
3750 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3751 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3752
3753 for i in 0..7 {
3754 add_labeled_item(&pane, format!("{}", i).as_str(), false, cx);
3755 }
3756 set_max_tabs(cx, Some(5));
3757 add_labeled_item(&pane, "7", false, cx);
3758 // Remove items to respect the max tab cap.
3759 assert_item_labels(&pane, ["3", "4", "5", "6", "7*"], cx);
3760 pane.update_in(cx, |pane, window, cx| {
3761 pane.activate_item(0, false, false, window, cx);
3762 });
3763 add_labeled_item(&pane, "X", false, cx);
3764 // Respect activation order.
3765 assert_item_labels(&pane, ["3", "X*", "5", "6", "7"], cx);
3766
3767 for i in 0..7 {
3768 add_labeled_item(&pane, format!("D{}", i).as_str(), true, cx);
3769 }
3770 // Keeps dirty items, even over max tab cap.
3771 assert_item_labels(
3772 &pane,
3773 ["D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6*^"],
3774 cx,
3775 );
3776
3777 set_max_tabs(cx, None);
3778 for i in 0..7 {
3779 add_labeled_item(&pane, format!("N{}", i).as_str(), false, cx);
3780 }
3781 // No cap when max tabs is None.
3782 assert_item_labels(
3783 &pane,
3784 [
3785 "D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6^", "N0", "N1", "N2", "N3", "N4",
3786 "N5", "N6*",
3787 ],
3788 cx,
3789 );
3790 }
3791
3792 #[gpui::test]
3793 async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
3794 init_test(cx);
3795 let fs = FakeFs::new(cx.executor());
3796
3797 let project = Project::test(fs, None, cx).await;
3798 let (workspace, cx) =
3799 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3800 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3801
3802 // 1. Add with a destination index
3803 // a. Add before the active item
3804 set_labeled_items(&pane, ["A", "B*", "C"], cx);
3805 pane.update_in(cx, |pane, window, cx| {
3806 pane.add_item(
3807 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3808 false,
3809 false,
3810 Some(0),
3811 window,
3812 cx,
3813 );
3814 });
3815 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3816
3817 // b. Add after the active item
3818 set_labeled_items(&pane, ["A", "B*", "C"], cx);
3819 pane.update_in(cx, |pane, window, cx| {
3820 pane.add_item(
3821 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3822 false,
3823 false,
3824 Some(2),
3825 window,
3826 cx,
3827 );
3828 });
3829 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3830
3831 // c. Add at the end of the item list (including off the length)
3832 set_labeled_items(&pane, ["A", "B*", "C"], cx);
3833 pane.update_in(cx, |pane, window, cx| {
3834 pane.add_item(
3835 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3836 false,
3837 false,
3838 Some(5),
3839 window,
3840 cx,
3841 );
3842 });
3843 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3844
3845 // 2. Add without a destination index
3846 // a. Add with active item at the start of the item list
3847 set_labeled_items(&pane, ["A*", "B", "C"], cx);
3848 pane.update_in(cx, |pane, window, cx| {
3849 pane.add_item(
3850 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3851 false,
3852 false,
3853 None,
3854 window,
3855 cx,
3856 );
3857 });
3858 set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
3859
3860 // b. Add with active item at the end of the item list
3861 set_labeled_items(&pane, ["A", "B", "C*"], cx);
3862 pane.update_in(cx, |pane, window, cx| {
3863 pane.add_item(
3864 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3865 false,
3866 false,
3867 None,
3868 window,
3869 cx,
3870 );
3871 });
3872 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3873 }
3874
3875 #[gpui::test]
3876 async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
3877 init_test(cx);
3878 let fs = FakeFs::new(cx.executor());
3879
3880 let project = Project::test(fs, None, cx).await;
3881 let (workspace, cx) =
3882 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3883 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3884
3885 // 1. Add with a destination index
3886 // 1a. Add before the active item
3887 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3888 pane.update_in(cx, |pane, window, cx| {
3889 pane.add_item(d, false, false, Some(0), window, cx);
3890 });
3891 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3892
3893 // 1b. Add after the active item
3894 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3895 pane.update_in(cx, |pane, window, cx| {
3896 pane.add_item(d, false, false, Some(2), window, cx);
3897 });
3898 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3899
3900 // 1c. Add at the end of the item list (including off the length)
3901 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3902 pane.update_in(cx, |pane, window, cx| {
3903 pane.add_item(a, false, false, Some(5), window, cx);
3904 });
3905 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3906
3907 // 1d. Add same item to active index
3908 let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3909 pane.update_in(cx, |pane, window, cx| {
3910 pane.add_item(b, false, false, Some(1), window, cx);
3911 });
3912 assert_item_labels(&pane, ["A", "B*", "C"], cx);
3913
3914 // 1e. Add item to index after same item in last position
3915 let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3916 pane.update_in(cx, |pane, window, cx| {
3917 pane.add_item(c, false, false, Some(2), window, cx);
3918 });
3919 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3920
3921 // 2. Add without a destination index
3922 // 2a. Add with active item at the start of the item list
3923 let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
3924 pane.update_in(cx, |pane, window, cx| {
3925 pane.add_item(d, false, false, None, window, cx);
3926 });
3927 assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
3928
3929 // 2b. Add with active item at the end of the item list
3930 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
3931 pane.update_in(cx, |pane, window, cx| {
3932 pane.add_item(a, false, false, None, window, cx);
3933 });
3934 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3935
3936 // 2c. Add active item to active item at end of list
3937 let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
3938 pane.update_in(cx, |pane, window, cx| {
3939 pane.add_item(c, false, false, None, window, cx);
3940 });
3941 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3942
3943 // 2d. Add active item to active item at start of list
3944 let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
3945 pane.update_in(cx, |pane, window, cx| {
3946 pane.add_item(a, false, false, None, window, cx);
3947 });
3948 assert_item_labels(&pane, ["A*", "B", "C"], cx);
3949 }
3950
3951 #[gpui::test]
3952 async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
3953 init_test(cx);
3954 let fs = FakeFs::new(cx.executor());
3955
3956 let project = Project::test(fs, None, cx).await;
3957 let (workspace, cx) =
3958 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3959 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3960
3961 // singleton view
3962 pane.update_in(cx, |pane, window, cx| {
3963 pane.add_item(
3964 Box::new(cx.new(|cx| {
3965 TestItem::new(cx)
3966 .with_singleton(true)
3967 .with_label("buffer 1")
3968 .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
3969 })),
3970 false,
3971 false,
3972 None,
3973 window,
3974 cx,
3975 );
3976 });
3977 assert_item_labels(&pane, ["buffer 1*"], cx);
3978
3979 // new singleton view with the same project entry
3980 pane.update_in(cx, |pane, window, cx| {
3981 pane.add_item(
3982 Box::new(cx.new(|cx| {
3983 TestItem::new(cx)
3984 .with_singleton(true)
3985 .with_label("buffer 1")
3986 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3987 })),
3988 false,
3989 false,
3990 None,
3991 window,
3992 cx,
3993 );
3994 });
3995 assert_item_labels(&pane, ["buffer 1*"], cx);
3996
3997 // new singleton view with different project entry
3998 pane.update_in(cx, |pane, window, cx| {
3999 pane.add_item(
4000 Box::new(cx.new(|cx| {
4001 TestItem::new(cx)
4002 .with_singleton(true)
4003 .with_label("buffer 2")
4004 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
4005 })),
4006 false,
4007 false,
4008 None,
4009 window,
4010 cx,
4011 );
4012 });
4013 assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
4014
4015 // new multibuffer view with the same project entry
4016 pane.update_in(cx, |pane, window, cx| {
4017 pane.add_item(
4018 Box::new(cx.new(|cx| {
4019 TestItem::new(cx)
4020 .with_singleton(false)
4021 .with_label("multibuffer 1")
4022 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4023 })),
4024 false,
4025 false,
4026 None,
4027 window,
4028 cx,
4029 );
4030 });
4031 assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
4032
4033 // another multibuffer view with the same project entry
4034 pane.update_in(cx, |pane, window, cx| {
4035 pane.add_item(
4036 Box::new(cx.new(|cx| {
4037 TestItem::new(cx)
4038 .with_singleton(false)
4039 .with_label("multibuffer 1b")
4040 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4041 })),
4042 false,
4043 false,
4044 None,
4045 window,
4046 cx,
4047 );
4048 });
4049 assert_item_labels(
4050 &pane,
4051 ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
4052 cx,
4053 );
4054 }
4055
4056 #[gpui::test]
4057 async fn test_remove_item_ordering_history(cx: &mut TestAppContext) {
4058 init_test(cx);
4059 let fs = FakeFs::new(cx.executor());
4060
4061 let project = Project::test(fs, None, cx).await;
4062 let (workspace, cx) =
4063 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4064 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4065
4066 add_labeled_item(&pane, "A", false, cx);
4067 add_labeled_item(&pane, "B", false, cx);
4068 add_labeled_item(&pane, "C", false, cx);
4069 add_labeled_item(&pane, "D", false, cx);
4070 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4071
4072 pane.update_in(cx, |pane, window, cx| {
4073 pane.activate_item(1, false, false, window, cx)
4074 });
4075 add_labeled_item(&pane, "1", false, cx);
4076 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
4077
4078 pane.update_in(cx, |pane, window, cx| {
4079 pane.close_active_item(
4080 &CloseActiveItem {
4081 save_intent: None,
4082 close_pinned: false,
4083 },
4084 window,
4085 cx,
4086 )
4087 })
4088 .await
4089 .unwrap();
4090 assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
4091
4092 pane.update_in(cx, |pane, window, cx| {
4093 pane.activate_item(3, false, false, window, cx)
4094 });
4095 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4096
4097 pane.update_in(cx, |pane, window, cx| {
4098 pane.close_active_item(
4099 &CloseActiveItem {
4100 save_intent: None,
4101 close_pinned: false,
4102 },
4103 window,
4104 cx,
4105 )
4106 })
4107 .await
4108 .unwrap();
4109 assert_item_labels(&pane, ["A", "B*", "C"], cx);
4110
4111 pane.update_in(cx, |pane, window, cx| {
4112 pane.close_active_item(
4113 &CloseActiveItem {
4114 save_intent: None,
4115 close_pinned: false,
4116 },
4117 window,
4118 cx,
4119 )
4120 })
4121 .await
4122 .unwrap();
4123 assert_item_labels(&pane, ["A", "C*"], cx);
4124
4125 pane.update_in(cx, |pane, window, cx| {
4126 pane.close_active_item(
4127 &CloseActiveItem {
4128 save_intent: None,
4129 close_pinned: false,
4130 },
4131 window,
4132 cx,
4133 )
4134 })
4135 .await
4136 .unwrap();
4137 assert_item_labels(&pane, ["A*"], cx);
4138 }
4139
4140 #[gpui::test]
4141 async fn test_remove_item_ordering_neighbour(cx: &mut TestAppContext) {
4142 init_test(cx);
4143 cx.update_global::<SettingsStore, ()>(|s, cx| {
4144 s.update_user_settings::<ItemSettings>(cx, |s| {
4145 s.activate_on_close = Some(ActivateOnClose::Neighbour);
4146 });
4147 });
4148 let fs = FakeFs::new(cx.executor());
4149
4150 let project = Project::test(fs, None, cx).await;
4151 let (workspace, cx) =
4152 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4153 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4154
4155 add_labeled_item(&pane, "A", false, cx);
4156 add_labeled_item(&pane, "B", false, cx);
4157 add_labeled_item(&pane, "C", false, cx);
4158 add_labeled_item(&pane, "D", false, cx);
4159 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4160
4161 pane.update_in(cx, |pane, window, cx| {
4162 pane.activate_item(1, false, false, window, cx)
4163 });
4164 add_labeled_item(&pane, "1", false, cx);
4165 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
4166
4167 pane.update_in(cx, |pane, window, cx| {
4168 pane.close_active_item(
4169 &CloseActiveItem {
4170 save_intent: None,
4171 close_pinned: false,
4172 },
4173 window,
4174 cx,
4175 )
4176 })
4177 .await
4178 .unwrap();
4179 assert_item_labels(&pane, ["A", "B", "C*", "D"], cx);
4180
4181 pane.update_in(cx, |pane, window, cx| {
4182 pane.activate_item(3, false, false, window, cx)
4183 });
4184 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4185
4186 pane.update_in(cx, |pane, window, cx| {
4187 pane.close_active_item(
4188 &CloseActiveItem {
4189 save_intent: None,
4190 close_pinned: false,
4191 },
4192 window,
4193 cx,
4194 )
4195 })
4196 .await
4197 .unwrap();
4198 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4199
4200 pane.update_in(cx, |pane, window, cx| {
4201 pane.close_active_item(
4202 &CloseActiveItem {
4203 save_intent: None,
4204 close_pinned: false,
4205 },
4206 window,
4207 cx,
4208 )
4209 })
4210 .await
4211 .unwrap();
4212 assert_item_labels(&pane, ["A", "B*"], cx);
4213
4214 pane.update_in(cx, |pane, window, cx| {
4215 pane.close_active_item(
4216 &CloseActiveItem {
4217 save_intent: None,
4218 close_pinned: false,
4219 },
4220 window,
4221 cx,
4222 )
4223 })
4224 .await
4225 .unwrap();
4226 assert_item_labels(&pane, ["A*"], cx);
4227 }
4228
4229 #[gpui::test]
4230 async fn test_remove_item_ordering_left_neighbour(cx: &mut TestAppContext) {
4231 init_test(cx);
4232 cx.update_global::<SettingsStore, ()>(|s, cx| {
4233 s.update_user_settings::<ItemSettings>(cx, |s| {
4234 s.activate_on_close = Some(ActivateOnClose::LeftNeighbour);
4235 });
4236 });
4237 let fs = FakeFs::new(cx.executor());
4238
4239 let project = Project::test(fs, None, cx).await;
4240 let (workspace, cx) =
4241 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4242 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4243
4244 add_labeled_item(&pane, "A", false, cx);
4245 add_labeled_item(&pane, "B", false, cx);
4246 add_labeled_item(&pane, "C", false, cx);
4247 add_labeled_item(&pane, "D", false, cx);
4248 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4249
4250 pane.update_in(cx, |pane, window, cx| {
4251 pane.activate_item(1, false, false, window, cx)
4252 });
4253 add_labeled_item(&pane, "1", false, cx);
4254 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
4255
4256 pane.update_in(cx, |pane, window, cx| {
4257 pane.close_active_item(
4258 &CloseActiveItem {
4259 save_intent: None,
4260 close_pinned: false,
4261 },
4262 window,
4263 cx,
4264 )
4265 })
4266 .await
4267 .unwrap();
4268 assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
4269
4270 pane.update_in(cx, |pane, window, cx| {
4271 pane.activate_item(3, false, false, window, cx)
4272 });
4273 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4274
4275 pane.update_in(cx, |pane, window, cx| {
4276 pane.close_active_item(
4277 &CloseActiveItem {
4278 save_intent: None,
4279 close_pinned: false,
4280 },
4281 window,
4282 cx,
4283 )
4284 })
4285 .await
4286 .unwrap();
4287 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4288
4289 pane.update_in(cx, |pane, window, cx| {
4290 pane.activate_item(0, false, false, window, cx)
4291 });
4292 assert_item_labels(&pane, ["A*", "B", "C"], cx);
4293
4294 pane.update_in(cx, |pane, window, cx| {
4295 pane.close_active_item(
4296 &CloseActiveItem {
4297 save_intent: None,
4298 close_pinned: false,
4299 },
4300 window,
4301 cx,
4302 )
4303 })
4304 .await
4305 .unwrap();
4306 assert_item_labels(&pane, ["B*", "C"], cx);
4307
4308 pane.update_in(cx, |pane, window, cx| {
4309 pane.close_active_item(
4310 &CloseActiveItem {
4311 save_intent: None,
4312 close_pinned: false,
4313 },
4314 window,
4315 cx,
4316 )
4317 })
4318 .await
4319 .unwrap();
4320 assert_item_labels(&pane, ["C*"], cx);
4321 }
4322
4323 #[gpui::test]
4324 async fn test_close_inactive_items(cx: &mut TestAppContext) {
4325 init_test(cx);
4326 let fs = FakeFs::new(cx.executor());
4327
4328 let project = Project::test(fs, None, cx).await;
4329 let (workspace, cx) =
4330 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4331 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4332
4333 let item_a = add_labeled_item(&pane, "A", false, cx);
4334 pane.update_in(cx, |pane, window, cx| {
4335 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4336 pane.pin_tab_at(ix, window, cx);
4337 });
4338 assert_item_labels(&pane, ["A*!"], cx);
4339
4340 let item_b = add_labeled_item(&pane, "B", false, cx);
4341 pane.update_in(cx, |pane, window, cx| {
4342 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4343 pane.pin_tab_at(ix, window, cx);
4344 });
4345 assert_item_labels(&pane, ["A!", "B*!"], cx);
4346
4347 add_labeled_item(&pane, "C", false, cx);
4348 assert_item_labels(&pane, ["A!", "B!", "C*"], cx);
4349
4350 add_labeled_item(&pane, "D", false, cx);
4351 add_labeled_item(&pane, "E", false, cx);
4352 assert_item_labels(&pane, ["A!", "B!", "C", "D", "E*"], cx);
4353
4354 pane.update_in(cx, |pane, window, cx| {
4355 pane.close_inactive_items(
4356 &CloseInactiveItems {
4357 save_intent: None,
4358 close_pinned: false,
4359 },
4360 window,
4361 cx,
4362 )
4363 })
4364 .await
4365 .unwrap();
4366 assert_item_labels(&pane, ["A!", "B!", "E*"], cx);
4367 }
4368
4369 #[gpui::test]
4370 async fn test_close_clean_items(cx: &mut TestAppContext) {
4371 init_test(cx);
4372 let fs = FakeFs::new(cx.executor());
4373
4374 let project = Project::test(fs, None, cx).await;
4375 let (workspace, cx) =
4376 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4377 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4378
4379 add_labeled_item(&pane, "A", true, cx);
4380 add_labeled_item(&pane, "B", false, cx);
4381 add_labeled_item(&pane, "C", true, cx);
4382 add_labeled_item(&pane, "D", false, cx);
4383 add_labeled_item(&pane, "E", false, cx);
4384 assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
4385
4386 pane.update_in(cx, |pane, window, cx| {
4387 pane.close_clean_items(
4388 &CloseCleanItems {
4389 close_pinned: false,
4390 },
4391 window,
4392 cx,
4393 )
4394 })
4395 .await
4396 .unwrap();
4397 assert_item_labels(&pane, ["A^", "C*^"], cx);
4398 }
4399
4400 #[gpui::test]
4401 async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
4402 init_test(cx);
4403 let fs = FakeFs::new(cx.executor());
4404
4405 let project = Project::test(fs, None, cx).await;
4406 let (workspace, cx) =
4407 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4408 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4409
4410 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
4411
4412 pane.update_in(cx, |pane, window, cx| {
4413 pane.close_items_to_the_left_by_id(
4414 None,
4415 &CloseItemsToTheLeft {
4416 close_pinned: false,
4417 },
4418 window,
4419 cx,
4420 )
4421 })
4422 .await
4423 .unwrap();
4424 assert_item_labels(&pane, ["C*", "D", "E"], cx);
4425 }
4426
4427 #[gpui::test]
4428 async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
4429 init_test(cx);
4430 let fs = FakeFs::new(cx.executor());
4431
4432 let project = Project::test(fs, None, cx).await;
4433 let (workspace, cx) =
4434 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4435 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4436
4437 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
4438
4439 pane.update_in(cx, |pane, window, cx| {
4440 pane.close_items_to_the_right_by_id(
4441 None,
4442 &CloseItemsToTheRight {
4443 close_pinned: false,
4444 },
4445 window,
4446 cx,
4447 )
4448 })
4449 .await
4450 .unwrap();
4451 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4452 }
4453
4454 #[gpui::test]
4455 async fn test_close_all_items(cx: &mut TestAppContext) {
4456 init_test(cx);
4457 let fs = FakeFs::new(cx.executor());
4458
4459 let project = Project::test(fs, None, cx).await;
4460 let (workspace, cx) =
4461 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4462 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4463
4464 let item_a = add_labeled_item(&pane, "A", false, cx);
4465 add_labeled_item(&pane, "B", false, cx);
4466 add_labeled_item(&pane, "C", false, cx);
4467 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4468
4469 pane.update_in(cx, |pane, window, cx| {
4470 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4471 pane.pin_tab_at(ix, window, cx);
4472 pane.close_all_items(
4473 &CloseAllItems {
4474 save_intent: None,
4475 close_pinned: false,
4476 },
4477 window,
4478 cx,
4479 )
4480 })
4481 .await
4482 .unwrap();
4483 assert_item_labels(&pane, ["A*!"], cx);
4484
4485 pane.update_in(cx, |pane, window, cx| {
4486 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4487 pane.unpin_tab_at(ix, window, cx);
4488 pane.close_all_items(
4489 &CloseAllItems {
4490 save_intent: None,
4491 close_pinned: false,
4492 },
4493 window,
4494 cx,
4495 )
4496 })
4497 .await
4498 .unwrap();
4499
4500 assert_item_labels(&pane, [], cx);
4501
4502 add_labeled_item(&pane, "A", true, cx).update(cx, |item, cx| {
4503 item.project_items
4504 .push(TestProjectItem::new_dirty(1, "A.txt", cx))
4505 });
4506 add_labeled_item(&pane, "B", true, cx).update(cx, |item, cx| {
4507 item.project_items
4508 .push(TestProjectItem::new_dirty(2, "B.txt", cx))
4509 });
4510 add_labeled_item(&pane, "C", true, cx).update(cx, |item, cx| {
4511 item.project_items
4512 .push(TestProjectItem::new_dirty(3, "C.txt", cx))
4513 });
4514 assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
4515
4516 let save = pane.update_in(cx, |pane, window, cx| {
4517 pane.close_all_items(
4518 &CloseAllItems {
4519 save_intent: None,
4520 close_pinned: false,
4521 },
4522 window,
4523 cx,
4524 )
4525 });
4526
4527 cx.executor().run_until_parked();
4528 cx.simulate_prompt_answer("Save all");
4529 save.await.unwrap();
4530 assert_item_labels(&pane, [], cx);
4531
4532 add_labeled_item(&pane, "A", true, cx);
4533 add_labeled_item(&pane, "B", true, cx);
4534 add_labeled_item(&pane, "C", true, cx);
4535 assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
4536 let save = pane.update_in(cx, |pane, window, cx| {
4537 pane.close_all_items(
4538 &CloseAllItems {
4539 save_intent: None,
4540 close_pinned: false,
4541 },
4542 window,
4543 cx,
4544 )
4545 });
4546
4547 cx.executor().run_until_parked();
4548 cx.simulate_prompt_answer("Discard all");
4549 save.await.unwrap();
4550 assert_item_labels(&pane, [], cx);
4551 }
4552
4553 #[gpui::test]
4554 async fn test_close_with_save_intent(cx: &mut TestAppContext) {
4555 init_test(cx);
4556 let fs = FakeFs::new(cx.executor());
4557
4558 let project = Project::test(fs, None, cx).await;
4559 let (workspace, cx) =
4560 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4561 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4562
4563 let a = cx.update(|_, cx| TestProjectItem::new_dirty(1, "A.txt", cx));
4564 let b = cx.update(|_, cx| TestProjectItem::new_dirty(1, "B.txt", cx));
4565 let c = cx.update(|_, cx| TestProjectItem::new_dirty(1, "C.txt", cx));
4566
4567 add_labeled_item(&pane, "AB", true, cx).update(cx, |item, _| {
4568 item.project_items.push(a.clone());
4569 item.project_items.push(b.clone());
4570 });
4571 add_labeled_item(&pane, "C", true, cx)
4572 .update(cx, |item, _| item.project_items.push(c.clone()));
4573 assert_item_labels(&pane, ["AB^", "C*^"], cx);
4574
4575 pane.update_in(cx, |pane, window, cx| {
4576 pane.close_all_items(
4577 &CloseAllItems {
4578 save_intent: Some(SaveIntent::Save),
4579 close_pinned: false,
4580 },
4581 window,
4582 cx,
4583 )
4584 })
4585 .await
4586 .unwrap();
4587
4588 assert_item_labels(&pane, [], cx);
4589 cx.update(|_, cx| {
4590 assert!(!a.read(cx).is_dirty);
4591 assert!(!b.read(cx).is_dirty);
4592 assert!(!c.read(cx).is_dirty);
4593 });
4594 }
4595
4596 #[gpui::test]
4597 async fn test_close_all_items_including_pinned(cx: &mut TestAppContext) {
4598 init_test(cx);
4599 let fs = FakeFs::new(cx.executor());
4600
4601 let project = Project::test(fs, None, cx).await;
4602 let (workspace, cx) =
4603 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4604 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4605
4606 let item_a = add_labeled_item(&pane, "A", false, cx);
4607 add_labeled_item(&pane, "B", false, cx);
4608 add_labeled_item(&pane, "C", false, cx);
4609 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4610
4611 pane.update_in(cx, |pane, window, cx| {
4612 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4613 pane.pin_tab_at(ix, window, cx);
4614 pane.close_all_items(
4615 &CloseAllItems {
4616 save_intent: None,
4617 close_pinned: true,
4618 },
4619 window,
4620 cx,
4621 )
4622 })
4623 .await
4624 .unwrap();
4625 assert_item_labels(&pane, [], cx);
4626 }
4627
4628 #[gpui::test]
4629 async fn test_close_pinned_tab_with_non_pinned_in_same_pane(cx: &mut TestAppContext) {
4630 init_test(cx);
4631 let fs = FakeFs::new(cx.executor());
4632 let project = Project::test(fs, None, cx).await;
4633 let (workspace, cx) =
4634 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4635
4636 // Non-pinned tabs in same pane
4637 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4638 add_labeled_item(&pane, "A", false, cx);
4639 add_labeled_item(&pane, "B", false, cx);
4640 add_labeled_item(&pane, "C", false, cx);
4641 pane.update_in(cx, |pane, window, cx| {
4642 pane.pin_tab_at(0, window, cx);
4643 });
4644 set_labeled_items(&pane, ["A*", "B", "C"], cx);
4645 pane.update_in(cx, |pane, window, cx| {
4646 pane.close_active_item(
4647 &CloseActiveItem {
4648 save_intent: None,
4649 close_pinned: false,
4650 },
4651 window,
4652 cx,
4653 )
4654 .unwrap();
4655 });
4656 // Non-pinned tab should be active
4657 assert_item_labels(&pane, ["A!", "B*", "C"], cx);
4658 }
4659
4660 #[gpui::test]
4661 async fn test_close_pinned_tab_with_non_pinned_in_different_pane(cx: &mut TestAppContext) {
4662 init_test(cx);
4663 let fs = FakeFs::new(cx.executor());
4664 let project = Project::test(fs, None, cx).await;
4665 let (workspace, cx) =
4666 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4667
4668 // No non-pinned tabs in same pane, non-pinned tabs in another pane
4669 let pane1 = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4670 let pane2 = workspace.update_in(cx, |workspace, window, cx| {
4671 workspace.split_pane(pane1.clone(), SplitDirection::Right, window, cx)
4672 });
4673 add_labeled_item(&pane1, "A", false, cx);
4674 pane1.update_in(cx, |pane, window, cx| {
4675 pane.pin_tab_at(0, window, cx);
4676 });
4677 set_labeled_items(&pane1, ["A*"], cx);
4678 add_labeled_item(&pane2, "B", false, cx);
4679 set_labeled_items(&pane2, ["B"], cx);
4680 pane1.update_in(cx, |pane, window, cx| {
4681 pane.close_active_item(
4682 &CloseActiveItem {
4683 save_intent: None,
4684 close_pinned: false,
4685 },
4686 window,
4687 cx,
4688 )
4689 .unwrap();
4690 });
4691 // Non-pinned tab of other pane should be active
4692 assert_item_labels(&pane2, ["B*"], cx);
4693 }
4694
4695 #[gpui::test]
4696 async fn ensure_item_closing_actions_do_not_panic_when_no_items_exist(cx: &mut TestAppContext) {
4697 init_test(cx);
4698 let fs = FakeFs::new(cx.executor());
4699 let project = Project::test(fs, None, cx).await;
4700 let (workspace, cx) =
4701 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4702
4703 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4704 assert_item_labels(&pane, [], cx);
4705
4706 pane.update_in(cx, |pane, window, cx| {
4707 pane.close_active_item(
4708 &CloseActiveItem {
4709 save_intent: None,
4710 close_pinned: false,
4711 },
4712 window,
4713 cx,
4714 )
4715 })
4716 .await
4717 .unwrap();
4718
4719 pane.update_in(cx, |pane, window, cx| {
4720 pane.close_inactive_items(
4721 &CloseInactiveItems {
4722 save_intent: None,
4723 close_pinned: false,
4724 },
4725 window,
4726 cx,
4727 )
4728 })
4729 .await
4730 .unwrap();
4731
4732 pane.update_in(cx, |pane, window, cx| {
4733 pane.close_all_items(
4734 &CloseAllItems {
4735 save_intent: None,
4736 close_pinned: false,
4737 },
4738 window,
4739 cx,
4740 )
4741 })
4742 .await
4743 .unwrap();
4744
4745 pane.update_in(cx, |pane, window, cx| {
4746 pane.close_clean_items(
4747 &CloseCleanItems {
4748 close_pinned: false,
4749 },
4750 window,
4751 cx,
4752 )
4753 })
4754 .await
4755 .unwrap();
4756
4757 pane.update_in(cx, |pane, window, cx| {
4758 pane.close_items_to_the_right_by_id(
4759 None,
4760 &CloseItemsToTheRight {
4761 close_pinned: false,
4762 },
4763 window,
4764 cx,
4765 )
4766 })
4767 .await
4768 .unwrap();
4769
4770 pane.update_in(cx, |pane, window, cx| {
4771 pane.close_items_to_the_left_by_id(
4772 None,
4773 &CloseItemsToTheLeft {
4774 close_pinned: false,
4775 },
4776 window,
4777 cx,
4778 )
4779 })
4780 .await
4781 .unwrap();
4782 }
4783
4784 fn init_test(cx: &mut TestAppContext) {
4785 cx.update(|cx| {
4786 let settings_store = SettingsStore::test(cx);
4787 cx.set_global(settings_store);
4788 theme::init(LoadThemes::JustBase, cx);
4789 crate::init_settings(cx);
4790 Project::init_settings(cx);
4791 });
4792 }
4793
4794 fn set_max_tabs(cx: &mut TestAppContext, value: Option<usize>) {
4795 cx.update_global(|store: &mut SettingsStore, cx| {
4796 store.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4797 settings.max_tabs = value.map(|v| NonZero::new(v).unwrap())
4798 });
4799 });
4800 }
4801
4802 fn add_labeled_item(
4803 pane: &Entity<Pane>,
4804 label: &str,
4805 is_dirty: bool,
4806 cx: &mut VisualTestContext,
4807 ) -> Box<Entity<TestItem>> {
4808 pane.update_in(cx, |pane, window, cx| {
4809 let labeled_item =
4810 Box::new(cx.new(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)));
4811 pane.add_item(labeled_item.clone(), false, false, None, window, cx);
4812 labeled_item
4813 })
4814 }
4815
4816 fn set_labeled_items<const COUNT: usize>(
4817 pane: &Entity<Pane>,
4818 labels: [&str; COUNT],
4819 cx: &mut VisualTestContext,
4820 ) -> [Box<Entity<TestItem>>; COUNT] {
4821 pane.update_in(cx, |pane, window, cx| {
4822 pane.items.clear();
4823 let mut active_item_index = 0;
4824
4825 let mut index = 0;
4826 let items = labels.map(|mut label| {
4827 if label.ends_with('*') {
4828 label = label.trim_end_matches('*');
4829 active_item_index = index;
4830 }
4831
4832 let labeled_item = Box::new(cx.new(|cx| TestItem::new(cx).with_label(label)));
4833 pane.add_item(labeled_item.clone(), false, false, None, window, cx);
4834 index += 1;
4835 labeled_item
4836 });
4837
4838 pane.activate_item(active_item_index, false, false, window, cx);
4839
4840 items
4841 })
4842 }
4843
4844 // Assert the item label, with the active item label suffixed with a '*'
4845 #[track_caller]
4846 fn assert_item_labels<const COUNT: usize>(
4847 pane: &Entity<Pane>,
4848 expected_states: [&str; COUNT],
4849 cx: &mut VisualTestContext,
4850 ) {
4851 let actual_states = pane.update(cx, |pane, cx| {
4852 pane.items
4853 .iter()
4854 .enumerate()
4855 .map(|(ix, item)| {
4856 let mut state = item
4857 .to_any()
4858 .downcast::<TestItem>()
4859 .unwrap()
4860 .read(cx)
4861 .label
4862 .clone();
4863 if ix == pane.active_item_index {
4864 state.push('*');
4865 }
4866 if item.is_dirty(cx) {
4867 state.push('^');
4868 }
4869 if pane.is_tab_pinned(ix) {
4870 state.push('!');
4871 }
4872 state
4873 })
4874 .collect::<Vec<_>>()
4875 });
4876 assert_eq!(
4877 actual_states, expected_states,
4878 "pane items do not match expectation"
4879 );
4880 }
4881}