1use crate::{
2 item::{ClosePosition, Item, ItemHandle, ItemSettings, WeakItemHandle},
3 toolbar::Toolbar,
4 workspace_settings::{AutosaveSetting, WorkspaceSettings},
5 NewCenterTerminal, NewFile, NewSearch, OpenVisible, SplitDirection, ToggleZoom, Workspace,
6};
7use anyhow::Result;
8use collections::{HashMap, HashSet, VecDeque};
9use futures::{stream::FuturesUnordered, StreamExt};
10use gpui::{
11 actions, impl_actions, overlay, prelude::*, Action, AnchorCorner, AnyElement, AppContext,
12 AsyncWindowContext, DismissEvent, Div, DragMoveEvent, EntityId, EventEmitter, ExternalPaths,
13 FocusHandle, FocusableView, Model, MouseButton, NavigationDirection, Pixels, Point,
14 PromptLevel, Render, ScrollHandle, Subscription, Task, View, ViewContext, VisualContext,
15 WeakView, WindowContext,
16};
17use parking_lot::Mutex;
18use project::{Project, ProjectEntryId, ProjectPath};
19use serde::Deserialize;
20use settings::Settings;
21use std::{
22 any::Any,
23 cmp, fmt, mem,
24 ops::ControlFlow,
25 path::{Path, PathBuf},
26 rc::Rc,
27 sync::{
28 atomic::{AtomicUsize, Ordering},
29 Arc,
30 },
31};
32use theme::ThemeSettings;
33
34use ui::{
35 prelude::*, right_click_menu, ButtonSize, Color, IconButton, IconButtonShape, IconName,
36 IconSize, Indicator, Label, Tab, TabBar, TabPosition, Tooltip,
37};
38use ui::{v_flex, ContextMenu};
39use util::{maybe, truncate_and_remove_front, ResultExt};
40
41#[derive(PartialEq, Clone, Copy, Deserialize, Debug)]
42#[serde(rename_all = "camelCase")]
43pub enum SaveIntent {
44 /// write all files (even if unchanged)
45 /// prompt before overwriting on-disk changes
46 Save,
47 /// write any files that have local changes
48 /// prompt before overwriting on-disk changes
49 SaveAll,
50 /// always prompt for a new path
51 SaveAs,
52 /// prompt "you have unsaved changes" before writing
53 Close,
54 /// write all dirty files, don't prompt on conflict
55 Overwrite,
56 /// skip all save-related behavior
57 Skip,
58}
59
60#[derive(Clone, Deserialize, PartialEq, Debug)]
61pub struct ActivateItem(pub usize);
62
63#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
64#[serde(rename_all = "camelCase")]
65pub struct CloseActiveItem {
66 pub save_intent: Option<SaveIntent>,
67}
68
69#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
70#[serde(rename_all = "camelCase")]
71pub struct CloseAllItems {
72 pub save_intent: Option<SaveIntent>,
73}
74
75#[derive(Clone, PartialEq, Debug, Deserialize)]
76#[serde(rename_all = "camelCase")]
77pub struct RevealInProjectPanel {
78 pub entry_id: u64,
79}
80
81impl_actions!(
82 pane,
83 [
84 CloseAllItems,
85 CloseActiveItem,
86 ActivateItem,
87 RevealInProjectPanel
88 ]
89);
90
91actions!(
92 pane,
93 [
94 ActivatePrevItem,
95 ActivateNextItem,
96 ActivateLastItem,
97 CloseInactiveItems,
98 CloseCleanItems,
99 CloseItemsToTheLeft,
100 CloseItemsToTheRight,
101 GoBack,
102 DeploySearch,
103 GoForward,
104 ReopenClosedItem,
105 SplitLeft,
106 SplitUp,
107 SplitRight,
108 SplitDown,
109 ]
110);
111
112const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
113
114pub enum Event {
115 AddItem { item: Box<dyn ItemHandle> },
116 ActivateItem { local: bool },
117 Remove,
118 RemoveItem { item_id: EntityId },
119 Split(SplitDirection),
120 ChangeItemTitle,
121 Focus,
122 ZoomIn,
123 ZoomOut,
124}
125
126impl fmt::Debug for Event {
127 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128 match self {
129 Event::AddItem { item } => f
130 .debug_struct("AddItem")
131 .field("item", &item.item_id())
132 .finish(),
133 Event::ActivateItem { local } => f
134 .debug_struct("ActivateItem")
135 .field("local", local)
136 .finish(),
137 Event::Remove => f.write_str("Remove"),
138 Event::RemoveItem { item_id } => f
139 .debug_struct("RemoveItem")
140 .field("item_id", item_id)
141 .finish(),
142 Event::Split(direction) => f
143 .debug_struct("Split")
144 .field("direction", direction)
145 .finish(),
146 Event::ChangeItemTitle => f.write_str("ChangeItemTitle"),
147 Event::Focus => f.write_str("Focus"),
148 Event::ZoomIn => f.write_str("ZoomIn"),
149 Event::ZoomOut => f.write_str("ZoomOut"),
150 }
151 }
152}
153
154pub struct Pane {
155 focus_handle: FocusHandle,
156 items: Vec<Box<dyn ItemHandle>>,
157 activation_history: Vec<EntityId>,
158 zoomed: bool,
159 was_focused: bool,
160 active_item_index: usize,
161 last_focused_view_by_item: HashMap<EntityId, FocusHandle>,
162 nav_history: NavHistory,
163 toolbar: View<Toolbar>,
164 new_item_menu: Option<View<ContextMenu>>,
165 split_item_menu: Option<View<ContextMenu>>,
166 // tab_context_menu: View<ContextMenu>,
167 workspace: WeakView<Workspace>,
168 project: Model<Project>,
169 drag_split_direction: Option<SplitDirection>,
170 can_drop_predicate: Option<Arc<dyn Fn(&dyn Any, &mut WindowContext) -> bool>>,
171 custom_drop_handle:
172 Option<Arc<dyn Fn(&mut Pane, &dyn Any, &mut ViewContext<Pane>) -> ControlFlow<(), ()>>>,
173 can_split: bool,
174 render_tab_bar_buttons: Rc<dyn Fn(&mut Pane, &mut ViewContext<Pane>) -> AnyElement>,
175 _subscriptions: Vec<Subscription>,
176 tab_bar_scroll_handle: ScrollHandle,
177 display_nav_history_buttons: bool,
178}
179
180pub struct ItemNavHistory {
181 history: NavHistory,
182 item: Arc<dyn WeakItemHandle>,
183}
184
185#[derive(Clone)]
186pub struct NavHistory(Arc<Mutex<NavHistoryState>>);
187
188struct NavHistoryState {
189 mode: NavigationMode,
190 backward_stack: VecDeque<NavigationEntry>,
191 forward_stack: VecDeque<NavigationEntry>,
192 closed_stack: VecDeque<NavigationEntry>,
193 paths_by_item: HashMap<EntityId, (ProjectPath, Option<PathBuf>)>,
194 pane: WeakView<Pane>,
195 next_timestamp: Arc<AtomicUsize>,
196}
197
198#[derive(Debug, Copy, Clone)]
199pub enum NavigationMode {
200 Normal,
201 GoingBack,
202 GoingForward,
203 ClosingItem,
204 ReopeningClosedItem,
205 Disabled,
206}
207
208impl Default for NavigationMode {
209 fn default() -> Self {
210 Self::Normal
211 }
212}
213
214pub struct NavigationEntry {
215 pub item: Arc<dyn WeakItemHandle>,
216 pub data: Option<Box<dyn Any + Send>>,
217 pub timestamp: usize,
218}
219
220#[derive(Clone)]
221pub struct DraggedTab {
222 pub pane: View<Pane>,
223 pub item: Box<dyn ItemHandle>,
224 pub ix: usize,
225 pub detail: usize,
226 pub is_active: bool,
227}
228
229impl EventEmitter<Event> for Pane {}
230
231impl Pane {
232 pub fn new(
233 workspace: WeakView<Workspace>,
234 project: Model<Project>,
235 next_timestamp: Arc<AtomicUsize>,
236 can_drop_predicate: Option<Arc<dyn Fn(&dyn Any, &mut WindowContext) -> bool + 'static>>,
237 cx: &mut ViewContext<Self>,
238 ) -> Self {
239 let focus_handle = cx.focus_handle();
240
241 let subscriptions = vec![
242 cx.on_focus(&focus_handle, Pane::focus_in),
243 cx.on_focus_in(&focus_handle, Pane::focus_in),
244 cx.on_focus_out(&focus_handle, Pane::focus_out),
245 ];
246
247 let handle = cx.view().downgrade();
248 Self {
249 focus_handle,
250 items: Vec::new(),
251 activation_history: Vec::new(),
252 was_focused: false,
253 zoomed: false,
254 active_item_index: 0,
255 last_focused_view_by_item: Default::default(),
256 nav_history: NavHistory(Arc::new(Mutex::new(NavHistoryState {
257 mode: NavigationMode::Normal,
258 backward_stack: Default::default(),
259 forward_stack: Default::default(),
260 closed_stack: Default::default(),
261 paths_by_item: Default::default(),
262 pane: handle.clone(),
263 next_timestamp,
264 }))),
265 toolbar: cx.new_view(|_| Toolbar::new()),
266 new_item_menu: None,
267 split_item_menu: None,
268 tab_bar_scroll_handle: ScrollHandle::new(),
269 drag_split_direction: None,
270 workspace,
271 project,
272 can_drop_predicate,
273 custom_drop_handle: None,
274 can_split: true,
275 render_tab_bar_buttons: Rc::new(move |pane, cx| {
276 h_flex()
277 .gap_2()
278 .child(
279 IconButton::new("plus", IconName::Plus)
280 .icon_size(IconSize::Small)
281 .icon_color(Color::Muted)
282 .on_click(cx.listener(|pane, _, cx| {
283 let menu = ContextMenu::build(cx, |menu, _| {
284 menu.action("New File", NewFile.boxed_clone())
285 .action("New Terminal", NewCenterTerminal.boxed_clone())
286 .action("New Search", NewSearch.boxed_clone())
287 });
288 cx.subscribe(&menu, |pane, _, _: &DismissEvent, cx| {
289 pane.focus(cx);
290 pane.new_item_menu = None;
291 })
292 .detach();
293 pane.new_item_menu = Some(menu);
294 }))
295 .tooltip(|cx| Tooltip::text("New...", cx)),
296 )
297 .when_some(pane.new_item_menu.as_ref(), |el, new_item_menu| {
298 el.child(Self::render_menu_overlay(new_item_menu))
299 })
300 .child(
301 IconButton::new("split", IconName::Split)
302 .icon_size(IconSize::Small)
303 .icon_color(Color::Muted)
304 .on_click(cx.listener(|pane, _, cx| {
305 let menu = ContextMenu::build(cx, |menu, _| {
306 menu.action("Split Right", SplitRight.boxed_clone())
307 .action("Split Left", SplitLeft.boxed_clone())
308 .action("Split Up", SplitUp.boxed_clone())
309 .action("Split Down", SplitDown.boxed_clone())
310 });
311 cx.subscribe(&menu, |pane, _, _: &DismissEvent, cx| {
312 pane.focus(cx);
313 pane.split_item_menu = None;
314 })
315 .detach();
316 pane.split_item_menu = Some(menu);
317 }))
318 .tooltip(|cx| Tooltip::text("Split Pane", cx)),
319 )
320 .child({
321 let zoomed = pane.is_zoomed();
322 IconButton::new("toggle_zoom", IconName::Maximize)
323 .icon_size(IconSize::Small)
324 .icon_color(Color::Muted)
325 .selected(zoomed)
326 .selected_icon(IconName::Minimize)
327 .on_click(cx.listener(|pane, _, cx| {
328 pane.toggle_zoom(&crate::ToggleZoom, cx);
329 }))
330 .tooltip(move |cx| {
331 Tooltip::text(if zoomed { "Zoom Out" } else { "Zoom In" }, cx)
332 })
333 })
334 .when_some(pane.split_item_menu.as_ref(), |el, split_item_menu| {
335 el.child(Self::render_menu_overlay(split_item_menu))
336 })
337 .into_any_element()
338 }),
339 display_nav_history_buttons: true,
340 _subscriptions: subscriptions,
341 }
342 }
343
344 pub fn has_focus(&self, cx: &WindowContext) -> bool {
345 // We not only check whether our focus handle contains focus, but also
346 // whether the active_item might have focus, because we might have just activated an item
347 // but that hasn't rendered yet.
348 // So before the next render, we might have transferred focus
349 // to the item and `focus_handle.contains_focus` returns false because the `active_item`
350 // is not hooked up to us in the dispatch tree.
351 self.focus_handle.contains_focused(cx)
352 || self
353 .active_item()
354 .map_or(false, |item| item.focus_handle(cx).contains_focused(cx))
355 }
356
357 fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
358 if !self.was_focused {
359 self.was_focused = true;
360 cx.emit(Event::Focus);
361 cx.notify();
362 }
363
364 self.toolbar.update(cx, |toolbar, cx| {
365 toolbar.focus_changed(true, cx);
366 });
367
368 if let Some(active_item) = self.active_item() {
369 if self.focus_handle.is_focused(cx) {
370 // Pane was focused directly. We need to either focus a view inside the active item,
371 // or focus the active item itself
372 if let Some(weak_last_focused_view) =
373 self.last_focused_view_by_item.get(&active_item.item_id())
374 {
375 weak_last_focused_view.focus(cx);
376 return;
377 }
378
379 active_item.focus_handle(cx).focus(cx);
380 } else if let Some(focused) = cx.focused() {
381 if !self.context_menu_focused(cx) {
382 self.last_focused_view_by_item
383 .insert(active_item.item_id(), focused);
384 }
385 }
386 }
387 }
388
389 fn context_menu_focused(&self, cx: &mut ViewContext<Self>) -> bool {
390 self.new_item_menu
391 .as_ref()
392 .or(self.split_item_menu.as_ref())
393 .map_or(false, |menu| menu.focus_handle(cx).is_focused(cx))
394 }
395
396 fn focus_out(&mut self, cx: &mut ViewContext<Self>) {
397 self.was_focused = false;
398 self.toolbar.update(cx, |toolbar, cx| {
399 toolbar.focus_changed(false, cx);
400 });
401 cx.notify();
402 }
403
404 pub fn active_item_index(&self) -> usize {
405 self.active_item_index
406 }
407
408 pub fn set_can_split(&mut self, can_split: bool, cx: &mut ViewContext<Self>) {
409 self.can_split = can_split;
410 cx.notify();
411 }
412
413 pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut ViewContext<Self>) {
414 self.toolbar.update(cx, |toolbar, cx| {
415 toolbar.set_can_navigate(can_navigate, cx);
416 });
417 cx.notify();
418 }
419
420 pub fn set_render_tab_bar_buttons<F>(&mut self, cx: &mut ViewContext<Self>, render: F)
421 where
422 F: 'static + Fn(&mut Pane, &mut ViewContext<Pane>) -> AnyElement,
423 {
424 self.render_tab_bar_buttons = Rc::new(render);
425 cx.notify();
426 }
427
428 pub fn set_custom_drop_handle<F>(&mut self, cx: &mut ViewContext<Self>, handle: F)
429 where
430 F: 'static + Fn(&mut Pane, &dyn Any, &mut ViewContext<Pane>) -> ControlFlow<(), ()>,
431 {
432 self.custom_drop_handle = Some(Arc::new(handle));
433 cx.notify();
434 }
435
436 pub fn nav_history_for_item<T: Item>(&self, item: &View<T>) -> ItemNavHistory {
437 ItemNavHistory {
438 history: self.nav_history.clone(),
439 item: Arc::new(item.downgrade()),
440 }
441 }
442
443 pub fn nav_history(&self) -> &NavHistory {
444 &self.nav_history
445 }
446
447 pub fn nav_history_mut(&mut self) -> &mut NavHistory {
448 &mut self.nav_history
449 }
450
451 pub fn disable_history(&mut self) {
452 self.nav_history.disable();
453 }
454
455 pub fn enable_history(&mut self) {
456 self.nav_history.enable();
457 }
458
459 pub fn can_navigate_backward(&self) -> bool {
460 !self.nav_history.0.lock().backward_stack.is_empty()
461 }
462
463 pub fn can_navigate_forward(&self) -> bool {
464 !self.nav_history.0.lock().forward_stack.is_empty()
465 }
466
467 fn navigate_backward(&mut self, cx: &mut ViewContext<Self>) {
468 if let Some(workspace) = self.workspace.upgrade() {
469 let pane = cx.view().downgrade();
470 cx.window_context().defer(move |cx| {
471 workspace.update(cx, |workspace, cx| {
472 workspace.go_back(pane, cx).detach_and_log_err(cx)
473 })
474 })
475 }
476 }
477
478 fn navigate_forward(&mut self, cx: &mut ViewContext<Self>) {
479 if let Some(workspace) = self.workspace.upgrade() {
480 let pane = cx.view().downgrade();
481 cx.window_context().defer(move |cx| {
482 workspace.update(cx, |workspace, cx| {
483 workspace.go_forward(pane, cx).detach_and_log_err(cx)
484 })
485 })
486 }
487 }
488
489 fn history_updated(&mut self, cx: &mut ViewContext<Self>) {
490 self.toolbar.update(cx, |_, cx| cx.notify());
491 }
492
493 pub(crate) fn open_item(
494 &mut self,
495 project_entry_id: Option<ProjectEntryId>,
496 focus_item: bool,
497 cx: &mut ViewContext<Self>,
498 build_item: impl FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
499 ) -> Box<dyn ItemHandle> {
500 let mut existing_item = None;
501 if let Some(project_entry_id) = project_entry_id {
502 for (index, item) in self.items.iter().enumerate() {
503 if item.is_singleton(cx)
504 && item.project_entry_ids(cx).as_slice() == [project_entry_id]
505 {
506 let item = item.boxed_clone();
507 existing_item = Some((index, item));
508 break;
509 }
510 }
511 }
512
513 if let Some((index, existing_item)) = existing_item {
514 self.activate_item(index, focus_item, focus_item, cx);
515 existing_item
516 } else {
517 let new_item = build_item(cx);
518 self.add_item(new_item.clone(), true, focus_item, None, cx);
519 new_item
520 }
521 }
522
523 pub fn add_item(
524 &mut self,
525 item: Box<dyn ItemHandle>,
526 activate_pane: bool,
527 focus_item: bool,
528 destination_index: Option<usize>,
529 cx: &mut ViewContext<Self>,
530 ) {
531 if item.is_singleton(cx) {
532 if let Some(&entry_id) = item.project_entry_ids(cx).get(0) {
533 let project = self.project.read(cx);
534 if let Some(project_path) = project.path_for_entry(entry_id, cx) {
535 let abs_path = project.absolute_path(&project_path, cx);
536 self.nav_history
537 .0
538 .lock()
539 .paths_by_item
540 .insert(item.item_id(), (project_path, abs_path));
541 }
542 }
543 }
544 // If no destination index is specified, add or move the item after the active item.
545 let mut insertion_index = {
546 cmp::min(
547 if let Some(destination_index) = destination_index {
548 destination_index
549 } else {
550 self.active_item_index + 1
551 },
552 self.items.len(),
553 )
554 };
555
556 // Does the item already exist?
557 let project_entry_id = if item.is_singleton(cx) {
558 item.project_entry_ids(cx).get(0).copied()
559 } else {
560 None
561 };
562
563 let existing_item_index = self.items.iter().position(|existing_item| {
564 if existing_item.item_id() == item.item_id() {
565 true
566 } else if existing_item.is_singleton(cx) {
567 existing_item
568 .project_entry_ids(cx)
569 .get(0)
570 .map_or(false, |existing_entry_id| {
571 Some(existing_entry_id) == project_entry_id.as_ref()
572 })
573 } else {
574 false
575 }
576 });
577
578 if let Some(existing_item_index) = existing_item_index {
579 // If the item already exists, move it to the desired destination and activate it
580
581 if existing_item_index != insertion_index {
582 let existing_item_is_active = existing_item_index == self.active_item_index;
583
584 // If the caller didn't specify a destination and the added item is already
585 // the active one, don't move it
586 if existing_item_is_active && destination_index.is_none() {
587 insertion_index = existing_item_index;
588 } else {
589 self.items.remove(existing_item_index);
590 if existing_item_index < self.active_item_index {
591 self.active_item_index -= 1;
592 }
593 insertion_index = insertion_index.min(self.items.len());
594
595 self.items.insert(insertion_index, item.clone());
596
597 if existing_item_is_active {
598 self.active_item_index = insertion_index;
599 } else if insertion_index <= self.active_item_index {
600 self.active_item_index += 1;
601 }
602 }
603
604 cx.notify();
605 }
606
607 self.activate_item(insertion_index, activate_pane, focus_item, cx);
608 } else {
609 self.items.insert(insertion_index, item.clone());
610 if insertion_index <= self.active_item_index {
611 self.active_item_index += 1;
612 }
613
614 self.activate_item(insertion_index, activate_pane, focus_item, cx);
615 cx.notify();
616 }
617
618 cx.emit(Event::AddItem { item });
619 }
620
621 pub fn items_len(&self) -> usize {
622 self.items.len()
623 }
624
625 pub fn items(&self) -> impl Iterator<Item = &Box<dyn ItemHandle>> + DoubleEndedIterator {
626 self.items.iter()
627 }
628
629 pub fn items_of_type<T: Render>(&self) -> impl '_ + Iterator<Item = View<T>> {
630 self.items
631 .iter()
632 .filter_map(|item| item.to_any().downcast().ok())
633 }
634
635 pub fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
636 self.items.get(self.active_item_index).cloned()
637 }
638
639 pub fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Point<Pixels>> {
640 self.items
641 .get(self.active_item_index)?
642 .pixel_position_of_cursor(cx)
643 }
644
645 pub fn item_for_entry(
646 &self,
647 entry_id: ProjectEntryId,
648 cx: &AppContext,
649 ) -> Option<Box<dyn ItemHandle>> {
650 self.items.iter().find_map(|item| {
651 if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
652 Some(item.boxed_clone())
653 } else {
654 None
655 }
656 })
657 }
658
659 pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option<usize> {
660 self.items
661 .iter()
662 .position(|i| i.item_id() == item.item_id())
663 }
664
665 pub fn item_for_index(&self, ix: usize) -> Option<&dyn ItemHandle> {
666 self.items.get(ix).map(|i| i.as_ref())
667 }
668
669 pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) {
670 if self.zoomed {
671 cx.emit(Event::ZoomOut);
672 } else if !self.items.is_empty() {
673 if !self.focus_handle.contains_focused(cx) {
674 cx.focus_self();
675 }
676 cx.emit(Event::ZoomIn);
677 }
678 }
679
680 pub fn activate_item(
681 &mut self,
682 index: usize,
683 activate_pane: bool,
684 focus_item: bool,
685 cx: &mut ViewContext<Self>,
686 ) {
687 use NavigationMode::{GoingBack, GoingForward};
688
689 if index < self.items.len() {
690 let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
691 if prev_active_item_ix != self.active_item_index
692 || matches!(self.nav_history.mode(), GoingBack | GoingForward)
693 {
694 if let Some(prev_item) = self.items.get(prev_active_item_ix) {
695 prev_item.deactivated(cx);
696 }
697
698 cx.emit(Event::ActivateItem {
699 local: activate_pane,
700 });
701 }
702
703 if let Some(newly_active_item) = self.items.get(index) {
704 self.activation_history
705 .retain(|&previously_active_item_id| {
706 previously_active_item_id != newly_active_item.item_id()
707 });
708 self.activation_history.push(newly_active_item.item_id());
709 }
710
711 self.update_toolbar(cx);
712 self.update_status_bar(cx);
713
714 if focus_item {
715 self.focus_active_item(cx);
716 }
717
718 self.tab_bar_scroll_handle.scroll_to_item(index);
719 cx.notify();
720 }
721 }
722
723 pub fn activate_prev_item(&mut self, activate_pane: bool, cx: &mut ViewContext<Self>) {
724 let mut index = self.active_item_index;
725 if index > 0 {
726 index -= 1;
727 } else if !self.items.is_empty() {
728 index = self.items.len() - 1;
729 }
730 self.activate_item(index, activate_pane, activate_pane, cx);
731 }
732
733 pub fn activate_next_item(&mut self, activate_pane: bool, cx: &mut ViewContext<Self>) {
734 let mut index = self.active_item_index;
735 if index + 1 < self.items.len() {
736 index += 1;
737 } else {
738 index = 0;
739 }
740 self.activate_item(index, activate_pane, activate_pane, cx);
741 }
742
743 pub fn close_active_item(
744 &mut self,
745 action: &CloseActiveItem,
746 cx: &mut ViewContext<Self>,
747 ) -> Option<Task<Result<()>>> {
748 if self.items.is_empty() {
749 return None;
750 }
751 let active_item_id = self.items[self.active_item_index].item_id();
752 Some(self.close_item_by_id(
753 active_item_id,
754 action.save_intent.unwrap_or(SaveIntent::Close),
755 cx,
756 ))
757 }
758
759 pub fn close_item_by_id(
760 &mut self,
761 item_id_to_close: EntityId,
762 save_intent: SaveIntent,
763 cx: &mut ViewContext<Self>,
764 ) -> Task<Result<()>> {
765 self.close_items(cx, save_intent, move |view_id| view_id == item_id_to_close)
766 }
767
768 pub fn close_inactive_items(
769 &mut self,
770 _: &CloseInactiveItems,
771 cx: &mut ViewContext<Self>,
772 ) -> Option<Task<Result<()>>> {
773 if self.items.is_empty() {
774 return None;
775 }
776
777 let active_item_id = self.items[self.active_item_index].item_id();
778 Some(self.close_items(cx, SaveIntent::Close, move |item_id| {
779 item_id != active_item_id
780 }))
781 }
782
783 pub fn close_clean_items(
784 &mut self,
785 _: &CloseCleanItems,
786 cx: &mut ViewContext<Self>,
787 ) -> Option<Task<Result<()>>> {
788 let item_ids: Vec<_> = self
789 .items()
790 .filter(|item| !item.is_dirty(cx))
791 .map(|item| item.item_id())
792 .collect();
793 Some(self.close_items(cx, SaveIntent::Close, move |item_id| {
794 item_ids.contains(&item_id)
795 }))
796 }
797
798 pub fn close_items_to_the_left(
799 &mut self,
800 _: &CloseItemsToTheLeft,
801 cx: &mut ViewContext<Self>,
802 ) -> Option<Task<Result<()>>> {
803 if self.items.is_empty() {
804 return None;
805 }
806 let active_item_id = self.items[self.active_item_index].item_id();
807 Some(self.close_items_to_the_left_by_id(active_item_id, cx))
808 }
809
810 pub fn close_items_to_the_left_by_id(
811 &mut self,
812 item_id: EntityId,
813 cx: &mut ViewContext<Self>,
814 ) -> Task<Result<()>> {
815 let item_ids: Vec<_> = self
816 .items()
817 .take_while(|item| item.item_id() != item_id)
818 .map(|item| item.item_id())
819 .collect();
820 self.close_items(cx, SaveIntent::Close, move |item_id| {
821 item_ids.contains(&item_id)
822 })
823 }
824
825 pub fn close_items_to_the_right(
826 &mut self,
827 _: &CloseItemsToTheRight,
828 cx: &mut ViewContext<Self>,
829 ) -> Option<Task<Result<()>>> {
830 if self.items.is_empty() {
831 return None;
832 }
833 let active_item_id = self.items[self.active_item_index].item_id();
834 Some(self.close_items_to_the_right_by_id(active_item_id, cx))
835 }
836
837 pub fn close_items_to_the_right_by_id(
838 &mut self,
839 item_id: EntityId,
840 cx: &mut ViewContext<Self>,
841 ) -> Task<Result<()>> {
842 let item_ids: Vec<_> = self
843 .items()
844 .rev()
845 .take_while(|item| item.item_id() != item_id)
846 .map(|item| item.item_id())
847 .collect();
848 self.close_items(cx, SaveIntent::Close, move |item_id| {
849 item_ids.contains(&item_id)
850 })
851 }
852
853 pub fn close_all_items(
854 &mut self,
855 action: &CloseAllItems,
856 cx: &mut ViewContext<Self>,
857 ) -> Option<Task<Result<()>>> {
858 if self.items.is_empty() {
859 return None;
860 }
861
862 Some(
863 self.close_items(cx, action.save_intent.unwrap_or(SaveIntent::Close), |_| {
864 true
865 }),
866 )
867 }
868
869 pub(super) fn file_names_for_prompt(
870 items: &mut dyn Iterator<Item = &Box<dyn ItemHandle>>,
871 all_dirty_items: usize,
872 cx: &AppContext,
873 ) -> String {
874 /// Quantity of item paths displayed in prompt prior to cutoff..
875 const FILE_NAMES_CUTOFF_POINT: usize = 10;
876 let mut file_names: Vec<_> = items
877 .filter_map(|item| {
878 item.project_path(cx).and_then(|project_path| {
879 project_path
880 .path
881 .file_name()
882 .and_then(|name| name.to_str().map(ToOwned::to_owned))
883 })
884 })
885 .take(FILE_NAMES_CUTOFF_POINT)
886 .collect();
887 let should_display_followup_text =
888 all_dirty_items > FILE_NAMES_CUTOFF_POINT || file_names.len() != all_dirty_items;
889 if should_display_followup_text {
890 let not_shown_files = all_dirty_items - file_names.len();
891 if not_shown_files == 1 {
892 file_names.push(".. 1 file not shown".into());
893 } else {
894 file_names.push(format!(".. {} files not shown", not_shown_files).into());
895 }
896 }
897 let file_names = file_names.join("\n");
898 format!(
899 "Do you want to save changes to the following {} files?\n{file_names}",
900 all_dirty_items
901 )
902 }
903
904 pub fn close_items(
905 &mut self,
906 cx: &mut ViewContext<Pane>,
907 mut save_intent: SaveIntent,
908 should_close: impl Fn(EntityId) -> bool,
909 ) -> Task<Result<()>> {
910 // Find the items to close.
911 let mut items_to_close = Vec::new();
912 let mut dirty_items = Vec::new();
913 for item in &self.items {
914 if should_close(item.item_id()) {
915 items_to_close.push(item.boxed_clone());
916 if item.is_dirty(cx) {
917 dirty_items.push(item.boxed_clone());
918 }
919 }
920 }
921
922 // If a buffer is open both in a singleton editor and in a multibuffer, make sure
923 // to focus the singleton buffer when prompting to save that buffer, as opposed
924 // to focusing the multibuffer, because this gives the user a more clear idea
925 // of what content they would be saving.
926 items_to_close.sort_by_key(|item| !item.is_singleton(cx));
927
928 let workspace = self.workspace.clone();
929 cx.spawn(|pane, mut cx| async move {
930 if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
931 let answer = pane.update(&mut cx, |_, cx| {
932 let prompt =
933 Self::file_names_for_prompt(&mut dirty_items.iter(), dirty_items.len(), cx);
934 cx.prompt(
935 PromptLevel::Warning,
936 &prompt,
937 &["Save all", "Discard all", "Cancel"],
938 )
939 })?;
940 match answer.await {
941 Ok(0) => save_intent = SaveIntent::SaveAll,
942 Ok(1) => save_intent = SaveIntent::Skip,
943 _ => {}
944 }
945 }
946 let mut saved_project_items_ids = HashSet::default();
947 for item in items_to_close.clone() {
948 // Find the item's current index and its set of project item models. Avoid
949 // storing these in advance, in case they have changed since this task
950 // was started.
951 let (item_ix, mut project_item_ids) = pane.update(&mut cx, |pane, cx| {
952 (pane.index_for_item(&*item), item.project_item_model_ids(cx))
953 })?;
954 let item_ix = if let Some(ix) = item_ix {
955 ix
956 } else {
957 continue;
958 };
959
960 // Check if this view has any project items that are not open anywhere else
961 // in the workspace, AND that the user has not already been prompted to save.
962 // If there are any such project entries, prompt the user to save this item.
963 let project = workspace.update(&mut cx, |workspace, cx| {
964 for item in workspace.items(cx) {
965 if !items_to_close
966 .iter()
967 .any(|item_to_close| item_to_close.item_id() == item.item_id())
968 {
969 let other_project_item_ids = item.project_item_model_ids(cx);
970 project_item_ids.retain(|id| !other_project_item_ids.contains(id));
971 }
972 }
973 workspace.project().clone()
974 })?;
975 let should_save = project_item_ids
976 .iter()
977 .any(|id| saved_project_items_ids.insert(*id));
978
979 if should_save
980 && !Self::save_item(
981 project.clone(),
982 &pane,
983 item_ix,
984 &*item,
985 save_intent,
986 &mut cx,
987 )
988 .await?
989 {
990 break;
991 }
992
993 // Remove the item from the pane.
994 pane.update(&mut cx, |pane, cx| {
995 if let Some(item_ix) = pane
996 .items
997 .iter()
998 .position(|i| i.item_id() == item.item_id())
999 {
1000 pane.remove_item(item_ix, false, cx);
1001 }
1002 })
1003 .ok();
1004 }
1005
1006 pane.update(&mut cx, |_, cx| cx.notify()).ok();
1007 Ok(())
1008 })
1009 }
1010
1011 pub fn remove_item(
1012 &mut self,
1013 item_index: usize,
1014 activate_pane: bool,
1015 cx: &mut ViewContext<Self>,
1016 ) {
1017 self.activation_history
1018 .retain(|&history_entry| history_entry != self.items[item_index].item_id());
1019
1020 if item_index == self.active_item_index {
1021 let index_to_activate = self
1022 .activation_history
1023 .pop()
1024 .and_then(|last_activated_item| {
1025 self.items.iter().enumerate().find_map(|(index, item)| {
1026 (item.item_id() == last_activated_item).then_some(index)
1027 })
1028 })
1029 // We didn't have a valid activation history entry, so fallback
1030 // to activating the item to the left
1031 .unwrap_or_else(|| item_index.min(self.items.len()).saturating_sub(1));
1032
1033 let should_activate = activate_pane || self.has_focus(cx);
1034 if self.items.len() == 1 && should_activate {
1035 self.focus_handle.focus(cx);
1036 } else {
1037 self.activate_item(index_to_activate, should_activate, should_activate, cx);
1038 }
1039 }
1040
1041 let item = self.items.remove(item_index);
1042
1043 cx.emit(Event::RemoveItem {
1044 item_id: item.item_id(),
1045 });
1046 if self.items.is_empty() {
1047 item.deactivated(cx);
1048 self.update_toolbar(cx);
1049 cx.emit(Event::Remove);
1050 }
1051
1052 if item_index < self.active_item_index {
1053 self.active_item_index -= 1;
1054 }
1055
1056 self.nav_history.set_mode(NavigationMode::ClosingItem);
1057 item.deactivated(cx);
1058 self.nav_history.set_mode(NavigationMode::Normal);
1059
1060 if let Some(path) = item.project_path(cx) {
1061 let abs_path = self
1062 .nav_history
1063 .0
1064 .lock()
1065 .paths_by_item
1066 .get(&item.item_id())
1067 .and_then(|(_, abs_path)| abs_path.clone());
1068
1069 self.nav_history
1070 .0
1071 .lock()
1072 .paths_by_item
1073 .insert(item.item_id(), (path, abs_path));
1074 } else {
1075 self.nav_history
1076 .0
1077 .lock()
1078 .paths_by_item
1079 .remove(&item.item_id());
1080 }
1081
1082 if self.items.is_empty() && self.zoomed {
1083 cx.emit(Event::ZoomOut);
1084 }
1085
1086 cx.notify();
1087 }
1088
1089 pub async fn save_item(
1090 project: Model<Project>,
1091 pane: &WeakView<Pane>,
1092 item_ix: usize,
1093 item: &dyn ItemHandle,
1094 save_intent: SaveIntent,
1095 cx: &mut AsyncWindowContext,
1096 ) -> Result<bool> {
1097 const CONFLICT_MESSAGE: &str =
1098 "This file has changed on disk since you started editing it. Do you want to overwrite it?";
1099
1100 if save_intent == SaveIntent::Skip {
1101 return Ok(true);
1102 }
1103
1104 let (mut has_conflict, mut is_dirty, mut can_save, can_save_as) = cx.update(|cx| {
1105 (
1106 item.has_conflict(cx),
1107 item.is_dirty(cx),
1108 item.can_save(cx),
1109 item.is_singleton(cx),
1110 )
1111 })?;
1112
1113 // when saving a single buffer, we ignore whether or not it's dirty.
1114 if save_intent == SaveIntent::Save {
1115 is_dirty = true;
1116 }
1117
1118 if save_intent == SaveIntent::SaveAs {
1119 is_dirty = true;
1120 has_conflict = false;
1121 can_save = false;
1122 }
1123
1124 if save_intent == SaveIntent::Overwrite {
1125 has_conflict = false;
1126 }
1127
1128 if has_conflict && can_save {
1129 let answer = pane.update(cx, |pane, cx| {
1130 pane.activate_item(item_ix, true, true, cx);
1131 cx.prompt(
1132 PromptLevel::Warning,
1133 CONFLICT_MESSAGE,
1134 &["Overwrite", "Discard", "Cancel"],
1135 )
1136 })?;
1137 match answer.await {
1138 Ok(0) => pane.update(cx, |_, cx| item.save(project, cx))?.await?,
1139 Ok(1) => pane.update(cx, |_, cx| item.reload(project, cx))?.await?,
1140 _ => return Ok(false),
1141 }
1142 } else if is_dirty && (can_save || can_save_as) {
1143 if save_intent == SaveIntent::Close {
1144 let will_autosave = cx.update(|cx| {
1145 matches!(
1146 WorkspaceSettings::get_global(cx).autosave,
1147 AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange
1148 ) && Self::can_autosave_item(&*item, cx)
1149 })?;
1150 if !will_autosave {
1151 let answer = pane.update(cx, |pane, cx| {
1152 pane.activate_item(item_ix, true, true, cx);
1153 let prompt = dirty_message_for(item.project_path(cx));
1154 cx.prompt(
1155 PromptLevel::Warning,
1156 &prompt,
1157 &["Save", "Don't Save", "Cancel"],
1158 )
1159 })?;
1160 match answer.await {
1161 Ok(0) => {}
1162 Ok(1) => return Ok(true), // Don't save this file
1163 _ => return Ok(false), // Cancel
1164 }
1165 }
1166 }
1167
1168 if can_save {
1169 pane.update(cx, |_, cx| item.save(project, cx))?.await?;
1170 } else if can_save_as {
1171 let start_abs_path = project
1172 .update(cx, |project, cx| {
1173 let worktree = project.visible_worktrees(cx).next()?;
1174 Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
1175 })?
1176 .unwrap_or_else(|| Path::new("").into());
1177
1178 let abs_path = cx.update(|cx| cx.prompt_for_new_path(&start_abs_path))?;
1179 if let Some(abs_path) = abs_path.await.ok().flatten() {
1180 pane.update(cx, |_, cx| item.save_as(project, abs_path, cx))?
1181 .await?;
1182 } else {
1183 return Ok(false);
1184 }
1185 }
1186 }
1187 Ok(true)
1188 }
1189
1190 fn can_autosave_item(item: &dyn ItemHandle, cx: &AppContext) -> bool {
1191 let is_deleted = item.project_entry_ids(cx).is_empty();
1192 item.is_dirty(cx) && !item.has_conflict(cx) && item.can_save(cx) && !is_deleted
1193 }
1194
1195 pub fn autosave_item(
1196 item: &dyn ItemHandle,
1197 project: Model<Project>,
1198 cx: &mut WindowContext,
1199 ) -> Task<Result<()>> {
1200 if Self::can_autosave_item(item, cx) {
1201 item.save(project, cx)
1202 } else {
1203 Task::ready(Ok(()))
1204 }
1205 }
1206
1207 pub fn focus(&mut self, cx: &mut ViewContext<Pane>) {
1208 cx.focus(&self.focus_handle);
1209 }
1210
1211 pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
1212 if let Some(active_item) = self.active_item() {
1213 let focus_handle = active_item.focus_handle(cx);
1214 cx.focus(&focus_handle);
1215 }
1216 }
1217
1218 pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
1219 cx.emit(Event::Split(direction));
1220 }
1221
1222 pub fn toolbar(&self) -> &View<Toolbar> {
1223 &self.toolbar
1224 }
1225
1226 pub fn handle_deleted_project_item(
1227 &mut self,
1228 entry_id: ProjectEntryId,
1229 cx: &mut ViewContext<Pane>,
1230 ) -> Option<()> {
1231 let (item_index_to_delete, item_id) = self.items().enumerate().find_map(|(i, item)| {
1232 if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
1233 Some((i, item.item_id()))
1234 } else {
1235 None
1236 }
1237 })?;
1238
1239 self.remove_item(item_index_to_delete, false, cx);
1240 self.nav_history.remove_item(item_id);
1241
1242 Some(())
1243 }
1244
1245 fn update_toolbar(&mut self, cx: &mut ViewContext<Self>) {
1246 let active_item = self
1247 .items
1248 .get(self.active_item_index)
1249 .map(|item| item.as_ref());
1250 self.toolbar.update(cx, |toolbar, cx| {
1251 toolbar.set_active_item(active_item, cx);
1252 });
1253 }
1254
1255 fn update_status_bar(&mut self, cx: &mut ViewContext<Self>) {
1256 let workspace = self.workspace.clone();
1257 let pane = cx.view().clone();
1258
1259 cx.window_context().defer(move |cx| {
1260 let Ok(status_bar) = workspace.update(cx, |workspace, _| workspace.status_bar.clone())
1261 else {
1262 return;
1263 };
1264
1265 status_bar.update(cx, move |status_bar, cx| {
1266 status_bar.set_active_pane(&pane, cx);
1267 });
1268 });
1269 }
1270
1271 fn render_tab(
1272 &self,
1273 ix: usize,
1274 item: &Box<dyn ItemHandle>,
1275 detail: usize,
1276 cx: &mut ViewContext<'_, Pane>,
1277 ) -> impl IntoElement {
1278 let is_active = ix == self.active_item_index;
1279
1280 let label = item.tab_content(Some(detail), is_active, cx);
1281 let close_side = &ItemSettings::get_global(cx).close_position;
1282
1283 let indicator = maybe!({
1284 let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
1285 (true, _) => Color::Warning,
1286 (_, true) => Color::Accent,
1287 (false, false) => return None,
1288 };
1289
1290 Some(Indicator::dot().color(indicator_color))
1291 });
1292
1293 let item_id = item.item_id();
1294 let is_first_item = ix == 0;
1295 let is_last_item = ix == self.items.len() - 1;
1296 let position_relative_to_active_item = ix.cmp(&self.active_item_index);
1297
1298 let tab = Tab::new(ix)
1299 .position(if is_first_item {
1300 TabPosition::First
1301 } else if is_last_item {
1302 TabPosition::Last
1303 } else {
1304 TabPosition::Middle(position_relative_to_active_item)
1305 })
1306 .close_side(match close_side {
1307 ClosePosition::Left => ui::TabCloseSide::Start,
1308 ClosePosition::Right => ui::TabCloseSide::End,
1309 })
1310 .selected(is_active)
1311 .on_click(
1312 cx.listener(move |pane: &mut Self, _, cx| pane.activate_item(ix, true, true, cx)),
1313 )
1314 // TODO: This should be a click listener with the middle mouse button instead of a mouse down listener.
1315 .on_mouse_down(
1316 MouseButton::Middle,
1317 cx.listener(move |pane, _event, cx| {
1318 pane.close_item_by_id(item_id, SaveIntent::Close, cx)
1319 .detach_and_log_err(cx);
1320 }),
1321 )
1322 .on_drag(
1323 DraggedTab {
1324 item: item.boxed_clone(),
1325 pane: cx.view().clone(),
1326 detail,
1327 is_active,
1328 ix,
1329 },
1330 |tab, cx| cx.new_view(|_| tab.clone()),
1331 )
1332 .drag_over::<DraggedTab>(|tab| tab.bg(cx.theme().colors().drop_target_background))
1333 .drag_over::<ProjectEntryId>(|tab| tab.bg(cx.theme().colors().drop_target_background))
1334 .when_some(self.can_drop_predicate.clone(), |this, p| {
1335 this.can_drop(move |a, cx| p(a, cx))
1336 })
1337 .on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {
1338 this.drag_split_direction = None;
1339 this.handle_tab_drop(dragged_tab, ix, cx)
1340 }))
1341 .on_drop(cx.listener(move |this, entry_id: &ProjectEntryId, cx| {
1342 this.drag_split_direction = None;
1343 this.handle_project_entry_drop(entry_id, cx)
1344 }))
1345 .on_drop(cx.listener(move |this, paths, cx| {
1346 this.drag_split_direction = None;
1347 this.handle_external_paths_drop(paths, cx)
1348 }))
1349 .when_some(item.tab_tooltip_text(cx), |tab, text| {
1350 tab.tooltip(move |cx| Tooltip::text(text.clone(), cx))
1351 })
1352 .start_slot::<Indicator>(indicator)
1353 .end_slot(
1354 IconButton::new("close tab", IconName::Close)
1355 .shape(IconButtonShape::Square)
1356 .icon_color(Color::Muted)
1357 .size(ButtonSize::None)
1358 .icon_size(IconSize::XSmall)
1359 .on_click(cx.listener(move |pane, _, cx| {
1360 pane.close_item_by_id(item_id, SaveIntent::Close, cx)
1361 .detach_and_log_err(cx);
1362 })),
1363 )
1364 .child(label);
1365
1366 let single_entry_to_resolve = {
1367 let item_entries = self.items[ix].project_entry_ids(cx);
1368 if item_entries.len() == 1 {
1369 Some(item_entries[0])
1370 } else {
1371 None
1372 }
1373 };
1374
1375 let pane = cx.view().downgrade();
1376 right_click_menu(ix).trigger(tab).menu(move |cx| {
1377 let pane = pane.clone();
1378 ContextMenu::build(cx, move |mut menu, cx| {
1379 if let Some(pane) = pane.upgrade() {
1380 menu = menu
1381 .entry(
1382 "Close",
1383 Some(Box::new(CloseActiveItem { save_intent: None })),
1384 cx.handler_for(&pane, move |pane, cx| {
1385 pane.close_item_by_id(item_id, SaveIntent::Close, cx)
1386 .detach_and_log_err(cx);
1387 }),
1388 )
1389 .entry(
1390 "Close Others",
1391 Some(Box::new(CloseInactiveItems)),
1392 cx.handler_for(&pane, move |pane, cx| {
1393 pane.close_items(cx, SaveIntent::Close, |id| id != item_id)
1394 .detach_and_log_err(cx);
1395 }),
1396 )
1397 .separator()
1398 .entry(
1399 "Close Left",
1400 Some(Box::new(CloseItemsToTheLeft)),
1401 cx.handler_for(&pane, move |pane, cx| {
1402 pane.close_items_to_the_left_by_id(item_id, cx)
1403 .detach_and_log_err(cx);
1404 }),
1405 )
1406 .entry(
1407 "Close Right",
1408 Some(Box::new(CloseItemsToTheRight)),
1409 cx.handler_for(&pane, move |pane, cx| {
1410 pane.close_items_to_the_right_by_id(item_id, cx)
1411 .detach_and_log_err(cx);
1412 }),
1413 )
1414 .separator()
1415 .entry(
1416 "Close Clean",
1417 Some(Box::new(CloseCleanItems)),
1418 cx.handler_for(&pane, move |pane, cx| {
1419 pane.close_clean_items(&CloseCleanItems, cx)
1420 .map(|task| task.detach_and_log_err(cx));
1421 }),
1422 )
1423 .entry(
1424 "Close All",
1425 Some(Box::new(CloseAllItems { save_intent: None })),
1426 cx.handler_for(&pane, |pane, cx| {
1427 pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
1428 .map(|task| task.detach_and_log_err(cx));
1429 }),
1430 );
1431
1432 if let Some(entry) = single_entry_to_resolve {
1433 let entry_id = entry.to_proto();
1434 menu = menu.separator().entry(
1435 "Reveal In Project Panel",
1436 Some(Box::new(RevealInProjectPanel { entry_id })),
1437 cx.handler_for(&pane, move |pane, cx| {
1438 pane.project.update(cx, |_, cx| {
1439 cx.emit(project::Event::RevealInProjectPanel(
1440 ProjectEntryId::from_proto(entry_id),
1441 ))
1442 });
1443 }),
1444 );
1445 }
1446 }
1447
1448 menu
1449 })
1450 })
1451 }
1452
1453 fn render_tab_bar(&mut self, cx: &mut ViewContext<'_, Pane>) -> impl IntoElement {
1454 TabBar::new("tab_bar")
1455 .track_scroll(self.tab_bar_scroll_handle.clone())
1456 .when(self.display_nav_history_buttons, |tab_bar| {
1457 tab_bar.start_child(
1458 h_flex()
1459 .gap_2()
1460 .child(
1461 IconButton::new("navigate_backward", IconName::ArrowLeft)
1462 .icon_size(IconSize::Small)
1463 .on_click({
1464 let view = cx.view().clone();
1465 move |_, cx| view.update(cx, Self::navigate_backward)
1466 })
1467 .disabled(!self.can_navigate_backward())
1468 .tooltip(|cx| Tooltip::for_action("Go Back", &GoBack, cx)),
1469 )
1470 .child(
1471 IconButton::new("navigate_forward", IconName::ArrowRight)
1472 .icon_size(IconSize::Small)
1473 .on_click({
1474 let view = cx.view().clone();
1475 move |_, cx| view.update(cx, Self::navigate_forward)
1476 })
1477 .disabled(!self.can_navigate_forward())
1478 .tooltip(|cx| Tooltip::for_action("Go Forward", &GoForward, cx)),
1479 ),
1480 )
1481 })
1482 .when(self.has_focus(cx), |tab_bar| {
1483 tab_bar.end_child({
1484 let render_tab_buttons = self.render_tab_bar_buttons.clone();
1485 render_tab_buttons(self, cx)
1486 })
1487 })
1488 .children(
1489 self.items
1490 .iter()
1491 .enumerate()
1492 .zip(self.tab_details(cx))
1493 .map(|((ix, item), detail)| self.render_tab(ix, item, detail, cx)),
1494 )
1495 .child(
1496 div()
1497 .min_w_6()
1498 // HACK: This empty child is currently necessary to force the drop target to appear
1499 // despite us setting a min width above.
1500 .child("")
1501 .h_full()
1502 .flex_grow()
1503 .drag_over::<DraggedTab>(|bar| {
1504 bar.bg(cx.theme().colors().drop_target_background)
1505 })
1506 .drag_over::<ProjectEntryId>(|bar| {
1507 bar.bg(cx.theme().colors().drop_target_background)
1508 })
1509 .on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {
1510 this.drag_split_direction = None;
1511 this.handle_tab_drop(dragged_tab, this.items.len(), cx)
1512 }))
1513 .on_drop(cx.listener(move |this, entry_id: &ProjectEntryId, cx| {
1514 this.drag_split_direction = None;
1515 this.handle_project_entry_drop(entry_id, cx)
1516 }))
1517 .on_drop(cx.listener(move |this, paths, cx| {
1518 this.drag_split_direction = None;
1519 this.handle_external_paths_drop(paths, cx)
1520 })),
1521 )
1522 }
1523
1524 fn render_menu_overlay(menu: &View<ContextMenu>) -> Div {
1525 div()
1526 .absolute()
1527 .z_index(1)
1528 .bottom_0()
1529 .right_0()
1530 .size_0()
1531 .child(overlay().anchor(AnchorCorner::TopRight).child(menu.clone()))
1532 }
1533
1534 fn tab_details(&self, cx: &AppContext) -> Vec<usize> {
1535 let mut tab_details = self.items.iter().map(|_| 0).collect::<Vec<_>>();
1536
1537 let mut tab_descriptions = HashMap::default();
1538 let mut done = false;
1539 while !done {
1540 done = true;
1541
1542 // Store item indices by their tab description.
1543 for (ix, (item, detail)) in self.items.iter().zip(&tab_details).enumerate() {
1544 if let Some(description) = item.tab_description(*detail, cx) {
1545 if *detail == 0
1546 || Some(&description) != item.tab_description(detail - 1, cx).as_ref()
1547 {
1548 tab_descriptions
1549 .entry(description)
1550 .or_insert(Vec::new())
1551 .push(ix);
1552 }
1553 }
1554 }
1555
1556 // If two or more items have the same tab description, increase eir level
1557 // of detail and try again.
1558 for (_, item_ixs) in tab_descriptions.drain() {
1559 if item_ixs.len() > 1 {
1560 done = false;
1561 for ix in item_ixs {
1562 tab_details[ix] += 1;
1563 }
1564 }
1565 }
1566 }
1567
1568 tab_details
1569 }
1570
1571 pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
1572 self.zoomed = zoomed;
1573 cx.notify();
1574 }
1575
1576 pub fn is_zoomed(&self) -> bool {
1577 self.zoomed
1578 }
1579
1580 fn handle_drag_move<T>(&mut self, event: &DragMoveEvent<T>, cx: &mut ViewContext<Self>) {
1581 if !self.can_split {
1582 return;
1583 }
1584
1585 let edge_width = cx.rem_size() * 8;
1586 let cursor = event.event.position;
1587 let direction = if cursor.x < event.bounds.left() + edge_width {
1588 Some(SplitDirection::Left)
1589 } else if cursor.x > event.bounds.right() - edge_width {
1590 Some(SplitDirection::Right)
1591 } else if cursor.y < event.bounds.top() + edge_width {
1592 Some(SplitDirection::Up)
1593 } else if cursor.y > event.bounds.bottom() - edge_width {
1594 Some(SplitDirection::Down)
1595 } else {
1596 None
1597 };
1598
1599 if direction != self.drag_split_direction {
1600 self.drag_split_direction = direction;
1601 }
1602 }
1603
1604 fn handle_tab_drop(
1605 &mut self,
1606 dragged_tab: &DraggedTab,
1607 ix: usize,
1608 cx: &mut ViewContext<'_, Self>,
1609 ) {
1610 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
1611 if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, cx) {
1612 return;
1613 }
1614 }
1615 let mut to_pane = cx.view().clone();
1616 let split_direction = self.drag_split_direction;
1617 let item_id = dragged_tab.item.item_id();
1618 let from_pane = dragged_tab.pane.clone();
1619 self.workspace
1620 .update(cx, |_, cx| {
1621 cx.defer(move |workspace, cx| {
1622 if let Some(split_direction) = split_direction {
1623 to_pane = workspace.split_pane(to_pane, split_direction, cx);
1624 }
1625 workspace.move_item(from_pane, to_pane, item_id, ix, cx);
1626 });
1627 })
1628 .log_err();
1629 }
1630
1631 fn handle_project_entry_drop(
1632 &mut self,
1633 project_entry_id: &ProjectEntryId,
1634 cx: &mut ViewContext<'_, Self>,
1635 ) {
1636 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
1637 if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, cx) {
1638 return;
1639 }
1640 }
1641 let mut to_pane = cx.view().clone();
1642 let split_direction = self.drag_split_direction;
1643 let project_entry_id = *project_entry_id;
1644 self.workspace
1645 .update(cx, |_, cx| {
1646 cx.defer(move |workspace, cx| {
1647 if let Some(path) = workspace
1648 .project()
1649 .read(cx)
1650 .path_for_entry(project_entry_id, cx)
1651 {
1652 if let Some(split_direction) = split_direction {
1653 to_pane = workspace.split_pane(to_pane, split_direction, cx);
1654 }
1655 workspace
1656 .open_path(path, Some(to_pane.downgrade()), true, cx)
1657 .detach_and_log_err(cx);
1658 }
1659 });
1660 })
1661 .log_err();
1662 }
1663
1664 fn handle_external_paths_drop(
1665 &mut self,
1666 paths: &ExternalPaths,
1667 cx: &mut ViewContext<'_, Self>,
1668 ) {
1669 if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
1670 if let ControlFlow::Break(()) = custom_drop_handle(self, paths, cx) {
1671 return;
1672 }
1673 }
1674 let mut to_pane = cx.view().clone();
1675 let mut split_direction = self.drag_split_direction;
1676 let paths = paths.paths().to_vec();
1677 self.workspace
1678 .update(cx, |workspace, cx| {
1679 let fs = Arc::clone(workspace.project().read(cx).fs());
1680 cx.spawn(|workspace, mut cx| async move {
1681 let mut is_file_checks = FuturesUnordered::new();
1682 for path in &paths {
1683 is_file_checks.push(fs.is_file(path))
1684 }
1685 let mut has_files_to_open = false;
1686 while let Some(is_file) = is_file_checks.next().await {
1687 if is_file {
1688 has_files_to_open = true;
1689 break;
1690 }
1691 }
1692 drop(is_file_checks);
1693 if !has_files_to_open {
1694 split_direction = None;
1695 }
1696
1697 if let Some(open_task) = workspace
1698 .update(&mut cx, |workspace, cx| {
1699 if let Some(split_direction) = split_direction {
1700 to_pane = workspace.split_pane(to_pane, split_direction, cx);
1701 }
1702 workspace.open_paths(
1703 paths,
1704 OpenVisible::OnlyDirectories,
1705 Some(to_pane.downgrade()),
1706 cx,
1707 )
1708 })
1709 .ok()
1710 {
1711 let _opened_items: Vec<_> = open_task.await;
1712 }
1713 })
1714 .detach();
1715 })
1716 .log_err();
1717 }
1718
1719 pub fn display_nav_history_buttons(&mut self, display: bool) {
1720 self.display_nav_history_buttons = display;
1721 }
1722}
1723
1724impl FocusableView for Pane {
1725 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
1726 self.focus_handle.clone()
1727 }
1728}
1729
1730impl Render for Pane {
1731 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1732 v_flex()
1733 .key_context("Pane")
1734 .track_focus(&self.focus_handle)
1735 .size_full()
1736 .flex_none()
1737 .overflow_hidden()
1738 .on_action(cx.listener(|pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx)))
1739 .on_action(cx.listener(|pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx)))
1740 .on_action(
1741 cx.listener(|pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx)),
1742 )
1743 .on_action(cx.listener(|pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx)))
1744 .on_action(cx.listener(|pane, _: &GoBack, cx| pane.navigate_backward(cx)))
1745 .on_action(cx.listener(|pane, _: &GoForward, cx| pane.navigate_forward(cx)))
1746 .on_action(cx.listener(Pane::toggle_zoom))
1747 .on_action(cx.listener(|pane: &mut Pane, action: &ActivateItem, cx| {
1748 pane.activate_item(action.0, true, true, cx);
1749 }))
1750 .on_action(cx.listener(|pane: &mut Pane, _: &ActivateLastItem, cx| {
1751 pane.activate_item(pane.items.len() - 1, true, true, cx);
1752 }))
1753 .on_action(cx.listener(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
1754 pane.activate_prev_item(true, cx);
1755 }))
1756 .on_action(cx.listener(|pane: &mut Pane, _: &ActivateNextItem, cx| {
1757 pane.activate_next_item(true, cx);
1758 }))
1759 .on_action(
1760 cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
1761 pane.close_active_item(action, cx)
1762 .map(|task| task.detach_and_log_err(cx));
1763 }),
1764 )
1765 .on_action(
1766 cx.listener(|pane: &mut Self, action: &CloseInactiveItems, cx| {
1767 pane.close_inactive_items(action, cx)
1768 .map(|task| task.detach_and_log_err(cx));
1769 }),
1770 )
1771 .on_action(
1772 cx.listener(|pane: &mut Self, action: &CloseCleanItems, cx| {
1773 pane.close_clean_items(action, cx)
1774 .map(|task| task.detach_and_log_err(cx));
1775 }),
1776 )
1777 .on_action(
1778 cx.listener(|pane: &mut Self, action: &CloseItemsToTheLeft, cx| {
1779 pane.close_items_to_the_left(action, cx)
1780 .map(|task| task.detach_and_log_err(cx));
1781 }),
1782 )
1783 .on_action(
1784 cx.listener(|pane: &mut Self, action: &CloseItemsToTheRight, cx| {
1785 pane.close_items_to_the_right(action, cx)
1786 .map(|task| task.detach_and_log_err(cx));
1787 }),
1788 )
1789 .on_action(cx.listener(|pane: &mut Self, action: &CloseAllItems, cx| {
1790 pane.close_all_items(action, cx)
1791 .map(|task| task.detach_and_log_err(cx));
1792 }))
1793 .on_action(
1794 cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
1795 pane.close_active_item(action, cx)
1796 .map(|task| task.detach_and_log_err(cx));
1797 }),
1798 )
1799 .on_action(
1800 cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, cx| {
1801 pane.project.update(cx, |_, cx| {
1802 cx.emit(project::Event::RevealInProjectPanel(
1803 ProjectEntryId::from_proto(action.entry_id),
1804 ))
1805 })
1806 }),
1807 )
1808 .when(self.active_item().is_some(), |pane| {
1809 pane.child(self.render_tab_bar(cx))
1810 })
1811 .child({
1812 let has_worktrees = self.project.read(cx).worktrees().next().is_some();
1813 // main content
1814 div()
1815 .flex_1()
1816 .relative()
1817 .group("")
1818 .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
1819 .on_drag_move::<ProjectEntryId>(cx.listener(Self::handle_drag_move))
1820 .on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
1821 .map(|div| {
1822 if let Some(item) = self.active_item() {
1823 div.v_flex()
1824 .child(self.toolbar.clone())
1825 .child(item.to_any())
1826 } else {
1827 let placeholder = div.h_flex().size_full().justify_center();
1828 if has_worktrees {
1829 placeholder
1830 } else {
1831 placeholder.child(
1832 Label::new("Open a file or project to get started.")
1833 .color(Color::Muted),
1834 )
1835 }
1836 }
1837 })
1838 .child(
1839 // drag target
1840 div()
1841 .z_index(1)
1842 .invisible()
1843 .absolute()
1844 .bg(theme::color_alpha(
1845 cx.theme().colors().drop_target_background,
1846 0.75,
1847 ))
1848 .group_drag_over::<DraggedTab>("", |style| style.visible())
1849 .group_drag_over::<ProjectEntryId>("", |style| style.visible())
1850 .group_drag_over::<ExternalPaths>("", |style| style.visible())
1851 .when_some(self.can_drop_predicate.clone(), |this, p| {
1852 this.can_drop(move |a, cx| p(a, cx))
1853 })
1854 .on_drop(cx.listener(move |this, dragged_tab, cx| {
1855 this.handle_tab_drop(dragged_tab, this.active_item_index(), cx)
1856 }))
1857 .on_drop(cx.listener(move |this, entry_id, cx| {
1858 this.handle_project_entry_drop(entry_id, cx)
1859 }))
1860 .on_drop(cx.listener(move |this, paths, cx| {
1861 this.handle_external_paths_drop(paths, cx)
1862 }))
1863 .map(|div| match self.drag_split_direction {
1864 None => div.top_0().left_0().right_0().bottom_0(),
1865 Some(SplitDirection::Up) => div.top_0().left_0().right_0().h_32(),
1866 Some(SplitDirection::Down) => {
1867 div.left_0().bottom_0().right_0().h_32()
1868 }
1869 Some(SplitDirection::Left) => {
1870 div.top_0().left_0().bottom_0().w_32()
1871 }
1872 Some(SplitDirection::Right) => {
1873 div.top_0().bottom_0().right_0().w_32()
1874 }
1875 }),
1876 )
1877 })
1878 .on_mouse_down(
1879 MouseButton::Navigate(NavigationDirection::Back),
1880 cx.listener(|pane, _, cx| {
1881 if let Some(workspace) = pane.workspace.upgrade() {
1882 let pane = cx.view().downgrade();
1883 cx.window_context().defer(move |cx| {
1884 workspace.update(cx, |workspace, cx| {
1885 workspace.go_back(pane, cx).detach_and_log_err(cx)
1886 })
1887 })
1888 }
1889 }),
1890 )
1891 .on_mouse_down(
1892 MouseButton::Navigate(NavigationDirection::Forward),
1893 cx.listener(|pane, _, cx| {
1894 if let Some(workspace) = pane.workspace.upgrade() {
1895 let pane = cx.view().downgrade();
1896 cx.window_context().defer(move |cx| {
1897 workspace.update(cx, |workspace, cx| {
1898 workspace.go_forward(pane, cx).detach_and_log_err(cx)
1899 })
1900 })
1901 }
1902 }),
1903 )
1904 }
1905}
1906
1907impl ItemNavHistory {
1908 pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut WindowContext) {
1909 self.history.push(data, self.item.clone(), cx);
1910 }
1911
1912 pub fn pop_backward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
1913 self.history.pop(NavigationMode::GoingBack, cx)
1914 }
1915
1916 pub fn pop_forward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
1917 self.history.pop(NavigationMode::GoingForward, cx)
1918 }
1919}
1920
1921impl NavHistory {
1922 pub fn for_each_entry(
1923 &self,
1924 cx: &AppContext,
1925 mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
1926 ) {
1927 let borrowed_history = self.0.lock();
1928 borrowed_history
1929 .forward_stack
1930 .iter()
1931 .chain(borrowed_history.backward_stack.iter())
1932 .chain(borrowed_history.closed_stack.iter())
1933 .for_each(|entry| {
1934 if let Some(project_and_abs_path) =
1935 borrowed_history.paths_by_item.get(&entry.item.id())
1936 {
1937 f(entry, project_and_abs_path.clone());
1938 } else if let Some(item) = entry.item.upgrade() {
1939 if let Some(path) = item.project_path(cx) {
1940 f(entry, (path, None));
1941 }
1942 }
1943 })
1944 }
1945
1946 pub fn set_mode(&mut self, mode: NavigationMode) {
1947 self.0.lock().mode = mode;
1948 }
1949
1950 pub fn mode(&self) -> NavigationMode {
1951 self.0.lock().mode
1952 }
1953
1954 pub fn disable(&mut self) {
1955 self.0.lock().mode = NavigationMode::Disabled;
1956 }
1957
1958 pub fn enable(&mut self) {
1959 self.0.lock().mode = NavigationMode::Normal;
1960 }
1961
1962 pub fn pop(&mut self, mode: NavigationMode, cx: &mut WindowContext) -> Option<NavigationEntry> {
1963 let mut state = self.0.lock();
1964 let entry = match mode {
1965 NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
1966 return None
1967 }
1968 NavigationMode::GoingBack => &mut state.backward_stack,
1969 NavigationMode::GoingForward => &mut state.forward_stack,
1970 NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
1971 }
1972 .pop_back();
1973 if entry.is_some() {
1974 state.did_update(cx);
1975 }
1976 entry
1977 }
1978
1979 pub fn push<D: 'static + Send + Any>(
1980 &mut self,
1981 data: Option<D>,
1982 item: Arc<dyn WeakItemHandle>,
1983 cx: &mut WindowContext,
1984 ) {
1985 let state = &mut *self.0.lock();
1986 match state.mode {
1987 NavigationMode::Disabled => {}
1988 NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
1989 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
1990 state.backward_stack.pop_front();
1991 }
1992 state.backward_stack.push_back(NavigationEntry {
1993 item,
1994 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
1995 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
1996 });
1997 state.forward_stack.clear();
1998 }
1999 NavigationMode::GoingBack => {
2000 if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2001 state.forward_stack.pop_front();
2002 }
2003 state.forward_stack.push_back(NavigationEntry {
2004 item,
2005 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2006 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2007 });
2008 }
2009 NavigationMode::GoingForward => {
2010 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2011 state.backward_stack.pop_front();
2012 }
2013 state.backward_stack.push_back(NavigationEntry {
2014 item,
2015 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2016 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2017 });
2018 }
2019 NavigationMode::ClosingItem => {
2020 if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2021 state.closed_stack.pop_front();
2022 }
2023 state.closed_stack.push_back(NavigationEntry {
2024 item,
2025 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2026 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2027 });
2028 }
2029 }
2030 state.did_update(cx);
2031 }
2032
2033 pub fn remove_item(&mut self, item_id: EntityId) {
2034 let mut state = self.0.lock();
2035 state.paths_by_item.remove(&item_id);
2036 state
2037 .backward_stack
2038 .retain(|entry| entry.item.id() != item_id);
2039 state
2040 .forward_stack
2041 .retain(|entry| entry.item.id() != item_id);
2042 state
2043 .closed_stack
2044 .retain(|entry| entry.item.id() != item_id);
2045 }
2046
2047 pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
2048 self.0.lock().paths_by_item.get(&item_id).cloned()
2049 }
2050}
2051
2052impl NavHistoryState {
2053 pub fn did_update(&self, cx: &mut WindowContext) {
2054 if let Some(pane) = self.pane.upgrade() {
2055 cx.defer(move |cx| {
2056 pane.update(cx, |pane, cx| pane.history_updated(cx));
2057 });
2058 }
2059 }
2060}
2061
2062fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
2063 let path = buffer_path
2064 .as_ref()
2065 .and_then(|p| p.path.to_str())
2066 .unwrap_or("This buffer");
2067 let path = truncate_and_remove_front(path, 80);
2068 format!("{path} contains unsaved edits. Do you want to save it?")
2069}
2070
2071#[cfg(test)]
2072mod tests {
2073 use super::*;
2074 use crate::item::test::{TestItem, TestProjectItem};
2075 use gpui::{TestAppContext, VisualTestContext};
2076 use project::FakeFs;
2077 use settings::SettingsStore;
2078 use theme::LoadThemes;
2079
2080 #[gpui::test]
2081 async fn test_remove_active_empty(cx: &mut TestAppContext) {
2082 init_test(cx);
2083 let fs = FakeFs::new(cx.executor());
2084
2085 let project = Project::test(fs, None, cx).await;
2086 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2087 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2088
2089 pane.update(cx, |pane, cx| {
2090 assert!(pane
2091 .close_active_item(&CloseActiveItem { save_intent: None }, cx)
2092 .is_none())
2093 });
2094 }
2095
2096 #[gpui::test]
2097 async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
2098 init_test(cx);
2099 let fs = FakeFs::new(cx.executor());
2100
2101 let project = Project::test(fs, None, cx).await;
2102 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2103 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2104
2105 // 1. Add with a destination index
2106 // a. Add before the active item
2107 set_labeled_items(&pane, ["A", "B*", "C"], cx);
2108 pane.update(cx, |pane, cx| {
2109 pane.add_item(
2110 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2111 false,
2112 false,
2113 Some(0),
2114 cx,
2115 );
2116 });
2117 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
2118
2119 // b. Add after the active item
2120 set_labeled_items(&pane, ["A", "B*", "C"], cx);
2121 pane.update(cx, |pane, cx| {
2122 pane.add_item(
2123 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2124 false,
2125 false,
2126 Some(2),
2127 cx,
2128 );
2129 });
2130 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
2131
2132 // c. Add at the end of the item list (including off the length)
2133 set_labeled_items(&pane, ["A", "B*", "C"], cx);
2134 pane.update(cx, |pane, cx| {
2135 pane.add_item(
2136 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2137 false,
2138 false,
2139 Some(5),
2140 cx,
2141 );
2142 });
2143 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2144
2145 // 2. Add without a destination index
2146 // a. Add with active item at the start of the item list
2147 set_labeled_items(&pane, ["A*", "B", "C"], cx);
2148 pane.update(cx, |pane, cx| {
2149 pane.add_item(
2150 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2151 false,
2152 false,
2153 None,
2154 cx,
2155 );
2156 });
2157 set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
2158
2159 // b. Add with active item at the end of the item list
2160 set_labeled_items(&pane, ["A", "B", "C*"], cx);
2161 pane.update(cx, |pane, cx| {
2162 pane.add_item(
2163 Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
2164 false,
2165 false,
2166 None,
2167 cx,
2168 );
2169 });
2170 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2171 }
2172
2173 #[gpui::test]
2174 async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
2175 init_test(cx);
2176 let fs = FakeFs::new(cx.executor());
2177
2178 let project = Project::test(fs, None, cx).await;
2179 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2180 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2181
2182 // 1. Add with a destination index
2183 // 1a. Add before the active item
2184 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2185 pane.update(cx, |pane, cx| {
2186 pane.add_item(d, false, false, Some(0), cx);
2187 });
2188 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
2189
2190 // 1b. Add after the active item
2191 let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2192 pane.update(cx, |pane, cx| {
2193 pane.add_item(d, false, false, Some(2), cx);
2194 });
2195 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
2196
2197 // 1c. Add at the end of the item list (including off the length)
2198 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2199 pane.update(cx, |pane, cx| {
2200 pane.add_item(a, false, false, Some(5), cx);
2201 });
2202 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
2203
2204 // 1d. Add same item to active index
2205 let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
2206 pane.update(cx, |pane, cx| {
2207 pane.add_item(b, false, false, Some(1), cx);
2208 });
2209 assert_item_labels(&pane, ["A", "B*", "C"], cx);
2210
2211 // 1e. Add item to index after same item in last position
2212 let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
2213 pane.update(cx, |pane, cx| {
2214 pane.add_item(c, false, false, Some(2), cx);
2215 });
2216 assert_item_labels(&pane, ["A", "B", "C*"], cx);
2217
2218 // 2. Add without a destination index
2219 // 2a. Add with active item at the start of the item list
2220 let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
2221 pane.update(cx, |pane, cx| {
2222 pane.add_item(d, false, false, None, cx);
2223 });
2224 assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
2225
2226 // 2b. Add with active item at the end of the item list
2227 let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
2228 pane.update(cx, |pane, cx| {
2229 pane.add_item(a, false, false, None, cx);
2230 });
2231 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
2232
2233 // 2c. Add active item to active item at end of list
2234 let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
2235 pane.update(cx, |pane, cx| {
2236 pane.add_item(c, false, false, None, cx);
2237 });
2238 assert_item_labels(&pane, ["A", "B", "C*"], cx);
2239
2240 // 2d. Add active item to active item at start of list
2241 let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
2242 pane.update(cx, |pane, cx| {
2243 pane.add_item(a, false, false, None, cx);
2244 });
2245 assert_item_labels(&pane, ["A*", "B", "C"], cx);
2246 }
2247
2248 #[gpui::test]
2249 async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
2250 init_test(cx);
2251 let fs = FakeFs::new(cx.executor());
2252
2253 let project = Project::test(fs, None, cx).await;
2254 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2255 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2256
2257 // singleton view
2258 pane.update(cx, |pane, cx| {
2259 pane.add_item(
2260 Box::new(cx.new_view(|cx| {
2261 TestItem::new(cx)
2262 .with_singleton(true)
2263 .with_label("buffer 1")
2264 .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
2265 })),
2266 false,
2267 false,
2268 None,
2269 cx,
2270 );
2271 });
2272 assert_item_labels(&pane, ["buffer 1*"], cx);
2273
2274 // new singleton view with the same project entry
2275 pane.update(cx, |pane, cx| {
2276 pane.add_item(
2277 Box::new(cx.new_view(|cx| {
2278 TestItem::new(cx)
2279 .with_singleton(true)
2280 .with_label("buffer 1")
2281 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
2282 })),
2283 false,
2284 false,
2285 None,
2286 cx,
2287 );
2288 });
2289 assert_item_labels(&pane, ["buffer 1*"], cx);
2290
2291 // new singleton view with different project entry
2292 pane.update(cx, |pane, cx| {
2293 pane.add_item(
2294 Box::new(cx.new_view(|cx| {
2295 TestItem::new(cx)
2296 .with_singleton(true)
2297 .with_label("buffer 2")
2298 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
2299 })),
2300 false,
2301 false,
2302 None,
2303 cx,
2304 );
2305 });
2306 assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
2307
2308 // new multibuffer view with the same project entry
2309 pane.update(cx, |pane, cx| {
2310 pane.add_item(
2311 Box::new(cx.new_view(|cx| {
2312 TestItem::new(cx)
2313 .with_singleton(false)
2314 .with_label("multibuffer 1")
2315 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
2316 })),
2317 false,
2318 false,
2319 None,
2320 cx,
2321 );
2322 });
2323 assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
2324
2325 // another multibuffer view with the same project entry
2326 pane.update(cx, |pane, cx| {
2327 pane.add_item(
2328 Box::new(cx.new_view(|cx| {
2329 TestItem::new(cx)
2330 .with_singleton(false)
2331 .with_label("multibuffer 1b")
2332 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
2333 })),
2334 false,
2335 false,
2336 None,
2337 cx,
2338 );
2339 });
2340 assert_item_labels(
2341 &pane,
2342 ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
2343 cx,
2344 );
2345 }
2346
2347 #[gpui::test]
2348 async fn test_remove_item_ordering(cx: &mut TestAppContext) {
2349 init_test(cx);
2350 let fs = FakeFs::new(cx.executor());
2351
2352 let project = Project::test(fs, None, cx).await;
2353 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2354 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2355
2356 add_labeled_item(&pane, "A", false, cx);
2357 add_labeled_item(&pane, "B", false, cx);
2358 add_labeled_item(&pane, "C", false, cx);
2359 add_labeled_item(&pane, "D", false, cx);
2360 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2361
2362 pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
2363 add_labeled_item(&pane, "1", false, cx);
2364 assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
2365
2366 pane.update(cx, |pane, cx| {
2367 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
2368 })
2369 .unwrap()
2370 .await
2371 .unwrap();
2372 assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
2373
2374 pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
2375 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2376
2377 pane.update(cx, |pane, cx| {
2378 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
2379 })
2380 .unwrap()
2381 .await
2382 .unwrap();
2383 assert_item_labels(&pane, ["A", "B*", "C"], cx);
2384
2385 pane.update(cx, |pane, cx| {
2386 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
2387 })
2388 .unwrap()
2389 .await
2390 .unwrap();
2391 assert_item_labels(&pane, ["A", "C*"], cx);
2392
2393 pane.update(cx, |pane, cx| {
2394 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
2395 })
2396 .unwrap()
2397 .await
2398 .unwrap();
2399 assert_item_labels(&pane, ["A*"], cx);
2400 }
2401
2402 #[gpui::test]
2403 async fn test_close_inactive_items(cx: &mut TestAppContext) {
2404 init_test(cx);
2405 let fs = FakeFs::new(cx.executor());
2406
2407 let project = Project::test(fs, None, cx).await;
2408 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2409 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2410
2411 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
2412
2413 pane.update(cx, |pane, cx| {
2414 pane.close_inactive_items(&CloseInactiveItems, cx)
2415 })
2416 .unwrap()
2417 .await
2418 .unwrap();
2419 assert_item_labels(&pane, ["C*"], cx);
2420 }
2421
2422 #[gpui::test]
2423 async fn test_close_clean_items(cx: &mut TestAppContext) {
2424 init_test(cx);
2425 let fs = FakeFs::new(cx.executor());
2426
2427 let project = Project::test(fs, None, cx).await;
2428 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2429 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2430
2431 add_labeled_item(&pane, "A", true, cx);
2432 add_labeled_item(&pane, "B", false, cx);
2433 add_labeled_item(&pane, "C", true, cx);
2434 add_labeled_item(&pane, "D", false, cx);
2435 add_labeled_item(&pane, "E", false, cx);
2436 assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
2437
2438 pane.update(cx, |pane, cx| pane.close_clean_items(&CloseCleanItems, cx))
2439 .unwrap()
2440 .await
2441 .unwrap();
2442 assert_item_labels(&pane, ["A^", "C*^"], cx);
2443 }
2444
2445 #[gpui::test]
2446 async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
2447 init_test(cx);
2448 let fs = FakeFs::new(cx.executor());
2449
2450 let project = Project::test(fs, None, cx).await;
2451 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2452 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2453
2454 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
2455
2456 pane.update(cx, |pane, cx| {
2457 pane.close_items_to_the_left(&CloseItemsToTheLeft, cx)
2458 })
2459 .unwrap()
2460 .await
2461 .unwrap();
2462 assert_item_labels(&pane, ["C*", "D", "E"], cx);
2463 }
2464
2465 #[gpui::test]
2466 async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
2467 init_test(cx);
2468 let fs = FakeFs::new(cx.executor());
2469
2470 let project = Project::test(fs, None, cx).await;
2471 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2472 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2473
2474 set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
2475
2476 pane.update(cx, |pane, cx| {
2477 pane.close_items_to_the_right(&CloseItemsToTheRight, cx)
2478 })
2479 .unwrap()
2480 .await
2481 .unwrap();
2482 assert_item_labels(&pane, ["A", "B", "C*"], cx);
2483 }
2484
2485 #[gpui::test]
2486 async fn test_close_all_items(cx: &mut TestAppContext) {
2487 init_test(cx);
2488 let fs = FakeFs::new(cx.executor());
2489
2490 let project = Project::test(fs, None, cx).await;
2491 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
2492 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
2493
2494 add_labeled_item(&pane, "A", false, cx);
2495 add_labeled_item(&pane, "B", false, cx);
2496 add_labeled_item(&pane, "C", false, cx);
2497 assert_item_labels(&pane, ["A", "B", "C*"], cx);
2498
2499 pane.update(cx, |pane, cx| {
2500 pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
2501 })
2502 .unwrap()
2503 .await
2504 .unwrap();
2505 assert_item_labels(&pane, [], cx);
2506
2507 add_labeled_item(&pane, "A", true, cx);
2508 add_labeled_item(&pane, "B", true, cx);
2509 add_labeled_item(&pane, "C", true, cx);
2510 assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
2511
2512 let save = pane
2513 .update(cx, |pane, cx| {
2514 pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
2515 })
2516 .unwrap();
2517
2518 cx.executor().run_until_parked();
2519 cx.simulate_prompt_answer(2);
2520 save.await.unwrap();
2521 assert_item_labels(&pane, [], cx);
2522 }
2523
2524 fn init_test(cx: &mut TestAppContext) {
2525 cx.update(|cx| {
2526 let settings_store = SettingsStore::test(cx);
2527 cx.set_global(settings_store);
2528 theme::init(LoadThemes::JustBase, cx);
2529 crate::init_settings(cx);
2530 Project::init_settings(cx);
2531 });
2532 }
2533
2534 fn add_labeled_item(
2535 pane: &View<Pane>,
2536 label: &str,
2537 is_dirty: bool,
2538 cx: &mut VisualTestContext,
2539 ) -> Box<View<TestItem>> {
2540 pane.update(cx, |pane, cx| {
2541 let labeled_item = Box::new(
2542 cx.new_view(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)),
2543 );
2544 pane.add_item(labeled_item.clone(), false, false, None, cx);
2545 labeled_item
2546 })
2547 }
2548
2549 fn set_labeled_items<const COUNT: usize>(
2550 pane: &View<Pane>,
2551 labels: [&str; COUNT],
2552 cx: &mut VisualTestContext,
2553 ) -> [Box<View<TestItem>>; COUNT] {
2554 pane.update(cx, |pane, cx| {
2555 pane.items.clear();
2556 let mut active_item_index = 0;
2557
2558 let mut index = 0;
2559 let items = labels.map(|mut label| {
2560 if label.ends_with("*") {
2561 label = label.trim_end_matches("*");
2562 active_item_index = index;
2563 }
2564
2565 let labeled_item = Box::new(cx.new_view(|cx| TestItem::new(cx).with_label(label)));
2566 pane.add_item(labeled_item.clone(), false, false, None, cx);
2567 index += 1;
2568 labeled_item
2569 });
2570
2571 pane.activate_item(active_item_index, false, false, cx);
2572
2573 items
2574 })
2575 }
2576
2577 // Assert the item label, with the active item label suffixed with a '*'
2578 fn assert_item_labels<const COUNT: usize>(
2579 pane: &View<Pane>,
2580 expected_states: [&str; COUNT],
2581 cx: &mut VisualTestContext,
2582 ) {
2583 pane.update(cx, |pane, cx| {
2584 let actual_states = pane
2585 .items
2586 .iter()
2587 .enumerate()
2588 .map(|(ix, item)| {
2589 let mut state = item
2590 .to_any()
2591 .downcast::<TestItem>()
2592 .unwrap()
2593 .read(cx)
2594 .label
2595 .clone();
2596 if ix == pane.active_item_index {
2597 state.push('*');
2598 }
2599 if item.is_dirty(cx) {
2600 state.push('^');
2601 }
2602 state
2603 })
2604 .collect::<Vec<_>>();
2605
2606 assert_eq!(
2607 actual_states, expected_states,
2608 "pane items do not match expectation"
2609 );
2610 })
2611 }
2612}
2613
2614impl Render for DraggedTab {
2615 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2616 let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
2617 let label = self.item.tab_content(Some(self.detail), false, cx);
2618 Tab::new("")
2619 .selected(self.is_active)
2620 .child(label)
2621 .render(cx)
2622 .font(ui_font)
2623 }
2624}