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