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