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