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