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