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