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