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