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