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