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