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