1/// NOTE: Focus only 'takes' after an update has flushed_effects. Pane sends an event in on_focus_in
2/// which the workspace uses to change the activated pane.
3///
4/// This may cause issues when you're trying to write tests that use workspace focus to add items at
5/// specific locations.
6pub mod dock;
7pub mod pane;
8pub mod pane_group;
9pub mod searchable;
10pub mod sidebar;
11mod status_bar;
12mod toolbar;
13
14use anyhow::{anyhow, Context, Result};
15use client::{proto, Client, Contact, PeerId, Subscription, TypedEnvelope, UserStore};
16use collections::{hash_map, HashMap, HashSet};
17use dock::{DefaultItemFactory, Dock, ToggleDockButton};
18use drag_and_drop::DragAndDrop;
19use futures::{channel::oneshot, FutureExt};
20use gpui::{
21 actions,
22 elements::*,
23 impl_actions, impl_internal_actions,
24 platform::{CursorStyle, WindowOptions},
25 AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
26 MouseButton, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View,
27 ViewContext, ViewHandle, WeakViewHandle,
28};
29use language::LanguageRegistry;
30use log::{error, warn};
31pub use pane::*;
32pub use pane_group::*;
33use postage::prelude::Stream;
34use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, ProjectStore, Worktree, WorktreeId};
35use searchable::SearchableItemHandle;
36use serde::Deserialize;
37use settings::{Autosave, DockAnchor, Settings};
38use sidebar::{Sidebar, SidebarButtons, SidebarSide, ToggleSidebarItem};
39use smallvec::SmallVec;
40use status_bar::StatusBar;
41pub use status_bar::StatusItemView;
42use std::{
43 any::{Any, TypeId},
44 borrow::Cow,
45 cell::RefCell,
46 fmt,
47 future::Future,
48 mem,
49 path::{Path, PathBuf},
50 rc::Rc,
51 sync::{
52 atomic::{AtomicBool, Ordering::SeqCst},
53 Arc,
54 },
55 time::Duration,
56};
57use theme::{Theme, ThemeRegistry};
58pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
59use util::ResultExt;
60
61type ProjectItemBuilders = HashMap<
62 TypeId,
63 fn(ModelHandle<Project>, AnyModelHandle, &mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
64>;
65
66type FollowableItemBuilder = fn(
67 ViewHandle<Pane>,
68 ModelHandle<Project>,
69 &mut Option<proto::view::Variant>,
70 &mut MutableAppContext,
71) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>;
72type FollowableItemBuilders = HashMap<
73 TypeId,
74 (
75 FollowableItemBuilder,
76 fn(AnyViewHandle) -> Box<dyn FollowableItemHandle>,
77 ),
78>;
79
80#[derive(Clone, PartialEq)]
81pub struct RemoveWorktreeFromProject(pub WorktreeId);
82
83actions!(
84 workspace,
85 [
86 Open,
87 NewFile,
88 NewWindow,
89 CloseWindow,
90 AddFolderToProject,
91 Unfollow,
92 Save,
93 SaveAs,
94 SaveAll,
95 ActivatePreviousPane,
96 ActivateNextPane,
97 FollowNextCollaborator,
98 ToggleLeftSidebar,
99 ToggleRightSidebar,
100 NewTerminal,
101 NewSearch
102 ]
103);
104
105#[derive(Clone, PartialEq)]
106pub struct OpenPaths {
107 pub paths: Vec<PathBuf>,
108}
109
110#[derive(Clone, Deserialize, PartialEq)]
111pub struct ActivatePane(pub usize);
112
113#[derive(Clone, PartialEq)]
114pub struct ToggleFollow(pub PeerId);
115
116#[derive(Clone, PartialEq)]
117pub struct JoinProject {
118 pub contact: Arc<Contact>,
119 pub project_index: usize,
120}
121
122impl_internal_actions!(
123 workspace,
124 [
125 OpenPaths,
126 ToggleFollow,
127 JoinProject,
128 RemoveWorktreeFromProject
129 ]
130);
131impl_actions!(workspace, [ActivatePane]);
132
133pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
134 pane::init(cx);
135 dock::init(cx);
136
137 cx.add_global_action(open);
138 cx.add_global_action({
139 let app_state = Arc::downgrade(&app_state);
140 move |action: &OpenPaths, cx: &mut MutableAppContext| {
141 if let Some(app_state) = app_state.upgrade() {
142 open_paths(&action.paths, &app_state, cx).detach();
143 }
144 }
145 });
146 cx.add_global_action({
147 let app_state = Arc::downgrade(&app_state);
148 move |_: &NewFile, cx: &mut MutableAppContext| {
149 if let Some(app_state) = app_state.upgrade() {
150 open_new(&app_state, cx)
151 }
152 }
153 });
154 cx.add_global_action({
155 let app_state = Arc::downgrade(&app_state);
156 move |_: &NewWindow, cx: &mut MutableAppContext| {
157 if let Some(app_state) = app_state.upgrade() {
158 open_new(&app_state, cx)
159 }
160 }
161 });
162
163 cx.add_async_action(Workspace::toggle_follow);
164 cx.add_async_action(Workspace::follow_next_collaborator);
165 cx.add_async_action(Workspace::close);
166 cx.add_async_action(Workspace::save_all);
167 cx.add_action(Workspace::add_folder_to_project);
168 cx.add_action(Workspace::remove_folder_from_project);
169 cx.add_action(
170 |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext<Workspace>| {
171 let pane = workspace.active_pane().clone();
172 workspace.unfollow(&pane, cx);
173 },
174 );
175 cx.add_action(
176 |workspace: &mut Workspace, _: &Save, cx: &mut ViewContext<Workspace>| {
177 workspace.save_active_item(false, cx).detach_and_log_err(cx);
178 },
179 );
180 cx.add_action(
181 |workspace: &mut Workspace, _: &SaveAs, cx: &mut ViewContext<Workspace>| {
182 workspace.save_active_item(true, cx).detach_and_log_err(cx);
183 },
184 );
185 cx.add_action(Workspace::toggle_sidebar_item);
186 cx.add_action(Workspace::focus_center);
187 cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| {
188 workspace.activate_previous_pane(cx)
189 });
190 cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| {
191 workspace.activate_next_pane(cx)
192 });
193 cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftSidebar, cx| {
194 workspace.toggle_sidebar(SidebarSide::Left, cx);
195 });
196 cx.add_action(|workspace: &mut Workspace, _: &ToggleRightSidebar, cx| {
197 workspace.toggle_sidebar(SidebarSide::Right, cx);
198 });
199 cx.add_action(Workspace::activate_pane_at_index);
200
201 let client = &app_state.client;
202 client.add_view_request_handler(Workspace::handle_follow);
203 client.add_view_message_handler(Workspace::handle_unfollow);
204 client.add_view_message_handler(Workspace::handle_update_followers);
205}
206
207pub fn register_project_item<I: ProjectItem>(cx: &mut MutableAppContext) {
208 cx.update_default_global(|builders: &mut ProjectItemBuilders, _| {
209 builders.insert(TypeId::of::<I::Item>(), |project, model, cx| {
210 let item = model.downcast::<I::Item>().unwrap();
211 Box::new(cx.add_view(|cx| I::for_project_item(project, item, cx)))
212 });
213 });
214}
215
216pub fn register_followable_item<I: FollowableItem>(cx: &mut MutableAppContext) {
217 cx.update_default_global(|builders: &mut FollowableItemBuilders, _| {
218 builders.insert(
219 TypeId::of::<I>(),
220 (
221 |pane, project, state, cx| {
222 I::from_state_proto(pane, project, state, cx).map(|task| {
223 cx.foreground()
224 .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
225 })
226 },
227 |this| Box::new(this.downcast::<I>().unwrap()),
228 ),
229 );
230 });
231}
232
233pub struct AppState {
234 pub languages: Arc<LanguageRegistry>,
235 pub themes: Arc<ThemeRegistry>,
236 pub client: Arc<client::Client>,
237 pub user_store: ModelHandle<client::UserStore>,
238 pub project_store: ModelHandle<ProjectStore>,
239 pub fs: Arc<dyn fs::Fs>,
240 pub build_window_options: fn() -> WindowOptions<'static>,
241 pub initialize_workspace: fn(&mut Workspace, &Arc<AppState>, &mut ViewContext<Workspace>),
242 pub default_item_factory: DefaultItemFactory,
243}
244
245#[derive(Eq, PartialEq, Hash)]
246pub enum ItemEvent {
247 CloseItem,
248 UpdateTab,
249 UpdateBreadcrumbs,
250 Edit,
251}
252
253pub trait Item: View {
254 fn deactivated(&mut self, _: &mut ViewContext<Self>) {}
255 fn workspace_deactivated(&mut self, _: &mut ViewContext<Self>) {}
256 fn navigate(&mut self, _: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
257 false
258 }
259 fn tab_description<'a>(&'a self, _: usize, _: &'a AppContext) -> Option<Cow<'a, str>> {
260 None
261 }
262 fn tab_content(&self, detail: Option<usize>, style: &theme::Tab, cx: &AppContext)
263 -> ElementBox;
264 fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
265 fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>;
266 fn is_singleton(&self, cx: &AppContext) -> bool;
267 fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext<Self>);
268 fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
269 where
270 Self: Sized,
271 {
272 None
273 }
274 fn is_dirty(&self, _: &AppContext) -> bool {
275 false
276 }
277 fn has_conflict(&self, _: &AppContext) -> bool {
278 false
279 }
280 fn can_save(&self, cx: &AppContext) -> bool;
281 fn save(
282 &mut self,
283 project: ModelHandle<Project>,
284 cx: &mut ViewContext<Self>,
285 ) -> Task<Result<()>>;
286 fn save_as(
287 &mut self,
288 project: ModelHandle<Project>,
289 abs_path: PathBuf,
290 cx: &mut ViewContext<Self>,
291 ) -> Task<Result<()>>;
292 fn reload(
293 &mut self,
294 project: ModelHandle<Project>,
295 cx: &mut ViewContext<Self>,
296 ) -> Task<Result<()>>;
297 fn to_item_events(event: &Self::Event) -> Vec<ItemEvent>;
298 fn act_as_type(
299 &self,
300 type_id: TypeId,
301 self_handle: &ViewHandle<Self>,
302 _: &AppContext,
303 ) -> Option<AnyViewHandle> {
304 if TypeId::of::<Self>() == type_id {
305 Some(self_handle.into())
306 } else {
307 None
308 }
309 }
310 fn as_searchable(&self, _: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
311 None
312 }
313
314 fn breadcrumb_location(&self) -> ToolbarItemLocation {
315 ToolbarItemLocation::Hidden
316 }
317 fn breadcrumbs(&self, _theme: &Theme, _cx: &AppContext) -> Option<Vec<ElementBox>> {
318 None
319 }
320}
321
322pub trait ProjectItem: Item {
323 type Item: project::Item;
324
325 fn for_project_item(
326 project: ModelHandle<Project>,
327 item: ModelHandle<Self::Item>,
328 cx: &mut ViewContext<Self>,
329 ) -> Self;
330}
331
332pub trait FollowableItem: Item {
333 fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant>;
334 fn from_state_proto(
335 pane: ViewHandle<Pane>,
336 project: ModelHandle<Project>,
337 state: &mut Option<proto::view::Variant>,
338 cx: &mut MutableAppContext,
339 ) -> Option<Task<Result<ViewHandle<Self>>>>;
340 fn add_event_to_update_proto(
341 &self,
342 event: &Self::Event,
343 update: &mut Option<proto::update_view::Variant>,
344 cx: &AppContext,
345 ) -> bool;
346 fn apply_update_proto(
347 &mut self,
348 message: proto::update_view::Variant,
349 cx: &mut ViewContext<Self>,
350 ) -> Result<()>;
351
352 fn set_leader_replica_id(&mut self, leader_replica_id: Option<u16>, cx: &mut ViewContext<Self>);
353 fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool;
354}
355
356pub trait FollowableItemHandle: ItemHandle {
357 fn set_leader_replica_id(&self, leader_replica_id: Option<u16>, cx: &mut MutableAppContext);
358 fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant>;
359 fn add_event_to_update_proto(
360 &self,
361 event: &dyn Any,
362 update: &mut Option<proto::update_view::Variant>,
363 cx: &AppContext,
364 ) -> bool;
365 fn apply_update_proto(
366 &self,
367 message: proto::update_view::Variant,
368 cx: &mut MutableAppContext,
369 ) -> Result<()>;
370 fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool;
371}
372
373impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
374 fn set_leader_replica_id(&self, leader_replica_id: Option<u16>, cx: &mut MutableAppContext) {
375 self.update(cx, |this, cx| {
376 this.set_leader_replica_id(leader_replica_id, cx)
377 })
378 }
379
380 fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
381 self.read(cx).to_state_proto(cx)
382 }
383
384 fn add_event_to_update_proto(
385 &self,
386 event: &dyn Any,
387 update: &mut Option<proto::update_view::Variant>,
388 cx: &AppContext,
389 ) -> bool {
390 if let Some(event) = event.downcast_ref() {
391 self.read(cx).add_event_to_update_proto(event, update, cx)
392 } else {
393 false
394 }
395 }
396
397 fn apply_update_proto(
398 &self,
399 message: proto::update_view::Variant,
400 cx: &mut MutableAppContext,
401 ) -> Result<()> {
402 self.update(cx, |this, cx| this.apply_update_proto(message, cx))
403 }
404
405 fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool {
406 if let Some(event) = event.downcast_ref() {
407 T::should_unfollow_on_event(event, cx)
408 } else {
409 false
410 }
411 }
412}
413
414pub trait ItemHandle: 'static + fmt::Debug {
415 fn subscribe_to_item_events(
416 &self,
417 cx: &mut MutableAppContext,
418 handler: Box<dyn Fn(ItemEvent, &mut MutableAppContext)>,
419 ) -> gpui::Subscription;
420 fn tab_description<'a>(&self, detail: usize, cx: &'a AppContext) -> Option<Cow<'a, str>>;
421 fn tab_content(&self, detail: Option<usize>, style: &theme::Tab, cx: &AppContext)
422 -> ElementBox;
423 fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
424 fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>;
425 fn is_singleton(&self, cx: &AppContext) -> bool;
426 fn boxed_clone(&self) -> Box<dyn ItemHandle>;
427 fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemHandle>>;
428 fn added_to_pane(
429 &self,
430 workspace: &mut Workspace,
431 pane: ViewHandle<Pane>,
432 cx: &mut ViewContext<Workspace>,
433 );
434 fn deactivated(&self, cx: &mut MutableAppContext);
435 fn workspace_deactivated(&self, cx: &mut MutableAppContext);
436 fn navigate(&self, data: Box<dyn Any>, cx: &mut MutableAppContext) -> bool;
437 fn id(&self) -> usize;
438 fn window_id(&self) -> usize;
439 fn to_any(&self) -> AnyViewHandle;
440 fn is_dirty(&self, cx: &AppContext) -> bool;
441 fn has_conflict(&self, cx: &AppContext) -> bool;
442 fn can_save(&self, cx: &AppContext) -> bool;
443 fn save(&self, project: ModelHandle<Project>, cx: &mut MutableAppContext) -> Task<Result<()>>;
444 fn save_as(
445 &self,
446 project: ModelHandle<Project>,
447 abs_path: PathBuf,
448 cx: &mut MutableAppContext,
449 ) -> Task<Result<()>>;
450 fn reload(&self, project: ModelHandle<Project>, cx: &mut MutableAppContext)
451 -> Task<Result<()>>;
452 fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyViewHandle>;
453 fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>>;
454 fn on_release(
455 &self,
456 cx: &mut MutableAppContext,
457 callback: Box<dyn FnOnce(&mut MutableAppContext)>,
458 ) -> gpui::Subscription;
459 fn to_searchable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>>;
460 fn breadcrumb_location(&self, cx: &AppContext) -> ToolbarItemLocation;
461 fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option<Vec<ElementBox>>;
462}
463
464pub trait WeakItemHandle {
465 fn id(&self) -> usize;
466 fn window_id(&self) -> usize;
467 fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>>;
468}
469
470impl dyn ItemHandle {
471 pub fn downcast<T: View>(&self) -> Option<ViewHandle<T>> {
472 self.to_any().downcast()
473 }
474
475 pub fn act_as<T: View>(&self, cx: &AppContext) -> Option<ViewHandle<T>> {
476 self.act_as_type(TypeId::of::<T>(), cx)
477 .and_then(|t| t.downcast())
478 }
479}
480
481impl<T: Item> ItemHandle for ViewHandle<T> {
482 fn subscribe_to_item_events(
483 &self,
484 cx: &mut MutableAppContext,
485 handler: Box<dyn Fn(ItemEvent, &mut MutableAppContext)>,
486 ) -> gpui::Subscription {
487 cx.subscribe(self, move |_, event, cx| {
488 for item_event in T::to_item_events(event) {
489 handler(item_event, cx)
490 }
491 })
492 }
493
494 fn tab_description<'a>(&self, detail: usize, cx: &'a AppContext) -> Option<Cow<'a, str>> {
495 self.read(cx).tab_description(detail, cx)
496 }
497
498 fn tab_content(
499 &self,
500 detail: Option<usize>,
501 style: &theme::Tab,
502 cx: &AppContext,
503 ) -> ElementBox {
504 self.read(cx).tab_content(detail, style, cx)
505 }
506
507 fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
508 self.read(cx).project_path(cx)
509 }
510
511 fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]> {
512 self.read(cx).project_entry_ids(cx)
513 }
514
515 fn is_singleton(&self, cx: &AppContext) -> bool {
516 self.read(cx).is_singleton(cx)
517 }
518
519 fn boxed_clone(&self) -> Box<dyn ItemHandle> {
520 Box::new(self.clone())
521 }
522
523 fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemHandle>> {
524 self.update(cx, |item, cx| {
525 cx.add_option_view(|cx| item.clone_on_split(cx))
526 })
527 .map(|handle| Box::new(handle) as Box<dyn ItemHandle>)
528 }
529
530 fn added_to_pane(
531 &self,
532 workspace: &mut Workspace,
533 pane: ViewHandle<Pane>,
534 cx: &mut ViewContext<Workspace>,
535 ) {
536 let history = pane.read(cx).nav_history_for_item(self);
537 self.update(cx, |this, cx| this.set_nav_history(history, cx));
538
539 if let Some(followed_item) = self.to_followable_item_handle(cx) {
540 if let Some(message) = followed_item.to_state_proto(cx) {
541 workspace.update_followers(
542 proto::update_followers::Variant::CreateView(proto::View {
543 id: followed_item.id() as u64,
544 variant: Some(message),
545 leader_id: workspace.leader_for_pane(&pane).map(|id| id.0),
546 }),
547 cx,
548 );
549 }
550 }
551
552 if workspace
553 .panes_by_item
554 .insert(self.id(), pane.downgrade())
555 .is_none()
556 {
557 let mut pending_autosave = None;
558 let mut cancel_pending_autosave = oneshot::channel::<()>().0;
559 let pending_update = Rc::new(RefCell::new(None));
560 let pending_update_scheduled = Rc::new(AtomicBool::new(false));
561
562 let mut event_subscription =
563 Some(cx.subscribe(self, move |workspace, item, event, cx| {
564 let pane = if let Some(pane) = workspace
565 .panes_by_item
566 .get(&item.id())
567 .and_then(|pane| pane.upgrade(cx))
568 {
569 pane
570 } else {
571 log::error!("unexpected item event after pane was dropped");
572 return;
573 };
574
575 if let Some(item) = item.to_followable_item_handle(cx) {
576 let leader_id = workspace.leader_for_pane(&pane);
577
578 if leader_id.is_some() && item.should_unfollow_on_event(event, cx) {
579 workspace.unfollow(&pane, cx);
580 }
581
582 if item.add_event_to_update_proto(
583 event,
584 &mut *pending_update.borrow_mut(),
585 cx,
586 ) && !pending_update_scheduled.load(SeqCst)
587 {
588 pending_update_scheduled.store(true, SeqCst);
589 cx.after_window_update({
590 let pending_update = pending_update.clone();
591 let pending_update_scheduled = pending_update_scheduled.clone();
592 move |this, cx| {
593 pending_update_scheduled.store(false, SeqCst);
594 this.update_followers(
595 proto::update_followers::Variant::UpdateView(
596 proto::UpdateView {
597 id: item.id() as u64,
598 variant: pending_update.borrow_mut().take(),
599 leader_id: leader_id.map(|id| id.0),
600 },
601 ),
602 cx,
603 );
604 }
605 });
606 }
607 }
608
609 for item_event in T::to_item_events(event).into_iter() {
610 match item_event {
611 ItemEvent::CloseItem => {
612 Pane::close_item(workspace, pane, item.id(), cx)
613 .detach_and_log_err(cx);
614 return;
615 }
616 ItemEvent::UpdateTab => {
617 pane.update(cx, |_, cx| {
618 cx.emit(pane::Event::ChangeItemTitle);
619 cx.notify();
620 });
621 }
622 ItemEvent::Edit => {
623 if let Autosave::AfterDelay { milliseconds } =
624 cx.global::<Settings>().autosave
625 {
626 let prev_autosave = pending_autosave
627 .take()
628 .unwrap_or_else(|| Task::ready(Some(())));
629 let (cancel_tx, mut cancel_rx) = oneshot::channel::<()>();
630 let prev_cancel_tx =
631 mem::replace(&mut cancel_pending_autosave, cancel_tx);
632 let project = workspace.project.downgrade();
633 let _ = prev_cancel_tx.send(());
634 let item = item.clone();
635 pending_autosave =
636 Some(cx.spawn_weak(|_, mut cx| async move {
637 let mut timer = cx
638 .background()
639 .timer(Duration::from_millis(milliseconds))
640 .fuse();
641 prev_autosave.await;
642 futures::select_biased! {
643 _ = cancel_rx => return None,
644 _ = timer => {}
645 }
646
647 let project = project.upgrade(&cx)?;
648 cx.update(|cx| Pane::autosave_item(&item, project, cx))
649 .await
650 .log_err();
651 None
652 }));
653 }
654 }
655 _ => {}
656 }
657 }
658 }));
659
660 cx.observe_focus(self, move |workspace, item, focused, cx| {
661 if !focused && cx.global::<Settings>().autosave == Autosave::OnFocusChange {
662 Pane::autosave_item(&item, workspace.project.clone(), cx)
663 .detach_and_log_err(cx);
664 }
665 })
666 .detach();
667
668 let item_id = self.id();
669 cx.observe_release(self, move |workspace, _, _| {
670 workspace.panes_by_item.remove(&item_id);
671 event_subscription.take();
672 })
673 .detach();
674 }
675 }
676
677 fn deactivated(&self, cx: &mut MutableAppContext) {
678 self.update(cx, |this, cx| this.deactivated(cx));
679 }
680
681 fn workspace_deactivated(&self, cx: &mut MutableAppContext) {
682 self.update(cx, |this, cx| this.workspace_deactivated(cx));
683 }
684
685 fn navigate(&self, data: Box<dyn Any>, cx: &mut MutableAppContext) -> bool {
686 self.update(cx, |this, cx| this.navigate(data, cx))
687 }
688
689 fn id(&self) -> usize {
690 self.id()
691 }
692
693 fn window_id(&self) -> usize {
694 self.window_id()
695 }
696
697 fn to_any(&self) -> AnyViewHandle {
698 self.into()
699 }
700
701 fn is_dirty(&self, cx: &AppContext) -> bool {
702 self.read(cx).is_dirty(cx)
703 }
704
705 fn has_conflict(&self, cx: &AppContext) -> bool {
706 self.read(cx).has_conflict(cx)
707 }
708
709 fn can_save(&self, cx: &AppContext) -> bool {
710 self.read(cx).can_save(cx)
711 }
712
713 fn save(&self, project: ModelHandle<Project>, cx: &mut MutableAppContext) -> Task<Result<()>> {
714 self.update(cx, |item, cx| item.save(project, cx))
715 }
716
717 fn save_as(
718 &self,
719 project: ModelHandle<Project>,
720 abs_path: PathBuf,
721 cx: &mut MutableAppContext,
722 ) -> Task<anyhow::Result<()>> {
723 self.update(cx, |item, cx| item.save_as(project, abs_path, cx))
724 }
725
726 fn reload(
727 &self,
728 project: ModelHandle<Project>,
729 cx: &mut MutableAppContext,
730 ) -> Task<Result<()>> {
731 self.update(cx, |item, cx| item.reload(project, cx))
732 }
733
734 fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyViewHandle> {
735 self.read(cx).act_as_type(type_id, self, cx)
736 }
737
738 fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>> {
739 if cx.has_global::<FollowableItemBuilders>() {
740 let builders = cx.global::<FollowableItemBuilders>();
741 let item = self.to_any();
742 Some(builders.get(&item.view_type())?.1(item))
743 } else {
744 None
745 }
746 }
747
748 fn on_release(
749 &self,
750 cx: &mut MutableAppContext,
751 callback: Box<dyn FnOnce(&mut MutableAppContext)>,
752 ) -> gpui::Subscription {
753 cx.observe_release(self, move |_, cx| callback(cx))
754 }
755
756 fn to_searchable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>> {
757 self.read(cx).as_searchable(self)
758 }
759
760 fn breadcrumb_location(&self, cx: &AppContext) -> ToolbarItemLocation {
761 self.read(cx).breadcrumb_location()
762 }
763
764 fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option<Vec<ElementBox>> {
765 self.read(cx).breadcrumbs(theme, cx)
766 }
767}
768
769impl From<Box<dyn ItemHandle>> for AnyViewHandle {
770 fn from(val: Box<dyn ItemHandle>) -> Self {
771 val.to_any()
772 }
773}
774
775impl From<&Box<dyn ItemHandle>> for AnyViewHandle {
776 fn from(val: &Box<dyn ItemHandle>) -> Self {
777 val.to_any()
778 }
779}
780
781impl Clone for Box<dyn ItemHandle> {
782 fn clone(&self) -> Box<dyn ItemHandle> {
783 self.boxed_clone()
784 }
785}
786
787impl<T: Item> WeakItemHandle for WeakViewHandle<T> {
788 fn id(&self) -> usize {
789 self.id()
790 }
791
792 fn window_id(&self) -> usize {
793 self.window_id()
794 }
795
796 fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
797 self.upgrade(cx).map(|v| Box::new(v) as Box<dyn ItemHandle>)
798 }
799}
800
801pub trait Notification: View {
802 fn should_dismiss_notification_on_event(&self, event: &<Self as Entity>::Event) -> bool;
803}
804
805pub trait NotificationHandle {
806 fn id(&self) -> usize;
807 fn to_any(&self) -> AnyViewHandle;
808}
809
810impl<T: Notification> NotificationHandle for ViewHandle<T> {
811 fn id(&self) -> usize {
812 self.id()
813 }
814
815 fn to_any(&self) -> AnyViewHandle {
816 self.into()
817 }
818}
819
820impl From<&dyn NotificationHandle> for AnyViewHandle {
821 fn from(val: &dyn NotificationHandle) -> Self {
822 val.to_any()
823 }
824}
825
826impl AppState {
827 #[cfg(any(test, feature = "test-support"))]
828 pub fn test(cx: &mut MutableAppContext) -> Arc<Self> {
829 let settings = Settings::test(cx);
830 cx.set_global(settings);
831
832 let fs = project::FakeFs::new(cx.background().clone());
833 let languages = Arc::new(LanguageRegistry::test());
834 let http_client = client::test::FakeHttpClient::with_404_response();
835 let client = Client::new(http_client.clone());
836 let project_store = cx.add_model(|_| ProjectStore::new());
837 let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
838 let themes = ThemeRegistry::new((), cx.font_cache().clone());
839 Arc::new(Self {
840 client,
841 themes,
842 fs,
843 languages,
844 user_store,
845 project_store,
846 initialize_workspace: |_, _, _| {},
847 build_window_options: Default::default,
848 default_item_factory: |_, _| unimplemented!(),
849 })
850 }
851}
852
853pub enum Event {
854 DockAnchorChanged,
855 PaneAdded(ViewHandle<Pane>),
856 ContactRequestedJoin(u64),
857}
858
859pub struct Workspace {
860 weak_self: WeakViewHandle<Self>,
861 client: Arc<Client>,
862 user_store: ModelHandle<client::UserStore>,
863 remote_entity_subscription: Option<Subscription>,
864 fs: Arc<dyn Fs>,
865 modal: Option<AnyViewHandle>,
866 center: PaneGroup,
867 left_sidebar: ViewHandle<Sidebar>,
868 right_sidebar: ViewHandle<Sidebar>,
869 panes: Vec<ViewHandle<Pane>>,
870 panes_by_item: HashMap<usize, WeakViewHandle<Pane>>,
871 active_pane: ViewHandle<Pane>,
872 last_active_center_pane: Option<ViewHandle<Pane>>,
873 status_bar: ViewHandle<StatusBar>,
874 titlebar_item: Option<AnyViewHandle>,
875 dock: Dock,
876 notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
877 project: ModelHandle<Project>,
878 leader_state: LeaderState,
879 follower_states_by_leader: FollowerStatesByLeader,
880 last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
881 window_edited: bool,
882 _observe_current_user: Task<()>,
883}
884
885#[derive(Default)]
886struct LeaderState {
887 followers: HashSet<PeerId>,
888}
889
890type FollowerStatesByLeader = HashMap<PeerId, HashMap<ViewHandle<Pane>, FollowerState>>;
891
892#[derive(Default)]
893struct FollowerState {
894 active_view_id: Option<u64>,
895 items_by_leader_view_id: HashMap<u64, FollowerItem>,
896}
897
898#[derive(Debug)]
899enum FollowerItem {
900 Loading(Vec<proto::update_view::Variant>),
901 Loaded(Box<dyn FollowableItemHandle>),
902}
903
904impl Workspace {
905 pub fn new(
906 project: ModelHandle<Project>,
907 dock_default_factory: DefaultItemFactory,
908 cx: &mut ViewContext<Self>,
909 ) -> Self {
910 cx.observe_fullscreen(|_, _, cx| cx.notify()).detach();
911
912 cx.observe_window_activation(Self::on_window_activation_changed)
913 .detach();
914 cx.observe(&project, |_, _, cx| cx.notify()).detach();
915 cx.subscribe(&project, move |this, _, event, cx| {
916 match event {
917 project::Event::RemoteIdChanged(remote_id) => {
918 this.project_remote_id_changed(*remote_id, cx);
919 }
920 project::Event::CollaboratorLeft(peer_id) => {
921 this.collaborator_left(*peer_id, cx);
922 }
923 project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded => {
924 this.update_window_title(cx);
925 }
926 project::Event::DisconnectedFromHost => {
927 this.update_window_edited(cx);
928 cx.blur();
929 }
930 _ => {}
931 }
932 cx.notify()
933 })
934 .detach();
935
936 let center_pane = cx.add_view(|cx| Pane::new(None, cx));
937 let pane_id = center_pane.id();
938 cx.subscribe(¢er_pane, move |this, _, event, cx| {
939 this.handle_pane_event(pane_id, event, cx)
940 })
941 .detach();
942 cx.focus(¢er_pane);
943 cx.emit(Event::PaneAdded(center_pane.clone()));
944
945 let fs = project.read(cx).fs().clone();
946 let user_store = project.read(cx).user_store();
947 let client = project.read(cx).client();
948 let mut current_user = user_store.read(cx).watch_current_user();
949 let mut connection_status = client.status();
950 let _observe_current_user = cx.spawn_weak(|this, mut cx| async move {
951 current_user.recv().await;
952 connection_status.recv().await;
953 let mut stream =
954 Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
955
956 while stream.recv().await.is_some() {
957 cx.update(|cx| {
958 if let Some(this) = this.upgrade(cx) {
959 this.update(cx, |_, cx| cx.notify());
960 }
961 })
962 }
963 });
964
965 let handle = cx.handle();
966 let weak_handle = cx.weak_handle();
967
968 cx.emit_global(WorkspaceCreated(weak_handle.clone()));
969
970 let dock = Dock::new(cx, dock_default_factory);
971 let dock_pane = dock.pane().clone();
972
973 let left_sidebar = cx.add_view(|_| Sidebar::new(SidebarSide::Left));
974 let right_sidebar = cx.add_view(|_| Sidebar::new(SidebarSide::Right));
975 let left_sidebar_buttons = cx.add_view(|cx| SidebarButtons::new(left_sidebar.clone(), cx));
976 let toggle_dock = cx.add_view(|cx| ToggleDockButton::new(handle, cx));
977 let right_sidebar_buttons =
978 cx.add_view(|cx| SidebarButtons::new(right_sidebar.clone(), cx));
979 let status_bar = cx.add_view(|cx| {
980 let mut status_bar = StatusBar::new(¢er_pane.clone(), cx);
981 status_bar.add_left_item(left_sidebar_buttons, cx);
982 status_bar.add_right_item(right_sidebar_buttons, cx);
983 status_bar.add_right_item(toggle_dock, cx);
984 status_bar
985 });
986
987 cx.update_default_global::<DragAndDrop<Workspace>, _, _>(|drag_and_drop, _| {
988 drag_and_drop.register_container(weak_handle.clone());
989 });
990
991 let mut this = Workspace {
992 modal: None,
993 weak_self: weak_handle,
994 center: PaneGroup::new(center_pane.clone()),
995 dock,
996 // When removing an item, the last element remaining in this array
997 // is used to find where focus should fallback to. As such, the order
998 // of these two variables is important.
999 panes: vec![dock_pane, center_pane.clone()],
1000 panes_by_item: Default::default(),
1001 active_pane: center_pane.clone(),
1002 last_active_center_pane: Some(center_pane.clone()),
1003 status_bar,
1004 titlebar_item: None,
1005 notifications: Default::default(),
1006 client,
1007 remote_entity_subscription: None,
1008 user_store,
1009 fs,
1010 left_sidebar,
1011 right_sidebar,
1012 project,
1013 leader_state: Default::default(),
1014 follower_states_by_leader: Default::default(),
1015 last_leaders_by_pane: Default::default(),
1016 window_edited: false,
1017 _observe_current_user,
1018 };
1019 this.project_remote_id_changed(this.project.read(cx).remote_id(), cx);
1020 cx.defer(|this, cx| this.update_window_title(cx));
1021
1022 this
1023 }
1024
1025 pub fn weak_handle(&self) -> WeakViewHandle<Self> {
1026 self.weak_self.clone()
1027 }
1028
1029 pub fn left_sidebar(&self) -> &ViewHandle<Sidebar> {
1030 &self.left_sidebar
1031 }
1032
1033 pub fn right_sidebar(&self) -> &ViewHandle<Sidebar> {
1034 &self.right_sidebar
1035 }
1036
1037 pub fn status_bar(&self) -> &ViewHandle<StatusBar> {
1038 &self.status_bar
1039 }
1040
1041 pub fn user_store(&self) -> &ModelHandle<UserStore> {
1042 &self.user_store
1043 }
1044
1045 pub fn project(&self) -> &ModelHandle<Project> {
1046 &self.project
1047 }
1048
1049 pub fn client(&self) -> &Arc<Client> {
1050 &self.client
1051 }
1052
1053 pub fn set_titlebar_item(
1054 &mut self,
1055 item: impl Into<AnyViewHandle>,
1056 cx: &mut ViewContext<Self>,
1057 ) {
1058 self.titlebar_item = Some(item.into());
1059 cx.notify();
1060 }
1061
1062 /// Call the given callback with a workspace whose project is local.
1063 ///
1064 /// If the given workspace has a local project, then it will be passed
1065 /// to the callback. Otherwise, a new empty window will be created.
1066 pub fn with_local_workspace<T, F>(
1067 &mut self,
1068 cx: &mut ViewContext<Self>,
1069 app_state: Arc<AppState>,
1070 callback: F,
1071 ) -> T
1072 where
1073 T: 'static,
1074 F: FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
1075 {
1076 if self.project.read(cx).is_local() {
1077 callback(self, cx)
1078 } else {
1079 let (_, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
1080 let mut workspace = Workspace::new(
1081 Project::local(
1082 app_state.client.clone(),
1083 app_state.user_store.clone(),
1084 app_state.project_store.clone(),
1085 app_state.languages.clone(),
1086 app_state.fs.clone(),
1087 cx,
1088 ),
1089 app_state.default_item_factory,
1090 cx,
1091 );
1092 (app_state.initialize_workspace)(&mut workspace, &app_state, cx);
1093 workspace
1094 });
1095 workspace.update(cx, callback)
1096 }
1097 }
1098
1099 pub fn worktrees<'a>(
1100 &self,
1101 cx: &'a AppContext,
1102 ) -> impl 'a + Iterator<Item = ModelHandle<Worktree>> {
1103 self.project.read(cx).worktrees(cx)
1104 }
1105
1106 pub fn visible_worktrees<'a>(
1107 &self,
1108 cx: &'a AppContext,
1109 ) -> impl 'a + Iterator<Item = ModelHandle<Worktree>> {
1110 self.project.read(cx).visible_worktrees(cx)
1111 }
1112
1113 pub fn worktree_scans_complete(&self, cx: &AppContext) -> impl Future<Output = ()> + 'static {
1114 let futures = self
1115 .worktrees(cx)
1116 .filter_map(|worktree| worktree.read(cx).as_local())
1117 .map(|worktree| worktree.scan_complete())
1118 .collect::<Vec<_>>();
1119 async move {
1120 for future in futures {
1121 future.await;
1122 }
1123 }
1124 }
1125
1126 pub fn close(
1127 &mut self,
1128 _: &CloseWindow,
1129 cx: &mut ViewContext<Self>,
1130 ) -> Option<Task<Result<()>>> {
1131 let prepare = self.prepare_to_close(cx);
1132 Some(cx.spawn(|this, mut cx| async move {
1133 if prepare.await? {
1134 this.update(&mut cx, |_, cx| {
1135 let window_id = cx.window_id();
1136 cx.remove_window(window_id);
1137 });
1138 }
1139 Ok(())
1140 }))
1141 }
1142
1143 pub fn prepare_to_close(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<bool>> {
1144 self.save_all_internal(true, cx)
1145 }
1146
1147 fn save_all(&mut self, _: &SaveAll, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
1148 let save_all = self.save_all_internal(false, cx);
1149 Some(cx.foreground().spawn(async move {
1150 save_all.await?;
1151 Ok(())
1152 }))
1153 }
1154
1155 fn save_all_internal(
1156 &mut self,
1157 should_prompt_to_save: bool,
1158 cx: &mut ViewContext<Self>,
1159 ) -> Task<Result<bool>> {
1160 if self.project.read(cx).is_read_only() {
1161 return Task::ready(Ok(true));
1162 }
1163
1164 let dirty_items = self
1165 .panes
1166 .iter()
1167 .flat_map(|pane| {
1168 pane.read(cx).items().filter_map(|item| {
1169 if item.is_dirty(cx) {
1170 Some((pane.clone(), item.boxed_clone()))
1171 } else {
1172 None
1173 }
1174 })
1175 })
1176 .collect::<Vec<_>>();
1177
1178 let project = self.project.clone();
1179 cx.spawn_weak(|_, mut cx| async move {
1180 for (pane, item) in dirty_items {
1181 let (singleton, project_entry_ids) =
1182 cx.read(|cx| (item.is_singleton(cx), item.project_entry_ids(cx)));
1183 if singleton || !project_entry_ids.is_empty() {
1184 if let Some(ix) =
1185 pane.read_with(&cx, |pane, _| pane.index_for_item(item.as_ref()))
1186 {
1187 if !Pane::save_item(
1188 project.clone(),
1189 &pane,
1190 ix,
1191 &*item,
1192 should_prompt_to_save,
1193 &mut cx,
1194 )
1195 .await?
1196 {
1197 return Ok(false);
1198 }
1199 }
1200 }
1201 }
1202 Ok(true)
1203 })
1204 }
1205
1206 #[allow(clippy::type_complexity)]
1207 pub fn open_paths(
1208 &mut self,
1209 mut abs_paths: Vec<PathBuf>,
1210 visible: bool,
1211 cx: &mut ViewContext<Self>,
1212 ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>>>> {
1213 let fs = self.fs.clone();
1214
1215 // Sort the paths to ensure we add worktrees for parents before their children.
1216 abs_paths.sort_unstable();
1217 cx.spawn(|this, mut cx| async move {
1218 let mut project_paths = Vec::new();
1219 for path in &abs_paths {
1220 project_paths.push(
1221 this.update(&mut cx, |this, cx| {
1222 this.project_path_for_path(path, visible, cx)
1223 })
1224 .await
1225 .log_err(),
1226 );
1227 }
1228
1229 let tasks = abs_paths
1230 .iter()
1231 .cloned()
1232 .zip(project_paths.into_iter())
1233 .map(|(abs_path, project_path)| {
1234 let this = this.clone();
1235 cx.spawn(|mut cx| {
1236 let fs = fs.clone();
1237 async move {
1238 let (_worktree, project_path) = project_path?;
1239 if fs.is_file(&abs_path).await {
1240 Some(
1241 this.update(&mut cx, |this, cx| {
1242 this.open_path(project_path, true, cx)
1243 })
1244 .await,
1245 )
1246 } else {
1247 None
1248 }
1249 }
1250 })
1251 })
1252 .collect::<Vec<_>>();
1253
1254 futures::future::join_all(tasks).await
1255 })
1256 }
1257
1258 fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext<Self>) {
1259 let mut paths = cx.prompt_for_paths(PathPromptOptions {
1260 files: false,
1261 directories: true,
1262 multiple: true,
1263 });
1264 cx.spawn(|this, mut cx| async move {
1265 if let Some(paths) = paths.recv().await.flatten() {
1266 let results = this
1267 .update(&mut cx, |this, cx| this.open_paths(paths, true, cx))
1268 .await;
1269 for result in results.into_iter().flatten() {
1270 result.log_err();
1271 }
1272 }
1273 })
1274 .detach();
1275 }
1276
1277 fn remove_folder_from_project(
1278 &mut self,
1279 RemoveWorktreeFromProject(worktree_id): &RemoveWorktreeFromProject,
1280 cx: &mut ViewContext<Self>,
1281 ) {
1282 self.project
1283 .update(cx, |project, cx| project.remove_worktree(*worktree_id, cx));
1284 }
1285
1286 fn project_path_for_path(
1287 &self,
1288 abs_path: &Path,
1289 visible: bool,
1290 cx: &mut ViewContext<Self>,
1291 ) -> Task<Result<(ModelHandle<Worktree>, ProjectPath)>> {
1292 let entry = self.project().update(cx, |project, cx| {
1293 project.find_or_create_local_worktree(abs_path, visible, cx)
1294 });
1295 cx.spawn(|_, cx| async move {
1296 let (worktree, path) = entry.await?;
1297 let worktree_id = worktree.read_with(&cx, |t, _| t.id());
1298 Ok((
1299 worktree,
1300 ProjectPath {
1301 worktree_id,
1302 path: path.into(),
1303 },
1304 ))
1305 })
1306 }
1307
1308 /// Returns the modal that was toggled closed if it was open.
1309 pub fn toggle_modal<V, F>(
1310 &mut self,
1311 cx: &mut ViewContext<Self>,
1312 add_view: F,
1313 ) -> Option<ViewHandle<V>>
1314 where
1315 V: 'static + View,
1316 F: FnOnce(&mut Self, &mut ViewContext<Self>) -> ViewHandle<V>,
1317 {
1318 cx.notify();
1319 // Whatever modal was visible is getting clobbered. If its the same type as V, then return
1320 // it. Otherwise, create a new modal and set it as active.
1321 let already_open_modal = self.modal.take().and_then(|modal| modal.downcast::<V>());
1322 if let Some(already_open_modal) = already_open_modal {
1323 cx.focus_self();
1324 Some(already_open_modal)
1325 } else {
1326 let modal = add_view(self, cx);
1327 cx.focus(&modal);
1328 self.modal = Some(modal.into());
1329 None
1330 }
1331 }
1332
1333 pub fn modal<V: 'static + View>(&self) -> Option<ViewHandle<V>> {
1334 self.modal
1335 .as_ref()
1336 .and_then(|modal| modal.clone().downcast::<V>())
1337 }
1338
1339 pub fn dismiss_modal(&mut self, cx: &mut ViewContext<Self>) {
1340 if self.modal.take().is_some() {
1341 cx.focus(&self.active_pane);
1342 cx.notify();
1343 }
1344 }
1345
1346 pub fn show_notification<V: Notification>(
1347 &mut self,
1348 id: usize,
1349 cx: &mut ViewContext<Self>,
1350 build_notification: impl FnOnce(&mut ViewContext<Self>) -> ViewHandle<V>,
1351 ) {
1352 let type_id = TypeId::of::<V>();
1353 if self
1354 .notifications
1355 .iter()
1356 .all(|(existing_type_id, existing_id, _)| {
1357 (*existing_type_id, *existing_id) != (type_id, id)
1358 })
1359 {
1360 let notification = build_notification(cx);
1361 cx.subscribe(¬ification, move |this, handle, event, cx| {
1362 if handle.read(cx).should_dismiss_notification_on_event(event) {
1363 this.dismiss_notification(type_id, id, cx);
1364 }
1365 })
1366 .detach();
1367 self.notifications
1368 .push((type_id, id, Box::new(notification)));
1369 cx.notify();
1370 }
1371 }
1372
1373 fn dismiss_notification(&mut self, type_id: TypeId, id: usize, cx: &mut ViewContext<Self>) {
1374 self.notifications
1375 .retain(|(existing_type_id, existing_id, _)| {
1376 if (*existing_type_id, *existing_id) == (type_id, id) {
1377 cx.notify();
1378 false
1379 } else {
1380 true
1381 }
1382 });
1383 }
1384
1385 pub fn items<'a>(
1386 &'a self,
1387 cx: &'a AppContext,
1388 ) -> impl 'a + Iterator<Item = &Box<dyn ItemHandle>> {
1389 self.panes.iter().flat_map(|pane| pane.read(cx).items())
1390 }
1391
1392 pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<ViewHandle<T>> {
1393 self.items_of_type(cx).max_by_key(|item| item.id())
1394 }
1395
1396 pub fn items_of_type<'a, T: Item>(
1397 &'a self,
1398 cx: &'a AppContext,
1399 ) -> impl 'a + Iterator<Item = ViewHandle<T>> {
1400 self.panes
1401 .iter()
1402 .flat_map(|pane| pane.read(cx).items_of_type())
1403 }
1404
1405 pub fn active_item(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
1406 self.active_pane().read(cx).active_item()
1407 }
1408
1409 fn active_project_path(&self, cx: &ViewContext<Self>) -> Option<ProjectPath> {
1410 self.active_item(cx).and_then(|item| item.project_path(cx))
1411 }
1412
1413 pub fn save_active_item(
1414 &mut self,
1415 force_name_change: bool,
1416 cx: &mut ViewContext<Self>,
1417 ) -> Task<Result<()>> {
1418 let project = self.project.clone();
1419 if let Some(item) = self.active_item(cx) {
1420 if !force_name_change && item.can_save(cx) {
1421 if item.has_conflict(cx.as_ref()) {
1422 const CONFLICT_MESSAGE: &str = "This file has changed on disk since you started editing it. Do you want to overwrite it?";
1423
1424 let mut answer = cx.prompt(
1425 PromptLevel::Warning,
1426 CONFLICT_MESSAGE,
1427 &["Overwrite", "Cancel"],
1428 );
1429 cx.spawn(|_, mut cx| async move {
1430 let answer = answer.recv().await;
1431 if answer == Some(0) {
1432 cx.update(|cx| item.save(project, cx)).await?;
1433 }
1434 Ok(())
1435 })
1436 } else {
1437 item.save(project, cx)
1438 }
1439 } else if item.is_singleton(cx) {
1440 let worktree = self.worktrees(cx).next();
1441 let start_abs_path = worktree
1442 .and_then(|w| w.read(cx).as_local())
1443 .map_or(Path::new(""), |w| w.abs_path())
1444 .to_path_buf();
1445 let mut abs_path = cx.prompt_for_new_path(&start_abs_path);
1446 cx.spawn(|_, mut cx| async move {
1447 if let Some(abs_path) = abs_path.recv().await.flatten() {
1448 cx.update(|cx| item.save_as(project, abs_path, cx)).await?;
1449 }
1450 Ok(())
1451 })
1452 } else {
1453 Task::ready(Ok(()))
1454 }
1455 } else {
1456 Task::ready(Ok(()))
1457 }
1458 }
1459
1460 pub fn toggle_sidebar(&mut self, sidebar_side: SidebarSide, cx: &mut ViewContext<Self>) {
1461 let sidebar = match sidebar_side {
1462 SidebarSide::Left => &mut self.left_sidebar,
1463 SidebarSide::Right => &mut self.right_sidebar,
1464 };
1465 let open = sidebar.update(cx, |sidebar, cx| {
1466 let open = !sidebar.is_open();
1467 sidebar.set_open(open, cx);
1468 open
1469 });
1470
1471 if open {
1472 Dock::hide_on_sidebar_shown(self, sidebar_side, cx);
1473 }
1474
1475 cx.focus_self();
1476 cx.notify();
1477 }
1478
1479 pub fn toggle_sidebar_item(&mut self, action: &ToggleSidebarItem, cx: &mut ViewContext<Self>) {
1480 let sidebar = match action.sidebar_side {
1481 SidebarSide::Left => &mut self.left_sidebar,
1482 SidebarSide::Right => &mut self.right_sidebar,
1483 };
1484 let active_item = sidebar.update(cx, move |sidebar, cx| {
1485 if sidebar.is_open() && sidebar.active_item_ix() == action.item_index {
1486 sidebar.set_open(false, cx);
1487 None
1488 } else {
1489 sidebar.set_open(true, cx);
1490 sidebar.activate_item(action.item_index, cx);
1491 sidebar.active_item().cloned()
1492 }
1493 });
1494
1495 if let Some(active_item) = active_item {
1496 Dock::hide_on_sidebar_shown(self, action.sidebar_side, cx);
1497
1498 if active_item.is_focused(cx) {
1499 cx.focus_self();
1500 } else {
1501 cx.focus(active_item.to_any());
1502 }
1503 } else {
1504 cx.focus_self();
1505 }
1506 cx.notify();
1507 }
1508
1509 pub fn toggle_sidebar_item_focus(
1510 &mut self,
1511 sidebar_side: SidebarSide,
1512 item_index: usize,
1513 cx: &mut ViewContext<Self>,
1514 ) {
1515 let sidebar = match sidebar_side {
1516 SidebarSide::Left => &mut self.left_sidebar,
1517 SidebarSide::Right => &mut self.right_sidebar,
1518 };
1519 let active_item = sidebar.update(cx, |sidebar, cx| {
1520 sidebar.set_open(true, cx);
1521 sidebar.activate_item(item_index, cx);
1522 sidebar.active_item().cloned()
1523 });
1524 if let Some(active_item) = active_item {
1525 Dock::hide_on_sidebar_shown(self, sidebar_side, cx);
1526
1527 if active_item.is_focused(cx) {
1528 cx.focus_self();
1529 } else {
1530 cx.focus(active_item.to_any());
1531 }
1532 }
1533 cx.notify();
1534 }
1535
1536 pub fn focus_center(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
1537 cx.focus_self();
1538 cx.notify();
1539 }
1540
1541 fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
1542 let pane = cx.add_view(|cx| Pane::new(None, cx));
1543 let pane_id = pane.id();
1544 cx.subscribe(&pane, move |this, _, event, cx| {
1545 this.handle_pane_event(pane_id, event, cx)
1546 })
1547 .detach();
1548 self.panes.push(pane.clone());
1549 cx.focus(pane.clone());
1550 cx.emit(Event::PaneAdded(pane.clone()));
1551 pane
1552 }
1553
1554 pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
1555 let active_pane = self.active_pane().clone();
1556 Pane::add_item(self, &active_pane, item, true, true, None, cx);
1557 }
1558
1559 pub fn open_path(
1560 &mut self,
1561 path: impl Into<ProjectPath>,
1562 focus_item: bool,
1563 cx: &mut ViewContext<Self>,
1564 ) -> Task<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>> {
1565 let pane = self.active_pane().downgrade();
1566 let task = self.load_path(path.into(), cx);
1567 cx.spawn(|this, mut cx| async move {
1568 let (project_entry_id, build_item) = task.await?;
1569 let pane = pane
1570 .upgrade(&cx)
1571 .ok_or_else(|| anyhow!("pane was closed"))?;
1572 this.update(&mut cx, |this, cx| {
1573 Ok(Pane::open_item(
1574 this,
1575 pane,
1576 project_entry_id,
1577 focus_item,
1578 cx,
1579 build_item,
1580 ))
1581 })
1582 })
1583 }
1584
1585 pub(crate) fn load_path(
1586 &mut self,
1587 path: ProjectPath,
1588 cx: &mut ViewContext<Self>,
1589 ) -> Task<
1590 Result<(
1591 ProjectEntryId,
1592 impl 'static + FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
1593 )>,
1594 > {
1595 let project = self.project().clone();
1596 let project_item = project.update(cx, |project, cx| project.open_path(path, cx));
1597 cx.as_mut().spawn(|mut cx| async move {
1598 let (project_entry_id, project_item) = project_item.await?;
1599 let build_item = cx.update(|cx| {
1600 cx.default_global::<ProjectItemBuilders>()
1601 .get(&project_item.model_type())
1602 .ok_or_else(|| anyhow!("no item builder for project item"))
1603 .cloned()
1604 })?;
1605 let build_item =
1606 move |cx: &mut ViewContext<Pane>| build_item(project, project_item, cx);
1607 Ok((project_entry_id, build_item))
1608 })
1609 }
1610
1611 pub fn open_project_item<T>(
1612 &mut self,
1613 project_item: ModelHandle<T::Item>,
1614 cx: &mut ViewContext<Self>,
1615 ) -> ViewHandle<T>
1616 where
1617 T: ProjectItem,
1618 {
1619 use project::Item as _;
1620
1621 let entry_id = project_item.read(cx).entry_id(cx);
1622 if let Some(item) = entry_id
1623 .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx))
1624 .and_then(|item| item.downcast())
1625 {
1626 self.activate_item(&item, cx);
1627 return item;
1628 }
1629
1630 let item = cx.add_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
1631 self.add_item(Box::new(item.clone()), cx);
1632 item
1633 }
1634
1635 pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut ViewContext<Self>) -> bool {
1636 let result = self.panes.iter().find_map(|pane| {
1637 pane.read(cx)
1638 .index_for_item(item)
1639 .map(|ix| (pane.clone(), ix))
1640 });
1641 if let Some((pane, ix)) = result {
1642 pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx));
1643 true
1644 } else {
1645 false
1646 }
1647 }
1648
1649 fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext<Self>) {
1650 let panes = self.center.panes();
1651 if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
1652 cx.focus(pane);
1653 } else {
1654 self.split_pane(self.active_pane.clone(), SplitDirection::Right, cx);
1655 }
1656 }
1657
1658 pub fn activate_next_pane(&mut self, cx: &mut ViewContext<Self>) {
1659 let next_pane = {
1660 let panes = self.center.panes();
1661 let ix = panes
1662 .iter()
1663 .position(|pane| **pane == self.active_pane)
1664 .unwrap();
1665 let next_ix = (ix + 1) % panes.len();
1666 panes[next_ix].clone()
1667 };
1668 cx.focus(next_pane);
1669 }
1670
1671 pub fn activate_previous_pane(&mut self, cx: &mut ViewContext<Self>) {
1672 let prev_pane = {
1673 let panes = self.center.panes();
1674 let ix = panes
1675 .iter()
1676 .position(|pane| **pane == self.active_pane)
1677 .unwrap();
1678 let prev_ix = if ix == 0 { panes.len() - 1 } else { ix - 1 };
1679 panes[prev_ix].clone()
1680 };
1681 cx.focus(prev_pane);
1682 }
1683
1684 fn handle_pane_focused(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
1685 if self.active_pane != pane {
1686 self.active_pane
1687 .update(cx, |pane, cx| pane.set_active(false, cx));
1688 self.active_pane = pane.clone();
1689 self.active_pane
1690 .update(cx, |pane, cx| pane.set_active(true, cx));
1691 self.status_bar.update(cx, |status_bar, cx| {
1692 status_bar.set_active_pane(&self.active_pane, cx);
1693 });
1694 self.active_item_path_changed(cx);
1695
1696 if &pane == self.dock_pane() {
1697 Dock::show(self, cx);
1698 } else {
1699 self.last_active_center_pane = Some(pane.clone());
1700 if self.dock.is_anchored_at(DockAnchor::Expanded) {
1701 Dock::hide(self, cx);
1702 }
1703 }
1704 cx.notify();
1705 }
1706
1707 self.update_followers(
1708 proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
1709 id: self.active_item(cx).map(|item| item.id() as u64),
1710 leader_id: self.leader_for_pane(&pane).map(|id| id.0),
1711 }),
1712 cx,
1713 );
1714 }
1715
1716 fn handle_pane_event(
1717 &mut self,
1718 pane_id: usize,
1719 event: &pane::Event,
1720 cx: &mut ViewContext<Self>,
1721 ) {
1722 if let Some(pane) = self.pane(pane_id) {
1723 let is_dock = &pane == self.dock.pane();
1724 match event {
1725 pane::Event::Split(direction) if !is_dock => {
1726 self.split_pane(pane, *direction, cx);
1727 }
1728 pane::Event::Remove if !is_dock => self.remove_pane(pane, cx),
1729 pane::Event::Remove if is_dock => Dock::hide(self, cx),
1730 pane::Event::Focused => self.handle_pane_focused(pane, cx),
1731 pane::Event::ActivateItem { local } => {
1732 if *local {
1733 self.unfollow(&pane, cx);
1734 }
1735 if &pane == self.active_pane() {
1736 self.active_item_path_changed(cx);
1737 }
1738 }
1739 pane::Event::ChangeItemTitle => {
1740 if pane == self.active_pane {
1741 self.active_item_path_changed(cx);
1742 }
1743 self.update_window_edited(cx);
1744 }
1745 pane::Event::RemoveItem { item_id } => {
1746 self.update_window_edited(cx);
1747 if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) {
1748 if entry.get().id() == pane.id() {
1749 entry.remove();
1750 }
1751 }
1752 }
1753 _ => {}
1754 }
1755 } else if self.dock.visible_pane().is_none() {
1756 error!("pane {} not found", pane_id);
1757 }
1758 }
1759
1760 pub fn split_pane(
1761 &mut self,
1762 pane: ViewHandle<Pane>,
1763 direction: SplitDirection,
1764 cx: &mut ViewContext<Self>,
1765 ) -> Option<ViewHandle<Pane>> {
1766 if &pane == self.dock_pane() {
1767 warn!("Can't split dock pane.");
1768 return None;
1769 }
1770
1771 pane.read(cx).active_item().map(|item| {
1772 let new_pane = self.add_pane(cx);
1773 if let Some(clone) = item.clone_on_split(cx.as_mut()) {
1774 Pane::add_item(self, &new_pane, clone, true, true, None, cx);
1775 }
1776 self.center.split(&pane, &new_pane, direction).unwrap();
1777 cx.notify();
1778 new_pane
1779 })
1780 }
1781
1782 fn remove_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
1783 if self.center.remove(&pane).unwrap() {
1784 self.panes.retain(|p| p != &pane);
1785 cx.focus(self.panes.last().unwrap().clone());
1786 self.unfollow(&pane, cx);
1787 self.last_leaders_by_pane.remove(&pane.downgrade());
1788 for removed_item in pane.read(cx).items() {
1789 self.panes_by_item.remove(&removed_item.id());
1790 }
1791 if self.last_active_center_pane == Some(pane) {
1792 self.last_active_center_pane = None;
1793 }
1794
1795 cx.notify();
1796 } else {
1797 self.active_item_path_changed(cx);
1798 }
1799 }
1800
1801 pub fn panes(&self) -> &[ViewHandle<Pane>] {
1802 &self.panes
1803 }
1804
1805 fn pane(&self, pane_id: usize) -> Option<ViewHandle<Pane>> {
1806 self.panes.iter().find(|pane| pane.id() == pane_id).cloned()
1807 }
1808
1809 pub fn active_pane(&self) -> &ViewHandle<Pane> {
1810 &self.active_pane
1811 }
1812
1813 pub fn dock_pane(&self) -> &ViewHandle<Pane> {
1814 self.dock.pane()
1815 }
1816
1817 fn project_remote_id_changed(&mut self, remote_id: Option<u64>, cx: &mut ViewContext<Self>) {
1818 if let Some(remote_id) = remote_id {
1819 self.remote_entity_subscription =
1820 Some(self.client.add_view_for_remote_entity(remote_id, cx));
1821 } else {
1822 self.remote_entity_subscription.take();
1823 }
1824 }
1825
1826 fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
1827 self.leader_state.followers.remove(&peer_id);
1828 if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) {
1829 for state in states_by_pane.into_values() {
1830 for item in state.items_by_leader_view_id.into_values() {
1831 if let FollowerItem::Loaded(item) = item {
1832 item.set_leader_replica_id(None, cx);
1833 }
1834 }
1835 }
1836 }
1837 cx.notify();
1838 }
1839
1840 pub fn toggle_follow(
1841 &mut self,
1842 ToggleFollow(leader_id): &ToggleFollow,
1843 cx: &mut ViewContext<Self>,
1844 ) -> Option<Task<Result<()>>> {
1845 let leader_id = *leader_id;
1846 let pane = self.active_pane().clone();
1847
1848 if let Some(prev_leader_id) = self.unfollow(&pane, cx) {
1849 if leader_id == prev_leader_id {
1850 return None;
1851 }
1852 }
1853
1854 self.last_leaders_by_pane
1855 .insert(pane.downgrade(), leader_id);
1856 self.follower_states_by_leader
1857 .entry(leader_id)
1858 .or_default()
1859 .insert(pane.clone(), Default::default());
1860 cx.notify();
1861
1862 let project_id = self.project.read(cx).remote_id()?;
1863 let request = self.client.request(proto::Follow {
1864 project_id,
1865 leader_id: leader_id.0,
1866 });
1867 Some(cx.spawn_weak(|this, mut cx| async move {
1868 let response = request.await?;
1869 if let Some(this) = this.upgrade(&cx) {
1870 this.update(&mut cx, |this, _| {
1871 let state = this
1872 .follower_states_by_leader
1873 .get_mut(&leader_id)
1874 .and_then(|states_by_pane| states_by_pane.get_mut(&pane))
1875 .ok_or_else(|| anyhow!("following interrupted"))?;
1876 state.active_view_id = response.active_view_id;
1877 Ok::<_, anyhow::Error>(())
1878 })?;
1879 Self::add_views_from_leader(this, leader_id, vec![pane], response.views, &mut cx)
1880 .await?;
1881 }
1882 Ok(())
1883 }))
1884 }
1885
1886 pub fn follow_next_collaborator(
1887 &mut self,
1888 _: &FollowNextCollaborator,
1889 cx: &mut ViewContext<Self>,
1890 ) -> Option<Task<Result<()>>> {
1891 let collaborators = self.project.read(cx).collaborators();
1892 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
1893 let mut collaborators = collaborators.keys().copied();
1894 for peer_id in collaborators.by_ref() {
1895 if peer_id == leader_id {
1896 break;
1897 }
1898 }
1899 collaborators.next()
1900 } else if let Some(last_leader_id) =
1901 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
1902 {
1903 if collaborators.contains_key(last_leader_id) {
1904 Some(*last_leader_id)
1905 } else {
1906 None
1907 }
1908 } else {
1909 None
1910 };
1911
1912 next_leader_id
1913 .or_else(|| collaborators.keys().copied().next())
1914 .and_then(|leader_id| self.toggle_follow(&ToggleFollow(leader_id), cx))
1915 }
1916
1917 pub fn unfollow(
1918 &mut self,
1919 pane: &ViewHandle<Pane>,
1920 cx: &mut ViewContext<Self>,
1921 ) -> Option<PeerId> {
1922 for (leader_id, states_by_pane) in &mut self.follower_states_by_leader {
1923 let leader_id = *leader_id;
1924 if let Some(state) = states_by_pane.remove(pane) {
1925 for (_, item) in state.items_by_leader_view_id {
1926 if let FollowerItem::Loaded(item) = item {
1927 item.set_leader_replica_id(None, cx);
1928 }
1929 }
1930
1931 if states_by_pane.is_empty() {
1932 self.follower_states_by_leader.remove(&leader_id);
1933 if let Some(project_id) = self.project.read(cx).remote_id() {
1934 self.client
1935 .send(proto::Unfollow {
1936 project_id,
1937 leader_id: leader_id.0,
1938 })
1939 .log_err();
1940 }
1941 }
1942
1943 cx.notify();
1944 return Some(leader_id);
1945 }
1946 }
1947 None
1948 }
1949
1950 pub fn is_following(&self, peer_id: PeerId) -> bool {
1951 self.follower_states_by_leader.contains_key(&peer_id)
1952 }
1953
1954 fn render_titlebar(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
1955 let project = &self.project.read(cx);
1956 let mut worktree_root_names = String::new();
1957 for (i, name) in project.worktree_root_names(cx).enumerate() {
1958 if i > 0 {
1959 worktree_root_names.push_str(", ");
1960 }
1961 worktree_root_names.push_str(name);
1962 }
1963
1964 // TODO: There should be a better system in place for this
1965 // (https://github.com/zed-industries/zed/issues/1290)
1966 let is_fullscreen = cx.window_is_fullscreen(cx.window_id());
1967 let container_theme = if is_fullscreen {
1968 let mut container_theme = theme.workspace.titlebar.container;
1969 container_theme.padding.left = container_theme.padding.right;
1970 container_theme
1971 } else {
1972 theme.workspace.titlebar.container
1973 };
1974
1975 enum TitleBar {}
1976 ConstrainedBox::new(
1977 MouseEventHandler::<TitleBar>::new(0, cx, |_, _| {
1978 Container::new(
1979 Stack::new()
1980 .with_child(
1981 Label::new(worktree_root_names, theme.workspace.titlebar.title.clone())
1982 .aligned()
1983 .left()
1984 .boxed(),
1985 )
1986 .with_children(
1987 self.titlebar_item
1988 .as_ref()
1989 .map(|item| ChildView::new(item).aligned().right().boxed()),
1990 )
1991 .boxed(),
1992 )
1993 .with_style(container_theme)
1994 .boxed()
1995 })
1996 .on_click(MouseButton::Left, |event, cx| {
1997 if event.click_count == 2 {
1998 cx.zoom_window(cx.window_id());
1999 }
2000 })
2001 .boxed(),
2002 )
2003 .with_height(theme.workspace.titlebar.height)
2004 .named("titlebar")
2005 }
2006
2007 fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
2008 let active_entry = self.active_project_path(cx);
2009 self.project
2010 .update(cx, |project, cx| project.set_active_path(active_entry, cx));
2011 self.update_window_title(cx);
2012 }
2013
2014 fn update_window_title(&mut self, cx: &mut ViewContext<Self>) {
2015 let mut title = String::new();
2016 let project = self.project().read(cx);
2017 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
2018 let filename = path
2019 .path
2020 .file_name()
2021 .map(|s| s.to_string_lossy())
2022 .or_else(|| {
2023 Some(Cow::Borrowed(
2024 project
2025 .worktree_for_id(path.worktree_id, cx)?
2026 .read(cx)
2027 .root_name(),
2028 ))
2029 });
2030 if let Some(filename) = filename {
2031 title.push_str(filename.as_ref());
2032 title.push_str(" — ");
2033 }
2034 }
2035 for (i, name) in project.worktree_root_names(cx).enumerate() {
2036 if i > 0 {
2037 title.push_str(", ");
2038 }
2039 title.push_str(name);
2040 }
2041 if title.is_empty() {
2042 title = "empty project".to_string();
2043 }
2044 cx.set_window_title(&title);
2045 }
2046
2047 fn update_window_edited(&mut self, cx: &mut ViewContext<Self>) {
2048 let is_edited = !self.project.read(cx).is_read_only()
2049 && self
2050 .items(cx)
2051 .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
2052 if is_edited != self.window_edited {
2053 self.window_edited = is_edited;
2054 cx.set_window_edited(self.window_edited)
2055 }
2056 }
2057
2058 fn render_disconnected_overlay(&self, cx: &mut RenderContext<Workspace>) -> Option<ElementBox> {
2059 if self.project.read(cx).is_read_only() {
2060 enum DisconnectedOverlay {}
2061 Some(
2062 MouseEventHandler::<DisconnectedOverlay>::new(0, cx, |_, cx| {
2063 let theme = &cx.global::<Settings>().theme;
2064 Label::new(
2065 "Your connection to the remote project has been lost.".to_string(),
2066 theme.workspace.disconnected_overlay.text.clone(),
2067 )
2068 .aligned()
2069 .contained()
2070 .with_style(theme.workspace.disconnected_overlay.container)
2071 .boxed()
2072 })
2073 .with_cursor_style(CursorStyle::Arrow)
2074 .capture_all()
2075 .boxed(),
2076 )
2077 } else {
2078 None
2079 }
2080 }
2081
2082 fn render_notifications(&self, theme: &theme::Workspace) -> Option<ElementBox> {
2083 if self.notifications.is_empty() {
2084 None
2085 } else {
2086 Some(
2087 Flex::column()
2088 .with_children(self.notifications.iter().map(|(_, _, notification)| {
2089 ChildView::new(notification.as_ref())
2090 .contained()
2091 .with_style(theme.notification)
2092 .boxed()
2093 }))
2094 .constrained()
2095 .with_width(theme.notifications.width)
2096 .contained()
2097 .with_style(theme.notifications.container)
2098 .aligned()
2099 .bottom()
2100 .right()
2101 .boxed(),
2102 )
2103 }
2104 }
2105
2106 // RPC handlers
2107
2108 async fn handle_follow(
2109 this: ViewHandle<Self>,
2110 envelope: TypedEnvelope<proto::Follow>,
2111 _: Arc<Client>,
2112 mut cx: AsyncAppContext,
2113 ) -> Result<proto::FollowResponse> {
2114 this.update(&mut cx, |this, cx| {
2115 this.leader_state
2116 .followers
2117 .insert(envelope.original_sender_id()?);
2118
2119 let active_view_id = this
2120 .active_item(cx)
2121 .and_then(|i| i.to_followable_item_handle(cx))
2122 .map(|i| i.id() as u64);
2123 Ok(proto::FollowResponse {
2124 active_view_id,
2125 views: this
2126 .panes()
2127 .iter()
2128 .flat_map(|pane| {
2129 let leader_id = this.leader_for_pane(pane).map(|id| id.0);
2130 pane.read(cx).items().filter_map({
2131 let cx = &cx;
2132 move |item| {
2133 let id = item.id() as u64;
2134 let item = item.to_followable_item_handle(cx)?;
2135 let variant = item.to_state_proto(cx)?;
2136 Some(proto::View {
2137 id,
2138 leader_id,
2139 variant: Some(variant),
2140 })
2141 }
2142 })
2143 })
2144 .collect(),
2145 })
2146 })
2147 }
2148
2149 async fn handle_unfollow(
2150 this: ViewHandle<Self>,
2151 envelope: TypedEnvelope<proto::Unfollow>,
2152 _: Arc<Client>,
2153 mut cx: AsyncAppContext,
2154 ) -> Result<()> {
2155 this.update(&mut cx, |this, _| {
2156 this.leader_state
2157 .followers
2158 .remove(&envelope.original_sender_id()?);
2159 Ok(())
2160 })
2161 }
2162
2163 async fn handle_update_followers(
2164 this: ViewHandle<Self>,
2165 envelope: TypedEnvelope<proto::UpdateFollowers>,
2166 _: Arc<Client>,
2167 mut cx: AsyncAppContext,
2168 ) -> Result<()> {
2169 let leader_id = envelope.original_sender_id()?;
2170 match envelope
2171 .payload
2172 .variant
2173 .ok_or_else(|| anyhow!("invalid update"))?
2174 {
2175 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
2176 this.update(&mut cx, |this, cx| {
2177 this.update_leader_state(leader_id, cx, |state, _| {
2178 state.active_view_id = update_active_view.id;
2179 });
2180 Ok::<_, anyhow::Error>(())
2181 })
2182 }
2183 proto::update_followers::Variant::UpdateView(update_view) => {
2184 this.update(&mut cx, |this, cx| {
2185 let variant = update_view
2186 .variant
2187 .ok_or_else(|| anyhow!("missing update view variant"))?;
2188 this.update_leader_state(leader_id, cx, |state, cx| {
2189 let variant = variant.clone();
2190 match state
2191 .items_by_leader_view_id
2192 .entry(update_view.id)
2193 .or_insert(FollowerItem::Loading(Vec::new()))
2194 {
2195 FollowerItem::Loaded(item) => {
2196 item.apply_update_proto(variant, cx).log_err();
2197 }
2198 FollowerItem::Loading(updates) => updates.push(variant),
2199 }
2200 });
2201 Ok(())
2202 })
2203 }
2204 proto::update_followers::Variant::CreateView(view) => {
2205 let panes = this.read_with(&cx, |this, _| {
2206 this.follower_states_by_leader
2207 .get(&leader_id)
2208 .into_iter()
2209 .flat_map(|states_by_pane| states_by_pane.keys())
2210 .cloned()
2211 .collect()
2212 });
2213 Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], &mut cx)
2214 .await?;
2215 Ok(())
2216 }
2217 }
2218 .log_err();
2219
2220 Ok(())
2221 }
2222
2223 async fn add_views_from_leader(
2224 this: ViewHandle<Self>,
2225 leader_id: PeerId,
2226 panes: Vec<ViewHandle<Pane>>,
2227 views: Vec<proto::View>,
2228 cx: &mut AsyncAppContext,
2229 ) -> Result<()> {
2230 let project = this.read_with(cx, |this, _| this.project.clone());
2231 let replica_id = project
2232 .read_with(cx, |project, _| {
2233 project
2234 .collaborators()
2235 .get(&leader_id)
2236 .map(|c| c.replica_id)
2237 })
2238 .ok_or_else(|| anyhow!("no such collaborator {}", leader_id))?;
2239
2240 let item_builders = cx.update(|cx| {
2241 cx.default_global::<FollowableItemBuilders>()
2242 .values()
2243 .map(|b| b.0)
2244 .collect::<Vec<_>>()
2245 });
2246
2247 let mut item_tasks_by_pane = HashMap::default();
2248 for pane in panes {
2249 let mut item_tasks = Vec::new();
2250 let mut leader_view_ids = Vec::new();
2251 for view in &views {
2252 let mut variant = view.variant.clone();
2253 if variant.is_none() {
2254 Err(anyhow!("missing variant"))?;
2255 }
2256 for build_item in &item_builders {
2257 let task =
2258 cx.update(|cx| build_item(pane.clone(), project.clone(), &mut variant, cx));
2259 if let Some(task) = task {
2260 item_tasks.push(task);
2261 leader_view_ids.push(view.id);
2262 break;
2263 } else {
2264 assert!(variant.is_some());
2265 }
2266 }
2267 }
2268
2269 item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids));
2270 }
2271
2272 for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
2273 let items = futures::future::try_join_all(item_tasks).await?;
2274 this.update(cx, |this, cx| {
2275 let state = this
2276 .follower_states_by_leader
2277 .get_mut(&leader_id)?
2278 .get_mut(&pane)?;
2279
2280 for (id, item) in leader_view_ids.into_iter().zip(items) {
2281 item.set_leader_replica_id(Some(replica_id), cx);
2282 match state.items_by_leader_view_id.entry(id) {
2283 hash_map::Entry::Occupied(e) => {
2284 let e = e.into_mut();
2285 if let FollowerItem::Loading(updates) = e {
2286 for update in updates.drain(..) {
2287 item.apply_update_proto(update, cx)
2288 .context("failed to apply view update")
2289 .log_err();
2290 }
2291 }
2292 *e = FollowerItem::Loaded(item);
2293 }
2294 hash_map::Entry::Vacant(e) => {
2295 e.insert(FollowerItem::Loaded(item));
2296 }
2297 }
2298 }
2299
2300 Some(())
2301 });
2302 }
2303 this.update(cx, |this, cx| this.leader_updated(leader_id, cx));
2304
2305 Ok(())
2306 }
2307
2308 fn update_followers(
2309 &self,
2310 update: proto::update_followers::Variant,
2311 cx: &AppContext,
2312 ) -> Option<()> {
2313 let project_id = self.project.read(cx).remote_id()?;
2314 if !self.leader_state.followers.is_empty() {
2315 self.client
2316 .send(proto::UpdateFollowers {
2317 project_id,
2318 follower_ids: self.leader_state.followers.iter().map(|f| f.0).collect(),
2319 variant: Some(update),
2320 })
2321 .log_err();
2322 }
2323 None
2324 }
2325
2326 pub fn leader_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<PeerId> {
2327 self.follower_states_by_leader
2328 .iter()
2329 .find_map(|(leader_id, state)| {
2330 if state.contains_key(pane) {
2331 Some(*leader_id)
2332 } else {
2333 None
2334 }
2335 })
2336 }
2337
2338 fn update_leader_state(
2339 &mut self,
2340 leader_id: PeerId,
2341 cx: &mut ViewContext<Self>,
2342 mut update_fn: impl FnMut(&mut FollowerState, &mut ViewContext<Self>),
2343 ) {
2344 for (_, state) in self
2345 .follower_states_by_leader
2346 .get_mut(&leader_id)
2347 .into_iter()
2348 .flatten()
2349 {
2350 update_fn(state, cx);
2351 }
2352 self.leader_updated(leader_id, cx);
2353 }
2354
2355 fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
2356 let mut items_to_add = Vec::new();
2357 for (pane, state) in self.follower_states_by_leader.get(&leader_id)? {
2358 if let Some(FollowerItem::Loaded(item)) = state
2359 .active_view_id
2360 .and_then(|id| state.items_by_leader_view_id.get(&id))
2361 {
2362 items_to_add.push((pane.clone(), item.boxed_clone()));
2363 }
2364 }
2365
2366 for (pane, item) in items_to_add {
2367 Pane::add_item(self, &pane, item.boxed_clone(), false, false, None, cx);
2368 if pane == self.active_pane {
2369 pane.update(cx, |pane, cx| pane.focus_active_item(cx));
2370 }
2371 cx.notify();
2372 }
2373 None
2374 }
2375
2376 pub fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
2377 if !active {
2378 for pane in &self.panes {
2379 pane.update(cx, |pane, cx| {
2380 if let Some(item) = pane.active_item() {
2381 item.workspace_deactivated(cx);
2382 }
2383 if matches!(
2384 cx.global::<Settings>().autosave,
2385 Autosave::OnWindowChange | Autosave::OnFocusChange
2386 ) {
2387 for item in pane.items() {
2388 Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
2389 .detach_and_log_err(cx);
2390 }
2391 }
2392 });
2393 }
2394 }
2395 }
2396}
2397
2398impl Entity for Workspace {
2399 type Event = Event;
2400}
2401
2402impl View for Workspace {
2403 fn ui_name() -> &'static str {
2404 "Workspace"
2405 }
2406
2407 fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
2408 let theme = cx.global::<Settings>().theme.clone();
2409 Stack::new()
2410 .with_child(
2411 Flex::column()
2412 .with_child(self.render_titlebar(&theme, cx))
2413 .with_child(
2414 Stack::new()
2415 .with_child({
2416 Flex::row()
2417 .with_children(
2418 if self.left_sidebar.read(cx).active_item().is_some() {
2419 Some(
2420 ChildView::new(&self.left_sidebar)
2421 .flex(0.8, false)
2422 .boxed(),
2423 )
2424 } else {
2425 None
2426 },
2427 )
2428 .with_child(
2429 FlexItem::new(
2430 Flex::column()
2431 .with_child(
2432 FlexItem::new(self.center.render(
2433 &theme,
2434 &self.follower_states_by_leader,
2435 self.project.read(cx).collaborators(),
2436 ))
2437 .flex(1., true)
2438 .boxed(),
2439 )
2440 .with_children(self.dock.render(
2441 &theme,
2442 DockAnchor::Bottom,
2443 cx,
2444 ))
2445 .boxed(),
2446 )
2447 .flex(1., true)
2448 .boxed(),
2449 )
2450 .with_children(self.dock.render(&theme, DockAnchor::Right, cx))
2451 .with_children(
2452 if self.right_sidebar.read(cx).active_item().is_some() {
2453 Some(
2454 ChildView::new(&self.right_sidebar)
2455 .flex(0.8, false)
2456 .boxed(),
2457 )
2458 } else {
2459 None
2460 },
2461 )
2462 .boxed()
2463 })
2464 .with_child(
2465 Overlay::new(
2466 Stack::new()
2467 .with_children(self.dock.render(
2468 &theme,
2469 DockAnchor::Expanded,
2470 cx,
2471 ))
2472 .with_children(self.modal.as_ref().map(|m| {
2473 ChildView::new(m)
2474 .contained()
2475 .with_style(theme.workspace.modal)
2476 .aligned()
2477 .top()
2478 .boxed()
2479 }))
2480 .with_children(self.render_notifications(&theme.workspace))
2481 .boxed(),
2482 )
2483 .boxed(),
2484 )
2485 .flex(1.0, true)
2486 .boxed(),
2487 )
2488 .with_child(ChildView::new(&self.status_bar).boxed())
2489 .contained()
2490 .with_background_color(theme.workspace.background)
2491 .boxed(),
2492 )
2493 .with_children(DragAndDrop::render(cx))
2494 .with_children(self.render_disconnected_overlay(cx))
2495 .named("workspace")
2496 }
2497
2498 fn on_focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
2499 if cx.is_self_focused() {
2500 cx.focus(&self.active_pane);
2501 }
2502 }
2503
2504 fn keymap_context(&self, _: &AppContext) -> gpui::keymap::Context {
2505 let mut keymap = Self::default_keymap_context();
2506 if self.active_pane() == self.dock_pane() {
2507 keymap.set.insert("Dock".into());
2508 }
2509 keymap
2510 }
2511}
2512
2513pub trait WorkspaceHandle {
2514 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
2515}
2516
2517impl WorkspaceHandle for ViewHandle<Workspace> {
2518 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
2519 self.read(cx)
2520 .worktrees(cx)
2521 .flat_map(|worktree| {
2522 let worktree_id = worktree.read(cx).id();
2523 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
2524 worktree_id,
2525 path: f.path.clone(),
2526 })
2527 })
2528 .collect::<Vec<_>>()
2529 }
2530}
2531
2532impl std::fmt::Debug for OpenPaths {
2533 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2534 f.debug_struct("OpenPaths")
2535 .field("paths", &self.paths)
2536 .finish()
2537 }
2538}
2539
2540fn open(_: &Open, cx: &mut MutableAppContext) {
2541 let mut paths = cx.prompt_for_paths(PathPromptOptions {
2542 files: true,
2543 directories: true,
2544 multiple: true,
2545 });
2546 cx.spawn(|mut cx| async move {
2547 if let Some(paths) = paths.recv().await.flatten() {
2548 cx.update(|cx| cx.dispatch_global_action(OpenPaths { paths }));
2549 }
2550 })
2551 .detach();
2552}
2553
2554pub struct WorkspaceCreated(WeakViewHandle<Workspace>);
2555
2556pub fn activate_workspace_for_project(
2557 cx: &mut MutableAppContext,
2558 predicate: impl Fn(&mut Project, &mut ModelContext<Project>) -> bool,
2559) -> Option<ViewHandle<Workspace>> {
2560 for window_id in cx.window_ids().collect::<Vec<_>>() {
2561 if let Some(workspace_handle) = cx.root_view::<Workspace>(window_id) {
2562 let project = workspace_handle.read(cx).project.clone();
2563 if project.update(cx, &predicate) {
2564 cx.activate_window(window_id);
2565 return Some(workspace_handle);
2566 }
2567 }
2568 }
2569 None
2570}
2571
2572#[allow(clippy::type_complexity)]
2573pub fn open_paths(
2574 abs_paths: &[PathBuf],
2575 app_state: &Arc<AppState>,
2576 cx: &mut MutableAppContext,
2577) -> Task<(
2578 ViewHandle<Workspace>,
2579 Vec<Option<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>>>,
2580)> {
2581 log::info!("open paths {:?}", abs_paths);
2582
2583 // Open paths in existing workspace if possible
2584 let existing =
2585 activate_workspace_for_project(cx, |project, cx| project.contains_paths(abs_paths, cx));
2586
2587 let app_state = app_state.clone();
2588 let abs_paths = abs_paths.to_vec();
2589 cx.spawn(|mut cx| async move {
2590 let mut new_project = None;
2591 let workspace = if let Some(existing) = existing {
2592 existing
2593 } else {
2594 let contains_directory =
2595 futures::future::join_all(abs_paths.iter().map(|path| app_state.fs.is_file(path)))
2596 .await
2597 .contains(&false);
2598
2599 cx.add_window((app_state.build_window_options)(), |cx| {
2600 let project = Project::local(
2601 app_state.client.clone(),
2602 app_state.user_store.clone(),
2603 app_state.project_store.clone(),
2604 app_state.languages.clone(),
2605 app_state.fs.clone(),
2606 cx,
2607 );
2608 new_project = Some(project.clone());
2609 let mut workspace = Workspace::new(project, app_state.default_item_factory, cx);
2610 (app_state.initialize_workspace)(&mut workspace, &app_state, cx);
2611 if contains_directory {
2612 workspace.toggle_sidebar(SidebarSide::Left, cx);
2613 }
2614 workspace
2615 })
2616 .1
2617 };
2618
2619 let items = workspace
2620 .update(&mut cx, |workspace, cx| {
2621 workspace.open_paths(abs_paths, true, cx)
2622 })
2623 .await;
2624
2625 (workspace, items)
2626 })
2627}
2628
2629fn open_new(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
2630 let (window_id, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
2631 let mut workspace = Workspace::new(
2632 Project::local(
2633 app_state.client.clone(),
2634 app_state.user_store.clone(),
2635 app_state.project_store.clone(),
2636 app_state.languages.clone(),
2637 app_state.fs.clone(),
2638 cx,
2639 ),
2640 app_state.default_item_factory,
2641 cx,
2642 );
2643 (app_state.initialize_workspace)(&mut workspace, app_state, cx);
2644 workspace
2645 });
2646 cx.dispatch_action_at(window_id, workspace.id(), NewFile);
2647}
2648
2649#[cfg(test)]
2650mod tests {
2651 use std::cell::Cell;
2652
2653 use crate::sidebar::SidebarItem;
2654
2655 use super::*;
2656 use gpui::{executor::Deterministic, ModelHandle, TestAppContext, ViewContext};
2657 use project::{FakeFs, Project, ProjectEntryId};
2658 use serde_json::json;
2659
2660 pub fn default_item_factory(
2661 _workspace: &mut Workspace,
2662 _cx: &mut ViewContext<Workspace>,
2663 ) -> Box<dyn ItemHandle> {
2664 unimplemented!();
2665 }
2666
2667 #[gpui::test]
2668 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
2669 cx.foreground().forbid_parking();
2670 Settings::test_async(cx);
2671
2672 let fs = FakeFs::new(cx.background());
2673 let project = Project::test(fs, [], cx).await;
2674 let (_, workspace) =
2675 cx.add_window(|cx| Workspace::new(project.clone(), default_item_factory, cx));
2676
2677 // Adding an item with no ambiguity renders the tab without detail.
2678 let item1 = cx.add_view(&workspace, |_| {
2679 let mut item = TestItem::new();
2680 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
2681 item
2682 });
2683 workspace.update(cx, |workspace, cx| {
2684 workspace.add_item(Box::new(item1.clone()), cx);
2685 });
2686 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), None));
2687
2688 // Adding an item that creates ambiguity increases the level of detail on
2689 // both tabs.
2690 let item2 = cx.add_view(&workspace, |_| {
2691 let mut item = TestItem::new();
2692 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
2693 item
2694 });
2695 workspace.update(cx, |workspace, cx| {
2696 workspace.add_item(Box::new(item2.clone()), cx);
2697 });
2698 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
2699 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
2700
2701 // Adding an item that creates ambiguity increases the level of detail only
2702 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
2703 // we stop at the highest detail available.
2704 let item3 = cx.add_view(&workspace, |_| {
2705 let mut item = TestItem::new();
2706 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
2707 item
2708 });
2709 workspace.update(cx, |workspace, cx| {
2710 workspace.add_item(Box::new(item3.clone()), cx);
2711 });
2712 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
2713 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
2714 item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
2715 }
2716
2717 #[gpui::test]
2718 async fn test_tracking_active_path(cx: &mut TestAppContext) {
2719 cx.foreground().forbid_parking();
2720 Settings::test_async(cx);
2721 let fs = FakeFs::new(cx.background());
2722 fs.insert_tree(
2723 "/root1",
2724 json!({
2725 "one.txt": "",
2726 "two.txt": "",
2727 }),
2728 )
2729 .await;
2730 fs.insert_tree(
2731 "/root2",
2732 json!({
2733 "three.txt": "",
2734 }),
2735 )
2736 .await;
2737
2738 let project = Project::test(fs, ["root1".as_ref()], cx).await;
2739 let (window_id, workspace) =
2740 cx.add_window(|cx| Workspace::new(project.clone(), default_item_factory, cx));
2741 let worktree_id = project.read_with(cx, |project, cx| {
2742 project.worktrees(cx).next().unwrap().read(cx).id()
2743 });
2744
2745 let item1 = cx.add_view(&workspace, |_| {
2746 let mut item = TestItem::new();
2747 item.project_path = Some((worktree_id, "one.txt").into());
2748 item
2749 });
2750 let item2 = cx.add_view(&workspace, |_| {
2751 let mut item = TestItem::new();
2752 item.project_path = Some((worktree_id, "two.txt").into());
2753 item
2754 });
2755
2756 // Add an item to an empty pane
2757 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
2758 project.read_with(cx, |project, cx| {
2759 assert_eq!(
2760 project.active_entry(),
2761 project
2762 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
2763 .map(|e| e.id)
2764 );
2765 });
2766 assert_eq!(
2767 cx.current_window_title(window_id).as_deref(),
2768 Some("one.txt — root1")
2769 );
2770
2771 // Add a second item to a non-empty pane
2772 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
2773 assert_eq!(
2774 cx.current_window_title(window_id).as_deref(),
2775 Some("two.txt — root1")
2776 );
2777 project.read_with(cx, |project, cx| {
2778 assert_eq!(
2779 project.active_entry(),
2780 project
2781 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
2782 .map(|e| e.id)
2783 );
2784 });
2785
2786 // Close the active item
2787 workspace
2788 .update(cx, |workspace, cx| {
2789 Pane::close_active_item(workspace, &Default::default(), cx).unwrap()
2790 })
2791 .await
2792 .unwrap();
2793 assert_eq!(
2794 cx.current_window_title(window_id).as_deref(),
2795 Some("one.txt — root1")
2796 );
2797 project.read_with(cx, |project, cx| {
2798 assert_eq!(
2799 project.active_entry(),
2800 project
2801 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
2802 .map(|e| e.id)
2803 );
2804 });
2805
2806 // Add a project folder
2807 project
2808 .update(cx, |project, cx| {
2809 project.find_or_create_local_worktree("/root2", true, cx)
2810 })
2811 .await
2812 .unwrap();
2813 assert_eq!(
2814 cx.current_window_title(window_id).as_deref(),
2815 Some("one.txt — root1, root2")
2816 );
2817
2818 // Remove a project folder
2819 project.update(cx, |project, cx| {
2820 project.remove_worktree(worktree_id, cx);
2821 });
2822 assert_eq!(
2823 cx.current_window_title(window_id).as_deref(),
2824 Some("one.txt — root2")
2825 );
2826 }
2827
2828 #[gpui::test]
2829 async fn test_close_window(cx: &mut TestAppContext) {
2830 cx.foreground().forbid_parking();
2831 Settings::test_async(cx);
2832 let fs = FakeFs::new(cx.background());
2833 fs.insert_tree("/root", json!({ "one": "" })).await;
2834
2835 let project = Project::test(fs, ["root".as_ref()], cx).await;
2836 let (window_id, workspace) =
2837 cx.add_window(|cx| Workspace::new(project.clone(), default_item_factory, cx));
2838
2839 // When there are no dirty items, there's nothing to do.
2840 let item1 = cx.add_view(&workspace, |_| TestItem::new());
2841 workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
2842 let task = workspace.update(cx, |w, cx| w.prepare_to_close(cx));
2843 assert!(task.await.unwrap());
2844
2845 // When there are dirty untitled items, prompt to save each one. If the user
2846 // cancels any prompt, then abort.
2847 let item2 = cx.add_view(&workspace, |_| {
2848 let mut item = TestItem::new();
2849 item.is_dirty = true;
2850 item
2851 });
2852 let item3 = cx.add_view(&workspace, |_| {
2853 let mut item = TestItem::new();
2854 item.is_dirty = true;
2855 item.project_entry_ids = vec![ProjectEntryId::from_proto(1)];
2856 item
2857 });
2858 workspace.update(cx, |w, cx| {
2859 w.add_item(Box::new(item2.clone()), cx);
2860 w.add_item(Box::new(item3.clone()), cx);
2861 });
2862 let task = workspace.update(cx, |w, cx| w.prepare_to_close(cx));
2863 cx.foreground().run_until_parked();
2864 cx.simulate_prompt_answer(window_id, 2 /* cancel */);
2865 cx.foreground().run_until_parked();
2866 assert!(!cx.has_pending_prompt(window_id));
2867 assert!(!task.await.unwrap());
2868 }
2869
2870 #[gpui::test]
2871 async fn test_close_pane_items(cx: &mut TestAppContext) {
2872 cx.foreground().forbid_parking();
2873 Settings::test_async(cx);
2874 let fs = FakeFs::new(cx.background());
2875
2876 let project = Project::test(fs, None, cx).await;
2877 let (window_id, workspace) =
2878 cx.add_window(|cx| Workspace::new(project, default_item_factory, cx));
2879
2880 let item1 = cx.add_view(&workspace, |_| {
2881 let mut item = TestItem::new();
2882 item.is_dirty = true;
2883 item.project_entry_ids = vec![ProjectEntryId::from_proto(1)];
2884 item
2885 });
2886 let item2 = cx.add_view(&workspace, |_| {
2887 let mut item = TestItem::new();
2888 item.is_dirty = true;
2889 item.has_conflict = true;
2890 item.project_entry_ids = vec![ProjectEntryId::from_proto(2)];
2891 item
2892 });
2893 let item3 = cx.add_view(&workspace, |_| {
2894 let mut item = TestItem::new();
2895 item.is_dirty = true;
2896 item.has_conflict = true;
2897 item.project_entry_ids = vec![ProjectEntryId::from_proto(3)];
2898 item
2899 });
2900 let item4 = cx.add_view(&workspace, |_| {
2901 let mut item = TestItem::new();
2902 item.is_dirty = true;
2903 item
2904 });
2905 let pane = workspace.update(cx, |workspace, cx| {
2906 workspace.add_item(Box::new(item1.clone()), cx);
2907 workspace.add_item(Box::new(item2.clone()), cx);
2908 workspace.add_item(Box::new(item3.clone()), cx);
2909 workspace.add_item(Box::new(item4.clone()), cx);
2910 workspace.active_pane().clone()
2911 });
2912
2913 let close_items = workspace.update(cx, |workspace, cx| {
2914 pane.update(cx, |pane, cx| {
2915 pane.activate_item(1, true, true, cx);
2916 assert_eq!(pane.active_item().unwrap().id(), item2.id());
2917 });
2918
2919 let item1_id = item1.id();
2920 let item3_id = item3.id();
2921 let item4_id = item4.id();
2922 Pane::close_items(workspace, pane.clone(), cx, move |id| {
2923 [item1_id, item3_id, item4_id].contains(&id)
2924 })
2925 });
2926
2927 cx.foreground().run_until_parked();
2928 pane.read_with(cx, |pane, _| {
2929 assert_eq!(pane.items().count(), 4);
2930 assert_eq!(pane.active_item().unwrap().id(), item1.id());
2931 });
2932
2933 cx.simulate_prompt_answer(window_id, 0);
2934 cx.foreground().run_until_parked();
2935 pane.read_with(cx, |pane, cx| {
2936 assert_eq!(item1.read(cx).save_count, 1);
2937 assert_eq!(item1.read(cx).save_as_count, 0);
2938 assert_eq!(item1.read(cx).reload_count, 0);
2939 assert_eq!(pane.items().count(), 3);
2940 assert_eq!(pane.active_item().unwrap().id(), item3.id());
2941 });
2942
2943 cx.simulate_prompt_answer(window_id, 1);
2944 cx.foreground().run_until_parked();
2945 pane.read_with(cx, |pane, cx| {
2946 assert_eq!(item3.read(cx).save_count, 0);
2947 assert_eq!(item3.read(cx).save_as_count, 0);
2948 assert_eq!(item3.read(cx).reload_count, 1);
2949 assert_eq!(pane.items().count(), 2);
2950 assert_eq!(pane.active_item().unwrap().id(), item4.id());
2951 });
2952
2953 cx.simulate_prompt_answer(window_id, 0);
2954 cx.foreground().run_until_parked();
2955 cx.simulate_new_path_selection(|_| Some(Default::default()));
2956 close_items.await.unwrap();
2957 pane.read_with(cx, |pane, cx| {
2958 assert_eq!(item4.read(cx).save_count, 0);
2959 assert_eq!(item4.read(cx).save_as_count, 1);
2960 assert_eq!(item4.read(cx).reload_count, 0);
2961 assert_eq!(pane.items().count(), 1);
2962 assert_eq!(pane.active_item().unwrap().id(), item2.id());
2963 });
2964 }
2965
2966 #[gpui::test]
2967 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
2968 cx.foreground().forbid_parking();
2969 Settings::test_async(cx);
2970 let fs = FakeFs::new(cx.background());
2971
2972 let project = Project::test(fs, [], cx).await;
2973 let (window_id, workspace) =
2974 cx.add_window(|cx| Workspace::new(project, default_item_factory, cx));
2975
2976 // Create several workspace items with single project entries, and two
2977 // workspace items with multiple project entries.
2978 let single_entry_items = (0..=4)
2979 .map(|project_entry_id| {
2980 let mut item = TestItem::new();
2981 item.is_dirty = true;
2982 item.project_entry_ids = vec![ProjectEntryId::from_proto(project_entry_id)];
2983 item.is_singleton = true;
2984 item
2985 })
2986 .collect::<Vec<_>>();
2987 let item_2_3 = {
2988 let mut item = TestItem::new();
2989 item.is_dirty = true;
2990 item.is_singleton = false;
2991 item.project_entry_ids =
2992 vec![ProjectEntryId::from_proto(2), ProjectEntryId::from_proto(3)];
2993 item
2994 };
2995 let item_3_4 = {
2996 let mut item = TestItem::new();
2997 item.is_dirty = true;
2998 item.is_singleton = false;
2999 item.project_entry_ids =
3000 vec![ProjectEntryId::from_proto(3), ProjectEntryId::from_proto(4)];
3001 item
3002 };
3003
3004 // Create two panes that contain the following project entries:
3005 // left pane:
3006 // multi-entry items: (2, 3)
3007 // single-entry items: 0, 1, 2, 3, 4
3008 // right pane:
3009 // single-entry items: 1
3010 // multi-entry items: (3, 4)
3011 let left_pane = workspace.update(cx, |workspace, cx| {
3012 let left_pane = workspace.active_pane().clone();
3013 workspace.add_item(Box::new(cx.add_view(|_| item_2_3.clone())), cx);
3014 for item in &single_entry_items {
3015 workspace.add_item(Box::new(cx.add_view(|_| item.clone())), cx);
3016 }
3017 left_pane.update(cx, |pane, cx| {
3018 pane.activate_item(2, true, true, cx);
3019 });
3020
3021 workspace
3022 .split_pane(left_pane.clone(), SplitDirection::Right, cx)
3023 .unwrap();
3024
3025 left_pane
3026 });
3027
3028 //Need to cause an effect flush in order to respect new focus
3029 workspace.update(cx, |workspace, cx| {
3030 workspace.add_item(Box::new(cx.add_view(|_| item_3_4.clone())), cx);
3031 cx.focus(left_pane.clone());
3032 });
3033
3034 // When closing all of the items in the left pane, we should be prompted twice:
3035 // once for project entry 0, and once for project entry 2. After those two
3036 // prompts, the task should complete.
3037
3038 let close = workspace.update(cx, |workspace, cx| {
3039 Pane::close_items(workspace, left_pane.clone(), cx, |_| true)
3040 });
3041
3042 cx.foreground().run_until_parked();
3043 left_pane.read_with(cx, |pane, cx| {
3044 assert_eq!(
3045 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
3046 &[ProjectEntryId::from_proto(0)]
3047 );
3048 });
3049 cx.simulate_prompt_answer(window_id, 0);
3050
3051 cx.foreground().run_until_parked();
3052 left_pane.read_with(cx, |pane, cx| {
3053 assert_eq!(
3054 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
3055 &[ProjectEntryId::from_proto(2)]
3056 );
3057 });
3058 cx.simulate_prompt_answer(window_id, 0);
3059
3060 cx.foreground().run_until_parked();
3061 close.await.unwrap();
3062 left_pane.read_with(cx, |pane, _| {
3063 assert_eq!(pane.items().count(), 0);
3064 });
3065 }
3066
3067 #[gpui::test]
3068 async fn test_autosave(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
3069 deterministic.forbid_parking();
3070
3071 Settings::test_async(cx);
3072 let fs = FakeFs::new(cx.background());
3073
3074 let project = Project::test(fs, [], cx).await;
3075 let (window_id, workspace) =
3076 cx.add_window(|cx| Workspace::new(project, default_item_factory, cx));
3077
3078 let item = cx.add_view(&workspace, |_| {
3079 let mut item = TestItem::new();
3080 item.project_entry_ids = vec![ProjectEntryId::from_proto(1)];
3081 item
3082 });
3083 let item_id = item.id();
3084 workspace.update(cx, |workspace, cx| {
3085 workspace.add_item(Box::new(item.clone()), cx);
3086 });
3087
3088 // Autosave on window change.
3089 item.update(cx, |item, cx| {
3090 cx.update_global(|settings: &mut Settings, _| {
3091 settings.autosave = Autosave::OnWindowChange;
3092 });
3093 item.is_dirty = true;
3094 });
3095
3096 // Deactivating the window saves the file.
3097 cx.simulate_window_activation(None);
3098 deterministic.run_until_parked();
3099 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
3100
3101 // Autosave on focus change.
3102 item.update(cx, |item, cx| {
3103 cx.focus_self();
3104 cx.update_global(|settings: &mut Settings, _| {
3105 settings.autosave = Autosave::OnFocusChange;
3106 });
3107 item.is_dirty = true;
3108 });
3109
3110 // Blurring the item saves the file.
3111 item.update(cx, |_, cx| cx.blur());
3112 deterministic.run_until_parked();
3113 item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
3114
3115 // Deactivating the window still saves the file.
3116 cx.simulate_window_activation(Some(window_id));
3117 item.update(cx, |item, cx| {
3118 cx.focus_self();
3119 item.is_dirty = true;
3120 });
3121 cx.simulate_window_activation(None);
3122
3123 deterministic.run_until_parked();
3124 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
3125
3126 // Autosave after delay.
3127 item.update(cx, |item, cx| {
3128 cx.update_global(|settings: &mut Settings, _| {
3129 settings.autosave = Autosave::AfterDelay { milliseconds: 500 };
3130 });
3131 item.is_dirty = true;
3132 cx.emit(TestItemEvent::Edit);
3133 });
3134
3135 // Delay hasn't fully expired, so the file is still dirty and unsaved.
3136 deterministic.advance_clock(Duration::from_millis(250));
3137 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
3138
3139 // After delay expires, the file is saved.
3140 deterministic.advance_clock(Duration::from_millis(250));
3141 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
3142
3143 // Autosave on focus change, ensuring closing the tab counts as such.
3144 item.update(cx, |item, cx| {
3145 cx.update_global(|settings: &mut Settings, _| {
3146 settings.autosave = Autosave::OnFocusChange;
3147 });
3148 item.is_dirty = true;
3149 });
3150
3151 workspace
3152 .update(cx, |workspace, cx| {
3153 let pane = workspace.active_pane().clone();
3154 Pane::close_items(workspace, pane, cx, move |id| id == item_id)
3155 })
3156 .await
3157 .unwrap();
3158 assert!(!cx.has_pending_prompt(window_id));
3159 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3160
3161 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
3162 workspace.update(cx, |workspace, cx| {
3163 workspace.add_item(Box::new(item.clone()), cx);
3164 });
3165 item.update(cx, |item, cx| {
3166 item.project_entry_ids = Default::default();
3167 item.is_dirty = true;
3168 cx.blur();
3169 });
3170 deterministic.run_until_parked();
3171 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3172
3173 // Ensure autosave is prevented for deleted files also when closing the buffer.
3174 let _close_items = workspace.update(cx, |workspace, cx| {
3175 let pane = workspace.active_pane().clone();
3176 Pane::close_items(workspace, pane, cx, move |id| id == item_id)
3177 });
3178 deterministic.run_until_parked();
3179 assert!(cx.has_pending_prompt(window_id));
3180 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3181 }
3182
3183 #[gpui::test]
3184 async fn test_pane_navigation(
3185 deterministic: Arc<Deterministic>,
3186 cx: &mut gpui::TestAppContext,
3187 ) {
3188 deterministic.forbid_parking();
3189 Settings::test_async(cx);
3190 let fs = FakeFs::new(cx.background());
3191
3192 let project = Project::test(fs, [], cx).await;
3193 let (_, workspace) = cx.add_window(|cx| Workspace::new(project, default_item_factory, cx));
3194
3195 let item = cx.add_view(&workspace, |_| {
3196 let mut item = TestItem::new();
3197 item.project_entry_ids = vec![ProjectEntryId::from_proto(1)];
3198 item
3199 });
3200 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3201 let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
3202 let toolbar_notify_count = Rc::new(RefCell::new(0));
3203
3204 workspace.update(cx, |workspace, cx| {
3205 workspace.add_item(Box::new(item.clone()), cx);
3206 let toolbar_notification_count = toolbar_notify_count.clone();
3207 cx.observe(&toolbar, move |_, _, _| {
3208 *toolbar_notification_count.borrow_mut() += 1
3209 })
3210 .detach();
3211 });
3212
3213 pane.read_with(cx, |pane, _| {
3214 assert!(!pane.can_navigate_backward());
3215 assert!(!pane.can_navigate_forward());
3216 });
3217
3218 item.update(cx, |item, cx| {
3219 item.set_state("one".to_string(), cx);
3220 });
3221
3222 // Toolbar must be notified to re-render the navigation buttons
3223 assert_eq!(*toolbar_notify_count.borrow(), 1);
3224
3225 pane.read_with(cx, |pane, _| {
3226 assert!(pane.can_navigate_backward());
3227 assert!(!pane.can_navigate_forward());
3228 });
3229
3230 workspace
3231 .update(cx, |workspace, cx| {
3232 Pane::go_back(workspace, Some(pane.clone()), cx)
3233 })
3234 .await;
3235
3236 assert_eq!(*toolbar_notify_count.borrow(), 3);
3237 pane.read_with(cx, |pane, _| {
3238 assert!(!pane.can_navigate_backward());
3239 assert!(pane.can_navigate_forward());
3240 });
3241 }
3242
3243 pub struct TestItem {
3244 state: String,
3245 pub label: String,
3246 save_count: usize,
3247 save_as_count: usize,
3248 reload_count: usize,
3249 is_dirty: bool,
3250 is_singleton: bool,
3251 has_conflict: bool,
3252 project_entry_ids: Vec<ProjectEntryId>,
3253 project_path: Option<ProjectPath>,
3254 nav_history: Option<ItemNavHistory>,
3255 tab_descriptions: Option<Vec<&'static str>>,
3256 tab_detail: Cell<Option<usize>>,
3257 }
3258
3259 pub enum TestItemEvent {
3260 Edit,
3261 }
3262
3263 impl Clone for TestItem {
3264 fn clone(&self) -> Self {
3265 Self {
3266 state: self.state.clone(),
3267 label: self.label.clone(),
3268 save_count: self.save_count,
3269 save_as_count: self.save_as_count,
3270 reload_count: self.reload_count,
3271 is_dirty: self.is_dirty,
3272 is_singleton: self.is_singleton,
3273 has_conflict: self.has_conflict,
3274 project_entry_ids: self.project_entry_ids.clone(),
3275 project_path: self.project_path.clone(),
3276 nav_history: None,
3277 tab_descriptions: None,
3278 tab_detail: Default::default(),
3279 }
3280 }
3281 }
3282
3283 impl TestItem {
3284 pub fn new() -> Self {
3285 Self {
3286 state: String::new(),
3287 label: String::new(),
3288 save_count: 0,
3289 save_as_count: 0,
3290 reload_count: 0,
3291 is_dirty: false,
3292 has_conflict: false,
3293 project_entry_ids: Vec::new(),
3294 project_path: None,
3295 is_singleton: true,
3296 nav_history: None,
3297 tab_descriptions: None,
3298 tab_detail: Default::default(),
3299 }
3300 }
3301
3302 pub fn with_label(mut self, state: &str) -> Self {
3303 self.label = state.to_string();
3304 self
3305 }
3306
3307 pub fn with_singleton(mut self, singleton: bool) -> Self {
3308 self.is_singleton = singleton;
3309 self
3310 }
3311
3312 pub fn with_project_entry_ids(mut self, project_entry_ids: &[u64]) -> Self {
3313 self.project_entry_ids.extend(
3314 project_entry_ids
3315 .iter()
3316 .copied()
3317 .map(ProjectEntryId::from_proto),
3318 );
3319 self
3320 }
3321
3322 fn set_state(&mut self, state: String, cx: &mut ViewContext<Self>) {
3323 self.push_to_nav_history(cx);
3324 self.state = state;
3325 }
3326
3327 fn push_to_nav_history(&mut self, cx: &mut ViewContext<Self>) {
3328 if let Some(history) = &mut self.nav_history {
3329 history.push(Some(Box::new(self.state.clone())), cx);
3330 }
3331 }
3332 }
3333
3334 impl Entity for TestItem {
3335 type Event = TestItemEvent;
3336 }
3337
3338 impl View for TestItem {
3339 fn ui_name() -> &'static str {
3340 "TestItem"
3341 }
3342
3343 fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
3344 Empty::new().boxed()
3345 }
3346 }
3347
3348 impl Item for TestItem {
3349 fn tab_description<'a>(&'a self, detail: usize, _: &'a AppContext) -> Option<Cow<'a, str>> {
3350 self.tab_descriptions.as_ref().and_then(|descriptions| {
3351 let description = *descriptions.get(detail).or_else(|| descriptions.last())?;
3352 Some(description.into())
3353 })
3354 }
3355
3356 fn tab_content(&self, detail: Option<usize>, _: &theme::Tab, _: &AppContext) -> ElementBox {
3357 self.tab_detail.set(detail);
3358 Empty::new().boxed()
3359 }
3360
3361 fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
3362 self.project_path.clone()
3363 }
3364
3365 fn project_entry_ids(&self, _: &AppContext) -> SmallVec<[ProjectEntryId; 3]> {
3366 self.project_entry_ids.iter().copied().collect()
3367 }
3368
3369 fn is_singleton(&self, _: &AppContext) -> bool {
3370 self.is_singleton
3371 }
3372
3373 fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext<Self>) {
3374 self.nav_history = Some(history);
3375 }
3376
3377 fn navigate(&mut self, state: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
3378 let state = *state.downcast::<String>().unwrap_or_default();
3379 if state != self.state {
3380 self.state = state;
3381 true
3382 } else {
3383 false
3384 }
3385 }
3386
3387 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
3388 self.push_to_nav_history(cx);
3389 }
3390
3391 fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
3392 where
3393 Self: Sized,
3394 {
3395 Some(self.clone())
3396 }
3397
3398 fn is_dirty(&self, _: &AppContext) -> bool {
3399 self.is_dirty
3400 }
3401
3402 fn has_conflict(&self, _: &AppContext) -> bool {
3403 self.has_conflict
3404 }
3405
3406 fn can_save(&self, _: &AppContext) -> bool {
3407 !self.project_entry_ids.is_empty()
3408 }
3409
3410 fn save(
3411 &mut self,
3412 _: ModelHandle<Project>,
3413 _: &mut ViewContext<Self>,
3414 ) -> Task<anyhow::Result<()>> {
3415 self.save_count += 1;
3416 self.is_dirty = false;
3417 Task::ready(Ok(()))
3418 }
3419
3420 fn save_as(
3421 &mut self,
3422 _: ModelHandle<Project>,
3423 _: std::path::PathBuf,
3424 _: &mut ViewContext<Self>,
3425 ) -> Task<anyhow::Result<()>> {
3426 self.save_as_count += 1;
3427 self.is_dirty = false;
3428 Task::ready(Ok(()))
3429 }
3430
3431 fn reload(
3432 &mut self,
3433 _: ModelHandle<Project>,
3434 _: &mut ViewContext<Self>,
3435 ) -> Task<anyhow::Result<()>> {
3436 self.reload_count += 1;
3437 self.is_dirty = false;
3438 Task::ready(Ok(()))
3439 }
3440
3441 fn to_item_events(_: &Self::Event) -> Vec<ItemEvent> {
3442 vec![ItemEvent::UpdateTab, ItemEvent::Edit]
3443 }
3444 }
3445
3446 impl SidebarItem for TestItem {}
3447}