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