1use crate::{
2 item::{Item, ItemHandle, ItemSettings, WeakItemHandle},
3 toolbar::Toolbar,
4 workspace_settings::{AutosaveSetting, WorkspaceSettings},
5 SplitDirection, Workspace,
6};
7use anyhow::Result;
8use collections::{HashMap, HashSet, VecDeque};
9use gpui::{
10 actions, prelude::*, Action, AppContext, AsyncWindowContext, Div, EntityId, EventEmitter,
11 FocusHandle, Focusable, FocusableView, Model, Pixels, Point, PromptLevel, Render, Task, View,
12 ViewContext, VisualContext, WeakView, WindowContext,
13};
14use parking_lot::Mutex;
15use project::{Project, ProjectEntryId, ProjectPath};
16use serde::Deserialize;
17use settings::Settings;
18use std::{
19 any::Any,
20 cmp, fmt, mem,
21 path::{Path, PathBuf},
22 sync::{
23 atomic::{AtomicUsize, Ordering},
24 Arc,
25 },
26};
27
28use ui::v_stack;
29use ui::{prelude::*, Color, Icon, IconButton, IconElement, Tooltip};
30use util::truncate_and_remove_front;
31
32#[derive(PartialEq, Clone, Copy, Deserialize, Debug)]
33#[serde(rename_all = "camelCase")]
34pub enum SaveIntent {
35 /// write all files (even if unchanged)
36 /// prompt before overwriting on-disk changes
37 Save,
38 /// write any files that have local changes
39 /// prompt before overwriting on-disk changes
40 SaveAll,
41 /// always prompt for a new path
42 SaveAs,
43 /// prompt "you have unsaved changes" before writing
44 Close,
45 /// write all dirty files, don't prompt on conflict
46 Overwrite,
47 /// skip all save-related behavior
48 Skip,
49}
50
51//todo!("Do we need the default bound on actions? Decide soon")
52// #[register_action]
53#[derive(Clone, Deserialize, PartialEq, Debug)]
54pub struct ActivateItem(pub usize);
55
56// #[derive(Clone, PartialEq)]
57// pub struct CloseItemById {
58// pub item_id: usize,
59// pub pane: WeakView<Pane>,
60// }
61
62// #[derive(Clone, PartialEq)]
63// pub struct CloseItemsToTheLeftById {
64// pub item_id: usize,
65// pub pane: WeakView<Pane>,
66// }
67
68// #[derive(Clone, PartialEq)]
69// pub struct CloseItemsToTheRightById {
70// pub item_id: usize,
71// pub pane: WeakView<Pane>,
72// }
73
74#[derive(Clone, PartialEq, Debug, Deserialize, Default, Action)]
75#[serde(rename_all = "camelCase")]
76pub struct CloseActiveItem {
77 pub save_intent: Option<SaveIntent>,
78}
79
80#[derive(Clone, PartialEq, Debug, Deserialize, Default, Action)]
81#[serde(rename_all = "camelCase")]
82pub struct CloseAllItems {
83 pub save_intent: Option<SaveIntent>,
84}
85
86// todo!(These used to be under pane::{Action}. Are they now workspace::pane::{Action}?)
87actions!(
88 ActivatePrevItem,
89 ActivateNextItem,
90 ActivateLastItem,
91 CloseInactiveItems,
92 CloseCleanItems,
93 CloseItemsToTheLeft,
94 CloseItemsToTheRight,
95 GoBack,
96 GoForward,
97 ReopenClosedItem,
98 SplitLeft,
99 SplitUp,
100 SplitRight,
101 SplitDown,
102);
103
104const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
105
106pub enum Event {
107 AddItem { item: Box<dyn ItemHandle> },
108 ActivateItem { local: bool },
109 Remove,
110 RemoveItem { item_id: EntityId },
111 Split(SplitDirection),
112 ChangeItemTitle,
113 Focus,
114 ZoomIn,
115 ZoomOut,
116}
117
118impl fmt::Debug for Event {
119 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120 match self {
121 Event::AddItem { item } => f
122 .debug_struct("AddItem")
123 .field("item", &item.item_id())
124 .finish(),
125 Event::ActivateItem { local } => f
126 .debug_struct("ActivateItem")
127 .field("local", local)
128 .finish(),
129 Event::Remove => f.write_str("Remove"),
130 Event::RemoveItem { item_id } => f
131 .debug_struct("RemoveItem")
132 .field("item_id", item_id)
133 .finish(),
134 Event::Split(direction) => f
135 .debug_struct("Split")
136 .field("direction", direction)
137 .finish(),
138 Event::ChangeItemTitle => f.write_str("ChangeItemTitle"),
139 Event::Focus => f.write_str("Focus"),
140 Event::ZoomIn => f.write_str("ZoomIn"),
141 Event::ZoomOut => f.write_str("ZoomOut"),
142 }
143 }
144}
145
146pub struct Pane {
147 focus_handle: FocusHandle,
148 items: Vec<Box<dyn ItemHandle>>,
149 activation_history: Vec<EntityId>,
150 zoomed: bool,
151 active_item_index: usize,
152 // last_focused_view_by_item: HashMap<usize, AnyWeakViewHandle>,
153 autoscroll: bool,
154 nav_history: NavHistory,
155 toolbar: View<Toolbar>,
156 // tab_bar_context_menu: TabBarContextMenu,
157 // tab_context_menu: ViewHandle<ContextMenu>,
158 workspace: WeakView<Workspace>,
159 project: Model<Project>,
160 // can_drop: Rc<dyn Fn(&DragAndDrop<Workspace>, &WindowContext) -> bool>,
161 can_split: bool,
162 // render_tab_bar_buttons: Rc<dyn Fn(&mut Pane, &mut ViewContext<Pane>) -> AnyElement<Pane>>,
163}
164
165pub struct ItemNavHistory {
166 history: NavHistory,
167 item: Arc<dyn WeakItemHandle>,
168}
169
170#[derive(Clone)]
171pub struct NavHistory(Arc<Mutex<NavHistoryState>>);
172
173struct NavHistoryState {
174 mode: NavigationMode,
175 backward_stack: VecDeque<NavigationEntry>,
176 forward_stack: VecDeque<NavigationEntry>,
177 closed_stack: VecDeque<NavigationEntry>,
178 paths_by_item: HashMap<EntityId, (ProjectPath, Option<PathBuf>)>,
179 pane: WeakView<Pane>,
180 next_timestamp: Arc<AtomicUsize>,
181}
182
183#[derive(Copy, Clone)]
184pub enum NavigationMode {
185 Normal,
186 GoingBack,
187 GoingForward,
188 ClosingItem,
189 ReopeningClosedItem,
190 Disabled,
191}
192
193impl Default for NavigationMode {
194 fn default() -> Self {
195 Self::Normal
196 }
197}
198
199pub struct NavigationEntry {
200 pub item: Arc<dyn WeakItemHandle>,
201 pub data: Option<Box<dyn Any + Send>>,
202 pub timestamp: usize,
203}
204
205// pub struct DraggedItem {
206// pub handle: Box<dyn ItemHandle>,
207// pub pane: WeakView<Pane>,
208// }
209
210// pub enum ReorderBehavior {
211// None,
212// MoveAfterActive,
213// MoveToIndex(usize),
214// }
215
216// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
217// enum TabBarContextMenuKind {
218// New,
219// Split,
220// }
221
222// struct TabBarContextMenu {
223// kind: TabBarContextMenuKind,
224// handle: ViewHandle<ContextMenu>,
225// }
226
227// impl TabBarContextMenu {
228// fn handle_if_kind(&self, kind: TabBarContextMenuKind) -> Option<ViewHandle<ContextMenu>> {
229// if self.kind == kind {
230// return Some(self.handle.clone());
231// }
232// None
233// }
234// }
235
236// #[allow(clippy::too_many_arguments)]
237// fn nav_button<A: Action, F: 'static + Fn(&mut Pane, &mut ViewContext<Pane>)>(
238// svg_path: &'static str,
239// style: theme2::Interactive<theme2::IconButton>,
240// nav_button_height: f32,
241// tooltip_style: TooltipStyle,
242// enabled: bool,
243// on_click: F,
244// tooltip_action: A,
245// action_name: &str,
246// cx: &mut ViewContext<Pane>,
247// ) -> AnyElement<Pane> {
248// MouseEventHandler::new::<A, _>(0, cx, |state, _| {
249// let style = if enabled {
250// style.style_for(state)
251// } else {
252// style.disabled_style()
253// };
254// Svg::new(svg_path)
255// .with_color(style.color)
256// .constrained()
257// .with_width(style.icon_width)
258// .aligned()
259// .contained()
260// .with_style(style.container)
261// .constrained()
262// .with_width(style.button_width)
263// .with_height(nav_button_height)
264// .aligned()
265// .top()
266// })
267// .with_cursor_style(if enabled {
268// CursorStyle::PointingHand
269// } else {
270// CursorStyle::default()
271// })
272// .on_click(MouseButton::Left, move |_, toolbar, cx| {
273// on_click(toolbar, cx)
274// })
275// .with_tooltip::<A>(
276// 0,
277// action_name.to_string(),
278// Some(Box::new(tooltip_action)),
279// tooltip_style,
280// cx,
281// )
282// .contained()
283// .into_any_named("nav button")
284// }
285
286impl EventEmitter<Event> for Pane {}
287
288impl Pane {
289 pub fn new(
290 workspace: WeakView<Workspace>,
291 project: Model<Project>,
292 next_timestamp: Arc<AtomicUsize>,
293 cx: &mut ViewContext<Self>,
294 ) -> Self {
295 // todo!("context menu")
296 // let pane_view_id = cx.view_id();
297 // let context_menu = cx.add_view(|cx| ContextMenu::new(pane_view_id, cx));
298 // context_menu.update(cx, |menu, _| {
299 // menu.set_position_mode(OverlayPositionMode::Local)
300 // });
301
302 let handle = cx.view().downgrade();
303 Self {
304 focus_handle: cx.focus_handle(),
305 items: Vec::new(),
306 activation_history: Vec::new(),
307 zoomed: false,
308 active_item_index: 0,
309 // last_focused_view_by_item: Default::default(),
310 autoscroll: false,
311 nav_history: NavHistory(Arc::new(Mutex::new(NavHistoryState {
312 mode: NavigationMode::Normal,
313 backward_stack: Default::default(),
314 forward_stack: Default::default(),
315 closed_stack: Default::default(),
316 paths_by_item: Default::default(),
317 pane: handle.clone(),
318 next_timestamp,
319 }))),
320 toolbar: cx.build_view(|_| Toolbar::new()),
321 // tab_bar_context_menu: TabBarContextMenu {
322 // kind: TabBarContextMenuKind::New,
323 // handle: context_menu,
324 // },
325 // tab_context_menu: cx.add_view(|cx| ContextMenu::new(pane_view_id, cx)),
326 workspace,
327 project,
328 // can_drop: Rc::new(|_, _| true),
329 can_split: true,
330 // render_tab_bar_buttons: Rc::new(move |pane, cx| {
331 // Flex::row()
332 // // New menu
333 // .with_child(Self::render_tab_bar_button(
334 // 0,
335 // "icons/plus.svg",
336 // false,
337 // Some(("New...".into(), None)),
338 // cx,
339 // |pane, cx| pane.deploy_new_menu(cx),
340 // |pane, cx| {
341 // pane.tab_bar_context_menu
342 // .handle
343 // .update(cx, |menu, _| menu.delay_cancel())
344 // },
345 // pane.tab_bar_context_menu
346 // .handle_if_kind(TabBarContextMenuKind::New),
347 // ))
348 // .with_child(Self::render_tab_bar_button(
349 // 1,
350 // "icons/split.svg",
351 // false,
352 // Some(("Split Pane".into(), None)),
353 // cx,
354 // |pane, cx| pane.deploy_split_menu(cx),
355 // |pane, cx| {
356 // pane.tab_bar_context_menu
357 // .handle
358 // .update(cx, |menu, _| menu.delay_cancel())
359 // },
360 // pane.tab_bar_context_menu
361 // .handle_if_kind(TabBarContextMenuKind::Split),
362 // ))
363 // .with_child({
364 // let icon_path;
365 // let tooltip_label;
366 // if pane.is_zoomed() {
367 // icon_path = "icons/minimize.svg";
368 // tooltip_label = "Zoom In";
369 // } else {
370 // icon_path = "icons/maximize.svg";
371 // tooltip_label = "Zoom In";
372 // }
373
374 // Pane::render_tab_bar_button(
375 // 2,
376 // icon_path,
377 // pane.is_zoomed(),
378 // Some((tooltip_label, Some(Box::new(ToggleZoom)))),
379 // cx,
380 // move |pane, cx| pane.toggle_zoom(&Default::default(), cx),
381 // move |_, _| {},
382 // None,
383 // )
384 // })
385 // .into_any()
386 // }),
387 }
388 }
389
390 pub(crate) fn workspace(&self) -> &WeakView<Workspace> {
391 &self.workspace
392 }
393
394 pub fn has_focus(&self, cx: &WindowContext) -> bool {
395 self.focus_handle.contains_focused(cx)
396 }
397
398 pub fn active_item_index(&self) -> usize {
399 self.active_item_index
400 }
401
402 // pub fn on_can_drop<F>(&mut self, can_drop: F)
403 // where
404 // F: 'static + Fn(&DragAndDrop<Workspace>, &WindowContext) -> bool,
405 // {
406 // self.can_drop = Rc::new(can_drop);
407 // }
408
409 pub fn set_can_split(&mut self, can_split: bool, cx: &mut ViewContext<Self>) {
410 self.can_split = can_split;
411 cx.notify();
412 }
413
414 pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut ViewContext<Self>) {
415 self.toolbar.update(cx, |toolbar, cx| {
416 toolbar.set_can_navigate(can_navigate, cx);
417 });
418 cx.notify();
419 }
420
421 // pub fn set_render_tab_bar_buttons<F>(&mut self, cx: &mut ViewContext<Self>, render: F)
422 // where
423 // F: 'static + Fn(&mut Pane, &mut ViewContext<Pane>) -> AnyElement<Pane>,
424 // {
425 // self.render_tab_bar_buttons = Rc::new(render);
426 // cx.notify();
427 // }
428
429 pub fn nav_history_for_item<T: Item>(&self, item: &View<T>) -> ItemNavHistory {
430 ItemNavHistory {
431 history: self.nav_history.clone(),
432 item: Arc::new(item.downgrade()),
433 }
434 }
435
436 pub fn nav_history(&self) -> &NavHistory {
437 &self.nav_history
438 }
439
440 pub fn nav_history_mut(&mut self) -> &mut NavHistory {
441 &mut self.nav_history
442 }
443
444 pub fn disable_history(&mut self) {
445 self.nav_history.disable();
446 }
447
448 pub fn enable_history(&mut self) {
449 self.nav_history.enable();
450 }
451
452 pub fn can_navigate_backward(&self) -> bool {
453 !self.nav_history.0.lock().backward_stack.is_empty()
454 }
455
456 pub fn can_navigate_forward(&self) -> bool {
457 !self.nav_history.0.lock().forward_stack.is_empty()
458 }
459
460 fn navigate_backward(&mut self, cx: &mut ViewContext<Self>) {
461 if let Some(workspace) = self.workspace.upgrade() {
462 let pane = cx.view().downgrade();
463 cx.window_context().defer(move |cx| {
464 workspace.update(cx, |workspace, cx| {
465 workspace.go_back(pane, cx).detach_and_log_err(cx)
466 })
467 })
468 }
469 }
470
471 fn navigate_forward(&mut self, cx: &mut ViewContext<Self>) {
472 if let Some(workspace) = self.workspace.upgrade() {
473 let pane = cx.view().downgrade();
474 cx.window_context().defer(move |cx| {
475 workspace.update(cx, |workspace, cx| {
476 workspace.go_forward(pane, cx).detach_and_log_err(cx)
477 })
478 })
479 }
480 }
481
482 fn history_updated(&mut self, cx: &mut ViewContext<Self>) {
483 self.toolbar.update(cx, |_, cx| cx.notify());
484 }
485
486 pub(crate) fn open_item(
487 &mut self,
488 project_entry_id: ProjectEntryId,
489 focus_item: bool,
490 cx: &mut ViewContext<Self>,
491 build_item: impl FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
492 ) -> Box<dyn ItemHandle> {
493 let mut existing_item = None;
494 for (index, item) in self.items.iter().enumerate() {
495 if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [project_entry_id]
496 {
497 let item = item.boxed_clone();
498 existing_item = Some((index, item));
499 break;
500 }
501 }
502
503 if let Some((index, existing_item)) = existing_item {
504 self.activate_item(index, focus_item, focus_item, cx);
505 existing_item
506 } else {
507 let new_item = build_item(cx);
508 self.add_item(new_item.clone(), true, focus_item, None, cx);
509 new_item
510 }
511 }
512
513 pub fn add_item(
514 &mut self,
515 item: Box<dyn ItemHandle>,
516 activate_pane: bool,
517 focus_item: bool,
518 destination_index: Option<usize>,
519 cx: &mut ViewContext<Self>,
520 ) {
521 if item.is_singleton(cx) {
522 if let Some(&entry_id) = item.project_entry_ids(cx).get(0) {
523 let project = self.project.read(cx);
524 if let Some(project_path) = project.path_for_entry(entry_id, cx) {
525 let abs_path = project.absolute_path(&project_path, cx);
526 self.nav_history
527 .0
528 .lock()
529 .paths_by_item
530 .insert(item.item_id(), (project_path, abs_path));
531 }
532 }
533 }
534 // If no destination index is specified, add or move the item after the active item.
535 let mut insertion_index = {
536 cmp::min(
537 if let Some(destination_index) = destination_index {
538 destination_index
539 } else {
540 self.active_item_index + 1
541 },
542 self.items.len(),
543 )
544 };
545
546 // Does the item already exist?
547 let project_entry_id = if item.is_singleton(cx) {
548 item.project_entry_ids(cx).get(0).copied()
549 } else {
550 None
551 };
552
553 let existing_item_index = self.items.iter().position(|existing_item| {
554 if existing_item.item_id() == item.item_id() {
555 true
556 } else if existing_item.is_singleton(cx) {
557 existing_item
558 .project_entry_ids(cx)
559 .get(0)
560 .map_or(false, |existing_entry_id| {
561 Some(existing_entry_id) == project_entry_id.as_ref()
562 })
563 } else {
564 false
565 }
566 });
567
568 if let Some(existing_item_index) = existing_item_index {
569 // If the item already exists, move it to the desired destination and activate it
570
571 if existing_item_index != insertion_index {
572 let existing_item_is_active = existing_item_index == self.active_item_index;
573
574 // If the caller didn't specify a destination and the added item is already
575 // the active one, don't move it
576 if existing_item_is_active && destination_index.is_none() {
577 insertion_index = existing_item_index;
578 } else {
579 self.items.remove(existing_item_index);
580 if existing_item_index < self.active_item_index {
581 self.active_item_index -= 1;
582 }
583 insertion_index = insertion_index.min(self.items.len());
584
585 self.items.insert(insertion_index, item.clone());
586
587 if existing_item_is_active {
588 self.active_item_index = insertion_index;
589 } else if insertion_index <= self.active_item_index {
590 self.active_item_index += 1;
591 }
592 }
593
594 cx.notify();
595 }
596
597 self.activate_item(insertion_index, activate_pane, focus_item, cx);
598 } else {
599 self.items.insert(insertion_index, item.clone());
600 if insertion_index <= self.active_item_index {
601 self.active_item_index += 1;
602 }
603
604 self.activate_item(insertion_index, activate_pane, focus_item, cx);
605 cx.notify();
606 }
607
608 cx.emit(Event::AddItem { item });
609 }
610
611 pub fn items_len(&self) -> usize {
612 self.items.len()
613 }
614
615 pub fn items(&self) -> impl Iterator<Item = &Box<dyn ItemHandle>> + DoubleEndedIterator {
616 self.items.iter()
617 }
618
619 pub fn items_of_type<T: Render>(&self) -> impl '_ + Iterator<Item = View<T>> {
620 self.items
621 .iter()
622 .filter_map(|item| item.to_any().downcast().ok())
623 }
624
625 pub fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
626 self.items.get(self.active_item_index).cloned()
627 }
628
629 pub fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Point<Pixels>> {
630 self.items
631 .get(self.active_item_index)?
632 .pixel_position_of_cursor(cx)
633 }
634
635 pub fn item_for_entry(
636 &self,
637 entry_id: ProjectEntryId,
638 cx: &AppContext,
639 ) -> Option<Box<dyn ItemHandle>> {
640 self.items.iter().find_map(|item| {
641 if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
642 Some(item.boxed_clone())
643 } else {
644 None
645 }
646 })
647 }
648
649 pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option<usize> {
650 self.items
651 .iter()
652 .position(|i| i.item_id() == item.item_id())
653 }
654
655 // pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) {
656 // // Potentially warn the user of the new keybinding
657 // let workspace_handle = self.workspace().clone();
658 // cx.spawn(|_, mut cx| async move { notify_of_new_dock(&workspace_handle, &mut cx) })
659 // .detach();
660
661 // if self.zoomed {
662 // cx.emit(Event::ZoomOut);
663 // } else if !self.items.is_empty() {
664 // if !self.has_focus {
665 // cx.focus_self();
666 // }
667 // cx.emit(Event::ZoomIn);
668 // }
669 // }
670
671 pub fn activate_item(
672 &mut self,
673 index: usize,
674 activate_pane: bool,
675 focus_item: bool,
676 cx: &mut ViewContext<Self>,
677 ) {
678 use NavigationMode::{GoingBack, GoingForward};
679
680 if index < self.items.len() {
681 let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
682 if prev_active_item_ix != self.active_item_index
683 || matches!(self.nav_history.mode(), GoingBack | GoingForward)
684 {
685 if let Some(prev_item) = self.items.get(prev_active_item_ix) {
686 prev_item.deactivated(cx);
687 }
688
689 cx.emit(Event::ActivateItem {
690 local: activate_pane,
691 });
692 }
693
694 if let Some(newly_active_item) = self.items.get(index) {
695 self.activation_history
696 .retain(|&previously_active_item_id| {
697 previously_active_item_id != newly_active_item.item_id()
698 });
699 self.activation_history.push(newly_active_item.item_id());
700 }
701
702 self.update_toolbar(cx);
703
704 if focus_item {
705 self.focus_active_item(cx);
706 }
707
708 self.autoscroll = true;
709 cx.notify();
710 }
711 }
712
713 pub fn activate_prev_item(&mut self, activate_pane: bool, cx: &mut ViewContext<Self>) {
714 let mut index = self.active_item_index;
715 if index > 0 {
716 index -= 1;
717 } else if !self.items.is_empty() {
718 index = self.items.len() - 1;
719 }
720 self.activate_item(index, activate_pane, activate_pane, cx);
721 }
722
723 pub fn activate_next_item(&mut self, activate_pane: bool, cx: &mut ViewContext<Self>) {
724 let mut index = self.active_item_index;
725 if index + 1 < self.items.len() {
726 index += 1;
727 } else {
728 index = 0;
729 }
730 self.activate_item(index, activate_pane, activate_pane, cx);
731 }
732
733 pub fn close_active_item(
734 &mut self,
735 action: &CloseActiveItem,
736 cx: &mut ViewContext<Self>,
737 ) -> Option<Task<Result<()>>> {
738 if self.items.is_empty() {
739 return None;
740 }
741 let active_item_id = self.items[self.active_item_index].item_id();
742 Some(self.close_item_by_id(
743 active_item_id,
744 action.save_intent.unwrap_or(SaveIntent::Close),
745 cx,
746 ))
747 }
748
749 pub fn close_item_by_id(
750 &mut self,
751 item_id_to_close: EntityId,
752 save_intent: SaveIntent,
753 cx: &mut ViewContext<Self>,
754 ) -> Task<Result<()>> {
755 self.close_items(cx, save_intent, move |view_id| view_id == item_id_to_close)
756 }
757
758 pub fn close_inactive_items(
759 &mut self,
760 _: &CloseInactiveItems,
761 cx: &mut ViewContext<Self>,
762 ) -> Option<Task<Result<()>>> {
763 if self.items.is_empty() {
764 return None;
765 }
766
767 let active_item_id = self.items[self.active_item_index].item_id();
768 Some(self.close_items(cx, SaveIntent::Close, move |item_id| {
769 item_id != active_item_id
770 }))
771 }
772
773 pub fn close_clean_items(
774 &mut self,
775 _: &CloseCleanItems,
776 cx: &mut ViewContext<Self>,
777 ) -> Option<Task<Result<()>>> {
778 let item_ids: Vec<_> = self
779 .items()
780 .filter(|item| !item.is_dirty(cx))
781 .map(|item| item.item_id())
782 .collect();
783 Some(self.close_items(cx, SaveIntent::Close, move |item_id| {
784 item_ids.contains(&item_id)
785 }))
786 }
787
788 pub fn close_items_to_the_left(
789 &mut self,
790 _: &CloseItemsToTheLeft,
791 cx: &mut ViewContext<Self>,
792 ) -> Option<Task<Result<()>>> {
793 if self.items.is_empty() {
794 return None;
795 }
796 let active_item_id = self.items[self.active_item_index].item_id();
797 Some(self.close_items_to_the_left_by_id(active_item_id, cx))
798 }
799
800 pub fn close_items_to_the_left_by_id(
801 &mut self,
802 item_id: EntityId,
803 cx: &mut ViewContext<Self>,
804 ) -> Task<Result<()>> {
805 let item_ids: Vec<_> = self
806 .items()
807 .take_while(|item| item.item_id() != item_id)
808 .map(|item| item.item_id())
809 .collect();
810 self.close_items(cx, SaveIntent::Close, move |item_id| {
811 item_ids.contains(&item_id)
812 })
813 }
814
815 pub fn close_items_to_the_right(
816 &mut self,
817 _: &CloseItemsToTheRight,
818 cx: &mut ViewContext<Self>,
819 ) -> Option<Task<Result<()>>> {
820 if self.items.is_empty() {
821 return None;
822 }
823 let active_item_id = self.items[self.active_item_index].item_id();
824 Some(self.close_items_to_the_right_by_id(active_item_id, cx))
825 }
826
827 pub fn close_items_to_the_right_by_id(
828 &mut self,
829 item_id: EntityId,
830 cx: &mut ViewContext<Self>,
831 ) -> Task<Result<()>> {
832 let item_ids: Vec<_> = self
833 .items()
834 .rev()
835 .take_while(|item| item.item_id() != item_id)
836 .map(|item| item.item_id())
837 .collect();
838 self.close_items(cx, SaveIntent::Close, move |item_id| {
839 item_ids.contains(&item_id)
840 })
841 }
842
843 pub fn close_all_items(
844 &mut self,
845 action: &CloseAllItems,
846 cx: &mut ViewContext<Self>,
847 ) -> Option<Task<Result<()>>> {
848 if self.items.is_empty() {
849 return None;
850 }
851
852 Some(
853 self.close_items(cx, action.save_intent.unwrap_or(SaveIntent::Close), |_| {
854 true
855 }),
856 )
857 }
858
859 pub(super) fn file_names_for_prompt(
860 items: &mut dyn Iterator<Item = &Box<dyn ItemHandle>>,
861 all_dirty_items: usize,
862 cx: &AppContext,
863 ) -> String {
864 /// Quantity of item paths displayed in prompt prior to cutoff..
865 const FILE_NAMES_CUTOFF_POINT: usize = 10;
866 let mut file_names: Vec<_> = items
867 .filter_map(|item| {
868 item.project_path(cx).and_then(|project_path| {
869 project_path
870 .path
871 .file_name()
872 .and_then(|name| name.to_str().map(ToOwned::to_owned))
873 })
874 })
875 .take(FILE_NAMES_CUTOFF_POINT)
876 .collect();
877 let should_display_followup_text =
878 all_dirty_items > FILE_NAMES_CUTOFF_POINT || file_names.len() != all_dirty_items;
879 if should_display_followup_text {
880 let not_shown_files = all_dirty_items - file_names.len();
881 if not_shown_files == 1 {
882 file_names.push(".. 1 file not shown".into());
883 } else {
884 file_names.push(format!(".. {} files not shown", not_shown_files).into());
885 }
886 }
887 let file_names = file_names.join("\n");
888 format!(
889 "Do you want to save changes to the following {} files?\n{file_names}",
890 all_dirty_items
891 )
892 }
893
894 pub fn close_items(
895 &mut self,
896 cx: &mut ViewContext<Pane>,
897 mut save_intent: SaveIntent,
898 should_close: impl 'static + Fn(EntityId) -> bool,
899 ) -> Task<Result<()>> {
900 // Find the items to close.
901 let mut items_to_close = Vec::new();
902 let mut dirty_items = Vec::new();
903 for item in &self.items {
904 if should_close(item.item_id()) {
905 items_to_close.push(item.boxed_clone());
906 if item.is_dirty(cx) {
907 dirty_items.push(item.boxed_clone());
908 }
909 }
910 }
911
912 // If a buffer is open both in a singleton editor and in a multibuffer, make sure
913 // to focus the singleton buffer when prompting to save that buffer, as opposed
914 // to focusing the multibuffer, because this gives the user a more clear idea
915 // of what content they would be saving.
916 items_to_close.sort_by_key(|item| !item.is_singleton(cx));
917
918 let workspace = self.workspace.clone();
919 cx.spawn(|pane, mut cx| async move {
920 if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
921 let answer = pane.update(&mut cx, |_, cx| {
922 let prompt =
923 Self::file_names_for_prompt(&mut dirty_items.iter(), dirty_items.len(), cx);
924 cx.prompt(
925 PromptLevel::Warning,
926 &prompt,
927 &["Save all", "Discard all", "Cancel"],
928 )
929 })?;
930 match answer.await {
931 Ok(0) => save_intent = SaveIntent::SaveAll,
932 Ok(1) => save_intent = SaveIntent::Skip,
933 _ => {}
934 }
935 }
936 let mut saved_project_items_ids = HashSet::default();
937 for item in items_to_close.clone() {
938 // Find the item's current index and its set of project item models. Avoid
939 // storing these in advance, in case they have changed since this task
940 // was started.
941 let (item_ix, mut project_item_ids) = pane.update(&mut cx, |pane, cx| {
942 (pane.index_for_item(&*item), item.project_item_model_ids(cx))
943 })?;
944 let item_ix = if let Some(ix) = item_ix {
945 ix
946 } else {
947 continue;
948 };
949
950 // Check if this view has any project items that are not open anywhere else
951 // in the workspace, AND that the user has not already been prompted to save.
952 // If there are any such project entries, prompt the user to save this item.
953 let project = workspace.update(&mut cx, |workspace, cx| {
954 for item in workspace.items(cx) {
955 if !items_to_close
956 .iter()
957 .any(|item_to_close| item_to_close.item_id() == item.item_id())
958 {
959 let other_project_item_ids = item.project_item_model_ids(cx);
960 project_item_ids.retain(|id| !other_project_item_ids.contains(id));
961 }
962 }
963 workspace.project().clone()
964 })?;
965 let should_save = project_item_ids
966 .iter()
967 .any(|id| saved_project_items_ids.insert(*id));
968
969 if should_save
970 && !Self::save_item(
971 project.clone(),
972 &pane,
973 item_ix,
974 &*item,
975 save_intent,
976 &mut cx,
977 )
978 .await?
979 {
980 break;
981 }
982
983 // Remove the item from the pane.
984 pane.update(&mut cx, |pane, cx| {
985 if let Some(item_ix) = pane
986 .items
987 .iter()
988 .position(|i| i.item_id() == item.item_id())
989 {
990 pane.remove_item(item_ix, false, cx);
991 }
992 })?;
993 }
994
995 pane.update(&mut cx, |_, cx| cx.notify())?;
996 Ok(())
997 })
998 }
999
1000 pub fn remove_item(
1001 &mut self,
1002 item_index: usize,
1003 activate_pane: bool,
1004 cx: &mut ViewContext<Self>,
1005 ) {
1006 self.activation_history
1007 .retain(|&history_entry| history_entry != self.items[item_index].item_id());
1008
1009 if item_index == self.active_item_index {
1010 let index_to_activate = self
1011 .activation_history
1012 .pop()
1013 .and_then(|last_activated_item| {
1014 self.items.iter().enumerate().find_map(|(index, item)| {
1015 (item.item_id() == last_activated_item).then_some(index)
1016 })
1017 })
1018 // We didn't have a valid activation history entry, so fallback
1019 // to activating the item to the left
1020 .unwrap_or_else(|| item_index.min(self.items.len()).saturating_sub(1));
1021
1022 let should_activate = activate_pane || self.has_focus(cx);
1023 if self.items.len() == 1 && should_activate {
1024 self.focus_handle.focus(cx);
1025 } else {
1026 self.activate_item(index_to_activate, should_activate, should_activate, cx);
1027 }
1028 }
1029
1030 let item = self.items.remove(item_index);
1031
1032 cx.emit(Event::RemoveItem {
1033 item_id: item.item_id(),
1034 });
1035 if self.items.is_empty() {
1036 item.deactivated(cx);
1037 self.update_toolbar(cx);
1038 cx.emit(Event::Remove);
1039 }
1040
1041 if item_index < self.active_item_index {
1042 self.active_item_index -= 1;
1043 }
1044
1045 self.nav_history.set_mode(NavigationMode::ClosingItem);
1046 item.deactivated(cx);
1047 self.nav_history.set_mode(NavigationMode::Normal);
1048
1049 if let Some(path) = item.project_path(cx) {
1050 let abs_path = self
1051 .nav_history
1052 .0
1053 .lock()
1054 .paths_by_item
1055 .get(&item.item_id())
1056 .and_then(|(_, abs_path)| abs_path.clone());
1057
1058 self.nav_history
1059 .0
1060 .lock()
1061 .paths_by_item
1062 .insert(item.item_id(), (path, abs_path));
1063 } else {
1064 self.nav_history
1065 .0
1066 .lock()
1067 .paths_by_item
1068 .remove(&item.item_id());
1069 }
1070
1071 if self.items.is_empty() && self.zoomed {
1072 cx.emit(Event::ZoomOut);
1073 }
1074
1075 cx.notify();
1076 }
1077
1078 pub async fn save_item(
1079 project: Model<Project>,
1080 pane: &WeakView<Pane>,
1081 item_ix: usize,
1082 item: &dyn ItemHandle,
1083 save_intent: SaveIntent,
1084 cx: &mut AsyncWindowContext,
1085 ) -> Result<bool> {
1086 const CONFLICT_MESSAGE: &str =
1087 "This file has changed on disk since you started editing it. Do you want to overwrite it?";
1088
1089 if save_intent == SaveIntent::Skip {
1090 return Ok(true);
1091 }
1092
1093 let (mut has_conflict, mut is_dirty, mut can_save, can_save_as) = cx.update(|_, cx| {
1094 (
1095 item.has_conflict(cx),
1096 item.is_dirty(cx),
1097 item.can_save(cx),
1098 item.is_singleton(cx),
1099 )
1100 })?;
1101
1102 // when saving a single buffer, we ignore whether or not it's dirty.
1103 if save_intent == SaveIntent::Save {
1104 is_dirty = true;
1105 }
1106
1107 if save_intent == SaveIntent::SaveAs {
1108 is_dirty = true;
1109 has_conflict = false;
1110 can_save = false;
1111 }
1112
1113 if save_intent == SaveIntent::Overwrite {
1114 has_conflict = false;
1115 }
1116
1117 if has_conflict && can_save {
1118 let answer = pane.update(cx, |pane, cx| {
1119 pane.activate_item(item_ix, true, true, cx);
1120 cx.prompt(
1121 PromptLevel::Warning,
1122 CONFLICT_MESSAGE,
1123 &["Overwrite", "Discard", "Cancel"],
1124 )
1125 })?;
1126 match answer.await {
1127 Ok(0) => pane.update(cx, |_, cx| item.save(project, cx))?.await?,
1128 Ok(1) => pane.update(cx, |_, cx| item.reload(project, cx))?.await?,
1129 _ => return Ok(false),
1130 }
1131 } else if is_dirty && (can_save || can_save_as) {
1132 if save_intent == SaveIntent::Close {
1133 let will_autosave = cx.update(|_, cx| {
1134 matches!(
1135 WorkspaceSettings::get_global(cx).autosave,
1136 AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange
1137 ) && Self::can_autosave_item(&*item, cx)
1138 })?;
1139 if !will_autosave {
1140 let answer = pane.update(cx, |pane, cx| {
1141 pane.activate_item(item_ix, true, true, cx);
1142 let prompt = dirty_message_for(item.project_path(cx));
1143 cx.prompt(
1144 PromptLevel::Warning,
1145 &prompt,
1146 &["Save", "Don't Save", "Cancel"],
1147 )
1148 })?;
1149 match answer.await {
1150 Ok(0) => {}
1151 Ok(1) => return Ok(true), // Don't save this file
1152 _ => return Ok(false), // Cancel
1153 }
1154 }
1155 }
1156
1157 if can_save {
1158 pane.update(cx, |_, cx| item.save(project, cx))?.await?;
1159 } else if can_save_as {
1160 let start_abs_path = project
1161 .update(cx, |project, cx| {
1162 let worktree = project.visible_worktrees(cx).next()?;
1163 Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
1164 })?
1165 .unwrap_or_else(|| Path::new("").into());
1166
1167 let abs_path = cx.update(|_, cx| cx.prompt_for_new_path(&start_abs_path))?;
1168 if let Some(abs_path) = abs_path.await.ok().flatten() {
1169 pane.update(cx, |_, cx| item.save_as(project, abs_path, cx))?
1170 .await?;
1171 } else {
1172 return Ok(false);
1173 }
1174 }
1175 }
1176 Ok(true)
1177 }
1178
1179 fn can_autosave_item(item: &dyn ItemHandle, cx: &AppContext) -> bool {
1180 let is_deleted = item.project_entry_ids(cx).is_empty();
1181 item.is_dirty(cx) && !item.has_conflict(cx) && item.can_save(cx) && !is_deleted
1182 }
1183
1184 pub fn autosave_item(
1185 item: &dyn ItemHandle,
1186 project: Model<Project>,
1187 cx: &mut WindowContext,
1188 ) -> Task<Result<()>> {
1189 if Self::can_autosave_item(item, cx) {
1190 item.save(project, cx)
1191 } else {
1192 Task::ready(Ok(()))
1193 }
1194 }
1195
1196 pub fn focus(&mut self, cx: &mut ViewContext<Pane>) {
1197 cx.focus(&self.focus_handle);
1198 }
1199
1200 pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
1201 if let Some(active_item) = self.active_item() {
1202 let focus_handle = active_item.focus_handle(cx);
1203 cx.focus(&focus_handle);
1204 }
1205 }
1206
1207 pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
1208 cx.emit(Event::Split(direction));
1209 }
1210
1211 // fn deploy_split_menu(&mut self, cx: &mut ViewContext<Self>) {
1212 // self.tab_bar_context_menu.handle.update(cx, |menu, cx| {
1213 // menu.toggle(
1214 // Default::default(),
1215 // AnchorCorner::TopRight,
1216 // vec![
1217 // ContextMenuItem::action("Split Right", SplitRight),
1218 // ContextMenuItem::action("Split Left", SplitLeft),
1219 // ContextMenuItem::action("Split Up", SplitUp),
1220 // ContextMenuItem::action("Split Down", SplitDown),
1221 // ],
1222 // cx,
1223 // );
1224 // });
1225
1226 // self.tab_bar_context_menu.kind = TabBarContextMenuKind::Split;
1227 // }
1228
1229 // fn deploy_new_menu(&mut self, cx: &mut ViewContext<Self>) {
1230 // self.tab_bar_context_menu.handle.update(cx, |menu, cx| {
1231 // menu.toggle(
1232 // Default::default(),
1233 // AnchorCorner::TopRight,
1234 // vec![
1235 // ContextMenuItem::action("New File", NewFile),
1236 // ContextMenuItem::action("New Terminal", NewCenterTerminal),
1237 // ContextMenuItem::action("New Search", NewSearch),
1238 // ],
1239 // cx,
1240 // );
1241 // });
1242
1243 // self.tab_bar_context_menu.kind = TabBarContextMenuKind::New;
1244 // }
1245
1246 // fn deploy_tab_context_menu(
1247 // &mut self,
1248 // position: Vector2F,
1249 // target_item_id: usize,
1250 // cx: &mut ViewContext<Self>,
1251 // ) {
1252 // let active_item_id = self.items[self.active_item_index].id();
1253 // let is_active_item = target_item_id == active_item_id;
1254 // let target_pane = cx.weak_handle();
1255
1256 // // The `CloseInactiveItems` action should really be called "CloseOthers" and the behaviour should be dynamically based on the tab the action is ran on. Currently, this is a weird action because you can run it on a non-active tab and it will close everything by the actual active tab
1257
1258 // self.tab_context_menu.update(cx, |menu, cx| {
1259 // menu.show(
1260 // position,
1261 // AnchorCorner::TopLeft,
1262 // if is_active_item {
1263 // vec![
1264 // ContextMenuItem::action(
1265 // "Close Active Item",
1266 // CloseActiveItem { save_intent: None },
1267 // ),
1268 // ContextMenuItem::action("Close Inactive Items", CloseInactiveItems),
1269 // ContextMenuItem::action("Close Clean Items", CloseCleanItems),
1270 // ContextMenuItem::action("Close Items To The Left", CloseItemsToTheLeft),
1271 // ContextMenuItem::action("Close Items To The Right", CloseItemsToTheRight),
1272 // ContextMenuItem::action(
1273 // "Close All Items",
1274 // CloseAllItems { save_intent: None },
1275 // ),
1276 // ]
1277 // } else {
1278 // // In the case of the user right clicking on a non-active tab, for some item-closing commands, we need to provide the id of the tab, for the others, we can reuse the existing command.
1279 // vec![
1280 // ContextMenuItem::handler("Close Inactive Item", {
1281 // let pane = target_pane.clone();
1282 // move |cx| {
1283 // if let Some(pane) = pane.upgrade(cx) {
1284 // pane.update(cx, |pane, cx| {
1285 // pane.close_item_by_id(
1286 // target_item_id,
1287 // SaveIntent::Close,
1288 // cx,
1289 // )
1290 // .detach_and_log_err(cx);
1291 // })
1292 // }
1293 // }
1294 // }),
1295 // ContextMenuItem::action("Close Inactive Items", CloseInactiveItems),
1296 // ContextMenuItem::action("Close Clean Items", CloseCleanItems),
1297 // ContextMenuItem::handler("Close Items To The Left", {
1298 // let pane = target_pane.clone();
1299 // move |cx| {
1300 // if let Some(pane) = pane.upgrade(cx) {
1301 // pane.update(cx, |pane, cx| {
1302 // pane.close_items_to_the_left_by_id(target_item_id, cx)
1303 // .detach_and_log_err(cx);
1304 // })
1305 // }
1306 // }
1307 // }),
1308 // ContextMenuItem::handler("Close Items To The Right", {
1309 // let pane = target_pane.clone();
1310 // move |cx| {
1311 // if let Some(pane) = pane.upgrade(cx) {
1312 // pane.update(cx, |pane, cx| {
1313 // pane.close_items_to_the_right_by_id(target_item_id, cx)
1314 // .detach_and_log_err(cx);
1315 // })
1316 // }
1317 // }
1318 // }),
1319 // ContextMenuItem::action(
1320 // "Close All Items",
1321 // CloseAllItems { save_intent: None },
1322 // ),
1323 // ]
1324 // },
1325 // cx,
1326 // );
1327 // });
1328 // }
1329
1330 pub fn toolbar(&self) -> &View<Toolbar> {
1331 &self.toolbar
1332 }
1333
1334 pub fn handle_deleted_project_item(
1335 &mut self,
1336 entry_id: ProjectEntryId,
1337 cx: &mut ViewContext<Pane>,
1338 ) -> Option<()> {
1339 let (item_index_to_delete, item_id) = self.items().enumerate().find_map(|(i, item)| {
1340 if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
1341 Some((i, item.item_id()))
1342 } else {
1343 None
1344 }
1345 })?;
1346
1347 self.remove_item(item_index_to_delete, false, cx);
1348 self.nav_history.remove_item(item_id);
1349
1350 Some(())
1351 }
1352
1353 fn update_toolbar(&mut self, cx: &mut ViewContext<Self>) {
1354 let active_item = self
1355 .items
1356 .get(self.active_item_index)
1357 .map(|item| item.as_ref());
1358 self.toolbar.update(cx, |toolbar, cx| {
1359 toolbar.set_active_item(active_item, cx);
1360 });
1361 }
1362
1363 fn render_tab(
1364 &self,
1365 ix: usize,
1366 item: &Box<dyn ItemHandle>,
1367 detail: usize,
1368 cx: &mut ViewContext<'_, Pane>,
1369 ) -> impl IntoElement {
1370 let label = item.tab_content(Some(detail), cx);
1371 let close_icon = || {
1372 let id = item.item_id();
1373
1374 div()
1375 .id(ix)
1376 .invisible()
1377 .group_hover("", |style| style.visible())
1378 .child(
1379 IconButton::new("close_tab", Icon::Close).on_click(cx.listener(
1380 move |pane, _, cx| {
1381 pane.close_item_by_id(id, SaveIntent::Close, cx)
1382 .detach_and_log_err(cx);
1383 },
1384 )),
1385 )
1386 };
1387
1388 let (text_color, tab_bg, tab_hover_bg, tab_active_bg) = match ix == self.active_item_index {
1389 false => (
1390 cx.theme().colors().text_muted,
1391 cx.theme().colors().tab_inactive_background,
1392 cx.theme().colors().ghost_element_hover,
1393 cx.theme().colors().ghost_element_active,
1394 ),
1395 true => (
1396 cx.theme().colors().text,
1397 cx.theme().colors().tab_active_background,
1398 cx.theme().colors().element_hover,
1399 cx.theme().colors().element_active,
1400 ),
1401 };
1402
1403 let close_right = ItemSettings::get_global(cx).close_position.right();
1404 let is_active = ix == self.active_item_index;
1405
1406 div()
1407 .group("")
1408 .id(ix)
1409 .cursor_pointer()
1410 .when_some(item.tab_tooltip_text(cx), |div, text| {
1411 div.tooltip(move |cx| cx.build_view(|cx| Tooltip::new(text.clone())).into())
1412 })
1413 .on_click(cx.listener(move |v: &mut Self, e, cx| v.activate_item(ix, true, true, cx)))
1414 // .on_drag(move |pane, cx| pane.render_tab(ix, item.boxed_clone(), detail, cx))
1415 // .drag_over::<DraggedTab>(|d| d.bg(cx.theme().colors().element_drop_target))
1416 // .on_drop(|_view, state: View<DraggedTab>, cx| {
1417 // eprintln!("{:?}", state.read(cx));
1418 // })
1419 .flex()
1420 .items_center()
1421 .justify_center()
1422 // todo!("Nate - I need to do some work to balance all the items in the tab once things stablize")
1423 .map(|this| {
1424 if close_right {
1425 this.pl_3().pr_1()
1426 } else {
1427 this.pr_1().pr_3()
1428 }
1429 })
1430 .py_1()
1431 .bg(tab_bg)
1432 .border_color(cx.theme().colors().border)
1433 .text_color(if is_active {
1434 cx.theme().colors().text
1435 } else {
1436 cx.theme().colors().text_muted
1437 })
1438 .map(|this| {
1439 let is_last_item = ix == self.items.len() - 1;
1440 match ix.cmp(&self.active_item_index) {
1441 cmp::Ordering::Less => this.border_l().mr_px(),
1442 cmp::Ordering::Greater => {
1443 if is_last_item {
1444 this.mr_px().ml_px()
1445 } else {
1446 this.border_r().ml_px()
1447 }
1448 }
1449 cmp::Ordering::Equal => this.border_l().border_r(),
1450 }
1451 })
1452 // .hover(|h| h.bg(tab_hover_bg))
1453 // .active(|a| a.bg(tab_active_bg))
1454 .child(
1455 div()
1456 .flex()
1457 .items_center()
1458 .gap_1()
1459 .text_color(text_color)
1460 .children(
1461 item.has_conflict(cx)
1462 .then(|| {
1463 div().border().border_color(gpui::red()).child(
1464 IconElement::new(Icon::ExclamationTriangle)
1465 .size(ui::IconSize::Small)
1466 .color(Color::Warning),
1467 )
1468 })
1469 .or(item.is_dirty(cx).then(|| {
1470 div().border().border_color(gpui::red()).child(
1471 IconElement::new(Icon::ExclamationTriangle)
1472 .size(ui::IconSize::Small)
1473 .color(Color::Info),
1474 )
1475 })),
1476 )
1477 .children((!close_right).then(|| close_icon()))
1478 .child(label)
1479 .children(close_right.then(|| close_icon())),
1480 )
1481 }
1482
1483 fn render_tab_bar(&mut self, cx: &mut ViewContext<'_, Pane>) -> impl IntoElement {
1484 div()
1485 .group("tab_bar")
1486 .id("tab_bar")
1487 .w_full()
1488 .flex()
1489 .bg(cx.theme().colors().tab_bar_background)
1490 // Left Side
1491 .child(
1492 div()
1493 .relative()
1494 .px_1()
1495 .flex()
1496 .flex_none()
1497 .gap_2()
1498 // Nav Buttons
1499 .child(
1500 div()
1501 .right_0()
1502 .flex()
1503 .items_center()
1504 .gap_px()
1505 .child(
1506 div().border().border_color(gpui::red()).child(
1507 IconButton::new("navigate_backward", Icon::ArrowLeft)
1508 .on_click({
1509 let view = cx.view().clone();
1510 move |_, cx| view.update(cx, Self::navigate_backward)
1511 })
1512 .disabled(!self.can_navigate_backward()),
1513 ),
1514 )
1515 .child(
1516 div().border().border_color(gpui::red()).child(
1517 IconButton::new("navigate_forward", Icon::ArrowRight)
1518 .on_click({
1519 let view = cx.view().clone();
1520 move |_, cx| view.update(cx, Self::navigate_backward)
1521 })
1522 .disabled(!self.can_navigate_forward()),
1523 ),
1524 ),
1525 ),
1526 )
1527 .child(
1528 div().flex_1().h_full().child(
1529 div().id("tabs").flex().overflow_x_scroll().children(
1530 self.items
1531 .iter()
1532 .enumerate()
1533 .zip(self.tab_details(cx))
1534 .map(|((ix, item), detail)| self.render_tab(ix, item, detail, cx)),
1535 ),
1536 ),
1537 )
1538 // Right Side
1539 .child(
1540 div()
1541 .px_1()
1542 .flex()
1543 .flex_none()
1544 .gap_2()
1545 // Nav Buttons
1546 .child(
1547 div()
1548 .flex()
1549 .items_center()
1550 .gap_px()
1551 .child(
1552 div()
1553 .border()
1554 .border_color(gpui::red())
1555 .child(IconButton::new("plus", Icon::Plus)),
1556 )
1557 .child(
1558 div()
1559 .border()
1560 .border_color(gpui::red())
1561 .child(IconButton::new("split", Icon::Split)),
1562 ),
1563 ),
1564 )
1565 }
1566
1567 // fn render_tabs(&mut self, cx: &mut ViewContext<Self>) -> impl Element<Self> {
1568 // let theme = theme::current(cx).clone();
1569
1570 // let pane = cx.handle().downgrade();
1571 // let autoscroll = if mem::take(&mut self.autoscroll) {
1572 // Some(self.active_item_index)
1573 // } else {
1574 // None
1575 // };
1576
1577 // let pane_active = self.has_focus;
1578
1579 // enum Tabs {}
1580 // let mut row = Flex::row().scrollable::<Tabs>(1, autoscroll, cx);
1581 // for (ix, (item, detail)) in self
1582 // .items
1583 // .iter()
1584 // .cloned()
1585 // .zip(self.tab_details(cx))
1586 // .enumerate()
1587 // {
1588 // let git_status = item
1589 // .project_path(cx)
1590 // .and_then(|path| self.project.read(cx).entry_for_path(&path, cx))
1591 // .and_then(|entry| entry.git_status());
1592
1593 // let detail = if detail == 0 { None } else { Some(detail) };
1594 // let tab_active = ix == self.active_item_index;
1595
1596 // row.add_child({
1597 // enum TabDragReceiver {}
1598 // let mut receiver =
1599 // dragged_item_receiver::<TabDragReceiver, _, _>(self, ix, ix, true, None, cx, {
1600 // let item = item.clone();
1601 // let pane = pane.clone();
1602 // let detail = detail.clone();
1603
1604 // let theme = theme::current(cx).clone();
1605 // let mut tooltip_theme = theme.tooltip.clone();
1606 // tooltip_theme.max_text_width = None;
1607 // let tab_tooltip_text =
1608 // item.tab_tooltip_text(cx).map(|text| text.into_owned());
1609
1610 // let mut tab_style = theme
1611 // .workspace
1612 // .tab_bar
1613 // .tab_style(pane_active, tab_active)
1614 // .clone();
1615 // let should_show_status = settings::get::<ItemSettings>(cx).git_status;
1616 // if should_show_status && git_status != None {
1617 // tab_style.label.text.color = match git_status.unwrap() {
1618 // GitFileStatus::Added => tab_style.git.inserted,
1619 // GitFileStatus::Modified => tab_style.git.modified,
1620 // GitFileStatus::Conflict => tab_style.git.conflict,
1621 // };
1622 // }
1623
1624 // move |mouse_state, cx| {
1625 // let hovered = mouse_state.hovered();
1626
1627 // enum Tab {}
1628 // let mouse_event_handler =
1629 // MouseEventHandler::new::<Tab, _>(ix, cx, |_, cx| {
1630 // Self::render_tab(
1631 // &item,
1632 // pane.clone(),
1633 // ix == 0,
1634 // detail,
1635 // hovered,
1636 // &tab_style,
1637 // cx,
1638 // )
1639 // })
1640 // .on_down(MouseButton::Left, move |_, this, cx| {
1641 // this.activate_item(ix, true, true, cx);
1642 // })
1643 // .on_click(MouseButton::Middle, {
1644 // let item_id = item.id();
1645 // move |_, pane, cx| {
1646 // pane.close_item_by_id(item_id, SaveIntent::Close, cx)
1647 // .detach_and_log_err(cx);
1648 // }
1649 // })
1650 // .on_down(
1651 // MouseButton::Right,
1652 // move |event, pane, cx| {
1653 // pane.deploy_tab_context_menu(event.position, item.id(), cx);
1654 // },
1655 // );
1656
1657 // if let Some(tab_tooltip_text) = tab_tooltip_text {
1658 // mouse_event_handler
1659 // .with_tooltip::<Self>(
1660 // ix,
1661 // tab_tooltip_text,
1662 // None,
1663 // tooltip_theme,
1664 // cx,
1665 // )
1666 // .into_any()
1667 // } else {
1668 // mouse_event_handler.into_any()
1669 // }
1670 // }
1671 // });
1672
1673 // if !pane_active || !tab_active {
1674 // receiver = receiver.with_cursor_style(CursorStyle::PointingHand);
1675 // }
1676
1677 // receiver.as_draggable(
1678 // DraggedItem {
1679 // handle: item,
1680 // pane: pane.clone(),
1681 // },
1682 // {
1683 // let theme = theme::current(cx).clone();
1684
1685 // let detail = detail.clone();
1686 // move |_, dragged_item: &DraggedItem, cx: &mut ViewContext<Workspace>| {
1687 // let tab_style = &theme.workspace.tab_bar.dragged_tab;
1688 // Self::render_dragged_tab(
1689 // &dragged_item.handle,
1690 // dragged_item.pane.clone(),
1691 // false,
1692 // detail,
1693 // false,
1694 // &tab_style,
1695 // cx,
1696 // )
1697 // }
1698 // },
1699 // )
1700 // })
1701 // }
1702
1703 // // Use the inactive tab style along with the current pane's active status to decide how to render
1704 // // the filler
1705 // let filler_index = self.items.len();
1706 // let filler_style = theme.workspace.tab_bar.tab_style(pane_active, false);
1707 // enum Filler {}
1708 // row.add_child(
1709 // dragged_item_receiver::<Filler, _, _>(self, 0, filler_index, true, None, cx, |_, _| {
1710 // Empty::new()
1711 // .contained()
1712 // .with_style(filler_style.container)
1713 // .with_border(filler_style.container.border)
1714 // })
1715 // .flex(1., true)
1716 // .into_any_named("filler"),
1717 // );
1718
1719 // row
1720 // }
1721
1722 fn tab_details(&self, cx: &AppContext) -> Vec<usize> {
1723 let mut tab_details = self.items.iter().map(|_| 0).collect::<Vec<_>>();
1724
1725 let mut tab_descriptions = HashMap::default();
1726 let mut done = false;
1727 while !done {
1728 done = true;
1729
1730 // Store item indices by their tab description.
1731 for (ix, (item, detail)) in self.items.iter().zip(&tab_details).enumerate() {
1732 if let Some(description) = item.tab_description(*detail, cx) {
1733 if *detail == 0
1734 || Some(&description) != item.tab_description(detail - 1, cx).as_ref()
1735 {
1736 tab_descriptions
1737 .entry(description)
1738 .or_insert(Vec::new())
1739 .push(ix);
1740 }
1741 }
1742 }
1743
1744 // If two or more items have the same tab description, increase eir level
1745 // of detail and try again.
1746 for (_, item_ixs) in tab_descriptions.drain() {
1747 if item_ixs.len() > 1 {
1748 done = false;
1749 for ix in item_ixs {
1750 tab_details[ix] += 1;
1751 }
1752 }
1753 }
1754 }
1755
1756 tab_details
1757 }
1758
1759 // fn render_tab(
1760 // item: &Box<dyn ItemHandle>,
1761 // pane: WeakView<Pane>,
1762 // first: bool,
1763 // detail: Option<usize>,
1764 // hovered: bool,
1765 // tab_style: &theme::Tab,
1766 // cx: &mut ViewContext<Self>,
1767 // ) -> AnyElement<Self> {
1768 // let title = item.tab_content(detail, &tab_style, cx);
1769 // Self::render_tab_with_title(title, item, pane, first, hovered, tab_style, cx)
1770 // }
1771
1772 // fn render_dragged_tab(
1773 // item: &Box<dyn ItemHandle>,
1774 // pane: WeakView<Pane>,
1775 // first: bool,
1776 // detail: Option<usize>,
1777 // hovered: bool,
1778 // tab_style: &theme::Tab,
1779 // cx: &mut ViewContext<Workspace>,
1780 // ) -> AnyElement<Workspace> {
1781 // let title = item.dragged_tab_content(detail, &tab_style, cx);
1782 // Self::render_tab_with_title(title, item, pane, first, hovered, tab_style, cx)
1783 // }
1784
1785 // fn render_tab_with_title<T: View>(
1786 // title: AnyElement<T>,
1787 // item: &Box<dyn ItemHandle>,
1788 // pane: WeakView<Pane>,
1789 // first: bool,
1790 // hovered: bool,
1791 // tab_style: &theme::Tab,
1792 // cx: &mut ViewContext<T>,
1793 // ) -> AnyElement<T> {
1794 // let mut container = tab_style.container.clone();
1795 // if first {
1796 // container.border.left = false;
1797 // }
1798
1799 // let buffer_jewel_element = {
1800 // let diameter = 7.0;
1801 // let icon_color = if item.has_conflict(cx) {
1802 // Some(tab_style.icon_conflict)
1803 // } else if item.is_dirty(cx) {
1804 // Some(tab_style.icon_dirty)
1805 // } else {
1806 // None
1807 // };
1808
1809 // Canvas::new(move |bounds, _, _, cx| {
1810 // if let Some(color) = icon_color {
1811 // let square = RectF::new(bounds.origin(), vec2f(diameter, diameter));
1812 // cx.scene().push_quad(Quad {
1813 // bounds: square,
1814 // background: Some(color),
1815 // border: Default::default(),
1816 // corner_radii: (diameter / 2.).into(),
1817 // });
1818 // }
1819 // })
1820 // .constrained()
1821 // .with_width(diameter)
1822 // .with_height(diameter)
1823 // .aligned()
1824 // };
1825
1826 // let title_element = title.aligned().contained().with_style(ContainerStyle {
1827 // margin: Margin {
1828 // left: tab_style.spacing,
1829 // right: tab_style.spacing,
1830 // ..Default::default()
1831 // },
1832 // ..Default::default()
1833 // });
1834
1835 // let close_element = if hovered {
1836 // let item_id = item.id();
1837 // enum TabCloseButton {}
1838 // let icon = Svg::new("icons/x.svg");
1839 // MouseEventHandler::new::<TabCloseButton, _>(item_id, cx, |mouse_state, _| {
1840 // if mouse_state.hovered() {
1841 // icon.with_color(tab_style.icon_close_active)
1842 // } else {
1843 // icon.with_color(tab_style.icon_close)
1844 // }
1845 // })
1846 // .with_padding(Padding::uniform(4.))
1847 // .with_cursor_style(CursorStyle::PointingHand)
1848 // .on_click(MouseButton::Left, {
1849 // let pane = pane.clone();
1850 // move |_, _, cx| {
1851 // let pane = pane.clone();
1852 // cx.window_context().defer(move |cx| {
1853 // if let Some(pane) = pane.upgrade(cx) {
1854 // pane.update(cx, |pane, cx| {
1855 // pane.close_item_by_id(item_id, SaveIntent::Close, cx)
1856 // .detach_and_log_err(cx);
1857 // });
1858 // }
1859 // });
1860 // }
1861 // })
1862 // .into_any_named("close-tab-icon")
1863 // .constrained()
1864 // } else {
1865 // Empty::new().constrained()
1866 // }
1867 // .with_width(tab_style.close_icon_width)
1868 // .aligned();
1869
1870 // let close_right = settings::get::<ItemSettings>(cx).close_position.right();
1871
1872 // if close_right {
1873 // Flex::row()
1874 // .with_child(buffer_jewel_element)
1875 // .with_child(title_element)
1876 // .with_child(close_element)
1877 // } else {
1878 // Flex::row()
1879 // .with_child(close_element)
1880 // .with_child(title_element)
1881 // .with_child(buffer_jewel_element)
1882 // }
1883 // .contained()
1884 // .with_style(container)
1885 // .constrained()
1886 // .with_height(tab_style.height)
1887 // .into_any()
1888 // }
1889
1890 // pub fn render_tab_bar_button<
1891 // F1: 'static + Fn(&mut Pane, &mut EventContext<Pane>),
1892 // F2: 'static + Fn(&mut Pane, &mut EventContext<Pane>),
1893 // >(
1894 // index: usize,
1895 // icon: &'static str,
1896 // is_active: bool,
1897 // tooltip: Option<(&'static str, Option<Box<dyn Action>>)>,
1898 // cx: &mut ViewContext<Pane>,
1899 // on_click: F1,
1900 // on_down: F2,
1901 // context_menu: Option<ViewHandle<ContextMenu>>,
1902 // ) -> AnyElement<Pane> {
1903 // enum TabBarButton {}
1904
1905 // let mut button = MouseEventHandler::new::<TabBarButton, _>(index, cx, |mouse_state, cx| {
1906 // let theme = &settings2::get::<ThemeSettings>(cx).theme.workspace.tab_bar;
1907 // let style = theme.pane_button.in_state(is_active).style_for(mouse_state);
1908 // Svg::new(icon)
1909 // .with_color(style.color)
1910 // .constrained()
1911 // .with_width(style.icon_width)
1912 // .aligned()
1913 // .constrained()
1914 // .with_width(style.button_width)
1915 // .with_height(style.button_width)
1916 // })
1917 // .with_cursor_style(CursorStyle::PointingHand)
1918 // .on_down(MouseButton::Left, move |_, pane, cx| on_down(pane, cx))
1919 // .on_click(MouseButton::Left, move |_, pane, cx| on_click(pane, cx))
1920 // .into_any();
1921 // if let Some((tooltip, action)) = tooltip {
1922 // let tooltip_style = settings::get::<ThemeSettings>(cx).theme.tooltip.clone();
1923 // button = button
1924 // .with_tooltip::<TabBarButton>(index, tooltip, action, tooltip_style, cx)
1925 // .into_any();
1926 // }
1927
1928 // Stack::new()
1929 // .with_child(button)
1930 // .with_children(
1931 // context_menu.map(|menu| ChildView::new(&menu, cx).aligned().bottom().right()),
1932 // )
1933 // .flex(1., false)
1934 // .into_any_named("tab bar button")
1935 // }
1936
1937 // fn render_blank_pane(&self, theme: &Theme, _cx: &mut ViewContext<Self>) -> AnyElement<Self> {
1938 // let background = theme.workspace.background;
1939 // Empty::new()
1940 // .contained()
1941 // .with_background_color(background)
1942 // .into_any()
1943 // }
1944
1945 pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
1946 self.zoomed = zoomed;
1947 cx.notify();
1948 }
1949
1950 pub fn is_zoomed(&self) -> bool {
1951 self.zoomed
1952 }
1953}
1954
1955impl FocusableView for Pane {
1956 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
1957 self.focus_handle.clone()
1958 }
1959}
1960
1961impl Render for Pane {
1962 type Element = Focusable<Div>;
1963
1964 fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
1965 v_stack()
1966 .key_context("Pane")
1967 .track_focus(&self.focus_handle)
1968 .on_action(cx.listener(|pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx)))
1969 .on_action(cx.listener(|pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx)))
1970 .on_action(
1971 cx.listener(|pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx)),
1972 )
1973 .on_action(cx.listener(|pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx)))
1974 .on_action(cx.listener(|pane, _: &GoBack, cx| pane.navigate_backward(cx)))
1975 .on_action(cx.listener(|pane, _: &GoForward, cx| pane.navigate_forward(cx)))
1976 // cx.add_action(Pane::toggle_zoom);
1977 // cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
1978 // pane.activate_item(action.0, true, true, cx);
1979 // });
1980 // cx.add_action(|pane: &mut Pane, _: &ActivateLastItem, cx| {
1981 // pane.activate_item(pane.items.len() - 1, true, true, cx);
1982 // });
1983 // cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
1984 // pane.activate_prev_item(true, cx);
1985 // });
1986 // cx.add_action(|pane: &mut Pane, _: &ActivateNextItem, cx| {
1987 // pane.activate_next_item(true, cx);
1988 // });
1989 // cx.add_async_action(Pane::close_active_item);
1990 // cx.add_async_action(Pane::close_inactive_items);
1991 // cx.add_async_action(Pane::close_clean_items);
1992 // cx.add_async_action(Pane::close_items_to_the_left);
1993 // cx.add_async_action(Pane::close_items_to_the_right);
1994 // cx.add_async_action(Pane::close_all_items);
1995 .size_full()
1996 .on_action(
1997 cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
1998 pane.close_active_item(action, cx)
1999 .map(|task| task.detach_and_log_err(cx));
2000 }),
2001 )
2002 .child(self.render_tab_bar(cx))
2003 .child(self.toolbar.clone())
2004 .child(if let Some(item) = self.active_item() {
2005 div().flex().flex_1().child(item.to_any())
2006 } else {
2007 // todo!()
2008 div().child("Empty Pane")
2009 })
2010
2011 // enum MouseNavigationHandler {}
2012
2013 // MouseEventHandler::new::<MouseNavigationHandler, _>(0, cx, |_, cx| {
2014 // let active_item_index = self.active_item_index;
2015
2016 // if let Some(active_item) = self.active_item() {
2017 // Flex::column()
2018 // .with_child({
2019 // let theme = theme::current(cx).clone();
2020
2021 // let mut stack = Stack::new();
2022
2023 // enum TabBarEventHandler {}
2024 // stack.add_child(
2025 // MouseEventHandler::new::<TabBarEventHandler, _>(0, cx, |_, _| {
2026 // Empty::new()
2027 // .contained()
2028 // .with_style(theme.workspace.tab_bar.container)
2029 // })
2030 // .on_down(
2031 // MouseButton::Left,
2032 // move |_, this, cx| {
2033 // this.activate_item(active_item_index, true, true, cx);
2034 // },
2035 // ),
2036 // );
2037 // let tooltip_style = theme.tooltip.clone();
2038 // let tab_bar_theme = theme.workspace.tab_bar.clone();
2039
2040 // let nav_button_height = tab_bar_theme.height;
2041 // let button_style = tab_bar_theme.nav_button;
2042 // let border_for_nav_buttons = tab_bar_theme
2043 // .tab_style(false, false)
2044 // .container
2045 // .border
2046 // .clone();
2047
2048 // let mut tab_row = Flex::row()
2049 // .with_child(nav_button(
2050 // "icons/arrow_left.svg",
2051 // button_style.clone(),
2052 // nav_button_height,
2053 // tooltip_style.clone(),
2054 // self.can_navigate_backward(),
2055 // {
2056 // move |pane, cx| {
2057 // if let Some(workspace) = pane.workspace.upgrade(cx) {
2058 // let pane = cx.weak_handle();
2059 // cx.window_context().defer(move |cx| {
2060 // workspace.update(cx, |workspace, cx| {
2061 // workspace
2062 // .go_back(pane, cx)
2063 // .detach_and_log_err(cx)
2064 // })
2065 // })
2066 // }
2067 // }
2068 // },
2069 // super::GoBack,
2070 // "Go Back",
2071 // cx,
2072 // ))
2073 // .with_child(
2074 // nav_button(
2075 // "icons/arrow_right.svg",
2076 // button_style.clone(),
2077 // nav_button_height,
2078 // tooltip_style,
2079 // self.can_navigate_forward(),
2080 // {
2081 // move |pane, cx| {
2082 // if let Some(workspace) = pane.workspace.upgrade(cx) {
2083 // let pane = cx.weak_handle();
2084 // cx.window_context().defer(move |cx| {
2085 // workspace.update(cx, |workspace, cx| {
2086 // workspace
2087 // .go_forward(pane, cx)
2088 // .detach_and_log_err(cx)
2089 // })
2090 // })
2091 // }
2092 // }
2093 // },
2094 // super::GoForward,
2095 // "Go Forward",
2096 // cx,
2097 // )
2098 // .contained()
2099 // .with_border(border_for_nav_buttons),
2100 // )
2101 // .with_child(self.render_tabs(cx).flex(1., true).into_any_named("tabs"));
2102
2103 // if self.has_focus {
2104 // let render_tab_bar_buttons = self.render_tab_bar_buttons.clone();
2105 // tab_row.add_child(
2106 // (render_tab_bar_buttons)(self, cx)
2107 // .contained()
2108 // .with_style(theme.workspace.tab_bar.pane_button_container)
2109 // .flex(1., false)
2110 // .into_any(),
2111 // )
2112 // }
2113
2114 // stack.add_child(tab_row);
2115 // stack
2116 // .constrained()
2117 // .with_height(theme.workspace.tab_bar.height)
2118 // .flex(1., false)
2119 // .into_any_named("tab bar")
2120 // })
2121 // .with_child({
2122 // enum PaneContentTabDropTarget {}
2123 // dragged_item_receiver::<PaneContentTabDropTarget, _, _>(
2124 // self,
2125 // 0,
2126 // self.active_item_index + 1,
2127 // !self.can_split,
2128 // if self.can_split { Some(100.) } else { None },
2129 // cx,
2130 // {
2131 // let toolbar = self.toolbar.clone();
2132 // let toolbar_hidden = toolbar.read(cx).hidden();
2133 // move |_, cx| {
2134 // Flex::column()
2135 // .with_children(
2136 // (!toolbar_hidden)
2137 // .then(|| ChildView::new(&toolbar, cx).expanded()),
2138 // )
2139 // .with_child(
2140 // ChildView::new(active_item.as_any(), cx).flex(1., true),
2141 // )
2142 // }
2143 // },
2144 // )
2145 // .flex(1., true)
2146 // })
2147 // .with_child(ChildView::new(&self.tab_context_menu, cx))
2148 // .into_any()
2149 // } else {
2150 // enum EmptyPane {}
2151 // let theme = theme::current(cx).clone();
2152
2153 // dragged_item_receiver::<EmptyPane, _, _>(self, 0, 0, false, None, cx, |_, cx| {
2154 // self.render_blank_pane(&theme, cx)
2155 // })
2156 // .on_down(MouseButton::Left, |_, _, cx| {
2157 // cx.focus_parent();
2158 // })
2159 // .into_any()
2160 // }
2161 // })
2162 // .on_down(
2163 // MouseButton::Navigate(NavigationDirection::Back),
2164 // move |_, pane, cx| {
2165 // if let Some(workspace) = pane.workspace.upgrade(cx) {
2166 // let pane = cx.weak_handle();
2167 // cx.window_context().defer(move |cx| {
2168 // workspace.update(cx, |workspace, cx| {
2169 // workspace.go_back(pane, cx).detach_and_log_err(cx)
2170 // })
2171 // })
2172 // }
2173 // },
2174 // )
2175 // .on_down(MouseButton::Navigate(NavigationDirection::Forward), {
2176 // move |_, pane, cx| {
2177 // if let Some(workspace) = pane.workspace.upgrade(cx) {
2178 // let pane = cx.weak_handle();
2179 // cx.window_context().defer(move |cx| {
2180 // workspace.update(cx, |workspace, cx| {
2181 // workspace.go_forward(pane, cx).detach_and_log_err(cx)
2182 // })
2183 // })
2184 // }
2185 // }
2186 // })
2187 // .into_any_named("pane")
2188 }
2189
2190 // fn focus_in(&mut self, focused: AnyViewHandle, cx: &mut ViewContext<Self>) {
2191 // if !self.has_focus {
2192 // self.has_focus = true;
2193 // cx.emit(Event::Focus);
2194 // cx.notify();
2195 // }
2196
2197 // self.toolbar.update(cx, |toolbar, cx| {
2198 // toolbar.focus_changed(true, cx);
2199 // });
2200
2201 // if let Some(active_item) = self.active_item() {
2202 // if cx.is_self_focused() {
2203 // // Pane was focused directly. We need to either focus a view inside the active item,
2204 // // or focus the active item itself
2205 // if let Some(weak_last_focused_view) =
2206 // self.last_focused_view_by_item.get(&active_item.id())
2207 // {
2208 // if let Some(last_focused_view) = weak_last_focused_view.upgrade(cx) {
2209 // cx.focus(&last_focused_view);
2210 // return;
2211 // } else {
2212 // self.last_focused_view_by_item.remove(&active_item.id());
2213 // }
2214 // }
2215
2216 // cx.focus(active_item.as_any());
2217 // } else if focused != self.tab_bar_context_menu.handle {
2218 // self.last_focused_view_by_item
2219 // .insert(active_item.id(), focused.downgrade());
2220 // }
2221 // }
2222 // }
2223
2224 // fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
2225 // self.has_focus = false;
2226 // self.toolbar.update(cx, |toolbar, cx| {
2227 // toolbar.focus_changed(false, cx);
2228 // });
2229 // cx.notify();
2230 // }
2231
2232 // fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) {
2233 // Self::reset_to_default_keymap_context(keymap);
2234 // }
2235}
2236
2237impl ItemNavHistory {
2238 pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut WindowContext) {
2239 self.history.push(data, self.item.clone(), cx);
2240 }
2241
2242 pub fn pop_backward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
2243 self.history.pop(NavigationMode::GoingBack, cx)
2244 }
2245
2246 pub fn pop_forward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
2247 self.history.pop(NavigationMode::GoingForward, cx)
2248 }
2249}
2250
2251impl NavHistory {
2252 pub fn for_each_entry(
2253 &self,
2254 cx: &AppContext,
2255 mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
2256 ) {
2257 let borrowed_history = self.0.lock();
2258 borrowed_history
2259 .forward_stack
2260 .iter()
2261 .chain(borrowed_history.backward_stack.iter())
2262 .chain(borrowed_history.closed_stack.iter())
2263 .for_each(|entry| {
2264 if let Some(project_and_abs_path) =
2265 borrowed_history.paths_by_item.get(&entry.item.id())
2266 {
2267 f(entry, project_and_abs_path.clone());
2268 } else if let Some(item) = entry.item.upgrade() {
2269 if let Some(path) = item.project_path(cx) {
2270 f(entry, (path, None));
2271 }
2272 }
2273 })
2274 }
2275
2276 pub fn set_mode(&mut self, mode: NavigationMode) {
2277 self.0.lock().mode = mode;
2278 }
2279
2280 pub fn mode(&self) -> NavigationMode {
2281 self.0.lock().mode
2282 }
2283
2284 pub fn disable(&mut self) {
2285 self.0.lock().mode = NavigationMode::Disabled;
2286 }
2287
2288 pub fn enable(&mut self) {
2289 self.0.lock().mode = NavigationMode::Normal;
2290 }
2291
2292 pub fn pop(&mut self, mode: NavigationMode, cx: &mut WindowContext) -> Option<NavigationEntry> {
2293 let mut state = self.0.lock();
2294 let entry = match mode {
2295 NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
2296 return None
2297 }
2298 NavigationMode::GoingBack => &mut state.backward_stack,
2299 NavigationMode::GoingForward => &mut state.forward_stack,
2300 NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
2301 }
2302 .pop_back();
2303 if entry.is_some() {
2304 state.did_update(cx);
2305 }
2306 entry
2307 }
2308
2309 pub fn push<D: 'static + Send + Any>(
2310 &mut self,
2311 data: Option<D>,
2312 item: Arc<dyn WeakItemHandle>,
2313 cx: &mut WindowContext,
2314 ) {
2315 let state = &mut *self.0.lock();
2316 match state.mode {
2317 NavigationMode::Disabled => {}
2318 NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
2319 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2320 state.backward_stack.pop_front();
2321 }
2322 state.backward_stack.push_back(NavigationEntry {
2323 item,
2324 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2325 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2326 });
2327 state.forward_stack.clear();
2328 }
2329 NavigationMode::GoingBack => {
2330 if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2331 state.forward_stack.pop_front();
2332 }
2333 state.forward_stack.push_back(NavigationEntry {
2334 item,
2335 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2336 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2337 });
2338 }
2339 NavigationMode::GoingForward => {
2340 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2341 state.backward_stack.pop_front();
2342 }
2343 state.backward_stack.push_back(NavigationEntry {
2344 item,
2345 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2346 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2347 });
2348 }
2349 NavigationMode::ClosingItem => {
2350 if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
2351 state.closed_stack.pop_front();
2352 }
2353 state.closed_stack.push_back(NavigationEntry {
2354 item,
2355 data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
2356 timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
2357 });
2358 }
2359 }
2360 state.did_update(cx);
2361 }
2362
2363 pub fn remove_item(&mut self, item_id: EntityId) {
2364 let mut state = self.0.lock();
2365 state.paths_by_item.remove(&item_id);
2366 state
2367 .backward_stack
2368 .retain(|entry| entry.item.id() != item_id);
2369 state
2370 .forward_stack
2371 .retain(|entry| entry.item.id() != item_id);
2372 state
2373 .closed_stack
2374 .retain(|entry| entry.item.id() != item_id);
2375 }
2376
2377 pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
2378 self.0.lock().paths_by_item.get(&item_id).cloned()
2379 }
2380}
2381
2382impl NavHistoryState {
2383 pub fn did_update(&self, cx: &mut WindowContext) {
2384 if let Some(pane) = self.pane.upgrade() {
2385 cx.defer(move |cx| {
2386 pane.update(cx, |pane, cx| pane.history_updated(cx));
2387 });
2388 }
2389 }
2390}
2391
2392// pub struct PaneBackdrop<V> {
2393// child_view: usize,
2394// child: AnyElement<V>,
2395// }
2396
2397// impl<V> PaneBackdrop<V> {
2398// pub fn new(pane_item_view: usize, child: AnyElement<V>) -> Self {
2399// PaneBackdrop {
2400// child,
2401// child_view: pane_item_view,
2402// }
2403// }
2404// }
2405
2406// impl<V: 'static> Element<V> for PaneBackdrop<V> {
2407// type LayoutState = ();
2408
2409// type PaintState = ();
2410
2411// fn layout(
2412// &mut self,
2413// constraint: gpui::SizeConstraint,
2414// view: &mut V,
2415// cx: &mut ViewContext<V>,
2416// ) -> (Vector2F, Self::LayoutState) {
2417// let size = self.child.layout(constraint, view, cx);
2418// (size, ())
2419// }
2420
2421// fn paint(
2422// &mut self,
2423// bounds: RectF,
2424// visible_bounds: RectF,
2425// _: &mut Self::LayoutState,
2426// view: &mut V,
2427// cx: &mut ViewContext<V>,
2428// ) -> Self::PaintState {
2429// let background = theme::current(cx).editor.background;
2430
2431// let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
2432
2433// cx.scene().push_quad(gpui::Quad {
2434// bounds: RectF::new(bounds.origin(), bounds.size()),
2435// background: Some(background),
2436// ..Default::default()
2437// });
2438
2439// let child_view_id = self.child_view;
2440// cx.scene().push_mouse_region(
2441// MouseRegion::new::<Self>(child_view_id, 0, visible_bounds).on_down(
2442// gpui::platform::MouseButton::Left,
2443// move |_, _: &mut V, cx| {
2444// let window = cx.window();
2445// cx.app_context().focus(window, Some(child_view_id))
2446// },
2447// ),
2448// );
2449
2450// cx.scene().push_layer(Some(bounds));
2451// self.child.paint(bounds.origin(), visible_bounds, view, cx);
2452// cx.scene().pop_layer();
2453// }
2454
2455// fn rect_for_text_range(
2456// &self,
2457// range_utf16: std::ops::Range<usize>,
2458// _bounds: RectF,
2459// _visible_bounds: RectF,
2460// _layout: &Self::LayoutState,
2461// _paint: &Self::PaintState,
2462// view: &V,
2463// cx: &gpui::ViewContext<V>,
2464// ) -> Option<RectF> {
2465// self.child.rect_for_text_range(range_utf16, view, cx)
2466// }
2467
2468// fn debug(
2469// &self,
2470// _bounds: RectF,
2471// _layout: &Self::LayoutState,
2472// _paint: &Self::PaintState,
2473// view: &V,
2474// cx: &gpui::ViewContext<V>,
2475// ) -> serde_json::Value {
2476// gpui::json::json!({
2477// "type": "Pane Back Drop",
2478// "view": self.child_view,
2479// "child": self.child.debug(view, cx),
2480// })
2481// }
2482// }
2483
2484fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
2485 let path = buffer_path
2486 .as_ref()
2487 .and_then(|p| p.path.to_str())
2488 .unwrap_or(&"This buffer");
2489 let path = truncate_and_remove_front(path, 80);
2490 format!("{path} contains unsaved edits. Do you want to save it?")
2491}
2492
2493// todo!("uncomment tests")
2494// #[cfg(test)]
2495// mod tests {
2496// use super::*;
2497// use crate::item::test::{TestItem, TestProjectItem};
2498// use gpui::TestAppContext;
2499// use project::FakeFs;
2500// use settings::SettingsStore;
2501
2502// #[gpui::test]
2503// async fn test_remove_active_empty(cx: &mut TestAppContext) {
2504// init_test(cx);
2505// let fs = FakeFs::new(cx.background());
2506
2507// let project = Project::test(fs, None, cx).await;
2508// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2509// let workspace = window.root(cx);
2510// let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2511
2512// pane.update(cx, |pane, cx| {
2513// assert!(pane
2514// .close_active_item(&CloseActiveItem { save_intent: None }, cx)
2515// .is_none())
2516// });
2517// }
2518
2519// #[gpui::test]
2520// async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
2521// cx.foreground().forbid_parking();
2522// init_test(cx);
2523// let fs = FakeFs::new(cx.background());
2524
2525// let project = Project::test(fs, None, cx).await;
2526// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2527// let workspace = window.root(cx);
2528// let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2529
2530// // 1. Add with a destination index
2531// // a. Add before the active item
2532// set_labeled_items(&pane, ["A", "B*", "C"], cx);
2533// pane.update(cx, |pane, cx| {
2534// pane.add_item(
2535// Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
2536// false,
2537// false,
2538// Some(0),
2539// cx,
2540// );
2541// });
2542// assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
2543
2544// // b. Add after the active item
2545// set_labeled_items(&pane, ["A", "B*", "C"], cx);
2546// pane.update(cx, |pane, cx| {
2547// pane.add_item(
2548// Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
2549// false,
2550// false,
2551// Some(2),
2552// cx,
2553// );
2554// });
2555// assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
2556
2557// // c. Add at the end of the item list (including off the length)
2558// set_labeled_items(&pane, ["A", "B*", "C"], cx);
2559// pane.update(cx, |pane, cx| {
2560// pane.add_item(
2561// Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
2562// false,
2563// false,
2564// Some(5),
2565// cx,
2566// );
2567// });
2568// assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2569
2570// // 2. Add without a destination index
2571// // a. Add with active item at the start of the item list
2572// set_labeled_items(&pane, ["A*", "B", "C"], cx);
2573// pane.update(cx, |pane, cx| {
2574// pane.add_item(
2575// Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
2576// false,
2577// false,
2578// None,
2579// cx,
2580// );
2581// });
2582// set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
2583
2584// // b. Add with active item at the end of the item list
2585// set_labeled_items(&pane, ["A", "B", "C*"], cx);
2586// pane.update(cx, |pane, cx| {
2587// pane.add_item(
2588// Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
2589// false,
2590// false,
2591// None,
2592// cx,
2593// );
2594// });
2595// assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2596// }
2597
2598// #[gpui::test]
2599// async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
2600// cx.foreground().forbid_parking();
2601// init_test(cx);
2602// let fs = FakeFs::new(cx.background());
2603
2604// let project = Project::test(fs, None, cx).await;
2605// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2606// let workspace = window.root(cx);
2607// let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2608
2609// // 1. Add with a destination index
2610// // 1a. Add before the active item
2611// let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2612// pane.update(cx, |pane, cx| {
2613// pane.add_item(d, false, false, Some(0), cx);
2614// });
2615// assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
2616
2617// // 1b. Add after the active item
2618// let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2619// pane.update(cx, |pane, cx| {
2620// pane.add_item(d, false, false, Some(2), cx);
2621// });
2622// assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
2623
2624// // 1c. Add at the end of the item list (including off the length)
2625// let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
2626// pane.update(cx, |pane, cx| {
2627// pane.add_item(a, false, false, Some(5), cx);
2628// });
2629// assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
2630
2631// // 1d. Add same item to active index
2632// let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
2633// pane.update(cx, |pane, cx| {
2634// pane.add_item(b, false, false, Some(1), cx);
2635// });
2636// assert_item_labels(&pane, ["A", "B*", "C"], cx);
2637
2638// // 1e. Add item to index after same item in last position
2639// let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
2640// pane.update(cx, |pane, cx| {
2641// pane.add_item(c, false, false, Some(2), cx);
2642// });
2643// assert_item_labels(&pane, ["A", "B", "C*"], cx);
2644
2645// // 2. Add without a destination index
2646// // 2a. Add with active item at the start of the item list
2647// let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
2648// pane.update(cx, |pane, cx| {
2649// pane.add_item(d, false, false, None, cx);
2650// });
2651// assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
2652
2653// // 2b. Add with active item at the end of the item list
2654// let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
2655// pane.update(cx, |pane, cx| {
2656// pane.add_item(a, false, false, None, cx);
2657// });
2658// assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
2659
2660// // 2c. Add active item to active item at end of list
2661// let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
2662// pane.update(cx, |pane, cx| {
2663// pane.add_item(c, false, false, None, cx);
2664// });
2665// assert_item_labels(&pane, ["A", "B", "C*"], cx);
2666
2667// // 2d. Add active item to active item at start of list
2668// let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
2669// pane.update(cx, |pane, cx| {
2670// pane.add_item(a, false, false, None, cx);
2671// });
2672// assert_item_labels(&pane, ["A*", "B", "C"], cx);
2673// }
2674
2675// #[gpui::test]
2676// async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
2677// cx.foreground().forbid_parking();
2678// init_test(cx);
2679// let fs = FakeFs::new(cx.background());
2680
2681// let project = Project::test(fs, None, cx).await;
2682// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2683// let workspace = window.root(cx);
2684// let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2685
2686// // singleton view
2687// pane.update(cx, |pane, cx| {
2688// let item = TestItem::new()
2689// .with_singleton(true)
2690// .with_label("buffer 1")
2691// .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)]);
2692
2693// pane.add_item(Box::new(cx.add_view(|_| item)), false, false, None, cx);
2694// });
2695// assert_item_labels(&pane, ["buffer 1*"], cx);
2696
2697// // new singleton view with the same project entry
2698// pane.update(cx, |pane, cx| {
2699// let item = TestItem::new()
2700// .with_singleton(true)
2701// .with_label("buffer 1")
2702// .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]);
2703
2704// pane.add_item(Box::new(cx.add_view(|_| item)), false, false, None, cx);
2705// });
2706// assert_item_labels(&pane, ["buffer 1*"], cx);
2707
2708// // new singleton view with different project entry
2709// pane.update(cx, |pane, cx| {
2710// let item = TestItem::new()
2711// .with_singleton(true)
2712// .with_label("buffer 2")
2713// .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)]);
2714// pane.add_item(Box::new(cx.add_view(|_| item)), false, false, None, cx);
2715// });
2716// assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
2717
2718// // new multibuffer view with the same project entry
2719// pane.update(cx, |pane, cx| {
2720// let item = TestItem::new()
2721// .with_singleton(false)
2722// .with_label("multibuffer 1")
2723// .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]);
2724
2725// pane.add_item(Box::new(cx.add_view(|_| item)), false, false, None, cx);
2726// });
2727// assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
2728
2729// // another multibuffer view with the same project entry
2730// pane.update(cx, |pane, cx| {
2731// let item = TestItem::new()
2732// .with_singleton(false)
2733// .with_label("multibuffer 1b")
2734// .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]);
2735
2736// pane.add_item(Box::new(cx.add_view(|_| item)), false, false, None, cx);
2737// });
2738// assert_item_labels(
2739// &pane,
2740// ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
2741// cx,
2742// );
2743// }
2744
2745// #[gpui::test]
2746// async fn test_remove_item_ordering(cx: &mut TestAppContext) {
2747// init_test(cx);
2748// let fs = FakeFs::new(cx.background());
2749
2750// let project = Project::test(fs, None, cx).await;
2751// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2752// let workspace = window.root(cx);
2753// let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2754
2755// add_labeled_item(&pane, "A", false, cx);
2756// add_labeled_item(&pane, "B", false, cx);
2757// add_labeled_item(&pane, "C", false, cx);
2758// add_labeled_item(&pane, "D", false, cx);
2759// assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2760
2761// pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
2762// add_labeled_item(&pane, "1", false, cx);
2763// assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
2764
2765// pane.update(cx, |pane, cx| {
2766// pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
2767// })
2768// .unwrap()
2769// .await
2770// .unwrap();
2771// assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
2772
2773// pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
2774// assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
2775
2776// pane.update(cx, |pane, cx| {
2777// pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
2778// })
2779// .unwrap()
2780// .await
2781// .unwrap();
2782// assert_item_labels(&pane, ["A", "B*", "C"], cx);
2783
2784// pane.update(cx, |pane, cx| {
2785// pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
2786// })
2787// .unwrap()
2788// .await
2789// .unwrap();
2790// assert_item_labels(&pane, ["A", "C*"], cx);
2791
2792// pane.update(cx, |pane, cx| {
2793// pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
2794// })
2795// .unwrap()
2796// .await
2797// .unwrap();
2798// assert_item_labels(&pane, ["A*"], cx);
2799// }
2800
2801// #[gpui::test]
2802// async fn test_close_inactive_items(cx: &mut TestAppContext) {
2803// init_test(cx);
2804// let fs = FakeFs::new(cx.background());
2805
2806// let project = Project::test(fs, None, cx).await;
2807// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2808// let workspace = window.root(cx);
2809// let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2810
2811// set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
2812
2813// pane.update(cx, |pane, cx| {
2814// pane.close_inactive_items(&CloseInactiveItems, cx)
2815// })
2816// .unwrap()
2817// .await
2818// .unwrap();
2819// assert_item_labels(&pane, ["C*"], cx);
2820// }
2821
2822// #[gpui::test]
2823// async fn test_close_clean_items(cx: &mut TestAppContext) {
2824// init_test(cx);
2825// let fs = FakeFs::new(cx.background());
2826
2827// let project = Project::test(fs, None, cx).await;
2828// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2829// let workspace = window.root(cx);
2830// let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2831
2832// add_labeled_item(&pane, "A", true, cx);
2833// add_labeled_item(&pane, "B", false, cx);
2834// add_labeled_item(&pane, "C", true, cx);
2835// add_labeled_item(&pane, "D", false, cx);
2836// add_labeled_item(&pane, "E", false, cx);
2837// assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
2838
2839// pane.update(cx, |pane, cx| pane.close_clean_items(&CloseCleanItems, cx))
2840// .unwrap()
2841// .await
2842// .unwrap();
2843// assert_item_labels(&pane, ["A^", "C*^"], cx);
2844// }
2845
2846// #[gpui::test]
2847// async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
2848// init_test(cx);
2849// let fs = FakeFs::new(cx.background());
2850
2851// let project = Project::test(fs, None, cx).await;
2852// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2853// let workspace = window.root(cx);
2854// let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2855
2856// set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
2857
2858// pane.update(cx, |pane, cx| {
2859// pane.close_items_to_the_left(&CloseItemsToTheLeft, cx)
2860// })
2861// .unwrap()
2862// .await
2863// .unwrap();
2864// assert_item_labels(&pane, ["C*", "D", "E"], cx);
2865// }
2866
2867// #[gpui::test]
2868// async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
2869// init_test(cx);
2870// let fs = FakeFs::new(cx.background());
2871
2872// let project = Project::test(fs, None, cx).await;
2873// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2874// let workspace = window.root(cx);
2875// let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2876
2877// set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
2878
2879// pane.update(cx, |pane, cx| {
2880// pane.close_items_to_the_right(&CloseItemsToTheRight, cx)
2881// })
2882// .unwrap()
2883// .await
2884// .unwrap();
2885// assert_item_labels(&pane, ["A", "B", "C*"], cx);
2886// }
2887
2888// #[gpui::test]
2889// async fn test_close_all_items(cx: &mut TestAppContext) {
2890// init_test(cx);
2891// let fs = FakeFs::new(cx.background());
2892
2893// let project = Project::test(fs, None, cx).await;
2894// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2895// let workspace = window.root(cx);
2896// let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2897
2898// add_labeled_item(&pane, "A", false, cx);
2899// add_labeled_item(&pane, "B", false, cx);
2900// add_labeled_item(&pane, "C", false, cx);
2901// assert_item_labels(&pane, ["A", "B", "C*"], cx);
2902
2903// pane.update(cx, |pane, cx| {
2904// pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
2905// })
2906// .unwrap()
2907// .await
2908// .unwrap();
2909// assert_item_labels(&pane, [], cx);
2910
2911// add_labeled_item(&pane, "A", true, cx);
2912// add_labeled_item(&pane, "B", true, cx);
2913// add_labeled_item(&pane, "C", true, cx);
2914// assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
2915
2916// let save = pane
2917// .update(cx, |pane, cx| {
2918// pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
2919// })
2920// .unwrap();
2921
2922// cx.foreground().run_until_parked();
2923// window.simulate_prompt_answer(2, cx);
2924// save.await.unwrap();
2925// assert_item_labels(&pane, [], cx);
2926// }
2927
2928// fn init_test(cx: &mut TestAppContext) {
2929// cx.update(|cx| {
2930// cx.set_global(SettingsStore::test(cx));
2931// theme::init((), cx);
2932// crate::init_settings(cx);
2933// Project::init_settings(cx);
2934// });
2935// }
2936
2937// fn add_labeled_item(
2938// pane: &ViewHandle<Pane>,
2939// label: &str,
2940// is_dirty: bool,
2941// cx: &mut TestAppContext,
2942// ) -> Box<ViewHandle<TestItem>> {
2943// pane.update(cx, |pane, cx| {
2944// let labeled_item =
2945// Box::new(cx.add_view(|_| TestItem::new().with_label(label).with_dirty(is_dirty)));
2946// pane.add_item(labeled_item.clone(), false, false, None, cx);
2947// labeled_item
2948// })
2949// }
2950
2951// fn set_labeled_items<const COUNT: usize>(
2952// pane: &ViewHandle<Pane>,
2953// labels: [&str; COUNT],
2954// cx: &mut TestAppContext,
2955// ) -> [Box<ViewHandle<TestItem>>; COUNT] {
2956// pane.update(cx, |pane, cx| {
2957// pane.items.clear();
2958// let mut active_item_index = 0;
2959
2960// let mut index = 0;
2961// let items = labels.map(|mut label| {
2962// if label.ends_with("*") {
2963// label = label.trim_end_matches("*");
2964// active_item_index = index;
2965// }
2966
2967// let labeled_item = Box::new(cx.add_view(|_| TestItem::new().with_label(label)));
2968// pane.add_item(labeled_item.clone(), false, false, None, cx);
2969// index += 1;
2970// labeled_item
2971// });
2972
2973// pane.activate_item(active_item_index, false, false, cx);
2974
2975// items
2976// })
2977// }
2978
2979// // Assert the item label, with the active item label suffixed with a '*'
2980// fn assert_item_labels<const COUNT: usize>(
2981// pane: &ViewHandle<Pane>,
2982// expected_states: [&str; COUNT],
2983// cx: &mut TestAppContext,
2984// ) {
2985// pane.read_with(cx, |pane, cx| {
2986// let actual_states = pane
2987// .items
2988// .iter()
2989// .enumerate()
2990// .map(|(ix, item)| {
2991// let mut state = item
2992// .as_any()
2993// .downcast_ref::<TestItem>()
2994// .unwrap()
2995// .read(cx)
2996// .label
2997// .clone();
2998// if ix == pane.active_item_index {
2999// state.push('*');
3000// }
3001// if item.is_dirty(cx) {
3002// state.push('^');
3003// }
3004// state
3005// })
3006// .collect::<Vec<_>>();
3007
3008// assert_eq!(
3009// actual_states, expected_states,
3010// "pane items do not match expectation"
3011// );
3012// })
3013// }
3014// }
3015
3016#[derive(Clone, Debug)]
3017struct DraggedTab {
3018 title: String,
3019}
3020
3021impl Render for DraggedTab {
3022 type Element = Div;
3023
3024 fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
3025 div().w_8().h_4().bg(gpui::red())
3026 }
3027}