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