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