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