1use super::{ItemHandle, SplitDirection};
2use crate::{toolbar::Toolbar, Item, NewFile, NewSearch, NewTerminal, WeakItemHandle, Workspace};
3use anyhow::Result;
4use collections::{HashMap, HashSet, VecDeque};
5use context_menu::{ContextMenu, ContextMenuItem};
6use drag_and_drop::{DragAndDrop, Draggable};
7use futures::StreamExt;
8use gpui::{
9 actions,
10 color::Color,
11 elements::*,
12 geometry::{
13 rect::RectF,
14 vector::{vec2f, Vector2F},
15 },
16 impl_actions, impl_internal_actions,
17 platform::{CursorStyle, NavigationDirection},
18 AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, EventContext,
19 ModelHandle, MouseButton, MutableAppContext, PromptLevel, Quad, RenderContext, Task, View,
20 ViewContext, ViewHandle, WeakViewHandle,
21};
22use project::{Project, ProjectEntryId, ProjectPath};
23use serde::Deserialize;
24use settings::{Autosave, Settings};
25use std::{any::Any, cell::RefCell, cmp, mem, path::Path, rc::Rc};
26use theme::Theme;
27use util::ResultExt;
28
29#[derive(Clone, Deserialize, PartialEq)]
30pub struct ActivateItem(pub usize);
31
32actions!(
33 pane,
34 [
35 ActivatePrevItem,
36 ActivateNextItem,
37 ActivateLastItem,
38 CloseActiveItem,
39 CloseInactiveItems,
40 ReopenClosedItem,
41 SplitLeft,
42 SplitUp,
43 SplitRight,
44 SplitDown,
45 ]
46);
47
48#[derive(Clone, PartialEq)]
49pub struct CloseItem {
50 pub item_id: usize,
51 pub pane: WeakViewHandle<Pane>,
52}
53
54#[derive(Clone, PartialEq)]
55pub struct MoveItem {
56 pub item_id: usize,
57 pub from: WeakViewHandle<Pane>,
58 pub to: WeakViewHandle<Pane>,
59 pub destination_index: usize,
60}
61
62#[derive(Clone, Deserialize, PartialEq)]
63pub struct GoBack {
64 #[serde(skip_deserializing)]
65 pub pane: Option<WeakViewHandle<Pane>>,
66}
67
68#[derive(Clone, Deserialize, PartialEq)]
69pub struct GoForward {
70 #[serde(skip_deserializing)]
71 pub pane: Option<WeakViewHandle<Pane>>,
72}
73
74#[derive(Clone, PartialEq)]
75pub struct DeploySplitMenu {
76 position: Vector2F,
77}
78
79#[derive(Clone, PartialEq)]
80pub struct DeployNewMenu {
81 position: Vector2F,
82}
83
84impl_actions!(pane, [GoBack, GoForward, ActivateItem]);
85impl_internal_actions!(pane, [CloseItem, DeploySplitMenu, DeployNewMenu, MoveItem]);
86
87const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
88
89pub fn init(cx: &mut MutableAppContext) {
90 cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
91 pane.activate_item(action.0, true, true, cx);
92 });
93 cx.add_action(|pane: &mut Pane, _: &ActivateLastItem, cx| {
94 pane.activate_item(pane.items.len() - 1, true, true, cx);
95 });
96 cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
97 pane.activate_prev_item(cx);
98 });
99 cx.add_action(|pane: &mut Pane, _: &ActivateNextItem, cx| {
100 pane.activate_next_item(cx);
101 });
102 cx.add_async_action(Pane::close_active_item);
103 cx.add_async_action(Pane::close_inactive_items);
104 cx.add_async_action(|workspace: &mut Workspace, action: &CloseItem, cx| {
105 let pane = action.pane.upgrade(cx)?;
106 let task = Pane::close_item(workspace, pane, action.item_id, cx);
107 Some(cx.foreground().spawn(async move {
108 task.await?;
109 Ok(())
110 }))
111 });
112 cx.add_action(
113 |workspace,
114 MoveItem {
115 from,
116 to,
117 item_id,
118 destination_index,
119 },
120 cx| {
121 // Get item handle to move
122 let from = if let Some(from) = from.upgrade(cx) {
123 from
124 } else {
125 return;
126 };
127
128 // Add item to new pane at given index
129 let to = if let Some(to) = to.upgrade(cx) {
130 to
131 } else {
132 return;
133 };
134
135 Pane::move_item(workspace, from, to, *item_id, *destination_index, cx)
136 },
137 );
138 cx.add_action(|pane: &mut Pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx));
139 cx.add_action(|pane: &mut Pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx));
140 cx.add_action(|pane: &mut Pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx));
141 cx.add_action(|pane: &mut Pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx));
142 cx.add_action(Pane::deploy_split_menu);
143 cx.add_action(Pane::deploy_new_menu);
144 cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
145 Pane::reopen_closed_item(workspace, cx).detach();
146 });
147 cx.add_action(|workspace: &mut Workspace, action: &GoBack, cx| {
148 Pane::go_back(
149 workspace,
150 action
151 .pane
152 .as_ref()
153 .and_then(|weak_handle| weak_handle.upgrade(cx)),
154 cx,
155 )
156 .detach();
157 });
158 cx.add_action(|workspace: &mut Workspace, action: &GoForward, cx| {
159 Pane::go_forward(
160 workspace,
161 action
162 .pane
163 .as_ref()
164 .and_then(|weak_handle| weak_handle.upgrade(cx)),
165 cx,
166 )
167 .detach();
168 });
169}
170
171pub enum Event {
172 Focused,
173 ActivateItem { local: bool },
174 Remove,
175 RemoveItem { item_id: usize },
176 Split(SplitDirection),
177 ChangeItemTitle,
178}
179
180pub struct Pane {
181 items: Vec<Box<dyn ItemHandle>>,
182 is_active: bool,
183 active_item_index: usize,
184 last_focused_view: Option<AnyWeakViewHandle>,
185 autoscroll: bool,
186 nav_history: Rc<RefCell<NavHistory>>,
187 toolbar: ViewHandle<Toolbar>,
188 context_menu: ViewHandle<ContextMenu>,
189}
190
191pub struct ItemNavHistory {
192 history: Rc<RefCell<NavHistory>>,
193 item: Rc<dyn WeakItemHandle>,
194}
195
196struct NavHistory {
197 mode: NavigationMode,
198 backward_stack: VecDeque<NavigationEntry>,
199 forward_stack: VecDeque<NavigationEntry>,
200 closed_stack: VecDeque<NavigationEntry>,
201 paths_by_item: HashMap<usize, ProjectPath>,
202 pane: WeakViewHandle<Pane>,
203}
204
205#[derive(Copy, Clone)]
206enum NavigationMode {
207 Normal,
208 GoingBack,
209 GoingForward,
210 ClosingItem,
211 ReopeningClosedItem,
212 Disabled,
213}
214
215impl Default for NavigationMode {
216 fn default() -> Self {
217 Self::Normal
218 }
219}
220
221pub struct NavigationEntry {
222 pub item: Rc<dyn WeakItemHandle>,
223 pub data: Option<Box<dyn Any>>,
224}
225
226struct DraggedItem {
227 item: Box<dyn ItemHandle>,
228 pane: WeakViewHandle<Pane>,
229}
230
231pub enum ReorderBehavior {
232 None,
233 MoveAfterActive,
234 MoveToIndex(usize),
235}
236
237impl Pane {
238 pub fn new(cx: &mut ViewContext<Self>) -> Self {
239 let handle = cx.weak_handle();
240 let context_menu = cx.add_view(ContextMenu::new);
241 Self {
242 items: Vec::new(),
243 is_active: true,
244 active_item_index: 0,
245 last_focused_view: None,
246 autoscroll: false,
247 nav_history: Rc::new(RefCell::new(NavHistory {
248 mode: NavigationMode::Normal,
249 backward_stack: Default::default(),
250 forward_stack: Default::default(),
251 closed_stack: Default::default(),
252 paths_by_item: Default::default(),
253 pane: handle.clone(),
254 })),
255 toolbar: cx.add_view(|_| Toolbar::new(handle)),
256 context_menu,
257 }
258 }
259
260 pub fn set_active(&mut self, is_active: bool, cx: &mut ViewContext<Self>) {
261 self.is_active = is_active;
262 cx.notify();
263 }
264
265 pub fn nav_history_for_item<T: Item>(&self, item: &ViewHandle<T>) -> ItemNavHistory {
266 ItemNavHistory {
267 history: self.nav_history.clone(),
268 item: Rc::new(item.downgrade()),
269 }
270 }
271
272 pub fn go_back(
273 workspace: &mut Workspace,
274 pane: Option<ViewHandle<Pane>>,
275 cx: &mut ViewContext<Workspace>,
276 ) -> Task<()> {
277 Self::navigate_history(
278 workspace,
279 pane.unwrap_or_else(|| workspace.active_pane().clone()),
280 NavigationMode::GoingBack,
281 cx,
282 )
283 }
284
285 pub fn go_forward(
286 workspace: &mut Workspace,
287 pane: Option<ViewHandle<Pane>>,
288 cx: &mut ViewContext<Workspace>,
289 ) -> Task<()> {
290 Self::navigate_history(
291 workspace,
292 pane.unwrap_or_else(|| workspace.active_pane().clone()),
293 NavigationMode::GoingForward,
294 cx,
295 )
296 }
297
298 pub fn reopen_closed_item(
299 workspace: &mut Workspace,
300 cx: &mut ViewContext<Workspace>,
301 ) -> Task<()> {
302 Self::navigate_history(
303 workspace,
304 workspace.active_pane().clone(),
305 NavigationMode::ReopeningClosedItem,
306 cx,
307 )
308 }
309
310 pub fn disable_history(&mut self) {
311 self.nav_history.borrow_mut().disable();
312 }
313
314 pub fn enable_history(&mut self) {
315 self.nav_history.borrow_mut().enable();
316 }
317
318 pub fn can_navigate_backward(&self) -> bool {
319 !self.nav_history.borrow().backward_stack.is_empty()
320 }
321
322 pub fn can_navigate_forward(&self) -> bool {
323 !self.nav_history.borrow().forward_stack.is_empty()
324 }
325
326 fn history_updated(&mut self, cx: &mut ViewContext<Self>) {
327 self.toolbar.update(cx, |_, cx| cx.notify());
328 }
329
330 fn navigate_history(
331 workspace: &mut Workspace,
332 pane: ViewHandle<Pane>,
333 mode: NavigationMode,
334 cx: &mut ViewContext<Workspace>,
335 ) -> Task<()> {
336 cx.focus(pane.clone());
337
338 let to_load = pane.update(cx, |pane, cx| {
339 loop {
340 // Retrieve the weak item handle from the history.
341 let entry = pane.nav_history.borrow_mut().pop(mode, cx)?;
342
343 // If the item is still present in this pane, then activate it.
344 if let Some(index) = entry
345 .item
346 .upgrade(cx)
347 .and_then(|v| pane.index_for_item(v.as_ref()))
348 {
349 let prev_active_item_index = pane.active_item_index;
350 pane.nav_history.borrow_mut().set_mode(mode);
351 pane.activate_item(index, true, true, cx);
352 pane.nav_history
353 .borrow_mut()
354 .set_mode(NavigationMode::Normal);
355
356 let mut navigated = prev_active_item_index != pane.active_item_index;
357 if let Some(data) = entry.data {
358 navigated |= pane.active_item()?.navigate(data, cx);
359 }
360
361 if navigated {
362 break None;
363 }
364 }
365 // If the item is no longer present in this pane, then retrieve its
366 // project path in order to reopen it.
367 else {
368 break pane
369 .nav_history
370 .borrow()
371 .paths_by_item
372 .get(&entry.item.id())
373 .cloned()
374 .map(|project_path| (project_path, entry));
375 }
376 }
377 });
378
379 if let Some((project_path, entry)) = to_load {
380 // If the item was no longer present, then load it again from its previous path.
381 let pane = pane.downgrade();
382 let task = workspace.load_path(project_path, cx);
383 cx.spawn(|workspace, mut cx| async move {
384 let task = task.await;
385 if let Some(pane) = pane.upgrade(&cx) {
386 let mut navigated = false;
387 if let Some((project_entry_id, build_item)) = task.log_err() {
388 let prev_active_item_id = pane.update(&mut cx, |pane, _| {
389 pane.nav_history.borrow_mut().set_mode(mode);
390 pane.active_item().map(|p| p.id())
391 });
392
393 let item = workspace.update(&mut cx, |workspace, cx| {
394 Self::open_item(
395 workspace,
396 pane.clone(),
397 project_entry_id,
398 true,
399 cx,
400 build_item,
401 )
402 });
403
404 pane.update(&mut cx, |pane, cx| {
405 navigated |= Some(item.id()) != prev_active_item_id;
406 pane.nav_history
407 .borrow_mut()
408 .set_mode(NavigationMode::Normal);
409 if let Some(data) = entry.data {
410 navigated |= item.navigate(data, cx);
411 }
412 });
413 }
414
415 if !navigated {
416 workspace
417 .update(&mut cx, |workspace, cx| {
418 Self::navigate_history(workspace, pane, mode, cx)
419 })
420 .await;
421 }
422 }
423 })
424 } else {
425 Task::ready(())
426 }
427 }
428
429 pub(crate) fn open_item(
430 workspace: &mut Workspace,
431 pane: ViewHandle<Pane>,
432 project_entry_id: ProjectEntryId,
433 focus_item: bool,
434 cx: &mut ViewContext<Workspace>,
435 build_item: impl FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
436 ) -> Box<dyn ItemHandle> {
437 let existing_item = pane.update(cx, |pane, cx| {
438 for item in pane.items.iter() {
439 if item.project_path(cx).is_some()
440 && item.project_entry_ids(cx).as_slice() == [project_entry_id]
441 {
442 let item = item.boxed_clone();
443 return Some(item);
444 }
445 }
446 None
447 });
448
449 // Even if the item exists, we re-add it to reorder it after the active item.
450 // We may revisit this behavior after adding an "activation history" for pane items.
451 let item = existing_item.unwrap_or_else(|| pane.update(cx, |_, cx| build_item(cx)));
452 Pane::add_item(workspace, &pane, item.clone(), true, focus_item, None, cx);
453 item
454 }
455
456 pub(crate) fn add_item(
457 workspace: &mut Workspace,
458 pane: &ViewHandle<Pane>,
459 item: Box<dyn ItemHandle>,
460 activate_pane: bool,
461 focus_item: bool,
462 destination_index: Option<usize>,
463 cx: &mut ViewContext<Workspace>,
464 ) {
465 // If no destination index is specified, add or move the item after the active item.
466 let mut insertion_index = {
467 let pane = pane.read(cx);
468 cmp::min(
469 if let Some(destination_index) = destination_index {
470 destination_index
471 } else {
472 pane.active_item_index + 1
473 },
474 pane.items.len(),
475 )
476 };
477
478 item.added_to_pane(workspace, pane.clone(), cx);
479
480 // Does the item already exist?
481 let project_entry_id = if item.is_singleton(cx) {
482 item.project_entry_ids(cx).get(0).copied()
483 } else {
484 None
485 };
486
487 let existing_item_index = pane.read(cx).items.iter().position(|existing_item| {
488 if existing_item.id() == item.id() {
489 true
490 } else if existing_item.is_singleton(cx) {
491 existing_item
492 .project_entry_ids(cx)
493 .get(0)
494 .map_or(false, |existing_entry_id| {
495 Some(existing_entry_id) == project_entry_id.as_ref()
496 })
497 } else {
498 false
499 }
500 });
501
502 if let Some(existing_item_index) = existing_item_index {
503 // If the item already exists, move it to the desired destination and activate it
504 pane.update(cx, |pane, cx| {
505 if existing_item_index != insertion_index {
506 cx.reparent(&item);
507 let existing_item_is_active = existing_item_index == pane.active_item_index;
508
509 // If the caller didn't specify a destination and the added item is already
510 // the active one, don't move it
511 if existing_item_is_active && destination_index.is_none() {
512 insertion_index = existing_item_index;
513 } else {
514 pane.items.remove(existing_item_index);
515 if existing_item_index < pane.active_item_index {
516 pane.active_item_index -= 1;
517 }
518 insertion_index = insertion_index.min(pane.items.len());
519
520 pane.items.insert(insertion_index, item.clone());
521
522 if existing_item_is_active {
523 pane.active_item_index = insertion_index;
524 } else if insertion_index <= pane.active_item_index {
525 pane.active_item_index += 1;
526 }
527 }
528
529 cx.notify();
530 }
531
532 pane.activate_item(insertion_index, activate_pane, focus_item, cx);
533 });
534 } else {
535 pane.update(cx, |pane, cx| {
536 cx.reparent(&item);
537 pane.items.insert(insertion_index, item);
538 if insertion_index <= pane.active_item_index {
539 pane.active_item_index += 1;
540 }
541
542 pane.activate_item(insertion_index, activate_pane, focus_item, cx);
543 cx.notify();
544 });
545 }
546 }
547
548 pub fn items(&self) -> impl Iterator<Item = &Box<dyn ItemHandle>> {
549 self.items.iter()
550 }
551
552 pub fn items_of_type<T: View>(&self) -> impl '_ + Iterator<Item = ViewHandle<T>> {
553 self.items
554 .iter()
555 .filter_map(|item| item.to_any().downcast())
556 }
557
558 pub fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
559 self.items.get(self.active_item_index).cloned()
560 }
561
562 pub fn item_for_entry(
563 &self,
564 entry_id: ProjectEntryId,
565 cx: &AppContext,
566 ) -> Option<Box<dyn ItemHandle>> {
567 self.items.iter().find_map(|item| {
568 if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
569 Some(item.boxed_clone())
570 } else {
571 None
572 }
573 })
574 }
575
576 pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option<usize> {
577 self.items.iter().position(|i| i.id() == item.id())
578 }
579
580 pub fn activate_item(
581 &mut self,
582 index: usize,
583 activate_pane: bool,
584 focus_item: bool,
585 cx: &mut ViewContext<Self>,
586 ) {
587 use NavigationMode::{GoingBack, GoingForward};
588 if index < self.items.len() {
589 let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
590 if prev_active_item_ix != self.active_item_index
591 || matches!(self.nav_history.borrow().mode, GoingBack | GoingForward)
592 {
593 if let Some(prev_item) = self.items.get(prev_active_item_ix) {
594 prev_item.deactivated(cx);
595 }
596 cx.emit(Event::ActivateItem {
597 local: activate_pane,
598 });
599 }
600 self.update_toolbar(cx);
601 if focus_item {
602 self.focus_active_item(cx);
603 }
604 if activate_pane {
605 cx.emit(Event::Focused);
606 }
607 self.autoscroll = true;
608 cx.notify();
609 }
610 }
611
612 pub fn activate_prev_item(&mut self, cx: &mut ViewContext<Self>) {
613 let mut index = self.active_item_index;
614 if index > 0 {
615 index -= 1;
616 } else if !self.items.is_empty() {
617 index = self.items.len() - 1;
618 }
619 self.activate_item(index, true, true, cx);
620 }
621
622 pub fn activate_next_item(&mut self, cx: &mut ViewContext<Self>) {
623 let mut index = self.active_item_index;
624 if index + 1 < self.items.len() {
625 index += 1;
626 } else {
627 index = 0;
628 }
629 self.activate_item(index, true, true, cx);
630 }
631
632 pub fn close_active_item(
633 workspace: &mut Workspace,
634 _: &CloseActiveItem,
635 cx: &mut ViewContext<Workspace>,
636 ) -> Option<Task<Result<()>>> {
637 let pane_handle = workspace.active_pane().clone();
638 let pane = pane_handle.read(cx);
639 if pane.items.is_empty() {
640 None
641 } else {
642 let item_id_to_close = pane.items[pane.active_item_index].id();
643 let task = Self::close_items(workspace, pane_handle, cx, move |item_id| {
644 item_id == item_id_to_close
645 });
646 Some(cx.foreground().spawn(async move {
647 task.await?;
648 Ok(())
649 }))
650 }
651 }
652
653 pub fn close_inactive_items(
654 workspace: &mut Workspace,
655 _: &CloseInactiveItems,
656 cx: &mut ViewContext<Workspace>,
657 ) -> Option<Task<Result<()>>> {
658 let pane_handle = workspace.active_pane().clone();
659 let pane = pane_handle.read(cx);
660 if pane.items.is_empty() {
661 None
662 } else {
663 let active_item_id = pane.items[pane.active_item_index].id();
664 let task =
665 Self::close_items(workspace, pane_handle, cx, move |id| id != active_item_id);
666 Some(cx.foreground().spawn(async move {
667 task.await?;
668 Ok(())
669 }))
670 }
671 }
672
673 pub fn close_item(
674 workspace: &mut Workspace,
675 pane: ViewHandle<Pane>,
676 item_id_to_close: usize,
677 cx: &mut ViewContext<Workspace>,
678 ) -> Task<Result<bool>> {
679 Self::close_items(workspace, pane, cx, move |view_id| {
680 view_id == item_id_to_close
681 })
682 }
683
684 pub fn close_items(
685 workspace: &mut Workspace,
686 pane: ViewHandle<Pane>,
687 cx: &mut ViewContext<Workspace>,
688 should_close: impl 'static + Fn(usize) -> bool,
689 ) -> Task<Result<bool>> {
690 let project = workspace.project().clone();
691
692 // Find the items to close.
693 let mut items_to_close = Vec::new();
694 for item in &pane.read(cx).items {
695 if should_close(item.id()) {
696 items_to_close.push(item.boxed_clone());
697 }
698 }
699
700 // If a buffer is open both in a singleton editor and in a multibuffer, make sure
701 // to focus the singleton buffer when prompting to save that buffer, as opposed
702 // to focusing the multibuffer, because this gives the user a more clear idea
703 // of what content they would be saving.
704 items_to_close.sort_by_key(|item| !item.is_singleton(cx));
705
706 cx.spawn(|workspace, mut cx| async move {
707 let mut saved_project_entry_ids = HashSet::default();
708 for item in items_to_close.clone() {
709 // Find the item's current index and its set of project entries. Avoid
710 // storing these in advance, in case they have changed since this task
711 // was started.
712 let (item_ix, mut project_entry_ids) = pane.read_with(&cx, |pane, cx| {
713 (pane.index_for_item(&*item), item.project_entry_ids(cx))
714 });
715 let item_ix = if let Some(ix) = item_ix {
716 ix
717 } else {
718 continue;
719 };
720
721 // If an item hasn't yet been associated with a project entry, then always
722 // prompt to save it before closing it. Otherwise, check if the item has
723 // any project entries that are not open anywhere else in the workspace,
724 // AND that the user has not already been prompted to save. If there are
725 // any such project entries, prompt the user to save this item.
726 let should_save = if project_entry_ids.is_empty() {
727 true
728 } else {
729 workspace.read_with(&cx, |workspace, cx| {
730 for item in workspace.items(cx) {
731 if !items_to_close
732 .iter()
733 .any(|item_to_close| item_to_close.id() == item.id())
734 {
735 let other_project_entry_ids = item.project_entry_ids(cx);
736 project_entry_ids
737 .retain(|id| !other_project_entry_ids.contains(id));
738 }
739 }
740 });
741 project_entry_ids
742 .iter()
743 .any(|id| saved_project_entry_ids.insert(*id))
744 };
745
746 if should_save
747 && !Self::save_item(project.clone(), &pane, item_ix, &*item, true, &mut cx)
748 .await?
749 {
750 break;
751 }
752
753 // Remove the item from the pane.
754 pane.update(&mut cx, |pane, cx| {
755 if let Some(item_ix) = pane.items.iter().position(|i| i.id() == item.id()) {
756 pane.remove_item(item_ix, cx);
757 }
758 });
759 }
760
761 pane.update(&mut cx, |_, cx| cx.notify());
762 Ok(true)
763 })
764 }
765
766 fn remove_item(&mut self, item_ix: usize, cx: &mut ViewContext<Self>) {
767 if item_ix == self.active_item_index {
768 // Activate the previous item if possible.
769 // This returns the user to the previously opened tab if they closed
770 // a new item they just navigated to.
771 if item_ix > 0 {
772 self.activate_prev_item(cx);
773 } else if item_ix + 1 < self.items.len() {
774 self.activate_next_item(cx);
775 }
776 }
777
778 let item = self.items.remove(item_ix);
779 cx.emit(Event::RemoveItem { item_id: item.id() });
780 if self.items.is_empty() {
781 item.deactivated(cx);
782 self.update_toolbar(cx);
783 cx.emit(Event::Remove);
784 }
785
786 if item_ix < self.active_item_index {
787 self.active_item_index -= 1;
788 }
789
790 self.nav_history
791 .borrow_mut()
792 .set_mode(NavigationMode::ClosingItem);
793 item.deactivated(cx);
794 self.nav_history
795 .borrow_mut()
796 .set_mode(NavigationMode::Normal);
797
798 if let Some(path) = item.project_path(cx) {
799 self.nav_history
800 .borrow_mut()
801 .paths_by_item
802 .insert(item.id(), path);
803 } else {
804 self.nav_history
805 .borrow_mut()
806 .paths_by_item
807 .remove(&item.id());
808 }
809
810 cx.notify();
811 }
812
813 pub async fn save_item(
814 project: ModelHandle<Project>,
815 pane: &ViewHandle<Pane>,
816 item_ix: usize,
817 item: &dyn ItemHandle,
818 should_prompt_for_save: bool,
819 cx: &mut AsyncAppContext,
820 ) -> Result<bool> {
821 const CONFLICT_MESSAGE: &str =
822 "This file has changed on disk since you started editing it. Do you want to overwrite it?";
823 const DIRTY_MESSAGE: &str = "This file contains unsaved edits. Do you want to save it?";
824
825 let (has_conflict, is_dirty, can_save, is_singleton) = cx.read(|cx| {
826 (
827 item.has_conflict(cx),
828 item.is_dirty(cx),
829 item.can_save(cx),
830 item.is_singleton(cx),
831 )
832 });
833
834 if has_conflict && can_save {
835 let mut answer = pane.update(cx, |pane, cx| {
836 pane.activate_item(item_ix, true, true, cx);
837 cx.prompt(
838 PromptLevel::Warning,
839 CONFLICT_MESSAGE,
840 &["Overwrite", "Discard", "Cancel"],
841 )
842 });
843 match answer.next().await {
844 Some(0) => cx.update(|cx| item.save(project, cx)).await?,
845 Some(1) => cx.update(|cx| item.reload(project, cx)).await?,
846 _ => return Ok(false),
847 }
848 } else if is_dirty && (can_save || is_singleton) {
849 let will_autosave = cx.read(|cx| {
850 matches!(
851 cx.global::<Settings>().autosave,
852 Autosave::OnFocusChange | Autosave::OnWindowChange
853 ) && Self::can_autosave_item(&*item, cx)
854 });
855 let should_save = if should_prompt_for_save && !will_autosave {
856 let mut answer = pane.update(cx, |pane, cx| {
857 pane.activate_item(item_ix, true, true, cx);
858 cx.prompt(
859 PromptLevel::Warning,
860 DIRTY_MESSAGE,
861 &["Save", "Don't Save", "Cancel"],
862 )
863 });
864 match answer.next().await {
865 Some(0) => true,
866 Some(1) => false,
867 _ => return Ok(false),
868 }
869 } else {
870 true
871 };
872
873 if should_save {
874 if can_save {
875 cx.update(|cx| item.save(project, cx)).await?;
876 } else if is_singleton {
877 let start_abs_path = project
878 .read_with(cx, |project, cx| {
879 let worktree = project.visible_worktrees(cx).next()?;
880 Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
881 })
882 .unwrap_or_else(|| Path::new("").into());
883
884 let mut abs_path = cx.update(|cx| cx.prompt_for_new_path(&start_abs_path));
885 if let Some(abs_path) = abs_path.next().await.flatten() {
886 cx.update(|cx| item.save_as(project, abs_path, cx)).await?;
887 } else {
888 return Ok(false);
889 }
890 }
891 }
892 }
893 Ok(true)
894 }
895
896 fn can_autosave_item(item: &dyn ItemHandle, cx: &AppContext) -> bool {
897 let is_deleted = item.project_entry_ids(cx).is_empty();
898 item.is_dirty(cx) && !item.has_conflict(cx) && item.can_save(cx) && !is_deleted
899 }
900
901 pub fn autosave_item(
902 item: &dyn ItemHandle,
903 project: ModelHandle<Project>,
904 cx: &mut MutableAppContext,
905 ) -> Task<Result<()>> {
906 if Self::can_autosave_item(item, cx) {
907 item.save(project, cx)
908 } else {
909 Task::ready(Ok(()))
910 }
911 }
912
913 pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
914 if let Some(active_item) = self.active_item() {
915 cx.focus(active_item);
916 }
917 }
918
919 fn move_item(
920 workspace: &mut Workspace,
921 from: ViewHandle<Pane>,
922 to: ViewHandle<Pane>,
923 item_to_move: usize,
924 destination_index: usize,
925 cx: &mut ViewContext<Workspace>,
926 ) {
927 let item_to_move = from
928 .read(cx)
929 .items()
930 .enumerate()
931 .find(|(_, item_handle)| item_handle.id() == item_to_move);
932
933 if item_to_move.is_none() {
934 log::warn!("Tried to move item handle which was not in `from` pane. Maybe tab was closed during drop");
935 return;
936 }
937
938 let (item_ix, item_handle) = item_to_move.unwrap();
939 // This automatically removes duplicate items in the pane
940 Pane::add_item(
941 workspace,
942 &to,
943 item_handle.clone(),
944 true,
945 true,
946 Some(destination_index),
947 cx,
948 );
949
950 if from != to {
951 // Close item from previous pane
952 from.update(cx, |from, cx| {
953 from.remove_item(item_ix, cx);
954 });
955 }
956
957 cx.focus(to);
958 }
959
960 pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
961 cx.emit(Event::Split(direction));
962 }
963
964 fn deploy_split_menu(&mut self, action: &DeploySplitMenu, cx: &mut ViewContext<Self>) {
965 self.context_menu.update(cx, |menu, cx| {
966 menu.show(
967 action.position,
968 vec![
969 ContextMenuItem::item("Split Right", SplitRight),
970 ContextMenuItem::item("Split Left", SplitLeft),
971 ContextMenuItem::item("Split Up", SplitUp),
972 ContextMenuItem::item("Split Down", SplitDown),
973 ],
974 cx,
975 );
976 });
977 }
978
979 fn deploy_new_menu(&mut self, action: &DeployNewMenu, cx: &mut ViewContext<Self>) {
980 self.context_menu.update(cx, |menu, cx| {
981 menu.show(
982 action.position,
983 vec![
984 ContextMenuItem::item("New File", NewFile),
985 ContextMenuItem::item("New Terminal", NewTerminal),
986 ContextMenuItem::item("New Search", NewSearch),
987 ],
988 cx,
989 );
990 });
991 }
992
993 pub fn toolbar(&self) -> &ViewHandle<Toolbar> {
994 &self.toolbar
995 }
996
997 fn update_toolbar(&mut self, cx: &mut ViewContext<Self>) {
998 let active_item = self
999 .items
1000 .get(self.active_item_index)
1001 .map(|item| item.as_ref());
1002 self.toolbar.update(cx, |toolbar, cx| {
1003 toolbar.set_active_pane_item(active_item, cx);
1004 });
1005 }
1006
1007 fn render_tab_bar(&mut self, cx: &mut RenderContext<Self>) -> impl Element {
1008 let theme = cx.global::<Settings>().theme.clone();
1009 let filler_index = self.items.len();
1010
1011 enum Tabs {}
1012 enum Tab {}
1013 enum Filler {}
1014 let pane = cx.handle();
1015 MouseEventHandler::new::<Tabs, _, _>(0, cx, |_, cx| {
1016 let autoscroll = if mem::take(&mut self.autoscroll) {
1017 Some(self.active_item_index)
1018 } else {
1019 None
1020 };
1021
1022 let pane_active = self.is_active;
1023
1024 let mut row = Flex::row().scrollable::<Tabs, _>(1, autoscroll, cx);
1025 for (ix, (item, detail)) in self
1026 .items
1027 .iter()
1028 .cloned()
1029 .zip(self.tab_details(cx))
1030 .enumerate()
1031 {
1032 let detail = if detail == 0 { None } else { Some(detail) };
1033 let tab_active = ix == self.active_item_index;
1034
1035 row.add_child({
1036 MouseEventHandler::new::<Tab, _, _>(ix, cx, {
1037 let item = item.clone();
1038 let pane = pane.clone();
1039 let detail = detail.clone();
1040
1041 let theme = cx.global::<Settings>().theme.clone();
1042
1043 move |mouse_state, cx| {
1044 let tab_style =
1045 theme.workspace.tab_bar.tab_style(pane_active, tab_active);
1046 let hovered = mouse_state.hovered;
1047 Self::render_tab(
1048 &item,
1049 pane,
1050 detail,
1051 hovered,
1052 Self::tab_overlay_color(hovered, theme.as_ref(), cx),
1053 tab_style,
1054 cx,
1055 )
1056 }
1057 })
1058 .with_cursor_style(if pane_active && tab_active {
1059 CursorStyle::Arrow
1060 } else {
1061 CursorStyle::PointingHand
1062 })
1063 .on_down(MouseButton::Left, move |_, cx| {
1064 cx.dispatch_action(ActivateItem(ix));
1065 })
1066 .on_click(MouseButton::Middle, {
1067 let item = item.clone();
1068 let pane = pane.clone();
1069 move |_, cx: &mut EventContext| {
1070 cx.dispatch_action(CloseItem {
1071 item_id: item.id(),
1072 pane: pane.clone(),
1073 })
1074 }
1075 })
1076 .on_up(MouseButton::Left, {
1077 let pane = pane.clone();
1078 move |_, cx: &mut EventContext| Pane::handle_dropped_item(&pane, ix, cx)
1079 })
1080 .as_draggable(
1081 DraggedItem {
1082 item,
1083 pane: pane.clone(),
1084 },
1085 {
1086 let theme = cx.global::<Settings>().theme.clone();
1087
1088 let detail = detail.clone();
1089 move |dragged_item, cx: &mut RenderContext<Workspace>| {
1090 let tab_style = &theme.workspace.tab_bar.dragged_tab;
1091 Self::render_tab(
1092 &dragged_item.item,
1093 dragged_item.pane.clone(),
1094 detail,
1095 false,
1096 None,
1097 &tab_style,
1098 cx,
1099 )
1100 }
1101 },
1102 )
1103 .boxed()
1104 })
1105 }
1106
1107 // Use the inactive tab style along with the current pane's active status to decide how to render
1108 // the filler
1109 let filler_style = theme.workspace.tab_bar.tab_style(pane_active, false);
1110 row.add_child(
1111 MouseEventHandler::new::<Filler, _, _>(0, cx, |mouse_state, cx| {
1112 let mut filler = Empty::new()
1113 .contained()
1114 .with_style(filler_style.container)
1115 .with_border(filler_style.container.border);
1116
1117 if let Some(overlay) = Self::tab_overlay_color(mouse_state.hovered, &theme, cx)
1118 {
1119 filler = filler.with_overlay_color(overlay);
1120 }
1121
1122 filler.boxed()
1123 })
1124 .flex(1., true)
1125 .named("filler"),
1126 );
1127
1128 row.boxed()
1129 })
1130 .on_up(MouseButton::Left, move |_, cx| {
1131 Pane::handle_dropped_item(&pane, filler_index, cx)
1132 })
1133 }
1134
1135 fn tab_details(&self, cx: &AppContext) -> Vec<usize> {
1136 let mut tab_details = (0..self.items.len()).map(|_| 0).collect::<Vec<_>>();
1137
1138 let mut tab_descriptions = HashMap::default();
1139 let mut done = false;
1140 while !done {
1141 done = true;
1142
1143 // Store item indices by their tab description.
1144 for (ix, (item, detail)) in self.items.iter().zip(&tab_details).enumerate() {
1145 if let Some(description) = item.tab_description(*detail, cx) {
1146 if *detail == 0
1147 || Some(&description) != item.tab_description(detail - 1, cx).as_ref()
1148 {
1149 tab_descriptions
1150 .entry(description)
1151 .or_insert(Vec::new())
1152 .push(ix);
1153 }
1154 }
1155 }
1156
1157 // If two or more items have the same tab description, increase their level
1158 // of detail and try again.
1159 for (_, item_ixs) in tab_descriptions.drain() {
1160 if item_ixs.len() > 1 {
1161 done = false;
1162 for ix in item_ixs {
1163 tab_details[ix] += 1;
1164 }
1165 }
1166 }
1167 }
1168
1169 tab_details
1170 }
1171
1172 fn render_tab<V: View>(
1173 item: &Box<dyn ItemHandle>,
1174 pane: WeakViewHandle<Pane>,
1175 detail: Option<usize>,
1176 hovered: bool,
1177 overlay: Option<Color>,
1178 tab_style: &theme::Tab,
1179 cx: &mut RenderContext<V>,
1180 ) -> ElementBox {
1181 let title = item.tab_content(detail, &tab_style, cx);
1182
1183 let mut tab = Flex::row()
1184 .with_child(
1185 Align::new({
1186 let diameter = 7.0;
1187 let icon_color = if item.has_conflict(cx) {
1188 Some(tab_style.icon_conflict)
1189 } else if item.is_dirty(cx) {
1190 Some(tab_style.icon_dirty)
1191 } else {
1192 None
1193 };
1194
1195 ConstrainedBox::new(
1196 Canvas::new(move |bounds, _, cx| {
1197 if let Some(color) = icon_color {
1198 let square = RectF::new(bounds.origin(), vec2f(diameter, diameter));
1199 cx.scene.push_quad(Quad {
1200 bounds: square,
1201 background: Some(color),
1202 border: Default::default(),
1203 corner_radius: diameter / 2.,
1204 });
1205 }
1206 })
1207 .boxed(),
1208 )
1209 .with_width(diameter)
1210 .with_height(diameter)
1211 .boxed()
1212 })
1213 .boxed(),
1214 )
1215 .with_child(
1216 Container::new(Align::new(title).boxed())
1217 .with_style(ContainerStyle {
1218 margin: Margin {
1219 left: tab_style.spacing,
1220 right: tab_style.spacing,
1221 ..Default::default()
1222 },
1223 ..Default::default()
1224 })
1225 .boxed(),
1226 )
1227 .with_child(
1228 Align::new(
1229 ConstrainedBox::new(if hovered {
1230 let item_id = item.id();
1231 enum TabCloseButton {}
1232 let icon = Svg::new("icons/x_mark_thin_8.svg");
1233 MouseEventHandler::new::<TabCloseButton, _, _>(
1234 item_id,
1235 cx,
1236 |mouse_state, _| {
1237 if mouse_state.hovered {
1238 icon.with_color(tab_style.icon_close_active).boxed()
1239 } else {
1240 icon.with_color(tab_style.icon_close).boxed()
1241 }
1242 },
1243 )
1244 .with_padding(Padding::uniform(4.))
1245 .with_cursor_style(CursorStyle::PointingHand)
1246 .on_click(MouseButton::Left, {
1247 let pane = pane.clone();
1248 move |_, cx| {
1249 cx.dispatch_action(CloseItem {
1250 item_id,
1251 pane: pane.clone(),
1252 })
1253 }
1254 })
1255 .on_click(MouseButton::Middle, |_, cx| cx.propogate_event())
1256 .named("close-tab-icon")
1257 } else {
1258 Empty::new().boxed()
1259 })
1260 .with_width(tab_style.icon_width)
1261 .boxed(),
1262 )
1263 .boxed(),
1264 )
1265 .contained()
1266 .with_style(tab_style.container);
1267
1268 if let Some(overlay) = overlay {
1269 tab = tab.with_overlay_color(overlay);
1270 }
1271
1272 tab.constrained().with_height(tab_style.height).boxed()
1273 }
1274
1275 fn handle_dropped_item(pane: &WeakViewHandle<Pane>, index: usize, cx: &mut EventContext) {
1276 if let Some((_, dragged_item)) = cx
1277 .global::<DragAndDrop<Workspace>>()
1278 .currently_dragged::<DraggedItem>(cx.window_id)
1279 {
1280 cx.dispatch_action(MoveItem {
1281 item_id: dragged_item.item.id(),
1282 from: dragged_item.pane.clone(),
1283 to: pane.clone(),
1284 destination_index: index,
1285 })
1286 } else {
1287 cx.propogate_event();
1288 }
1289 }
1290
1291 fn tab_overlay_color(
1292 hovered: bool,
1293 theme: &Theme,
1294 cx: &mut RenderContext<Self>,
1295 ) -> Option<Color> {
1296 if hovered
1297 && cx
1298 .global::<DragAndDrop<Workspace>>()
1299 .currently_dragged::<DraggedItem>(cx.window_id())
1300 .is_some()
1301 {
1302 Some(theme.workspace.tab_bar.drop_target_overlay_color)
1303 } else {
1304 None
1305 }
1306 }
1307}
1308
1309impl Entity for Pane {
1310 type Event = Event;
1311}
1312
1313impl View for Pane {
1314 fn ui_name() -> &'static str {
1315 "Pane"
1316 }
1317
1318 fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
1319 enum SplitIcon {}
1320
1321 let this = cx.handle();
1322
1323 Stack::new()
1324 .with_child(
1325 EventHandler::new(if let Some(active_item) = self.active_item() {
1326 Flex::column()
1327 .with_child({
1328 let mut tab_row = Flex::row()
1329 .with_child(self.render_tab_bar(cx).flex(1., true).named("tabs"));
1330
1331 if self.is_active {
1332 tab_row.add_children([
1333 MouseEventHandler::new::<SplitIcon, _, _>(
1334 0,
1335 cx,
1336 |mouse_state, cx| {
1337 let theme =
1338 &cx.global::<Settings>().theme.workspace.tab_bar;
1339 let style =
1340 theme.pane_button.style_for(mouse_state, false);
1341 Svg::new("icons/plus_12.svg")
1342 .with_color(style.color)
1343 .constrained()
1344 .with_width(style.icon_width)
1345 .aligned()
1346 .contained()
1347 .with_style(style.container)
1348 .constrained()
1349 .with_width(style.button_width)
1350 .with_height(style.button_width)
1351 .aligned()
1352 .boxed()
1353 },
1354 )
1355 .with_cursor_style(CursorStyle::PointingHand)
1356 .on_down(MouseButton::Left, |e, cx| {
1357 cx.dispatch_action(DeployNewMenu {
1358 position: e.position,
1359 });
1360 })
1361 .boxed(),
1362 MouseEventHandler::new::<SplitIcon, _, _>(
1363 1,
1364 cx,
1365 |mouse_state, cx| {
1366 let theme =
1367 &cx.global::<Settings>().theme.workspace.tab_bar;
1368 let style =
1369 theme.pane_button.style_for(mouse_state, false);
1370 Svg::new("icons/split_12.svg")
1371 .with_color(style.color)
1372 .constrained()
1373 .with_width(style.icon_width)
1374 .aligned()
1375 .contained()
1376 .with_style(style.container)
1377 .constrained()
1378 .with_width(style.button_width)
1379 .with_height(style.button_width)
1380 .aligned()
1381 .boxed()
1382 },
1383 )
1384 .with_cursor_style(CursorStyle::PointingHand)
1385 .on_down(MouseButton::Left, |e, cx| {
1386 cx.dispatch_action(DeploySplitMenu {
1387 position: e.position,
1388 });
1389 })
1390 .boxed(),
1391 ])
1392 }
1393
1394 tab_row
1395 .constrained()
1396 .with_height(cx.global::<Settings>().theme.workspace.tab_bar.height)
1397 .named("tab bar")
1398 })
1399 .with_child(ChildView::new(&self.toolbar).boxed())
1400 .with_child(ChildView::new(active_item).flex(1., true).boxed())
1401 .boxed()
1402 } else {
1403 enum EmptyPane {}
1404 let theme = cx.global::<Settings>().theme.clone();
1405
1406 MouseEventHandler::new::<EmptyPane, _, _>(0, cx, |_, _| {
1407 Empty::new()
1408 .contained()
1409 .with_background_color(theme.workspace.background)
1410 .boxed()
1411 })
1412 .on_down(MouseButton::Left, |_, cx| {
1413 cx.focus_parent_view();
1414 })
1415 .boxed()
1416 })
1417 .on_navigate_mouse_down(move |direction, cx| {
1418 let this = this.clone();
1419 match direction {
1420 NavigationDirection::Back => {
1421 cx.dispatch_action(GoBack { pane: Some(this) })
1422 }
1423 NavigationDirection::Forward => {
1424 cx.dispatch_action(GoForward { pane: Some(this) })
1425 }
1426 }
1427
1428 true
1429 })
1430 .boxed(),
1431 )
1432 .with_child(ChildView::new(&self.context_menu).boxed())
1433 .named("pane")
1434 }
1435
1436 fn on_focus_in(&mut self, focused: AnyViewHandle, cx: &mut ViewContext<Self>) {
1437 if cx.is_self_focused() {
1438 if let Some(last_focused_view) = self
1439 .last_focused_view
1440 .as_ref()
1441 .and_then(|handle| handle.upgrade(cx))
1442 {
1443 cx.focus(last_focused_view);
1444 } else {
1445 self.focus_active_item(cx);
1446 }
1447 } else {
1448 self.last_focused_view = Some(focused.downgrade());
1449 }
1450 cx.emit(Event::Focused);
1451 }
1452}
1453
1454impl ItemNavHistory {
1455 pub fn push<D: 'static + Any>(&self, data: Option<D>, cx: &mut MutableAppContext) {
1456 self.history.borrow_mut().push(data, self.item.clone(), cx);
1457 }
1458
1459 pub fn pop_backward(&self, cx: &mut MutableAppContext) -> Option<NavigationEntry> {
1460 self.history.borrow_mut().pop(NavigationMode::GoingBack, cx)
1461 }
1462
1463 pub fn pop_forward(&self, cx: &mut MutableAppContext) -> Option<NavigationEntry> {
1464 self.history
1465 .borrow_mut()
1466 .pop(NavigationMode::GoingForward, cx)
1467 }
1468}
1469
1470impl NavHistory {
1471 fn set_mode(&mut self, mode: NavigationMode) {
1472 self.mode = mode;
1473 }
1474
1475 fn disable(&mut self) {
1476 self.mode = NavigationMode::Disabled;
1477 }
1478
1479 fn enable(&mut self) {
1480 self.mode = NavigationMode::Normal;
1481 }
1482
1483 fn pop(&mut self, mode: NavigationMode, cx: &mut MutableAppContext) -> Option<NavigationEntry> {
1484 let entry = match mode {
1485 NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
1486 return None
1487 }
1488 NavigationMode::GoingBack => &mut self.backward_stack,
1489 NavigationMode::GoingForward => &mut self.forward_stack,
1490 NavigationMode::ReopeningClosedItem => &mut self.closed_stack,
1491 }
1492 .pop_back();
1493 if entry.is_some() {
1494 self.did_update(cx);
1495 }
1496 entry
1497 }
1498
1499 fn push<D: 'static + Any>(
1500 &mut self,
1501 data: Option<D>,
1502 item: Rc<dyn WeakItemHandle>,
1503 cx: &mut MutableAppContext,
1504 ) {
1505 match self.mode {
1506 NavigationMode::Disabled => {}
1507 NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
1508 if self.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
1509 self.backward_stack.pop_front();
1510 }
1511 self.backward_stack.push_back(NavigationEntry {
1512 item,
1513 data: data.map(|data| Box::new(data) as Box<dyn Any>),
1514 });
1515 self.forward_stack.clear();
1516 }
1517 NavigationMode::GoingBack => {
1518 if self.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
1519 self.forward_stack.pop_front();
1520 }
1521 self.forward_stack.push_back(NavigationEntry {
1522 item,
1523 data: data.map(|data| Box::new(data) as Box<dyn Any>),
1524 });
1525 }
1526 NavigationMode::GoingForward => {
1527 if self.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
1528 self.backward_stack.pop_front();
1529 }
1530 self.backward_stack.push_back(NavigationEntry {
1531 item,
1532 data: data.map(|data| Box::new(data) as Box<dyn Any>),
1533 });
1534 }
1535 NavigationMode::ClosingItem => {
1536 if self.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
1537 self.closed_stack.pop_front();
1538 }
1539 self.closed_stack.push_back(NavigationEntry {
1540 item,
1541 data: data.map(|data| Box::new(data) as Box<dyn Any>),
1542 });
1543 }
1544 }
1545 self.did_update(cx);
1546 }
1547
1548 fn did_update(&self, cx: &mut MutableAppContext) {
1549 if let Some(pane) = self.pane.upgrade(cx) {
1550 cx.defer(move |cx| pane.update(cx, |pane, cx| pane.history_updated(cx)));
1551 }
1552 }
1553}
1554
1555#[cfg(test)]
1556mod tests {
1557 use super::*;
1558 use crate::tests::TestItem;
1559 use gpui::TestAppContext;
1560 use project::FakeFs;
1561
1562 #[gpui::test]
1563 async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
1564 cx.foreground().forbid_parking();
1565 Settings::test_async(cx);
1566 let fs = FakeFs::new(cx.background());
1567
1568 let project = Project::test(fs, None, cx).await;
1569 let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
1570 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
1571
1572 // 1. Add with a destination index
1573 // a. Add before the active item
1574 set_labeled_items(&workspace, &pane, ["A", "B*", "C"], cx);
1575 workspace.update(cx, |workspace, cx| {
1576 Pane::add_item(
1577 workspace,
1578 &pane,
1579 Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
1580 false,
1581 false,
1582 Some(0),
1583 cx,
1584 );
1585 });
1586 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
1587
1588 // b. Add after the active item
1589 set_labeled_items(&workspace, &pane, ["A", "B*", "C"], cx);
1590 workspace.update(cx, |workspace, cx| {
1591 Pane::add_item(
1592 workspace,
1593 &pane,
1594 Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
1595 false,
1596 false,
1597 Some(2),
1598 cx,
1599 );
1600 });
1601 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
1602
1603 // c. Add at the end of the item list (including off the length)
1604 set_labeled_items(&workspace, &pane, ["A", "B*", "C"], cx);
1605 workspace.update(cx, |workspace, cx| {
1606 Pane::add_item(
1607 workspace,
1608 &pane,
1609 Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
1610 false,
1611 false,
1612 Some(5),
1613 cx,
1614 );
1615 });
1616 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
1617
1618 // 2. Add without a destination index
1619 // a. Add with active item at the start of the item list
1620 set_labeled_items(&workspace, &pane, ["A*", "B", "C"], cx);
1621 workspace.update(cx, |workspace, cx| {
1622 Pane::add_item(
1623 workspace,
1624 &pane,
1625 Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
1626 false,
1627 false,
1628 None,
1629 cx,
1630 );
1631 });
1632 set_labeled_items(&workspace, &pane, ["A", "D*", "B", "C"], cx);
1633
1634 // b. Add with active item at the end of the item list
1635 set_labeled_items(&workspace, &pane, ["A", "B", "C*"], cx);
1636 workspace.update(cx, |workspace, cx| {
1637 Pane::add_item(
1638 workspace,
1639 &pane,
1640 Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
1641 false,
1642 false,
1643 None,
1644 cx,
1645 );
1646 });
1647 assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
1648 }
1649
1650 #[gpui::test]
1651 async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
1652 cx.foreground().forbid_parking();
1653 Settings::test_async(cx);
1654 let fs = FakeFs::new(cx.background());
1655
1656 let project = Project::test(fs, None, cx).await;
1657 let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
1658 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
1659
1660 // 1. Add with a destination index
1661 // 1a. Add before the active item
1662 let [_, _, _, d] = set_labeled_items(&workspace, &pane, ["A", "B*", "C", "D"], cx);
1663 workspace.update(cx, |workspace, cx| {
1664 Pane::add_item(workspace, &pane, d, false, false, Some(0), cx);
1665 });
1666 assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
1667
1668 // 1b. Add after the active item
1669 let [_, _, _, d] = set_labeled_items(&workspace, &pane, ["A", "B*", "C", "D"], cx);
1670 workspace.update(cx, |workspace, cx| {
1671 Pane::add_item(workspace, &pane, d, false, false, Some(2), cx);
1672 });
1673 assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
1674
1675 // 1c. Add at the end of the item list (including off the length)
1676 let [a, _, _, _] = set_labeled_items(&workspace, &pane, ["A", "B*", "C", "D"], cx);
1677 workspace.update(cx, |workspace, cx| {
1678 Pane::add_item(workspace, &pane, a, false, false, Some(5), cx);
1679 });
1680 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
1681
1682 // 1d. Add same item to active index
1683 let [_, b, _] = set_labeled_items(&workspace, &pane, ["A", "B*", "C"], cx);
1684 workspace.update(cx, |workspace, cx| {
1685 Pane::add_item(workspace, &pane, b, false, false, Some(1), cx);
1686 });
1687 assert_item_labels(&pane, ["A", "B*", "C"], cx);
1688
1689 // 1e. Add item to index after same item in last position
1690 let [_, _, c] = set_labeled_items(&workspace, &pane, ["A", "B*", "C"], cx);
1691 workspace.update(cx, |workspace, cx| {
1692 Pane::add_item(workspace, &pane, c, false, false, Some(2), cx);
1693 });
1694 assert_item_labels(&pane, ["A", "B", "C*"], cx);
1695
1696 // 2. Add without a destination index
1697 // 2a. Add with active item at the start of the item list
1698 let [_, _, _, d] = set_labeled_items(&workspace, &pane, ["A*", "B", "C", "D"], cx);
1699 workspace.update(cx, |workspace, cx| {
1700 Pane::add_item(workspace, &pane, d, false, false, None, cx);
1701 });
1702 assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
1703
1704 // 2b. Add with active item at the end of the item list
1705 let [a, _, _, _] = set_labeled_items(&workspace, &pane, ["A", "B", "C", "D*"], cx);
1706 workspace.update(cx, |workspace, cx| {
1707 Pane::add_item(workspace, &pane, a, false, false, None, cx);
1708 });
1709 assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
1710
1711 // 2c. Add active item to active item at end of list
1712 let [_, _, c] = set_labeled_items(&workspace, &pane, ["A", "B", "C*"], cx);
1713 workspace.update(cx, |workspace, cx| {
1714 Pane::add_item(workspace, &pane, c, false, false, None, cx);
1715 });
1716 assert_item_labels(&pane, ["A", "B", "C*"], cx);
1717
1718 // 2d. Add active item to active item at start of list
1719 let [a, _, _] = set_labeled_items(&workspace, &pane, ["A*", "B", "C"], cx);
1720 workspace.update(cx, |workspace, cx| {
1721 Pane::add_item(workspace, &pane, a, false, false, None, cx);
1722 });
1723 assert_item_labels(&pane, ["A*", "B", "C"], cx);
1724 }
1725
1726 #[gpui::test]
1727 async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
1728 cx.foreground().forbid_parking();
1729 Settings::test_async(cx);
1730 let fs = FakeFs::new(cx.background());
1731
1732 let project = Project::test(fs, None, cx).await;
1733 let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
1734 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
1735
1736 // singleton view
1737 workspace.update(cx, |workspace, cx| {
1738 let item = TestItem::new()
1739 .with_singleton(true)
1740 .with_label("buffer 1")
1741 .with_project_entry_ids(&[1]);
1742
1743 Pane::add_item(
1744 workspace,
1745 &pane,
1746 Box::new(cx.add_view(|_| item)),
1747 false,
1748 false,
1749 None,
1750 cx,
1751 );
1752 });
1753 assert_item_labels(&pane, ["buffer 1*"], cx);
1754
1755 // new singleton view with the same project entry
1756 workspace.update(cx, |workspace, cx| {
1757 let item = TestItem::new()
1758 .with_singleton(true)
1759 .with_label("buffer 1")
1760 .with_project_entry_ids(&[1]);
1761
1762 Pane::add_item(
1763 workspace,
1764 &pane,
1765 Box::new(cx.add_view(|_| item)),
1766 false,
1767 false,
1768 None,
1769 cx,
1770 );
1771 });
1772 assert_item_labels(&pane, ["buffer 1*"], cx);
1773
1774 // new singleton view with different project entry
1775 workspace.update(cx, |workspace, cx| {
1776 let item = TestItem::new()
1777 .with_singleton(true)
1778 .with_label("buffer 2")
1779 .with_project_entry_ids(&[2]);
1780
1781 Pane::add_item(
1782 workspace,
1783 &pane,
1784 Box::new(cx.add_view(|_| item)),
1785 false,
1786 false,
1787 None,
1788 cx,
1789 );
1790 });
1791 assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
1792
1793 // new multibuffer view with the same project entry
1794 workspace.update(cx, |workspace, cx| {
1795 let item = TestItem::new()
1796 .with_singleton(false)
1797 .with_label("multibuffer 1")
1798 .with_project_entry_ids(&[1]);
1799
1800 Pane::add_item(
1801 workspace,
1802 &pane,
1803 Box::new(cx.add_view(|_| item)),
1804 false,
1805 false,
1806 None,
1807 cx,
1808 );
1809 });
1810 assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
1811
1812 // another multibuffer view with the same project entry
1813 workspace.update(cx, |workspace, cx| {
1814 let item = TestItem::new()
1815 .with_singleton(false)
1816 .with_label("multibuffer 1b")
1817 .with_project_entry_ids(&[1]);
1818
1819 Pane::add_item(
1820 workspace,
1821 &pane,
1822 Box::new(cx.add_view(|_| item)),
1823 false,
1824 false,
1825 None,
1826 cx,
1827 );
1828 });
1829 assert_item_labels(
1830 &pane,
1831 ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
1832 cx,
1833 );
1834 }
1835
1836 fn set_labeled_items<const COUNT: usize>(
1837 workspace: &ViewHandle<Workspace>,
1838 pane: &ViewHandle<Pane>,
1839 labels: [&str; COUNT],
1840 cx: &mut TestAppContext,
1841 ) -> [Box<ViewHandle<TestItem>>; COUNT] {
1842 pane.update(cx, |pane, _| {
1843 pane.items.clear();
1844 });
1845
1846 workspace.update(cx, |workspace, cx| {
1847 let mut active_item_index = 0;
1848
1849 let mut index = 0;
1850 let items = labels.map(|mut label| {
1851 if label.ends_with("*") {
1852 label = label.trim_end_matches("*");
1853 active_item_index = index;
1854 }
1855
1856 let labeled_item = Box::new(cx.add_view(|_| TestItem::new().with_label(label)));
1857 Pane::add_item(
1858 workspace,
1859 pane,
1860 labeled_item.clone(),
1861 false,
1862 false,
1863 None,
1864 cx,
1865 );
1866 index += 1;
1867 labeled_item
1868 });
1869
1870 pane.update(cx, |pane, cx| {
1871 pane.activate_item(active_item_index, false, false, cx)
1872 });
1873
1874 items
1875 })
1876 }
1877
1878 // Assert the item label, with the active item label suffixed with a '*'
1879 fn assert_item_labels<const COUNT: usize>(
1880 pane: &ViewHandle<Pane>,
1881 expected_states: [&str; COUNT],
1882 cx: &mut TestAppContext,
1883 ) {
1884 pane.read_with(cx, |pane, cx| {
1885 let actual_states = pane
1886 .items
1887 .iter()
1888 .enumerate()
1889 .map(|(ix, item)| {
1890 let mut state = item
1891 .to_any()
1892 .downcast::<TestItem>()
1893 .unwrap()
1894 .read(cx)
1895 .label
1896 .clone();
1897 if ix == pane.active_item_index {
1898 state.push('*');
1899 }
1900 state
1901 })
1902 .collect::<Vec<_>>();
1903
1904 assert_eq!(
1905 actual_states, expected_states,
1906 "pane items do not match expectation"
1907 );
1908 })
1909 }
1910}