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