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