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