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 .read_with(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) =
2021 workspace.read_with(cx, |workspace, _| workspace.status_bar.clone())
2022 else {
2023 return;
2024 };
2025
2026 status_bar.update(cx, move |status_bar, cx| {
2027 status_bar.set_active_pane(&pane, window, cx);
2028 });
2029 });
2030 }
2031
2032 fn entry_abs_path(&self, entry: ProjectEntryId, cx: &App) -> Option<PathBuf> {
2033 let worktree = self
2034 .workspace
2035 .upgrade()?
2036 .read(cx)
2037 .project()
2038 .read(cx)
2039 .worktree_for_entry(entry, cx)?
2040 .read(cx);
2041 let entry = worktree.entry_for_id(entry)?;
2042 match &entry.canonical_path {
2043 Some(canonical_path) => Some(canonical_path.to_path_buf()),
2044 None => worktree.absolutize(&entry.path).ok(),
2045 }
2046 }
2047
2048 pub fn icon_color(selected: bool) -> Color {
2049 if selected {
2050 Color::Default
2051 } else {
2052 Color::Muted
2053 }
2054 }
2055
2056 fn toggle_pin_tab(&mut self, _: &TogglePinTab, window: &mut Window, cx: &mut Context<Self>) {
2057 if self.items.is_empty() {
2058 return;
2059 }
2060 let active_tab_ix = self.active_item_index();
2061 if self.is_tab_pinned(active_tab_ix) {
2062 self.unpin_tab_at(active_tab_ix, window, cx);
2063 } else {
2064 self.pin_tab_at(active_tab_ix, window, cx);
2065 }
2066 }
2067
2068 fn pin_tab_at(&mut self, ix: usize, window: &mut Window, cx: &mut Context<Self>) {
2069 maybe!({
2070 let pane = cx.entity().clone();
2071 let destination_index = self.pinned_tab_count.min(ix);
2072 self.pinned_tab_count += 1;
2073 let id = self.item_for_index(ix)?.item_id();
2074
2075 if self.is_active_preview_item(id) {
2076 self.set_preview_item_id(None, cx);
2077 }
2078
2079 self.workspace
2080 .update(cx, |_, cx| {
2081 cx.defer_in(window, move |_, window, cx| {
2082 move_item(&pane, &pane, id, destination_index, window, cx)
2083 });
2084 })
2085 .ok()?;
2086
2087 Some(())
2088 });
2089 }
2090
2091 fn unpin_tab_at(&mut self, ix: usize, window: &mut Window, cx: &mut Context<Self>) {
2092 maybe!({
2093 let pane = cx.entity().clone();
2094 self.pinned_tab_count = self.pinned_tab_count.checked_sub(1)?;
2095 let destination_index = self.pinned_tab_count;
2096
2097 let id = self.item_for_index(ix)?.item_id();
2098
2099 self.workspace
2100 .update(cx, |_, cx| {
2101 cx.defer_in(window, move |_, window, cx| {
2102 move_item(&pane, &pane, id, destination_index, window, cx)
2103 });
2104 })
2105 .ok()?;
2106
2107 Some(())
2108 });
2109 }
2110
2111 fn is_tab_pinned(&self, ix: usize) -> bool {
2112 self.pinned_tab_count > ix
2113 }
2114
2115 fn has_pinned_tabs(&self) -> bool {
2116 self.pinned_tab_count != 0
2117 }
2118
2119 fn has_unpinned_tabs(&self) -> bool {
2120 self.pinned_tab_count < self.items.len()
2121 }
2122
2123 fn activate_unpinned_tab(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2124 if self.items.is_empty() {
2125 return;
2126 }
2127 let Some(index) = self
2128 .items()
2129 .enumerate()
2130 .find_map(|(index, _item)| (!self.is_tab_pinned(index)).then_some(index))
2131 else {
2132 return;
2133 };
2134 self.activate_item(index, true, true, window, cx);
2135 }
2136
2137 fn render_tab(
2138 &self,
2139 ix: usize,
2140 item: &dyn ItemHandle,
2141 detail: usize,
2142 focus_handle: &FocusHandle,
2143 window: &mut Window,
2144 cx: &mut Context<Pane>,
2145 ) -> impl IntoElement + use<> {
2146 let is_active = ix == self.active_item_index;
2147 let is_preview = self
2148 .preview_item_id
2149 .map(|id| id == item.item_id())
2150 .unwrap_or(false);
2151
2152 let label = item.tab_content(
2153 TabContentParams {
2154 detail: Some(detail),
2155 selected: is_active,
2156 preview: is_preview,
2157 deemphasized: !self.has_focus(window, cx),
2158 },
2159 window,
2160 cx,
2161 );
2162
2163 let item_diagnostic = item
2164 .project_path(cx)
2165 .map_or(None, |project_path| self.diagnostics.get(&project_path));
2166
2167 let decorated_icon = item_diagnostic.map_or(None, |diagnostic| {
2168 let icon = match item.tab_icon(window, cx) {
2169 Some(icon) => icon,
2170 None => return None,
2171 };
2172
2173 let knockout_item_color = if is_active {
2174 cx.theme().colors().tab_active_background
2175 } else {
2176 cx.theme().colors().tab_bar_background
2177 };
2178
2179 let (icon_decoration, icon_color) = if matches!(diagnostic, &DiagnosticSeverity::ERROR)
2180 {
2181 (IconDecorationKind::X, Color::Error)
2182 } else {
2183 (IconDecorationKind::Triangle, Color::Warning)
2184 };
2185
2186 Some(DecoratedIcon::new(
2187 icon.size(IconSize::Small).color(Color::Muted),
2188 Some(
2189 IconDecoration::new(icon_decoration, knockout_item_color, cx)
2190 .color(icon_color.color(cx))
2191 .position(Point {
2192 x: px(-2.),
2193 y: px(-2.),
2194 }),
2195 ),
2196 ))
2197 });
2198
2199 let icon = if decorated_icon.is_none() {
2200 match item_diagnostic {
2201 Some(&DiagnosticSeverity::ERROR) => None,
2202 Some(&DiagnosticSeverity::WARNING) => None,
2203 _ => item
2204 .tab_icon(window, cx)
2205 .map(|icon| icon.color(Color::Muted)),
2206 }
2207 .map(|icon| icon.size(IconSize::Small))
2208 } else {
2209 None
2210 };
2211
2212 let settings = ItemSettings::get_global(cx);
2213 let close_side = &settings.close_position;
2214 let show_close_button = &settings.show_close_button;
2215 let indicator = render_item_indicator(item.boxed_clone(), cx);
2216 let item_id = item.item_id();
2217 let is_first_item = ix == 0;
2218 let is_last_item = ix == self.items.len() - 1;
2219 let is_pinned = self.is_tab_pinned(ix);
2220 let position_relative_to_active_item = ix.cmp(&self.active_item_index);
2221
2222 let tab = Tab::new(ix)
2223 .position(if is_first_item {
2224 TabPosition::First
2225 } else if is_last_item {
2226 TabPosition::Last
2227 } else {
2228 TabPosition::Middle(position_relative_to_active_item)
2229 })
2230 .close_side(match close_side {
2231 ClosePosition::Left => ui::TabCloseSide::Start,
2232 ClosePosition::Right => ui::TabCloseSide::End,
2233 })
2234 .toggle_state(is_active)
2235 .on_click(cx.listener(move |pane: &mut Self, _, window, cx| {
2236 pane.activate_item(ix, true, true, window, cx)
2237 }))
2238 // TODO: This should be a click listener with the middle mouse button instead of a mouse down listener.
2239 .on_mouse_down(
2240 MouseButton::Middle,
2241 cx.listener(move |pane, _event, window, cx| {
2242 pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
2243 .detach_and_log_err(cx);
2244 }),
2245 )
2246 .on_mouse_down(
2247 MouseButton::Left,
2248 cx.listener(move |pane, event: &MouseDownEvent, _, cx| {
2249 if let Some(id) = pane.preview_item_id {
2250 if id == item_id && event.click_count > 1 {
2251 pane.set_preview_item_id(None, cx);
2252 }
2253 }
2254 }),
2255 )
2256 .on_drag(
2257 DraggedTab {
2258 item: item.boxed_clone(),
2259 pane: cx.entity().clone(),
2260 detail,
2261 is_active,
2262 ix,
2263 },
2264 |tab, _, _, cx| cx.new(|_| tab.clone()),
2265 )
2266 .drag_over::<DraggedTab>(|tab, _, _, cx| {
2267 tab.bg(cx.theme().colors().drop_target_background)
2268 })
2269 .drag_over::<DraggedSelection>(|tab, _, _, cx| {
2270 tab.bg(cx.theme().colors().drop_target_background)
2271 })
2272 .when_some(self.can_drop_predicate.clone(), |this, p| {
2273 this.can_drop(move |a, window, cx| p(a, window, cx))
2274 })
2275 .on_drop(
2276 cx.listener(move |this, dragged_tab: &DraggedTab, window, cx| {
2277 this.drag_split_direction = None;
2278 this.handle_tab_drop(dragged_tab, ix, window, cx)
2279 }),
2280 )
2281 .on_drop(
2282 cx.listener(move |this, selection: &DraggedSelection, window, cx| {
2283 this.drag_split_direction = None;
2284 this.handle_dragged_selection_drop(selection, Some(ix), window, cx)
2285 }),
2286 )
2287 .on_drop(cx.listener(move |this, paths, window, cx| {
2288 this.drag_split_direction = None;
2289 this.handle_external_paths_drop(paths, window, cx)
2290 }))
2291 .when_some(item.tab_tooltip_content(cx), |tab, content| match content {
2292 TabTooltipContent::Text(text) => tab.tooltip(Tooltip::text(text.clone())),
2293 TabTooltipContent::Custom(element_fn) => {
2294 tab.tooltip(move |window, cx| element_fn(window, cx))
2295 }
2296 })
2297 .start_slot::<Indicator>(indicator)
2298 .map(|this| {
2299 let end_slot_action: &'static dyn Action;
2300 let end_slot_tooltip_text: &'static str;
2301 let end_slot = if is_pinned {
2302 end_slot_action = &TogglePinTab;
2303 end_slot_tooltip_text = "Unpin Tab";
2304 IconButton::new("unpin tab", IconName::Pin)
2305 .shape(IconButtonShape::Square)
2306 .icon_color(Color::Muted)
2307 .size(ButtonSize::None)
2308 .icon_size(IconSize::XSmall)
2309 .on_click(cx.listener(move |pane, _, window, cx| {
2310 pane.unpin_tab_at(ix, window, cx);
2311 }))
2312 } else {
2313 end_slot_action = &CloseActiveItem {
2314 save_intent: None,
2315 close_pinned: false,
2316 };
2317 end_slot_tooltip_text = "Close Tab";
2318 match show_close_button {
2319 ShowCloseButton::Always => IconButton::new("close tab", IconName::Close),
2320 ShowCloseButton::Hover => {
2321 IconButton::new("close tab", IconName::Close).visible_on_hover("")
2322 }
2323 ShowCloseButton::Hidden => return this,
2324 }
2325 .shape(IconButtonShape::Square)
2326 .icon_color(Color::Muted)
2327 .size(ButtonSize::None)
2328 .icon_size(IconSize::XSmall)
2329 .on_click(cx.listener(move |pane, _, window, cx| {
2330 pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
2331 .detach_and_log_err(cx);
2332 }))
2333 }
2334 .map(|this| {
2335 if is_active {
2336 let focus_handle = focus_handle.clone();
2337 this.tooltip(move |window, cx| {
2338 Tooltip::for_action_in(
2339 end_slot_tooltip_text,
2340 end_slot_action,
2341 &focus_handle,
2342 window,
2343 cx,
2344 )
2345 })
2346 } else {
2347 this.tooltip(Tooltip::text(end_slot_tooltip_text))
2348 }
2349 });
2350 this.end_slot(end_slot)
2351 })
2352 .child(
2353 h_flex()
2354 .gap_1()
2355 .items_center()
2356 .children(
2357 std::iter::once(if let Some(decorated_icon) = decorated_icon {
2358 Some(div().child(decorated_icon.into_any_element()))
2359 } else if let Some(icon) = icon {
2360 Some(div().child(icon.into_any_element()))
2361 } else {
2362 None
2363 })
2364 .flatten(),
2365 )
2366 .child(label),
2367 );
2368
2369 let single_entry_to_resolve = self.items[ix]
2370 .is_singleton(cx)
2371 .then(|| self.items[ix].project_entry_ids(cx).get(0).copied())
2372 .flatten();
2373
2374 let total_items = self.items.len();
2375 let has_items_to_left = ix > 0;
2376 let has_items_to_right = ix < total_items - 1;
2377 let is_pinned = self.is_tab_pinned(ix);
2378 let pane = cx.entity().downgrade();
2379 let menu_context = item.item_focus_handle(cx);
2380 right_click_menu(ix)
2381 .trigger(|_| tab)
2382 .menu(move |window, cx| {
2383 let pane = pane.clone();
2384 let menu_context = menu_context.clone();
2385 ContextMenu::build(window, cx, move |mut menu, window, cx| {
2386 if let Some(pane) = pane.upgrade() {
2387 menu = menu
2388 .entry(
2389 "Close",
2390 Some(Box::new(CloseActiveItem {
2391 save_intent: None,
2392 close_pinned: true,
2393 })),
2394 window.handler_for(&pane, move |pane, window, cx| {
2395 pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
2396 .detach_and_log_err(cx);
2397 }),
2398 )
2399 .item(ContextMenuItem::Entry(
2400 ContextMenuEntry::new("Close Others")
2401 .action(Box::new(CloseInactiveItems {
2402 save_intent: None,
2403 close_pinned: false,
2404 }))
2405 .disabled(total_items == 1)
2406 .handler(window.handler_for(&pane, move |pane, window, cx| {
2407 pane.close_items(window, cx, SaveIntent::Close, |id| {
2408 id != item_id
2409 })
2410 .detach_and_log_err(cx);
2411 })),
2412 ))
2413 .separator()
2414 .item(ContextMenuItem::Entry(
2415 ContextMenuEntry::new("Close Left")
2416 .action(Box::new(CloseItemsToTheLeft {
2417 close_pinned: false,
2418 }))
2419 .disabled(!has_items_to_left)
2420 .handler(window.handler_for(&pane, move |pane, window, cx| {
2421 pane.close_items_to_the_left_by_id(
2422 item_id,
2423 &CloseItemsToTheLeft {
2424 close_pinned: false,
2425 },
2426 pane.get_non_closeable_item_ids(false),
2427 window,
2428 cx,
2429 )
2430 .detach_and_log_err(cx);
2431 })),
2432 ))
2433 .item(ContextMenuItem::Entry(
2434 ContextMenuEntry::new("Close Right")
2435 .action(Box::new(CloseItemsToTheRight {
2436 close_pinned: false,
2437 }))
2438 .disabled(!has_items_to_right)
2439 .handler(window.handler_for(&pane, move |pane, window, cx| {
2440 pane.close_items_to_the_right_by_id(
2441 item_id,
2442 &CloseItemsToTheRight {
2443 close_pinned: false,
2444 },
2445 pane.get_non_closeable_item_ids(false),
2446 window,
2447 cx,
2448 )
2449 .detach_and_log_err(cx);
2450 })),
2451 ))
2452 .separator()
2453 .entry(
2454 "Close Clean",
2455 Some(Box::new(CloseCleanItems {
2456 close_pinned: false,
2457 })),
2458 window.handler_for(&pane, move |pane, window, cx| {
2459 if let Some(task) = pane.close_clean_items(
2460 &CloseCleanItems {
2461 close_pinned: false,
2462 },
2463 window,
2464 cx,
2465 ) {
2466 task.detach_and_log_err(cx)
2467 }
2468 }),
2469 )
2470 .entry(
2471 "Close All",
2472 Some(Box::new(CloseAllItems {
2473 save_intent: None,
2474 close_pinned: false,
2475 })),
2476 window.handler_for(&pane, |pane, window, cx| {
2477 if let Some(task) = pane.close_all_items(
2478 &CloseAllItems {
2479 save_intent: None,
2480 close_pinned: false,
2481 },
2482 window,
2483 cx,
2484 ) {
2485 task.detach_and_log_err(cx)
2486 }
2487 }),
2488 );
2489
2490 let pin_tab_entries = |menu: ContextMenu| {
2491 menu.separator().map(|this| {
2492 if is_pinned {
2493 this.entry(
2494 "Unpin Tab",
2495 Some(TogglePinTab.boxed_clone()),
2496 window.handler_for(&pane, move |pane, window, cx| {
2497 pane.unpin_tab_at(ix, window, cx);
2498 }),
2499 )
2500 } else {
2501 this.entry(
2502 "Pin Tab",
2503 Some(TogglePinTab.boxed_clone()),
2504 window.handler_for(&pane, move |pane, window, cx| {
2505 pane.pin_tab_at(ix, window, cx);
2506 }),
2507 )
2508 }
2509 })
2510 };
2511 if let Some(entry) = single_entry_to_resolve {
2512 let project_path = pane
2513 .read(cx)
2514 .item_for_entry(entry, cx)
2515 .and_then(|item| item.project_path(cx));
2516 let worktree = project_path.as_ref().and_then(|project_path| {
2517 pane.read(cx)
2518 .project
2519 .upgrade()?
2520 .read(cx)
2521 .worktree_for_id(project_path.worktree_id, cx)
2522 });
2523 let has_relative_path = worktree.as_ref().is_some_and(|worktree| {
2524 worktree
2525 .read(cx)
2526 .root_entry()
2527 .map_or(false, |entry| entry.is_dir())
2528 });
2529
2530 let entry_abs_path = pane.read(cx).entry_abs_path(entry, cx);
2531 let parent_abs_path = entry_abs_path
2532 .as_deref()
2533 .and_then(|abs_path| Some(abs_path.parent()?.to_path_buf()));
2534 let relative_path = project_path
2535 .map(|project_path| project_path.path)
2536 .filter(|_| has_relative_path);
2537
2538 let visible_in_project_panel = relative_path.is_some()
2539 && worktree.is_some_and(|worktree| worktree.read(cx).is_visible());
2540
2541 let entry_id = entry.to_proto();
2542 menu = menu
2543 .separator()
2544 .when_some(entry_abs_path, |menu, abs_path| {
2545 menu.entry(
2546 "Copy Path",
2547 Some(Box::new(zed_actions::workspace::CopyPath)),
2548 window.handler_for(&pane, move |_, _, cx| {
2549 cx.write_to_clipboard(ClipboardItem::new_string(
2550 abs_path.to_string_lossy().to_string(),
2551 ));
2552 }),
2553 )
2554 })
2555 .when_some(relative_path, |menu, relative_path| {
2556 menu.entry(
2557 "Copy Relative Path",
2558 Some(Box::new(zed_actions::workspace::CopyRelativePath)),
2559 window.handler_for(&pane, move |_, _, cx| {
2560 cx.write_to_clipboard(ClipboardItem::new_string(
2561 relative_path.to_string_lossy().to_string(),
2562 ));
2563 }),
2564 )
2565 })
2566 .map(pin_tab_entries)
2567 .separator()
2568 .when(visible_in_project_panel, |menu| {
2569 menu.entry(
2570 "Reveal In Project Panel",
2571 Some(Box::new(RevealInProjectPanel {
2572 entry_id: Some(entry_id),
2573 })),
2574 window.handler_for(&pane, move |pane, _, cx| {
2575 pane.project
2576 .update(cx, |_, cx| {
2577 cx.emit(project::Event::RevealInProjectPanel(
2578 ProjectEntryId::from_proto(entry_id),
2579 ))
2580 })
2581 .ok();
2582 }),
2583 )
2584 })
2585 .when_some(parent_abs_path, |menu, parent_abs_path| {
2586 menu.entry(
2587 "Open in Terminal",
2588 Some(Box::new(OpenInTerminal)),
2589 window.handler_for(&pane, move |_, window, cx| {
2590 window.dispatch_action(
2591 OpenTerminal {
2592 working_directory: parent_abs_path.clone(),
2593 }
2594 .boxed_clone(),
2595 cx,
2596 );
2597 }),
2598 )
2599 });
2600 } else {
2601 menu = menu.map(pin_tab_entries);
2602 }
2603 }
2604
2605 menu.context(menu_context)
2606 })
2607 })
2608 }
2609
2610 fn render_tab_bar(&mut self, window: &mut Window, cx: &mut Context<Pane>) -> AnyElement {
2611 let focus_handle = self.focus_handle.clone();
2612 let navigate_backward = IconButton::new("navigate_backward", IconName::ArrowLeft)
2613 .icon_size(IconSize::Small)
2614 .on_click({
2615 let entity = cx.entity().clone();
2616 move |_, window, cx| {
2617 entity.update(cx, |pane, cx| pane.navigate_backward(window, cx))
2618 }
2619 })
2620 .disabled(!self.can_navigate_backward())
2621 .tooltip({
2622 let focus_handle = focus_handle.clone();
2623 move |window, cx| {
2624 Tooltip::for_action_in("Go Back", &GoBack, &focus_handle, window, cx)
2625 }
2626 });
2627
2628 let navigate_forward = IconButton::new("navigate_forward", IconName::ArrowRight)
2629 .icon_size(IconSize::Small)
2630 .on_click({
2631 let entity = cx.entity().clone();
2632 move |_, window, cx| entity.update(cx, |pane, cx| pane.navigate_forward(window, cx))
2633 })
2634 .disabled(!self.can_navigate_forward())
2635 .tooltip({
2636 let focus_handle = focus_handle.clone();
2637 move |window, cx| {
2638 Tooltip::for_action_in("Go Forward", &GoForward, &focus_handle, window, cx)
2639 }
2640 });
2641
2642 let mut tab_items = self
2643 .items
2644 .iter()
2645 .enumerate()
2646 .zip(tab_details(&self.items, window, cx))
2647 .map(|((ix, item), detail)| {
2648 self.render_tab(ix, &**item, detail, &focus_handle, window, cx)
2649 })
2650 .collect::<Vec<_>>();
2651 let tab_count = tab_items.len();
2652 let unpinned_tabs = tab_items.split_off(self.pinned_tab_count);
2653 let pinned_tabs = tab_items;
2654 TabBar::new("tab_bar")
2655 .when(
2656 self.display_nav_history_buttons.unwrap_or_default(),
2657 |tab_bar| {
2658 tab_bar
2659 .start_child(navigate_backward)
2660 .start_child(navigate_forward)
2661 },
2662 )
2663 .map(|tab_bar| {
2664 if self.show_tab_bar_buttons {
2665 let render_tab_buttons = self.render_tab_bar_buttons.clone();
2666 let (left_children, right_children) = render_tab_buttons(self, window, cx);
2667 tab_bar
2668 .start_children(left_children)
2669 .end_children(right_children)
2670 } else {
2671 tab_bar
2672 }
2673 })
2674 .children(pinned_tabs.len().ne(&0).then(|| {
2675 let content_width = self.tab_bar_scroll_handle.content_size().width;
2676 let viewport_width = self.tab_bar_scroll_handle.viewport().size.width;
2677 // We need to check both because offset returns delta values even when the scroll handle is not scrollable
2678 let is_scrollable = content_width > viewport_width;
2679 let is_scrolled = self.tab_bar_scroll_handle.offset().x < px(0.);
2680 let has_active_unpinned_tab = self.active_item_index >= self.pinned_tab_count;
2681 h_flex()
2682 .children(pinned_tabs)
2683 .when(is_scrollable && is_scrolled, |this| {
2684 this.when(has_active_unpinned_tab, |this| this.border_r_2())
2685 .when(!has_active_unpinned_tab, |this| this.border_r_1())
2686 .border_color(cx.theme().colors().border)
2687 })
2688 }))
2689 .child(
2690 h_flex()
2691 .id("unpinned tabs")
2692 .overflow_x_scroll()
2693 .w_full()
2694 .track_scroll(&self.tab_bar_scroll_handle)
2695 .children(unpinned_tabs)
2696 .child(
2697 div()
2698 .id("tab_bar_drop_target")
2699 .min_w_6()
2700 // HACK: This empty child is currently necessary to force the drop target to appear
2701 // despite us setting a min width above.
2702 .child("")
2703 .h_full()
2704 .flex_grow()
2705 .drag_over::<DraggedTab>(|bar, _, _, cx| {
2706 bar.bg(cx.theme().colors().drop_target_background)
2707 })
2708 .drag_over::<DraggedSelection>(|bar, _, _, cx| {
2709 bar.bg(cx.theme().colors().drop_target_background)
2710 })
2711 .on_drop(cx.listener(
2712 move |this, dragged_tab: &DraggedTab, window, cx| {
2713 this.drag_split_direction = None;
2714 this.handle_tab_drop(dragged_tab, this.items.len(), window, cx)
2715 },
2716 ))
2717 .on_drop(cx.listener(
2718 move |this, selection: &DraggedSelection, window, cx| {
2719 this.drag_split_direction = None;
2720 this.handle_project_entry_drop(
2721 &selection.active_selection.entry_id,
2722 Some(tab_count),
2723 window,
2724 cx,
2725 )
2726 },
2727 ))
2728 .on_drop(cx.listener(move |this, paths, window, cx| {
2729 this.drag_split_direction = None;
2730 this.handle_external_paths_drop(paths, window, cx)
2731 }))
2732 .on_click(cx.listener(move |this, event: &ClickEvent, window, cx| {
2733 if event.up.click_count == 2 {
2734 window.dispatch_action(
2735 this.double_click_dispatch_action.boxed_clone(),
2736 cx,
2737 );
2738 }
2739 })),
2740 ),
2741 )
2742 .into_any_element()
2743 }
2744
2745 pub fn render_menu_overlay(menu: &Entity<ContextMenu>) -> Div {
2746 div().absolute().bottom_0().right_0().size_0().child(
2747 deferred(anchored().anchor(Corner::TopRight).child(menu.clone())).with_priority(1),
2748 )
2749 }
2750
2751 pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut Context<Self>) {
2752 self.zoomed = zoomed;
2753 cx.notify();
2754 }
2755
2756 pub fn is_zoomed(&self) -> bool {
2757 self.zoomed
2758 }
2759
2760 fn handle_drag_move<T: 'static>(
2761 &mut self,
2762 event: &DragMoveEvent<T>,
2763 window: &mut Window,
2764 cx: &mut Context<Self>,
2765 ) {
2766 let can_split_predicate = self.can_split_predicate.take();
2767 let can_split = match &can_split_predicate {
2768 Some(can_split_predicate) => {
2769 can_split_predicate(self, event.dragged_item(), window, cx)
2770 }
2771 None => false,
2772 };
2773 self.can_split_predicate = can_split_predicate;
2774 if !can_split {
2775 return;
2776 }
2777
2778 let rect = event.bounds.size;
2779
2780 let size = event.bounds.size.width.min(event.bounds.size.height)
2781 * WorkspaceSettings::get_global(cx).drop_target_size;
2782
2783 let relative_cursor = Point::new(
2784 event.event.position.x - event.bounds.left(),
2785 event.event.position.y - event.bounds.top(),
2786 );
2787
2788 let direction = if relative_cursor.x < size
2789 || relative_cursor.x > rect.width - size
2790 || relative_cursor.y < size
2791 || relative_cursor.y > rect.height - size
2792 {
2793 [
2794 SplitDirection::Up,
2795 SplitDirection::Right,
2796 SplitDirection::Down,
2797 SplitDirection::Left,
2798 ]
2799 .iter()
2800 .min_by_key(|side| match side {
2801 SplitDirection::Up => relative_cursor.y,
2802 SplitDirection::Right => rect.width - relative_cursor.x,
2803 SplitDirection::Down => rect.height - relative_cursor.y,
2804 SplitDirection::Left => relative_cursor.x,
2805 })
2806 .cloned()
2807 } else {
2808 None
2809 };
2810
2811 if direction != self.drag_split_direction {
2812 self.drag_split_direction = direction;
2813 }
2814 }
2815
2816 pub fn handle_tab_drop(
2817 &mut self,
2818 dragged_tab: &DraggedTab,
2819 ix: usize,
2820 window: &mut Window,
2821 cx: &mut Context<Self>,
2822 ) {
2823 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2824 if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, window, cx) {
2825 return;
2826 }
2827 }
2828 let mut to_pane = cx.entity().clone();
2829 let split_direction = self.drag_split_direction;
2830 let item_id = dragged_tab.item.item_id();
2831 if let Some(preview_item_id) = self.preview_item_id {
2832 if item_id == preview_item_id {
2833 self.set_preview_item_id(None, cx);
2834 }
2835 }
2836
2837 let is_clone = cfg!(target_os = "macos") && window.modifiers().alt
2838 || cfg!(not(target_os = "macos")) && window.modifiers().control;
2839
2840 let from_pane = dragged_tab.pane.clone();
2841 self.workspace
2842 .update(cx, |_, cx| {
2843 cx.defer_in(window, move |workspace, window, cx| {
2844 if let Some(split_direction) = split_direction {
2845 to_pane = workspace.split_pane(to_pane, split_direction, window, cx);
2846 }
2847 let database_id = workspace.database_id();
2848 let old_ix = from_pane.read(cx).index_for_item_id(item_id);
2849 let old_len = to_pane.read(cx).items.len();
2850 if is_clone {
2851 let Some(item) = from_pane
2852 .read(cx)
2853 .items()
2854 .find(|item| item.item_id() == item_id)
2855 .map(|item| item.clone())
2856 else {
2857 return;
2858 };
2859 if let Some(item) = item.clone_on_split(database_id, window, cx) {
2860 to_pane.update(cx, |pane, cx| {
2861 pane.add_item(item, true, true, None, window, cx);
2862 })
2863 }
2864 } else {
2865 move_item(&from_pane, &to_pane, item_id, ix, window, cx);
2866 }
2867 if to_pane == from_pane {
2868 if let Some(old_index) = old_ix {
2869 to_pane.update(cx, |this, _| {
2870 if old_index < this.pinned_tab_count
2871 && (ix == this.items.len() || ix > this.pinned_tab_count)
2872 {
2873 this.pinned_tab_count -= 1;
2874 } else if this.has_pinned_tabs()
2875 && old_index >= this.pinned_tab_count
2876 && ix < this.pinned_tab_count
2877 {
2878 this.pinned_tab_count += 1;
2879 }
2880 });
2881 }
2882 } else {
2883 to_pane.update(cx, |this, _| {
2884 if this.items.len() > old_len // Did we not deduplicate on drag?
2885 && this.has_pinned_tabs()
2886 && ix < this.pinned_tab_count
2887 {
2888 this.pinned_tab_count += 1;
2889 }
2890 });
2891 from_pane.update(cx, |this, _| {
2892 if let Some(index) = old_ix {
2893 if this.pinned_tab_count > index {
2894 this.pinned_tab_count -= 1;
2895 }
2896 }
2897 })
2898 }
2899 });
2900 })
2901 .log_err();
2902 }
2903
2904 fn handle_dragged_selection_drop(
2905 &mut self,
2906 dragged_selection: &DraggedSelection,
2907 dragged_onto: Option<usize>,
2908 window: &mut Window,
2909 cx: &mut Context<Self>,
2910 ) {
2911 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2912 if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_selection, window, cx)
2913 {
2914 return;
2915 }
2916 }
2917 self.handle_project_entry_drop(
2918 &dragged_selection.active_selection.entry_id,
2919 dragged_onto,
2920 window,
2921 cx,
2922 );
2923 }
2924
2925 fn handle_project_entry_drop(
2926 &mut self,
2927 project_entry_id: &ProjectEntryId,
2928 target: Option<usize>,
2929 window: &mut Window,
2930 cx: &mut Context<Self>,
2931 ) {
2932 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2933 if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, window, cx) {
2934 return;
2935 }
2936 }
2937 let mut to_pane = cx.entity().clone();
2938 let split_direction = self.drag_split_direction;
2939 let project_entry_id = *project_entry_id;
2940 self.workspace
2941 .update(cx, |_, cx| {
2942 cx.defer_in(window, move |workspace, window, cx| {
2943 if let Some(project_path) = workspace
2944 .project()
2945 .read(cx)
2946 .path_for_entry(project_entry_id, cx)
2947 {
2948 let load_path_task = workspace.load_path(project_path.clone(), window, cx);
2949 cx.spawn_in(window, async move |workspace, cx| {
2950 if let Some((project_entry_id, build_item)) =
2951 load_path_task.await.notify_async_err(cx)
2952 {
2953 let (to_pane, new_item_handle) = workspace
2954 .update_in(cx, |workspace, window, cx| {
2955 if let Some(split_direction) = split_direction {
2956 to_pane = workspace.split_pane(
2957 to_pane,
2958 split_direction,
2959 window,
2960 cx,
2961 );
2962 }
2963 let new_item_handle = to_pane.update(cx, |pane, cx| {
2964 pane.open_item(
2965 project_entry_id,
2966 project_path,
2967 true,
2968 false,
2969 true,
2970 target,
2971 window,
2972 cx,
2973 build_item,
2974 )
2975 });
2976 (to_pane, new_item_handle)
2977 })
2978 .log_err()?;
2979 to_pane
2980 .update_in(cx, |this, window, cx| {
2981 let Some(index) = this.index_for_item(&*new_item_handle)
2982 else {
2983 return;
2984 };
2985
2986 if target.map_or(false, |target| this.is_tab_pinned(target))
2987 {
2988 this.pin_tab_at(index, window, cx);
2989 }
2990 })
2991 .ok()?
2992 }
2993 Some(())
2994 })
2995 .detach();
2996 };
2997 });
2998 })
2999 .log_err();
3000 }
3001
3002 fn handle_external_paths_drop(
3003 &mut self,
3004 paths: &ExternalPaths,
3005 window: &mut Window,
3006 cx: &mut Context<Self>,
3007 ) {
3008 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
3009 if let ControlFlow::Break(()) = custom_drop_handle(self, paths, window, cx) {
3010 return;
3011 }
3012 }
3013 let mut to_pane = cx.entity().clone();
3014 let mut split_direction = self.drag_split_direction;
3015 let paths = paths.paths().to_vec();
3016 let is_remote = self
3017 .workspace
3018 .update(cx, |workspace, cx| {
3019 if workspace.project().read(cx).is_via_collab() {
3020 workspace.show_error(
3021 &anyhow::anyhow!("Cannot drop files on a remote project"),
3022 cx,
3023 );
3024 true
3025 } else {
3026 false
3027 }
3028 })
3029 .unwrap_or(true);
3030 if is_remote {
3031 return;
3032 }
3033
3034 self.workspace
3035 .update(cx, |workspace, cx| {
3036 let fs = Arc::clone(workspace.project().read(cx).fs());
3037 cx.spawn_in(window, async move |workspace, cx| {
3038 let mut is_file_checks = FuturesUnordered::new();
3039 for path in &paths {
3040 is_file_checks.push(fs.is_file(path))
3041 }
3042 let mut has_files_to_open = false;
3043 while let Some(is_file) = is_file_checks.next().await {
3044 if is_file {
3045 has_files_to_open = true;
3046 break;
3047 }
3048 }
3049 drop(is_file_checks);
3050 if !has_files_to_open {
3051 split_direction = None;
3052 }
3053
3054 if let Ok(open_task) = workspace.update_in(cx, |workspace, window, cx| {
3055 if let Some(split_direction) = split_direction {
3056 to_pane = workspace.split_pane(to_pane, split_direction, window, cx);
3057 }
3058 workspace.open_paths(
3059 paths,
3060 OpenOptions {
3061 visible: Some(OpenVisible::OnlyDirectories),
3062 ..Default::default()
3063 },
3064 Some(to_pane.downgrade()),
3065 window,
3066 cx,
3067 )
3068 }) {
3069 let opened_items: Vec<_> = open_task.await;
3070 _ = workspace.update(cx, |workspace, cx| {
3071 for item in opened_items.into_iter().flatten() {
3072 if let Err(e) = item {
3073 workspace.show_error(&e, cx);
3074 }
3075 }
3076 });
3077 }
3078 })
3079 .detach();
3080 })
3081 .log_err();
3082 }
3083
3084 pub fn display_nav_history_buttons(&mut self, display: Option<bool>) {
3085 self.display_nav_history_buttons = display;
3086 }
3087
3088 fn get_non_closeable_item_ids(&self, close_pinned: bool) -> Vec<EntityId> {
3089 if close_pinned {
3090 return vec![];
3091 }
3092
3093 self.items
3094 .iter()
3095 .enumerate()
3096 .filter(|(index, _item)| self.is_tab_pinned(*index))
3097 .map(|(_, item)| item.item_id())
3098 .collect()
3099 }
3100
3101 pub fn drag_split_direction(&self) -> Option<SplitDirection> {
3102 self.drag_split_direction
3103 }
3104
3105 pub fn set_zoom_out_on_close(&mut self, zoom_out_on_close: bool) {
3106 self.zoom_out_on_close = zoom_out_on_close;
3107 }
3108}
3109
3110fn default_render_tab_bar_buttons(
3111 pane: &mut Pane,
3112 window: &mut Window,
3113 cx: &mut Context<Pane>,
3114) -> (Option<AnyElement>, Option<AnyElement>) {
3115 if !pane.has_focus(window, cx) && !pane.context_menu_focused(window, cx) {
3116 return (None, None);
3117 }
3118 // Ideally we would return a vec of elements here to pass directly to the [TabBar]'s
3119 // `end_slot`, but due to needing a view here that isn't possible.
3120 let right_children = h_flex()
3121 // Instead we need to replicate the spacing from the [TabBar]'s `end_slot` here.
3122 .gap(DynamicSpacing::Base04.rems(cx))
3123 .child(
3124 PopoverMenu::new("pane-tab-bar-popover-menu")
3125 .trigger_with_tooltip(
3126 IconButton::new("plus", IconName::Plus).icon_size(IconSize::Small),
3127 Tooltip::text("New..."),
3128 )
3129 .anchor(Corner::TopRight)
3130 .with_handle(pane.new_item_context_menu_handle.clone())
3131 .menu(move |window, cx| {
3132 Some(ContextMenu::build(window, cx, |menu, _, _| {
3133 menu.action("New File", NewFile.boxed_clone())
3134 .action("Open File", ToggleFileFinder::default().boxed_clone())
3135 .separator()
3136 .action(
3137 "Search Project",
3138 DeploySearch {
3139 replace_enabled: false,
3140 included_files: None,
3141 excluded_files: None,
3142 }
3143 .boxed_clone(),
3144 )
3145 .action("Search Symbols", ToggleProjectSymbols.boxed_clone())
3146 .separator()
3147 .action("New Terminal", NewTerminal.boxed_clone())
3148 }))
3149 }),
3150 )
3151 .child(
3152 PopoverMenu::new("pane-tab-bar-split")
3153 .trigger_with_tooltip(
3154 IconButton::new("split", IconName::Split).icon_size(IconSize::Small),
3155 Tooltip::text("Split Pane"),
3156 )
3157 .anchor(Corner::TopRight)
3158 .with_handle(pane.split_item_context_menu_handle.clone())
3159 .menu(move |window, cx| {
3160 ContextMenu::build(window, cx, |menu, _, _| {
3161 menu.action("Split Right", SplitRight.boxed_clone())
3162 .action("Split Left", SplitLeft.boxed_clone())
3163 .action("Split Up", SplitUp.boxed_clone())
3164 .action("Split Down", SplitDown.boxed_clone())
3165 })
3166 .into()
3167 }),
3168 )
3169 .child({
3170 let zoomed = pane.is_zoomed();
3171 IconButton::new("toggle_zoom", IconName::Maximize)
3172 .icon_size(IconSize::Small)
3173 .toggle_state(zoomed)
3174 .selected_icon(IconName::Minimize)
3175 .on_click(cx.listener(|pane, _, window, cx| {
3176 pane.toggle_zoom(&crate::ToggleZoom, window, cx);
3177 }))
3178 .tooltip(move |window, cx| {
3179 Tooltip::for_action(
3180 if zoomed { "Zoom Out" } else { "Zoom In" },
3181 &ToggleZoom,
3182 window,
3183 cx,
3184 )
3185 })
3186 })
3187 .into_any_element()
3188 .into();
3189 (None, right_children)
3190}
3191
3192impl Focusable for Pane {
3193 fn focus_handle(&self, _cx: &App) -> FocusHandle {
3194 self.focus_handle.clone()
3195 }
3196}
3197
3198impl Render for Pane {
3199 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3200 let mut key_context = KeyContext::new_with_defaults();
3201 key_context.add("Pane");
3202 if self.active_item().is_none() {
3203 key_context.add("EmptyPane");
3204 }
3205
3206 let should_display_tab_bar = self.should_display_tab_bar.clone();
3207 let display_tab_bar = should_display_tab_bar(window, cx);
3208 let Some(project) = self.project.upgrade() else {
3209 return div().track_focus(&self.focus_handle(cx));
3210 };
3211 let is_local = project.read(cx).is_local();
3212
3213 v_flex()
3214 .key_context(key_context)
3215 .track_focus(&self.focus_handle(cx))
3216 .size_full()
3217 .flex_none()
3218 .overflow_hidden()
3219 .on_action(cx.listener(|pane, _: &AlternateFile, window, cx| {
3220 pane.alternate_file(window, cx);
3221 }))
3222 .on_action(
3223 cx.listener(|pane, _: &SplitLeft, _, cx| pane.split(SplitDirection::Left, cx)),
3224 )
3225 .on_action(cx.listener(|pane, _: &SplitUp, _, cx| pane.split(SplitDirection::Up, cx)))
3226 .on_action(cx.listener(|pane, _: &SplitHorizontal, _, cx| {
3227 pane.split(SplitDirection::horizontal(cx), cx)
3228 }))
3229 .on_action(cx.listener(|pane, _: &SplitVertical, _, cx| {
3230 pane.split(SplitDirection::vertical(cx), cx)
3231 }))
3232 .on_action(
3233 cx.listener(|pane, _: &SplitRight, _, cx| pane.split(SplitDirection::Right, cx)),
3234 )
3235 .on_action(
3236 cx.listener(|pane, _: &SplitDown, _, cx| pane.split(SplitDirection::Down, cx)),
3237 )
3238 .on_action(
3239 cx.listener(|pane, _: &GoBack, window, cx| pane.navigate_backward(window, cx)),
3240 )
3241 .on_action(
3242 cx.listener(|pane, _: &GoForward, window, cx| pane.navigate_forward(window, cx)),
3243 )
3244 .on_action(cx.listener(|_, _: &JoinIntoNext, _, cx| {
3245 cx.emit(Event::JoinIntoNext);
3246 }))
3247 .on_action(cx.listener(|_, _: &JoinAll, _, cx| {
3248 cx.emit(Event::JoinAll);
3249 }))
3250 .on_action(cx.listener(Pane::toggle_zoom))
3251 .on_action(
3252 cx.listener(|pane: &mut Pane, action: &ActivateItem, window, cx| {
3253 pane.activate_item(
3254 action.0.min(pane.items.len().saturating_sub(1)),
3255 true,
3256 true,
3257 window,
3258 cx,
3259 );
3260 }),
3261 )
3262 .on_action(
3263 cx.listener(|pane: &mut Pane, _: &ActivateLastItem, window, cx| {
3264 pane.activate_item(pane.items.len().saturating_sub(1), true, true, window, cx);
3265 }),
3266 )
3267 .on_action(
3268 cx.listener(|pane: &mut Pane, _: &ActivatePreviousItem, window, cx| {
3269 pane.activate_prev_item(true, window, cx);
3270 }),
3271 )
3272 .on_action(
3273 cx.listener(|pane: &mut Pane, _: &ActivateNextItem, window, cx| {
3274 pane.activate_next_item(true, window, cx);
3275 }),
3276 )
3277 .on_action(
3278 cx.listener(|pane, _: &SwapItemLeft, window, cx| pane.swap_item_left(window, cx)),
3279 )
3280 .on_action(
3281 cx.listener(|pane, _: &SwapItemRight, window, cx| pane.swap_item_right(window, cx)),
3282 )
3283 .on_action(cx.listener(|pane, action, window, cx| {
3284 pane.toggle_pin_tab(action, window, cx);
3285 }))
3286 .when(PreviewTabsSettings::get_global(cx).enabled, |this| {
3287 this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, _, cx| {
3288 if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) {
3289 if pane.is_active_preview_item(active_item_id) {
3290 pane.set_preview_item_id(None, cx);
3291 } else {
3292 pane.set_preview_item_id(Some(active_item_id), cx);
3293 }
3294 }
3295 }))
3296 })
3297 .on_action(
3298 cx.listener(|pane: &mut Self, action: &CloseActiveItem, window, cx| {
3299 if let Some(task) = pane.close_active_item(action, window, cx) {
3300 task.detach_and_log_err(cx)
3301 }
3302 }),
3303 )
3304 .on_action(
3305 cx.listener(|pane: &mut Self, action: &CloseInactiveItems, window, cx| {
3306 if let Some(task) = pane.close_inactive_items(action, window, cx) {
3307 task.detach_and_log_err(cx)
3308 }
3309 }),
3310 )
3311 .on_action(
3312 cx.listener(|pane: &mut Self, action: &CloseCleanItems, window, cx| {
3313 if let Some(task) = pane.close_clean_items(action, window, cx) {
3314 task.detach_and_log_err(cx)
3315 }
3316 }),
3317 )
3318 .on_action(cx.listener(
3319 |pane: &mut Self, action: &CloseItemsToTheLeft, window, cx| {
3320 if let Some(task) = pane.close_items_to_the_left(action, window, cx) {
3321 task.detach_and_log_err(cx)
3322 }
3323 },
3324 ))
3325 .on_action(cx.listener(
3326 |pane: &mut Self, action: &CloseItemsToTheRight, window, cx| {
3327 if let Some(task) = pane.close_items_to_the_right(action, window, cx) {
3328 task.detach_and_log_err(cx)
3329 }
3330 },
3331 ))
3332 .on_action(
3333 cx.listener(|pane: &mut Self, action: &CloseAllItems, window, cx| {
3334 if let Some(task) = pane.close_all_items(action, window, cx) {
3335 task.detach_and_log_err(cx)
3336 }
3337 }),
3338 )
3339 .on_action(
3340 cx.listener(|pane: &mut Self, action: &CloseActiveItem, window, cx| {
3341 if let Some(task) = pane.close_active_item(action, window, cx) {
3342 task.detach_and_log_err(cx)
3343 }
3344 }),
3345 )
3346 .on_action(
3347 cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, _, cx| {
3348 let entry_id = action
3349 .entry_id
3350 .map(ProjectEntryId::from_proto)
3351 .or_else(|| pane.active_item()?.project_entry_ids(cx).first().copied());
3352 if let Some(entry_id) = entry_id {
3353 pane.project
3354 .update(cx, |_, cx| {
3355 cx.emit(project::Event::RevealInProjectPanel(entry_id))
3356 })
3357 .ok();
3358 }
3359 }),
3360 )
3361 .on_action(cx.listener(|_, _: &menu::Cancel, window, cx| {
3362 if cx.stop_active_drag(window) {
3363 return;
3364 } else {
3365 cx.propagate();
3366 }
3367 }))
3368 .when(self.active_item().is_some() && display_tab_bar, |pane| {
3369 pane.child((self.render_tab_bar.clone())(self, window, cx))
3370 })
3371 .child({
3372 let has_worktrees = project.read(cx).visible_worktrees(cx).next().is_some();
3373 // main content
3374 div()
3375 .flex_1()
3376 .relative()
3377 .group("")
3378 .overflow_hidden()
3379 .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
3380 .on_drag_move::<DraggedSelection>(cx.listener(Self::handle_drag_move))
3381 .when(is_local, |div| {
3382 div.on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
3383 })
3384 .map(|div| {
3385 if let Some(item) = self.active_item() {
3386 div.id("pane_placeholder")
3387 .v_flex()
3388 .size_full()
3389 .overflow_hidden()
3390 .child(self.toolbar.clone())
3391 .child(item.to_any())
3392 } else {
3393 let placeholder = div
3394 .id("pane_placeholder")
3395 .h_flex()
3396 .size_full()
3397 .justify_center()
3398 .on_click(cx.listener(
3399 move |this, event: &ClickEvent, window, cx| {
3400 if event.up.click_count == 2 {
3401 window.dispatch_action(
3402 this.double_click_dispatch_action.boxed_clone(),
3403 cx,
3404 );
3405 }
3406 },
3407 ));
3408 if has_worktrees {
3409 placeholder
3410 } else {
3411 placeholder.child(
3412 Label::new("Open a file or project to get started.")
3413 .color(Color::Muted),
3414 )
3415 }
3416 }
3417 })
3418 .child(
3419 // drag target
3420 div()
3421 .invisible()
3422 .absolute()
3423 .bg(cx.theme().colors().drop_target_background)
3424 .group_drag_over::<DraggedTab>("", |style| style.visible())
3425 .group_drag_over::<DraggedSelection>("", |style| style.visible())
3426 .when(is_local, |div| {
3427 div.group_drag_over::<ExternalPaths>("", |style| style.visible())
3428 })
3429 .when_some(self.can_drop_predicate.clone(), |this, p| {
3430 this.can_drop(move |a, window, cx| p(a, window, cx))
3431 })
3432 .on_drop(cx.listener(move |this, dragged_tab, window, cx| {
3433 this.handle_tab_drop(
3434 dragged_tab,
3435 this.active_item_index(),
3436 window,
3437 cx,
3438 )
3439 }))
3440 .on_drop(cx.listener(
3441 move |this, selection: &DraggedSelection, window, cx| {
3442 this.handle_dragged_selection_drop(selection, None, window, cx)
3443 },
3444 ))
3445 .on_drop(cx.listener(move |this, paths, window, cx| {
3446 this.handle_external_paths_drop(paths, window, cx)
3447 }))
3448 .map(|div| {
3449 let size = DefiniteLength::Fraction(0.5);
3450 match self.drag_split_direction {
3451 None => div.top_0().right_0().bottom_0().left_0(),
3452 Some(SplitDirection::Up) => {
3453 div.top_0().left_0().right_0().h(size)
3454 }
3455 Some(SplitDirection::Down) => {
3456 div.left_0().bottom_0().right_0().h(size)
3457 }
3458 Some(SplitDirection::Left) => {
3459 div.top_0().left_0().bottom_0().w(size)
3460 }
3461 Some(SplitDirection::Right) => {
3462 div.top_0().bottom_0().right_0().w(size)
3463 }
3464 }
3465 }),
3466 )
3467 })
3468 .on_mouse_down(
3469 MouseButton::Navigate(NavigationDirection::Back),
3470 cx.listener(|pane, _, window, cx| {
3471 if let Some(workspace) = pane.workspace.upgrade() {
3472 let pane = cx.entity().downgrade();
3473 window.defer(cx, move |window, cx| {
3474 workspace.update(cx, |workspace, cx| {
3475 workspace.go_back(pane, window, cx).detach_and_log_err(cx)
3476 })
3477 })
3478 }
3479 }),
3480 )
3481 .on_mouse_down(
3482 MouseButton::Navigate(NavigationDirection::Forward),
3483 cx.listener(|pane, _, window, cx| {
3484 if let Some(workspace) = pane.workspace.upgrade() {
3485 let pane = cx.entity().downgrade();
3486 window.defer(cx, move |window, cx| {
3487 workspace.update(cx, |workspace, cx| {
3488 workspace
3489 .go_forward(pane, window, cx)
3490 .detach_and_log_err(cx)
3491 })
3492 })
3493 }
3494 }),
3495 )
3496 }
3497}
3498
3499impl ItemNavHistory {
3500 pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut App) {
3501 if self
3502 .item
3503 .upgrade()
3504 .is_some_and(|item| item.include_in_nav_history())
3505 {
3506 self.history
3507 .push(data, self.item.clone(), self.is_preview, cx);
3508 }
3509 }
3510
3511 pub fn pop_backward(&mut self, cx: &mut App) -> Option<NavigationEntry> {
3512 self.history.pop(NavigationMode::GoingBack, cx)
3513 }
3514
3515 pub fn pop_forward(&mut self, cx: &mut App) -> Option<NavigationEntry> {
3516 self.history.pop(NavigationMode::GoingForward, cx)
3517 }
3518}
3519
3520impl NavHistory {
3521 pub fn for_each_entry(
3522 &self,
3523 cx: &App,
3524 mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
3525 ) {
3526 let borrowed_history = self.0.lock();
3527 borrowed_history
3528 .forward_stack
3529 .iter()
3530 .chain(borrowed_history.backward_stack.iter())
3531 .chain(borrowed_history.closed_stack.iter())
3532 .for_each(|entry| {
3533 if let Some(project_and_abs_path) =
3534 borrowed_history.paths_by_item.get(&entry.item.id())
3535 {
3536 f(entry, project_and_abs_path.clone());
3537 } else if let Some(item) = entry.item.upgrade() {
3538 if let Some(path) = item.project_path(cx) {
3539 f(entry, (path, None));
3540 }
3541 }
3542 })
3543 }
3544
3545 pub fn set_mode(&mut self, mode: NavigationMode) {
3546 self.0.lock().mode = mode;
3547 }
3548
3549 pub fn mode(&self) -> NavigationMode {
3550 self.0.lock().mode
3551 }
3552
3553 pub fn disable(&mut self) {
3554 self.0.lock().mode = NavigationMode::Disabled;
3555 }
3556
3557 pub fn enable(&mut self) {
3558 self.0.lock().mode = NavigationMode::Normal;
3559 }
3560
3561 pub fn pop(&mut self, mode: NavigationMode, cx: &mut App) -> Option<NavigationEntry> {
3562 let mut state = self.0.lock();
3563 let entry = match mode {
3564 NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
3565 return None;
3566 }
3567 NavigationMode::GoingBack => &mut state.backward_stack,
3568 NavigationMode::GoingForward => &mut state.forward_stack,
3569 NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
3570 }
3571 .pop_back();
3572 if entry.is_some() {
3573 state.did_update(cx);
3574 }
3575 entry
3576 }
3577
3578 pub fn push<D: 'static + Send + Any>(
3579 &mut self,
3580 data: Option<D>,
3581 item: Arc<dyn WeakItemHandle>,
3582 is_preview: bool,
3583 cx: &mut App,
3584 ) {
3585 let state = &mut *self.0.lock();
3586 match state.mode {
3587 NavigationMode::Disabled => {}
3588 NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
3589 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3590 state.backward_stack.pop_front();
3591 }
3592 state.backward_stack.push_back(NavigationEntry {
3593 item,
3594 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3595 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3596 is_preview,
3597 });
3598 state.forward_stack.clear();
3599 }
3600 NavigationMode::GoingBack => {
3601 if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3602 state.forward_stack.pop_front();
3603 }
3604 state.forward_stack.push_back(NavigationEntry {
3605 item,
3606 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3607 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3608 is_preview,
3609 });
3610 }
3611 NavigationMode::GoingForward => {
3612 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3613 state.backward_stack.pop_front();
3614 }
3615 state.backward_stack.push_back(NavigationEntry {
3616 item,
3617 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3618 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3619 is_preview,
3620 });
3621 }
3622 NavigationMode::ClosingItem => {
3623 if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3624 state.closed_stack.pop_front();
3625 }
3626 state.closed_stack.push_back(NavigationEntry {
3627 item,
3628 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3629 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3630 is_preview,
3631 });
3632 }
3633 }
3634 state.did_update(cx);
3635 }
3636
3637 pub fn remove_item(&mut self, item_id: EntityId) {
3638 let mut state = self.0.lock();
3639 state.paths_by_item.remove(&item_id);
3640 state
3641 .backward_stack
3642 .retain(|entry| entry.item.id() != item_id);
3643 state
3644 .forward_stack
3645 .retain(|entry| entry.item.id() != item_id);
3646 state
3647 .closed_stack
3648 .retain(|entry| entry.item.id() != item_id);
3649 }
3650
3651 pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
3652 self.0.lock().paths_by_item.get(&item_id).cloned()
3653 }
3654}
3655
3656impl NavHistoryState {
3657 pub fn did_update(&self, cx: &mut App) {
3658 if let Some(pane) = self.pane.upgrade() {
3659 cx.defer(move |cx| {
3660 pane.update(cx, |pane, cx| pane.history_updated(cx));
3661 });
3662 }
3663 }
3664}
3665
3666fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
3667 let path = buffer_path
3668 .as_ref()
3669 .and_then(|p| {
3670 p.path
3671 .to_str()
3672 .and_then(|s| if s.is_empty() { None } else { Some(s) })
3673 })
3674 .unwrap_or("This buffer");
3675 let path = truncate_and_remove_front(path, 80);
3676 format!("{path} contains unsaved edits. Do you want to save it?")
3677}
3678
3679pub fn tab_details(items: &[Box<dyn ItemHandle>], _window: &Window, cx: &App) -> Vec<usize> {
3680 let mut tab_details = items.iter().map(|_| 0).collect::<Vec<_>>();
3681 let mut tab_descriptions = HashMap::default();
3682 let mut done = false;
3683 while !done {
3684 done = true;
3685
3686 // Store item indices by their tab description.
3687 for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() {
3688 let description = item.tab_content_text(*detail, cx);
3689 if *detail == 0 || description != item.tab_content_text(detail - 1, cx) {
3690 tab_descriptions
3691 .entry(description)
3692 .or_insert(Vec::new())
3693 .push(ix);
3694 }
3695 }
3696
3697 // If two or more items have the same tab description, increase their level
3698 // of detail and try again.
3699 for (_, item_ixs) in tab_descriptions.drain() {
3700 if item_ixs.len() > 1 {
3701 done = false;
3702 for ix in item_ixs {
3703 tab_details[ix] += 1;
3704 }
3705 }
3706 }
3707 }
3708
3709 tab_details
3710}
3711
3712pub fn render_item_indicator(item: Box<dyn ItemHandle>, cx: &App) -> Option<Indicator> {
3713 maybe!({
3714 let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
3715 (true, _) => Color::Warning,
3716 (_, true) => Color::Accent,
3717 (false, false) => return None,
3718 };
3719
3720 Some(Indicator::dot().color(indicator_color))
3721 })
3722}
3723
3724impl Render for DraggedTab {
3725 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3726 let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
3727 let label = self.item.tab_content(
3728 TabContentParams {
3729 detail: Some(self.detail),
3730 selected: false,
3731 preview: false,
3732 deemphasized: false,
3733 },
3734 window,
3735 cx,
3736 );
3737 Tab::new("")
3738 .toggle_state(self.is_active)
3739 .child(label)
3740 .render(window, cx)
3741 .font(ui_font)
3742 }
3743}
3744
3745#[cfg(test)]
3746mod tests {
3747 use std::num::NonZero;
3748
3749 use super::*;
3750 use crate::item::test::{TestItem, TestProjectItem};
3751 use gpui::{TestAppContext, VisualTestContext};
3752 use project::FakeFs;
3753 use settings::SettingsStore;
3754 use theme::LoadThemes;
3755
3756 #[gpui::test]
3757 async fn test_remove_active_empty(cx: &mut TestAppContext) {
3758 init_test(cx);
3759 let fs = FakeFs::new(cx.executor());
3760
3761 let project = Project::test(fs, None, cx).await;
3762 let (workspace, cx) =
3763 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3764 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3765
3766 pane.update_in(cx, |pane, window, cx| {
3767 assert!(
3768 pane.close_active_item(
3769 &CloseActiveItem {
3770 save_intent: None,
3771 close_pinned: false
3772 },
3773 window,
3774 cx
3775 )
3776 .is_none()
3777 )
3778 });
3779 }
3780
3781 #[gpui::test]
3782 async fn test_add_item_capped_to_max_tabs(cx: &mut TestAppContext) {
3783 init_test(cx);
3784 let fs = FakeFs::new(cx.executor());
3785
3786 let project = Project::test(fs, None, cx).await;
3787 let (workspace, cx) =
3788 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3789 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3790
3791 for i in 0..7 {
3792 add_labeled_item(&pane, format!("{}", i).as_str(), false, cx);
3793 }
3794 set_max_tabs(cx, Some(5));
3795 add_labeled_item(&pane, "7", false, cx);
3796 // Remove items to respect the max tab cap.
3797 assert_item_labels(&pane, ["3", "4", "5", "6", "7*"], cx);
3798 pane.update_in(cx, |pane, window, cx| {
3799 pane.activate_item(0, false, false, window, cx);
3800 });
3801 add_labeled_item(&pane, "X", false, cx);
3802 // Respect activation order.
3803 assert_item_labels(&pane, ["3", "X*", "5", "6", "7"], cx);
3804
3805 for i in 0..7 {
3806 add_labeled_item(&pane, format!("D{}", i).as_str(), true, cx);
3807 }
3808 // Keeps dirty items, even over max tab cap.
3809 assert_item_labels(
3810 &pane,
3811 ["D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6*^"],
3812 cx,
3813 );
3814
3815 set_max_tabs(cx, None);
3816 for i in 0..7 {
3817 add_labeled_item(&pane, format!("N{}", i).as_str(), false, cx);
3818 }
3819 // No cap when max tabs is None.
3820 assert_item_labels(
3821 &pane,
3822 [
3823 "D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6^", "N0", "N1", "N2", "N3", "N4",
3824 "N5", "N6*",
3825 ],
3826 cx,
3827 );
3828 }
3829
3830 #[gpui::test]
3831 async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
3832 init_test(cx);
3833 let fs = FakeFs::new(cx.executor());
3834
3835 let project = Project::test(fs, None, cx).await;
3836 let (workspace, cx) =
3837 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3838 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3839
3840 // 1. Add with a destination index
3841 // a. Add before the active item
3842 set_labeled_items(&pane, ["A", "B*", "C"], cx);
3843 pane.update_in(cx, |pane, window, cx| {
3844 pane.add_item(
3845 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3846 false,
3847 false,
3848 Some(0),
3849 window,
3850 cx,
3851 );
3852 });
3853 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3854
3855 // b. Add after the active item
3856 set_labeled_items(&pane, ["A", "B*", "C"], cx);
3857 pane.update_in(cx, |pane, window, cx| {
3858 pane.add_item(
3859 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3860 false,
3861 false,
3862 Some(2),
3863 window,
3864 cx,
3865 );
3866 });
3867 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3868
3869 // c. Add at the end of the item list (including off the length)
3870 set_labeled_items(&pane, ["A", "B*", "C"], cx);
3871 pane.update_in(cx, |pane, window, cx| {
3872 pane.add_item(
3873 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3874 false,
3875 false,
3876 Some(5),
3877 window,
3878 cx,
3879 );
3880 });
3881 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3882
3883 // 2. Add without a destination index
3884 // a. Add with active item at the start of the item list
3885 set_labeled_items(&pane, ["A*", "B", "C"], cx);
3886 pane.update_in(cx, |pane, window, cx| {
3887 pane.add_item(
3888 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3889 false,
3890 false,
3891 None,
3892 window,
3893 cx,
3894 );
3895 });
3896 set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
3897
3898 // b. Add with active item at the end of the item list
3899 set_labeled_items(&pane, ["A", "B", "C*"], cx);
3900 pane.update_in(cx, |pane, window, cx| {
3901 pane.add_item(
3902 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3903 false,
3904 false,
3905 None,
3906 window,
3907 cx,
3908 );
3909 });
3910 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3911 }
3912
3913 #[gpui::test]
3914 async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
3915 init_test(cx);
3916 let fs = FakeFs::new(cx.executor());
3917
3918 let project = Project::test(fs, None, cx).await;
3919 let (workspace, cx) =
3920 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3921 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3922
3923 // 1. Add with a destination index
3924 // 1a. Add before the active item
3925 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3926 pane.update_in(cx, |pane, window, cx| {
3927 pane.add_item(d, false, false, Some(0), window, cx);
3928 });
3929 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3930
3931 // 1b. Add after the active item
3932 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3933 pane.update_in(cx, |pane, window, cx| {
3934 pane.add_item(d, false, false, Some(2), window, cx);
3935 });
3936 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3937
3938 // 1c. Add at the end of the item list (including off the length)
3939 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3940 pane.update_in(cx, |pane, window, cx| {
3941 pane.add_item(a, false, false, Some(5), window, cx);
3942 });
3943 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3944
3945 // 1d. Add same item to active index
3946 let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3947 pane.update_in(cx, |pane, window, cx| {
3948 pane.add_item(b, false, false, Some(1), window, cx);
3949 });
3950 assert_item_labels(&pane, ["A", "B*", "C"], cx);
3951
3952 // 1e. Add item to index after same item in last position
3953 let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3954 pane.update_in(cx, |pane, window, cx| {
3955 pane.add_item(c, false, false, Some(2), window, cx);
3956 });
3957 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3958
3959 // 2. Add without a destination index
3960 // 2a. Add with active item at the start of the item list
3961 let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
3962 pane.update_in(cx, |pane, window, cx| {
3963 pane.add_item(d, false, false, None, window, cx);
3964 });
3965 assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
3966
3967 // 2b. Add with active item at the end of the item list
3968 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
3969 pane.update_in(cx, |pane, window, cx| {
3970 pane.add_item(a, false, false, None, window, cx);
3971 });
3972 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3973
3974 // 2c. Add active item to active item at end of list
3975 let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
3976 pane.update_in(cx, |pane, window, cx| {
3977 pane.add_item(c, false, false, None, window, cx);
3978 });
3979 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3980
3981 // 2d. Add active item to active item at start of list
3982 let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
3983 pane.update_in(cx, |pane, window, cx| {
3984 pane.add_item(a, false, false, None, window, cx);
3985 });
3986 assert_item_labels(&pane, ["A*", "B", "C"], cx);
3987 }
3988
3989 #[gpui::test]
3990 async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
3991 init_test(cx);
3992 let fs = FakeFs::new(cx.executor());
3993
3994 let project = Project::test(fs, None, cx).await;
3995 let (workspace, cx) =
3996 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3997 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3998
3999 // singleton view
4000 pane.update_in(cx, |pane, window, cx| {
4001 pane.add_item(
4002 Box::new(cx.new(|cx| {
4003 TestItem::new(cx)
4004 .with_singleton(true)
4005 .with_label("buffer 1")
4006 .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
4007 })),
4008 false,
4009 false,
4010 None,
4011 window,
4012 cx,
4013 );
4014 });
4015 assert_item_labels(&pane, ["buffer 1*"], cx);
4016
4017 // new singleton view with the same project entry
4018 pane.update_in(cx, |pane, window, cx| {
4019 pane.add_item(
4020 Box::new(cx.new(|cx| {
4021 TestItem::new(cx)
4022 .with_singleton(true)
4023 .with_label("buffer 1")
4024 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4025 })),
4026 false,
4027 false,
4028 None,
4029 window,
4030 cx,
4031 );
4032 });
4033 assert_item_labels(&pane, ["buffer 1*"], cx);
4034
4035 // new singleton view with different project entry
4036 pane.update_in(cx, |pane, window, cx| {
4037 pane.add_item(
4038 Box::new(cx.new(|cx| {
4039 TestItem::new(cx)
4040 .with_singleton(true)
4041 .with_label("buffer 2")
4042 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
4043 })),
4044 false,
4045 false,
4046 None,
4047 window,
4048 cx,
4049 );
4050 });
4051 assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
4052
4053 // new multibuffer view with the same project entry
4054 pane.update_in(cx, |pane, window, cx| {
4055 pane.add_item(
4056 Box::new(cx.new(|cx| {
4057 TestItem::new(cx)
4058 .with_singleton(false)
4059 .with_label("multibuffer 1")
4060 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4061 })),
4062 false,
4063 false,
4064 None,
4065 window,
4066 cx,
4067 );
4068 });
4069 assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
4070
4071 // another multibuffer view with the same project entry
4072 pane.update_in(cx, |pane, window, cx| {
4073 pane.add_item(
4074 Box::new(cx.new(|cx| {
4075 TestItem::new(cx)
4076 .with_singleton(false)
4077 .with_label("multibuffer 1b")
4078 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4079 })),
4080 false,
4081 false,
4082 None,
4083 window,
4084 cx,
4085 );
4086 });
4087 assert_item_labels(
4088 &pane,
4089 ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
4090 cx,
4091 );
4092 }
4093
4094 #[gpui::test]
4095 async fn test_remove_item_ordering_history(cx: &mut TestAppContext) {
4096 init_test(cx);
4097 let fs = FakeFs::new(cx.executor());
4098
4099 let project = Project::test(fs, None, cx).await;
4100 let (workspace, cx) =
4101 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4102 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4103
4104 add_labeled_item(&pane, "A", false, cx);
4105 add_labeled_item(&pane, "B", false, cx);
4106 add_labeled_item(&pane, "C", false, cx);
4107 add_labeled_item(&pane, "D", false, cx);
4108 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4109
4110 pane.update_in(cx, |pane, window, cx| {
4111 pane.activate_item(1, false, false, window, cx)
4112 });
4113 add_labeled_item(&pane, "1", false, cx);
4114 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
4115
4116 pane.update_in(cx, |pane, window, cx| {
4117 pane.close_active_item(
4118 &CloseActiveItem {
4119 save_intent: None,
4120 close_pinned: false,
4121 },
4122 window,
4123 cx,
4124 )
4125 })
4126 .unwrap()
4127 .await
4128 .unwrap();
4129 assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
4130
4131 pane.update_in(cx, |pane, window, cx| {
4132 pane.activate_item(3, false, false, window, cx)
4133 });
4134 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4135
4136 pane.update_in(cx, |pane, window, cx| {
4137 pane.close_active_item(
4138 &CloseActiveItem {
4139 save_intent: None,
4140 close_pinned: false,
4141 },
4142 window,
4143 cx,
4144 )
4145 })
4146 .unwrap()
4147 .await
4148 .unwrap();
4149 assert_item_labels(&pane, ["A", "B*", "C"], cx);
4150
4151 pane.update_in(cx, |pane, window, cx| {
4152 pane.close_active_item(
4153 &CloseActiveItem {
4154 save_intent: None,
4155 close_pinned: false,
4156 },
4157 window,
4158 cx,
4159 )
4160 })
4161 .unwrap()
4162 .await
4163 .unwrap();
4164 assert_item_labels(&pane, ["A", "C*"], cx);
4165
4166 pane.update_in(cx, |pane, window, cx| {
4167 pane.close_active_item(
4168 &CloseActiveItem {
4169 save_intent: None,
4170 close_pinned: false,
4171 },
4172 window,
4173 cx,
4174 )
4175 })
4176 .unwrap()
4177 .await
4178 .unwrap();
4179 assert_item_labels(&pane, ["A*"], cx);
4180 }
4181
4182 #[gpui::test]
4183 async fn test_remove_item_ordering_neighbour(cx: &mut TestAppContext) {
4184 init_test(cx);
4185 cx.update_global::<SettingsStore, ()>(|s, cx| {
4186 s.update_user_settings::<ItemSettings>(cx, |s| {
4187 s.activate_on_close = Some(ActivateOnClose::Neighbour);
4188 });
4189 });
4190 let fs = FakeFs::new(cx.executor());
4191
4192 let project = Project::test(fs, None, cx).await;
4193 let (workspace, cx) =
4194 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4195 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4196
4197 add_labeled_item(&pane, "A", false, cx);
4198 add_labeled_item(&pane, "B", false, cx);
4199 add_labeled_item(&pane, "C", false, cx);
4200 add_labeled_item(&pane, "D", false, cx);
4201 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4202
4203 pane.update_in(cx, |pane, window, cx| {
4204 pane.activate_item(1, false, false, window, cx)
4205 });
4206 add_labeled_item(&pane, "1", false, cx);
4207 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
4208
4209 pane.update_in(cx, |pane, window, cx| {
4210 pane.close_active_item(
4211 &CloseActiveItem {
4212 save_intent: None,
4213 close_pinned: false,
4214 },
4215 window,
4216 cx,
4217 )
4218 })
4219 .unwrap()
4220 .await
4221 .unwrap();
4222 assert_item_labels(&pane, ["A", "B", "C*", "D"], cx);
4223
4224 pane.update_in(cx, |pane, window, cx| {
4225 pane.activate_item(3, false, false, window, cx)
4226 });
4227 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4228
4229 pane.update_in(cx, |pane, window, cx| {
4230 pane.close_active_item(
4231 &CloseActiveItem {
4232 save_intent: None,
4233 close_pinned: false,
4234 },
4235 window,
4236 cx,
4237 )
4238 })
4239 .unwrap()
4240 .await
4241 .unwrap();
4242 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4243
4244 pane.update_in(cx, |pane, window, cx| {
4245 pane.close_active_item(
4246 &CloseActiveItem {
4247 save_intent: None,
4248 close_pinned: false,
4249 },
4250 window,
4251 cx,
4252 )
4253 })
4254 .unwrap()
4255 .await
4256 .unwrap();
4257 assert_item_labels(&pane, ["A", "B*"], cx);
4258
4259 pane.update_in(cx, |pane, window, cx| {
4260 pane.close_active_item(
4261 &CloseActiveItem {
4262 save_intent: None,
4263 close_pinned: false,
4264 },
4265 window,
4266 cx,
4267 )
4268 })
4269 .unwrap()
4270 .await
4271 .unwrap();
4272 assert_item_labels(&pane, ["A*"], cx);
4273 }
4274
4275 #[gpui::test]
4276 async fn test_remove_item_ordering_left_neighbour(cx: &mut TestAppContext) {
4277 init_test(cx);
4278 cx.update_global::<SettingsStore, ()>(|s, cx| {
4279 s.update_user_settings::<ItemSettings>(cx, |s| {
4280 s.activate_on_close = Some(ActivateOnClose::LeftNeighbour);
4281 });
4282 });
4283 let fs = FakeFs::new(cx.executor());
4284
4285 let project = Project::test(fs, None, cx).await;
4286 let (workspace, cx) =
4287 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4288 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4289
4290 add_labeled_item(&pane, "A", false, cx);
4291 add_labeled_item(&pane, "B", false, cx);
4292 add_labeled_item(&pane, "C", false, cx);
4293 add_labeled_item(&pane, "D", false, cx);
4294 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4295
4296 pane.update_in(cx, |pane, window, cx| {
4297 pane.activate_item(1, false, false, window, cx)
4298 });
4299 add_labeled_item(&pane, "1", false, cx);
4300 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
4301
4302 pane.update_in(cx, |pane, window, cx| {
4303 pane.close_active_item(
4304 &CloseActiveItem {
4305 save_intent: None,
4306 close_pinned: false,
4307 },
4308 window,
4309 cx,
4310 )
4311 })
4312 .unwrap()
4313 .await
4314 .unwrap();
4315 assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
4316
4317 pane.update_in(cx, |pane, window, cx| {
4318 pane.activate_item(3, false, false, window, cx)
4319 });
4320 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4321
4322 pane.update_in(cx, |pane, window, cx| {
4323 pane.close_active_item(
4324 &CloseActiveItem {
4325 save_intent: None,
4326 close_pinned: false,
4327 },
4328 window,
4329 cx,
4330 )
4331 })
4332 .unwrap()
4333 .await
4334 .unwrap();
4335 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4336
4337 pane.update_in(cx, |pane, window, cx| {
4338 pane.activate_item(0, false, false, window, cx)
4339 });
4340 assert_item_labels(&pane, ["A*", "B", "C"], cx);
4341
4342 pane.update_in(cx, |pane, window, cx| {
4343 pane.close_active_item(
4344 &CloseActiveItem {
4345 save_intent: None,
4346 close_pinned: false,
4347 },
4348 window,
4349 cx,
4350 )
4351 })
4352 .unwrap()
4353 .await
4354 .unwrap();
4355 assert_item_labels(&pane, ["B*", "C"], cx);
4356
4357 pane.update_in(cx, |pane, window, cx| {
4358 pane.close_active_item(
4359 &CloseActiveItem {
4360 save_intent: None,
4361 close_pinned: false,
4362 },
4363 window,
4364 cx,
4365 )
4366 })
4367 .unwrap()
4368 .await
4369 .unwrap();
4370 assert_item_labels(&pane, ["C*"], cx);
4371 }
4372
4373 #[gpui::test]
4374 async fn test_close_inactive_items(cx: &mut TestAppContext) {
4375 init_test(cx);
4376 let fs = FakeFs::new(cx.executor());
4377
4378 let project = Project::test(fs, None, cx).await;
4379 let (workspace, cx) =
4380 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4381 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4382
4383 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
4384
4385 pane.update_in(cx, |pane, window, cx| {
4386 pane.close_inactive_items(
4387 &CloseInactiveItems {
4388 save_intent: None,
4389 close_pinned: false,
4390 },
4391 window,
4392 cx,
4393 )
4394 })
4395 .unwrap()
4396 .await
4397 .unwrap();
4398 assert_item_labels(&pane, ["C*"], cx);
4399 }
4400
4401 #[gpui::test]
4402 async fn test_close_clean_items(cx: &mut TestAppContext) {
4403 init_test(cx);
4404 let fs = FakeFs::new(cx.executor());
4405
4406 let project = Project::test(fs, None, cx).await;
4407 let (workspace, cx) =
4408 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4409 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4410
4411 add_labeled_item(&pane, "A", true, cx);
4412 add_labeled_item(&pane, "B", false, cx);
4413 add_labeled_item(&pane, "C", true, cx);
4414 add_labeled_item(&pane, "D", false, cx);
4415 add_labeled_item(&pane, "E", false, cx);
4416 assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
4417
4418 pane.update_in(cx, |pane, window, cx| {
4419 pane.close_clean_items(
4420 &CloseCleanItems {
4421 close_pinned: false,
4422 },
4423 window,
4424 cx,
4425 )
4426 })
4427 .unwrap()
4428 .await
4429 .unwrap();
4430 assert_item_labels(&pane, ["A^", "C*^"], cx);
4431 }
4432
4433 #[gpui::test]
4434 async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
4435 init_test(cx);
4436 let fs = FakeFs::new(cx.executor());
4437
4438 let project = Project::test(fs, None, cx).await;
4439 let (workspace, cx) =
4440 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4441 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4442
4443 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
4444
4445 pane.update_in(cx, |pane, window, cx| {
4446 pane.close_items_to_the_left(
4447 &CloseItemsToTheLeft {
4448 close_pinned: false,
4449 },
4450 window,
4451 cx,
4452 )
4453 })
4454 .unwrap()
4455 .await
4456 .unwrap();
4457 assert_item_labels(&pane, ["C*", "D", "E"], cx);
4458 }
4459
4460 #[gpui::test]
4461 async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
4462 init_test(cx);
4463 let fs = FakeFs::new(cx.executor());
4464
4465 let project = Project::test(fs, None, cx).await;
4466 let (workspace, cx) =
4467 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4468 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4469
4470 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
4471
4472 pane.update_in(cx, |pane, window, cx| {
4473 pane.close_items_to_the_right(
4474 &CloseItemsToTheRight {
4475 close_pinned: false,
4476 },
4477 window,
4478 cx,
4479 )
4480 })
4481 .unwrap()
4482 .await
4483 .unwrap();
4484 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4485 }
4486
4487 #[gpui::test]
4488 async fn test_close_all_items(cx: &mut TestAppContext) {
4489 init_test(cx);
4490 let fs = FakeFs::new(cx.executor());
4491
4492 let project = Project::test(fs, None, cx).await;
4493 let (workspace, cx) =
4494 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4495 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4496
4497 let item_a = add_labeled_item(&pane, "A", false, cx);
4498 add_labeled_item(&pane, "B", false, cx);
4499 add_labeled_item(&pane, "C", false, cx);
4500 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4501
4502 pane.update_in(cx, |pane, window, cx| {
4503 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4504 pane.pin_tab_at(ix, window, cx);
4505 pane.close_all_items(
4506 &CloseAllItems {
4507 save_intent: None,
4508 close_pinned: false,
4509 },
4510 window,
4511 cx,
4512 )
4513 })
4514 .unwrap()
4515 .await
4516 .unwrap();
4517 assert_item_labels(&pane, ["A*"], cx);
4518
4519 pane.update_in(cx, |pane, window, cx| {
4520 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4521 pane.unpin_tab_at(ix, window, cx);
4522 pane.close_all_items(
4523 &CloseAllItems {
4524 save_intent: None,
4525 close_pinned: false,
4526 },
4527 window,
4528 cx,
4529 )
4530 })
4531 .unwrap()
4532 .await
4533 .unwrap();
4534
4535 assert_item_labels(&pane, [], cx);
4536
4537 add_labeled_item(&pane, "A", true, cx).update(cx, |item, cx| {
4538 item.project_items
4539 .push(TestProjectItem::new_dirty(1, "A.txt", cx))
4540 });
4541 add_labeled_item(&pane, "B", true, cx).update(cx, |item, cx| {
4542 item.project_items
4543 .push(TestProjectItem::new_dirty(2, "B.txt", cx))
4544 });
4545 add_labeled_item(&pane, "C", true, cx).update(cx, |item, cx| {
4546 item.project_items
4547 .push(TestProjectItem::new_dirty(3, "C.txt", cx))
4548 });
4549 assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
4550
4551 let save = pane
4552 .update_in(cx, |pane, window, cx| {
4553 pane.close_all_items(
4554 &CloseAllItems {
4555 save_intent: None,
4556 close_pinned: false,
4557 },
4558 window,
4559 cx,
4560 )
4561 })
4562 .unwrap();
4563
4564 cx.executor().run_until_parked();
4565 cx.simulate_prompt_answer("Save all");
4566 save.await.unwrap();
4567 assert_item_labels(&pane, [], cx);
4568
4569 add_labeled_item(&pane, "A", true, cx);
4570 add_labeled_item(&pane, "B", true, cx);
4571 add_labeled_item(&pane, "C", true, cx);
4572 assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
4573 let save = pane
4574 .update_in(cx, |pane, window, cx| {
4575 pane.close_all_items(
4576 &CloseAllItems {
4577 save_intent: None,
4578 close_pinned: false,
4579 },
4580 window,
4581 cx,
4582 )
4583 })
4584 .unwrap();
4585
4586 cx.executor().run_until_parked();
4587 cx.simulate_prompt_answer("Discard all");
4588 save.await.unwrap();
4589 assert_item_labels(&pane, [], cx);
4590 }
4591
4592 #[gpui::test]
4593 async fn test_close_with_save_intent(cx: &mut TestAppContext) {
4594 init_test(cx);
4595 let fs = FakeFs::new(cx.executor());
4596
4597 let project = Project::test(fs, None, cx).await;
4598 let (workspace, cx) =
4599 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4600 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4601
4602 let a = cx.update(|_, cx| TestProjectItem::new_dirty(1, "A.txt", cx));
4603 let b = cx.update(|_, cx| TestProjectItem::new_dirty(1, "B.txt", cx));
4604 let c = cx.update(|_, cx| TestProjectItem::new_dirty(1, "C.txt", cx));
4605
4606 add_labeled_item(&pane, "AB", true, cx).update(cx, |item, _| {
4607 item.project_items.push(a.clone());
4608 item.project_items.push(b.clone());
4609 });
4610 add_labeled_item(&pane, "C", true, cx)
4611 .update(cx, |item, _| item.project_items.push(c.clone()));
4612 assert_item_labels(&pane, ["AB^", "C*^"], cx);
4613
4614 pane.update_in(cx, |pane, window, cx| {
4615 pane.close_all_items(
4616 &CloseAllItems {
4617 save_intent: Some(SaveIntent::Save),
4618 close_pinned: false,
4619 },
4620 window,
4621 cx,
4622 )
4623 })
4624 .unwrap()
4625 .await
4626 .unwrap();
4627
4628 assert_item_labels(&pane, [], cx);
4629 cx.update(|_, cx| {
4630 assert!(!a.read(cx).is_dirty);
4631 assert!(!b.read(cx).is_dirty);
4632 assert!(!c.read(cx).is_dirty);
4633 });
4634 }
4635
4636 #[gpui::test]
4637 async fn test_close_all_items_including_pinned(cx: &mut TestAppContext) {
4638 init_test(cx);
4639 let fs = FakeFs::new(cx.executor());
4640
4641 let project = Project::test(fs, None, cx).await;
4642 let (workspace, cx) =
4643 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4644 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4645
4646 let item_a = add_labeled_item(&pane, "A", false, cx);
4647 add_labeled_item(&pane, "B", false, cx);
4648 add_labeled_item(&pane, "C", false, cx);
4649 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4650
4651 pane.update_in(cx, |pane, window, cx| {
4652 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4653 pane.pin_tab_at(ix, window, cx);
4654 pane.close_all_items(
4655 &CloseAllItems {
4656 save_intent: None,
4657 close_pinned: true,
4658 },
4659 window,
4660 cx,
4661 )
4662 })
4663 .unwrap()
4664 .await
4665 .unwrap();
4666 assert_item_labels(&pane, [], cx);
4667 }
4668
4669 #[gpui::test]
4670 async fn test_close_pinned_tab_with_non_pinned_in_same_pane(cx: &mut TestAppContext) {
4671 init_test(cx);
4672 let fs = FakeFs::new(cx.executor());
4673 let project = Project::test(fs, None, cx).await;
4674 let (workspace, cx) =
4675 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4676
4677 // Non-pinned tabs in same pane
4678 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4679 add_labeled_item(&pane, "A", false, cx);
4680 add_labeled_item(&pane, "B", false, cx);
4681 add_labeled_item(&pane, "C", false, cx);
4682 pane.update_in(cx, |pane, window, cx| {
4683 pane.pin_tab_at(0, window, cx);
4684 });
4685 set_labeled_items(&pane, ["A*", "B", "C"], cx);
4686 pane.update_in(cx, |pane, window, cx| {
4687 pane.close_active_item(
4688 &CloseActiveItem {
4689 save_intent: None,
4690 close_pinned: false,
4691 },
4692 window,
4693 cx,
4694 );
4695 });
4696 // Non-pinned tab should be active
4697 assert_item_labels(&pane, ["A", "B*", "C"], cx);
4698 }
4699
4700 #[gpui::test]
4701 async fn test_close_pinned_tab_with_non_pinned_in_different_pane(cx: &mut TestAppContext) {
4702 init_test(cx);
4703 let fs = FakeFs::new(cx.executor());
4704 let project = Project::test(fs, None, cx).await;
4705 let (workspace, cx) =
4706 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4707
4708 // No non-pinned tabs in same pane, non-pinned tabs in another pane
4709 let pane1 = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4710 let pane2 = workspace.update_in(cx, |workspace, window, cx| {
4711 workspace.split_pane(pane1.clone(), SplitDirection::Right, window, cx)
4712 });
4713 add_labeled_item(&pane1, "A", false, cx);
4714 pane1.update_in(cx, |pane, window, cx| {
4715 pane.pin_tab_at(0, window, cx);
4716 });
4717 set_labeled_items(&pane1, ["A*"], cx);
4718 add_labeled_item(&pane2, "B", false, cx);
4719 set_labeled_items(&pane2, ["B"], cx);
4720 pane1.update_in(cx, |pane, window, cx| {
4721 pane.close_active_item(
4722 &CloseActiveItem {
4723 save_intent: None,
4724 close_pinned: false,
4725 },
4726 window,
4727 cx,
4728 );
4729 });
4730 // Non-pinned tab of other pane should be active
4731 assert_item_labels(&pane2, ["B*"], cx);
4732 }
4733
4734 fn init_test(cx: &mut TestAppContext) {
4735 cx.update(|cx| {
4736 let settings_store = SettingsStore::test(cx);
4737 cx.set_global(settings_store);
4738 theme::init(LoadThemes::JustBase, cx);
4739 crate::init_settings(cx);
4740 Project::init_settings(cx);
4741 });
4742 }
4743
4744 fn set_max_tabs(cx: &mut TestAppContext, value: Option<usize>) {
4745 cx.update_global(|store: &mut SettingsStore, cx| {
4746 store.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4747 settings.max_tabs = value.map(|v| NonZero::new(v).unwrap())
4748 });
4749 });
4750 }
4751
4752 fn add_labeled_item(
4753 pane: &Entity<Pane>,
4754 label: &str,
4755 is_dirty: bool,
4756 cx: &mut VisualTestContext,
4757 ) -> Box<Entity<TestItem>> {
4758 pane.update_in(cx, |pane, window, cx| {
4759 let labeled_item =
4760 Box::new(cx.new(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)));
4761 pane.add_item(labeled_item.clone(), false, false, None, window, cx);
4762 labeled_item
4763 })
4764 }
4765
4766 fn set_labeled_items<const COUNT: usize>(
4767 pane: &Entity<Pane>,
4768 labels: [&str; COUNT],
4769 cx: &mut VisualTestContext,
4770 ) -> [Box<Entity<TestItem>>; COUNT] {
4771 pane.update_in(cx, |pane, window, cx| {
4772 pane.items.clear();
4773 let mut active_item_index = 0;
4774
4775 let mut index = 0;
4776 let items = labels.map(|mut label| {
4777 if label.ends_with('*') {
4778 label = label.trim_end_matches('*');
4779 active_item_index = index;
4780 }
4781
4782 let labeled_item = Box::new(cx.new(|cx| TestItem::new(cx).with_label(label)));
4783 pane.add_item(labeled_item.clone(), false, false, None, window, cx);
4784 index += 1;
4785 labeled_item
4786 });
4787
4788 pane.activate_item(active_item_index, false, false, window, cx);
4789
4790 items
4791 })
4792 }
4793
4794 // Assert the item label, with the active item label suffixed with a '*'
4795 #[track_caller]
4796 fn assert_item_labels<const COUNT: usize>(
4797 pane: &Entity<Pane>,
4798 expected_states: [&str; COUNT],
4799 cx: &mut VisualTestContext,
4800 ) {
4801 let actual_states = pane.update(cx, |pane, cx| {
4802 pane.items
4803 .iter()
4804 .enumerate()
4805 .map(|(ix, item)| {
4806 let mut state = item
4807 .to_any()
4808 .downcast::<TestItem>()
4809 .unwrap()
4810 .read(cx)
4811 .label
4812 .clone();
4813 if ix == pane.active_item_index {
4814 state.push('*');
4815 }
4816 if item.is_dirty(cx) {
4817 state.push('^');
4818 }
4819 state
4820 })
4821 .collect::<Vec<_>>()
4822 });
4823 assert_eq!(
4824 actual_states, expected_states,
4825 "pane items do not match expectation"
4826 );
4827 }
4828}