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