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