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