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