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