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