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 pane_was_focused = pane.read(cx).has_focus();
2343 if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) {
2344 pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx));
2345 } else {
2346 Pane::add_item(self, &pane, item.boxed_clone(), false, false, None, cx);
2347 }
2348
2349 if pane_was_focused {
2350 pane.update(cx, |pane, cx| pane.focus_active_item(cx));
2351 }
2352 }
2353
2354 None
2355 }
2356
2357 fn shared_screen_for_peer(
2358 &self,
2359 peer_id: PeerId,
2360 pane: &ViewHandle<Pane>,
2361 cx: &mut ViewContext<Self>,
2362 ) -> Option<ViewHandle<SharedScreen>> {
2363 let call = self.active_call()?;
2364 let room = call.read(cx).room()?.read(cx);
2365 let participant = room.remote_participant_for_peer_id(peer_id)?;
2366 let track = participant.tracks.values().next()?.clone();
2367 let user = participant.user.clone();
2368
2369 for item in pane.read(cx).items_of_type::<SharedScreen>() {
2370 if item.read(cx).peer_id == peer_id {
2371 return Some(item);
2372 }
2373 }
2374
2375 Some(cx.add_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
2376 }
2377
2378 pub fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
2379 if active {
2380 cx.background()
2381 .spawn(persistence::DB.update_timestamp(self.database_id()))
2382 .detach();
2383 } else {
2384 for pane in &self.panes {
2385 pane.update(cx, |pane, cx| {
2386 if let Some(item) = pane.active_item() {
2387 item.workspace_deactivated(cx);
2388 }
2389 if matches!(
2390 cx.global::<Settings>().autosave,
2391 Autosave::OnWindowChange | Autosave::OnFocusChange
2392 ) {
2393 for item in pane.items() {
2394 Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
2395 .detach_and_log_err(cx);
2396 }
2397 }
2398 });
2399 }
2400 }
2401 }
2402
2403 fn active_call(&self) -> Option<&ModelHandle<ActiveCall>> {
2404 self.active_call.as_ref().map(|(call, _)| call)
2405 }
2406
2407 fn on_active_call_event(
2408 &mut self,
2409 _: ModelHandle<ActiveCall>,
2410 event: &call::room::Event,
2411 cx: &mut ViewContext<Self>,
2412 ) {
2413 match event {
2414 call::room::Event::ParticipantLocationChanged { participant_id }
2415 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
2416 self.leader_updated(*participant_id, cx);
2417 }
2418 _ => {}
2419 }
2420 }
2421
2422 pub fn database_id(&self) -> WorkspaceId {
2423 self.database_id
2424 }
2425
2426 fn location(&self, cx: &AppContext) -> Option<WorkspaceLocation> {
2427 let project = self.project().read(cx);
2428
2429 if project.is_local() {
2430 Some(
2431 project
2432 .visible_worktrees(cx)
2433 .map(|worktree| worktree.read(cx).abs_path())
2434 .collect::<Vec<_>>()
2435 .into(),
2436 )
2437 } else {
2438 None
2439 }
2440 }
2441
2442 fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
2443 match member {
2444 Member::Axis(PaneAxis { members, .. }) => {
2445 for child in members.iter() {
2446 self.remove_panes(child.clone(), cx)
2447 }
2448 }
2449 Member::Pane(pane) => self.remove_pane(pane.clone(), cx),
2450 }
2451 }
2452
2453 fn serialize_workspace(&self, cx: &AppContext) {
2454 fn serialize_pane_handle(
2455 pane_handle: &ViewHandle<Pane>,
2456 cx: &AppContext,
2457 ) -> SerializedPane {
2458 let (items, active) = {
2459 let pane = pane_handle.read(cx);
2460 let active_item_id = pane.active_item().map(|item| item.id());
2461 (
2462 pane.items()
2463 .filter_map(|item_handle| {
2464 Some(SerializedItem {
2465 kind: Arc::from(item_handle.serialized_item_kind()?),
2466 item_id: item_handle.id(),
2467 active: Some(item_handle.id()) == active_item_id,
2468 })
2469 })
2470 .collect::<Vec<_>>(),
2471 pane.is_active(),
2472 )
2473 };
2474
2475 SerializedPane::new(items, active)
2476 }
2477
2478 fn build_serialized_pane_group(
2479 pane_group: &Member,
2480 cx: &AppContext,
2481 ) -> SerializedPaneGroup {
2482 match pane_group {
2483 Member::Axis(PaneAxis { axis, members }) => SerializedPaneGroup::Group {
2484 axis: *axis,
2485 children: members
2486 .iter()
2487 .map(|member| build_serialized_pane_group(member, cx))
2488 .collect::<Vec<_>>(),
2489 },
2490 Member::Pane(pane_handle) => {
2491 SerializedPaneGroup::Pane(serialize_pane_handle(&pane_handle, cx))
2492 }
2493 }
2494 }
2495
2496 if let Some(location) = self.location(cx) {
2497 // Load bearing special case:
2498 // - with_local_workspace() relies on this to not have other stuff open
2499 // when you open your log
2500 if !location.paths().is_empty() {
2501 let dock_pane = serialize_pane_handle(self.dock.pane(), cx);
2502 let center_group = build_serialized_pane_group(&self.center.root, cx);
2503
2504 let serialized_workspace = SerializedWorkspace {
2505 id: self.database_id,
2506 location,
2507 dock_position: self.dock.position(),
2508 dock_pane,
2509 center_group,
2510 left_sidebar_open: self.left_sidebar.read(cx).is_open(),
2511 bounds: Default::default(),
2512 display: Default::default(),
2513 };
2514
2515 cx.background()
2516 .spawn(persistence::DB.save_workspace(serialized_workspace))
2517 .detach();
2518 }
2519 }
2520 }
2521
2522 fn load_from_serialized_workspace(
2523 workspace: WeakViewHandle<Workspace>,
2524 serialized_workspace: SerializedWorkspace,
2525 cx: &mut AppContext,
2526 ) {
2527 cx.spawn(|mut cx| async move {
2528 let (project, dock_pane_handle, old_center_pane) =
2529 workspace.read_with(&cx, |workspace, _| {
2530 (
2531 workspace.project().clone(),
2532 workspace.dock_pane().downgrade(),
2533 workspace.last_active_center_pane.clone(),
2534 )
2535 })?;
2536
2537 serialized_workspace
2538 .dock_pane
2539 .deserialize_to(
2540 &project,
2541 &dock_pane_handle,
2542 serialized_workspace.id,
2543 &workspace,
2544 &mut cx,
2545 )
2546 .await?;
2547
2548 // Traverse the splits tree and add to things
2549 let center_group = serialized_workspace
2550 .center_group
2551 .deserialize(&project, serialized_workspace.id, &workspace, &mut cx)
2552 .await;
2553
2554 // Remove old panes from workspace panes list
2555 workspace.update(&mut cx, |workspace, cx| {
2556 if let Some((center_group, active_pane)) = center_group {
2557 workspace.remove_panes(workspace.center.root.clone(), cx);
2558
2559 // Swap workspace center group
2560 workspace.center = PaneGroup::with_root(center_group);
2561
2562 // Change the focus to the workspace first so that we retrigger focus in on the pane.
2563 cx.focus_self();
2564
2565 if let Some(active_pane) = active_pane {
2566 cx.focus(&active_pane);
2567 } else {
2568 cx.focus(workspace.panes.last().unwrap());
2569 }
2570 } else {
2571 let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade(cx));
2572 if let Some(old_center_handle) = old_center_handle {
2573 cx.focus(&old_center_handle)
2574 } else {
2575 cx.focus_self()
2576 }
2577 }
2578
2579 if workspace.left_sidebar().read(cx).is_open()
2580 != serialized_workspace.left_sidebar_open
2581 {
2582 workspace.toggle_sidebar(SidebarSide::Left, cx);
2583 }
2584
2585 // Note that without after_window, the focus_self() and
2586 // the focus the dock generates start generating alternating
2587 // focus due to the deferred execution each triggering each other
2588 cx.after_window_update(move |workspace, cx| {
2589 Dock::set_dock_position(
2590 workspace,
2591 serialized_workspace.dock_position,
2592 true,
2593 cx,
2594 );
2595 });
2596
2597 cx.notify();
2598 })?;
2599
2600 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
2601 workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))?;
2602 anyhow::Ok(())
2603 })
2604 .detach_and_log_err(cx);
2605 }
2606
2607 #[cfg(any(test, feature = "test-support"))]
2608 pub fn test_new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
2609 let app_state = Arc::new(AppState {
2610 languages: project.read(cx).languages().clone(),
2611 themes: ThemeRegistry::new((), cx.font_cache().clone()),
2612 client: project.read(cx).client(),
2613 user_store: project.read(cx).user_store(),
2614 fs: project.read(cx).fs().clone(),
2615 build_window_options: |_, _, _| Default::default(),
2616 initialize_workspace: |_, _, _| {},
2617 dock_default_item_factory: |_, _| None,
2618 background_actions: || &[],
2619 });
2620 Self::new(None, 0, project, app_state, cx)
2621 }
2622}
2623
2624fn notify_if_database_failed(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
2625 const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
2626
2627 workspace
2628 .update(cx, |workspace, cx| {
2629 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
2630 workspace.show_notification_once(0, cx, |cx| {
2631 cx.add_view(|_| {
2632 MessageNotification::new("Failed to load any database file.")
2633 .with_click_message("Click to let us know about this error")
2634 .on_click(|cx| cx.platform().open_url(REPORT_ISSUE_URL))
2635 })
2636 });
2637 } else {
2638 let backup_path = (*db::BACKUP_DB_PATH).read();
2639 if let Some(backup_path) = backup_path.clone() {
2640 workspace.show_notification_once(0, cx, move |cx| {
2641 cx.add_view(move |_| {
2642 MessageNotification::new(format!(
2643 "Database file was corrupted. Old database backed up to {}",
2644 backup_path.display()
2645 ))
2646 .with_click_message("Click to show old database in finder")
2647 .on_click(move |cx| {
2648 cx.platform().open_url(&backup_path.to_string_lossy())
2649 })
2650 })
2651 });
2652 }
2653 }
2654 })
2655 .log_err();
2656}
2657
2658impl Entity for Workspace {
2659 type Event = Event;
2660}
2661
2662impl View for Workspace {
2663 fn ui_name() -> &'static str {
2664 "Workspace"
2665 }
2666
2667 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
2668 let theme = cx.global::<Settings>().theme.clone();
2669 Stack::new()
2670 .with_child(
2671 Flex::column()
2672 .with_child(self.render_titlebar(&theme, cx))
2673 .with_child(
2674 Stack::new()
2675 .with_child({
2676 let project = self.project.clone();
2677 Flex::row()
2678 .with_children(
2679 if self.left_sidebar.read(cx).active_item().is_some() {
2680 Some(
2681 ChildView::new(&self.left_sidebar, cx)
2682 .constrained()
2683 .dynamically(|constraint, _, cx| {
2684 SizeConstraint::new(
2685 Vector2F::new(20., constraint.min.y()),
2686 Vector2F::new(
2687 cx.window_size().x() * 0.8,
2688 constraint.max.y(),
2689 ),
2690 )
2691 }),
2692 )
2693 } else {
2694 None
2695 },
2696 )
2697 .with_child(
2698 FlexItem::new(
2699 Flex::column()
2700 .with_child(
2701 FlexItem::new(self.center.render(
2702 &project,
2703 &theme,
2704 &self.follower_states_by_leader,
2705 self.active_call(),
2706 self.active_pane(),
2707 &self.app_state,
2708 cx,
2709 ))
2710 .flex(1., true),
2711 )
2712 .with_children(self.dock.render(
2713 &theme,
2714 DockAnchor::Bottom,
2715 cx,
2716 )),
2717 )
2718 .flex(1., true),
2719 )
2720 .with_children(self.dock.render(&theme, DockAnchor::Right, cx))
2721 .with_children(
2722 if self.right_sidebar.read(cx).active_item().is_some() {
2723 Some(
2724 ChildView::new(&self.right_sidebar, cx)
2725 .constrained()
2726 .dynamically(|constraint, _, cx| {
2727 SizeConstraint::new(
2728 Vector2F::new(20., constraint.min.y()),
2729 Vector2F::new(
2730 cx.window_size().x() * 0.8,
2731 constraint.max.y(),
2732 ),
2733 )
2734 }),
2735 )
2736 } else {
2737 None
2738 },
2739 )
2740 })
2741 .with_child(Overlay::new(
2742 Stack::new()
2743 .with_children(self.dock.render(
2744 &theme,
2745 DockAnchor::Expanded,
2746 cx,
2747 ))
2748 .with_children(self.modal.as_ref().map(|modal| {
2749 ChildView::new(modal, cx)
2750 .contained()
2751 .with_style(theme.workspace.modal)
2752 .aligned()
2753 .top()
2754 }))
2755 .with_children(self.render_notifications(&theme.workspace, cx)),
2756 ))
2757 .flex(1.0, true),
2758 )
2759 .with_child(ChildView::new(&self.status_bar, cx))
2760 .contained()
2761 .with_background_color(theme.workspace.background),
2762 )
2763 .with_children(DragAndDrop::render(cx))
2764 .with_children(self.render_disconnected_overlay(cx))
2765 .into_any_named("workspace")
2766 }
2767
2768 fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
2769 if cx.is_self_focused() {
2770 cx.focus(&self.active_pane);
2771 }
2772 }
2773
2774 fn keymap_context(&self, _: &AppContext) -> KeymapContext {
2775 Self::default_keymap_context()
2776 }
2777}
2778
2779impl ViewId {
2780 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
2781 Ok(Self {
2782 creator: message
2783 .creator
2784 .ok_or_else(|| anyhow!("creator is missing"))?,
2785 id: message.id,
2786 })
2787 }
2788
2789 pub(crate) fn to_proto(&self) -> proto::ViewId {
2790 proto::ViewId {
2791 creator: Some(self.creator),
2792 id: self.id,
2793 }
2794 }
2795}
2796
2797pub trait WorkspaceHandle {
2798 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
2799}
2800
2801impl WorkspaceHandle for ViewHandle<Workspace> {
2802 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
2803 self.read(cx)
2804 .worktrees(cx)
2805 .flat_map(|worktree| {
2806 let worktree_id = worktree.read(cx).id();
2807 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
2808 worktree_id,
2809 path: f.path.clone(),
2810 })
2811 })
2812 .collect::<Vec<_>>()
2813 }
2814}
2815
2816impl std::fmt::Debug for OpenPaths {
2817 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2818 f.debug_struct("OpenPaths")
2819 .field("paths", &self.paths)
2820 .finish()
2821 }
2822}
2823
2824pub struct WorkspaceCreated(WeakViewHandle<Workspace>);
2825
2826pub fn activate_workspace_for_project(
2827 cx: &mut AsyncAppContext,
2828 predicate: impl Fn(&mut Project, &mut ModelContext<Project>) -> bool,
2829) -> Option<WeakViewHandle<Workspace>> {
2830 for window_id in cx.window_ids() {
2831 let handle = cx
2832 .update_window(window_id, |cx| {
2833 if let Some(workspace_handle) = cx.root_view().clone().downcast::<Workspace>() {
2834 let project = workspace_handle.read(cx).project.clone();
2835 if project.update(cx, &predicate) {
2836 cx.activate_window();
2837 return Some(workspace_handle.clone());
2838 }
2839 }
2840 None
2841 })
2842 .flatten();
2843
2844 if let Some(handle) = handle {
2845 return Some(handle.downgrade());
2846 }
2847 }
2848 None
2849}
2850
2851pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
2852 DB.last_workspace().await.log_err().flatten()
2853}
2854
2855#[allow(clippy::type_complexity)]
2856pub fn open_paths(
2857 abs_paths: &[PathBuf],
2858 app_state: &Arc<AppState>,
2859 requesting_window_id: Option<usize>,
2860 cx: &mut AppContext,
2861) -> Task<
2862 Result<(
2863 WeakViewHandle<Workspace>,
2864 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
2865 )>,
2866> {
2867 log::info!("open paths {:?}", abs_paths);
2868
2869 let app_state = app_state.clone();
2870 let abs_paths = abs_paths.to_vec();
2871 cx.spawn(|mut cx| async move {
2872 // Open paths in existing workspace if possible
2873 let existing = activate_workspace_for_project(&mut cx, |project, cx| {
2874 project.contains_paths(&abs_paths, cx)
2875 });
2876
2877 if let Some(existing) = existing {
2878 Ok((
2879 existing.clone(),
2880 existing
2881 .update(&mut cx, |workspace, cx| {
2882 workspace.open_paths(abs_paths, true, cx)
2883 })?
2884 .await,
2885 ))
2886 } else {
2887 let contains_directory =
2888 futures::future::join_all(abs_paths.iter().map(|path| app_state.fs.is_file(path)))
2889 .await
2890 .contains(&false);
2891
2892 cx.update(|cx| {
2893 let task =
2894 Workspace::new_local(abs_paths, app_state.clone(), requesting_window_id, cx);
2895
2896 cx.spawn(|mut cx| async move {
2897 let (workspace, items) = task.await;
2898
2899 workspace.update(&mut cx, |workspace, cx| {
2900 if contains_directory {
2901 workspace.toggle_sidebar(SidebarSide::Left, cx);
2902 }
2903 })?;
2904
2905 anyhow::Ok((workspace, items))
2906 })
2907 })
2908 .await
2909 }
2910 })
2911}
2912
2913pub fn open_new(
2914 app_state: &Arc<AppState>,
2915 cx: &mut AppContext,
2916 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static,
2917) -> Task<()> {
2918 let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx);
2919 cx.spawn(|mut cx| async move {
2920 let (workspace, opened_paths) = task.await;
2921
2922 workspace
2923 .update(&mut cx, |workspace, cx| {
2924 if opened_paths.is_empty() {
2925 init(workspace, cx)
2926 }
2927 })
2928 .log_err();
2929 })
2930}
2931
2932pub fn join_remote_project(
2933 project_id: u64,
2934 follow_user_id: u64,
2935 app_state: Arc<AppState>,
2936 cx: &mut AppContext,
2937) -> Task<Result<()>> {
2938 cx.spawn(|mut cx| async move {
2939 let existing_workspace = cx
2940 .window_ids()
2941 .into_iter()
2942 .filter_map(|window_id| cx.root_view(window_id)?.clone().downcast::<Workspace>())
2943 .find(|workspace| {
2944 cx.read_window(workspace.window_id(), |cx| {
2945 workspace.read(cx).project().read(cx).remote_id() == Some(project_id)
2946 })
2947 .unwrap_or(false)
2948 });
2949
2950 let workspace = if let Some(existing_workspace) = existing_workspace {
2951 existing_workspace.downgrade()
2952 } else {
2953 let active_call = cx.read(ActiveCall::global);
2954 let room = active_call
2955 .read_with(&cx, |call, _| call.room().cloned())
2956 .ok_or_else(|| anyhow!("not in a call"))?;
2957 let project = room
2958 .update(&mut cx, |room, cx| {
2959 room.join_project(
2960 project_id,
2961 app_state.languages.clone(),
2962 app_state.fs.clone(),
2963 cx,
2964 )
2965 })
2966 .await?;
2967
2968 let (_, workspace) = cx.add_window(
2969 (app_state.build_window_options)(None, None, cx.platform().as_ref()),
2970 |cx| {
2971 let mut workspace =
2972 Workspace::new(Default::default(), 0, project, app_state.clone(), cx);
2973 (app_state.initialize_workspace)(&mut workspace, &app_state, cx);
2974 workspace
2975 },
2976 );
2977 workspace.downgrade()
2978 };
2979
2980 cx.activate_window(workspace.window_id());
2981 cx.platform().activate(true);
2982
2983 workspace.update(&mut cx, |workspace, cx| {
2984 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
2985 let follow_peer_id = room
2986 .read(cx)
2987 .remote_participants()
2988 .iter()
2989 .find(|(_, participant)| participant.user.id == follow_user_id)
2990 .map(|(_, p)| p.peer_id)
2991 .or_else(|| {
2992 // If we couldn't follow the given user, follow the host instead.
2993 let collaborator = workspace
2994 .project()
2995 .read(cx)
2996 .collaborators()
2997 .values()
2998 .find(|collaborator| collaborator.replica_id == 0)?;
2999 Some(collaborator.peer_id)
3000 });
3001
3002 if let Some(follow_peer_id) = follow_peer_id {
3003 if !workspace.is_being_followed(follow_peer_id) {
3004 workspace
3005 .toggle_follow(follow_peer_id, cx)
3006 .map(|follow| follow.detach_and_log_err(cx));
3007 }
3008 }
3009 }
3010 })?;
3011
3012 anyhow::Ok(())
3013 })
3014}
3015
3016pub fn restart(_: &Restart, cx: &mut AppContext) {
3017 let should_confirm = cx.global::<Settings>().confirm_quit;
3018 cx.spawn(|mut cx| async move {
3019 let mut workspaces = cx
3020 .window_ids()
3021 .into_iter()
3022 .filter_map(|window_id| {
3023 Some(
3024 cx.root_view(window_id)?
3025 .clone()
3026 .downcast::<Workspace>()?
3027 .downgrade(),
3028 )
3029 })
3030 .collect::<Vec<_>>();
3031
3032 // If multiple windows have unsaved changes, and need a save prompt,
3033 // prompt in the active window before switching to a different window.
3034 workspaces.sort_by_key(|workspace| !cx.window_is_active(workspace.window_id()));
3035
3036 if let (true, Some(workspace)) = (should_confirm, workspaces.first()) {
3037 let answer = cx.prompt(
3038 workspace.window_id(),
3039 PromptLevel::Info,
3040 "Are you sure you want to restart?",
3041 &["Restart", "Cancel"],
3042 );
3043
3044 if let Some(mut answer) = answer {
3045 let answer = answer.next().await;
3046 if answer != Some(0) {
3047 return Ok(());
3048 }
3049 }
3050 }
3051
3052 // If the user cancels any save prompt, then keep the app open.
3053 for workspace in workspaces {
3054 if !workspace
3055 .update(&mut cx, |workspace, cx| {
3056 workspace.prepare_to_close(true, cx)
3057 })?
3058 .await?
3059 {
3060 return Ok(());
3061 }
3062 }
3063 cx.platform().restart();
3064 anyhow::Ok(())
3065 })
3066 .detach_and_log_err(cx);
3067}
3068
3069fn parse_pixel_position_env_var(value: &str) -> Option<Vector2F> {
3070 let mut parts = value.split(',');
3071 let width: usize = parts.next()?.parse().ok()?;
3072 let height: usize = parts.next()?.parse().ok()?;
3073 Some(vec2f(width as f32, height as f32))
3074}
3075
3076#[cfg(test)]
3077mod tests {
3078 use std::{cell::RefCell, rc::Rc};
3079
3080 use crate::item::test::{TestItem, TestItemEvent, TestProjectItem};
3081
3082 use super::*;
3083 use fs::FakeFs;
3084 use gpui::{executor::Deterministic, TestAppContext};
3085 use project::{Project, ProjectEntryId};
3086 use serde_json::json;
3087
3088 #[gpui::test]
3089 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
3090 cx.foreground().forbid_parking();
3091 Settings::test_async(cx);
3092
3093 let fs = FakeFs::new(cx.background());
3094 let project = Project::test(fs, [], cx).await;
3095 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
3096
3097 // Adding an item with no ambiguity renders the tab without detail.
3098 let item1 = cx.add_view(window_id, |_| {
3099 let mut item = TestItem::new();
3100 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
3101 item
3102 });
3103 workspace.update(cx, |workspace, cx| {
3104 workspace.add_item(Box::new(item1.clone()), cx);
3105 });
3106 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), None));
3107
3108 // Adding an item that creates ambiguity increases the level of detail on
3109 // both tabs.
3110 let item2 = cx.add_view(window_id, |_| {
3111 let mut item = TestItem::new();
3112 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
3113 item
3114 });
3115 workspace.update(cx, |workspace, cx| {
3116 workspace.add_item(Box::new(item2.clone()), cx);
3117 });
3118 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3119 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3120
3121 // Adding an item that creates ambiguity increases the level of detail only
3122 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
3123 // we stop at the highest detail available.
3124 let item3 = cx.add_view(window_id, |_| {
3125 let mut item = TestItem::new();
3126 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
3127 item
3128 });
3129 workspace.update(cx, |workspace, cx| {
3130 workspace.add_item(Box::new(item3.clone()), cx);
3131 });
3132 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3133 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
3134 item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
3135 }
3136
3137 #[gpui::test]
3138 async fn test_tracking_active_path(cx: &mut TestAppContext) {
3139 cx.foreground().forbid_parking();
3140 Settings::test_async(cx);
3141 let fs = FakeFs::new(cx.background());
3142 fs.insert_tree(
3143 "/root1",
3144 json!({
3145 "one.txt": "",
3146 "two.txt": "",
3147 }),
3148 )
3149 .await;
3150 fs.insert_tree(
3151 "/root2",
3152 json!({
3153 "three.txt": "",
3154 }),
3155 )
3156 .await;
3157
3158 let project = Project::test(fs, ["root1".as_ref()], cx).await;
3159 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
3160 let worktree_id = project.read_with(cx, |project, cx| {
3161 project.worktrees(cx).next().unwrap().read(cx).id()
3162 });
3163
3164 let item1 = cx.add_view(window_id, |cx| {
3165 TestItem::new().with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
3166 });
3167 let item2 = cx.add_view(window_id, |cx| {
3168 TestItem::new().with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
3169 });
3170
3171 // Add an item to an empty pane
3172 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
3173 project.read_with(cx, |project, cx| {
3174 assert_eq!(
3175 project.active_entry(),
3176 project
3177 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
3178 .map(|e| e.id)
3179 );
3180 });
3181 assert_eq!(
3182 cx.current_window_title(window_id).as_deref(),
3183 Some("one.txt — root1")
3184 );
3185
3186 // Add a second item to a non-empty pane
3187 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
3188 assert_eq!(
3189 cx.current_window_title(window_id).as_deref(),
3190 Some("two.txt — root1")
3191 );
3192 project.read_with(cx, |project, cx| {
3193 assert_eq!(
3194 project.active_entry(),
3195 project
3196 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
3197 .map(|e| e.id)
3198 );
3199 });
3200
3201 // Close the active item
3202 workspace
3203 .update(cx, |workspace, cx| {
3204 Pane::close_active_item(workspace, &Default::default(), cx).unwrap()
3205 })
3206 .await
3207 .unwrap();
3208 assert_eq!(
3209 cx.current_window_title(window_id).as_deref(),
3210 Some("one.txt — root1")
3211 );
3212 project.read_with(cx, |project, cx| {
3213 assert_eq!(
3214 project.active_entry(),
3215 project
3216 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
3217 .map(|e| e.id)
3218 );
3219 });
3220
3221 // Add a project folder
3222 project
3223 .update(cx, |project, cx| {
3224 project.find_or_create_local_worktree("/root2", true, cx)
3225 })
3226 .await
3227 .unwrap();
3228 assert_eq!(
3229 cx.current_window_title(window_id).as_deref(),
3230 Some("one.txt — root1, root2")
3231 );
3232
3233 // Remove a project folder
3234 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
3235 assert_eq!(
3236 cx.current_window_title(window_id).as_deref(),
3237 Some("one.txt — root2")
3238 );
3239 }
3240
3241 #[gpui::test]
3242 async fn test_close_window(cx: &mut TestAppContext) {
3243 cx.foreground().forbid_parking();
3244 Settings::test_async(cx);
3245 let fs = FakeFs::new(cx.background());
3246 fs.insert_tree("/root", json!({ "one": "" })).await;
3247
3248 let project = Project::test(fs, ["root".as_ref()], cx).await;
3249 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
3250
3251 // When there are no dirty items, there's nothing to do.
3252 let item1 = cx.add_view(window_id, |_| TestItem::new());
3253 workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
3254 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
3255 assert!(task.await.unwrap());
3256
3257 // When there are dirty untitled items, prompt to save each one. If the user
3258 // cancels any prompt, then abort.
3259 let item2 = cx.add_view(window_id, |_| TestItem::new().with_dirty(true));
3260 let item3 = cx.add_view(window_id, |cx| {
3261 TestItem::new()
3262 .with_dirty(true)
3263 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3264 });
3265 workspace.update(cx, |w, cx| {
3266 w.add_item(Box::new(item2.clone()), cx);
3267 w.add_item(Box::new(item3.clone()), cx);
3268 });
3269 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
3270 cx.foreground().run_until_parked();
3271 cx.simulate_prompt_answer(window_id, 2 /* cancel */);
3272 cx.foreground().run_until_parked();
3273 assert!(!cx.has_pending_prompt(window_id));
3274 assert!(!task.await.unwrap());
3275 }
3276
3277 #[gpui::test]
3278 async fn test_close_pane_items(cx: &mut TestAppContext) {
3279 cx.foreground().forbid_parking();
3280 Settings::test_async(cx);
3281 let fs = FakeFs::new(cx.background());
3282
3283 let project = Project::test(fs, None, cx).await;
3284 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
3285
3286 let item1 = cx.add_view(window_id, |cx| {
3287 TestItem::new()
3288 .with_dirty(true)
3289 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3290 });
3291 let item2 = cx.add_view(window_id, |cx| {
3292 TestItem::new()
3293 .with_dirty(true)
3294 .with_conflict(true)
3295 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
3296 });
3297 let item3 = cx.add_view(window_id, |cx| {
3298 TestItem::new()
3299 .with_dirty(true)
3300 .with_conflict(true)
3301 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
3302 });
3303 let item4 = cx.add_view(window_id, |cx| {
3304 TestItem::new()
3305 .with_dirty(true)
3306 .with_project_items(&[TestProjectItem::new_untitled(cx)])
3307 });
3308 let pane = workspace.update(cx, |workspace, cx| {
3309 workspace.add_item(Box::new(item1.clone()), cx);
3310 workspace.add_item(Box::new(item2.clone()), cx);
3311 workspace.add_item(Box::new(item3.clone()), cx);
3312 workspace.add_item(Box::new(item4.clone()), cx);
3313 workspace.active_pane().clone()
3314 });
3315
3316 let close_items = workspace.update(cx, |workspace, cx| {
3317 pane.update(cx, |pane, cx| {
3318 pane.activate_item(1, true, true, cx);
3319 assert_eq!(pane.active_item().unwrap().id(), item2.id());
3320 });
3321
3322 let item1_id = item1.id();
3323 let item3_id = item3.id();
3324 let item4_id = item4.id();
3325 Pane::close_items(workspace, pane.clone(), cx, move |id| {
3326 [item1_id, item3_id, item4_id].contains(&id)
3327 })
3328 });
3329 cx.foreground().run_until_parked();
3330
3331 // There's a prompt to save item 1.
3332 pane.read_with(cx, |pane, _| {
3333 assert_eq!(pane.items_len(), 4);
3334 assert_eq!(pane.active_item().unwrap().id(), item1.id());
3335 });
3336 assert!(cx.has_pending_prompt(window_id));
3337
3338 // Confirm saving item 1.
3339 cx.simulate_prompt_answer(window_id, 0);
3340 cx.foreground().run_until_parked();
3341
3342 // Item 1 is saved. There's a prompt to save item 3.
3343 pane.read_with(cx, |pane, cx| {
3344 assert_eq!(item1.read(cx).save_count, 1);
3345 assert_eq!(item1.read(cx).save_as_count, 0);
3346 assert_eq!(item1.read(cx).reload_count, 0);
3347 assert_eq!(pane.items_len(), 3);
3348 assert_eq!(pane.active_item().unwrap().id(), item3.id());
3349 });
3350 assert!(cx.has_pending_prompt(window_id));
3351
3352 // Cancel saving item 3.
3353 cx.simulate_prompt_answer(window_id, 1);
3354 cx.foreground().run_until_parked();
3355
3356 // Item 3 is reloaded. There's a prompt to save item 4.
3357 pane.read_with(cx, |pane, cx| {
3358 assert_eq!(item3.read(cx).save_count, 0);
3359 assert_eq!(item3.read(cx).save_as_count, 0);
3360 assert_eq!(item3.read(cx).reload_count, 1);
3361 assert_eq!(pane.items_len(), 2);
3362 assert_eq!(pane.active_item().unwrap().id(), item4.id());
3363 });
3364 assert!(cx.has_pending_prompt(window_id));
3365
3366 // Confirm saving item 4.
3367 cx.simulate_prompt_answer(window_id, 0);
3368 cx.foreground().run_until_parked();
3369
3370 // There's a prompt for a path for item 4.
3371 cx.simulate_new_path_selection(|_| Some(Default::default()));
3372 close_items.await.unwrap();
3373
3374 // The requested items are closed.
3375 pane.read_with(cx, |pane, cx| {
3376 assert_eq!(item4.read(cx).save_count, 0);
3377 assert_eq!(item4.read(cx).save_as_count, 1);
3378 assert_eq!(item4.read(cx).reload_count, 0);
3379 assert_eq!(pane.items_len(), 1);
3380 assert_eq!(pane.active_item().unwrap().id(), item2.id());
3381 });
3382 }
3383
3384 #[gpui::test]
3385 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
3386 cx.foreground().forbid_parking();
3387 Settings::test_async(cx);
3388 let fs = FakeFs::new(cx.background());
3389
3390 let project = Project::test(fs, [], cx).await;
3391 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
3392
3393 // Create several workspace items with single project entries, and two
3394 // workspace items with multiple project entries.
3395 let single_entry_items = (0..=4)
3396 .map(|project_entry_id| {
3397 cx.add_view(window_id, |cx| {
3398 TestItem::new()
3399 .with_dirty(true)
3400 .with_project_items(&[TestProjectItem::new(
3401 project_entry_id,
3402 &format!("{project_entry_id}.txt"),
3403 cx,
3404 )])
3405 })
3406 })
3407 .collect::<Vec<_>>();
3408 let item_2_3 = cx.add_view(window_id, |cx| {
3409 TestItem::new()
3410 .with_dirty(true)
3411 .with_singleton(false)
3412 .with_project_items(&[
3413 single_entry_items[2].read(cx).project_items[0].clone(),
3414 single_entry_items[3].read(cx).project_items[0].clone(),
3415 ])
3416 });
3417 let item_3_4 = cx.add_view(window_id, |cx| {
3418 TestItem::new()
3419 .with_dirty(true)
3420 .with_singleton(false)
3421 .with_project_items(&[
3422 single_entry_items[3].read(cx).project_items[0].clone(),
3423 single_entry_items[4].read(cx).project_items[0].clone(),
3424 ])
3425 });
3426
3427 // Create two panes that contain the following project entries:
3428 // left pane:
3429 // multi-entry items: (2, 3)
3430 // single-entry items: 0, 1, 2, 3, 4
3431 // right pane:
3432 // single-entry items: 1
3433 // multi-entry items: (3, 4)
3434 let left_pane = workspace.update(cx, |workspace, cx| {
3435 let left_pane = workspace.active_pane().clone();
3436 workspace.add_item(Box::new(item_2_3.clone()), cx);
3437 for item in single_entry_items {
3438 workspace.add_item(Box::new(item), cx);
3439 }
3440 left_pane.update(cx, |pane, cx| {
3441 pane.activate_item(2, true, true, cx);
3442 });
3443
3444 workspace
3445 .split_pane(left_pane.clone(), SplitDirection::Right, cx)
3446 .unwrap();
3447
3448 left_pane
3449 });
3450
3451 //Need to cause an effect flush in order to respect new focus
3452 workspace.update(cx, |workspace, cx| {
3453 workspace.add_item(Box::new(item_3_4.clone()), cx);
3454 cx.focus(&left_pane);
3455 });
3456
3457 // When closing all of the items in the left pane, we should be prompted twice:
3458 // once for project entry 0, and once for project entry 2. After those two
3459 // prompts, the task should complete.
3460
3461 let close = workspace.update(cx, |workspace, cx| {
3462 Pane::close_items(workspace, left_pane.clone(), cx, |_| true)
3463 });
3464
3465 cx.foreground().run_until_parked();
3466 left_pane.read_with(cx, |pane, cx| {
3467 assert_eq!(
3468 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
3469 &[ProjectEntryId::from_proto(0)]
3470 );
3471 });
3472 cx.simulate_prompt_answer(window_id, 0);
3473
3474 cx.foreground().run_until_parked();
3475 left_pane.read_with(cx, |pane, cx| {
3476 assert_eq!(
3477 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
3478 &[ProjectEntryId::from_proto(2)]
3479 );
3480 });
3481 cx.simulate_prompt_answer(window_id, 0);
3482
3483 cx.foreground().run_until_parked();
3484 close.await.unwrap();
3485 left_pane.read_with(cx, |pane, _| {
3486 assert_eq!(pane.items_len(), 0);
3487 });
3488 }
3489
3490 #[gpui::test]
3491 async fn test_autosave(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
3492 deterministic.forbid_parking();
3493
3494 Settings::test_async(cx);
3495 let fs = FakeFs::new(cx.background());
3496
3497 let project = Project::test(fs, [], cx).await;
3498 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
3499
3500 let item = cx.add_view(window_id, |cx| {
3501 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3502 });
3503 let item_id = item.id();
3504 workspace.update(cx, |workspace, cx| {
3505 workspace.add_item(Box::new(item.clone()), cx);
3506 });
3507
3508 // Autosave on window change.
3509 item.update(cx, |item, cx| {
3510 cx.update_global(|settings: &mut Settings, _| {
3511 settings.autosave = Autosave::OnWindowChange;
3512 });
3513 item.is_dirty = true;
3514 });
3515
3516 // Deactivating the window saves the file.
3517 cx.simulate_window_activation(None);
3518 deterministic.run_until_parked();
3519 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
3520
3521 // Autosave on focus change.
3522 item.update(cx, |item, cx| {
3523 cx.focus_self();
3524 cx.update_global(|settings: &mut Settings, _| {
3525 settings.autosave = Autosave::OnFocusChange;
3526 });
3527 item.is_dirty = true;
3528 });
3529
3530 // Blurring the item saves the file.
3531 item.update(cx, |_, cx| cx.blur());
3532 deterministic.run_until_parked();
3533 item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
3534
3535 // Deactivating the window still saves the file.
3536 cx.simulate_window_activation(Some(window_id));
3537 item.update(cx, |item, cx| {
3538 cx.focus_self();
3539 item.is_dirty = true;
3540 });
3541 cx.simulate_window_activation(None);
3542
3543 deterministic.run_until_parked();
3544 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
3545
3546 // Autosave after delay.
3547 item.update(cx, |item, cx| {
3548 cx.update_global(|settings: &mut Settings, _| {
3549 settings.autosave = Autosave::AfterDelay { milliseconds: 500 };
3550 });
3551 item.is_dirty = true;
3552 cx.emit(TestItemEvent::Edit);
3553 });
3554
3555 // Delay hasn't fully expired, so the file is still dirty and unsaved.
3556 deterministic.advance_clock(Duration::from_millis(250));
3557 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
3558
3559 // After delay expires, the file is saved.
3560 deterministic.advance_clock(Duration::from_millis(250));
3561 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
3562
3563 // Autosave on focus change, ensuring closing the tab counts as such.
3564 item.update(cx, |item, cx| {
3565 cx.update_global(|settings: &mut Settings, _| {
3566 settings.autosave = Autosave::OnFocusChange;
3567 });
3568 item.is_dirty = true;
3569 });
3570
3571 workspace
3572 .update(cx, |workspace, cx| {
3573 let pane = workspace.active_pane().clone();
3574 Pane::close_items(workspace, pane, cx, move |id| id == item_id)
3575 })
3576 .await
3577 .unwrap();
3578 assert!(!cx.has_pending_prompt(window_id));
3579 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3580
3581 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
3582 workspace.update(cx, |workspace, cx| {
3583 workspace.add_item(Box::new(item.clone()), cx);
3584 });
3585 item.update(cx, |item, cx| {
3586 item.project_items[0].update(cx, |item, _| {
3587 item.entry_id = None;
3588 });
3589 item.is_dirty = true;
3590 cx.blur();
3591 });
3592 deterministic.run_until_parked();
3593 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3594
3595 // Ensure autosave is prevented for deleted files also when closing the buffer.
3596 let _close_items = workspace.update(cx, |workspace, cx| {
3597 let pane = workspace.active_pane().clone();
3598 Pane::close_items(workspace, pane, cx, move |id| id == item_id)
3599 });
3600 deterministic.run_until_parked();
3601 assert!(cx.has_pending_prompt(window_id));
3602 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3603 }
3604
3605 #[gpui::test]
3606 async fn test_pane_navigation(
3607 deterministic: Arc<Deterministic>,
3608 cx: &mut gpui::TestAppContext,
3609 ) {
3610 deterministic.forbid_parking();
3611 Settings::test_async(cx);
3612 let fs = FakeFs::new(cx.background());
3613
3614 let project = Project::test(fs, [], cx).await;
3615 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
3616
3617 let item = cx.add_view(window_id, |cx| {
3618 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3619 });
3620 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3621 let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
3622 let toolbar_notify_count = Rc::new(RefCell::new(0));
3623
3624 workspace.update(cx, |workspace, cx| {
3625 workspace.add_item(Box::new(item.clone()), cx);
3626 let toolbar_notification_count = toolbar_notify_count.clone();
3627 cx.observe(&toolbar, move |_, _, _| {
3628 *toolbar_notification_count.borrow_mut() += 1
3629 })
3630 .detach();
3631 });
3632
3633 pane.read_with(cx, |pane, _| {
3634 assert!(!pane.can_navigate_backward());
3635 assert!(!pane.can_navigate_forward());
3636 });
3637
3638 item.update(cx, |item, cx| {
3639 item.set_state("one".to_string(), cx);
3640 });
3641
3642 // Toolbar must be notified to re-render the navigation buttons
3643 assert_eq!(*toolbar_notify_count.borrow(), 1);
3644
3645 pane.read_with(cx, |pane, _| {
3646 assert!(pane.can_navigate_backward());
3647 assert!(!pane.can_navigate_forward());
3648 });
3649
3650 workspace
3651 .update(cx, |workspace, cx| {
3652 Pane::go_back(workspace, Some(pane.downgrade()), cx)
3653 })
3654 .await
3655 .unwrap();
3656
3657 assert_eq!(*toolbar_notify_count.borrow(), 3);
3658 pane.read_with(cx, |pane, _| {
3659 assert!(!pane.can_navigate_backward());
3660 assert!(pane.can_navigate_forward());
3661 });
3662 }
3663}