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) -> &Arc<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 new_pane = self.add_pane(cx);
1593 if let Some(clone) = item.clone_on_split(self.database_id(), cx.as_mut()) {
1594 Pane::add_item(self, &new_pane, clone, true, true, None, cx);
1595 }
1596 self.center.split(&pane, &new_pane, direction).unwrap();
1597 cx.notify();
1598 Some(new_pane)
1599 }
1600
1601 pub fn split_pane_with_item(&mut self, action: &SplitWithItem, cx: &mut ViewContext<Self>) {
1602 let Some(pane_to_split) = action.pane_to_split.upgrade(cx) else { return; };
1603 let Some(from) = action.from.upgrade(cx) else { return; };
1604 if &pane_to_split == self.dock_pane() {
1605 warn!("Can't split dock pane.");
1606 return;
1607 }
1608
1609 let new_pane = self.add_pane(cx);
1610 Pane::move_item(
1611 self,
1612 from.clone(),
1613 new_pane.clone(),
1614 action.item_id_to_move,
1615 0,
1616 cx,
1617 );
1618 self.center
1619 .split(&pane_to_split, &new_pane, action.split_direction)
1620 .unwrap();
1621 cx.notify();
1622 }
1623
1624 pub fn split_pane_with_project_entry(
1625 &mut self,
1626 action: &SplitWithProjectEntry,
1627 cx: &mut ViewContext<Self>,
1628 ) -> Option<Task<Result<()>>> {
1629 let pane_to_split = action.pane_to_split.upgrade(cx)?;
1630 if &pane_to_split == self.dock_pane() {
1631 warn!("Can't split dock pane.");
1632 return None;
1633 }
1634
1635 let new_pane = self.add_pane(cx);
1636 self.center
1637 .split(&pane_to_split, &new_pane, action.split_direction)
1638 .unwrap();
1639
1640 let path = self
1641 .project
1642 .read(cx)
1643 .path_for_entry(action.project_entry, cx)?;
1644 let task = self.open_path(path, Some(new_pane.downgrade()), true, cx);
1645 Some(cx.foreground().spawn(async move {
1646 task.await?;
1647 Ok(())
1648 }))
1649 }
1650
1651 fn remove_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
1652 if self.center.remove(&pane).unwrap() {
1653 self.panes.retain(|p| p != &pane);
1654 cx.focus(self.panes.last().unwrap().clone());
1655 self.unfollow(&pane, cx);
1656 self.last_leaders_by_pane.remove(&pane.downgrade());
1657 for removed_item in pane.read(cx).items() {
1658 self.panes_by_item.remove(&removed_item.id());
1659 }
1660 if self.last_active_center_pane == Some(pane.downgrade()) {
1661 self.last_active_center_pane = None;
1662 }
1663
1664 cx.notify();
1665 } else {
1666 self.active_item_path_changed(cx);
1667 }
1668 }
1669
1670 pub fn panes(&self) -> &[ViewHandle<Pane>] {
1671 &self.panes
1672 }
1673
1674 fn pane(&self, pane_id: usize) -> Option<ViewHandle<Pane>> {
1675 self.panes.iter().find(|pane| pane.id() == pane_id).cloned()
1676 }
1677
1678 pub fn active_pane(&self) -> &ViewHandle<Pane> {
1679 &self.active_pane
1680 }
1681
1682 pub fn dock_pane(&self) -> &ViewHandle<Pane> {
1683 self.dock.pane()
1684 }
1685
1686 fn dock_active(&self) -> bool {
1687 &self.active_pane == self.dock.pane()
1688 }
1689
1690 fn project_remote_id_changed(&mut self, remote_id: Option<u64>, cx: &mut ViewContext<Self>) {
1691 if let Some(remote_id) = remote_id {
1692 self.remote_entity_subscription =
1693 Some(self.client.add_view_for_remote_entity(remote_id, cx));
1694 } else {
1695 self.remote_entity_subscription.take();
1696 }
1697 }
1698
1699 fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
1700 self.leader_state.followers.remove(&peer_id);
1701 if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) {
1702 for state in states_by_pane.into_values() {
1703 for item in state.items_by_leader_view_id.into_values() {
1704 item.set_leader_replica_id(None, cx);
1705 }
1706 }
1707 }
1708 cx.notify();
1709 }
1710
1711 pub fn toggle_follow(
1712 &mut self,
1713 ToggleFollow(leader_id): &ToggleFollow,
1714 cx: &mut ViewContext<Self>,
1715 ) -> Option<Task<Result<()>>> {
1716 let leader_id = *leader_id;
1717 let pane = self.active_pane().clone();
1718
1719 if let Some(prev_leader_id) = self.unfollow(&pane, cx) {
1720 if leader_id == prev_leader_id {
1721 return None;
1722 }
1723 }
1724
1725 self.last_leaders_by_pane
1726 .insert(pane.downgrade(), leader_id);
1727 self.follower_states_by_leader
1728 .entry(leader_id)
1729 .or_default()
1730 .insert(pane.clone(), Default::default());
1731 cx.notify();
1732
1733 let project_id = self.project.read(cx).remote_id()?;
1734 let request = self.client.request(proto::Follow {
1735 project_id,
1736 leader_id: Some(leader_id),
1737 });
1738
1739 Some(cx.spawn_weak(|this, mut cx| async move {
1740 let response = request.await?;
1741 if let Some(this) = this.upgrade(&cx) {
1742 this.update(&mut cx, |this, _| {
1743 let state = this
1744 .follower_states_by_leader
1745 .get_mut(&leader_id)
1746 .and_then(|states_by_pane| states_by_pane.get_mut(&pane))
1747 .ok_or_else(|| anyhow!("following interrupted"))?;
1748 state.active_view_id = if let Some(active_view_id) = response.active_view_id {
1749 Some(ViewId::from_proto(active_view_id)?)
1750 } else {
1751 None
1752 };
1753 Ok::<_, anyhow::Error>(())
1754 })?;
1755 Self::add_views_from_leader(
1756 this.clone(),
1757 leader_id,
1758 vec![pane],
1759 response.views,
1760 &mut cx,
1761 )
1762 .await?;
1763 this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx));
1764 }
1765 Ok(())
1766 }))
1767 }
1768
1769 pub fn follow_next_collaborator(
1770 &mut self,
1771 _: &FollowNextCollaborator,
1772 cx: &mut ViewContext<Self>,
1773 ) -> Option<Task<Result<()>>> {
1774 let collaborators = self.project.read(cx).collaborators();
1775 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
1776 let mut collaborators = collaborators.keys().copied();
1777 for peer_id in collaborators.by_ref() {
1778 if peer_id == leader_id {
1779 break;
1780 }
1781 }
1782 collaborators.next()
1783 } else if let Some(last_leader_id) =
1784 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
1785 {
1786 if collaborators.contains_key(last_leader_id) {
1787 Some(*last_leader_id)
1788 } else {
1789 None
1790 }
1791 } else {
1792 None
1793 };
1794
1795 next_leader_id
1796 .or_else(|| collaborators.keys().copied().next())
1797 .and_then(|leader_id| self.toggle_follow(&ToggleFollow(leader_id), cx))
1798 }
1799
1800 pub fn unfollow(
1801 &mut self,
1802 pane: &ViewHandle<Pane>,
1803 cx: &mut ViewContext<Self>,
1804 ) -> Option<PeerId> {
1805 for (leader_id, states_by_pane) in &mut self.follower_states_by_leader {
1806 let leader_id = *leader_id;
1807 if let Some(state) = states_by_pane.remove(pane) {
1808 for (_, item) in state.items_by_leader_view_id {
1809 item.set_leader_replica_id(None, cx);
1810 }
1811
1812 if states_by_pane.is_empty() {
1813 self.follower_states_by_leader.remove(&leader_id);
1814 if let Some(project_id) = self.project.read(cx).remote_id() {
1815 self.client
1816 .send(proto::Unfollow {
1817 project_id,
1818 leader_id: Some(leader_id),
1819 })
1820 .log_err();
1821 }
1822 }
1823
1824 cx.notify();
1825 return Some(leader_id);
1826 }
1827 }
1828 None
1829 }
1830
1831 pub fn is_following(&self, peer_id: PeerId) -> bool {
1832 self.follower_states_by_leader.contains_key(&peer_id)
1833 }
1834
1835 pub fn is_followed(&self, peer_id: PeerId) -> bool {
1836 self.leader_state.followers.contains(&peer_id)
1837 }
1838
1839 fn render_titlebar(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
1840 // TODO: There should be a better system in place for this
1841 // (https://github.com/zed-industries/zed/issues/1290)
1842 let is_fullscreen = cx.window_is_fullscreen(cx.window_id());
1843 let container_theme = if is_fullscreen {
1844 let mut container_theme = theme.workspace.titlebar.container;
1845 container_theme.padding.left = container_theme.padding.right;
1846 container_theme
1847 } else {
1848 theme.workspace.titlebar.container
1849 };
1850
1851 enum TitleBar {}
1852 ConstrainedBox::new(
1853 MouseEventHandler::<TitleBar>::new(0, cx, |_, cx| {
1854 Container::new(
1855 Stack::new()
1856 .with_children(
1857 self.titlebar_item
1858 .as_ref()
1859 .map(|item| ChildView::new(item, cx).boxed()),
1860 )
1861 .boxed(),
1862 )
1863 .with_style(container_theme)
1864 .boxed()
1865 })
1866 .on_click(MouseButton::Left, |event, cx| {
1867 if event.click_count == 2 {
1868 cx.zoom_window(cx.window_id());
1869 }
1870 })
1871 .boxed(),
1872 )
1873 .with_height(theme.workspace.titlebar.height)
1874 .named("titlebar")
1875 }
1876
1877 fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
1878 let active_entry = self.active_project_path(cx);
1879 self.project
1880 .update(cx, |project, cx| project.set_active_path(active_entry, cx));
1881 self.update_window_title(cx);
1882 }
1883
1884 fn update_window_title(&mut self, cx: &mut ViewContext<Self>) {
1885 let project = self.project().read(cx);
1886 let mut title = String::new();
1887
1888 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
1889 let filename = path
1890 .path
1891 .file_name()
1892 .map(|s| s.to_string_lossy())
1893 .or_else(|| {
1894 Some(Cow::Borrowed(
1895 project
1896 .worktree_for_id(path.worktree_id, cx)?
1897 .read(cx)
1898 .root_name(),
1899 ))
1900 });
1901
1902 if let Some(filename) = filename {
1903 title.push_str(filename.as_ref());
1904 title.push_str(" — ");
1905 }
1906 }
1907
1908 for (i, name) in project.worktree_root_names(cx).enumerate() {
1909 if i > 0 {
1910 title.push_str(", ");
1911 }
1912 title.push_str(name);
1913 }
1914
1915 if title.is_empty() {
1916 title = "empty project".to_string();
1917 }
1918
1919 if project.is_remote() {
1920 title.push_str(" ↙");
1921 } else if project.is_shared() {
1922 title.push_str(" ↗");
1923 }
1924
1925 cx.set_window_title(&title);
1926 }
1927
1928 fn update_window_edited(&mut self, cx: &mut ViewContext<Self>) {
1929 let is_edited = !self.project.read(cx).is_read_only()
1930 && self
1931 .items(cx)
1932 .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
1933 if is_edited != self.window_edited {
1934 self.window_edited = is_edited;
1935 cx.set_window_edited(self.window_edited)
1936 }
1937 }
1938
1939 fn render_disconnected_overlay(&self, cx: &mut RenderContext<Workspace>) -> Option<ElementBox> {
1940 if self.project.read(cx).is_read_only() {
1941 enum DisconnectedOverlay {}
1942 Some(
1943 MouseEventHandler::<DisconnectedOverlay>::new(0, cx, |_, cx| {
1944 let theme = &cx.global::<Settings>().theme;
1945 Label::new(
1946 "Your connection to the remote project has been lost.",
1947 theme.workspace.disconnected_overlay.text.clone(),
1948 )
1949 .aligned()
1950 .contained()
1951 .with_style(theme.workspace.disconnected_overlay.container)
1952 .boxed()
1953 })
1954 .with_cursor_style(CursorStyle::Arrow)
1955 .capture_all()
1956 .boxed(),
1957 )
1958 } else {
1959 None
1960 }
1961 }
1962
1963 fn render_notifications(
1964 &self,
1965 theme: &theme::Workspace,
1966 cx: &AppContext,
1967 ) -> Option<ElementBox> {
1968 if self.notifications.is_empty() {
1969 None
1970 } else {
1971 Some(
1972 Flex::column()
1973 .with_children(self.notifications.iter().map(|(_, _, notification)| {
1974 ChildView::new(notification.as_ref(), cx)
1975 .contained()
1976 .with_style(theme.notification)
1977 .boxed()
1978 }))
1979 .constrained()
1980 .with_width(theme.notifications.width)
1981 .contained()
1982 .with_style(theme.notifications.container)
1983 .aligned()
1984 .bottom()
1985 .right()
1986 .boxed(),
1987 )
1988 }
1989 }
1990
1991 // RPC handlers
1992
1993 async fn handle_follow(
1994 this: ViewHandle<Self>,
1995 envelope: TypedEnvelope<proto::Follow>,
1996 _: Arc<Client>,
1997 mut cx: AsyncAppContext,
1998 ) -> Result<proto::FollowResponse> {
1999 this.update(&mut cx, |this, cx| {
2000 let client = &this.client;
2001 this.leader_state
2002 .followers
2003 .insert(envelope.original_sender_id()?);
2004
2005 let active_view_id = this.active_item(cx).and_then(|i| {
2006 Some(
2007 i.to_followable_item_handle(cx)?
2008 .remote_id(client, cx)?
2009 .to_proto(),
2010 )
2011 });
2012
2013 cx.notify();
2014
2015 Ok(proto::FollowResponse {
2016 active_view_id,
2017 views: this
2018 .panes()
2019 .iter()
2020 .flat_map(|pane| {
2021 let leader_id = this.leader_for_pane(pane);
2022 pane.read(cx).items().filter_map({
2023 let cx = &cx;
2024 move |item| {
2025 let item = item.to_followable_item_handle(cx)?;
2026 let id = item.remote_id(client, cx)?.to_proto();
2027 let variant = item.to_state_proto(cx)?;
2028 Some(proto::View {
2029 id: Some(id),
2030 leader_id,
2031 variant: Some(variant),
2032 })
2033 }
2034 })
2035 })
2036 .collect(),
2037 })
2038 })
2039 }
2040
2041 async fn handle_unfollow(
2042 this: ViewHandle<Self>,
2043 envelope: TypedEnvelope<proto::Unfollow>,
2044 _: Arc<Client>,
2045 mut cx: AsyncAppContext,
2046 ) -> Result<()> {
2047 this.update(&mut cx, |this, cx| {
2048 this.leader_state
2049 .followers
2050 .remove(&envelope.original_sender_id()?);
2051 cx.notify();
2052 Ok(())
2053 })
2054 }
2055
2056 async fn handle_update_followers(
2057 this: ViewHandle<Self>,
2058 envelope: TypedEnvelope<proto::UpdateFollowers>,
2059 _: Arc<Client>,
2060 cx: AsyncAppContext,
2061 ) -> Result<()> {
2062 let leader_id = envelope.original_sender_id()?;
2063 this.read_with(&cx, |this, _| {
2064 this.leader_updates_tx
2065 .unbounded_send((leader_id, envelope.payload))
2066 })?;
2067 Ok(())
2068 }
2069
2070 async fn process_leader_update(
2071 this: ViewHandle<Self>,
2072 leader_id: PeerId,
2073 update: proto::UpdateFollowers,
2074 cx: &mut AsyncAppContext,
2075 ) -> Result<()> {
2076 match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
2077 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
2078 this.update(cx, |this, _| {
2079 if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
2080 for state in state.values_mut() {
2081 state.active_view_id =
2082 if let Some(active_view_id) = update_active_view.id.clone() {
2083 Some(ViewId::from_proto(active_view_id)?)
2084 } else {
2085 None
2086 };
2087 }
2088 }
2089 anyhow::Ok(())
2090 })?;
2091 }
2092 proto::update_followers::Variant::UpdateView(update_view) => {
2093 let variant = update_view
2094 .variant
2095 .ok_or_else(|| anyhow!("missing update view variant"))?;
2096 let id = update_view
2097 .id
2098 .ok_or_else(|| anyhow!("missing update view id"))?;
2099 let mut tasks = Vec::new();
2100 this.update(cx, |this, cx| {
2101 let project = this.project.clone();
2102 if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
2103 for state in state.values_mut() {
2104 let view_id = ViewId::from_proto(id.clone())?;
2105 if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
2106 tasks.push(item.apply_update_proto(&project, variant.clone(), cx));
2107 }
2108 }
2109 }
2110 anyhow::Ok(())
2111 })?;
2112 try_join_all(tasks).await.log_err();
2113 }
2114 proto::update_followers::Variant::CreateView(view) => {
2115 let panes = this.read_with(cx, |this, _| {
2116 this.follower_states_by_leader
2117 .get(&leader_id)
2118 .into_iter()
2119 .flat_map(|states_by_pane| states_by_pane.keys())
2120 .cloned()
2121 .collect()
2122 });
2123 Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], cx).await?;
2124 }
2125 }
2126 this.update(cx, |this, cx| this.leader_updated(leader_id, cx));
2127 Ok(())
2128 }
2129
2130 async fn add_views_from_leader(
2131 this: ViewHandle<Self>,
2132 leader_id: PeerId,
2133 panes: Vec<ViewHandle<Pane>>,
2134 views: Vec<proto::View>,
2135 cx: &mut AsyncAppContext,
2136 ) -> Result<()> {
2137 let project = this.read_with(cx, |this, _| this.project.clone());
2138 let replica_id = project
2139 .read_with(cx, |project, _| {
2140 project
2141 .collaborators()
2142 .get(&leader_id)
2143 .map(|c| c.replica_id)
2144 })
2145 .ok_or_else(|| anyhow!("no such collaborator {}", leader_id))?;
2146
2147 let item_builders = cx.update(|cx| {
2148 cx.default_global::<FollowableItemBuilders>()
2149 .values()
2150 .map(|b| b.0)
2151 .collect::<Vec<_>>()
2152 });
2153
2154 let mut item_tasks_by_pane = HashMap::default();
2155 for pane in panes {
2156 let mut item_tasks = Vec::new();
2157 let mut leader_view_ids = Vec::new();
2158 for view in &views {
2159 let Some(id) = &view.id else { continue };
2160 let id = ViewId::from_proto(id.clone())?;
2161 let mut variant = view.variant.clone();
2162 if variant.is_none() {
2163 Err(anyhow!("missing variant"))?;
2164 }
2165 for build_item in &item_builders {
2166 let task = cx.update(|cx| {
2167 build_item(pane.clone(), project.clone(), id, &mut variant, cx)
2168 });
2169 if let Some(task) = task {
2170 item_tasks.push(task);
2171 leader_view_ids.push(id);
2172 break;
2173 } else {
2174 assert!(variant.is_some());
2175 }
2176 }
2177 }
2178
2179 item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids));
2180 }
2181
2182 for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
2183 let items = futures::future::try_join_all(item_tasks).await?;
2184 this.update(cx, |this, cx| {
2185 let state = this
2186 .follower_states_by_leader
2187 .get_mut(&leader_id)?
2188 .get_mut(&pane)?;
2189
2190 for (id, item) in leader_view_ids.into_iter().zip(items) {
2191 item.set_leader_replica_id(Some(replica_id), cx);
2192 state.items_by_leader_view_id.insert(id, item);
2193 }
2194
2195 Some(())
2196 });
2197 }
2198 Ok(())
2199 }
2200
2201 fn update_followers(
2202 &self,
2203 update: proto::update_followers::Variant,
2204 cx: &AppContext,
2205 ) -> Option<()> {
2206 let project_id = self.project.read(cx).remote_id()?;
2207 if !self.leader_state.followers.is_empty() {
2208 self.client
2209 .send(proto::UpdateFollowers {
2210 project_id,
2211 follower_ids: self.leader_state.followers.iter().copied().collect(),
2212 variant: Some(update),
2213 })
2214 .log_err();
2215 }
2216 None
2217 }
2218
2219 pub fn leader_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<PeerId> {
2220 self.follower_states_by_leader
2221 .iter()
2222 .find_map(|(leader_id, state)| {
2223 if state.contains_key(pane) {
2224 Some(*leader_id)
2225 } else {
2226 None
2227 }
2228 })
2229 }
2230
2231 fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
2232 cx.notify();
2233
2234 let call = self.active_call()?;
2235 let room = call.read(cx).room()?.read(cx);
2236 let participant = room.remote_participant_for_peer_id(leader_id)?;
2237 let mut items_to_activate = Vec::new();
2238 match participant.location {
2239 call::ParticipantLocation::SharedProject { project_id } => {
2240 if Some(project_id) == self.project.read(cx).remote_id() {
2241 for (pane, state) in self.follower_states_by_leader.get(&leader_id)? {
2242 if let Some(item) = state
2243 .active_view_id
2244 .and_then(|id| state.items_by_leader_view_id.get(&id))
2245 {
2246 items_to_activate.push((pane.clone(), item.boxed_clone()));
2247 } else {
2248 if let Some(shared_screen) =
2249 self.shared_screen_for_peer(leader_id, pane, cx)
2250 {
2251 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
2252 }
2253 }
2254 }
2255 }
2256 }
2257 call::ParticipantLocation::UnsharedProject => {}
2258 call::ParticipantLocation::External => {
2259 for (pane, _) in self.follower_states_by_leader.get(&leader_id)? {
2260 if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
2261 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
2262 }
2263 }
2264 }
2265 }
2266
2267 for (pane, item) in items_to_activate {
2268 let active_item_was_focused = pane
2269 .read(cx)
2270 .active_item()
2271 .map(|active_item| cx.is_child_focused(active_item.to_any()))
2272 .unwrap_or_default();
2273
2274 if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) {
2275 pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx));
2276 } else {
2277 Pane::add_item(self, &pane, item.boxed_clone(), false, false, None, cx);
2278 }
2279
2280 if active_item_was_focused {
2281 pane.update(cx, |pane, cx| pane.focus_active_item(cx));
2282 }
2283 }
2284
2285 None
2286 }
2287
2288 fn shared_screen_for_peer(
2289 &self,
2290 peer_id: PeerId,
2291 pane: &ViewHandle<Pane>,
2292 cx: &mut ViewContext<Self>,
2293 ) -> Option<ViewHandle<SharedScreen>> {
2294 let call = self.active_call()?;
2295 let room = call.read(cx).room()?.read(cx);
2296 let participant = room.remote_participant_for_peer_id(peer_id)?;
2297 let track = participant.tracks.values().next()?.clone();
2298 let user = participant.user.clone();
2299
2300 for item in pane.read(cx).items_of_type::<SharedScreen>() {
2301 if item.read(cx).peer_id == peer_id {
2302 return Some(item);
2303 }
2304 }
2305
2306 Some(cx.add_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
2307 }
2308
2309 pub fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
2310 if active {
2311 cx.background()
2312 .spawn(persistence::DB.update_timestamp(self.database_id()))
2313 .detach();
2314 } else {
2315 for pane in &self.panes {
2316 pane.update(cx, |pane, cx| {
2317 if let Some(item) = pane.active_item() {
2318 item.workspace_deactivated(cx);
2319 }
2320 if matches!(
2321 cx.global::<Settings>().autosave,
2322 Autosave::OnWindowChange | Autosave::OnFocusChange
2323 ) {
2324 for item in pane.items() {
2325 Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
2326 .detach_and_log_err(cx);
2327 }
2328 }
2329 });
2330 }
2331 }
2332 }
2333
2334 fn active_call(&self) -> Option<&ModelHandle<ActiveCall>> {
2335 self.active_call.as_ref().map(|(call, _)| call)
2336 }
2337
2338 fn on_active_call_event(
2339 &mut self,
2340 _: ModelHandle<ActiveCall>,
2341 event: &call::room::Event,
2342 cx: &mut ViewContext<Self>,
2343 ) {
2344 match event {
2345 call::room::Event::ParticipantLocationChanged { participant_id }
2346 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
2347 self.leader_updated(*participant_id, cx);
2348 }
2349 _ => {}
2350 }
2351 }
2352
2353 pub fn database_id(&self) -> WorkspaceId {
2354 self.database_id
2355 }
2356
2357 fn location(&self, cx: &AppContext) -> Option<WorkspaceLocation> {
2358 let project = self.project().read(cx);
2359
2360 if project.is_local() {
2361 Some(
2362 project
2363 .visible_worktrees(cx)
2364 .map(|worktree| worktree.read(cx).abs_path())
2365 .collect::<Vec<_>>()
2366 .into(),
2367 )
2368 } else {
2369 None
2370 }
2371 }
2372
2373 fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
2374 match member {
2375 Member::Axis(PaneAxis { members, .. }) => {
2376 for child in members.iter() {
2377 self.remove_panes(child.clone(), cx)
2378 }
2379 }
2380 Member::Pane(pane) => self.remove_pane(pane.clone(), cx),
2381 }
2382 }
2383
2384 fn serialize_workspace(&self, cx: &AppContext) {
2385 fn serialize_pane_handle(
2386 pane_handle: &ViewHandle<Pane>,
2387 cx: &AppContext,
2388 ) -> SerializedPane {
2389 let (items, active) = {
2390 let pane = pane_handle.read(cx);
2391 let active_item_id = pane.active_item().map(|item| item.id());
2392 (
2393 pane.items()
2394 .filter_map(|item_handle| {
2395 Some(SerializedItem {
2396 kind: Arc::from(item_handle.serialized_item_kind()?),
2397 item_id: item_handle.id(),
2398 active: Some(item_handle.id()) == active_item_id,
2399 })
2400 })
2401 .collect::<Vec<_>>(),
2402 pane.is_active(),
2403 )
2404 };
2405
2406 SerializedPane::new(items, active)
2407 }
2408
2409 fn build_serialized_pane_group(
2410 pane_group: &Member,
2411 cx: &AppContext,
2412 ) -> SerializedPaneGroup {
2413 match pane_group {
2414 Member::Axis(PaneAxis { axis, members }) => SerializedPaneGroup::Group {
2415 axis: *axis,
2416 children: members
2417 .iter()
2418 .map(|member| build_serialized_pane_group(member, cx))
2419 .collect::<Vec<_>>(),
2420 },
2421 Member::Pane(pane_handle) => {
2422 SerializedPaneGroup::Pane(serialize_pane_handle(&pane_handle, cx))
2423 }
2424 }
2425 }
2426
2427 if let Some(location) = self.location(cx) {
2428 // Load bearing special case:
2429 // - with_local_workspace() relies on this to not have other stuff open
2430 // when you open your log
2431 if !location.paths().is_empty() {
2432 let dock_pane = serialize_pane_handle(self.dock.pane(), cx);
2433 let center_group = build_serialized_pane_group(&self.center.root, cx);
2434
2435 let serialized_workspace = SerializedWorkspace {
2436 id: self.database_id,
2437 location,
2438 dock_position: self.dock.position(),
2439 dock_pane,
2440 center_group,
2441 left_sidebar_open: self.left_sidebar.read(cx).is_open(),
2442 bounds: Default::default(),
2443 display: Default::default(),
2444 };
2445
2446 cx.background()
2447 .spawn(persistence::DB.save_workspace(serialized_workspace))
2448 .detach();
2449 }
2450 }
2451 }
2452
2453 fn load_from_serialized_workspace(
2454 workspace: WeakViewHandle<Workspace>,
2455 serialized_workspace: SerializedWorkspace,
2456 cx: &mut MutableAppContext,
2457 ) {
2458 cx.spawn(|mut cx| async move {
2459 if let Some(workspace) = workspace.upgrade(&cx) {
2460 let (project, dock_pane_handle, old_center_pane) =
2461 workspace.read_with(&cx, |workspace, _| {
2462 (
2463 workspace.project().clone(),
2464 workspace.dock_pane().clone(),
2465 workspace.last_active_center_pane.clone(),
2466 )
2467 });
2468
2469 serialized_workspace
2470 .dock_pane
2471 .deserialize_to(
2472 &project,
2473 &dock_pane_handle,
2474 serialized_workspace.id,
2475 &workspace,
2476 &mut cx,
2477 )
2478 .await;
2479
2480 // Traverse the splits tree and add to things
2481 let center_group = serialized_workspace
2482 .center_group
2483 .deserialize(&project, serialized_workspace.id, &workspace, &mut cx)
2484 .await;
2485
2486 // Remove old panes from workspace panes list
2487 workspace.update(&mut cx, |workspace, cx| {
2488 if let Some((center_group, active_pane)) = center_group {
2489 workspace.remove_panes(workspace.center.root.clone(), cx);
2490
2491 // Swap workspace center group
2492 workspace.center = PaneGroup::with_root(center_group);
2493
2494 // Change the focus to the workspace first so that we retrigger focus in on the pane.
2495 cx.focus_self();
2496
2497 if let Some(active_pane) = active_pane {
2498 cx.focus(active_pane);
2499 } else {
2500 cx.focus(workspace.panes.last().unwrap().clone());
2501 }
2502 } else {
2503 let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade(cx));
2504 if let Some(old_center_handle) = old_center_handle {
2505 cx.focus(old_center_handle)
2506 } else {
2507 cx.focus_self()
2508 }
2509 }
2510
2511 if workspace.left_sidebar().read(cx).is_open()
2512 != serialized_workspace.left_sidebar_open
2513 {
2514 workspace.toggle_sidebar(SidebarSide::Left, cx);
2515 }
2516
2517 // Note that without after_window, the focus_self() and
2518 // the focus the dock generates start generating alternating
2519 // focus due to the deferred execution each triggering each other
2520 cx.after_window_update(move |workspace, cx| {
2521 Dock::set_dock_position(workspace, serialized_workspace.dock_position, cx);
2522 });
2523
2524 cx.notify();
2525 });
2526
2527 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
2528 workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))
2529 }
2530 })
2531 .detach();
2532 }
2533}
2534
2535fn notify_if_database_failed(workspace: &ViewHandle<Workspace>, cx: &mut AsyncAppContext) {
2536 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
2537 workspace.update(cx, |workspace, cx| {
2538 workspace.show_notification_once(0, cx, |cx| {
2539 cx.add_view(|_| {
2540 MessageNotification::new(
2541 indoc::indoc! {"
2542 Failed to load any database file :(
2543 "},
2544 OsOpen("https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml".to_string()),
2545 "Click to let us know about this error"
2546 )
2547 })
2548 });
2549 });
2550 } else {
2551 let backup_path = (*db::BACKUP_DB_PATH).read();
2552 if let Some(backup_path) = &*backup_path {
2553 workspace.update(cx, |workspace, cx| {
2554 workspace.show_notification_once(0, cx, |cx| {
2555 cx.add_view(|_| {
2556 let backup_path = backup_path.to_string_lossy();
2557 MessageNotification::new(
2558 format!(
2559 indoc::indoc! {"
2560 Database file was corrupted :(
2561 Old database backed up to:
2562 {}
2563 "},
2564 backup_path
2565 ),
2566 OsOpen(backup_path.to_string()),
2567 "Click to show old database in finder",
2568 )
2569 })
2570 });
2571 });
2572 }
2573 }
2574}
2575
2576impl Entity for Workspace {
2577 type Event = Event;
2578}
2579
2580impl View for Workspace {
2581 fn ui_name() -> &'static str {
2582 "Workspace"
2583 }
2584
2585 fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
2586 let theme = cx.global::<Settings>().theme.clone();
2587 Stack::new()
2588 .with_child(
2589 Flex::column()
2590 .with_child(self.render_titlebar(&theme, cx))
2591 .with_child(
2592 Stack::new()
2593 .with_child({
2594 let project = self.project.clone();
2595 Flex::row()
2596 .with_children(
2597 if self.left_sidebar.read(cx).active_item().is_some() {
2598 Some(
2599 ChildView::new(&self.left_sidebar, cx)
2600 .constrained()
2601 .dynamically(|constraint, cx| {
2602 SizeConstraint::new(
2603 Vector2F::new(20., constraint.min.y()),
2604 Vector2F::new(
2605 cx.window_size.x() * 0.8,
2606 constraint.max.y(),
2607 ),
2608 )
2609 })
2610 .boxed(),
2611 )
2612 } else {
2613 None
2614 },
2615 )
2616 .with_child(
2617 FlexItem::new(
2618 Flex::column()
2619 .with_child(
2620 FlexItem::new(self.center.render(
2621 &project,
2622 &theme,
2623 &self.follower_states_by_leader,
2624 self.active_call(),
2625 self.active_pane(),
2626 cx,
2627 ))
2628 .flex(1., true)
2629 .boxed(),
2630 )
2631 .with_children(self.dock.render(
2632 &theme,
2633 DockAnchor::Bottom,
2634 cx,
2635 ))
2636 .boxed(),
2637 )
2638 .flex(1., true)
2639 .boxed(),
2640 )
2641 .with_children(self.dock.render(&theme, DockAnchor::Right, cx))
2642 .with_children(
2643 if self.right_sidebar.read(cx).active_item().is_some() {
2644 Some(
2645 ChildView::new(&self.right_sidebar, cx)
2646 .constrained()
2647 .dynamically(|constraint, cx| {
2648 SizeConstraint::new(
2649 Vector2F::new(20., constraint.min.y()),
2650 Vector2F::new(
2651 cx.window_size.x() * 0.8,
2652 constraint.max.y(),
2653 ),
2654 )
2655 })
2656 .boxed(),
2657 )
2658 } else {
2659 None
2660 },
2661 )
2662 .boxed()
2663 })
2664 .with_child(
2665 Overlay::new(
2666 Stack::new()
2667 .with_children(self.dock.render(
2668 &theme,
2669 DockAnchor::Expanded,
2670 cx,
2671 ))
2672 .with_children(self.modal.as_ref().map(|modal| {
2673 ChildView::new(modal, cx)
2674 .contained()
2675 .with_style(theme.workspace.modal)
2676 .aligned()
2677 .top()
2678 .boxed()
2679 }))
2680 .with_children(
2681 self.render_notifications(&theme.workspace, cx),
2682 )
2683 .boxed(),
2684 )
2685 .boxed(),
2686 )
2687 .flex(1.0, true)
2688 .boxed(),
2689 )
2690 .with_child(ChildView::new(&self.status_bar, cx).boxed())
2691 .contained()
2692 .with_background_color(theme.workspace.background)
2693 .boxed(),
2694 )
2695 .with_children(DragAndDrop::render(cx))
2696 .with_children(self.render_disconnected_overlay(cx))
2697 .named("workspace")
2698 }
2699
2700 fn focus_in(&mut self, view: AnyViewHandle, cx: &mut ViewContext<Self>) {
2701 if cx.is_self_focused() {
2702 cx.focus(&self.active_pane);
2703 } else {
2704 for pane in self.panes() {
2705 let view = view.clone();
2706 if pane.update(cx, |_, cx| view.id() == cx.view_id() || cx.is_child(view)) {
2707 self.handle_pane_focused(pane.clone(), cx);
2708 break;
2709 }
2710 }
2711 }
2712 }
2713
2714 fn keymap_context(&self, _: &AppContext) -> KeymapContext {
2715 let mut keymap = Self::default_keymap_context();
2716 if self.active_pane() == self.dock_pane() {
2717 keymap.set.insert("Dock".into());
2718 }
2719 keymap
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}