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