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