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