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