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