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 fn active_item_id(&self) -> EntityId {
1039 self.items[self.active_item_index].item_id()
1040 }
1041
1042 pub fn pixel_position_of_cursor(&self, cx: &App) -> Option<Point<Pixels>> {
1043 self.items
1044 .get(self.active_item_index)?
1045 .pixel_position_of_cursor(cx)
1046 }
1047
1048 pub fn item_for_entry(
1049 &self,
1050 entry_id: ProjectEntryId,
1051 cx: &App,
1052 ) -> Option<Box<dyn ItemHandle>> {
1053 self.items.iter().find_map(|item| {
1054 if item.is_singleton(cx) && (item.project_entry_ids(cx).as_slice() == [entry_id]) {
1055 Some(item.boxed_clone())
1056 } else {
1057 None
1058 }
1059 })
1060 }
1061
1062 pub fn item_for_path(
1063 &self,
1064 project_path: ProjectPath,
1065 cx: &App,
1066 ) -> Option<Box<dyn ItemHandle>> {
1067 self.items.iter().find_map(move |item| {
1068 if item.is_singleton(cx) && (item.project_path(cx).as_slice() == [project_path.clone()])
1069 {
1070 Some(item.boxed_clone())
1071 } else {
1072 None
1073 }
1074 })
1075 }
1076
1077 pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option<usize> {
1078 self.index_for_item_id(item.item_id())
1079 }
1080
1081 fn index_for_item_id(&self, item_id: EntityId) -> Option<usize> {
1082 self.items.iter().position(|i| i.item_id() == item_id)
1083 }
1084
1085 pub fn item_for_index(&self, ix: usize) -> Option<&dyn ItemHandle> {
1086 self.items.get(ix).map(|i| i.as_ref())
1087 }
1088
1089 pub fn toggle_zoom(&mut self, _: &ToggleZoom, window: &mut Window, cx: &mut Context<Self>) {
1090 if self.zoomed {
1091 cx.emit(Event::ZoomOut);
1092 } else if !self.items.is_empty() {
1093 if !self.focus_handle.contains_focused(window, cx) {
1094 cx.focus_self(window);
1095 }
1096 cx.emit(Event::ZoomIn);
1097 }
1098 }
1099
1100 pub fn activate_item(
1101 &mut self,
1102 index: usize,
1103 activate_pane: bool,
1104 focus_item: bool,
1105 window: &mut Window,
1106 cx: &mut Context<Self>,
1107 ) {
1108 use NavigationMode::{GoingBack, GoingForward};
1109 if index < self.items.len() {
1110 let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
1111 if prev_active_item_ix != self.active_item_index
1112 || matches!(self.nav_history.mode(), GoingBack | GoingForward)
1113 {
1114 if let Some(prev_item) = self.items.get(prev_active_item_ix) {
1115 prev_item.deactivated(window, cx);
1116 }
1117 }
1118 self.update_history(index);
1119 self.update_toolbar(window, cx);
1120 self.update_status_bar(window, cx);
1121
1122 if focus_item {
1123 self.focus_active_item(window, cx);
1124 }
1125
1126 cx.emit(Event::ActivateItem {
1127 local: activate_pane,
1128 focus_changed: focus_item,
1129 });
1130
1131 if !self.is_tab_pinned(index) {
1132 self.tab_bar_scroll_handle
1133 .scroll_to_item(index - self.pinned_tab_count);
1134 }
1135
1136 cx.notify();
1137 }
1138 }
1139
1140 fn update_history(&mut self, index: usize) {
1141 if let Some(newly_active_item) = self.items.get(index) {
1142 self.activation_history
1143 .retain(|entry| entry.entity_id != newly_active_item.item_id());
1144 self.activation_history.push(ActivationHistoryEntry {
1145 entity_id: newly_active_item.item_id(),
1146 timestamp: self
1147 .next_activation_timestamp
1148 .fetch_add(1, Ordering::SeqCst),
1149 });
1150 }
1151 }
1152
1153 pub fn activate_prev_item(
1154 &mut self,
1155 activate_pane: bool,
1156 window: &mut Window,
1157 cx: &mut Context<Self>,
1158 ) {
1159 let mut index = self.active_item_index;
1160 if index > 0 {
1161 index -= 1;
1162 } else if !self.items.is_empty() {
1163 index = self.items.len() - 1;
1164 }
1165 self.activate_item(index, activate_pane, activate_pane, window, cx);
1166 }
1167
1168 pub fn activate_next_item(
1169 &mut self,
1170 activate_pane: bool,
1171 window: &mut Window,
1172 cx: &mut Context<Self>,
1173 ) {
1174 let mut index = self.active_item_index;
1175 if index + 1 < self.items.len() {
1176 index += 1;
1177 } else {
1178 index = 0;
1179 }
1180 self.activate_item(index, activate_pane, activate_pane, window, cx);
1181 }
1182
1183 pub fn swap_item_left(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1184 let index = self.active_item_index;
1185 if index == 0 {
1186 return;
1187 }
1188
1189 self.items.swap(index, index - 1);
1190 self.activate_item(index - 1, true, true, window, cx);
1191 }
1192
1193 pub fn swap_item_right(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1194 let index = self.active_item_index;
1195 if index + 1 == self.items.len() {
1196 return;
1197 }
1198
1199 self.items.swap(index, index + 1);
1200 self.activate_item(index + 1, true, true, window, cx);
1201 }
1202
1203 pub fn close_active_item(
1204 &mut self,
1205 action: &CloseActiveItem,
1206 window: &mut Window,
1207 cx: &mut Context<Self>,
1208 ) -> Task<Result<()>> {
1209 if self.items.is_empty() {
1210 // Close the window when there's no active items to close, if configured
1211 if WorkspaceSettings::get_global(cx)
1212 .when_closing_with_no_tabs
1213 .should_close()
1214 {
1215 window.dispatch_action(Box::new(CloseWindow), cx);
1216 }
1217
1218 return Task::ready(Ok(()));
1219 }
1220 if self.is_tab_pinned(self.active_item_index) && !action.close_pinned {
1221 // Activate any non-pinned tab in same pane
1222 let non_pinned_tab_index = self
1223 .items()
1224 .enumerate()
1225 .find(|(index, _item)| !self.is_tab_pinned(*index))
1226 .map(|(index, _item)| index);
1227 if let Some(index) = non_pinned_tab_index {
1228 self.activate_item(index, false, false, window, cx);
1229 return Task::ready(Ok(()));
1230 }
1231
1232 // Activate any non-pinned tab in different pane
1233 let current_pane = cx.entity();
1234 self.workspace
1235 .update(cx, |workspace, cx| {
1236 let panes = workspace.center.panes();
1237 let pane_with_unpinned_tab = panes.iter().find(|pane| {
1238 if **pane == ¤t_pane {
1239 return false;
1240 }
1241 pane.read(cx).has_unpinned_tabs()
1242 });
1243 if let Some(pane) = pane_with_unpinned_tab {
1244 pane.update(cx, |pane, cx| pane.activate_unpinned_tab(window, cx));
1245 }
1246 })
1247 .ok();
1248
1249 return Task::ready(Ok(()));
1250 };
1251
1252 let active_item_id = self.active_item_id();
1253
1254 self.close_item_by_id(
1255 active_item_id,
1256 action.save_intent.unwrap_or(SaveIntent::Close),
1257 window,
1258 cx,
1259 )
1260 }
1261
1262 pub fn close_item_by_id(
1263 &mut self,
1264 item_id_to_close: EntityId,
1265 save_intent: SaveIntent,
1266 window: &mut Window,
1267 cx: &mut Context<Self>,
1268 ) -> Task<Result<()>> {
1269 self.close_items(window, cx, save_intent, move |view_id| {
1270 view_id == item_id_to_close
1271 })
1272 }
1273
1274 pub fn close_inactive_items(
1275 &mut self,
1276 action: &CloseInactiveItems,
1277 window: &mut Window,
1278 cx: &mut Context<Self>,
1279 ) -> Task<Result<()>> {
1280 if self.items.is_empty() {
1281 return Task::ready(Ok(()));
1282 }
1283
1284 let active_item_id = self.active_item_id();
1285 let pinned_item_ids = self.pinned_item_ids();
1286
1287 self.close_items(
1288 window,
1289 cx,
1290 action.save_intent.unwrap_or(SaveIntent::Close),
1291 move |item_id| {
1292 item_id != active_item_id
1293 && (action.close_pinned || !pinned_item_ids.contains(&item_id))
1294 },
1295 )
1296 }
1297
1298 pub fn close_clean_items(
1299 &mut self,
1300 action: &CloseCleanItems,
1301 window: &mut Window,
1302 cx: &mut Context<Self>,
1303 ) -> Task<Result<()>> {
1304 if self.items.is_empty() {
1305 return Task::ready(Ok(()));
1306 }
1307
1308 let clean_item_ids = self.clean_item_ids(cx);
1309 let pinned_item_ids = self.pinned_item_ids();
1310
1311 self.close_items(window, cx, SaveIntent::Close, move |item_id| {
1312 clean_item_ids.contains(&item_id)
1313 && (action.close_pinned || !pinned_item_ids.contains(&item_id))
1314 })
1315 }
1316
1317 pub fn close_items_to_the_left(
1318 &mut self,
1319 action: &CloseItemsToTheLeft,
1320 window: &mut Window,
1321 cx: &mut Context<Self>,
1322 ) -> Task<Result<()>> {
1323 if self.items.is_empty() {
1324 return Task::ready(Ok(()));
1325 }
1326
1327 let active_item_id = self.active_item_id();
1328 let pinned_item_ids = self.pinned_item_ids();
1329
1330 self.close_items_to_the_left_by_id(active_item_id, action, pinned_item_ids, window, cx)
1331 }
1332
1333 pub fn close_items_to_the_left_by_id(
1334 &mut self,
1335 item_id: EntityId,
1336 action: &CloseItemsToTheLeft,
1337 pinned_item_ids: HashSet<EntityId>,
1338 window: &mut Window,
1339 cx: &mut Context<Self>,
1340 ) -> Task<Result<()>> {
1341 if self.items.is_empty() {
1342 return Task::ready(Ok(()));
1343 }
1344
1345 let to_the_left_item_ids = self.to_the_left_item_ids(item_id);
1346
1347 self.close_items(window, cx, SaveIntent::Close, move |item_id| {
1348 to_the_left_item_ids.contains(&item_id)
1349 && (action.close_pinned || !pinned_item_ids.contains(&item_id))
1350 })
1351 }
1352
1353 pub fn close_items_to_the_right(
1354 &mut self,
1355 action: &CloseItemsToTheRight,
1356 window: &mut Window,
1357 cx: &mut Context<Self>,
1358 ) -> Task<Result<()>> {
1359 if self.items.is_empty() {
1360 return Task::ready(Ok(()));
1361 }
1362
1363 let active_item_id = self.active_item_id();
1364 let pinned_item_ids = self.pinned_item_ids();
1365
1366 self.close_items_to_the_right_by_id(active_item_id, action, pinned_item_ids, window, cx)
1367 }
1368
1369 pub fn close_items_to_the_right_by_id(
1370 &mut self,
1371 item_id: EntityId,
1372 action: &CloseItemsToTheRight,
1373 pinned_item_ids: HashSet<EntityId>,
1374 window: &mut Window,
1375 cx: &mut Context<Self>,
1376 ) -> Task<Result<()>> {
1377 if self.items.is_empty() {
1378 return Task::ready(Ok(()));
1379 }
1380
1381 let to_the_right_item_ids = self.to_the_right_item_ids(item_id);
1382
1383 self.close_items(window, cx, SaveIntent::Close, move |item_id| {
1384 to_the_right_item_ids.contains(&item_id)
1385 && (action.close_pinned || !pinned_item_ids.contains(&item_id))
1386 })
1387 }
1388
1389 pub fn close_all_items(
1390 &mut self,
1391 action: &CloseAllItems,
1392 window: &mut Window,
1393 cx: &mut Context<Self>,
1394 ) -> Task<Result<()>> {
1395 if self.items.is_empty() {
1396 return Task::ready(Ok(()));
1397 }
1398
1399 let pinned_item_ids = self.pinned_item_ids();
1400
1401 self.close_items(
1402 window,
1403 cx,
1404 action.save_intent.unwrap_or(SaveIntent::Close),
1405 |item_id| action.close_pinned || !pinned_item_ids.contains(&item_id),
1406 )
1407 }
1408
1409 pub fn close_items_over_max_tabs(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1410 let Some(max_tabs) = WorkspaceSettings::get_global(cx).max_tabs.map(|i| i.get()) else {
1411 return;
1412 };
1413
1414 // Reduce over the activation history to get every dirty items up to max_tabs
1415 // count.
1416 let mut index_list = Vec::new();
1417 let mut items_len = self.items_len();
1418 let mut indexes: HashMap<EntityId, usize> = HashMap::default();
1419 for (index, item) in self.items.iter().enumerate() {
1420 indexes.insert(item.item_id(), index);
1421 }
1422 for entry in self.activation_history.iter() {
1423 if items_len < max_tabs {
1424 break;
1425 }
1426 let Some(&index) = indexes.get(&entry.entity_id) else {
1427 continue;
1428 };
1429 if let Some(true) = self.items.get(index).map(|item| item.is_dirty(cx)) {
1430 continue;
1431 }
1432
1433 index_list.push(index);
1434 items_len -= 1;
1435 }
1436 // The sort and reverse is necessary since we remove items
1437 // using their index position, hence removing from the end
1438 // of the list first to avoid changing indexes.
1439 index_list.sort_unstable();
1440 index_list
1441 .iter()
1442 .rev()
1443 .for_each(|&index| self._remove_item(index, false, false, None, window, cx));
1444 }
1445
1446 // Usually when you close an item that has unsaved changes, we prompt you to
1447 // save it. That said, if you still have the buffer open in a different pane
1448 // we can close this one without fear of losing data.
1449 pub fn skip_save_on_close(item: &dyn ItemHandle, workspace: &Workspace, cx: &App) -> bool {
1450 let mut dirty_project_item_ids = Vec::new();
1451 item.for_each_project_item(cx, &mut |project_item_id, project_item| {
1452 if project_item.is_dirty() {
1453 dirty_project_item_ids.push(project_item_id);
1454 }
1455 });
1456 if dirty_project_item_ids.is_empty() {
1457 return !(item.is_singleton(cx) && item.is_dirty(cx));
1458 }
1459
1460 for open_item in workspace.items(cx) {
1461 if open_item.item_id() == item.item_id() {
1462 continue;
1463 }
1464 if !open_item.is_singleton(cx) {
1465 continue;
1466 }
1467 let other_project_item_ids = open_item.project_item_model_ids(cx);
1468 dirty_project_item_ids.retain(|id| !other_project_item_ids.contains(id));
1469 }
1470 return dirty_project_item_ids.is_empty();
1471 }
1472
1473 pub(super) fn file_names_for_prompt(
1474 items: &mut dyn Iterator<Item = &Box<dyn ItemHandle>>,
1475 cx: &App,
1476 ) -> String {
1477 let mut file_names = BTreeSet::default();
1478 for item in items {
1479 item.for_each_project_item(cx, &mut |_, project_item| {
1480 if !project_item.is_dirty() {
1481 return;
1482 }
1483 let filename = project_item.project_path(cx).and_then(|path| {
1484 path.path
1485 .file_name()
1486 .and_then(|name| name.to_str().map(ToOwned::to_owned))
1487 });
1488 file_names.insert(filename.unwrap_or("untitled".to_string()));
1489 });
1490 }
1491 if file_names.len() > 6 {
1492 format!(
1493 "{}\n.. and {} more",
1494 file_names.iter().take(5).join("\n"),
1495 file_names.len() - 5
1496 )
1497 } else {
1498 file_names.into_iter().join("\n")
1499 }
1500 }
1501
1502 pub fn close_items(
1503 &self,
1504 window: &mut Window,
1505 cx: &mut Context<Pane>,
1506 mut save_intent: SaveIntent,
1507 should_close: impl Fn(EntityId) -> bool,
1508 ) -> Task<Result<()>> {
1509 // Find the items to close.
1510 let mut items_to_close = Vec::new();
1511 for item in &self.items {
1512 if should_close(item.item_id()) {
1513 items_to_close.push(item.boxed_clone());
1514 }
1515 }
1516
1517 let active_item_id = self.active_item().map(|item| item.item_id());
1518
1519 items_to_close.sort_by_key(|item| {
1520 let path = item.project_path(cx);
1521 // Put the currently active item at the end, because if the currently active item is not closed last
1522 // closing the currently active item will cause the focus to switch to another item
1523 // This will cause Zed to expand the content of the currently active item
1524 //
1525 // Beyond that sort in order of project path, with untitled files and multibuffers coming last.
1526 (active_item_id == Some(item.item_id()), path.is_none(), path)
1527 });
1528
1529 let workspace = self.workspace.clone();
1530 let Some(project) = self.project.upgrade() else {
1531 return Task::ready(Ok(()));
1532 };
1533 cx.spawn_in(window, async move |pane, cx| {
1534 let dirty_items = workspace.update(cx, |workspace, cx| {
1535 items_to_close
1536 .iter()
1537 .filter(|item| {
1538 item.is_dirty(cx)
1539 && !Self::skip_save_on_close(item.as_ref(), &workspace, cx)
1540 })
1541 .map(|item| item.boxed_clone())
1542 .collect::<Vec<_>>()
1543 })?;
1544
1545 if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
1546 let answer = pane.update_in(cx, |_, window, cx| {
1547 let detail = Self::file_names_for_prompt(&mut dirty_items.iter(), cx);
1548 window.prompt(
1549 PromptLevel::Warning,
1550 "Do you want to save changes to the following files?",
1551 Some(&detail),
1552 &["Save all", "Discard all", "Cancel"],
1553 cx,
1554 )
1555 })?;
1556 match answer.await {
1557 Ok(0) => save_intent = SaveIntent::SaveAll,
1558 Ok(1) => save_intent = SaveIntent::Skip,
1559 Ok(2) => return Ok(()),
1560 _ => {}
1561 }
1562 }
1563
1564 for item_to_close in items_to_close {
1565 let mut should_save = true;
1566 if save_intent == SaveIntent::Close {
1567 workspace.update(cx, |workspace, cx| {
1568 if Self::skip_save_on_close(item_to_close.as_ref(), &workspace, cx) {
1569 should_save = false;
1570 }
1571 })?;
1572 }
1573
1574 if should_save {
1575 if !Self::save_item(project.clone(), &pane, &*item_to_close, save_intent, cx)
1576 .await?
1577 {
1578 break;
1579 }
1580 }
1581
1582 // Remove the item from the pane.
1583 pane.update_in(cx, |pane, window, cx| {
1584 pane.remove_item(
1585 item_to_close.item_id(),
1586 false,
1587 pane.close_pane_if_empty,
1588 window,
1589 cx,
1590 );
1591 })
1592 .ok();
1593 }
1594
1595 pane.update(cx, |_, cx| cx.notify()).ok();
1596 Ok(())
1597 })
1598 }
1599
1600 pub fn remove_item(
1601 &mut self,
1602 item_id: EntityId,
1603 activate_pane: bool,
1604 close_pane_if_empty: bool,
1605 window: &mut Window,
1606 cx: &mut Context<Self>,
1607 ) {
1608 let Some(item_index) = self.index_for_item_id(item_id) else {
1609 return;
1610 };
1611 self._remove_item(
1612 item_index,
1613 activate_pane,
1614 close_pane_if_empty,
1615 None,
1616 window,
1617 cx,
1618 )
1619 }
1620
1621 pub fn remove_item_and_focus_on_pane(
1622 &mut self,
1623 item_index: usize,
1624 activate_pane: bool,
1625 focus_on_pane_if_closed: Entity<Pane>,
1626 window: &mut Window,
1627 cx: &mut Context<Self>,
1628 ) {
1629 self._remove_item(
1630 item_index,
1631 activate_pane,
1632 true,
1633 Some(focus_on_pane_if_closed),
1634 window,
1635 cx,
1636 )
1637 }
1638
1639 fn _remove_item(
1640 &mut self,
1641 item_index: usize,
1642 activate_pane: bool,
1643 close_pane_if_empty: bool,
1644 focus_on_pane_if_closed: Option<Entity<Pane>>,
1645 window: &mut Window,
1646 cx: &mut Context<Self>,
1647 ) {
1648 let activate_on_close = &ItemSettings::get_global(cx).activate_on_close;
1649 self.activation_history
1650 .retain(|entry| entry.entity_id != self.items[item_index].item_id());
1651
1652 if self.is_tab_pinned(item_index) {
1653 self.pinned_tab_count -= 1;
1654 }
1655 if item_index == self.active_item_index {
1656 let left_neighbour_index = || item_index.min(self.items.len()).saturating_sub(1);
1657 let index_to_activate = match activate_on_close {
1658 ActivateOnClose::History => self
1659 .activation_history
1660 .pop()
1661 .and_then(|last_activated_item| {
1662 self.items.iter().enumerate().find_map(|(index, item)| {
1663 (item.item_id() == last_activated_item.entity_id).then_some(index)
1664 })
1665 })
1666 // We didn't have a valid activation history entry, so fallback
1667 // to activating the item to the left
1668 .unwrap_or_else(left_neighbour_index),
1669 ActivateOnClose::Neighbour => {
1670 self.activation_history.pop();
1671 if item_index + 1 < self.items.len() {
1672 item_index + 1
1673 } else {
1674 item_index.saturating_sub(1)
1675 }
1676 }
1677 ActivateOnClose::LeftNeighbour => {
1678 self.activation_history.pop();
1679 left_neighbour_index()
1680 }
1681 };
1682
1683 let should_activate = activate_pane || self.has_focus(window, cx);
1684 if self.items.len() == 1 && should_activate {
1685 self.focus_handle.focus(window);
1686 } else {
1687 self.activate_item(
1688 index_to_activate,
1689 should_activate,
1690 should_activate,
1691 window,
1692 cx,
1693 );
1694 }
1695 }
1696
1697 let item = self.items.remove(item_index);
1698
1699 cx.emit(Event::RemovedItem { item: item.clone() });
1700 if self.items.is_empty() {
1701 item.deactivated(window, cx);
1702 if close_pane_if_empty {
1703 self.update_toolbar(window, cx);
1704 cx.emit(Event::Remove {
1705 focus_on_pane: focus_on_pane_if_closed,
1706 });
1707 }
1708 }
1709
1710 if item_index < self.active_item_index {
1711 self.active_item_index -= 1;
1712 }
1713
1714 let mode = self.nav_history.mode();
1715 self.nav_history.set_mode(NavigationMode::ClosingItem);
1716 item.deactivated(window, cx);
1717 self.nav_history.set_mode(mode);
1718
1719 if self.is_active_preview_item(item.item_id()) {
1720 self.set_preview_item_id(None, cx);
1721 }
1722
1723 if let Some(path) = item.project_path(cx) {
1724 let abs_path = self
1725 .nav_history
1726 .0
1727 .lock()
1728 .paths_by_item
1729 .get(&item.item_id())
1730 .and_then(|(_, abs_path)| abs_path.clone());
1731
1732 self.nav_history
1733 .0
1734 .lock()
1735 .paths_by_item
1736 .insert(item.item_id(), (path, abs_path));
1737 } else {
1738 self.nav_history
1739 .0
1740 .lock()
1741 .paths_by_item
1742 .remove(&item.item_id());
1743 }
1744
1745 if self.zoom_out_on_close && self.items.is_empty() && close_pane_if_empty && self.zoomed {
1746 cx.emit(Event::ZoomOut);
1747 }
1748
1749 cx.notify();
1750 }
1751
1752 pub async fn save_item(
1753 project: Entity<Project>,
1754 pane: &WeakEntity<Pane>,
1755 item: &dyn ItemHandle,
1756 save_intent: SaveIntent,
1757 cx: &mut AsyncWindowContext,
1758 ) -> Result<bool> {
1759 const CONFLICT_MESSAGE: &str = "This file has changed on disk since you started editing it. Do you want to overwrite it?";
1760
1761 const DELETED_MESSAGE: &str = "This file has been deleted on disk since you started editing it. Do you want to recreate it?";
1762
1763 if save_intent == SaveIntent::Skip {
1764 return Ok(true);
1765 }
1766 let Some(item_ix) = pane
1767 .read_with(cx, |pane, _| pane.index_for_item(item))
1768 .ok()
1769 .flatten()
1770 else {
1771 return Ok(true);
1772 };
1773
1774 let (
1775 mut has_conflict,
1776 mut is_dirty,
1777 mut can_save,
1778 can_save_as,
1779 is_singleton,
1780 has_deleted_file,
1781 ) = cx.update(|_window, cx| {
1782 (
1783 item.has_conflict(cx),
1784 item.is_dirty(cx),
1785 item.can_save(cx),
1786 item.can_save_as(cx),
1787 item.is_singleton(cx),
1788 item.has_deleted_file(cx),
1789 )
1790 })?;
1791
1792 // when saving a single buffer, we ignore whether or not it's dirty.
1793 if save_intent == SaveIntent::Save || save_intent == SaveIntent::SaveWithoutFormat {
1794 is_dirty = true;
1795 }
1796
1797 if save_intent == SaveIntent::SaveAs {
1798 is_dirty = true;
1799 has_conflict = false;
1800 can_save = false;
1801 }
1802
1803 if save_intent == SaveIntent::Overwrite {
1804 has_conflict = false;
1805 }
1806
1807 let should_format = save_intent != SaveIntent::SaveWithoutFormat;
1808
1809 if has_conflict && can_save {
1810 if has_deleted_file && is_singleton {
1811 let answer = pane.update_in(cx, |pane, window, cx| {
1812 pane.activate_item(item_ix, true, true, window, cx);
1813 window.prompt(
1814 PromptLevel::Warning,
1815 DELETED_MESSAGE,
1816 None,
1817 &["Save", "Close", "Cancel"],
1818 cx,
1819 )
1820 })?;
1821 match answer.await {
1822 Ok(0) => {
1823 pane.update_in(cx, |_, window, cx| {
1824 item.save(should_format, project, window, cx)
1825 })?
1826 .await?
1827 }
1828 Ok(1) => {
1829 pane.update_in(cx, |pane, window, cx| {
1830 pane.remove_item(item.item_id(), false, true, window, cx)
1831 })?;
1832 }
1833 _ => return Ok(false),
1834 }
1835 return Ok(true);
1836 } else {
1837 let answer = pane.update_in(cx, |pane, window, cx| {
1838 pane.activate_item(item_ix, true, true, window, cx);
1839 window.prompt(
1840 PromptLevel::Warning,
1841 CONFLICT_MESSAGE,
1842 None,
1843 &["Overwrite", "Discard", "Cancel"],
1844 cx,
1845 )
1846 })?;
1847 match answer.await {
1848 Ok(0) => {
1849 pane.update_in(cx, |_, window, cx| {
1850 item.save(should_format, project, window, cx)
1851 })?
1852 .await?
1853 }
1854 Ok(1) => {
1855 pane.update_in(cx, |_, window, cx| item.reload(project, window, cx))?
1856 .await?
1857 }
1858 _ => return Ok(false),
1859 }
1860 }
1861 } else if is_dirty && (can_save || can_save_as) {
1862 if save_intent == SaveIntent::Close {
1863 let will_autosave = cx.update(|_window, cx| {
1864 matches!(
1865 item.workspace_settings(cx).autosave,
1866 AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange
1867 ) && item.can_autosave(cx)
1868 })?;
1869 if !will_autosave {
1870 let item_id = item.item_id();
1871 let answer_task = pane.update_in(cx, |pane, window, cx| {
1872 if pane.save_modals_spawned.insert(item_id) {
1873 pane.activate_item(item_ix, true, true, window, cx);
1874 let prompt = dirty_message_for(item.project_path(cx));
1875 Some(window.prompt(
1876 PromptLevel::Warning,
1877 &prompt,
1878 None,
1879 &["Save", "Don't Save", "Cancel"],
1880 cx,
1881 ))
1882 } else {
1883 None
1884 }
1885 })?;
1886 if let Some(answer_task) = answer_task {
1887 let answer = answer_task.await;
1888 pane.update(cx, |pane, _| {
1889 if !pane.save_modals_spawned.remove(&item_id) {
1890 debug_panic!(
1891 "save modal was not present in spawned modals after awaiting for its answer"
1892 )
1893 }
1894 })?;
1895 match answer {
1896 Ok(0) => {}
1897 Ok(1) => {
1898 // Don't save this file
1899 pane.update_in(cx, |pane, window, cx| {
1900 if pane.is_tab_pinned(item_ix) && !item.can_save(cx) {
1901 pane.pinned_tab_count -= 1;
1902 }
1903 item.discarded(project, window, cx)
1904 })
1905 .log_err();
1906 return Ok(true);
1907 }
1908 _ => return Ok(false), // Cancel
1909 }
1910 } else {
1911 return Ok(false);
1912 }
1913 }
1914 }
1915
1916 if can_save {
1917 pane.update_in(cx, |pane, window, cx| {
1918 if pane.is_active_preview_item(item.item_id()) {
1919 pane.set_preview_item_id(None, cx);
1920 }
1921 item.save(should_format, project, window, cx)
1922 })?
1923 .await?;
1924 } else if can_save_as && is_singleton {
1925 let abs_path = pane.update_in(cx, |pane, window, cx| {
1926 pane.activate_item(item_ix, true, true, window, cx);
1927 pane.workspace.update(cx, |workspace, cx| {
1928 workspace.prompt_for_new_path(window, cx)
1929 })
1930 })??;
1931 if let Some(abs_path) = abs_path.await.ok().flatten() {
1932 pane.update_in(cx, |pane, window, cx| {
1933 if let Some(item) = pane.item_for_path(abs_path.clone(), cx) {
1934 pane.remove_item(item.item_id(), false, false, window, cx);
1935 }
1936
1937 item.save_as(project, abs_path, window, cx)
1938 })?
1939 .await?;
1940 } else {
1941 return Ok(false);
1942 }
1943 }
1944 }
1945
1946 pane.update(cx, |_, cx| {
1947 cx.emit(Event::UserSavedItem {
1948 item: item.downgrade_item(),
1949 save_intent,
1950 });
1951 true
1952 })
1953 }
1954
1955 pub fn autosave_item(
1956 item: &dyn ItemHandle,
1957 project: Entity<Project>,
1958 window: &mut Window,
1959 cx: &mut App,
1960 ) -> Task<Result<()>> {
1961 let format = !matches!(
1962 item.workspace_settings(cx).autosave,
1963 AutosaveSetting::AfterDelay { .. }
1964 );
1965 if item.can_autosave(cx) {
1966 item.save(format, project, window, cx)
1967 } else {
1968 Task::ready(Ok(()))
1969 }
1970 }
1971
1972 pub fn focus_active_item(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1973 if let Some(active_item) = self.active_item() {
1974 let focus_handle = active_item.item_focus_handle(cx);
1975 window.focus(&focus_handle);
1976 }
1977 }
1978
1979 pub fn split(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
1980 cx.emit(Event::Split(direction));
1981 }
1982
1983 pub fn toolbar(&self) -> &Entity<Toolbar> {
1984 &self.toolbar
1985 }
1986
1987 pub fn handle_deleted_project_item(
1988 &mut self,
1989 entry_id: ProjectEntryId,
1990 window: &mut Window,
1991 cx: &mut Context<Pane>,
1992 ) -> Option<()> {
1993 let item_id = self.items().find_map(|item| {
1994 if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
1995 Some(item.item_id())
1996 } else {
1997 None
1998 }
1999 })?;
2000
2001 self.remove_item(item_id, false, true, window, cx);
2002 self.nav_history.remove_item(item_id);
2003
2004 Some(())
2005 }
2006
2007 fn update_toolbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2008 let active_item = self
2009 .items
2010 .get(self.active_item_index)
2011 .map(|item| item.as_ref());
2012 self.toolbar.update(cx, |toolbar, cx| {
2013 toolbar.set_active_item(active_item, window, cx);
2014 });
2015 }
2016
2017 fn update_status_bar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2018 let workspace = self.workspace.clone();
2019 let pane = cx.entity().clone();
2020
2021 window.defer(cx, move |window, cx| {
2022 let Ok(status_bar) =
2023 workspace.read_with(cx, |workspace, _| workspace.status_bar.clone())
2024 else {
2025 return;
2026 };
2027
2028 status_bar.update(cx, move |status_bar, cx| {
2029 status_bar.set_active_pane(&pane, window, cx);
2030 });
2031 });
2032 }
2033
2034 fn entry_abs_path(&self, entry: ProjectEntryId, cx: &App) -> Option<PathBuf> {
2035 let worktree = self
2036 .workspace
2037 .upgrade()?
2038 .read(cx)
2039 .project()
2040 .read(cx)
2041 .worktree_for_entry(entry, cx)?
2042 .read(cx);
2043 let entry = worktree.entry_for_id(entry)?;
2044 match &entry.canonical_path {
2045 Some(canonical_path) => Some(canonical_path.to_path_buf()),
2046 None => worktree.absolutize(&entry.path).ok(),
2047 }
2048 }
2049
2050 pub fn icon_color(selected: bool) -> Color {
2051 if selected {
2052 Color::Default
2053 } else {
2054 Color::Muted
2055 }
2056 }
2057
2058 fn toggle_pin_tab(&mut self, _: &TogglePinTab, window: &mut Window, cx: &mut Context<Self>) {
2059 if self.items.is_empty() {
2060 return;
2061 }
2062 let active_tab_ix = self.active_item_index();
2063 if self.is_tab_pinned(active_tab_ix) {
2064 self.unpin_tab_at(active_tab_ix, window, cx);
2065 } else {
2066 self.pin_tab_at(active_tab_ix, window, cx);
2067 }
2068 }
2069
2070 fn pin_tab_at(&mut self, ix: usize, window: &mut Window, cx: &mut Context<Self>) {
2071 maybe!({
2072 let pane = cx.entity().clone();
2073 let destination_index = self.pinned_tab_count.min(ix);
2074 self.pinned_tab_count += 1;
2075 let id = self.item_for_index(ix)?.item_id();
2076
2077 if self.is_active_preview_item(id) {
2078 self.set_preview_item_id(None, cx);
2079 }
2080
2081 self.workspace
2082 .update(cx, |_, cx| {
2083 cx.defer_in(window, move |_, window, cx| {
2084 move_item(&pane, &pane, id, destination_index, window, cx)
2085 });
2086 })
2087 .ok()?;
2088
2089 Some(())
2090 });
2091 }
2092
2093 fn unpin_tab_at(&mut self, ix: usize, window: &mut Window, cx: &mut Context<Self>) {
2094 maybe!({
2095 let pane = cx.entity().clone();
2096 self.pinned_tab_count = self.pinned_tab_count.checked_sub(1)?;
2097 let destination_index = self.pinned_tab_count;
2098
2099 let id = self.item_for_index(ix)?.item_id();
2100
2101 self.workspace
2102 .update(cx, |_, cx| {
2103 cx.defer_in(window, move |_, window, cx| {
2104 move_item(&pane, &pane, id, destination_index, window, cx)
2105 });
2106 })
2107 .ok()?;
2108
2109 Some(())
2110 });
2111 }
2112
2113 fn is_tab_pinned(&self, ix: usize) -> bool {
2114 self.pinned_tab_count > ix
2115 }
2116
2117 fn has_pinned_tabs(&self) -> bool {
2118 self.pinned_tab_count != 0
2119 }
2120
2121 fn has_unpinned_tabs(&self) -> bool {
2122 self.pinned_tab_count < self.items.len()
2123 }
2124
2125 fn activate_unpinned_tab(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2126 if self.items.is_empty() {
2127 return;
2128 }
2129 let Some(index) = self
2130 .items()
2131 .enumerate()
2132 .find_map(|(index, _item)| (!self.is_tab_pinned(index)).then_some(index))
2133 else {
2134 return;
2135 };
2136 self.activate_item(index, true, true, window, cx);
2137 }
2138
2139 fn render_tab(
2140 &self,
2141 ix: usize,
2142 item: &dyn ItemHandle,
2143 detail: usize,
2144 focus_handle: &FocusHandle,
2145 window: &mut Window,
2146 cx: &mut Context<Pane>,
2147 ) -> impl IntoElement + use<> {
2148 let is_active = ix == self.active_item_index;
2149 let is_preview = self
2150 .preview_item_id
2151 .map(|id| id == item.item_id())
2152 .unwrap_or(false);
2153
2154 let label = item.tab_content(
2155 TabContentParams {
2156 detail: Some(detail),
2157 selected: is_active,
2158 preview: is_preview,
2159 deemphasized: !self.has_focus(window, cx),
2160 },
2161 window,
2162 cx,
2163 );
2164
2165 let item_diagnostic = item
2166 .project_path(cx)
2167 .map_or(None, |project_path| self.diagnostics.get(&project_path));
2168
2169 let decorated_icon = item_diagnostic.map_or(None, |diagnostic| {
2170 let icon = match item.tab_icon(window, cx) {
2171 Some(icon) => icon,
2172 None => return None,
2173 };
2174
2175 let knockout_item_color = if is_active {
2176 cx.theme().colors().tab_active_background
2177 } else {
2178 cx.theme().colors().tab_bar_background
2179 };
2180
2181 let (icon_decoration, icon_color) = if matches!(diagnostic, &DiagnosticSeverity::ERROR)
2182 {
2183 (IconDecorationKind::X, Color::Error)
2184 } else {
2185 (IconDecorationKind::Triangle, Color::Warning)
2186 };
2187
2188 Some(DecoratedIcon::new(
2189 icon.size(IconSize::Small).color(Color::Muted),
2190 Some(
2191 IconDecoration::new(icon_decoration, knockout_item_color, cx)
2192 .color(icon_color.color(cx))
2193 .position(Point {
2194 x: px(-2.),
2195 y: px(-2.),
2196 }),
2197 ),
2198 ))
2199 });
2200
2201 let icon = if decorated_icon.is_none() {
2202 match item_diagnostic {
2203 Some(&DiagnosticSeverity::ERROR) => None,
2204 Some(&DiagnosticSeverity::WARNING) => None,
2205 _ => item
2206 .tab_icon(window, cx)
2207 .map(|icon| icon.color(Color::Muted)),
2208 }
2209 .map(|icon| icon.size(IconSize::Small))
2210 } else {
2211 None
2212 };
2213
2214 let settings = ItemSettings::get_global(cx);
2215 let close_side = &settings.close_position;
2216 let show_close_button = &settings.show_close_button;
2217 let indicator = render_item_indicator(item.boxed_clone(), cx);
2218 let item_id = item.item_id();
2219 let is_first_item = ix == 0;
2220 let is_last_item = ix == self.items.len() - 1;
2221 let is_pinned = self.is_tab_pinned(ix);
2222 let position_relative_to_active_item = ix.cmp(&self.active_item_index);
2223
2224 let tab = Tab::new(ix)
2225 .position(if is_first_item {
2226 TabPosition::First
2227 } else if is_last_item {
2228 TabPosition::Last
2229 } else {
2230 TabPosition::Middle(position_relative_to_active_item)
2231 })
2232 .close_side(match close_side {
2233 ClosePosition::Left => ui::TabCloseSide::Start,
2234 ClosePosition::Right => ui::TabCloseSide::End,
2235 })
2236 .toggle_state(is_active)
2237 .on_click(cx.listener(move |pane: &mut Self, _, window, cx| {
2238 pane.activate_item(ix, true, true, window, cx)
2239 }))
2240 // TODO: This should be a click listener with the middle mouse button instead of a mouse down listener.
2241 .on_mouse_down(
2242 MouseButton::Middle,
2243 cx.listener(move |pane, _event, window, cx| {
2244 pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
2245 .detach_and_log_err(cx);
2246 }),
2247 )
2248 .on_mouse_down(
2249 MouseButton::Left,
2250 cx.listener(move |pane, event: &MouseDownEvent, _, cx| {
2251 if let Some(id) = pane.preview_item_id {
2252 if id == item_id && event.click_count > 1 {
2253 pane.set_preview_item_id(None, cx);
2254 }
2255 }
2256 }),
2257 )
2258 .on_drag(
2259 DraggedTab {
2260 item: item.boxed_clone(),
2261 pane: cx.entity().clone(),
2262 detail,
2263 is_active,
2264 ix,
2265 },
2266 |tab, _, _, cx| cx.new(|_| tab.clone()),
2267 )
2268 .drag_over::<DraggedTab>(|tab, _, _, cx| {
2269 tab.bg(cx.theme().colors().drop_target_background)
2270 })
2271 .drag_over::<DraggedSelection>(|tab, _, _, cx| {
2272 tab.bg(cx.theme().colors().drop_target_background)
2273 })
2274 .when_some(self.can_drop_predicate.clone(), |this, p| {
2275 this.can_drop(move |a, window, cx| p(a, window, cx))
2276 })
2277 .on_drop(
2278 cx.listener(move |this, dragged_tab: &DraggedTab, window, cx| {
2279 this.drag_split_direction = None;
2280 this.handle_tab_drop(dragged_tab, ix, window, cx)
2281 }),
2282 )
2283 .on_drop(
2284 cx.listener(move |this, selection: &DraggedSelection, window, cx| {
2285 this.drag_split_direction = None;
2286 this.handle_dragged_selection_drop(selection, Some(ix), window, cx)
2287 }),
2288 )
2289 .on_drop(cx.listener(move |this, paths, window, cx| {
2290 this.drag_split_direction = None;
2291 this.handle_external_paths_drop(paths, window, cx)
2292 }))
2293 .when_some(item.tab_tooltip_content(cx), |tab, content| match content {
2294 TabTooltipContent::Text(text) => tab.tooltip(Tooltip::text(text.clone())),
2295 TabTooltipContent::Custom(element_fn) => {
2296 tab.tooltip(move |window, cx| element_fn(window, cx))
2297 }
2298 })
2299 .start_slot::<Indicator>(indicator)
2300 .map(|this| {
2301 let end_slot_action: &'static dyn Action;
2302 let end_slot_tooltip_text: &'static str;
2303 let end_slot = if is_pinned {
2304 end_slot_action = &TogglePinTab;
2305 end_slot_tooltip_text = "Unpin Tab";
2306 IconButton::new("unpin tab", IconName::Pin)
2307 .shape(IconButtonShape::Square)
2308 .icon_color(Color::Muted)
2309 .size(ButtonSize::None)
2310 .icon_size(IconSize::XSmall)
2311 .on_click(cx.listener(move |pane, _, window, cx| {
2312 pane.unpin_tab_at(ix, window, cx);
2313 }))
2314 } else {
2315 end_slot_action = &CloseActiveItem {
2316 save_intent: None,
2317 close_pinned: false,
2318 };
2319 end_slot_tooltip_text = "Close Tab";
2320 match show_close_button {
2321 ShowCloseButton::Always => IconButton::new("close tab", IconName::Close),
2322 ShowCloseButton::Hover => {
2323 IconButton::new("close tab", IconName::Close).visible_on_hover("")
2324 }
2325 ShowCloseButton::Hidden => return this,
2326 }
2327 .shape(IconButtonShape::Square)
2328 .icon_color(Color::Muted)
2329 .size(ButtonSize::None)
2330 .icon_size(IconSize::XSmall)
2331 .on_click(cx.listener(move |pane, _, window, cx| {
2332 pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
2333 .detach_and_log_err(cx);
2334 }))
2335 }
2336 .map(|this| {
2337 if is_active {
2338 let focus_handle = focus_handle.clone();
2339 this.tooltip(move |window, cx| {
2340 Tooltip::for_action_in(
2341 end_slot_tooltip_text,
2342 end_slot_action,
2343 &focus_handle,
2344 window,
2345 cx,
2346 )
2347 })
2348 } else {
2349 this.tooltip(Tooltip::text(end_slot_tooltip_text))
2350 }
2351 });
2352 this.end_slot(end_slot)
2353 })
2354 .child(
2355 h_flex()
2356 .gap_1()
2357 .items_center()
2358 .children(
2359 std::iter::once(if let Some(decorated_icon) = decorated_icon {
2360 Some(div().child(decorated_icon.into_any_element()))
2361 } else if let Some(icon) = icon {
2362 Some(div().child(icon.into_any_element()))
2363 } else {
2364 None
2365 })
2366 .flatten(),
2367 )
2368 .child(label),
2369 );
2370
2371 let single_entry_to_resolve = self.items[ix]
2372 .is_singleton(cx)
2373 .then(|| self.items[ix].project_entry_ids(cx).get(0).copied())
2374 .flatten();
2375
2376 let total_items = self.items.len();
2377 let has_items_to_left = ix > 0;
2378 let has_items_to_right = ix < total_items - 1;
2379 let is_pinned = self.is_tab_pinned(ix);
2380 let pane = cx.entity().downgrade();
2381 let menu_context = item.item_focus_handle(cx);
2382 right_click_menu(ix)
2383 .trigger(|_| tab)
2384 .menu(move |window, cx| {
2385 let pane = pane.clone();
2386 let menu_context = menu_context.clone();
2387 ContextMenu::build(window, cx, move |mut menu, window, cx| {
2388 let close_active_item_action = CloseActiveItem {
2389 save_intent: None,
2390 close_pinned: true,
2391 };
2392 let close_inactive_items_action = CloseInactiveItems {
2393 save_intent: None,
2394 close_pinned: false,
2395 };
2396 let close_items_to_the_left_action = CloseItemsToTheLeft {
2397 close_pinned: false,
2398 };
2399 let close_items_to_the_right_action = CloseItemsToTheRight {
2400 close_pinned: false,
2401 };
2402 let close_clean_items_action = CloseCleanItems {
2403 close_pinned: false,
2404 };
2405 let close_all_items_action = CloseAllItems {
2406 save_intent: None,
2407 close_pinned: false,
2408 };
2409 if let Some(pane) = pane.upgrade() {
2410 menu = menu
2411 .entry(
2412 "Close",
2413 Some(Box::new(close_active_item_action)),
2414 window.handler_for(&pane, move |pane, window, cx| {
2415 pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
2416 .detach_and_log_err(cx);
2417 }),
2418 )
2419 .item(ContextMenuItem::Entry(
2420 ContextMenuEntry::new("Close Others")
2421 .action(Box::new(close_inactive_items_action.clone()))
2422 .disabled(total_items == 1)
2423 .handler(window.handler_for(&pane, move |pane, window, cx| {
2424 pane.close_inactive_items(
2425 &close_inactive_items_action,
2426 window,
2427 cx,
2428 )
2429 .detach_and_log_err(cx);
2430 })),
2431 ))
2432 .separator()
2433 .item(ContextMenuItem::Entry(
2434 ContextMenuEntry::new("Close Left")
2435 .action(Box::new(close_items_to_the_left_action.clone()))
2436 .disabled(!has_items_to_left)
2437 .handler(window.handler_for(&pane, move |pane, window, cx| {
2438 pane.close_items_to_the_left_by_id(
2439 item_id,
2440 &close_items_to_the_left_action,
2441 pane.pinned_item_ids(),
2442 window,
2443 cx,
2444 )
2445 .detach_and_log_err(cx);
2446 })),
2447 ))
2448 .item(ContextMenuItem::Entry(
2449 ContextMenuEntry::new("Close Right")
2450 .action(Box::new(close_items_to_the_right_action.clone()))
2451 .disabled(!has_items_to_right)
2452 .handler(window.handler_for(&pane, move |pane, window, cx| {
2453 pane.close_items_to_the_right_by_id(
2454 item_id,
2455 &close_items_to_the_right_action,
2456 pane.pinned_item_ids(),
2457 window,
2458 cx,
2459 )
2460 .detach_and_log_err(cx);
2461 })),
2462 ))
2463 .separator()
2464 .entry(
2465 "Close Clean",
2466 Some(Box::new(close_clean_items_action.clone())),
2467 window.handler_for(&pane, move |pane, window, cx| {
2468 pane.close_clean_items(&close_clean_items_action, window, cx)
2469 .detach_and_log_err(cx)
2470 }),
2471 )
2472 .entry(
2473 "Close All",
2474 Some(Box::new(close_all_items_action.clone())),
2475 window.handler_for(&pane, move |pane, window, cx| {
2476 pane.close_all_items(&close_all_items_action, window, cx)
2477 .detach_and_log_err(cx)
2478 }),
2479 );
2480
2481 let pin_tab_entries = |menu: ContextMenu| {
2482 menu.separator().map(|this| {
2483 if is_pinned {
2484 this.entry(
2485 "Unpin Tab",
2486 Some(TogglePinTab.boxed_clone()),
2487 window.handler_for(&pane, move |pane, window, cx| {
2488 pane.unpin_tab_at(ix, window, cx);
2489 }),
2490 )
2491 } else {
2492 this.entry(
2493 "Pin Tab",
2494 Some(TogglePinTab.boxed_clone()),
2495 window.handler_for(&pane, move |pane, window, cx| {
2496 pane.pin_tab_at(ix, window, cx);
2497 }),
2498 )
2499 }
2500 })
2501 };
2502 if let Some(entry) = single_entry_to_resolve {
2503 let project_path = pane
2504 .read(cx)
2505 .item_for_entry(entry, cx)
2506 .and_then(|item| item.project_path(cx));
2507 let worktree = project_path.as_ref().and_then(|project_path| {
2508 pane.read(cx)
2509 .project
2510 .upgrade()?
2511 .read(cx)
2512 .worktree_for_id(project_path.worktree_id, cx)
2513 });
2514 let has_relative_path = worktree.as_ref().is_some_and(|worktree| {
2515 worktree
2516 .read(cx)
2517 .root_entry()
2518 .map_or(false, |entry| entry.is_dir())
2519 });
2520
2521 let entry_abs_path = pane.read(cx).entry_abs_path(entry, cx);
2522 let parent_abs_path = entry_abs_path
2523 .as_deref()
2524 .and_then(|abs_path| Some(abs_path.parent()?.to_path_buf()));
2525 let relative_path = project_path
2526 .map(|project_path| project_path.path)
2527 .filter(|_| has_relative_path);
2528
2529 let visible_in_project_panel = relative_path.is_some()
2530 && worktree.is_some_and(|worktree| worktree.read(cx).is_visible());
2531
2532 let entry_id = entry.to_proto();
2533 menu = menu
2534 .separator()
2535 .when_some(entry_abs_path, |menu, abs_path| {
2536 menu.entry(
2537 "Copy Path",
2538 Some(Box::new(zed_actions::workspace::CopyPath)),
2539 window.handler_for(&pane, move |_, _, cx| {
2540 cx.write_to_clipboard(ClipboardItem::new_string(
2541 abs_path.to_string_lossy().to_string(),
2542 ));
2543 }),
2544 )
2545 })
2546 .when_some(relative_path, |menu, relative_path| {
2547 menu.entry(
2548 "Copy Relative Path",
2549 Some(Box::new(zed_actions::workspace::CopyRelativePath)),
2550 window.handler_for(&pane, move |_, _, cx| {
2551 cx.write_to_clipboard(ClipboardItem::new_string(
2552 relative_path.to_string_lossy().to_string(),
2553 ));
2554 }),
2555 )
2556 })
2557 .map(pin_tab_entries)
2558 .separator()
2559 .when(visible_in_project_panel, |menu| {
2560 menu.entry(
2561 "Reveal In Project Panel",
2562 Some(Box::new(RevealInProjectPanel {
2563 entry_id: Some(entry_id),
2564 })),
2565 window.handler_for(&pane, move |pane, _, cx| {
2566 pane.project
2567 .update(cx, |_, cx| {
2568 cx.emit(project::Event::RevealInProjectPanel(
2569 ProjectEntryId::from_proto(entry_id),
2570 ))
2571 })
2572 .ok();
2573 }),
2574 )
2575 })
2576 .when_some(parent_abs_path, |menu, parent_abs_path| {
2577 menu.entry(
2578 "Open in Terminal",
2579 Some(Box::new(OpenInTerminal)),
2580 window.handler_for(&pane, move |_, window, cx| {
2581 window.dispatch_action(
2582 OpenTerminal {
2583 working_directory: parent_abs_path.clone(),
2584 }
2585 .boxed_clone(),
2586 cx,
2587 );
2588 }),
2589 )
2590 });
2591 } else {
2592 menu = menu.map(pin_tab_entries);
2593 }
2594 }
2595
2596 menu.context(menu_context)
2597 })
2598 })
2599 }
2600
2601 fn render_tab_bar(&mut self, window: &mut Window, cx: &mut Context<Pane>) -> AnyElement {
2602 let focus_handle = self.focus_handle.clone();
2603 let navigate_backward = IconButton::new("navigate_backward", IconName::ArrowLeft)
2604 .icon_size(IconSize::Small)
2605 .on_click({
2606 let entity = cx.entity().clone();
2607 move |_, window, cx| {
2608 entity.update(cx, |pane, cx| pane.navigate_backward(window, cx))
2609 }
2610 })
2611 .disabled(!self.can_navigate_backward())
2612 .tooltip({
2613 let focus_handle = focus_handle.clone();
2614 move |window, cx| {
2615 Tooltip::for_action_in("Go Back", &GoBack, &focus_handle, window, cx)
2616 }
2617 });
2618
2619 let navigate_forward = IconButton::new("navigate_forward", IconName::ArrowRight)
2620 .icon_size(IconSize::Small)
2621 .on_click({
2622 let entity = cx.entity().clone();
2623 move |_, window, cx| entity.update(cx, |pane, cx| pane.navigate_forward(window, cx))
2624 })
2625 .disabled(!self.can_navigate_forward())
2626 .tooltip({
2627 let focus_handle = focus_handle.clone();
2628 move |window, cx| {
2629 Tooltip::for_action_in("Go Forward", &GoForward, &focus_handle, window, cx)
2630 }
2631 });
2632
2633 let mut tab_items = self
2634 .items
2635 .iter()
2636 .enumerate()
2637 .zip(tab_details(&self.items, window, cx))
2638 .map(|((ix, item), detail)| {
2639 self.render_tab(ix, &**item, detail, &focus_handle, window, cx)
2640 })
2641 .collect::<Vec<_>>();
2642 let tab_count = tab_items.len();
2643 let unpinned_tabs = tab_items.split_off(self.pinned_tab_count);
2644 let pinned_tabs = tab_items;
2645 TabBar::new("tab_bar")
2646 .when(
2647 self.display_nav_history_buttons.unwrap_or_default(),
2648 |tab_bar| {
2649 tab_bar
2650 .start_child(navigate_backward)
2651 .start_child(navigate_forward)
2652 },
2653 )
2654 .map(|tab_bar| {
2655 if self.show_tab_bar_buttons {
2656 let render_tab_buttons = self.render_tab_bar_buttons.clone();
2657 let (left_children, right_children) = render_tab_buttons(self, window, cx);
2658 tab_bar
2659 .start_children(left_children)
2660 .end_children(right_children)
2661 } else {
2662 tab_bar
2663 }
2664 })
2665 .children(pinned_tabs.len().ne(&0).then(|| {
2666 let content_width = self.tab_bar_scroll_handle.content_size().width;
2667 let viewport_width = self.tab_bar_scroll_handle.viewport().size.width;
2668 // We need to check both because offset returns delta values even when the scroll handle is not scrollable
2669 let is_scrollable = content_width > viewport_width;
2670 let is_scrolled = self.tab_bar_scroll_handle.offset().x < px(0.);
2671 let has_active_unpinned_tab = self.active_item_index >= self.pinned_tab_count;
2672 h_flex()
2673 .children(pinned_tabs)
2674 .when(is_scrollable && is_scrolled, |this| {
2675 this.when(has_active_unpinned_tab, |this| this.border_r_2())
2676 .when(!has_active_unpinned_tab, |this| this.border_r_1())
2677 .border_color(cx.theme().colors().border)
2678 })
2679 }))
2680 .child(
2681 h_flex()
2682 .id("unpinned tabs")
2683 .overflow_x_scroll()
2684 .w_full()
2685 .track_scroll(&self.tab_bar_scroll_handle)
2686 .children(unpinned_tabs)
2687 .child(
2688 div()
2689 .id("tab_bar_drop_target")
2690 .min_w_6()
2691 // HACK: This empty child is currently necessary to force the drop target to appear
2692 // despite us setting a min width above.
2693 .child("")
2694 .h_full()
2695 .flex_grow()
2696 .drag_over::<DraggedTab>(|bar, _, _, cx| {
2697 bar.bg(cx.theme().colors().drop_target_background)
2698 })
2699 .drag_over::<DraggedSelection>(|bar, _, _, cx| {
2700 bar.bg(cx.theme().colors().drop_target_background)
2701 })
2702 .on_drop(cx.listener(
2703 move |this, dragged_tab: &DraggedTab, window, cx| {
2704 this.drag_split_direction = None;
2705 this.handle_tab_drop(dragged_tab, this.items.len(), window, cx)
2706 },
2707 ))
2708 .on_drop(cx.listener(
2709 move |this, selection: &DraggedSelection, window, cx| {
2710 this.drag_split_direction = None;
2711 this.handle_project_entry_drop(
2712 &selection.active_selection.entry_id,
2713 Some(tab_count),
2714 window,
2715 cx,
2716 )
2717 },
2718 ))
2719 .on_drop(cx.listener(move |this, paths, window, cx| {
2720 this.drag_split_direction = None;
2721 this.handle_external_paths_drop(paths, window, cx)
2722 }))
2723 .on_click(cx.listener(move |this, event: &ClickEvent, window, cx| {
2724 if event.up.click_count == 2 {
2725 window.dispatch_action(
2726 this.double_click_dispatch_action.boxed_clone(),
2727 cx,
2728 );
2729 }
2730 })),
2731 ),
2732 )
2733 .into_any_element()
2734 }
2735
2736 pub fn render_menu_overlay(menu: &Entity<ContextMenu>) -> Div {
2737 div().absolute().bottom_0().right_0().size_0().child(
2738 deferred(anchored().anchor(Corner::TopRight).child(menu.clone())).with_priority(1),
2739 )
2740 }
2741
2742 pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut Context<Self>) {
2743 self.zoomed = zoomed;
2744 cx.notify();
2745 }
2746
2747 pub fn is_zoomed(&self) -> bool {
2748 self.zoomed
2749 }
2750
2751 fn handle_drag_move<T: 'static>(
2752 &mut self,
2753 event: &DragMoveEvent<T>,
2754 window: &mut Window,
2755 cx: &mut Context<Self>,
2756 ) {
2757 let can_split_predicate = self.can_split_predicate.take();
2758 let can_split = match &can_split_predicate {
2759 Some(can_split_predicate) => {
2760 can_split_predicate(self, event.dragged_item(), window, cx)
2761 }
2762 None => false,
2763 };
2764 self.can_split_predicate = can_split_predicate;
2765 if !can_split {
2766 return;
2767 }
2768
2769 let rect = event.bounds.size;
2770
2771 let size = event.bounds.size.width.min(event.bounds.size.height)
2772 * WorkspaceSettings::get_global(cx).drop_target_size;
2773
2774 let relative_cursor = Point::new(
2775 event.event.position.x - event.bounds.left(),
2776 event.event.position.y - event.bounds.top(),
2777 );
2778
2779 let direction = if relative_cursor.x < size
2780 || relative_cursor.x > rect.width - size
2781 || relative_cursor.y < size
2782 || relative_cursor.y > rect.height - size
2783 {
2784 [
2785 SplitDirection::Up,
2786 SplitDirection::Right,
2787 SplitDirection::Down,
2788 SplitDirection::Left,
2789 ]
2790 .iter()
2791 .min_by_key(|side| match side {
2792 SplitDirection::Up => relative_cursor.y,
2793 SplitDirection::Right => rect.width - relative_cursor.x,
2794 SplitDirection::Down => rect.height - relative_cursor.y,
2795 SplitDirection::Left => relative_cursor.x,
2796 })
2797 .cloned()
2798 } else {
2799 None
2800 };
2801
2802 if direction != self.drag_split_direction {
2803 self.drag_split_direction = direction;
2804 }
2805 }
2806
2807 pub fn handle_tab_drop(
2808 &mut self,
2809 dragged_tab: &DraggedTab,
2810 ix: usize,
2811 window: &mut Window,
2812 cx: &mut Context<Self>,
2813 ) {
2814 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2815 if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, window, cx) {
2816 return;
2817 }
2818 }
2819 let mut to_pane = cx.entity().clone();
2820 let split_direction = self.drag_split_direction;
2821 let item_id = dragged_tab.item.item_id();
2822 if let Some(preview_item_id) = self.preview_item_id {
2823 if item_id == preview_item_id {
2824 self.set_preview_item_id(None, cx);
2825 }
2826 }
2827
2828 let is_clone = cfg!(target_os = "macos") && window.modifiers().alt
2829 || cfg!(not(target_os = "macos")) && window.modifiers().control;
2830
2831 let from_pane = dragged_tab.pane.clone();
2832 self.workspace
2833 .update(cx, |_, cx| {
2834 cx.defer_in(window, move |workspace, window, cx| {
2835 if let Some(split_direction) = split_direction {
2836 to_pane = workspace.split_pane(to_pane, split_direction, window, cx);
2837 }
2838 let database_id = workspace.database_id();
2839 let old_ix = from_pane.read(cx).index_for_item_id(item_id);
2840 let old_len = to_pane.read(cx).items.len();
2841 if is_clone {
2842 let Some(item) = from_pane
2843 .read(cx)
2844 .items()
2845 .find(|item| item.item_id() == item_id)
2846 .map(|item| item.clone())
2847 else {
2848 return;
2849 };
2850 if let Some(item) = item.clone_on_split(database_id, window, cx) {
2851 to_pane.update(cx, |pane, cx| {
2852 pane.add_item(item, true, true, None, window, cx);
2853 })
2854 }
2855 } else {
2856 move_item(&from_pane, &to_pane, item_id, ix, window, cx);
2857 }
2858 if to_pane == from_pane {
2859 if let Some(old_index) = old_ix {
2860 to_pane.update(cx, |this, _| {
2861 if old_index < this.pinned_tab_count
2862 && (ix == this.items.len() || ix > this.pinned_tab_count)
2863 {
2864 this.pinned_tab_count -= 1;
2865 } else if this.has_pinned_tabs()
2866 && old_index >= this.pinned_tab_count
2867 && ix < this.pinned_tab_count
2868 {
2869 this.pinned_tab_count += 1;
2870 }
2871 });
2872 }
2873 } else {
2874 to_pane.update(cx, |this, _| {
2875 if this.items.len() > old_len // Did we not deduplicate on drag?
2876 && this.has_pinned_tabs()
2877 && ix < this.pinned_tab_count
2878 {
2879 this.pinned_tab_count += 1;
2880 }
2881 });
2882 from_pane.update(cx, |this, _| {
2883 if let Some(index) = old_ix {
2884 if this.pinned_tab_count > index {
2885 this.pinned_tab_count -= 1;
2886 }
2887 }
2888 })
2889 }
2890 });
2891 })
2892 .log_err();
2893 }
2894
2895 fn handle_dragged_selection_drop(
2896 &mut self,
2897 dragged_selection: &DraggedSelection,
2898 dragged_onto: Option<usize>,
2899 window: &mut Window,
2900 cx: &mut Context<Self>,
2901 ) {
2902 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2903 if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_selection, window, cx)
2904 {
2905 return;
2906 }
2907 }
2908 self.handle_project_entry_drop(
2909 &dragged_selection.active_selection.entry_id,
2910 dragged_onto,
2911 window,
2912 cx,
2913 );
2914 }
2915
2916 fn handle_project_entry_drop(
2917 &mut self,
2918 project_entry_id: &ProjectEntryId,
2919 target: Option<usize>,
2920 window: &mut Window,
2921 cx: &mut Context<Self>,
2922 ) {
2923 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
2924 if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, window, cx) {
2925 return;
2926 }
2927 }
2928 let mut to_pane = cx.entity().clone();
2929 let split_direction = self.drag_split_direction;
2930 let project_entry_id = *project_entry_id;
2931 self.workspace
2932 .update(cx, |_, cx| {
2933 cx.defer_in(window, move |workspace, window, cx| {
2934 if let Some(project_path) = workspace
2935 .project()
2936 .read(cx)
2937 .path_for_entry(project_entry_id, cx)
2938 {
2939 let load_path_task = workspace.load_path(project_path.clone(), window, cx);
2940 cx.spawn_in(window, async move |workspace, cx| {
2941 if let Some((project_entry_id, build_item)) =
2942 load_path_task.await.notify_async_err(cx)
2943 {
2944 let (to_pane, new_item_handle) = workspace
2945 .update_in(cx, |workspace, window, cx| {
2946 if let Some(split_direction) = split_direction {
2947 to_pane = workspace.split_pane(
2948 to_pane,
2949 split_direction,
2950 window,
2951 cx,
2952 );
2953 }
2954 let new_item_handle = to_pane.update(cx, |pane, cx| {
2955 pane.open_item(
2956 project_entry_id,
2957 project_path,
2958 true,
2959 false,
2960 true,
2961 target,
2962 window,
2963 cx,
2964 build_item,
2965 )
2966 });
2967 (to_pane, new_item_handle)
2968 })
2969 .log_err()?;
2970 to_pane
2971 .update_in(cx, |this, window, cx| {
2972 let Some(index) = this.index_for_item(&*new_item_handle)
2973 else {
2974 return;
2975 };
2976
2977 if target.map_or(false, |target| this.is_tab_pinned(target))
2978 {
2979 this.pin_tab_at(index, window, cx);
2980 }
2981 })
2982 .ok()?
2983 }
2984 Some(())
2985 })
2986 .detach();
2987 };
2988 });
2989 })
2990 .log_err();
2991 }
2992
2993 fn handle_external_paths_drop(
2994 &mut self,
2995 paths: &ExternalPaths,
2996 window: &mut Window,
2997 cx: &mut Context<Self>,
2998 ) {
2999 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
3000 if let ControlFlow::Break(()) = custom_drop_handle(self, paths, window, cx) {
3001 return;
3002 }
3003 }
3004 let mut to_pane = cx.entity().clone();
3005 let mut split_direction = self.drag_split_direction;
3006 let paths = paths.paths().to_vec();
3007 let is_remote = self
3008 .workspace
3009 .update(cx, |workspace, cx| {
3010 if workspace.project().read(cx).is_via_collab() {
3011 workspace.show_error(
3012 &anyhow::anyhow!("Cannot drop files on a remote project"),
3013 cx,
3014 );
3015 true
3016 } else {
3017 false
3018 }
3019 })
3020 .unwrap_or(true);
3021 if is_remote {
3022 return;
3023 }
3024
3025 self.workspace
3026 .update(cx, |workspace, cx| {
3027 let fs = Arc::clone(workspace.project().read(cx).fs());
3028 cx.spawn_in(window, async move |workspace, cx| {
3029 let mut is_file_checks = FuturesUnordered::new();
3030 for path in &paths {
3031 is_file_checks.push(fs.is_file(path))
3032 }
3033 let mut has_files_to_open = false;
3034 while let Some(is_file) = is_file_checks.next().await {
3035 if is_file {
3036 has_files_to_open = true;
3037 break;
3038 }
3039 }
3040 drop(is_file_checks);
3041 if !has_files_to_open {
3042 split_direction = None;
3043 }
3044
3045 if let Ok(open_task) = workspace.update_in(cx, |workspace, window, cx| {
3046 if let Some(split_direction) = split_direction {
3047 to_pane = workspace.split_pane(to_pane, split_direction, window, cx);
3048 }
3049 workspace.open_paths(
3050 paths,
3051 OpenOptions {
3052 visible: Some(OpenVisible::OnlyDirectories),
3053 ..Default::default()
3054 },
3055 Some(to_pane.downgrade()),
3056 window,
3057 cx,
3058 )
3059 }) {
3060 let opened_items: Vec<_> = open_task.await;
3061 _ = workspace.update(cx, |workspace, cx| {
3062 for item in opened_items.into_iter().flatten() {
3063 if let Err(e) = item {
3064 workspace.show_error(&e, cx);
3065 }
3066 }
3067 });
3068 }
3069 })
3070 .detach();
3071 })
3072 .log_err();
3073 }
3074
3075 pub fn display_nav_history_buttons(&mut self, display: Option<bool>) {
3076 self.display_nav_history_buttons = display;
3077 }
3078
3079 fn pinned_item_ids(&self) -> HashSet<EntityId> {
3080 self.items
3081 .iter()
3082 .enumerate()
3083 .filter_map(|(index, item)| {
3084 if self.is_tab_pinned(index) {
3085 return Some(item.item_id());
3086 }
3087
3088 None
3089 })
3090 .collect()
3091 }
3092
3093 fn clean_item_ids(&self, cx: &mut Context<Pane>) -> HashSet<EntityId> {
3094 self.items()
3095 .filter_map(|item| {
3096 if !item.is_dirty(cx) {
3097 return Some(item.item_id());
3098 }
3099
3100 None
3101 })
3102 .collect()
3103 }
3104
3105 fn to_the_left_item_ids(&self, item_id: EntityId) -> HashSet<EntityId> {
3106 self.items()
3107 .take_while(|item| item.item_id() != item_id)
3108 .map(|item| item.item_id())
3109 .collect()
3110 }
3111
3112 fn to_the_right_item_ids(&self, item_id: EntityId) -> HashSet<EntityId> {
3113 self.items()
3114 .rev()
3115 .take_while(|item| item.item_id() != item_id)
3116 .map(|item| item.item_id())
3117 .collect()
3118 }
3119
3120 pub fn drag_split_direction(&self) -> Option<SplitDirection> {
3121 self.drag_split_direction
3122 }
3123
3124 pub fn set_zoom_out_on_close(&mut self, zoom_out_on_close: bool) {
3125 self.zoom_out_on_close = zoom_out_on_close;
3126 }
3127}
3128
3129fn default_render_tab_bar_buttons(
3130 pane: &mut Pane,
3131 window: &mut Window,
3132 cx: &mut Context<Pane>,
3133) -> (Option<AnyElement>, Option<AnyElement>) {
3134 if !pane.has_focus(window, cx) && !pane.context_menu_focused(window, cx) {
3135 return (None, None);
3136 }
3137 // Ideally we would return a vec of elements here to pass directly to the [TabBar]'s
3138 // `end_slot`, but due to needing a view here that isn't possible.
3139 let right_children = h_flex()
3140 // Instead we need to replicate the spacing from the [TabBar]'s `end_slot` here.
3141 .gap(DynamicSpacing::Base04.rems(cx))
3142 .child(
3143 PopoverMenu::new("pane-tab-bar-popover-menu")
3144 .trigger_with_tooltip(
3145 IconButton::new("plus", IconName::Plus).icon_size(IconSize::Small),
3146 Tooltip::text("New..."),
3147 )
3148 .anchor(Corner::TopRight)
3149 .with_handle(pane.new_item_context_menu_handle.clone())
3150 .menu(move |window, cx| {
3151 Some(ContextMenu::build(window, cx, |menu, _, _| {
3152 menu.action("New File", NewFile.boxed_clone())
3153 .action("Open File", ToggleFileFinder::default().boxed_clone())
3154 .separator()
3155 .action(
3156 "Search Project",
3157 DeploySearch {
3158 replace_enabled: false,
3159 included_files: None,
3160 excluded_files: None,
3161 }
3162 .boxed_clone(),
3163 )
3164 .action("Search Symbols", ToggleProjectSymbols.boxed_clone())
3165 .separator()
3166 .action("New Terminal", NewTerminal.boxed_clone())
3167 }))
3168 }),
3169 )
3170 .child(
3171 PopoverMenu::new("pane-tab-bar-split")
3172 .trigger_with_tooltip(
3173 IconButton::new("split", IconName::Split).icon_size(IconSize::Small),
3174 Tooltip::text("Split Pane"),
3175 )
3176 .anchor(Corner::TopRight)
3177 .with_handle(pane.split_item_context_menu_handle.clone())
3178 .menu(move |window, cx| {
3179 ContextMenu::build(window, cx, |menu, _, _| {
3180 menu.action("Split Right", SplitRight.boxed_clone())
3181 .action("Split Left", SplitLeft.boxed_clone())
3182 .action("Split Up", SplitUp.boxed_clone())
3183 .action("Split Down", SplitDown.boxed_clone())
3184 })
3185 .into()
3186 }),
3187 )
3188 .child({
3189 let zoomed = pane.is_zoomed();
3190 IconButton::new("toggle_zoom", IconName::Maximize)
3191 .icon_size(IconSize::Small)
3192 .toggle_state(zoomed)
3193 .selected_icon(IconName::Minimize)
3194 .on_click(cx.listener(|pane, _, window, cx| {
3195 pane.toggle_zoom(&crate::ToggleZoom, window, cx);
3196 }))
3197 .tooltip(move |window, cx| {
3198 Tooltip::for_action(
3199 if zoomed { "Zoom Out" } else { "Zoom In" },
3200 &ToggleZoom,
3201 window,
3202 cx,
3203 )
3204 })
3205 })
3206 .into_any_element()
3207 .into();
3208 (None, right_children)
3209}
3210
3211impl Focusable for Pane {
3212 fn focus_handle(&self, _cx: &App) -> FocusHandle {
3213 self.focus_handle.clone()
3214 }
3215}
3216
3217impl Render for Pane {
3218 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3219 let mut key_context = KeyContext::new_with_defaults();
3220 key_context.add("Pane");
3221 if self.active_item().is_none() {
3222 key_context.add("EmptyPane");
3223 }
3224
3225 let should_display_tab_bar = self.should_display_tab_bar.clone();
3226 let display_tab_bar = should_display_tab_bar(window, cx);
3227 let Some(project) = self.project.upgrade() else {
3228 return div().track_focus(&self.focus_handle(cx));
3229 };
3230 let is_local = project.read(cx).is_local();
3231
3232 v_flex()
3233 .key_context(key_context)
3234 .track_focus(&self.focus_handle(cx))
3235 .size_full()
3236 .flex_none()
3237 .overflow_hidden()
3238 .on_action(cx.listener(|pane, _: &AlternateFile, window, cx| {
3239 pane.alternate_file(window, cx);
3240 }))
3241 .on_action(
3242 cx.listener(|pane, _: &SplitLeft, _, cx| pane.split(SplitDirection::Left, cx)),
3243 )
3244 .on_action(cx.listener(|pane, _: &SplitUp, _, cx| pane.split(SplitDirection::Up, cx)))
3245 .on_action(cx.listener(|pane, _: &SplitHorizontal, _, cx| {
3246 pane.split(SplitDirection::horizontal(cx), cx)
3247 }))
3248 .on_action(cx.listener(|pane, _: &SplitVertical, _, cx| {
3249 pane.split(SplitDirection::vertical(cx), cx)
3250 }))
3251 .on_action(
3252 cx.listener(|pane, _: &SplitRight, _, cx| pane.split(SplitDirection::Right, cx)),
3253 )
3254 .on_action(
3255 cx.listener(|pane, _: &SplitDown, _, cx| pane.split(SplitDirection::Down, cx)),
3256 )
3257 .on_action(
3258 cx.listener(|pane, _: &GoBack, window, cx| pane.navigate_backward(window, cx)),
3259 )
3260 .on_action(
3261 cx.listener(|pane, _: &GoForward, window, cx| pane.navigate_forward(window, cx)),
3262 )
3263 .on_action(cx.listener(|_, _: &JoinIntoNext, _, cx| {
3264 cx.emit(Event::JoinIntoNext);
3265 }))
3266 .on_action(cx.listener(|_, _: &JoinAll, _, cx| {
3267 cx.emit(Event::JoinAll);
3268 }))
3269 .on_action(cx.listener(Pane::toggle_zoom))
3270 .on_action(
3271 cx.listener(|pane: &mut Pane, action: &ActivateItem, window, cx| {
3272 pane.activate_item(
3273 action.0.min(pane.items.len().saturating_sub(1)),
3274 true,
3275 true,
3276 window,
3277 cx,
3278 );
3279 }),
3280 )
3281 .on_action(
3282 cx.listener(|pane: &mut Pane, _: &ActivateLastItem, window, cx| {
3283 pane.activate_item(pane.items.len().saturating_sub(1), true, true, window, cx);
3284 }),
3285 )
3286 .on_action(
3287 cx.listener(|pane: &mut Pane, _: &ActivatePreviousItem, window, cx| {
3288 pane.activate_prev_item(true, window, cx);
3289 }),
3290 )
3291 .on_action(
3292 cx.listener(|pane: &mut Pane, _: &ActivateNextItem, window, cx| {
3293 pane.activate_next_item(true, window, cx);
3294 }),
3295 )
3296 .on_action(
3297 cx.listener(|pane, _: &SwapItemLeft, window, cx| pane.swap_item_left(window, cx)),
3298 )
3299 .on_action(
3300 cx.listener(|pane, _: &SwapItemRight, window, cx| pane.swap_item_right(window, cx)),
3301 )
3302 .on_action(cx.listener(|pane, action, window, cx| {
3303 pane.toggle_pin_tab(action, window, cx);
3304 }))
3305 .when(PreviewTabsSettings::get_global(cx).enabled, |this| {
3306 this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, _, cx| {
3307 if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) {
3308 if pane.is_active_preview_item(active_item_id) {
3309 pane.set_preview_item_id(None, cx);
3310 } else {
3311 pane.set_preview_item_id(Some(active_item_id), cx);
3312 }
3313 }
3314 }))
3315 })
3316 .on_action(
3317 cx.listener(|pane: &mut Self, action: &CloseActiveItem, window, cx| {
3318 pane.close_active_item(action, window, cx)
3319 .detach_and_log_err(cx)
3320 }),
3321 )
3322 .on_action(
3323 cx.listener(|pane: &mut Self, action: &CloseInactiveItems, window, cx| {
3324 pane.close_inactive_items(action, window, cx)
3325 .detach_and_log_err(cx);
3326 }),
3327 )
3328 .on_action(
3329 cx.listener(|pane: &mut Self, action: &CloseCleanItems, window, cx| {
3330 pane.close_clean_items(action, window, cx)
3331 .detach_and_log_err(cx)
3332 }),
3333 )
3334 .on_action(cx.listener(
3335 |pane: &mut Self, action: &CloseItemsToTheLeft, window, cx| {
3336 pane.close_items_to_the_left(action, window, cx)
3337 .detach_and_log_err(cx)
3338 },
3339 ))
3340 .on_action(cx.listener(
3341 |pane: &mut Self, action: &CloseItemsToTheRight, window, cx| {
3342 pane.close_items_to_the_right(action, window, cx)
3343 .detach_and_log_err(cx)
3344 },
3345 ))
3346 .on_action(
3347 cx.listener(|pane: &mut Self, action: &CloseAllItems, window, cx| {
3348 pane.close_all_items(action, window, cx)
3349 .detach_and_log_err(cx)
3350 }),
3351 )
3352 .on_action(
3353 cx.listener(|pane: &mut Self, action: &CloseActiveItem, window, cx| {
3354 pane.close_active_item(action, window, cx)
3355 .detach_and_log_err(cx)
3356 }),
3357 )
3358 .on_action(
3359 cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, _, cx| {
3360 let entry_id = action
3361 .entry_id
3362 .map(ProjectEntryId::from_proto)
3363 .or_else(|| pane.active_item()?.project_entry_ids(cx).first().copied());
3364 if let Some(entry_id) = entry_id {
3365 pane.project
3366 .update(cx, |_, cx| {
3367 cx.emit(project::Event::RevealInProjectPanel(entry_id))
3368 })
3369 .ok();
3370 }
3371 }),
3372 )
3373 .on_action(cx.listener(|_, _: &menu::Cancel, window, cx| {
3374 if cx.stop_active_drag(window) {
3375 return;
3376 } else {
3377 cx.propagate();
3378 }
3379 }))
3380 .when(self.active_item().is_some() && display_tab_bar, |pane| {
3381 pane.child((self.render_tab_bar.clone())(self, window, cx))
3382 })
3383 .child({
3384 let has_worktrees = project.read(cx).visible_worktrees(cx).next().is_some();
3385 // main content
3386 div()
3387 .flex_1()
3388 .relative()
3389 .group("")
3390 .overflow_hidden()
3391 .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
3392 .on_drag_move::<DraggedSelection>(cx.listener(Self::handle_drag_move))
3393 .when(is_local, |div| {
3394 div.on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
3395 })
3396 .map(|div| {
3397 if let Some(item) = self.active_item() {
3398 div.id("pane_placeholder")
3399 .v_flex()
3400 .size_full()
3401 .overflow_hidden()
3402 .child(self.toolbar.clone())
3403 .child(item.to_any())
3404 } else {
3405 let placeholder = div
3406 .id("pane_placeholder")
3407 .h_flex()
3408 .size_full()
3409 .justify_center()
3410 .on_click(cx.listener(
3411 move |this, event: &ClickEvent, window, cx| {
3412 if event.up.click_count == 2 {
3413 window.dispatch_action(
3414 this.double_click_dispatch_action.boxed_clone(),
3415 cx,
3416 );
3417 }
3418 },
3419 ));
3420 if has_worktrees {
3421 placeholder
3422 } else {
3423 placeholder.child(
3424 Label::new("Open a file or project to get started.")
3425 .color(Color::Muted),
3426 )
3427 }
3428 }
3429 })
3430 .child(
3431 // drag target
3432 div()
3433 .invisible()
3434 .absolute()
3435 .bg(cx.theme().colors().drop_target_background)
3436 .group_drag_over::<DraggedTab>("", |style| style.visible())
3437 .group_drag_over::<DraggedSelection>("", |style| style.visible())
3438 .when(is_local, |div| {
3439 div.group_drag_over::<ExternalPaths>("", |style| style.visible())
3440 })
3441 .when_some(self.can_drop_predicate.clone(), |this, p| {
3442 this.can_drop(move |a, window, cx| p(a, window, cx))
3443 })
3444 .on_drop(cx.listener(move |this, dragged_tab, window, cx| {
3445 this.handle_tab_drop(
3446 dragged_tab,
3447 this.active_item_index(),
3448 window,
3449 cx,
3450 )
3451 }))
3452 .on_drop(cx.listener(
3453 move |this, selection: &DraggedSelection, window, cx| {
3454 this.handle_dragged_selection_drop(selection, None, window, cx)
3455 },
3456 ))
3457 .on_drop(cx.listener(move |this, paths, window, cx| {
3458 this.handle_external_paths_drop(paths, window, cx)
3459 }))
3460 .map(|div| {
3461 let size = DefiniteLength::Fraction(0.5);
3462 match self.drag_split_direction {
3463 None => div.top_0().right_0().bottom_0().left_0(),
3464 Some(SplitDirection::Up) => {
3465 div.top_0().left_0().right_0().h(size)
3466 }
3467 Some(SplitDirection::Down) => {
3468 div.left_0().bottom_0().right_0().h(size)
3469 }
3470 Some(SplitDirection::Left) => {
3471 div.top_0().left_0().bottom_0().w(size)
3472 }
3473 Some(SplitDirection::Right) => {
3474 div.top_0().bottom_0().right_0().w(size)
3475 }
3476 }
3477 }),
3478 )
3479 })
3480 .on_mouse_down(
3481 MouseButton::Navigate(NavigationDirection::Back),
3482 cx.listener(|pane, _, window, cx| {
3483 if let Some(workspace) = pane.workspace.upgrade() {
3484 let pane = cx.entity().downgrade();
3485 window.defer(cx, move |window, cx| {
3486 workspace.update(cx, |workspace, cx| {
3487 workspace.go_back(pane, window, cx).detach_and_log_err(cx)
3488 })
3489 })
3490 }
3491 }),
3492 )
3493 .on_mouse_down(
3494 MouseButton::Navigate(NavigationDirection::Forward),
3495 cx.listener(|pane, _, window, cx| {
3496 if let Some(workspace) = pane.workspace.upgrade() {
3497 let pane = cx.entity().downgrade();
3498 window.defer(cx, move |window, cx| {
3499 workspace.update(cx, |workspace, cx| {
3500 workspace
3501 .go_forward(pane, window, cx)
3502 .detach_and_log_err(cx)
3503 })
3504 })
3505 }
3506 }),
3507 )
3508 }
3509}
3510
3511impl ItemNavHistory {
3512 pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut App) {
3513 if self
3514 .item
3515 .upgrade()
3516 .is_some_and(|item| item.include_in_nav_history())
3517 {
3518 self.history
3519 .push(data, self.item.clone(), self.is_preview, cx);
3520 }
3521 }
3522
3523 pub fn pop_backward(&mut self, cx: &mut App) -> Option<NavigationEntry> {
3524 self.history.pop(NavigationMode::GoingBack, cx)
3525 }
3526
3527 pub fn pop_forward(&mut self, cx: &mut App) -> Option<NavigationEntry> {
3528 self.history.pop(NavigationMode::GoingForward, cx)
3529 }
3530}
3531
3532impl NavHistory {
3533 pub fn for_each_entry(
3534 &self,
3535 cx: &App,
3536 mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
3537 ) {
3538 let borrowed_history = self.0.lock();
3539 borrowed_history
3540 .forward_stack
3541 .iter()
3542 .chain(borrowed_history.backward_stack.iter())
3543 .chain(borrowed_history.closed_stack.iter())
3544 .for_each(|entry| {
3545 if let Some(project_and_abs_path) =
3546 borrowed_history.paths_by_item.get(&entry.item.id())
3547 {
3548 f(entry, project_and_abs_path.clone());
3549 } else if let Some(item) = entry.item.upgrade() {
3550 if let Some(path) = item.project_path(cx) {
3551 f(entry, (path, None));
3552 }
3553 }
3554 })
3555 }
3556
3557 pub fn set_mode(&mut self, mode: NavigationMode) {
3558 self.0.lock().mode = mode;
3559 }
3560
3561 pub fn mode(&self) -> NavigationMode {
3562 self.0.lock().mode
3563 }
3564
3565 pub fn disable(&mut self) {
3566 self.0.lock().mode = NavigationMode::Disabled;
3567 }
3568
3569 pub fn enable(&mut self) {
3570 self.0.lock().mode = NavigationMode::Normal;
3571 }
3572
3573 pub fn pop(&mut self, mode: NavigationMode, cx: &mut App) -> Option<NavigationEntry> {
3574 let mut state = self.0.lock();
3575 let entry = match mode {
3576 NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
3577 return None;
3578 }
3579 NavigationMode::GoingBack => &mut state.backward_stack,
3580 NavigationMode::GoingForward => &mut state.forward_stack,
3581 NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
3582 }
3583 .pop_back();
3584 if entry.is_some() {
3585 state.did_update(cx);
3586 }
3587 entry
3588 }
3589
3590 pub fn push<D: 'static + Send + Any>(
3591 &mut self,
3592 data: Option<D>,
3593 item: Arc<dyn WeakItemHandle>,
3594 is_preview: bool,
3595 cx: &mut App,
3596 ) {
3597 let state = &mut *self.0.lock();
3598 match state.mode {
3599 NavigationMode::Disabled => {}
3600 NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
3601 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3602 state.backward_stack.pop_front();
3603 }
3604 state.backward_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 state.forward_stack.clear();
3611 }
3612 NavigationMode::GoingBack => {
3613 if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3614 state.forward_stack.pop_front();
3615 }
3616 state.forward_stack.push_back(NavigationEntry {
3617 item,
3618 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3619 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3620 is_preview,
3621 });
3622 }
3623 NavigationMode::GoingForward => {
3624 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3625 state.backward_stack.pop_front();
3626 }
3627 state.backward_stack.push_back(NavigationEntry {
3628 item,
3629 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3630 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3631 is_preview,
3632 });
3633 }
3634 NavigationMode::ClosingItem => {
3635 if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
3636 state.closed_stack.pop_front();
3637 }
3638 state.closed_stack.push_back(NavigationEntry {
3639 item,
3640 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
3641 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
3642 is_preview,
3643 });
3644 }
3645 }
3646 state.did_update(cx);
3647 }
3648
3649 pub fn remove_item(&mut self, item_id: EntityId) {
3650 let mut state = self.0.lock();
3651 state.paths_by_item.remove(&item_id);
3652 state
3653 .backward_stack
3654 .retain(|entry| entry.item.id() != item_id);
3655 state
3656 .forward_stack
3657 .retain(|entry| entry.item.id() != item_id);
3658 state
3659 .closed_stack
3660 .retain(|entry| entry.item.id() != item_id);
3661 }
3662
3663 pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
3664 self.0.lock().paths_by_item.get(&item_id).cloned()
3665 }
3666}
3667
3668impl NavHistoryState {
3669 pub fn did_update(&self, cx: &mut App) {
3670 if let Some(pane) = self.pane.upgrade() {
3671 cx.defer(move |cx| {
3672 pane.update(cx, |pane, cx| pane.history_updated(cx));
3673 });
3674 }
3675 }
3676}
3677
3678fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
3679 let path = buffer_path
3680 .as_ref()
3681 .and_then(|p| {
3682 p.path
3683 .to_str()
3684 .and_then(|s| if s.is_empty() { None } else { Some(s) })
3685 })
3686 .unwrap_or("This buffer");
3687 let path = truncate_and_remove_front(path, 80);
3688 format!("{path} contains unsaved edits. Do you want to save it?")
3689}
3690
3691pub fn tab_details(items: &[Box<dyn ItemHandle>], _window: &Window, cx: &App) -> Vec<usize> {
3692 let mut tab_details = items.iter().map(|_| 0).collect::<Vec<_>>();
3693 let mut tab_descriptions = HashMap::default();
3694 let mut done = false;
3695 while !done {
3696 done = true;
3697
3698 // Store item indices by their tab description.
3699 for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() {
3700 let description = item.tab_content_text(*detail, cx);
3701 if *detail == 0 || description != item.tab_content_text(detail - 1, cx) {
3702 tab_descriptions
3703 .entry(description)
3704 .or_insert(Vec::new())
3705 .push(ix);
3706 }
3707 }
3708
3709 // If two or more items have the same tab description, increase their level
3710 // of detail and try again.
3711 for (_, item_ixs) in tab_descriptions.drain() {
3712 if item_ixs.len() > 1 {
3713 done = false;
3714 for ix in item_ixs {
3715 tab_details[ix] += 1;
3716 }
3717 }
3718 }
3719 }
3720
3721 tab_details
3722}
3723
3724pub fn render_item_indicator(item: Box<dyn ItemHandle>, cx: &App) -> Option<Indicator> {
3725 maybe!({
3726 let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
3727 (true, _) => Color::Warning,
3728 (_, true) => Color::Accent,
3729 (false, false) => return None,
3730 };
3731
3732 Some(Indicator::dot().color(indicator_color))
3733 })
3734}
3735
3736impl Render for DraggedTab {
3737 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3738 let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
3739 let label = self.item.tab_content(
3740 TabContentParams {
3741 detail: Some(self.detail),
3742 selected: false,
3743 preview: false,
3744 deemphasized: false,
3745 },
3746 window,
3747 cx,
3748 );
3749 Tab::new("")
3750 .toggle_state(self.is_active)
3751 .child(label)
3752 .render(window, cx)
3753 .font(ui_font)
3754 }
3755}
3756
3757#[cfg(test)]
3758mod tests {
3759 use std::num::NonZero;
3760
3761 use super::*;
3762 use crate::item::test::{TestItem, TestProjectItem};
3763 use gpui::{TestAppContext, VisualTestContext};
3764 use project::FakeFs;
3765 use settings::SettingsStore;
3766 use theme::LoadThemes;
3767 use util::TryFutureExt;
3768
3769 #[gpui::test]
3770 async fn test_add_item_capped_to_max_tabs(cx: &mut TestAppContext) {
3771 init_test(cx);
3772 let fs = FakeFs::new(cx.executor());
3773
3774 let project = Project::test(fs, None, cx).await;
3775 let (workspace, cx) =
3776 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3777 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3778
3779 for i in 0..7 {
3780 add_labeled_item(&pane, format!("{}", i).as_str(), false, cx);
3781 }
3782 set_max_tabs(cx, Some(5));
3783 add_labeled_item(&pane, "7", false, cx);
3784 // Remove items to respect the max tab cap.
3785 assert_item_labels(&pane, ["3", "4", "5", "6", "7*"], cx);
3786 pane.update_in(cx, |pane, window, cx| {
3787 pane.activate_item(0, false, false, window, cx);
3788 });
3789 add_labeled_item(&pane, "X", false, cx);
3790 // Respect activation order.
3791 assert_item_labels(&pane, ["3", "X*", "5", "6", "7"], cx);
3792
3793 for i in 0..7 {
3794 add_labeled_item(&pane, format!("D{}", i).as_str(), true, cx);
3795 }
3796 // Keeps dirty items, even over max tab cap.
3797 assert_item_labels(
3798 &pane,
3799 ["D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6*^"],
3800 cx,
3801 );
3802
3803 set_max_tabs(cx, None);
3804 for i in 0..7 {
3805 add_labeled_item(&pane, format!("N{}", i).as_str(), false, cx);
3806 }
3807 // No cap when max tabs is None.
3808 assert_item_labels(
3809 &pane,
3810 [
3811 "D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6^", "N0", "N1", "N2", "N3", "N4",
3812 "N5", "N6*",
3813 ],
3814 cx,
3815 );
3816 }
3817
3818 #[gpui::test]
3819 async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
3820 init_test(cx);
3821 let fs = FakeFs::new(cx.executor());
3822
3823 let project = Project::test(fs, None, cx).await;
3824 let (workspace, cx) =
3825 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3826 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3827
3828 // 1. Add with a destination index
3829 // a. Add before the active item
3830 set_labeled_items(&pane, ["A", "B*", "C"], cx);
3831 pane.update_in(cx, |pane, window, cx| {
3832 pane.add_item(
3833 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3834 false,
3835 false,
3836 Some(0),
3837 window,
3838 cx,
3839 );
3840 });
3841 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3842
3843 // b. Add after the active item
3844 set_labeled_items(&pane, ["A", "B*", "C"], cx);
3845 pane.update_in(cx, |pane, window, cx| {
3846 pane.add_item(
3847 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3848 false,
3849 false,
3850 Some(2),
3851 window,
3852 cx,
3853 );
3854 });
3855 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3856
3857 // c. Add at the end of the item list (including off the length)
3858 set_labeled_items(&pane, ["A", "B*", "C"], cx);
3859 pane.update_in(cx, |pane, window, cx| {
3860 pane.add_item(
3861 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3862 false,
3863 false,
3864 Some(5),
3865 window,
3866 cx,
3867 );
3868 });
3869 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3870
3871 // 2. Add without a destination index
3872 // a. Add with active item at the start of the item list
3873 set_labeled_items(&pane, ["A*", "B", "C"], cx);
3874 pane.update_in(cx, |pane, window, cx| {
3875 pane.add_item(
3876 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3877 false,
3878 false,
3879 None,
3880 window,
3881 cx,
3882 );
3883 });
3884 set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
3885
3886 // b. Add with active item at the end of the item list
3887 set_labeled_items(&pane, ["A", "B", "C*"], cx);
3888 pane.update_in(cx, |pane, window, cx| {
3889 pane.add_item(
3890 Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))),
3891 false,
3892 false,
3893 None,
3894 window,
3895 cx,
3896 );
3897 });
3898 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
3899 }
3900
3901 #[gpui::test]
3902 async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
3903 init_test(cx);
3904 let fs = FakeFs::new(cx.executor());
3905
3906 let project = Project::test(fs, None, cx).await;
3907 let (workspace, cx) =
3908 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3909 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3910
3911 // 1. Add with a destination index
3912 // 1a. Add before the active item
3913 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3914 pane.update_in(cx, |pane, window, cx| {
3915 pane.add_item(d, false, false, Some(0), window, cx);
3916 });
3917 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
3918
3919 // 1b. Add after the active item
3920 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3921 pane.update_in(cx, |pane, window, cx| {
3922 pane.add_item(d, false, false, Some(2), window, cx);
3923 });
3924 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
3925
3926 // 1c. Add at the end of the item list (including off the length)
3927 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
3928 pane.update_in(cx, |pane, window, cx| {
3929 pane.add_item(a, false, false, Some(5), window, cx);
3930 });
3931 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3932
3933 // 1d. Add same item to active index
3934 let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3935 pane.update_in(cx, |pane, window, cx| {
3936 pane.add_item(b, false, false, Some(1), window, cx);
3937 });
3938 assert_item_labels(&pane, ["A", "B*", "C"], cx);
3939
3940 // 1e. Add item to index after same item in last position
3941 let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
3942 pane.update_in(cx, |pane, window, cx| {
3943 pane.add_item(c, false, false, Some(2), window, cx);
3944 });
3945 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3946
3947 // 2. Add without a destination index
3948 // 2a. Add with active item at the start of the item list
3949 let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
3950 pane.update_in(cx, |pane, window, cx| {
3951 pane.add_item(d, false, false, None, window, cx);
3952 });
3953 assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
3954
3955 // 2b. Add with active item at the end of the item list
3956 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
3957 pane.update_in(cx, |pane, window, cx| {
3958 pane.add_item(a, false, false, None, window, cx);
3959 });
3960 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
3961
3962 // 2c. Add active item to active item at end of list
3963 let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
3964 pane.update_in(cx, |pane, window, cx| {
3965 pane.add_item(c, false, false, None, window, cx);
3966 });
3967 assert_item_labels(&pane, ["A", "B", "C*"], cx);
3968
3969 // 2d. Add active item to active item at start of list
3970 let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
3971 pane.update_in(cx, |pane, window, cx| {
3972 pane.add_item(a, false, false, None, window, cx);
3973 });
3974 assert_item_labels(&pane, ["A*", "B", "C"], cx);
3975 }
3976
3977 #[gpui::test]
3978 async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
3979 init_test(cx);
3980 let fs = FakeFs::new(cx.executor());
3981
3982 let project = Project::test(fs, None, cx).await;
3983 let (workspace, cx) =
3984 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3985 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3986
3987 // singleton view
3988 pane.update_in(cx, |pane, window, cx| {
3989 pane.add_item(
3990 Box::new(cx.new(|cx| {
3991 TestItem::new(cx)
3992 .with_singleton(true)
3993 .with_label("buffer 1")
3994 .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
3995 })),
3996 false,
3997 false,
3998 None,
3999 window,
4000 cx,
4001 );
4002 });
4003 assert_item_labels(&pane, ["buffer 1*"], cx);
4004
4005 // new singleton view with the same project entry
4006 pane.update_in(cx, |pane, window, cx| {
4007 pane.add_item(
4008 Box::new(cx.new(|cx| {
4009 TestItem::new(cx)
4010 .with_singleton(true)
4011 .with_label("buffer 1")
4012 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4013 })),
4014 false,
4015 false,
4016 None,
4017 window,
4018 cx,
4019 );
4020 });
4021 assert_item_labels(&pane, ["buffer 1*"], cx);
4022
4023 // new singleton view with different project entry
4024 pane.update_in(cx, |pane, window, cx| {
4025 pane.add_item(
4026 Box::new(cx.new(|cx| {
4027 TestItem::new(cx)
4028 .with_singleton(true)
4029 .with_label("buffer 2")
4030 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
4031 })),
4032 false,
4033 false,
4034 None,
4035 window,
4036 cx,
4037 );
4038 });
4039 assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
4040
4041 // new multibuffer view with the same project entry
4042 pane.update_in(cx, |pane, window, cx| {
4043 pane.add_item(
4044 Box::new(cx.new(|cx| {
4045 TestItem::new(cx)
4046 .with_singleton(false)
4047 .with_label("multibuffer 1")
4048 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4049 })),
4050 false,
4051 false,
4052 None,
4053 window,
4054 cx,
4055 );
4056 });
4057 assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
4058
4059 // another multibuffer view with the same project entry
4060 pane.update_in(cx, |pane, window, cx| {
4061 pane.add_item(
4062 Box::new(cx.new(|cx| {
4063 TestItem::new(cx)
4064 .with_singleton(false)
4065 .with_label("multibuffer 1b")
4066 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4067 })),
4068 false,
4069 false,
4070 None,
4071 window,
4072 cx,
4073 );
4074 });
4075 assert_item_labels(
4076 &pane,
4077 ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
4078 cx,
4079 );
4080 }
4081
4082 #[gpui::test]
4083 async fn test_remove_item_ordering_history(cx: &mut TestAppContext) {
4084 init_test(cx);
4085 let fs = FakeFs::new(cx.executor());
4086
4087 let project = Project::test(fs, None, cx).await;
4088 let (workspace, cx) =
4089 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4090 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4091
4092 add_labeled_item(&pane, "A", false, cx);
4093 add_labeled_item(&pane, "B", false, cx);
4094 add_labeled_item(&pane, "C", false, cx);
4095 add_labeled_item(&pane, "D", false, cx);
4096 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4097
4098 pane.update_in(cx, |pane, window, cx| {
4099 pane.activate_item(1, false, false, window, cx)
4100 });
4101 add_labeled_item(&pane, "1", false, cx);
4102 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
4103
4104 pane.update_in(cx, |pane, window, cx| {
4105 pane.close_active_item(
4106 &CloseActiveItem {
4107 save_intent: None,
4108 close_pinned: false,
4109 },
4110 window,
4111 cx,
4112 )
4113 })
4114 .await
4115 .unwrap();
4116 assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
4117
4118 pane.update_in(cx, |pane, window, cx| {
4119 pane.activate_item(3, false, false, window, cx)
4120 });
4121 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4122
4123 pane.update_in(cx, |pane, window, cx| {
4124 pane.close_active_item(
4125 &CloseActiveItem {
4126 save_intent: None,
4127 close_pinned: false,
4128 },
4129 window,
4130 cx,
4131 )
4132 })
4133 .await
4134 .unwrap();
4135 assert_item_labels(&pane, ["A", "B*", "C"], cx);
4136
4137 pane.update_in(cx, |pane, window, cx| {
4138 pane.close_active_item(
4139 &CloseActiveItem {
4140 save_intent: None,
4141 close_pinned: false,
4142 },
4143 window,
4144 cx,
4145 )
4146 })
4147 .await
4148 .unwrap();
4149 assert_item_labels(&pane, ["A", "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 .await
4162 .unwrap();
4163 assert_item_labels(&pane, ["A*"], cx);
4164 }
4165
4166 #[gpui::test]
4167 async fn test_remove_item_ordering_neighbour(cx: &mut TestAppContext) {
4168 init_test(cx);
4169 cx.update_global::<SettingsStore, ()>(|s, cx| {
4170 s.update_user_settings::<ItemSettings>(cx, |s| {
4171 s.activate_on_close = Some(ActivateOnClose::Neighbour);
4172 });
4173 });
4174 let fs = FakeFs::new(cx.executor());
4175
4176 let project = Project::test(fs, None, cx).await;
4177 let (workspace, cx) =
4178 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4179 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4180
4181 add_labeled_item(&pane, "A", false, cx);
4182 add_labeled_item(&pane, "B", false, cx);
4183 add_labeled_item(&pane, "C", false, cx);
4184 add_labeled_item(&pane, "D", false, cx);
4185 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4186
4187 pane.update_in(cx, |pane, window, cx| {
4188 pane.activate_item(1, false, false, window, cx)
4189 });
4190 add_labeled_item(&pane, "1", false, cx);
4191 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
4192
4193 pane.update_in(cx, |pane, window, cx| {
4194 pane.close_active_item(
4195 &CloseActiveItem {
4196 save_intent: None,
4197 close_pinned: false,
4198 },
4199 window,
4200 cx,
4201 )
4202 })
4203 .await
4204 .unwrap();
4205 assert_item_labels(&pane, ["A", "B", "C*", "D"], cx);
4206
4207 pane.update_in(cx, |pane, window, cx| {
4208 pane.activate_item(3, false, false, window, cx)
4209 });
4210 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4211
4212 pane.update_in(cx, |pane, window, cx| {
4213 pane.close_active_item(
4214 &CloseActiveItem {
4215 save_intent: None,
4216 close_pinned: false,
4217 },
4218 window,
4219 cx,
4220 )
4221 })
4222 .await
4223 .unwrap();
4224 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4225
4226 pane.update_in(cx, |pane, window, cx| {
4227 pane.close_active_item(
4228 &CloseActiveItem {
4229 save_intent: None,
4230 close_pinned: false,
4231 },
4232 window,
4233 cx,
4234 )
4235 })
4236 .await
4237 .unwrap();
4238 assert_item_labels(&pane, ["A", "B*"], cx);
4239
4240 pane.update_in(cx, |pane, window, cx| {
4241 pane.close_active_item(
4242 &CloseActiveItem {
4243 save_intent: None,
4244 close_pinned: false,
4245 },
4246 window,
4247 cx,
4248 )
4249 })
4250 .await
4251 .unwrap();
4252 assert_item_labels(&pane, ["A*"], cx);
4253 }
4254
4255 #[gpui::test]
4256 async fn test_remove_item_ordering_left_neighbour(cx: &mut TestAppContext) {
4257 init_test(cx);
4258 cx.update_global::<SettingsStore, ()>(|s, cx| {
4259 s.update_user_settings::<ItemSettings>(cx, |s| {
4260 s.activate_on_close = Some(ActivateOnClose::LeftNeighbour);
4261 });
4262 });
4263 let fs = FakeFs::new(cx.executor());
4264
4265 let project = Project::test(fs, None, cx).await;
4266 let (workspace, cx) =
4267 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4268 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4269
4270 add_labeled_item(&pane, "A", false, cx);
4271 add_labeled_item(&pane, "B", false, cx);
4272 add_labeled_item(&pane, "C", false, cx);
4273 add_labeled_item(&pane, "D", false, cx);
4274 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4275
4276 pane.update_in(cx, |pane, window, cx| {
4277 pane.activate_item(1, false, false, window, cx)
4278 });
4279 add_labeled_item(&pane, "1", false, cx);
4280 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
4281
4282 pane.update_in(cx, |pane, window, cx| {
4283 pane.close_active_item(
4284 &CloseActiveItem {
4285 save_intent: None,
4286 close_pinned: false,
4287 },
4288 window,
4289 cx,
4290 )
4291 })
4292 .await
4293 .unwrap();
4294 assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
4295
4296 pane.update_in(cx, |pane, window, cx| {
4297 pane.activate_item(3, false, false, window, cx)
4298 });
4299 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
4300
4301 pane.update_in(cx, |pane, window, cx| {
4302 pane.close_active_item(
4303 &CloseActiveItem {
4304 save_intent: None,
4305 close_pinned: false,
4306 },
4307 window,
4308 cx,
4309 )
4310 })
4311 .await
4312 .unwrap();
4313 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4314
4315 pane.update_in(cx, |pane, window, cx| {
4316 pane.activate_item(0, false, false, window, cx)
4317 });
4318 assert_item_labels(&pane, ["A*", "B", "C"], cx);
4319
4320 pane.update_in(cx, |pane, window, cx| {
4321 pane.close_active_item(
4322 &CloseActiveItem {
4323 save_intent: None,
4324 close_pinned: false,
4325 },
4326 window,
4327 cx,
4328 )
4329 })
4330 .await
4331 .unwrap();
4332 assert_item_labels(&pane, ["B*", "C"], cx);
4333
4334 pane.update_in(cx, |pane, window, cx| {
4335 pane.close_active_item(
4336 &CloseActiveItem {
4337 save_intent: None,
4338 close_pinned: false,
4339 },
4340 window,
4341 cx,
4342 )
4343 })
4344 .await
4345 .unwrap();
4346 assert_item_labels(&pane, ["C*"], cx);
4347 }
4348
4349 #[gpui::test]
4350 async fn test_close_inactive_items(cx: &mut TestAppContext) {
4351 init_test(cx);
4352 let fs = FakeFs::new(cx.executor());
4353
4354 let project = Project::test(fs, None, cx).await;
4355 let (workspace, cx) =
4356 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4357 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4358
4359 let item_a = add_labeled_item(&pane, "A", false, cx);
4360 pane.update_in(cx, |pane, window, cx| {
4361 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4362 pane.pin_tab_at(ix, window, cx);
4363 });
4364 assert_item_labels(&pane, ["A*!"], cx);
4365
4366 let item_b = add_labeled_item(&pane, "B", false, cx);
4367 pane.update_in(cx, |pane, window, cx| {
4368 let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
4369 pane.pin_tab_at(ix, window, cx);
4370 });
4371 assert_item_labels(&pane, ["A!", "B*!"], cx);
4372
4373 add_labeled_item(&pane, "C", false, cx);
4374 assert_item_labels(&pane, ["A!", "B!", "C*"], cx);
4375
4376 add_labeled_item(&pane, "D", false, cx);
4377 add_labeled_item(&pane, "E", false, cx);
4378 assert_item_labels(&pane, ["A!", "B!", "C", "D", "E*"], cx);
4379
4380 pane.update_in(cx, |pane, window, cx| {
4381 pane.close_inactive_items(
4382 &CloseInactiveItems {
4383 save_intent: None,
4384 close_pinned: false,
4385 },
4386 window,
4387 cx,
4388 )
4389 })
4390 .await
4391 .unwrap();
4392 assert_item_labels(&pane, ["A!", "B!", "E*"], cx);
4393 }
4394
4395 #[gpui::test]
4396 async fn test_close_clean_items(cx: &mut TestAppContext) {
4397 init_test(cx);
4398 let fs = FakeFs::new(cx.executor());
4399
4400 let project = Project::test(fs, None, cx).await;
4401 let (workspace, cx) =
4402 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4403 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4404
4405 add_labeled_item(&pane, "A", true, cx);
4406 add_labeled_item(&pane, "B", false, cx);
4407 add_labeled_item(&pane, "C", true, cx);
4408 add_labeled_item(&pane, "D", false, cx);
4409 add_labeled_item(&pane, "E", false, cx);
4410 assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
4411
4412 pane.update_in(cx, |pane, window, cx| {
4413 pane.close_clean_items(
4414 &CloseCleanItems {
4415 close_pinned: false,
4416 },
4417 window,
4418 cx,
4419 )
4420 })
4421 .await
4422 .unwrap();
4423 assert_item_labels(&pane, ["A^", "C*^"], cx);
4424 }
4425
4426 #[gpui::test]
4427 async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
4428 init_test(cx);
4429 let fs = FakeFs::new(cx.executor());
4430
4431 let project = Project::test(fs, None, cx).await;
4432 let (workspace, cx) =
4433 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4434 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4435
4436 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
4437
4438 pane.update_in(cx, |pane, window, cx| {
4439 pane.close_items_to_the_left(
4440 &CloseItemsToTheLeft {
4441 close_pinned: false,
4442 },
4443 window,
4444 cx,
4445 )
4446 })
4447 .await
4448 .unwrap();
4449 assert_item_labels(&pane, ["C*", "D", "E"], cx);
4450 }
4451
4452 #[gpui::test]
4453 async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
4454 init_test(cx);
4455 let fs = FakeFs::new(cx.executor());
4456
4457 let project = Project::test(fs, None, cx).await;
4458 let (workspace, cx) =
4459 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4460 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4461
4462 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
4463
4464 pane.update_in(cx, |pane, window, cx| {
4465 pane.close_items_to_the_right(
4466 &CloseItemsToTheRight {
4467 close_pinned: false,
4468 },
4469 window,
4470 cx,
4471 )
4472 })
4473 .await
4474 .unwrap();
4475 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4476 }
4477
4478 #[gpui::test]
4479 async fn test_close_all_items(cx: &mut TestAppContext) {
4480 init_test(cx);
4481 let fs = FakeFs::new(cx.executor());
4482
4483 let project = Project::test(fs, None, cx).await;
4484 let (workspace, cx) =
4485 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4486 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4487
4488 let item_a = add_labeled_item(&pane, "A", false, cx);
4489 add_labeled_item(&pane, "B", false, cx);
4490 add_labeled_item(&pane, "C", false, cx);
4491 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4492
4493 pane.update_in(cx, |pane, window, cx| {
4494 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4495 pane.pin_tab_at(ix, window, cx);
4496 pane.close_all_items(
4497 &CloseAllItems {
4498 save_intent: None,
4499 close_pinned: false,
4500 },
4501 window,
4502 cx,
4503 )
4504 })
4505 .await
4506 .unwrap();
4507 assert_item_labels(&pane, ["A*!"], cx);
4508
4509 pane.update_in(cx, |pane, window, cx| {
4510 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4511 pane.unpin_tab_at(ix, window, cx);
4512 pane.close_all_items(
4513 &CloseAllItems {
4514 save_intent: None,
4515 close_pinned: false,
4516 },
4517 window,
4518 cx,
4519 )
4520 })
4521 .await
4522 .unwrap();
4523
4524 assert_item_labels(&pane, [], cx);
4525
4526 add_labeled_item(&pane, "A", true, cx).update(cx, |item, cx| {
4527 item.project_items
4528 .push(TestProjectItem::new_dirty(1, "A.txt", cx))
4529 });
4530 add_labeled_item(&pane, "B", true, cx).update(cx, |item, cx| {
4531 item.project_items
4532 .push(TestProjectItem::new_dirty(2, "B.txt", cx))
4533 });
4534 add_labeled_item(&pane, "C", true, cx).update(cx, |item, cx| {
4535 item.project_items
4536 .push(TestProjectItem::new_dirty(3, "C.txt", cx))
4537 });
4538 assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
4539
4540 let save = pane.update_in(cx, |pane, window, cx| {
4541 pane.close_all_items(
4542 &CloseAllItems {
4543 save_intent: None,
4544 close_pinned: false,
4545 },
4546 window,
4547 cx,
4548 )
4549 });
4550
4551 cx.executor().run_until_parked();
4552 cx.simulate_prompt_answer("Save all");
4553 save.await.unwrap();
4554 assert_item_labels(&pane, [], cx);
4555
4556 add_labeled_item(&pane, "A", true, cx);
4557 add_labeled_item(&pane, "B", true, cx);
4558 add_labeled_item(&pane, "C", true, cx);
4559 assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
4560 let save = pane.update_in(cx, |pane, window, cx| {
4561 pane.close_all_items(
4562 &CloseAllItems {
4563 save_intent: None,
4564 close_pinned: false,
4565 },
4566 window,
4567 cx,
4568 )
4569 });
4570
4571 cx.executor().run_until_parked();
4572 cx.simulate_prompt_answer("Discard all");
4573 save.await.unwrap();
4574 assert_item_labels(&pane, [], cx);
4575 }
4576
4577 #[gpui::test]
4578 async fn test_close_with_save_intent(cx: &mut TestAppContext) {
4579 init_test(cx);
4580 let fs = FakeFs::new(cx.executor());
4581
4582 let project = Project::test(fs, None, cx).await;
4583 let (workspace, cx) =
4584 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4585 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4586
4587 let a = cx.update(|_, cx| TestProjectItem::new_dirty(1, "A.txt", cx));
4588 let b = cx.update(|_, cx| TestProjectItem::new_dirty(1, "B.txt", cx));
4589 let c = cx.update(|_, cx| TestProjectItem::new_dirty(1, "C.txt", cx));
4590
4591 add_labeled_item(&pane, "AB", true, cx).update(cx, |item, _| {
4592 item.project_items.push(a.clone());
4593 item.project_items.push(b.clone());
4594 });
4595 add_labeled_item(&pane, "C", true, cx)
4596 .update(cx, |item, _| item.project_items.push(c.clone()));
4597 assert_item_labels(&pane, ["AB^", "C*^"], cx);
4598
4599 pane.update_in(cx, |pane, window, cx| {
4600 pane.close_all_items(
4601 &CloseAllItems {
4602 save_intent: Some(SaveIntent::Save),
4603 close_pinned: false,
4604 },
4605 window,
4606 cx,
4607 )
4608 })
4609 .await
4610 .unwrap();
4611
4612 assert_item_labels(&pane, [], cx);
4613 cx.update(|_, cx| {
4614 assert!(!a.read(cx).is_dirty);
4615 assert!(!b.read(cx).is_dirty);
4616 assert!(!c.read(cx).is_dirty);
4617 });
4618 }
4619
4620 #[gpui::test]
4621 async fn test_close_all_items_including_pinned(cx: &mut TestAppContext) {
4622 init_test(cx);
4623 let fs = FakeFs::new(cx.executor());
4624
4625 let project = Project::test(fs, None, cx).await;
4626 let (workspace, cx) =
4627 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4628 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4629
4630 let item_a = add_labeled_item(&pane, "A", false, cx);
4631 add_labeled_item(&pane, "B", false, cx);
4632 add_labeled_item(&pane, "C", false, cx);
4633 assert_item_labels(&pane, ["A", "B", "C*"], cx);
4634
4635 pane.update_in(cx, |pane, window, cx| {
4636 let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
4637 pane.pin_tab_at(ix, window, cx);
4638 pane.close_all_items(
4639 &CloseAllItems {
4640 save_intent: None,
4641 close_pinned: true,
4642 },
4643 window,
4644 cx,
4645 )
4646 })
4647 .await
4648 .unwrap();
4649 assert_item_labels(&pane, [], cx);
4650 }
4651
4652 #[gpui::test]
4653 async fn test_close_pinned_tab_with_non_pinned_in_same_pane(cx: &mut TestAppContext) {
4654 init_test(cx);
4655 let fs = FakeFs::new(cx.executor());
4656 let project = Project::test(fs, None, cx).await;
4657 let (workspace, cx) =
4658 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4659
4660 // Non-pinned tabs in same pane
4661 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4662 add_labeled_item(&pane, "A", false, cx);
4663 add_labeled_item(&pane, "B", false, cx);
4664 add_labeled_item(&pane, "C", false, cx);
4665 pane.update_in(cx, |pane, window, cx| {
4666 pane.pin_tab_at(0, window, cx);
4667 });
4668 set_labeled_items(&pane, ["A*", "B", "C"], cx);
4669 pane.update_in(cx, |pane, window, cx| {
4670 pane.close_active_item(
4671 &CloseActiveItem {
4672 save_intent: None,
4673 close_pinned: false,
4674 },
4675 window,
4676 cx,
4677 )
4678 .unwrap();
4679 });
4680 // Non-pinned tab should be active
4681 assert_item_labels(&pane, ["A!", "B*", "C"], cx);
4682 }
4683
4684 #[gpui::test]
4685 async fn test_close_pinned_tab_with_non_pinned_in_different_pane(cx: &mut TestAppContext) {
4686 init_test(cx);
4687 let fs = FakeFs::new(cx.executor());
4688 let project = Project::test(fs, None, cx).await;
4689 let (workspace, cx) =
4690 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4691
4692 // No non-pinned tabs in same pane, non-pinned tabs in another pane
4693 let pane1 = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4694 let pane2 = workspace.update_in(cx, |workspace, window, cx| {
4695 workspace.split_pane(pane1.clone(), SplitDirection::Right, window, cx)
4696 });
4697 add_labeled_item(&pane1, "A", false, cx);
4698 pane1.update_in(cx, |pane, window, cx| {
4699 pane.pin_tab_at(0, window, cx);
4700 });
4701 set_labeled_items(&pane1, ["A*"], cx);
4702 add_labeled_item(&pane2, "B", false, cx);
4703 set_labeled_items(&pane2, ["B"], cx);
4704 pane1.update_in(cx, |pane, window, cx| {
4705 pane.close_active_item(
4706 &CloseActiveItem {
4707 save_intent: None,
4708 close_pinned: false,
4709 },
4710 window,
4711 cx,
4712 )
4713 .unwrap();
4714 });
4715 // Non-pinned tab of other pane should be active
4716 assert_item_labels(&pane2, ["B*"], cx);
4717 }
4718
4719 #[gpui::test]
4720 async fn ensure_item_closing_actions_do_not_panic_when_no_items_exist(cx: &mut TestAppContext) {
4721 init_test(cx);
4722 let fs = FakeFs::new(cx.executor());
4723 let project = Project::test(fs, None, cx).await;
4724 let (workspace, cx) =
4725 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
4726
4727 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4728 assert_item_labels(&pane, [], cx);
4729
4730 pane.update_in(cx, |pane, window, cx| {
4731 pane.close_active_item(
4732 &CloseActiveItem {
4733 save_intent: None,
4734 close_pinned: false,
4735 },
4736 window,
4737 cx,
4738 )
4739 })
4740 .await
4741 .unwrap();
4742
4743 pane.update_in(cx, |pane, window, cx| {
4744 pane.close_inactive_items(
4745 &CloseInactiveItems {
4746 save_intent: None,
4747 close_pinned: false,
4748 },
4749 window,
4750 cx,
4751 )
4752 })
4753 .await
4754 .unwrap();
4755
4756 pane.update_in(cx, |pane, window, cx| {
4757 pane.close_all_items(
4758 &CloseAllItems {
4759 save_intent: None,
4760 close_pinned: false,
4761 },
4762 window,
4763 cx,
4764 )
4765 })
4766 .await
4767 .unwrap();
4768
4769 pane.update_in(cx, |pane, window, cx| {
4770 pane.close_clean_items(
4771 &CloseCleanItems {
4772 close_pinned: false,
4773 },
4774 window,
4775 cx,
4776 )
4777 })
4778 .await
4779 .unwrap();
4780
4781 pane.update_in(cx, |pane, window, cx| {
4782 pane.close_items_to_the_right(
4783 &CloseItemsToTheRight {
4784 close_pinned: false,
4785 },
4786 window,
4787 cx,
4788 )
4789 })
4790 .await
4791 .unwrap();
4792
4793 pane.update_in(cx, |pane, window, cx| {
4794 pane.close_items_to_the_left(
4795 &CloseItemsToTheLeft {
4796 close_pinned: false,
4797 },
4798 window,
4799 cx,
4800 )
4801 })
4802 .await
4803 .unwrap();
4804 }
4805
4806 fn init_test(cx: &mut TestAppContext) {
4807 cx.update(|cx| {
4808 let settings_store = SettingsStore::test(cx);
4809 cx.set_global(settings_store);
4810 theme::init(LoadThemes::JustBase, cx);
4811 crate::init_settings(cx);
4812 Project::init_settings(cx);
4813 });
4814 }
4815
4816 fn set_max_tabs(cx: &mut TestAppContext, value: Option<usize>) {
4817 cx.update_global(|store: &mut SettingsStore, cx| {
4818 store.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4819 settings.max_tabs = value.map(|v| NonZero::new(v).unwrap())
4820 });
4821 });
4822 }
4823
4824 fn add_labeled_item(
4825 pane: &Entity<Pane>,
4826 label: &str,
4827 is_dirty: bool,
4828 cx: &mut VisualTestContext,
4829 ) -> Box<Entity<TestItem>> {
4830 pane.update_in(cx, |pane, window, cx| {
4831 let labeled_item =
4832 Box::new(cx.new(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)));
4833 pane.add_item(labeled_item.clone(), false, false, None, window, cx);
4834 labeled_item
4835 })
4836 }
4837
4838 fn set_labeled_items<const COUNT: usize>(
4839 pane: &Entity<Pane>,
4840 labels: [&str; COUNT],
4841 cx: &mut VisualTestContext,
4842 ) -> [Box<Entity<TestItem>>; COUNT] {
4843 pane.update_in(cx, |pane, window, cx| {
4844 pane.items.clear();
4845 let mut active_item_index = 0;
4846
4847 let mut index = 0;
4848 let items = labels.map(|mut label| {
4849 if label.ends_with('*') {
4850 label = label.trim_end_matches('*');
4851 active_item_index = index;
4852 }
4853
4854 let labeled_item = Box::new(cx.new(|cx| TestItem::new(cx).with_label(label)));
4855 pane.add_item(labeled_item.clone(), false, false, None, window, cx);
4856 index += 1;
4857 labeled_item
4858 });
4859
4860 pane.activate_item(active_item_index, false, false, window, cx);
4861
4862 items
4863 })
4864 }
4865
4866 // Assert the item label, with the active item label suffixed with a '*'
4867 #[track_caller]
4868 fn assert_item_labels<const COUNT: usize>(
4869 pane: &Entity<Pane>,
4870 expected_states: [&str; COUNT],
4871 cx: &mut VisualTestContext,
4872 ) {
4873 let actual_states = pane.update(cx, |pane, cx| {
4874 pane.items
4875 .iter()
4876 .enumerate()
4877 .map(|(ix, item)| {
4878 let mut state = item
4879 .to_any()
4880 .downcast::<TestItem>()
4881 .unwrap()
4882 .read(cx)
4883 .label
4884 .clone();
4885 if ix == pane.active_item_index {
4886 state.push('*');
4887 }
4888 if item.is_dirty(cx) {
4889 state.push('^');
4890 }
4891 if pane.is_tab_pinned(ix) {
4892 state.push('!');
4893 }
4894 state
4895 })
4896 .collect::<Vec<_>>()
4897 });
4898 assert_eq!(
4899 actual_states, expected_states,
4900 "pane items do not match expectation"
4901 );
4902 }
4903}