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